feat(rvw): Complete Phase 4-5 - Bug fixes and Word export

Summary:
- Fix methodology score display issue in task list (show score instead of 'warn')
- Add methodology_score field to database schema
- Fix report display when only methodology agent is selected
- Implement Word document export using docx library
- Update documentation to v3.0/v3.1

Backend changes:
- Add methodologyScore to Prisma schema and TaskSummary type
- Update reviewWorker to save methodologyScore
- Update getTaskList to return methodologyScore

Frontend changes:
- Install docx and file-saver libraries
- Implement handleExportReport with Word generation
- Fix activeTab auto-selection based on available data
- Add proper imports for docx components

Documentation:
- Update RVW module status to 90% (Phase 1-5 complete)
- Update system status document to v3.0

Tested: All review workflows verified, Word export functional
This commit is contained in:
2026-01-10 22:52:15 +08:00
parent 179afa2c6b
commit 440f75255e
237 changed files with 3942 additions and 657 deletions

View File

@@ -144,3 +144,5 @@ https://iit.xunzhengyixue.com/api/v1/iit/health

View File

@@ -45,3 +45,5 @@

View File

@@ -305,3 +305,5 @@ npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts

View File

@@ -167,3 +167,5 @@ npm run dev

View File

@@ -61,6 +61,8 @@ WHERE table_schema = 'dc_schema'

View File

@@ -99,6 +99,8 @@ ORDER BY ordinal_position;

View File

@@ -112,6 +112,8 @@ runMigration()

View File

@@ -46,6 +46,8 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名

View File

@@ -73,6 +73,8 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创

View File

@@ -314,6 +314,7 @@ model ReviewTask {
// 🆕 结果摘要Phase 2新增用于列表展示
editorialScore Float? @map("editorial_score")
methodologyScore Float? @map("methodology_score")
methodologyStatus String? @map("methodology_status") // pass/warn/fail
// 🆕 预留字段(暂不使用)

View File

@@ -113,6 +113,8 @@ Write-Host ""

View File

@@ -223,6 +223,8 @@ function extractCodeBlocks(obj, blocks = []) {

View File

@@ -242,6 +242,8 @@ checkDCTables();

View File

@@ -194,6 +194,8 @@ createAiHistoryTable()

View File

@@ -181,6 +181,8 @@ createToolCTable()

View File

@@ -178,6 +178,8 @@ createToolCTable()

View File

@@ -328,3 +328,5 @@ runTests().catch(error => {

View File

@@ -293,3 +293,5 @@ verifySchemas()

View File

@@ -310,6 +310,8 @@ export function getBatchItems<T>(

View File

@@ -20,6 +20,7 @@ import { registerTestRoutes } from './test-platform-api.js';
import { registerScreeningWorkers } from './modules/asl/services/screeningWorker.js';
import { registerExtractionWorkers } from './modules/dc/tool-b/workers/extractionWorker.js';
import { registerParseExcelWorker } from './modules/dc/tool-c/workers/parseExcelWorker.js';
import { registerReviewWorker } from './modules/rvw/workers/reviewWorker.js';
import { jobQueue } from './common/jobs/index.js';
@@ -183,6 +184,10 @@ const start = async () => {
registerParseExcelWorker();
logger.info('✅ DC Tool C parse excel worker registered');
// 注册RVW审稿Worker
registerReviewWorker();
logger.info('✅ RVW review worker registered');
// 注册IIT Manager Workers
await initIitManager();
logger.info('✅ IIT Manager workers registered');
@@ -201,6 +206,7 @@ const start = async () => {
console.log(' - asl_screening_batch (文献筛选批次处理)');
console.log(' - dc_extraction_batch (数据提取批次处理)');
console.log(' - dc_toolc_parse_excel (Tool C Excel解析)');
console.log(' - rvw_review_task (稿件审查任务)');
console.log(' - iit_quality_check (IIT质控+企微推送)');
console.log(' - iit_redcap_poll (IIT REDCap轮询)');
console.log('='.repeat(60) + '\n');

View File

@@ -346,6 +346,8 @@ runTests().catch((error) => {

View File

@@ -325,6 +325,8 @@ Content-Type: application/json

View File

@@ -261,6 +261,8 @@ export const conflictDetectionService = new ConflictDetectionService();

View File

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

View File

@@ -265,6 +265,8 @@ export const streamAIController = new StreamAIController();

View File

@@ -177,3 +177,5 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {

View File

@@ -111,3 +111,5 @@ checkTableStructure();

View File

@@ -98,3 +98,5 @@ checkProjectConfig().catch(console.error);

View File

@@ -80,3 +80,5 @@ main();

View File

@@ -537,3 +537,5 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback

View File

@@ -172,3 +172,5 @@ console.log('');

View File

@@ -489,3 +489,5 @@ export const patientWechatService = new PatientWechatService();

View File

@@ -134,3 +134,5 @@ testDifyIntegration().catch(error => {

View File

@@ -163,3 +163,5 @@ testIitDatabase()

View File

@@ -149,3 +149,5 @@ if (hasError) {

View File

@@ -175,3 +175,5 @@ async function testUrlVerification() {

View File

@@ -256,3 +256,5 @@ main().catch((error) => {

View File

@@ -140,3 +140,5 @@ Write-Host ""

View File

@@ -233,3 +233,5 @@ export interface CachedProtocolRules {

View File

@@ -46,3 +46,5 @@ export default async function healthRoutes(fastify: FastifyInstance) {

View File

@@ -124,3 +124,5 @@ Content-Type: application/json
GET {{baseUrl}}/api/v1/review/tasks/{{taskId}}/report
Content-Type: application/json

View File

@@ -109,3 +109,5 @@ Write-Host " - 查看报告: GET $BaseUrl/api/v2/rvw/tasks/{taskId}/report" -Fo
Write-Host " - 批量运行: POST $BaseUrl/api/v2/rvw/tasks/batch/run" -ForegroundColor Gray
Write-Host " - 删除任务: DELETE $BaseUrl/api/v2/rvw/tasks/{taskId}" -ForegroundColor Gray

View File

@@ -136,6 +136,8 @@ export async function createTask(
/**
* 运行审查(选择智能体)
* POST /api/v2/rvw/tasks/:taskId/run
*
* ✅ Platform-Only架构返回 jobId 供前端轮询
*/
export async function runReview(
request: FastifyRequest<{
@@ -151,11 +153,16 @@ export async function runReview(
logger.info('[RVW:Controller] 运行审查', { taskId, agents });
await reviewService.runReview({ taskId, agents, userId });
// ✅ 返回 jobIdPlatform-Only架构
const { jobId } = await reviewService.runReview({ taskId, agents, userId });
return reply.send({
success: true,
message: '审查任务已启动',
data: {
taskId,
jobId, // ✅ 供前端轮询状态
},
});
} catch (error) {
logger.error('[RVW:Controller] 运行审查失败', {

View File

@@ -23,3 +23,5 @@ export * from './types/index.js';
// 导出工具函数
export * from './services/utils.js';

View File

@@ -44,3 +44,5 @@ export default async function rvwRoutes(fastify: FastifyInstance) {
fastify.post('/tasks/batch/run', reviewController.batchRunReview);
}

View File

@@ -68,3 +68,5 @@ export async function reviewEditorialStandards(
}
}

View File

@@ -68,3 +68,5 @@ export async function reviewMethodology(
}
}

View File

@@ -15,6 +15,7 @@ import { prisma } from '../../../config/database.js';
import { extractionClient } from '../../../common/document/ExtractionClient.js';
import { ModelType } from '../../../common/llm/adapters/types.js';
import { logger } from '../../../common/logging/index.js';
import { jobQueue } from '../../../common/jobs/index.js';
import { Prisma } from '@prisma/client';
import {
AgentType,
@@ -124,9 +125,15 @@ async function extractDocumentAsync(taskId: string, file: Buffer, filename: stri
/**
* 运行审查(核心功能)
* 支持选择1个或2个智能体
*
* ✅ Platform-Only架构
* - 使用 pg-boss 队列处理审查任务
* - API 立即返回 jobId
* - 后台 Worker 执行实际审查
*
* @returns jobId 供前端轮询状态
*/
export async function runReview(params: RunReviewParams): Promise<void> {
export async function runReview(params: RunReviewParams): Promise<{ jobId: string }> {
const { taskId, agents, userId } = params;
// 验证智能体选择
@@ -149,91 +156,38 @@ export async function runReview(params: RunReviewParams): Promise<void> {
throw new Error('文档尚未提取完成,请稍后再试');
}
const startTime = Date.now();
// 更新任务状态为reviewing
await prisma.reviewTask.update({
where: { id: taskId },
data: {
status: 'reviewing',
selectedAgents: agents,
startedAt: new Date(),
// 清除之前的结果(如果重新运行)
editorialReview: Prisma.JsonNull,
methodologyReview: Prisma.JsonNull,
overallScore: null,
errorMessage: null,
},
});
try {
// 更新任务状态
await prisma.reviewTask.update({
where: { id: taskId },
data: {
status: 'reviewing',
startedAt: new Date(),
// 清除之前的结果(如果重新运行)
editorialReview: Prisma.JsonNull,
methodologyReview: Prisma.JsonNull,
overallScore: null,
errorMessage: null,
},
});
// ✅ 推送任务到 pg-boss 队列Platform-Only架构
const job = await jobQueue.push('rvw_review_task', {
taskId,
userId,
agents,
extractedText: task.extractedText,
modelType: (task.modelUsed || 'deepseek-v3') as ModelType,
});
logger.info('[RVW] 开始审查', { taskId, agents });
logger.info('[RVW] 审查任务已推送到队列', {
taskId,
jobId: job.id,
agents,
});
// 获取模型类型
const modelType = (task.modelUsed || 'deepseek-v3') as ModelType;
// 运行选中的智能体
let editorialResult: EditorialReview | null = null;
let methodologyResult: MethodologyReview | null = null;
if (agents.includes('editorial')) {
logger.info('[RVW] 运行稿约规范性智能体', { taskId });
editorialResult = await reviewEditorialStandards(task.extractedText, modelType);
}
if (agents.includes('methodology')) {
logger.info('[RVW] 运行方法学智能体', { taskId });
methodologyResult = await reviewMethodology(task.extractedText, modelType);
}
// 计算综合分数
const editorialScore = editorialResult?.overall_score ?? null;
const methodologyScore = methodologyResult?.overall_score ?? null;
const overallScore = calculateOverallScore(editorialScore, methodologyScore, agents);
// 计算耗时
const endTime = Date.now();
const durationSeconds = Math.floor((endTime - startTime) / 1000);
// 更新任务结果(使用新字段)
await prisma.reviewTask.update({
where: { id: taskId },
data: {
status: 'completed',
editorialReview: editorialResult as unknown as Prisma.InputJsonValue ?? Prisma.JsonNull,
methodologyReview: methodologyResult as unknown as Prisma.InputJsonValue ?? Prisma.JsonNull,
overallScore,
// 🆕 使用新字段存储摘要信息
editorialScore: editorialScore,
methodologyStatus: getMethodologyStatus(methodologyResult),
completedAt: new Date(),
durationSeconds,
},
});
logger.info('[RVW] 审查完成', {
taskId,
agents,
editorialScore,
methodologyScore,
overallScore,
durationSeconds,
});
} catch (error) {
logger.error('[RVW] 审查失败', {
taskId,
error: error instanceof Error ? error.message : 'Unknown error'
});
await prisma.reviewTask.update({
where: { id: taskId },
data: {
status: 'failed',
errorMessage: error instanceof Error ? error.message : 'Review failed',
},
});
throw error;
}
// ✅ 立即返回 jobId不阻塞请求
return { jobId: job.id };
}
/**
@@ -324,6 +278,7 @@ export async function getTaskList(params: TaskListParams): Promise<TaskListRespo
// 🆕 使用新字段
selectedAgents: true,
editorialScore: true,
methodologyScore: true,
methodologyStatus: true,
overallScore: true,
modelUsed: true,
@@ -344,6 +299,7 @@ export async function getTaskList(params: TaskListParams): Promise<TaskListRespo
status: task.status as TaskStatus,
selectedAgents: (task.selectedAgents || ['editorial', 'methodology']) as AgentType[],
editorialScore: task.editorialScore ?? undefined,
methodologyScore: task.methodologyScore ?? undefined,
methodologyStatus: task.methodologyStatus as any,
overallScore: task.overallScore ?? undefined,
modelUsed: task.modelUsed ?? undefined,

View File

@@ -114,3 +114,5 @@ export function validateAgentSelection(agents: string[]): void {
}
}

View File

@@ -107,6 +107,7 @@ export interface TaskSummary {
status: TaskStatus;
selectedAgents: AgentType[];
editorialScore?: number;
methodologyScore?: number;
methodologyStatus?: MethodologyStatus;
overallScore?: number;
modelUsed?: string;
@@ -155,3 +156,5 @@ export interface LLMMessage {
content: string;
}

View File

@@ -0,0 +1,190 @@
/**
* RVW稿件审查 WorkerPlatform-Only架构
*
* ✅ Platform-Only架构
* - 使用 pg-boss 队列处理审查任务
* - 任务状态存储在 job.state (pg-boss管理)
* - 审查结果更新到 ReviewTask表业务信息
*
* 任务流程:
* 1. 获取任务信息和提取的文本
* 2. 根据选择的智能体执行审查
* 3. 更新任务状态和结果
*/
import { prisma } from '../../../config/database.js';
import { logger } from '../../../common/logging/index.js';
import { jobQueue } from '../../../common/jobs/index.js';
import type { Job } from '../../../common/jobs/types.js';
import { Prisma } from '@prisma/client';
import { ModelType } from '../../../common/llm/adapters/types.js';
import { reviewEditorialStandards } from '../services/editorialService.js';
import { reviewMethodology } from '../services/methodologyService.js';
import { calculateOverallScore, getMethodologyStatus } from '../services/utils.js';
import type { AgentType, EditorialReview, MethodologyReview } from '../types/index.js';
/**
* 审查任务数据结构
*/
interface ReviewJob {
taskId: string;
userId: string;
agents: AgentType[];
extractedText: string;
modelType: ModelType;
}
/**
* 注册审查 Worker 到队列
*
* 此函数应在应用启动时调用index.ts
*/
export function registerReviewWorker() {
logger.info('[reviewWorker] Registering reviewWorker');
// 注册审查Worker队列名使用下划线不用冒号
jobQueue.process<ReviewJob>('rvw_review_task', async (job: Job<ReviewJob>) => {
const { taskId, userId, agents, extractedText, modelType } = job.data;
const startTime = Date.now();
logger.info('[reviewWorker] Processing review job', {
jobId: job.id,
taskId,
userId,
agents,
textLength: extractedText.length,
});
console.log(`\n📝 处理审查任务`);
console.log(` Job ID: ${job.id}`);
console.log(` Task ID: ${taskId}`);
console.log(` 智能体: ${agents.join(', ')}`);
console.log(` 文本长度: ${extractedText.length} 字符`);
try {
// ========================================
// 1. 运行选中的智能体
// ========================================
let editorialResult: EditorialReview | null = null;
let methodologyResult: MethodologyReview | null = null;
if (agents.includes('editorial')) {
// 更新进度状态
await prisma.reviewTask.update({
where: { id: taskId },
data: { status: 'reviewing_editorial' },
});
logger.info('[reviewWorker] Running editorial review', { taskId });
console.log(' 🔍 运行稿约规范性智能体...');
editorialResult = await reviewEditorialStandards(extractedText, modelType);
logger.info('[reviewWorker] Editorial review completed', {
taskId,
score: editorialResult?.overall_score,
});
console.log(` ✅ 稿约规范性完成,得分: ${editorialResult?.overall_score}`);
}
if (agents.includes('methodology')) {
// 更新进度状态
await prisma.reviewTask.update({
where: { id: taskId },
data: { status: 'reviewing_methodology' },
});
logger.info('[reviewWorker] Running methodology review', { taskId });
console.log(' 🔬 运行方法学智能体...');
methodologyResult = await reviewMethodology(extractedText, modelType);
logger.info('[reviewWorker] Methodology review completed', {
taskId,
score: methodologyResult?.overall_score,
});
console.log(` ✅ 方法学评估完成,得分: ${methodologyResult?.overall_score}`);
}
// ========================================
// 2. 计算综合分数
// ========================================
const editorialScore = editorialResult?.overall_score ?? null;
const methodologyScore = methodologyResult?.overall_score ?? null;
const overallScore = calculateOverallScore(editorialScore, methodologyScore, agents);
// 计算耗时
const endTime = Date.now();
const durationSeconds = Math.floor((endTime - startTime) / 1000);
// ========================================
// 3. 更新任务结果
// ========================================
logger.info('[reviewWorker] Updating task result', { taskId });
await prisma.reviewTask.update({
where: { id: taskId },
data: {
status: 'completed',
editorialReview: editorialResult as unknown as Prisma.InputJsonValue ?? Prisma.JsonNull,
methodologyReview: methodologyResult as unknown as Prisma.InputJsonValue ?? Prisma.JsonNull,
overallScore,
editorialScore: editorialScore,
methodologyScore: methodologyScore,
methodologyStatus: getMethodologyStatus(methodologyResult),
completedAt: new Date(),
durationSeconds,
},
});
logger.info('[reviewWorker] ✅ Review completed', {
jobId: job.id,
taskId,
agents,
editorialScore,
methodologyScore,
overallScore,
durationSeconds,
});
console.log('\n✅ 审查完成:');
console.log(` Task ID: ${taskId}`);
console.log(` 综合得分: ${overallScore}`);
console.log(` 耗时: ${durationSeconds}`);
return {
taskId,
overallScore,
editorialScore,
methodologyScore,
durationSeconds,
success: true,
};
} catch (error: any) {
logger.error('[reviewWorker] ❌ Review failed', {
jobId: job.id,
taskId,
error: error.message,
stack: error.stack,
});
console.error(`\n❌ 审查失败: ${error.message}`);
// 更新任务状态为失败
await prisma.reviewTask.update({
where: { id: taskId },
data: {
status: 'failed',
errorMessage: error.message || 'Review failed',
},
});
// 抛出错误,让 pg-boss 处理重试
throw error;
}
});
logger.info('[reviewWorker] ✅ Worker registered: rvw_review_task');
}

View File

@@ -411,6 +411,8 @@ SET session_replication_role = 'origin';

View File

@@ -113,6 +113,8 @@ WHERE key = 'verify_test';

View File

@@ -256,6 +256,8 @@ verifyDatabase()

View File

@@ -46,6 +46,8 @@ export {}

View File

@@ -69,6 +69,8 @@ Write-Host "✅ 完成!" -ForegroundColor Green

View File

@@ -160,3 +160,5 @@ DELETE {{baseUrl}}/api/v2/pkb/knowledge/knowledge-bases/{{testKbId}}

View File

@@ -356,6 +356,8 @@ runAdvancedTests().catch(error => {

View File

@@ -422,6 +422,8 @@ runAllTests()

View File

@@ -380,6 +380,8 @@ runAllTests()