Summary: - Implement WechatService (314 lines, push notifications) - Implement WechatCallbackController (501 lines, async reply mode) - Complete iit_quality_check Worker with WeChat notifications - Configure WeChat routes (GET + POST /wechat/callback) - Configure natapp tunnel for local development - WeChat URL verification test passed Technical Highlights: - Async reply mode to avoid 5-second timeout - Message encryption/decryption using @wecom/crypto - Signature verification using getSignature - natapp tunnel: https://iit.nat100.top - Environment variables configuration completed Technical Challenges Solved: - Fix environment variable naming (WECHAT_CORP_SECRET) - Fix @wecom/crypto import (createRequire for CommonJS) - Fix decrypt function parameters (2 params, not 4) - Fix Token character recognition (lowercase l vs digit 1) - Regenerate EncodingAESKey (43 chars, correct format) - Configure natapp for internal network penetration Test Results: - WeChat developer tool verification: PASSED - Return status: request success - HTTP 200, decrypted 23 characters correctly - Backend logs: URL verification successful Documentation: - Add Day3 WeChat integration development record - Update MVP development task list (Day 2-3 completed) - Update module status guide (v1.2 -> v1.3) - Overall completion: 35% -> 50% Progress: - Module completion: 35% -> 50% - Day 3 development: COMPLETED - Ready for end-to-end testing (REDCap -> WeChat)
1392 lines
40 KiB
Markdown
1392 lines
40 KiB
Markdown
# IIT Manager Agent - 最小MVP闭环开发计划
|
||
|
||
> **版本**: v1.0
|
||
> **创建日期**: 2026-01-02
|
||
> **目标**: 打通 REDCap → Node.js → 企业微信 的完整闭环
|
||
> **预估工作量**: 2天(Day 3-4)
|
||
> **核心理念**: 先打通最小闭环,验证技术可行性,再逐步丰富功能
|
||
|
||
---
|
||
|
||
## 🎯 一、MVP定义与目标
|
||
|
||
### 1.1 什么是最小闭环?
|
||
|
||
**核心闭环**:
|
||
```
|
||
REDCap录入数据 → Node.js实时捕获 → 企业微信智能通知 → PI对话查询
|
||
```
|
||
|
||
**验证价值**:
|
||
1. ✅ **实时感知**:PI无需登录REDCap,随时掌握项目进展
|
||
2. ✅ **主动通知**:数据录入后立即推送,不会遗漏
|
||
3. ✅ **智能交互**:在企业微信中即可查询数据、获取报告
|
||
4. ✅ **易扩展**:闭环打通后,可以快速添加质控、提醒、统计等功能
|
||
|
||
### 1.2 暂不实现的功能(后续扩展)
|
||
|
||
- ⏸️ **数据质控规则生成**(Phase 1.5):上传研究方案 → AI生成规则库 → 基于规则质控
|
||
- ⏸️ **PC Workbench前端**(Phase 2):复核AI建议的Web界面
|
||
- ⏸️ **REDCap数据回写**(Phase 2):AI建议审批后回写到REDCap
|
||
- ⏸️ **微信小程序**(Phase 3):移动端原生体验
|
||
- ⏸️ **复杂对话能力**(Phase 3):多轮对话、上下文理解
|
||
|
||
---
|
||
|
||
## 📊 二、当前状态总结
|
||
|
||
### 2.1 已完成的准备工作(Day 1-2)
|
||
|
||
#### ✅ Day 1:环境初始化(2026-01-01)
|
||
|
||
**数据库**:
|
||
- ✅ 创建 `iit_schema`(5个表)
|
||
- ✅ Prisma Schema完整(IitProject, IitPendingAction, IitTaskRun, IitUserMapping, IitAuditLog)
|
||
- ✅ CRUD测试通过(11/11)
|
||
|
||
**企业微信**:
|
||
- ✅ 企业微信开发者账号注册
|
||
- ✅ 自建应用创建(IIT Manager Agent)
|
||
- ✅ 凭证获取:
|
||
- **CorpID**: `ww01cb7b72ea2db83c`
|
||
- **AgentID**: `1000002`
|
||
- **Secret**: `F3XqlAqKdcOKHi9pLGv5a2dSUowWbevdcDRrBk2pXLM`
|
||
- ✅ **网页授权及JS-SDK授权已获取**
|
||
- ✅ **可信域名配置成功**:`iit.xunzhengyixue.com`
|
||
- ✅ **域名验证文件部署**:`WW_verify_YnhsQBwI0ARnNoG0.txt`
|
||
- ✅ **Access Token获取测试通过**
|
||
|
||
**模块骨架**:
|
||
- ✅ 目录结构创建
|
||
- ✅ 类型定义完整(223行)
|
||
- ✅ 路由前缀配置(`/api/v1/iit`)
|
||
|
||
---
|
||
|
||
#### ✅ Day 2:REDCap实时集成(2026-01-02)
|
||
|
||
**核心交付物**(~2,200行代码):
|
||
1. ✅ **RedcapAdapter.ts**(271行)
|
||
- `exportRecords()` - 导出记录(支持全量/增量/指定记录)
|
||
- `exportMetadata()` - 导出字段定义
|
||
- `importRecords()` - 导入记录(预留)
|
||
- `testConnection()` - 连接测试
|
||
|
||
2. ✅ **WebhookController.ts**(327行)
|
||
- `handleWebhook()` - 接收REDCap DET触发(<10ms响应)
|
||
- 幂等性检查(防止重复处理)
|
||
- 审计日志记录
|
||
- 推送到质控队列(`iit_quality_check`)
|
||
|
||
3. ✅ **SyncManager.ts**(398行)
|
||
- `initScheduledJob()` - 初始化定时任务(每5分钟)
|
||
- `handlePoll()` - 轮询所有active项目
|
||
- `manualSync()` - 手动同步
|
||
- `fullSync()` - 全量同步
|
||
|
||
4. ✅ **Worker注册**(91行)
|
||
- `iit_quality_check` - 质控任务(已注册,待补充逻辑)
|
||
- `iit_redcap_poll` - 定时轮询任务
|
||
|
||
5. ✅ **路由配置**(203行)
|
||
- `/api/v1/iit/health` - 健康检查
|
||
- `/api/v1/iit/webhooks/redcap` - DET回调接收(支持form-urlencoded)
|
||
- `/api/v1/iit/webhooks/health` - Webhook健康检查
|
||
- `/api/v1/iit/projects/:id/sync` - 手动同步
|
||
- `/api/v1/iit/projects/:id/full-sync` - 全量同步
|
||
|
||
**测试脚本**(912行):
|
||
- ✅ `test-redcap-api.ts`(189行)- API适配器测试
|
||
- ✅ `test-redcap-webhook.ts`(274行)- Webhook接收器测试
|
||
- ✅ `test-redcap-integration.ts`(449行)- 端到端集成测试
|
||
|
||
**测试结果**:
|
||
- ✅ **集成测试通过率**:12/12(100%)
|
||
- ✅ **Webhook响应时间**:<10ms(目标<100ms)
|
||
- ✅ **DET触发延迟**:0秒(实时)
|
||
- ✅ **数据同步准确性**:100%
|
||
|
||
**REDCap环境**:
|
||
- ✅ REDCap 15.8.0本地Docker部署
|
||
- ✅ 测试项目创建(test0102, PID 16)
|
||
- ✅ 6条测试数据录入
|
||
- ✅ DET配置成功:`http://host.docker.internal:3001/api/v1/iit/webhooks/redcap`
|
||
- ✅ API Token生成:`FCB30F9CBD12EE9E8E9B3E3A0106701B`
|
||
|
||
**关键技术突破**:
|
||
1. ✅ REDCap DET实时触发(0秒延迟)
|
||
2. ✅ form-urlencoded格式支持(REDCap原生格式)
|
||
3. ✅ 双保险机制(Webhook + 定时轮询)
|
||
4. ✅ 符合团队开发规范(队列命名、Worker注册)
|
||
|
||
---
|
||
|
||
### 2.2 当前架构状态
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────┐
|
||
│ 已完成的基础设施 │
|
||
├─────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────┐ │
|
||
│ │ REDCap │ ← 数据录入 │
|
||
│ │ 15.8.0 │ ← DET配置 ✅ │
|
||
│ └──────┬──────┘ │
|
||
│ │ DET触发(0秒延迟)✅ │
|
||
│ ↓ │
|
||
│ ┌─────────────────────┐ │
|
||
│ │ Node.js Backend │ │
|
||
│ │ (Fastify + pg-boss) │ │
|
||
│ ├─────────────────────┤ │
|
||
│ │ ✅ WebhookController │ ← 接收DET (<10ms) │
|
||
│ │ ✅ RedcapAdapter │ ← 拉取数据 │
|
||
│ │ ✅ SyncManager │ ← 轮询补充 │
|
||
│ │ ✅ Worker队列 │ ← 异步处理 │
|
||
│ │ ⏳ WechatService │ ← 待开发 🔥 │
|
||
│ └──────┬──────────────┘ │
|
||
│ │ 待打通 🔥 │
|
||
│ ↓ │
|
||
│ ┌─────────────────────┐ │
|
||
│ │ 企业微信 │ │
|
||
│ │ (已配置 ✅) │ │
|
||
│ ├─────────────────────┤ │
|
||
│ │ ✅ 应用创建 │ │
|
||
│ │ ✅ 凭证配置 │ │
|
||
│ │ ✅ 域名授权 │ │
|
||
│ │ ✅ AccessToken获取 │ │
|
||
│ │ ⏳ 消息推送 │ ← 待开发 🔥 │
|
||
│ │ ⏳ 消息接收 │ ← 待开发 🔥 │
|
||
│ └─────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## ⚠️ 三、关键技术风险与规避方案
|
||
|
||
在正式开发前,**必须了解**以下3个企业微信对接的致命陷阱:
|
||
|
||
### 风险A:5秒超时魔咒 🔥 Critical
|
||
|
||
**问题**:企业微信要求被动回复消息必须在**5秒内**完成。如果Node.js等AI计算完再return,企微早已提示"服务暂时不可用"并断开连接。
|
||
|
||
**错误做法**:
|
||
```typescript
|
||
❌ async handleCallback(request, reply) {
|
||
const answer = await callLLM(message); // 可能耗时10秒
|
||
return reply.send(answer); // 超时了!
|
||
}
|
||
```
|
||
|
||
**正确做法**:采用**异步回复模式**
|
||
```typescript
|
||
✅ async handleCallback(request, reply) {
|
||
// 1. 立即返回(<1秒)
|
||
reply.send('success');
|
||
|
||
// 2. 异步处理(不阻塞)
|
||
setImmediate(async () => {
|
||
const answer = await callLLM(message); // 可以慢慢算
|
||
await wechatService.sendTextMessage(userId, answer); // 主动推送
|
||
});
|
||
}
|
||
```
|
||
|
||
**规避措施**:在Day 3开发中**必须**使用异步回复模式,禁止在HTTP Response中直接返回AI结果。
|
||
|
||
---
|
||
|
||
### 风险B:XML加解密 🔥 P0
|
||
|
||
**问题**:企业微信的消息体是**加密的XML**(Encrypt字段),不是明文JSON。解密需要AES算法和复杂的Padding规则。
|
||
|
||
**错误做法**:自己写加解密算法(极易出错)
|
||
|
||
**正确做法**:使用官方库
|
||
```bash
|
||
npm install @wecom/crypto
|
||
```
|
||
|
||
```typescript
|
||
import { WXBizMsgCrypt } from '@wecom/crypto';
|
||
|
||
const crypt = new WXBizMsgCrypt(
|
||
WECHAT_TOKEN, // 自定义Token
|
||
WECHAT_ENCODING_AES_KEY, // 企微后台生成(43位)
|
||
WECHAT_CORP_ID
|
||
);
|
||
|
||
// 解密消息
|
||
const decrypted = crypt.decrypt(encryptedXml);
|
||
```
|
||
|
||
**规避措施**:强制使用官方库,禁止自己实现加解密。
|
||
|
||
---
|
||
|
||
### 风险C:IP白名单限制 🔥 P0
|
||
|
||
**问题**:获取AccessToken时,企业微信会校验请求来源IP。
|
||
|
||
**现状**:SAE通过NAT网关访问外网
|
||
- **NAT网关IP**:`182.92.176.14`
|
||
- **企业微信配置**:未添加到"企业可信IP"列表
|
||
|
||
**后果**:如不配置,getAccessToken()会报**60020错误**
|
||
|
||
**规避措施**:
|
||
1. 登录企业微信管理后台
|
||
2. 进入"应用管理" → "IIT Manager Agent"
|
||
3. 找到"企业可信IP"配置
|
||
4. 添加:`182.92.176.14`
|
||
|
||
---
|
||
|
||
### 风险D:关键词匹配 vs AI Agent ⚠️ P2
|
||
|
||
**问题**:简单的关键词匹配(`if (msg.includes('汇总'))`)不是AI Agent,只是if-else机器人。
|
||
|
||
**正确做法**:使用LLM做意图分类
|
||
```typescript
|
||
// ✅ 真正的AI Agent
|
||
const intent = await classifyIntent(userMessage); // 调用LLM
|
||
switch(intent.type) {
|
||
case 'query_weekly_summary': ...
|
||
case 'query_patient_info': ...
|
||
}
|
||
```
|
||
|
||
**规避措施**:在Day 3下午实现基于LLM的意图识别。
|
||
|
||
---
|
||
|
||
## 🚀 四、Day 3-4 开发计划
|
||
|
||
### Day 3:企业微信集成(2026-01-03,9小时)🔥
|
||
|
||
#### 准备工作:企业微信配置检查(1小时)⚠️ 必须先做
|
||
|
||
**任务0:企业微信配置完整性检查**
|
||
|
||
**配置清单**:
|
||
1. [ ] **企业可信IP配置** 🔥 Critical
|
||
- 登录企业微信管理后台
|
||
- 应用管理 → IIT Manager Agent → 企业可信IP
|
||
- 添加:`182.92.176.14`(SAE NAT网关IP)
|
||
- 测试:调用getAccessToken,验证不报60020错误
|
||
|
||
2. [ ] **获取EncodingAESKey** 🔥 Critical
|
||
- 企业微信后台 → 应用管理 → IIT Manager Agent
|
||
- 开发者接口 → 接收消息
|
||
- 点击"随机生成"按钮,获取43位AESKey
|
||
- 复制保存(用于消息解密)
|
||
|
||
3. [ ] **设置Token** 🔥 Critical
|
||
- 自定义一个Token(用于签名验证)
|
||
- 建议:32位随机字符串
|
||
- 保存到环境变量
|
||
|
||
4. [ ] **配置回调URL**
|
||
- URL:`https://iit.xunzhengyixue.com/api/v1/iit/wechat/callback`
|
||
- 验证Token、EncodingAESKey配置正确
|
||
|
||
**环境变量新增**:
|
||
```env
|
||
# 企业微信配置(已有)
|
||
WECHAT_CORP_ID=ww6ab493470ab4f377
|
||
WECHAT_AGENT_ID=1000002
|
||
WECHAT_CORP_SECRET=AZIVxMtoLb0rEszXS81e4dBRl-I9kgTjygIS0cFfENU
|
||
|
||
# 企业微信加解密配置(新增)🔥
|
||
WECHAT_TOKEN=your_32_char_random_token_here
|
||
WECHAT_ENCODING_AES_KEY=your_43_char_aes_key_from_wechat_console
|
||
```
|
||
|
||
**验收标准**:
|
||
- ✅ IP白名单已配置
|
||
- ✅ EncodingAESKey已获取并保存
|
||
- ✅ Token已设置并保存
|
||
- ✅ AccessToken可以正常获取(无60020错误)
|
||
|
||
---
|
||
|
||
#### 上午:企业微信推送服务(4小时)
|
||
|
||
**目标**:完成从Node.js到企业微信的推送通道
|
||
|
||
**任务1:创建 WechatService.ts(2小时)**
|
||
|
||
**文件位置**:`backend/src/modules/iit-manager/services/WechatService.ts`
|
||
|
||
**核心功能**:
|
||
```typescript
|
||
class WechatService {
|
||
/**
|
||
* 获取Access Token(带缓存)
|
||
*/
|
||
async getAccessToken(): Promise<string>
|
||
|
||
/**
|
||
* 发送数据录入通知
|
||
*/
|
||
async sendDataEntryNotification(params: {
|
||
userId: string;
|
||
projectName: string;
|
||
recordId: string;
|
||
action: 'created' | 'updated';
|
||
fieldsSummary: string;
|
||
}): Promise<any>
|
||
|
||
/**
|
||
* 发送文本消息(通用)
|
||
*/
|
||
async sendTextMessage(userId: string, content: string): Promise<any>
|
||
|
||
/**
|
||
* 发送卡片消息(通用)
|
||
*/
|
||
async sendTextCard(params: {
|
||
userId: string;
|
||
title: string;
|
||
description: string;
|
||
url?: string;
|
||
btntxt?: string;
|
||
}): Promise<any>
|
||
}
|
||
```
|
||
|
||
**实现要点**:
|
||
- ✅ AccessToken缓存到Postgres(7000秒过期时间)
|
||
- ✅ 重试机制(3次重试,指数退避)
|
||
- ✅ 错误处理和日志记录
|
||
- ✅ 支持文本消息和卡片消息
|
||
|
||
**验收标准**:
|
||
- ✅ AccessToken可以正确获取和缓存
|
||
- ✅ 可以发送文本消息到指定用户
|
||
- ✅ 可以发送卡片消息
|
||
- ✅ 缓存机制正常工作
|
||
|
||
---
|
||
|
||
**任务2:完善 iit_quality_check Worker(1小时)**
|
||
|
||
**文件位置**:`backend/src/modules/iit-manager/index.ts`
|
||
|
||
**当前状态**:Worker已注册,但逻辑为空(返回pending_implementation)
|
||
|
||
**更新内容**:
|
||
```typescript
|
||
jobQueue.process('iit_quality_check', async (job) => {
|
||
const { projectId, recordId, recordData, instrument } = job.data;
|
||
|
||
try {
|
||
// 1. 获取项目配置
|
||
const project = await prisma.projects.findUnique({
|
||
where: { id: projectId }
|
||
});
|
||
|
||
// 2. 构造数据摘要
|
||
const fieldsList = Object.keys(recordData);
|
||
const summary = `录入${fieldsList.length}个字段`;
|
||
|
||
// 3. 发送企业微信通知
|
||
const wechatService = new WechatService();
|
||
await wechatService.sendDataEntryNotification({
|
||
userId: 'GaoFeng', // TODO: 从项目配置中获取
|
||
projectName: project.name,
|
||
recordId,
|
||
action: instrument === 'demographics' ? 'created' : 'updated',
|
||
fieldsSummary: summary
|
||
});
|
||
|
||
// 4. 记录审计日志
|
||
await prisma.auditLogs.create({
|
||
data: {
|
||
projectId,
|
||
actionType: 'WECHAT_NOTIFICATION_SENT',
|
||
entityId: recordId,
|
||
details: {
|
||
recordId,
|
||
instrument,
|
||
fieldCount: fieldsList.length,
|
||
notified: true
|
||
}
|
||
}
|
||
});
|
||
|
||
logger.info('Wechat notification sent', {
|
||
recordId,
|
||
instrument,
|
||
fieldCount: fieldsList.length
|
||
});
|
||
|
||
return { success: true, notified: true };
|
||
|
||
} catch (error: any) {
|
||
logger.error('Wechat notification failed', {
|
||
recordId,
|
||
error: error.message
|
||
});
|
||
throw error;
|
||
}
|
||
});
|
||
```
|
||
|
||
**验收标准**:
|
||
- ✅ Worker可以正常执行
|
||
- ✅ 企业微信通知发送成功
|
||
- ✅ 审计日志正确记录
|
||
- ✅ 错误处理完善
|
||
|
||
---
|
||
|
||
**任务3:端到端测试(1小时)**
|
||
|
||
**测试场景1:数据录入通知**
|
||
```
|
||
操作:在REDCap中新增记录ID 8
|
||
预期:
|
||
1. DET实时触发(0秒延迟)
|
||
2. WebhookController接收(<10ms响应)
|
||
3. Worker执行(2秒内)
|
||
4. 企业微信收到通知(5秒内)
|
||
```
|
||
|
||
**测试场景2:数据更新通知**
|
||
```
|
||
操作:在REDCap中修改记录ID 2
|
||
预期:
|
||
1. DET实时触发
|
||
2. 企业微信收到更新通知
|
||
3. 通知内容包含修改的字段信息
|
||
```
|
||
|
||
**验收标准**:
|
||
- ✅ 完整闭环(REDCap → Node.js → 企微)打通
|
||
- ✅ 响应时间<5秒
|
||
- ✅ 通知内容准确
|
||
- ✅ 审计日志完整
|
||
|
||
---
|
||
|
||
#### 下午:企业微信对话接收(4小时)
|
||
|
||
**目标**:实现PI在企业微信中与AI Agent对话
|
||
|
||
**任务4:安装并配置消息解密库(15分钟)**
|
||
|
||
```bash
|
||
cd backend
|
||
npm install @wecom/crypto
|
||
npm install xml2js # XML解析
|
||
```
|
||
|
||
**验收标准**:
|
||
- ✅ 依赖安装成功
|
||
- ✅ 可以import成功
|
||
|
||
---
|
||
|
||
**任务5:创建 WechatCallbackController.ts(2.5小时)🔥 关键**
|
||
|
||
**文件位置**:`backend/src/modules/iit-manager/controllers/WechatCallbackController.ts`
|
||
|
||
**核心功能**(**异步回复模式**):
|
||
```typescript
|
||
import { WXBizMsgCrypt } from '@wecom/crypto';
|
||
import { parseString } from 'xml2js';
|
||
|
||
class WechatCallbackController {
|
||
private crypt: WXBizMsgCrypt;
|
||
|
||
constructor() {
|
||
this.crypt = new WXBizMsgCrypt(
|
||
process.env.WECHAT_TOKEN!,
|
||
process.env.WECHAT_ENCODING_AES_KEY!,
|
||
process.env.WECHAT_CORP_ID!
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 验证企业微信签名(GET请求)
|
||
* 企业微信首次配置回调URL时会发送验证请求
|
||
*/
|
||
async verifyCallback(request: any, reply: any): Promise<any> {
|
||
const { msg_signature, timestamp, nonce, echostr } = request.query;
|
||
|
||
try {
|
||
// 验证签名并解密echostr
|
||
const decrypted = this.crypt.verifyURL(
|
||
msg_signature,
|
||
timestamp,
|
||
nonce,
|
||
echostr
|
||
);
|
||
|
||
logger.info('Wechat callback URL verified');
|
||
return reply.send(decrypted);
|
||
|
||
} catch (error: any) {
|
||
logger.error('Wechat callback verification failed', { error: error.message });
|
||
return reply.code(403).send('Verification failed');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 接收企业微信用户消息(POST请求)
|
||
* 🔥 关键:必须在5秒内返回,使用异步处理
|
||
*/
|
||
async handleCallback(request: any, reply: any): Promise<any> {
|
||
const { msg_signature, timestamp, nonce } = request.query;
|
||
const encryptedXml = request.body;
|
||
|
||
try {
|
||
// 1. 解密消息体
|
||
const decryptedXml = this.crypt.decrypt(
|
||
msg_signature,
|
||
timestamp,
|
||
nonce,
|
||
encryptedXml
|
||
);
|
||
|
||
// 2. 解析XML
|
||
const message = await this.parseXML(decryptedXml);
|
||
const { FromUserName, Content, MsgId, MsgType } = message;
|
||
|
||
logger.info('Wechat message received', {
|
||
userId: FromUserName,
|
||
msgType: MsgType,
|
||
msgId: MsgId
|
||
});
|
||
|
||
// 3. 🔥 立即返回'success'(<1秒)
|
||
reply.send('success');
|
||
|
||
// 4. 🔥 异步处理(不阻塞,可以慢慢算)
|
||
if (MsgType === 'text') {
|
||
setImmediate(async () => {
|
||
try {
|
||
// 可能耗时5-10秒
|
||
const answer = await this.processUserMessage(FromUserName, Content);
|
||
|
||
// 使用主动推送接口返回结果
|
||
const wechatService = new WechatService();
|
||
await wechatService.sendTextMessage(FromUserName, answer);
|
||
|
||
// 记录审计日志
|
||
await this.logInteraction(MsgId, FromUserName, Content, answer);
|
||
|
||
} catch (error: any) {
|
||
logger.error('Async message processing failed', {
|
||
msgId: MsgId,
|
||
error: error.message
|
||
});
|
||
|
||
// 发送错误提示
|
||
await wechatService.sendTextMessage(
|
||
FromUserName,
|
||
'抱歉,处理您的消息时出错了,请稍后重试。'
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
return; // 已经返回过'success'了
|
||
|
||
} catch (error: any) {
|
||
logger.error('Wechat callback failed', { error: error.message });
|
||
return reply.code(500).send('Internal error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理用户消息(🔥 使用LLM做意图识别,而非关键词匹配)
|
||
*/
|
||
private async processUserMessage(
|
||
userId: string,
|
||
message: string
|
||
): Promise<string> {
|
||
// 1. 调用LLM做意图识别
|
||
const intent = await this.classifyIntent(message);
|
||
|
||
logger.info('Intent classified', { userId, intent });
|
||
|
||
// 2. 根据意图执行对应操作
|
||
try {
|
||
switch(intent.type) {
|
||
case 'query_weekly_summary':
|
||
return await this.getWeeklySummary(intent.params);
|
||
|
||
case 'query_patient_info':
|
||
return await this.getPatientInfo(intent.params.recordId);
|
||
|
||
case 'query_project_stats':
|
||
return await this.getProjectStats(intent.params.projectId);
|
||
|
||
case 'unknown':
|
||
default:
|
||
return this.getHelpMessage();
|
||
}
|
||
} catch (error: any) {
|
||
logger.error('Query execution failed', { intent, error: error.message });
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 🔥 使用LLM做意图分类(真正的AI Agent)
|
||
*/
|
||
private async classifyIntent(message: string): Promise<{
|
||
type: string;
|
||
params: any;
|
||
}> {
|
||
const prompt = `
|
||
你是一个意图分类器。用户说:"${message}"
|
||
|
||
请判断用户的意图并提取参数,返回JSON格式:
|
||
|
||
意图类型:
|
||
- query_weekly_summary: 查询最近一周数据汇总
|
||
- query_patient_info: 查询患者信息
|
||
- query_project_stats: 查询项目统计
|
||
- unknown: 无法识别
|
||
|
||
示例:
|
||
用户:"看看这周有没有新病人?"
|
||
返回:{"type": "query_weekly_summary", "params": {"timeRange": "this_week"}}
|
||
|
||
用户:"患者8的情况怎么样?"
|
||
返回:{"type": "query_patient_info", "params": {"recordId": "8"}}
|
||
|
||
用户:"项目进度如何?"
|
||
返回:{"type": "query_project_stats", "params": {}}
|
||
|
||
请返回JSON:
|
||
`.trim();
|
||
|
||
try {
|
||
// 调用DeepSeek API(或其他LLM)
|
||
const response = await this.callLLM(prompt);
|
||
const intent = JSON.parse(response);
|
||
|
||
return intent;
|
||
|
||
} catch (error: any) {
|
||
logger.error('Intent classification failed', {
|
||
message,
|
||
error: error.message
|
||
});
|
||
|
||
// 降级:返回unknown
|
||
return { type: 'unknown', params: {} };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 调用LLM API
|
||
*/
|
||
private async callLLM(prompt: string): Promise<string> {
|
||
// TODO: 实现DeepSeek API调用
|
||
// 暂时返回mock数据用于测试
|
||
|
||
// 简单的关键词匹配作为fallback
|
||
if (prompt.includes('一周') || prompt.includes('汇总')) {
|
||
return JSON.stringify({
|
||
type: 'query_weekly_summary',
|
||
params: { timeRange: 'this_week' }
|
||
});
|
||
}
|
||
|
||
const patientMatch = prompt.match(/患者(\d+)/);
|
||
if (patientMatch) {
|
||
return JSON.stringify({
|
||
type: 'query_patient_info',
|
||
params: { recordId: patientMatch[1] }
|
||
});
|
||
}
|
||
|
||
if (prompt.includes('进度') || prompt.includes('统计')) {
|
||
return JSON.stringify({
|
||
type: 'query_project_stats',
|
||
params: {}
|
||
});
|
||
}
|
||
|
||
return JSON.stringify({ type: 'unknown', params: {} });
|
||
}
|
||
|
||
/**
|
||
* 获取最近一周数据汇总
|
||
*/
|
||
private async getWeeklySummary(params: any): Promise<string>
|
||
|
||
/**
|
||
* 获取患者信息
|
||
*/
|
||
private async getPatientInfo(recordId: string): Promise<string>
|
||
|
||
/**
|
||
* 获取项目统计
|
||
*/
|
||
private async getProjectStats(projectId: string): Promise<string>
|
||
|
||
/**
|
||
* 帮助消息
|
||
*/
|
||
private getHelpMessage(): string {
|
||
return `您好!我是IIT Manager AI助手 🤖
|
||
|
||
您可以这样问我:
|
||
📊 "最近一周数据汇总"
|
||
👤 "查询患者8的情况"
|
||
📈 "项目进度如何"
|
||
|
||
我会尽力帮助您!`;
|
||
}
|
||
|
||
/**
|
||
* 解析XML消息
|
||
*/
|
||
private async parseXML(xml: string): Promise<any> {
|
||
return new Promise((resolve, reject) => {
|
||
parseString(xml, { explicitArray: false }, (err, result) => {
|
||
if (err) reject(err);
|
||
else resolve(result.xml);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 记录交互日志
|
||
*/
|
||
private async logInteraction(
|
||
msgId: string,
|
||
userId: string,
|
||
userMessage: string,
|
||
botResponse: string
|
||
): Promise<void> {
|
||
await prisma.auditLogs.create({
|
||
data: {
|
||
actionType: 'WECHAT_USER_MESSAGE',
|
||
entityId: userId,
|
||
details: {
|
||
msgId,
|
||
userMessage,
|
||
botResponse,
|
||
timestamp: new Date()
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
**实现要点(关键修正)**:
|
||
- ✅ 使用 `@wecom/crypto` 解密消息(不自己实现)
|
||
- ✅ 立即返回'success'(<1秒,避免5秒超时)
|
||
- ✅ 使用 `setImmediate` 异步处理
|
||
- ✅ 使用主动推送接口返回结果
|
||
- ✅ 使用LLM做意图分类(而非关键词匹配)
|
||
- ✅ 完善的错误处理和日志记录
|
||
|
||
**验收标准**:
|
||
- ✅ 企业微信回调URL验证通过
|
||
- ✅ 可以接收并解密用户消息
|
||
- ✅ 响应时间<1秒(异步处理)
|
||
- ✅ LLM意图识别正确率>80%
|
||
- ✅ 可以正确返回查询结果
|
||
|
||
---
|
||
|
||
**任务6:注册企业微信回调路由(30分钟)**
|
||
|
||
**文件位置**:`backend/src/modules/iit-manager/routes/index.ts`
|
||
|
||
**新增路由**:
|
||
```typescript
|
||
const wechatCallbackController = new WechatCallbackController();
|
||
|
||
// 企业微信回调验证(GET)
|
||
fastify.get(
|
||
'/api/v1/iit/wechat/callback',
|
||
wechatCallbackController.verifyCallback.bind(wechatCallbackController)
|
||
);
|
||
|
||
// 企业微信消息接收(POST)
|
||
fastify.post(
|
||
'/api/v1/iit/wechat/callback',
|
||
{
|
||
schema: {
|
||
body: {
|
||
type: 'object'
|
||
}
|
||
}
|
||
},
|
||
wechatCallbackController.handleCallback.bind(wechatCallbackController)
|
||
);
|
||
|
||
logger.info('Registered route: GET/POST /api/v1/iit/wechat/callback');
|
||
```
|
||
|
||
**验收标准**:
|
||
- ✅ 路由注册成功
|
||
- ✅ GET请求可以验证签名
|
||
- ✅ POST请求可以接收消息
|
||
|
||
---
|
||
|
||
**任务7:对话功能测试(1小时)**
|
||
|
||
**测试场景1:最近一周数据汇总**
|
||
```
|
||
操作:在企业微信中发送:"最近一周数据汇总"
|
||
预期:
|
||
AI Agent回复:
|
||
"📊 最近一周数据汇总:
|
||
新增受试者:2例
|
||
数据更新:5次
|
||
总计:7条操作记录"
|
||
```
|
||
|
||
**测试场景2:患者信息查询**
|
||
```
|
||
操作:在企业微信中发送:"查询患者4的情况"
|
||
预期:
|
||
AI Agent回复:
|
||
"📋 患者 4 基本信息:
|
||
姓名:test 4 test 4
|
||
年龄:18岁
|
||
性别:男
|
||
BMI:18.3
|
||
录入状态:已完成"
|
||
```
|
||
|
||
**测试场景3:项目进度查询**
|
||
```
|
||
操作:在企业微信中发送:"项目进度"
|
||
预期:
|
||
AI Agent回复:
|
||
"📊 test0102 项目统计:
|
||
总受试者数:8例
|
||
本周新增:2例
|
||
数据完整率:87.5%"
|
||
```
|
||
|
||
**验收标准**:
|
||
- ✅ 3个测试场景全部通过
|
||
- ✅ 回复内容准确
|
||
- ✅ 响应时间<3秒
|
||
- ✅ 错误处理友好
|
||
|
||
---
|
||
|
||
### Day 4:完善与文档(2026-01-04,6小时)
|
||
|
||
#### 上午:优化与测试(3小时)
|
||
|
||
**任务8:完善审计日志(1小时)**
|
||
|
||
**新增日志类型**:
|
||
```typescript
|
||
// 企业微信交互日志
|
||
await prisma.auditLogs.create({
|
||
data: {
|
||
actionType: 'WECHAT_USER_MESSAGE',
|
||
entityId: userId,
|
||
details: {
|
||
userMessage: message,
|
||
botResponse: response,
|
||
intent: 'query_patient_info',
|
||
timestamp: new Date()
|
||
}
|
||
}
|
||
});
|
||
```
|
||
|
||
**验收标准**:
|
||
- ✅ 所有企业微信交互都有日志
|
||
- ✅ 日志包含完整的上下文信息
|
||
- ✅ 可以追溯每次对话
|
||
|
||
---
|
||
|
||
**任务9:错误处理优化(1小时)**
|
||
|
||
**优化内容**:
|
||
1. **企业微信API调用失败**:
|
||
- 自动重试3次
|
||
- 记录错误日志
|
||
- 降级处理(通知失败不影响主流程)
|
||
|
||
2. **REDCap API超时**:
|
||
- 30秒超时
|
||
- 友好错误提示
|
||
- 返回缓存数据(如果有)
|
||
|
||
3. **用户消息解析失败**:
|
||
- 返回友好提示
|
||
- 记录错误日志
|
||
- 不影响其他功能
|
||
|
||
**验收标准**:
|
||
- ✅ 所有错误场景有友好提示
|
||
- ✅ 系统能自动重试
|
||
- ✅ 错误不影响其他任务执行
|
||
|
||
---
|
||
|
||
**任务10:完整闭环压力测试(1小时)**
|
||
|
||
**测试内容**:
|
||
1. **连续录入测试**:
|
||
- 在REDCap中连续录入10条数据
|
||
- 验证所有通知都能正确发送
|
||
- 验证没有遗漏和重复
|
||
|
||
2. **并发对话测试**:
|
||
- 同时发送5个查询请求
|
||
- 验证所有请求都能正确响应
|
||
- 验证响应时间<5秒
|
||
|
||
3. **长时间运行测试**:
|
||
- 系统运行1小时
|
||
- 期间随机录入数据和查询
|
||
- 验证系统稳定性
|
||
|
||
**验收标准**:
|
||
- ✅ 连续录入测试通过(10/10)
|
||
- ✅ 并发对话测试通过(5/5)
|
||
- ✅ 长时间运行无异常
|
||
- ✅ 内存和性能正常
|
||
|
||
---
|
||
|
||
#### 下午:文档编写(3小时)
|
||
|
||
**任务11:创建开发完成文档(2小时)**
|
||
|
||
**文档位置**:`docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-4-最小MVP闭环完成记录.md`
|
||
|
||
**文档内容**:
|
||
1. 开发概述
|
||
2. 完成的功能模块(详细说明)
|
||
3. 测试验证结果
|
||
4. 性能指标
|
||
5. 遇到的问题与解决方案
|
||
6. 闭环效果演示
|
||
7. 下一步计划
|
||
|
||
---
|
||
|
||
**任务12:更新模块状态文档(30分钟)**
|
||
|
||
**文档位置**:`docs/03-业务模块/IIT Manager Agent/00-模块当前状态与开发指南.md`
|
||
|
||
**更新内容**:
|
||
- 更新整体完成度(35% → 50%)
|
||
- 更新已完成功能列表
|
||
- 更新Day 3-4完成情况
|
||
- 更新下一步工作计划
|
||
|
||
---
|
||
|
||
**任务13:创建使用手册(30分钟)**
|
||
|
||
**文档位置**:`docs/03-业务模块/IIT Manager Agent/05-使用手册/企业微信对话指南.md`
|
||
|
||
**文档内容**:
|
||
1. 企业微信应用配置
|
||
2. 用户如何加入应用
|
||
3. 支持的对话指令列表
|
||
4. 常见问题FAQ
|
||
5. 故障排查指南
|
||
|
||
---
|
||
|
||
## ✅ 四、Day 4结束时的闭环效果
|
||
|
||
### 4.1 完整闭环验证
|
||
|
||
```
|
||
🎯 最小MVP闭环完全打通:
|
||
|
||
┌─────────────────────────────────────────┐
|
||
│ Step 1: REDCap数据录入 │
|
||
│ ────────────────────────────────────────│
|
||
│ 用户在REDCap中: │
|
||
│ - 新增患者ID 8 │
|
||
│ - 或修改患者ID 2 │
|
||
└──────────┬──────────────────────────────┘
|
||
│ 0秒延迟
|
||
↓
|
||
┌─────────────────────────────────────────┐
|
||
│ Step 2: Node.js实时捕获 │
|
||
│ ────────────────────────────────────────│
|
||
│ ✅ DET触发 (0秒) │
|
||
│ ✅ Webhook接收 (<10ms) │
|
||
│ ✅ Worker处理 (~2秒) │
|
||
└──────────┬──────────────────────────────┘
|
||
│
|
||
↓
|
||
┌─────────────────────────────────────────┐
|
||
│ Step 3: 企业微信推送通知 │
|
||
│ ────────────────────────────────────────│
|
||
│ ✅ PI收到卡片消息 (<5秒) │
|
||
│ ✅ 通知内容: │
|
||
│ "📊 test0102 - 数据录入 │
|
||
│ 受试者:8 │
|
||
│ 操作:新增 │
|
||
│ 录入8个字段" │
|
||
└──────────┬──────────────────────────────┘
|
||
│
|
||
↓
|
||
┌─────────────────────────────────────────┐
|
||
│ Step 4: PI在企业微信中对话 │
|
||
│ ────────────────────────────────────────│
|
||
│ ✅ 发送:"最近一周数据汇总" │
|
||
│ ✅ AI回复: │
|
||
│ "📊 最近一周数据汇总: │
|
||
│ 新增受试者:2例 │
|
||
│ 数据更新:5次" │
|
||
│ │
|
||
│ ✅ 发送:"查询患者8的情况" │
|
||
│ ✅ AI回复:患者基本信息 │
|
||
│ │
|
||
│ ✅ 发送:"项目进度" │
|
||
│ ✅ AI回复:项目统计数据 │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
### 4.2 验收标准(最终)
|
||
|
||
**功能完整性**:
|
||
- ✅ REDCap数据实时监听(DET + 轮询)
|
||
- ✅ Node.js数据捕获与处理
|
||
- ✅ 企业微信通知推送
|
||
- ✅ 企业微信对话查询
|
||
- ✅ 审计日志完整记录
|
||
|
||
**性能指标**:
|
||
| 指标 | 目标值 | 实际值 | 状态 |
|
||
|------|--------|--------|------|
|
||
| DET触发延迟 | <1秒 | 0秒 | ✅ |
|
||
| Webhook响应时间 | <100ms | <10ms | ✅ |
|
||
| 通知推送延迟 | <10秒 | <5秒 | ✅ |
|
||
| 对话响应时间 | <5秒 | ~3秒 | ✅ |
|
||
| 数据同步准确性 | 100% | 100% | ✅ |
|
||
|
||
**技术指标**:
|
||
- ✅ 代码符合团队规范
|
||
- ✅ 错误处理完善
|
||
- ✅ 日志记录完整
|
||
- ✅ 测试覆盖率>90%
|
||
|
||
**可演示性**:
|
||
- ✅ 可以现场演示完整闭环
|
||
- ✅ 可以演示对话查询功能
|
||
- ✅ 效果明显、价值清晰
|
||
|
||
---
|
||
|
||
## 📚 五、技术实现细节
|
||
|
||
### 5.1 企业微信异步回复模式(🔥 Critical)
|
||
|
||
#### 问题描述
|
||
企业微信要求被动回复消息必须在**5秒内**完成。如果超时,用户会看到"服务暂时不可用"的提示。
|
||
|
||
#### 错误做法
|
||
```typescript
|
||
❌ async handleCallback(request, reply) {
|
||
const message = parseMessage(request.body);
|
||
|
||
// 调用LLM(可能耗时10秒)
|
||
const answer = await classifyIntent(message);
|
||
const result = await queryData(answer);
|
||
|
||
// 超时了!企微已断开连接
|
||
return reply.send(result);
|
||
}
|
||
```
|
||
|
||
#### 正确做法:异步回复模式
|
||
```typescript
|
||
✅ async handleCallback(request, reply) {
|
||
const message = parseMessage(request.body);
|
||
|
||
// 1. 立即返回'success'(<1秒)
|
||
reply.send('success');
|
||
|
||
// 2. 异步处理(不阻塞)
|
||
setImmediate(async () => {
|
||
// 可以慢慢算(10秒、20秒都可以)
|
||
const answer = await classifyIntent(message);
|
||
const result = await queryData(answer);
|
||
|
||
// 3. 使用主动推送接口返回结果
|
||
await wechatService.sendTextMessage(userId, result);
|
||
});
|
||
}
|
||
```
|
||
|
||
#### 关键点
|
||
1. ✅ **立即返回**:收到消息后<1秒返回'success'
|
||
2. ✅ **异步处理**:使用`setImmediate`或消息队列
|
||
3. ✅ **主动推送**:调用发送消息接口,而非HTTP Response
|
||
4. ✅ **错误处理**:异步任务失败时,推送友好错误提示
|
||
|
||
---
|
||
|
||
### 5.2 企业微信XML加解密(🔥 Critical)
|
||
|
||
#### 问题描述
|
||
企业微信的消息体是**加密的XML**,不是明文。格式如下:
|
||
```xml
|
||
<xml>
|
||
<ToUserName><![CDATA[ww01cb7b72ea2db83c]]></ToUserName>
|
||
<Encrypt><![CDATA[加密的消息内容]]></Encrypt>
|
||
</xml>
|
||
```
|
||
|
||
#### 解密流程
|
||
```
|
||
加密XML → Base64解码 → AES解密 → 去除Padding → 验证CorpID → 明文XML
|
||
```
|
||
|
||
#### 错误做法
|
||
```typescript
|
||
❌ // 自己实现AES解密(极易出错)
|
||
function decrypt(encryptedData) {
|
||
// 手写AES、Padding、Base64...
|
||
// 99%的人都会写错
|
||
}
|
||
```
|
||
|
||
#### 正确做法:使用官方库
|
||
```bash
|
||
npm install @wecom/crypto
|
||
```
|
||
|
||
```typescript
|
||
✅ import { WXBizMsgCrypt } from '@wecom/crypto';
|
||
|
||
const crypt = new WXBizMsgCrypt(
|
||
process.env.WECHAT_TOKEN!, // 自定义Token
|
||
process.env.WECHAT_ENCODING_AES_KEY!, // 企微后台生成(43位)
|
||
process.env.WECHAT_CORP_ID!
|
||
);
|
||
|
||
// 一行代码完成解密
|
||
const decryptedXml = crypt.decrypt(
|
||
msg_signature,
|
||
timestamp,
|
||
nonce,
|
||
encryptedXml
|
||
);
|
||
```
|
||
|
||
#### 需要的配置
|
||
| 配置项 | 来源 | 示例 |
|
||
|--------|------|------|
|
||
| `WECHAT_TOKEN` | 自定义 | 32位随机字符串 |
|
||
| `WECHAT_ENCODING_AES_KEY` | 企微后台生成 | 43位字符(如:abcdefghijklmnopqrstuvwxyz0123456789ABC) |
|
||
| `WECHAT_CORP_ID` | 企微后台 | ww01cb7b72ea2db83c |
|
||
|
||
---
|
||
|
||
### 5.3 企业微信IP白名单(🔥 P0)
|
||
|
||
#### 问题描述
|
||
获取AccessToken时,企业微信会校验请求来源IP。如果IP不在白名单中,会返回**60020错误**。
|
||
|
||
#### 配置步骤
|
||
1. 确认SAE NAT网关IP:`182.92.176.14`
|
||
2. 登录企业微信管理后台
|
||
3. 应用管理 → IIT Manager Agent
|
||
4. 找到"企业可信IP"配置
|
||
5. 添加:`182.92.176.14`
|
||
6. 保存并测试
|
||
|
||
#### 验证方法
|
||
```typescript
|
||
// 测试AccessToken获取
|
||
const response = await fetch(
|
||
`https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${CORP_ID}&corpsecret=${SECRET}`
|
||
);
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.errcode === 60020) {
|
||
console.error('❌ IP白名单未配置!');
|
||
} else if (data.errcode === 0) {
|
||
console.log('✅ IP白名单配置正确');
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5.4 企业微信消息格式
|
||
|
||
#### 文本消息格式
|
||
```json
|
||
{
|
||
"touser": "GaoFeng",
|
||
"msgtype": "text",
|
||
"agentid": 1000002,
|
||
"text": {
|
||
"content": "这是一条文本消息"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 卡片消息格式
|
||
```json
|
||
{
|
||
"touser": "GaoFeng",
|
||
"msgtype": "textcard",
|
||
"agentid": 1000002,
|
||
"textcard": {
|
||
"title": "📊 test0102 - 数据录入",
|
||
"description": "<div>受试者:8</div><div>操作:新增</div><div>录入8个字段</div>",
|
||
"url": "https://iit.xunzhengyixue.com/chat",
|
||
"btntxt": "查看详情"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.5 企业微信回调消息格式
|
||
|
||
#### 用户文本消息(XML格式)
|
||
```xml
|
||
<xml>
|
||
<ToUserName><![CDATA[ww01cb7b72ea2db83c]]></ToUserName>
|
||
<FromUserName><![CDATA[GaoFeng]]></FromUserName>
|
||
<CreateTime>1641024000</CreateTime>
|
||
<MsgType><![CDATA[text]]></MsgType>
|
||
<Content><![CDATA[最近一周数据汇总]]></Content>
|
||
<MsgId>1234567890</MsgId>
|
||
<AgentID>1000002</AgentID>
|
||
</xml>
|
||
```
|
||
|
||
#### 回复消息格式(XML格式)
|
||
```xml
|
||
<xml>
|
||
<ToUserName><![CDATA[GaoFeng]]></ToUserName>
|
||
<FromUserName><![CDATA[ww01cb7b72ea2db83c]]></FromUserName>
|
||
<CreateTime>1641024001</CreateTime>
|
||
<MsgType><![CDATA[text]]></MsgType>
|
||
<Content><![CDATA[📊 最近一周数据汇总:
|
||
新增受试者:2例
|
||
数据更新:5次]]></Content>
|
||
</xml>
|
||
```
|
||
|
||
### 5.6 企业微信签名验证
|
||
|
||
```typescript
|
||
function verifySignature(signature: string, timestamp: string, nonce: string, echostr: string): boolean {
|
||
const token = WECHAT_TOKEN; // 企业微信配置的Token
|
||
const tmpArr = [token, timestamp, nonce, echostr].sort();
|
||
const tmpStr = tmpArr.join('');
|
||
const sha1 = crypto.createHash('sha1');
|
||
sha1.update(tmpStr);
|
||
const calculatedSignature = sha1.digest('hex');
|
||
return calculatedSignature === signature;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 六、下一步扩展方向(Phase 1.5+)
|
||
|
||
### Phase 1.5:数据质控规则生成(1-2天)
|
||
|
||
**目标**:实现基于规则的数据质控
|
||
|
||
**核心功能**:
|
||
1. 上传研究方案PDF
|
||
2. AI自动提取质控规则
|
||
3. 规则库管理(可编辑、可审核)
|
||
4. 基于规则进行数据质控
|
||
5. 质控结果推送到企业微信
|
||
|
||
**技术方案**:
|
||
- 使用大模型提取规则(非实时调用)
|
||
- 规则保存到数据库(`quality_rules`表)
|
||
- 录入时基于规则判断(快速、可靠)
|
||
- 证据链追溯(规则来源于方案第X页第Y条)
|
||
|
||
---
|
||
|
||
### Phase 2:PC Workbench前端(2-3天)
|
||
|
||
**目标**:提供Web端复核界面
|
||
|
||
**核心功能**:
|
||
1. 质控建议列表
|
||
2. 当前数据 vs AI建议对比
|
||
3. 证据链展示(PDF高亮)
|
||
4. 审批操作(确认/拒绝)
|
||
5. REDCap数据回写
|
||
|
||
---
|
||
|
||
### Phase 3:智能化演进(长期)
|
||
|
||
**功能扩展**:
|
||
1. 多轮对话能力
|
||
2. 上下文理解
|
||
3. 任务驱动引擎
|
||
4. 患者随访Agent
|
||
5. 智能汇报Agent
|
||
|
||
---
|
||
|
||
## 📋 七、检查清单
|
||
|
||
### Day 3开始前
|
||
- [ ] 企业微信UserID确认(用于测试)
|
||
- [ ] 企业微信回调URL配置(在企业微信管理后台)
|
||
- [ ] REDCap测试数据准备(至少5条)
|
||
- [ ] 后端服务运行正常
|
||
|
||
### Day 3结束时
|
||
- [ ] WechatService开发完成
|
||
- [ ] iit_quality_check Worker完善
|
||
- [ ] 企业微信推送测试通过
|
||
- [ ] 端到端闭环打通(REDCap → 企微)
|
||
|
||
### Day 4结束时
|
||
- [ ] WechatCallbackController开发完成
|
||
- [ ] 企业微信对话功能测试通过
|
||
- [ ] 完整闭环压力测试通过
|
||
- [ ] 文档全部更新完成
|
||
- [ ] 代码提交到Git
|
||
|
||
---
|
||
|
||
## 🎊 八、成功标准
|
||
|
||
**MVP成功的标志**:
|
||
1. ✅ PI可以在企业微信中实时收到数据录入通知
|
||
2. ✅ PI可以在企业微信中查询项目数据
|
||
3. ✅ 完整闭环(REDCap → Node.js → 企微)稳定运行
|
||
4. ✅ 响应时间满足要求(<5秒)
|
||
5. ✅ 可以现场演示给团队看
|
||
|
||
**价值验证**:
|
||
- ✅ PI不用每天登录REDCap查看进度
|
||
- ✅ 数据录入后立即知晓,不会遗漏
|
||
- ✅ 随时随地可以查询数据(手机端)
|
||
- ✅ 为后续功能扩展打下基础
|
||
|
||
---
|
||
|
||
**文档维护者**:开发团队
|
||
**最后更新**:2026-01-02
|
||
**状态**:📋 开发计划(待执行)
|
||
|