feat(ssa): finalize strict stepwise agent execution flow
Align Agent mode to strict stepwise generation and execution, add deterministic and safety hardening, and sync deployment/module documentation for Phase 5A.5/5B/5C rollout. - implement strict stepwise execution path and dependency short-circuiting - persist step-level errors/results and stream step_* progress events - add agent plan params patch route and schema/migration support - improve R sanitizer/security checks and step result rendering in workspace - update SSA module guide and deployment change checklist Made-with: Cursor
This commit is contained in:
@@ -15,13 +15,28 @@ import {
|
||||
FileText,
|
||||
Play,
|
||||
Ban,
|
||||
Save,
|
||||
} from 'lucide-react';
|
||||
import { useSSAStore } from '../stores/ssaStore';
|
||||
import type { AgentExecutionStatus } from '../types';
|
||||
import type { AgentExecutionStatus, AgentStepStatus, DataOverviewColumn, VariableDictEntry } from '../types';
|
||||
import apiClient from '@/common/api/axios';
|
||||
import {
|
||||
MultiVarTags,
|
||||
PARAM_LABELS,
|
||||
SingleVarSelect,
|
||||
TOOL_CONSTRAINTS,
|
||||
SINGLE_VAR_KEYS,
|
||||
MULTI_VAR_KEYS,
|
||||
checkMismatch,
|
||||
type VarInfo,
|
||||
} from './WorkflowTimeline';
|
||||
import { DynamicReport } from './DynamicReport';
|
||||
|
||||
export interface AgentCodePanelProps {
|
||||
onAction?: (action: 'confirm_plan' | 'confirm_code' | 'cancel') => void;
|
||||
actionLoading?: boolean;
|
||||
variableDictionary?: VariableDictEntry[];
|
||||
dataOverviewColumns?: DataOverviewColumn[];
|
||||
}
|
||||
|
||||
const STATUS_LABEL: Record<AgentExecutionStatus, string> = {
|
||||
@@ -49,8 +64,24 @@ const StatusBadge: React.FC<{ status: AgentExecutionStatus }> = ({ status }) =>
|
||||
);
|
||||
};
|
||||
|
||||
export const AgentCodePanel: React.FC<AgentCodePanelProps> = ({ onAction, actionLoading }) => {
|
||||
const { agentExecution, executionMode } = useSSAStore();
|
||||
const STEP_STATUS_LABEL: Record<AgentStepStatus, string> = {
|
||||
pending: '待执行',
|
||||
coding: '生成代码中',
|
||||
executing: '执行中',
|
||||
completed: '已完成',
|
||||
error: '失败',
|
||||
skipped: '已跳过',
|
||||
};
|
||||
|
||||
export const AgentCodePanel: React.FC<AgentCodePanelProps> = ({
|
||||
onAction,
|
||||
actionLoading,
|
||||
variableDictionary = [],
|
||||
dataOverviewColumns = [],
|
||||
}) => {
|
||||
const { agentExecution, executionMode, addToast } = useSSAStore();
|
||||
const [paramDrafts, setParamDrafts] = useState<Record<number, Record<string, unknown>>>({});
|
||||
const [savingParams, setSavingParams] = useState(false);
|
||||
|
||||
if (executionMode !== 'agent') return null;
|
||||
|
||||
@@ -69,10 +100,28 @@ export const AgentCodePanel: React.FC<AgentCodePanelProps> = ({ onAction, action
|
||||
);
|
||||
}
|
||||
|
||||
const { status, planText, planSteps: rawPlanSteps, generatedCode, partialCode, errorMessage, retryCount, durationMs } = agentExecution;
|
||||
const {
|
||||
status,
|
||||
planText,
|
||||
planSteps: rawPlanSteps,
|
||||
generatedCode,
|
||||
partialCode,
|
||||
errorMessage,
|
||||
retryCount,
|
||||
durationMs,
|
||||
stepResults,
|
||||
currentStep,
|
||||
} = agentExecution;
|
||||
|
||||
// 防御性:从 planText JSON 解析步骤(支持 steps / plan.steps),绝不展示原始 JSON
|
||||
const planSteps = React.useMemo(() => {
|
||||
const planSteps = React.useMemo<Array<{
|
||||
order: number;
|
||||
method: string;
|
||||
description: string;
|
||||
rationale?: string;
|
||||
toolCode?: string;
|
||||
params?: Record<string, unknown>;
|
||||
}> | undefined>(() => {
|
||||
if (rawPlanSteps && rawPlanSteps.length > 0) return rawPlanSteps;
|
||||
if (!planText) return undefined;
|
||||
|
||||
@@ -98,6 +147,8 @@ export const AgentCodePanel: React.FC<AgentCodePanelProps> = ({ onAction, action
|
||||
method: s.method ?? '',
|
||||
description: s.description ?? '',
|
||||
rationale: s.rationale,
|
||||
toolCode: s.toolCode || s.tool_code,
|
||||
params: s.params,
|
||||
}));
|
||||
}
|
||||
return undefined;
|
||||
@@ -129,6 +180,99 @@ export const AgentCodePanel: React.FC<AgentCodePanelProps> = ({ onAction, action
|
||||
|
||||
const isStreamingCode = status === 'coding' && !!partialCode;
|
||||
const displayCode = isStreamingCode ? partialCode : (generatedCode || partialCode);
|
||||
const varsMap = React.useMemo(() => {
|
||||
const map = new Map<string, VarInfo>();
|
||||
for (const v of variableDictionary) {
|
||||
const col = dataOverviewColumns.find(c => c.name === v.name);
|
||||
map.set(v.name, {
|
||||
name: v.name,
|
||||
type: v.confirmedType || v.inferredType,
|
||||
totalLevels: col?.totalLevels,
|
||||
});
|
||||
}
|
||||
for (const col of dataOverviewColumns) {
|
||||
if (!map.has(col.name)) {
|
||||
map.set(col.name, {
|
||||
name: col.name,
|
||||
type: col.type,
|
||||
totalLevels: col.totalLevels,
|
||||
});
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [variableDictionary, dataOverviewColumns]);
|
||||
|
||||
const allVars = React.useMemo(() => {
|
||||
if (variableDictionary.length > 0) {
|
||||
return variableDictionary
|
||||
.filter(v => !v.isIdLike)
|
||||
.map(v => {
|
||||
const col = dataOverviewColumns.find(c => c.name === v.name);
|
||||
return {
|
||||
name: v.name,
|
||||
type: v.confirmedType || v.inferredType,
|
||||
totalLevels: col?.totalLevels,
|
||||
};
|
||||
});
|
||||
}
|
||||
return dataOverviewColumns
|
||||
.filter(c => !c.isIdLike)
|
||||
.map(c => ({ name: c.name, type: c.type, totalLevels: c.totalLevels }));
|
||||
}, [variableDictionary, dataOverviewColumns]);
|
||||
|
||||
const updateDraft = (stepOrder: number, key: string, value: unknown) => {
|
||||
setParamDrafts(prev => ({
|
||||
...prev,
|
||||
[stepOrder]: {
|
||||
...(prev[stepOrder] || {}),
|
||||
[key]: value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const hasDrafts = Object.keys(paramDrafts).length > 0;
|
||||
|
||||
const savePlanParams = async () => {
|
||||
if (!agentExecution?.id || !hasDrafts) return;
|
||||
const payload = {
|
||||
steps: Object.entries(paramDrafts).map(([stepOrder, params]) => ({
|
||||
stepOrder: Number(stepOrder),
|
||||
params,
|
||||
})),
|
||||
};
|
||||
setSavingParams(true);
|
||||
try {
|
||||
const resp = await apiClient.patch(`/api/v1/ssa/agent-executions/${agentExecution.id}/plan-params`, payload);
|
||||
const nextReview = resp?.data?.reviewResult;
|
||||
const nextSteps = Array.isArray(nextReview?.steps)
|
||||
? nextReview.steps.map((s: any) => ({
|
||||
order: s.order ?? 0,
|
||||
method: s.method ?? '',
|
||||
description: s.description ?? '',
|
||||
rationale: s.rationale,
|
||||
toolCode: s.toolCode || s.tool_code,
|
||||
params: s.params,
|
||||
}))
|
||||
: planSteps;
|
||||
useSSAStore.getState().updateAgentExecution({
|
||||
planSteps: nextSteps,
|
||||
});
|
||||
setParamDrafts({});
|
||||
addToast('变量参数已保存', 'success');
|
||||
} catch (err: any) {
|
||||
addToast(err?.response?.data?.error || err?.message || '保存变量参数失败', 'error');
|
||||
throw err;
|
||||
} finally {
|
||||
setSavingParams(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmPlan = async () => {
|
||||
if (hasDrafts) {
|
||||
await savePlanParams();
|
||||
}
|
||||
onAction?.('confirm_plan');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="agent-code-panel">
|
||||
@@ -186,6 +330,46 @@ export const AgentCodePanel: React.FC<AgentCodePanelProps> = ({ onAction, action
|
||||
<span className="step-method">{s.method}</span>
|
||||
<span className="step-desc">{s.description}</span>
|
||||
{s.rationale && <span className="step-rationale">{s.rationale}</span>}
|
||||
{status === 'plan_pending' && s.params && Object.keys(s.params).length > 0 && (
|
||||
<div style={{ marginTop: 8, display: 'grid', gap: 8 }}>
|
||||
{Object.entries(s.params).map(([k, raw]) => {
|
||||
const toolCode = s.toolCode || '';
|
||||
const rule = TOOL_CONSTRAINTS[toolCode]?.[k];
|
||||
const edited = (paramDrafts[s.order] || {})[k];
|
||||
const value = edited !== undefined ? edited : raw;
|
||||
const isSingle = SINGLE_VAR_KEYS.has(k);
|
||||
const isMulti = MULTI_VAR_KEYS.has(k);
|
||||
const mismatch = rule && typeof value === 'string'
|
||||
? checkMismatch(value, rule, varsMap)
|
||||
: null;
|
||||
return (
|
||||
<div key={k}>
|
||||
<div className="step-rationale" style={{ marginBottom: 4 }}>{PARAM_LABELS[k] || k}</div>
|
||||
{isSingle ? (
|
||||
<SingleVarSelect
|
||||
value={typeof value === 'string' ? value : null}
|
||||
constraint={rule}
|
||||
varsMap={varsMap}
|
||||
allVars={allVars}
|
||||
onChange={(v) => updateDraft(s.order, k, v)}
|
||||
/>
|
||||
) : isMulti ? (
|
||||
<MultiVarTags
|
||||
values={Array.isArray(value) ? value.map(String) : []}
|
||||
constraint={rule}
|
||||
varsMap={varsMap}
|
||||
allVars={allVars}
|
||||
onChange={(v) => updateDraft(s.order, k, v)}
|
||||
/>
|
||||
) : (
|
||||
<span className="step-rationale">{String(value ?? '—')}</span>
|
||||
)}
|
||||
{mismatch && <span className="step-rationale" style={{ color: '#dc2626' }}>{mismatch}</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -200,10 +384,21 @@ export const AgentCodePanel: React.FC<AgentCodePanelProps> = ({ onAction, action
|
||||
{/* 计划确认操作按钮 */}
|
||||
{status === 'plan_pending' && onAction && (
|
||||
<div className="agent-action-bar">
|
||||
{hasDrafts && (
|
||||
<button
|
||||
className="agent-action-btn secondary"
|
||||
onClick={savePlanParams}
|
||||
disabled={actionLoading || savingParams}
|
||||
title="保存变量修改"
|
||||
>
|
||||
{savingParams ? <Loader2 size={14} className="spin" /> : <Save size={14} />}
|
||||
保存变量修改
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="agent-action-btn primary"
|
||||
onClick={() => onAction('confirm_plan')}
|
||||
disabled={actionLoading}
|
||||
onClick={handleConfirmPlan}
|
||||
disabled={actionLoading || savingParams}
|
||||
>
|
||||
{actionLoading ? <Loader2 size={14} className="spin" /> : <CheckCircle size={14} />}
|
||||
确认计划,生成代码
|
||||
@@ -221,8 +416,49 @@ export const AgentCodePanel: React.FC<AgentCodePanelProps> = ({ onAction, action
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: R 代码 */}
|
||||
{(displayCode || status === 'coding') && (
|
||||
{/* Step-by-step 执行状态(Phase 5B) */}
|
||||
{stepResults && stepResults.length > 0 && (
|
||||
<div className="agent-section">
|
||||
<div className="agent-section-title">
|
||||
<Sparkles size={13} />
|
||||
<span>分步执行状态</span>
|
||||
</div>
|
||||
<div className="agent-plan-steps">
|
||||
{stepResults
|
||||
.slice()
|
||||
.sort((a, b) => a.stepOrder - b.stepOrder)
|
||||
.map((s, i) => (
|
||||
<div key={i} className="plan-step-item">
|
||||
<span className="step-num">{s.stepOrder}</span>
|
||||
<div className="plan-step-body">
|
||||
<span className="step-method">{s.method || `Step ${s.stepOrder}`}</span>
|
||||
<span className="step-desc">
|
||||
{STEP_STATUS_LABEL[s.status]}
|
||||
{currentStep === s.stepOrder && ['coding', 'executing'].includes(s.status) ? ' · 当前步骤' : ''}
|
||||
{typeof s.durationMs === 'number' ? ` · ${(s.durationMs / 1000).toFixed(1)}s` : ''}
|
||||
</span>
|
||||
{!!s.errorMessage && <span className="step-rationale">{s.errorMessage}</span>}
|
||||
{s.status === 'completed' && s.reportBlocks && s.reportBlocks.length > 0 && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<details>
|
||||
<summary style={{ cursor: 'pointer', color: '#93c5fd' }}>
|
||||
查看该步骤结果({s.reportBlocks.length} 个模块)
|
||||
</summary>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<DynamicReport blocks={s.reportBlocks} />
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: R 代码(严格分步模式下展示首步预览;后续步骤执行时逐步生成) */}
|
||||
{(displayCode || ['coding', 'code_pending', 'executing', 'completed', 'error'].includes(status)) && (
|
||||
<div className="agent-section">
|
||||
<div className="agent-section-title">
|
||||
<Code size={13} />
|
||||
@@ -234,6 +470,11 @@ export const AgentCodePanel: React.FC<AgentCodePanelProps> = ({ onAction, action
|
||||
<div className="agent-code-body">
|
||||
{displayCode ? (
|
||||
<pre className={isStreamingCode ? 'streaming' : ''}>{displayCode}</pre>
|
||||
) : status === 'code_pending' ? (
|
||||
<div className="agent-code-loading">
|
||||
<Sparkles size={16} />
|
||||
<span>当前为分步执行模式:后续步骤代码将在执行阶段逐步生成。</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="agent-code-loading">
|
||||
<Loader2 size={16} className="spin" />
|
||||
|
||||
@@ -31,6 +31,7 @@ import { AgentCodePanel } from './AgentCodePanel';
|
||||
import { DynamicReport } from './DynamicReport';
|
||||
import { exportBlocksToWord } from '../utils/exportBlocksToWord';
|
||||
import apiClient from '@/common/api/axios';
|
||||
import type { VariableDictEntry, DataOverviewColumn } from '../types';
|
||||
|
||||
const stepHasResult = (s: WorkflowStepResult) =>
|
||||
(s.status === 'success' || s.status === 'warning') && s.result;
|
||||
@@ -340,6 +341,8 @@ export const SSAWorkspacePane: React.FC = () => {
|
||||
<AgentCodePanel
|
||||
onAction={handleAgentAction}
|
||||
actionLoading={agentActionLoading}
|
||||
variableDictionary={variableDictionary as VariableDictEntry[]}
|
||||
dataOverviewColumns={dataOverviewColumns as DataOverviewColumn[]}
|
||||
/>
|
||||
{/* Agent 模式的报告输出复用 DynamicReport */}
|
||||
{agentExecution?.status === 'completed' && agentExecution.reportBlocks && agentExecution.reportBlocks.length > 0 && (
|
||||
|
||||
@@ -30,7 +30,7 @@ import type {
|
||||
|
||||
// ────────────────────────── Param constraints ──────────────────────────
|
||||
|
||||
interface ParamConstraint {
|
||||
export interface ParamConstraint {
|
||||
paramType: 'single' | 'multi';
|
||||
requiredType: 'categorical' | 'numeric' | 'any';
|
||||
minLevels?: number;
|
||||
@@ -38,9 +38,9 @@ interface ParamConstraint {
|
||||
hint: string;
|
||||
}
|
||||
|
||||
type ToolConstraints = Record<string, Record<string, ParamConstraint>>;
|
||||
export type ToolConstraints = Record<string, Record<string, ParamConstraint>>;
|
||||
|
||||
const TOOL_CONSTRAINTS: ToolConstraints = {
|
||||
export const TOOL_CONSTRAINTS: ToolConstraints = {
|
||||
ST_DESCRIPTIVE: {
|
||||
variables: { paramType: 'multi', requiredType: 'any', hint: '选择需要描述的变量' },
|
||||
group_var: { paramType: 'single', requiredType: 'categorical', hint: '分组变量(可选)' },
|
||||
@@ -93,25 +93,25 @@ const TOOL_CONSTRAINTS: ToolConstraints = {
|
||||
},
|
||||
};
|
||||
|
||||
const SINGLE_VAR_KEYS = new Set([
|
||||
export const SINGLE_VAR_KEYS = new Set([
|
||||
'group_var', 'outcome_var', 'value_var', 'var_x', 'var_y',
|
||||
'before_var', 'after_var', 'var1', 'var2',
|
||||
]);
|
||||
const MULTI_VAR_KEYS = new Set([
|
||||
export const MULTI_VAR_KEYS = new Set([
|
||||
'analyze_vars', 'predictors', 'variables', 'confounders',
|
||||
]);
|
||||
|
||||
function isVariableParam(key: string): boolean {
|
||||
export function isVariableParam(key: string): boolean {
|
||||
return SINGLE_VAR_KEYS.has(key) || MULTI_VAR_KEYS.has(key);
|
||||
}
|
||||
|
||||
interface VarInfo {
|
||||
export interface VarInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
totalLevels?: number;
|
||||
}
|
||||
|
||||
function checkMismatch(
|
||||
export function checkMismatch(
|
||||
varName: string,
|
||||
constraint: ParamConstraint,
|
||||
varsMap: Map<string, VarInfo>
|
||||
@@ -202,7 +202,7 @@ const formatValue = (value: unknown): string => {
|
||||
return s.length > 50 ? s.slice(0, 47) + '…' : s;
|
||||
};
|
||||
|
||||
const PARAM_LABELS: Record<string, string> = {
|
||||
export const PARAM_LABELS: Record<string, string> = {
|
||||
variables: '分析变量',
|
||||
outcome_var: '结局变量 (Y)',
|
||||
predictors: '自变量 (X)',
|
||||
@@ -231,7 +231,7 @@ interface SingleVarSelectProps {
|
||||
onChange: (v: string | null) => void;
|
||||
}
|
||||
|
||||
const SingleVarSelect: React.FC<SingleVarSelectProps> = ({ value, constraint, varsMap, allVars, onChange }) => {
|
||||
export const SingleVarSelect: React.FC<SingleVarSelectProps> = ({ value, constraint, varsMap, allVars, onChange }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -318,7 +318,7 @@ interface MultiVarTagsProps {
|
||||
onChange: (v: string[]) => void;
|
||||
}
|
||||
|
||||
const MultiVarTags: React.FC<MultiVarTagsProps> = ({ values, constraint, varsMap, allVars, onChange }) => {
|
||||
export const MultiVarTags: React.FC<MultiVarTagsProps> = ({ values, constraint, varsMap, allVars, onChange }) => {
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useState, useCallback, useRef } from 'react';
|
||||
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';
|
||||
import type { WorkflowPlan, AgentStepResult } from '../types';
|
||||
|
||||
// ────────────────────────────────────────────
|
||||
// Types
|
||||
@@ -229,7 +229,19 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
setIntentMeta(null);
|
||||
setPendingQuestion(null);
|
||||
|
||||
const isAgentAction = !!metadata?.agentAction;
|
||||
const { executionMode, agentExecution } = useSSAStore.getState();
|
||||
const isAgentInlineInstruction =
|
||||
executionMode === 'agent'
|
||||
&& !!agentExecution
|
||||
&& (agentExecution.status === 'plan_pending' || agentExecution.status === 'code_pending')
|
||||
&& !metadata?.agentAction
|
||||
&& !metadata?.askUserResponse;
|
||||
|
||||
const finalMetadata = isAgentInlineInstruction
|
||||
? { ...(metadata || {}), agentInlineInstruction: true }
|
||||
: metadata;
|
||||
|
||||
const isAgentAction = !!finalMetadata?.agentAction || isAgentInlineInstruction;
|
||||
|
||||
const assistantMsgId = crypto.randomUUID();
|
||||
const assistantPlaceholder: ChatMessage = {
|
||||
@@ -254,7 +266,8 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
setChatMessages(prev => [...prev, userMsg, assistantPlaceholder]);
|
||||
}
|
||||
|
||||
abortRef.current = new AbortController();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
let fullContent = '';
|
||||
let fullThinking = '';
|
||||
|
||||
@@ -267,8 +280,8 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
Accept: 'text/event-stream',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ content, ...(metadata ? { metadata } : {}) }),
|
||||
signal: abortRef.current.signal,
|
||||
body: JSON.stringify({ content, ...(finalMetadata ? { metadata: finalMetadata } : {}) }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -359,10 +372,115 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
|
||||
if (parsed.type === 'agent_plan_ready') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
const initialStepResults: AgentStepResult[] = Array.isArray(parsed.plan?.steps)
|
||||
? parsed.plan.steps.map((s: any) => ({
|
||||
stepOrder: s.order || 0,
|
||||
method: s.method || '',
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
}))
|
||||
: [];
|
||||
updateAgentExecution({
|
||||
planText: parsed.planText,
|
||||
planSteps: parsed.plan?.steps,
|
||||
status: 'plan_pending',
|
||||
stepResults: initialStepResults.length > 0 ? initialStepResults : undefined,
|
||||
currentStep: initialStepResults.length > 0 ? initialStepResults[0].stepOrder : undefined,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const patchStepResult = (stepOrder: number, patch: Partial<AgentStepResult>) => {
|
||||
const { agentExecution, updateAgentExecution } = useSSAStore.getState();
|
||||
const existing = agentExecution?.stepResults || [];
|
||||
const idx = existing.findIndex(s => s.stepOrder === stepOrder);
|
||||
let next: AgentStepResult[];
|
||||
if (idx >= 0) {
|
||||
next = existing.map((s, i) => (i === idx ? { ...s, ...patch } : s));
|
||||
} else {
|
||||
next = [
|
||||
...existing,
|
||||
{
|
||||
stepOrder,
|
||||
method: '',
|
||||
status: 'pending',
|
||||
retryCount: 0,
|
||||
...patch,
|
||||
} as AgentStepResult,
|
||||
].sort((a, b) => a.stepOrder - b.stepOrder);
|
||||
}
|
||||
updateAgentExecution({ stepResults: next, currentStep: stepOrder });
|
||||
};
|
||||
|
||||
if (parsed.type === 'step_coding') {
|
||||
patchStepResult(parsed.stepOrder || 0, {
|
||||
status: 'coding',
|
||||
partialCode: parsed.partialCode,
|
||||
retryCount: parsed.retryCount || 0,
|
||||
});
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
partialCode: parsed.partialCode,
|
||||
status: 'coding',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'step_code_ready') {
|
||||
patchStepResult(parsed.stepOrder || 0, {
|
||||
status: 'coding',
|
||||
code: parsed.code,
|
||||
partialCode: undefined,
|
||||
});
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
generatedCode: parsed.code,
|
||||
partialCode: undefined,
|
||||
status: 'coding',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'step_executing') {
|
||||
patchStepResult(parsed.stepOrder || 0, { status: 'executing' });
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({ status: 'executing' });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'step_result') {
|
||||
patchStepResult(parsed.stepOrder || 0, {
|
||||
status: 'completed',
|
||||
reportBlocks: parsed.reportBlocks,
|
||||
durationMs: parsed.durationMs,
|
||||
errorMessage: undefined,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'step_error') {
|
||||
patchStepResult(parsed.stepOrder || 0, {
|
||||
status: parsed.willRetry ? 'coding' : 'error',
|
||||
errorMessage: parsed.message,
|
||||
retryCount: parsed.retryCount || 0,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'step_skipped') {
|
||||
patchStepResult(parsed.stepOrder || 0, {
|
||||
status: 'skipped',
|
||||
errorMessage: parsed.reason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed.type === 'pipeline_aborted') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
status: 'error',
|
||||
errorMessage: parsed.error || '执行已终止',
|
||||
currentStep: parsed.stepOrder,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -379,9 +497,9 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
if (parsed.type === 'code_generated') {
|
||||
const { updateAgentExecution } = useSSAStore.getState();
|
||||
updateAgentExecution({
|
||||
generatedCode: parsed.code,
|
||||
generatedCode: parsed.code || undefined,
|
||||
partialCode: undefined,
|
||||
status: parsed.code ? 'code_pending' : 'coding',
|
||||
status: 'code_pending',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -399,6 +517,7 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
generatedCode: parsed.code || curExec?.generatedCode,
|
||||
status: 'completed',
|
||||
durationMs: parsed.durationMs,
|
||||
stepResults: parsed.stepResults || curExec?.stepResults,
|
||||
});
|
||||
|
||||
// 在对话中插入可点击的结果卡片
|
||||
@@ -503,7 +622,9 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
: m
|
||||
));
|
||||
setIsGenerating(false);
|
||||
abortRef.current = null;
|
||||
if (abortRef.current === controller) {
|
||||
abortRef.current = null;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
return sendChatMessage(sessionId, content, metadata);
|
||||
}
|
||||
@@ -521,7 +642,9 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
setIsGenerating(false);
|
||||
setStreamingContent('');
|
||||
setThinkingContent('');
|
||||
abortRef.current = null;
|
||||
if (abortRef.current === controller) {
|
||||
abortRef.current = null;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -533,7 +656,7 @@ export function useSSAChat(): UseSSAChatReturn {
|
||||
*/
|
||||
const executeAgentAction = useCallback(async (sessionId: string, action: AgentActionType) => {
|
||||
const AUDIT_MESSAGES: Record<AgentActionType, string> = {
|
||||
confirm_plan: '✅ 方案已确认,正在生成 R 代码...',
|
||||
confirm_plan: '✅ 方案已确认,已进入执行确认(执行时将分步生成代码)...',
|
||||
confirm_code: '✅ 代码已确认,R 引擎正在执行...',
|
||||
cancel: '❌ 已取消当前分析',
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
FiveSectionReport,
|
||||
VariableDetailData,
|
||||
AgentExecutionRecord,
|
||||
AgentStepResult,
|
||||
} from '../types';
|
||||
|
||||
type ArtifactPane = 'empty' | 'sap' | 'execution' | 'result';
|
||||
@@ -250,6 +251,8 @@ export const useSSAStore = create<SSAState>((set) => ({
|
||||
method: s.method,
|
||||
description: s.description,
|
||||
rationale: s.rationale,
|
||||
toolCode: s.toolCode || s.tool_code,
|
||||
params: s.params,
|
||||
}));
|
||||
}
|
||||
planMeta = { title: structured.title, designType: structured.designType };
|
||||
@@ -261,7 +264,12 @@ export const useSSAStore = create<SSAState>((set) => ({
|
||||
const arr = Array.isArray(parsed?.steps) ? parsed.steps : parsed?.plan?.steps;
|
||||
if (Array.isArray(arr)) {
|
||||
planSteps = arr.map((s: any) => ({
|
||||
order: s.order, method: s.method, description: s.description, rationale: s.rationale,
|
||||
order: s.order,
|
||||
method: s.method,
|
||||
description: s.description,
|
||||
rationale: s.rationale,
|
||||
toolCode: s.toolCode || s.tool_code,
|
||||
params: s.params,
|
||||
}));
|
||||
}
|
||||
if (!planMeta) planMeta = { title: parsed.title, designType: parsed.designType };
|
||||
@@ -278,6 +286,8 @@ export const useSSAStore = create<SSAState>((set) => ({
|
||||
reportBlocks: e.reportBlocks,
|
||||
retryCount: e.retryCount || 0,
|
||||
status: e.status,
|
||||
stepResults: Array.isArray(e.stepResults) ? (e.stepResults as AgentStepResult[]) : undefined,
|
||||
currentStep: typeof e.currentStep === 'number' ? e.currentStep : undefined,
|
||||
errorMessage: e.errorMessage,
|
||||
durationMs: e.durationMs,
|
||||
createdAt: e.createdAt,
|
||||
|
||||
@@ -377,6 +377,7 @@ export type SSEMessageType =
|
||||
| 'workflow_complete' | 'workflow_error'
|
||||
| 'qper_status' | 'reflection_complete'
|
||||
| 'agent_planning' | 'agent_plan_ready'
|
||||
| 'step_coding' | 'step_code_ready' | 'step_executing' | 'step_result' | 'step_skipped' | 'pipeline_aborted'
|
||||
| 'code_generating' | 'code_generated'
|
||||
| 'code_executing' | 'code_result' | 'code_error' | 'code_retry';
|
||||
|
||||
@@ -386,17 +387,40 @@ export type AgentExecutionStatus =
|
||||
| 'coding' | 'code_pending'
|
||||
| 'executing' | 'completed' | 'error';
|
||||
|
||||
export type AgentStepStatus = 'pending' | 'coding' | 'executing' | 'completed' | 'error' | 'skipped';
|
||||
|
||||
export interface AgentStepResult {
|
||||
stepOrder: number;
|
||||
method: string;
|
||||
status: AgentStepStatus;
|
||||
code?: string;
|
||||
partialCode?: string;
|
||||
reportBlocks?: ReportBlock[];
|
||||
errorMessage?: string;
|
||||
retryCount: number;
|
||||
durationMs?: number;
|
||||
}
|
||||
|
||||
export interface AgentExecutionRecord {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
query: string;
|
||||
planText?: string;
|
||||
planSteps?: Array<{ order: number; method: string; description: string; rationale?: string }>;
|
||||
planSteps?: Array<{
|
||||
order: number;
|
||||
method: string;
|
||||
description: string;
|
||||
rationale?: string;
|
||||
toolCode?: string;
|
||||
params?: Record<string, unknown>;
|
||||
}>;
|
||||
generatedCode?: string;
|
||||
partialCode?: string;
|
||||
reportBlocks?: ReportBlock[];
|
||||
retryCount: number;
|
||||
status: AgentExecutionStatus;
|
||||
stepResults?: AgentStepResult[];
|
||||
currentStep?: number;
|
||||
errorMessage?: string;
|
||||
durationMs?: number;
|
||||
createdAt?: string;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user