feat(iit): Implement real-time quality control system
Summary: - Add 4 new database tables: iit_field_metadata, iit_qc_logs, iit_record_summary, iit_qc_project_stats - Implement pg-boss debounce mechanism in WebhookController - Refactor QC Worker for dual output: QC logs + record summary - Enhance HardRuleEngine to support form-based rule filtering - Create QcService for QC data queries - Optimize ChatService with new intents: query_enrollment, query_qc_status - Add admin batch operations: one-click full QC + one-click full summary - Create IIT Admin management module: project config, QC rules, user mapping Status: Code complete, pending end-to-end testing Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
385
backend/src/modules/iit-manager/engines/HardRuleEngine.ts
Normal file
385
backend/src/modules/iit-manager/engines/HardRuleEngine.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* HardRuleEngine - 硬规则质控引擎
|
||||
*
|
||||
* 功能:
|
||||
* - 基于 JSON Logic 执行质控规则
|
||||
* - 支持纳入标准、排除标准、变量范围检查
|
||||
* - 返回结构化的质控结果
|
||||
*
|
||||
* 设计原则:
|
||||
* - 零容忍:规则判断是确定性的,不依赖 AI 猜测
|
||||
* - 可追溯:每条规则执行结果都有详细记录
|
||||
* - 高性能:纯逻辑计算,无 LLM 调用
|
||||
*/
|
||||
|
||||
import jsonLogic from 'json-logic-js';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// ============================================================
|
||||
// 类型定义
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 质控规则定义
|
||||
*/
|
||||
export interface QCRule {
|
||||
id: string;
|
||||
name: string;
|
||||
field: string | string[]; // 单字段或多字段
|
||||
logic: Record<string, any>; // JSON Logic 表达式
|
||||
message: string;
|
||||
severity: 'error' | 'warning' | 'info';
|
||||
category: 'inclusion' | 'exclusion' | 'lab_values' | 'logic_check';
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条规则执行结果
|
||||
*/
|
||||
export interface RuleResult {
|
||||
ruleId: string;
|
||||
ruleName: string;
|
||||
field: string | string[];
|
||||
passed: boolean;
|
||||
message: string;
|
||||
severity: 'error' | 'warning' | 'info';
|
||||
category: string;
|
||||
actualValue?: any;
|
||||
expectedCondition?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 质控执行结果
|
||||
*/
|
||||
export interface QCResult {
|
||||
recordId: string;
|
||||
projectId: string;
|
||||
timestamp: string;
|
||||
overallStatus: 'PASS' | 'FAIL' | 'WARNING';
|
||||
summary: {
|
||||
totalRules: number;
|
||||
passed: number;
|
||||
failed: number;
|
||||
warnings: number;
|
||||
};
|
||||
results: RuleResult[];
|
||||
errors: RuleResult[];
|
||||
warnings: RuleResult[];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HardRuleEngine 实现
|
||||
// ============================================================
|
||||
|
||||
export class HardRuleEngine {
|
||||
private projectId: string;
|
||||
private rules: QCRule[] = [];
|
||||
private fieldMappings: Map<string, string> = new Map();
|
||||
|
||||
constructor(projectId: string) {
|
||||
this.projectId = projectId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化引擎(加载规则和字段映射)
|
||||
*
|
||||
* @param formName 可选,按表单名过滤规则(用于单表实时质控)
|
||||
*/
|
||||
async initialize(formName?: string): Promise<void> {
|
||||
// 1. 加载质控规则
|
||||
const skill = await prisma.iitSkill.findFirst({
|
||||
where: {
|
||||
projectId: this.projectId,
|
||||
skillType: 'qc_process',
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!skill) {
|
||||
throw new Error(`No active QC rules found for project: ${this.projectId}`);
|
||||
}
|
||||
|
||||
const config = skill.config as any;
|
||||
let allRules = config.rules || [];
|
||||
|
||||
// ⭐ 如果指定了 formName,则只加载该表单相关的规则
|
||||
// 规则通过 formName 或 field 字段来判断所属表单
|
||||
if (formName) {
|
||||
allRules = allRules.filter((rule: any) => {
|
||||
// 优先使用规则中的 formName 字段
|
||||
if (rule.formName) {
|
||||
return rule.formName === formName;
|
||||
}
|
||||
// 如果规则没有 formName,则默认包含(兼容旧规则)
|
||||
// TODO: 后续可以通过 field_metadata 表来判断字段所属表单
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
this.rules = allRules;
|
||||
|
||||
logger.info('[HardRuleEngine] Rules loaded', {
|
||||
projectId: this.projectId,
|
||||
ruleCount: this.rules.length
|
||||
});
|
||||
|
||||
// 2. 加载字段映射
|
||||
const mappings = await prisma.iitFieldMapping.findMany({
|
||||
where: { projectId: this.projectId }
|
||||
});
|
||||
|
||||
for (const m of mappings) {
|
||||
this.fieldMappings.set(m.aliasName, m.actualName);
|
||||
}
|
||||
|
||||
logger.info('[HardRuleEngine] Field mappings loaded', {
|
||||
mappingCount: this.fieldMappings.size
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行质控检查
|
||||
*
|
||||
* @param recordId 记录ID
|
||||
* @param data 记录数据(REDCap 格式)
|
||||
* @returns 质控结果
|
||||
*/
|
||||
execute(recordId: string, data: Record<string, any>): QCResult {
|
||||
const startTime = Date.now();
|
||||
const results: RuleResult[] = [];
|
||||
const errors: RuleResult[] = [];
|
||||
const warnings: RuleResult[] = [];
|
||||
|
||||
// 1. 数据预处理:应用字段映射
|
||||
const normalizedData = this.normalizeData(data);
|
||||
|
||||
// 2. 执行每条规则
|
||||
for (const rule of this.rules) {
|
||||
const result = this.executeRule(rule, normalizedData);
|
||||
results.push(result);
|
||||
|
||||
if (!result.passed) {
|
||||
if (result.severity === 'error') {
|
||||
errors.push(result);
|
||||
} else if (result.severity === 'warning') {
|
||||
warnings.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 计算整体状态
|
||||
let overallStatus: 'PASS' | 'FAIL' | 'WARNING' = 'PASS';
|
||||
if (errors.length > 0) {
|
||||
overallStatus = 'FAIL';
|
||||
} else if (warnings.length > 0) {
|
||||
overallStatus = 'WARNING';
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
logger.info('[HardRuleEngine] QC completed', {
|
||||
recordId,
|
||||
overallStatus,
|
||||
totalRules: this.rules.length,
|
||||
errors: errors.length,
|
||||
warnings: warnings.length,
|
||||
duration: `${duration}ms`
|
||||
});
|
||||
|
||||
return {
|
||||
recordId,
|
||||
projectId: this.projectId,
|
||||
timestamp: new Date().toISOString(),
|
||||
overallStatus,
|
||||
summary: {
|
||||
totalRules: this.rules.length,
|
||||
passed: results.filter(r => r.passed).length,
|
||||
failed: errors.length,
|
||||
warnings: warnings.length
|
||||
},
|
||||
results,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量质控检查
|
||||
*
|
||||
* @param records 记录数组
|
||||
* @returns 质控结果数组
|
||||
*/
|
||||
executeBatch(records: Array<{ recordId: string; data: Record<string, any> }>): QCResult[] {
|
||||
return records.map(r => this.execute(r.recordId, r.data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单条规则
|
||||
*/
|
||||
private executeRule(rule: QCRule, data: Record<string, any>): RuleResult {
|
||||
try {
|
||||
// 获取字段值
|
||||
const fieldValue = this.getFieldValue(rule.field, data);
|
||||
|
||||
// 执行 JSON Logic
|
||||
const passed = jsonLogic.apply(rule.logic, data) as boolean;
|
||||
|
||||
return {
|
||||
ruleId: rule.id,
|
||||
ruleName: rule.name,
|
||||
field: rule.field,
|
||||
passed,
|
||||
message: passed ? '通过' : rule.message,
|
||||
severity: rule.severity,
|
||||
category: rule.category,
|
||||
actualValue: fieldValue,
|
||||
expectedCondition: this.describeLogic(rule.logic)
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error('[HardRuleEngine] Rule execution error', {
|
||||
ruleId: rule.id,
|
||||
error: error.message
|
||||
});
|
||||
|
||||
return {
|
||||
ruleId: rule.id,
|
||||
ruleName: rule.name,
|
||||
field: rule.field,
|
||||
passed: false,
|
||||
message: `规则执行出错: ${error.message}`,
|
||||
severity: 'error',
|
||||
category: rule.category
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段值(支持映射)
|
||||
*/
|
||||
private getFieldValue(field: string | string[], data: Record<string, any>): any {
|
||||
if (Array.isArray(field)) {
|
||||
return field.map(f => data[f]);
|
||||
}
|
||||
|
||||
// 先尝试直接获取
|
||||
if (data[field] !== undefined) {
|
||||
return data[field];
|
||||
}
|
||||
|
||||
// 再尝试通过映射获取
|
||||
const mappedField = this.fieldMappings.get(field);
|
||||
if (mappedField && data[mappedField] !== undefined) {
|
||||
return data[mappedField];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据标准化(应用字段映射,转换类型)
|
||||
*/
|
||||
private normalizeData(data: Record<string, any>): Record<string, any> {
|
||||
const normalized: Record<string, any> = { ...data };
|
||||
|
||||
// 1. 应用字段映射(反向映射:actualName -> aliasName)
|
||||
for (const [alias, actual] of this.fieldMappings.entries()) {
|
||||
if (data[actual] !== undefined && normalized[alias] === undefined) {
|
||||
normalized[alias] = data[actual];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 类型转换(字符串数字转数字)
|
||||
for (const [key, value] of Object.entries(normalized)) {
|
||||
if (typeof value === 'string' && value !== '' && !isNaN(Number(value))) {
|
||||
normalized[key] = Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 描述 JSON Logic 表达式(用于报告)
|
||||
*/
|
||||
private describeLogic(logic: Record<string, any>): string {
|
||||
const operator = Object.keys(logic)[0];
|
||||
const args = logic[operator];
|
||||
|
||||
switch (operator) {
|
||||
case '>=':
|
||||
return `>= ${args[1]}`;
|
||||
case '<=':
|
||||
return `<= ${args[1]}`;
|
||||
case '>':
|
||||
return `> ${args[1]}`;
|
||||
case '<':
|
||||
return `< ${args[1]}`;
|
||||
case '==':
|
||||
return `= ${args[1]}`;
|
||||
case '!=':
|
||||
return `≠ ${args[1]}`;
|
||||
case 'and':
|
||||
return args.map((a: any) => this.describeLogic(a)).join(' 且 ');
|
||||
case 'or':
|
||||
return args.map((a: any) => this.describeLogic(a)).join(' 或 ');
|
||||
case '!!':
|
||||
return '非空';
|
||||
default:
|
||||
return JSON.stringify(logic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规则列表
|
||||
*/
|
||||
getRules(): QCRule[] {
|
||||
return this.rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规则统计
|
||||
*/
|
||||
getRuleStats(): {
|
||||
total: number;
|
||||
byCategory: Record<string, number>;
|
||||
bySeverity: Record<string, number>;
|
||||
} {
|
||||
const byCategory: Record<string, number> = {};
|
||||
const bySeverity: Record<string, number> = {};
|
||||
|
||||
for (const rule of this.rules) {
|
||||
byCategory[rule.category] = (byCategory[rule.category] || 0) + 1;
|
||||
bySeverity[rule.severity] = (bySeverity[rule.severity] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
total: this.rules.length,
|
||||
byCategory,
|
||||
bySeverity
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工厂函数
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 创建并初始化 HardRuleEngine
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @param formName 可选,按表单名过滤规则(用于单表实时质控)
|
||||
* 如果不传,则加载所有规则(用于全案批量质控)
|
||||
*/
|
||||
export async function createHardRuleEngine(
|
||||
projectId: string,
|
||||
formName?: string
|
||||
): Promise<HardRuleEngine> {
|
||||
const engine = new HardRuleEngine(projectId);
|
||||
await engine.initialize(formName);
|
||||
return engine;
|
||||
}
|
||||
472
backend/src/modules/iit-manager/engines/SopEngine.ts
Normal file
472
backend/src/modules/iit-manager/engines/SopEngine.ts
Normal file
@@ -0,0 +1,472 @@
|
||||
/**
|
||||
* SopEngine - SOP 执行引擎(简化版)
|
||||
*
|
||||
* 功能:
|
||||
* - 状态机驱动的 SOP 执行
|
||||
* - 步骤自动执行
|
||||
* - 工具调用集成
|
||||
* - 结果汇总生成
|
||||
*
|
||||
* 状态流转:
|
||||
* PENDING → RUNNING → COMPLETED
|
||||
* ↘ FAILED
|
||||
* ↘ SUSPENDED (需人工确认)
|
||||
*
|
||||
* MVP 简化:
|
||||
* - 不实现 SUSPENDED(人工确认)机制
|
||||
* - 不实现持久化到数据库
|
||||
* - 专注于质控 SOP 闭环
|
||||
*/
|
||||
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
import { ToolsService, ToolResult, createToolsService } from '../services/ToolsService.js';
|
||||
|
||||
// ============================================================
|
||||
// 类型定义
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* SOP 任务状态
|
||||
*/
|
||||
export type SopStatus = 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'SUSPENDED';
|
||||
|
||||
/**
|
||||
* SOP 步骤定义
|
||||
*/
|
||||
export interface SopStep {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
tool: string; // 工具名称
|
||||
params: Record<string, any> | ((context: SopContext) => Record<string, any>); // 参数(静态或动态)
|
||||
onSuccess?: (result: ToolResult, context: SopContext) => void; // 成功回调
|
||||
onError?: (error: string, context: SopContext) => void; // 失败回调
|
||||
continueOnError?: boolean; // 失败时是否继续
|
||||
}
|
||||
|
||||
/**
|
||||
* SOP 定义
|
||||
*/
|
||||
export interface SopDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
steps: SopStep[];
|
||||
generateSummary: (context: SopContext) => string; // 生成汇总
|
||||
}
|
||||
|
||||
/**
|
||||
* SOP 执行上下文
|
||||
*/
|
||||
export interface SopContext {
|
||||
sopId: string;
|
||||
projectId: string;
|
||||
userId: string;
|
||||
sessionId?: string;
|
||||
variables: Record<string, any>; // 执行过程中的变量
|
||||
stepResults: Map<string, ToolResult>; // 步骤执行结果
|
||||
currentStepIndex: number;
|
||||
status: SopStatus;
|
||||
startTime: Date;
|
||||
endTime?: Date;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* SOP 执行结果
|
||||
*/
|
||||
export interface SopResult {
|
||||
sopId: string;
|
||||
sopName: string;
|
||||
status: SopStatus;
|
||||
summary: string;
|
||||
stepCount: number;
|
||||
successCount: number;
|
||||
failedCount: number;
|
||||
duration: number; // 毫秒
|
||||
stepResults: Array<{
|
||||
stepId: string;
|
||||
stepName: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 预定义 SOP:质控检查
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 单条记录质控 SOP
|
||||
*/
|
||||
export const QC_SINGLE_RECORD_SOP: SopDefinition = {
|
||||
id: 'qc_single_record',
|
||||
name: '单条记录质控',
|
||||
description: '对指定患者记录执行完整质控检查',
|
||||
steps: [
|
||||
{
|
||||
id: 'step_1',
|
||||
name: '获取项目信息',
|
||||
description: '获取当前研究项目基本信息',
|
||||
tool: 'get_project_info',
|
||||
params: {},
|
||||
onSuccess: (result, context) => {
|
||||
if (result.data) {
|
||||
context.variables.projectName = result.data.name;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'step_2',
|
||||
name: '读取患者数据',
|
||||
description: '从 REDCap 读取患者临床数据',
|
||||
tool: 'read_clinical_data',
|
||||
params: (context) => ({
|
||||
record_id: context.variables.recordId
|
||||
}),
|
||||
onSuccess: (result, context) => {
|
||||
if (result.data && result.data.length > 0) {
|
||||
context.variables.patientData = result.data[0];
|
||||
context.variables.hasData = true;
|
||||
} else {
|
||||
context.variables.hasData = false;
|
||||
}
|
||||
},
|
||||
continueOnError: false
|
||||
},
|
||||
{
|
||||
id: 'step_3',
|
||||
name: '执行质控检查',
|
||||
description: '验证患者数据是否符合纳入/排除标准和变量范围',
|
||||
tool: 'run_quality_check',
|
||||
params: (context) => ({
|
||||
record_id: context.variables.recordId
|
||||
}),
|
||||
onSuccess: (result, context) => {
|
||||
if (result.data) {
|
||||
context.variables.qcResult = result.data;
|
||||
}
|
||||
},
|
||||
continueOnError: false
|
||||
}
|
||||
],
|
||||
generateSummary: (context) => {
|
||||
const qcResult = context.variables.qcResult;
|
||||
if (!qcResult) {
|
||||
return `❌ 质控检查失败:无法获取记录 ${context.variables.recordId} 的数据`;
|
||||
}
|
||||
|
||||
const { overallStatus, summary, errors, warnings } = qcResult;
|
||||
let statusEmoji = '✅';
|
||||
if (overallStatus === 'FAIL') statusEmoji = '❌';
|
||||
else if (overallStatus === 'WARNING') statusEmoji = '⚠️';
|
||||
|
||||
let summaryText = `${statusEmoji} **记录 ${context.variables.recordId} 质控结果:${overallStatus}**\n\n`;
|
||||
summaryText += `📊 统计:通过 ${summary.passed}/${summary.totalRules} 条规则\n`;
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
summaryText += `\n❌ **错误(${errors.length}项):**\n`;
|
||||
for (const e of errors) {
|
||||
summaryText += `- ${e.rule}:${e.message}(当前值:${e.actualValue ?? '空'})\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings && warnings.length > 0) {
|
||||
summaryText += `\n⚠️ **警告(${warnings.length}项):**\n`;
|
||||
for (const w of warnings) {
|
||||
summaryText += `- ${w.rule}:${w.message}(当前值:${w.actualValue ?? '空'})\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (overallStatus === 'PASS') {
|
||||
summaryText += `\n🎉 该记录完全符合入组标准!`;
|
||||
}
|
||||
|
||||
return summaryText;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量质控 SOP
|
||||
*/
|
||||
export const QC_BATCH_SOP: SopDefinition = {
|
||||
id: 'qc_batch',
|
||||
name: '批量质控检查',
|
||||
description: '对所有患者记录执行质控检查并生成汇总报告',
|
||||
steps: [
|
||||
{
|
||||
id: 'step_1',
|
||||
name: '获取项目信息',
|
||||
description: '获取当前研究项目基本信息',
|
||||
tool: 'get_project_info',
|
||||
params: {},
|
||||
onSuccess: (result, context) => {
|
||||
if (result.data) {
|
||||
context.variables.projectName = result.data.name;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'step_2',
|
||||
name: '执行批量质控',
|
||||
description: '对所有记录执行质控检查',
|
||||
tool: 'batch_quality_check',
|
||||
params: {},
|
||||
onSuccess: (result, context) => {
|
||||
if (result.data) {
|
||||
context.variables.batchResult = result.data;
|
||||
}
|
||||
},
|
||||
continueOnError: false
|
||||
}
|
||||
],
|
||||
generateSummary: (context) => {
|
||||
const batchResult = context.variables.batchResult;
|
||||
if (!batchResult) {
|
||||
return `❌ 批量质控失败:无法获取数据`;
|
||||
}
|
||||
|
||||
if (batchResult.message) {
|
||||
return `📭 ${batchResult.message}`;
|
||||
}
|
||||
|
||||
const { totalRecords, summary, problemRecords } = batchResult;
|
||||
|
||||
let summaryText = `📊 **批量质控报告**\n\n`;
|
||||
summaryText += `**项目:** ${context.variables.projectName || '未知'}\n`;
|
||||
summaryText += `**记录总数:** ${totalRecords}\n\n`;
|
||||
|
||||
summaryText += `**质控结果:**\n`;
|
||||
summaryText += `- ✅ 通过:${summary.pass} (${summary.passRate})\n`;
|
||||
summaryText += `- ❌ 失败:${summary.fail}\n`;
|
||||
summaryText += `- ⚠️ 警告:${summary.warning}\n`;
|
||||
|
||||
if (problemRecords && problemRecords.length > 0) {
|
||||
summaryText += `\n**问题记录(前${problemRecords.length}条):**\n`;
|
||||
for (const r of problemRecords) {
|
||||
summaryText += `\n📌 **记录 ${r.recordId}** [${r.status}]\n`;
|
||||
for (const issue of r.issues || []) {
|
||||
summaryText += ` - ${issue.rule}:${issue.message}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summaryText;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// SopEngine 实现
|
||||
// ============================================================
|
||||
|
||||
export class SopEngine {
|
||||
private toolsService: ToolsService;
|
||||
private sops: Map<string, SopDefinition> = new Map();
|
||||
|
||||
constructor(toolsService: ToolsService) {
|
||||
this.toolsService = toolsService;
|
||||
|
||||
// 注册预定义 SOP
|
||||
this.registerSop(QC_SINGLE_RECORD_SOP);
|
||||
this.registerSop(QC_BATCH_SOP);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 SOP
|
||||
*/
|
||||
registerSop(sop: SopDefinition): void {
|
||||
this.sops.set(sop.id, sop);
|
||||
logger.debug('[SopEngine] SOP registered', { sopId: sop.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 SOP
|
||||
*/
|
||||
getSop(sopId: string): SopDefinition | undefined {
|
||||
return this.sops.get(sopId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 SOP
|
||||
*/
|
||||
async execute(
|
||||
sopId: string,
|
||||
userId: string,
|
||||
variables: Record<string, any> = {},
|
||||
sessionId?: string
|
||||
): Promise<SopResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
// 1. 获取 SOP 定义
|
||||
const sop = this.sops.get(sopId);
|
||||
if (!sop) {
|
||||
return {
|
||||
sopId,
|
||||
sopName: 'Unknown',
|
||||
status: 'FAILED',
|
||||
summary: `SOP "${sopId}" 不存在`,
|
||||
stepCount: 0,
|
||||
successCount: 0,
|
||||
failedCount: 0,
|
||||
duration: Date.now() - startTime,
|
||||
stepResults: []
|
||||
};
|
||||
}
|
||||
|
||||
logger.info('[SopEngine] Executing SOP', {
|
||||
sopId,
|
||||
sopName: sop.name,
|
||||
userId,
|
||||
variables
|
||||
});
|
||||
|
||||
// 2. 创建执行上下文
|
||||
const context: SopContext = {
|
||||
sopId,
|
||||
projectId: (this.toolsService as any).projectId,
|
||||
userId,
|
||||
sessionId,
|
||||
variables: { ...variables },
|
||||
stepResults: new Map(),
|
||||
currentStepIndex: 0,
|
||||
status: 'RUNNING',
|
||||
startTime: new Date()
|
||||
};
|
||||
|
||||
// 3. 逐步执行
|
||||
const stepResults: SopResult['stepResults'] = [];
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
for (let i = 0; i < sop.steps.length; i++) {
|
||||
const step = sop.steps[i];
|
||||
context.currentStepIndex = i;
|
||||
|
||||
logger.debug('[SopEngine] Executing step', {
|
||||
sopId,
|
||||
stepId: step.id,
|
||||
stepName: step.name
|
||||
});
|
||||
|
||||
// 3.1 计算参数
|
||||
const params = typeof step.params === 'function'
|
||||
? step.params(context)
|
||||
: step.params;
|
||||
|
||||
// 3.2 执行工具
|
||||
const result = await this.toolsService.execute(
|
||||
step.tool,
|
||||
params,
|
||||
userId,
|
||||
sessionId
|
||||
);
|
||||
|
||||
// 3.3 保存结果
|
||||
context.stepResults.set(step.id, result);
|
||||
|
||||
if (result.success) {
|
||||
successCount++;
|
||||
stepResults.push({
|
||||
stepId: step.id,
|
||||
stepName: step.name,
|
||||
success: true
|
||||
});
|
||||
|
||||
// 执行成功回调
|
||||
if (step.onSuccess) {
|
||||
step.onSuccess(result, context);
|
||||
}
|
||||
} else {
|
||||
failedCount++;
|
||||
stepResults.push({
|
||||
stepId: step.id,
|
||||
stepName: step.name,
|
||||
success: false,
|
||||
error: result.error
|
||||
});
|
||||
|
||||
// 执行失败回调
|
||||
if (step.onError) {
|
||||
step.onError(result.error || 'Unknown error', context);
|
||||
}
|
||||
|
||||
// 检查是否继续
|
||||
if (!step.continueOnError) {
|
||||
context.status = 'FAILED';
|
||||
context.error = result.error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 设置最终状态
|
||||
if (context.status !== 'FAILED') {
|
||||
context.status = 'COMPLETED';
|
||||
}
|
||||
context.endTime = new Date();
|
||||
|
||||
// 5. 生成汇总
|
||||
let summary: string;
|
||||
try {
|
||||
summary = sop.generateSummary(context);
|
||||
} catch (e: any) {
|
||||
summary = `SOP 执行${context.status === 'COMPLETED' ? '完成' : '失败'}`;
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
logger.info('[SopEngine] SOP execution completed', {
|
||||
sopId,
|
||||
status: context.status,
|
||||
stepCount: sop.steps.length,
|
||||
successCount,
|
||||
failedCount,
|
||||
duration: `${duration}ms`
|
||||
});
|
||||
|
||||
return {
|
||||
sopId,
|
||||
sopName: sop.name,
|
||||
status: context.status,
|
||||
summary,
|
||||
stepCount: sop.steps.length,
|
||||
successCount,
|
||||
failedCount,
|
||||
duration,
|
||||
stepResults
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行质控 SOP(便捷方法)
|
||||
*/
|
||||
async runQualityCheck(
|
||||
recordId: string,
|
||||
userId: string,
|
||||
sessionId?: string
|
||||
): Promise<SopResult> {
|
||||
return this.execute('qc_single_record', userId, { recordId }, sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行批量质控 SOP(便捷方法)
|
||||
*/
|
||||
async runBatchQualityCheck(
|
||||
userId: string,
|
||||
sessionId?: string
|
||||
): Promise<SopResult> {
|
||||
return this.execute('qc_batch', userId, {}, sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工厂函数
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 创建并初始化 SopEngine
|
||||
*/
|
||||
export async function createSopEngine(projectId: string): Promise<SopEngine> {
|
||||
const toolsService = await createToolsService(projectId);
|
||||
return new SopEngine(toolsService);
|
||||
}
|
||||
6
backend/src/modules/iit-manager/engines/index.ts
Normal file
6
backend/src/modules/iit-manager/engines/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* IIT Manager Agent 引擎层导出
|
||||
*/
|
||||
|
||||
export * from './HardRuleEngine.js';
|
||||
export * from './SopEngine.js';
|
||||
Reference in New Issue
Block a user