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>
360 lines
13 KiB
TypeScript
360 lines
13 KiB
TypeScript
/**
|
||
* IIT Manager Agent - 质控规则初始化脚本
|
||
*
|
||
* 项目:原发性痛经队列研究
|
||
*
|
||
* 使用方法:
|
||
* npx tsx prisma/seed-iit-qc-rules.ts
|
||
*
|
||
* 创建日期:2026-02-07
|
||
*/
|
||
|
||
import { PrismaClient } from '@prisma/client';
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
// ============================================================
|
||
// 项目配置
|
||
// ============================================================
|
||
const PROJECT_ID = 'test0102-project-id'; // 需要替换为实际的项目 ID
|
||
|
||
// ============================================================
|
||
// 纳入标准规则
|
||
// ============================================================
|
||
const INCLUSION_RULES = [
|
||
{
|
||
id: 'inc_001',
|
||
name: '年龄范围检查',
|
||
field: 'age',
|
||
logic: {
|
||
and: [
|
||
{ '>=': [{ var: 'age' }, 16] },
|
||
{ '<=': [{ var: 'age' }, 35] }
|
||
]
|
||
},
|
||
message: '年龄不在 16-35 岁范围内',
|
||
severity: 'error',
|
||
category: 'inclusion'
|
||
},
|
||
{
|
||
id: 'inc_002',
|
||
name: '出生日期范围检查',
|
||
field: 'birth_date',
|
||
logic: {
|
||
and: [
|
||
{ '>=': [{ var: 'birth_date' }, '1989-01-01'] },
|
||
{ '<=': [{ var: 'birth_date' }, '2008-01-01'] }
|
||
]
|
||
},
|
||
message: '出生日期不在 1989-01-01 至 2008-01-01 范围内',
|
||
severity: 'error',
|
||
category: 'inclusion'
|
||
},
|
||
{
|
||
id: 'inc_003',
|
||
name: '月经周期规律性检查',
|
||
field: 'menstrual_cycle',
|
||
logic: {
|
||
and: [
|
||
{ '>=': [{ var: 'menstrual_cycle' }, 21] },
|
||
{ '<=': [{ var: 'menstrual_cycle' }, 35] }
|
||
]
|
||
},
|
||
message: '月经周期不在 21-35 天范围内(28±7天)',
|
||
severity: 'error',
|
||
category: 'inclusion'
|
||
},
|
||
{
|
||
id: 'inc_004',
|
||
name: 'VAS 评分检查',
|
||
field: 'vas_score',
|
||
logic: { '>=': [{ var: 'vas_score' }, 4] },
|
||
message: 'VAS 疼痛评分 < 4 分,不符合入组条件',
|
||
severity: 'error',
|
||
category: 'inclusion'
|
||
},
|
||
{
|
||
id: 'inc_005',
|
||
name: '知情同意书签署检查',
|
||
field: 'informed_consent',
|
||
logic: { '==': [{ var: 'informed_consent' }, 1] },
|
||
message: '未签署知情同意书',
|
||
severity: 'error',
|
||
category: 'inclusion'
|
||
}
|
||
];
|
||
|
||
// ============================================================
|
||
// 排除标准规则
|
||
// ============================================================
|
||
const EXCLUSION_RULES = [
|
||
{
|
||
id: 'exc_001',
|
||
name: '继发性痛经排除',
|
||
field: 'secondary_dysmenorrhea',
|
||
logic: { '!=': [{ var: 'secondary_dysmenorrhea' }, 1] },
|
||
message: '存在继发性痛经(盆腔炎、子宫内膜异位症、子宫腺肌病等)',
|
||
severity: 'error',
|
||
category: 'exclusion'
|
||
},
|
||
{
|
||
id: 'exc_002',
|
||
name: '妊娠哺乳期排除',
|
||
field: 'pregnancy_lactation',
|
||
logic: { '!=': [{ var: 'pregnancy_lactation' }, 1] },
|
||
message: '妊娠或哺乳期妇女,不符合入组条件',
|
||
severity: 'error',
|
||
category: 'exclusion'
|
||
},
|
||
{
|
||
id: 'exc_003',
|
||
name: '严重疾病排除',
|
||
field: 'severe_disease',
|
||
logic: { '!=': [{ var: 'severe_disease' }, 1] },
|
||
message: '合并有心脑血管、肝、肾、造血系统等严重疾病或精神病',
|
||
severity: 'error',
|
||
category: 'exclusion'
|
||
},
|
||
{
|
||
id: 'exc_004',
|
||
name: '月经周期不规律排除',
|
||
field: 'irregular_menstruation',
|
||
logic: { '!=': [{ var: 'irregular_menstruation' }, 1] },
|
||
message: '月经周期不规律或间歇性痛经发作',
|
||
severity: 'error',
|
||
category: 'exclusion'
|
||
}
|
||
];
|
||
|
||
// ============================================================
|
||
// 血常规变量范围规则
|
||
// ============================================================
|
||
const LAB_VALUE_RULES = [
|
||
// 1. 白细胞计数
|
||
{ id: 'lab_001', name: '白细胞计数', field: 'wbc', min: 3.5, max: 9.5, unit: '*10^9/L' },
|
||
// 2. 红细胞计数
|
||
{ id: 'lab_002', name: '红细胞计数', field: 'rbc', min: 3.8, max: 5.1, unit: '*10^12/L' },
|
||
// 3. 血红蛋白
|
||
{ id: 'lab_003', name: '血红蛋白', field: 'hgb', min: 115, max: 150, unit: 'g/L' },
|
||
// 4. 红细胞压积
|
||
{ id: 'lab_004', name: '红细胞压积', field: 'hct', min: 35, max: 45, unit: '%' },
|
||
// 5. 平均红细胞体积
|
||
{ id: 'lab_005', name: '平均红细胞体积', field: 'mcv', min: 82, max: 100, unit: 'fL' },
|
||
// 6. 平均血红蛋白量
|
||
{ id: 'lab_006', name: '平均血红蛋白量', field: 'mch', min: 27, max: 34, unit: 'pg' },
|
||
// 7. 平均血红蛋白浓度
|
||
{ id: 'lab_007', name: '平均血红蛋白浓度', field: 'mchc', min: 316, max: 354, unit: 'g/L' },
|
||
// 8. 红细胞体积分布宽度-SD
|
||
{ id: 'lab_008', name: '红细胞体积分布宽度-SD', field: 'rdw_sd', min: 37, max: 51, unit: 'fL' },
|
||
// 9. 红细胞体积分布宽度-CV
|
||
{ id: 'lab_009', name: '红细胞体积分布宽度-CV', field: 'rdw_cv', min: 0, max: 14.9, unit: '%' },
|
||
// 10. 血小板计数
|
||
{ id: 'lab_010', name: '血小板计数', field: 'plt', min: 125, max: 350, unit: '*10^9/L' },
|
||
// 11. 血小板平均体积
|
||
{ id: 'lab_011', name: '血小板平均体积', field: 'mpv', min: 7.2, max: 13.2, unit: 'fL' },
|
||
// 12. 血小板体积分布宽度
|
||
{ id: 'lab_012', name: '血小板体积分布宽度', field: 'pdw', min: 9, max: 13, unit: 'fL' },
|
||
// 13. 血小板比积
|
||
{ id: 'lab_013', name: '血小板比积', field: 'pct', min: 0.18, max: 0.22, unit: '%' },
|
||
// 14. 大血小板比率
|
||
{ id: 'lab_014', name: '大血小板比率', field: 'p_lcr', min: 13, max: 43, unit: '%' },
|
||
// 15. 中性粒细胞绝对值
|
||
{ id: 'lab_015', name: '中性粒细胞绝对值', field: 'neut_abs', min: 1.8, max: 6.3, unit: '*10^9/L' },
|
||
// 16. 淋巴细胞绝对值
|
||
{ id: 'lab_016', name: '淋巴细胞绝对值', field: 'lymph_abs', min: 1.1, max: 3.2, unit: '*10^9/L' },
|
||
// 17. 单核细胞绝对值
|
||
{ id: 'lab_017', name: '单核细胞绝对值', field: 'mono_abs', min: 0.1, max: 0.6, unit: '*10^9/L' },
|
||
// 18. 嗜酸细胞绝对值
|
||
{ id: 'lab_018', name: '嗜酸细胞绝对值', field: 'eo_abs', min: 0.02, max: 0.52, unit: '*10^9/L' },
|
||
// 19. 嗜碱细胞绝对值
|
||
{ id: 'lab_019', name: '嗜碱细胞绝对值', field: 'baso_abs', min: 0, max: 0.06, unit: '*10^9/L' },
|
||
// 20. 中性粒细胞相对值
|
||
{ id: 'lab_020', name: '中性粒细胞相对值', field: 'neut_pct', min: 40, max: 75, unit: '%' },
|
||
// 21. 淋巴细胞相对值
|
||
{ id: 'lab_021', name: '淋巴细胞相对值', field: 'lymph_pct', min: 20, max: 50, unit: '%' },
|
||
// 22. 单核细胞相对值
|
||
{ id: 'lab_022', name: '单核细胞相对值', field: 'mono_pct', min: 3, max: 10, unit: '%' },
|
||
// 23. 嗜酸细胞相对值
|
||
{ id: 'lab_023', name: '嗜酸细胞相对值', field: 'eo_pct', min: 0.4, max: 8, unit: '%' },
|
||
// 24. 嗜碱细胞相对值
|
||
{ id: 'lab_024', name: '嗜碱细胞相对值', field: 'baso_pct', min: 0, max: 1, unit: '%' },
|
||
];
|
||
|
||
// 将实验室检查转换为 JSON Logic 规则
|
||
const LAB_RULES = LAB_VALUE_RULES.map(item => ({
|
||
id: item.id,
|
||
name: `${item.name}范围检查`,
|
||
field: item.field,
|
||
logic: {
|
||
or: [
|
||
{ '==': [{ var: item.field }, null] }, // 允许为空(可选检查项)
|
||
{
|
||
and: [
|
||
{ '>=': [{ var: item.field }, item.min] },
|
||
{ '<=': [{ var: item.field }, item.max] }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
message: `${item.name}超出正常范围(${item.min}-${item.max} ${item.unit})`,
|
||
severity: 'warning',
|
||
category: 'lab_values',
|
||
metadata: { min: item.min, max: item.max, unit: item.unit }
|
||
}));
|
||
|
||
// ============================================================
|
||
// 字段映射配置
|
||
// ============================================================
|
||
const FIELD_MAPPINGS = [
|
||
// 基本信息
|
||
{ aliasName: '年龄', actualName: 'age', fieldType: 'number', fieldLabel: '年龄' },
|
||
{ aliasName: 'age', actualName: 'age', fieldType: 'number', fieldLabel: '年龄' },
|
||
{ aliasName: '出生日期', actualName: 'birth_date', fieldType: 'date', fieldLabel: '出生日期' },
|
||
{ aliasName: 'birth_date', actualName: 'birth_date', fieldType: 'date', fieldLabel: '出生日期' },
|
||
|
||
// 入排标准相关
|
||
{ aliasName: '月经周期', actualName: 'menstrual_cycle', fieldType: 'number', fieldLabel: '月经周期(天)' },
|
||
{ aliasName: 'menstrual_cycle', actualName: 'menstrual_cycle', fieldType: 'number', fieldLabel: '月经周期(天)' },
|
||
{ aliasName: 'VAS评分', actualName: 'vas_score', fieldType: 'number', fieldLabel: 'VAS疼痛评分' },
|
||
{ aliasName: 'vas_score', actualName: 'vas_score', fieldType: 'number', fieldLabel: 'VAS疼痛评分' },
|
||
{ aliasName: '知情同意', actualName: 'informed_consent', fieldType: 'radio', fieldLabel: '是否签署知情同意书' },
|
||
{ aliasName: 'informed_consent', actualName: 'informed_consent', fieldType: 'radio', fieldLabel: '是否签署知情同意书' },
|
||
|
||
// 排除标准相关
|
||
{ aliasName: '继发性痛经', actualName: 'secondary_dysmenorrhea', fieldType: 'radio', fieldLabel: '继发性痛经' },
|
||
{ aliasName: '妊娠哺乳', actualName: 'pregnancy_lactation', fieldType: 'radio', fieldLabel: '妊娠或哺乳期' },
|
||
{ aliasName: '严重疾病', actualName: 'severe_disease', fieldType: 'radio', fieldLabel: '严重疾病' },
|
||
{ aliasName: '月经不规律', actualName: 'irregular_menstruation', fieldType: 'radio', fieldLabel: '月经周期不规律' },
|
||
|
||
// 血常规字段映射
|
||
{ aliasName: '白细胞', actualName: 'wbc', fieldType: 'number', fieldLabel: '白细胞计数' },
|
||
{ aliasName: '红细胞', actualName: 'rbc', fieldType: 'number', fieldLabel: '红细胞计数' },
|
||
{ aliasName: '血红蛋白', actualName: 'hgb', fieldType: 'number', fieldLabel: '血红蛋白' },
|
||
{ aliasName: '血小板', actualName: 'plt', fieldType: 'number', fieldLabel: '血小板计数' },
|
||
// ... 更多字段可按需添加
|
||
];
|
||
|
||
// ============================================================
|
||
// 主函数
|
||
// ============================================================
|
||
async function main() {
|
||
console.log('🚀 开始初始化 IIT Manager 质控规则...\n');
|
||
|
||
// 1. 先获取项目 ID
|
||
const project = await prisma.iitProject.findFirst({
|
||
where: { name: 'test0102' }
|
||
});
|
||
|
||
if (!project) {
|
||
console.error('❌ 未找到项目 test0102,请先创建项目');
|
||
console.log('💡 提示:可以在 iit_schema.projects 表中创建项目');
|
||
return;
|
||
}
|
||
|
||
const projectId = project.id;
|
||
console.log(`✅ 找到项目: ${project.name} (${projectId})\n`);
|
||
|
||
// 2. 创建质控技能配置
|
||
console.log('📋 创建质控技能配置...');
|
||
|
||
const allRules = [...INCLUSION_RULES, ...EXCLUSION_RULES, ...LAB_RULES];
|
||
|
||
await prisma.iitSkill.upsert({
|
||
where: {
|
||
projectId_skillType: {
|
||
projectId: projectId,
|
||
skillType: 'qc_process'
|
||
}
|
||
},
|
||
update: {
|
||
name: '原发性痛经队列研究-入组质控',
|
||
description: '包含纳入标准、排除标准、血常规范围检查',
|
||
config: {
|
||
version: '1.0',
|
||
rules: allRules,
|
||
summary: {
|
||
totalRules: allRules.length,
|
||
inclusionRules: INCLUSION_RULES.length,
|
||
exclusionRules: EXCLUSION_RULES.length,
|
||
labRules: LAB_RULES.length
|
||
}
|
||
},
|
||
isActive: true,
|
||
triggerType: 'webhook',
|
||
updatedAt: new Date()
|
||
},
|
||
create: {
|
||
projectId: projectId,
|
||
skillType: 'qc_process',
|
||
name: '原发性痛经队列研究-入组质控',
|
||
description: '包含纳入标准、排除标准、血常规范围检查',
|
||
config: {
|
||
version: '1.0',
|
||
rules: allRules,
|
||
summary: {
|
||
totalRules: allRules.length,
|
||
inclusionRules: INCLUSION_RULES.length,
|
||
exclusionRules: EXCLUSION_RULES.length,
|
||
labRules: LAB_RULES.length
|
||
}
|
||
},
|
||
isActive: true,
|
||
triggerType: 'webhook'
|
||
}
|
||
});
|
||
|
||
console.log(` ✅ 已创建质控技能,共 ${allRules.length} 条规则`);
|
||
console.log(` - 纳入标准: ${INCLUSION_RULES.length} 条`);
|
||
console.log(` - 排除标准: ${EXCLUSION_RULES.length} 条`);
|
||
console.log(` - 血常规范围: ${LAB_RULES.length} 条\n`);
|
||
|
||
// 3. 创建字段映射
|
||
console.log('🔗 创建字段映射...');
|
||
|
||
for (const mapping of FIELD_MAPPINGS) {
|
||
await prisma.iitFieldMapping.upsert({
|
||
where: {
|
||
projectId_aliasName: {
|
||
projectId: projectId,
|
||
aliasName: mapping.aliasName
|
||
}
|
||
},
|
||
update: {
|
||
actualName: mapping.actualName,
|
||
fieldType: mapping.fieldType,
|
||
fieldLabel: mapping.fieldLabel
|
||
},
|
||
create: {
|
||
projectId: projectId,
|
||
aliasName: mapping.aliasName,
|
||
actualName: mapping.actualName,
|
||
fieldType: mapping.fieldType,
|
||
fieldLabel: mapping.fieldLabel
|
||
}
|
||
});
|
||
}
|
||
|
||
console.log(` ✅ 已创建 ${FIELD_MAPPINGS.length} 条字段映射\n`);
|
||
|
||
// 4. 输出汇总
|
||
console.log('=' .repeat(60));
|
||
console.log('📊 初始化完成汇总');
|
||
console.log('=' .repeat(60));
|
||
console.log(`项目名称: ${project.name}`);
|
||
console.log(`项目 ID: ${projectId}`);
|
||
console.log(`质控规则总数: ${allRules.length}`);
|
||
console.log(`字段映射总数: ${FIELD_MAPPINGS.length}`);
|
||
console.log('=' .repeat(60));
|
||
|
||
console.log('\n✅ IIT Manager 质控规则初始化完成!');
|
||
}
|
||
|
||
main()
|
||
.catch((e) => {
|
||
console.error('❌ 初始化失败:', e);
|
||
process.exit(1);
|
||
})
|
||
.finally(async () => {
|
||
await prisma.$disconnect();
|
||
});
|