Finalize RVW tenant portal UX and reliability updates by aligning login/profile interactions, stabilizing SMS code sends in weak-network scenarios, and fixing multi-tenant assignment payload handling to prevent runtime errors. Refresh RVW status and deployment checklist docs with SAE routing, frontend image build, and post-release validation guidance. Made-with: Cursor
172 lines
3.9 KiB
TypeScript
172 lines
3.9 KiB
TypeScript
/**
|
||
* RVW稿件审查模块 - 工具函数
|
||
* @module rvw/services/utils
|
||
*/
|
||
|
||
import { MethodologyReview, MethodologyStatus } from '../types/index.js';
|
||
import { jsonrepair } from 'jsonrepair';
|
||
|
||
/**
|
||
* 从LLM响应中解析JSON
|
||
* 支持多种格式:纯JSON、```json代码块、混合文本
|
||
*/
|
||
export function parseJSONFromLLMResponse<T>(content: string): T {
|
||
try {
|
||
// 1. 尝试直接解析
|
||
return JSON.parse(content) as T;
|
||
} catch {
|
||
// 1.1 先尝试 jsonrepair(处理尾逗号、引号缺失等常见脏 JSON)
|
||
try {
|
||
const repaired = jsonrepair(content);
|
||
return JSON.parse(repaired) 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 {
|
||
// 尝试修复代码块 JSON
|
||
try {
|
||
const repaired = jsonrepair(jsonMatch[1].trim());
|
||
return JSON.parse(repaired) as T;
|
||
} catch {
|
||
// 继续尝试其他方法
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 尝试提取{}或[]包裹的内容
|
||
const objectMatch = content.match(/(\{[\s\S]*\})/);
|
||
if (objectMatch) {
|
||
try {
|
||
return JSON.parse(objectMatch[1]) as T;
|
||
} catch {
|
||
try {
|
||
const repaired = jsonrepair(objectMatch[1]);
|
||
return JSON.parse(repaired) as T;
|
||
} catch {
|
||
// 继续尝试其他方法
|
||
}
|
||
}
|
||
}
|
||
|
||
const arrayMatch = content.match(/(\[[\s\S]*\])/);
|
||
if (arrayMatch) {
|
||
try {
|
||
return JSON.parse(arrayMatch[1]) as T;
|
||
} catch {
|
||
try {
|
||
const repaired = jsonrepair(arrayMatch[1]);
|
||
return JSON.parse(repaired) 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个智能体');
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|