feat(ssa): Complete QPER architecture - Query, Planner, Execute, Reflection layers

Implement the full QPER intelligent analysis pipeline:

- Phase E+: Block-based standardization for all 7 R tools, DynamicReport renderer, Word export enhancement

- Phase Q: LLM intent parsing with dynamic Zod validation against real column names, ClarificationCard component, DataProfile is_id_like tagging

- Phase P: ConfigLoader with Zod schema validation and hot-reload API, DecisionTableService (4-dimension matching), FlowTemplateService with EPV protection, PlannedTrace audit output

- Phase R: ReflectionService with statistical slot injection, sensitivity analysis conflict rules, ConclusionReport with section reveal animation, conclusion caching API, graceful R error classification

End-to-end test: 40/40 passed across two complete analysis scenarios.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-21 18:15:53 +08:00
parent 428a22adf2
commit 371e1c069c
73 changed files with 9242 additions and 706 deletions

View File

@@ -9,6 +9,7 @@
import { FastifyInstance, FastifyRequest } from 'fastify';
import { prisma } from '../../../config/database.js';
import { logger } from '../../../common/logging/index.js';
import { reloadAllConfigs } from '../config/index.js';
function getUserId(request: FastifyRequest): string {
const userId = (request as any).user?.userId;
@@ -106,14 +107,29 @@ export default async function configRoutes(app: FastifyInstance) {
return reply.send([]);
});
// 热加载配置
// 热加载配置 — 重新读取所有领域 JSON 文件并 Zod 校验
app.post('/reload', async (req, reply) => {
// TODO: 重新加载所有配置到缓存
return reply.send({
success: true,
timestamp: new Date().toISOString()
});
logger.info('[SSA:Config] Reloading all config files...');
const results = reloadAllConfigs();
const allSuccess = results.every(r => r.success);
const failures = results.filter(r => !r.success);
if (allSuccess) {
logger.info('[SSA:Config] All configs reloaded successfully');
return reply.send({
success: true,
timestamp: new Date().toISOString(),
results,
});
} else {
logger.warn('[SSA:Config] Some configs failed to reload', { failures });
return reply.status(400).send({
success: false,
message: `${failures.length} 个配置文件校验失败,已保留旧配置`,
results,
});
}
});
// 校验配置文件

View File

@@ -13,6 +13,10 @@ import { logger } from '../../../common/logging/index.js';
import { workflowPlannerService } from '../services/WorkflowPlannerService.js';
import { workflowExecutorService } from '../services/WorkflowExecutorService.js';
import { dataProfileService } from '../services/DataProfileService.js';
import { queryService } from '../services/QueryService.js';
import { reflectionService } from '../services/ReflectionService.js';
import { prisma } from '../../../config/database.js';
import { cache } from '../../../common/cache/index.js';
// 请求类型定义
interface PlanWorkflowBody {
@@ -74,6 +78,109 @@ export default async function workflowRoutes(app: FastifyInstance) {
}
);
/**
* POST /workflow/intent
* Phase Q: LLM 意图理解 — 解析用户自然语言为结构化 ParsedQuery
*/
app.post<{ Body: PlanWorkflowBody }>(
'/intent',
async (request, reply) => {
const { sessionId, userQuery } = request.body;
if (!sessionId || !userQuery) {
return reply.status(400).send({
success: false,
error: 'sessionId and userQuery are required'
});
}
try {
logger.info('[SSA:API] Parsing intent', { sessionId, userQuery });
const parsed = await queryService.parseIntent(userQuery, sessionId);
return reply.send({
success: true,
intent: parsed,
needsClarification: parsed.needsClarification,
clarificationCards: parsed.clarificationCards || [],
});
} catch (error: any) {
logger.error('[SSA:API] Intent parsing failed', {
sessionId,
error: error.message
});
return reply.status(500).send({
success: false,
error: error.message
});
}
}
);
/**
* POST /workflow/clarify
* Phase Q: 用户回答追问卡片后,补全 ParsedQuery 并重新规划
*/
app.post<{ Body: { sessionId: string; userQuery: string; selections: Record<string, string> } }>(
'/clarify',
async (request, reply) => {
const { sessionId, userQuery, selections } = request.body;
if (!sessionId) {
return reply.status(400).send({
success: false,
error: 'sessionId is required'
});
}
try {
logger.info('[SSA:API] Processing clarification', { sessionId, selections });
// 将用户选择拼接到原始 query 中,重新走 intent 解析
const selectionText = Object.entries(selections)
.map(([key, value]) => `${key}: ${value}`)
.join('; ');
const enrichedQuery = userQuery
? `${userQuery}(补充说明:${selectionText}`
: selectionText;
const parsed = await queryService.parseIntent(enrichedQuery, sessionId);
// 如果这次置信度足够,直接生成工作流计划
if (!parsed.needsClarification) {
const plan = await workflowPlannerService.planWorkflow(sessionId, enrichedQuery);
return reply.send({
success: true,
intent: parsed,
plan,
needsClarification: false,
});
}
return reply.send({
success: true,
intent: parsed,
needsClarification: true,
clarificationCards: parsed.clarificationCards || [],
});
} catch (error: any) {
logger.error('[SSA:API] Clarification failed', {
sessionId,
error: error.message
});
return reply.status(500).send({
success: false,
error: error.message
});
}
}
);
/**
* POST /workflow/:workflowId/execute
* 执行工作流
@@ -329,6 +436,73 @@ export default async function workflowRoutes(app: FastifyInstance) {
}
}
);
/**
* GET /workflow/sessions/:sessionId/conclusion
* 获取会话的分析结论(优先返回缓存,无缓存则从 workflow 结果重新生成)
*/
app.get<{ Params: { sessionId: string } }>(
'/sessions/:sessionId/conclusion',
async (request, reply) => {
const { sessionId } = request.params;
try {
// 查找该 session 最新的 completed workflow
const workflow = await prisma.ssaWorkflow.findFirst({
where: { sessionId: sessionId, status: { in: ['completed', 'partial'] } },
orderBy: { createdAt: 'desc' },
});
if (!workflow) {
return reply.status(404).send({
success: false,
error: 'No completed workflow found for this session',
});
}
// 检查缓存
const cacheKey = `ssa:conclusion:${workflow.id}`;
const cached = await cache.get(cacheKey);
if (cached) {
return reply.send({ success: true, conclusion: cached, source: 'cache' });
}
// 无缓存:获取 workflow steps 结果并重新生成
const steps = await prisma.ssaWorkflowStep.findMany({
where: { workflowId: workflow.id },
orderBy: { stepOrder: 'asc' },
});
const results = steps.map((s: any) => ({
stepOrder: s.stepOrder,
toolCode: s.toolCode,
toolName: s.toolName,
status: s.status,
result: s.outputResult,
reportBlocks: s.reportBlocks,
executionMs: s.executionMs || 0,
}));
const workflowPlan = workflow.workflowPlan as any;
const conclusion = await reflectionService.reflect(
{
workflowId: workflow.id,
goal: workflowPlan?.goal || '统计分析',
title: workflowPlan?.title,
methodology: workflowPlan?.methodology,
plannedTrace: workflowPlan?.planned_trace,
},
results,
);
return reply.send({ success: true, conclusion, source: conclusion.source });
} catch (error: any) {
logger.error('[SSA:API] Get conclusion failed', { sessionId, error: error.message });
return reply.status(500).send({ success: false, error: error.message });
}
}
);
}
/**