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';
|
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: 生成分析计划 → 等用户确认 ──
|
// ── Agent Step 1: 生成分析计划 → 等用户确认 ──
|
||||||
|
|
||||||
private async agentGeneratePlan(
|
private async agentGeneratePlan(
|
||||||
@@ -491,6 +516,12 @@ export class ChatHandlerService {
|
|||||||
writer.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
|
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({
|
const execution = await (prisma as any).ssaAgentExecution.create({
|
||||||
data: { sessionId, query: userContent, status: 'planning' },
|
data: { sessionId, query: userContent, status: 'planning' },
|
||||||
});
|
});
|
||||||
@@ -516,17 +547,9 @@ export class ChatHandlerService {
|
|||||||
planText: plan.rawText,
|
planText: plan.rawText,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 简短视线牵引语(方案 B:左侧只输出简短提示,详细内容在右侧工作区)
|
// 固定文本引导语(不经过 LLM,彻底避免 SystemPrompt 的 intent 指令冲突)
|
||||||
const hint = [
|
const hintText = `我已为您拟定了分析计划(${plan.steps.length} 步),👉 请在右侧工作区核对并点击确认。`;
|
||||||
`[系统指令——严格遵守] 分析计划已生成(${plan.steps.length} 步),标题「${plan.title}」。`,
|
await this.sendFixedHint(writer, placeholderMessageId, hintText);
|
||||||
'你只需回复 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);
|
|
||||||
|
|
||||||
return { messageId: placeholderMessageId, intent: 'analyze', success: true };
|
return { messageId: placeholderMessageId, intent: 'analyze', success: true };
|
||||||
}
|
}
|
||||||
@@ -571,17 +594,9 @@ export class ChatHandlerService {
|
|||||||
explanation: generated.explanation,
|
explanation: generated.explanation,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 简短视线牵引语
|
// 固定文本引导语
|
||||||
const hint = [
|
const hintText = `R 代码已生成(${generated.code.split('\n').length} 行),👉 请在右侧工作区核对代码并点击「确认并执行」。`;
|
||||||
`[系统指令——严格遵守] R 代码已生成(${generated.code.split('\n').length} 行),使用 ${generated.requiredPackages.join(', ') || '基础包'}。`,
|
await this.sendFixedHint(writer, placeholderMessageId, hintText);
|
||||||
'你只需回复 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);
|
|
||||||
|
|
||||||
return { messageId: placeholderMessageId, intent: 'analyze', success: true };
|
return { messageId: placeholderMessageId, intent: 'analyze', success: true };
|
||||||
}
|
}
|
||||||
@@ -639,16 +654,11 @@ export class ChatHandlerService {
|
|||||||
durationMs,
|
durationMs,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 流式总结结果
|
// 固定文本引导语(结果解读应在右侧工作区,不在对话区)
|
||||||
const hint = [
|
const blockCount = (execResult.reportBlocks || []).length;
|
||||||
`[系统指令] R 代码执行完成(${durationMs}ms),生成 ${(execResult.reportBlocks || []).length} 个报告模块。`,
|
const seconds = (durationMs / 1000).toFixed(1);
|
||||||
'请简要解释结果的统计学意义。',
|
const hintText = `✅ 分析完成(${seconds}s),共生成 ${blockCount} 个结果模块。👉 请在右侧工作区查看完整结果和图表。`;
|
||||||
'【禁止】不要编造数值(工作区已展示完整结果)。',
|
await this.sendFixedHint(writer, placeholderMessageId, hintText);
|
||||||
].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);
|
|
||||||
|
|
||||||
return { messageId: placeholderMessageId, intent: 'analyze', success: true };
|
return { messageId: placeholderMessageId, intent: 'analyze', success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,9 @@ const TopNavigation = () => {
|
|||||||
if (module.isExternal && module.externalUrl) {
|
if (module.isExternal && module.externalUrl) {
|
||||||
window.open(module.externalUrl, '_blank', 'noopener');
|
window.open(module.externalUrl, '_blank', 'noopener');
|
||||||
} else {
|
} else {
|
||||||
navigate(module.path);
|
// 重复点击已激活模块时使用 replace 避免浏览器历史堆积,
|
||||||
|
// 同时确保 location.key 变化触发模块重置
|
||||||
|
navigate(module.path, isActive ? { replace: true } : undefined);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`
|
className={`
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* - 右侧工作区(动态 60% 宽度)
|
* - 右侧工作区(动态 60% 宽度)
|
||||||
*/
|
*/
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useSSAStore } from './stores/ssaStore';
|
import { useSSAStore } from './stores/ssaStore';
|
||||||
import SSAWorkspace from './SSAWorkspace';
|
import SSAWorkspace from './SSAWorkspace';
|
||||||
import './styles/ssa.css';
|
import './styles/ssa.css';
|
||||||
@@ -14,11 +15,13 @@ import './styles/ssa-workspace.css';
|
|||||||
|
|
||||||
const SSAModule: React.FC = () => {
|
const SSAModule: React.FC = () => {
|
||||||
const { reset } = useSSAStore();
|
const { reset } = useSSAStore();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
// 组件挂载时重置 store,确保不同用户看到独立的状态
|
// location.key 在每次 navigate() 调用时都会变化(即使 URL 相同),
|
||||||
|
// 确保从其他模块返回或重复点击导航栏都能重置到全新状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset();
|
reset();
|
||||||
}, [reset]);
|
}, [location.key, reset]);
|
||||||
|
|
||||||
return <SSAWorkspace />;
|
return <SSAWorkspace />;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user