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"]
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
# 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 cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.ProxyProperties;
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.support.DiscordHelper;
import eu.maxschuster.dataurl.DataUrl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray;
import org.json.JSONObject;
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.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class DiscordServiceImpl implements DiscordService {
private final ProxyProperties properties;
private final DiscordHelper discordHelper;
private String discordApiUrl;
private String userAgent;
private String discordUploadUrl;
private String discordSendMessageUrl;
private String imagineParamsJson;
private String upscaleParamsJson;
private String variationParamsJson;
private String rerollParamsJson;
private String describeParamsJson;
private String blendParamsJson;
private String messageParamsJson;
private String discordUserToken;
private String discordGuildId;
private String discordChannelId;
private String discordSessionId;
@PostConstruct
void init() {
ProxyProperties.DiscordConfig discord = this.properties.getDiscord();
this.discordUserToken = discord.getUserToken();
this.discordGuildId = discord.getGuildId();
this.discordChannelId = discord.getChannelId();
this.discordSessionId = discord.getSessionId();
this.userAgent = discord.getUserAgent();
String serverUrl = this.discordHelper.getServer();
this.discordApiUrl = serverUrl + "/api/v9/interactions";
this.discordUploadUrl = serverUrl + "/api/v9/channels/" + this.discordChannelId + "/attachments";
this.discordSendMessageUrl = serverUrl + "/api/v9/channels/" + this.discordChannelId + "/messages";
this.imagineParamsJson = ResourceUtil.readUtf8Str("api-params/imagine.json");
this.upscaleParamsJson = ResourceUtil.readUtf8Str("api-params/upscale.json");
this.variationParamsJson = ResourceUtil.readUtf8Str("api-params/variation.json");
this.rerollParamsJson = ResourceUtil.readUtf8Str("api-params/reroll.json");
this.describeParamsJson = ResourceUtil.readUtf8Str("api-params/describe.json");
this.blendParamsJson = ResourceUtil.readUtf8Str("api-params/blend.json");
this.messageParamsJson = ResourceUtil.readUtf8Str("api-params/message.json");
}
@Override
public Message<Void> imagine(String prompt) {
String paramsStr = this.imagineParamsJson.replace("$guild_id", this.discordGuildId)
.replace("$channel_id", this.discordChannelId)
.replace("$session_id", this.discordSessionId);
JSONObject params = new JSONObject(paramsStr);
params.getJSONObject("data").getJSONArray("options").getJSONObject(0)
.put("value", prompt);
return postJsonAndCheckStatus(params.toString());
}
@Override
public Message<Void> upscale(String messageId, int index, String messageHash, int messageFlags) {
String paramsStr = this.upscaleParamsJson.replace("$guild_id", this.discordGuildId)
.replace("$channel_id", this.discordChannelId)
.replace("$session_id", this.discordSessionId)
.replace("$message_id", messageId)
.replace("$index", String.valueOf(index))
.replace("$message_hash", messageHash);
paramsStr = new JSONObject(paramsStr).put("message_flags", messageFlags).toString();
return postJsonAndCheckStatus(paramsStr);
}
@Override
public Message<Void> variation(String messageId, int index, String messageHash, int messageFlags) {
String paramsStr = this.variationParamsJson.replace("$guild_id", this.discordGuildId)
.replace("$channel_id", this.discordChannelId)
.replace("$session_id", this.discordSessionId)
.replace("$message_id", messageId)
.replace("$index", String.valueOf(index))
.replace("$message_hash", messageHash);
paramsStr = new JSONObject(paramsStr).put("message_flags", messageFlags).toString();
return postJsonAndCheckStatus(paramsStr);
}
@Override
public Message<Void> reroll(String messageId, String messageHash, int messageFlags) {
String paramsStr = this.rerollParamsJson.replace("$guild_id", this.discordGuildId)
.replace("$channel_id", this.discordChannelId)
.replace("$session_id", this.discordSessionId)
.replace("$message_id", messageId)
.replace("$message_hash", messageHash);
paramsStr = new JSONObject(paramsStr).put("message_flags", messageFlags).toString();
return postJsonAndCheckStatus(paramsStr);
}
@Override
public Message<Void> describe(String finalFileName) {
String fileName = CharSequenceUtil.subAfter(finalFileName, "/", true);
String paramsStr = this.describeParamsJson.replace("$guild_id", this.discordGuildId)
.replace("$channel_id", this.discordChannelId)
.replace("$session_id", this.discordSessionId)
.replace("$file_name", fileName)
.replace("$final_file_name", finalFileName);
return postJsonAndCheckStatus(paramsStr);
}
@Override
public Message<Void> blend(List<String> finalFileNames, BlendDimensions dimensions) {
String paramsStr = this.blendParamsJson.replace("$guild_id", this.discordGuildId)
.replace("$channel_id", this.discordChannelId)
.replace("$session_id", this.discordSessionId);
JSONObject params = new JSONObject(paramsStr);
JSONArray options = params.getJSONObject("data").getJSONArray("options");
JSONArray attachments = params.getJSONObject("data").getJSONArray("attachments");
for (int i = 0; i < finalFileNames.size(); i++) {
String finalFileName = finalFileNames.get(i);
String fileName = CharSequenceUtil.subAfter(finalFileName, "/", true);
JSONObject attachment = new JSONObject().put("id", String.valueOf(i))
.put("filename", fileName)
.put("uploaded_filename", finalFileName);
attachments.put(attachment);
JSONObject option = new JSONObject().put("type", 11)
.put("name", "image" + (i + 1))
.put("value", i);
options.put(option);
}
options.put(new JSONObject().put("type", 3)
.put("name", "dimensions")
.put("value", "--ar " + dimensions.getValue()));
return postJsonAndCheckStatus(params.toString());
}
@Override
public Message<String> upload(String fileName, DataUrl dataUrl) {
try {
JSONObject fileObj = new JSONObject();
fileObj.put("filename", fileName);
fileObj.put("file_size", dataUrl.getData().length);
fileObj.put("id", "0");
JSONObject params = new JSONObject()
.put("files", new JSONArray().put(fileObj));
ResponseEntity<String> responseEntity = postJson(this.discordUploadUrl, params.toString());
if (responseEntity.getStatusCode() != HttpStatus.OK) {
log.error("上传图片到discord失败, status: {}, msg: {}", responseEntity.getStatusCodeValue(), responseEntity.getBody());
return Message.of(ReturnCode.VALIDATION_ERROR, "上传图片到discord失败");
}
JSONArray array = new JSONObject(responseEntity.getBody()).getJSONArray("attachments");
if (array.length() == 0) {
return Message.of(ReturnCode.VALIDATION_ERROR, "上传图片到discord失败");
}
String uploadUrl = array.getJSONObject(0).getString("upload_url");
String uploadFilename = array.getJSONObject(0).getString("upload_filename");
putFile(uploadUrl, dataUrl);
return Message.success(uploadFilename);
} catch (Exception e) {
log.error("上传图片到discord失败", e);
return Message.of(ReturnCode.FAILURE, "上传图片到discord失败");
}
}
@Override
public Message<String> sendImageMessage(String content, String finalFileName) {
String fileName = CharSequenceUtil.subAfter(finalFileName, "/", true);
String paramsStr = this.messageParamsJson.replace("$content", content)
.replace("$channel_id", this.discordChannelId)
.replace("$file_name", fileName)
.replace("$final_file_name", finalFileName);
ResponseEntity<String> responseEntity = postJson(this.discordSendMessageUrl, paramsStr);
if (responseEntity.getStatusCode() != HttpStatus.OK) {
log.error("发送图片消息到discord失败, status: {}, msg: {}", responseEntity.getStatusCodeValue(), responseEntity.getBody());
return Message.of(ReturnCode.VALIDATION_ERROR, "发送图片消息到discord失败");
}
JSONObject result = new JSONObject(responseEntity.getBody());
JSONArray attachments = result.optJSONArray("attachments");
if (!attachments.isEmpty()) {
return Message.success(attachments.getJSONObject(0).optString("url"));
}
return Message.failure("发送图片消息到discord失败: 图片不存在");
}
private void putFile(String uploadUrl, DataUrl dataUrl) {
HttpHeaders headers = new HttpHeaders();
headers.add("User-Agent", this.userAgent);
headers.setContentType(MediaType.valueOf(dataUrl.getMimeType()));
headers.setContentLength(dataUrl.getData().length);
HttpEntity<byte[]> requestEntity = new HttpEntity<>(dataUrl.getData(), headers);
new RestTemplate().put(uploadUrl, requestEntity);
}
private ResponseEntity<String> postJson(String paramsStr) {
return postJson(discordApiUrl, paramsStr);
}
private ResponseEntity<String> postJson(String url, String paramsStr) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", this.discordUserToken);
headers.add("User-Agent", this.userAgent);
HttpEntity<String> httpEntity = new HttpEntity<>(paramsStr, headers);
return new RestTemplate().postForEntity(url, httpEntity, String.class);
}
private Message<Void> postJsonAndCheckStatus(String paramsStr) {
try {
ResponseEntity<String> responseEntity = postJson(paramsStr);
if (responseEntity.getStatusCode() == HttpStatus.NO_CONTENT) {
return Message.success();
}
return Message.of(responseEntity.getStatusCodeValue(), CharSequenceUtil.sub(responseEntity.getBody(), 0, 100));
} catch (HttpClientErrorException e) {
try {
JSONObject error = new JSONObject(e.getResponseBodyAsString());
return Message.of(error.optInt("code", e.getRawStatusCode()), error.optString("message"));
} catch (Exception je) {
return Message.of(e.getRawStatusCode(), CharSequenceUtil.sub(e.getMessage(), 0, 100));
}
}
}
}
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();
}
}
package com.github.novicezk.midjourney.wss.bot;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.exceptions.InvalidTokenException;
import net.dv8tion.jda.api.requests.Request;
import net.dv8tion.jda.api.requests.Response;
import net.dv8tion.jda.api.requests.Route;
import net.dv8tion.jda.api.utils.ConcurrentSessionController;
import net.dv8tion.jda.api.utils.data.DataObject;
import net.dv8tion.jda.internal.requests.RestActionImpl;
public class CustomSessionController extends ConcurrentSessionController {
private final String gateway;
public CustomSessionController(String gateway) {
this.gateway = gateway;
}
@Override
public String getGateway() {
return this.gateway;
}
@Override
public ShardedGateway getShardedGateway(JDA api) {
return new RestActionImpl<ShardedGateway>(api, Route.Misc.GATEWAY_BOT.compile()) {
@Override
public void handleResponse(Response response, Request<ShardedGateway> request) {
if (response.isOk()) {
DataObject object = response.getObject();
String url = getGateway();
int shards = object.getInt("shards");
int concurrency = object.getObject("session_start_limit").getInt("max_concurrency", 1);
request.onSuccess(new ShardedGateway(url, shards, concurrency));
} else if (response.code == 401) {
this.api.shutdownNow();
request.onFailure(new InvalidTokenException("The provided token is invalid!"));
} else {
request.onFailure(response);
}
}
}.priority().complete();
}
}
package com.github.novicezk.midjourney.wss.handle;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.Constants;
import com.github.novicezk.midjourney.enums.MessageType;
import com.github.novicezk.midjourney.enums.TaskAction;
import com.github.novicezk.midjourney.enums.TaskStatus;
import com.github.novicezk.midjourney.support.DiscordHelper;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskCondition;
import com.github.novicezk.midjourney.util.ContentParseData;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.utils.data.DataObject;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* blend消息处理.
* 开始(create): **<https://s.mj.run/JWu6jaL1D-8> <https://s.mj.run/QhfnQY-l68o> --v 5.1** - <@1012983546824114217> (Waiting to start)
* 进度(update): **<https://s.mj.run/JWu6jaL1D-8> <https://s.mj.run/QhfnQY-l68o> --v 5.1** - <@1012983546824114217> (0%) (relaxed)
* 完成(create): **<https://s.mj.run/JWu6jaL1D-8> <https://s.mj.run/QhfnQY-l68o> --v 5.1** - <@1012983546824114217> (relaxed)
*/
@Component
public class BlendMessageHandler extends MessageHandler {
private static final String CONTENT_REGEX = "\\*\\*(.*?)\\*\\* - <@\\d+> \\((.*?)\\)";
@Override
public void handle(MessageType messageType, DataObject message) {
Optional<DataObject> interaction = message.optObject("interaction");
String content = getMessageContent(message);
boolean match = CharSequenceUtil.startWith(content, "**<" + DiscordHelper.SIMPLE_URL_PREFIX) || (interaction.isPresent() && "blend".equals(interaction.get().getString("name")));
if (!match) {
return;
}
ContentParseData parseData = parse(content);
if (parseData == null) {
return;
}
if (MessageType.CREATE == messageType) {
if ("Waiting to start".equals(parseData.getStatus())) {
// 开始
List<String> urls = CharSequenceUtil.split(parseData.getPrompt(), " ");
if (urls.isEmpty()) {
return;
}
String url = getRealUrl(urls.get(0));
String taskId = this.discordHelper.findTaskIdWithCdnUrl(url);
TaskCondition condition = new TaskCondition()
.setId(taskId)
.setActionSet(Set.of(TaskAction.BLEND))
.setStatusSet(Set.of(TaskStatus.SUBMITTED));
Task task = this.taskQueueHelper.findRunningTask(condition).findFirst().orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_PROGRESS_MESSAGE_ID, message.getString("id"));
task.setPrompt(parseData.getPrompt());
task.setPromptEn(parseData.getPrompt());
task.setStatus(TaskStatus.IN_PROGRESS);
task.awake();
} else {
// 完成
TaskCondition condition = new TaskCondition()
.setActionSet(Set.of(TaskAction.BLEND))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(condition)
.max(Comparator.comparing(Task::getProgress))
.orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_FINAL_PROMPT, parseData.getPrompt());
finishTask(task, message);
task.awake();
}
} else if (MessageType.UPDATE == messageType) {
// 进度
TaskCondition condition = new TaskCondition()
.setProgressMessageId(message.getString("id"))
.setActionSet(Set.of(TaskAction.BLEND))
.setStatusSet(Set.of(TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(condition).findFirst().orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_PROGRESS_MESSAGE_ID, message.getString("id"));
task.setProgress(parseData.getStatus());
task.setImageUrl(getImageUrl(message));
task.awake();
}
}
@Override
public void handle(MessageType messageType, Message message) {
String content = message.getContentRaw();
boolean match = CharSequenceUtil.startWith(content, "**<" + DiscordHelper.SIMPLE_URL_PREFIX) || (message.getInteraction() != null && "blend".equals(message.getInteraction().getName()));
if (!match) {
return;
}
ContentParseData parseData = parse(content);
if (parseData == null) {
return;
}
if (MessageType.CREATE == messageType) {
if ("Waiting to start".equals(parseData.getStatus())) {
// 开始
List<String> urls = CharSequenceUtil.split(parseData.getPrompt(), " ");
if (urls.isEmpty()) {
return;
}
String url = getRealUrl(urls.get(0));
String taskId = this.discordHelper.findTaskIdWithCdnUrl(url);
TaskCondition condition = new TaskCondition()
.setId(taskId)
.setActionSet(Set.of(TaskAction.BLEND))
.setStatusSet(Set.of(TaskStatus.SUBMITTED));
Task task = this.taskQueueHelper.findRunningTask(condition).findFirst().orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_PROGRESS_MESSAGE_ID, message.getId());
task.setPrompt(parseData.getPrompt());
task.setPromptEn(parseData.getPrompt());
task.setStatus(TaskStatus.IN_PROGRESS);
task.awake();
} else {
// 完成
TaskCondition condition = new TaskCondition()
.setActionSet(Set.of(TaskAction.BLEND))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(condition)
.max(Comparator.comparing(Task::getProgress))
.orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_FINAL_PROMPT, parseData.getPrompt());
finishTask(task, message);
task.awake();
}
} else if (MessageType.UPDATE == messageType) {
// 进度
TaskCondition condition = new TaskCondition()
.setProgressMessageId(message.getId())
.setActionSet(Set.of(TaskAction.BLEND))
.setStatusSet(Set.of(TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(condition).findFirst().orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_PROGRESS_MESSAGE_ID, message.getId());
task.setProgress(parseData.getStatus());
task.setImageUrl(getImageUrl(message));
task.awake();
}
}
private ContentParseData parse(String content) {
Matcher matcher = Pattern.compile(CONTENT_REGEX).matcher(content);
if (!matcher.find()) {
return null;
}
ContentParseData parseData = new ContentParseData();
parseData.setPrompt(matcher.group(1));
parseData.setStatus(matcher.group(2));
return parseData;
}
private String getRealUrl(String url) {
if (CharSequenceUtil.startWith(url, "<" + DiscordHelper.SIMPLE_URL_PREFIX)) {
return this.discordHelper.getRealUrl(url.substring(1, url.length() - 1));
}
return url;
}
}
package com.github.novicezk.midjourney.wss.handle;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.Constants;
import com.github.novicezk.midjourney.enums.MessageType;
import com.github.novicezk.midjourney.support.Task;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.utils.data.DataArray;
import net.dv8tion.jda.api.utils.data.DataObject;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
/**
* describe消息处理.
*/
@Component
public class DescribeMessageHandler extends MessageHandler {
@Override
public void handle(MessageType messageType, DataObject message) {
Optional<DataObject> interaction = message.optObject("interaction");
if (interaction.isEmpty() || !"describe".equals(interaction.get().getString("name"))) {
return;
}
DataArray embeds = message.getArray("embeds");
if (embeds.isEmpty()) {
return;
}
String prompt = embeds.getObject(0).getString("description");
Optional<DataObject> imageOptional = embeds.getObject(0).optObject("image");
if (imageOptional.isEmpty()) {
return;
}
String imageUrl = imageOptional.get().getString("url");
int hashStartIndex = imageUrl.lastIndexOf("/");
String taskId = CharSequenceUtil.subBefore(imageUrl.substring(hashStartIndex + 1), ".", true);
Task task = this.taskQueueHelper.getRunningTask(taskId);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_MESSAGE_ID, message.getString("id"));
task.setProperty(Constants.TASK_PROPERTY_FLAGS, message.getInt("flags", 0));
task.setPrompt(prompt);
task.setPromptEn(prompt);
task.setImageUrl(replaceCdnUrl(imageUrl));
task.success();
task.awake();
}
@Override
public void handle(MessageType messageType, Message message) {
if (message.getInteraction() == null || !"describe".equals(message.getInteraction().getName())) {
return;
}
List<MessageEmbed> embeds = message.getEmbeds();
if (embeds.isEmpty()) {
return;
}
String prompt = embeds.get(0).getDescription();
String imageUrl = embeds.get(0).getImage().getUrl();
int hashStartIndex = imageUrl.lastIndexOf("/");
String taskId = CharSequenceUtil.subBefore(imageUrl.substring(hashStartIndex + 1), ".", true);
Task task = this.taskQueueHelper.getRunningTask(taskId);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_MESSAGE_ID, message.getId());
task.setProperty(Constants.TASK_PROPERTY_FLAGS, (int) message.getFlagsRaw());
task.setPrompt(prompt);
task.setPromptEn(prompt);
task.setImageUrl(replaceCdnUrl(imageUrl));
task.success();
task.awake();
}
}
package com.github.novicezk.midjourney.wss.handle;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.enums.MessageType;
import com.github.novicezk.midjourney.enums.TaskAction;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskCondition;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.utils.data.DataArray;
import net.dv8tion.jda.api.utils.data.DataObject;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Slf4j
@Component
public class ErrorMessageHandler extends MessageHandler {
@Override
public void handle(MessageType messageType, DataObject message) {
Optional<DataArray> embedsOptional = message.optArray("embeds");
if (embedsOptional.isEmpty() || embedsOptional.get().isEmpty()) {
return;
}
DataObject embed = embedsOptional.get().getObject(0);
String title = embed.getString("title", null);
if (CharSequenceUtil.isBlank(title) || CharSequenceUtil.startWith(title, "Your info - ")) {
// 排除正常信息.
return;
}
String description = embed.getString("description", null);
String footerText = "";
Optional<DataObject> footer = embed.optObject("footer");
if (footer.isPresent()) {
footerText = footer.get().getString("text", "");
}
log.warn("检测到可能异常的信息: {}\n{}\nfooter: {}", title, description, footerText);
if (CharSequenceUtil.contains(description, "this job will start")) {
// mj队列中, 不认为是异常
return;
}
if (CharSequenceUtil.contains(description, "verify you're human")) {
String reason = "需要人工验证,请联系管理员";
this.taskQueueHelper.findRunningTask(new TaskCondition()).forEach(task -> {
task.fail(reason);
task.awake();
});
return;
}
Task targetTask = null;
if (CharSequenceUtil.startWith(footerText, "/imagine ")) {
String finalPrompt = CharSequenceUtil.subAfter(footerText, "/imagine ", false);
if (CharSequenceUtil.contains(finalPrompt, "https://")) {
// 有可能为blend操作
String taskId = this.discordHelper.findTaskIdWithCdnUrl(finalPrompt.split(" ")[0]);
if (taskId != null) {
targetTask = this.taskQueueHelper.getRunningTask(taskId);
}
}
if (targetTask == null) {
targetTask = this.taskQueueHelper.findRunningTask(t ->
t.getAction() == TaskAction.IMAGINE && finalPrompt.startsWith(t.getPromptEn()))
.findFirst().orElse(null);
}
} else if (CharSequenceUtil.startWith(footerText, "/describe ")) {
String imageUrl = CharSequenceUtil.subAfter(footerText, "/describe ", false);
String taskId = this.discordHelper.findTaskIdWithCdnUrl(imageUrl);
targetTask = this.taskQueueHelper.getRunningTask(taskId);
}
if (targetTask == null) {
return;
}
String reason;
if (CharSequenceUtil.contains(description, "against our community standards")) {
reason = "可能包含违规信息";
} else {
reason = description;
}
targetTask.fail(reason);
targetTask.awake();
}
@Override
public void handle(MessageType messageType, Message message) {
// bot-wss 获取不到错误
}
}
package com.github.novicezk.midjourney.wss.handle;
import com.github.novicezk.midjourney.Constants;
import com.github.novicezk.midjourney.enums.MessageType;
import com.github.novicezk.midjourney.enums.TaskAction;
import com.github.novicezk.midjourney.enums.TaskStatus;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskCondition;
import com.github.novicezk.midjourney.util.ContentParseData;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.utils.data.DataObject;
import org.springframework.stereotype.Component;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* imagine消息处理.
* 开始(create): **cat** - <@1012983546824114217> (Waiting to start)
* 进度(update): **cat** - <@1012983546824114217> (0%) (relaxed)
* 完成(create): **cat** - <@1012983546824114217> (relaxed)
*/
@Slf4j
@Component
public class ImagineMessageHandler extends MessageHandler {
private static final String CONTENT_REGEX = "\\*\\*(.*?)\\*\\* - <@\\d+> \\((.*?)\\)";
@Override
public void handle(MessageType messageType, DataObject message) {
String content = getMessageContent(message);
ContentParseData parseData = parse(content);
if (parseData == null) {
return;
}
String realPrompt = this.discordHelper.getRealPrompt(parseData.getPrompt());
if (MessageType.CREATE == messageType) {
if ("Waiting to start".equals(parseData.getStatus())) {
// 开始
TaskCondition condition = new TaskCondition()
.setActionSet(Set.of(TaskAction.IMAGINE))
.setStatusSet(Set.of(TaskStatus.SUBMITTED));
Task task = this.taskQueueHelper.findRunningTask(taskPredicate(condition, realPrompt))
.findFirst().orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_PROGRESS_MESSAGE_ID, message.getString("id"));
task.setProperty(Constants.TASK_PROPERTY_FINAL_PROMPT, parseData.getPrompt());
task.setStatus(TaskStatus.IN_PROGRESS);
task.awake();
} else {
// 完成
TaskCondition condition = new TaskCondition()
.setActionSet(Set.of(TaskAction.IMAGINE))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(taskPredicate(condition, realPrompt))
.findFirst().orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_FINAL_PROMPT, parseData.getPrompt());
finishTask(task, message);
task.awake();
}
} else if (MessageType.UPDATE == messageType) {
// 进度
TaskCondition condition = new TaskCondition()
.setActionSet(Set.of(TaskAction.IMAGINE))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(taskPredicate(condition, realPrompt))
.findFirst().orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_PROGRESS_MESSAGE_ID, message.getString("id"));
task.setProperty(Constants.TASK_PROPERTY_FINAL_PROMPT, parseData.getPrompt());
task.setStatus(TaskStatus.IN_PROGRESS);
task.setProgress(parseData.getStatus());
task.setImageUrl(getImageUrl(message));
task.awake();
}
}
@Override
public void handle(MessageType messageType, Message message) {
String content = message.getContentRaw();
ContentParseData parseData = parse(content);
if (parseData == null) {
return;
}
String realPrompt = this.discordHelper.getRealPrompt(parseData.getPrompt());
if (MessageType.CREATE == messageType) {
if ("Waiting to start".equals(parseData.getStatus())) {
// 开始
TaskCondition condition = new TaskCondition()
.setActionSet(Set.of(TaskAction.IMAGINE))
.setStatusSet(Set.of(TaskStatus.SUBMITTED));
Task task = this.taskQueueHelper.findRunningTask(taskPredicate(condition, realPrompt))
.findFirst().orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_PROGRESS_MESSAGE_ID, message.getId());
task.setProperty(Constants.TASK_PROPERTY_FINAL_PROMPT, parseData.getPrompt());
task.setStatus(TaskStatus.IN_PROGRESS);
task.awake();
} else {
// 完成
TaskCondition condition = new TaskCondition()
.setActionSet(Set.of(TaskAction.IMAGINE))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(taskPredicate(condition, realPrompt))
.findFirst().orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_FINAL_PROMPT, parseData.getPrompt());
finishTask(task, message);
task.awake();
}
} else if (MessageType.UPDATE == messageType) {
// 进度
TaskCondition condition = new TaskCondition()
.setActionSet(Set.of(TaskAction.IMAGINE))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(taskPredicate(condition, realPrompt))
.findFirst().orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_PROGRESS_MESSAGE_ID, message.getId());
task.setProperty(Constants.TASK_PROPERTY_FINAL_PROMPT, parseData.getPrompt());
task.setStatus(TaskStatus.IN_PROGRESS);
task.setProgress(parseData.getStatus());
task.setImageUrl(getImageUrl(message));
task.awake();
}
}
private Predicate<Task> taskPredicate(TaskCondition condition, String prompt) {
return condition.and(t -> prompt.startsWith(t.getPromptEn()));
}
private ContentParseData parse(String content) {
Matcher matcher = Pattern.compile(CONTENT_REGEX).matcher(content);
if (!matcher.find()) {
return null;
}
ContentParseData parseData = new ContentParseData();
parseData.setPrompt(matcher.group(1));
parseData.setStatus(matcher.group(2));
return parseData;
}
}
package com.github.novicezk.midjourney.wss.handle;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.Constants;
import com.github.novicezk.midjourney.enums.MessageType;
import com.github.novicezk.midjourney.support.DiscordHelper;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskQueueHelper;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.utils.data.DataArray;
import net.dv8tion.jda.api.utils.data.DataObject;
import javax.annotation.Resource;
public abstract class MessageHandler {
@Resource
protected TaskQueueHelper taskQueueHelper;
@Resource
protected DiscordHelper discordHelper;
public abstract void handle(MessageType messageType, DataObject message);
public abstract void handle(MessageType messageType, Message message);
protected String getMessageContent(DataObject message) {
return message.hasKey("content") ? message.getString("content") : "";
}
protected void finishTask(Task task, DataObject message) {
task.setProperty(Constants.TASK_PROPERTY_MESSAGE_ID, message.getString("id"));
task.setProperty(Constants.TASK_PROPERTY_FLAGS, message.getInt("flags", 0));
DataArray attachments = message.getArray("attachments");
if (!attachments.isEmpty()) {
String imageUrl = attachments.getObject(0).getString("url");
task.setImageUrl(replaceCdnUrl(imageUrl));
task.setProperty(Constants.TASK_PROPERTY_MESSAGE_HASH, getMessageHash(imageUrl));
task.success();
} else {
task.fail("关联图片不存在");
}
}
protected void finishTask(Task task, Message message) {
task.setProperty(Constants.TASK_PROPERTY_MESSAGE_ID, message.getId());
task.setProperty(Constants.TASK_PROPERTY_FLAGS, (int) message.getFlagsRaw());
if (!message.getAttachments().isEmpty()) {
String imageUrl = message.getAttachments().get(0).getUrl();
task.setImageUrl(replaceCdnUrl(imageUrl));
task.setProperty(Constants.TASK_PROPERTY_MESSAGE_HASH, getMessageHash(imageUrl));
task.success();
} else {
task.fail("关联图片不存在");
}
}
protected String getMessageHash(String imageUrl) {
int hashStartIndex = imageUrl.lastIndexOf("_");
return CharSequenceUtil.subBefore(imageUrl.substring(hashStartIndex + 1), ".", true);
}
protected String getImageUrl(DataObject message) {
DataArray attachments = message.getArray("attachments");
if (!attachments.isEmpty()) {
String imageUrl = attachments.getObject(0).getString("url");
return replaceCdnUrl(imageUrl);
}
return null;
}
protected String getImageUrl(Message message) {
if (!message.getAttachments().isEmpty()) {
String imageUrl = message.getAttachments().get(0).getUrl();
return replaceCdnUrl(imageUrl);
}
return null;
}
protected String replaceCdnUrl(String imageUrl) {
if (CharSequenceUtil.isBlank(imageUrl)) {
return imageUrl;
}
String cdn = this.discordHelper.getCdn();
if (CharSequenceUtil.startWith(imageUrl, cdn)) {
return imageUrl;
}
return CharSequenceUtil.replaceFirst(imageUrl, DiscordHelper.DISCORD_CDN_URL, cdn);
}
}
package com.github.novicezk.midjourney.wss.handle;
import com.github.novicezk.midjourney.enums.MessageType;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.utils.data.DataObject;
/**
* todo reroll 消息处理.
* 原图reroll时,跟imagine相同;
* 变换后的图reroll
* 开始(create): **[4619231091196848] cat** - <@1012983546824114217> (Waiting to start)
* 进度(update): **[4619231091196848] cat** - Variations by <@1012983546824114217> (0%) (relaxed)
* 完成(create): **[4619231091196848] cat** - Variations by <@1012983546824114217> (relaxed)
*/
public class RerollMessageHandler extends MessageHandler {
@Override
public void handle(MessageType messageType, DataObject message) {
}
@Override
public void handle(MessageType messageType, Message message) {
}
}
\ No newline at end of file
package com.github.novicezk.midjourney.wss.handle;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.enums.MessageType;
import com.github.novicezk.midjourney.enums.TaskAction;
import com.github.novicezk.midjourney.enums.TaskStatus;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskCondition;
import com.github.novicezk.midjourney.util.UVContentParseData;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.utils.data.DataObject;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* upscale消息处理.
* 开始(create): Upscaling image #1 with **cat** - <@1012983546824114217> (Waiting to start)
* 进度: 无
* 完成(create): **cat** - Image #1 <@1012983546824114217>
* 完成-其他情况(create): **cat** - Upscaled by <@1083152202048217169> (fast)
*/
@Slf4j
@Component
public class UpscaleMessageHandler extends MessageHandler {
private static final String START_CONTENT_REGEX = "Upscaling image #(\\d) with \\*\\*(.*?)\\*\\* - <@\\d+> \\((.*?)\\)";
private static final String END_CONTENT_REGEX = "\\*\\*(.*?)\\*\\* - Image #(\\d) <@\\d+>";
private static final String END2_CONTENT_REGEX = "\\*\\*(.*?)\\*\\* - Upscaled by <@\\d+> \\((.*?)\\)";
@Override
public void handle(MessageType messageType, DataObject message) {
if (MessageType.CREATE != messageType) {
return;
}
String content = getMessageContent(message);
UVContentParseData start = parseStart(content);
if (start != null) {
TaskCondition condition = new TaskCondition()
.setFinalPromptEn(start.getPrompt())
.setActionSet(Set.of(TaskAction.UPSCALE))
.setStatusSet(Set.of(TaskStatus.SUBMITTED));
Task task = this.taskQueueHelper.findRunningTask(condition)
.filter(t -> CharSequenceUtil.endWith(t.getDescription(), "U" + start.getIndex()))
.min(Comparator.comparing(Task::getSubmitTime))
.orElse(null);
if (task == null) {
return;
}
task.setStatus(TaskStatus.IN_PROGRESS);
task.awake();
return;
}
UVContentParseData end = parseEnd(content);
if (end != null) {
TaskCondition condition = new TaskCondition()
.setFinalPromptEn(end.getPrompt())
.setActionSet(Set.of(TaskAction.UPSCALE))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(condition)
.filter(t -> CharSequenceUtil.endWith(t.getDescription(), "U" + end.getIndex()))
.min(Comparator.comparing(Task::getSubmitTime))
.orElse(null);
if (task == null) {
return;
}
finishTask(task, message);
task.awake();
return;
}
UVContentParseData end2 = parseEnd2(content);
if (end2 != null) {
TaskCondition condition = new TaskCondition()
.setFinalPromptEn(end2.getPrompt())
.setActionSet(Set.of(TaskAction.UPSCALE))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(condition)
.min(Comparator.comparing(Task::getSubmitTime))
.orElse(null);
if (task == null) {
return;
}
finishTask(task, message);
task.awake();
}
}
@Override
public void handle(MessageType messageType, Message message) {
if (MessageType.CREATE != messageType) {
return;
}
String content = message.getContentRaw();
UVContentParseData parseData = parseEnd(content);
if (parseData != null) {
TaskCondition condition = new TaskCondition()
.setFinalPromptEn(parseData.getPrompt())
.setActionSet(Set.of(TaskAction.UPSCALE))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(condition)
.filter(t -> CharSequenceUtil.endWith(t.getDescription(), "U" + parseData.getIndex()))
.min(Comparator.comparing(Task::getSubmitTime))
.orElse(null);
if (task == null) {
return;
}
finishTask(task, message);
task.awake();
return;
}
UVContentParseData end2 = parseEnd2(content);
if (end2 != null) {
TaskCondition condition = new TaskCondition()
.setFinalPromptEn(end2.getPrompt())
.setActionSet(Set.of(TaskAction.UPSCALE))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(condition)
.min(Comparator.comparing(Task::getSubmitTime))
.orElse(null);
if (task == null) {
return;
}
finishTask(task, message);
task.awake();
}
}
private UVContentParseData parseStart(String content) {
Matcher matcher = Pattern.compile(START_CONTENT_REGEX).matcher(content);
if (!matcher.find()) {
return null;
}
UVContentParseData parseData = new UVContentParseData();
parseData.setIndex(Integer.parseInt(matcher.group(1)));
parseData.setPrompt(matcher.group(2));
parseData.setStatus(matcher.group(3));
return parseData;
}
private UVContentParseData parseEnd(String content) {
Matcher matcher = Pattern.compile(END_CONTENT_REGEX).matcher(content);
if (!matcher.find()) {
return null;
}
UVContentParseData parseData = new UVContentParseData();
parseData.setPrompt(matcher.group(1));
parseData.setIndex(Integer.parseInt(matcher.group(2)));
parseData.setStatus("done");
return parseData;
}
private UVContentParseData parseEnd2(String content) {
Matcher matcher = Pattern.compile(END2_CONTENT_REGEX).matcher(content);
if (!matcher.find()) {
return null;
}
UVContentParseData parseData = new UVContentParseData();
parseData.setPrompt(matcher.group(1));
parseData.setStatus(matcher.group(2));
return parseData;
}
}
\ No newline at end of file
package com.github.novicezk.midjourney.wss.handle;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.Constants;
import com.github.novicezk.midjourney.enums.MessageType;
import com.github.novicezk.midjourney.enums.TaskAction;
import com.github.novicezk.midjourney.enums.TaskStatus;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskCondition;
import com.github.novicezk.midjourney.util.UVContentParseData;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.utils.data.DataObject;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* variation消息处理. todo 5.2之后V1-4操作返回的index始终为1, 暂时不判断index
* 开始(create): Making variations for image #1 with prompt **cat** - <@1012983546824114217> (Waiting to start)
* 进度(update): **cat** - Variations (Strong) by <@1012983546824114217> (0%) (relaxed)
* 5.2前-进度(update): **cat** - Variations by <@1012983546824114217> (0%) (relaxed)
* 完成(create): **cat** - Variations (Strong或Subtle) by <@1012983546824114217> (relaxed)
* 5.2前-完成(create): **cat** - Variations by <@1012983546824114217> (relaxed)
*/
@Slf4j
@Component
public class VariationMessageHandler extends MessageHandler {
private static final String START_CONTENT_REGEX = "Making variations for image #(\\d) with prompt \\*\\*(.*?)\\*\\* - <@\\d+> \\((.*?)\\)";
private static final String OLD_CONTENT_REGEX = "\\*\\*(.*?)\\*\\* - Variations by <@\\d+> \\((.*?)\\)";
private static final String CONTENT_REGEX = "\\*\\*(.*?)\\*\\* - Variations \\(.*?\\) by <@\\d+> \\((.*?)\\)";
@Override
public void handle(MessageType messageType, DataObject message) {
String content = getMessageContent(message);
if (MessageType.CREATE.equals(messageType)) {
UVContentParseData start = parseStart(content);
if (start != null) {
// 开始
TaskCondition condition = new TaskCondition()
.setFinalPromptEn(start.getPrompt())
.setActionSet(Set.of(TaskAction.VARIATION))
.setStatusSet(Set.of(TaskStatus.SUBMITTED));
Task task = this.taskQueueHelper.findRunningTask(condition)
.min(Comparator.comparing(Task::getSubmitTime))
.orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_PROGRESS_MESSAGE_ID, message.getString("id"));
task.setStatus(TaskStatus.IN_PROGRESS);
task.awake();
return;
}
UVContentParseData end = parse(content);
if (end == null) {
return;
}
TaskCondition condition = new TaskCondition()
.setFinalPromptEn(end.getPrompt())
.setActionSet(Set.of(TaskAction.VARIATION))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(condition)
.max(Comparator.comparing(Task::getProgress))
.orElse(null);
if (task == null) {
return;
}
finishTask(task, message);
task.awake();
} else if (MessageType.UPDATE == messageType) {
UVContentParseData parseData = parse(content);
if (parseData == null || CharSequenceUtil.equalsAny(parseData.getStatus(), "relaxed", "fast")) {
return;
}
TaskCondition condition = new TaskCondition()
.setProgressMessageId(message.getString("id"))
.setActionSet(Set.of(TaskAction.VARIATION))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(condition)
.findFirst().orElse(null);
if (task == null) {
return;
}
task.setProperty(Constants.TASK_PROPERTY_PROGRESS_MESSAGE_ID, message.getString("id"));
task.setStatus(TaskStatus.IN_PROGRESS);
task.setProgress(parseData.getStatus());
task.setImageUrl(getImageUrl(message));
task.awake();
}
}
/**
* bot-wss模式,取不到执行进度; todo: 同个任务不同变换对应不上.
*
* @param messageType messageType
* @param message message
*/
@Override
public void handle(MessageType messageType, Message message) {
String content = message.getContentRaw();
if (MessageType.CREATE.equals(messageType)) {
UVContentParseData parseData = parse(content);
if (parseData == null) {
return;
}
TaskCondition condition = new TaskCondition()
.setFinalPromptEn(parseData.getPrompt())
.setActionSet(Set.of(TaskAction.VARIATION))
.setStatusSet(Set.of(TaskStatus.SUBMITTED, TaskStatus.IN_PROGRESS));
Task task = this.taskQueueHelper.findRunningTask(condition)
.min(Comparator.comparing(Task::getSubmitTime))
.orElse(null);
if (task == null) {
return;
}
finishTask(task, message);
task.awake();
}
}
private UVContentParseData parseStart(String content) {
Matcher matcher = Pattern.compile(START_CONTENT_REGEX).matcher(content);
if (!matcher.find()) {
return null;
}
UVContentParseData parseData = new UVContentParseData();
parseData.setIndex(Integer.parseInt(matcher.group(1)));
parseData.setPrompt(matcher.group(2));
parseData.setStatus(matcher.group(3));
return parseData;
}
private UVContentParseData parse(String content) {
UVContentParseData data = parse(content, CONTENT_REGEX);
if (data == null) {
return parse(content, OLD_CONTENT_REGEX);
}
return data;
}
private UVContentParseData parse(String content, String regex) {
Matcher matcher = Pattern.compile(regex).matcher(content);
if (!matcher.find()) {
return null;
}
UVContentParseData parseData = new UVContentParseData();
parseData.setPrompt(matcher.group(1));
parseData.setStatus(matcher.group(2));
return parseData;
}
}
package com.github.novicezk.midjourney.wss.user;
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.utils.data.DataObject;
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 UserMessageListener 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());
}
public void onMessage(DataObject raw) {
MessageType messageType = MessageType.of(raw.getString("t"));
if (messageType == null || MessageType.DELETE == messageType) {
return;
}
DataObject data = raw.getObject("d");
if (ignoreAndLogMessage(data, messageType)) {
return;
}
for (MessageHandler messageHandler : this.messageHandlers) {
messageHandler.handle(messageType, data);
}
}
private boolean ignoreAndLogMessage(DataObject data, MessageType messageType) {
String channelId = data.getString("channel_id");
if (!this.properties.getDiscord().getChannelId().equals(channelId)) {
return true;
}
String authorName = data.optObject("author").map(a -> a.getString("username")).orElse("System");
log.debug("{} - {}: {}", messageType.name(), authorName, data.opt("content").orElse(""));
return false;
}
}
\ No newline at end of file
package com.github.novicezk.midjourney.wss.user;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.thread.ThreadUtil;
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.WebSocket;
import com.neovisionaries.ws.client.WebSocketAdapter;
import com.neovisionaries.ws.client.WebSocketFactory;
import com.neovisionaries.ws.client.WebSocketFrame;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.utils.data.DataArray;
import net.dv8tion.jda.api.utils.data.DataObject;
import net.dv8tion.jda.api.utils.data.DataType;
import net.dv8tion.jda.internal.requests.WebSocketCode;
import net.dv8tion.jda.internal.utils.compress.Decompressor;
import net.dv8tion.jda.internal.utils.compress.ZlibDecompressor;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class UserWebSocketStarter extends WebSocketAdapter implements WebSocketStarter {
private static final int CONNECT_RETRY_LIMIT = 3;
private final String userToken;
private final String userAgent;
private final DataObject auth;
private ScheduledExecutorService heartExecutor;
private WebSocket socket = null;
private String sessionId;
private Future<?> heartbeatTask;
private Decompressor decompressor;
private boolean connected = false;
private final AtomicInteger sequence = new AtomicInteger(0);
@Resource
private UserMessageListener userMessageListener;
@Resource
private DiscordHelper discordHelper;
private final ProxyProperties properties;
public UserWebSocketStarter(ProxyProperties properties) {
initProxy(properties);
this.properties = properties;
this.userToken = properties.getDiscord().getUserToken();
this.userAgent = properties.getDiscord().getUserAgent();
this.auth = createAuthData();
}
@Override
public synchronized void start() throws Exception {
this.decompressor = new ZlibDecompressor(2048);
this.heartExecutor = Executors.newSingleThreadScheduledExecutor();
WebSocketFactory webSocketFactory = createWebSocketFactory(this.properties);
this.socket = webSocketFactory.createSocket(this.discordHelper.getWss() + "/?encoding=json&v=9&compress=zlib-stream");
this.socket.addListener(this);
this.socket.addHeader("Accept-Encoding", "gzip, deflate, br").addHeader("Accept-Language", "en-US,en;q=0.9")
.addHeader("Cache-Control", "no-cache").addHeader("Pragma", "no-cache")
.addHeader("Sec-WebSocket-Extensions", "permessage-deflate; client_max_window_bits")
.addHeader("User-Agent", this.userAgent);
this.socket.connect();
}
@Override
public void onConnected(WebSocket websocket, Map<String, List<String>> headers) {
log.debug("[gateway] Connected to websocket.");
this.connected = true;
}
@Override
public void onBinaryMessage(WebSocket websocket, byte[] binary) throws Exception {
byte[] decompressBinary = this.decompressor.decompress(binary);
if (decompressBinary == null) {
return;
}
String json = new String(decompressBinary, StandardCharsets.UTF_8);
DataObject data = DataObject.fromJson(json);
int opCode = data.getInt("op");
if (opCode != WebSocketCode.HEARTBEAT_ACK) {
this.sequence.incrementAndGet();
}
if (opCode == WebSocketCode.HELLO) {
if (this.heartbeatTask == null && this.heartExecutor != null) {
long interval = data.getObject("d").getLong("heartbeat_interval");
this.heartbeatTask =
this.heartExecutor.scheduleAtFixedRate(this::heartbeat, interval, interval, TimeUnit.MILLISECONDS);
}
sayHello();
} else if (opCode == WebSocketCode.HEARTBEAT_ACK) {
log.trace("[gateway] Heartbeat ack.");
} else if (opCode == WebSocketCode.HEARTBEAT) {
send(DataObject.empty().put("op", WebSocketCode.HEARTBEAT).put("d", this.sequence));
} else if (opCode == WebSocketCode.INVALIDATE_SESSION) {
log.debug("[gateway] Invalid session.");
close("session invalid");
} else if (opCode == WebSocketCode.RECONNECT) {
log.debug("[gateway] Received opcode 7 (reconnect).");
close("reconnect");
} else if (opCode == WebSocketCode.DISPATCH) {
onDispatch(data);
}
}
@Override
public void onDisconnected(WebSocket websocket, WebSocketFrame serverCloseFrame, WebSocketFrame clientCloseFrame,
boolean closedByServer) {
reset();
int code = 1000;
String closeReason = "";
if (clientCloseFrame != null) {
code = clientCloseFrame.getCloseCode();
closeReason = clientCloseFrame.getCloseReason();
} else if (serverCloseFrame != null) {
code = serverCloseFrame.getCloseCode();
closeReason = serverCloseFrame.getCloseReason();
}
if (code >= 4010 || code == 4004) {
log.warn("[gateway] Websocket closed and can't reconnect! code: {}, reason: {}", code, closeReason);
System.exit(code);
return;
}
log.warn("[gateway] Websocket closed and will be reconnect... code: {}, reason: {}", code, closeReason);
ThreadUtil.execute(() -> {
try {
retryStart(0);
} catch (Exception e) {
log.error("[gateway] Websocket reconnect error", e);
System.exit(1);
}
});
}
private void retryStart(int currentRetryTime) throws Exception {
try {
start();
} catch (Exception e) {
if (currentRetryTime < CONNECT_RETRY_LIMIT) {
currentRetryTime++;
log.warn("[gateway] Websocket start fail, retry {} time... error: {}", currentRetryTime,
e.getMessage());
Thread.sleep(5000L);
retryStart(currentRetryTime);
} else {
throw e;
}
}
}
@Override
public void handleCallbackError(WebSocket websocket, Throwable cause) throws Exception {
log.error("[gateway] There was some websocket error", cause);
}
private void sayHello() {
DataObject data;
if (CharSequenceUtil.isBlank(this.sessionId)) {
data = DataObject.empty().put("op", WebSocketCode.IDENTIFY).put("d", this.auth);
log.trace("[gateway] Say hello: identify");
} else {
data = DataObject.empty().put("op", WebSocketCode.RESUME).put("d",
DataObject.empty().put("token", this.userToken).put("session_id", this.sessionId).put("seq",
Math.max(this.sequence.get() - 1, 0)));
log.trace("[gateway] Say hello: resume");
}
send(data);
}
private void close(String reason) {
this.connected = false;
this.socket.disconnect(1000, reason);
}
private void reset() {
this.connected = false;
this.sessionId = null;
this.sequence.set(0);
this.decompressor = null;
this.socket = null;
if (this.heartbeatTask != null) {
this.heartbeatTask.cancel(true);
this.heartbeatTask = null;
}
}
private void heartbeat() {
if (!this.connected) {
return;
}
send(DataObject.empty().put("op", WebSocketCode.HEARTBEAT).put("d", this.sequence));
}
private void onDispatch(DataObject raw) {
if (!raw.isType("d", DataType.OBJECT)) {
return;
}
DataObject content = raw.getObject("d");
String t = raw.getString("t", null);
if ("READY".equals(t)) {
this.sessionId = content.getString("session_id");
return;
}
try {
this.userMessageListener.onMessage(raw);
} catch (Exception e) {
log.error("user-wss handle message error", e);
}
}
protected void send(DataObject message) {
log.trace("[gateway] > {}", message);
this.socket.sendText(message.toString());
}
private DataObject createAuthData() {
UserAgent agent = UserAgent.parseUserAgentString(this.userAgent);
DataObject connectionProperties = DataObject.empty().put("os", agent.getOperatingSystem().getName())
.put("browser", agent.getBrowser().getGroup().getName()).put("device", "").put("system_locale", "zh-CN")
.put("browser_version", agent.getBrowserVersion().toString()).put("browser_user_agent", this.userAgent)
.put("referer", "").put("referring_domain", "").put("referrer_current", "")
.put("referring_domain_current", "").put("release_channel", "stable").put("client_build_number", 117300)
.put("client_event_source", null);
DataObject presence = DataObject.empty().put("status", "online").put("since", 0)
.put("activities", DataArray.empty()).put("afk", false);
DataObject clientState = DataObject.empty().put("guild_hashes", DataArray.empty()).put("highest_last_message_id", "0")
.put("read_state_version", 0).put("user_guild_settings_version", -1).put("user_settings_version", -1);
return DataObject.empty().put("token", this.userToken).put("capabilities", 4093)
.put("properties", connectionProperties).put("presence", presence).put("compress", false)
.put("client_state", clientState);
}
}
package spring.config;
import com.github.novicezk.midjourney.ProxyProperties;
import com.github.novicezk.midjourney.service.TaskStoreService;
import com.github.novicezk.midjourney.service.TranslateService;
import com.github.novicezk.midjourney.service.store.InMemoryTaskStoreServiceImpl;
import com.github.novicezk.midjourney.service.store.RedisTaskStoreServiceImpl;
import com.github.novicezk.midjourney.service.translate.BaiduTranslateServiceImpl;
import com.github.novicezk.midjourney.service.translate.GPTTranslateServiceImpl;
import com.github.novicezk.midjourney.support.Task;
import com.github.novicezk.midjourney.support.TaskMixin;
import com.github.novicezk.midjourney.wss.WebSocketStarter;
import com.github.novicezk.midjourney.wss.bot.BotMessageListener;
import com.github.novicezk.midjourney.wss.bot.BotWebSocketStarter;
import com.github.novicezk.midjourney.wss.user.UserMessageListener;
import com.github.novicezk.midjourney.wss.user.UserWebSocketStarter;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
public class BeanConfig {
@Bean
TranslateService translateService(ProxyProperties properties) {
return switch (properties.getTranslateWay()) {
case BAIDU -> new BaiduTranslateServiceImpl(properties.getBaiduTranslate());
case GPT -> new GPTTranslateServiceImpl(properties);
default -> prompt -> prompt;
};
}
@Bean
TaskStoreService taskStoreService(ProxyProperties proxyProperties, RedisConnectionFactory redisConnectionFactory) {
ProxyProperties.TaskStore.Type type = proxyProperties.getTaskStore().getType();
Duration timeout = proxyProperties.getTaskStore().getTimeout();
return switch (type) {
case IN_MEMORY -> new InMemoryTaskStoreServiceImpl(timeout);
case REDIS -> new RedisTaskStoreServiceImpl(timeout, taskRedisTemplate(redisConnectionFactory));
};
}
@Bean
RedisTemplate<String, Task> taskRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Task> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Task.class));
return redisTemplate;
}
@Bean
WebSocketStarter webSocketStarter(ProxyProperties properties) {
return properties.getDiscord().isUserWss() ? new UserWebSocketStarter(properties) : new BotWebSocketStarter(properties);
}
@Bean
@ConditionalOnProperty(prefix = "mj.discord", name = "user-wss", havingValue = "true")
UserMessageListener userMessageListener() {
return new UserMessageListener();
}
@Bean
@ConditionalOnProperty(prefix = "mj.discord", name = "user-wss", havingValue = "false")
BotMessageListener botMessageListener() {
return new BotMessageListener();
}
@Bean
ApplicationRunner enableMetaChangeReceiverInitializer(WebSocketStarter webSocketStarter) {
return args -> webSocketStarter.start();
}
@Bean
Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(ProxyProperties properties) {
if (properties.isIncludeTaskExtended()) {
return builder -> {
};
}
return builder -> builder.mixIn(Task.class, TaskMixin.class);
}
}
package spring.config;
import cn.hutool.core.text.CharSequenceUtil;
import com.github.novicezk.midjourney.ProxyProperties;
import com.github.novicezk.midjourney.support.ApiAuthorizeInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private ApiAuthorizeInterceptor apiAuthorizeInterceptor;
@Resource
private ProxyProperties properties;
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:doc.html");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
if (CharSequenceUtil.isNotBlank(this.properties.getApiSecret())) {
registry.addInterceptor(this.apiAuthorizeInterceptor)
.addPathPatterns("/submit/**", "/task/**");
}
}
}
{
"type":2,
"guild_id": "$guild_id",
"channel_id": "$channel_id",
"application_id":"936929561302675456",
"session_id":"$session_id",
"data":{
"version":"1118961510123847773",
"id":"1062880104792997970",
"name":"blend",
"type":1,
"options":[],
"attachments":[]
}
}
\ No newline at end of file
{
"type": 2,
"guild_id": "$guild_id",
"channel_id": "$channel_id",
"application_id": "936929561302675456",
"session_id": "$session_id",
"data": {
"version": "1118961510123847774",
"id": "1092492867185950852",
"name": "describe",
"type": 1,
"options": [
{
"type": 11,
"name": "image",
"value": 0
}
],
"attachments": [
{
"id": "0",
"filename": "$file_name",
"uploaded_filename": "$final_file_name"
}
]
}
}
\ No newline at end of file
{
"type": 2,
"guild_id": "$guild_id",
"channel_id": "$channel_id",
"application_id": "936929561302675456",
"session_id": "$session_id",
"data": {
"version": "1118961510123847772",
"id": "938956540159881230",
"name": "imagine",
"type": 1,
"options": [
{
"type": 3,
"name": "prompt",
"value": "$prompt"
}
]
}
}
\ No newline at end of file
{
"content":"$content",
"channel_id":"$channel_id",
"type":0,
"sticker_ids":[],
"attachments":[
{
"id":"0",
"filename": "$file_name",
"uploaded_filename": "$final_file_name"
}
]
}
\ No newline at end of file
{
"type": 3,
"guild_id": "$guild_id",
"channel_id": "$channel_id",
"message_id": "$message_id",
"application_id": "936929561302675456",
"session_id": "$session_id",
"message_flags": 0,
"data": {
"component_type": 2,
"custom_id": "MJ::JOB::reroll::0::$message_hash::SOLO"
}
}
\ No newline at end of file
{
"type": 3,
"guild_id": "$guild_id",
"channel_id": "$channel_id",
"message_id": "$message_id",
"application_id": "936929561302675456",
"session_id": "$session_id",
"message_flags": 0,
"data": {
"component_type": 2,
"custom_id": "MJ::JOB::upsample::$index::$message_hash"
}
}
\ No newline at end of file
{
"type": 3,
"guild_id": "$guild_id",
"channel_id": "$channel_id",
"message_id": "$message_id",
"application_id": "936929561302675456",
"session_id": "$session_id",
"message_flags": 0,
"data": {
"component_type": 2,
"custom_id": "MJ::JOB::variation::$index::$message_hash"
}
}
\ No newline at end of file
proxy:
host: 127.0.0.1
port: 33210
\ No newline at end of file
mj:
discord:
guild-id: 1092793633834537050
channel-id: 1092793638490222638
user-token: MTA5Mjc5MjYxNTI5ODE0NjQxNw.G4Pf5u.ScNr8HlfKbFq2-uwsnXgBZdctUaoPQsLstsFdY
bot-token: MTA5Mjc5MjYxNTI5ODE0NjQxNw.G4Pf5u.ScNr8HlfKbFq2-uwsnXgBZdctUaoPQsLstsFdY
session-id: 6768fe4d1fe14a87bc186f794c8a087e
user-agent: 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-wss: true
task-store:
type: in_memory
timeout: 30d
translate-way: null
queue:
timeout-minutes: 5
core-size: 3
queue-size: 10
spring:
application:
name: midjourney-proxy
profiles:
active: dev
server:
port: 9090
servlet:
context-path: /mj
logging:
level:
ROOT: info
com.github.novicezk.midjourney: debug
knife4j:
enable: true
openapi:
title: Midjourney Proxy API文档
description: 代理 MidJourney 的discord频道,实现api形式调用AI绘图
concat: novicezk
url: https://github.com/novicezk/midjourney-proxy
version: v2.3.5
terms-of-service-url: https://github.com/novicezk/midjourney-proxy
group:
api:
group-name: API
api-rule: package
api-rule-resources:
- com.github.novicezk.midjourney.controller
blood
twerk
making love
voluptuous
naughty
wincest
orgy
no clothes
au naturel
no shirt
decapitate
bare
nude
barely dressed
nude
bra
risque
scantily clad
cleavage
stripped
infested
full frontal
unclothed
invisible clothes
wearing nothing
lingerie
with no shirt
naked
without clothes on
negligee
zero clothes
gruesome
fascist
nazi
prophet mohammed
slave
coon
honkey
cocaine
heroin
meth
crack
kill
belle delphine
hitler
jinping
lolita
president xi
torture
disturbing
farts
fart
poop
infected
warts
shit
brown pudding
bunghole
vomit
voluptuous
seductive
sperm
sexy
sadist
sensored
censored
silenced
deepfake
inappropriate
waifu
succubus
slaughter
surgery
reproduce
crucified
seductively
explicit
inappropriate
large bust
explicit
wang
inappropriate
teratoma
intimate
see through
tryphophobia
bloodbath
wound
cronenberg
khorne
cannibal
cannibalism
visceral
guts
bloodshot
gory
killing
crucifixion
surgery
vivisection
massacre
hemoglobin
suicide
arse
labia
ass
mammaries
badonkers
bloody
minge
big ass
mommy milker
booba
nipple
oppai
booty
organs
bosom
ovaries
flesh
breasts
penis
busty
phallus
clunge
sexy female
crotch
skimpy
dick
thick
bruises
girth
titty
honkers
vagina
hooters
veiny
knob
ahegao
pinup
ballgag
car crash
playboy
bimbo
pleasure
bodily fluids
pleasures
boudoir
rule34
brothel
seducing
dominatrix
corpse
seductive
erotic
seductive
fuck
sensual
hardcore
sexy
hentai
shag
horny
crucified
shibari
incest
smut
jav
succubus
jerk off king at pic
thot
kinbaku
legs spread
sensuality
belly button
porn
patriotic
bleed
excrement
petite
seduction
mccurry
provocative
sultry
erected
camisole
tight white
arrest
see-through
feces
anus
revealing clothing
vein
loli
-edge
boobs
-backed
tied up
zedong
bathing
jail
reticulum
rear end
sakimichan
behind bars
shirtless
sakimichan
seductive
sexi
sexualiz
sexual
\ No newline at end of file
, /) ,
___ _(/ ___ __ __ _ __ __ _____/
// (__(_(_(_ /_(_)(_(_/ (_/ (__(/_(_/_ /_)_/ (_(_) /(__(_/_
.-/ .-/ .-/ / .-/
(_/ (_/ (_/ (_/
:: MidJourney Proxy :: v2.3.5
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="3 seconds">
<property name="application" value="mjproxy" />
<property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS}[TraceId:%X{X-B3-TraceId:-} SpanId:%X{X-B3-SpanId:-} ParentSpanId:%X{X-B3-ParentSpanId:-}] [%thread] %-5level %logger{50}:%L - %msg%n" />
<!-- 控制台调试输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>${pattern}</pattern>
</encoder>
<!--日志级别过滤-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>
<!-- INFO级别日志 -->
<appender name="info_apd" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>logs/info/${application}-info-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>10</MaxHistory>
<!--日志文件最大的大小 -->
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- or whenever the file size reaches 50MB -->
<maxFileSize>70MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!--日志级别过滤-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<!-- WARN级别日志 -->
<appender name="warn_apd" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>logs/warn/${application}-warn-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>20</MaxHistory>
<!--日志文件最大的大小 -->
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- or whenever the file size reaches 50MB -->
<maxFileSize>20MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!--日志级别过滤-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
</appender>
<!-- ERROR级别日志 -->
<appender name="error_apd" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>logs/error/${application}-error-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>30</MaxHistory>
<!--日志文件最大的大小 -->
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- or whenever the file size reaches 50MB -->
<maxFileSize>20MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!--日志级别过滤-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!-- 监控日志 -->
<appender name="monitorLogFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>logs/monitor/${application}-monitor-%d{yyyy-MM-dd_HH}.%i.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>10</MaxHistory>
<!--日志文件最大的大小 -->
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- or whenever the file size reaches 50MB -->
<maxFileSize>3GB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%msg%n</pattern>
</encoder>
<!--日志级别过滤-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<appender name="asyncMonitorLogAppender" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="monitorLogFileAppender" />
</appender>
<logger name="com.pcloud.universe.monitorlog.MonitorUtilImpl" level="INFO" additivity="false">
<appender-ref ref="asyncMonitorLogAppender" />
</logger>
<!-- INFO级别以上的日志全部都输出,不同的级别输出在不同的文件里面 -->
<root>
<level value="INFO"/>
<appender-ref ref="STDOUT" />
<appender-ref ref="info_apd" />
<appender-ref ref="warn_apd" />
<appender-ref ref="error_apd" />
</root>
<logger name="net.sf.ehcache" level="INFO"/>
<logger name="druid.sql" level="INFO"/>
<logger name="org.springframework.cloud.openfeign.FeignClientFactoryBean" level="ERROR"/>
<logger name="com.netflix.discovery.shared.resolver.aws.ConfigClusterResolver" level="WARN"/>
<logger name="com.pcloud.mj.proxy.mybatis.mapper" level="DEBUG"/>
</configuration>
\ No newline at end of file
text/html:html htm shtml
text/css:css
text/xml:xml
text/mathml:mml
text/plain:txt
text/vnd.sun.j2me.app-descriptor:jad
text/vnd.wap.wml:wml
text/x-component:htc
image/gif:gif
image/jpeg:jpg jpeg
image/png:png
image/tiff:tif tiff
image/vnd.wap.wbmp:wbmp
image/x-icon:ico
image/x-jng:jng
image/x-ms-bmp:bmp
image/svg+xml:svg svgz
image/webp:webp
application/javascript:js
application/x-javascript:js
application/atom+xml:atom
application/rss+xml:rss
application/font-woff:woff
application/java-archive:jar war ear
application/json:json
application/mac-binhex40:hqx
application/msword:doc
application/pdf:pdf
application/postscript:ps eps ai
application/rtf:rtf
application/vnd.apple.mpegurl:m3u8
application/vnd.ms-excel:xls
application/vnd.ms-fontobject:eot
application/vnd.ms-powerpoint:ppt
application/vnd.wap.wmlc:wmlc
application/vnd.google-earth.kml+xml:kml
application/vnd.google-earth.kmz:kmz
application/x-7z-compressed:7z
application/x-cocoa:cco
application/x-java-archive-diff:jardiff
application/x-java-jnlp-file:jnlp
application/x-makeself:run
application/x-perl:pl pm
application/x-pilot:prc pdb
application/x-rar-compressed:rar
application/x-redhat-package-manager:rpm
application/x-sea:sea
application/x-shockwave-flash:swf
application/x-stuffit:sit
application/x-tcl:tcl tk
application/x-x509-ca-cert:der pem crt
application/x-xpinstall:xpi
application/xhtml+xml:xhtml
application/xspf+xml:xspf
application/zip:zip
application/vnd.openxmlformats-officedocument.wordprocessingml.document:docx
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:xlsx
application/vnd.openxmlformats-officedocument.presentationml.presentation:pptx
audio/midi:mid midi kar
audio/mpeg:mp3
audio/ogg:ogg
audio/x-m4a:m4a
audio/x-realaudio:ra
video/3gpp:3gpp 3gp
video/mp2t:ts
video/mp4:mp4
video/mpeg:mpeg mpg
video/quicktime:mov
video/webm:webm
video/x-flv:flv
video/x-m4v:m4v
video/x-mng:mng
video/x-ms-asf:asx asf
video/x-ms-wmv:wmv
video/x-msvideo:avi
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