From 740ef8b526e20aadf0c0d50cb9f927fbe28b7d16 Mon Sep 17 00:00:00 2001 From: HaHafeng Date: Mon, 9 Mar 2026 08:38:02 +0800 Subject: [PATCH] fix(ssa): Fix Agent mode chat leaking plan/result details + navigation reset Backend (ChatHandlerService): - Replace LLM-generated hints with fixed-text hints for plan/code/result steps to prevent SystemPrompt intent instruction conflict causing verbose chat output - Add sendFixedHint() helper for deterministic SSE text delivery (no LLM call) - Cancel old plan_pending/code_pending executions when regenerating plan - Eliminates 3 unnecessary LLM calls per analysis cycle (faster response) Frontend (SSAModule): - Use location.key as useEffect dependency to ensure store reset on every navigation (fixes stale session when re-entering from other modules) - TopNavigation uses replace:true for active module re-click to avoid browser history clutter Tested: Agent mode plan/code/result hints now show brief fixed text in chat, detailed content exclusively in right workspace panel Made-with: Cursor --- .../ssa/services/ChatHandlerService.ts | 74 +++++++++++-------- .../src/framework/layout/TopNavigation.tsx | 4 +- frontend-v2/src/modules/ssa/index.tsx | 7 +- 3 files changed, 50 insertions(+), 35 deletions(-) diff --git a/backend/src/modules/ssa/services/ChatHandlerService.ts b/backend/src/modules/ssa/services/ChatHandlerService.ts index 22ac595f..bdc47187 100644 --- a/backend/src/modules/ssa/services/ChatHandlerService.ts +++ b/backend/src/modules/ssa/services/ChatHandlerService.ts @@ -478,6 +478,31 @@ export class ChatHandlerService { return 'other'; } + /** + * 发送固定文本引导语(不经过 LLM,避免指令冲突导致 LLM 输出冗长内容) + */ + private async sendFixedHint( + writer: StreamWriter, + placeholderMessageId: string, + text: string, + ): Promise { + const sseData = JSON.stringify({ + id: `chatcmpl-ssa-${Date.now()}`, + object: 'chat.completion.chunk', + choices: [{ delta: { content: text }, finish_reason: null }], + }); + writer.write(`data: ${sseData}\n\n`); + + const doneData = JSON.stringify({ + id: `chatcmpl-ssa-${Date.now()}`, + object: 'chat.completion.chunk', + choices: [{ delta: {}, finish_reason: 'stop' }], + }); + writer.write(`data: ${doneData}\n\n`); + + await conversationService.finalizeAssistantMessage(placeholderMessageId, text); + } + // ── Agent Step 1: 生成分析计划 → 等用户确认 ── private async agentGeneratePlan( @@ -491,6 +516,12 @@ export class ChatHandlerService { writer.write(`data: ${JSON.stringify({ type, ...data })}\n\n`); }; + // 取消该 session 下所有未完成的 pending 执行(防止旧计划干扰) + await (prisma as any).ssaAgentExecution.updateMany({ + where: { sessionId, status: { in: ['plan_pending', 'code_pending'] } }, + data: { status: 'error', errorMessage: '用户发起了新的分析请求' }, + }); + const execution = await (prisma as any).ssaAgentExecution.create({ data: { sessionId, query: userContent, status: 'planning' }, }); @@ -516,17 +547,9 @@ export class ChatHandlerService { planText: plan.rawText, }); - // 简短视线牵引语(方案 B:左侧只输出简短提示,详细内容在右侧工作区) - const hint = [ - `[系统指令——严格遵守] 分析计划已生成(${plan.steps.length} 步),标题「${plan.title}」。`, - '你只需回复 1-2 句话,引导用户去右侧工作区查看。', - '示例回复:「我已为您拟定了详细的分析计划,请在右侧工作区核对并点击确认。」', - '【铁律】禁止在对话中重复计划内容、禁止编造任何数值。回复不超过 2 句话。', - ].join('\n'); - - const msgs = await conversationService.buildContext(sessionId, conversationId, 'analyze', hint); - const sr = await conversationService.streamToSSE(msgs, writer, { temperature: 0.3, maxTokens: 150 }); - await conversationService.finalizeAssistantMessage(placeholderMessageId, sr.content, sr.thinking, sr.tokens); + // 固定文本引导语(不经过 LLM,彻底避免 SystemPrompt 的 intent 指令冲突) + const hintText = `我已为您拟定了分析计划(${plan.steps.length} 步),👉 请在右侧工作区核对并点击确认。`; + await this.sendFixedHint(writer, placeholderMessageId, hintText); return { messageId: placeholderMessageId, intent: 'analyze', success: true }; } @@ -571,17 +594,9 @@ export class ChatHandlerService { explanation: generated.explanation, }); - // 简短视线牵引语 - const hint = [ - `[系统指令——严格遵守] R 代码已生成(${generated.code.split('\n').length} 行),使用 ${generated.requiredPackages.join(', ') || '基础包'}。`, - '你只需回复 1-2 句话,引导用户去右侧工作区核对代码并点击执行。', - '示例回复:「R 代码已生成,请在右侧工作区核对代码并点击 "确认并执行"。」', - '【铁律】禁止在对话中贴代码、禁止编造结果。回复不超过 2 句话。', - ].join('\n'); - - const msgs = await conversationService.buildContext(sessionId, conversationId, 'analyze', hint); - const sr = await conversationService.streamToSSE(msgs, writer, { temperature: 0.3, maxTokens: 150 }); - await conversationService.finalizeAssistantMessage(placeholderMessageId, sr.content, sr.thinking, sr.tokens); + // 固定文本引导语 + const hintText = `R 代码已生成(${generated.code.split('\n').length} 行),👉 请在右侧工作区核对代码并点击「确认并执行」。`; + await this.sendFixedHint(writer, placeholderMessageId, hintText); return { messageId: placeholderMessageId, intent: 'analyze', success: true }; } @@ -639,16 +654,11 @@ export class ChatHandlerService { durationMs, }); - // 流式总结结果 - const hint = [ - `[系统指令] R 代码执行完成(${durationMs}ms),生成 ${(execResult.reportBlocks || []).length} 个报告模块。`, - '请简要解释结果的统计学意义。', - '【禁止】不要编造数值(工作区已展示完整结果)。', - ].join('\n'); - - const msgs = await conversationService.buildContext(sessionId, conversationId, 'analyze', hint); - const sr = await conversationService.streamToSSE(msgs, writer, { temperature: 0.5, maxTokens: 800 }); - await conversationService.finalizeAssistantMessage(placeholderMessageId, sr.content, sr.thinking, sr.tokens); + // 固定文本引导语(结果解读应在右侧工作区,不在对话区) + const blockCount = (execResult.reportBlocks || []).length; + const seconds = (durationMs / 1000).toFixed(1); + const hintText = `✅ 分析完成(${seconds}s),共生成 ${blockCount} 个结果模块。👉 请在右侧工作区查看完整结果和图表。`; + await this.sendFixedHint(writer, placeholderMessageId, hintText); return { messageId: placeholderMessageId, intent: 'analyze', success: true }; } diff --git a/frontend-v2/src/framework/layout/TopNavigation.tsx b/frontend-v2/src/framework/layout/TopNavigation.tsx index 1aa252de..7399d24e 100644 --- a/frontend-v2/src/framework/layout/TopNavigation.tsx +++ b/frontend-v2/src/framework/layout/TopNavigation.tsx @@ -124,7 +124,9 @@ const TopNavigation = () => { if (module.isExternal && module.externalUrl) { window.open(module.externalUrl, '_blank', 'noopener'); } else { - navigate(module.path); + // 重复点击已激活模块时使用 replace 避免浏览器历史堆积, + // 同时确保 location.key 变化触发模块重置 + navigate(module.path, isActive ? { replace: true } : undefined); } }} className={` diff --git a/frontend-v2/src/modules/ssa/index.tsx b/frontend-v2/src/modules/ssa/index.tsx index 37fc0dbd..4a20e736 100644 --- a/frontend-v2/src/modules/ssa/index.tsx +++ b/frontend-v2/src/modules/ssa/index.tsx @@ -7,6 +7,7 @@ * - 右侧工作区(动态 60% 宽度) */ import React, { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; import { useSSAStore } from './stores/ssaStore'; import SSAWorkspace from './SSAWorkspace'; import './styles/ssa.css'; @@ -14,11 +15,13 @@ import './styles/ssa-workspace.css'; const SSAModule: React.FC = () => { const { reset } = useSSAStore(); + const location = useLocation(); - // 组件挂载时重置 store,确保不同用户看到独立的状态 + // location.key 在每次 navigate() 调用时都会变化(即使 URL 相同), + // 确保从其他模块返回或重复点击导航栏都能重置到全新状态 useEffect(() => { reset(); - }, [reset]); + }, [location.key, reset]); return ; };