首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >构建企业级AI绘画中台:Midjourney自动化服务从设计到落地的Java实践

构建企业级AI绘画中台:Midjourney自动化服务从设计到落地的Java实践

原创
作者头像
97java-xyz
发布2026-08-07 14:26:12
发布2026-08-07 14:26:12
970
举报

构建企业级AI绘画中台:Midjourney自动化服务从设计到落地的Java实践

1. 业务痛点与技术选型

在企业内部设计中,我们经常面临批量生成电商主图、营销海报变体的需求。官方Discord的手动操作无法满足高并发、可审计、可回调的工程要求。

我们的目标是构建一个异步任务处理系统:用户通过REST API提交Prompt -> 系统自动下发至Midjourney Bot -> 监听生成完成事件 -> 图像上传至OSS -> 回调业务系统。

技术栈

  • 通信层JDA (Java Discord API) 5.0+,用于模拟用户操作Discord。
  • 存储层:Redis(任务状态缓存)+ MongoDB(任务元数据持久化)。
  • 文件处理ImageIO + TwelveMonkeys 插件(处理WebP格式)。
  • 异步调度:Spring Boot 3.2 虚拟线程 + Resilience4j 重试。

2. 核心难点攻克:登录态管理与消息监听

2.1 多账号池化管理

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进行缺失补偿。

2.2 消息拦截与任务关联

Midjourney Bot返回的图片是带附件的Embed消息,且包含upscaledvariations等后缀。我们需要解析Message中的content字段匹配任务ID(通常为<@userId> + imagine + 任务序列号)。

核心监听逻辑:

代码语言:javascript
复制
@Override
public 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指纹或自定义TraceId
    String traceId = extractTraceIdFromContent(rawContent);
    if (traceId != null) {
        // 异步处理图片下载与上传
        handleImageResult(traceId, msg.getAttachments().get(0).getUrl());
    }
}

3. 自动化动作执行器:Imagine与Upscale

我们不需要模拟点击按钮,而是直接发送Slash Command交互载荷。JDA提供了InteractionReference,但更稳定的是直接发送消息指令

代码语言:javascript
复制
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的UpscaleVary操作需要点击交互按钮(Button)。JDA必须通过MessageActionRow获取ButtonCustomId来触发:

代码语言:javascript
复制
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();
}

4. 稳定性的命脉:异步回调与补偿机制

由于MJ生成耗时通常在30s-90s,我们必须设计非阻塞回调

4.1 Webhook回调设计

当监听器接收到最终生成的图片URL时,触发回调网关。为了防止下游服务不可用,我们引入事件表(Event Table)模式:

代码语言:javascript
复制
@Transactional
public 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));
}

4.2 超时监控与重试

利用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));
    }
}

5. 图像处理与存储优化

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());
    }
}

6. 性能压测与并发控制

上线前,我们针对100个任务并发进行了模拟。发现瓶颈不在于HTTP请求,而在于Discord WebSocket连接数。每个JDA实例会单独维护一个WebSocket,内存占用极高。

优化手段

  • 限制单Pod的最大Discord连接数(-Ddiscord.max_connections=5)。
  • 使用协程(虚拟线程)处理图片下载,避免阻塞Netty的EventLoop。
代码语言:javascript
复制
# application.yml
spring.threads.virtual.enabled=true
discord.connection.pool.size=5

7. 总结:Java做自动化Bot的优势

这套系统已在我们内部稳定运行3个月,累计生成图片超过2万张。相比Python脚本,Java的强类型Spring事务管理在处理复杂状态流转(待处理->生成中->回调中->已完成)时,Bug率显著降低。

避坑指南

  1. Token安全:不要在日志中打印任何Authorization信息,使用@Value配合Jasypt加密。
  2. 限流应对:遇到429错误时,务必解析Retry-After头进行指数退避,否则账号极易被封禁。
  3. 消息去重:Midjourney可能推送重复的MessageUpdate事件,必须通过messageId + imageHash做幂等处理。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 构建企业级AI绘画中台:Midjourney自动化服务从设计到落地的Java实践
    • 1. 业务痛点与技术选型
    • 2. 核心难点攻克:登录态管理与消息监听
      • 2.1 多账号池化管理
      • 2.2 消息拦截与任务关联
    • 3. 自动化动作执行器:Imagine与Upscale
    • 4. 稳定性的命脉:异步回调与补偿机制
      • 4.1 Webhook回调设计
      • 4.2 超时监控与重试
    • 5. 图像处理与存储优化
    • 6. 性能压测与并发控制
    • 7. 总结:Java做自动化Bot的优势
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档