feat(ssa): Agent channel UX optimization (Solution B) + Plan-and-Execute architecture design

SSA Agent channel improvements (12 code files, +931/-203 lines):
- Solution B: left/right separation of concerns (gaze guiding + state mutex + time-travel)
- JWT token refresh mechanism (ensureFreshToken) to fix HTTP 401 during pipeline
- Code truncation fix: LLM maxTokens 4000->8000 + CSS max-height 60vh
- Retry streaming code generation with generateCodeStream()
- R Docker structured errors: 20+ pattern matching + format_agent_error + line extraction
- Prompt iron rules: strict output format in CoderAgent System Prompt
- parseCode robustness: XML/Markdown/inference 3-tier matching + length validation
- consoleOutput type defense: handle both array and scalar from R Docker unboxedJSON
- Agent progress bar sync: derive phase from agentExecution.status
- Export report / view code buttons restored for Agent mode
- ExecutingProgress component: real-time timer + dynamic tips + step pulse animation

Architecture design (3 review reports):
- Plan-and-Execute step-by-step execution architecture approved
- Code accumulation strategy (R Docker stays stateless)
- 5 engineering guardrails: XML tags, AST pre-check, defensive prompts, high-fidelity schema, error classification circuit breaker

Docs: update SSA module status v4.1, system status v6.7, deployment changelist
Made-with: Cursor
This commit is contained in:
2026-03-07 22:32:32 +08:00
parent 87655ea7e6
commit 52989cd03f
18 changed files with 1334 additions and 230 deletions

View File

@@ -14,7 +14,7 @@
*/
import { useState, useCallback, useRef } from 'react';
import { getAccessToken } from '@/framework/auth/api';
import { getAccessToken, isTokenExpired, refreshAccessToken } from '@/framework/auth/api';
import { useSSAStore } from '../stores/ssaStore';
import type { AskUserEventData, AskUserResponseData } from '../components/AskUserCard';
import type { WorkflowPlan } from '../types';
@@ -54,6 +54,8 @@ interface OpenAIChunk {
}>;
}
export type AgentActionType = 'confirm_plan' | 'confirm_code' | 'cancel';
export interface UseSSAChatReturn {
chatMessages: ChatMessage[];
isGenerating: boolean;
@@ -65,6 +67,7 @@ export interface UseSSAChatReturn {
pendingQuestion: AskUserEventData | null;
pendingPlanConfirm: { workflowId: string } | null;
sendChatMessage: (sessionId: string, content: string, metadata?: Record<string, any>) => Promise<void>;
executeAgentAction: (sessionId: string, action: AgentActionType) => Promise<void>;
respondToQuestion: (sessionId: string, response: AskUserResponseData) => Promise<void>;
skipQuestion: (sessionId: string, questionId: string) => Promise<void>;
loadHistory: (sessionId: string) => Promise<void>;
@@ -89,6 +92,17 @@ export function useSSAChat(): UseSSAChatReturn {
const abortRef = useRef<AbortController | null>(null);
const ensureFreshToken = useCallback(async (): Promise<string> => {
if (isTokenExpired()) {
try {
await refreshAccessToken();
} catch {
throw new Error('登录已过期,请重新登录');
}
}
return getAccessToken() || '';
}, []);
const abort = useCallback(() => {
if (abortRef.current) {
abortRef.current.abort();
@@ -113,7 +127,7 @@ export function useSSAChat(): UseSSAChatReturn {
*/
const loadHistory = useCallback(async (sessionId: string) => {
try {
const token = getAccessToken();
const token = await ensureFreshToken();
const resp = await fetch(`/api/v1/ssa/sessions/${sessionId}/chat/history`, {
headers: { Authorization: `Bearer ${token}` },
});
@@ -151,13 +165,7 @@ export function useSSAChat(): UseSSAChatReturn {
setIntentMeta(null);
setPendingQuestion(null);
const userMsg: ChatMessage = {
id: crypto.randomUUID(),
role: 'user',
content,
status: 'complete',
createdAt: new Date().toISOString(),
};
const isAgentAction = !!metadata?.agentAction;
const assistantMsgId = crypto.randomUUID();
const assistantPlaceholder: ChatMessage = {
@@ -168,14 +176,26 @@ export function useSSAChat(): UseSSAChatReturn {
createdAt: new Date().toISOString(),
};
setChatMessages(prev => [...prev, userMsg, assistantPlaceholder]);
if (isAgentAction) {
// 右侧工作区操作:不添加用户气泡,只添加 AI 占位
setChatMessages(prev => [...prev, assistantPlaceholder]);
} else {
const userMsg: ChatMessage = {
id: crypto.randomUUID(),
role: 'user',
content,
status: 'complete',
createdAt: new Date().toISOString(),
};
setChatMessages(prev => [...prev, userMsg, assistantPlaceholder]);
}
abortRef.current = new AbortController();
let fullContent = '';
let fullThinking = '';
try {
const token = getAccessToken();
const token = await ensureFreshToken();
const response = await fetch(`/api/v1/ssa/sessions/${sessionId}/chat`, {
method: 'POST',
headers: {
@@ -260,10 +280,10 @@ export function useSSAChat(): UseSSAChatReturn {
// ── Agent 通道 SSE 事件 ──
if (parsed.type === 'agent_planning') {
const { setAgentExecution, setWorkspaceOpen, setActivePane } = useSSAStore.getState();
setAgentExecution({
const { pushAgentExecution, setWorkspaceOpen, setActivePane } = useSSAStore.getState();
pushAgentExecution({
id: parsed.executionId || crypto.randomUUID(),
sessionId: '',
sessionId: sessionId,
query: content,
retryCount: 0,
status: 'planning',
@@ -321,10 +341,16 @@ export function useSSAChat(): UseSSAChatReturn {
if (parsed.type === 'code_error') {
const { updateAgentExecution } = useSSAStore.getState();
updateAgentExecution({
status: 'error',
errorMessage: parsed.message,
});
if (parsed.willRetry) {
updateAgentExecution({
errorMessage: parsed.message,
});
} else {
updateAgentExecution({
status: 'error',
errorMessage: parsed.message,
});
}
continue;
}
@@ -333,8 +359,7 @@ export function useSSAChat(): UseSSAChatReturn {
updateAgentExecution({
status: 'coding',
retryCount: parsed.retryCount || 0,
generatedCode: parsed.code,
errorMessage: undefined,
partialCode: undefined,
});
continue;
}
@@ -401,6 +426,36 @@ export function useSSAChat(): UseSSAChatReturn {
}
}, []);
/**
* 右侧工作区按钮触发 Agent 操作(方案 B
* 1. 在左侧对话追加审计轨迹消息(系统风格)
* 2. 调用后端 API 触发下一步
* 3. 后端返回 SSE 事件更新右侧工作区 + 左侧简短 LLM 回复
*/
const executeAgentAction = useCallback(async (sessionId: string, action: AgentActionType) => {
const AUDIT_MESSAGES: Record<AgentActionType, string> = {
confirm_plan: '✅ 方案已确认,正在生成 R 代码...',
confirm_code: '✅ 代码已确认R 引擎正在执行...',
cancel: '❌ 已取消当前分析',
};
const auditContent = AUDIT_MESSAGES[action];
// 1. 追加审计轨迹消息(系统风格,不是用户消息)
const auditMsgId = crypto.randomUUID();
setChatMessages(prev => [...prev, {
id: auditMsgId,
role: 'assistant' as const,
content: auditContent,
status: 'complete' as const,
intent: 'system' as any,
createdAt: new Date().toISOString(),
}]);
// 2. 通过 sendChatMessage 触发后端(带 agentAction metadata
await sendChatMessage(sessionId, auditContent, { agentAction: action });
}, [sendChatMessage]);
/**
* 响应 ask_user 卡片Phase III
* 将 value 解析为中文 label 用于显示
@@ -448,6 +503,7 @@ export function useSSAChat(): UseSSAChatReturn {
pendingQuestion,
pendingPlanConfirm,
sendChatMessage,
executeAgentAction,
respondToQuestion,
skipQuestion,
loadHistory,