feat(ssa): Complete Phase V-A editable analysis plan variables

Features:
- Add editable variable selection in workflow plan (SingleVarSelect + MultiVarTags)
- Implement 3-layer flexible interception (warning bar + icon + blocking dialog)
- Add tool_param_constraints.json for 12 statistical tools parameter validation
- Add PATCH /workflow/:id/params API with Zod structural validation
- Implement synchronous parameter sync before execution (Promise chaining)
- Fix LLM hallucination by strict system prompt constraints
- Fix DynamicReport object-based rows compatibility (R baseline_table)
- Fix Word export row.map error with same normalization logic
- Restore inferGroupingVar for smart default variable selection
- Add ReactMarkdown rendering in SSAChatPane
- Update SSA module status document to v3.5

Modified files:
- backend: workflow.routes, ChatHandlerService, SystemPromptService, FlowTemplateService
- frontend: WorkflowTimeline, SSAWorkspacePane, DynamicReport, SSAChatPane, ssaStore, ssa.css
- config: tool_param_constraints.json (new)
- docs: SSA status doc, team review reports

Tested: Cohort study end-to-end execution + report export verified
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-24 13:08:29 +08:00
parent dc6b292308
commit 85fda830c2
27 changed files with 2732 additions and 154 deletions

View File

@@ -239,18 +239,20 @@ export function useSSAChat(): UseSSAChatReturn {
}
// analysis_plan 事件Phase IV: 对话驱动分析)
// 仅创建记录,不打开工作区 — 等用户确认方案后再打开
if (parsed.type === 'analysis_plan' && parsed.plan) {
const plan = parsed.plan as WorkflowPlan;
const { addRecord, setActivePane, setWorkspaceOpen } = useSSAStore.getState();
const { addRecord } = useSSAStore.getState();
addRecord(content, plan);
setActivePane('sap');
setWorkspaceOpen(true);
continue;
}
// plan_confirmed 事件Phase IV: 用户确认方案后触发执行
if (parsed.type === 'plan_confirmed' && parsed.workflowId) {
setPendingPlanConfirm({ workflowId: parsed.workflowId });
// plan_confirmed 事件Phase IV: 用户确认方案后打开工作区
// 不自动触发 executeWorkflow — 由用户在工作区手动点击「开始执行分析」
if (parsed.type === 'plan_confirmed') {
const { setActivePane, setWorkspaceOpen } = useSSAStore.getState();
setActivePane('sap');
setWorkspaceOpen(true);
setPendingQuestion(null);
continue;
}
@@ -319,14 +321,27 @@ export function useSSAChat(): UseSSAChatReturn {
/**
* 响应 ask_user 卡片Phase III
* 将 value 解析为中文 label 用于显示
*/
const respondToQuestion = useCallback(async (sessionId: string, response: AskUserResponseData) => {
const question = pendingQuestion;
setPendingQuestion(null);
const displayText = response.action === 'select'
? `选择了: ${response.selectedValues?.join(', ')}`
: response.freeText || '(已回复)';
let displayText: string;
if (response.action === 'select' && response.selectedValues) {
const labels = response.selectedValues.map(val => {
const opt = question?.options?.find(o => o.value === val);
return opt?.label || val;
});
displayText = `选择了 ${labels.join('、')}`;
} else if (response.action === 'free_text') {
displayText = response.freeText || '(已回复)';
} else {
displayText = '(已回复)';
}
await sendChatMessage(sessionId, displayText, { askUserResponse: response });
}, [sendChatMessage]);
}, [sendChatMessage, pendingQuestion]);
/**
* H1: 跳过 ask_user 卡片
@@ -337,7 +352,7 @@ export function useSSAChat(): UseSSAChatReturn {
questionId,
action: 'skip',
};
await sendChatMessage(sessionId, '跳过此问题', { askUserResponse: skipResponse });
await sendChatMessage(sessionId, '跳过此问题', { askUserResponse: skipResponse });
}, [sendChatMessage]);
return {