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:
@@ -346,6 +346,8 @@ runTests().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -287,6 +287,8 @@ runTest()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -325,6 +325,8 @@ Content-Type: application/json
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -261,6 +261,8 @@ export const conflictDetectionService = new ConflictDetectionService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -211,6 +211,8 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -265,6 +265,8 @@ export const streamAIController = new StreamAIController();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -177,3 +177,5 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -111,3 +111,5 @@ checkTableStructure();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -98,3 +98,5 @@ checkProjectConfig().catch(console.error);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -80,3 +80,5 @@ main();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -537,3 +537,5 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -172,3 +172,5 @@ console.log('');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -489,3 +489,5 @@ export const patientWechatService = new PatientWechatService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -134,3 +134,5 @@ testDifyIntegration().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -163,3 +163,5 @@ testIitDatabase()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -149,3 +149,5 @@ if (hasError) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -175,3 +175,5 @@ async function testUrlVerification() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -256,3 +256,5 @@ main().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -140,3 +140,5 @@ Write-Host ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -233,3 +233,5 @@ export interface CachedProtocolRules {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -46,3 +46,5 @@ export default async function healthRoutes(fastify: FastifyInstance) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -124,3 +124,5 @@ Content-Type: application/json
|
||||
GET {{baseUrl}}/api/v1/review/tasks/{{taskId}}/report
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 });
|
||||
// ✅ 返回 jobId(Platform-Only架构)
|
||||
const { jobId } = await reviewService.runReview({ taskId, agents, userId });
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
message: '审查任务已启动',
|
||||
data: {
|
||||
taskId,
|
||||
jobId, // ✅ 供前端轮询状态
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[RVW:Controller] 运行审查失败', {
|
||||
|
||||
@@ -23,3 +23,5 @@ export * from './types/index.js';
|
||||
// 导出工具函数
|
||||
export * from './services/utils.js';
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -44,3 +44,5 @@ export default async function rvwRoutes(fastify: FastifyInstance) {
|
||||
fastify.post('/tasks/batch/run', reviewController.batchRunReview);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -68,3 +68,5 @@ export async function reviewEditorialStandards(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -68,3 +68,5 @@ export async function reviewMethodology(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -114,3 +114,5 @@ export function validateAgentSelection(agents: string[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
190
backend/src/modules/rvw/workers/reviewWorker.ts
Normal file
190
backend/src/modules/rvw/workers/reviewWorker.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* RVW稿件审查 Worker(Platform-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');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user