Completed features: - Created Dify dataset (Dify_test0102) with 2 processed documents - Linked test0102 project with Dify dataset ID - Extended intent detection to recognize query_protocol intent - Implemented queryDifyKnowledge method (semantic search Top 5) - Integrated hybrid retrieval (REDCap data + Dify documents) - Fixed AI hallucination bugs (intent detection + API field path) - Developed debugging scripts - Completed end-to-end testing (5 scenarios passed) - Generated comprehensive documentation (600+ lines) - Updated development plans and module status Technical highlights: - Single project single knowledge base architecture - Smart routing based on user intent - Prevent AI hallucination by injecting real data/documents - Session memory for multi-turn conversations - Reused LLMFactory for DeepSeek-V3 integration Bug fixes: - Fixed intent detection missing keywords - Fixed Dify API response field path error Testing: All scenarios verified in WeChat production environment Status: Fully tested and deployed
47 KiB
IIT Manager Agent - 最小MVP闭环开发计划
版本: v1.0
创建日期: 2026-01-02
目标: 打通 REDCap → Node.js → 企业微信 的完整闭环
预估工作量: 2天(Day 3-4)
核心理念: 先打通最小闭环,验证技术可行性,再逐步丰富功能
🎯 一、MVP定义与目标
1.1 什么是最小闭环?
核心闭环:
REDCap录入数据 → Node.js实时捕获 → 企业微信智能通知 → PI对话查询
验证价值:
- ✅ 实时感知:PI无需登录REDCap,随时掌握项目进展
- ✅ 主动通知:数据录入后立即推送,不会遗漏
- ✅ 智能交互:在企业微信中即可查询数据、获取报告
- ✅ 易扩展:闭环打通后,可以快速添加质控、提醒、统计等功能
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
- CorpID:
- ✅ 网页授权及JS-SDK授权已获取
- ✅ 可信域名配置成功:
iit.xunzhengyixue.com - ✅ 域名验证文件部署:
WW_verify_YnhsQBwI0ARnNoG0.txt - ✅ Access Token获取测试通过
模块骨架:
- ✅ 目录结构创建
- ✅ 类型定义完整(223行)
- ✅ 路由前缀配置(
/api/v1/iit)
✅ Day 2:REDCap实时集成(2026-01-02)
核心交付物(~2,200行代码):
-
✅ RedcapAdapter.ts(271行)
exportRecords()- 导出记录(支持全量/增量/指定记录)exportMetadata()- 导出字段定义importRecords()- 导入记录(预留)testConnection()- 连接测试
-
✅ WebhookController.ts(327行)
handleWebhook()- 接收REDCap DET触发(<10ms响应)- 幂等性检查(防止重复处理)
- 审计日志记录
- 推送到质控队列(
iit_quality_check)
-
✅ SyncManager.ts(398行)
initScheduledJob()- 初始化定时任务(每5分钟)handlePoll()- 轮询所有active项目manualSync()- 手动同步fullSync()- 全量同步
-
✅ Worker注册(91行)
iit_quality_check- 质控任务(已注册,待补充逻辑)iit_redcap_poll- 定时轮询任务
-
✅ 路由配置(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
关键技术突破:
- ✅ REDCap DET实时触发(0秒延迟)
- ✅ form-urlencoded格式支持(REDCap原生格式)
- ✅ 双保险机制(Webhook + 定时轮询)
- ✅ 符合团队开发规范(队列命名、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,企微早已提示"服务暂时不可用"并断开连接。
错误做法:
❌ async handleCallback(request, reply) {
const answer = await callLLM(message); // 可能耗时10秒
return reply.send(answer); // 超时了!
}
正确做法:采用异步回复模式
✅ 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规则。
错误做法:自己写加解密算法(极易出错)
正确做法:使用官方库
npm install @wecom/crypto
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错误
规避措施:
- 登录企业微信管理后台
- 进入"应用管理" → "IIT Manager Agent"
- 找到"企业可信IP"配置
- 添加:
182.92.176.14
风险D:关键词匹配 vs AI Agent ⚠️ P2
问题:简单的关键词匹配(if (msg.includes('汇总')))不是AI Agent,只是if-else机器人。
正确做法:使用LLM做意图分类
// ✅ 真正的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:企业微信配置完整性检查
配置清单:
-
企业可信IP配置 🔥 Critical
- 登录企业微信管理后台
- 应用管理 → IIT Manager Agent → 企业可信IP
- 添加:
182.92.176.14(SAE NAT网关IP) - 测试:调用getAccessToken,验证不报60020错误
-
获取EncodingAESKey 🔥 Critical
- 企业微信后台 → 应用管理 → IIT Manager Agent
- 开发者接口 → 接收消息
- 点击"随机生成"按钮,获取43位AESKey
- 复制保存(用于消息解密)
-
设置Token 🔥 Critical
- 自定义一个Token(用于签名验证)
- 建议:32位随机字符串
- 保存到环境变量
-
配置回调URL
- URL:
https://iit.xunzhengyixue.com/api/v1/iit/wechat/callback - 验证Token、EncodingAESKey配置正确
- URL:
环境变量新增:
# 企业微信配置(已有)
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
核心功能:
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)
更新内容:
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分钟)
cd backend
npm install @wecom/crypto
npm install xml2js # XML解析
验收标准:
- ✅ 依赖安装成功
- ✅ 可以import成功
任务5:创建 WechatCallbackController.ts(2.5小时)🔥 关键
文件位置:backend/src/modules/iit-manager/controllers/WechatCallbackController.ts
核心功能(异步回复模式):
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
新增路由:
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 3 完成总结(2026-01-03)✅
实际完成时间:2026-01-03
任务完成度:100%
核心成果
| 交付物 | 代码量 | 状态 |
|---|---|---|
| WechatService(企业微信推送) | 314行 | ✅ 完成 |
| WechatCallbackController(回调处理) | 501行 | ✅ 完成 |
| 质控Worker完善 | 336行 | ✅ 完成 |
Worker注册修复(initIitManager) |
- | ✅ 完成 |
数据库字段修复(action_type) |
- | ✅ 完成 |
| 端到端测试 | - | ✅ 通过 |
| WECHAT_ENV_CONFIG.md | 401行 | ✅ 完成 |
| 总计 | 1,755行 | ✅ 完成 |
关键里程碑
🎯 MVP闭环完全打通:
REDCap录入数据 → Node.js实时捕获(<10ms)
→ Worker处理(~50ms)
→ 企业微信推送通知(<2秒)
→ 手机端接收✅
性能指标
| 指标 | 目标 | 实际 | 状态 |
|---|---|---|---|
| Webhook响应时间 | <10ms | 5.8ms | ✅ 超出预期 |
| Worker执行时间 | <100ms | ~50ms | ✅ 超出预期 |
| 端到端延迟 | <5秒 | <2秒 | ✅ 超出预期 |
| 消息发送成功率 | >99% | 100% | ✅ 超出预期 |
测试验证
端到端测试(已通过):
- ✅ REDCap创建记录 ID 9
- ✅ DET实时触发(0秒延迟)
- ✅ Webhook接收(5.8ms响应)
- ✅ 任务推送到pg-boss队列
- ✅ Worker执行质控检查
- ✅ 发送企业微信通知
- ✅ 手机端成功接收通知
- ✅ 审计日志记录成功
- ✅ 无循环发送问题
企业微信推送测试(已通过):
- ✅ 文本消息推送成功
- ✅ Textcard卡片消息推送成功
- ✅ Markdown消息推送成功
- ✅ 手机端全部接收正常
技术亮点
- 异步Worker架构:符合Postgres-Only最佳范式
- 企业微信消息加解密:完整实现签名验证和加解密
- 异步回复模式:
setImmediate确保5秒内响应 - 完整的错误处理:审计日志失败不影响主流程
- pg-boss重试机制:自动重试3次,确保可靠性
临时措施与技术债务
| 序号 | 临时措施 | 改进计划 |
|---|---|---|
| 1 | UserID硬编码(环境变量) | Phase 2: 从项目配置表读取 |
| 2 | 定时轮询禁用 | Phase 2: 使用node-cron或扩展PgBossQueue |
| 3 | 质控逻辑简化(无AI) | Phase 1.5: 集成Dify RAG |
| 4 | notification_config字段未创建 |
Phase 2: 添加JSONB字段 |
| 5 | Access Token内存缓存 | Phase 2: 使用Redis或数据库 |
问题与解决
- Worker未注册:
initIitManager()未调用 → 在src/index.ts中添加调用 - 字段名错误:
action→action_type(2处修复) - 循环发送:审计日志错误导致Worker失败重试 → 添加try-catch
notification_config不存在:移除字段查询,直接使用环境变量
参考文档
06-开发记录/Day3-企业微信集成与端到端测试完成记录.mdbackend/WECHAT_ENV_CONFIG.md
📊 Phase 1.5:AI对话能力(2026-01-03 & 2026-01-04)✅
任务目标:在企业微信中实现AI对话查询能力
实际完成时间:
- Day 3 下午(2026-01-03):基础对话 + REDCap集成
- Day 4 上午(2026-01-04):Dify知识库集成
任务完成度:100%
核心成果(Phase 1.5)
| 交付物 | 代码量 | 状态 |
|---|---|---|
| ChatService.ts(对话服务) | 485行 | ✅ 完成 |
| SessionMemory.ts(会话记忆) | 120行 | ✅ 完成 |
| WechatCallbackController(对话集成) | 更新 | ✅ 完成 |
| Dify知识库关联 | 脚本 | ✅ 完成 |
| 意图识别优化 | 扩展 | ✅ 完成 |
| Bug修复(字段路径) | 关键修复 | ✅ 完成 |
| 调试脚本(2个) | 280行 | ✅ 完成 |
| 开发记录文档 | 600+行 | ✅ 完成 |
| 总计 | ~1,485行 | ✅ 完成 |
关键里程碑
🎯 混合检索架构实现:
用户提问(企业微信)
↓
意图识别(ChatService.detectIntent)
↓
┌───────────────┬───────────────┬──────────────┐
│ query_protocol│ query_record │ count_records│
│ (文档查询) │ (记录查询) │ (统计查询) │
└───────┬───────┴───────┬───────┴──────┬───────┘
↓ ↓ ↓
Dify API REDCap API REDCap API
(知识库) (患者数据) (患者数据)
↓ ↓ ↓
文档片段 JSON数据 JSON数据
↓ ↓ ↓
└───────────────┴──────────────┘
↓
构建LLM Prompt
(System + Context + Data)
↓
DeepSeek-V3
↓
AI回答
功能验证
测试项目: test0102
- REDCap PID: 16, 11条记录
- Dify Dataset ID:
b49595b2-bf71-4e47-9988-4aa2816d3c6f - 文档: 研究方案、CRF表格(2个文件,已处理)
测试场景:
| 场景 | 用户问题 | 数据源 | 结果 | 状态 |
|---|---|---|---|---|
| 文档查询 | "这个研究的排除标准是什么?" | Dify | 基于文档准确回答 | ✅ 通过 |
| CRF查询 | "CRF表格中有哪些观察指标?" | Dify | 基于文档准确回答 | ✅ 通过 |
| 患者查询 | "ID 7的患者情况" | REDCap | 完全匹配真实数据 | ✅ 通过 |
| 统计查询 | "目前入组了多少人?" | REDCap | 准确统计11人 | ✅ 通过 |
| 混合查询 | "这个研究的主要研究目的是什么?" | Dify | 基于文档回答 | ✅ 通过 |
| 上下文记忆 | 多轮对话 | SessionMemory | 记得上一轮内容 | ✅ 通过 |
技术亮点
- 混合检索:同时支持结构化数据(REDCap)和非结构化文档(Dify)
- 智能路由:根据意图自动选择数据源
- 防止幻觉:所有回答基于真实数据/文档,绝不编造
- 上下文记忆:SessionMemory保存最近3轮对话
- 复用LLMFactory:零配置使用DeepSeek-V3
- 即时反馈:"正在查询"消息,避免用户焦虑
问题排查与修复
关键Bug修复:
| 问题 | 根因 | 解决方案 | 状态 |
|---|---|---|---|
| AI编造答案 | 意图识别关键词不全(缺"入选"等) | 扩充关键词列表 | ✅ 已修复 |
| Dify内容为undefined | 错误的API响应字段路径 | 修正为record.segment.document.name和record.segment.content |
✅ 已修复 |
调试工具:
debug-dify-injection.ts:追踪Dify结果注入流程inspect-dify-response.ts:查看Dify API实际返回结构
核心能力
- ✅ 实时数据查询:通过REDCap API查询患者CRF数据
- ✅ 研究方案查询:通过Dify知识库检索研究方案、CRF、伦理文件
- ✅ 智能理解:自然语言提问,无需记忆命令
- ✅ 上下文理解:多轮对话,支持代词解析
- ✅ 数据真实性:所有回答基于真实数据,绝不编造
- ✅ 混合检索:同时查询多个数据源,统一呈现
参考文档
Day 4:完善与文档(2026-01-04,6小时)⏸️
上午:优化与测试(3小时)
任务8:完善审计日志(1小时)
新增日志类型:
// 企业微信交互日志
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小时)
优化内容:
-
企业微信API调用失败:
- 自动重试3次
- 记录错误日志
- 降级处理(通知失败不影响主流程)
-
REDCap API超时:
- 30秒超时
- 友好错误提示
- 返回缓存数据(如果有)
-
用户消息解析失败:
- 返回友好提示
- 记录错误日志
- 不影响其他功能
验收标准:
- ✅ 所有错误场景有友好提示
- ✅ 系统能自动重试
- ✅ 错误不影响其他任务执行
任务10:完整闭环压力测试(1小时)
测试内容:
-
连续录入测试:
- 在REDCap中连续录入10条数据
- 验证所有通知都能正确发送
- 验证没有遗漏和重复
-
并发对话测试:
- 同时发送5个查询请求
- 验证所有请求都能正确响应
- 验证响应时间<5秒
-
长时间运行测试:
- 系统运行1小时
- 期间随机录入数据和查询
- 验证系统稳定性
验收标准:
- ✅ 连续录入测试通过(10/10)
- ✅ 并发对话测试通过(5/5)
- ✅ 长时间运行无异常
- ✅ 内存和性能正常
下午:文档编写(3小时)
任务11:创建开发完成文档(2小时)
文档位置:docs/03-业务模块/IIT Manager Agent/06-开发记录/Day3-4-最小MVP闭环完成记录.md
文档内容:
- 开发概述
- 完成的功能模块(详细说明)
- 测试验证结果
- 性能指标
- 遇到的问题与解决方案
- 闭环效果演示
- 下一步计划
任务12:更新模块状态文档(30分钟)
文档位置:docs/03-业务模块/IIT Manager Agent/00-模块当前状态与开发指南.md
更新内容:
- 更新整体完成度(35% → 50%)
- 更新已完成功能列表
- 更新Day 3-4完成情况
- 更新下一步工作计划
任务13:创建使用手册(30分钟)
文档位置:docs/03-业务模块/IIT Manager Agent/05-使用手册/企业微信对话指南.md
文档内容:
- 企业微信应用配置
- 用户如何加入应用
- 支持的对话指令列表
- 常见问题FAQ
- 故障排查指南
✅ 四、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秒内完成。如果超时,用户会看到"服务暂时不可用"的提示。
错误做法
❌ 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);
}
正确做法:异步回复模式
✅ 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秒返回'success'
- ✅ 异步处理:使用
setImmediate或消息队列 - ✅ 主动推送:调用发送消息接口,而非HTTP Response
- ✅ 错误处理:异步任务失败时,推送友好错误提示
5.2 企业微信XML加解密(🔥 Critical)
问题描述
企业微信的消息体是加密的XML,不是明文。格式如下:
<xml>
<ToUserName><![CDATA[ww01cb7b72ea2db83c]]></ToUserName>
<Encrypt><![CDATA[加密的消息内容]]></Encrypt>
</xml>
解密流程
加密XML → Base64解码 → AES解密 → 去除Padding → 验证CorpID → 明文XML
错误做法
❌ // 自己实现AES解密(极易出错)
function decrypt(encryptedData) {
// 手写AES、Padding、Base64...
// 99%的人都会写错
}
正确做法:使用官方库
npm install @wecom/crypto
✅ 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错误。
配置步骤
- 确认SAE NAT网关IP:
182.92.176.14 - 登录企业微信管理后台
- 应用管理 → IIT Manager Agent
- 找到"企业可信IP"配置
- 添加:
182.92.176.14 - 保存并测试
验证方法
// 测试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 企业微信消息格式
文本消息格式
{
"touser": "GaoFeng",
"msgtype": "text",
"agentid": 1000002,
"text": {
"content": "这是一条文本消息"
}
}
卡片消息格式
{
"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>
<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>
<ToUserName><![CDATA[GaoFeng]]></ToUserName>
<FromUserName><![CDATA[ww01cb7b72ea2db83c]]></FromUserName>
<CreateTime>1641024001</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[📊 最近一周数据汇总:
新增受试者:2例
数据更新:5次]]></Content>
</xml>
5.6 企业微信签名验证
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天)
目标:实现基于规则的数据质控
核心功能:
- 上传研究方案PDF
- AI自动提取质控规则
- 规则库管理(可编辑、可审核)
- 基于规则进行数据质控
- 质控结果推送到企业微信
技术方案:
- 使用大模型提取规则(非实时调用)
- 规则保存到数据库(
quality_rules表) - 录入时基于规则判断(快速、可靠)
- 证据链追溯(规则来源于方案第X页第Y条)
Phase 2:PC Workbench前端(2-3天)
目标:提供Web端复核界面
核心功能:
- 质控建议列表
- 当前数据 vs AI建议对比
- 证据链展示(PDF高亮)
- 审批操作(确认/拒绝)
- REDCap数据回写
Phase 3:智能化演进(长期)
功能扩展:
- 多轮对话能力
- 上下文理解
- 任务驱动引擎
- 患者随访Agent
- 智能汇报Agent
📋 七、检查清单
Day 3开始前
- 企业微信UserID确认(用于测试)
- 企业微信回调URL配置(在企业微信管理后台)
- REDCap测试数据准备(至少5条)
- 后端服务运行正常
Day 3结束时
- WechatService开发完成
- iit_quality_check Worker完善
- 企业微信推送测试通过
- 端到端闭环打通(REDCap → 企微)
Day 4结束时
- WechatCallbackController开发完成
- 企业微信对话功能测试通过
- 完整闭环压力测试通过
- 文档全部更新完成
- 代码提交到Git
🎊 八、成功标准
MVP成功的标志:
- ✅ PI可以在企业微信中实时收到数据录入通知
- ✅ PI可以在企业微信中查询项目数据
- ✅ 完整闭环(REDCap → Node.js → 企微)稳定运行
- ✅ 响应时间满足要求(<5秒)
- ✅ 可以现场演示给团队看
价值验证:
- ✅ PI不用每天登录REDCap查看进度
- ✅ 数据录入后立即知晓,不会遗漏
- ✅ 随时随地可以查询数据(手机端)
- ✅ 为后续功能扩展打下基础
文档维护者:开发团队
最后更新:2026-01-02
状态:📋 开发计划(待执行)