/** * RVW稿件审查模块 - 工具函数 * @module rvw/services/utils */ import { MethodologyReview, MethodologyStatus } from '../types/index.js'; import { jsonrepair } from 'jsonrepair'; function tryParseJsonCandidate(candidate: string): T | null { const normalized = candidate.trim().replace(/^\uFEFF/, ''); if (!normalized) return null; try { return JSON.parse(normalized) as T; } catch { try { const repaired = jsonrepair(normalized); return JSON.parse(repaired) as T; } catch { return null; } } } function extractBalancedJsonCandidates(content: string): string[] { const text = content || ''; const candidates: string[] = []; const stack: string[] = []; let start = -1; let inString = false; let escaped = false; for (let i = 0; i < text.length; i++) { const ch = text[i]; if (inString) { if (escaped) { escaped = false; } else if (ch === '\\') { escaped = true; } else if (ch === '"') { inString = false; } continue; } if (ch === '"') { inString = true; continue; } if (ch === '{' || ch === '[') { if (stack.length === 0) start = i; stack.push(ch); continue; } if (ch === '}' || ch === ']') { if (stack.length === 0) continue; const open = stack[stack.length - 1]; if ((open === '{' && ch === '}') || (open === '[' && ch === ']')) { stack.pop(); if (stack.length === 0 && start >= 0) { candidates.push(text.slice(start, i + 1)); start = -1; } } else { // 栈失配时重置,继续寻找下一个合法片段 stack.length = 0; start = -1; } } } return candidates; } /** * 从LLM响应中解析JSON * 支持多种格式:纯JSON、```json代码块、混合文本 */ export function parseJSONFromLLMResponse(content: string): T { // 1) 直接解析 + jsonrepair const direct = tryParseJsonCandidate(content); if (direct !== null) return direct; // 2) 提取 Markdown 代码块(```json / ```) const fenceRegex = /```(?:json)?\s*\n?([\s\S]*?)\n?```/gi; for (const match of content.matchAll(fenceRegex)) { const parsed = tryParseJsonCandidate(match[1] || ''); if (parsed !== null) return parsed; } // 3) 平衡括号提取,逐候选尝试 const balancedCandidates = extractBalancedJsonCandidates(content); for (const candidate of balancedCandidates) { const parsed = tryParseJsonCandidate(candidate); if (parsed !== null) return parsed; } // 4) 最后兜底:贪婪正则对象 / 数组(兼容极端场景) const objectMatch = content.match(/(\{[\s\S]*\})/); if (objectMatch) { const parsed = tryParseJsonCandidate(objectMatch[1]); if (parsed !== null) return parsed; } const arrayMatch = content.match(/(\[[\s\S]*\])/); if (arrayMatch) { const parsed = tryParseJsonCandidate(arrayMatch[1]); if (parsed !== null) return parsed; } // 5) 所有尝试都失败 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个智能体'); } }