作者:互联网 时间: 2026-08-30 10:03:56
处理构建企业级AI绘画中台:Midjourney自动化服务从设计到落地的Java实践这类问题时,先确认目标场景,再按步骤核对配置或玩法细节。
在企业内部设计中,我们经常面临批量生成电商主图、营销海报变体的需求。官方Discord的手动操作无法满足高并发、可审计、可回调的工程要求。

我们的目标是构建一个异步任务处理系统:用户通过REST API提交Prompt -> 系统自动下发至Midjourney Bot -> 监听生成完成事件 -> 图像上传至OSS -> 回调业务系统。
技术栈:
通信层:JDA (Java Discord API) 5.0 ,用于模拟用户操作Discord。存储层:Redis(任务状态缓存) MongoDB(任务元数据持久化)。文件处理:ImageIO TwelveMonkeys 插件(处理WebP格式)。异步调度:Spring Boot 3.2 虚拟线程 Resilience4j 重试。Midjourney对单账号并发有限制,我们必须维护一个账号池(Account Pool)。使用JDA构建每个账号的实例时,必须开启Gateway Intents以接收消息事件:
代码语言:javascript复制public DiscordBotInstance buildInstance(String token) {return DiscordBotInstance.builder().token(token).gatewayIntents(GatewayIntent.MESSAGE_CONTENT, GatewayIntent.DIRECT_MESSAGES).listener(new MidjourneyMessageListener()) // 自定义监听器.build();}
踩坑点:官方JDA默认只缓存最近1万条消息,若系统重启期间漏收消息,会导致任务永久挂起。解决方式是结合Redis存储lastMessageId,并在启动时通过MessageHistory进行缺失补偿。
Midjourney Bot返回的图片是带附件的Embed消息,且包含upscaled、variations等后缀。我们需要解析Message中的content字段匹配任务ID(通常为<@userId>imagine 任务序列号)。
核心监听逻辑:
代码语言:javascript复制@Overridepublic void onMessageReceived(MessageReceivedEvent event) {User author = event.getAuthor();// 只有Midjourney Bot发的消息且包含图片附件才处理if (!author.getId().equals(MJ_BOT_ID) || event.getMessage().getAttachments().isEmpty()) {return;}Message msg = event.getMessage();String rawContent = msg.getContentRaw();// 提取Prompt指纹或自定义TraceIdString traceId = extractTraceIdFromContent(rawContent);if (traceId != null) {// 异步处理图片下载与上传handleImageResult(traceId, msg.getAttachments().get(0).getUrl());}}
我们不需要模拟点击按钮,而是直接发送Slash Command交互载荷。JDA提供了InteractionReference,但更稳定的是直接发送消息指令:
public void sendImagineCommand(String prompt, String traceId) {// 拼接自定义后缀以识别任务 (Midjourney不支持metadata,只能拼在prompt后)String fullPrompt = prompt " --" traceId; // 通过文本频道发送Imagine指令textChannel.sendMessage("/imagine " fullPrompt).queue(success -> log.info("任务下发成功: {}", traceId),error -> {// 处理限流或验证码拦截if (error.getMessage().contains("rate")) {triggerRateLimitStrategy(traceId);}});}
难点突破:Midjourney的Upscale或Vary操作需要点击交互按钮(Button)。JDA必须通过Message的ActionRow获取Button的CustomId来触发:
public void clickUpscaleButton(String messageId, int buttonIndex) {Message message = textChannel.retrieveMessageById(messageId).complete();// 遍历ActionRows,找到第buttonIndex个按钮List<ActionRow> actionRows = message.getActionRows();if (actionRows.isEmpty()) return;Button button = (Button) actionRows.get(0).getComponents().get(buttonIndex);// 触发交互textChannel.sendMessage(button.getCustomId()).queue();}
由于MJ生成耗时通常在30s-90s,我们必须设计非阻塞回调。
当监听器接收到最终生成的图片URL时,触发回调网关。为了防止下游服务不可用,我们引入事件表(Event Table)模式:
代码语言:javascript复制@Transactionalpublic void callbackBusiness(String traceId, String imageUrl) {CallbackTask task = callbackTaskRepository.findByTraceId(traceId);if (task == null) return;// 先存结果,再发送回调task.setImageUrl(imageUrl);task.setStatus(Status.SUCCESS);callbackTaskRepository.save(task);// 异步发送HTTP回调,若失败由定时任务重试asyncHttpClient.post(task.getCallbackUrl(), buildPayload(task));}
利用Redis过期通知或Netty时间轮,对超过120秒未完成的任务进行强制失败或重新下发:
代码语言:javascript复制@Scheduled(fixedDelay = 60000)public void checkTimeoutTasks() {List<String> timeoutIds = redisOps.opsForSet().pop("pending_tasks", 100);for (String id : timeoutIds) {// 触发补偿:重新发送Imagine指令,或标记失败retryExecutor.submit(() -> sendImagineCommand(getPrompt(id), id));}}
MJ生成的图片格式通常为WebP,部分老旧OSS不支持直接显示。我们在中间层进行格式转换并压缩:
代码语言:javascript复制public String convertAndUpload(String imageUrl) throws IOException {// 使用TwelveMonkeys解决ImageIO原生不支持WebP的问题try (InputStream in = new URL(imageUrl).openStream()) {BufferedImage image = ImageIO.read(in);// 转为JPG (RGB模式)BufferedImage jpgImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);jpgImage.createGraphics().drawImage(image, 0, 0, Color.WHITE, null);// 上传至OSS (ByteArrayOutputStream)ByteArrayOutputStream baos = new ByteArrayOutputStream();ImageIO.write(jpgImage, "jpg", baos);return ossClient.upload(baos.toByteArray());}}
上线前,我们针对100个任务并发进行了模拟。发现瓶颈不在于HTTP请求,而在于Discord WebSocket连接数。每个JDA实例会单独维护一个WebSocket,内存占用极高。
优化手段:
限制单Pod的最大Discord连接数(-Ddiscord.max_connections=5)。使用协程(虚拟线程)处理图片下载,避免阻塞Netty的EventLoop。代码语言:javascript复制# application.ymlspring.threads.virtual.enabled=truediscord.connection.pool.size=5
这套系统已在我们内部稳定运行3个月,累计生成图片超过2万张。相比Python脚本,Java的强类型和Spring事务管理在处理复杂状态流转(待处理->生成中->回调中->已完成)时,Bug率显著降低。
避坑指南:
Token安全:不要在日志中打印任何Authorization信息,使用@Value配合Jasypt加密。限流应对:遇到429错误时,务必解析Retry-After头进行指数退避,否则账号极易被封禁。消息去重:Midjourney可能推送重复的MessageUpdate事件,必须通过messageId imageHash做幂等处理。