
在企业内部设计中,我们经常面临批量生成电商主图、营销海报变体的需求。官方Discord的手动操作无法满足高并发、可审计、可回调的工程要求。
我们的目标是构建一个异步任务处理系统:用户通过REST API提交Prompt -> 系统自动下发至Midjourney Bot -> 监听生成完成事件 -> 图像上传至OSS -> 回调业务系统。
技术栈:
Midjourney对单账号并发有限制,我们必须维护一个账号池(Account Pool)。使用JDA构建每个账号的实例时,必须开启Gateway Intents以接收消息事件:
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 + 任务序列号)。
核心监听逻辑:
@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());
}
}我们不需要模拟点击按钮,而是直接发送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)模式:
@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));
}利用Redis过期通知或Netty时间轮,对超过120秒未完成的任务进行强制失败或重新下发:
@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不支持直接显示。我们在中间层进行格式转换并压缩:
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,内存占用极高。
优化手段:
-Ddiscord.max_connections=5)。# application.yml
spring.threads.virtual.enabled=true
discord.connection.pool.size=5这套系统已在我们内部稳定运行3个月,累计生成图片超过2万张。相比Python脚本,Java的强类型和Spring事务管理在处理复杂状态流转(待处理->生成中->回调中->已完成)时,Bug率显著降低。
避坑指南:
@Value配合Jasypt加密。429错误时,务必解析Retry-After头进行指数退避,否则账号极易被封禁。MessageUpdate事件,必须通过messageId + imageHash做幂等处理。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。