Commit 44e44dc4bb1431a293afd2d3cd9c57fccb65258e
1 parent
5044a581
增加注释
Showing
3 changed files
with
113 additions
and
53 deletions
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/constants/MjNotifyCode.java
| ... | ... | @@ -8,35 +8,7 @@ public final class MjNotifyCode { |
| 8 | 8 | * 成功. |
| 9 | 9 | */ |
| 10 | 10 | public static final int SUCCESS = 1; |
| 11 | - /** | |
| 12 | - * 数据未找到. | |
| 13 | - */ | |
| 14 | - public static final int NOT_FOUND = 3; | |
| 15 | - /** | |
| 16 | - * 校验错误. | |
| 17 | - */ | |
| 18 | - public static final int VALIDATION_ERROR = 4; | |
| 19 | - /** | |
| 20 | - * 系统异常. | |
| 21 | - */ | |
| 22 | - public static final int FAILURE = 9; | |
| 23 | 11 | |
| 24 | - /** | |
| 25 | - * 已存在. | |
| 26 | - */ | |
| 27 | - public static final int EXISTED = 21; | |
| 28 | - /** | |
| 29 | - * 排队中. | |
| 30 | - */ | |
| 31 | - public static final int IN_QUEUE = 22; | |
| 32 | - /** | |
| 33 | - * 队列已满. | |
| 34 | - */ | |
| 35 | - public static final int QUEUE_REJECTED = 23; | |
| 36 | - /** | |
| 37 | - * prompt包含敏感词. | |
| 38 | - */ | |
| 39 | - public static final int BANNED_PROMPT = 24; | |
| 40 | 12 | |
| 41 | 13 | |
| 42 | 14 | } |
| 43 | 15 | \ No newline at end of file | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/webSocket/MjWebSocketStarter.java
| ... | ... | @@ -7,9 +7,11 @@ import cn.iocoder.yudao.framework.ai.midjourney.MidjourneyConfig; |
| 7 | 7 | import cn.iocoder.yudao.framework.ai.midjourney.constants.MjNotifyCode; |
| 8 | 8 | import cn.iocoder.yudao.framework.ai.midjourney.webSocket.handler.MjWebSocketHandler; |
| 9 | 9 | import cn.iocoder.yudao.framework.ai.midjourney.webSocket.listener.MjMessageListener; |
| 10 | +import lombok.Getter; | |
| 10 | 11 | import lombok.extern.slf4j.Slf4j; |
| 11 | 12 | import org.apache.tomcat.websocket.Constants; |
| 12 | 13 | import org.jetbrains.annotations.NotNull; |
| 14 | +import org.springframework.util.concurrent.ListenableFuture; | |
| 13 | 15 | import org.springframework.util.concurrent.ListenableFutureCallback; |
| 14 | 16 | import org.springframework.web.socket.CloseStatus; |
| 15 | 17 | import org.springframework.web.socket.WebSocketHttpHeaders; |
| ... | ... | @@ -22,19 +24,43 @@ import java.util.concurrent.TimeoutException; |
| 22 | 24 | |
| 23 | 25 | @Slf4j |
| 24 | 26 | public class MjWebSocketStarter implements WebSocketStarter { |
| 27 | + /** | |
| 28 | + * 链接重试次数 | |
| 29 | + */ | |
| 25 | 30 | private static final int CONNECT_RETRY_LIMIT = 5; |
| 26 | - | |
| 31 | + /** | |
| 32 | + * mj 配置文件 | |
| 33 | + */ | |
| 27 | 34 | private final MidjourneyConfig midjourneyConfig; |
| 35 | + /** | |
| 36 | + * mj 监听(所有message 都会 callback到这里) | |
| 37 | + */ | |
| 28 | 38 | private final MjMessageListener userMessageListener; |
| 39 | + /** | |
| 40 | + * wss 服务器 | |
| 41 | + */ | |
| 29 | 42 | private final String wssServer; |
| 43 | + /** | |
| 44 | + * | |
| 45 | + */ | |
| 30 | 46 | private final String resumeWss; |
| 31 | - | |
| 47 | + /** | |
| 48 | + * | |
| 49 | + */ | |
| 50 | + private ResumeData resumeData = null; | |
| 51 | + /** | |
| 52 | + * 是否运行成功 | |
| 53 | + */ | |
| 32 | 54 | private boolean running = false; |
| 33 | - | |
| 55 | + /** | |
| 56 | + * 链接成功的 session | |
| 57 | + */ | |
| 34 | 58 | private WebSocketSession webSocketSession = null; |
| 35 | - private ResumeData resumeData = null; | |
| 36 | 59 | |
| 37 | - public MjWebSocketStarter(String wssServer, String resumeWss, MidjourneyConfig midjourneyConfig, MjMessageListener userMessageListener) { | |
| 60 | + public MjWebSocketStarter(String wssServer, | |
| 61 | + String resumeWss, | |
| 62 | + MidjourneyConfig midjourneyConfig, | |
| 63 | + MjMessageListener userMessageListener) { | |
| 38 | 64 | this.wssServer = wssServer; |
| 39 | 65 | this.resumeWss = resumeWss; |
| 40 | 66 | this.midjourneyConfig = midjourneyConfig; |
| ... | ... | @@ -42,11 +68,12 @@ public class MjWebSocketStarter implements WebSocketStarter { |
| 42 | 68 | } |
| 43 | 69 | |
| 44 | 70 | @Override |
| 45 | - public void start() throws Exception { | |
| 71 | + public void start() { | |
| 46 | 72 | start(false); |
| 47 | 73 | } |
| 48 | 74 | |
| 49 | 75 | private void start(boolean reconnect) { |
| 76 | + // 设置header | |
| 50 | 77 | WebSocketHttpHeaders headers = new WebSocketHttpHeaders(); |
| 51 | 78 | headers.add("Accept-Encoding", "gzip, deflate, br"); |
| 52 | 79 | headers.add("Accept-Language", "zh-CN,zh;q=0.9"); |
| ... | ... | @@ -54,19 +81,26 @@ public class MjWebSocketStarter implements WebSocketStarter { |
| 54 | 81 | headers.add("Pragma", "no-cache"); |
| 55 | 82 | headers.add("Sec-Websocket-Extensions", "permessage-deflate; client_max_window_bits"); |
| 56 | 83 | headers.add("User-Agent", this.midjourneyConfig.getUserAage()); |
| 57 | - var handler = new MjWebSocketHandler(this.midjourneyConfig, this.userMessageListener, this::onSocketSuccess, this::onSocketFailure); | |
| 84 | + // 创建 mjHeader | |
| 85 | + MjWebSocketHandler mjWebSocketHandler = new MjWebSocketHandler( | |
| 86 | + this.midjourneyConfig, this.userMessageListener, this::onSocketSuccess, this::onSocketFailure); | |
| 87 | + // | |
| 58 | 88 | String gatewayUrl; |
| 59 | 89 | if (reconnect) { |
| 60 | - gatewayUrl = getGatewayServer(this.resumeData.resumeGatewayUrl()) + "/?encoding=json&v=9&compress=zlib-stream"; | |
| 61 | - handler.setSessionId(this.resumeData.sessionId()); | |
| 62 | - handler.setSequence(this.resumeData.sequence()); | |
| 63 | - handler.setResumeGatewayUrl(this.resumeData.resumeGatewayUrl()); | |
| 90 | + gatewayUrl = getGatewayServer(this.resumeData.getResumeGatewayUrl()) + "/?encoding=json&v=9&compress=zlib-stream"; | |
| 91 | + mjWebSocketHandler.setSessionId(this.resumeData.getSessionId()); | |
| 92 | + mjWebSocketHandler.setSequence(this.resumeData.getSequence()); | |
| 93 | + mjWebSocketHandler.setResumeGatewayUrl(this.resumeData.getResumeGatewayUrl()); | |
| 64 | 94 | } else { |
| 65 | 95 | gatewayUrl = getGatewayServer(null) + "/?encoding=json&v=9&compress=zlib-stream"; |
| 66 | 96 | } |
| 67 | - var webSocketClient = new StandardWebSocketClient(); | |
| 97 | + // 创建 StandardWebSocketClient | |
| 98 | + StandardWebSocketClient webSocketClient = new StandardWebSocketClient(); | |
| 99 | + // 设置 io timeout 时间 | |
| 68 | 100 | webSocketClient.getUserProperties().put(Constants.IO_TIMEOUT_MS_PROPERTY, "10000"); |
| 69 | - var socketSessionFuture = webSocketClient.doHandshake(handler, headers, URI.create(gatewayUrl)); | |
| 101 | + // | |
| 102 | + ListenableFuture<WebSocketSession> socketSessionFuture = webSocketClient.doHandshake(mjWebSocketHandler, headers, URI.create(gatewayUrl)); | |
| 103 | + // 添加 callback 进行回调 | |
| 70 | 104 | socketSessionFuture.addCallback(new ListenableFutureCallback<>() { |
| 71 | 105 | @Override |
| 72 | 106 | public void onFailure(@NotNull Throwable e) { |
| ... | ... | @@ -87,14 +121,18 @@ public class MjWebSocketStarter implements WebSocketStarter { |
| 87 | 121 | } |
| 88 | 122 | |
| 89 | 123 | private void onSocketFailure(int code, String reason) { |
| 124 | + // 1001异常可以忽略 | |
| 90 | 125 | if (code == 1001) { |
| 91 | 126 | return; |
| 92 | 127 | } |
| 128 | + // 关闭 socket | |
| 93 | 129 | closeSocketSessionWhenIsOpen(); |
| 130 | + // 没有运行通知 | |
| 94 | 131 | if (!this.running) { |
| 95 | 132 | notifyWssLock(code, reason); |
| 96 | 133 | return; |
| 97 | 134 | } |
| 135 | + // 已经运行先设置为false,发起 | |
| 98 | 136 | this.running = false; |
| 99 | 137 | if (code >= 4000) { |
| 100 | 138 | log.warn("[wss-{}] Can't reconnect! Account disabled. Closed by {}({}).", this.midjourneyConfig.getChannelId(), code, reason); |
| ... | ... | @@ -107,36 +145,34 @@ public class MjWebSocketStarter implements WebSocketStarter { |
| 107 | 145 | } |
| 108 | 146 | } |
| 109 | 147 | |
| 148 | + /** | |
| 149 | + * 重连 | |
| 150 | + */ | |
| 110 | 151 | private void tryReconnect() { |
| 111 | 152 | try { |
| 112 | 153 | tryStart(true); |
| 113 | 154 | } catch (Exception e) { |
| 114 | - if (e instanceof TimeoutException) { | |
| 115 | - closeSocketSessionWhenIsOpen(); | |
| 116 | - } | |
| 117 | - log.warn("[wss-{}] Reconnect fail: {}, Try new connection...", this.midjourneyConfig.getChannelId(), e.getMessage()); | |
| 155 | + log.warn("[wss-{}] Reconnect fail: {}, Try new connection...", this.midjourneyConfig.getChannelId(), e.getMessage()); | |
| 118 | 156 | ThreadUtil.sleep(1000); |
| 119 | 157 | tryNewConnect(); |
| 120 | 158 | } |
| 121 | 159 | } |
| 122 | 160 | |
| 123 | 161 | private void tryNewConnect() { |
| 162 | + // 链接重试次数5 | |
| 124 | 163 | for (int i = 1; i <= CONNECT_RETRY_LIMIT; i++) { |
| 125 | 164 | try { |
| 126 | 165 | tryStart(false); |
| 127 | 166 | return; |
| 128 | 167 | } catch (Exception e) { |
| 129 | - if (e instanceof TimeoutException) { | |
| 130 | - closeSocketSessionWhenIsOpen(); | |
| 131 | - } | |
| 132 | - log.warn("[wss-{}] New connect fail ({}): {}", this.midjourneyConfig.getChannelId(), i, e.getMessage()); | |
| 168 | + log.warn("[wss-{}] New connect fail ({}): {}", this.midjourneyConfig.getChannelId(), i, e.getMessage()); | |
| 133 | 169 | ThreadUtil.sleep(5000); |
| 134 | 170 | } |
| 135 | 171 | } |
| 136 | 172 | log.error("[wss-{}] Account disabled", this.midjourneyConfig.getChannelId()); |
| 137 | 173 | } |
| 138 | 174 | |
| 139 | - public void tryStart(boolean reconnect) throws Exception { | |
| 175 | + public void tryStart(boolean reconnect) { | |
| 140 | 176 | start(reconnect); |
| 141 | 177 | } |
| 142 | 178 | |
| ... | ... | @@ -144,6 +180,9 @@ public class MjWebSocketStarter implements WebSocketStarter { |
| 144 | 180 | System.err.println("notifyWssLock: " + code + " - " + reason); |
| 145 | 181 | } |
| 146 | 182 | |
| 183 | + /** | |
| 184 | + * 关闭 socket session | |
| 185 | + */ | |
| 147 | 186 | private void closeSocketSessionWhenIsOpen() { |
| 148 | 187 | try { |
| 149 | 188 | if (this.webSocketSession != null && this.webSocketSession.isOpen()) { |
| ... | ... | @@ -161,6 +200,20 @@ public class MjWebSocketStarter implements WebSocketStarter { |
| 161 | 200 | return this.wssServer; |
| 162 | 201 | } |
| 163 | 202 | |
| 164 | - public record ResumeData(String sessionId, Object sequence, String resumeGatewayUrl) { | |
| 203 | + @Getter | |
| 204 | + public static class ResumeData { | |
| 205 | + | |
| 206 | + public ResumeData(String sessionId, Object sequence, String resumeGatewayUrl) { | |
| 207 | + this.sessionId = sessionId; | |
| 208 | + this.sequence = sequence; | |
| 209 | + this.resumeGatewayUrl = resumeGatewayUrl; | |
| 210 | + } | |
| 211 | + | |
| 212 | + /** | |
| 213 | + * socket session | |
| 214 | + */ | |
| 215 | + private final String sessionId; | |
| 216 | + private final Object sequence; | |
| 217 | + private final String resumeGatewayUrl; | |
| 165 | 218 | } |
| 166 | 219 | } |
| 167 | 220 | \ No newline at end of file | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/midjourney/webSocket/handler/MjWebSocketHandler.java
| ... | ... | @@ -30,16 +30,41 @@ import java.util.concurrent.TimeUnit; |
| 30 | 30 | |
| 31 | 31 | @Slf4j |
| 32 | 32 | public class MjWebSocketHandler implements WebSocketHandler { |
| 33 | + /** | |
| 34 | + * close 错误码:重连 | |
| 35 | + */ | |
| 33 | 36 | public static final int CLOSE_CODE_RECONNECT = 2001; |
| 37 | + /** | |
| 38 | + * close 错误码:无效、作废 | |
| 39 | + */ | |
| 34 | 40 | public static final int CLOSE_CODE_INVALIDATE = 1009; |
| 41 | + /** | |
| 42 | + * close 错误码:异常 | |
| 43 | + */ | |
| 35 | 44 | public static final int CLOSE_CODE_EXCEPTION = 1011; |
| 36 | - | |
| 45 | + /** | |
| 46 | + * mj配置文件 | |
| 47 | + */ | |
| 37 | 48 | private final MidjourneyConfig midjourneyConfig; |
| 49 | + /** | |
| 50 | + * mj 消息监听 | |
| 51 | + */ | |
| 38 | 52 | private final MjMessageListener userMessageListener; |
| 53 | + /** | |
| 54 | + * 成功回调 | |
| 55 | + */ | |
| 39 | 56 | private final SuccessCallback successCallback; |
| 57 | + /** | |
| 58 | + * 失败回调 | |
| 59 | + */ | |
| 40 | 60 | private final FailureCallback failureCallback; |
| 41 | - | |
| 61 | + /** | |
| 62 | + * 心跳执行器 | |
| 63 | + */ | |
| 42 | 64 | private final ScheduledExecutorService heartExecutor; |
| 65 | + /** | |
| 66 | + * auth数据 | |
| 67 | + */ | |
| 43 | 68 | private final DataObject authData; |
| 44 | 69 | |
| 45 | 70 | @Setter |
| ... | ... | @@ -55,6 +80,9 @@ public class MjWebSocketHandler implements WebSocketHandler { |
| 55 | 80 | private Future<?> heartbeatInterval; |
| 56 | 81 | private Future<?> heartbeatTimeout; |
| 57 | 82 | |
| 83 | + /** | |
| 84 | + * 处理 message 消息的 Decompressor | |
| 85 | + */ | |
| 58 | 86 | private final Decompressor decompressor = new ZlibDecompressor(2048); |
| 59 | 87 | |
| 60 | 88 | public MjWebSocketHandler(MidjourneyConfig account, |
| ... | ... | @@ -77,11 +105,13 @@ public class MjWebSocketHandler implements WebSocketHandler { |
| 77 | 105 | @Override |
| 78 | 106 | public void handleTransportError(@NotNull WebSocketSession session, @NotNull Throwable e) throws Exception { |
| 79 | 107 | log.error("[wss-{}] Transport error", this.midjourneyConfig.getChannelId(), e); |
| 108 | + // 通知链接异常 | |
| 80 | 109 | onFailure(CLOSE_CODE_EXCEPTION, "transport error"); |
| 81 | 110 | } |
| 82 | 111 | |
| 83 | 112 | @Override |
| 84 | 113 | public void afterConnectionClosed(@NotNull WebSocketSession session, @NotNull CloseStatus closeStatus) throws Exception { |
| 114 | + // 链接关闭 | |
| 85 | 115 | onFailure(closeStatus.getCode(), closeStatus.getReason()); |
| 86 | 116 | } |
| 87 | 117 | |
| ... | ... | @@ -92,13 +122,18 @@ public class MjWebSocketHandler implements WebSocketHandler { |
| 92 | 122 | |
| 93 | 123 | @Override |
| 94 | 124 | public void handleMessage(@NotNull WebSocketSession session, WebSocketMessage<?> message) throws Exception { |
| 125 | + // 获取 message 消息 | |
| 95 | 126 | ByteBuffer buffer = (ByteBuffer) message.getPayload(); |
| 127 | + // 解析 message | |
| 96 | 128 | byte[] decompressed = decompressor.decompress(buffer.array()); |
| 97 | 129 | if (decompressed == null) { |
| 98 | 130 | return; |
| 99 | 131 | } |
| 132 | + // 转换 json | |
| 100 | 133 | String json = new String(decompressed, StandardCharsets.UTF_8); |
| 134 | + // 转换 jda 自带的 dataObject(和json object 差不多) | |
| 101 | 135 | DataObject data = DataObject.fromJson(json); |
| 136 | + // 获取消息类型 | |
| 102 | 137 | int opCode = data.getInt("op"); |
| 103 | 138 | switch (opCode) { |
| 104 | 139 | case WebSocketCode.HEARTBEAT -> handleHeartbeat(session); | ... | ... |