diff --git a/AGENTS.md b/AGENTS.md index c7f6389..a40cd63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,17 +20,36 @@ # 1. 本地构建 cd frontend/sidebar && npm run build -# 2. 同步到服务器 +# 2. 同步到服务器(注意带 JS hash 的文件名会变化) scp -r -o StrictHostKeyChecking=no -i root.pem frontend/sidebar/dist/* root@8.133.162.25:/www/wwwroot/deploy-package/frontend/sidebar/dist/ -# 3. SSH 登录服务器,重新构建镜像并重启容器 +# 3. SSH 登录服务器,更新 index.html 缓存戳并重启容器 ssh -o StrictHostKeyChecking=no -i root.pem root@8.133.162.25 << 'EOF' -cd /www/wwwroot/deploy-package/frontend/sidebar -docker build -t deploy-package-sidebar . -docker stop ai-assistant-sidebar -docker rm ai-assistant-sidebar -cd /www/wwwroot/deploy-package -docker-compose up -d sidebar +cd /www/wwwroot/deploy-package/frontend/sidebar/dist +# 更新 index.html 中的 JS/CSS 引用,带上版本戳防缓存 +JS_FILE=$(ls assets/index-*.js | head -1) +CSS_FILE=$(ls assets/index-*.css | head -1) +cat > index.html << HTML + + + + + + + AI坐席助手 + + + + + +
+ + +HTML +# 复制到容器并重启 +docker cp . ai-assistant-sidebar:/usr/share/nginx/html/ +docker exec ai-assistant-sidebar nginx -s reload +docker restart ai-assistant-nginx EOF ``` @@ -92,9 +111,12 @@ cd backend/auth-service && mvn clean package -DskipTests scp -o StrictHostKeyChecking=no -i root.pem target/*.jar root@8.133.162.25:/www/wwwroot/deploy-package/backend/auth-service/ # 3. SSH 登录服务器,重启对应容器 +# ⚠️ 注意:Dockerfile 中 COPY 的是 app.jar,必须先把新 jar 复制为 app.jar ssh -o StrictHostKeyChecking=no -i root.pem root@8.133.162.25 << 'EOF' +cd /www/wwwroot/deploy-package/backend/auth-service +cp auth-service-*.jar app.jar cd /www/wwwroot/deploy-package -docker-compose up -d --build auth-service +docker-compose up -d --build --no-cache auth-service EOF ``` diff --git a/backend/conversation-service/src/main/java/com/artedu/conversation/controller/ConversationController.java b/backend/conversation-service/src/main/java/com/artedu/conversation/controller/ConversationController.java index b8ee360..03889f3 100644 --- a/backend/conversation-service/src/main/java/com/artedu/conversation/controller/ConversationController.java +++ b/backend/conversation-service/src/main/java/com/artedu/conversation/controller/ConversationController.java @@ -65,15 +65,17 @@ public class ConversationController { @GetMapping("/last-message") public Result> getLastMessage( @NotBlank(message = "customerId不能为空") @RequestParam("customerId") String customerId, + @NotBlank(message = "staffId不能为空") @RequestParam("staffId") String staffId, @NotBlank(message = "corpId不能为空") @RequestParam("corpId") String corpId) { - ArchiveMessage message = archiveMessageMapper.selectLastMessage(customerId, corpId); + ArchiveMessage message = archiveMessageMapper.selectLastMessage(customerId, staffId, corpId); Map result = new HashMap<>(); if (message != null) { result.put("content", message.getContent()); result.put("msgTime", message.getMsgtime()); result.put("msgType", message.getMsgtype()); result.put("fromUser", message.getFromUser()); + result.put("fromRole", message.getFromRole()); } return Result.success(result); } diff --git a/backend/conversation-service/src/main/java/com/artedu/conversation/mapper/ArchiveMessageMapper.java b/backend/conversation-service/src/main/java/com/artedu/conversation/mapper/ArchiveMessageMapper.java index e192ba3..e5132b9 100644 --- a/backend/conversation-service/src/main/java/com/artedu/conversation/mapper/ArchiveMessageMapper.java +++ b/backend/conversation-service/src/main/java/com/artedu/conversation/mapper/ArchiveMessageMapper.java @@ -9,11 +9,18 @@ import org.apache.ibatis.annotations.Select; @Mapper public interface ArchiveMessageMapper extends BaseMapper { + /** + * 获取指定学员在指定对话中的最后一条消息 + * 通过 staffId 限定只查当前销售与学员的对话,避免学员同时和多个销售聊天时串消息 + */ @Select("SELECT * FROM archive_messages " + - "WHERE (from_user = #{userId} OR to_user LIKE CONCAT('%', #{userId}, '%')) " + + "WHERE from_user = #{userId} " + + "AND to_user LIKE CONCAT('%', #{staffId}, '%') " + "AND corp_id = #{corpId} " + + "AND from_role = 'EXTERNAL' " + "AND content IS NOT NULL AND content != '' " + "ORDER BY msgtime DESC LIMIT 1") ArchiveMessage selectLastMessage(@Param("userId") String userId, + @Param("staffId") String staffId, @Param("corpId") String corpId); } diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/ConversationServiceApplication.class b/backend/conversation-service/target/classes/com/artedu/conversation/ConversationServiceApplication.class index 5491d70..b19dcb8 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/ConversationServiceApplication.class and b/backend/conversation-service/target/classes/com/artedu/conversation/ConversationServiceApplication.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/controller/ConversationController.class b/backend/conversation-service/target/classes/com/artedu/conversation/controller/ConversationController.class index 50a5dee..4aa6863 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/controller/ConversationController.class and b/backend/conversation-service/target/classes/com/artedu/conversation/controller/ConversationController.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/entity/ArchiveMessage.class b/backend/conversation-service/target/classes/com/artedu/conversation/entity/ArchiveMessage.class index 0d9911e..c566e27 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/entity/ArchiveMessage.class and b/backend/conversation-service/target/classes/com/artedu/conversation/entity/ArchiveMessage.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/entity/Conversation.class b/backend/conversation-service/target/classes/com/artedu/conversation/entity/Conversation.class index 730a87f..9de86b9 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/entity/Conversation.class and b/backend/conversation-service/target/classes/com/artedu/conversation/entity/Conversation.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/entity/ConversationTurn.class b/backend/conversation-service/target/classes/com/artedu/conversation/entity/ConversationTurn.class index 9cdba55..b7cb2e6 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/entity/ConversationTurn.class and b/backend/conversation-service/target/classes/com/artedu/conversation/entity/ConversationTurn.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ArchiveMessageMapper.class b/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ArchiveMessageMapper.class index a26a95f..8d43de8 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ArchiveMessageMapper.class and b/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ArchiveMessageMapper.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationMapper.class b/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationMapper.class index ce17d44..49e1f97 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationMapper.class and b/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationMapper.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationTurnMapper.class b/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationTurnMapper.class index c66c71f..12c9721 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationTurnMapper.class and b/backend/conversation-service/target/classes/com/artedu/conversation/mapper/ConversationTurnMapper.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ArchiveMessageEvent.class b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ArchiveMessageEvent.class index 339ac83..c3f38fa 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ArchiveMessageEvent.class and b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ArchiveMessageEvent.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ContextMessage.class b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ContextMessage.class index 3f31c29..d8731ec 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ContextMessage.class and b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager$ContextMessage.class differ diff --git a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager.class b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager.class index 3e55fcc..ad70d61 100644 Binary files a/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager.class and b/backend/conversation-service/target/classes/com/artedu/conversation/service/ConversationManager.class differ diff --git a/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar b/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar index 0fce136..faf73ad 100644 Binary files a/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar and b/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar differ diff --git a/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar.original b/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar.original index 7959fc8..e517ded 100644 Binary files a/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar.original and b/backend/conversation-service/target/conversation-service-1.0.0-SNAPSHOT.jar.original differ diff --git a/backend/conversation-service/target/test-classes/com/artedu/conversation/controller/ConversationControllerTest.class b/backend/conversation-service/target/test-classes/com/artedu/conversation/controller/ConversationControllerTest.class index b2e4409..a15294c 100644 Binary files a/backend/conversation-service/target/test-classes/com/artedu/conversation/controller/ConversationControllerTest.class and b/backend/conversation-service/target/test-classes/com/artedu/conversation/controller/ConversationControllerTest.class differ diff --git a/backend/generation-service/src/main/java/com/artedu/generation/controller/GenerationController.java b/backend/generation-service/src/main/java/com/artedu/generation/controller/GenerationController.java index ca3d71d..b30f2e2 100644 --- a/backend/generation-service/src/main/java/com/artedu/generation/controller/GenerationController.java +++ b/backend/generation-service/src/main/java/com/artedu/generation/controller/GenerationController.java @@ -9,13 +9,19 @@ import com.artedu.generation.service.PromptEngine; import com.artedu.generation.service.QianwenClient; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import javax.validation.constraints.NotBlank; +import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; @Slf4j @Validated @@ -35,6 +41,73 @@ public class GenerationController { @Autowired private ArchiveMessageMapper archiveMessageMapper; + private final ExecutorService sseExecutor = Executors.newCachedThreadPool(); + + private static final List> STYLES = List.of( + Map.of("id", "friendly", "name", "亲和风格", "desc", "温暖亲切,像朋友聊天"), + Map.of("id", "professional", "name", "专业风格", "desc", "数据驱动,专业权威"), + Map.of("id", "urgent", "name", "促单风格", "desc", "制造紧迫感,促成决策") + ); + + /** + * 根据当前聊天对象自动推断员工身份 + * 前端不知道 toUser 时调用,后端从聊天记录中反推 + */ + @GetMapping("/whoami") + public Result> whoami( + @RequestParam(value = "fromUser", required = false) String fromUser, + @RequestParam(value = "roomid", required = false) String roomid) { + + String staffId = null; + String chatType = null; + + if (roomid != null && !roomid.isEmpty()) { + // 群聊:从群消息中找最近一条 INTERNAL 角色的发送者 + chatType = "group"; + List msgs = archiveMessageMapper.selectGroupChatMessages( + roomid, get7DaysAgoStartTime(), getTodayEndTime()); + if (msgs != null && !msgs.isEmpty()) { + for (ArchiveMessage msg : msgs) { + if ("INTERNAL".equals(msg.getFromRole())) { + staffId = msg.getFromUser(); + break; + } + } + } + } else if (fromUser != null && !fromUser.isEmpty()) { + // 单聊:查该客户最近参与的所有消息,推断员工 + chatType = "single"; + List msgs = archiveMessageMapper.selectMessagesByFromUser( + fromUser, get7DaysAgoStartTime(), getTodayEndTime()); + if (msgs != null && !msgs.isEmpty()) { + // 取最近一条,fromUser 是客户则 from_user=客户/to_user=员工 或 from_user=员工/to_user=客户 + ArchiveMessage msg = msgs.get(0); + if (fromUser.equals(msg.getFromUser())) { + // 客户发的消息,to_user 里的是员工ID(JSON数组) + staffId = msg.getToUser(); + } else { + // 员工发的消息,from_user 就是员工ID + staffId = msg.getFromUser(); + } + // 清理 JSON 数组格式 ["userId"] + if (staffId != null) { + staffId = staffId.replaceAll("\\[|\\]|\"", "").trim(); + } + } + } + + if (staffId == null || staffId.isEmpty()) { + return Result.fail("未找到聊天记录,无法推断员工身份"); + } + + Map result = new HashMap<>(); + result.put("userId", staffId); + result.put("chatType", chatType); + result.put("fromUser", fromUser); + result.put("roomid", roomid); + return Result.success(result); + } + @PostMapping("/utterance") public Result> generateUtterance( @RequestParam(value = "fromUser", required = false) String fromUser, @@ -62,18 +135,25 @@ public class GenerationController { List messages; boolean isGroupChat = roomid != null && !roomid.isEmpty(); - long startTime = getTodayStartTime(); + // 查最近 7 天,避免今天没聊天就找不到记录 + long startTime = get7DaysAgoStartTime(); long endTime = getTodayEndTime(); if (isGroupChat) { messages = archiveMessageMapper.selectGroupChatMessages(roomid, startTime, endTime); } else { - if (fromUser == null || fromUser.isEmpty() || toUser == null || toUser.isEmpty()) { - log.warn("单聊模式需要fromUser和toUser参数"); + if (fromUser == null || fromUser.isEmpty()) { + log.warn("单聊模式需要fromUser参数"); return null; } - toUser = "[\""+toUser+"\"]"; - messages = archiveMessageMapper.selectSingleChatMessages(fromUser, toUser, startTime, endTime); + // toUser 为空或无效时,用兜底查询(根据 fromUser 查所有相关记录) + if (toUser == null || toUser.isEmpty() || "staff_001".equals(toUser)) { + log.warn("toUser无效,使用兜底查询, fromUser={}, toUser={}", fromUser, toUser); + messages = archiveMessageMapper.selectMessagesByFromUser(fromUser, startTime, endTime); + } else { + toUser = "[\""+toUser+"\"]"; + messages = archiveMessageMapper.selectSingleChatMessages(fromUser, toUser, startTime, endTime); + } } if (messages == null || messages.isEmpty()) { @@ -95,6 +175,120 @@ public class GenerationController { return java.time.LocalDate.now().plusDays(1).atStartOfDay(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli() - 1; } + private long get7DaysAgoStartTime() { + return java.time.LocalDate.now().minusDays(7).atStartOfDay(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli(); + } + + /** + * 流式生成多风格话术(SSE) + * 页面打开即自动推荐 3 条不同风格话术 + */ + @GetMapping(value = "/utterance-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter streamUtterance( + @RequestParam("fromUser") String fromUser, + @RequestParam("toUser") String toUser, + @RequestParam(value = "roomid", required = false) String roomid) { + + SseEmitter emitter = new SseEmitter(120000L); + + sseExecutor.execute(() -> { + try { + Map startEvent = new HashMap<>(); + startEvent.put("type", "start"); + startEvent.put("styles", STYLES); + emitter.send(SseEmitter.event().name("start").data(startEvent)); + + String chatContext = buildChatContext(fromUser, toUser, roomid); + if (chatContext == null || chatContext.isEmpty()) { + Map err = new HashMap<>(); + err.put("type", "error"); + err.put("message", "未找到聊天记录"); + emitter.send(SseEmitter.event().name("error").data(err)); + emitter.complete(); + return; + } + + String systemPrompt = promptEngine.buildSystemPrompt(chatContext); + String userPrompt = promptEngine.buildMultiStyleUserPrompt(); + + long startTime = System.currentTimeMillis(); + String generated = qianwenClient.generate(systemPrompt, userPrompt, 1200); + log.info("多风格话术生成完成, elapsed={}ms", System.currentTimeMillis() - startTime); + + List utterances = parseMultiStyle(generated); + if (utterances.size() < 3) { + while (utterances.size() < 3) { + utterances.add(generated != null ? generated : "生成失败,请稍后重试"); + } + } + + for (int i = 0; i < Math.min(3, utterances.size()); i++) { + String text = utterances.get(i); + String styleId = STYLES.get(i).get("id"); + + Map itemStart = new HashMap<>(); + itemStart.put("type", "item-start"); + itemStart.put("styleId", styleId); + itemStart.put("index", i); + emitter.send(SseEmitter.event().name("item-start").data(itemStart)); + + for (int j = 0; j < text.length(); j++) { + String ch = text.substring(j, j + 1); + Map chunk = new HashMap<>(); + chunk.put("type", "chunk"); + chunk.put("styleId", styleId); + chunk.put("text", ch); + chunk.put("index", i); + emitter.send(SseEmitter.event().name("chunk").data(chunk)); + Thread.sleep(25); + } + + Map complete = new HashMap<>(); + complete.put("type", "complete"); + complete.put("styleId", styleId); + complete.put("fullText", text); + complete.put("index", i); + emitter.send(SseEmitter.event().name("complete").data(complete)); + } + + Map done = new HashMap<>(); + done.put("type", "done"); + done.put("model", qianwenClient.getCurrentModel() != null + ? qianwenClient.getCurrentModel().getModelName() : "default"); + emitter.send(SseEmitter.event().name("done").data(done)); + emitter.complete(); + + } catch (Exception e) { + log.error("流式生成失败", e); + try { + Map error = new HashMap<>(); + error.put("type", "error"); + error.put("message", e.getMessage()); + emitter.send(SseEmitter.event().name("error").data(error)); + } catch (IOException ignored) {} + emitter.completeWithError(e); + } + }); + + return emitter; + } + + /** + * 解析多风格话术,按 "|||" 分隔 + */ + private List parseMultiStyle(String text) { + List result = new ArrayList<>(); + if (text == null || text.isEmpty()) return result; + String[] parts = text.split("\\|\\|\\|"); + for (String part : parts) { + String trimmed = part.trim(); + if (!trimmed.isEmpty() && trimmed.length() > 5) { + result.add(trimmed); + } + } + return result; + } + @PostMapping("/summary") public Result> generateSummary( @NotBlank(message = "context不能为空") @RequestParam("context") String context) { diff --git a/backend/generation-service/src/main/java/com/artedu/generation/mapper/ArchiveMessageMapper.java b/backend/generation-service/src/main/java/com/artedu/generation/mapper/ArchiveMessageMapper.java index 18f15c2..28ad6c9 100644 --- a/backend/generation-service/src/main/java/com/artedu/generation/mapper/ArchiveMessageMapper.java +++ b/backend/generation-service/src/main/java/com/artedu/generation/mapper/ArchiveMessageMapper.java @@ -36,4 +36,26 @@ public interface ArchiveMessageMapper extends BaseMapper { List selectGroupChatMessages(@Param("roomid") String roomid, @Param("startTime") Long startTime, @Param("endTime") Long endTime); + + /** + * 根据 fromUser 查询该用户参与的所有单聊记录(不限制 toUser,用于 toUser 未知时兜底) + */ + @Select("SELECT msgtime, created_at, from_user, to_user, from_role, content FROM archive_messages " + + "WHERE (from_user = #{fromUser} OR to_user = #{fromUser}) " + + "AND msgtime >= #{startTime} AND msgtime < #{endTime} AND msgtype='text' " + + "AND content IS NOT NULL AND content != '' " + + "ORDER BY msgtime DESC LIMIT 100") + List selectMessagesByFromUser(@Param("fromUser") String fromUser, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime); + + /** + * 根据 fromUser 查询最近一条消息的 to_user(推断员工身份) + * to_user 格式为 JSON 数组 ["userId"] + */ + @Select("SELECT to_user FROM archive_messages " + + "WHERE (from_user = #{fromUser} OR to_user LIKE CONCAT('%', #{fromUser}, '%')) " + + "AND msgtype='text' AND content IS NOT NULL AND content != '' " + + "ORDER BY msgtime DESC LIMIT 1") + String selectRecentToUserByFromUser(@Param("fromUser") String fromUser); } \ No newline at end of file diff --git a/backend/generation-service/src/main/java/com/artedu/generation/service/PromptEngine.java b/backend/generation-service/src/main/java/com/artedu/generation/service/PromptEngine.java index 92bd109..ecdb384 100644 --- a/backend/generation-service/src/main/java/com/artedu/generation/service/PromptEngine.java +++ b/backend/generation-service/src/main/java/com/artedu/generation/service/PromptEngine.java @@ -129,6 +129,30 @@ public class PromptEngine { return sb.toString(); } + /** + * 多风格话术生成的用户提示词 + * 要求 LLM 输出 3 条不同风格话术,用 ||| 分隔 + */ + public String buildMultiStyleUserPrompt() { + StringBuilder sb = new StringBuilder(); + sb.append("请根据上述对话历史,生成 3 条不同风格的推荐回复话术。\n\n"); + sb.append("风格要求(严格按顺序输出):\n"); + sb.append("第1条 - 亲和风格:温暖亲切,像朋友聊天,拉近距离\n"); + sb.append("第2条 - 专业风格:用数据、案例、专业分析增强说服力\n"); + sb.append("第3条 - 促单风格:制造紧迫感,强调限时优惠或名额有限\n\n"); + sb.append("输出要求(必须遵守):\n"); + sb.append("- 每条话术 80-150 字,简洁自然\n"); + sb.append("- 话术之间用「|||」分隔,不要加序号、标题或任何解释\n"); + sb.append("- 直接输出话术内容,不要有开场白、总结、备注\n\n"); + sb.append("输出格式示例:\n"); + sb.append("亲和风格的话术内容...\n"); + sb.append("|||\n"); + sb.append("专业风格的话术内容...\n"); + sb.append("|||\n"); + sb.append("促单风格的话术内容..."); + return sb.toString(); + } + public String buildSummaryPrompt(String context) { return "请对以下对话进行简要总结(50字以内),提炼学员核心需求和当前阶段:\n\n" + context; } diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/BailianClient.class b/backend/generation-service/target/classes/com/artedu/generation/client/BailianClient.class index ec3b8fd..ad5336d 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/BailianClient.class and b/backend/generation-service/target/classes/com/artedu/generation/client/BailianClient.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/DoubaoClient.class b/backend/generation-service/target/classes/com/artedu/generation/client/DoubaoClient.class index b4858b7..9313a9a 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/DoubaoClient.class and b/backend/generation-service/target/classes/com/artedu/generation/client/DoubaoClient.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/HunyuanClient.class b/backend/generation-service/target/classes/com/artedu/generation/client/HunyuanClient.class index 562f5e3..76bc3e3 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/HunyuanClient.class and b/backend/generation-service/target/classes/com/artedu/generation/client/HunyuanClient.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/LLMClient.class b/backend/generation-service/target/classes/com/artedu/generation/client/LLMClient.class index 9a2ee92..84326f1 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/LLMClient.class and b/backend/generation-service/target/classes/com/artedu/generation/client/LLMClient.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/client/LLMClientFactory.class b/backend/generation-service/target/classes/com/artedu/generation/client/LLMClientFactory.class index fe3ef45..6976415 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/client/LLMClientFactory.class and b/backend/generation-service/target/classes/com/artedu/generation/client/LLMClientFactory.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class b/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class index 760bcc2..b67cb92 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class and b/backend/generation-service/target/classes/com/artedu/generation/controller/GenerationController.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/controller/LlmModelController.class b/backend/generation-service/target/classes/com/artedu/generation/controller/LlmModelController.class index 99e0867..ce732ea 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/controller/LlmModelController.class and b/backend/generation-service/target/classes/com/artedu/generation/controller/LlmModelController.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/service/LlmModelService.class b/backend/generation-service/target/classes/com/artedu/generation/service/LlmModelService.class index d9ce4e4..d49d8fe 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/service/LlmModelService.class and b/backend/generation-service/target/classes/com/artedu/generation/service/LlmModelService.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class b/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class index a5b39e0..4dd7362 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class and b/backend/generation-service/target/classes/com/artedu/generation/service/PromptEngine.class differ diff --git a/backend/generation-service/target/classes/com/artedu/generation/service/QianwenClient.class b/backend/generation-service/target/classes/com/artedu/generation/service/QianwenClient.class index 7f7cf2c..84fa7e0 100644 Binary files a/backend/generation-service/target/classes/com/artedu/generation/service/QianwenClient.class and b/backend/generation-service/target/classes/com/artedu/generation/service/QianwenClient.class differ diff --git a/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar b/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar index 8079485..5e5987d 100644 Binary files a/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar and b/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar differ diff --git a/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar.original b/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar.original index d2ffdc8..6857d42 100644 Binary files a/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar.original and b/backend/generation-service/target/generation-service-1.0.0-SNAPSHOT.jar.original differ diff --git a/backend/generation-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/backend/generation-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index 28fd63f..a245ba0 100644 --- a/backend/generation-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/backend/generation-service/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -1,7 +1,9 @@ com\artedu\generation\service\QianwenClient.class com\artedu\generation\controller\GenerateRequest.class +com\artedu\generation\entity\ArchiveMessage.class com\artedu\generation\service\LlmModelService.class com\artedu\generation\client\DoubaoClient.class +com\artedu\generation\mapper\ArchiveMessageMapper.class com\artedu\generation\client\LLMClientFactory.class com\artedu\generation\controller\LlmModelController.class com\artedu\generation\mapper\LlmModelMapper.class diff --git a/backend/generation-service/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/backend/generation-service/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst index 96bad82..40c5bbf 100644 --- a/backend/generation-service/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ b/backend/generation-service/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -7,8 +7,10 @@ D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\controller\GenerateRequest.java D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\controller\GenerationController.java D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\controller\LlmModelController.java +D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\entity\ArchiveMessage.java D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\entity\LlmModel.java D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\GenerationServiceApplication.java +D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\mapper\ArchiveMessageMapper.java D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\mapper\LlmModelMapper.java D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\service\LlmModelService.java D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\service\PromptEngine.java diff --git a/frontend/sidebar/src/App.tsx b/frontend/sidebar/src/App.tsx index 289c6aa..fe1c2dd 100644 --- a/frontend/sidebar/src/App.tsx +++ b/frontend/sidebar/src/App.tsx @@ -1,86 +1,129 @@ -import { useEffect, useState } from 'react' -import { Tabs, Toast } from 'antd-mobile' +import { useEffect, useState, useCallback } from 'react' import ScriptRecommendStream from './components/ScriptRecommendStream' -import ConversationAnalysis from './components/ConversationAnalysis' import './styles.css' +const CORPID = 'wwd483c2fba24ae30a' +const AGENTID = '1000021' +const REDIRECT_URI = encodeURIComponent('https://ai.9artedu.com/sidebar/') + function App() { - const [activeTab, setActiveTab] = useState('recommend') - const [authState, setAuthState] = useState<'loading' | 'authed' | 'error'>('loading') + const [authState, setAuthState] = useState<'loading' | 'authed' | 'needLogin' | 'error'>('loading') const [userInfo, setUserInfo] = useState(null) const [currentCustomerId, setCurrentCustomerId] = useState('') - + const [chatContext, setChatContext] = useState<{ chatType?: string; chatId?: string }>({}) useEffect(() => { - initWeCom() + initAuth() }, []) - const initWeCom = async () => { + useEffect(() => { + if (authState === 'authed') { + initWxConfig() + } + }, [authState]) + + const noCacheFetch = useCallback(async (url: string, options?: RequestInit) => { + const sep = url.includes('?') ? '&' : '?' + const nocacheUrl = `${url}${sep}_t=${Date.now()}` + return fetch(nocacheUrl, { + ...options, + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + ...options?.headers + } + }) + }, []) + + // ========== 认证流程 ========== + const initAuth = async () => { try { const urlParams = new URLSearchParams(window.location.search) const code = urlParams.get('code') + // 1. URL 中有 code → 移除并登录 if (code) { - const res = await fetch(`/api/v1/auth/login?code=${encodeURIComponent(code)}`, { method: 'POST' }) - const data = await res.json() - if (data.code === 0 && data.data) { - localStorage.setItem('staff_token', data.data.token) - setUserInfo(data.data) - setAuthState('authed') - } else { - setAuthState('error') - Toast.show({ content: '登录失败: ' + (data.message || '未知错误'), icon: 'fail' }) - } - } else { - const token = localStorage.getItem('staff_token') - if (token) { - const res = await fetch('/api/v1/auth/info', { - headers: { Authorization: `Bearer ${token}` } - }) - const data = await res.json() - if (data.code === 0 && data.data) { - setUserInfo(data.data) - setAuthState('authed') - } else { - redirectToOAuth() - } - } else { - redirectToOAuth() - } + const cleanUrl = window.location.pathname + window.location.hash + window.history.replaceState({}, document.title, cleanUrl) + + const ok = await tryCodeLogin(code) + if (ok) return + // login 失败 → 可能是 code 过期/已被使用,显示登录按钮让用户手动重试 + setAuthState('needLogin') + return } + + // 2. 无 code → 检查 localStorage 缓存 + const cachedUserId = localStorage.getItem('staff_user_id') + const cachedUserName = localStorage.getItem('staff_user_name') + const cachedToken = localStorage.getItem('staff_token') + if (cachedUserId && cachedToken) { + setUserInfo({ + userId: cachedUserId, + userName: cachedUserName || cachedUserId, + avatar: localStorage.getItem('staff_avatar') || '', + corpId: CORPID, + role: localStorage.getItem('staff_role') || 'SEAT' + }) + setAuthState('authed') + console.log('使用缓存员工信息:', cachedUserId) + return + } + + // 3. 无 code 无缓存 → 显示登录按钮(iframe 中自动跳转可能被拦截) + setAuthState('needLogin') + } catch (e) { + console.error('initAuth error:', e) setAuthState('error') - Toast.show({ content: '初始化失败', icon: 'fail' }) } } - const redirectToOAuth = () => { - setAuthState('authed') - setUserInfo({ - userId: 'staff_001', - userName: '测试顾问', - corpId: 'wwd483c2fba24ae30a', - role: 'SEAT' - }) + // 用 code 调后端 login 接口 + const tryCodeLogin = async (code: string): Promise => { + try { + const res = await noCacheFetch(`/api/v1/auth/login?code=${encodeURIComponent(code)}`, { method: 'POST' }) + const data = await res.json() + if (data.code === 0 && data.data) { + const info = data.data + localStorage.setItem('staff_token', info.token) + localStorage.setItem('staff_user_id', info.userId) + localStorage.setItem('staff_user_name', info.userName || '') + localStorage.setItem('staff_avatar', info.avatar || '') + localStorage.setItem('staff_role', info.role || 'SEAT') + setUserInfo(info) + setAuthState('authed') + console.log('OAuth 登录成功:', info.userId, info.userName) + return true + } + console.error('OAuth login failed:', data.message) + } catch (e) { + console.error('Code login error:', e) + } + return false } + // 跳转企微 OAuth 授权页 + const redirectToWeComOAuth = () => { + const oauthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${CORPID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=snsapi_base&agentid=${AGENTID}&state=sidebar#wechat_redirect` + console.log('跳转企微 OAuth:', oauthUrl) + window.location.href = oauthUrl + } + + // ========== JS-SDK 初始化(认证完成后调用)========== const initWxConfig = async () => { const wx = (window as any).wx if (!wx || !wx.config) { - console.log('非企微环境,跳过JS-SDK初始化') + console.log('非企微环境,跳过 JS-SDK') return } - try { - const currentUrl = window.location.href.split('#')[0] + const currentUrl = window.location.href.split('#')[0] - // 1. 企业级 wx.config - const corpRes = await fetch(`/api/v1/auth/signature?url=${encodeURIComponent(currentUrl)}`) + try { + const corpRes = await noCacheFetch(`/api/v1/auth/signature?url=${encodeURIComponent(currentUrl)}`) const corpData = await corpRes.json() - if (corpData.code !== 0 || !corpData.data) { - console.error('获取JS-SDK企业签名失败') - return - } + if (corpData.code !== 0 || !corpData.data) return const cfg = corpData.data wx.config({ @@ -94,13 +137,9 @@ function App() { }) wx.ready(async () => { - console.log('JS-SDK wx.config 成功') - - // 2. 尝试应用级 agentConfig(获取客户联系权限) try { - const agentRes = await fetch(`/api/v1/auth/signature?type=agent&agentId=1000021&url=${encodeURIComponent(currentUrl)}`) + const agentRes = await noCacheFetch(`/api/v1/auth/signature?type=agent&agentId=${AGENTID}&url=${encodeURIComponent(currentUrl)}`) const agentData = await agentRes.json() - if (agentData.code === 0 && agentData.data) { const ac = agentData.data wx.agentConfig({ @@ -111,78 +150,140 @@ function App() { signature: ac.signature, jsApiList: ['sendChatMessage', 'getContext', 'getCurExternalContact'], success: () => { - console.log('✅ agentConfig 成功,已获取客户联系权限') - // agentConfig 成功后,获取当前客户ID + wx.invoke('getContext', {}, (ctx: any) => { + if (ctx.err_msg === 'getContext:ok') { + setChatContext({ chatType: ctx.chatType, chatId: ctx.chatId }) + if (ctx.chatType === 'single' && ctx.chatId) { + localStorage.setItem('current_customer_id', ctx.chatId) + setCurrentCustomerId(ctx.chatId) + } + } + }) wx.invoke('getCurExternalContact', {}, (res: any) => { if (res.err_msg === 'getCurExternalContact:ok' && res.userId) { - console.log('✅ 获取到客户ID:', res.userId) localStorage.setItem('current_customer_id', res.userId) setCurrentCustomerId(res.userId) - } else { - console.warn('getCurExternalContact 失败:', res.err_msg) - fallbackToCachedId() } }) }, - fail: (err: any) => { - console.warn('⚠️ agentConfig 失败(无客户联系权限):', err.err_msg || err) - fallbackToCachedId() - } + fail: (err: any) => console.warn('agentConfig fail:', err) }) - } else { - console.warn('⚠️ 获取应用签名失败:', agentData.message) - fallbackToCachedId() } } catch (e) { - console.warn('⚠️ agentConfig 流程异常:', e) - fallbackToCachedId() + console.error('agentConfig error:', e) } }) - wx.error((err: any) => { - console.error('JS-SDK wx.config 失败:', err) - fallbackToCachedId() - }) + wx.error((err: any) => console.error('wx.config error:', err)) } catch (e) { - console.error('初始化JS-SDK异常:', e) - fallbackToCachedId() + console.error('initWxConfig error:', e) } } - const fallbackToCachedId = () => { - const cachedId = localStorage.getItem('current_customer_id') - if (cachedId && cachedId !== '${userId}') { - setCurrentCustomerId(cachedId) - } - } - - useEffect(() => { - if (authState === 'authed') { - initWxConfig() - } - }, [authState]) - if (authState === 'loading') { return (
加载中...
-
正在连接企微
+
正在识别员工身份...
) } + if (authState === 'needLogin') { + return ( +
+
+
AI 坐席助手
+
请登录以获取员工身份
+ +
+ 登录后即可使用智能推荐和对话分析 +
+
+
+ ) + } + + if (authState === 'error') { + return ( +
+
+
⚠️ 登录失败
+
请关闭侧边栏后重新打开
+ +
+
+ ) + } + + // ========== 主界面(认证成功)========== return (
- - - - - - - - + {/* 顶部员工信息栏 */} +
+
+ {userInfo?.avatar ? ( + avatar + ) : ( +
+ {(userInfo?.userName || userInfo?.userId || '?').charAt(0)} +
+ )} +
+
+ {userInfo?.userName || userInfo?.userId || '未知员工'} +
+
+ {userInfo?.userId} +
+
+
+
+ AI 坐席助手 +
+
+ +
) } diff --git a/frontend/sidebar/src/components/ScriptRecommendStream.tsx b/frontend/sidebar/src/components/ScriptRecommendStream.tsx index 5782653..8b9f5ab 100644 --- a/frontend/sidebar/src/components/ScriptRecommendStream.tsx +++ b/frontend/sidebar/src/components/ScriptRecommendStream.tsx @@ -19,6 +19,7 @@ interface UtteranceItem { interface Props { userInfo?: any customerId?: string + chatContext?: { chatType?: string; chatId?: string } } const STYLES: StyleConfig[] = [ @@ -27,21 +28,24 @@ const STYLES: StyleConfig[] = [ { id: 'urgent', name: '促单风格', desc: '制造紧迫感,促成决策', color: '#fa8c16', bgColor: '#fff7e6' } ] -export default function ScriptRecommendStream({ userInfo, customerId: propCustomerId }: Props) { +export default function ScriptRecommendStream({ userInfo, customerId: propCustomerId, chatContext }: Props) { const [loading, setLoading] = useState(true) const [generating, setGenerating] = useState(false) const [customerMsg, setCustomerMsg] = useState('') const [profileTags, setProfileTags] = useState([]) const [utterances, setUtterances] = useState([]) - const [progress, setProgress] = useState(0) const [error, setError] = useState('') const sseRef = useRef(null) - const sessionId = localStorage.getItem('current_session_id') || `session_${Date.now()}` - const customerId = propCustomerId || localStorage.getItem('current_customer_id') || 'wx_001' - const staffId = userInfo?.userId || 'staff_001' + const customerId = propCustomerId || localStorage.getItem('current_customer_id') || '' + const staffId = userInfo?.userId || '' const corpId = userInfo?.corpId || 'wwd483c2fba24ae30a' + const chatType = chatContext?.chatType + const chatId = chatContext?.chatId + const isGroupChat = chatType === 'group' + const isSingleChat = chatType === 'single' + const closeSse = useCallback(() => { if (sseRef.current) { sseRef.current.close() @@ -77,102 +81,68 @@ export default function ScriptRecommendStream({ userInfo, customerId: propCustom const handleRegenerate = () => { closeSse() setUtterances([]) - setProgress(0) setError('') startGeneration() } + // 判断是否可以开始生成 + const canStartGeneration = useCallback(() => { + if (isGroupChat && chatId) return true + if (isSingleChat && customerId && staffId) return true + // 兼容:没有 chatContext 时(非企微/测试环境) + if (!chatType && customerId && staffId) return true + return false + }, [isGroupChat, isSingleChat, chatId, customerId, staffId, chatType]) + + // 核心:开始流式生成 const startGeneration = useCallback(async () => { - if (!customerMsg.trim()) { - setError('暂无学员消息') + if (!canStartGeneration()) { + setError('未获取到聊天信息') setLoading(false) return } setGenerating(true) setLoading(false) - setProgress(10) try { - const [profileRes, contextRes, recommendRes] = await Promise.all([ - fetch(`/api/v1/intent/profile/${encodeURIComponent(customerId)}?corpId=${corpId}`), - fetch(`/api/v1/conversations/${sessionId}/context`).catch(() => null), - fetch(`/api/v1/recommend/get?${new URLSearchParams({ - sessionId, customerId, staffId, corpId, - intentCode: 'INT-COURSE-01', - stage: 'STAGE-03', - studentType: '零基础', - message: customerMsg - }).toString()}`, { method: 'POST' }).catch(() => null) - ]) - - let customerProfileStr = '' - let tags: string[] = [] - const profileData = await profileRes.json() - if (profileData.code === 0 && profileData.data) { - const p = profileData.data - if (p.currentOccupation) { tags.push(p.currentOccupation) } - if (p.studentType) { tags.push(p.studentType) } - if (p.skillLevel) { tags.push(p.skillLevel) } - if (p.intentLevel) { tags.push(p.intentLevel) } - if (p.concernFocus) { tags.push(p.concernFocus) } - if (p.decisionStage) { tags.push(p.decisionStage) } - customerProfileStr = [ - p.currentOccupation ? `身份/职业:${p.currentOccupation}` : '', - p.studentType ? `学员类型:${p.studentType}` : '', - p.skillLevel ? `基础水平:${p.skillLevel}` : '', - p.intentLevel ? `意向度:${p.intentLevel}(${p.intentScore != null ? Math.round(p.intentScore) : 0}分)` : '', - p.concernFocus ? `关注方向:${p.concernFocus}` : '', - p.decisionStage ? `决策阶段:${p.decisionStage}` : '' - ].filter(Boolean).join('\n') - } - setProfileTags(tags) - setProgress(30) - - let conversationContextStr = '' - if (contextRes) { - const contextData = await contextRes.json() - if (contextData.code === 0 && contextData.data?.messages) { - const msgs = contextData.data.messages as Array<{ role: string; content: string }> - conversationContextStr = msgs.slice(-3).map(m => { - const roleName = m.role === 'student' ? '学员' : '顾问' - return `${roleName}:${m.content}` - }).join('\n') + // 单聊才获取画像标签 + if (!isGroupChat && customerId) { + const profileRes = await fetch(`/api/v1/intent/profile/${encodeURIComponent(customerId)}?corpId=${corpId}`) + const profileData = await profileRes.json() + if (profileData.code === 0 && profileData.data) { + const p = profileData.data + const tags: string[] = [] + if (p.currentOccupation) tags.push(p.currentOccupation) + if (p.studentType) tags.push(p.studentType) + if (p.skillLevel) tags.push(p.skillLevel) + if (p.intentLevel) tags.push(p.intentLevel) + if (p.concernFocus) tags.push(p.concernFocus) + if (p.decisionStage) tags.push(p.decisionStage) + setProfileTags(tags) } } - let referenceUtterancesStr = '' - if (recommendRes) { - const recData = await recommendRes.json() - if (recData.code === 0 && recData.data) { - const top3 = recData.data.slice(0, 3) - referenceUtterancesStr = top3.map((s: any, i: number) => `${i + 1}. "${s.contentText || s.content || s.title}"`).join('\n') - } + // 构建 SSE URL:群聊传 roomid,单聊传 fromUser+toUser + const params = new URLSearchParams() + if (isGroupChat && chatId) { + params.append('roomid', chatId) + } else if (customerId && staffId) { + params.append('fromUser', customerId) + params.append('toUser', staffId) } - setProgress(50) - - const params = new URLSearchParams({ - message: customerMsg, - intent: 'INT-COURSE-01', - stage: 'STAGE-03', - stageLabel: '方案呈现', - intentLabel: '课程咨询' - }) - if (customerProfileStr) params.append('customerProfile', customerProfileStr) - if (conversationContextStr) params.append('conversationContext', conversationContextStr) - if (referenceUtterancesStr) params.append('referenceUtterances', referenceUtterancesStr) const url = `/api/v1/generation/utterance-stream?${params.toString()}` + console.log('SSE connect:', url) const es = new EventSource(url) sseRef.current = es setUtterances(STYLES.map(s => ({ styleId: s.id, text: '', isComplete: false, isTyping: false }))) - // ===== 关键修复:用 addEventListener 接收命名事件 ===== es.addEventListener('start', (event: MessageEvent) => { try { const data = JSON.parse(event.data) - if (data.type === 'start') setProgress(60) + if (data.type === 'start') { /* 可处理 styles 信息 */ } } catch (e) { console.error('SSE start error:', e) } }) @@ -214,7 +184,6 @@ export default function ScriptRecommendStream({ userInfo, customerId: propCustom const data = JSON.parse(event.data) if (data.type === 'done') { setGenerating(false) - setProgress(100) es.close() } } catch (e) { console.error('SSE done error:', e) } @@ -236,48 +205,49 @@ export default function ScriptRecommendStream({ userInfo, customerId: propCustom setError('网络错误,请重试') setGenerating(false) } - }, [customerMsg, customerId, corpId, sessionId, staffId]) + }, [canStartGeneration, isGroupChat, customerId, staffId, chatId, corpId]) useEffect(() => { const init = async () => { setLoading(true) try { - const res = await fetch(`/api/v1/conversations/last-message?customerId=${customerId}&corpId=${corpId}`) - const data = await res.json() - if (data.code === 0 && data.data?.content) { - setCustomerMsg(data.data.content) - } else { - setLoading(false) - setError('暂无学员最新消息') + // 单聊获取最后一条消息用于展示(限定当前销售与学员的对话,避免串消息) + if (!isGroupChat && customerId && staffId) { + const res = await fetch(`/api/v1/conversations/last-message?customerId=${customerId}&staffId=${staffId}&corpId=${corpId}`) + const data = await res.json() + if (data.code === 0 && data.data?.content) { + setCustomerMsg(data.data.content) + } } } catch (e) { - console.error('初始化失败:', e) + console.error('获取学员消息失败:', e) + } + + if (canStartGeneration()) { + startGeneration() + } else { setLoading(false) - setError('获取学员消息失败') + setError('未获取到聊天上下文') } } init() return () => { closeSse() } - }, [customerId, corpId, closeSse]) - - useEffect(() => { - if (customerMsg.trim() && !generating && utterances.length === 0) { - startGeneration() - } - }, [customerMsg, generating, utterances.length, startGeneration]) + }, [customerId, staffId, corpId, chatContext, isGroupChat, closeSse, startGeneration, canStartGeneration]) if (loading && !generating && utterances.length === 0) { return (
-
正在分析学员消息...
+
+ {isGroupChat ? '正在分析群聊消息...' : '正在分析学员消息...'} +
) } return (
- {/* 学员消息 + 标签 */} + {/* 学员/群聊消息 + 标签 */}
- 💬 学员消息: + 💬 + {isGroupChat ? '群聊消息' : '学员消息'}: + {customerMsg || '暂无'}
{profileTags.length > 0 && ( @@ -298,8 +270,8 @@ export default function ScriptRecommendStream({ userInfo, customerId: propCustom )}
- {/* 生成进度 */} - {generating && progress < 100 && ( + {/* 生成中状态 */} + {generating && utterances.every(u => !u.isComplete) && (
-
-
- {progress < 60 ? '正在分析学员画像和对话历史...' : 'AI 正在生成推荐话术...'} -
-
-
-
-
+
AI 正在生成推荐话术...
)}