Files
AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/02-技术设计/IIT Manager Agent 完整技术开发方案 (V1.0).md
HaHafeng 1b53ab9d52 feat(aia): Complete AIA V2.0 with universal streaming capabilities
Major Changes:
- Add StreamingService with OpenAI Compatible format
- Upgrade Chat component V2 with Ant Design X integration
- Implement AIA module with 12 intelligent agents
- Update API routes to unified /api/v1 prefix
- Update system documentation

Backend (~1300 lines):
- common/streaming: OpenAI Compatible adapter
- modules/aia: 12 agents, conversation service, streaming integration
- Update route versions (RVW, PKB to v1)

Frontend (~3500 lines):
- modules/aia: AgentHub + ChatWorkspace (100% prototype restoration)
- shared/Chat: AIStreamChat, ThinkingBlock, useAIStream Hook
- Update API endpoints to v1

Documentation:
- AIA module status guide
- Universal capabilities catalog
- System overview updates
- All module documentation sync

Tested: Stream response verified, authentication working
Status: AIA V2.0 core completed (85%)
2026-01-14 19:15:01 +08:00

1868 lines
64 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# IIT Manager Agent 瀹屾暣鎶€鏈<E282AC>紑鍙戞柟妗?(V1.1)
> **鏂囨。鐗堟湰锛?* V1.1锛堟灦鏋勮瘎瀹′慨姝g増锛?
> **鍒涘缓鏃ユ湡锛?* 2025-12-31
> **鏈€鍚庢洿鏂帮細** 2025-12-31
> **缁存姢鑰咃細** 鏋舵瀯鍥㈤槦
> **閫傜敤闃舵<E99783>锛?* MVP + Phase 1-4 瀹屾暣寮€鍙?
> **鏂囨。鐩<E38082>殑锛?* 鍩轰簬鐜版湁绯荤粺鏋舵瀯锛屾彁渚涘彲鐩存帴鎵ц<E98EB5>鐨勬妧鏈<E5A6A7>疄鏂芥柟妗?
> **V1.1 鏇存柊锛?* 鏁村悎鏋舵瀯璇勫<E79287>鎰忚<E98EB0>锛屼慨姝綉缁滆繛閫氭€ч<E282AC>闄┿€佸<E282AC>鍔犲巻鍙叉暟鎹<E69A9F>壂鎻忋€佹槑纭<E6A791>墠绔<E5A2A0>妧鏈<E5A6A7>
---
## 馃敟 V1.1 鏍稿績淇<E7B8BE><E6B787>
鍩轰簬鏋舵瀯璇勫<EFBFBD>锛堝弬鑰冿細`06-寮€鍙戣<E98D99>褰?IIT Manager Agent 鎶€鏈<E282AC>柟妗堝<E5A697>鏌ヤ笌琛ヤ竵.md`锛夛紝鏈<EFBFBD>増鏈<EFBFBD>慨姝簡3涓<EFBFBD>叧閿<EFBFBD>棶棰橈細
1. **鉁?鑷村懡椋庨櫓淇<E6AB93><E6B787>**锛氭贩鍚堝悓姝ユā寮忥紙Webhook + 杞<><E69D9E>锛夛紝瑙喅鍖婚櫌鍐呯綉杩為€氭€ч棶棰?2. **鉁?鍔熻兘琛ュ厖**锛氬巻鍙叉暟鎹<E69A9F>叏閲忔壂鎻忥紝鏀<E7B49D>寔瀛橀噺鏁版嵁璐ㄦ帶
3. **鉁?鎶€鏈<E282AC>爤鏄庣**锛氬墠绔<E5A2A0>噰鐢═aro锛圧eact璇<74>硶锛夛紝鏀<E7B49D>寔灏忕▼搴?H5鍙岀<E98D99>
---
## 馃搵 鏂囨。瀵艰埅
1. [绯荤粺鏋舵瀯璁捐<EFBFBD>](#1-绯荤粺鏋舵瀯璁捐<E79281>)
2. [鐜版湁鑳藉姏澶嶇敤](#2-鐜版湁鑳藉姏澶嶇敤)
3. [鏍稿績鎶€鏈<E282AC>疄鐜癩(#3-鏍稿績鎶€鏈<E282AC>疄鐜?
4. [鏁版嵁搴撹<E690B4><EFBFBD>(#4-鏁版嵁搴撹<E690B4>璁?
5. [API璁捐<EFBFBD>](#5-api璁捐<E79281>)
6. [閮ㄧ讲鏋舵瀯](#6-閮ㄧ讲鏋舵瀯)
7. [寮€鍙戣<E98D99>鍒抅(#7-寮€鍙戣<E98D99>鍒?
8. [椋庨櫓璇勪及涓庡<E6B693>绛朷(#8-椋庨櫓璇勪及涓庡<E6B693>绛?
---
## 1. 绯荤粺鏋舵瀯璁捐<E79281>
### 1.1 鎬讳綋鏋舵瀯锛堝熀浜庣幇鏈夊钩鍙帮級
```
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹? 鐢ㄦ埛浜や簰灞?(Frontend) 鈹?鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹? 鈹?浼佷笟寰<E7AC9F>俊 鈹? 鈹?寰<>俊灏忕▼搴?鈹? 鈹?PC 鈹? 鈹?鈹? 鈹?(閫氱煡) 鈹? 鈹?(PI鏌ョ湅) 鈹? 鈹?Workbench 鈹? 鈹?鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈫?鈫?REST API / WebSocket
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹? 涓氬姟妯″潡灞?(IIT Manager Module) 鈹?鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹? 鈹? Node.js Backend (Fastify + TypeScript) 鈹? 鈹?鈹? 鈹? 鈹溾攢鈹€ controllers/ - HTTP璺<50>敱澶勭悊 鈹? 鈹?鈹? 鈹? 鈹溾攢鈹€ services/ - 涓氬姟閫昏緫灞? 鈹? 鈹?鈹? 鈹? 鈹溾攢鈹€ agents/ - 4涓<34>櫤鑳戒綋 鈹? 鈹?鈹? 鈹? 鈹斺攢鈹€ adapters/ - 澶栭儴绯荤粺閫傞厤鍣? 鈹? 鈹?鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈫?鈫?澶嶇敤骞冲彴鑳藉姏
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹? 骞冲彴鍩虹<E98DA9>灞?(Platform - 宸叉湁) 鈹?鈹? 鉁?Storage (OSS/Local) 鉁?Logger (Winston) 鈹?鈹? 鉁?Cache (Postgres) 鉁?JobQueue (pg-boss) 鈹?鈹? 鉁?LLMFactory (澶氭ā鍨? 鉁?CheckpointService 鈹?鈹? 鉁?DifyClient (RAG) 鉁?Database (Prisma) 鈹?鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈫?鈫?
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹? 澶栭儴绯荤粺闆嗘垚灞?(External) 鈹?鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹? 鈹?REDCap 鈹? 鈹?Dify RAG 鈹? 鈹?浼佷笟寰<E7AC9F>俊 鈹? 鈹?Python 鈹? 鈹?鈹? 鈹?(EDC) 鈹? 鈹?(鐭ヨ瘑搴? 鈹? 鈹?(閫氱煡) 鈹? 鈹?寰<>湇鍔? 鈹? 鈹?鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈫?鈫?鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹? 鏁版嵁瀛樺偍灞?(Storage) 鈹?鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹? 鈹?RDS PostgreSQL 鈹? 鈹?OSS瀵硅薄瀛樺偍 鈹? 鈹?鈹? 鈹?(涓氬姟鏁版嵁+闃熷垪) 鈹? 鈹?(鏂囦欢/Protocol) 鈹? 鈹?鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?```
### 1.2 鏋舵瀯浜<E780AF>偣锛堢<E9949B>鍚堢幇鏈夎<E98F88>鑼冿級
#### 鉁?1. 瀹屽叏澶嶇敤骞冲彴鑳藉姏
```typescript
// 鉁?涓嶉噸澶嶅疄鐜板熀纭€璁炬柦
import { storage } from '@/common/storage'; // 鏂囦欢瀛樺偍
import { logger } from '@/common/logging'; // 鏃ュ織绯荤粺
import { jobQueue } from '@/common/jobs'; // 寮傛<E5AFAE>浠诲姟
import { cache } from '@/common/cache'; // Postgres缂撳瓨
import { CheckpointService } from '@/common/jobs'; // 鏂<>偣缁<E581A3>
import { LLMFactory } from '@/common/llm'; // LLM璋冪敤
import { DifyClient } from '@/clients/DifyClient'; // RAG妫€绱?import { prisma } from '@/config/database'; // 鏁版嵁搴?```
#### 鉁?2. Postgres-Only 鏋舵瀯锛堥伒寰<E4BC92><E5AFB0>鑼冿級
```typescript
// 浠诲姟绠$悊淇℃伅瀛樺偍鍦?job.data锛屼笟鍔¤〃鍙<E38083>瓨鍌ㄤ笟鍔俊鎭?await jobQueue.push('iit:quality-check:batch', {
// 涓氬姟淇℃伅
projectId: 'proj_001',
recordIds: ['P001', 'P002', ...],
// 鉁?浠诲姟鎷嗗垎淇℃伅锛堣嚜鍔ㄥ瓨鍌ㄥ湪 platform_schema.job.data锛? batchIndex: 1,
totalBatches: 10,
// 鉁?杩涘害杩借釜锛堣嚜鍔ㄥ瓨鍌<E793A8>
processedCount: 0,
successCount: 0,
failedCount: 0
});
// 浣跨敤 CheckpointService 绠悊鏂<E6828A>
const checkpointService = new CheckpointService(prisma);
await checkpointService.saveCheckpoint(job.id, {
currentBatchIndex: 5,
currentIndex: 250
});
```
#### 鉁?3. Schema 闅旂<E99785>锛堟柊澧?iit_schema锛?
```prisma
// prisma/schema.prisma
// 鐜版湁Schema锛歱latform, aia, pkb, asl, dc, ssa, st, rvw, admin, common
// 鏂板<E98F82>Schema锛歩it
generator client {
provider = "prisma-client-js"
previewFeatures = ["multiSchema"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
schemas = ["platform", "aia", "pkb", "asl", "dc", "iit"] // 鏂板<E98F82> iit
}
// IIT Manager 鐨勬墍鏈夎〃閮藉湪 iit_schema 涓?model IitProject {
id String @id @default(uuid())
// ...
@@schema("iit")
}
```
#### 鉁?4. 浜戝師鐢熷氨缁<E6B0A8>紙SAE閮ㄧ讲锛?
- 鏃犵姸鎬佸簲鐢<E7B0B2>紙涓嶄緷璧栨湰鍦版枃浠讹級
- 瀛樺偍鎶借薄灞傦紙Local 鈫?OSS 闆朵唬鐮佸垏鎹<E59E8F>
- 寮傛<E5AFAE>浠诲姟锛堥伩鍏?0绉掕秴鏃讹級
- 鏁版嵁搴撹繛鎺ユ睜锛堥槻姝㈣繛鎺ヨ€楀敖锛?
---
## 2. 鐜版湁鑳藉姏澶嶇敤
### 2.1 Dify RAG 闆嗘垚锛堝凡鏈夊熀纭€锛?
#### 鐜版湁鑳藉姏锛圥KB妯″潡锛?
```typescript
// backend/src/clients/DifyClient.ts (宸叉湁 282琛屼唬鐮?
export class DifyClient {
async createDataset(name: string): Promise<string>;
async uploadDocument(datasetId: string, file: Buffer): Promise<string>;
async query(datasetId: string, query: string): Promise<QueryResult>;
// ... 鍏朵粬鏂规硶
}
```
#### IIT Manager 浣跨敤鏂瑰紡
```typescript
// backend/src/modules/iit-manager/services/ProtocolService.ts
import { DifyClient } from '@/clients/DifyClient';
export class ProtocolService {
private difyClient: DifyClient;
constructor() {
this.difyClient = new DifyClient(
process.env.DIFY_API_KEY!,
process.env.DIFY_BASE_URL!
);
}
/**
* 涓洪」鐩<E3808D>垱寤篜rotocol鐭ヨ瘑搴? */
async initializeProtocolKnowledgeBase(
projectId: string,
protocolPdf: Buffer
): Promise<string> {
// 1. 鍒涘缓Dify Dataset
const datasetId = await this.difyClient.createDataset(
`IIT_Project_${projectId}_Protocol`
);
// 2. 涓婁紶Protocol PDF
const documentId = await this.difyClient.uploadDocument(
datasetId,
protocolPdf
);
// 3. 淇濆瓨鍒版暟鎹<E69A9F>
await prisma.iitProject.update({
where: { id: projectId },
data: { difyDatasetId: datasetId }
});
return datasetId;
}
/**
* 妫€鏌ユ暟鎹<E69A9F>槸鍚︾<E98D9A>鍚圥rotocol锛堣川鎺<E5B79D>gent鏍稿績锛? */
async checkProtocolCompliance(params: {
projectId: string;
fieldName: string;
value: any;
context: Record<string, any>;
}): Promise<ComplianceResult> {
// 1. 鑾峰彇椤圭洰鐨凞ify鐭ヨ瘑搴揑D
const project = await prisma.iitProject.findUnique({
where: { id: params.projectId },
select: { difyDatasetId: true }
});
// 2. 鏋勯€犳煡璇<E785A1>rompt
const query = `
€呮暟鎹<E69A9F>${JSON.stringify(params.context)}
褰撳墠瀛楁<E7809B>锛?{params.fieldName} = ${params.value}
璇锋<E79287>鏌ユ<E98F8C>鏁版嵁鏄<E5B581>惁绗﹀悎鐮旂┒鏂规<E98F82>锛圥rotocol锛夌殑瑕佹眰銆? 濡傛灉鍙戠幇闂<E5B987><E99782>锛岃<E9949B>鎸囧嚭锛? 1. 杩濆弽浜嗗摢鏉¤<E98F89>鍒? 2. 璇ヨ<E79287>鍒欏湪鏂规<E98F82><EFBFBD>殑椤电爜
3. 姝g‘鐨勫€煎簲璇ユ槸浠€涔? 4. 缃<>俊搴︼紙0-1锛? `;
// 3. 璋冪敤Dify RAG妫€绱? const result = await this.difyClient.query(
project.difyDatasetId,
query
);
// 4. 瑙瀽AI鍝嶅簲
return this.parseComplianceResult(result);
}
}
```
### 2.2 LLM 璋冪敤锛堝凡鏈夊伐鍘傦級
```typescript
// 鉁?澶嶇敤鐜版湁 LLMFactory
import { LLMFactory } from '@/common/llm';
const llm = LLMFactory.getAdapter('deepseek-v3');
const response = await llm.chat([
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userInput }
]);
```
### 2.3 寮傛<E5AFAE>浠诲姟闃熷垪锛圥ostgres-Only锛?
```typescript
// 鉁?浣跨敤 pg-boss 闃熷垪锛堝钩鍙板凡鏈夛級
import { jobQueue } from '@/common/jobs';
// 鎺ㄩ€佽川鎺т换鍔?await jobQueue.push('iit:quality-check:batch', {
projectId: 'proj_001',
recordIds: ['P001', 'P002', 'P003']
});
// Worker澶勭悊锛堣嚜鍔ㄦ柇鐐圭画浼狅級
jobQueue.registerWorker('iit:quality-check:batch', async (job) => {
const checkpointService = new CheckpointService(prisma);
// 鍔犺浇鏂<E6B587>
const checkpoint = await checkpointService.loadCheckpoint(job.id);
const startIndex = checkpoint?.currentIndex || 0;
// 鎵归噺澶勭悊
for (let i = startIndex; i < job.data.recordIds.length; i++) {
await processRecord(job.data.recordIds[i]);
// 姣?0鏉繚瀛樻柇鐐? if (i % 10 === 0) {
await checkpointService.saveCheckpoint(job.id, {
currentIndex: i,
processedCount: i
});
}
}
});
```
### 2.4 鏂囦欢瀛樺偍锛圤SS鎶借薄灞傦級
```typescript
// 鉁?浣跨敤瀛樺偍鎶借薄灞?import { storage } from '@/common/storage';
// 涓婁紶Protocol PDF
const key = `iit/projects/${projectId}/protocol.pdf`;
const url = await storage.upload(key, pdfBuffer);
// 涓嬭浇Protocol PDF
const pdfBuffer = await storage.download(key);
```
### 2.5 鏃ュ織绯荤粺锛圵inston锛?
```typescript
// 鉁?浣跨敤骞冲彴鏃ュ織绯荤粺
import { logger } from '@/common/logging';
logger.info('Quality check started', {
projectId,
recordId,
agent: 'DataQualityAgent'
});
logger.error('Quality check failed', {
error: err.message,
stack: err.stack,
projectId,
recordId
});
```
---
## 3. 鏍稿績鎶€鏈<E282AC>疄鐜?
### 3.1 REDCap 闆嗘垚锛堝弻鍚戝<E98D9A>鎺ワ級
#### 3.1.1 REDCap External Module锛圥HP渚э級
```php
<?php
// redcap/modules/iit_manager_connector_v1.0.0/IITManagerConnector.php
namespace YiZhengXun\IITManagerConnector;
use ExternalModules\AbstractExternalModule;
class IITManagerConnector extends AbstractExternalModule {
/**
* Hook: 褰撹<E8A4B0>褰曚繚瀛樻椂瑙﹀彂
*/
public function redcap_save_record($project_id, $record, $instrument,
$event_id, $group_id, $survey_hash,
$response_id, $repeat_instance) {
// 1. 鑾峰彇鍙樻洿鏁版嵁
$data = \REDCap::getData($project_id, 'array', $record);
// 2. 鎺ㄩ€乄ebhook鍒癐IT Manager
$this->pushWebhook([
'event' => 'record_updated',
'project_id' => $project_id,
'record_id' => $record,
'instrument' => $instrument,
'event_id' => $event_id,
'data' => $data,
'timestamp' => time()
]);
}
/**
* 鎺ㄩ€乄ebhook锛堝甫绛惧悕楠岃瘉锛? */
private function pushWebhook($payload) {
$apiKey = $this->getSystemSetting('iit_manager_api_key');
$webhookUrl = $this->getSystemSetting('iit_manager_webhook_url');
// HMAC-SHA256绛惧悕
$signature = hash_hmac('sha256', json_encode($payload), $apiKey);
$ch = curl_init($webhookUrl);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-Signature: ' . $signature,
'X-Timestamp: ' . time()
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode !== 200) {
// 璁板綍鍒癛EDCap鏃ュ織
\REDCap::logEvent('IIT Manager Webhook Failed',
"HTTP $httpCode: $response", '', $record);
}
curl_close($ch);
}
}
```
#### 3.1.2 Node.js Webhook鎺ユ敹鍣?
```typescript
// backend/src/modules/iit-manager/controllers/webhookController.ts
import { FastifyRequest, FastifyReply } from 'fastify';
import { logger } from '@/common/logging';
import { jobQueue } from '@/common/jobs';
import crypto from 'crypto';
interface RedcapWebhookPayload {
event: 'record_updated' | 'record_created' | 'record_deleted';
project_id: string;
record_id: string;
instrument: string;
event_id: string;
data: Record<string, any>;
timestamp: number;
}
export async function handleRedcapWebhook(
request: FastifyRequest<{ Body: RedcapWebhookPayload }>,
reply: FastifyReply
) {
// 1. 楠岃瘉绛惧悕
const signature = request.headers['x-signature'] as string;
const timestamp = request.headers['x-timestamp'] as string;
if (!verifyWebhookSignature(request.body, signature, timestamp)) {
logger.warn('Invalid webhook signature', {
project_id: request.body.project_id
});
return reply.code(403).send({ error: 'Invalid signature' });
}
// 2. 闃查噸鏀炬敾鍑伙紙5鍒嗛挓鏈夋晥鏈燂級
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return reply.code(403).send({ error: 'Timestamp expired' });
}
// 3. 绔嬪嵆杩斿洖200锛堜笉闃诲<E99783>REDCap锛? reply.code(200).send({ status: 'accepted' });
// 4. 寮傛<E5AFAE>瑙﹀彂璐ㄦ帶妫€鏌ワ紙涓嶇瓑寰呭畬鎴愶級
setImmediate(async () => {
try {
const { project_id, record_id, data } = request.body;
// 鎺ㄩ€佸埌璐ㄦ帶闃熷垪
await jobQueue.push('iit:quality-check', {
projectId: project_id,
recordId: record_id,
data: data
});
logger.info('Quality check queued', {
project_id,
record_id
});
} catch (error) {
logger.error('Failed to queue quality check', {
error: error.message,
payload: request.body
});
}
});
}
/**
* 楠岃瘉Webhook绛惧悕
*/
function verifyWebhookSignature(
payload: any,
signature: string,
timestamp: string
): boolean {
const apiKey = process.env.REDCAP_WEBHOOK_SECRET!;
const expectedSignature = crypto
.createHmac('sha256', apiKey)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
```
#### 3.1.3 REDCap API 閫傞厤鍣<E58EA4>紙鏁版嵁鍥炲啓锛?
```typescript
// backend/src/modules/iit-manager/adapters/RedcapAdapter.ts
import axios, { AxiosInstance } from 'axios';
import { logger } from '@/common/logging';
export class RedcapAdapter {
private client: AxiosInstance;
private projectApiToken: string;
constructor(redcapUrl: string, apiToken: string) {
this.projectApiToken = apiToken;
this.client = axios.create({
baseURL: redcapUrl,
timeout: 30000
});
}
/**
* 瀵煎嚭璁板綍
*/
async exportRecords(params: {
records?: string[];
fields?: string[];
events?: string[];
}): Promise<any[]> {
const response = await this.client.post('', {
token: this.projectApiToken,
content: 'record',
action: 'export',
format: 'json',
type: 'flat',
records: params.records,
fields: params.fields,
events: params.events
});
return response.data;
}
/**
* 瀵煎叆璁板綍锛堝奖瀛愮姸鎬佸洖鍐欙級
*/
async importRecords(records: Record<string, any>[]): Promise<void> {
try {
const response = await this.client.post('', {
token: this.projectApiToken,
content: 'record',
action: 'import',
format: 'json',
type: 'flat',
overwriteBehavior: 'normal',
data: JSON.stringify(records)
});
logger.info('REDCap records imported', {
count: response.data.count,
ids: response.data.ids
});
} catch (error) {
logger.error('REDCap import failed', {
error: error.message,
records: records.map(r => r.record_id)
});
throw error;
}
}
/**
* 瀵煎嚭鍏冩暟鎹<E69A9F>紙琛ㄥ崟缁撴瀯锛? */
async exportMetadata(): Promise<any[]> {
const response = await this.client.post('', {
token: this.projectApiToken,
content: 'metadata',
format: 'json'
});
return response.data;
}
}
```
#### 3.1.4 娣峰悎鍚屾<E98D9A>妯″紡锛堭煍?V1.1 鏍稿績淇<E7B8BE><E6B787>锛?
```typescript
// backend/src/modules/iit-manager/services/SyncManager.ts
import { logger } from '@/common/logging';
import { jobQueue } from '@/common/jobs';
import { cache } from '@/common/cache';
import { prisma } from '@/config/database';
import { RedcapAdapter } from '../adapters/RedcapAdapter';
/**
* 鍚屾<E98D9A>悊鍣<E6828A>細瑙喅鍖婚櫌鍐呯綉杩為€氭€ч棶棰? *
* 鏍稿績绛栫暐锛? * 1. 浼樺厛浣跨敤Webhook锛堝疄鏃舵€э級- 閫傜敤浜嶳EDCap鍙<70><E98D99><EFBFBD>叕缃戠殑鍦烘櫙
* 2. 瀹氭椂杞<E6A482><E69D9E>浣滀负鍏滃簳锛堝彲闈犳€э級- 閫傜敤浜庢墍鏈夊満鏅? */
export class SyncManager {
private redcapAdapter: RedcapAdapter;
constructor(redcapAdapter: RedcapAdapter) {
this.redcapAdapter = redcapAdapter;
}
/**
* 鏅鸿兘鍚屾<E98D9A>绛栫暐锛堣嚜閫傚簲锛? * 鍚<>姩鏃舵祴璇昗ebhook杩為€氭€э紝鑷<E7B49D>姩閫夋嫨鏈€浣虫ā寮? */
async initializeSync(projectId: string) {
logger.info('Initializing sync strategy', { projectId });
// 1. 娴嬭瘯Webhook杩為€氭€? const webhookWorking = await this.testWebhookConnectivity(projectId);
if (webhookWorking) {
logger.info('Webhook connectivity OK, using real-time mode', { projectId });
// 杞<><E69D9E>浣滀负澶囦唤锛堥棿闅?0鍒嗛挓锛? await this.schedulePolling(projectId, 30);
} else {
logger.warn('Webhook blocked by firewall, using polling mode', { projectId });
// 杞<><E69D9E>浣滀负涓绘ā寮忥紙闂撮殧5鍒嗛挓锛? await this.schedulePolling(projectId, 5);
}
}
/**
* 娴嬭瘯Webhook杩為€氭€? */
private async testWebhookConnectivity(projectId: string): Promise<boolean> {
try {
const project = await prisma.iitProject.findUnique({
where: { id: projectId },
select: { redcapUrl: true }
});
// 璋冪敤REDCap EM鐨勬祴璇曠<E79287>鐐? const response = await axios.post(
`${project.redcapUrl}/api/?type=module&prefix=iit_manager_connector&page=test`,
{ projectId, test: 'ping' },
{ timeout: 5000 }
);
return response.status === 200;
} catch (error) {
logger.warn('Webhook connectivity test failed', {
projectId,
error: error.message
});
return false;
}
}
/**
* 瀹氭椂杞<E6A482><E69D9E>锛堟牳蹇冨厹搴曟満鍒讹級
*/
async schedulePolling(projectId: string, intervalMinutes: number = 10) {
// 浣跨敤 pg-boss 鐨?schedule 鍔熻兘
await jobQueue.schedule(
'iit:redcap:poll',
{ projectId },
{
every: `${intervalMinutes} minutes`,
// 閲嶈<E996B2>锛氳<E9949B><EFBFBD>悎鐞嗙殑瓒呮椂鏃堕棿
expireIn: `${intervalMinutes * 2} minutes`
}
);
logger.info('Polling scheduled', {
projectId,
intervalMinutes
});
}
/**
* 杞<><E69D9E>澶勭悊鍣<E6828A>紙Worker锛? */
async handlePoll(projectId: string) {
const startTime = Date.now();
try {
// 1. 鑾峰彇涓婃<E6B693>鍚屾<E98D9A>鏃堕棿锛堜粠缂撳瓨鎴栨暟鎹<E69A9F>簱锛? const cacheKey = `iit:sync:${projectId}:last`;
const lastSync = await cache.get(cacheKey) ||
(await this.getLastSyncFromDB(projectId));
logger.debug('Polling started', { projectId, lastSync });
// 2. 璋冪敤REDCap API鑾峰彇淇<E5BD87>敼鐨勮<E990A8>褰曪紙杞婚噺绾э級
// REDCap API鏀<49>寔鎸夋椂闂磋繃婊わ細dateRangeBegin
const records = await this.redcapAdapter.exportRecords({
dateRangeBegin: lastSync,
fields: ['record_id', 'last_modified'] // 鍏堝彧鎷塈D鍜屾椂闂存埑
});
if (records.length === 0) {
logger.debug('No new records to sync', { projectId });
return;
}
logger.info('New records detected', {
projectId,
count: records.length,
since: lastSync
});
// 3. 鎵归噺鎺ㄩ€佽川鎺т换鍔★紙鏅鸿兘闃堝€煎垽鏂<E59EBD>
const THRESHOLD = 50;
if (records.length >= THRESHOLD) {
// 澶ф壒閲忥細闃熷垪妯″紡 + 浠诲姟鎷嗗垎
const chunks = this.splitIntoChunks(records, 50);
for (const chunk of chunks) {
await jobQueue.push('iit:quality-check:batch', {
projectId,
recordIds: chunk.map(r => r.record_id)
});
}
} else {
// 灏忔壒閲忥細鐩存帴鎺ㄩ€? for (const record of records) {
// 骞傜瓑鎬ф<E98EAC>鏌ワ紙闃叉<E99783>閲嶅<E996B2>澶勭悊锛? const isDuplicate = await this.isDuplicate(projectId, record.record_id);
if (!isDuplicate) {
await jobQueue.push('iit:quality-check', {
projectId,
recordId: record.record_id
});
}
}
}
// 4. 鏇存柊鍚屾<E98D9A>鏃堕棿锛堝弻鍐欙細缂撳瓨 + 鏁版嵁搴擄級
const now = new Date().toISOString();
await cache.set(cacheKey, now, 3600 * 24); // 缂撳瓨24灏忔椂
await this.updateLastSyncInDB(projectId, now);
logger.info('Polling completed', {
projectId,
recordsFound: records.length,
duration: Date.now() - startTime
});
} catch (error) {
logger.error('Polling failed', {
error: error.message,
projectId,
duration: Date.now() - startTime
});
throw error; // 璁?pg-boss 閲嶈瘯
}
}
/**
* 骞傜瓑鎬т繚鎶わ紙闃叉<E99783>閲嶅<E996B2>璐ㄦ帶锛? */
private async isDuplicate(projectId: string, recordId: string): Promise<boolean> {
const key = `iit:processed:${projectId}:${recordId}`;
const exists = await cache.get(key);
if (!exists) {
await cache.set(key, 'true', 3600); // 缂撳瓨1灏忔椂
return false;
}
return true;
}
/**
* 浠庢暟鎹<E69A9F>簱鑾峰彇涓婃<E6B693>鍚屾<E98D9A>鏃堕棿
*/
private async getLastSyncFromDB(projectId: string): Promise<string> {
const project = await prisma.iitProject.findUnique({
where: { id: projectId },
select: { lastSyncAt: true }
});
return project?.lastSyncAt?.toISOString() ||
new Date(Date.now() - 24 * 3600 * 1000).toISOString(); // 榛樿<E6A69B>24灏忔椂鍓? }
/**
* 鏇存柊鏁版嵁搴撲腑鐨勫悓姝ユ椂闂? */
private async updateLastSyncInDB(projectId: string, syncTime: string) {
await prisma.iitProject.update({
where: { id: projectId },
data: { lastSyncAt: new Date(syncTime) }
});
}
/**
* 浠诲姟鎷嗗垎宸ュ叿
*/
private splitIntoChunks<T>(array: T[], chunkSize: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
}
}
```
#### 3.1.5 鍘嗗彶鏁版嵁鍏ㄩ噺鎵<E599BA>弿锛堭煍?V1.1 鍔熻兘琛ュ厖锛?
```typescript
// backend/src/modules/iit-manager/services/BulkScanService.ts
import { logger } from '@/common/logging';
import { jobQueue } from '@/common/jobs';
import { prisma } from '@/config/database';
import { CheckpointService } from '@/common/jobs';
import { RedcapAdapter } from '../adapters/RedcapAdapter';
import { DataQualityAgent } from '../agents/DataQualityAgent';
/**
* 鍏ㄩ噺鎵<E599BA>弿鏈嶅姟锛氭敮鎸佸瓨閲忔暟鎹<E69A9F>川鎺? *
* 搴旂敤鍦烘櫙锛? * 1. 椤圭洰鍒濆<E98D92>鍖栨椂锛屾壂鎻忓巻鍙叉暟鎹? * 2. Protocol鏇存柊鍚庯紝閲嶆柊鎵<E69F8A>弿鎵€鏈夋暟鎹? * 3. 鎵嬪姩瑙﹀彂鍏ㄩ噺璐ㄦ帶
*/
export class BulkScanService {
private redcapAdapter: RedcapAdapter;
constructor(redcapAdapter: RedcapAdapter) {
this.redcapAdapter = redcapAdapter;
}
/**
* 鍏ㄩ噺鎵<E599BA>弿锛堝惎鍔ㄦ椂鎴栨墜鍔ㄨЕ鍙戯級
*/
async scanAllRecords(projectId: string): Promise<string> {
logger.info('Starting bulk scan', { projectId });
// 1. 杞婚噺绾ф媺鍙栨墍鏈塺ecord_id锛堜笉鎷夊畬鏁存暟鎹<E69A9F>
const allRecords = await this.redcapAdapter.exportRecords({
fields: ['record_id'], // 鍙<><E98D99>ID锛岄€熷害蹇? rawOrLabel: 'raw'
});
const totalRecords = allRecords.length;
logger.info('Total records to scan', {
projectId,
totalRecords
});
// 2. 鏅鸿兘闃堝€煎垽鏂? const THRESHOLD = 50;
const useQueue = totalRecords >= THRESHOLD;
if (useQueue) {
// 闃熷垪妯″紡锛氫换鍔℃媶鍒?+ 鏂<>偣缁<E581A3>
return await this.scanViaQueue(projectId, allRecords);
} else {
// 鐩存帴妯″紡锛氬揩閫熷<E996AB>鐞? return await this.scanDirectly(projectId, allRecords);
}
}
/**
* 闃熷垪妯″紡锛氬ぇ鎵归噺鏁版嵁锛堚墺50鏉★級
*/
private async scanViaQueue(
projectId: string,
allRecords: { record_id: string }[]
): Promise<string> {
// 1. 鍒涘缓浠诲姟璁板綍
const taskRun = await prisma.iitTaskRun.create({
data: {
projectId,
taskType: 'bulk-scan',
status: 'pending',
totalItems: allRecords.length,
processedItems: 0,
successItems: 0,
failedItems: 0
}
});
// 2. 浠诲姟鎷嗗垎锛堟瘡鎵?0鏉★級
const chunks = this.splitIntoChunks(allRecords, 50);
// 3. 鎺ㄩ€佹壒娆′换鍔? for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const jobId = await jobQueue.push('iit:bulk-scan:batch', {
// 涓氬姟淇℃伅
taskRunId: taskRun.id,
projectId,
recordIds: chunk.map(r => r.record_id),
// 鉁?浠诲姟鎷嗗垎淇℃伅锛堣嚜鍔ㄥ瓨鍌ㄥ湪 job.data锛? batchIndex: i,
totalBatches: chunks.length,
startIndex: i * 50,
endIndex: Math.min((i + 1) * 50, allRecords.length)
});
// 鍏宠仈 job_id 鍒颁换鍔¤<E98D94>褰? await prisma.iitTaskRun.update({
where: { id: taskRun.id },
data: { jobId }
});
}
logger.info('Bulk scan queued', {
projectId,
totalRecords: allRecords.length,
totalBatches: chunks.length,
taskRunId: taskRun.id
});
return taskRun.id;
}
/**
* Worker澶勭悊鎵规<E98EB5>锛堟敮鎸佹柇鐐圭画浼狅級
*/
async processBatch(job: any) {
const { taskRunId, projectId, recordIds, batchIndex, totalBatches } = job.data;
const checkpointService = new CheckpointService(prisma);
// 1. 鍔犺浇鏂<E6B587>
const checkpoint = await checkpointService.loadCheckpoint(job.id);
const startIndex = checkpoint?.currentIndex || 0;
logger.info('Processing batch', {
taskRunId,
batchIndex,
totalBatches,
recordCount: recordIds.length,
resumeFrom: startIndex
});
let successCount = 0;
let failedCount = 0;
// 2. 閫愪釜澶勭悊璁板綍
for (let i = startIndex; i < recordIds.length; i++) {
const recordId = recordIds[i];
try {
// 2.1 鎷夊彇瀹屾暣鏁版嵁锛堟寜闇€鎷夊彇锛岄伩鍏嶅唴瀛樻孩鍑猴級
const recordData = await this.redcapAdapter.exportRecords({
records: [recordId]
});
// 2.2 璋冪敤璐ㄦ帶Agent
const agent = new DataQualityAgent();
await agent.checkRecord({
projectId,
recordId,
data: recordData[0]
});
successCount++;
} catch (error) {
logger.error('Record scan failed', {
recordId,
error: error.message
});
failedCount++;
}
// 2.3 姣?0鏉繚瀛樻柇鐐? if (i % 10 === 0 || i === recordIds.length - 1) {
await checkpointService.saveCheckpoint(job.id, {
currentIndex: i + 1,
processedCount: i + 1,
successCount,
failedCount
});
// 鏇存柊浠诲姟缁熻<E7BC81>
await this.updateTaskProgress(taskRunId, i + 1, successCount, failedCount);
}
}
logger.info('Batch completed', {
taskRunId,
batchIndex,
successCount,
failedCount
});
}
/**
* 鐩存帴妯″紡锛氬皬鎵归噺鏁版嵁锛?50鏉★級
*/
private async scanDirectly(
projectId: string,
allRecords: { record_id: string }[]
): Promise<string> {
// 鍒涘缓浠诲姟璁板綍
const taskRun = await prisma.iitTaskRun.create({
data: {
projectId,
taskType: 'bulk-scan',
status: 'processing',
totalItems: allRecords.length,
processedItems: 0,
successItems: 0,
failedItems: 0,
startedAt: new Date()
}
});
const agent = new DataQualityAgent();
let successCount = 0;
let failedCount = 0;
// 鐩存帴澶勭悊锛堜笉鍏ラ槦鍒楋級
for (const record of allRecords) {
try {
const recordData = await this.redcapAdapter.exportRecords({
records: [record.record_id]
});
await agent.checkRecord({
projectId,
recordId: record.record_id,
data: recordData[0]
});
successCount++;
} catch (error) {
logger.error('Record scan failed', {
recordId: record.record_id,
error: error.message
});
failedCount++;
}
}
// 鏇存柊浠诲姟瀹屾垚
await prisma.iitTaskRun.update({
where: { id: taskRun.id },
data: {
status: 'completed',
processedItems: allRecords.length,
successItems: successCount,
failedItems: failedCount,
completedAt: new Date(),
duration: Math.floor((Date.now() - taskRun.startedAt.getTime()) / 1000)
}
});
return taskRun.id;
}
/**
* 鏇存柊浠诲姟杩涘害锛堜緵鍓嶇<E98D93><EFBFBD><E69D9E>锛? */
private async updateTaskProgress(
taskRunId: string,
processedItems: number,
successItems: number,
failedItems: number
) {
const task = await prisma.iitTaskRun.findUnique({
where: { id: taskRunId },
select: { totalItems: true }
});
await prisma.iitTaskRun.update({
where: { id: taskRunId },
data: {
processedItems,
successItems,
failedItems,
status: processedItems >= task!.totalItems ? 'completed' : 'processing'
}
});
}
/**
* 浠诲姟鎷嗗垎宸ュ叿
*/
private splitIntoChunks<T>(array: T[], chunkSize: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
}
}
```
### 3.2 鏁版嵁璐ㄦ帶 Agent锛堟牳蹇冧笟鍔★級
```typescript
// backend/src/modules/iit-manager/agents/DataQualityAgent.ts
import { logger } from '@/common/logging';
import { prisma } from '@/config/database';
import { ProtocolService } from '../services/ProtocolService';
export class DataQualityAgent {
private protocolService: ProtocolService;
constructor() {
this.protocolService = new ProtocolService();
}
/**
* 妫€鏌ュ崟鏉¤<E98F89>褰? */
async checkRecord(params: {
projectId: string;
recordId: string;
data: Record<string, any>;
}): Promise<void> {
logger.info('Quality check started', params);
// 1. 鑾峰彇椤圭洰閰嶇疆锛堝叧閿<E58FA7>瓧娈垫槧灏勶級
const project = await prisma.iitProject.findUnique({
where: { id: params.projectId },
select: {
fieldMappings: true, // JSON: { age: 'patient_age', gender: 'sex', ... }
difyDatasetId: true
}
});
if (!project || !project.difyDatasetId) {
logger.warn('Project not configured', { projectId: params.projectId });
return;
}
// 2. 鎻愬彇鍏抽敭瀛楁<E7809B>鍊? const mappings = project.fieldMappings as Record<string, string>;
const context = {
age: params.data[mappings.age],
gender: params.data[mappings.gender],
enrollmentDate: params.data[mappings.enrollmentDate],
// ... 鍏朵粬鏄犲皠瀛楁<E7809B>
};
// 3. 閫愪釜瀛楁<E7809B>妫€鏌? const issues: any[] = [];
for (const [logicalField, redcapField] of Object.entries(mappings)) {
const value = params.data[redcapField];
// 璋冪敤Protocol鏈嶅姟妫€鏌ュ悎瑙勬€? const result = await this.protocolService.checkProtocolCompliance({
projectId: params.projectId,
fieldName: logicalField,
value: value,
context: context
});
if (!result.isCompliant) {
issues.push({
fieldName: logicalField,
currentValue: value,
suggestedValue: result.suggestedValue,
reasoning: result.reasoning,
protocolPage: result.protocolPage,
confidence: result.confidence
});
}
}
// 4. 濡傛灉鍙戠幇闂<E5B987><E99782>锛屽垱寤哄奖瀛愬缓璁? if (issues.length > 0) {
await this.createPendingActions(
params.projectId,
params.recordId,
issues
);
// 5. 鍙戦€佷紒寰<E7B492>€氱煡锛堜弗閲嶈繚鑳岋級
const severeIssues = issues.filter(i => i.confidence > 0.85);
if (severeIssues.length > 0) {
await this.sendWeChatNotification(
params.projectId,
params.recordId,
severeIssues
);
}
}
logger.info('Quality check completed', {
projectId: params.projectId,
recordId: params.recordId,
issuesFound: issues.length
});
}
/**
* 鍒涘缓褰卞瓙寤鸿<E5AFA4>锛圥ROPOSED鐘舵€侊級
*/
private async createPendingActions(
projectId: string,
recordId: string,
issues: any[]
): Promise<void> {
for (const issue of issues) {
await prisma.iitPendingAction.create({
data: {
projectId: projectId,
recordId: recordId,
fieldName: issue.fieldName,
currentValue: issue.currentValue,
suggestedValue: issue.suggestedValue,
status: 'PROPOSED',
agentType: 'DATA_QUALITY',
reasoning: issue.reasoning,
evidence: {
protocolPage: issue.protocolPage,
confidence: issue.confidence
},
createdAt: new Date()
}
});
}
}
/**
* 鍙戦€佷紒寰<E7B492>€氱煡
*/
private async sendWeChatNotification(
projectId: string,
recordId: string,
issues: any[]
): Promise<void> {
// TODO: 瀹炵幇浼佸井閫氱煡锛圥hase 3锛? logger.info('WeChat notification sent', {
projectId,
recordId,
issuesCount: issues.length
});
}
}
```
### 3.3 浼佷笟寰<E7AC9F>俊闆嗘垚
```typescript
// backend/src/modules/iit-manager/adapters/WeChatAdapter.ts
import axios, { AxiosInstance } from 'axios';
import { cache } from '@/common/cache';
import { logger } from '@/common/logging';
export class WeChatAdapter {
private client: AxiosInstance;
private corpId: string;
private corpSecret: string;
private agentId: string;
constructor() {
this.corpId = process.env.WECHAT_CORP_ID!;
this.corpSecret = process.env.WECHAT_CORP_SECRET!;
this.agentId = process.env.WECHAT_AGENT_ID!;
this.client = axios.create({
baseURL: 'https://qyapi.weixin.qq.com/cgi-bin',
timeout: 10000
});
}
/**
* 鑾峰彇Access Token锛堢紦瀛?灏忔椂锛? */
private async getAccessToken(): Promise<string> {
// 1. 浠庣紦瀛樿<E7809B>鍙? const cacheKey = `wechat:access_token:${this.corpId}`;
const cached = await cache.get(cacheKey);
if (cached) {
return cached as string;
}
// 2. 璋冪敤API鑾峰彇
const response = await this.client.get('/gettoken', {
params: {
corpid: this.corpId,
corpsecret: this.corpSecret
}
});
if (response.data.errcode !== 0) {
throw new Error(`Failed to get access token: ${response.data.errmsg}`);
}
const accessToken = response.data.access_token;
// 3. 缂撳瓨7000绉掞紙鐣?00绉抌uffer锛? await cache.set(cacheKey, accessToken, 7000);
return accessToken;
}
/**
* 鍙戦€佸簲鐢ㄦ秷鎭<E7A7B7>紙鍗墖閫氱煡锛? */
async sendMessage(params: {
toUser: string; // 浼佸井UserID
title: string;
description: string;
url: string; // 璺宠浆URL锛圵orkbench锛? }): Promise<void> {
const accessToken = await this.getAccessToken();
const payload = {
touser: params.toUser,
msgtype: 'textcard',
agentid: this.agentId,
textcard: {
title: params.title,
description: params.description,
url: params.url,
btntxt: '绔嬪嵆鏌ョ湅'
}
};
const response = await this.client.post('/message/send', payload, {
params: { access_token: accessToken }
});
if (response.data.errcode !== 0) {
logger.error('WeChat message send failed', {
error: response.data.errmsg,
toUser: params.toUser
});
throw new Error(`Failed to send WeChat message: ${response.data.errmsg}`);
}
logger.info('WeChat message sent', {
toUser: params.toUser,
title: params.title
});
}
/**
* 鍙戦€佽川鎺ч<E98EBA>璀﹀崱鐗? */
async sendQualityAlert(params: {
toUser: string;
projectName: string;
recordId: string;
issuesCount: number;
workbenchUrl: string;
}): Promise<void> {
await this.sendMessage({
toUser: params.toUser,
title: '馃毃 鏁版嵁璐ㄦ帶棰勮<E6A3B0>',
description: `椤圭洰锛?{params.projectName}\n鎮€咃細${params.recordId}\nAI妫€娴嬪埌${params.issuesCount}<EFBFBD>棶棰榎n缃<EFBFBD>俊搴︼細楂榎n璇峰敖蹇<EFBFBD><EFBFBD>鐞哷,
url: params.workbenchUrl
});
}
}
```
---
## 4. 鏁版嵁搴撹<E690B4>璁?
### 4.1 Prisma Schema 瀹氫箟
```prisma
// prisma/schema.prisma
// ==============================
// IIT Manager Schema
// ==============================
// 椤圭洰琛?model IitProject {
id String @id @default(uuid())
name String
description String? @db.Text
// Protocol鐭ヨ瘑搴? difyDatasetId String? @unique // Dify Dataset ID
protocolFileKey String? // OSS Key: iit/projects/{id}/protocol.pdf
// 馃敟 V1.1 鏂板<E98F82>锛欴ify鎬ц兘浼樺寲 - 缂撳瓨鍏抽敭瑙勫垯
cachedRules Json? // { inclusionCriteria: [...], exclusionCriteria: [...], fields: {...} }
// 瀛楁<E7809B>鏄犲皠閰嶇疆锛圝SON锛? fieldMappings Json // { age: 'patient_age', gender: 'sex', ... }
// REDCap閰嶇疆
redcapProjectId String
redcapApiToken String @db.Text // 鍔犲瘑瀛樺偍
redcapUrl String
// 馃敟 V1.1 鏂板<E98F82>锛氬悓姝ョ<E5A79D>鐞?- 璁板綍涓婃<E6B693>鍚屾<E98D9A>鏃堕棿
lastSyncAt DateTime? // 涓婃<E6B693><EFBFBD><E69D9E>鍚屾<E98D9A>鏃堕棿锛堢敤浜庡<E6B59C>閲忔媺鍙栵級
// 椤圭洰鐘舵€? status String @default("active") // active/paused/completed
// 鏃堕棿鎴? createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
// 鍏崇郴
pendingActions IitPendingAction[]
taskRuns IitTaskRun[]
userMappings IitUserMapping[]
auditLogs IitAuditLog[]
@@index([status, deletedAt])
@@schema("iit")
}
// 褰卞瓙鐘舵€佽〃锛堟牳蹇冿級
model IitPendingAction {
id String @id @default(uuid())
projectId String
recordId String // REDCap Record ID
fieldName String // 瀛楁<E7809B>鍚嶏紙閫昏緫鍚嶏紝濡?'age'锛?
// 鏁版嵁瀵规瘮
currentValue Json? // 褰撳墠鍊? suggestedValue Json? // AI寤鸿<E5AFA4>鍊?
// 鐘舵€佹祦杞? status String // PROPOSED/APPROVED/REJECTED/EXECUTED/FAILED
agentType String // DATA_QUALITY/TASK_DRIVEN/COUNSELING/REPORTING
// AI鎺ㄧ悊淇℃伅
reasoning String @db.Text // AI鎺ㄧ悊杩囩▼
evidence Json // { protocolPage: 12, confidence: 0.92, ... }
// 浜虹被纭<E8A2AB><E7BAAD>淇℃伅
approvedBy String? // User ID
approvedAt DateTime?
rejectionReason String? @db.Text
// 鎵ц<E98EB5>淇℃伅
executedAt DateTime?
errorMessage String? @db.Text
// 鏃堕棿鎴? createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 鍏崇郴
project IitProject @relation(fields: [projectId], references: [id])
@@index([projectId, status])
@@index([projectId, recordId])
@@index([status, createdAt])
@@schema("iit")
}
// 浠诲姟杩愯<E69DA9>璁板綍锛堜笌 pg-boss 鍏宠仈锛?model IitTaskRun {
id String @id @default(uuid())
projectId String
taskType String // quality-check/follow-up/report-generation
// 鍏宠仈 pg-boss job
jobId String @unique // platform_schema.job.id
// 浠诲姟鐘舵€侊紙闀滃儚job鐘舵€侊紝渚夸簬涓氬姟鏌ヨ<E98F8C>锛? status String // pending/processing/completed/failed
// 涓氬姟缁撴灉
totalItems Int
processedItems Int @default(0)
successItems Int @default(0)
failedItems Int @default(0)
// 鏃堕棿淇℃伅
startedAt DateTime?
completedAt DateTime?
duration Int? // 绉?
// 鏃堕棿鎴? createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 鍏崇郴
project IitProject @relation(fields: [projectId], references: [id])
@@index([projectId, taskType, status])
@@index([jobId])
@@schema("iit")
}
// 鐢ㄦ埛鏄犲皠琛<E79AA0>紙寮傛瀯绯荤粺韬<E7B2BA>唤鍏宠仈锛?model IitUserMapping {
id String @id @default(uuid())
projectId String
// 绯荤粺鐢ㄦ埛ID锛堟湰绯荤粺锛? systemUserId String
// REDCap鐢ㄦ埛鍚? redcapUsername String
// 浼佸井OpenID
wecomUserId String?
// 馃敟 V1.1 鏂板<E98F82>锛氬皬绋嬪簭鏀<E7B0AD>寔锛堜笌浼佸井OpenID涓嶅悓锛? miniProgramOpenId String? @unique // 寰<>俊灏忕▼搴廜penID
sessionKey String? // 寰<>俊session_key锛堝姞瀵嗗瓨鍌<E793A8>
// 瑙掕壊
role String // PI/CRC/SUB_I
// 鏃堕棿鎴? createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 鍏崇郴
project IitProject @relation(fields: [projectId], references: [id])
@@unique([projectId, systemUserId])
@@unique([projectId, redcapUsername])
@@index([wecomUserId])
@@index([miniProgramOpenId]) // 馃敟 V1.1 鏂板<E98F82>绱㈠紩
@@schema("iit")
}
// 瀹¤<E780B9>鏃ュ織锛堝悎瑙勬€э級
model IitAuditLog {
id String @id @default(uuid())
projectId String
// 鎿嶄綔淇℃伅
actionType String // AI_SUGGESTION/HUMAN_APPROVAL/REDCAP_WRITE/...
actionId String? // PendingAction ID 鎴栧叾浠朓D
// 鐢ㄦ埛淇℃伅
userId String
ipAddress String?
userAgent String? @db.Text
// 璇︾粏淇℃伅
details Json? // 鎿嶄綔璇︽儏
// 杩借釜閾? traceId String // 鍏宠仈澶氫釜鎿嶄綔
// 鏃堕棿鎴? createdAt DateTime @default(now())
// 鍏崇郴
project IitProject @relation(fields: [projectId], references: [id])
@@index([projectId, createdAt])
@@index([userId, createdAt])
@@index([actionType, createdAt])
@@index([traceId])
@@schema("iit")
}
```
### 4.2 鏁版嵁搴撹縼绉?
```bash
# 鐢熸垚杩佺Щ鏂囦欢
npx prisma migrate dev --name add_iit_schema
# 鐢熸垚Prisma Client
npx prisma generate
```
---
## 5. API 璁捐<E79281>
### 5.1 API 绔<>偣娓呭崟
#### 椤圭洰绠$悊
| 绔<>偣 | 鏂规硶 | 鍔熻兘 | 浼樺厛绾?|
|------|------|------|--------|
| `/api/v1/iit/projects` | POST | 鍒涘缓椤圭洰 | P0 |
| `/api/v1/iit/projects/:id` | GET | 鑾峰彇椤圭洰璇︽儏 | P0 |
| `/api/v1/iit/projects/:id` | PUT | 鏇存柊椤圭洰 | P1 |
| `/api/v1/iit/projects/:id/protocol` | POST | 涓婁紶Protocol | P0 |
| `/api/v1/iit/projects/:id/field-mappings` | PUT | 閰嶇疆瀛楁<E7809B>鏄犲皠 | P0 |
| 馃敟 `/api/v1/iit/projects/:id/scan-all` | POST | **鍏ㄩ噺鎵<E599BA>弿锛圴1.1鏂板<EFBFBD>锛?* | P0 |
#### Webhook鎺ユ敹
| 绔<>偣 | 鏂规硶 | 鍔熻兘 | 浼樺厛绾?|
|------|------|------|--------|
| `/api/v1/iit/webhooks/redcap` | POST | REDCap Webhook | P0 |
#### 褰卞瓙鐘舵€佺<E282AC>鐞?
| 绔<>偣 | 鏂规硶 | 鍔熻兘 | 浼樺厛绾?|
|------|------|------|--------|
| `/api/v1/iit/pending-actions` | GET | 鑾峰彇寰呭<E5AFB0>鐞嗗缓璁<E7BC93>垪琛?| P0 |
| `/api/v1/iit/pending-actions/:id` | GET | 鑾峰彇寤鸿<E5AFA4>璇︽儏 | P0 |
| `/api/v1/iit/pending-actions/:id/approve` | POST | 纭<><E7BAAD>寤鸿<E5AFA4> | P0 |
| `/api/v1/iit/pending-actions/:id/reject` | POST | 鎷掔粷寤鸿<E5AFA4> | P1 |
#### 浠诲姟绠$悊
| 绔<>偣 | 鏂规硶 | 鍔熻兘 | 浼樺厛绾?|
|------|------|------|--------|
| `/api/v1/iit/tasks` | GET | 鑾峰彇浠诲姟鍒楄〃 | P1 |
| `/api/v1/iit/tasks/:id` | GET | 鑾峰彇浠诲姟璇︽儏 | P1 |
| `/api/v1/iit/tasks/:id/progress` | GET | 鑾峰彇浠诲姟杩涘害 | P1 |
### 5.2 API 瀹炵幇绀轰緥
```typescript
// backend/src/modules/iit-manager/routes/projects.ts
import { FastifyInstance } from 'fastify';
import { ProjectController } from '../controllers/ProjectController';
export async function projectRoutes(fastify: FastifyInstance) {
const controller = new ProjectController();
// 鍒涘缓椤圭洰
fastify.post('/projects', {
schema: {
body: {
type: 'object',
required: ['name', 'redcapProjectId', 'redcapApiToken', 'redcapUrl'],
properties: {
name: { type: 'string' },
description: { type: 'string' },
redcapProjectId: { type: 'string' },
redcapApiToken: { type: 'string' },
redcapUrl: { type: 'string' }
}
}
}
}, controller.createProject);
// 涓婁紶Protocol
fastify.post('/projects/:id/protocol', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
}
}
}, controller.uploadProtocol);
// 閰嶇疆瀛楁<E7809B>鏄犲皠
fastify.put('/projects/:id/field-mappings', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
body: {
type: 'object',
properties: {
mappings: { type: 'object' }
}
}
}
}, controller.updateFieldMappings);
}
```
---
## 6. 閮ㄧ讲鏋舵瀯
### 6.1 闃块噷浜慡AE閮ㄧ讲锛堢<E9949B>鍚堢幇鏈夋灦鏋勶級
```
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹? 闃块噷浜?SAE 鍛藉悕绌洪棿 鈹?鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹? 鈹? 搴旂敤1: Node.js Backend锛圛IT Manager妯″潡锛? 鈹? 鈹?鈹? 鈹? - 闀滃儚: backend-service:v1.1 鈹? 鈹?鈹? 鈹? - 瑙勬牸: 2鏍?GB 脳 1瀹炰緥 鈹? 鈹?鈹? 鈹? - 绔<>彛: 3001 鈹? 鈹?鈹? 鈹? - 鍋ュ悍妫€鏌? /api/health 鈹? 鈹?鈹? 鈹? - 鍐呯綉璁块棶 鈹? 鈹?鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹? 鈹? 搴旂敤2: Python 寰<>湇鍔★紙宸叉湁锛? 鈹? 鈹?鈹? 鈹? - 闀滃儚: python-extraction:v1.0 鈹? 鈹?鈹? 鈹? - 瑙勬牸: 1鏍?GB 脳 1瀹炰緥 鈹? 鈹?鈹? 鈹? - 绔<>彛: 8000 鈹? 鈹?鈹? 鈹? - 鍐呯綉璁块棶 鈹? 鈹?鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹? 鈹? 搴旂敤3: Frontend Nginx锛堝凡鏈夛級 鈹? 鈹?鈹? 鈹? - 闀滃儚: frontend-nginx:v1.0 鈹? 鈹?鈹? 鈹? - 瑙勬牸: 1鏍?GB 脳 1瀹炰緥 鈹? 鈹?鈹? 鈹? - 绔<>彛: 80 鈹? 鈹?鈹? 鈹? - 鍏<>綉璁块棶锛堥€氳繃CLB锛? 鈹? 鈹?鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈫?鈫?鍐呯綉閫氫俊
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹? 鏁版嵁瀛樺偍灞? 鈹?鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹? 鈹?RDS PostgreSQL 鈹? 鈹?OSS 瀵硅薄瀛樺偍 鈹? 鈹?鈹? 鈹?- 2鏍?GB 鈹? 鈹?- Protocol PDF 鈹? 鈹?鈹? 鈹?- 11 Schemas 鈹? 鈹?- 鏂囦欢涓婁紶 鈹? 鈹?鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹? 鈹?鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?```
### 6.2 鐜<><E9909C>鍙橀噺閰嶇疆
```bash
# backend/.env.production
# 鏁版嵁搴?DATABASE_URL=postgresql://user:pass@pgm-xxx.rds.aliyuncs.com:5432/ai_clinical_research
# OSS瀛樺偍
STORAGE_MODE=oss
OSS_REGION=cn-beijing
OSS_BUCKET=ai-clinical-research
OSS_ACCESS_KEY_ID=xxx
OSS_ACCESS_KEY_SECRET=xxx
# LLM
LLM_API_KEY=sk-xxx
LLM_BASE_URL=https://api.deepseek.com
# Dify锛堝凡鏈夛級
DIFY_API_KEY=xxx
DIFY_BASE_URL=http://dify-service:5001
# REDCap
REDCAP_WEBHOOK_SECRET=xxx # 涓嶦M閰嶇疆涓€鑷?
# 浼佷笟寰<E7AC9F>
WECHAT_CORP_ID=xxx
WECHAT_CORP_SECRET=xxx
WECHAT_AGENT_ID=xxx
# Python寰<6E>湇鍔★紙鍐呯綉锛?PYTHON_SERVICE_URL=http://172.17.173.66:8000
# 鏃ュ織绾у埆
LOG_LEVEL=info
```
---
## 7. 寮€鍙戣<E98D99>鍒?
### 7.1 MVP 闃舵<E99783>锛?鍛<>紝P0锛?
#### Week 1: 鍩虹<E98DA9>杩炴帴灞傦紙馃敟 V1.1 浼樺厛绾ц皟鏁达級
**鐩<>爣**锛氭墦閫?REDCap 鈫?Node.js锛堟媺鍙栵級 + 浼佸井鎺ㄩ€?
**馃敟 浼樺厛绾ц皟鏁寸悊鐢?*锛?- API鎷夊彇鏇村彲鎺э紙涓嶄緷璧栧尰闄㈢綉缁滐級
- 鑳借В鍐冲巻鍙叉暟鎹<E69A9F>棶棰?- Webhook浣滀负澧炲己锛岃€岄潪鏍稿績渚濊禆
**浠诲姟娓呭崟**锛?
1. **鏁版嵁搴撳垵濮嬪寲**锛圖ay 1, 4灏忔椂锛? - [ ] 鍒涘缓 iit_schema
- [ ] 缂栧啓Prisma Schema锛?涓<>〃锛屽惈V1.1鏂板<EFBFBD>瀛楁<EFBFBD>锛? - [ ] 杩愯<E69DA9>杩佺Щ锛歚npx prisma migrate dev --name init_iit_schema`
- [ ] 鐢熸垚Prisma Client锛歚npx prisma generate`
- [ ] 楠岃瘉锛氳兘鍦∟ode.js涓<73>墽琛孋RUD
2. **浼佷笟寰<E7AC9F>俊娉ㄥ唽**锛圖ay 1, 2灏忔椂锛? - [ ] 娉ㄥ唽浼佷笟寰<E7AC9F>俊寮€鍙戣€呰处鍙? - [ ] 鍒涘缓鑷<E7BC93>缓搴旂敤锛欼IT Manager Agent锛堟祴璇曪級
- [ ] 鑾峰彇鍑<E5BD87>瘉锛欳orpID銆丄gentID銆丼ecret
- [ ] 娴嬭瘯鎺ㄩ€侊細鐢≒ostman鍙戦€佷竴鏉″崱鐗囨秷鎭?
3. **馃敟 REDCap API Adapter寮€鍙?*锛圖ay 2, 8灏忔椂锛?*鈫?浼樺厛**
- [ ] 鍒涘缓 `RedcapAdapter.ts`
- [ ] 瀹炵幇 `exportRecords()`锛堟敮鎸佹椂闂磋繃婊わ級
- [ ] 瀹炵幇 `importRecords()`锛堝洖鍐欐暟鎹<EFBFBD>
- [ ] 瀹炵幇 `exportMetadata()`锛堣幏鍙栧瓧娈靛畾涔夛級
- [ ] 娴嬭瘯锛氳兘鎷夊彇REDCap鏁版嵁
4. **馃敟 SyncManager寮€鍙?*锛圖ay 2, 8灏忔椂锛?*鈫?鏍稿績鍏滃簳**
- [ ] 鍒涘缓 `SyncManager.ts`
- [ ] 瀹炵幇 `initializeSync()`锛堟櫤鑳藉悓姝ョ瓥鐣ワ級
- [ ] 瀹炵幇 `schedulePolling()`锛堝畾鏃惰疆璇<EFBFBD>
- [ ] 瀹炵幇 `handlePoll()`锛堣疆璇㈠<EFBFBD>鐞嗗櫒锛? - [ ] 瀹炵幇骞傜瓑鎬т繚鎶わ紙闃查噸澶嶏級
- [ ] 娴嬭瘯锛氳疆璇㈣兘姝g‘鎷夊彇鏂版暟鎹?
5. **馃敟 鍏ㄩ噺鎵<E599BA>弿鍔熻兘**锛圖ay 3, 8灏忔椂锛?*鈫?鏀<>寔鍘嗗彶鏁版嵁**
- [ ] 鍒涘缓 `BulkScanService.ts`
- [ ] 瀹炵幇 `scanAllRecords()`锛堟櫤鑳介槇鍊煎垽鏂<EFBFBD>
- [ ] 瀹炵幇 `processBatch()`锛堟敮鎸佹柇鐐圭画浼狅級
- [ ] API绔<49>偣锛歚POST /api/v1/iit/projects/:id/scan-all`
- [ ] 娴嬭瘯锛?00鏉″巻鍙叉暟鎹<E69A9F>壂鎻忔垚鍔?
6. **REDCap EM寮€鍙?*锛圖ay 4, 8灏忔椂锛?*鈫?浣滀负澧炲己**
- [ ] 鍒涘缓EM鐩<4D>綍缁撴瀯
- [ ] 缂栧啓 `config.json`锛圗M閰嶇疆鏂囦欢锛? - [ ] 瀹炵幇 `IITManagerConnector.php`
- [ ] 瀹炵幇 `redcap_save_record` Hook
- [ ] 瀹炵幇Webhook鎺ㄩ€侊紙甯︾<E794AF>鍚嶏級
7. **Node.js Webhook鎺ユ敹鍣?*锛圖ay 4, 8灏忔椂锛? - [ ] 鍒涘缓 `webhookController.ts`
- [ ] 瀹炵幇绛惧悕楠岃瘉
- [ ] 瀹炵幇闃查噸鏀炬敾鍑? - [ ] 寮傛<E5AFAE>鎺ㄩ€佸埌璐ㄦ帶闃熷垪
- [ ] Webhook杩為€氭€ф祴璇曪紙鑷<E7B499>€傚簲鍒囨崲锛?
8. **浼佸井閫傞厤鍣?*锛圖ay 5, 8灏忔椂锛? - [ ] 鍒涘缓 `WeChatAdapter.ts`
- [ ] 瀹炵幇Access Token缂撳瓨
- [ ] 瀹炵幇鍗$墖娑堟伅鎺ㄩ€? - [ ] 娴嬭瘯锛氬彂閫佽川鎺ч<E98EBA>璀﹀崱鐗?
**楠屾敹鏍囧噯锛圴1.1锛?*锛?- 鉁?**鏍稿績鑳藉姏**锛氳疆璇㈣兘鎷夊彇REDCap鏂版暟鎹<E69A9F>紙寤惰繜<10鍒嗛挓锛?- 鉁?**澧炲己鑳藉姏**锛歐ebhook鑳芥帹閫侊紙濡傛灉缃戠粶閫氾級锛堝欢杩?2绉掞級
- 鉁?**鍘嗗彶鏁版嵁**锛氬叏閲忔壂鎻忚兘澶勭悊瀛橀噺鏁版嵁
- 鉁?**浼佸井閫氱煡**锛氳兘鏀跺埌璐ㄦ帶棰勮<E6A3B0>
- 鉁?**鑷<>€傚簲**锛氱郴缁熻嚜鍔ㄩ€夋嫨鏈€浣冲悓姝ユā寮?
#### Week 2: AI 鏅鸿兘璐ㄦ帶
**鐩<>爣**锛氬疄鐜拌川鎺<E5B79D>gent鐨勫畬鏁撮棴鐜?
**浠诲姟娓呭崟**锛?
6. **Protocol鏈嶅姟**锛圖ay 6-7, 16灏忔椂锛? - [ ] 鍒涘缓 `ProtocolService.ts`
- [ ] 瀹炵幇Protocol PDF涓婁紶鍒癘SS
- [ ] 璋冪敤Dify鍒涘缓Dataset
- [ ] 瀹炵幇 `checkProtocolCompliance()` 鏂规硶
- [ ] 娴嬭瘯锛氫笂浼燩rotocol锛岃兘妫€绱㈠埌鍐呭<E98D90>
7. **璐ㄦ帶Agent**锛圖ay 8-9, 16灏忔椂锛? - [ ] 鍒涘缓 `DataQualityAgent.ts`
- [ ] 瀹炵幇 `checkRecord()` 鏂规硶
- [ ] 璋冪敤Protocol鏈嶅姟妫€鏌ュ悎瑙勬€? - [ ] 鍒涘缓褰卞瓙寤鸿<E5AFA4>锛坧ending_actions琛<73>
- [ ] 鍙戦€佷紒寰<E7B492>€氱煡锛堜弗閲嶈繚鑳岋級
- [ ] 娴嬭瘯锛氳緭鍏ヨ繚鑳屾暟鎹<E69A9F>紝鐢熸垚姝寤鸿<E5AFA4>
8. **PC Workbench鍓嶇<E98D93>楠ㄦ灦**锛圖ay 10-12, 24灏忔椂锛? - [ ] 鍒涘缓鍓嶇<E98D93><EFBFBD>敱锛歚/iit/workbench`
- [ ] 浠诲姟鍒楄〃椤碉紙鏄剧ず鎵€鏈塒ROPOSED寤鸿<E5AFA4>锛? - [ ] 璇︽儏瀵规瘮椤碉細
- 宸︿晶锛氬綋鍓嶆暟鎹? - 鍙充晶锛欰I寤鸿<E5AFA4> + 璇佹嵁鐗囨<E99097>
- [ ] 鎿嶄綔鎸夐挳锛歔鎷掔粷] [纭<><E7BAAD>]
- [ ] 娴嬭瘯锛氳兘姝g‘鏄剧ず鍜屾搷浣?
9. **褰卞瓙鐘舵€佹祦杞?*锛圖ay 13, 8灏忔椂锛? - [ ] 瀹炵幇 `PendingActionService.approveAction()`
- [ ] 璋冪敤REDCap API鍥炲啓鏁版嵁
- [ ] 鏇存柊鐘舵€侊細PROPOSED 鈫?APPROVED 鈫?EXECUTED
- [ ] 璁板綍瀹¤<E780B9>鏃ュ織
- [ ] 娴嬭瘯锛氬畬鏁撮棴鐜<E6A3B4>紙鍙戠幇鈫掔璁も啋鍥炲啓锛?
10. **绔<>埌绔<E59F8C>祴璇?*锛圖ay 14, 8灏忔椂锛? - [ ] 瀹屾暣娴佺▼娴嬭瘯
- [ ] 鎬ц兘娴嬭瘯锛?00鏉¤<E98F89>褰曪級
- [ ] 閿欒<E996BF>澶勭悊娴嬭瘯
- [ ] Demo褰曞埗
**楠屾敹鏍囧噯**锛?- 鉁?AI鑳藉彂鐜癙rotocol杩濊儗锛堝噯纭<E599AF>巼>80%锛?- 鉁?Workbench鑳藉睍绀篈I寤鸿<E5AFA4>鍜岃瘉鎹<E79889>
- 鉁?纭<><E7BAAD>鍚庢暟鎹<E69A9F><E98EB9><EFBFBD>洖鍐欏埌REDCap
- 鉁?瀹屾暣瀹¤<E780B9>鏃ュ織
- 鉁?5鍒嗛挓Demo褰曞埗瀹屾垚
### 7.2 Phase 1: 澶氱粓绔<E7B293>崗鍚岋紙2鍛<32>紝P1锛?
**浠诲姟娓呭崟**锛?
11. **馃敟 寰<>俊灏忕▼搴忓紑鍙戯紙V1.1 鎶€鏈<E282AC>爤鏄庣锛歍aro锛?*锛圵eek 3-4锛? - [ ] **Taro 4.x椤圭洰鍒濆<E98D92>鍖?*锛圧eact璇<74>硶锛? - [ ] 閰嶇疆Taro缂栬瘧涓哄井淇″皬绋嬪簭 + H5
- [ ] 澶嶇敤 `shared/components` 閫氱敤閫昏緫
- [ ] 鍔ㄦ€佸搧鐗屾覆鏌擄紙Logo銆佷富棰樿壊锛? - [ ] 鎶ヨ〃灞曠ず椤甸潰锛圱aro UI缁勪欢锛? - [ ] 瀹℃壒鎿嶄綔鐣岄潰
- [ ] 浼佸井璺宠浆闆嗘垚
- [ ] 灏忕▼搴忕櫥褰曪紙wx.login + sessionKey锛?
**鎶€鏈<E282AC>爤浼樺娍**锛? - 鉁?React Hooks璇<73>硶锛堝洟闃熺啛鎮夛級
- 鉁?鍙<><E98D99>鐢ㄥ墠绔<E5A2A0>唬鐮佸拰閫昏緫
- 鉁?涓€娆″紑鍙戯紝澶氱<E6BEB6>杩愯<E69DA9>锛堝皬绋嬪簭 + H5锛? - 鉁?TypeScript鏀<74>寔瀹屽杽
12. **浠诲姟椹卞姩Agent**锛圵eek 3-4锛? - [ ] 鎮h€呴殢璁挎彁閱? - [ ] 璁胯<E79281>绐楀彛鐩戞帶
- [ ] 娑堟伅鎺ㄩ€佺瓥鐣?
### 7.3 Phase 2-4锛堝悗缁<E68297>凯浠
- Phase 2: OCR鏅鸿兘閲囬泦锛?鍛<>
- Phase 3: 鏅鸿兘姹囨姤Agent锛?鍛<>
- Phase 4: 瑙勬ā鍖栦紭鍖栵紙3鍛<33>
---
## 8. 椋庨櫓璇勪及涓庡<E6B693>绛?
### 8.1 鎶€鏈<E282AC><E98F88>闄?
#### 椋庨櫓1锛欴ify RAG鍑嗙鐜囦笉瓒?
**椋庨櫓绛夌骇**锛氶珮
**褰卞搷**锛欰I妫€娴嬪噯纭<E599AF>巼<60%锛屽亣闃虫€ц繃澶?
**搴斿<E690B4>绛栫暐**锛?
**Plan A锛堜紭鍏堬級**锛?- 涓ユ牸闄愬埗MVP妫€鏌ヨ寖鍥达紙鍙<E7B499><E98D99>鏌?绫荤畝鍗曡<E98D97>鍒欙級
- 骞撮緞銆佹€у埆銆佸繀濉<E7B980>」 = 瑙勫垯鏄庣锛屽噯纭<E599AF>巼楂?- 鍏堥獙璇佹灦鏋勶紝鍚庝紭鍖栧噯纭<E599AF>€?
**Plan B锛堝<E9949B>閫夛級**锛?- 濡傛灉Dify鏁堟灉涓嶄匠锛屼复鏃剁敤纭<E695A4>紪鐮佽<E990AE>鍒?- MVP閲嶇偣楠岃瘉"褰卞瓙鐘舵€佹満鍒?锛岃€岄潪AI鑳藉姏
- 瑙勫垯寮曟搸鍦≒hase 2鍐嶄紭鍖?
**楠岃瘉鏂规硶**锛?- 鐢?0涓<30>湡瀹炵梾渚嬫祴璇?- 鍑嗙‘鐜囩洰鏍囷細>85%
- 鍋囬槼鎬х巼锛?15%
#### 椋庨櫓2锛歊EDCap閮ㄧ讲鍥伴毦
**椋庨櫓绛夌骇**锛氫腑
**褰卞搷**锛氬尰闄㈢殑REDCap鐗堟湰澶<E6B9B0>€?娌℃湁閮ㄧ讲鏉冮檺
**搴斿<E690B4>绛栫暐**锛?
**Plan A锛堟帹鑽愶級**锛?- 鑷<>繁閮ㄧ讲涓€涓<E282AC>祴璇昍EDCap锛圖ocker锛?- 鐢ㄤ簬MVP Demo鍜屽唴閮ㄦ祴璇?- 绛夌<E7BB9B>绾﹀尰闄㈠悗鍐嶅<E98D90>鎺ヤ粬浠<E7B2AC>殑鐢熶骇REDCap
**Plan B锛堝<E9949B>閫夛級**锛?- 鍏堣烦杩嘡EDCap锛岀敤Mock鏁版嵁
- 閲嶇偣灞曠ずWorkbench鍜屼紒寰<E7B492>€氱煡
- REDCap闆嗘垚浣滀负"鎶€鏈<E282AC>彲琛屾€?璇存槑
**Docker閮ㄧ讲**锛?```bash
# 浣跨敤瀹樻柟REDCap Docker闀滃儚锛堟祴璇曠幆澧冿級
docker-compose up -d redcap mysql
```
#### 椋庨櫓3锛氫紒寰<E7B492><E5AFB0>鏍镐笉閫氳繃
**椋庨櫓绛夌骇**锛氫綆
**褰卞搷**锛氫紒涓氬井淇¤嚜寤哄簲鐢ㄥ<E990A2>鏍歌<E98F8D>鎷?
**搴斿<E690B4>绛栫暐**锛?
**Plan A**锛?- 鎻愬墠鍑嗗<E98D91>瀹℃牳鏉愭枡锛堝叕鍙歌祫璐ㄣ€佷骇鍝佽<E98D9D>鏄庯級
- 搴旂敤绫诲瀷閫夋嫨"浼佷笟鍐呴儴宸ュ叿"锛堝<E9949B>鏍稿<E98F8D>鏉撅級
**Plan B**锛?- 濡傛灉瀹℃牳鎱<E789B3>紝鍏堢敤浼佸井Webhook娴嬭瘯鍙?- 鎴栦复鏃剁敤閽夐拤/椋炰功锛堟妧鏈<E5A6A7>柟妗堥€氱敤锛?
**鍏抽敭**锛氫紒寰<E7B492>笉鏄<E7AC89>敮涓€閫夋嫨锛屾灦鏋勮<E98F8B>璁″凡缁忚В鑰?
### 8.2 涓氬姟椋庨櫓
#### 椋庨櫓4锛氬瓧娈垫槧灏勫<E7818F>鏉傛€?
**椋庨櫓绛夌骇**锛氫腑
**褰卞搷**锛氫笉鍚屽尰闄㈢殑REDCap瀛楁<E7809B>鍚嶅樊寮傚ぇ
**搴斿<E690B4>绛栫暐**锛?
- MVP闃舵<E99783>锛氭墜鍔ㄩ厤缃?涓<>叧閿<E58FA7>瓧娈垫槧灏?- Phase 2锛氬紑鍙慉I鑷<49>姩鏄犲皠宸ュ叿锛圢ER璇嗗埆锛?- Phase 3锛氬缓绔嬫爣鍑嗗瓧娈靛簱锛?00+甯哥敤瀛楁<E7809B>锛?
#### 椋庨櫓5锛氬尰鐤楀悎瑙勬€у<E282AC>鏌?
**椋庨櫓绛夌骇**锛氶珮
**褰卞搷**锛欰I淇<49>敼涓村簥鏁版嵁鐨勫悎瑙勬€ч棶棰?
**搴斿<E690B4>绛栫暐**锛?
- 鉁?**鏍稿績璁捐<E79281>**锛氬奖瀛愮姸鎬佹満鍒讹紙AI鍙<49>缓璁<E7BC93>紝浜虹被纭<E8A2AB>潈锛?- 鉁?**瀹屾暣瀹¤<E780B9>**锛氭墍鏈夋搷浣滆<E6B5A3>褰曞埌audit_logs琛?- 鉁?**绗﹀悎FDA 21 CFR Part 11**锛氱數瀛愮<E7809B>鍚嶅拰瀹¤<E780B9>杩借釜
- 鉁?**鍙<>洖婊?*锛氭墍鏈変慨鏀瑰彲杩芥函鍜屾挙閿€
### 8.3 鎬ц兘椋庨櫓
#### 椋庨櫓6锛歊EDCap Webhook寤惰繜
**椋庨櫓绛夌骇**锛氫綆
**褰卞搷**锛歐ebhook鎺ㄩ€佸け璐ユ垨寤惰繜
**搴斿<E690B4>绛栫暐**锛?
- 鉁?骞傜瓑鎬ц<E98EAC>璁★細閲嶅<E996B2>Webhook涓嶄細閲嶅<E996B2>澶勭悊
- 鉁?寮傛<E5AFAE>澶勭悊锛歐ebhook绔嬪嵆杩斿洖200锛屽悗鍙板紓姝ユ墽琛?- 鉁?閲嶈瘯鏈哄埗锛歱g-boss鑷<73>姩閲嶈瘯3娆?- 鉁?姝讳俊闃熷垪锛氬け璐ヤ换鍔″崟鐙<E5B49F>瓨鍌<E793A8>紝浜哄伐浠嬪叆
#### 椋庨櫓7锛氬ぇ閲忓苟鍙戣川鎺?
**椋庨櫓绛夌骇**锛氫腑
**褰卞搷**锛?00涓<30>」鐩<E3808D>悓鏃跺綍鍏ユ暟鎹?
**搴斿<E690B4>绛栫暐**锛?
- 鉁?闃熷垪闄愭祦锛歱g-boss骞跺彂闄愬埗锛堟瘡绉?0涓<30>
- 鉁?LLM闄愭祦锛欴eepSeek API闄愭祦淇濇姢
- 鉁?Dify闄愭祦锛歊AG妫€绱㈤檺娴侊紙姣忕<E5A7A3>5娆★級
- 鉁?浼樺厛绾ч槦鍒楋細绱ф€ラ」鐩<E3808D>紭鍏堝<E98D8F>鐞?
---
## 馃搳 鎬荤粨
### 鏍稿績浼樺娍
1. **瀹屽叏澶嶇敤骞冲彴鑳藉姏**
- 鉁?涓嶉噸澶嶅疄鐜板熀纭€璁炬柦
- 鉁?寮€鍙戞晥鐜囨彁鍗?0%
- 鉁?缁存姢鎴愭湰闄嶄綆
2. **Postgres-Only鏋舵瀯**
- 鉁?闆堕<E99786>澶栨垚鏈<E59E9A>紙鏃犻渶Redis锛? - 鉁?鏂<>偣缁<E581A3>紶锛堟敮鎸侀暱浠诲姟锛? - 鉁?绗﹀悎浜戝師鐢熻<E990A2>鑼?
3. **褰卞瓙鐘舵€佹満鍒?*
- 鉁?鍖荤枟鍚堣<E98D9A>锛團DA璁よ瘉锛? - 鉁?AI鍙<49>帶锛堜汉绫荤鏉冿級
- 鉁?鍙<>拷婧<E68BB7>紙瀹屾暣瀹¤<E780B9>锛?
4. **娓愯繘寮忔紨杩?*
- 鉁?MVP 2鍛ㄩ獙璇佹牳蹇冧环鍊? - 鉁?Phase 1-4閫愭<E996AB><EFBFBD>
- 鉁?椋庨櫓鍙<E6AB93>
### MVP鎴愬姛鏍囧噯
**Demo鍦烘櫙**锛?鍒嗛挓锛夛細
1. CRC鍦≧EDCap褰曞叆杩濊儗鏁版嵁锛堝勾榫?5宀侊級
2. 30绉掑悗锛孭I鏀跺埌浼佸井鍗墖锛?骞撮緞瓒呭嚭鍏ユ帓鏍囧噯"
3. CRC鎵撳紑Workbench锛岀湅鍒癆I寤鸿<E5AFA4>鍜孭rotocol璇佹嵁锛堢<E9949B>12椤碉級
4. CRC纭<43><E7BAAD>锛屾暟鎹<E69A9F>嚜鍔ㄥ洖鍐橰EDCap
**鎶€鏈<E282AC>寚鏍?*锛?- Webhook鍝嶅簲鏃堕棿 < 100ms
- AI璐ㄦ帶瀹屾垚鏃堕棿 < 30绉?- 浼佸井鎺ㄩ€佸欢杩?< 5绉?- AI鍑嗙鐜?> 80%
### 涓嬩竴姝ヨ<E5A79D>鍔?
**绔嬪嵆鎵ц<E98EB5>**锛堜粖澶╋級锛?1. 鉁?纭<><E7BAAD>浼佷笟寰<E7AC9F>俊娉ㄥ唽杩涘害
2. 鉁?纭<><E7BAAD>鎶€鏈<E282AC>爤锛圢ode.js 22銆丳ostgreSQL 15銆乀ypeScript 5锛?3. 鉁?鍒涘缓椤圭洰鐪嬫澘锛堥<E9949B>涔?Notion锛?
**Week 1 鍚<>姩**锛堟槑澶╁紑濮嬶紝V1.1浼樺厛绾э級锛?1. 鉁?鏁版嵁搴揝chema鍒濆<E98D92>鍖?2. 馃敟 REDCap API Adapter寮€鍙戯紙浼樺厛锛?3. 馃敟 SyncManager寮€鍙戯紙鏍稿績鍏滃簳锛?4. 馃敟 鍏ㄩ噺鎵<E599BA>弿鍔熻兘锛堟敮鎸佸巻鍙叉暟鎹<E69A9F>
5. 鉁?REDCap EM寮€鍙戯紙浣滀负澧炲己锛?6. 鉁?浼佸井閫傞厤鍣ㄥ紑鍙?
---
## 馃摑 V1.1 鏇存柊鎬荤粨
### 鏋舵瀯淇<E780AF><E6B787>
**1. 娣峰悎鍚屾<E98D9A>妯″紡锛圫yncManager锛?*
- 鉁?瑙e喅鍖婚櫌鍐呯綉杩為€氭€ч棶棰橈紙鑷村懡椋庨櫓锛?- 鉁?浼樺厛浣跨敤Webhook锛堝疄鏃舵€э級锛岃疆璇<E79686>綔涓哄厹搴曪紙鍙<E7B499>潬鎬э級
- 鉁?鏅鸿兘鑷<E58598>€傚簲锛氳嚜鍔ㄩ€夋嫨鏈€浣冲悓姝ユā寮?- 鉁?骞傜瓑鎬т繚鎶わ細闃叉<E99783>閲嶅<E996B2>璐ㄦ帶
**2. 鍘嗗彶鏁版嵁鍏ㄩ噺鎵<E599BA>弿锛圔ulkScanService锛?*
- 鉁?鏀<>寔瀛橀噺鏁版嵁璐ㄦ帶锛堝姛鑳借ˉ鍏咃級
- 鉁?鏅鸿兘闃堝€煎垽鏂<E59EBD>紙<50鏉洿鎺ュ<E98EBA>鐞嗭紝鈮?0鏉¢槦鍒楀<E98D92>鐞嗭級
- 鉁?鏂<>偣缁<E581A3>紶锛堟敮鎸侀暱鏃堕棿浠诲姟锛?- 鉁?API绔<49>偣锛歚POST /api/v1/iit/projects/:id/scan-all`
### 鎶€鏈<E282AC>爤鏄庣
**3. 鍓嶇<E98D93>鎶€鏈<E282AC>爤锛歍aro 4.x**
- 鉁?React Hooks璇<73>硶锛堝洟闃熺啛鎮夛級
- 鉁?鍙<><E98D99>鐢?shared/components 閫昏緫
- 鉁?涓€娆″紑鍙戯紝澶氱<E6BEB6>杩愯<E69DA9>锛堝皬绋嬪簭 + H5锛?- 鉁?TypeScript鏀<74>寔瀹屽杽
### 鏁版嵁搴撳<E690B4>寮?
**4. Prisma Schema鏂板<E98F82>瀛楁<E7809B>**
- 鉁?`IitProject.cachedRules`锛氱紦瀛楶rotocol鍏抽敭瑙勫垯锛堟€ц兘浼樺寲锛?- 鉁?`IitProject.lastSyncAt`锛氳<EFBFBD>褰曚笂娆″悓姝ユ椂闂达紙澧為噺鎷夊彇锛?- 鉁?`IitUserMapping.miniProgramOpenId`锛氬皬绋嬪簭OpenID锛堝<EFBFBD><EFBFBD>敮鎸侊級
- 鉁?`IitUserMapping.sessionKey`锛氬井淇<EFBFBD>ession_key锛堢櫥褰曡<EFBFBD>璇侊級
### 寮€鍙戜紭鍏堢骇璋冩暣
**5. MVP寮€鍙戣<E98D99>鍒掗噸鎺?*
- 馃敟 **Day 2浼樺厛绾?*锛歊EDCap API Adapter + SyncManager锛堟媺鍙栬兘鍔涳級
- 馃敟 **Day 3鏍稿績**锛氬叏閲忔壂鎻忓姛鑳斤紙鍘嗗彶鏁版嵁鏀<E5B581>寔锛?- 馃敟 **Day 4琛ュ厖**锛歊EDCap EM + Webhook锛堟帹閫佽兘鍔涳紝浣滀负澧炲己锛?
**璋冩暣鐞嗙敱**锛?- API鎷夊彇鏇村彲鎺э紙涓嶄緷璧栧尰闄㈢綉缁滐級
- 鑳借В鍐冲巻鍙叉暟鎹<E69A9F>棶棰?- Webhook浣滀负澧炲己锛岃€岄潪鏍稿績渚濊禆
### 椋庨櫓搴斿<E690B4>
**6. 缃戠粶杩為€氭€ч<E282AC>闄╋紙宸茶В鍐筹級**
- 鉂?**V1.0椋庨櫓**锛氬畬鍏ㄤ緷璧朩ebhook锛屽尰闄㈠唴缃戞棤娉曟帹閫?- 鉁?**V1.1淇<EFBFBD><EFBFBD>**锛氭贩鍚堝悓姝ユā寮忥紝杞<E7B49D><E69D9E>浣滀负鍏滃簳
- 鉁?**鍙<>潬鎬?*锛?9.9%锛堜笉渚濊禆鍖婚櫌缃戠粶锛?
**7. 鍘嗗彶鏁版嵁椋庨櫓锛堝凡瑙e喅锛?*
- 鉂?**V1.0椋庨櫓**锛氬彧鐩戝惉鏂版暟鎹<E69A9F>紝鍘嗗彶鏁版嵁鏃犳硶璐ㄦ帶
- 鉁?**V1.1淇<EFBFBD><EFBFBD>**锛氬叏閲忔壂鎻忓姛鑳斤紝鏀<E7B49D>寔瀛橀噺鏁版嵁
- 鉁?**浠峰€兼彁鍗?*锛氬尰闄㈣兘瀵瑰巻鍙?00涓<30>偅鑰呰繘琛岃川鎺?
### 鎬ц兘浼樺寲
**8. Dify RAG鎬ц兘浼樺寲锛堥<E9949B>鍔犺浇锛?*
- 鉁?Protocol涓婁紶鏃讹紝棰勬彁鍙栧叧閿<E58FA7><E996BF>鍒?- 鉁?缂撳瓨鍒癭cachedRules`瀛楁<E7809B>锛圝SONB锛?- 鉁?绠€鍗曡<E98D97>鍒欑洿鎺ュ垽鏂<E59EBD>紙鏃犻渶璋冪敤Dify锛?- 鉁?澶嶆潅瑙勫垯鎵嶈皟鐢―ify RAG锛堟參璺<E58F83>緞锛?
---
**鏂囨。鐗堟湰**锛歏1.1锛堟灦鏋勮瘎瀹′慨姝g増锛?
**鍒涘缓鏃ユ湡**锛?025-12-31
**鏈€鍚庢洿鏂?*锛?025-12-31
**缁存姢鑰?*锛氭灦鏋勫洟闃?
**瀹℃煡鍙傝€?*锛歚06-寮€鍙戣<E98D99>褰?IIT Manager Agent 鎶€鏈<E282AC>柟妗堝<E5A697>鏌ヤ笌琛ヤ竵.md`
**涓嬩竴姝?*锛氱瓑寰呯敤鎴风璁わ紝鍑嗗<E98D91><EFBFBD>姩MVP寮€鍙戯紙鎸塚1.1浼樺厛绾э級