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:
@@ -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,
|
||||
|
||||
@@ -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 表
|
||||
-- 用于解决表头特殊字符问题
|
||||
|
||||
@@ -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");
|
||||
@@ -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分钟)';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 // 核心配置 JSON(SOP 流程图)
|
||||
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
|
||||
|
||||
359
backend/prisma/seed-iit-qc-rules.ts
Normal file
359
backend/prisma/seed-iit-qc-rules.ts
Normal 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();
|
||||
});
|
||||
Reference in New Issue
Block a user