Commit e43f611f by xushaohua

mj代理

parent 7065dc88
target/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### VS Code ###
.vscode/
### Macos ###
.DS_Store
### application config #
config/application.yml
.git
.gitignore
docker
docs
README.md
\ No newline at end of file
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**
bin/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
### VS Code ###
.vscode/
### Macos ###
.DS_Store
### application config #
config/application.yml
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
\ No newline at end of file
FROM maven:3.8.5-openjdk-17
ARG user=spring
ARG group=spring
ENV SPRING_HOME=/home/spring
RUN groupadd -g 1000 ${group} \
&& useradd -d "$SPRING_HOME" -u 1000 -g 1000 -m -s /bin/bash ${user} \
&& mkdir -p $SPRING_HOME/config \
&& mkdir -p $SPRING_HOME/logs \
&& chown -R ${user}:${group} $SPRING_HOME/config $SPRING_HOME/logs
# Railway 不支持使用 VOLUME, 本地需要构建时,取消下一行的注释
# VOLUME ["$SPRING_HOME/config", "$SPRING_HOME/logs"]
USER ${user}
WORKDIR $SPRING_HOME
COPY . .
RUN mvn clean package \
&& mv target/midjourney-proxy-*.jar ./app.jar \
&& rm -rf target
EXPOSE 8080 9876
ENV JAVA_OPTS -XX:MaxRAMPercentage=85 -Djava.awt.headless=true -XX:+HeapDumpOnOutOfMemoryError \
-XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -Xlog:gc:file=/home/spring/logs/gc.log \
-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9876 -Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=false -Dlogging.file.path=/home/spring/logs \
-Dserver.port=8080 -Duser.timezone=Asia/Shanghai
ENTRYPOINT ["bash","-c","java $JAVA_OPTS -jar app.jar"]
# midjourney-proxy
> 更多功能:[midjourney-proxy-plus](https://github.com/litter-coder/midjourney-proxy-plus)
代理 MidJourney 的discord频道,实现api形式调用AI绘图
[![GitHub release](https://img.shields.io/static/v1?label=release&message=v2.3.5&color=blue)](https://www.github.com/novicezk/midjourney-proxy)
[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
## 现有功能
- [x] 支持 Imagine 指令和相关U、V操作
- [x] Imagine 时支持添加图片base64,作为垫图
- [x] 支持 Blend(图片混合) 指令和相关U、V操作
- [x] 支持 Describe 指令,根据图片生成 prompt
- [x] 支持 Imagine、V、Blend 图片生成进度
- [x] 支持中文 prompt 翻译,需配置百度翻译或 gpt
- [x] prompt 敏感词判断,支持覆盖调整
- [x] 任务队列,默认队列10,并发3。可参考 [MidJourney订阅级别](https://docs.midjourney.com/docs/plans) 调整mj.queue
- [x] user-token 连接 wss,可以获取错误信息和完整功能
- [x] 支持 discord域名(server、cdn、wss)反代,配置 mj.ng-discord
## 后续计划
- [ ] 支持 Reroll 操作
- [ ] 支持配置账号池,分发绘图任务
- [ ] 修复相关Bug,[Wiki / 已知问题](https://github.com/novicezk/midjourney-proxy/wiki/%E5%B7%B2%E7%9F%A5%E9%97%AE%E9%A2%98)
## 使用前提
1. 注册 MidJourney,创建自己的频道,参考 https://docs.midjourney.com/docs/quick-start
2. 获取用户Token、服务器ID、频道ID:[获取方式](./docs/discord-params.md)
## 风险须知
1. 作图频繁等行为,可能会触发midjourney账号警告,请谨慎使用
2. 为减少风险,请设置`mj.discord.user-agent``mj.discord.session-id`
3. 默认使用user-wss方式,可以获取midjourney的错误信息、图片变换进度等,但可能会增加账号风险
4. 支持设置mj.discord.user-wss为false,使用bot-token连接wss,需添加自定义机器人:[流程说明](./docs/discord-bot.md)
## Railway 部署
基于Railway平台部署,不需要自己的服务器: [部署方式](./docs/railway-start.md);若Railway不能使用,可用下方的Zeabur部署
## Zeabur 部署
基于Zeabur平台部署,不需要自己的服务器: [部署方式](./docs/zeabur-start.md)
## Docker 部署
1. /xxx/xxx/config目录下创建 application.yml(mj配置项)、banned-words.txt(可选,覆盖默认的敏感词文件);参考src/main/resources下的文件
2. 启动容器,映射config目录
```shell
docker run -d --name midjourney-proxy \
-p 8080:8080 \
-v /xxx/xxx/config:/home/spring/config \
--restart=always \
novicezk/midjourney-proxy:2.3.5
```
3. 访问 `http://ip:port/mj` 查看API文档
附: 不映射config目录方式,直接在启动命令中设置参数
```shell
docker run -d --name midjourney-proxy \
-p 8080:8080 \
-e mj.discord.guild-id=xxx \
-e mj.discord.channel-id=xxx \
-e mj.discord.user-token=xxx \
--restart=always \
novicezk/midjourney-proxy:2.3.5
```
## 配置项
- mj.discord.guild-id:discord服务器ID
- mj.discord.channel-id:discord频道ID
- mj.discord.user-token:discord用户Token
- mj.discord.session-id:discord用户的sessionId,不设置时使用默认的,建议从interactions请求中复制替换掉
- mj.discord.user-agent:调用discord接口、连接wss时的user-agent,默认使用作者的,建议从浏览器network复制替换掉
- mj.discord.user-wss:是否使用user-token连接wss,默认true
- mj.discord.bot-token:自定义机器人Token,user-wss=false时必填
- 更多配置查看 [Wiki / 配置项](https://github.com/novicezk/midjourney-proxy/wiki/%E9%85%8D%E7%BD%AE%E9%A1%B9)
## Wiki链接
1. [Wiki / API接口说明](https://github.com/novicezk/midjourney-proxy/wiki/API%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E)
2. [Wiki / 任务变更回调](https://github.com/novicezk/midjourney-proxy/wiki/%E4%BB%BB%E5%8A%A1%E5%8F%98%E6%9B%B4%E5%9B%9E%E8%B0%83)
2. [Wiki / 更新记录](https://github.com/novicezk/midjourney-proxy/wiki/%E6%9B%B4%E6%96%B0%E8%AE%B0%E5%BD%95)
## 注意事项
1. 常见问题及解决办法见 [Wiki / FAQ](https://github.com/novicezk/midjourney-proxy/wiki/FAQ)
2.[Issues](https://github.com/novicezk/midjourney-proxy/issues) 中提出其他问题或建议
3. 感兴趣的朋友也欢迎加入交流群讨论一下,扫码进群名额已满,加管理员微信邀请进群
<img src="https://raw.githubusercontent.com/novicezk/midjourney-proxy/main/docs/manager-qrcode.png" width="320" alt="微信二维码"/>
## 本地开发
- 依赖java17和maven
- 更改配置项: 修改src/main/application.yml
- 项目运行: 启动ProxyApplication的main函数
- 更改代码后,构建镜像: Dockerfile取消VOLUME的注释,执行 `docker build . -t midjourney-proxy`
## 应用项目
- [wechat-midjourney](https://github.com/novicezk/wechat-midjourney) : 代理微信客户端,接入MidJourney,仅示例应用场景,不再更新
- [stable-diffusion-mobileui](https://github.com/yuanyuekeji/stable-diffusion-mobileui) : SDUI,基于本接口和SD,可一键打包生成H5和小程序
- [ChatGPT-Midjourney](https://github.com/Licoy/ChatGPT-Midjourney) : 一键拥有你自己的 ChatGPT+Midjourney 网页服务
- [MidJourney-Web](https://github.com/ConnectAI-E/MidJourney-Web) : 🍎 Supercharged Experience For MidJourney On Web UI
- [koishi-plugin-midjourney-discord](https://github.com/araea/koishi-plugin-midjourney-discord) : Koishi插件,在Koishi支持的聊天平台中调用Midjourney
- 依赖此项目且开源的,欢迎联系作者,加到此处展示
## 其它
如果觉得这个项目对你有所帮助,请帮忙点个star;也可以请作者喝杯茶~
<img src="https://raw.githubusercontent.com/novicezk/midjourney-proxy/main/docs/receipt-code.png" width="220" alt="二维码"/>
![Star History Chart](https://api.star-history.com/svg?repos=novicezk/midjourney-proxy&type=Date)
FROM openjdk:17.0
ARG user=spring
ARG group=spring
ENV SPRING_HOME=/home/spring
ENV APP_HOME=$SPRING_HOME/app
ENV JAVA_OPTS -XX:MaxRAMPercentage=85 -Djava.awt.headless=true -XX:+HeapDumpOnOutOfMemoryError \
-XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -Xlog:gc:file=/home/spring/logs/gc.log \
-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9876 -Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=false -Dlogging.file.path=/home/spring/logs \
-Dserver.port=8080 -Duser.timezone=Asia/Shanghai
RUN groupadd -g 1000 ${group} \
&& useradd -d "$SPRING_HOME" -u 1000 -g 1000 -m -s /bin/bash ${user} \
&& mkdir -p $SPRING_HOME/config \
&& mkdir -p $SPRING_HOME/logs \
&& mkdir -p $APP_HOME \
&& chown -R ${user}:${group} $SPRING_HOME/config $SPRING_HOME/logs $APP_HOME
VOLUME ["$SPRING_HOME/config", "$SPRING_HOME/logs"]
USER ${user}
WORKDIR $SPRING_HOME
EXPOSE 8080 9876
ENTRYPOINT ["bash","-c","java $JAVA_OPTS -cp ./app org.springframework.boot.loader.JarLauncher"]
COPY --chown=${user}:${group} dependencies $APP_HOME/
COPY --chown=${user}:${group} spring-boot-loader $APP_HOME/
COPY --chown=${user}:${group} snapshot-dependencies $APP_HOME/
COPY --chown=${user}:${group} application $APP_HOME/
#!/bin/bash
set -e -u -o pipefail
if [ $# -lt 1 ]; then
echo 'version is required'
exit 1
fi
VERSION=$1
ARCH=amd64
if [ $# -ge 2 ]; then
ARCH=$2
fi
JAR_FILE_COUNT=$(find "../target/" -maxdepth 1 -name '*.jar' | wc -l)
if [ $JAR_FILE_COUNT == 0 ]; then
echo "jar file not found, please execute: mvn clean package"
exit 1
fi
JAR_FILE_NAME=$(ls ../target/*.jar|grep -v source)
echo ${JAR_FILE_NAME}
cp ${JAR_FILE_NAME} ./app.jar
java -Djarmode=layertools -jar app.jar extract
docker build . -t midjourney-proxy:${VERSION}
rm -rf application dependencies snapshot-dependencies spring-boot-loader app.jar
docker tag midjourney-proxy:${VERSION} novicezk/midjourney-proxy-${ARCH}:${VERSION}
docker push novicezk/midjourney-proxy-${ARCH}:${VERSION}
\ No newline at end of file
#!/bin/bash
set -e -u -o pipefail
if [ $# -lt 1 ]; then
echo 'version is required'
exit 1
fi
VERSION=$1
echo "create manifest..."
docker manifest create novicezk/midjourney-proxy:${VERSION} novicezk/midjourney-proxy-amd64:${VERSION} novicezk/midjourney-proxy-arm64v8:${VERSION}
echo "annotate amd64..."
docker manifest annotate novicezk/midjourney-proxy:${VERSION} novicezk/midjourney-proxy-amd64:${VERSION} --os linux --arch amd64
echo "annotate arm64v8..."
docker manifest annotate novicezk/midjourney-proxy:${VERSION} novicezk/midjourney-proxy-arm64v8:${VERSION} --os linux --arch arm64 --variant v8
echo "push manifest..."
docker manifest push novicezk/midjourney-proxy:${VERSION}
\ No newline at end of file
## discord 添加机器人
### 1. 创建应用
https://discord.com/developers/applications
![New Application](img_1.png)
![Create](img_2.png)
### 2. 获取机器人Token
![Reset Token](img_3.png)
刷新token后显示,即机器人Token,后续配置到 `mj.discord.bot-token`
### 3. 机器人授权
![Url Generator](img_4.png)
如图勾选后,打开url进行授权
![Authorize](img_5.png)
选择Midjourney Bot所在的服务器
![Confirm](img_6.png)
![Tick And Save Changes](img_7.png)
勾上这两个选项,点击 `Save Changes`
### 4. 检查机器人
在频道中确认是否存在mj机器人和新创建的机器人
![Check Bot](img_10.png)
若不存在,把 MidJourney Bot 邀请到要让它作图的频道, 把自己的 bot 也拉到 MidJourney Bot 所在的频道
Edit Channel -> Permissions -> Add Members or roles;Member下面选中bot名字,按 Done 按钮
\ No newline at end of file
## 获取discord配置参数
### 1. 获取用户Token
进入频道,打开network,刷新页面,找到 `messages` 的请求,这里的 authorization 即用户Token,后续设置到 `mj.discord.user-token`
![User Token](img_8.png)
### 2. 获取用户sessionId
进入频道,打开network,发送/imagine作图指令,找到 `interactions` 的请求,这里的 session_id 即用户sessionId,后续设置到 `mj.discord.session-id`
![User Session](params_session_id.png)
### 3. 获取服务器ID、频道ID
频道的url里取出 服务器ID、频道ID,后续设置到配置项
![Guild Channel ID](img_9.png)
## Railway 部署教程
Railway是一个提供弹性部署方案的平台,服务器在海外,方便MidJourney的调用。
**Railway 提供 5 美元,500 个小时/月的免费额度**
### 1. Fork本仓库
### 2. Railway使用github账号登录
进入 [railway官网](https://railway.app) 选择 `Login` -> `Github`,登录github账号
### 3. [New Project](https://railway.app/new) 添加对fork仓库的授权
![railway_img_1](./railway_img_1.png)
![railway_img_2](./railway_img_2.png)
![railway_img_3](./railway_img_3.png)
### 4. 选择该fork仓库,新建项目,设置环境变量
![railway_img_4](./railway_img_4.png)
![railway_img_5](./railway_img_5.png)
![railway_img_6](./railway_img_6.png)
![railway_img_7](./railway_img_7.png)
此处配置项参考 [Wiki / 配置项](https://github.com/novicezk/midjourney-proxy/wiki/%E9%85%8D%E7%BD%AE%E9%A1%B9) ,建议配置api密钥启用鉴权,接口调用时需添加请求头 `mj-api-secret`
### 5. 启动服务
进入刚才的Project,它应该已经在自动部署了,后续更新配置之后会自动重新部署
![railway_img_8](./railway_img_8.png)
若部署启动失败请查看日志,检查配置项
![railway_img_9](./railway_img_9.png)
![railway_img_10](./railway_img_10.png)
### 6. 开始使用
等待部署成功后,生成随机域名
![railway_img_11](./railway_img_11.png)
![railway_img_12](./railway_img_12.png)
访问 `https://midjourney-proxy-***.app/mj`
## Zeabur 部署教程
### Zeabur 优势
1. 新注册的 `Github` 账号可能无法使用 `Railway`,但是能用 `Zeabur`
2. 通过 `Railway` 部署的项目会自动生成一个域名,然而因为某些原因,形如 `*.up.railway.app` 的域名在国内无法访问
3. `Zeabur` 服务器运行在国外,但是其生成的域名 `*.zeabur.app` 没有被污染,国内可直接访问
### 开始部署
1. 打开网址 https://zeabur.com/zh-CN
2. 点击现在开始
3. 点击 `Sign in with GitHub`
4. 登陆你的 `Github` 账号
5. 点击 `Authorize zeabur` 授权
6. 点击 `创建项目` 并输入一个项目名称,点击 `创建`
7. 点击 `+` 添加服务,选择 `Git-Deploy service from source code in GitHub repository.`
8. 点击 `Configure GitHub` 根据需要选择 `All repositories` 或者 `Only select repositories`
9. 点击 `install`,之后自动跳转,最好再刷新一下页面
10. 点击 你 fork 的 `midjourney-proxy` 项目
11. 点击环境变量,点击编辑原始环境变量,添加你需要的环境变量
12. 关于环境变量,与 `Railway` 稍有不同,需要把 `.``-` 全部换成 `_`,例如如下格式
```properties
PORT=8080
mj_discord_guild_id=xxx
mj_discord_channel_id=xxx
mj_discord_user_token=xxx
mj_api_secret=***
```
此处配置项参考 [Wiki / 配置项](https://github.com/novicezk/midjourney-proxy/wiki/%E9%85%8D%E7%BD%AE%E9%A1%B9) ,建议配置api密钥启用鉴权,接口调用时需添加请求头 `mj-api-secret`
13. 然后取消 `Building`,点击 `Redeploy` (此做法是为了让环境变量生效)
14. 部署 `midjourney-proxy` 大概需要 `2` 分钟,此时你可以做的是:配置域名
15. 点击下方的域名,点击生成域名,输入前缀,例如 `midjourney-proxy-demo`,点击保存;或者添加自定义域名,之后加上 `CNAME` 解析
16. 等待部署成功,访问 `https://midjourney-proxy-demo.zeabur.app/mj`
\ No newline at end of file
2023-07-24 16:01:23.316[TraceId: SpanId: ParentSpanId:] [main] ERROR org.springframework.boot.SpringApplication:835 - Application run failed
java.lang.IllegalStateException: Failed to execute ApplicationRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:776)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:763)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:314)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1317)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
at com.github.novicezk.midjourney.ProxyApplication.main(ProxyApplication.java:16)
Caused by: com.neovisionaries.ws.client.WebSocketException: Failed to connect to 'gateway.discord.gg:443': Connect timed out
at com.neovisionaries.ws.client.SocketConnector.connectSocket(SocketConnector.java:126)
at com.neovisionaries.ws.client.SocketConnector.doConnect(SocketConnector.java:238)
at com.neovisionaries.ws.client.SocketConnector.connect(SocketConnector.java:189)
at com.neovisionaries.ws.client.WebSocket.connect(WebSocket.java:2351)
at com.github.novicezk.midjourney.wss.user.UserWebSocketStarter.start(UserWebSocketStarter.java:74)
at spring.config.BeanConfig.lambda$enableMetaChangeReceiverInitializer$1(BeanConfig.java:80)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:773)
... 5 common frames omitted
Caused by: java.net.SocketTimeoutException: Connect timed out
at java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:546)
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:597)
at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327)
at java.base/java.net.Socket.connect(Socket.java:633)
at java.base/sun.security.ssl.SSLSocketImpl.connect(SSLSocketImpl.java:304)
at com.neovisionaries.ws.client.SocketInitiator$SocketRacer.run(SocketInitiator.java:126)
2023-07-24 15:51:44.495[TraceId: SpanId: ParentSpanId:] [WritingThread] WARN c.g.n.midjourney.wss.user.UserWebSocketStarter:135 - [gateway] Websocket closed and will be reconnect... code: 1002, reason: No more WebSocket frame from the server.
2023-07-24 15:51:57.951[TraceId: SpanId: ParentSpanId:] [WritingThread] WARN c.g.n.midjourney.wss.user.UserWebSocketStarter:135 - [gateway] Websocket closed and will be reconnect... code: 1002, reason: No more WebSocket frame from the server.
2023-07-24 15:52:05.895[TraceId: SpanId: ParentSpanId:] [WritingThread] WARN c.g.n.midjourney.wss.user.UserWebSocketStarter:135 - [gateway] Websocket closed and will be reconnect... code: 1002, reason: No more WebSocket frame from the server.
2023-07-24 15:52:15.726[TraceId: SpanId: ParentSpanId:] [WritingThread] WARN c.g.n.midjourney.wss.user.UserWebSocketStarter:135 - [gateway] Websocket closed and will be reconnect... code: 1002, reason: No more WebSocket frame from the server.
2023-07-24 15:52:18.615[TraceId: SpanId: ParentSpanId:] [WritingThread] WARN c.g.n.midjourney.wss.user.UserWebSocketStarter:135 - [gateway] Websocket closed and will be reconnect... code: 1002, reason: No more WebSocket frame from the server.
2023-07-24 16:01:10.035[TraceId: SpanId: ParentSpanId:] [main] INFO com.github.novicezk.midjourney.ProxyApplication:55 - Starting ProxyApplication using Java 17.0.8 on DCG027072 with PID 29864 (D:\PROJECT\midjourney-proxy\target\classes started by xushaohua in D:\PROJECT\midjourney-proxy)
2023-07-24 16:01:10.038[TraceId: SpanId: ParentSpanId:] [main] INFO com.github.novicezk.midjourney.ProxyApplication:651 - The following 1 profile is active: "dev"
2023-07-24 16:01:10.722[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.d.r.config.RepositoryConfigurationDelegate:262 - Multiple Spring Data modules found, entering strict repository configuration mode
2023-07-24 16:01:10.724[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.d.r.config.RepositoryConfigurationDelegate:132 - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
2023-07-24 16:01:10.773[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.d.r.config.RepositoryConfigurationDelegate:201 - Finished Spring Data repository scanning in 36 ms. Found 0 Redis repository interfaces.
2023-07-24 16:01:11.441[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer:108 - Tomcat initialized with port(s): 9090 (http)
2023-07-24 16:01:11.448[TraceId: SpanId: ParentSpanId:] [main] INFO org.apache.coyote.http11.Http11NioProtocol:173 - Initializing ProtocolHandler ["http-nio-9090"]
2023-07-24 16:01:11.449[TraceId: SpanId: ParentSpanId:] [main] INFO org.apache.catalina.core.StandardService:173 - Starting service [Tomcat]
2023-07-24 16:01:11.449[TraceId: SpanId: ParentSpanId:] [main] INFO org.apache.catalina.core.StandardEngine:173 - Starting Servlet engine: [Apache Tomcat/9.0.69]
2023-07-24 16:01:11.553[TraceId: SpanId: ParentSpanId:] [main] INFO o.a.c.c.ContainerBase.[Tomcat].[localhost].[/mj]:173 - Initializing Spring embedded WebApplicationContext
2023-07-24 16:01:11.553[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext:290 - Root WebApplicationContext: initialization completed in 1474 ms
2023-07-24 16:01:12.708[TraceId: SpanId: ParentSpanId:] [main] INFO s.d.s.w.WebMvcPropertySourcedRequestMappingHandlerMapping:69 - Mapped URL path [/v2/api-docs] onto method [springfox.documentation.swagger2.web.Swagger2ControllerWebMvc#getDocumentation(String, HttpServletRequest)]
2023-07-24 16:01:12.817[TraceId: SpanId: ParentSpanId:] [main] INFO org.apache.coyote.http11.Http11NioProtocol:173 - Starting ProtocolHandler ["http-nio-9090"]
2023-07-24 16:01:12.850[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer:220 - Tomcat started on port(s): 9090 (http) with context path '/mj'
2023-07-24 16:01:12.851[TraceId: SpanId: ParentSpanId:] [main] INFO s.d.s.web.plugins.DocumentationPluginsBootstrapper:93 - Documentation plugins bootstrapped
2023-07-24 16:01:12.855[TraceId: SpanId: ParentSpanId:] [main] INFO s.d.s.web.plugins.DocumentationPluginsBootstrapper:79 - Found 1 custom documentation plugin(s)
2023-07-24 16:01:12.887[TraceId: SpanId: ParentSpanId:] [main] INFO s.d.spring.web.scanners.ApiListingReferenceScanner:44 - Scanning for api listing references
2023-07-24 16:01:13.098[TraceId: SpanId: ParentSpanId:] [main] INFO com.github.novicezk.midjourney.ProxyApplication:61 - Started ProxyApplication in 3.558 seconds (JVM running for 3.92)
2023-07-24 16:01:23.286[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.b.a.l.ConditionEvaluationReportLoggingListener:136 -
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-07-24 16:01:23.316[TraceId: SpanId: ParentSpanId:] [main] ERROR org.springframework.boot.SpringApplication:835 - Application run failed
java.lang.IllegalStateException: Failed to execute ApplicationRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:776)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:763)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:314)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1317)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
at com.github.novicezk.midjourney.ProxyApplication.main(ProxyApplication.java:16)
Caused by: com.neovisionaries.ws.client.WebSocketException: Failed to connect to 'gateway.discord.gg:443': Connect timed out
at com.neovisionaries.ws.client.SocketConnector.connectSocket(SocketConnector.java:126)
at com.neovisionaries.ws.client.SocketConnector.doConnect(SocketConnector.java:238)
at com.neovisionaries.ws.client.SocketConnector.connect(SocketConnector.java:189)
at com.neovisionaries.ws.client.WebSocket.connect(WebSocket.java:2351)
at com.github.novicezk.midjourney.wss.user.UserWebSocketStarter.start(UserWebSocketStarter.java:74)
at spring.config.BeanConfig.lambda$enableMetaChangeReceiverInitializer$1(BeanConfig.java:80)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:773)
... 5 common frames omitted
Caused by: java.net.SocketTimeoutException: Connect timed out
at java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:546)
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:597)
at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327)
at java.base/java.net.Socket.connect(Socket.java:633)
at java.base/sun.security.ssl.SSLSocketImpl.connect(SSLSocketImpl.java:304)
at com.neovisionaries.ws.client.SocketInitiator$SocketRacer.run(SocketInitiator.java:126)
2023-07-24 15:16:43.142[TraceId: SpanId: ParentSpanId:] [main] INFO com.github.novicezk.midjourney.ProxyApplication:55 - Starting ProxyApplication using Java 17.0.8 on DCG027072 with PID 18048 (D:\PROJECT\midjourney-proxy\target\classes started by xushaohua in D:\PROJECT\midjourney-proxy)
2023-07-24 15:16:43.146[TraceId: SpanId: ParentSpanId:] [main] INFO com.github.novicezk.midjourney.ProxyApplication:651 - The following 1 profile is active: "dev"
2023-07-24 15:16:43.839[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.d.r.config.RepositoryConfigurationDelegate:262 - Multiple Spring Data modules found, entering strict repository configuration mode
2023-07-24 15:16:43.842[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.d.r.config.RepositoryConfigurationDelegate:132 - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
2023-07-24 15:16:43.884[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.d.r.config.RepositoryConfigurationDelegate:201 - Finished Spring Data repository scanning in 27 ms. Found 0 Redis repository interfaces.
2023-07-24 15:16:44.412[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer:108 - Tomcat initialized with port(s): 9090 (http)
2023-07-24 15:16:44.422[TraceId: SpanId: ParentSpanId:] [main] INFO org.apache.coyote.http11.Http11NioProtocol:173 - Initializing ProtocolHandler ["http-nio-9090"]
2023-07-24 15:16:44.423[TraceId: SpanId: ParentSpanId:] [main] INFO org.apache.catalina.core.StandardService:173 - Starting service [Tomcat]
2023-07-24 15:16:44.423[TraceId: SpanId: ParentSpanId:] [main] INFO org.apache.catalina.core.StandardEngine:173 - Starting Servlet engine: [Apache Tomcat/9.0.69]
2023-07-24 15:16:44.529[TraceId: SpanId: ParentSpanId:] [main] INFO o.a.c.c.ContainerBase.[Tomcat].[localhost].[/mj]:173 - Initializing Spring embedded WebApplicationContext
2023-07-24 15:16:44.530[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext:290 - Root WebApplicationContext: initialization completed in 1338 ms
2023-07-24 15:16:45.772[TraceId: SpanId: ParentSpanId:] [main] INFO s.d.s.w.WebMvcPropertySourcedRequestMappingHandlerMapping:69 - Mapped URL path [/v2/api-docs] onto method [springfox.documentation.swagger2.web.Swagger2ControllerWebMvc#getDocumentation(String, HttpServletRequest)]
2023-07-24 15:16:45.890[TraceId: SpanId: ParentSpanId:] [main] INFO org.apache.coyote.http11.Http11NioProtocol:173 - Starting ProtocolHandler ["http-nio-9090"]
2023-07-24 15:16:45.922[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer:220 - Tomcat started on port(s): 9090 (http) with context path '/mj'
2023-07-24 15:16:45.923[TraceId: SpanId: ParentSpanId:] [main] INFO s.d.s.web.plugins.DocumentationPluginsBootstrapper:93 - Documentation plugins bootstrapped
2023-07-24 15:16:45.927[TraceId: SpanId: ParentSpanId:] [main] INFO s.d.s.web.plugins.DocumentationPluginsBootstrapper:79 - Found 1 custom documentation plugin(s)
2023-07-24 15:16:45.964[TraceId: SpanId: ParentSpanId:] [main] INFO s.d.spring.web.scanners.ApiListingReferenceScanner:44 - Scanning for api listing references
2023-07-24 15:16:46.186[TraceId: SpanId: ParentSpanId:] [main] INFO com.github.novicezk.midjourney.ProxyApplication:61 - Started ProxyApplication in 3.572 seconds (JVM running for 4.01)
11112023-07-24 15:17:44.860[TraceId: SpanId: ParentSpanId:] [main] INFO com.github.novicezk.midjourney.ProxyApplication:55 - Starting ProxyApplication using Java 17.0.8 on DCG027072 with PID 39216 (D:\PROJECT\midjourney-proxy\target\classes started by xushaohua in D:\PROJECT\midjourney-proxy)
11112023-07-24 15:17:44.865[TraceId: SpanId: ParentSpanId:] [main] INFO com.github.novicezk.midjourney.ProxyApplication:651 - The following 1 profile is active: "dev"
11112023-07-24 15:17:45.741[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.d.r.config.RepositoryConfigurationDelegate:262 - Multiple Spring Data modules found, entering strict repository configuration mode
11112023-07-24 15:17:45.744[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.d.r.config.RepositoryConfigurationDelegate:132 - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
11112023-07-24 15:17:45.790[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.d.r.config.RepositoryConfigurationDelegate:201 - Finished Spring Data repository scanning in 30 ms. Found 0 Redis repository interfaces.
11112023-07-24 15:17:46.347[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer:108 - Tomcat initialized with port(s): 9090 (http)
11112023-07-24 15:17:46.359[TraceId: SpanId: ParentSpanId:] [main] INFO org.apache.coyote.http11.Http11NioProtocol:173 - Initializing ProtocolHandler ["http-nio-9090"]
11112023-07-24 15:17:46.360[TraceId: SpanId: ParentSpanId:] [main] INFO org.apache.catalina.core.StandardService:173 - Starting service [Tomcat]
11112023-07-24 15:17:46.360[TraceId: SpanId: ParentSpanId:] [main] INFO org.apache.catalina.core.StandardEngine:173 - Starting Servlet engine: [Apache Tomcat/9.0.69]
11112023-07-24 15:17:46.465[TraceId: SpanId: ParentSpanId:] [main] INFO o.a.c.c.ContainerBase.[Tomcat].[localhost].[/mj]:173 - Initializing Spring embedded WebApplicationContext
11112023-07-24 15:17:46.467[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext:290 - Root WebApplicationContext: initialization completed in 1546 ms
11112023-07-24 15:17:47.795[TraceId: SpanId: ParentSpanId:] [main] INFO s.d.s.w.WebMvcPropertySourcedRequestMappingHandlerMapping:69 - Mapped URL path [/v2/api-docs] onto method [springfox.documentation.swagger2.web.Swagger2ControllerWebMvc#getDocumentation(String, HttpServletRequest)]
11112023-07-24 15:17:47.918[TraceId: SpanId: ParentSpanId:] [main] INFO org.apache.coyote.http11.Http11NioProtocol:173 - Starting ProtocolHandler ["http-nio-9090"]
11112023-07-24 15:17:47.954[TraceId: SpanId: ParentSpanId:] [main] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer:220 - Tomcat started on port(s): 9090 (http) with context path '/mj'
11112023-07-24 15:17:47.955[TraceId: SpanId: ParentSpanId:] [main] INFO s.d.s.web.plugins.DocumentationPluginsBootstrapper:93 - Documentation plugins bootstrapped
11112023-07-24 15:17:47.962[TraceId: SpanId: ParentSpanId:] [main] INFO s.d.s.web.plugins.DocumentationPluginsBootstrapper:79 - Found 1 custom documentation plugin(s)
11112023-07-24 15:17:48.000[TraceId: SpanId: ParentSpanId:] [main] INFO s.d.spring.web.scanners.ApiListingReferenceScanner:44 - Scanning for api listing references
11112023-07-24 15:17:48.249[TraceId: SpanId: ParentSpanId:] [main] INFO com.github.novicezk.midjourney.ProxyApplication:61 - Started ProxyApplication in 4.381 seconds (JVM running for 5.115)
2023-07-24 15:47:42.909[TraceId: SpanId: ParentSpanId:] [ReadingThread] WARN c.g.n.midjourney.wss.user.UserWebSocketStarter:135 - [gateway] Websocket closed and will be reconnect... code: 1000, reason: reconnect
2023-07-24 15:12:04.945 [main 35808] INFO com.github.novicezk.midjourney.ProxyApplication:55 - Starting ProxyApplication using Java 17.0.8 on DCG027072 with PID 35808 (D:\PROJECT\midjourney-proxy\target\classes started by xushaohua in D:\PROJECT\midjourney-proxy)
2023-07-24 15:12:04.947 [main 35808] DEBUG com.github.novicezk.midjourney.ProxyApplication:56 - Running with Spring Boot v2.6.14, Spring v5.3.24
2023-07-24 15:12:04.948 [main 35808] INFO com.github.novicezk.midjourney.ProxyApplication:651 - The following 1 profile is active: "dev"
2023-07-24 15:12:05.603 [main 35808] INFO o.s.d.r.config.RepositoryConfigurationDelegate:262 - Multiple Spring Data modules found, entering strict repository configuration mode
2023-07-24 15:12:05.605 [main 35808] INFO o.s.d.r.config.RepositoryConfigurationDelegate:132 - Bootstrapping Spring Data Redis repositories in DEFAULT mode.
2023-07-24 15:12:05.647 [main 35808] INFO o.s.d.r.config.RepositoryConfigurationDelegate:201 - Finished Spring Data repository scanning in 27 ms. Found 0 Redis repository interfaces.
2023-07-24 15:12:06.273 [main 35808] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer:108 - Tomcat initialized with port(s): 9090 (http)
2023-07-24 15:12:06.283 [main 35808] INFO org.apache.coyote.http11.Http11NioProtocol:173 - Initializing ProtocolHandler ["http-nio-9090"]
2023-07-24 15:12:06.283 [main 35808] INFO org.apache.catalina.core.StandardService:173 - Starting service [Tomcat]
2023-07-24 15:12:06.283 [main 35808] INFO org.apache.catalina.core.StandardEngine:173 - Starting Servlet engine: [Apache Tomcat/9.0.69]
2023-07-24 15:12:06.404 [main 35808] INFO o.a.c.c.ContainerBase.[Tomcat].[localhost].[/mj]:173 - Initializing Spring embedded WebApplicationContext
2023-07-24 15:12:06.404 [main 35808] INFO o.s.b.w.s.c.ServletWebServerApplicationContext:290 - Root WebApplicationContext: initialization completed in 1413 ms
2023-07-24 15:12:07.610 [main 35808] INFO s.d.s.w.WebMvcPropertySourcedRequestMappingHandlerMapping:69 - Mapped URL path [/v2/api-docs] onto method [springfox.documentation.swagger2.web.Swagger2ControllerWebMvc#getDocumentation(String, HttpServletRequest)]
2023-07-24 15:12:07.746 [main 35808] INFO org.apache.coyote.http11.Http11NioProtocol:173 - Starting ProtocolHandler ["http-nio-9090"]
2023-07-24 15:12:07.787 [main 35808] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer:220 - Tomcat started on port(s): 9090 (http) with context path '/mj'
2023-07-24 15:12:07.788 [main 35808] INFO s.d.s.web.plugins.DocumentationPluginsBootstrapper:93 - Documentation plugins bootstrapped
2023-07-24 15:12:07.801 [main 35808] INFO s.d.s.web.plugins.DocumentationPluginsBootstrapper:79 - Found 1 custom documentation plugin(s)
2023-07-24 15:12:07.848 [main 35808] INFO s.d.spring.web.scanners.ApiListingReferenceScanner:44 - Scanning for api listing references
2023-07-24 15:12:08.107 [main 35808] INFO com.github.novicezk.midjourney.ProxyApplication:61 - Started ProxyApplication in 3.529 seconds (JVM running for 4.156)
2023-07-24 15:12:09.205 [ReadingThread 35808] DEBUG c.g.n.midjourney.wss.user.UserWebSocketStarter:79 - [gateway] Connected to websocket.
2023-07-24 15:51:44.495[TraceId: SpanId: ParentSpanId:] [WritingThread] WARN c.g.n.midjourney.wss.user.UserWebSocketStarter:135 - [gateway] Websocket closed and will be reconnect... code: 1002, reason: No more WebSocket frame from the server.
2023-07-24 15:51:57.951[TraceId: SpanId: ParentSpanId:] [WritingThread] WARN c.g.n.midjourney.wss.user.UserWebSocketStarter:135 - [gateway] Websocket closed and will be reconnect... code: 1002, reason: No more WebSocket frame from the server.
2023-07-24 15:52:05.895[TraceId: SpanId: ParentSpanId:] [WritingThread] WARN c.g.n.midjourney.wss.user.UserWebSocketStarter:135 - [gateway] Websocket closed and will be reconnect... code: 1002, reason: No more WebSocket frame from the server.
2023-07-24 15:52:15.726[TraceId: SpanId: ParentSpanId:] [WritingThread] WARN c.g.n.midjourney.wss.user.UserWebSocketStarter:135 - [gateway] Websocket closed and will be reconnect... code: 1002, reason: No more WebSocket frame from the server.
2023-07-24 15:52:18.615[TraceId: SpanId: ParentSpanId:] [WritingThread] WARN c.g.n.midjourney.wss.user.UserWebSocketStarter:135 - [gateway] Websocket closed and will be reconnect... code: 1002, reason: No more WebSocket frame from the server.
2023-07-24 16:01:23.316[TraceId: SpanId: ParentSpanId:] [main] ERROR org.springframework.boot.SpringApplication:835 - Application run failed
java.lang.IllegalStateException: Failed to execute ApplicationRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:776)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:763)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:314)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1317)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
at com.github.novicezk.midjourney.ProxyApplication.main(ProxyApplication.java:16)
Caused by: com.neovisionaries.ws.client.WebSocketException: Failed to connect to 'gateway.discord.gg:443': Connect timed out
at com.neovisionaries.ws.client.SocketConnector.connectSocket(SocketConnector.java:126)
at com.neovisionaries.ws.client.SocketConnector.doConnect(SocketConnector.java:238)
at com.neovisionaries.ws.client.SocketConnector.connect(SocketConnector.java:189)
at com.neovisionaries.ws.client.WebSocket.connect(WebSocket.java:2351)
at com.github.novicezk.midjourney.wss.user.UserWebSocketStarter.start(UserWebSocketStarter.java:74)
at spring.config.BeanConfig.lambda$enableMetaChangeReceiverInitializer$1(BeanConfig.java:80)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:773)
... 5 common frames omitted
Caused by: java.net.SocketTimeoutException: Connect timed out
at java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:546)
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:597)
at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327)
at java.base/java.net.Socket.connect(Socket.java:633)
at java.base/sun.security.ssl.SSLSocketImpl.connect(SSLSocketImpl.java:304)
at com.neovisionaries.ws.client.SocketInitiator$SocketRacer.run(SocketInitiator.java:126)
2023-07-24 15:47:42.909[TraceId: SpanId: ParentSpanId:] [ReadingThread] WARN c.g.n.midjourney.wss.user.UserWebSocketStarter:135 - [gateway] Websocket closed and will be reconnect... code: 1000, reason: reconnect
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.14</version>
</parent>
<groupId>com.github.novicezk</groupId>
<artifactId>midjourney-proxy</artifactId>
<version>2.3.5</version>
<properties>
<hutool.version>5.8.18</hutool.version>
<org-json.version>20220924</org-json.version>
<jda.version>5.0.0-beta.9</jda.version>
<chatgpt-java.version>1.0.14-beta1</chatgpt-java.version>
<dataurl.version>2.0.0</dataurl.version>
<knife4j.verison>4.1.0</knife4j.verison>
<user-agent-utils.verison>1.21</user-agent-utils.verison>
<httpclient.verison>4.5.14</httpclient.verison>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-cache</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>${org-json.version}</version>
</dependency>
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>${jda.version}</version>
<exclusions>
<exclusion>
<groupId>club.minnced</groupId>
<artifactId>opus-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.unfbx</groupId>
<artifactId>chatgpt-java</artifactId>
<version>${chatgpt-java.version}</version>
<exclusions>
<exclusion>
<artifactId>slf4j-simple</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>eu.maxschuster</groupId>
<artifactId>dataurl</artifactId>
<version>${dataurl.version}</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>${knife4j.verison}</version>
</dependency>
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>${user-agent-utils.verison}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.verison}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<finalName>midjourney-proxy-8090</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package com.github.novicezk.midjourney;
import lombok.experimental.UtilityClass;
@UtilityClass
public final class Constants {
// 任务扩展属性 start
public static final String TASK_PROPERTY_NOTIFY_HOOK = "notifyHook";
public static final String TASK_PROPERTY_FINAL_PROMPT = "finalPrompt";
public static final String TASK_PROPERTY_RELATED_TASK_ID = "relatedTaskId";
public static final String TASK_PROPERTY_MESSAGE_ID = "messageId";
public static final String TASK_PROPERTY_PROGRESS_MESSAGE_ID = "progressMessageId";
public static final String TASK_PROPERTY_FLAGS = "flags";
public static final String TASK_PROPERTY_MESSAGE_HASH = "messageHash";
// 任务扩展属性 end
public static final String API_SECRET_HEADER_NAME = "mj-api-secret";
}
package com.github.novicezk.midjourney;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableScheduling;
import spring.config.BeanConfig;
import spring.config.WebMvcConfig;
@EnableScheduling
@SpringBootApplication
@Import({BeanConfig.class, WebMvcConfig.class})
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
}
package com.github.novicezk.midjourney;
import com.github.novicezk.midjourney.enums.TranslateWay;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Data
@Component
@ConfigurationProperties(prefix = "mj")
public class ProxyProperties {
/**
* task存储配置.
*/
private final TaskStore taskStore = new TaskStore();
/**
* discord配置.
*/
private final DiscordConfig discord = new DiscordConfig();
/**
* 代理配置.
*/
private final ProxyConfig proxy = new ProxyConfig();
/**
* 反代配置.
*/
private final NgDiscordConfig ngDiscord = new NgDiscordConfig();
/**
* 任务队列配置.
*/
private final TaskQueueConfig queue = new TaskQueueConfig();
/**
* 百度翻译配置.
*/
private final BaiduTranslateConfig baiduTranslate = new BaiduTranslateConfig();
/**
* openai配置.
*/
private final OpenaiConfig openai = new OpenaiConfig();
/**
* 中文prompt翻译方式.
*/
private TranslateWay translateWay = TranslateWay.NULL;
/**
* 接口密钥,为空不启用鉴权;调用接口时需要加请求头 mj-api-secret.
*/
private String apiSecret;
/**
* 任务状态变更回调地址.
*/
private String notifyHook;
/**
* 通知回调线程池大小.
*/
private int notifyPoolSize = 10;
/**
* 接口是否返回任务扩展属性.
*/
private boolean includeTaskExtended = false;
@Data
public static class DiscordConfig {
/**
* 你的服务器id.
*/
private String guildId;
/**
* 你的频道id.
*/
private String channelId;
/**
* 你的登录token.
*/
private String userToken;
/**
* 你的频道id.
*/
private String sessionId = "9c4055428e13bcbf2248a6b36084c5f3";
/**
* 调用discord接口、连接wss时的user-agent.
*/
private String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36";
/**
* 是否使用user_token连接wss,默认启用.
*/
private boolean userWss = true;
/**
* 你的机器人token.
*/
private String botToken;
}
@Data
public static class BaiduTranslateConfig {
/**
* 百度翻译的APP_ID.
*/
private String appid;
/**
* 百度翻译的密钥.
*/
private String appSecret;
}
@Data
public static class OpenaiConfig {
/**
* 自定义gpt的api-url.
*/
private String gptApiUrl;
/**
* gpt的api-key.
*/
private String gptApiKey;
/**
* 超时时间.
*/
private Duration timeout = Duration.ofSeconds(30);
/**
* 使用的模型.
*/
private String model = "gpt-3.5-turbo";
/**
* 返回结果的最大分词数.
*/
private int maxTokens = 2048;
/**
* 相似度,取值 0-2.
*/
private double temperature = 0;
}
@Data
public static class TaskStore {
/**
* 任务过期时间,默认30天.
*/
private Duration timeout = Duration.ofDays(30);
/**
* 任务存储方式: redis(默认)、in_memory.
*/
private Type type = Type.IN_MEMORY;
public enum Type {
/**
* redis.
*/
REDIS,
/**
* in_memory.
*/
IN_MEMORY
}
}
@Data
public static class ProxyConfig {
/**
* 代理host.
*/
private String host;
/**
* 代理端口.
*/
private Integer port;
}
@Data
public static class NgDiscordConfig {
/**
* https://discord.com 反代.
*/
private String server;
/**
* https://cdn.discordapp.com 反代.
*/
private String cdn;
/**
* wss://gateway.discord.gg 反代.
*/
private String wss;
}
@Data
public static class TaskQueueConfig {
/**
* 并发数.
*/
private int coreSize = 3;
/**
* 等待队列长度.
*/
private int queueSize = 10;
/**
* 任务超时时间(分钟).
*/
private int timeoutMinutes = 5;
}
}
package com.github.novicezk.midjourney;
import lombok.experimental.UtilityClass;
@UtilityClass
public final class ReturnCode {
/**
* 成功.
*/
public static final int SUCCESS = 1;
/**
* 数据未找到.
*/
public static final int NOT_FOUND = 3;
/**
* 校验错误.
*/
public static final int VALIDATION_ERROR = 4;
/**
* 系统异常.
*/
public static final int FAILURE = 9;
/**
* 已存在.
*/
public static final int EXISTED = 21;
/**
* 排队中.
*/
public static final int IN_QUEUE = 22;
/**
* 队列已满.
*/
public static final int QUEUE_REJECTED = 23;
/**
* prompt包含敏感词.
*/
public static final int BANNED_PROMPT = 24;
}
\ No newline at end of file
package com.github.novicezk.midjourney.controller;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.RandomUtil;
import com.github.novicezk.midjourney.Constants;
import com.github.novicezk.midjourney.ProxyProperties;
import com.github.novicezk.midjourney.ReturnCode;
import com.github.novicezk.midjourney.dto.BaseSubmitDTO;
import com.github.novicezk.midjourney.dto.SubmitBlendDTO;
import com.github.novicezk.midjourney.dto.SubmitChangeDTO;
import com.github.novicezk.midjourney.dto.SubmitDescribeDTO;
import com.github.novicezk.midjourney.dto.SubmitImagineDTO;
import com.github.novicezk.midjourney.dto.SubmitSimpleChangeDTO;
import com.github.novicezk.midjourney.enums.TaskAction;
import com.github.novicezk.midjourney.enums.TaskStatus;
import com.github.novicezk.midjourney.result.SubmitResultVO;
import com.github.novicezk.midjourney.service.TaskService;
import com.github.novicezk.midjourney.service.TaskStoreService;
import com.github.novicezk.midjourney.service.TranslateService;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskCondition;
import com.github.novicezk.midjourney.util.BannedPromptUtils;
import com.github.novicezk.midjourney.util.ConvertUtils;
import com.github.novicezk.midjourney.util.MimeTypeUtils;
import com.github.novicezk.midjourney.util.TaskChangeParams;
import eu.maxschuster.dataurl.DataUrl;
import eu.maxschuster.dataurl.DataUrlSerializer;
import eu.maxschuster.dataurl.IDataUrlSerializer;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Api(tags = "任务提交")
@RestController
@RequestMapping("/submit")
@RequiredArgsConstructor
public class SubmitController {
private final TranslateService translateService;
private final TaskStoreService taskStoreService;
private final ProxyProperties properties;
private final TaskService taskService;
@ApiOperation(value = "提交Imagine任务")
@PostMapping("/imagine")
public SubmitResultVO imagine(@RequestBody SubmitImagineDTO imagineDTO) {
String prompt = imagineDTO.getPrompt();
if (CharSequenceUtil.isBlank(prompt)) {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "prompt不能为空");
}
prompt = prompt.trim();
Task task = newTask(imagineDTO);
task.setAction(TaskAction.IMAGINE);
task.setPrompt(prompt);
String promptEn;
int paramStart = prompt.indexOf(" --");
if (paramStart > 0) {
promptEn = this.translateService.translateToEnglish(prompt.substring(0, paramStart)).trim() + prompt.substring(paramStart);
} else {
promptEn = this.translateService.translateToEnglish(prompt).trim();
}
if (CharSequenceUtil.isBlank(promptEn)) {
promptEn = prompt;
}
if (BannedPromptUtils.isBanned(promptEn)) {
return SubmitResultVO.fail(ReturnCode.BANNED_PROMPT, "可能包含敏感词");
}
DataUrl dataUrl = null;
if (CharSequenceUtil.isNotBlank(imagineDTO.getBase64())) {
IDataUrlSerializer serializer = new DataUrlSerializer();
try {
dataUrl = serializer.unserialize(imagineDTO.getBase64());
} catch (MalformedURLException e) {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "basisImageBase64格式错误");
}
}
task.setPromptEn(promptEn);
task.setDescription("/imagine " + prompt);
return this.taskService.submitImagine(task, dataUrl);
}
@ApiOperation(value = "绘图变化-simple")
@PostMapping("/simple-change")
public SubmitResultVO simpleChange(@RequestBody SubmitSimpleChangeDTO simpleChangeDTO) {
TaskChangeParams changeParams = ConvertUtils.convertChangeParams(simpleChangeDTO.getContent());
if (changeParams == null) {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "content参数错误");
}
SubmitChangeDTO changeDTO = new SubmitChangeDTO();
changeDTO.setAction(changeParams.getAction());
changeDTO.setTaskId(changeParams.getId());
changeDTO.setIndex(changeParams.getIndex());
changeDTO.setState(simpleChangeDTO.getState());
changeDTO.setNotifyHook(simpleChangeDTO.getNotifyHook());
return change(changeDTO);
}
@ApiOperation(value = "绘图变化")
@PostMapping("/change")
public SubmitResultVO change(@RequestBody SubmitChangeDTO changeDTO) {
if (CharSequenceUtil.isBlank(changeDTO.getTaskId())) {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "taskId不能为空");
}
if (!Set.of(TaskAction.UPSCALE, TaskAction.VARIATION, TaskAction.REROLL).contains(changeDTO.getAction())) {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "action参数错误");
}
String description = "/up " + changeDTO.getTaskId();
if (TaskAction.REROLL.equals(changeDTO.getAction())) {
description += " R";
} else {
description += " " + changeDTO.getAction().name().charAt(0) + changeDTO.getIndex();
}
TaskCondition condition = new TaskCondition().setDescription(description);
Task existTask = this.taskStoreService.findOne(condition);
if (existTask != null) {
return SubmitResultVO.of(ReturnCode.EXISTED, "任务已存在", existTask.getId())
.setProperty("status", existTask.getStatus())
.setProperty("imageUrl", existTask.getImageUrl());
}
Task targetTask = this.taskStoreService.get(changeDTO.getTaskId());
if (targetTask == null) {
return SubmitResultVO.fail(ReturnCode.NOT_FOUND, "关联任务不存在或已失效");
}
if (!TaskStatus.SUCCESS.equals(targetTask.getStatus())) {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "关联任务状态错误");
}
if (!Set.of(TaskAction.IMAGINE, TaskAction.VARIATION, TaskAction.BLEND).contains(targetTask.getAction())) {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "关联任务不允许执行变化");
}
Task task = newTask(changeDTO);
task.setAction(changeDTO.getAction());
task.setPrompt(targetTask.getPrompt());
task.setPromptEn(targetTask.getPromptEn());
task.setProperty(Constants.TASK_PROPERTY_FINAL_PROMPT, targetTask.getProperty(Constants.TASK_PROPERTY_FINAL_PROMPT));
task.setDescription(description);
int messageFlags = targetTask.getPropertyGeneric(Constants.TASK_PROPERTY_FLAGS);
String messageId = targetTask.getPropertyGeneric(Constants.TASK_PROPERTY_MESSAGE_ID);
String messageHash = targetTask.getPropertyGeneric(Constants.TASK_PROPERTY_MESSAGE_HASH);
if (TaskAction.UPSCALE.equals(changeDTO.getAction())) {
return this.taskService.submitUpscale(task, messageId, messageHash, changeDTO.getIndex(), messageFlags);
} else if (TaskAction.VARIATION.equals(changeDTO.getAction())) {
return this.taskService.submitVariation(task, messageId, messageHash, changeDTO.getIndex(), messageFlags);
} else {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "不支持的操作: " + changeDTO.getAction());
}
}
@ApiOperation(value = "提交Describe任务")
@PostMapping("/describe")
public SubmitResultVO describe(@RequestBody SubmitDescribeDTO describeDTO) {
if (CharSequenceUtil.isBlank(describeDTO.getBase64())) {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "base64不能为空");
}
IDataUrlSerializer serializer = new DataUrlSerializer();
DataUrl dataUrl;
try {
dataUrl = serializer.unserialize(describeDTO.getBase64());
} catch (MalformedURLException e) {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "base64格式错误");
}
Task task = newTask(describeDTO);
task.setAction(TaskAction.DESCRIBE);
String taskFileName = task.getId() + "." + MimeTypeUtils.guessFileSuffix(dataUrl.getMimeType());
task.setDescription("/describe " + taskFileName);
return this.taskService.submitDescribe(task, dataUrl);
}
@ApiOperation(value = "提交Blend任务")
@PostMapping("/blend")
public SubmitResultVO blend(@RequestBody SubmitBlendDTO blendDTO) {
List<String> base64Array = blendDTO.getBase64Array();
if (base64Array == null || base64Array.size() < 2 || base64Array.size() > 5) {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "base64List参数错误");
}
if (blendDTO.getDimensions() == null) {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "dimensions参数错误");
}
IDataUrlSerializer serializer = new DataUrlSerializer();
List<DataUrl> dataUrlList = new ArrayList<>();
try {
for (String base64 : base64Array) {
DataUrl dataUrl = serializer.unserialize(base64);
dataUrlList.add(dataUrl);
}
} catch (MalformedURLException e) {
return SubmitResultVO.fail(ReturnCode.VALIDATION_ERROR, "base64格式错误");
}
Task task = newTask(blendDTO);
task.setAction(TaskAction.BLEND);
task.setDescription("/blend " + task.getId() + " " + dataUrlList.size());
return this.taskService.submitBlend(task, dataUrlList, blendDTO.getDimensions());
}
private Task newTask(BaseSubmitDTO base) {
Task task = new Task();
task.setId(RandomUtil.randomNumbers(16));
task.setSubmitTime(System.currentTimeMillis());
task.setState(base.getState());
String notifyHook = CharSequenceUtil.isBlank(base.getNotifyHook()) ? this.properties.getNotifyHook() : base.getNotifyHook();
task.setProperty(Constants.TASK_PROPERTY_NOTIFY_HOOK, notifyHook);
return task;
}
}
package com.github.novicezk.midjourney.controller;
import cn.hutool.core.comparator.CompareUtil;
import com.github.novicezk.midjourney.dto.TaskConditionDTO;
import com.github.novicezk.midjourney.service.TaskStoreService;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskQueueHelper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@Api(tags = "任务查询")
@RestController
@RequestMapping("/task")
@RequiredArgsConstructor
public class TaskController {
private final TaskStoreService taskStoreService;
private final TaskQueueHelper taskQueueHelper;
@ApiOperation(value = "查询所有任务")
@GetMapping("/list")
public List<Task> list() {
return this.taskStoreService.list().stream()
.sorted((t1, t2) -> CompareUtil.compare(t2.getSubmitTime(), t1.getSubmitTime()))
.toList();
}
@ApiOperation(value = "指定ID获取任务")
@GetMapping("/{id}/fetch")
public Task fetch(@ApiParam(value = "任务ID") @PathVariable String id) {
return this.taskStoreService.get(id);
}
@ApiOperation(value = "查询任务队列")
@GetMapping("/queue")
public List<Task> queue() {
Set<String> queueTaskIds = this.taskQueueHelper.getQueueTaskIds();
return queueTaskIds.stream().map(this.taskStoreService::get).filter(Objects::nonNull)
.sorted(Comparator.comparing(Task::getSubmitTime))
.toList();
}
@ApiOperation(value = "根据条件查询任务")
@PostMapping("/list-by-condition")
public List<Task> listByCondition(@RequestBody TaskConditionDTO conditionDTO) {
if (conditionDTO.getIds() == null) {
return Collections.emptyList();
}
return conditionDTO.getIds().stream().map(this.taskStoreService::get).filter(Objects::nonNull).toList();
}
}
package com.github.novicezk.midjourney.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public abstract class BaseSubmitDTO {
@ApiModelProperty("自定义参数")
protected String state;
@ApiModelProperty("回调地址, 为空时使用全局notifyHook")
protected String notifyHook;
}
package com.github.novicezk.midjourney.dto;
import com.github.novicezk.midjourney.enums.BlendDimensions;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Data
@ApiModel("Blend提交参数")
@EqualsAndHashCode(callSuper = true)
public class SubmitBlendDTO extends BaseSubmitDTO {
@ApiModelProperty(value = "图片base64数组", required = true, example = "[\"data:image/png;base64,xxx1\", \"data:image/png;base64,xxx2\"]")
private List<String> base64Array;
@ApiModelProperty(value = "比例: PORTRAIT(2:3); SQUARE(1:1); LANDSCAPE(3:2)", example = "SQUARE")
private BlendDimensions dimensions = BlendDimensions.SQUARE;
}
package com.github.novicezk.midjourney.dto;
import com.github.novicezk.midjourney.enums.TaskAction;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@ApiModel("变化任务提交参数")
@EqualsAndHashCode(callSuper = true)
public class SubmitChangeDTO extends BaseSubmitDTO {
@ApiModelProperty(value = "任务ID", required = true, example = "\"1320098173412546\"")
private String taskId;
@ApiModelProperty(value = "UPSCALE(放大); VARIATION(变换); REROLL(重新生成)", required = true,
allowableValues = "UPSCALE, VARIATION, REROLL", example = "UPSCALE")
private TaskAction action;
@ApiModelProperty(value = "序号(1~4), action为UPSCALE,VARIATION时必传", allowableValues = "range[1, 4]", example = "1")
private Integer index;
}
package com.github.novicezk.midjourney.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@ApiModel("Describe提交参数")
@EqualsAndHashCode(callSuper = true)
public class SubmitDescribeDTO extends BaseSubmitDTO {
@ApiModelProperty(value = "图片base64", required = true, example = "data:image/png;base64,xxx")
private String base64;
}
package com.github.novicezk.midjourney.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@ApiModel("Imagine提交参数")
@EqualsAndHashCode(callSuper = true)
public class SubmitImagineDTO extends BaseSubmitDTO {
@ApiModelProperty(value = "提示词", required = true, example = "Cat")
private String prompt;
@ApiModelProperty(value = "垫图base64")
private String base64;
}
package com.github.novicezk.midjourney.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@ApiModel("变化任务提交参数-simple")
@EqualsAndHashCode(callSuper = true)
public class SubmitSimpleChangeDTO extends BaseSubmitDTO {
@ApiModelProperty(value = "变化描述: ID $action$index", required = true, example = "1320098173412546 U2")
private String content;
}
package com.github.novicezk.midjourney.dto;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import java.util.List;
@Data
@ApiModel("任务查询参数")
public class TaskConditionDTO {
private List<String> ids;
}
package com.github.novicezk.midjourney.enums;
public enum BlendDimensions {
PORTRAIT("2:3"),
SQUARE("1:1"),
LANDSCAPE("3:2");
private final String value;
BlendDimensions(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
package com.github.novicezk.midjourney.enums;
public enum MessageType {
/**
* 创建.
*/
CREATE,
/**
* 修改.
*/
UPDATE,
/**
* 删除.
*/
DELETE;
public static MessageType of(String type) {
return switch (type) {
case "MESSAGE_CREATE" -> CREATE;
case "MESSAGE_UPDATE" -> UPDATE;
case "MESSAGE_DELETE" -> DELETE;
default -> null;
};
}
}
package com.github.novicezk.midjourney.enums;
public enum TaskAction {
/**
* 生成图片.
*/
IMAGINE,
/**
* 选中放大.
*/
UPSCALE,
/**
* 选中其中的一张图,生成四张相似的.
*/
VARIATION,
/**
* 重新生成.
*/
REROLL,
/**
* 图转prompt.
*/
DESCRIBE,
/**
* 多图混合.
*/
BLEND
}
package com.github.novicezk.midjourney.enums;
public enum TaskStatus {
/**
* 未启动.
*/
NOT_START,
/**
* 已提交.
*/
SUBMITTED,
/**
* 执行中.
*/
IN_PROGRESS,
/**
* 失败.
*/
FAILURE,
/**
* 成功.
*/
SUCCESS
}
package com.github.novicezk.midjourney.enums;
public enum TranslateWay {
/**
* 百度翻译.
*/
BAIDU,
/**
* GPT翻译.
*/
GPT,
/**
* 不翻译.
*/
NULL
}
package com.github.novicezk.midjourney.exception;
public class ConnectionManuallyClosedException extends Exception {
public ConnectionManuallyClosedException(String message) {
super(message);
}
}
\ No newline at end of file
package com.github.novicezk.midjourney.exception;
public class ConnectionResumableException extends Exception {
public ConnectionResumableException(String message) {
super(message);
}
}
\ No newline at end of file
package com.github.novicezk.midjourney.exception;
public class InvalidSessionException extends Exception {
public InvalidSessionException(String message) {
super(message);
}
}
\ No newline at end of file
package com.github.novicezk.midjourney.exception;
public class NeedToReconnectException extends Exception {
public NeedToReconnectException(String message) {
super(message);
}
}
\ No newline at end of file
package com.github.novicezk.midjourney.result;
import com.github.novicezk.midjourney.ReturnCode;
import lombok.Getter;
@Getter
public class Message<T> {
private final int code;
private final String description;
private final T result;
public static <Y> Message<Y> success() {
return new Message<>(ReturnCode.SUCCESS, "成功");
}
public static <T> Message<T> success(T result) {
return new Message<>(ReturnCode.SUCCESS, "成功", result);
}
public static <T> Message<T> success(int code, String description, T result) {
return new Message<>(code, description, result);
}
public static <Y> Message<Y> notFound() {
return new Message<>(ReturnCode.NOT_FOUND, "数据未找到");
}
public static <Y> Message<Y> validationError() {
return new Message<>(ReturnCode.VALIDATION_ERROR, "校验错误");
}
public static <Y> Message<Y> failure() {
return new Message<>(ReturnCode.FAILURE, "系统异常");
}
public static <Y> Message<Y> failure(String description) {
return new Message<>(ReturnCode.FAILURE, description);
}
public static <Y> Message<Y> of(int code, String description) {
return new Message<>(code, description);
}
public static <T> Message<T> of(int code, String description, T result) {
return new Message<>(code, description, result);
}
private Message(int code, String description) {
this(code, description, null);
}
private Message(int code, String description, T result) {
this.code = code;
this.description = description;
this.result = result;
}
}
package com.github.novicezk.midjourney.result;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
@ApiModel("提交结果")
public class SubmitResultVO {
@ApiModelProperty(value = "状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误)", required = true, example = "1")
private int code;
@ApiModelProperty(value = "描述", required = true, example = "提交成功")
private String description;
@ApiModelProperty(value = "任务ID", example = "1320098173412546")
private String result;
@ApiModelProperty(value = "扩展字段")
private Map<String, Object> properties = new HashMap<>();
public SubmitResultVO setProperty(String name, Object value) {
this.properties.put(name, value);
return this;
}
public SubmitResultVO removeProperty(String name) {
this.properties.remove(name);
return this;
}
public Object getProperty(String name) {
return this.properties.get(name);
}
@SuppressWarnings("unchecked")
public <T> T getPropertyGeneric(String name) {
return (T) getProperty(name);
}
public <T> T getProperty(String name, Class<T> clz) {
return clz.cast(getProperty(name));
}
public static SubmitResultVO of(int code, String description, String result) {
return new SubmitResultVO(code, description, result);
}
public static SubmitResultVO fail(int code, String description) {
return new SubmitResultVO(code, description, null);
}
private SubmitResultVO(int code, String description, String result) {
this.code = code;
this.description = description;
this.result = result;
}
}
package com.github.novicezk.midjourney.service;
import com.github.novicezk.midjourney.enums.BlendDimensions;
import com.github.novicezk.midjourney.result.Message;
import eu.maxschuster.dataurl.DataUrl;
import java.util.List;
public interface DiscordService {
Message<Void> imagine(String prompt);
Message<Void> upscale(String messageId, int index, String messageHash, int messageFlags);
Message<Void> variation(String messageId, int index, String messageHash, int messageFlags);
Message<Void> reroll(String messageId, String messageHash, int messageFlags);
Message<Void> describe(String finalFileName);
Message<Void> blend(List<String> finalFileNames, BlendDimensions dimensions);
Message<String> upload(String fileName, DataUrl dataUrl);
Message<String> sendImageMessage(String content, String finalFileName);
}
package com.github.novicezk.midjourney.service;
import com.github.novicezk.midjourney.support.Task;
public interface NotifyService {
void notifyTaskChange(Task task);
}
package com.github.novicezk.midjourney.service;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.exceptions.CheckedUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.novicezk.midjourney.Constants;
import com.github.novicezk.midjourney.ProxyProperties;
import com.github.novicezk.midjourney.enums.TaskStatus;
import com.github.novicezk.midjourney.support.Task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
@Slf4j
@Service
public class NotifyServiceImpl implements NotifyService {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final ThreadPoolTaskExecutor executor;
private final TimedCache<String, Object> taskLocks = CacheUtil.newTimedCache(Duration.ofHours(1).toMillis());
public NotifyServiceImpl(ProxyProperties properties) {
this.executor = new ThreadPoolTaskExecutor();
this.executor.setCorePoolSize(properties.getNotifyPoolSize());
this.executor.setThreadNamePrefix("TaskNotify-");
this.executor.initialize();
}
@Override
public void notifyTaskChange(Task task) {
String notifyHook = task.getPropertyGeneric(Constants.TASK_PROPERTY_NOTIFY_HOOK);
if (CharSequenceUtil.isBlank(notifyHook)) {
return;
}
String taskId = task.getId();
TaskStatus taskStatus = task.getStatus();
Object taskLock = this.taskLocks.get(taskId, (CheckedUtil.Func0Rt<Object>) Object::new);
try {
String paramsStr = OBJECT_MAPPER.writeValueAsString(task);
this.executor.execute(() -> {
synchronized (taskLock) {
try {
ResponseEntity<String> responseEntity = postJson(notifyHook, paramsStr);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
log.debug("推送任务变更成功, 任务ID: {}, status: {}, notifyHook: {}", taskId, taskStatus, notifyHook);
} else {
log.warn("推送任务变更失败, 任务ID: {}, notifyHook: {}, code: {}, msg: {}", taskId, notifyHook, responseEntity.getStatusCodeValue(), responseEntity.getBody());
}
} catch (Exception e) {
log.warn("推送任务变更失败, 任务ID: {}, notifyHook: {}, 描述: {}", taskId, notifyHook, e.getMessage());
}
}
});
} catch (JsonProcessingException e) {
log.warn("推送任务变更失败, 任务ID: {}, notifyHook: {}, 描述: {}", taskId, notifyHook, e.getMessage());
}
}
private ResponseEntity<String> postJson(String notifyHook, String paramsJson) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> httpEntity = new HttpEntity<>(paramsJson, headers);
return new RestTemplate().postForEntity(notifyHook, httpEntity, String.class);
}
}
package com.github.novicezk.midjourney.service;
import com.github.novicezk.midjourney.enums.BlendDimensions;
import com.github.novicezk.midjourney.result.SubmitResultVO;
import com.github.novicezk.midjourney.support.Task;
import eu.maxschuster.dataurl.DataUrl;
import java.util.List;
public interface TaskService {
SubmitResultVO submitImagine(Task task, DataUrl dataUrl);
SubmitResultVO submitUpscale(Task task, String targetMessageId, String targetMessageHash, int index, int messageFlags);
SubmitResultVO submitVariation(Task task, String targetMessageId, String targetMessageHash, int index, int messageFlags);
SubmitResultVO submitDescribe(Task task, DataUrl dataUrl);
SubmitResultVO submitBlend(Task task, List<DataUrl> dataUrls, BlendDimensions dimensions);
}
\ No newline at end of file
package com.github.novicezk.midjourney.service;
import com.github.novicezk.midjourney.ReturnCode;
import com.github.novicezk.midjourney.enums.BlendDimensions;
import com.github.novicezk.midjourney.result.Message;
import com.github.novicezk.midjourney.result.SubmitResultVO;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskQueueHelper;
import com.github.novicezk.midjourney.util.MimeTypeUtils;
import eu.maxschuster.dataurl.DataUrl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class TaskServiceImpl implements TaskService {
private final TaskStoreService taskStoreService;
private final DiscordService discordService;
private final TaskQueueHelper taskQueueHelper;
@Override
public SubmitResultVO submitImagine(Task task, DataUrl dataUrl) {
return this.taskQueueHelper.submitTask(task, () -> {
if (dataUrl != null) {
String taskFileName = task.getId() + "." + MimeTypeUtils.guessFileSuffix(dataUrl.getMimeType());
Message<String> uploadResult = this.discordService.upload(taskFileName, dataUrl);
if (uploadResult.getCode() != ReturnCode.SUCCESS) {
return Message.of(uploadResult.getCode(), uploadResult.getDescription());
}
String finalFileName = uploadResult.getResult();
Message<String> sendImageResult = this.discordService.sendImageMessage("upload image: " + finalFileName, finalFileName);
if (sendImageResult.getCode() != ReturnCode.SUCCESS) {
return Message.of(sendImageResult.getCode(), sendImageResult.getDescription());
}
task.setPrompt(sendImageResult.getResult() + " " + task.getPrompt());
task.setPromptEn(sendImageResult.getResult() + " " + task.getPromptEn());
task.setDescription("/imagine " + task.getPrompt());
this.taskStoreService.save(task);
}
return this.discordService.imagine(task.getPromptEn());
});
}
@Override
public SubmitResultVO submitUpscale(Task task, String targetMessageId, String targetMessageHash, int index, int messageFlags) {
return this.taskQueueHelper.submitTask(task, () -> this.discordService.upscale(targetMessageId, index, targetMessageHash, messageFlags));
}
@Override
public SubmitResultVO submitVariation(Task task, String targetMessageId, String targetMessageHash, int index, int messageFlags) {
return this.taskQueueHelper.submitTask(task, () -> this.discordService.variation(targetMessageId, index, targetMessageHash, messageFlags));
}
@Override
public SubmitResultVO submitDescribe(Task task, DataUrl dataUrl) {
return this.taskQueueHelper.submitTask(task, () -> {
String taskFileName = task.getId() + "." + MimeTypeUtils.guessFileSuffix(dataUrl.getMimeType());
Message<String> uploadResult = this.discordService.upload(taskFileName, dataUrl);
if (uploadResult.getCode() != ReturnCode.SUCCESS) {
return Message.of(uploadResult.getCode(), uploadResult.getDescription());
}
String finalFileName = uploadResult.getResult();
return this.discordService.describe(finalFileName);
});
}
@Override
public SubmitResultVO submitBlend(Task task, List<DataUrl> dataUrls, BlendDimensions dimensions) {
return this.taskQueueHelper.submitTask(task, () -> {
List<String> finalFileNames = new ArrayList<>();
for (DataUrl dataUrl : dataUrls) {
String taskFileName = task.getId() + "." + MimeTypeUtils.guessFileSuffix(dataUrl.getMimeType());
Message<String> uploadResult = this.discordService.upload(taskFileName, dataUrl);
if (uploadResult.getCode() != ReturnCode.SUCCESS) {
return Message.of(uploadResult.getCode(), uploadResult.getDescription());
}
finalFileNames.add(uploadResult.getResult());
}
return this.discordService.blend(finalFileNames, dimensions);
});
}
}
package com.github.novicezk.midjourney.service;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskCondition;
import java.util.List;
public interface TaskStoreService {
void save(Task task);
void delete(String id);
Task get(String id);
List<Task> list();
List<Task> list(TaskCondition condition);
Task findOne(TaskCondition condition);
}
package com.github.novicezk.midjourney.service;
import java.util.regex.Pattern;
public interface TranslateService {
String translateToEnglish(String prompt);
default boolean containsChinese(String prompt) {
return Pattern.compile("[\u4e00-\u9fa5]").matcher(prompt).find();
}
}
package com.github.novicezk.midjourney.service.store;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.stream.StreamUtil;
import com.github.novicezk.midjourney.service.TaskStoreService;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskCondition;
import java.time.Duration;
import java.util.List;
public class InMemoryTaskStoreServiceImpl implements TaskStoreService {
private final TimedCache<String, Task> taskMap;
public InMemoryTaskStoreServiceImpl(Duration timeout) {
this.taskMap = CacheUtil.newTimedCache(timeout.toMillis());
}
@Override
public void save(Task task) {
this.taskMap.put(task.getId(), task);
}
@Override
public void delete(String key) {
this.taskMap.remove(key);
}
@Override
public Task get(String key) {
return this.taskMap.get(key);
}
@Override
public List<Task> list() {
return ListUtil.toList(this.taskMap.iterator());
}
@Override
public List<Task> list(TaskCondition condition) {
return StreamUtil.of(this.taskMap.iterator()).filter(condition).toList();
}
@Override
public Task findOne(TaskCondition condition) {
return StreamUtil.of(this.taskMap.iterator()).filter(condition).findFirst().orElse(null);
}
}
package com.github.novicezk.midjourney.service.store;
import com.github.novicezk.midjourney.service.TaskStoreService;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskCondition;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.ValueOperations;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
public class RedisTaskStoreServiceImpl implements TaskStoreService {
private static final String KEY_PREFIX = "mj-task-store::";
private final Duration timeout;
private final RedisTemplate<String, Task> redisTemplate;
public RedisTaskStoreServiceImpl(Duration timeout, RedisTemplate<String, Task> redisTemplate) {
this.timeout = timeout;
this.redisTemplate = redisTemplate;
}
@Override
public void save(Task task) {
this.redisTemplate.opsForValue().set(getRedisKey(task.getId()), task, this.timeout);
}
@Override
public void delete(String id) {
this.redisTemplate.delete(getRedisKey(id));
}
@Override
public Task get(String id) {
return this.redisTemplate.opsForValue().get(getRedisKey(id));
}
@Override
public List<Task> list() {
Set<String> keys = this.redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().match(KEY_PREFIX + "*").count(1000).build());
return cursor.stream().map(String::new).collect(Collectors.toSet());
});
if (keys == null || keys.isEmpty()) {
return Collections.emptyList();
}
ValueOperations<String, Task> operations = this.redisTemplate.opsForValue();
return keys.stream().map(operations::get)
.filter(Objects::nonNull)
.toList();
}
@Override
public List<Task> list(TaskCondition condition) {
return list().stream().filter(condition).toList();
}
@Override
public Task findOne(TaskCondition condition) {
return list().stream().filter(condition).findFirst().orElse(null);
}
private String getRedisKey(String id) {
return KEY_PREFIX + id;
}
}
package com.github.novicezk.midjourney.service.translate;
import cn.hutool.core.exceptions.ValidateException;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.digest.MD5;
import com.github.novicezk.midjourney.ProxyProperties;
import com.github.novicezk.midjourney.service.TranslateService;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;
import org.springframework.beans.factory.support.BeanDefinitionValidationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
@Slf4j
public class BaiduTranslateServiceImpl implements TranslateService {
private static final String TRANSLATE_API = "https://fanyi-api.baidu.com/api/trans/vip/translate";
private final String appid;
private final String appSecret;
public BaiduTranslateServiceImpl(ProxyProperties.BaiduTranslateConfig translateConfig) {
this.appid = translateConfig.getAppid();
this.appSecret = translateConfig.getAppSecret();
if (!CharSequenceUtil.isAllNotBlank(this.appid, this.appSecret)) {
throw new BeanDefinitionValidationException("mj-proxy.baidu-translate.appid或mj-proxy.baidu-translate.app-secret未配置");
}
}
@Override
public String translateToEnglish(String prompt) {
if (!containsChinese(prompt)) {
return prompt;
}
String salt = RandomUtil.randomNumbers(5);
String sign = MD5.create().digestHex(this.appid + prompt + salt + this.appSecret);
String url = TRANSLATE_API + "?from=zh&to=en&appid=" + this.appid + "&salt=" + salt + "&q=" + prompt + "&sign=" + sign;
try {
ResponseEntity<String> responseEntity = new RestTemplate().getForEntity(url, String.class);
if (responseEntity.getStatusCode() != HttpStatus.OK || CharSequenceUtil.isBlank(responseEntity.getBody())) {
throw new ValidateException(responseEntity.getStatusCodeValue() + " - " + responseEntity.getBody());
}
JSONObject result = new JSONObject(responseEntity.getBody());
if (result.has("error_code")) {
throw new ValidateException(result.getString("error_code") + " - " + result.getString("error_msg"));
}
return result.getJSONArray("trans_result").getJSONObject(0).getString("dst");
} catch (Exception e) {
log.warn("调用百度翻译失败: {}", e.getMessage());
}
return prompt;
}
}
package com.github.novicezk.midjourney.service.translate;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.ProxyProperties;
import com.github.novicezk.midjourney.service.TranslateService;
import com.unfbx.chatgpt.OpenAiClient;
import com.unfbx.chatgpt.entity.chat.ChatChoice;
import com.unfbx.chatgpt.entity.chat.ChatCompletion;
import com.unfbx.chatgpt.entity.chat.ChatCompletionResponse;
import com.unfbx.chatgpt.entity.chat.Message;
import com.unfbx.chatgpt.function.KeyRandomStrategy;
import com.unfbx.chatgpt.interceptor.OpenAILogger;
import com.unfbx.chatgpt.interceptor.OpenAiResponseInterceptor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import org.springframework.beans.factory.support.BeanDefinitionValidationException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
public class GPTTranslateServiceImpl implements TranslateService {
private final OpenAiClient openAiClient;
private final ProxyProperties.OpenaiConfig openaiConfig;
public GPTTranslateServiceImpl(ProxyProperties properties) {
this.openaiConfig = properties.getOpenai();
if (CharSequenceUtil.isBlank(this.openaiConfig.getGptApiKey())) {
throw new BeanDefinitionValidationException("mj-proxy.openai.gpt-api-key未配置");
}
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger());
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient.Builder okHttpBuilder = new OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(new OpenAiResponseInterceptor())
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS);
if (CharSequenceUtil.isNotBlank(properties.getProxy().getHost())) {
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(properties.getProxy().getHost(), properties.getProxy().getPort()));
okHttpBuilder.proxy(proxy);
}
OpenAiClient.Builder apiBuilder = OpenAiClient.builder()
.apiKey(Collections.singletonList(this.openaiConfig.getGptApiKey()))
.keyStrategy(new KeyRandomStrategy())
.okHttpClient(okHttpBuilder.build());
if (CharSequenceUtil.isNotBlank(this.openaiConfig.getGptApiUrl())) {
apiBuilder.apiHost(this.openaiConfig.getGptApiUrl());
}
this.openAiClient = apiBuilder.build();
}
@Override
public String translateToEnglish(String prompt) {
if (!containsChinese(prompt)) {
return prompt;
}
Message m1 = Message.builder().role(Message.Role.SYSTEM).content("把中文翻译成英文").build();
Message m2 = Message.builder().role(Message.Role.USER).content(prompt).build();
ChatCompletion chatCompletion = ChatCompletion.builder()
.messages(Arrays.asList(m1, m2))
.model(this.openaiConfig.getModel())
.temperature(this.openaiConfig.getTemperature())
.maxTokens(this.openaiConfig.getMaxTokens())
.build();
ChatCompletionResponse chatCompletionResponse = this.openAiClient.chatCompletion(chatCompletion);
try {
List<ChatChoice> choices = chatCompletionResponse.getChoices();
if (!choices.isEmpty()) {
return choices.get(0).getMessage().getContent();
}
} catch (Exception e) {
log.warn("调用chat-gpt接口翻译中文失败: {}", e.getMessage());
}
return prompt;
}
}
\ No newline at end of file
package com.github.novicezk.midjourney.support;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.Constants;
import com.github.novicezk.midjourney.ProxyProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@RequiredArgsConstructor
public class ApiAuthorizeInterceptor implements HandlerInterceptor {
private final ProxyProperties properties;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (CharSequenceUtil.isBlank(this.properties.getApiSecret())) {
return true;
}
String apiSecret = request.getHeader(Constants.API_SECRET_HEADER_NAME);
boolean authorized = CharSequenceUtil.equals(apiSecret, this.properties.getApiSecret());
if (!authorized) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
return authorized;
}
}
package com.github.novicezk.midjourney.support;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.ProxyProperties;
import lombok.RequiredArgsConstructor;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
@RequiredArgsConstructor
public class DiscordHelper {
private final ProxyProperties properties;
/**
* SIMPLE_URL_PREFIX.
*/
public static final String SIMPLE_URL_PREFIX = "https://s.mj.run/";
/**
* DISCORD_SERVER_URL.
*/
public static final String DISCORD_SERVER_URL = "https://discord.com";
/**
* DISCORD_CDN_URL.
*/
public static final String DISCORD_CDN_URL = "https://cdn.discordapp.com";
/**
* DISCORD_WSS_URL.
*/
public static final String DISCORD_WSS_URL = "wss://gateway.discord.gg";
public String getServer() {
if (CharSequenceUtil.isBlank(this.properties.getNgDiscord().getServer())) {
return DISCORD_SERVER_URL;
}
String serverUrl = this.properties.getNgDiscord().getServer();
if (serverUrl.endsWith("/")) {
serverUrl = serverUrl.substring(0, serverUrl.length() - 1);
}
return serverUrl;
}
public String getCdn() {
if (CharSequenceUtil.isBlank(this.properties.getNgDiscord().getCdn())) {
return DISCORD_CDN_URL;
}
String cdnUrl = this.properties.getNgDiscord().getCdn();
if (cdnUrl.endsWith("/")) {
cdnUrl = cdnUrl.substring(0, cdnUrl.length() - 1);
}
return cdnUrl;
}
public String getWss() {
if (CharSequenceUtil.isBlank(this.properties.getNgDiscord().getWss())) {
return DISCORD_WSS_URL;
}
String wssUrl = this.properties.getNgDiscord().getWss();
if (wssUrl.endsWith("/")) {
wssUrl = wssUrl.substring(0, wssUrl.length() - 1);
}
return wssUrl;
}
public String getRealPrompt(String prompt) {
String regex = "<https?://\\S+>";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(prompt);
while (matcher.find()) {
String url = matcher.group();
String realUrl = getRealUrl(url.substring(1, url.length() - 1));
prompt = prompt.replace(url, realUrl);
}
return prompt;
}
public String getRealUrl(String url) {
if (!CharSequenceUtil.startWith(url, SIMPLE_URL_PREFIX)) {
return url;
}
ResponseEntity<Void> res = getDisableRedirectRestTemplate().getForEntity(url, Void.class);
if (res.getStatusCode() == HttpStatus.FOUND) {
return res.getHeaders().getFirst("Location");
}
return url;
}
public String findTaskIdWithCdnUrl(String url) {
if (!CharSequenceUtil.startWith(url, DISCORD_CDN_URL)) {
return null;
}
int hashStartIndex = url.lastIndexOf("/");
String taskId = CharSequenceUtil.subBefore(url.substring(hashStartIndex + 1), ".", true);
if (CharSequenceUtil.length(taskId) == 16) {
return taskId;
}
return null;
}
private RestTemplate getDisableRedirectRestTemplate() {
CloseableHttpClient httpClient = HttpClientBuilder.create()
.disableRedirectHandling()
.build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
return new RestTemplate(factory);
}
}
package com.github.novicezk.midjourney.support;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.github.novicezk.midjourney.enums.TaskAction;
import com.github.novicezk.midjourney.enums.TaskStatus;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
@Data
@ApiModel("任务")
public class Task implements Serializable {
@Serial
private static final long serialVersionUID = -674915748204390789L;
private TaskAction action;
@ApiModelProperty("任务ID")
private String id;
@ApiModelProperty("提示词")
private String prompt;
@ApiModelProperty("提示词-英文")
private String promptEn;
@ApiModelProperty("任务描述")
private String description;
@ApiModelProperty("自定义参数")
private String state;
@ApiModelProperty("提交时间")
private Long submitTime;
@ApiModelProperty("开始执行时间")
private Long startTime;
@ApiModelProperty("结束时间")
private Long finishTime;
@ApiModelProperty("图片url")
private String imageUrl;
@ApiModelProperty("任务状态")
private TaskStatus status = TaskStatus.NOT_START;
@ApiModelProperty("任务进度")
private String progress;
@ApiModelProperty("失败原因")
private String failReason;
// 任务扩展属性,仅支持基本类型
private Map<String, Object> properties;
@JsonIgnore
private final transient Object lock = new Object();
public void sleep() throws InterruptedException {
synchronized (this.lock) {
this.lock.wait();
}
}
public void awake() {
synchronized (this.lock) {
this.lock.notifyAll();
}
}
public void start() {
this.startTime = System.currentTimeMillis();
this.status = TaskStatus.SUBMITTED;
this.progress = "0%";
}
public void success() {
this.finishTime = System.currentTimeMillis();
this.status = TaskStatus.SUCCESS;
this.progress = "100%";
}
public void fail(String reason) {
this.finishTime = System.currentTimeMillis();
this.status = TaskStatus.FAILURE;
this.failReason = reason;
this.progress = "";
}
public Task setProperty(String name, Object value) {
getProperties().put(name, value);
return this;
}
public Task removeProperty(String name) {
getProperties().remove(name);
return this;
}
public Object getProperty(String name) {
return getProperties().get(name);
}
@SuppressWarnings("unchecked")
public <T> T getPropertyGeneric(String name) {
return (T) getProperty(name);
}
public <T> T getProperty(String name, Class<T> clz) {
return clz.cast(getProperty(name));
}
public Map<String, Object> getProperties() {
if (this.properties == null) {
this.properties = new HashMap<>();
}
return this.properties;
}
}
package com.github.novicezk.midjourney.support;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.Constants;
import com.github.novicezk.midjourney.enums.TaskAction;
import com.github.novicezk.midjourney.enums.TaskStatus;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Set;
import java.util.function.Predicate;
@Data
@Accessors(chain = true)
public class TaskCondition implements Predicate<Task> {
private String id;
private Set<TaskStatus> statusSet;
private Set<TaskAction> actionSet;
private String prompt;
private String promptEn;
private String description;
private String finalPromptEn;
private String relatedTaskId;
private String messageId;
private String progressMessageId;
@Override
public boolean test(Task task) {
if (task == null) {
return false;
}
if (CharSequenceUtil.isNotBlank(this.id) && !this.id.equals(task.getId())) {
return false;
}
if (this.statusSet != null && !this.statusSet.isEmpty() && !this.statusSet.contains(task.getStatus())) {
return false;
}
if (this.actionSet != null && !this.actionSet.isEmpty() && !this.actionSet.contains(task.getAction())) {
return false;
}
if (CharSequenceUtil.isNotBlank(this.prompt) && !this.prompt.equals(task.getPrompt())) {
return false;
}
if (CharSequenceUtil.isNotBlank(this.promptEn) && !this.promptEn.equals(task.getPromptEn())) {
return false;
}
if (CharSequenceUtil.isNotBlank(this.description) && !this.description.equals(task.getDescription())) {
return false;
}
if (CharSequenceUtil.isNotBlank(this.finalPromptEn) && !this.finalPromptEn.equals(task.getProperty(Constants.TASK_PROPERTY_FINAL_PROMPT))) {
return false;
}
if (CharSequenceUtil.isNotBlank(this.relatedTaskId) && !this.relatedTaskId.equals(task.getProperty(Constants.TASK_PROPERTY_RELATED_TASK_ID))) {
return false;
}
if (CharSequenceUtil.isNotBlank(this.messageId) && !this.messageId.equals(task.getProperty(Constants.TASK_PROPERTY_MESSAGE_ID))) {
return false;
}
if (CharSequenceUtil.isNotBlank(this.progressMessageId) && !this.progressMessageId.equals(task.getProperty(Constants.TASK_PROPERTY_PROGRESS_MESSAGE_ID))) {
return false;
}
return true;
}
}
package com.github.novicezk.midjourney.support;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.Map;
public abstract class TaskMixin {
@JsonIgnore
private Map<String, Object> properties;
}
package com.github.novicezk.midjourney.support;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.ProxyProperties;
import com.github.novicezk.midjourney.ReturnCode;
import com.github.novicezk.midjourney.enums.TaskStatus;
import com.github.novicezk.midjourney.result.Message;
import com.github.novicezk.midjourney.result.SubmitResultVO;
import com.github.novicezk.midjourney.service.NotifyService;
import com.github.novicezk.midjourney.service.TaskStoreService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.function.Predicate;
import java.util.stream.Stream;
@Slf4j
@Component
public class TaskQueueHelper {
@Resource
private TaskStoreService taskStoreService;
@Resource
private NotifyService notifyService;
private final ThreadPoolTaskExecutor taskExecutor;
private final List<Task> runningTasks;
private final Map<String, Future<?>> taskFutureMap = Collections.synchronizedMap(new HashMap<>());
public TaskQueueHelper(ProxyProperties properties) {
ProxyProperties.TaskQueueConfig queueConfig = properties.getQueue();
this.runningTasks = new CopyOnWriteArrayList<>();
this.taskExecutor = new ThreadPoolTaskExecutor();
this.taskExecutor.setCorePoolSize(queueConfig.getCoreSize());
this.taskExecutor.setMaxPoolSize(queueConfig.getCoreSize());
this.taskExecutor.setQueueCapacity(queueConfig.getQueueSize());
this.taskExecutor.setThreadNamePrefix("TaskQueue-");
this.taskExecutor.initialize();
}
public Set<String> getQueueTaskIds() {
return this.taskFutureMap.keySet();
}
public Task getRunningTask(String id) {
if (CharSequenceUtil.isBlank(id)) {
return null;
}
return this.runningTasks.stream().filter(t -> id.equals(t.getId())).findFirst().orElse(null);
}
public Stream<Task> findRunningTask(Predicate<Task> condition) {
return this.runningTasks.stream().filter(condition);
}
public Future<?> getRunningFuture(String taskId) {
return this.taskFutureMap.get(taskId);
}
public SubmitResultVO submitTask(Task task, Callable<Message<Void>> discordSubmit) {
this.taskStoreService.save(task);
int size;
try {
size = this.taskExecutor.getThreadPoolExecutor().getQueue().size();
Future<?> future = this.taskExecutor.submit(() -> executeTask(task, discordSubmit));
this.taskFutureMap.put(task.getId(), future);
} catch (RejectedExecutionException e) {
this.taskStoreService.delete(task.getId());
return SubmitResultVO.fail(ReturnCode.QUEUE_REJECTED, "队列已满,请稍后尝试");
} catch (Exception e) {
log.error("submit task error", e);
return SubmitResultVO.fail(ReturnCode.FAILURE, "提交失败,系统异常");
}
if (size == 0) {
return SubmitResultVO.of(ReturnCode.SUCCESS, "提交成功", task.getId());
} else {
return SubmitResultVO.of(ReturnCode.IN_QUEUE, "排队中,前面还有" + size + "个任务", task.getId())
.setProperty("numberOfQueues", size);
}
}
private void executeTask(Task task, Callable<Message<Void>> discordSubmit) {
this.runningTasks.add(task);
try {
task.start();
Message<Void> result = discordSubmit.call();
if (result.getCode() != ReturnCode.SUCCESS) {
task.fail(result.getDescription());
changeStatusAndNotify(task, TaskStatus.FAILURE);
return;
}
changeStatusAndNotify(task, TaskStatus.SUBMITTED);
do {
task.sleep();
changeStatusAndNotify(task, task.getStatus());
} while (task.getStatus() == TaskStatus.IN_PROGRESS);
log.debug("task finished, id: {}, status: {}", task.getId(), task.getStatus());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
log.error("task execute error", e);
task.fail("执行错误,系统异常");
changeStatusAndNotify(task, TaskStatus.FAILURE);
} finally {
this.runningTasks.remove(task);
this.taskFutureMap.remove(task.getId());
}
}
public void changeStatusAndNotify(Task task, TaskStatus status) {
task.setStatus(status);
this.taskStoreService.save(task);
this.notifyService.notifyTaskChange(task);
}
}
\ No newline at end of file
package com.github.novicezk.midjourney.support;
import com.github.novicezk.midjourney.ProxyProperties;
import com.github.novicezk.midjourney.enums.TaskStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@RequiredArgsConstructor
public class TaskTimeoutSchedule {
private final TaskQueueHelper taskQueueHelper;
private final ProxyProperties properties;
@Scheduled(fixedRate = 30000L)
public void checkTasks() {
long currentTime = System.currentTimeMillis();
long timeout = TimeUnit.MINUTES.toMillis(this.properties.getQueue().getTimeoutMinutes());
List<Task> tasks = this.taskQueueHelper.findRunningTask(new TaskCondition())
.filter(t -> currentTime - t.getStartTime() > timeout)
.toList();
for (Task task : tasks) {
if (Set.of(TaskStatus.FAILURE, TaskStatus.SUCCESS).contains(task.getStatus())) {
log.warn("task status is failure/success but is in the queue, end it. id: {}", task.getId());
} else {
log.debug("task timeout, id: {}", task.getId());
task.fail("任务超时");
}
Future<?> future = this.taskQueueHelper.getRunningFuture(task.getId());
if (future != null) {
future.cancel(true);
}
this.taskQueueHelper.changeStatusAndNotify(task, task.getStatus());
}
}
}
package com.github.novicezk.midjourney.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.CharSequenceUtil;
import lombok.experimental.UtilityClass;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
@UtilityClass
public class BannedPromptUtils {
private static final String BANNED_WORDS_FILE_PATH = "/home/spring/config/banned-words.txt";
private final List<String> BANNED_WORDS;
static {
List<String> lines;
File file = new File(BANNED_WORDS_FILE_PATH);
if (file.exists()) {
lines = FileUtil.readLines(file, StandardCharsets.UTF_8);
} else {
var resource = BannedPromptUtils.class.getResource("/banned-words.txt");
lines = FileUtil.readLines(resource, StandardCharsets.UTF_8);
}
BANNED_WORDS = lines.stream().filter(CharSequenceUtil::isNotBlank).toList();
}
public static boolean isBanned(String promptEn) {
String finalPromptEn = promptEn.toLowerCase(Locale.ENGLISH);
return BANNED_WORDS.stream().anyMatch(bannedWord -> Pattern.compile("\\b" + bannedWord + "\\b").matcher(finalPromptEn).find());
}
}
package com.github.novicezk.midjourney.util;
import lombok.Data;
@Data
public class ContentParseData {
protected String prompt;
protected String status;
}
package com.github.novicezk.midjourney.util;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.enums.TaskAction;
import lombok.experimental.UtilityClass;
import java.util.List;
@UtilityClass
public class ConvertUtils {
public static TaskChangeParams convertChangeParams(String content) {
List<String> split = CharSequenceUtil.split(content, " ");
if (split.size() != 2) {
return null;
}
String action = split.get(1).toLowerCase();
TaskChangeParams changeParams = new TaskChangeParams();
if (action.charAt(0) == 'u') {
changeParams.setAction(TaskAction.UPSCALE);
} else if (action.charAt(0) == 'v') {
changeParams.setAction(TaskAction.VARIATION);
} else if (action.equals("r")) {
changeParams.setAction(TaskAction.REROLL);
} else {
return null;
}
try {
int index = Integer.parseInt(action.substring(1, 2));
if (index < 1 || index > 4) {
return null;
}
changeParams.setIndex(index);
} catch (NumberFormatException e) {
return null;
}
changeParams.setId(split.get(0));
return changeParams;
}
}
package com.github.novicezk.midjourney.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.CharSequenceUtil;
import lombok.experimental.UtilityClass;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@UtilityClass
public class MimeTypeUtils {
private final Map<String, List<String>> MIME_TYPE_MAP;
static {
MIME_TYPE_MAP = new HashMap<>();
var resource = MimeTypeUtils.class.getResource("/mime.types");
var lines = FileUtil.readLines(resource, StandardCharsets.UTF_8);
for (var line : lines) {
if (CharSequenceUtil.isBlank(line)) {
continue;
}
var arr = line.split(":");
MIME_TYPE_MAP.put(arr[0], CharSequenceUtil.split(arr[1], ' '));
}
}
public static String guessFileSuffix(String mimeType) {
if (CharSequenceUtil.isBlank(mimeType)) {
return null;
}
String key = mimeType;
if (!MIME_TYPE_MAP.containsKey(key)) {
key = MIME_TYPE_MAP.keySet().stream().filter(k -> CharSequenceUtil.startWithIgnoreCase(mimeType, k))
.findFirst().orElse(null);
}
var suffixList = MIME_TYPE_MAP.get(key);
if (suffixList == null || suffixList.isEmpty()) {
return null;
}
return suffixList.iterator().next();
}
}
package com.github.novicezk.midjourney.util;
import com.github.novicezk.midjourney.enums.TaskAction;
import lombok.Data;
@Data
public class TaskChangeParams {
private String id;
private TaskAction action;
private Integer index;
}
package com.github.novicezk.midjourney.util;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class UVContentParseData extends ContentParseData {
protected Integer index;
}
package com.github.novicezk.midjourney.wss;
import com.github.novicezk.midjourney.ProxyProperties;
import com.neovisionaries.ws.client.ProxySettings;
import com.neovisionaries.ws.client.WebSocketFactory;
import org.apache.logging.log4j.util.Strings;
public interface WebSocketStarter {
void start() throws Exception;
default void initProxy(ProxyProperties properties) {
ProxyProperties.ProxyConfig proxy = properties.getProxy();
if (Strings.isNotBlank(proxy.getHost())) {
System.setProperty("http.proxyHost", proxy.getHost());
System.setProperty("http.proxyPort", String.valueOf(proxy.getPort()));
System.setProperty("https.proxyHost", proxy.getHost());
System.setProperty("https.proxyPort", String.valueOf(proxy.getPort()));
}
}
default WebSocketFactory createWebSocketFactory(ProxyProperties properties) {
ProxyProperties.ProxyConfig proxy = properties.getProxy();
WebSocketFactory webSocketFactory = new WebSocketFactory().setConnectionTimeout(10000);
if (Strings.isNotBlank(proxy.getHost())) {
ProxySettings proxySettings = webSocketFactory.getProxySettings();
proxySettings.setHost(proxy.getHost());
proxySettings.setPort(proxy.getPort());
}
return webSocketFactory;
}
}
package com.github.novicezk.midjourney.wss.bot;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.ProxyProperties;
import com.github.novicezk.midjourney.enums.MessageType;
import com.github.novicezk.midjourney.wss.handle.MessageHandler;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class BotMessageListener extends ListenerAdapter implements ApplicationListener<ApplicationStartedEvent> {
@Resource
private ProxyProperties properties;
private final List<MessageHandler> messageHandlers = new ArrayList<>();
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
this.messageHandlers.addAll(event.getApplicationContext().getBeansOfType(MessageHandler.class).values());
}
@Override
public void onMessageReceived(MessageReceivedEvent event) {
Message message = event.getMessage();
if (ignoreAndLogMessage(message, MessageType.CREATE)) {
return;
}
for (MessageHandler messageHandler : this.messageHandlers) {
messageHandler.handle(MessageType.CREATE, message);
}
}
@Override
public void onMessageUpdate(MessageUpdateEvent event) {
Message message = event.getMessage();
if (ignoreAndLogMessage(message, MessageType.UPDATE)) {
return;
}
for (MessageHandler messageHandler : this.messageHandlers) {
messageHandler.handle(MessageType.UPDATE, message);
}
}
private boolean ignoreAndLogMessage(Message message, MessageType messageType) {
String channelId = message.getChannel().getId();
if (!this.properties.getDiscord().getChannelId().equals(channelId)) {
return true;
}
String authorName = message.getAuthor().getName();
if (CharSequenceUtil.isBlank(authorName)) {
authorName = "System";
}
log.debug("{} - {}: {}", messageType.name(), authorName, message.getContentRaw());
return false;
}
}
package com.github.novicezk.midjourney.wss.bot;
import com.github.novicezk.midjourney.ProxyProperties;
import com.github.novicezk.midjourney.support.DiscordHelper;
import com.github.novicezk.midjourney.wss.WebSocketStarter;
import com.neovisionaries.ws.client.WebSocketFactory;
import net.dv8tion.jda.api.requests.GatewayIntent;
import net.dv8tion.jda.api.requests.RestConfig;
import net.dv8tion.jda.api.sharding.DefaultShardManagerBuilder;
import javax.annotation.Resource;
public class BotWebSocketStarter implements WebSocketStarter {
@Resource
private BotMessageListener botMessageListener;
@Resource
private DiscordHelper discordHelper;
private final ProxyProperties properties;
public BotWebSocketStarter(ProxyProperties properties) {
initProxy(properties);
this.properties = properties;
}
@Override
public void start() throws Exception {
DefaultShardManagerBuilder builder = DefaultShardManagerBuilder.createDefault(
this.properties.getDiscord().getBotToken(), GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT);
builder.addEventListeners(this.botMessageListener);
WebSocketFactory webSocketFactory = createWebSocketFactory(this.properties);
builder.setWebsocketFactory(webSocketFactory);
builder.setSessionController(new CustomSessionController(this.discordHelper.getWss()));
builder.setRestConfigProvider(value -> new RestConfig().setBaseUrl(this.discordHelper.getServer() + "/api/v10/"));
builder.build();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment