MidjourneyServiceImpl.java 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. package com.cyksj.service.midjourney.impl;
  2. import cn.hutool.core.collection.CollectionUtil;
  3. import cn.hutool.core.map.MapUtil;
  4. import cn.hutool.http.HttpRequest;
  5. import cn.hutool.http.HttpUtil;
  6. import cn.hutool.json.JSONArray;
  7. import cn.hutool.json.JSONObject;
  8. import cn.hutool.json.JSONUtil;
  9. import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  10. import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  11. import com.cyksj.common.EnvCommonService;
  12. import com.cyksj.common.exception.BusinessRuntimeException;
  13. import com.cyksj.common.task.GlobalThreadPoolTaskExecutor;
  14. import com.cyksj.common.util.Codec;
  15. import com.cyksj.common.util.J11HttpC;
  16. import com.cyksj.common.util.Jsons;
  17. import com.cyksj.common.util.StringUtil;
  18. import com.cyksj.mapper.MidjourneyUserConversationMapper;
  19. import com.cyksj.mapper.MidjourneyUserMapper;
  20. import com.cyksj.model.dto.BlendDimensions;
  21. import com.cyksj.model.dto.MessageButton;
  22. import com.cyksj.model.entity.MidjourneyAccount;
  23. import com.cyksj.model.entity.MidjourneyUser;
  24. import com.cyksj.model.entity.MidjourneyUserConversation;
  25. import com.cyksj.model.response.SubmitResult;
  26. import com.cyksj.redis.RedisService;
  27. import com.cyksj.service.midjourney.MidjourneyAccountService;
  28. import com.cyksj.service.midjourney.MidjourneyService;
  29. import lombok.RequiredArgsConstructor;
  30. import lombok.extern.slf4j.Slf4j;
  31. import org.apache.commons.lang3.StringUtils;
  32. import org.springframework.beans.factory.annotation.Value;
  33. import org.springframework.stereotype.Service;
  34. import javax.imageio.ImageIO;
  35. import javax.imageio.stream.FileImageOutputStream;
  36. import java.awt.image.BufferedImage;
  37. import java.io.*;
  38. import java.net.URI;
  39. import java.net.URLEncoder;
  40. import java.net.http.HttpResponse;
  41. import java.nio.charset.StandardCharsets;
  42. import java.nio.file.Files;
  43. import java.nio.file.Path;
  44. import java.security.MessageDigest;
  45. import java.time.ZoneOffset;
  46. import java.util.*;
  47. import java.util.concurrent.atomic.AtomicBoolean;
  48. import java.util.stream.Collectors;
  49. /**
  50. * @author zwhui
  51. * @date 2024/4/23 15:44
  52. */
  53. @Service
  54. @Slf4j
  55. @RequiredArgsConstructor
  56. public class MidjourneyServiceImpl implements MidjourneyService {
  57. private static final String RELAX_HOST = "http://43.154.230.104:8080";
  58. private static final String FAST_HOST = "https://aigc.api4midjourney.com/api";
  59. private static final String FAST_TOKEN = "14a6b469-7cb2-4b99-9238-676c7c5cffef";
  60. private final MidjourneyUserConversationMapper conversationMapper;
  61. private static final List<String> progress = List.of("100%");
  62. private static final List<String> status = List.of("MODAL","CANCEL","FAILURE");
  63. private final RedisService redisService;
  64. private final MidjourneyUserMapper midjourneyUserMapper;
  65. @Value("${midjourney.url}")
  66. private String midjourneyHost;
  67. private final EnvCommonService envCommonService;
  68. private static final GlobalThreadPoolTaskExecutor TASK_EXECUTOR = GlobalThreadPoolTaskExecutor.getInstance();
  69. private final MidjourneyAccountService midjourneyAccountService;
  70. @Override
  71. public MidjourneyUserConversation submitImagine(MidjourneyUser user, String prompt, List<String> base64Array) throws Exception {
  72. Map<String, Object> imagineParam = MapUtil.builder(new HashMap<String,Object>())
  73. .put("prompt", prompt).put("state", user.getId()).build();
  74. if (CollectionUtil.isNotEmpty(base64Array)) {
  75. imagineParam.put("base64Array", base64Array);
  76. }
  77. SubmitResult result = submit(user.getMode(),"imagine", null,imagineParam);
  78. return saveConversation(user.getId(), user.getMode(),Long.parseLong(result.getResult()),result.getProperties(),"IMAGINE", StringUtils.EMPTY);
  79. }
  80. public MidjourneyUserConversation saveConversation(Long userId,Integer mode,Long taskId,Map<String,Object> properties, String action,String prompt) throws Exception {
  81. MidjourneyUserConversation conversation = new MidjourneyUserConversation()
  82. .setUserId(userId).setTaskId(taskId).setAction(action).setPrompt(prompt)
  83. .setMode(mode).setStartTime(System.currentTimeMillis()).setProgress("0%")
  84. .setStatus("IN_PROGRESS").setTaskId(taskId);
  85. if (MapUtil.isNotEmpty(properties)) {
  86. conversation.setProperties(Jsons.toJson(properties));
  87. conversation.setChannelId(properties.get("discordChannelId") == null ? null : Long.parseLong(properties.get("discordChannelId").toString()))
  88. .setInstanceId(properties.get("discordInstanceId") == null ? null : Long.parseLong(properties.get("discordInstanceId").toString()));
  89. }
  90. conversationMapper.insert(conversation);
  91. return conversation;
  92. }
  93. @Override
  94. public MidjourneyUserConversation submitDescribe(MidjourneyUser user, String base64) throws Exception {
  95. Map<String, Object> param = MapUtil.builder(new HashMap<String,Object>()).put("state", user.getId()).put("base64", base64).build();
  96. SubmitResult result = submit(user.getMode(),"describe", null, param);
  97. return saveConversation(user.getId(), user.getMode(),Long.parseLong(result.getResult()),result.getProperties(),"DESCRIBE", StringUtils.EMPTY);
  98. }
  99. @Override
  100. public MidjourneyUserConversation submitBlend(MidjourneyUser user, BlendDimensions dimensions, List<String> base64Array) throws Exception {
  101. Map<String, Object> param = MapUtil.builder(new HashMap<String,Object>())
  102. .put("base64Array", base64Array).put("state", user.getId()).build();
  103. if (dimensions != null) {
  104. param.put("dimensions", dimensions);
  105. }
  106. SubmitResult result = submit(user.getMode(),"blend", null, param);
  107. Map<String, Object> properties = result.getProperties();
  108. // if (user.getMode() == 1) {
  109. // String finalPrompt = "%s --ar %s --style raw --s 250";
  110. // List<String> picList = uploadBase64Pic(base64Array);
  111. // String pics = picList.stream().map(pic -> "<" + pic + ">").collect(Collectors.joining(" "));
  112. // properties.put("finalPrompt", String.format(finalPrompt, pics,dimensions.getValue()));
  113. // }
  114. return saveConversation(user.getId(), user.getMode(), Long.parseLong(result.getResult()),properties,"BLEND", StringUtils.EMPTY);
  115. }
  116. @Override
  117. public MidjourneyUserConversation submitModal(MidjourneyUser user, Long taskId, String prompt, String maskBase64) throws Exception {
  118. Map<String, Object> param = MapUtil.builder(new HashMap<String,Object>())
  119. .put("taskId", taskId).put("state", user.getId()).build();
  120. if (StringUtils.isNotBlank(prompt)) {
  121. param.put("prompt", prompt);
  122. }
  123. if (StringUtils.isNotBlank(maskBase64)) {
  124. param.put("maskBase64", maskBase64);
  125. }
  126. SubmitResult result = submit(user.getMode(),"modal", null, param);
  127. return saveConversation(user.getId(), user.getMode(),Long.parseLong(result.getResult()),result.getProperties(),"MODAL", StringUtils.EMPTY);
  128. }
  129. @Override
  130. public MidjourneyUserConversation submitShorten(MidjourneyUser user, String prompt) throws Exception {
  131. Map<String, Object> param = MapUtil.builder(new HashMap<String,Object>())
  132. .put("prompt", prompt).put("state", user.getId()).build();
  133. SubmitResult result = submit(user.getMode(),"shorten", null, param);
  134. return saveConversation(user.getId(), user.getMode(), Long.parseLong(result.getResult()),result.getProperties(),"SHORTEN", StringUtils.EMPTY);
  135. }
  136. /**
  137. * 恢复次数
  138. */
  139. public Long recoverUserLimit(Long id,Integer mode,Long num){
  140. if (mode == 1){
  141. num = redisService.incr(RedisService.key.MIDJOURNEY_FAST_LIMIT.getName() + id, 1L);
  142. }
  143. if (mode == 2){
  144. if (num != null) {
  145. num = redisService.incr(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + id, 1L);
  146. }
  147. }
  148. return num;
  149. }
  150. /**
  151. * 同步数据库
  152. */
  153. public void syncUser(Long id,Integer mode,Long num){
  154. log.info("同步次数 id:{},mode:{},num:{}",id,mode,num);
  155. if (num == null){
  156. return;
  157. }
  158. LambdaUpdateWrapper<MidjourneyUser> wrapper = Wrappers.lambdaUpdate(MidjourneyUser.class)
  159. .eq(MidjourneyUser::getId, id);
  160. if (mode == 1){
  161. wrapper.set(MidjourneyUser::getMjFastNum, num);
  162. }
  163. if (mode == 2){
  164. wrapper.set(MidjourneyUser::getMjRelaxNum, num);
  165. }
  166. midjourneyUserMapper.update(null, wrapper);
  167. }
  168. @Override
  169. public SubmitResult submitAction(MidjourneyUser user, Long taskId, String customId,Long num) throws Exception {
  170. MidjourneyUserConversation conversation = conversationMapper.selectOne(Wrappers.lambdaQuery(MidjourneyUserConversation.class).eq(MidjourneyUserConversation::getTaskId, taskId).last("limit 1"));
  171. if (conversation == null) {
  172. throw BusinessRuntimeException.getInstance("关联任务不存在或已失效");
  173. }
  174. if (!user.getMode().equals(conversation.getMode())) {
  175. throw BusinessRuntimeException.getInstance("当前出图模式与关联任务出图模式不符");
  176. }
  177. if (StringUtils.isNotBlank(conversation.getButtons())){
  178. List<MessageButton> messageButtons = Jsons.parseList(conversation.getButtons(), MessageButton.class);
  179. messageButtons.forEach(button -> {
  180. if (button.getCustomId().equals(customId)) {
  181. log.info("action customId:{}", customId);
  182. button.setStyle(3);
  183. }
  184. });
  185. conversation.setButtons(Jsons.toJson(messageButtons));
  186. }
  187. if (customId.contains("BOOKMARK")) {
  188. conversation.setBookmark(true);
  189. conversationMapper.updateById(conversation);
  190. return null;
  191. }
  192. Map<String, Object> param = MapUtil.builder(new HashMap<String,Object>())
  193. .put("taskId", taskId).put("state", user.getId()).put("customId", customId).build();
  194. SubmitResult result = submit(user.getMode(),"action", conversation.getInstanceId(),param);
  195. if (result.getCode() == 21) {
  196. // 以上操作有弹窗确认,恢复次数
  197. recoverUserLimit(user.getId(), user.getMode(),num);
  198. }else {
  199. syncUser(user.getId(), user.getMode(),num);
  200. }
  201. saveConversation(user.getId(), user.getMode(), Long.parseLong(result.getResult()),result.getProperties(),"ACTION", StringUtils.EMPTY);
  202. return result;
  203. }
  204. public SubmitResult submit(Integer mode,String action,Long instanceId, Map<String, Object> param) throws Exception {
  205. String url = "";
  206. if (mode == 1) {
  207. param.put("mode", "FAST");
  208. url = FAST_HOST;
  209. } else if (mode == 2) {
  210. url = RELAX_HOST;
  211. String accountWithMinUsage;
  212. //慢速查询在使用次数最少的账号
  213. accountWithMinUsage = getAccountWithMinUsage(instanceId);
  214. param.put("accountFilter",MapUtil.builder(new HashMap<String,Object>())
  215. .put("instanceId",accountWithMinUsage).build());
  216. }
  217. url = url + getActionUrl(action);
  218. param.put("notifyHook",midjourneyHost +(EnvCommonService.active.equals(envCommonService.getEnv()) ? "/8081":"/8082") + "/api/applets/midjourney/notifyHook");
  219. String body = HttpRequest.post(url).body(Jsons.toJson(param)).header("Authorization", FAST_TOKEN).execute().body();
  220. log.info("action body:{}", body);
  221. SubmitResult submitResult = Jsons.parseObject(body, SubmitResult.class);
  222. int code = submitResult.getCode();
  223. if (code != 1 && code != 21 && code != 22) {
  224. if (code == 3) {
  225. throw BusinessRuntimeException.getInstance("账号不存在");
  226. }
  227. if (code == 4) {
  228. throw BusinessRuntimeException.getInstance("图片重复");
  229. }
  230. if (code == 24) {
  231. throw BusinessRuntimeException.getInstance("prompt包含敏感词");
  232. }
  233. log.error("action:" + action + " error message:" + submitResult.getDescription());
  234. throw BusinessRuntimeException.getInstance("队列已满,请稍后尝试");
  235. }
  236. return submitResult;
  237. }
  238. public String getAccountWithMinUsage(Long instanceId) {
  239. String key = RedisService.key.MIDJOURNEY_ACCOUNT.getName();
  240. try {
  241. // 获取所有账号ID和使用次数
  242. Map<Object, Object> accountsUsage = redisService.hmget(key);
  243. if (accountsUsage == null || accountsUsage.isEmpty()) {
  244. Map<Object, Object> map = midjourneyAccountService.list(Wrappers.lambdaQuery(MidjourneyAccount.class)).stream().collect(Collectors.toMap(k -> k.getInstanceId().toString(), v -> 0));
  245. // 保存所有账号ID和使用次数
  246. redisService.hmset(key, map);
  247. }
  248. // 找到使用次数最少的账号ID
  249. String minAccountId = null;
  250. if (instanceId != null) {
  251. minAccountId = instanceId.toString();
  252. }else {
  253. int minUsage = Integer.MAX_VALUE;
  254. for (Map.Entry<Object, Object> entry : accountsUsage.entrySet()) {
  255. int usage = Integer.parseInt(entry.getValue().toString());
  256. if (usage < minUsage) {
  257. minUsage = usage;
  258. minAccountId = entry.getKey().toString();
  259. }
  260. }
  261. }
  262. // 增加使用次数
  263. if (minAccountId != null) {
  264. redisService.hincr(key, minAccountId, 1.0);
  265. }
  266. return minAccountId;
  267. } catch (Exception e) {
  268. throw BusinessRuntimeException.getInstance("获取账号失败");
  269. }
  270. }
  271. private String getActionUrl(String action) {
  272. switch (action) {
  273. case "imagine":
  274. return "/mj/submit/imagine";
  275. case "action":
  276. return "/mj/submit/action";
  277. case "describe":
  278. return "/mj/submit/describe";
  279. case "blend":
  280. return"/mj/submit/blend";
  281. case "modal":
  282. return "/mj/submit/modal";
  283. case "shorten":
  284. return "/mj/submit/shorten";
  285. default: throw new IllegalArgumentException("Unknown action: " + action);
  286. }
  287. }
  288. @Override
  289. public List<MidjourneyUserConversation> listConversationByIds(Integer mode, List<Long> ids) {
  290. List<MidjourneyUserConversation> list = new ArrayList<>();
  291. ids.forEach(id ->{
  292. String queryKey = RedisService.key.MIDJOURNEY_QUERY.getName();
  293. Long count = redisService.incr(queryKey + id, 1L);
  294. if (count%5 == 0) {
  295. try {
  296. listByIds(mode,List.of(id)).forEach(json ->{
  297. JSONObject jsons = JSONUtil.parseObj(json);
  298. MidjourneyUserConversation conversation = JSONUtil.toBean(jsons, MidjourneyUserConversation.class);
  299. conversation.setUserId(jsons.getLong("state"));
  300. conversation.setTaskId(jsons.getLong("id"));
  301. list.add(conversation);
  302. TASK_EXECUTOR.execute(() -> {
  303. try {
  304. sync(conversation);
  305. } catch (IOException e) {
  306. throw new RuntimeException(e);
  307. }
  308. });
  309. });
  310. } catch (Exception e) {
  311. throw new RuntimeException(e);
  312. }
  313. }else {
  314. list.add((MidjourneyUserConversation) redisService.get(RedisService.key.MIDJOURNEY_CONVERSATION.getName() + id));
  315. }
  316. });
  317. return list;
  318. }
  319. public void sync(MidjourneyUserConversation conversation) throws IOException {
  320. if ((StringUtils.isNotBlank(conversation.getProgress()) && progress.contains(conversation.getProgress())) || status.contains(conversation.getStatus())) {
  321. log.info("同步任务:{},进度:{}",conversation.getTaskId(),conversation.getProgress());
  322. Long userId = conversation.getUserId();
  323. MidjourneyUserConversation dbConversation = conversationMapper.selectOne(Wrappers.lambdaQuery(MidjourneyUserConversation.class).eq(MidjourneyUserConversation::getTaskId, conversation.getTaskId())
  324. .eq(MidjourneyUserConversation::getUserId, userId).orderByDesc(MidjourneyUserConversation::getId).last("limit 1"));
  325. if (dbConversation != null) {
  326. try {
  327. String imageUrl = conversation.getImageUrl();
  328. if (StringUtil.isNotBlank(imageUrl)) {
  329. conversation.setImageUrl(uploadPic(imageUrl, "conversation"+dbConversation.getId()));
  330. }
  331. } catch (IOException e) {
  332. log.error("上传图片失败",e);
  333. }finally {
  334. conversation.setId(dbConversation.getId());
  335. conversationMapper.updateById(conversation);
  336. if (dbConversation.getMode() == 2){
  337. redisService.hdecr(RedisService.key.MIDJOURNEY_ACCOUNT.getName() , dbConversation.getInstanceId().toString(), 1.0);
  338. }
  339. }
  340. //失败返还次数
  341. if ("FAILURE".equals(conversation.getStatus())){
  342. Object num = redisService.get(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + userId);
  343. LambdaUpdateWrapper<MidjourneyUser> wrapper = Wrappers.lambdaUpdate(MidjourneyUser.class)
  344. .eq(MidjourneyUser::getId, userId);
  345. Integer mode = dbConversation.getMode();
  346. if (mode == 1){
  347. num = redisService.incr(RedisService.key.MIDJOURNEY_FAST_LIMIT.getName() + userId, 1L);
  348. wrapper.set(MidjourneyUser::getMjFastNum, num);
  349. }
  350. if (mode == 2){
  351. if (num != null) {
  352. num = redisService.incr(RedisService.key.MIDJOURNEY_RELAX_LIMIT.getName() + userId, 1L);
  353. }
  354. wrapper.set(MidjourneyUser::getMjRelaxNum, num);
  355. }
  356. midjourneyUserMapper.update(null, wrapper);
  357. }
  358. }
  359. }
  360. }
  361. private static String uploadPic(String url, String prefix) throws IOException {
  362. byte[] body = HttpUtil.downloadBytes(url);
  363. Path tempFile = Files.createTempFile(prefix, ".png");
  364. try (FileImageOutputStream imageOutput = new FileImageOutputStream(tempFile.toFile())) {
  365. imageOutput.write(body, 0, body.length);
  366. }
  367. Map<String, Object> paramMap = new HashMap<>();
  368. paramMap.put("file", tempFile.toFile());
  369. String json = HttpUtil.post("https://files.liuliangbang.vip/pic/ups", paramMap);
  370. log.info("上传图片结果:url:{},json:{}",url, json);
  371. return new JSONObject(json).getJSONObject("value").getJSONArray("saved").getJSONObject(0).getJSONObject("info").getStr("cdnUrl");
  372. }
  373. private static List<String> uploadBase64Pic(List<String> base64Array) throws IOException {
  374. List<String> list = new ArrayList<>();
  375. for (String base64 : base64Array) {
  376. String fileExt = "jpeg";
  377. if (base64.contains(";")){
  378. fileExt = base64.split(";")[0].split("/")[1];
  379. base64 = base64.split(",")[1];
  380. }
  381. byte[] body = Base64.getDecoder().decode(base64);
  382. Path tempFile = Files.createTempFile(UUID.randomUUID().toString(), "." + fileExt);
  383. try (FileImageOutputStream imageOutput = new FileImageOutputStream(tempFile.toFile())) {
  384. imageOutput.write(body, 0, body.length);
  385. }
  386. Map<String, Object> paramMap = new HashMap<>();
  387. paramMap.put("file", tempFile.toFile());
  388. String json = HttpUtil.post("https://files.liuliangbang.vip/pic/ups", paramMap);
  389. list.add(new JSONObject(json).getJSONObject("value").getJSONArray("saved").getJSONObject(0).getJSONObject("info").getStr("cdnUrl"));
  390. }
  391. return list;
  392. }
  393. public JSONArray listByIds(Integer mode,List<Long> ids) throws Exception {
  394. Map<String, Object> param = MapUtil.builder(new HashMap<String, Object>()).put("ids", ids).build();
  395. String body = HttpRequest.post((mode == 1 ? FAST_HOST : RELAX_HOST) + "/mj/task/list-by-condition").header("Authorization", FAST_TOKEN).body(Jsons.toJson(param)).execute().body();
  396. log.info("listByIds body:{}", body);
  397. return JSONUtil.parseArray(body);
  398. }
  399. @Override
  400. public MidjourneyUserConversation cancelConversation(MidjourneyUser user, Long id) {
  401. MidjourneyUserConversation conversation = conversationMapper.selectOne(Wrappers.lambdaQuery(MidjourneyUserConversation.class).eq(MidjourneyUserConversation::getId, id));
  402. if (conversation == null) {
  403. throw BusinessRuntimeException.getInstance("会话不存在");
  404. }
  405. if (!Objects.equals(conversation.getUserId(), user.getId())) {
  406. throw BusinessRuntimeException.getInstance("不是您的会话");
  407. }
  408. conversation.setStatus("CANCEL");
  409. conversationMapper.updateById(conversation);
  410. TASK_EXECUTOR.execute(() -> {
  411. cancel(user.getMode(),conversation.getTaskId());
  412. });
  413. return conversation;
  414. }
  415. public void cancel(Integer mode,Long taskId) {
  416. String body = HttpUtil.post(mode == 1 ? FAST_HOST : RELAX_HOST + "/mj/task/"+taskId+"/cancel",MapUtil.empty());
  417. log.info("cancel body:{}", body);
  418. JSONObject jsonObject = JSONUtil.parseObj(body);
  419. if (jsonObject.getInt("code") != 1) {
  420. throw BusinessRuntimeException.getInstance("cancel error message:" + jsonObject.getStr("description"));
  421. }
  422. }
  423. @Override
  424. public void notifyHook(String json) {
  425. log.info("notifyHook:{}", json);
  426. JSONObject jsons = JSONUtil.parseObj(json);
  427. MidjourneyUserConversation conversation = JSONUtil.toBean(jsons, MidjourneyUserConversation.class);
  428. conversation.setUserId(jsons.getLong("state"));
  429. conversation.setTaskId(jsons.getLong("id"));
  430. if (StringUtil.isNotBlank(conversation.getImageUrl())) {
  431. conversation.setImageUrl(conversation.getImageUrl().replace("cdn.discordapp.com", "mj.galaxydvd.com"));
  432. }
  433. redisService.set(RedisService.key.MIDJOURNEY_CONVERSATION.getName() + conversation.getTaskId(), conversation,RedisService.key.MIDJOURNEY_CONVERSATION.getTimeout());
  434. TASK_EXECUTOR.execute(() -> {
  435. try {
  436. sync(conversation);
  437. } catch (IOException e) {
  438. throw new RuntimeException(e);
  439. }
  440. });
  441. }
  442. }