RVW module (V3.0 Smart Review Enhancement): - Add LLM data validation via PromptService (RVW_DATA_VALIDATION) - Add ClinicalAssessmentSkill with FINER-based evaluation (RVW_CLINICAL) - Remove all numeric scores from UI (editorial, methodology, overall) - Implement partial_completed status with Promise.allSettled - Add error_details JSON field to ReviewTask for granular failure info - Fix overallStatus logic: warning status now counts as success - Restructure ForensicsReport: per-table LLM results, remove top-level block - Refactor ClinicalReport: structured collapsible sections - Increase all skill timeouts to 300s for long manuscripts (20+ pages) - Increase DataForensics LLM timeout to 180s, pg-boss to 15min - Executor default fallback timeout 30s -> 60s ASL module: - Add deep research history with sidebar accordion UI - Implement waterfall flow for historical task display - Upgrade Unifuncs DeepSearch API from S2 to S3 with fallback - Add ASL_SR module seed for admin configurability - Fix new search button inconsistency Docs: - Update RVW module status to V3.0 - Update deployment changelist - Add 0305 deployment summary DB Migration: - Add error_details JSONB column to rvw_schema.review_tasks Tested: All 4 review modules verified, partial completion working Made-with: Cursor
147 lines
3.2 KiB
TypeScript
147 lines
3.2 KiB
TypeScript
/**
|
||
* 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 综合分数(保留1位小数)
|
||
*/
|
||
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;
|
||
|
||
let score: number | null = null;
|
||
|
||
if (hasEditorial && hasMethodology) {
|
||
// 两个都选:稿约40% + 方法学60%
|
||
score = editorialScore! * 0.4 + methodologyScore! * 0.6;
|
||
} else if (hasEditorial) {
|
||
// 只选规范性
|
||
score = editorialScore!;
|
||
} else if (hasMethodology) {
|
||
// 只选方法学
|
||
score = methodologyScore!;
|
||
}
|
||
|
||
// 修复浮点数精度问题:保留1位小数
|
||
return score !== null ? Math.round(score * 10) / 10 : null;
|
||
}
|
||
|
||
/**
|
||
* 验证智能体选择
|
||
* @param agents 选择的智能体列表
|
||
* @throws 如果选择无效
|
||
*/
|
||
export function validateAgentSelection(agents: string[]): void {
|
||
if (!agents || agents.length === 0) {
|
||
throw new Error('请至少选择一个智能体');
|
||
}
|
||
|
||
const validAgents = ['editorial', 'methodology', 'clinical'];
|
||
for (const agent of agents) {
|
||
if (!validAgents.includes(agent)) {
|
||
throw new Error(`无效的智能体类型: ${agent}`);
|
||
}
|
||
}
|
||
|
||
if (agents.length > 3) {
|
||
throw new Error('最多只能选择3个智能体');
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|