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:
@@ -38,3 +38,4 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -266,5 +266,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -214,3 +214,4 @@ https://iit.xunzhengyixue.com/api/v1/iit/health
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -143,3 +143,4 @@ https://iit.xunzhengyixue.com/api/v1/iit/health
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -44,3 +44,4 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -304,3 +304,4 @@ npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -166,3 +166,4 @@ npm run dev
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -61,5 +61,6 @@ WHERE table_schema = 'dc_schema'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -99,5 +99,6 @@ ORDER BY ordinal_position;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -112,5 +112,6 @@ runMigration()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -46,5 +46,6 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,5 +73,6 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -303,9 +303,25 @@ model ReviewTask {
|
||||
extractedText String @map("extracted_text")
|
||||
wordCount Int? @map("word_count")
|
||||
status String @default("pending")
|
||||
|
||||
// 🆕 智能体选择(Phase 2新增)
|
||||
selectedAgents String[] @default(["editorial", "methodology"]) @map("selected_agents")
|
||||
|
||||
// 评估结果
|
||||
editorialReview Json? @map("editorial_review")
|
||||
methodologyReview Json? @map("methodology_review")
|
||||
overallScore Float? @map("overall_score")
|
||||
|
||||
// 🆕 结果摘要(Phase 2新增,用于列表展示)
|
||||
editorialScore Float? @map("editorial_score")
|
||||
methodologyStatus String? @map("methodology_status") // pass/warn/fail
|
||||
|
||||
// 🆕 预留字段(暂不使用)
|
||||
picoExtract Json? @map("pico_extract")
|
||||
isArchived Boolean @default(false) @map("is_archived")
|
||||
archivedAt DateTime? @map("archived_at")
|
||||
|
||||
// 元数据
|
||||
modelUsed String? @map("model_used")
|
||||
startedAt DateTime? @map("started_at")
|
||||
completedAt DateTime? @map("completed_at")
|
||||
@@ -318,6 +334,7 @@ model ReviewTask {
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@index([createdAt])
|
||||
@@index([isArchived])
|
||||
@@map("review_tasks")
|
||||
@@schema("public")
|
||||
}
|
||||
|
||||
@@ -114,4 +114,5 @@ Write-Host ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -223,5 +223,6 @@ function extractCodeBlocks(obj, blocks = []) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -242,5 +242,6 @@ checkDCTables();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -194,5 +194,6 @@ createAiHistoryTable()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -181,5 +181,6 @@ createToolCTable()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -178,5 +178,6 @@ createToolCTable()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -327,3 +327,4 @@ runTests().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -292,3 +292,4 @@ verifySchemas()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -310,5 +310,6 @@ export function getBatchItems<T>(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
// ============================================
|
||||
|
||||
@@ -346,5 +346,6 @@ runTests().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -287,5 +287,6 @@ runTest()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -325,5 +325,6 @@ Content-Type: application/json
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -261,5 +261,6 @@ export const conflictDetectionService = new ConflictDetectionService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -211,5 +211,6 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -265,5 +265,6 @@ export const streamAIController = new StreamAIController();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -176,3 +176,4 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -110,3 +110,4 @@ checkTableStructure();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -97,3 +97,4 @@ checkProjectConfig().catch(console.error);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -79,3 +79,4 @@ main();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -536,3 +536,4 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -171,3 +171,4 @@ console.log('');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -488,3 +488,4 @@ export const patientWechatService = new PatientWechatService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -133,3 +133,4 @@ testDifyIntegration().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -162,3 +162,4 @@ testIitDatabase()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -148,3 +148,4 @@ if (hasError) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -174,3 +174,4 @@ async function testUrlVerification() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -255,3 +255,4 @@ main().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -139,3 +139,4 @@ Write-Host ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -232,3 +232,4 @@ export interface CachedProtocolRules {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -45,3 +45,4 @@ export default async function healthRoutes(fastify: FastifyInstance) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
126
backend/src/modules/rvw/__tests__/api.http
Normal file
126
backend/src/modules/rvw/__tests__/api.http
Normal 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
|
||||
|
||||
111
backend/src/modules/rvw/__tests__/test-api.ps1
Normal file
111
backend/src/modules/rvw/__tests__/test-api.ps1
Normal 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
|
||||
|
||||
374
backend/src/modules/rvw/controllers/reviewController.ts
Normal file
374
backend/src/modules/rvw/controllers/reviewController.ts
Normal 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 : '删除失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
25
backend/src/modules/rvw/index.ts
Normal file
25
backend/src/modules/rvw/index.ts
Normal 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';
|
||||
|
||||
46
backend/src/modules/rvw/routes/index.ts
Normal file
46
backend/src/modules/rvw/routes/index.ts
Normal 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);
|
||||
}
|
||||
|
||||
70
backend/src/modules/rvw/services/editorialService.ts
Normal file
70
backend/src/modules/rvw/services/editorialService.ts
Normal 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'}`);
|
||||
}
|
||||
}
|
||||
|
||||
70
backend/src/modules/rvw/services/methodologyService.ts
Normal file
70
backend/src/modules/rvw/services/methodologyService.ts
Normal 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'}`);
|
||||
}
|
||||
}
|
||||
|
||||
430
backend/src/modules/rvw/services/reviewService.ts
Normal file
430
backend/src/modules/rvw/services/reviewService.ts
Normal file
@@ -0,0 +1,430 @@
|
||||
/**
|
||||
* RVW稿件审查模块 - 主服务
|
||||
* @module rvw/services/reviewService
|
||||
*
|
||||
* 基于旧代码迁移:backend/src/legacy/services/reviewService.ts
|
||||
* 改造内容:
|
||||
* 1. console.log → logger
|
||||
* 2. 新增智能体选择逻辑
|
||||
* 3. 新增批量运行接口
|
||||
*
|
||||
* 注意:当前适配现有schema,Phase 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 });
|
||||
}
|
||||
116
backend/src/modules/rvw/services/utils.ts
Normal file
116
backend/src/modules/rvw/services/utils.ts
Normal 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个智能体');
|
||||
}
|
||||
}
|
||||
|
||||
157
backend/src/modules/rvw/types/index.ts
Normal file
157
backend/src/modules/rvw/types/index.ts
Normal 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;
|
||||
}
|
||||
|
||||
@@ -411,5 +411,6 @@ SET session_replication_role = 'origin';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -113,5 +113,6 @@ WHERE key = 'verify_test';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -256,5 +256,6 @@ verifyDatabase()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1
backend/src/types/global.d.ts
vendored
1
backend/src/types/global.d.ts
vendored
@@ -46,5 +46,6 @@ export {}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -69,5 +69,6 @@ Write-Host "✅ 完成!" -ForegroundColor Green
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -159,3 +159,4 @@ DELETE {{baseUrl}}/api/v2/pkb/knowledge/knowledge-bases/{{testKbId}}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -356,5 +356,6 @@ runAdvancedTests().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -422,5 +422,6 @@ runAllTests()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -380,5 +380,6 @@ runAllTests()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -164,5 +164,6 @@ Set-Location ..
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# AIclinicalresearch 系统当前状态与开发指南
|
||||
|
||||
> **文档版本:** v2.8
|
||||
> **文档版本:** v2.9
|
||||
> **创建日期:** 2025-11-28
|
||||
> **维护者:** 开发团队
|
||||
> **最后更新:** 2026-01-07
|
||||
> **重大进展:** 🎉 **PKB模块核心功能全部实现,具备生产可用性!** - 批处理完整流程验证通过
|
||||
> **重大进展:** 🎉 **RVW稿件审查模块开发完成(85%)!** - 后端迁移+数据库扩展+前端重构全部完成
|
||||
> **部署状态:** ✅ 生产环境运行中 | 公网地址:http://8.140.53.236/
|
||||
> **文档目的:** 快速了解系统当前状态,为新AI助手提供上下文
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
| **IIT** | IIT Manager Agent | AI驱动IIT研究助手 - 智能质控+REDCap集成 | ⭐⭐⭐⭐⭐ | 🎉 **Phase 1.5完成(60%)- AI对话+REDCap数据集成** | **P0** |
|
||||
| **SSA** | 智能统计分析 | 队列/预测模型/RCT分析 | ⭐⭐⭐⭐⭐ | 📋 规划中 | P2 |
|
||||
| **ST** | 统计分析工具 | 100+轻量化统计工具 | ⭐⭐⭐⭐ | 📋 规划中 | P2 |
|
||||
| **RVW** | 稿件审查系统 | 方法学评估、审稿流程 | ⭐⭐⭐⭐ | 📋 规划中 | P3 |
|
||||
| **RVW** | 稿件审查系统 | 方法学评估、审稿流程 | ⭐⭐⭐⭐ | ✅ **开发完成(85%)** | P3 |
|
||||
|
||||
---
|
||||
|
||||
@@ -661,6 +661,7 @@ AIclinicalresearch/
|
||||
| **2026-01-07 上午** | **PKB前端V3** 🎉 | ✅ PKB模块前端V3设计实现完成(Dashboard+Workspace+3种工作模式) |
|
||||
| **2026-01-07 下午** | **PKB批处理完善** 🏆 | ✅ 批处理完整流程调试通过(执行+进度+结果导出)+ 文档上传功能 + UI优化 |
|
||||
| **当前** | **PKB模块生产可用** | ✅ 核心功能全部实现(90%),具备生产环境部署条件 |
|
||||
| **2026-01-07 晚** | **RVW模块开发完成** 🎉 | ✅ Phase 1-3完成(后端迁移+数据库扩展+前端重构) |
|
||||
|
||||
---
|
||||
|
||||
@@ -814,9 +815,9 @@ npm run dev # http://localhost:3000
|
||||
- **总计**:约 85,000 行
|
||||
|
||||
### 模块完成度
|
||||
- ✅ **已完成**:AIA(100%)、平台基础层(100%)
|
||||
- 🚧 **开发中**:PKB(75%,前端V3设计完成)、ASL(80%)、DC(Tool C 98%,Tool B后端100%,Tool B前端0%)、IIT(60%,Phase 1.5完成)
|
||||
- 📋 **未开始**:SSA、ST、RVW
|
||||
- ✅ **已完成**:AIA(100%)、平台基础层(100%)、RVW(85%,Phase 1-3完成)
|
||||
- 🚧 **开发中**:PKB(90%,核心功能完成)、ASL(80%)、DC(Tool C 98%,Tool B后端100%,Tool B前端0%)、IIT(60%,Phase 1.5完成)
|
||||
- 📋 **未开始**:SSA、ST
|
||||
|
||||
### 部署完成度
|
||||
- ✅ **基础设施**:VPC(100%)、NAT网关(100%)、安全组(100%)
|
||||
@@ -952,9 +953,9 @@ if (items.length >= 50) {
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v2.8
|
||||
**文档版本**:v2.9
|
||||
**最后更新**:2026-01-07
|
||||
**下次更新**:ASL智能文献筛选模块启动 或 IIT Manager Agent Phase 2
|
||||
**下次更新**:RVW生产环境部署 或 ASL智能文献筛选模块启动
|
||||
|
||||
---
|
||||
|
||||
@@ -1016,3 +1017,36 @@ if (items.length >= 50) {
|
||||
- ✅ 测试通过:查询test0102项目,ID 7患者详细信息
|
||||
|
||||
**模块进度**:60%完成(Phase 1.5)
|
||||
|
||||
---
|
||||
|
||||
**RVW稿件审查模块开发完成(2026-01-07)**:
|
||||
|
||||
### Phase 1:后端模块迁移与扩展
|
||||
- ✅ 创建 `backend/src/modules/rvw/` 模块结构
|
||||
- ✅ 迁移 reviewService、editorialService、methodologyService
|
||||
- ✅ 实现智能体选择(selectedAgents)
|
||||
- ✅ 实现批量运行API(batchRunReviewTasks)
|
||||
- ✅ 替换 console.log 为 logger 服务
|
||||
- ✅ 注册 v2 API路由(/api/v2/rvw)
|
||||
|
||||
### Phase 2:数据库字段扩展
|
||||
- ✅ 添加 selectedAgents、editorialScore、methodologyStatus 字段
|
||||
- ✅ 添加 picoExtract、isArchived、archivedAt 字段
|
||||
- ✅ 使用 prisma db push 同步到数据库
|
||||
|
||||
### Phase 3:前端重构(frontend-v2)
|
||||
- ✅ 创建 `frontend-v2/src/modules/rvw/index.tsx`(~503行)
|
||||
- ✅ 实现 Dashboard 组件(任务列表、筛选、批量操作)
|
||||
- ✅ 实现 ReportDetail 组件(双标签页切换)
|
||||
- ✅ 实现 AgentModal 组件(智能体选择弹窗)
|
||||
- ✅ 注册到 moduleRegistry.ts
|
||||
- ✅ 添加顶部导航"预审稿"入口
|
||||
|
||||
**技术亮点**:
|
||||
- 🔥 **新旧API兼容**:v1 + v2 API同时运行
|
||||
- 🔥 **智能体可选**:用户可选择运行稿约规范性/方法学/两者
|
||||
- 🔥 **批量操作**:支持多选任务批量运行
|
||||
- 🔥 **云原生改造**:使用 logger 服务,遵循开发规范
|
||||
|
||||
**模块进度**:85%完成(Phase 1-3)
|
||||
|
||||
@@ -606,5 +606,6 @@ async saveProcessedData(recordId, newData) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -793,5 +793,6 @@ export const AsyncProgressBar: React.FC<AsyncProgressBarProps> = ({
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1286,5 +1286,6 @@ interface FulltextScreeningResult {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -400,5 +400,6 @@ GET /api/v1/asl/fulltext-screening/tasks/:taskId/export
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -343,5 +343,6 @@ Linter错误:0个
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -502,5 +502,6 @@ Failed to open file '\\tmp\\extraction_service\\temp_10000_test.pdf'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -568,5 +568,6 @@ df['creatinine'] = pd.to_numeric(df['creatinine'], errors='coerce')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -406,5 +406,6 @@ npm run dev
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -983,5 +983,6 @@ export const aiController = new AIController();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1317,5 +1317,6 @@ npm install react-markdown
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -225,5 +225,6 @@ FMA___基线 | FMA___1个月 | FMA___2个月
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -383,5 +383,6 @@ formula = "FMA总分(0-100) / 100"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -217,5 +217,6 @@ async handleFillnaMice(request, reply) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -189,5 +189,6 @@ method: 'mean' | 'median' | 'mode' | 'constant' | 'ffill' | 'bfill'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -339,5 +339,6 @@ Changes:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -411,5 +411,6 @@ cd path; command
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -640,5 +640,6 @@ import { logger } from '../../../../common/logging/index.js';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -644,5 +644,6 @@ Content-Length: 45234
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -296,5 +296,6 @@ Response:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -449,5 +449,6 @@ Response:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -443,5 +443,6 @@ import { ChatContainer } from '@/shared/components/Chat';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -353,5 +353,6 @@ const initialMessages = defaultMessages.length > 0 ? defaultMessages : [{
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -393,5 +393,6 @@ python main.py
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -641,5 +641,6 @@ http://localhost:5173/data-cleaning/tool-c
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -251,5 +251,6 @@ Day 5 (6-8小时):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -429,5 +429,6 @@ Docs: docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -404,5 +404,6 @@ const mockAssets: Asset[] = [
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -388,5 +388,6 @@ frontend-v2/src/modules/dc/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -348,5 +348,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -302,5 +302,6 @@ ConflictDetectionService // 冲突检测(字段级对比)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -351,5 +351,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -314,5 +314,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -378,5 +378,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -466,5 +466,6 @@ Tool B后端代码**100%复用**了平台通用能力层,无任何重复开发
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -312,5 +312,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user