TranslationServiceImpl.java 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. package com.agent.management.service.impl;
  2. import com.agent.management.model.vo.TranslationVO;
  3. import com.agent.management.service.AiModelService;
  4. import com.agent.management.service.SseService;
  5. import com.agent.management.service.TranslationService;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.ai.chat.client.ChatClient;
  8. import org.springframework.scheduling.annotation.Async;
  9. import org.springframework.stereotype.Service;
  10. import java.util.ArrayList;
  11. import java.util.List;
  12. import java.util.Map;
  13. import java.util.concurrent.CompletableFuture;
  14. import java.util.concurrent.ConcurrentHashMap;
  15. import java.util.concurrent.TimeUnit;
  16. import java.util.concurrent.TimeoutException;
  17. /**
  18. * 基于 Spring AI 的翻译服务实现
  19. * 支持同步/异步翻译去重:同一文本只会发起一次 AI 请求,后续调用复用结果
  20. */
  21. @Slf4j
  22. @Service
  23. public class TranslationServiceImpl implements TranslationService {
  24. private static final long TRANSLATE_TIMEOUT_SECONDS = 120;
  25. private static final int MAX_CHUNK_LENGTH = 4000;
  26. private final ChatClient chatClient;
  27. private final SseService sseService;
  28. /** 已完成的翻译缓存:原文 -> 译文 */
  29. private final Map<String, String> translationCache = new ConcurrentHashMap<>();
  30. /** 正在进行中的翻译:原文 -> CompletableFuture<译文> */
  31. private final Map<String, CompletableFuture<String>> pendingTranslations = new ConcurrentHashMap<>();
  32. public TranslationServiceImpl(ChatClient.Builder chatClientBuilder,
  33. AiModelService aiModelService,
  34. SseService sseService) {
  35. this.chatClient = aiModelService.getChatClientOrFallback(chatClientBuilder);
  36. this.sseService = sseService;
  37. }
  38. @Override
  39. public String translateToChinese(String text) {
  40. if (text == null || text.trim().isEmpty()) return text;
  41. if (containsChinese(text)) return text;
  42. String key = text.trim();
  43. // 1. 检查已完成缓存
  44. String cached = translationCache.get(key);
  45. if (cached != null) return cached;
  46. // 2. 获取或创建 Future(去重核心)
  47. CompletableFuture<String> future = getOrCreateFuture(key, text);
  48. // 3. 阻塞等待结果
  49. try {
  50. return future.get(TRANSLATE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
  51. } catch (TimeoutException e) {
  52. log.warn("[翻译] 等待翻译超时({}s), 返回原文: {}", TRANSLATE_TIMEOUT_SECONDS, truncate(text, 80));
  53. return text;
  54. } catch (Exception e) {
  55. log.error("[翻译] 等待翻译异常: {}", e.getMessage());
  56. return text;
  57. }
  58. }
  59. @Override
  60. public String getCachedTranslation(String text) {
  61. if (text == null || text.trim().isEmpty()) return text;
  62. if (containsChinese(text)) return text;
  63. return translationCache.get(text.trim());
  64. }
  65. @Async
  66. @Override
  67. public void asyncTranslateAndPush(String folderName, String name, String description) {
  68. log.info("[翻译-SSE] 开始异步翻译, folderName={}", folderName);
  69. String translatedName = translateToChinese(name);
  70. String translatedDescription = translateToChinese(description);
  71. TranslationVO vo = new TranslationVO(folderName, translatedName, translatedDescription);
  72. sseService.pushTranslation(vo);
  73. log.info("[翻译-SSE] 推送翻译结果完成, folderName={}", folderName);
  74. }
  75. /**
  76. * 获取或创建翻译 Future。使用 putIfAbsent 保证同一文本只创建一个 Future。
  77. * 如果已有 pending Future,直接复用(阻塞等待其完成)。
  78. * 如果缓存已存在,返回已完成的 Future。
  79. */
  80. private CompletableFuture<String> getOrCreateFuture(String key, String text) {
  81. // 快速路径:已完成缓存
  82. String cached = translationCache.get(key);
  83. if (cached != null) {
  84. return CompletableFuture.completedFuture(cached);
  85. }
  86. // 创建新 Future
  87. CompletableFuture<String> newFuture = new CompletableFuture<>();
  88. CompletableFuture<String> existing = pendingTranslations.putIfAbsent(key, newFuture);
  89. if (existing != null) {
  90. // 另一个线程已经在翻译该文本,复用其 Future
  91. log.info("[翻译] 复用已有的翻译任务: {}", truncate(text, 80));
  92. return existing;
  93. }
  94. // 当前线程负责执行翻译
  95. try {
  96. String result = doTranslate(text);
  97. newFuture.complete(result);
  98. } catch (Exception e) {
  99. newFuture.complete(text); // 降级返回原文
  100. } finally {
  101. pendingTranslations.remove(key);
  102. }
  103. return newFuture;
  104. }
  105. private String doTranslate(String text) {
  106. String key = text.trim();
  107. String cached = translationCache.get(key);
  108. if (cached != null) return cached;
  109. // 短文本直接翻译
  110. if (text.length() <= MAX_CHUNK_LENGTH) {
  111. return doTranslateSingle(text);
  112. }
  113. // 长文本:按段落分段翻译
  114. return doTranslateChunked(text);
  115. }
  116. private String doTranslateSingle(String text) {
  117. String key = text.trim();
  118. String cached = translationCache.get(key);
  119. if (cached != null) return cached;
  120. log.info("[翻译] 开始调用大模型翻译, 原文长度: {} 字符", text.length());
  121. long startTime = System.currentTimeMillis();
  122. try {
  123. String translated = chatClient.prompt()
  124. .user(u -> u.text("将以下英文翻译为简体中文,专有英文缩写、代码等不用翻译,只返回翻译结果,不要添加任何解释或额外文本:\n\n{text}")
  125. .param("text", text))
  126. .call()
  127. .content();
  128. long elapsed = System.currentTimeMillis() - startTime;
  129. log.info("[翻译] 大模型响应耗时: {}ms", elapsed);
  130. if (translated != null && !translated.trim().isEmpty()) {
  131. String result = translated.trim();
  132. translationCache.put(key, result);
  133. log.info("[翻译] 翻译成功: {} -> {}", truncate(text, 80), truncate(result, 80));
  134. return result;
  135. }
  136. log.warn("[翻译] 大模型返回空结果, 耗时: {}ms", elapsed);
  137. return text;
  138. } catch (Exception e) {
  139. log.error("[翻译] 大模型调用异常: {}", e.getMessage());
  140. return text;
  141. }
  142. }
  143. /**
  144. * 长文本分段翻译:按空行切分段落,每段不超过 MAX_CHUNK_LENGTH,逐段翻译后拼接
  145. */
  146. private String doTranslateChunked(String text) {
  147. log.info("[翻译] 长文本分段翻译开始, 总长度: {} 字符", text.length());
  148. long startTime = System.currentTimeMillis();
  149. String[] paragraphs = text.split("\n\\s*\n");
  150. List<String> chunks = new ArrayList<>();
  151. StringBuilder currentChunk = new StringBuilder();
  152. for (String para : paragraphs) {
  153. if (currentChunk.length() + para.length() + 2 > MAX_CHUNK_LENGTH && currentChunk.length() > 0) {
  154. chunks.add(currentChunk.toString());
  155. currentChunk = new StringBuilder();
  156. }
  157. if (currentChunk.length() > 0) currentChunk.append("\n\n");
  158. currentChunk.append(para);
  159. }
  160. if (currentChunk.length() > 0) {
  161. chunks.add(currentChunk.toString());
  162. }
  163. log.info("[翻译] 分为 {} 段", chunks.size());
  164. StringBuilder result = new StringBuilder();
  165. for (int i = 0; i < chunks.size(); i++) {
  166. String chunk = chunks.get(i);
  167. log.info("[翻译] 翻译第 {}/{} 段, 长度: {}", i + 1, chunks.size(), chunk.length());
  168. String translated = doTranslateSingle(chunk);
  169. if (i > 0) result.append("\n\n");
  170. result.append(translated);
  171. }
  172. long elapsed = System.currentTimeMillis() - startTime;
  173. log.info("[翻译] 长文本分段翻译完成, 总耗时: {}ms", elapsed);
  174. String finalResult = result.toString();
  175. translationCache.put(text.trim(), finalResult);
  176. return finalResult;
  177. }
  178. private String truncate(String text, int maxLen) {
  179. if (text == null) return "null";
  180. if (text.length() <= maxLen) return text;
  181. return text.substring(0, maxLen) + "...";
  182. }
  183. private boolean containsChinese(String text) {
  184. if (text == null) return false;
  185. for (int i = 0; i < text.length(); i++) {
  186. char c = text.charAt(i);
  187. if (c >= '\u4e00' && c <= '\u9fff') return true;
  188. }
  189. return false;
  190. }
  191. }