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:
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
import { workflowPlannerService } from '../services/WorkflowPlannerService.js';
|
||||
import { workflowExecutorService } from '../services/WorkflowExecutorService.js';
|
||||
@@ -372,6 +373,143 @@ export default async function workflowRoutes(app: FastifyInstance) {
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* PATCH /workflow/:workflowId/params
|
||||
* Phase V: 批量更新 workflow step 参数(变量可编辑化)
|
||||
*
|
||||
* Zod 结构校验防火墙:
|
||||
* - 结构非法(字段类型错误、必填字段缺失)→ 400 Bad Request
|
||||
* - 统计学不合理但结构合法 → 放行,交给 R 引擎 tryCatch
|
||||
*/
|
||||
app.patch<{ Params: { workflowId: string }; Body: { steps: Array<{ stepOrder: number; params: Record<string, any> }> } }>(
|
||||
'/:workflowId/params',
|
||||
async (request, reply) => {
|
||||
const { workflowId } = request.params;
|
||||
const { steps } = request.body;
|
||||
|
||||
const PatchStepSchema = z.object({
|
||||
stepOrder: z.number().int().positive(),
|
||||
params: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
const PatchBodySchema = z.object({
|
||||
steps: z.array(PatchStepSchema).min(1),
|
||||
});
|
||||
|
||||
const validation = PatchBodySchema.safeParse({ steps });
|
||||
if (!validation.success) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: 'Invalid request body',
|
||||
details: validation.error.flatten(),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const workflow = await prisma.ssaWorkflow.findUnique({
|
||||
where: { id: workflowId },
|
||||
select: { id: true, status: true, sessionId: true },
|
||||
});
|
||||
|
||||
if (!workflow) {
|
||||
return reply.status(404).send({ success: false, error: 'Workflow not found' });
|
||||
}
|
||||
|
||||
if (workflow.status !== 'planned' && workflow.status !== 'pending') {
|
||||
return reply.status(409).send({
|
||||
success: false,
|
||||
error: `Cannot modify params for workflow in '${workflow.status}' state`,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate variable names exist in session data schema
|
||||
const session = await prisma.ssaSession.findUnique({
|
||||
where: { id: workflow.sessionId },
|
||||
select: { dataSchema: true },
|
||||
});
|
||||
const schema = session?.dataSchema as any;
|
||||
const validColumnNames = new Set<string>(
|
||||
(schema?.columns || []).map((c: any) => c.name)
|
||||
);
|
||||
|
||||
for (const stepPatch of validation.data.steps) {
|
||||
for (const [key, value] of Object.entries(stepPatch.params)) {
|
||||
if (typeof value === 'string' && value && !key.startsWith('_')) {
|
||||
if (['group_var', 'outcome_var', 'value_var', 'var_x', 'var_y',
|
||||
'before_var', 'after_var', 'var1', 'var2'].includes(key)) {
|
||||
if (validColumnNames.size > 0 && !validColumnNames.has(value)) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: `Variable '${value}' in step ${stepPatch.stepOrder}.${key} does not exist in the dataset`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Array.isArray(value) && ['analyze_vars', 'predictors', 'variables', 'confounders'].includes(key)) {
|
||||
for (const v of value) {
|
||||
if (typeof v === 'string' && validColumnNames.size > 0 && !validColumnNames.has(v)) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
error: `Variable '${v}' in step ${stepPatch.stepOrder}.${key} does not exist in the dataset`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update each step's inputParams in the database
|
||||
const updatePromises = validation.data.steps.map((stepPatch) =>
|
||||
prisma.ssaWorkflowStep.updateMany({
|
||||
where: {
|
||||
workflowId,
|
||||
stepOrder: stepPatch.stepOrder,
|
||||
},
|
||||
data: {
|
||||
inputParams: stepPatch.params as any,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
// Also update the workflowPlan JSON blob's steps params
|
||||
const currentPlan = await prisma.ssaWorkflow.findUnique({
|
||||
where: { id: workflowId },
|
||||
select: { workflowPlan: true },
|
||||
});
|
||||
if (currentPlan?.workflowPlan) {
|
||||
const plan = currentPlan.workflowPlan as any;
|
||||
if (plan.steps) {
|
||||
for (const stepPatch of validation.data.steps) {
|
||||
const planStep = plan.steps.find((s: any) => s.step_number === stepPatch.stepOrder);
|
||||
if (planStep) {
|
||||
planStep.params = stepPatch.params;
|
||||
}
|
||||
}
|
||||
await prisma.ssaWorkflow.update({
|
||||
where: { id: workflowId },
|
||||
data: { workflowPlan: plan },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('[SSA:API] Workflow params updated', {
|
||||
workflowId,
|
||||
stepsUpdated: validation.data.steps.length,
|
||||
});
|
||||
|
||||
return reply.send({ success: true, stepsUpdated: validation.data.steps.length });
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error('[SSA:API] Patch workflow params failed', {
|
||||
workflowId,
|
||||
error: error.message,
|
||||
});
|
||||
return reply.status(500).send({ success: false, error: error.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /workflow/profile
|
||||
* 生成数据画像
|
||||
|
||||
Reference in New Issue
Block a user