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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user