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

@@ -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个智能体');
}
}