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
This commit is contained in:
@@ -478,6 +478,31 @@ export class ChatHandlerService {
|
||||
return 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送固定文本引导语(不经过 LLM,避免指令冲突导致 LLM 输出冗长内容)
|
||||
*/
|
||||
private async sendFixedHint(
|
||||
writer: StreamWriter,
|
||||
placeholderMessageId: string,
|
||||
text: string,
|
||||
): Promise<void> {
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -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={`
|
||||
|
||||
@@ -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 <SSAWorkspace />;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user