feat(rvw): Complete RVW module development Phase 1-3

Summary:
- Migrate backend to modules/rvw with v2 API routes (/api/v2/rvw)
- Add new database fields: selectedAgents, editorialScore, methodologyStatus, picoExtract, isArchived
- Create frontend module in frontend-v2/src/modules/rvw
- Implement Dashboard with task list, filtering, batch operations
- Implement ReportDetail with dual tabs (editorial/methodology)
- Implement AgentModal for intelligent agent selection
- Register RVW module in moduleRegistry.ts
- Add navigation entry in TopNavigation
- Update documentation for RVW module status (v3.0)
- Update system status document (v2.9)

Features:
- User can select agents: editorial, methodology, or both
- Support batch task execution
- Task status filtering
- Replace console.log with logger service
- Maintain v1 API backward compatibility

Tested: Frontend and backend verified locally
Status: 85% complete (Phase 1-3 done)
This commit is contained in:
2026-01-07 22:39:08 +08:00
parent 06028c6952
commit 179afa2c6b
226 changed files with 5860 additions and 21 deletions

View File

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

View File

@@ -10,6 +10,7 @@ import knowledgeBaseRoutes from './legacy/routes/knowledgeBases.js';
import { chatRoutes } from './legacy/routes/chatRoutes.js';
import { batchRoutes } from './legacy/routes/batchRoutes.js';
import reviewRoutes from './legacy/routes/reviewRoutes.js';
import { rvwRoutes } from './modules/rvw/index.js';
import { aslRoutes } from './modules/asl/routes/index.js';
import { registerDCRoutes, initDCModule } from './modules/dc/index.js';
import pkbRoutes from './modules/pkb/routes/index.js';
@@ -109,9 +110,16 @@ await fastify.register(chatRoutes, { prefix: '/api/v1' });
// Phase 3: 注册批处理路由
await fastify.register(batchRoutes, { prefix: '/api/v1' });
// 注册稿件审查路由
// 注册稿件审查路由(旧版,保留兼容)
await fastify.register(reviewRoutes, { prefix: '/api/v1' });
// ============================================
// 【业务模块】RVW - 稿件审查系统(新架构 v2
// ============================================
await fastify.register(rvwRoutes, { prefix: '/api/v2/rvw' });
logger.info('✅ RVW稿件审查路由已注册v2新架构: /api/v2/rvw');
logger.info(' ⚠️ 旧版路由仍可用: /api/v1/review');
// ============================================
// 【业务模块】PKB - 个人知识库(新架构 v2
// ============================================

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -110,3 +110,4 @@ checkTableStructure();

View File

@@ -97,3 +97,4 @@ checkProjectConfig().catch(console.error);

View File

@@ -79,3 +79,4 @@ main();

View File

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

View File

@@ -171,3 +171,4 @@ console.log('');

View File

@@ -488,3 +488,4 @@ export const patientWechatService = new PatientWechatService();

View File

@@ -133,3 +133,4 @@ testDifyIntegration().catch(error => {

View File

@@ -162,3 +162,4 @@ testIitDatabase()

View File

@@ -148,3 +148,4 @@ if (hasError) {

View File

@@ -174,3 +174,4 @@ async function testUrlVerification() {

View File

@@ -255,3 +255,4 @@ main().catch((error) => {

View File

@@ -139,3 +139,4 @@ Write-Host ""

View File

@@ -232,3 +232,4 @@ export interface CachedProtocolRules {

View File

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

View File

@@ -0,0 +1,126 @@
### RVW稿件审查模块 - API测试
### Phase 1 验证用例
@baseUrl = http://localhost:3001
@taskId = {{$uuid}}
### ========================================
### 1. 创建任务(上传稿件)
### POST /api/v2/rvw/tasks
### ========================================
# 注意需要使用工具如Postman上传文件
# curl -X POST http://localhost:3001/api/v2/rvw/tasks \
# -F "file=@test.docx" \
# -F "modelType=deepseek-v3"
### ========================================
### 2. 获取任务列表
### GET /api/v2/rvw/tasks
### ========================================
### 获取全部任务
GET {{baseUrl}}/api/v2/rvw/tasks
Content-Type: application/json
### 获取待处理任务
GET {{baseUrl}}/api/v2/rvw/tasks?status=pending
Content-Type: application/json
### 获取已完成任务
GET {{baseUrl}}/api/v2/rvw/tasks?status=completed
Content-Type: application/json
### 分页获取
GET {{baseUrl}}/api/v2/rvw/tasks?page=1&limit=10
Content-Type: application/json
### ========================================
### 3. 运行审查(选择智能体)
### POST /api/v2/rvw/tasks/:taskId/run
### ========================================
### 只选择规范性智能体
POST {{baseUrl}}/api/v2/rvw/tasks/{{taskId}}/run
Content-Type: application/json
{
"agents": ["editorial"]
}
### 只选择方法学智能体
POST {{baseUrl}}/api/v2/rvw/tasks/{{taskId}}/run
Content-Type: application/json
{
"agents": ["methodology"]
}
### 同时选择两个智能体(默认)
POST {{baseUrl}}/api/v2/rvw/tasks/{{taskId}}/run
Content-Type: application/json
{
"agents": ["editorial", "methodology"]
}
### ========================================
### 4. 批量运行审查
### POST /api/v2/rvw/tasks/batch/run
### ========================================
POST {{baseUrl}}/api/v2/rvw/tasks/batch/run
Content-Type: application/json
{
"taskIds": ["task-id-1", "task-id-2", "task-id-3"],
"agents": ["editorial", "methodology"]
}
### ========================================
### 5. 获取任务详情
### GET /api/v2/rvw/tasks/:taskId
### ========================================
GET {{baseUrl}}/api/v2/rvw/tasks/{{taskId}}
Content-Type: application/json
### ========================================
### 6. 获取审查报告
### GET /api/v2/rvw/tasks/:taskId/report
### ========================================
GET {{baseUrl}}/api/v2/rvw/tasks/{{taskId}}/report
Content-Type: application/json
### ========================================
### 7. 删除任务
### DELETE /api/v2/rvw/tasks/:taskId
### ========================================
DELETE {{baseUrl}}/api/v2/rvw/tasks/{{taskId}}
Content-Type: application/json
### ========================================
### 旧版API兼容性测试
### ========================================
### 旧版:获取任务列表
GET {{baseUrl}}/api/v1/review/tasks
Content-Type: application/json
### 旧版:获取任务状态
GET {{baseUrl}}/api/v1/review/tasks/{{taskId}}
Content-Type: application/json
### 旧版:获取报告
GET {{baseUrl}}/api/v1/review/tasks/{{taskId}}/report
Content-Type: application/json

View File

@@ -0,0 +1,111 @@
# RVW稿件审查模块 - API测试脚本
# Phase 1 验证用例
$BaseUrl = "http://localhost:3001"
$TestFile = "D:\MyCursor\AIclinicalresearch\docs\03-业务模块\ASL-AI智能文献\05-测试文档\03-测试数据\pdf-extraction\rayyan-256859669.pdf"
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "RVW模块 Phase 1 API测试" -ForegroundColor Cyan
Write-Host "========================================`n" -ForegroundColor Cyan
# 检查服务器是否运行
Write-Host "1. 检查服务器状态..." -ForegroundColor Yellow
try {
$health = Invoke-RestMethod -Uri "$BaseUrl/health" -Method Get
Write-Host " ✅ 服务器运行中" -ForegroundColor Green
} catch {
Write-Host " ❌ 服务器未运行,请先启动后端: cd backend && npm run dev" -ForegroundColor Red
exit 1
}
# 测试1: 获取任务列表(不需要上传)
Write-Host "`n2. 测试获取任务列表 (GET /api/v2/rvw/tasks)..." -ForegroundColor Yellow
try {
$response = Invoke-RestMethod -Uri "$BaseUrl/api/v2/rvw/tasks" -Method Get
Write-Host " ✅ 成功! 当前任务数: $($response.pagination.total)" -ForegroundColor Green
if ($response.data.Count -gt 0) {
Write-Host " 最近任务: $($response.data[0].fileName) - $($response.data[0].status)" -ForegroundColor Gray
}
} catch {
Write-Host " ❌ 失败: $($_.Exception.Message)" -ForegroundColor Red
}
# 测试2: 按状态筛选
Write-Host "`n3. 测试筛选待处理任务 (GET /api/v2/rvw/tasks?status=pending)..." -ForegroundColor Yellow
try {
$response = Invoke-RestMethod -Uri "$BaseUrl/api/v2/rvw/tasks?status=pending" -Method Get
Write-Host " ✅ 成功! 待处理任务数: $($response.pagination.total)" -ForegroundColor Green
} catch {
Write-Host " ❌ 失败: $($_.Exception.Message)" -ForegroundColor Red
}
# 测试3: 上传文件创建任务
Write-Host "`n4. 测试上传文件 (POST /api/v2/rvw/tasks)..." -ForegroundColor Yellow
if (Test-Path $TestFile) {
try {
# 使用curl上传PowerShell的Invoke-RestMethod对multipart支持不好
$curlResult = & curl.exe -s -X POST "$BaseUrl/api/v2/rvw/tasks" `
-F "file=@$TestFile" `
-F "modelType=deepseek-v3"
$uploadResponse = $curlResult | ConvertFrom-Json
if ($uploadResponse.success) {
$taskId = $uploadResponse.data.taskId
Write-Host " ✅ 上传成功! TaskId: $taskId" -ForegroundColor Green
Write-Host " 文件名: $($uploadResponse.data.fileName)" -ForegroundColor Gray
# 等待文档提取
Write-Host "`n5. 等待文档提取3秒..." -ForegroundColor Yellow
Start-Sleep -Seconds 3
# 测试4: 获取任务详情
Write-Host "`n6. 测试获取任务详情 (GET /api/v2/rvw/tasks/$taskId)..." -ForegroundColor Yellow
try {
$detail = Invoke-RestMethod -Uri "$BaseUrl/api/v2/rvw/tasks/$taskId" -Method Get
Write-Host " ✅ 成功! 状态: $($detail.data.status)" -ForegroundColor Green
} catch {
Write-Host " ❌ 失败: $($_.Exception.Message)" -ForegroundColor Red
}
# 测试5: 运行审查(只选规范性)
Write-Host "`n7. 测试运行审查-只选规范性 (POST /api/v2/rvw/tasks/$taskId/run)..." -ForegroundColor Yellow
try {
$body = @{ agents = @("editorial") } | ConvertTo-Json
$runResult = Invoke-RestMethod -Uri "$BaseUrl/api/v2/rvw/tasks/$taskId/run" `
-Method Post -Body $body -ContentType "application/json"
Write-Host " ✅ 审查任务已启动!" -ForegroundColor Green
Write-Host " ⏳ 注意AI评估需要1-2分钟可稍后查看报告" -ForegroundColor Yellow
} catch {
$errorBody = $_.ErrorDetails.Message | ConvertFrom-Json
Write-Host " ⚠️ $($errorBody.message)" -ForegroundColor Yellow
}
} else {
Write-Host " ❌ 上传失败: $($uploadResponse.message)" -ForegroundColor Red
}
} catch {
Write-Host " ❌ 失败: $($_.Exception.Message)" -ForegroundColor Red
}
} else {
Write-Host " ⚠️ 测试文件不存在: $TestFile" -ForegroundColor Yellow
Write-Host " 跳过上传测试,请手动测试" -ForegroundColor Gray
}
# 测试6: 旧版API兼容性
Write-Host "`n8. 测试旧版API兼容性 (GET /api/v1/review/tasks)..." -ForegroundColor Yellow
try {
$response = Invoke-RestMethod -Uri "$BaseUrl/api/v1/review/tasks" -Method Get
Write-Host " ✅ 旧版API正常! 任务数: $($response.pagination.total)" -ForegroundColor Green
} catch {
Write-Host " ❌ 旧版API异常: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "测试完成!" -ForegroundColor Cyan
Write-Host "========================================`n" -ForegroundColor Cyan
Write-Host "后续操作:" -ForegroundColor Yellow
Write-Host " - 查看报告: GET $BaseUrl/api/v2/rvw/tasks/{taskId}/report" -ForegroundColor Gray
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

@@ -0,0 +1,374 @@
/**
* RVW稿件审查模块 - 控制器
* @module rvw/controllers/reviewController
*
* 基于旧代码迁移backend/src/legacy/controllers/reviewController.ts
* 改造内容:
* 1. console.log → logger
* 2. MOCK_USER_ID → 从请求中获取暂时保留Mock待JWT集成
* 3. 新增智能体选择、批量运行接口
*/
import type { FastifyRequest, FastifyReply } from 'fastify';
import { logger } from '../../../common/logging/index.js';
import { ModelType } from '../../../common/llm/adapters/types.js';
import * as reviewService from '../services/reviewService.js';
import { AgentType } from '../types/index.js';
// TODO: 集成JWT认证后移除
const MOCK_USER_ID = 'user-mock-001';
/**
* 获取用户ID暂时使用Mock待JWT集成
*/
function getUserId(request: FastifyRequest): string {
// TODO: 从JWT token中获取
// return request.user?.id;
return MOCK_USER_ID;
}
// ==================== 任务创建 ====================
/**
* 上传稿件创建任务
* POST /api/v2/rvw/tasks
*/
export async function createTask(
request: FastifyRequest<{
Body: {
modelType?: string;
};
}>,
reply: FastifyReply
) {
try {
const userId = getUserId(request);
logger.info('[RVW:Controller] 上传稿件', { userId });
// 获取上传的文件
const data = await request.file();
if (!data) {
logger.warn('[RVW:Controller] 没有接收到文件');
return reply.status(400).send({
success: false,
message: '请上传文件',
});
}
const file = await data.toBuffer();
const filename = data.filename;
const fileType = data.mimetype;
const fileSizeBytes = file.length;
logger.info('[RVW:Controller] 接收文件', {
filename,
fileType,
sizeMB: (fileSizeBytes / 1024 / 1024).toFixed(2)
});
// 文件大小限制50MB根据MVP需求
const maxSize = 50 * 1024 * 1024;
if (fileSizeBytes > maxSize) {
logger.warn('[RVW:Controller] 文件太大', { sizeMB: (fileSizeBytes / 1024 / 1024).toFixed(2) });
return reply.status(400).send({
success: false,
message: '文件大小不能超过50MB',
});
}
// 文件类型限制
const allowedTypes = [
'application/msword', // .doc
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
'application/pdf', // .pdf
];
if (!allowedTypes.includes(fileType)) {
logger.warn('[RVW:Controller] 不支持的文件类型', { fileType });
return reply.status(400).send({
success: false,
message: '仅支持 Word (.doc, .docx) 和 PDF 文件',
});
}
// 获取模型类型
const modelType = ((data.fields.modelType as any)?.value || 'deepseek-v3') as ModelType;
// 验证模型类型
const validModels: ModelType[] = ['deepseek-v3', 'qwen3-72b', 'qwen-long'];
if (!validModels.includes(modelType)) {
return reply.status(400).send({
success: false,
message: `无效的模型类型,可选: ${validModels.join(', ')}`,
});
}
// 创建任务
const task = await reviewService.createTask(file, filename, userId, modelType);
logger.info('[RVW:Controller] 任务已创建', { taskId: task.id });
return reply.send({
success: true,
message: '稿件上传成功,任务已创建',
data: {
taskId: task.id,
fileName: task.fileName,
fileSize: task.fileSize,
status: task.status,
createdAt: task.createdAt,
},
});
} catch (error) {
logger.error('[RVW:Controller] 上传失败', {
error: error instanceof Error ? error.message : 'Unknown error'
});
return reply.status(500).send({
success: false,
message: error instanceof Error ? error.message : '上传失败',
});
}
}
// ==================== 运行审查 ====================
/**
* 运行审查(选择智能体)
* POST /api/v2/rvw/tasks/:taskId/run
*/
export async function runReview(
request: FastifyRequest<{
Params: { taskId: string };
Body: { agents: AgentType[] };
}>,
reply: FastifyReply
) {
try {
const userId = getUserId(request);
const { taskId } = request.params;
const { agents } = request.body;
logger.info('[RVW:Controller] 运行审查', { taskId, agents });
await reviewService.runReview({ taskId, agents, userId });
return reply.send({
success: true,
message: '审查任务已启动',
});
} catch (error) {
logger.error('[RVW:Controller] 运行审查失败', {
error: error instanceof Error ? error.message : 'Unknown error'
});
return reply.status(400).send({
success: false,
message: error instanceof Error ? error.message : '运行审查失败',
});
}
}
/**
* 批量运行审查
* POST /api/v2/rvw/tasks/batch/run
*/
export async function batchRunReview(
request: FastifyRequest<{
Body: {
taskIds: string[];
agents: AgentType[];
};
}>,
reply: FastifyReply
) {
try {
const userId = getUserId(request);
const { taskIds, agents } = request.body;
logger.info('[RVW:Controller] 批量运行审查', { taskCount: taskIds.length, agents });
const result = await reviewService.batchRunReview({ taskIds, agents, userId });
return reply.send({
success: true,
message: `批量审查完成: ${result.success.length} 成功, ${result.failed.length} 失败`,
data: result,
});
} catch (error) {
logger.error('[RVW:Controller] 批量运行失败', {
error: error instanceof Error ? error.message : 'Unknown error'
});
return reply.status(400).send({
success: false,
message: error instanceof Error ? error.message : '批量运行失败',
});
}
}
// ==================== 任务查询 ====================
/**
* 获取任务列表
* GET /api/v2/rvw/tasks
*/
export async function getTaskList(
request: FastifyRequest<{
Querystring: {
status?: 'all' | 'pending' | 'completed';
page?: string;
limit?: string;
};
}>,
reply: FastifyReply
) {
try {
const userId = getUserId(request);
const status = request.query.status || 'all';
const page = parseInt(request.query.page || '1', 10);
const limit = parseInt(request.query.limit || '20', 10);
logger.info('[RVW:Controller] 获取任务列表', { status, page, limit });
const result = await reviewService.getTaskList({ userId, status, page, limit });
return reply.send({
success: true,
data: result.tasks,
pagination: result.pagination,
});
} catch (error) {
logger.error('[RVW:Controller] 获取列表失败', {
error: error instanceof Error ? error.message : 'Unknown error'
});
return reply.status(500).send({
success: false,
message: error instanceof Error ? error.message : '获取列表失败',
});
}
}
/**
* 获取任务详情
* GET /api/v2/rvw/tasks/:taskId
*/
export async function getTaskDetail(
request: FastifyRequest<{
Params: { taskId: string };
}>,
reply: FastifyReply
) {
try {
const userId = getUserId(request);
const { taskId } = request.params;
logger.info('[RVW:Controller] 获取任务详情', { taskId });
const task = await reviewService.getTaskDetail(userId, taskId);
// 🆕 直接使用新字段
return reply.send({
success: true,
data: {
id: task.id,
fileName: task.fileName,
fileSize: task.fileSize,
status: task.status,
selectedAgents: task.selectedAgents || ['editorial', 'methodology'],
wordCount: task.wordCount,
editorialScore: task.editorialScore,
methodologyStatus: task.methodologyStatus,
overallScore: task.overallScore,
modelUsed: task.modelUsed,
createdAt: task.createdAt,
startedAt: task.startedAt,
completedAt: task.completedAt,
durationSeconds: task.durationSeconds,
errorMessage: task.errorMessage,
},
});
} catch (error) {
logger.error('[RVW:Controller] 获取详情失败', {
error: error instanceof Error ? error.message : 'Unknown error'
});
return reply.status(404).send({
success: false,
message: error instanceof Error ? error.message : '任务不存在',
});
}
}
/**
* 获取审查报告
* GET /api/v2/rvw/tasks/:taskId/report
*/
export async function getTaskReport(
request: FastifyRequest<{
Params: { taskId: string };
}>,
reply: FastifyReply
) {
try {
const userId = getUserId(request);
const { taskId } = request.params;
logger.info('[RVW:Controller] 获取审查报告', { taskId });
const report = await reviewService.getTaskReport(userId, taskId);
return reply.send({
success: true,
data: report,
});
} catch (error) {
logger.error('[RVW:Controller] 获取报告失败', {
error: error instanceof Error ? error.message : 'Unknown error'
});
// 如果报告尚未完成返回202
if (error instanceof Error && error.message.includes('尚未完成')) {
return reply.status(202).send({
success: false,
message: error.message,
});
}
return reply.status(404).send({
success: false,
message: error instanceof Error ? error.message : '报告不存在',
});
}
}
/**
* 删除任务
* DELETE /api/v2/rvw/tasks/:taskId
*/
export async function deleteTask(
request: FastifyRequest<{
Params: { taskId: string };
}>,
reply: FastifyReply
) {
try {
const userId = getUserId(request);
const { taskId } = request.params;
logger.info('[RVW:Controller] 删除任务', { taskId });
await reviewService.deleteTask(userId, taskId);
return reply.send({
success: true,
message: '任务已删除',
});
} catch (error) {
logger.error('[RVW:Controller] 删除失败', {
error: error instanceof Error ? error.message : 'Unknown error'
});
return reply.status(404).send({
success: false,
message: error instanceof Error ? error.message : '删除失败',
});
}
}

View File

@@ -0,0 +1,25 @@
/**
* RVW稿件审查模块 - 模块入口
* @module rvw
*
* 功能:
* - 稿约规范性评估11项标准
* - 方法学评估3部分20个检查点
* - 智能体选择可选1个或2个
* - 批量审查支持
*/
// 导出路由
export { default as rvwRoutes } from './routes/index.js';
// 导出服务(供其他模块使用)
export * as reviewService from './services/reviewService.js';
export * as editorialService from './services/editorialService.js';
export * as methodologyService from './services/methodologyService.js';
// 导出类型
export * from './types/index.js';
// 导出工具函数
export * from './services/utils.js';

View File

@@ -0,0 +1,46 @@
/**
* RVW稿件审查模块 - 路由定义
* @module rvw/routes
*
* API前缀: /api/v2/rvw
*/
import type { FastifyInstance } from 'fastify';
import * as reviewController from '../controllers/reviewController.js';
export default async function rvwRoutes(fastify: FastifyInstance) {
// ==================== 任务管理 ====================
// 创建任务(上传稿件)
// POST /api/v2/rvw/tasks
fastify.post('/tasks', reviewController.createTask);
// 获取任务列表
// GET /api/v2/rvw/tasks?status=all|pending|completed&page=1&limit=20
fastify.get('/tasks', reviewController.getTaskList);
// 获取任务详情
// GET /api/v2/rvw/tasks/:taskId
fastify.get('/tasks/:taskId', reviewController.getTaskDetail);
// 获取审查报告
// GET /api/v2/rvw/tasks/:taskId/report
fastify.get('/tasks/:taskId/report', reviewController.getTaskReport);
// 删除任务
// DELETE /api/v2/rvw/tasks/:taskId
fastify.delete('/tasks/:taskId', reviewController.deleteTask);
// ==================== 运行审查 ====================
// 运行审查(选择智能体)
// POST /api/v2/rvw/tasks/:taskId/run
// Body: { agents: ['editorial', 'methodology'] }
fastify.post('/tasks/:taskId/run', reviewController.runReview);
// 批量运行审查
// POST /api/v2/rvw/tasks/batch/run
// Body: { taskIds: [...], agents: ['editorial', 'methodology'] }
fastify.post('/tasks/batch/run', reviewController.batchRunReview);
}

View File

@@ -0,0 +1,70 @@
/**
* RVW稿件审查模块 - 稿约规范性评估服务
* @module rvw/services/editorialService
*/
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js';
import { ModelType } from '../../../common/llm/adapters/types.js';
import { logger } from '../../../common/logging/index.js';
import { EditorialReview } from '../types/index.js';
import { parseJSONFromLLMResponse } from './utils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Prompt文件路径
const PROMPT_PATH = path.join(__dirname, '../../../../prompts/review_editorial_system.txt');
/**
* 稿约规范性评估
* @param text 稿件文本
* @param modelType 模型类型
* @returns 评估结果
*/
export async function reviewEditorialStandards(
text: string,
modelType: ModelType = 'deepseek-v3'
): Promise<EditorialReview> {
try {
// 1. 读取系统Prompt
const systemPrompt = await fs.readFile(PROMPT_PATH, 'utf-8');
// 2. 构建消息
const messages = [
{ role: 'system' as const, content: systemPrompt },
{ role: 'user' as const, content: `请对以下稿件进行稿约规范性评估:\n\n${text}` },
];
// 3. 调用LLM
logger.info('[RVW:Editorial] 开始稿约规范性评估', { modelType });
const llmAdapter = LLMFactory.getAdapter(modelType);
const response = await llmAdapter.chat(messages, {
temperature: 0.3, // 较低温度以获得更稳定的评估
maxTokens: 8000, // 确保完整输出
});
logger.info('[RVW:Editorial] 评估完成', {
modelType,
responseLength: response.content.length
});
// 4. 解析JSON响应
const result = parseJSONFromLLMResponse<EditorialReview>(response.content);
// 5. 验证响应格式
if (!result || typeof result.overall_score !== 'number' || !Array.isArray(result.items)) {
throw new Error('LLM返回的数据格式不正确');
}
return result;
} catch (error) {
logger.error('[RVW:Editorial] 稿约规范性评估失败', {
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
});
throw new Error(`稿约规范性评估失败: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}

View File

@@ -0,0 +1,70 @@
/**
* RVW稿件审查模块 - 方法学评估服务
* @module rvw/services/methodologyService
*/
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js';
import { ModelType } from '../../../common/llm/adapters/types.js';
import { logger } from '../../../common/logging/index.js';
import { MethodologyReview } from '../types/index.js';
import { parseJSONFromLLMResponse } from './utils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Prompt文件路径
const PROMPT_PATH = path.join(__dirname, '../../../../prompts/review_methodology_system.txt');
/**
* 方法学评估
* @param text 稿件文本
* @param modelType 模型类型
* @returns 评估结果
*/
export async function reviewMethodology(
text: string,
modelType: ModelType = 'deepseek-v3'
): Promise<MethodologyReview> {
try {
// 1. 读取系统Prompt
const systemPrompt = await fs.readFile(PROMPT_PATH, 'utf-8');
// 2. 构建消息
const messages = [
{ role: 'system' as const, content: systemPrompt },
{ role: 'user' as const, content: `请对以下稿件进行方法学评估:\n\n${text}` },
];
// 3. 调用LLM
logger.info('[RVW:Methodology] 开始方法学评估', { modelType });
const llmAdapter = LLMFactory.getAdapter(modelType);
const response = await llmAdapter.chat(messages, {
temperature: 0.3,
maxTokens: 8000,
});
logger.info('[RVW:Methodology] 评估完成', {
modelType,
responseLength: response.content.length
});
// 4. 解析JSON响应
const result = parseJSONFromLLMResponse<MethodologyReview>(response.content);
// 5. 验证响应格式
if (!result || typeof result.overall_score !== 'number' || !Array.isArray(result.parts)) {
throw new Error('LLM返回的数据格式不正确');
}
return result;
} catch (error) {
logger.error('[RVW:Methodology] 方法学评估失败', {
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
});
throw new Error(`方法学评估失败: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}

View File

@@ -0,0 +1,430 @@
/**
* RVW稿件审查模块 - 主服务
* @module rvw/services/reviewService
*
* 基于旧代码迁移backend/src/legacy/services/reviewService.ts
* 改造内容:
* 1. console.log → logger
* 2. 新增智能体选择逻辑
* 3. 新增批量运行接口
*
* 注意当前适配现有schemaPhase 2数据库迁移后可启用新字段
*/
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 { Prisma } from '@prisma/client';
import {
AgentType,
TaskStatus,
TaskListParams,
TaskListResponse,
TaskSummary,
ReviewReport,
RunReviewParams,
BatchRunParams,
EditorialReview,
MethodologyReview,
} from '../types/index.js';
import { reviewEditorialStandards } from './editorialService.js';
import { reviewMethodology } from './methodologyService.js';
import {
calculateOverallScore,
getMethodologyStatus,
validateAgentSelection,
} from './utils.js';
// ==================== 任务创建 ====================
/**
* 创建审查任务(上传稿件)
* @param file 文件Buffer
* @param filename 文件名
* @param userId 用户ID
* @param modelType 模型类型
* @returns 创建的任务
*/
export async function createTask(
file: Buffer,
filename: string,
userId: string,
modelType: ModelType = 'deepseek-v3'
) {
logger.info('[RVW] 创建审查任务', { filename, userId, modelType });
// 创建任务记录状态为pending等待用户选择智能体后运行
const task = await prisma.reviewTask.create({
data: {
userId,
fileName: filename,
fileSize: file.length,
extractedText: '', // 初始为空,运行时提取
status: 'pending',
modelUsed: modelType,
selectedAgents: ['editorial', 'methodology'], // 默认选择两个智能体
},
});
logger.info('[RVW] 任务已创建', { taskId: task.id, status: task.status });
// 异步提取文档文本(预处理,不运行评估)
extractDocumentAsync(task.id, file, filename).catch(error => {
logger.error('[RVW] 文档提取失败', { taskId: task.id, error: error.message });
});
return task;
}
/**
* 异步提取文档文本(预处理)
*/
async function extractDocumentAsync(taskId: string, file: Buffer, filename: string) {
try {
logger.info('[RVW] 开始提取文档', { taskId, filename });
const extractionResult = await extractionClient.extractDocument(file, filename);
if (!extractionResult.success || !extractionResult.text) {
throw new Error('文档提取失败或内容为空');
}
const extractedText = extractionResult.text;
const wordCount = extractionResult.metadata?.char_count || extractedText.length;
// 更新提取的文本保持pending状态
await prisma.reviewTask.update({
where: { id: taskId },
data: {
extractedText,
wordCount,
},
});
logger.info('[RVW] 文档提取成功', { taskId, wordCount });
} 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 : 'Document extraction failed',
},
});
}
}
// ==================== 运行审查 ====================
/**
* 运行审查(核心功能)
* 支持选择1个或2个智能体
*/
export async function runReview(params: RunReviewParams): Promise<void> {
const { taskId, agents, userId } = params;
// 验证智能体选择
validateAgentSelection(agents);
// 获取任务
const task = await prisma.reviewTask.findFirst({
where: { id: taskId, userId },
});
if (!task) {
throw new Error('任务不存在或无权限访问');
}
if (task.status === 'reviewing' || task.status === 'reviewing_editorial' || task.status === 'reviewing_methodology') {
throw new Error('任务正在审查中,请稍后');
}
if (!task.extractedText) {
throw new Error('文档尚未提取完成,请稍后再试');
}
const startTime = Date.now();
try {
// 更新任务状态
await prisma.reviewTask.update({
where: { id: taskId },
data: {
status: 'reviewing',
startedAt: new Date(),
// 清除之前的结果(如果重新运行)
editorialReview: Prisma.JsonNull,
methodologyReview: Prisma.JsonNull,
overallScore: null,
errorMessage: null,
},
});
logger.info('[RVW] 开始审查', { taskId, 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;
}
}
/**
* 批量运行审查
* 最多并发5个任务
*/
export async function batchRunReview(params: BatchRunParams): Promise<{
success: string[];
failed: { taskId: string; error: string }[]
}> {
const { taskIds, agents, userId } = params;
// 验证智能体选择
validateAgentSelection(agents);
if (taskIds.length === 0) {
throw new Error('请选择至少一个任务');
}
logger.info('[RVW] 开始批量审查', {
taskCount: taskIds.length,
agents
});
const success: string[] = [];
const failed: { taskId: string; error: string }[] = [];
// 并发控制最多5个
const MAX_CONCURRENT = 5;
for (let i = 0; i < taskIds.length; i += MAX_CONCURRENT) {
const batch = taskIds.slice(i, i + MAX_CONCURRENT);
const results = await Promise.allSettled(
batch.map(taskId => runReview({ taskId, agents, userId }))
);
results.forEach((result, index) => {
const taskId = batch[index];
if (result.status === 'fulfilled') {
success.push(taskId);
} else {
failed.push({
taskId,
error: result.reason?.message || 'Unknown error',
});
}
});
}
logger.info('[RVW] 批量审查完成', {
successCount: success.length,
failedCount: failed.length
});
return { success, failed };
}
// ==================== 任务查询 ====================
/**
* 获取任务列表
*/
export async function getTaskList(params: TaskListParams): Promise<TaskListResponse> {
const { userId, status = 'all', page = 1, limit = 20 } = params;
const skip = (page - 1) * limit;
// 构建查询条件
const where: Prisma.ReviewTaskWhereInput = { userId };
if (status === 'pending') {
where.status = { in: ['pending', 'extracting', 'reviewing', 'reviewing_editorial', 'reviewing_methodology'] };
} else if (status === 'completed') {
where.status = { in: ['completed', 'failed'] };
}
const [tasks, total] = await Promise.all([
prisma.reviewTask.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take: limit,
select: {
id: true,
fileName: true,
fileSize: true,
status: true,
// 🆕 使用新字段
selectedAgents: true,
editorialScore: true,
methodologyStatus: true,
overallScore: true,
modelUsed: true,
createdAt: true,
completedAt: true,
durationSeconds: true,
wordCount: true,
},
}),
prisma.reviewTask.count({ where }),
]);
// 转换为TaskSummary格式直接使用新字段
const taskSummaries: TaskSummary[] = tasks.map(task => ({
id: task.id,
fileName: task.fileName,
fileSize: task.fileSize,
status: task.status as TaskStatus,
selectedAgents: (task.selectedAgents || ['editorial', 'methodology']) as AgentType[],
editorialScore: task.editorialScore ?? undefined,
methodologyStatus: task.methodologyStatus as any,
overallScore: task.overallScore ?? undefined,
modelUsed: task.modelUsed ?? undefined,
createdAt: task.createdAt,
completedAt: task.completedAt ?? undefined,
durationSeconds: task.durationSeconds ?? undefined,
wordCount: task.wordCount ?? undefined,
}));
return {
tasks: taskSummaries,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
}
/**
* 获取任务详情
*/
export async function getTaskDetail(userId: string, taskId: string) {
const task = await prisma.reviewTask.findFirst({
where: { id: taskId, userId },
});
if (!task) {
throw new Error('任务不存在或无权限访问');
}
return task;
}
/**
* 获取任务报告
*/
export async function getTaskReport(userId: string, taskId: string): Promise<ReviewReport> {
const task = await prisma.reviewTask.findFirst({
where: { id: taskId, userId },
});
if (!task) {
throw new Error('任务不存在或无权限访问');
}
if (task.status !== 'completed') {
throw new Error(`报告尚未完成,当前状态: ${task.status}`);
}
return {
taskId: task.id,
fileName: task.fileName,
wordCount: task.wordCount ?? undefined,
modelUsed: task.modelUsed ?? undefined,
// 🆕 直接使用新字段
selectedAgents: (task.selectedAgents || ['editorial', 'methodology']) as AgentType[],
overallScore: task.overallScore ?? undefined,
editorialReview: task.editorialReview as unknown as EditorialReview | undefined,
methodologyReview: task.methodologyReview as unknown as MethodologyReview | undefined,
completedAt: task.completedAt ?? undefined,
durationSeconds: task.durationSeconds ?? undefined,
};
}
/**
* 删除任务
*/
export async function deleteTask(userId: string, taskId: string): Promise<void> {
const task = await prisma.reviewTask.findFirst({
where: { id: taskId, userId },
});
if (!task) {
throw new Error('任务不存在或无权限访问');
}
await prisma.reviewTask.delete({
where: { id: taskId },
});
logger.info('[RVW] 任务已删除', { taskId });
}

View File

@@ -0,0 +1,116 @@
/**
* RVW稿件审查模块 - 工具函数
* @module rvw/services/utils
*/
import { MethodologyReview, MethodologyStatus } from '../types/index.js';
/**
* 从LLM响应中解析JSON
* 支持多种格式纯JSON、```json代码块、混合文本
*/
export function parseJSONFromLLMResponse<T>(content: string): T {
try {
// 1. 尝试直接解析
return JSON.parse(content) as T;
} catch {
// 2. 尝试提取```json代码块
const jsonMatch = content.match(/```json\s*\n?([\s\S]*?)\n?```/);
if (jsonMatch) {
try {
return JSON.parse(jsonMatch[1].trim()) as T;
} catch {
// 继续尝试其他方法
}
}
// 3. 尝试提取{}或[]包裹的内容
const objectMatch = content.match(/(\{[\s\S]*\})/);
if (objectMatch) {
try {
return JSON.parse(objectMatch[1]) as T;
} catch {
// 继续尝试其他方法
}
}
const arrayMatch = content.match(/(\[[\s\S]*\])/);
if (arrayMatch) {
try {
return JSON.parse(arrayMatch[1]) as T;
} catch {
// 失败
}
}
// 4. 所有尝试都失败
throw new Error('无法从LLM响应中解析JSON');
}
}
/**
* 根据方法学评估结果判断状态
* @param review 方法学评估结果
* @returns pass | warn | fail
*/
export function getMethodologyStatus(review: MethodologyReview | null | undefined): MethodologyStatus | undefined {
if (!review) return undefined;
const score = review.overall_score;
if (score >= 80) return 'pass';
if (score >= 60) return 'warn';
return 'fail';
}
/**
* 根据选择的智能体计算综合分数
* @param editorialScore 稿约规范性分数
* @param methodologyScore 方法学分数
* @param agents 选择的智能体
* @returns 综合分数
*/
export function calculateOverallScore(
editorialScore: number | null | undefined,
methodologyScore: number | null | undefined,
agents: string[]
): number | null {
const hasEditorial = agents.includes('editorial') && editorialScore != null;
const hasMethodology = agents.includes('methodology') && methodologyScore != null;
if (hasEditorial && hasMethodology) {
// 两个都选稿约40% + 方法学60%
return editorialScore! * 0.4 + methodologyScore! * 0.6;
} else if (hasEditorial) {
// 只选规范性
return editorialScore!;
} else if (hasMethodology) {
// 只选方法学
return methodologyScore!;
}
return null;
}
/**
* 验证智能体选择
* @param agents 选择的智能体列表
* @throws 如果选择无效
*/
export function validateAgentSelection(agents: string[]): void {
if (!agents || agents.length === 0) {
throw new Error('请至少选择一个智能体');
}
const validAgents = ['editorial', 'methodology'];
for (const agent of agents) {
if (!validAgents.includes(agent)) {
throw new Error(`无效的智能体类型: ${agent}`);
}
}
if (agents.length > 2) {
throw new Error('最多只能选择2个智能体');
}
}

View File

@@ -0,0 +1,157 @@
/**
* RVW稿件审查模块 - 类型定义
* @module rvw/types
*/
// ==================== 智能体类型 ====================
/**
* 智能体类型
* - editorial: 稿约规范性智能体
* - methodology: 方法学统计智能体
*/
export type AgentType = 'editorial' | 'methodology';
/**
* 任务状态
*/
export type TaskStatus =
| 'pending' // 待处理
| 'extracting' // 正在提取文档
| 'reviewing' // 正在评估
| 'completed' // 已完成
| 'failed'; // 失败
/**
* 方法学评估状态
*/
export type MethodologyStatus = 'pass' | 'warn' | 'fail';
// ==================== 稿约规范性评估 ====================
export interface EditorialItem {
criterion: string;
status: 'pass' | 'warning' | 'fail';
score: number;
issues: string[];
suggestions: string[];
}
export interface EditorialReview {
overall_score: number;
summary: string;
items: EditorialItem[];
}
// ==================== 方法学评估 ====================
export interface MethodologyIssue {
type: string;
severity: 'major' | 'minor';
description: string;
location: string;
suggestion: string;
}
export interface MethodologyPart {
part: string;
score: number;
issues: MethodologyIssue[];
}
export interface MethodologyReview {
overall_score: number;
summary: string;
parts: MethodologyPart[];
}
// ==================== 请求参数 ====================
/**
* 运行审查的参数
*/
export interface RunReviewParams {
taskId: string;
agents: AgentType[]; // 可选1个或2个
userId: string;
}
/**
* 批量运行审查的参数
*/
export interface BatchRunParams {
taskIds: string[];
agents: AgentType[];
userId: string;
}
/**
* 任务列表查询参数
*/
export interface TaskListParams {
userId: string;
status?: 'all' | 'pending' | 'completed';
page?: number;
limit?: number;
}
// ==================== 响应类型 ====================
/**
* 任务摘要(用于列表展示)
*/
export interface TaskSummary {
id: string;
fileName: string;
fileSize: number;
status: TaskStatus;
selectedAgents: AgentType[];
editorialScore?: number;
methodologyStatus?: MethodologyStatus;
overallScore?: number;
modelUsed?: string;
createdAt: Date;
completedAt?: Date;
durationSeconds?: number;
wordCount?: number;
}
/**
* 任务列表响应
*/
export interface TaskListResponse {
tasks: TaskSummary[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
/**
* 完整报告
*/
export interface ReviewReport {
taskId: string;
fileName: string;
wordCount?: number;
modelUsed?: string;
selectedAgents: AgentType[];
overallScore?: number;
editorialReview?: EditorialReview;
methodologyReview?: MethodologyReview;
completedAt?: Date;
durationSeconds?: number;
}
// ==================== 工具函数类型 ====================
/**
* LLM消息格式
*/
export interface LLMMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}

View File

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

View File

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

View File

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

View File

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