Files
AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/04-开发计划/最小MVP闭环开发计划.md
HaHafeng 6a567f028f feat(iit-manager): 完成MVP闭环 - 企业微信集成与端到端测试
核心交付物:
- WechatService (314行): Access Token缓存 + 消息推送
- WechatCallbackController (501行): URL验证 + 消息接收
- 质控Worker完善: 质控逻辑 + 企业微信推送 + 审计日志
- Worker注册修复: initIitManager() 在启动时调用
- 数据库字段修复: action -> action_type
- 端到端测试通过: <2秒延迟, 100%成功率

性能指标:
- Webhook响应: 5.8ms (目标<10ms)
- Worker执行: ~50ms (目标<100ms)
- 端到端延迟: <2秒 (目标<5秒)
- 消息成功率: 100% (测试5次)

临时措施:
- UserID从环境变量获取 (Phase 2改进)
- 定时轮询暂时禁用 (Phase 2添加)
- 质控逻辑简化 (Phase 1.5集成Dify)

Closes #IIT-MVP-Day3
2026-01-03 14:19:08 +08:00

42 KiB
Raw Blame History

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 2AI建议审批后回写到REDCap
  • ⏸️ 微信小程序Phase 3移动端原生体验
  • ⏸️ 复杂对话能力Phase 3多轮对话、上下文理解

📊 二、当前状态总结

2.1 已完成的准备工作Day 1-2

Day 1环境初始化2026-01-01

数据库

  • 创建 iit_schema5个表
  • 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 2REDCap实时集成2026-01-02

核心交付物~2,200行代码

  1. RedcapAdapter.ts271行

    • exportRecords() - 导出记录(支持全量/增量/指定记录)
    • exportMetadata() - 导出字段定义
    • importRecords() - 导入记录(预留)
    • testConnection() - 连接测试
  2. WebhookController.ts327行

    • handleWebhook() - 接收REDCap DET触发<10ms响应
    • 幂等性检查(防止重复处理)
    • 审计日志记录
    • 推送到质控队列(iit_quality_check
  3. SyncManager.ts398行

    • 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.ts189行- API适配器测试
  • test-redcap-webhook.ts274行- Webhook接收器测试
  • test-redcap-integration.ts449行- 端到端集成测试

测试结果

  • 集成测试通过率12/12100%
  • 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个企业微信对接的致命陷阱

风险A5秒超时魔咒 🔥 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结果。


风险BXML加解密 🔥 P0

问题:企业微信的消息体是加密的XMLEncrypt字段不是明文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);

规避措施:强制使用官方库,禁止自己实现加解密。


风险CIP白名单限制 🔥 P0

问题获取AccessToken时企业微信会校验请求来源IP。

现状SAE通过NAT网关访问外网

  • NAT网关IP182.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做意图分类

// ✅ 真正的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-039小时🔥

准备工作企业微信配置检查1小时⚠️ 必须先做

任务0企业微信配置完整性检查

配置清单

  1. 企业可信IP配置 🔥 Critical

    • 登录企业微信管理后台
    • 应用管理 → IIT Manager Agent → 企业可信IP
    • 添加:182.92.176.14SAE NAT网关IP
    • 测试调用getAccessToken验证不报60020错误
  2. 获取EncodingAESKey 🔥 Critical

    • 企业微信后台 → 应用管理 → IIT Manager Agent
    • 开发者接口 → 接收消息
    • 点击"随机生成"按钮获取43位AESKey
    • 复制保存(用于消息解密)
  3. 设置Token 🔥 Critical

    • 自定义一个Token用于签名验证
    • 建议32位随机字符串
    • 保存到环境变量
  4. 配置回调URL

    • URLhttps://iit.xunzhengyixue.com/api/v1/iit/wechat/callback
    • 验证Token、EncodingAESKey配置正确

环境变量新增

# 企业微信配置(已有)
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.ts2小时

文件位置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缓存到Postgres7000秒过期时间
  • 重试机制3次重试指数退避
  • 错误处理和日志记录
  • 支持文本消息和卡片消息

验收标准

  • AccessToken可以正确获取和缓存
  • 可以发送文本消息到指定用户
  • 可以发送卡片消息
  • 缓存机制正常工作

任务2完善 iit_quality_check Worker1小时

文件位置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.ts2.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岁
性别:男
BMI18.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消息推送成功
  • 手机端全部接收正常

技术亮点

  1. 异步Worker架构符合Postgres-Only最佳范式
  2. 企业微信消息加解密:完整实现签名验证和加解密
  3. 异步回复模式setImmediate 确保5秒内响应
  4. 完整的错误处理:审计日志失败不影响主流程
  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或数据库

问题与解决

  1. Worker未注册initIitManager() 未调用 → 在 src/index.ts 中添加调用
  2. 字段名错误actionaction_type2处修复
  3. 循环发送审计日志错误导致Worker失败重试 → 添加try-catch
  4. notification_config不存在:移除字段查询,直接使用环境变量

参考文档

  • 06-开发记录/Day3-企业微信集成与端到端测试完成记录.md
  • backend/WECHAT_ENV_CONFIG.md

Day 4完善与文档2026-01-046小时⏸️

上午优化与测试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小时

优化内容

  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秒内完成。如果超时,用户会看到"服务暂时不可用"的提示。

错误做法

 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. 立即返回:收到消息后<1秒返回'success'
  2. 异步处理:使用setImmediate或消息队列
  3. 主动推送调用发送消息接口而非HTTP Response
  4. 错误处理:异步任务失败时,推送友好错误提示

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错误

配置步骤

  1. 确认SAE NAT网关IP182.92.176.14
  2. 登录企业微信管理后台
  3. 应用管理 → IIT Manager Agent
  4. 找到"企业可信IP"配置
  5. 添加:182.92.176.14
  6. 保存并测试

验证方法

// 测试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天

目标:实现基于规则的数据质控

核心功能

  1. 上传研究方案PDF
  2. AI自动提取质控规则
  3. 规则库管理(可编辑、可审核)
  4. 基于规则进行数据质控
  5. 质控结果推送到企业微信

技术方案

  • 使用大模型提取规则(非实时调用)
  • 规则保存到数据库(quality_rules表)
  • 录入时基于规则判断(快速、可靠)
  • 证据链追溯规则来源于方案第X页第Y条

Phase 2PC 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
状态📋 开发计划(待执行)