Files
AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/04-开发计划/最小MVP闭环开发计划.md
HaHafeng dfc472810b feat(iit-manager): Integrate Dify knowledge base for hybrid retrieval
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
2026-01-04 15:44:11 +08:00

47 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

📊 Phase 1.5AI对话能力2026-01-03 & 2026-01-04

任务目标在企业微信中实现AI对话查询能力

实际完成时间

  • Day 3 下午2026-01-03基础对话 + REDCap集成
  • Day 4 上午2026-01-04Dify知识库集成

任务完成度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 记得上一轮内容 通过

技术亮点

  1. 混合检索同时支持结构化数据REDCap和非结构化文档Dify
  2. 智能路由:根据意图自动选择数据源
  3. 防止幻觉:所有回答基于真实数据/文档,绝不编造
  4. 上下文记忆SessionMemory保存最近3轮对话
  5. 复用LLMFactory零配置使用DeepSeek-V3
  6. 即时反馈"正在查询"消息,避免用户焦虑

问题排查与修复

关键Bug修复

问题 根因 解决方案 状态
AI编造答案 意图识别关键词不全(缺"入选"等) 扩充关键词列表 已修复
Dify内容为undefined 错误的API响应字段路径 修正为record.segment.document.namerecord.segment.content 已修复

调试工具

  • debug-dify-injection.ts追踪Dify结果注入流程
  • inspect-dify-response.ts查看Dify API实际返回结构

核心能力

  1. 实时数据查询通过REDCap API查询患者CRF数据
  2. 研究方案查询通过Dify知识库检索研究方案、CRF、伦理文件
  3. 智能理解:自然语言提问,无需记忆命令
  4. 上下文理解:多轮对话,支持代词解析
  5. 数据真实性:所有回答基于真实数据,绝不编造
  6. 混合检索:同时查询多个数据源,统一呈现

参考文档


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