From 56369fdeb25d0013586368bddf6226f5efa20d43 Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 28 Jun 2026 17:15:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20EpicScriptDialogue?= =?UTF-8?q?Service=20=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../service/EpicScriptDialogueService.java | 15 ++ .../impl/EpicScriptDialogueServiceImpl.java | 232 ++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 server/src/main/java/com/emotion/service/EpicScriptDialogueService.java create mode 100644 server/src/main/java/com/emotion/service/impl/EpicScriptDialogueServiceImpl.java diff --git a/server/src/main/java/com/emotion/service/EpicScriptDialogueService.java b/server/src/main/java/com/emotion/service/EpicScriptDialogueService.java new file mode 100644 index 0000000..13ce492 --- /dev/null +++ b/server/src/main/java/com/emotion/service/EpicScriptDialogueService.java @@ -0,0 +1,15 @@ +package com.emotion.service; + +import com.emotion.dto.request.EpicScriptCreateWithDialogueRequest; +import com.emotion.dto.response.EpicScriptCreateWithDialogueResponse; + +/** + * 剧本对话创建服务 + */ +public interface EpicScriptDialogueService { + + /** + * 创建剧本并创建对话,生成第一条 AI 剧本消息 + */ + EpicScriptCreateWithDialogueResponse createWithDialogue(EpicScriptCreateWithDialogueRequest request); +} diff --git a/server/src/main/java/com/emotion/service/impl/EpicScriptDialogueServiceImpl.java b/server/src/main/java/com/emotion/service/impl/EpicScriptDialogueServiceImpl.java new file mode 100644 index 0000000..06c98c9 --- /dev/null +++ b/server/src/main/java/com/emotion/service/impl/EpicScriptDialogueServiceImpl.java @@ -0,0 +1,232 @@ +package com.emotion.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.emotion.dto.request.EpicScriptCreateWithDialogueRequest; +import com.emotion.dto.request.ai.AiRuntimeRequest; +import com.emotion.dto.response.EpicScriptCreateWithDialogueResponse; +import com.emotion.dto.response.ai.AiRuntimeTestResponse; +import com.emotion.entity.Conversation; +import com.emotion.entity.EpicScript; +import com.emotion.entity.Message; +import com.emotion.mapper.EpicScriptMapper; +import com.emotion.service.AiRuntimeService; +import com.emotion.service.ConversationService; +import com.emotion.service.EpicScriptDialogueService; +import com.emotion.service.EpicScriptService; +import com.emotion.service.MessageService; +import com.emotion.util.SnowflakeIdGenerator; +import com.emotion.util.UserContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** + * 剧本对话创建服务实现类 + */ +@Slf4j +@Service +public class EpicScriptDialogueServiceImpl implements EpicScriptDialogueService { + + @Autowired + private EpicScriptService epicScriptService; + + @Autowired + private ConversationService conversationService; + + @Autowired + private MessageService messageService; + + @Autowired + private AiRuntimeService aiRuntimeService; + + @Autowired + private SnowflakeIdGenerator snowflakeIdGenerator; + + @Override + @Transactional(rollbackFor = Exception.class) + public EpicScriptCreateWithDialogueResponse createWithDialogue(EpicScriptCreateWithDialogueRequest request) { + String currentUserId = UserContextHolder.getCurrentUserId(); + if (currentUserId == null) { + throw new IllegalStateException("用户未登录"); + } + + String scriptId = snowflakeIdGenerator.nextIdAsString(); + String conversationId = snowflakeIdGenerator.nextIdAsString(); + LocalDateTime now = LocalDateTime.now(); + + // 1. 创建剧本 + EpicScript script = new EpicScript(); + script.setId(scriptId); + script.setUserId(currentUserId); + script.setTitle(buildTitle(request.getTheme())); + script.setTheme(request.getTheme()); + script.setStyle(StringUtils.hasText(request.getStyle()) ? request.getStyle() : "career"); + script.setLength(StringUtils.hasText(request.getLength()) ? request.getLength() : "medium"); + script.setConversationId(conversationId); + script.setPlotJson(new HashMap<>()); + script.setIsSelected(0); + epicScriptService.save(script); + + // 2. 创建对话 + Conversation conversation = new Conversation(); + conversation.setId(conversationId); + conversation.setUserId(currentUserId); + conversation.setUserType("registered"); + conversation.setScriptId(scriptId); + conversation.setTitle(script.getTitle()); + conversation.setType("script"); + conversation.setConversationStatus("active"); + conversation.setStartTime(now); + conversation.setLastActiveTime(now); + conversation.setMessageCount(0); + conversationService.save(conversation); + + // 3. 保存用户消息 + Message userMessage = new Message(); + userMessage.setId(snowflakeIdGenerator.nextIdAsString()); + userMessage.setConversationId(conversationId); + userMessage.setScriptId(scriptId); + userMessage.setUserId(currentUserId); + userMessage.setContent(request.getTheme()); + userMessage.setType("chat"); + userMessage.setSender("user"); + userMessage.setTimestamp(now); + userMessage.setMessageOrder(1L); + userMessage.setStatus("sent"); + userMessage.setIsRead(1); + messageService.createMessage(userMessage); + + // 4. 调用 AI 生成完整剧本 + String aiOutput = invokeScriptGenerate(request, currentUserId, conversationId, userMessage.getId()); + + // 5. 解析并保存 AI 剧本消息 + Map metadata = parseScriptOutput(aiOutput); + String content = buildDisplayContent(metadata); + + Message aiMessage = new Message(); + aiMessage.setId(snowflakeIdGenerator.nextIdAsString()); + aiMessage.setConversationId(conversationId); + aiMessage.setScriptId(scriptId); + aiMessage.setUserId(currentUserId); + aiMessage.setContent(content); + aiMessage.setType("script"); + aiMessage.setSender("assistant"); + aiMessage.setTimestamp(LocalDateTime.now()); + aiMessage.setMessageOrder(2L); + aiMessage.setVersionNumber(1); + aiMessage.setStatus("sent"); + aiMessage.setMetadata(JSON.toJSONString(metadata)); + messageService.createMessage(aiMessage); + + // 6. 更新剧本快照 + script.setCurrentVersionMessageId(aiMessage.getId()); + script.setTitle((String) metadata.getOrDefault("title", script.getTitle())); + script.setPlotIntro((String) metadata.getOrDefault("plotIntro", "")); + script.setPlotTurning((String) metadata.getOrDefault("plotTurning", "")); + script.setPlotClimax((String) metadata.getOrDefault("plotClimax", "")); + script.setPlotEnding((String) metadata.getOrDefault("plotEnding", "")); + Map plotJson = script.getPlotJson(); + plotJson.putAll(metadata); + script.setPlotJson(plotJson); + epicScriptService.updateById(script); + + // 7. 更新对话当前消息 + conversation.setCurrentMessageId(aiMessage.getId()); + conversation.setMessageCount(2); + conversationService.updateById(conversation); + + // 8. 构建响应 + EpicScriptCreateWithDialogueResponse response = new EpicScriptCreateWithDialogueResponse(); + BeanUtils.copyProperties(script, response); + response.setScriptId(scriptId); + response.setConversationId(conversationId); + response.setCurrentVersionMessageId(aiMessage.getId()); + return response; + } + + private String buildTitle(String theme) { + if (!StringUtils.hasText(theme)) { + return "我的人生剧本"; + } + return theme.length() > 20 ? theme.substring(0, 20) + "..." : theme; + } + + private String invokeScriptGenerate(EpicScriptCreateWithDialogueRequest request, String userId, + String conversationId, String userMessageId) { + JSONObject inputs = new JSONObject(); + inputs.put("input", request.getTheme()); + inputs.put("prompt", request.getTheme()); + inputs.put("theme", request.getTheme()); + inputs.put("style", StringUtils.hasText(request.getStyle()) ? request.getStyle() : "career"); + inputs.put("length", StringUtils.hasText(request.getLength()) ? request.getLength() : "medium"); + inputs.put("characterInfo", request.getCharacterInfo()); + inputs.put("lifeEventsSummary", request.getLifeEventsSummary()); + inputs.put("useSocialInsights", request.getUseSocialInsights()); + inputs.put("conversationId", conversationId); + inputs.put("userMessageId", userMessageId); + + // 使用 AiRuntimeRequest 调用 AI 运行时 + AiRuntimeRequest runtimeRequest = new AiRuntimeRequest(); + runtimeRequest.setSceneCode("script_generate"); + runtimeRequest.setUserId(userId); + runtimeRequest.setInputs(inputs); + + AiRuntimeTestResponse response = aiRuntimeService.test(runtimeRequest); + if (response == null || !"success".equals(response.getStatus()) || !StringUtils.hasText(response.getOutput())) { + String message = response == null ? "AI_RUNTIME_EMPTY_RESPONSE" : response.getErrorMessage(); + throw new IllegalStateException(StringUtils.hasText(message) ? message : "AI_RUNTIME_FAILED"); + } + return response.getOutput(); + } + + private Map parseScriptOutput(String output) { + if (!StringUtils.hasText(output)) { + return new HashMap<>(); + } + try { + return JSON.parseObject(output, Map.class); + } catch (Exception e) { + log.warn("剧本输出解析失败,使用原始文本作为内容: {}", e.getMessage()); + Map fallback = new HashMap<>(); + fallback.put("title", "我的人生剧本"); + fallback.put("plotIntro", output); + return fallback; + } + } + + private String buildDisplayContent(Map metadata) { + StringBuilder sb = new StringBuilder(); + appendSection(sb, metadata.get("title"), null); + appendSection(sb, metadata.get("plotIntro"), "序幕:低谷回响"); + appendSection(sb, metadata.get("plotTurning"), "转折:契机出现"); + appendSection(sb, metadata.get("plotClimax"), "高潮:命运抉择"); + appendSection(sb, metadata.get("plotEnding"), "结局:新的开始"); + return sb.toString(); + } + + private void appendSection(StringBuilder sb, Object value, String label) { + if (value == null) { + return; + } + String text = String.valueOf(value); + if (!StringUtils.hasText(text)) { + return; + } + if (sb.length() > 0) { + sb.append("\n\n"); + } + if (StringUtils.hasText(label)) { + sb.append("【").append(label).append("】\n"); + } + sb.append(text); + } +}