feat(iit): Initialize IIT Manager Agent MVP - Day 1 complete

- Add iit_schema with 5 tables
- Create module structure and types (223 lines)
- WeChat integration verified (Access Token success)
- Update system docs to v2.4
- Add REDCap source folders to .gitignore
- Day 1/14 complete (11/11 tasks)
This commit is contained in:
2025-12-31 18:35:05 +08:00
parent decff0bb1f
commit 4c5bb3d174
154 changed files with 13759 additions and 8 deletions

View File

@@ -323,6 +323,12 @@ runTests().catch((error) => {

View File

@@ -302,6 +302,12 @@ Content-Type: application/json

View File

@@ -238,6 +238,12 @@ export const conflictDetectionService = new ConflictDetectionService();

View File

@@ -188,6 +188,12 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \

View File

@@ -242,6 +242,12 @@ export const streamAIController = new StreamAIController();

View File

@@ -0,0 +1,16 @@
/**
* IIT Manager Agent 模块
*
* 核心功能:
* - REDCap数据同步混合模式Webhook + 轮询)
* - AI智能质控Dify RAG
* - 影子状态管理
* - 企微卡片通知
*
* @module iit-manager
* @version 1.1.0
*/
export * from './routes';
export * from './types';

View File

@@ -0,0 +1,24 @@
/**
* IIT Manager 路由注册
*/
import { FastifyInstance } from 'fastify';
export async function registerIitRoutes(fastify: FastifyInstance) {
// 健康检查
fastify.get('/api/v1/iit/health', async (request, reply) => {
return {
status: 'ok',
module: 'iit-manager',
version: '1.1.0',
timestamp: new Date().toISOString()
};
});
// TODO: 注册其他路由
// - 项目管理路由
// - Webhook路由
// - 影子状态路由
// - 任务管理路由
}

View File

@@ -0,0 +1,152 @@
/**
* IIT Manager 数据库测试脚本
* 验证Schema和CRUD操作
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function testIitDatabase() {
console.log('🔍 Testing IIT Manager Database...\n');
try {
// ==================== 测试1创建项目 ====================
console.log('✅ Test 1: 创建IIT项目');
const testProject = await prisma.iitProject.create({
data: {
name: 'Test IIT Project',
description: '这是一个测试项目',
fieldMappings: {
age: 'patient_age',
gender: 'sex',
enrollmentDate: 'consent_date'
},
redcapProjectId: '123',
redcapApiToken: 'test_token_12345',
redcapUrl: 'https://redcap.example.com',
status: 'active'
}
});
console.log(` ✓ 项目ID: ${testProject.id}`);
console.log(` ✓ 项目名称: ${testProject.name}\n`);
// ==================== 测试2创建影子状态记录 ====================
console.log('✅ Test 2: 创建影子状态记录');
const testAction = await prisma.iitPendingAction.create({
data: {
projectId: testProject.id,
recordId: 'RECORD_001',
fieldName: 'age',
currentValue: 65,
suggestedValue: null,
status: 'PROPOSED',
agentType: 'DATA_QUALITY',
reasoning: 'AI detected: 年龄65岁超出入排标准18-60岁',
evidence: {
protocolPage: 12,
protocolSection: '入排标准',
confidence: 0.95,
ruleType: 'inclusion'
}
}
});
console.log(` ✓ 影子状态ID: ${testAction.id}`);
console.log(` ✓ 状态: ${testAction.status}\n`);
// ==================== 测试3创建任务记录 ====================
console.log('✅ Test 3: 创建任务运行记录');
const testTaskRun = await prisma.iitTaskRun.create({
data: {
projectId: testProject.id,
taskType: 'bulk-scan',
status: 'pending',
totalItems: 100,
processedItems: 0,
successItems: 0,
failedItems: 0
}
});
console.log(` ✓ 任务ID: ${testTaskRun.id}`);
console.log(` ✓ 任务类型: ${testTaskRun.taskType}\n`);
// ==================== 测试4创建用户映射 ====================
console.log('✅ Test 4: 创建用户映射');
const testUserMapping = await prisma.iitUserMapping.create({
data: {
projectId: testProject.id,
systemUserId: 'user_123',
redcapUsername: 'test_crc',
wecomUserId: 'wecom_123',
role: 'CRC'
}
});
console.log(` ✓ 用户映射ID: ${testUserMapping.id}`);
console.log(` ✓ 角色: ${testUserMapping.role}\n`);
// ==================== 测试5创建审计日志 ====================
console.log('✅ Test 5: 创建审计日志');
const testAuditLog = await prisma.iitAuditLog.create({
data: {
projectId: testProject.id,
userId: 'user_123',
actionType: 'APPROVE_ACTION',
entityType: 'PENDING_ACTION',
entityId: testAction.id,
details: {
before: { status: 'PROPOSED' },
after: { status: 'APPROVED' }
},
traceId: 'trace_' + Date.now()
}
});
console.log(` ✓ 审计日志ID: ${testAuditLog.id}`);
console.log(` ✓ 操作类型: ${testAuditLog.actionType}\n`);
// ==================== 测试6查询和关联 ====================
console.log('✅ Test 6: 查询项目及关联数据');
const projectWithRelations = await prisma.iitProject.findUnique({
where: { id: testProject.id },
include: {
pendingActions: true,
taskRuns: true,
userMappings: true,
auditLogs: true
}
});
console.log(` ✓ 项目名称: ${projectWithRelations?.name}`);
console.log(` ✓ 影子状态记录数: ${projectWithRelations?.pendingActions.length}`);
console.log(` ✓ 任务记录数: ${projectWithRelations?.taskRuns.length}`);
console.log(` ✓ 用户映射数: ${projectWithRelations?.userMappings.length}`);
console.log(` ✓ 审计日志数: ${projectWithRelations?.auditLogs.length}\n`);
// ==================== 清理测试数据 ====================
console.log('🧹 清理测试数据...');
await prisma.iitAuditLog.delete({ where: { id: testAuditLog.id } });
await prisma.iitUserMapping.delete({ where: { id: testUserMapping.id } });
await prisma.iitTaskRun.delete({ where: { id: testTaskRun.id } });
await prisma.iitPendingAction.delete({ where: { id: testAction.id } });
await prisma.iitProject.delete({ where: { id: testProject.id } });
console.log(' ✓ 测试数据已清理\n');
console.log('🎉 所有测试通过IIT Schema工作正常\n');
} catch (error) {
console.error('❌ 测试失败:', error);
throw error;
} finally {
await prisma.$disconnect();
}
}
// 运行测试
testIitDatabase()
.then(() => {
console.log('✅ 数据库验证完成');
process.exit(0);
})
.catch((error) => {
console.error('❌ 数据库验证失败:', error);
process.exit(1);
});

View File

@@ -0,0 +1,137 @@
/**
* 企业微信推送测试脚本
* 验证企微API配置是否正确
*/
import axios from 'axios';
import { config } from '../../config/env.js';
// 从配置文件读取配置
const CORP_ID = config.wechatCorpId;
const CORP_SECRET = config.wechatCorpSecret;
const AGENT_ID = config.wechatAgentId;
async function testWeChatPush() {
console.log('🔍 测试企业微信推送功能...\n');
try {
// ==================== 步骤1验证环境变量 ====================
console.log('✅ 步骤1: 验证环境变量');
if (!CORP_ID || !CORP_SECRET || !AGENT_ID) {
console.error('❌ 缺少企业微信配置!');
console.log(' 请检查 .env 文件中的配置:');
console.log(' - WECHAT_CORP_ID');
console.log(' - WECHAT_CORP_SECRET');
console.log(' - WECHAT_AGENT_ID');
process.exit(1);
}
console.log(` ✓ CORP_ID: ${CORP_ID}`);
console.log(` ✓ AGENT_ID: ${AGENT_ID}`);
console.log(` ✓ SECRET: ${CORP_SECRET.substring(0, 10)}...(已隐藏)\n`);
// ==================== 步骤2获取Access Token ====================
console.log('✅ 步骤2: 获取Access Token');
const tokenUrl = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${CORP_ID}&corpsecret=${CORP_SECRET}`;
const tokenResponse = await axios.get(tokenUrl);
if (tokenResponse.data.errcode !== undefined && tokenResponse.data.errcode !== 0) {
console.error('❌ 获取Access Token失败');
console.error(` 错误码: ${tokenResponse.data.errcode}`);
console.error(` 错误信息: ${tokenResponse.data.errmsg}`);
console.log('\n常见错误码');
console.log(' 40013: invalid corpid - 企业ID错误');
console.log(' 40001: invalid secret - Secret错误');
process.exit(1);
}
const accessToken = tokenResponse.data.access_token;
console.log(` ✓ Access Token: ${accessToken.substring(0, 20)}...`);
console.log(` ✓ 有效期: ${tokenResponse.data.expires_in}秒 (约${Math.floor(tokenResponse.data.expires_in / 60)}分钟)\n`);
// ==================== 步骤3发送测试消息 ====================
console.log('✅ 步骤3: 发送测试文本消息');
// 注意touser 使用 "@all" 发送给所有人(测试阶段)
// 生产环境需要指定具体的UserID
const message = {
touser: '@all', // 发送给所有人
msgtype: 'text',
agentid: parseInt(AGENT_ID!),
text: {
content: '🎉 IIT Manager Agent 测试消息\n\n这是一条来自后端服务的测试推送。\n\n如果您看到这条消息说明企业微信配置成功✅'
},
safe: 0
};
const sendUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}`;
const sendResponse = await axios.post(sendUrl, message);
if (sendResponse.data.errcode !== 0) {
console.error('❌ 发送消息失败:');
console.error(` 错误码: ${sendResponse.data.errcode}`);
console.error(` 错误信息: ${sendResponse.data.errmsg}`);
console.log('\n常见错误码');
console.log(' 40014: invalid access_token - Token无效或过期');
console.log(' 40003: invalid openid - UserID无效');
console.log(' 60020: not allow to access from your ip - IP白名单限制');
process.exit(1);
}
console.log(' ✓ 消息发送成功!');
console.log(` ✓ 消息ID: ${sendResponse.data.msgid}`);
console.log(` ✓ 无效用户: ${sendResponse.data.invaliduser || '无'}\n`);
// ==================== 步骤4发送测试卡片消息 ====================
console.log('✅ 步骤4: 发送测试卡片消息(质控预警样式)');
const cardMessage = {
touser: '@all',
msgtype: 'textcard',
agentid: parseInt(AGENT_ID!),
textcard: {
title: '🚨 数据质控预警(测试)',
description: '<div class="gray">项目骨科IIT研究</div><div class="gray">患者RECORD_001</div><div class="normal">AI检测到1个问题</div><div class="highlight">置信度95%</div><div class="normal">请尽快处理</div>',
url: 'http://localhost:5173',
btntxt: '查看详情'
}
};
const cardResponse = await axios.post(sendUrl, cardMessage);
if (cardResponse.data.errcode !== 0) {
console.error('❌ 发送卡片消息失败:');
console.error(` 错误码: ${cardResponse.data.errcode}`);
console.error(` 错误信息: ${cardResponse.data.errmsg}`);
} else {
console.log(' ✓ 卡片消息发送成功!');
console.log(` ✓ 消息ID: ${cardResponse.data.msgid}\n`);
}
// ==================== 完成 ====================
console.log('🎉 所有测试通过!\n');
console.log('📱 请在企业微信APP中查看收到的消息');
console.log(' 1. 文本消息:确认配置成功');
console.log(' 2. 卡片消息:预览质控预警样式\n');
console.log('✅ 企业微信集成就绪可以进入Day 2开发\n');
} catch (error: any) {
console.error('❌ 测试失败:', error.message);
if (error.response) {
console.error(' 响应数据:', error.response.data);
}
process.exit(1);
}
}
// 运行测试
testWeChatPush()
.then(() => {
console.log('✅ 企业微信测试完成');
process.exit(0);
})
.catch((error) => {
console.error('❌ 企业微信测试失败:', error);
process.exit(1);
});

View File

@@ -0,0 +1,222 @@
/**
* IIT Manager 类型定义
*/
// ==================== REDCap相关 ====================
export interface RedcapRecord {
record_id: string;
[key: string]: any;
}
export interface RedcapExportOptions {
records?: string[];
fields?: string[];
dateRangeBegin?: string;
dateRangeEnd?: string;
rawOrLabel?: 'raw' | 'label';
}
export interface RedcapMetadataField {
field_name: string;
form_name: string;
section_header?: string;
field_type: string;
field_label: string;
select_choices_or_calculations?: string;
field_note?: string;
text_validation_type_or_show_slider_number?: string;
text_validation_min?: string;
text_validation_max?: string;
identifier?: string;
branching_logic?: string;
required_field?: string;
custom_alignment?: string;
question_number?: string;
matrix_group_name?: string;
matrix_ranking?: string;
field_annotation?: string;
}
// ==================== Webhook相关 ====================
export interface WebhookPayload {
project_id: string;
record_id: string;
instrument: string;
event_id?: string;
repeat_instance?: number;
data: Record<string, any>;
timestamp: string;
}
// ==================== 质控相关 ====================
export interface QualityCheckParams {
projectId: string;
recordId: string;
data: Record<string, any>;
}
export interface QualityIssue {
fieldName: string;
logicalField: string;
currentValue: any;
suggestedValue?: any;
reason: string;
evidence: {
protocolPage?: number;
protocolSection?: string;
confidence: number;
ruleType: 'inclusion' | 'exclusion' | 'validation' | 'logic';
};
severity: 'critical' | 'major' | 'minor';
}
export interface ProtocolComplianceResult {
compliant: boolean;
issues: QualityIssue[];
reasoning: string;
}
// ==================== 影子状态相关 ====================
export type PendingActionStatus =
| 'PROPOSED' // AI建议已创建
| 'APPROVED' // 人类已确认
| 'REJECTED' // 人类已拒绝
| 'EXECUTED' // 已回写REDCap
| 'FAILED'; // 回写失败
export type AgentType =
| 'DATA_QUALITY' // 数据质控
| 'TASK_DRIVEN' // 任务驱动
| 'COUNSELING' // 智能咨询
| 'REPORTING'; // 智能汇报
export interface PendingActionDetail {
id: string;
projectId: string;
recordId: string;
fieldName: string;
currentValue: any;
suggestedValue: any;
status: PendingActionStatus;
agentType: AgentType;
reasoning: string;
evidence: QualityIssue['evidence'];
approvedBy?: string;
approvedAt?: Date;
rejectionReason?: string;
createdAt: Date;
}
// ==================== 任务相关 ====================
export type TaskType =
| 'quality-check' // 单条质控
| 'bulk-scan' // 全量扫描
| 'bulk-scan:batch' // 批次扫描
| 'follow-up' // 随访提醒
| 'report-generation'; // 报告生成
export interface TaskRunStatus {
id: string;
projectId: string;
taskType: TaskType;
status: 'pending' | 'processing' | 'completed' | 'failed';
totalItems: number;
processedItems: number;
successItems: number;
failedItems: number;
startedAt?: Date;
completedAt?: Date;
duration?: number;
}
// ==================== 企微相关 ====================
export interface WeChatMessageCard {
toUser: string;
title: string;
description: string;
url: string;
btntxt?: string;
}
// ==================== 同步相关 ====================
export interface SyncMode {
webhook: boolean; // Webhook是否可用
polling: boolean; // 是否启用轮询
pollingInterval: number; // 轮询间隔(分钟)
}
export interface SyncCheckpoint {
projectId: string;
lastSyncAt: Date;
lastRecordId?: string;
recordsProcessed: number;
}
// ==================== Dify RAG相关 ====================
export interface DifyQueryParams {
datasetId: string;
query: string;
retrieval_model?: {
search_method: 'keyword_search' | 'semantic_search' | 'full_text_search' | 'hybrid_search';
reranking_enable?: boolean;
reranking_model?: {
reranking_provider_name: string;
reranking_model_name: string;
};
top_k?: number;
score_threshold_enabled?: boolean;
score_threshold?: number;
};
}
export interface DifyQueryResult {
records: Array<{
segment: {
content: string;
answer: string;
id: string;
position: number;
document_id: string;
score: number;
};
}>;
}
// ==================== Protocol缓存规则 ====================
export interface CachedProtocolRules {
// 入排标准
inclusionCriteria: Array<{
field: string;
rule: string;
type: 'range' | 'enum' | 'boolean' | 'expression';
value: any;
}>;
exclusionCriteria: Array<{
field: string;
rule: string;
type: 'range' | 'enum' | 'boolean' | 'expression';
value: any;
}>;
// 字段验证规则
fields: Record<string, {
required?: boolean;
type?: string;
min?: number;
max?: number;
enum?: string[];
pattern?: string;
dependencies?: string[];
}>;
// 提取时间
extractedAt: string;
}