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
95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
/**
|
||
* RVW稿件审查模块 - 临床专业评估服务
|
||
* @module rvw/services/clinicalService
|
||
*
|
||
* 使用 PromptService 获取 RVW_CLINICAL prompt
|
||
* 返回纯 Markdown 报告(无分数)
|
||
*/
|
||
|
||
import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js';
|
||
import { ModelType } from '../../../common/llm/adapters/types.js';
|
||
import { logger } from '../../../common/logging/index.js';
|
||
import { prisma } from '../../../config/database.js';
|
||
import { getPromptService } from '../../../common/prompt/index.js';
|
||
import { composeRvwSystemPrompt, sanitizeRvwBusinessPrompt } from './promptProtocols.js';
|
||
|
||
export interface ClinicalReviewResult {
|
||
report: string;
|
||
summary: string;
|
||
}
|
||
|
||
/**
|
||
* 临床专业评估
|
||
* @param text 稿件文本
|
||
* @param modelType 模型类型
|
||
* @param userId 用户ID(用于灰度预览判断)
|
||
* @returns 评估结果(Markdown 报告 + 摘要)
|
||
*/
|
||
export async function reviewClinical(
|
||
text: string,
|
||
modelType: ModelType = 'deepseek-v3',
|
||
userId?: string,
|
||
tenantExpertPrompt?: string | null
|
||
): Promise<ClinicalReviewResult> {
|
||
try {
|
||
let businessPrompt = tenantExpertPrompt?.trim() || '';
|
||
let isDraft = false;
|
||
if (!businessPrompt) {
|
||
const promptService = getPromptService(prisma);
|
||
const result = await promptService.get('RVW_CLINICAL', {}, { userId });
|
||
businessPrompt = result.content;
|
||
isDraft = result.isDraft;
|
||
}
|
||
businessPrompt = sanitizeRvwBusinessPrompt('clinical', businessPrompt);
|
||
|
||
if (isDraft) {
|
||
logger.info('[RVW:Clinical] 使用 DRAFT 版本 Prompt(调试模式)', { userId });
|
||
}
|
||
|
||
const messages = [
|
||
{ role: 'system' as const, content: composeRvwSystemPrompt('clinical', businessPrompt) },
|
||
{
|
||
role: 'user' as const,
|
||
content: `请对以下医学稿件进行临床专业评估。\n\n稿件内容如下:\n${text}`,
|
||
},
|
||
];
|
||
|
||
logger.info('[RVW:Clinical] 开始临床专业评估', { modelType });
|
||
const llmAdapter = LLMFactory.getAdapter(modelType);
|
||
const response = await llmAdapter.chat(messages, {
|
||
temperature: 0.3,
|
||
maxTokens: 8000,
|
||
});
|
||
const content = response.content ?? '';
|
||
logger.info('[RVW:Clinical] 评估完成', {
|
||
modelType,
|
||
responseLength: content.length,
|
||
});
|
||
|
||
// 优先提取“总体评价”段落作为摘要,提取失败再兜底首段文本
|
||
let summary = '临床专业评估已完成';
|
||
const overallMatch = content.match(/(?:^|\n)\s*(?:#*\s*)?1[\.\、]\s*总体评价[\s\S]*?(?:\n(?:#*\s*)?2[\.\、]\s*详细问题清单与建议|$)/);
|
||
if (overallMatch) {
|
||
const extracted = overallMatch[0]
|
||
.replace(/(?:^|\n)\s*(?:#*\s*)?1[\.\、]\s*总体评价\s*/m, '')
|
||
.trim();
|
||
if (extracted) {
|
||
summary = extracted.substring(0, 200);
|
||
}
|
||
} else {
|
||
const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
|
||
if (lines.length > 0) {
|
||
summary = lines[0].trim().substring(0, 200);
|
||
}
|
||
}
|
||
|
||
return { report: content, summary };
|
||
} catch (error) {
|
||
logger.error('[RVW:Clinical] 临床专业评估失败', {
|
||
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'}`);
|
||
}
|
||
}
|