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>
497 lines
14 KiB
Markdown
497 lines
14 KiB
Markdown
# IIT Manager Agent 数据库设计
|
||
|
||
> **版本:** V2.9
|
||
> **更新日期:** 2026-02-05
|
||
> **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md)
|
||
>
|
||
> **V2.9.1 更新**:
|
||
> - 扩展 `iit_skills` 表支持 Cron Skill(主动提醒)
|
||
> - 扩展 `iit_conversation_history` 表增加反馈字段
|
||
> - 更新 `iit_project_memory` 内容结构(用户画像)
|
||
> - **新增 `iit_pii_audit_log` 表**:PII 脱敏审计日志(合规必需)
|
||
|
||
---
|
||
|
||
## 1. 数据库表总览
|
||
|
||
| 表名 | 用途 | Phase | 优先级 |
|
||
|------|------|-------|--------|
|
||
| `iit_skills` | Skill 配置存储 | 1 | P0 |
|
||
| `iit_field_mapping` | 字段名映射字典 | 1 | P0 |
|
||
| `iit_pii_audit_log` | PII 脱敏审计日志 | 1.5 | P0 |
|
||
| `iit_task_run` | SOP 任务执行记录 | 2 | P0 |
|
||
| `iit_pending_actions` | 待处理的违规记录 | 2 | P0 |
|
||
| `iit_conversation_history` | 对话历史(流水账) | 2 | P1 |
|
||
| `iit_project_memory` | 项目级热记忆(Markdown) | 2 | P1 |
|
||
| `iit_weekly_reports` | 周报归档(历史书) | 4 | P1 |
|
||
| `iit_agent_trace` | ReAct 推理轨迹 | 5 | P2 |
|
||
| `iit_form_templates` | 表单模板(视觉识别) | 6 | 延后 |
|
||
|
||
---
|
||
|
||
## 2. Phase 1:基础配置表
|
||
|
||
### 2.1 iit_skills - Skill 配置存储
|
||
|
||
```prisma
|
||
model IitSkill {
|
||
id String @id @default(uuid())
|
||
projectId String // 绑定项目
|
||
skillType String // qc_process | daily_briefing | general_chat | weekly_report | visit_reminder
|
||
name String // 技能名称
|
||
config Json // 核心配置 JSON(SOP 流程图)
|
||
isActive Boolean @default(true)
|
||
version Int @default(1)
|
||
|
||
// V2.9 新增:主动触发能力
|
||
triggerType String @default("webhook") // 'webhook' | 'cron' | 'event'
|
||
cronSchedule String? // Cron 表达式,如 "0 9 * * *" (每天9点)
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([projectId, skillType])
|
||
@@map("iit_skills")
|
||
@@schema("iit_schema")
|
||
}
|
||
```
|
||
|
||
**触发类型说明**:
|
||
|
||
| triggerType | 触发方式 | 示例场景 |
|
||
|-------------|----------|----------|
|
||
| `webhook` | 用户消息触发(默认) | 质控任务、问答查询 |
|
||
| `cron` | 定时触发 | 访视提醒、周报生成 |
|
||
| `event` | 事件触发(预留) | AE 预警、数据变更通知 |
|
||
|
||
**Skill 配置示例(SOP 流程图)**:
|
||
|
||
```json
|
||
{
|
||
"name": "肺癌研究入组质控",
|
||
"start_node": "baseline_check",
|
||
"nodes": {
|
||
"baseline_check": {
|
||
"type": "hard_rule",
|
||
"rules": [
|
||
{ "field": "age", "logic": { ">=": [{"var":"age"}, 18] }, "message": "年龄<18岁" },
|
||
{ "field": "age", "logic": { "<=": [{"var":"age"}, 75] }, "message": "年龄>75岁" },
|
||
{ "field": "ecog", "logic": { "<=": [{"var":"ecog"}, 2] }, "message": "ECOG>2" }
|
||
],
|
||
"on_pass": "history_check",
|
||
"on_fail": "end_with_violation"
|
||
},
|
||
"history_check": {
|
||
"type": "soft_instruction",
|
||
"instruction": "检查既往史,排除间质性肺炎、活动性感染、严重心血管疾病。",
|
||
"tools": ["read_clinical_data"],
|
||
"on_pass": "medication_check",
|
||
"on_fail": "end_review_required"
|
||
},
|
||
"human_review": {
|
||
"type": "human_review",
|
||
"description": "需要 CRC 人工复核",
|
||
"on_approve": "end_success",
|
||
"on_reject": "end_rejected"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Cron Skill 配置示例(V2.9 新增)**:
|
||
|
||
```json
|
||
{
|
||
"skillType": "visit_reminder",
|
||
"name": "每日访视提醒",
|
||
"triggerType": "cron",
|
||
"cronSchedule": "0 9 * * *",
|
||
"config": {
|
||
"start_node": "check_upcoming_visits",
|
||
"nodes": {
|
||
"check_upcoming_visits": {
|
||
"type": "soft_instruction",
|
||
"instruction": "查询未来 3 天内到期的访视,生成提醒列表",
|
||
"tools": ["read_clinical_data"],
|
||
"on_pass": "send_reminder",
|
||
"on_fail": "end_no_visits"
|
||
},
|
||
"send_reminder": {
|
||
"type": "soft_instruction",
|
||
"instruction": "根据用户画像选择合适的通知方式和语气,发送提醒",
|
||
"tools": ["send_message"],
|
||
"on_pass": "end_success"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2.2 iit_field_mapping - 字段名映射字典
|
||
|
||
```prisma
|
||
model IitFieldMapping {
|
||
id String @id @default(uuid())
|
||
projectId String // 项目级别映射
|
||
aliasName String // LLM 可能传的名称(如 "gender", "性别")
|
||
actualName String // REDCap 实际字段名(如 "sex")
|
||
createdAt DateTime @default(now())
|
||
|
||
@@unique([projectId, aliasName])
|
||
@@index([projectId])
|
||
@@map("iit_field_mapping")
|
||
@@schema("iit_schema")
|
||
}
|
||
```
|
||
|
||
**初始化示例**:
|
||
|
||
```sql
|
||
INSERT INTO iit_field_mapping (project_id, alias_name, actual_name) VALUES
|
||
('project-uuid', 'age', 'age_calculated'),
|
||
('project-uuid', '年龄', 'age_calculated'),
|
||
('project-uuid', 'ecog', 'ecog_score'),
|
||
('project-uuid', '既往史', 'medical_history_text'),
|
||
('project-uuid', 'history', 'medical_history_text'),
|
||
('project-uuid', '性别', 'sex'),
|
||
('project-uuid', 'gender', 'sex');
|
||
```
|
||
|
||
---
|
||
|
||
## 2.5 Phase 1.5:隐私安全表(P0 合规必需)
|
||
|
||
### 2.5.1 iit_pii_audit_log - PII 脱敏审计日志
|
||
|
||
> **重要**:临床数据包含大量患者隐私信息(姓名、身份证、手机号),在调用第三方 LLM 之前**必须脱敏**。
|
||
> 此表用于存储脱敏记录,便于事后合规审计。
|
||
|
||
```prisma
|
||
model IitPiiAuditLog {
|
||
id String @id @default(uuid())
|
||
projectId String
|
||
userId String // 操作者
|
||
sessionId String // 会话 ID(关联 conversation_history)
|
||
|
||
// 脱敏内容(加密存储)
|
||
originalHash String // 原始内容的 SHA256 哈希(不存明文)
|
||
maskedPayload String @db.Text // 脱敏后发送给 LLM 的内容
|
||
maskingMap String @db.Text // 加密存储的映射表 { "[PATIENT_1]": "张三", ... }
|
||
|
||
// 元数据
|
||
piiCount Int // 检测到的 PII 数量
|
||
piiTypes String[] // 检测到的 PII 类型 ['name', 'id_card', 'phone']
|
||
llmProvider String // 'qwen' | 'deepseek' | 'openai'
|
||
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([projectId, userId])
|
||
@@index([sessionId])
|
||
@@index([createdAt])
|
||
@@map("iit_pii_audit_log")
|
||
@@schema("iit_schema")
|
||
}
|
||
```
|
||
|
||
**PII 类型说明**:
|
||
|
||
| PII 类型 | 正则模式 | 脱敏示例 |
|
||
|----------|----------|----------|
|
||
| `name` | 中文姓名(2-4字) | 张三 → [PATIENT_1] |
|
||
| `id_card` | 身份证号(18位) | 420101... → [ID_CARD_1] |
|
||
| `phone` | 手机号(11位) | 13800138000 → [PHONE_1] |
|
||
| `mrn` | 病历号 | MRN123456 → [MRN_1] |
|
||
|
||
**脱敏流程**:
|
||
|
||
```
|
||
用户输入: "张三(身份证420101199001011234)今天血压偏高"
|
||
↓ AnonymizerService.mask()
|
||
LLM 收到: "[PATIENT_1](身份证[ID_CARD_1])今天血压偏高"
|
||
↓ 同时写入 iit_pii_audit_log
|
||
↓ LLM 处理
|
||
LLM 返回: "[PATIENT_1] 的血压需要关注..."
|
||
↓ AnonymizerService.unmask()
|
||
用户看到: "张三 的血压需要关注..."
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Phase 2:SOP 执行与记忆表
|
||
|
||
### 3.1 iit_task_run - SOP 任务执行记录
|
||
|
||
```prisma
|
||
model IitTaskRun {
|
||
id String @id @default(uuid())
|
||
projectId String
|
||
skillId String // 关联的 Skill
|
||
recordId String? // 关联的患者(如有)
|
||
triggeredBy String // 触发者 userId
|
||
status String // RUNNING | SUSPENDED | COMPLETED | FAILED
|
||
currentNode String? // 当前执行到的节点
|
||
trace Json? // 执行轨迹
|
||
resumeCallback String? // SUSPENDED 时,恢复后的下一步
|
||
rejectCallback String? // SUSPENDED 被拒绝时的下一步
|
||
suspendedAt DateTime?
|
||
resumedAt DateTime?
|
||
completedAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([projectId, status])
|
||
@@index([recordId])
|
||
@@map("iit_task_run")
|
||
@@schema("iit_schema")
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.2 iit_pending_actions - 待处理的违规记录
|
||
|
||
```prisma
|
||
model IitPendingAction {
|
||
id String @id @default(uuid())
|
||
projectId String
|
||
taskRunId String // 来源任务
|
||
recordId String? // 关联患者
|
||
actionType String // violation | warning | review_required
|
||
field String? // 违规字段
|
||
message String // 违规描述
|
||
severity String // error | warning | info
|
||
resolvedBy String? // 处理人
|
||
resolvedAt DateTime?
|
||
resolution String? // 处理结论
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([projectId, actionType])
|
||
@@index([recordId])
|
||
@@map("iit_pending_actions")
|
||
@@schema("iit_schema")
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.3 iit_conversation_history - 对话历史(流水账)
|
||
|
||
> **V2.8 设计**:这是原始对话流水,不直接注入 Prompt,只用于生成周报
|
||
>
|
||
> **V2.9 新增**:增加反馈字段,支持用户点赞/点踩
|
||
|
||
```prisma
|
||
model IitConversationHistory {
|
||
id String @id @default(uuid())
|
||
projectId String
|
||
userId String
|
||
recordId String? // 关联的患者(如有)
|
||
role String // user | assistant
|
||
content String @db.Text
|
||
intent String? // 识别出的意图类型
|
||
entities Json? // 提取的实体 { record_id, visit, ... }
|
||
|
||
// V2.9 新增:反馈循环
|
||
feedback String? // 'thumbs_up' | 'thumbs_down' | null
|
||
feedbackReason String? // 点踩原因:'too_long' | 'inaccurate' | 'unclear'
|
||
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([projectId, userId])
|
||
@@index([projectId, recordId])
|
||
@@index([createdAt])
|
||
@@map("iit_conversation_history")
|
||
@@schema("iit_schema")
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.4 iit_project_memory - 项目级热记忆
|
||
|
||
> **V2.8 核心表**:存储 Markdown 格式的热记忆,每次对话都注入 System Prompt
|
||
>
|
||
> **V2.9 扩展**:增加用户画像结构
|
||
|
||
```prisma
|
||
model IitProjectMemory {
|
||
id String @id @default(uuid())
|
||
projectId String @unique
|
||
content String @db.Text // Markdown 格式的热记忆
|
||
lastUpdatedBy String // 'system_daily_job' | 'admin_user_id' | 'profiler_job'
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@map("iit_project_memory")
|
||
@@schema("iit_schema")
|
||
}
|
||
```
|
||
|
||
**内容示例(V2.9 增强版)**:
|
||
|
||
```markdown
|
||
# 用户画像 (User Profiles)
|
||
|
||
## 张医生 (user_id: zhangyi)
|
||
- **角色**: PI
|
||
- **偏好**: 简洁汇报,只看结论,不要废话
|
||
- **关注点**: AE、入组进度
|
||
- **最佳通知时间**: 08:30
|
||
- **禁令**: 回复不超过 100 字
|
||
- **反馈统计**: 👍 12 / 👎 1
|
||
|
||
## 王护士 (user_id: wanghushi)
|
||
- **角色**: CRC
|
||
- **偏好**: 详细步骤,需要解释
|
||
- **关注点**: 访视安排、数据录入
|
||
- **最佳通知时间**: 09:00
|
||
|
||
# 当前状态 (Active Context)
|
||
- 当前阶段:入组冲刺期
|
||
- 重点关注:P005 患者依从性差,需每日提醒
|
||
- 本周目标:完成 3 例入组
|
||
- P003 SAE 已判定为"可能无关"(2月5日 PI 决策)
|
||
|
||
# 常见问题 (FAQ)
|
||
- 访视窗口:V1 Day 1±3, V2 Day 28±7
|
||
|
||
# 系统禁令 (Rules)
|
||
- 严禁在未授权情况下删除数据
|
||
- 写操作必须经过人工确认
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Phase 4:周报归档
|
||
|
||
### 4.1 iit_weekly_reports - 周报归档(历史书)
|
||
|
||
> **V2.8 核心表**:存储每周的关键决策、进度、踩坑记录
|
||
|
||
```prisma
|
||
model IitWeeklyReport {
|
||
id String @id @default(uuid())
|
||
projectId String
|
||
weekNumber Int // 第几周(从项目开始计算)
|
||
weekStart DateTime // 周起始日期
|
||
weekEnd DateTime // 周结束日期
|
||
summary String @db.Text // Markdown 格式的周报内容
|
||
metrics Json? // 结构化指标 { enrolled: 3, queries: 5, ... }
|
||
createdBy String // 'system_scheduler' | 'admin_user_id'
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([projectId, weekNumber])
|
||
@@index([projectId])
|
||
@@map("iit_weekly_reports")
|
||
@@schema("iit_schema")
|
||
}
|
||
```
|
||
|
||
**内容示例**:
|
||
|
||
```markdown
|
||
## Week 5 (2026-02-03 ~ 2026-02-09)
|
||
|
||
### 进度
|
||
- 本周新入组:3 例
|
||
- 累计入组:15 / 30 例
|
||
- 完成率:50%
|
||
|
||
### 关键决策
|
||
- 2026-02-05: PI 会议决定放宽入排标准,允许 ECOG 3 分患者入组
|
||
- 2026-02-07: 确认 P003 AE 与研究药物"可能无关"
|
||
|
||
### 踩坑记录
|
||
- 曾尝试自动录入化验单,因 OCR 精度不足失败,已回退为人工复核
|
||
|
||
### 待办
|
||
- 清理 1 月份遗留的 Query
|
||
- 准备 2 月底期中分析数据
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Phase 5:ReAct 追踪表
|
||
|
||
### 5.1 iit_agent_trace - ReAct 推理轨迹
|
||
|
||
> 用于调试,不发送给用户,仅在 Admin 后台查看
|
||
|
||
```prisma
|
||
model IitAgentTrace {
|
||
id String @id @default(uuid())
|
||
projectId String
|
||
userId String
|
||
query String @db.Text // 用户原始问题
|
||
intentType String? // 识别的意图类型
|
||
trace Json // ReAct 的完整思考过程
|
||
tokenUsage Int? // 消耗的 Token 数
|
||
duration Int? // 执行时长(ms)
|
||
success Boolean
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([projectId, createdAt])
|
||
@@index([userId])
|
||
@@map("iit_agent_trace")
|
||
@@schema("iit_schema")
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Phase 6:视觉能力表(延后到 V3.0)
|
||
|
||
### 6.1 iit_form_templates - 表单模板
|
||
|
||
```prisma
|
||
model IitFormTemplate {
|
||
id String @id @default(uuid())
|
||
projectId String
|
||
formName String // REDCap 表单名称
|
||
fieldSchema Json // 表单字段结构
|
||
keywords String[] // 用于匹配的关键词
|
||
createdAt DateTime @default(now())
|
||
|
||
@@map("iit_form_templates")
|
||
@@schema("iit_schema")
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 数据库迁移命令
|
||
|
||
```bash
|
||
# 生成迁移
|
||
npx prisma db push
|
||
|
||
# 生成客户端
|
||
npx prisma generate
|
||
|
||
# 查看表结构
|
||
npx prisma studio
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 索引设计总结
|
||
|
||
| 表 | 索引 | 用途 |
|
||
|----|------|------|
|
||
| `iit_skills` | `[projectId, skillType]` (unique) | 按项目查询 Skill |
|
||
| `iit_field_mapping` | `[projectId, aliasName]` (unique) | 字段映射查询 |
|
||
| `iit_task_run` | `[projectId, status]` | 查询运行中/挂起的任务 |
|
||
| `iit_conversation_history` | `[projectId, userId]` | 按用户查询对话 |
|
||
| `iit_conversation_history` | `[projectId, recordId]` | 按患者查询对话 |
|
||
| `iit_conversation_history` | `[createdAt]` | 按时间范围查询 |
|
||
| `iit_weekly_reports` | `[projectId, weekNumber]` (unique) | 按周查询报告 |
|
||
| `iit_agent_trace` | `[projectId, createdAt]` | 按时间查询调试日志 |
|
||
|
||
---
|
||
|
||
**文档维护人**:AI Agent
|
||
**最后更新**:2026-02-05
|