Files
AIclinicalresearch/backend/src/modules/rvw/services/clinicalService.ts
HaHafeng 707f783229 feat(rvw): complete tenant portal polish and ops assignment fixes
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
2026-03-15 18:22:01 +08:00

95 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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'}`);
}
}