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:
2026-02-07 21:56:11 +08:00
parent 0c590854b5
commit 5db4a7064c
74 changed files with 13383 additions and 2129 deletions

View File

@@ -1,3 +1,20 @@
-- CreateSchemas (added for shadow database compatibility)
CREATE SCHEMA IF NOT EXISTS "admin_schema";
CREATE SCHEMA IF NOT EXISTS "agent_schema";
CREATE SCHEMA IF NOT EXISTS "aia_schema";
CREATE SCHEMA IF NOT EXISTS "asl_schema";
CREATE SCHEMA IF NOT EXISTS "capability_schema";
CREATE SCHEMA IF NOT EXISTS "common_schema";
CREATE SCHEMA IF NOT EXISTS "dc_schema";
CREATE SCHEMA IF NOT EXISTS "ekb_schema";
CREATE SCHEMA IF NOT EXISTS "iit_schema";
CREATE SCHEMA IF NOT EXISTS "pkb_schema";
CREATE SCHEMA IF NOT EXISTS "platform_schema";
CREATE SCHEMA IF NOT EXISTS "protocol_schema";
CREATE SCHEMA IF NOT EXISTS "rvw_schema";
CREATE SCHEMA IF NOT EXISTS "ssa_schema";
CREATE SCHEMA IF NOT EXISTS "st_schema";
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,

View File

@@ -1,3 +1,22 @@
-- CreateTable (merged from create_tool_c_session.sql for shadow database compatibility)
CREATE TABLE IF NOT EXISTS "dc_schema"."dc_tool_c_sessions" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id VARCHAR(255) NOT NULL,
file_name VARCHAR(500) NOT NULL,
file_key VARCHAR(500) NOT NULL,
total_rows INTEGER NOT NULL,
total_cols INTEGER NOT NULL,
columns JSONB NOT NULL,
encoding VARCHAR(50),
file_size INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_dc_tool_c_sessions_user_id ON "dc_schema"."dc_tool_c_sessions"(user_id);
CREATE INDEX IF NOT EXISTS idx_dc_tool_c_sessions_expires_at ON "dc_schema"."dc_tool_c_sessions"(expires_at);
-- AlterTable
-- 添加 column_mapping 字段到 dc_tool_c_sessions 表
-- 用于解决表头特殊字符问题

View File

@@ -0,0 +1,162 @@
-- IIT Manager Agent V2.9.1 数据库迁移
-- 创建 8 张新表用于支持完整的 Agent 功能
-- 日期: 2026-02-07
-- CreateTable: iit_skills (Skill 配置存储)
CREATE TABLE "iit_schema"."skills" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"skill_type" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"config" JSONB NOT NULL,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"version" INTEGER NOT NULL DEFAULT 1,
"trigger_type" TEXT NOT NULL DEFAULT 'webhook',
"cron_schedule" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "skills_pkey" PRIMARY KEY ("id")
);
-- CreateTable: iit_field_mapping (字段名映射字典)
CREATE TABLE "iit_schema"."field_mapping" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"alias_name" TEXT NOT NULL,
"actual_name" TEXT NOT NULL,
"field_type" TEXT,
"field_label" TEXT,
"validation" JSONB,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "field_mapping_pkey" PRIMARY KEY ("id")
);
-- CreateTable: iit_conversation_history (对话历史/流水账)
CREATE TABLE "iit_schema"."conversation_history" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"record_id" TEXT,
"role" TEXT NOT NULL,
"content" TEXT NOT NULL,
"intent" TEXT,
"entities" JSONB,
"feedback" TEXT,
"feedback_reason" TEXT,
"embedding" vector(1536),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "conversation_history_pkey" PRIMARY KEY ("id")
);
-- CreateTable: iit_project_memory (项目级热记忆)
CREATE TABLE "iit_schema"."project_memory" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"content" TEXT NOT NULL,
"last_updated_by" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "project_memory_pkey" PRIMARY KEY ("id")
);
-- CreateTable: iit_weekly_reports (周报归档/历史书)
CREATE TABLE "iit_schema"."weekly_reports" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"week_number" INTEGER NOT NULL,
"week_start" TIMESTAMP(3) NOT NULL,
"week_end" TIMESTAMP(3) NOT NULL,
"summary" TEXT NOT NULL,
"metrics" JSONB,
"created_by" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "weekly_reports_pkey" PRIMARY KEY ("id")
);
-- CreateTable: iit_agent_trace (ReAct 推理轨迹)
CREATE TABLE "iit_schema"."agent_trace" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"query" TEXT NOT NULL,
"intent_type" TEXT,
"trace" JSONB NOT NULL,
"token_usage" INTEGER,
"duration" INTEGER,
"success" BOOLEAN NOT NULL DEFAULT true,
"error_msg" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "agent_trace_pkey" PRIMARY KEY ("id")
);
-- CreateTable: iit_pii_audit_log (PII 脱敏审计日志)
CREATE TABLE "iit_schema"."pii_audit_log" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"session_id" TEXT NOT NULL,
"original_hash" TEXT NOT NULL,
"masked_payload" TEXT NOT NULL,
"masking_map" TEXT NOT NULL,
"pii_count" INTEGER NOT NULL,
"pii_types" TEXT[],
"llm_provider" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "pii_audit_log_pkey" PRIMARY KEY ("id")
);
-- CreateTable: iit_form_templates (表单模板)
CREATE TABLE "iit_schema"."form_templates" (
"id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"form_name" TEXT NOT NULL,
"field_schema" JSONB NOT NULL,
"keywords" TEXT[],
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "form_templates_pkey" PRIMARY KEY ("id")
);
-- CreateIndex: iit_skills
CREATE INDEX "idx_iit_skill_project" ON "iit_schema"."skills"("project_id");
CREATE INDEX "idx_iit_skill_active" ON "iit_schema"."skills"("is_active");
CREATE UNIQUE INDEX "unique_iit_skill_project_type" ON "iit_schema"."skills"("project_id", "skill_type");
-- CreateIndex: iit_field_mapping
CREATE INDEX "idx_iit_field_mapping_project" ON "iit_schema"."field_mapping"("project_id");
CREATE UNIQUE INDEX "unique_iit_field_mapping" ON "iit_schema"."field_mapping"("project_id", "alias_name");
-- CreateIndex: iit_conversation_history
CREATE INDEX "idx_iit_conv_project_user" ON "iit_schema"."conversation_history"("project_id", "user_id");
CREATE INDEX "idx_iit_conv_project_record" ON "iit_schema"."conversation_history"("project_id", "record_id");
CREATE INDEX "idx_iit_conv_created" ON "iit_schema"."conversation_history"("created_at");
-- CreateIndex: iit_project_memory
CREATE UNIQUE INDEX "project_memory_project_id_key" ON "iit_schema"."project_memory"("project_id");
-- CreateIndex: iit_weekly_reports
CREATE INDEX "idx_iit_weekly_report_project" ON "iit_schema"."weekly_reports"("project_id");
CREATE UNIQUE INDEX "unique_iit_weekly_report" ON "iit_schema"."weekly_reports"("project_id", "week_number");
-- CreateIndex: iit_agent_trace
CREATE INDEX "idx_iit_trace_project_time" ON "iit_schema"."agent_trace"("project_id", "created_at");
CREATE INDEX "idx_iit_trace_user" ON "iit_schema"."agent_trace"("user_id");
-- CreateIndex: iit_pii_audit_log
CREATE INDEX "idx_iit_pii_project_user" ON "iit_schema"."pii_audit_log"("project_id", "user_id");
CREATE INDEX "idx_iit_pii_session" ON "iit_schema"."pii_audit_log"("session_id");
CREATE INDEX "idx_iit_pii_created" ON "iit_schema"."pii_audit_log"("created_at");
-- CreateIndex: iit_form_templates
CREATE INDEX "idx_iit_form_template_project" ON "iit_schema"."form_templates"("project_id");
CREATE UNIQUE INDEX "unique_iit_form_template" ON "iit_schema"."form_templates"("project_id", "form_name");

View File

@@ -1,105 +0,0 @@
-- 创建 Tool C Session 表
-- 日期: 2025-12-06
-- 用途: 科研数据编辑器会话管理
CREATE TABLE IF NOT EXISTS dc_schema.dc_tool_c_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id VARCHAR(255) NOT NULL,
file_name VARCHAR(500) NOT NULL,
file_key VARCHAR(500) NOT NULL,
-- 数据元信息
total_rows INTEGER NOT NULL,
total_cols INTEGER NOT NULL,
columns JSONB NOT NULL,
encoding VARCHAR(50),
file_size INTEGER NOT NULL,
-- 时间戳
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_dc_tool_c_sessions_user_id ON dc_schema.dc_tool_c_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_dc_tool_c_sessions_expires_at ON dc_schema.dc_tool_c_sessions(expires_at);
-- 添加注释
COMMENT ON TABLE dc_schema.dc_tool_c_sessions IS 'Tool C (科研数据编辑器) Session会话表';
COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.file_key IS 'OSS存储路径: dc/tool-c/sessions/{timestamp}-{fileName}';
COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.columns IS '列名数组 ["age", "gender", "diagnosis"]';
COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间创建后10分钟';

View File

@@ -1,71 +0,0 @@
-- ============================================================
-- EKB Schema 索引创建脚本
-- 执行时机prisma migrate 之后手动执行
-- 参考文档docs/02-通用能力层/03-RAG引擎/04-数据模型设计.md
-- ============================================================
-- 1. 确保 pgvector 扩展已启用
CREATE EXTENSION IF NOT EXISTS vector;
-- 2. 确保 pg_bigm 扩展已启用(中文关键词检索)
CREATE EXTENSION IF NOT EXISTS pg_bigm;
-- ===== MVP 阶段必须创建 =====
-- 3. HNSW 向量索引(语义检索核心)
-- 参数说明m=16 每层最大连接数ef_construction=64 构建时搜索范围
CREATE INDEX IF NOT EXISTS idx_ekb_chunk_embedding
ON "ekb_schema"."ekb_chunk"
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- ===== Phase 2 阶段使用(可预创建)=====
-- 4. pg_bigm 中文关键词索引
CREATE INDEX IF NOT EXISTS idx_ekb_chunk_content_bigm
ON "ekb_schema"."ekb_chunk"
USING gin (content gin_bigm_ops);
-- 5. 文档摘要关键词索引
CREATE INDEX IF NOT EXISTS idx_ekb_doc_summary_bigm
ON "ekb_schema"."ekb_document"
USING gin (summary gin_bigm_ops);
-- 6. 全文内容关键词索引
CREATE INDEX IF NOT EXISTS idx_ekb_doc_text_bigm
ON "ekb_schema"."ekb_document"
USING gin (extracted_text gin_bigm_ops);
-- ===== Phase 3 阶段使用(可预创建)=====
-- 7. JSONB GIN 索引metadata 查询加速)
CREATE INDEX IF NOT EXISTS idx_ekb_doc_metadata_gin
ON "ekb_schema"."ekb_document"
USING gin (metadata jsonb_path_ops);
-- 8. JSONB GIN 索引structuredData 查询加速)
CREATE INDEX IF NOT EXISTS idx_ekb_doc_structured_gin
ON "ekb_schema"."ekb_document"
USING gin (structured_data jsonb_path_ops);
-- 9. 标签数组索引
CREATE INDEX IF NOT EXISTS idx_ekb_doc_tags_gin
ON "ekb_schema"."ekb_document"
USING gin (tags);
-- 10. 切片元数据索引
CREATE INDEX IF NOT EXISTS idx_ekb_chunk_metadata_gin
ON "ekb_schema"."ekb_chunk"
USING gin (metadata jsonb_path_ops);
-- ===== 验证索引创建 =====
-- SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'ekb_schema';

View File

@@ -1,38 +0,0 @@
-- ============================================================
-- EKB Schema MVP 索引创建脚本
-- 执行时机prisma db push 之后手动执行
-- 说明MVP 阶段只创建 HNSW 向量索引pg_bigm 索引在 Phase 2 创建
-- ============================================================
-- 1. 确保 pgvector 扩展已启用
CREATE EXTENSION IF NOT EXISTS vector;
-- 2. HNSW 向量索引(语义检索核心)
-- 参数说明m=16 每层最大连接数ef_construction=64 构建时搜索范围
CREATE INDEX IF NOT EXISTS idx_ekb_chunk_embedding
ON "ekb_schema"."ekb_chunk"
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 3. JSONB GIN 索引(可选,提升查询性能)
CREATE INDEX IF NOT EXISTS idx_ekb_doc_metadata_gin
ON "ekb_schema"."ekb_document"
USING gin (metadata jsonb_path_ops);
CREATE INDEX IF NOT EXISTS idx_ekb_doc_structured_gin
ON "ekb_schema"."ekb_document"
USING gin (structured_data jsonb_path_ops);
-- 4. 标签数组索引
CREATE INDEX IF NOT EXISTS idx_ekb_doc_tags_gin
ON "ekb_schema"."ekb_document"
USING gin (tags);

View File

@@ -1,141 +0,0 @@
-- =====================================================
-- 全文复筛数据库迁移脚本(手动执行)
-- Schema: asl_schema
-- 日期: 2025-11-23
-- 说明: 只操作asl_schema不影响其他schema
-- =====================================================
-- 1. 修改 literatures 表,添加全文复筛相关字段
ALTER TABLE asl_schema.literatures
ADD COLUMN IF NOT EXISTS stage TEXT DEFAULT 'imported',
ADD COLUMN IF NOT EXISTS has_pdf BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS pdf_storage_type TEXT,
ADD COLUMN IF NOT EXISTS pdf_storage_ref TEXT,
ADD COLUMN IF NOT EXISTS pdf_status TEXT DEFAULT 'pending',
ADD COLUMN IF NOT EXISTS pdf_uploaded_at TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS full_text_storage_type TEXT,
ADD COLUMN IF NOT EXISTS full_text_storage_ref TEXT,
ADD COLUMN IF NOT EXISTS full_text_url TEXT,
ADD COLUMN IF NOT EXISTS full_text_format TEXT,
ADD COLUMN IF NOT EXISTS full_text_source TEXT,
ADD COLUMN IF NOT EXISTS full_text_token_count INTEGER,
ADD COLUMN IF NOT EXISTS full_text_extracted_at TIMESTAMP(3);
-- 添加索引
CREATE INDEX IF NOT EXISTS idx_literatures_stage ON asl_schema.literatures(stage);
CREATE INDEX IF NOT EXISTS idx_literatures_has_pdf ON asl_schema.literatures(has_pdf);
CREATE INDEX IF NOT EXISTS idx_literatures_pdf_status ON asl_schema.literatures(pdf_status);
-- 2. 创建 fulltext_screening_tasks 表
CREATE TABLE IF NOT EXISTS asl_schema.fulltext_screening_tasks (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
model_a TEXT NOT NULL,
model_b TEXT NOT NULL,
prompt_version TEXT,
status TEXT NOT NULL DEFAULT 'pending',
total_count INTEGER NOT NULL DEFAULT 0,
processed_count INTEGER NOT NULL DEFAULT 0,
success_count INTEGER NOT NULL DEFAULT 0,
failed_count INTEGER NOT NULL DEFAULT 0,
degraded_count INTEGER NOT NULL DEFAULT 0,
total_tokens INTEGER DEFAULT 0,
total_cost DOUBLE PRECISION DEFAULT 0,
started_at TIMESTAMP(3),
completed_at TIMESTAMP(3),
estimated_end_at TIMESTAMP(3),
error_message TEXT,
error_stack TEXT,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_fulltext_task_project FOREIGN KEY (project_id)
REFERENCES asl_schema.screening_projects(id) ON DELETE CASCADE
);
-- 添加索引
CREATE INDEX IF NOT EXISTS idx_fulltext_tasks_project_id ON asl_schema.fulltext_screening_tasks(project_id);
CREATE INDEX IF NOT EXISTS idx_fulltext_tasks_status ON asl_schema.fulltext_screening_tasks(status);
CREATE INDEX IF NOT EXISTS idx_fulltext_tasks_created_at ON asl_schema.fulltext_screening_tasks(created_at);
-- 3. 创建 fulltext_screening_results 表
CREATE TABLE IF NOT EXISTS asl_schema.fulltext_screening_results (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
project_id TEXT NOT NULL,
literature_id TEXT NOT NULL,
-- Model A (DeepSeek-V3) 结果
model_a_name TEXT,
model_a_status TEXT,
model_a_fields JSONB,
model_a_overall JSONB,
model_a_processing_log JSONB,
model_a_verification JSONB,
model_a_tokens INTEGER,
model_a_cost DOUBLE PRECISION,
model_a_error TEXT,
-- Model B (Qwen-Max) 结果
model_b_name TEXT,
model_b_status TEXT,
model_b_fields JSONB,
model_b_overall JSONB,
model_b_processing_log JSONB,
model_b_verification JSONB,
model_b_tokens INTEGER,
model_b_cost DOUBLE PRECISION,
model_b_error TEXT,
-- 验证结果
medical_logic_issues JSONB,
evidence_chain_issues JSONB,
-- 冲突检测
is_conflict BOOLEAN DEFAULT false,
conflict_severity TEXT,
conflict_fields TEXT[],
conflict_details JSONB,
review_priority INTEGER DEFAULT 50,
review_deadline TIMESTAMP(3),
-- 人工复核
final_decision TEXT,
final_decision_by TEXT,
final_decision_at TIMESTAMP(3),
exclusion_reason TEXT,
review_notes TEXT,
-- 处理状态
processing_status TEXT DEFAULT 'pending',
is_degraded BOOLEAN DEFAULT false,
degraded_model TEXT,
-- 元数据
processed_at TIMESTAMP(3),
prompt_version TEXT,
raw_output_a JSONB,
raw_output_b JSONB,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_fulltext_result_task FOREIGN KEY (task_id)
REFERENCES asl_schema.fulltext_screening_tasks(id) ON DELETE CASCADE,
CONSTRAINT fk_fulltext_result_project FOREIGN KEY (project_id)
REFERENCES asl_schema.screening_projects(id) ON DELETE CASCADE,
CONSTRAINT fk_fulltext_result_literature FOREIGN KEY (literature_id)
REFERENCES asl_schema.literatures(id) ON DELETE CASCADE,
CONSTRAINT unique_project_literature_fulltext UNIQUE (project_id, literature_id)
);
-- 添加索引
CREATE INDEX IF NOT EXISTS idx_fulltext_results_task_id ON asl_schema.fulltext_screening_results(task_id);
CREATE INDEX IF NOT EXISTS idx_fulltext_results_project_id ON asl_schema.fulltext_screening_results(project_id);
CREATE INDEX IF NOT EXISTS idx_fulltext_results_literature_id ON asl_schema.fulltext_screening_results(literature_id);
CREATE INDEX IF NOT EXISTS idx_fulltext_results_is_conflict ON asl_schema.fulltext_screening_results(is_conflict);
CREATE INDEX IF NOT EXISTS idx_fulltext_results_final_decision ON asl_schema.fulltext_screening_results(final_decision);
CREATE INDEX IF NOT EXISTS idx_fulltext_results_review_priority ON asl_schema.fulltext_screening_results(review_priority);
-- 完成
SELECT 'Migration completed successfully!' AS status;

View File

@@ -833,7 +833,8 @@ model IitProject {
id String @id @default(uuid())
name String
description String?
difyDatasetId String? @unique @map("dify_dataset_id")
difyDatasetId String? @unique @map("dify_dataset_id") // 已废弃,使用 knowledgeBaseId
knowledgeBaseId String? @map("knowledge_base_id") // 关联 ekb_schema.knowledge_bases
protocolFileKey String? @map("protocol_file_key")
cachedRules Json? @map("cached_rules")
fieldMappings Json @map("field_mappings")
@@ -851,6 +852,7 @@ model IitProject {
userMappings IitUserMapping[]
@@index([status, deletedAt])
@@index([knowledgeBaseId], map: "idx_iit_project_kb")
@@map("projects")
@@schema("iit_schema")
}
@@ -950,6 +952,336 @@ model IitAuditLog {
@@schema("iit_schema")
}
// ============================================================
// IIT Manager Agent 新增表 (V2.9.1)
// Phase 1-6 完整数据库设计
// ============================================================
/// Skill 配置存储表 - 存储质控规则、SOP流程图
/// 支持 webhook/cron/event 三种触发方式
model IitSkill {
id String @id @default(uuid())
projectId String @map("project_id")
skillType String @map("skill_type") // qc_process | daily_briefing | general_chat | weekly_report | visit_reminder
name String // 技能名称
description String? // 技能描述
config Json @db.JsonB // 核心配置 JSONSOP 流程图)
isActive Boolean @default(true) @map("is_active")
version Int @default(1)
// V2.9 新增:主动触发能力
triggerType String @default("webhook") @map("trigger_type") // 'webhook' | 'cron' | 'event'
cronSchedule String? @map("cron_schedule") // Cron 表达式,如 "0 9 * * *"
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([projectId, skillType], map: "unique_iit_skill_project_type")
@@index([projectId], map: "idx_iit_skill_project")
@@index([isActive], map: "idx_iit_skill_active")
@@map("skills")
@@schema("iit_schema")
}
/// 字段名映射字典表 - 解决 LLM 生成的字段名与 REDCap 实际字段名不一致的问题
model IitFieldMapping {
id String @id @default(uuid())
projectId String @map("project_id")
aliasName String @map("alias_name") // LLM 可能传的名称(如 "gender", "性别"
actualName String @map("actual_name") // REDCap 实际字段名(如 "sex"
fieldType String? @map("field_type") // 字段类型text, number, date, radio, checkbox
fieldLabel String? @map("field_label") // 字段显示标签
validation Json? @db.JsonB // 验证规则 { min, max, pattern, choices }
createdAt DateTime @default(now()) @map("created_at")
@@unique([projectId, aliasName], map: "unique_iit_field_mapping")
@@index([projectId], map: "idx_iit_field_mapping_project")
@@map("field_mapping")
@@schema("iit_schema")
}
/// 对话历史表(流水账)- 存储原始对话,用于生成周报
/// V2.9 新增反馈字段支持用户点赞/点踩
model IitConversationHistory {
id String @id @default(uuid())
projectId String @map("project_id")
userId String @map("user_id")
recordId String? @map("record_id") // 关联的患者(如有)
role String // user | assistant
content String @db.Text
intent String? // 识别出的意图类型
entities Json? @db.JsonB // 提取的实体 { record_id, visit, ... }
// V2.9 新增:反馈循环
feedback String? @map("feedback") // 'thumbs_up' | 'thumbs_down' | null
feedbackReason String? @map("feedback_reason") // 点踩原因:'too_long' | 'inaccurate' | 'unclear'
// 向量嵌入(用于语义搜索)
embedding Unsupported("vector(1536)")?
createdAt DateTime @default(now()) @map("created_at")
@@index([projectId, userId], map: "idx_iit_conv_project_user")
@@index([projectId, recordId], map: "idx_iit_conv_project_record")
@@index([createdAt], map: "idx_iit_conv_created")
@@map("conversation_history")
@@schema("iit_schema")
}
/// 项目级热记忆表 - 存储 Markdown 格式的热记忆
/// 每次对话都注入 System Prompt包含用户画像、当前状态、系统禁令
model IitProjectMemory {
id String @id @default(uuid())
projectId String @unique @map("project_id")
content String @db.Text // Markdown 格式的热记忆
lastUpdatedBy String @map("last_updated_by") // 'system_daily_job' | 'admin_user_id' | 'profiler_job'
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("project_memory")
@@schema("iit_schema")
}
/// 周报归档表(历史书)- 存储每周的关键决策、进度、踩坑记录
model IitWeeklyReport {
id String @id @default(uuid())
projectId String @map("project_id")
weekNumber Int @map("week_number") // 第几周(从项目开始计算)
weekStart DateTime @map("week_start") // 周起始日期
weekEnd DateTime @map("week_end") // 周结束日期
summary String @db.Text // Markdown 格式的周报内容
metrics Json? @db.JsonB // 结构化指标 { enrolled, queries, ... }
createdBy String @map("created_by") // 'system_scheduler' | 'admin_user_id'
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([projectId, weekNumber], map: "unique_iit_weekly_report")
@@index([projectId], map: "idx_iit_weekly_report_project")
@@map("weekly_reports")
@@schema("iit_schema")
}
/// ReAct 推理轨迹表 - 用于调试,记录 AI 的思考过程
model IitAgentTrace {
id String @id @default(uuid())
projectId String @map("project_id")
userId String @map("user_id")
query String @db.Text // 用户原始问题
intentType String? @map("intent_type") // 识别的意图类型
trace Json @db.JsonB // ReAct 的完整思考过程
tokenUsage Int? @map("token_usage") // 消耗的 Token 数
duration Int? // 执行时长ms
success Boolean @default(true)
errorMsg String? @map("error_msg")
createdAt DateTime @default(now()) @map("created_at")
@@index([projectId, createdAt], map: "idx_iit_trace_project_time")
@@index([userId], map: "idx_iit_trace_user")
@@map("agent_trace")
@@schema("iit_schema")
}
/// PII 脱敏审计日志表 - 记录所有发送给 LLM 的脱敏信息(合规必需)
/// 暂时创建表结构,功能延后到 Phase 1.5 实现
model IitPiiAuditLog {
id String @id @default(uuid())
projectId String @map("project_id")
userId String @map("user_id") // 操作者
sessionId String @map("session_id") // 会话 ID
// 脱敏内容(加密存储)
originalHash String @map("original_hash") // 原始内容的 SHA256 哈希
maskedPayload String @db.Text @map("masked_payload") // 脱敏后发送给 LLM 的内容
maskingMap String @db.Text @map("masking_map") // 加密存储的映射表
// 元数据
piiCount Int @map("pii_count") // 检测到的 PII 数量
piiTypes String[] @map("pii_types") // 检测到的 PII 类型
llmProvider String @map("llm_provider") // 'qwen' | 'deepseek' | 'openai'
createdAt DateTime @default(now()) @map("created_at")
@@index([projectId, userId], map: "idx_iit_pii_project_user")
@@index([sessionId], map: "idx_iit_pii_session")
@@index([createdAt], map: "idx_iit_pii_created")
@@map("pii_audit_log")
@@schema("iit_schema")
}
/// 表单模板表Phase 6 视觉能力)- 预留表结构
model IitFormTemplate {
id String @id @default(uuid())
projectId String @map("project_id")
formName String @map("form_name") // REDCap 表单名称
fieldSchema Json @db.JsonB @map("field_schema") // 表单字段结构
keywords String[] // 用于匹配的关键词
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([projectId, formName], map: "unique_iit_form_template")
@@index([projectId], map: "idx_iit_form_template_project")
@@map("form_templates")
@@schema("iit_schema")
}
// ============================================================
// IIT Manager Agent - 实时质控系统表 (V2.9.1)
// 参考文档: docs/03-业务模块/IIT Manager Agent/04-开发计划/06-实时质控系统开发计划.md
// ============================================================
/// REDCap 字段元数据表 - 从 REDCap 同步的字段信息,用于自动生成质控规则
/// 注意:与 IitFieldMapping别名映射表不同此表存储 REDCap 原始元数据
model IitFieldMetadata {
id String @id @default(uuid())
projectId String @map("project_id")
// REDCap 字段信息
fieldName String @map("field_name") // REDCap 字段名
fieldLabel String @map("field_label") // 字段标签(中文)
fieldType String @map("field_type") // text, radio, checkbox, dropdown, etc.
formName String @map("form_name") // ⭐ 所属表单名,用于单表质控规则过滤
sectionHeader String? @map("section_header") // 所属区段
// 验证规则(从 REDCap 元数据提取)
validation String? @map("validation") // text_validation_type
validationMin String? @map("validation_min") // 最小值
validationMax String? @map("validation_max") // 最大值
choices String? @map("choices") // 选项,如 "1, 男 | 2, 女"
required Boolean @default(false) // 是否必填
branching String? @map("branching") // 分支逻辑
// LLM 友好名称(可选)
alias String?
// 规则来源标记
ruleSource String? @map("rule_source") // 'auto' | 'manual'
// 时间戳
syncedAt DateTime @default(now()) @map("synced_at")
@@unique([projectId, fieldName], map: "unique_iit_field_metadata")
@@index([projectId], map: "idx_iit_field_metadata_project")
@@index([projectId, formName], map: "idx_iit_field_metadata_form")
@@map("field_metadata")
@@schema("iit_schema")
}
/// 质控日志表 - 仅新增,不覆盖,保留完整审计轨迹
/// 设计原则:每次质控都新增记录,用于审计轨迹和趋势分析
model IitQcLog {
id String @id @default(uuid())
projectId String @map("project_id")
recordId String @map("record_id")
eventId String? @map("event_id")
// 质控类型
qcType String @map("qc_type") // 'form' | 'holistic'
formName String? @map("form_name") // 单表质控时记录表单名
// 核心结果
status String // 'PASS' | 'FAIL' | 'WARNING'
// 字段级详情 (JSONB)
// 格式: [{ field: "age", rule: "range_check", level: "RED", message: "..." }, ...]
issues Json @default("[]") @db.JsonB
// 规则统计
rulesEvaluated Int @default(0) @map("rules_evaluated") // 实际评估的规则数
rulesSkipped Int @default(0) @map("rules_skipped") // 逻辑守卫跳过的规则数
rulesPassed Int @default(0) @map("rules_passed")
rulesFailed Int @default(0) @map("rules_failed")
// 规则版本(用于历史追溯)
ruleVersion String @map("rule_version")
// 入排标准检查(全案质控时填充)
inclusionPassed Boolean? @map("inclusion_passed")
exclusionPassed Boolean? @map("exclusion_passed")
// 审计信息
triggeredBy String @map("triggered_by") // 'webhook' | 'manual' | 'batch'
createdAt DateTime @default(now()) @map("created_at")
// 索引 - 支持历史查询和趋势分析
@@index([projectId, recordId, createdAt], map: "idx_iit_qc_log_record_time")
@@index([projectId, status, createdAt], map: "idx_iit_qc_log_status_time")
@@index([projectId, qcType, createdAt], map: "idx_iit_qc_log_type_time")
@@map("qc_logs")
@@schema("iit_schema")
}
/// 录入汇总表 - 记录入组和录入进度
/// 设计原则:使用 upsert每个记录只有一条汇总
model IitRecordSummary {
id String @id @default(uuid())
projectId String @map("project_id")
recordId String @map("record_id")
// 入组信息
enrolledAt DateTime? @map("enrolled_at") // 首次录入时间 = 入组时间
enrolledBy String? @map("enrolled_by") // 入组录入人REDCap username
// 最新录入信息
lastUpdatedAt DateTime @map("last_updated_at")
lastUpdatedBy String? @map("last_updated_by")
lastFormName String? @map("last_form_name") // 最后更新的表单
// 表单完成状态 (JSONB)
// 格式: { "demographics": 2, "baseline": 1, "visit1": 0 }
// 0=未开始, 1=进行中, 2=完成
formStatus Json @default("{}") @db.JsonB @map("form_status")
// 数据完整度
totalForms Int @default(0) @map("total_forms")
completedForms Int @default(0) @map("completed_forms")
completionRate Float @default(0) @map("completion_rate") // 0-100%
// 最新质控状态(冗余存储,查询更快)
latestQcStatus String? @map("latest_qc_status") // 'PASS' | 'FAIL' | 'WARNING'
latestQcAt DateTime? @map("latest_qc_at")
// 更新次数(用于趋势分析)
updateCount Int @default(0) @map("update_count")
// 时间戳
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 唯一约束 - 每个记录只有一条汇总
@@unique([projectId, recordId], map: "unique_iit_record_summary")
@@index([projectId, enrolledAt], map: "idx_iit_record_summary_enrolled")
@@index([projectId, latestQcStatus], map: "idx_iit_record_summary_qc_status")
@@index([projectId, completionRate], map: "idx_iit_record_summary_completion")
@@map("record_summary")
@@schema("iit_schema")
}
/// 项目级汇总表 - 用于 Dashboard 快速展示
/// 设计原则:使用 upsert每个项目只有一条汇总
model IitQcProjectStats {
id String @id @default(uuid())
projectId String @unique @map("project_id")
// 汇总统计
totalRecords Int @default(0) @map("total_records")
passedRecords Int @default(0) @map("passed_records")
failedRecords Int @default(0) @map("failed_records")
warningRecords Int @default(0) @map("warning_records")
// 入排标准统计
inclusionMet Int @default(0) @map("inclusion_met")
exclusionMet Int @default(0) @map("exclusion_met")
// 录入进度统计
avgCompletionRate Float @default(0) @map("avg_completion_rate")
// 更新时间
updatedAt DateTime @updatedAt @map("updated_at")
@@map("qc_project_stats")
@@schema("iit_schema")
}
model admin_operation_logs {
id Int @id @default(autoincrement())
admin_id String

View File

@@ -0,0 +1,359 @@
/**
* 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();
});