feat(rvw,asl): RVW V3.0 smart review + ASL deep research history + stability

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
This commit is contained in:
2026-03-07 19:24:21 +08:00
parent 91ae80888e
commit 87655ea7e6
46 changed files with 2929 additions and 511 deletions

View File

@@ -0,0 +1,541 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI智能文献平台 v6.0 (互斥手风琴版)</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
.fade-in { animation: fadeIn 0.3s ease-out forwards; }
.slide-in-from-bottom { animation: slideInFromBottom 0.3s ease-out forwards; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideInFromBottom { from { transform: translateY(1rem); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
/* 隐藏滚动条但保留滚动功能 */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* * 核心黑科技:利用 CSS Grid 实现完美的柔性折叠动画
* grid-rows-[0fr] 会将高度压缩到 0
* grid-rows-[1fr] 会让高度填满 flex 分配的空间
*/
.accordion-grid {
transition: grid-template-rows 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.accordion-content {
overflow: hidden;
}
</style>
</head>
<body class="bg-slate-50 font-sans text-slate-900 overflow-hidden h-screen flex flex-col">
<!-- 全局顶部导航 -->
<header class="h-14 bg-slate-900 text-slate-300 flex items-center justify-between px-6 shadow-md z-30 shrink-0">
<div class="flex items-center space-x-8">
<div class="flex items-center space-x-2 text-white font-bold tracking-wide">
<div class="bg-blue-600 p-1 rounded"><i data-lucide="activity" class="w-4 h-4 text-white"></i></div>
<span>医学科研 AI 平台</span>
</div>
<nav class="flex space-x-1">
<a href="#" class="px-4 py-2 text-sm font-medium hover:text-white transition rounded-md">AI 问答</a>
<a href="#" class="px-4 py-2 text-sm font-medium text-white bg-slate-800 rounded-md shadow-inner flex items-center">
<i data-lucide="book-open" class="w-4 h-4 mr-2 text-blue-400"></i> AI 文献
</a>
<a href="#" class="px-4 py-2 text-sm font-medium hover:text-white transition rounded-md">数据清洗</a>
<a href="#" class="px-4 py-2 text-sm font-medium hover:text-white transition rounded-md">统计分析</a>
</nav>
</div>
<div class="flex items-center space-x-4">
<button class="text-slate-400 hover:text-white"><i data-lucide="bell" class="w-5 h-5"></i></button>
<div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white font-bold text-sm">Dr</div>
</div>
</header>
<div class="flex-1 flex overflow-hidden">
<!-- ============================================== -->
<!-- 核心重构 V6: 互斥手风琴侧边栏 (Mutually Exclusive Accordion) -->
<!-- ============================================== -->
<div class="w-72 bg-white border-r border-slate-200 flex flex-col z-20 shrink-0 shadow-sm relative">
<!-- 【面板 A】智能文献检索 -->
<div id="panel-search-wrapper" class="flex flex-col transition-all duration-400">
<!-- Header A -->
<button onclick="togglePanel('SEARCH')" class="flex items-center justify-between px-5 py-4 hover:bg-slate-50 transition-colors w-full group border-b border-transparent" id="header-search">
<div class="flex items-center text-slate-800 font-bold text-[14px] transition-colors" id="title-search">
<i data-lucide="sparkles" class="w-4 h-4 mr-2.5 text-indigo-500" id="icon-search"></i>
智能文献检索
</div>
<i data-lucide="chevron-down" id="chevron-search" class="w-4 h-4 text-slate-400 transition-transform duration-400"></i>
</button>
<!-- Content A (利用 Grid 实现流畅折叠) -->
<div id="grid-search" class="grid accordion-grid grid-rows-[1fr]">
<div class="accordion-content flex flex-col min-h-0 bg-white">
<div class="p-4 pb-2 pt-0 shrink-0">
<button onclick="createNewSearch()" class="w-full bg-indigo-600 text-white hover:bg-indigo-700 py-2.5 rounded-lg flex items-center justify-center text-sm font-semibold transition-colors shadow-sm shadow-indigo-200">
<i data-lucide="plus" class="w-4 h-4 mr-1.5"></i> 新建智能检索
</button>
</div>
<div class="px-4 py-2 flex items-center justify-between text-xs font-bold text-slate-400 uppercase tracking-wider shrink-0">
<span>最近检索历史</span>
<i data-lucide="history" class="w-3.5 h-3.5"></i>
</div>
<!-- List A -->
<div class="flex-1 overflow-y-auto px-3 pb-3 space-y-1 no-scrollbar" id="search-list-container">
<!-- 由 JS 渲染 -->
</div>
</div>
</div>
</div>
<!-- 始终存在的分割线 -->
<div class="h-px bg-slate-200 shrink-0 z-10 shadow-sm"></div>
<!-- 【面板 B】系统综述项目 -->
<div id="panel-project-wrapper" class="flex flex-col transition-all duration-400">
<!-- Header B -->
<button onclick="togglePanel('PROJECT')" class="flex items-center justify-between px-5 py-4 hover:bg-slate-50 transition-colors w-full group bg-slate-50/50" id="header-project">
<div class="flex items-center text-slate-700 font-bold text-[14px] transition-colors" id="title-project">
<i data-lucide="folder-kanban" class="w-4 h-4 mr-2.5 text-slate-500" id="icon-project"></i>
系统综述项目 (SR)
</div>
<i data-lucide="chevron-up" id="chevron-project" class="w-4 h-4 text-slate-400 transition-transform duration-400"></i>
</button>
<!-- Content B -->
<div id="grid-project" class="grid accordion-grid grid-rows-[0fr]">
<div class="accordion-content flex flex-col min-h-0 bg-slate-50">
<div class="px-4 py-2 flex items-center justify-between text-xs font-bold text-slate-400 uppercase tracking-wider shrink-0 mt-1">
<span>我的工作区</span>
<i data-lucide="layout-grid" class="w-3.5 h-3.5"></i>
</div>
<!-- List B -->
<div class="flex-1 overflow-y-auto px-3 pb-2 space-y-1 no-scrollbar" id="project-list-container">
<!-- 由 JS 渲染项目列表 -->
</div>
<!-- 弱化设计的新建项目按钮 -->
<div class="px-4 pb-4 shrink-0">
<button onclick="createNewProject()" class="w-full border border-dashed border-slate-300 text-slate-500 hover:text-blue-600 hover:border-blue-300 hover:bg-blue-50 py-2 rounded-lg flex items-center justify-center text-[13px] font-medium transition-colors bg-white">
<i data-lucide="plus" class="w-3.5 h-3.5 mr-1.5"></i> 创建新 SR 项目
</button>
</div>
</div>
</div>
</div>
<!-- 侧边栏底部弹簧垫片,把剩余空间挤上去 -->
<div class="flex-1 bg-slate-50"></div>
<!-- 底部配额信息 (固定在最底) -->
<div class="p-4 border-t border-slate-200 bg-white shrink-0">
<div class="text-[10px] text-slate-500 mb-1.5 font-medium flex justify-between uppercase tracking-wider">
<span>DeepSearch 配额</span>
<span>45k/100k</span>
</div>
<div class="w-full bg-slate-100 rounded-full h-1.5 overflow-hidden">
<div class="bg-indigo-400 h-full rounded-full" style="width: 45%"></div>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="flex-1 flex flex-col overflow-hidden bg-slate-50 relative" id="main-content-container">
<!-- 由 JS 渲染核心界面 -->
</div>
</div>
<script>
// ==========================================
// 1. 数据模型与状态 (Data Models & State)
// ==========================================
let searchHistories = [
{ id: 1, title: 'SGLT2 抑制剂对糖尿病的心血管影响', date: '今天', hasResult: true },
{ id: 2, title: 'PD-1 联合化疗在非小细胞肺癌中的疗效', date: '昨天', hasResult: true },
{ id: 3, title: 'Aspirin 一级预防最新 RCT', date: '前天', hasResult: true },
{ id: 4, title: '阿兹海默症的最新靶向治疗方案综述', date: '本周', hasResult: true },
{ id: 5, title: '肠道微生态与抑郁症发病机制的相关性研究', date: '本周', hasResult: true },
{ id: 6, title: 'COVID-19 长新冠症状的系统性回顾', date: '更早', hasResult: true },
{ id: 7, title: '间歇性禁食对代谢综合征的影响', date: '更早', hasResult: true },
{ id: 8, title: 'CAR-T 细胞疗法在实体瘤中的进展', date: '更早', hasResult: true }
];
let srProjects = [
{ id: 1, title: 'SGLT2 抑制剂 Meta 分析', phase: 4, status: '进行中', count: 42, tiabConflictsResolved: true, metaRun: false },
{ id: 2, title: '二甲双胍对认知功能的系统综述', phase: 2, status: '初筛中', count: 856, tiabConflictsResolved: false, metaRun: false },
{ id: 3, title: 'CAR-T 治疗严重不良事件汇总', phase: 6, status: '已闭环', count: 18, tiabConflictsResolved: true, metaRun: true }
];
const PHASES = [
{ id: 1, name: '检索', icon: 'search' },
{ id: 2, name: '初筛', icon: 'file-text' },
{ id: 3, name: '复筛', icon: 'file-text' },
{ id: 4, name: '提取/RoB', icon: 'database' },
{ id: 5, name: 'Meta', icon: 'activity' },
{ id: 6, name: '报告', icon: 'file-output' }
];
// 核心全局状态 V6.0
const state = {
expandedPanel: 'SEARCH', // 核心变量:控制当前哪个抽屉打开 ('SEARCH' 或 'PROJECT')
activeContext: 'SEARCH', // 核心变量:控制右侧主屏显示什么内容
currentSearchId: 1,
currentProjectId: null,
};
// ==========================================
// 2. 状态变更函数 (Actions)
// ==========================================
// ⭐ 手风琴核心逻辑
function togglePanel(panelName) {
if (state.expandedPanel !== panelName) {
state.expandedPanel = panelName;
// 联动:当用户点开某个抽屉时,如果右侧内容不属于该抽屉,自动切换右侧内容
if (panelName === 'SEARCH' && state.activeContext !== 'SEARCH') {
state.activeContext = 'SEARCH';
if (!state.currentSearchId && searchHistories.length > 0) state.currentSearchId = searchHistories[0].id;
} else if (panelName === 'PROJECT' && state.activeContext !== 'PROJECT') {
state.activeContext = 'PROJECT';
if (!state.currentProjectId && srProjects.length > 0) state.currentProjectId = srProjects[0].id;
}
renderApp();
}
}
function selectSearch(id) {
state.activeContext = 'SEARCH';
state.currentSearchId = id;
if (state.expandedPanel !== 'SEARCH') state.expandedPanel = 'SEARCH';
renderApp();
}
function selectProject(id) {
state.activeContext = 'PROJECT';
state.currentProjectId = id;
if (state.expandedPanel !== 'PROJECT') state.expandedPanel = 'PROJECT';
renderApp();
}
function createNewSearch() {
state.activeContext = 'SEARCH';
state.currentSearchId = 'new';
renderApp();
}
function createNewProject() {
state.activeContext = 'PROJECT';
state.currentProjectId = 'new';
renderApp();
}
// ⭐ 转化逻辑:从左上角直接飞入左下角
function convertToProject() {
const sourceSearch = searchHistories.find(s => s.id === state.currentSearchId);
const newProject = {
id: Date.now(),
title: `${sourceSearch ? sourceSearch.title : '未命名'} (AI 转化)`,
phase: 2,
status: '初筛中',
count: 1250,
tiabConflictsResolved: false,
metaRun: false
};
srProjects.unshift(newProject);
// 切换上下文并强制展开下方抽屉,收起上方抽屉
state.activeContext = 'PROJECT';
state.currentProjectId = newProject.id;
state.expandedPanel = 'PROJECT';
renderApp();
}
// 项目流程控制函数 (同前)
function resolveProjectConflicts(projectId) {
const p = srProjects.find(p => p.id === projectId);
if(p) p.tiabConflictsResolved = true;
renderApp();
}
function advanceProjectPhase(projectId, targetPhase) {
const p = srProjects.find(p => p.id === projectId);
if(p) { p.phase = targetPhase; p.status = targetPhase === 6 ? '已闭环' : '进行中'; }
renderApp();
}
function runProjectMeta(projectId) {
const p = srProjects.find(p => p.id === projectId);
if(p) p.metaRun = true;
renderApp();
}
// ==========================================
// 3. 视图渲染逻辑 (Views)
// ==========================================
function renderSidebar() {
const isSearchExpanded = state.expandedPanel === 'SEARCH';
// 1. 处理 CSS Grid 动画折叠
const gridSearch = document.getElementById('grid-search');
const gridProject = document.getElementById('grid-project');
const panelSearchWrap = document.getElementById('panel-search-wrapper');
const panelProjectWrap = document.getElementById('panel-project-wrapper');
if (isSearchExpanded) {
// 展开上方,收起下方
gridSearch.classList.replace('grid-rows-[0fr]', 'grid-rows-[1fr]');
gridProject.classList.replace('grid-rows-[1fr]', 'grid-rows-[0fr]');
panelSearchWrap.classList.replace('shrink-0', 'flex-1');
panelSearchWrap.classList.replace('min-h-0', 'min-h-0'); // keep
panelProjectWrap.classList.replace('flex-1', 'shrink-0');
} else {
// 收起上方,展开下方
gridSearch.classList.replace('grid-rows-[1fr]', 'grid-rows-[0fr]');
gridProject.classList.replace('grid-rows-[0fr]', 'grid-rows-[1fr]');
panelSearchWrap.classList.replace('flex-1', 'shrink-0');
panelProjectWrap.classList.replace('shrink-0', 'flex-1');
panelProjectWrap.classList.replace('min-h-0', 'min-h-0'); // keep
}
// 2. 处理 Header 的图标旋转与高亮
const chevSearch = document.getElementById('chevron-search');
const chevProject = document.getElementById('chevron-project');
const titleSearch = document.getElementById('title-search');
const titleProject = document.getElementById('title-project');
const iconSearch = document.getElementById('icon-search');
const iconProject = document.getElementById('icon-project');
if (isSearchExpanded) {
chevSearch.classList.add('rotate-180');
chevProject.classList.remove('rotate-180');
titleSearch.classList.add('text-indigo-700'); titleSearch.classList.remove('text-slate-800');
iconSearch.classList.replace('text-slate-400', 'text-indigo-500');
titleProject.classList.replace('text-blue-700', 'text-slate-700');
iconProject.classList.replace('text-blue-500', 'text-slate-500');
} else {
chevSearch.classList.remove('rotate-180');
chevProject.classList.add('rotate-180');
titleSearch.classList.remove('text-indigo-700'); titleSearch.classList.add('text-slate-800');
iconSearch.classList.replace('text-indigo-500', 'text-slate-400');
titleProject.classList.replace('text-slate-700', 'text-blue-700');
iconProject.classList.replace('text-slate-500', 'text-blue-500');
}
// 3. 渲染列表内部项目
const searchContainer = document.getElementById('search-list-container');
searchContainer.innerHTML = searchHistories.map(h => {
const isActive = (state.activeContext === 'SEARCH' && state.currentSearchId === h.id);
return `
<div onclick="selectSearch(${h.id})" class="cursor-pointer group flex items-start p-2.5 rounded-lg transition-colors ${isActive ? 'bg-indigo-50 border border-indigo-100 shadow-sm' : 'hover:bg-slate-100 border border-transparent'}">
<i data-lucide="message-square" class="w-4 h-4 mt-0.5 mr-2.5 shrink-0 ${isActive ? 'text-indigo-600' : 'text-slate-400 group-hover:text-slate-600'}"></i>
<div class="flex-1 overflow-hidden">
<h4 class="text-sm font-medium truncate ${isActive ? 'text-indigo-900' : 'text-slate-700'}">${h.title}</h4>
<span class="text-[10px] text-slate-400 mt-0.5 block">${h.date}</span>
</div>
</div>
`;
}).join('');
const projectContainer = document.getElementById('project-list-container');
projectContainer.innerHTML = srProjects.map(p => {
const isActive = (state.activeContext === 'PROJECT' && state.currentProjectId === p.id);
const statusColor = p.status === '已闭环' ? 'emerald' : p.status === '初筛中' ? 'amber' : 'blue';
return `
<div onclick="selectProject(${p.id})" class="cursor-pointer group flex items-start p-2.5 mb-1.5 rounded-lg transition-colors ${isActive ? 'bg-white border border-blue-200 shadow-sm ring-1 ring-blue-50' : 'hover:bg-white border border-transparent'}">
<i data-lucide="folder" class="w-4 h-4 mt-0.5 mr-2.5 shrink-0 ${isActive ? 'text-blue-600' : 'text-slate-400 group-hover:text-blue-500'}"></i>
<div class="flex-1 overflow-hidden">
<h4 class="text-[13px] font-bold truncate ${isActive ? 'text-blue-900' : 'text-slate-600'}">${p.title}</h4>
<div class="flex items-center mt-1.5 space-x-2">
<span class="text-[10px] font-bold text-${statusColor}-700 bg-${statusColor}-100 px-1.5 rounded">${p.status}</span>
<span class="text-[10px] font-mono text-slate-500">P${p.phase}</span>
</div>
</div>
</div>
`;
}).join('');
}
// 主内容区渲染 (与 V5 逻辑一致)
function renderMainContent() {
const container = document.getElementById('main-content-container');
if (state.activeContext === 'SEARCH') {
if (state.currentSearchId === 'new') {
container.innerHTML = `
<div class="flex-1 flex flex-col items-center justify-center p-8 fade-in h-full bg-white">
<div class="w-16 h-16 bg-indigo-50 border border-indigo-100 rounded-2xl flex items-center justify-center mb-6 shadow-sm">
<i data-lucide="sparkles" class="w-8 h-8 text-indigo-500"></i>
</div>
<h2 class="text-2xl font-bold text-slate-800 mb-2">探索医学前沿</h2>
<p class="text-slate-500 mb-8 max-w-md text-center text-sm">输入您的研究问题或 PICOS 关键词DeepSearch 将为您生成高质量综述报告与文献库。</p>
<div class="w-full max-w-2xl bg-white p-2 rounded-xl border border-slate-300 shadow-sm flex items-center focus-within:ring-2 ring-indigo-500/50 transition-all focus-within:border-indigo-500">
<input type="text" class="flex-1 outline-none px-4 py-3 text-slate-700 placeholder-slate-400 bg-transparent" placeholder="例如SGLT2 抑制剂对 2 型糖尿病的心血管结局影响...">
<button class="bg-indigo-600 hover:bg-indigo-700 text-white p-3 rounded-lg shadow-sm transition">
<i data-lucide="arrow-up" class="w-5 h-5"></i>
</button>
</div>
</div>
`;
} else {
const searchInfo = searchHistories.find(s => s.id === state.currentSearchId) || searchHistories[0];
container.innerHTML = `
<div class="flex flex-col h-full fade-in bg-white">
<header class="h-14 bg-white border-b border-slate-100 flex items-center px-6 shrink-0">
<h1 class="text-sm font-bold text-slate-700 truncate">${searchInfo.title}</h1>
<span class="ml-3 text-[10px] font-medium text-slate-500 bg-slate-100 px-2 py-1 rounded-md border border-slate-200 flex items-center"><i data-lucide="layers" class="w-3 h-3 mr-1"></i>1250 篇</span>
</header>
<div class="flex-1 overflow-y-auto p-6 md:p-8 space-y-8 pb-32">
<div class="max-w-4xl mx-auto flex space-x-4">
<div class="w-8 h-8 rounded-full bg-slate-200 flex items-center justify-center shrink-0 mt-1"><span class="text-slate-600 text-xs font-bold">Dr</span></div>
<div class="flex-1"><div class="inline-block bg-slate-100 px-5 py-3.5 rounded-2xl rounded-tl-none text-slate-800 text-sm shadow-sm">帮我查一下相关文献要求是近5年的英文随机对照试验(RCT)。</div></div>
</div>
<div class="max-w-4xl mx-auto flex space-x-4">
<div class="w-8 h-8 rounded-full bg-indigo-600 flex items-center justify-center shrink-0 mt-1 shadow-md shadow-indigo-200"><i data-lucide="bot" class="w-4 h-4 text-white"></i></div>
<div class="flex-1">
<div class="bg-white border border-slate-200 rounded-2xl p-6 shadow-sm">
<h4 class="font-bold text-slate-800 flex items-center mb-3">
<i data-lucide="check-circle-2" class="w-5 h-5 text-emerald-500 mr-2"></i> 智能检索完成:获取有效文献 1,250 篇
</h4>
<p class="text-sm text-slate-600 leading-relaxed mb-5">
基于您的查询,我从 PubMed 和 Cochrane Library 汇总了最新高质量研究。主要发现包括:目标干预不仅能有效改善主要结局指标,还在多个大型试验中显著降低了不良事件风险。
</p>
<div class="mt-4 p-5 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-xl flex items-center justify-between">
<div>
<h5 class="font-bold text-blue-900 text-sm flex items-center"><i data-lucide="workflow" class="w-4 h-4 mr-1.5 text-blue-600"></i> 需要进行严格的系统综述吗?</h5>
<p class="text-xs text-blue-700 mt-1.5 opacity-80">将这 1,250 篇文献转入流水线,即可启动自动化双盲初筛。</p>
</div>
<button onclick="convertToProject()" class="bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium py-2 px-5 rounded-lg shadow-sm shadow-blue-200 transition flex items-center shrink-0">
转入 SR 项目流水线 <i data-lucide="arrow-right" class="w-4 h-4 ml-1.5"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-white via-white to-transparent p-6 pb-8">
<div class="max-w-4xl mx-auto bg-white p-1.5 rounded-xl border border-slate-300 shadow-lg shadow-slate-200/50 flex items-center">
<input type="text" class="flex-1 outline-none px-4 py-2 text-sm text-slate-700 bg-transparent" placeholder="追问检索结果,或修改条件...">
<button class="bg-indigo-600 text-white p-2.5 rounded-lg shadow-sm"><i data-lucide="arrow-up" class="w-4 h-4"></i></button>
</div>
</div>
</div>
`;
}
} else if (state.activeContext === 'PROJECT') {
if (state.currentProjectId === 'new') {
container.innerHTML = `
<div class="flex-1 flex flex-col items-center justify-center p-8 fade-in h-full bg-slate-50">
<div class="w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mb-6 shadow-sm border border-blue-200">
<i data-lucide="folder-plus" class="w-8 h-8 text-blue-600"></i>
</div>
<h2 class="text-2xl font-bold text-slate-800 mb-2">创建系统综述项目 (SR)</h2>
<p class="text-slate-500 mb-8 max-w-md text-center text-sm">建立严谨的瀑布流科研管线。支持通过 DeepSearch 抓取或本地导入文献库。</p>
<div class="w-full max-w-lg bg-white p-7 rounded-2xl border border-slate-200 shadow-sm space-y-5">
<div>
<label class="block text-sm font-bold text-slate-700 mb-2">项目名称</label>
<input type="text" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 outline-none transition-all bg-slate-50 hover:bg-white" placeholder="例如:某某药物的疗效与安全性 Meta 分析">
</div>
<div class="pt-4 flex justify-end space-x-3 border-t border-slate-100">
<button class="px-5 py-2.5 text-sm font-medium text-slate-600 hover:bg-slate-100 rounded-lg transition-colors">取消</button>
<button class="px-5 py-2.5 text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 rounded-lg shadow-sm shadow-blue-200 transition-colors">创建并配置方案</button>
</div>
</div>
</div>
`;
} else {
const project = srProjects.find(p => p.id === state.currentProjectId) || srProjects[0];
container.innerHTML = `
<div class="flex flex-col h-full fade-in bg-slate-50/50">
<header class="h-14 bg-white border-b border-slate-200 flex items-center justify-between px-6 shadow-sm shrink-0">
<div class="flex items-center">
<div class="bg-blue-100 p-1 rounded mr-3"><i data-lucide="folder-kanban" class="w-4 h-4 text-blue-600"></i></div>
<h1 class="text-sm font-bold text-slate-800">${project.title}</h1>
</div>
<span class="text-[11px] font-bold text-slate-500 bg-slate-100 px-2.5 py-1 rounded-md border border-slate-200 tracking-wide uppercase">Workspace</span>
</header>
<!-- 瀑布流步进器 Stepper -->
<div class="bg-white border-b border-slate-200 px-8 py-3 shadow-sm shrink-0 overflow-x-auto">
<div class="flex items-center justify-between min-w-max">
${PHASES.map((phase, index) => {
const isActive = project.phase === phase.id;
const isPast = project.phase > phase.id;
const circleClasses = isActive ? 'border-blue-600 bg-blue-50 text-blue-600 shadow-sm ring-2 ring-blue-100'
: isPast ? 'border-emerald-500 bg-emerald-500 text-white'
: 'border-slate-300 bg-slate-50 text-slate-400';
const textClasses = isActive ? 'text-blue-800 font-bold' : isPast ? 'text-slate-700' : 'text-slate-400 font-medium';
const lineClasses = isPast ? 'bg-emerald-500' : 'bg-slate-200';
return `
<div class="flex items-center cursor-pointer group" onclick="advanceProjectPhase(${project.id}, ${phase.id})">
<div class="flex items-center justify-center w-7 h-7 rounded-full border-2 transition-all ${circleClasses}">
${isPast ? `<i data-lucide="check" class="w-3.5 h-3.5"></i>` : `<i data-lucide="${phase.icon}" class="w-3 h-3"></i>`}
</div>
<span class="ml-2.5 text-[13px] transition-colors ${textClasses} group-hover:text-blue-600">${phase.name}</span>
${index < PHASES.length - 1 ? `<div class="w-8 h-[2px] mx-4 transition-colors ${lineClasses}"></div>` : ''}
</div>
`;
}).join('')}
</div>
</div>
<main class="flex-1 overflow-y-auto p-8" id="sr-step-content">
${renderProjectPhaseContent(project)}
</main>
</div>
`;
}
}
}
function renderProjectPhaseContent(project) {
const SectionHeader = (title, desc) => `
<div class="mb-6">
<h2 class="text-xl font-bold text-slate-800">${title}</h2>
<p class="text-sm text-slate-500 mt-1.5">${desc}</p>
</div>
`;
switch(project.phase) {
case 1: return `<div class="max-w-4xl mx-auto fade-in">${SectionHeader("Step 1: 选题与检索数据源", "准备阶段...")}<div class="bg-white p-10 rounded-2xl border border-slate-200 text-center shadow-sm"><button onclick="advanceProjectPhase(${project.id}, 2)" class="bg-blue-600 text-white px-5 py-2.5 rounded-lg text-sm font-medium hover:bg-blue-700 shadow-sm">模拟导入完成,进入初筛</button></div></div>`;
case 2:
let conflictUI = !project.tiabConflictsResolved
? `<div class="bg-amber-50 border border-amber-200 rounded-xl p-6 mb-6"><h4 class="font-bold text-amber-800 text-sm flex items-center mb-2"><i data-lucide="alert-triangle" class="w-4 h-4 mr-2"></i> 必须解决冲突</h4><div class="bg-white rounded-lg border border-amber-100 p-4 flex justify-between items-center shadow-sm"><span class="text-sm font-medium text-slate-700">冲突记录: Example Paper 2024</span><button onclick="resolveProjectConflicts(${project.id})" class="text-xs font-bold bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 shadow-sm">一键解决</button></div></div>`
: `<div class="bg-emerald-50 border border-emerald-200 rounded-xl p-6 mb-6 flex items-center"><i data-lucide="check-circle" class="w-6 h-6 text-emerald-500 mr-3"></i><div><h4 class="font-bold text-emerald-800 text-sm">初筛已完成</h4></div></div>`;
let nxtBtn = !project.tiabConflictsResolved
? `<button disabled class="px-6 py-2.5 rounded-lg bg-slate-200 text-slate-400 text-sm font-medium cursor-not-allowed">进入复筛</button>`
: `<button onclick="advanceProjectPhase(${project.id}, 3)" class="bg-blue-600 text-white px-6 py-2.5 rounded-lg text-sm font-bold shadow-sm hover:bg-blue-700">进入复筛</button>`;
return `<div class="max-w-4xl mx-auto fade-in">${SectionHeader("Step 2: 标题摘要初筛", "双盲初筛流水线。")}${conflictUI}<div class="flex justify-end">${nxtBtn}</div></div>`;
case 3:
case 4:
case 5:
let btnText = project.phase === 3 ? '进入数据提取' : project.phase === 4 ? '进入Meta分析' : '生成最终报告';
return `<div class="max-w-4xl mx-auto fade-in">${SectionHeader(`Step ${project.phase}: ${PHASES[project.phase-1].name}`, "专业操作区。")}<div class="bg-white p-16 text-center rounded-2xl border border-slate-200 mb-6 shadow-sm"><i data-lucide="${PHASES[project.phase-1].icon}" class="w-12 h-12 text-slate-300 mx-auto mb-4"></i><p class="text-slate-500 text-sm font-medium">这里是 ${PHASES[project.phase-1].name} 的详细工作台区域。</p></div><div class="flex justify-end"><button onclick="advanceProjectPhase(${project.id}, ${project.phase + 1})" class="bg-slate-800 text-white px-6 py-2.5 rounded-lg text-sm font-bold flex items-center shadow-sm hover:bg-slate-700">${btnText} <i data-lucide="arrow-right" class="w-4 h-4 ml-2"></i></button></div></div>`;
case 6:
return `<div class="max-w-4xl mx-auto fade-in">${SectionHeader("Step 6: 报告生成", "项目成果。")}<div class="bg-white border border-slate-200 rounded-2xl p-12 flex flex-col items-center text-center shadow-sm"><div class="bg-emerald-50 p-4 rounded-full border border-emerald-100 mb-4"><i data-lucide="check-circle" class="w-12 h-12 text-emerald-500"></i></div><h2 class="text-2xl font-black text-slate-800">流程闭环完成</h2><button class="mt-8 bg-blue-600 text-white px-6 py-3 rounded-xl shadow-md text-sm font-bold flex items-center hover:bg-blue-700 transition"><i data-lucide="download" class="w-4 h-4 mr-2"></i> 下载 PRISMA & Word 报告</button></div></div>`;
}
}
// ==========================================
// 4. 初始化引擎
// ==========================================
function renderApp() {
renderSidebar();
renderMainContent();
lucide.createIcons();
}
document.addEventListener('DOMContentLoaded', () => {
renderApp();
});
</script>
</body>
</html>

View File

@@ -1,30 +1,25 @@
# RVW稿件审查模块 - 当前状态与开发指南
> **文档版本:** v5.1
> **文档版本:** v6.0
> **创建日期:** 2026-01-07
> **最后更新:** 2026-02-18
> **最后更新:** 2026-03-07
> **维护者:** 开发团队
> **当前状态:** 🚀 **V2.0 "数据侦探" Week 3 完成(统计验证扩展+用户体验优化**
> **当前状态:** 🚀 **V3.0 "智能审稿增强" 完成LLM数据核查 + 临床评估 + 稳定性增强**
> **文档目的:** 快速了解RVW模块状态为新AI助手提供上下文
>
> **🎉 V2.0 进展2026-02-18 Week 3**
> - ✅ **负号归一化**:防止 float() 崩溃,覆盖 6 种负号变体
> - ✅ **T 检验验证增强**:智能样本量提取 + subrow 精确高亮
> - ✅ **SE 三角验证增强**:多行单元格 subrow 支持
> - ✅ **CI vs P 值验证增强**subrow 支持 + 灵活 P 值解析
> - ✅ **前端翻译映射**:新增 6 种 IssueType 中文翻译
> - ✅ **文件格式提示**PDF/.doc 上传时提示无法数据验证
> **🎉 V3.0 进展2026-03-07**
> - ✅ **LLM 数据核查**DataForensicsSkill 增加 LLM 验证通道,规则验证兜底,独立 60s 超时
> - ✅ **临床专业评估**:新增 ClinicalAssessmentSkill基于 FINER 标准的研究选题系统评估
> - ✅ **PromptService 集成**RVW_DATA_VALIDATION + RVW_CLINICAL 两个 Prompt 可在运营管理端配置
> - ✅ **稳定性增强**SkillExecutor 使用 Promise.allSettled 实现并行故障隔离
> - ✅ **部分完成支持**:新增 `partial_completed` 状态 + `errorDetails` 字段,部分模块失败仍展示成功结果
> - ✅ **前端 4 Tab 报告**:稿约规范性 / 方法学 / 数据验证 / 临床评估Word 导出全覆盖
>
> **🎉 V2.0 进展Week 1-2**
> - ✅ **L1 算术验证器**行列加总、百分比验证Day 3
> - ✅ **L2 统计验证器**CI↔P 值一致性、卡方检验逆向验证Day 6
> - ✅ **L2.5 一致性取证**SE三角验证、SD>Mean检查Day 6 终审提权)
> - ✅ **Word 文档解析**python-docx 表格提取 + 特殊符号提取Day 2
> - ✅ **Skills 核心框架**types、registry、executor、profile、contextDay 7
> - ✅ **DataForensicsSkill**OSS 集成、依赖注入、优雅降级Day 8
> - ✅ **EditorialSkill + MethodologySkill**封装现有服务Day 9
> - ✅ **ReviewWorker 改造**:集成 SkillExecutor支持 V1/V2 切换Day 10
> - ✅ **前端数据验证 Tab**ForensicsReport 组件、精确单元格高亮Week 3
> **V2.0 进展回顾**
> - ✅ L1 算术验证 + L2 统计验证 + L2.5 一致性取证
> - ✅ Skills 核心框架types, registry, executor, profile
> - ✅ DataForensicsSkill + EditorialSkill + MethodologySkill
> - ✅ ReviewWorker 改造 + 前端数据验证 Tab
---
@@ -39,13 +34,13 @@
| **商业价值** | ⭐⭐⭐⭐⭐ 极高 |
| **独立性** | ⭐⭐⭐⭐⭐ 极高(用户群完全不同) |
| **目标用户** | 期刊初审编辑 |
| **开发状态** | ✅ **核心功能100%完成支持Word导出已集成到 frontend-v2** |
| **开发状态** | ✅ **V3.0 完成4维审查规范性+方法学+数据验证+临床评估)+ 稳定性增强 + Word导出** |
### 核心目标
> 打造一个**"开箱即用"**的智能审稿工具。编辑上传稿件,系统自动运行双重检查(规范性+方法学),输出可供参考的审稿报告。
> 打造一个**"开箱即用"**的智能审稿工具。编辑上传稿件,系统自动运行多维检查(规范性+方法学+数据验证+临床评估),输出可供参考的审稿报告。
>
> **核心指标**:上传到出报告 < 2分钟;规范性问题检出率 > 80%
> **核心指标**:上传到出报告 < 3分钟4模块并行;规范性问题检出率 > 80%
### 功能规格
@@ -60,7 +55,19 @@
- 统计学方法描述评估5个检查点
- 统计分析评估6个检查点
3. **综合评分 + PICO提取**
3. **数据验证**V2.0 规则 + V3.0 LLM 双通道)
- L1 算术验证:行列加总、百分比计算
- L2 统计验证CI↔P 一致性、T检验、卡方检验
- L2.5 一致性取证SE三角验证、SD>Mean检查
- LLM 智能核查:批量表格发给 LLM 核查(独立 60s 超时,失败降级为纯规则验证)
4. **临床专业评估**V3.0 新增)
- 基于 FINER 标准(可行性/创新性/伦理性/相关性)
- 研究问题明确性评估PICO 完整性)
- 创新性 + 临床价值 + 科学性 + 可行性多维分析
- Prompt 可在运营管理端配置RVW_CLINICAL
5. **综合评分 + PICO提取**
- 规范性分数0-100
- 方法学状态(🔴错误 🟡存疑 🟢通过)
- PICO结构化提取P/I/C/O
@@ -95,14 +102,15 @@ backend/src/modules/rvw/
│ ├── reviewService.ts # 核心服务(任务创建、执行)
│ ├── editorialService.ts # 稿约规范性评估
│ ├── methodologyService.ts # 方法学评估
│ ├── clinicalService.ts # 🆕 V3.0 临床专业评估服务
│ └── utils.ts # 工具函数
├── workers/
│ └── reviewWorker.ts # pg-boss异步任务处理V2.0 Skills集成
├── skills/ # 🆕 V2.0 Skills 架构
│ ├── core/ # 核心框架types, registry, executor等
│ ├── library/ # Skill 实现Forensics, Editorial, Methodology
│ └── reviewWorker.ts # pg-boss异步任务处理V2.0 Skills集成 + V3.0 partial_completed
├── skills/ # V2.0 Skills 架构
│ ├── core/ # 核心框架types, registry, executor[allSettled]等)
│ ├── library/ # Skill 实现Forensics[+LLM], Editorial, Methodology, 🆕Clinical
│ └── index.ts # 模块入口
├── types/index.ts # TypeScript类型定义
├── types/index.ts # TypeScript类型定义(含 partial_completed 状态)
└── __tests__/ # API测试脚本
前端(✅ 已完成):
@@ -116,11 +124,13 @@ frontend-v2/src/modules/rvw/
└── components/
├── Header.tsx # 页头(上传按钮)
├── Sidebar.tsx # 侧边栏导航
├── TaskTable.tsx # 任务列表表格
├── TaskDetail.tsx # 任务详情(进度条+报告+Word导出
├── TaskTable.tsx # 任务列表表格(支持 partial_completed 状态)
├── TaskDetail.tsx # 任务详情(进度条+报告+Word导出+部分完成警告
├── EditorialReport.tsx # 稿约规范性报告
├── MethodologyReport.tsx # 方法学评估报告
├── AgentModal.tsx # 智能体选择弹窗
├── ForensicsReport.tsx # 数据验证报告(含 LLM 核查结果)
├── ClinicalReport.tsx # 🆕 V3.0 临床专业评估报告
├── AgentModal.tsx # 智能体选择弹窗4 个维度)
└── ScoreRing.tsx # 评分环组件
旧版本(保留兼容):
@@ -129,13 +139,17 @@ backend/src/legacy/
├── controllers/reviewController.ts
└── services/reviewService.ts
Prompt保持不变
Prompt文件 + PromptService 数据库双通道
backend/prompts/
├── review_editorial_system.txt # 稿约评估266行
└── review_methodology_system.txt # 方法学评估257行
数据库 prompt_templates 表(运营管理端可配置):
├── RVW_DATA_VALIDATION # 🆕 V3.0 数据验证 LLM 核查 Prompt
└── RVW_CLINICAL # 🆕 V3.0 临床专业评估 Prompt
数据库(✅ 已完成):
- ReviewTask表已添加新字段selectedAgents, editorialScore, methodologyScore, methodologyStatus, picoExtract, isArchived, archivedAt
- 🆕 V3.0 新增 error_details JSONB 字段Skill 级失败详情,支持 partial_completed 状态)
- ✅ Schema已迁移到 rvw_schema2026-01-10完成
```
@@ -211,11 +225,12 @@ backend/prompts/
| 能力 | 位置 | 用途 |
|------|------|------|
| **LLM网关** | `@/common/llm/LLMFactory` | AI评估 |
| **LLM网关** | `@/common/llm/LLMFactory` | AI评估(稿约/方法学/数据核查/临床评估) |
| **PromptService** | `@/common/prompt` | 🆕 V3.0 Prompt 管理(灰度预览、运营端配置) |
| **文档处理** | `ExtractionClient` | Word/PDF文本提取 |
| **存储** | `@/common/storage` | 文件存储 |
| **存储** | `@/common/storage` | 文件存储OSS/本地) |
| **日志** | `@/common/logging` | 结构化日志 |
| **任务队列** | `jobQueue` | 异步任务处理 |
| **任务队列** | `jobQueue` | 异步任务处理pg-boss |
### LLM模型
@@ -326,11 +341,13 @@ Content-Type: multipart/form-data
### 对新AI助手
1.**核心功能已完成**前后端已迁移到新架构,可正常使用
1.**V3.0 已完成**4 维审查 + 稳定性增强 + partial_completed 支持
2.**已集成到 frontend-v2**:通过顶部导航栏"预审稿"进入
3.**v2 API 已就绪**/api/v2/rvw/* 路由可用
4.**遵循云原生规范**:使用 logger 服务替代 console.log
5. ⚠️ **保留旧API**v1路由保持兼容支持旧前端
5. **PromptService 集成**RVW_DATA_VALIDATION + RVW_CLINICAL Prompt 可在运营管理端配置
6.**并行故障隔离**SkillExecutor 使用 Promise.allSettled单个 Skill 崩溃不影响其他
7. ⚠️ **保留旧API**v1路由保持兼容支持旧前端
### 已完成改造
@@ -414,9 +431,25 @@ Content-Type: multipart/form-data
- ✅ 前端翻译映射更新6 种新 IssueType
- ✅ 文件格式提示Header、ReportDetail、TaskDetail
### 后续版本V2.1+
### 🚀 V3.0 "智能审稿增强" 开发进度2026-03-07
- [ ] Week 4 功能测试和 Bug 修复
| 任务 | 状态 | 说明 |
|------|------|------|
| LLM 数据核查通道 | ✅ 已完成 | DataForensicsSkill 增加 LLM 验证,独立 60s 超时,规则验证兜底 |
| RVW_DATA_VALIDATION Prompt | ✅ 已完成 | PromptService 集成,运营管理端可配置 |
| 临床专业评估 Skill | ✅ 已完成 | ClinicalAssessmentSkill基于 FINER 标准 |
| RVW_CLINICAL Prompt | ✅ 已完成 | PromptService 集成,运营管理端可配置 |
| SkillExecutor Promise.allSettled | ✅ 已完成 | 并行 Skill 故障隔离,单个崩溃不影响其他 |
| partial_completed 状态 | ✅ 已完成 | 新增任务状态 + error_details JSONB 字段 |
| reviewWorker 写入 errorDetails | ✅ 已完成 | 记录每个失败/超时 Skill 的名称和原因 |
| 前端 4 Tab 报告 | ✅ 已完成 | 稿约规范性/方法学/数据验证/临床评估 |
| 前端 partial_completed UI | ✅ 已完成 | 琥珀色警告横幅 + 列表"部分完成"标签 |
| Word 导出覆盖临床评估 | ✅ 已完成 | 导出报告包含临床专业评估章节 |
### 后续版本V3.1+
- [ ] 全面移除评分机制(只列问题,不打分)
- [ ] 单模块重试机制partial_completed → 重试失败模块)
- [ ] ANOVA 验证(多组比较)
- [ ] 配对 T 检验验证
- [ ] 非参数检验Mann-Whitney、Wilcoxon
@@ -429,7 +462,7 @@ Content-Type: multipart/form-data
---
**文档版本:** v5.1
**最后更新:** 2026-02-18
**当前状态:** 🚀 V2.0 "数据侦探" Week 3 完成Skills 架构 + 统计验证 + 用户体验优化
**下一步:** Week 4 功能测试和 Bug 修复
**文档版本:** v6.0
**最后更新:** 2026-03-07
**当前状态:** 🚀 V3.0 "智能审稿增强" 完成LLM数据核查 + 临床评估 + 稳定性增强)
**下一步:** V3.1 移除评分机制 + 单模块重试

View File

@@ -15,19 +15,32 @@
| # | 变更内容 | 迁移文件 | 优先级 | 备注 |
|---|---------|---------|--------|------|
| — | *暂无* | | | |
| DB-1 | modules 表 seed 新增 ASL_SR 模块(系统综述项目) | `backend/scripts/seed-modules.js` | 高 | 部署后需执行 `node scripts/seed-modules.js`,并在运营管理端为目标用户/租户开通 |
| DB-2 | prompt_templates 表新增 RVW_DATA_VALIDATION + RVW_CLINICAL 两个 Prompt | `backend/scripts/migrate-rvw-prompts.ts` | 高 | 部署后需执行 `npx tsx scripts/migrate-rvw-prompts.ts`,运营管理端可配置修改 |
| DB-3 | ReviewTask 表新增 `error_details` JSONB 字段(存储 Skill 级失败详情) | `prisma/migrations/20260307_add_error_details_to_review_task/migration.sql` | 高 | 支持 partial_completed 状态,记录每个失败/超时 Skill 的名称和原因 |
### 后端变更 (Node.js)
| # | 变更内容 | 涉及文件 | 需要操作 | 备注 |
|---|---------|---------|---------|------|
| — | *暂无* | | | |
| BE-1 | Deep Research V2.0 新增历史列表 + 删除接口 + getTask 鉴权修复 | `deepResearchController.ts`, `routes/index.ts` | 重新构建镜像 | GET /research/v2/tasks, DELETE /research/tasks/:taskId, getTask 增加 userId 校验 |
| BE-2 | SR 相关路由projects/literatures/screening/fulltext-screening/extraction/charting/meta-analysis增加 `requireModule('ASL_SR')` 中间件 | `asl/routes/index.ts`, `extraction/routes/index.ts`, `charting/routes/index.ts`, `meta-analysis/routes/index.ts` | 重新构建镜像 | 需先完成 DB-1 seed否则无 ASL_SR 模块会 403 |
| BE-3 | Unifuncs DeepSearch API 从 S2 升级至 S3新增 `language: "zh"` | `unifuncsSseClient.ts`, `unifuncsAsyncClient.ts` | 重新构建镜像 | 通过 `UNIFUNCS_MODEL` 环境变量控制,默认 s3设为 s2 可降级 |
| BE-4 | RVW 数据验证增加 LLM 核查通道DataForensicsSkill 增强) | `DataForensicsSkill.ts`, `prompt.fallbacks.ts` | 重新构建镜像 | 规则验证完成后批量调用 LLM 核查所有表格,失败时降级为纯规则验证 |
| BE-5 | RVW 新增临床专业评估维度ClinicalAssessmentSkill | `clinicalService.ts`(新), `ClinicalAssessmentSkill.ts`(新), `library/index.ts`, `profile.ts`, `utils.ts`, `reviewWorker.ts`, `reviewService.ts` | 重新构建镜像 | 新增 clinical Agent + Skill存储在 contextData.clinicalReview |
| BE-6 | RVW 稳定性增强SkillExecutor Promise.allSettled + partial_completed 状态 + errorDetails | `executor.ts`, `reviewWorker.ts`, `reviewService.ts`, `reviewController.ts`, `types/index.ts` | 重新构建镜像 | 并行 Skill 故障隔离,部分模块失败时仍返回成功模块结果,新增 `partial_completed` 任务状态 |
| BE-7 | DataForensicsSkill LLM 核查增加独立 60s 超时 | `DataForensicsSkill.ts` | 重新构建镜像 | LLM 核查超时不阻塞整体 Skillgraceful 降级为纯规则验证 |
### 前端变更
| # | 变更内容 | 涉及文件 | 需要操作 | 备注 |
|---|---------|---------|---------|------|
| — | *暂无* | | | |
| FE-1 | ASL 左侧导航栏重构为互斥手风琴Deep Research 历史记录 + SR 工具导航) | `ASLLayout.tsx`, `asl-sidebar.css`(新建), `DeepResearchPage.tsx`, `asl/index.tsx` | 重新构建镜像 | Panel A: 智能文献检索历史Panel B: 系统综述项目5 工具) |
| FE-2 | Deep Research 历史记录功能API 客户端 + 类型定义 + URL 任务恢复) | `asl/api/index.ts`, `types/deepResearch.ts`, `DeepResearchPage.tsx`, `asl/index.tsx` | 重新构建镜像 | 新增 listDeepResearchTasks / deleteDeepResearchTask API新增 /research/deep/:taskId 路由 |
| FE-3 | Panel B SR 工具导航权限控制hasModule('ASL_SR') | `ASLLayout.tsx`, `asl-sidebar.css` | 重新构建镜像 | 未开通时显示"请联系管理员开通";已开通显示 5 个 SR 工具导航项 |
| FE-4 | RVW 数据验证报告增加 LLM 核查结果展示 | `ForensicsReport.tsx`, `rvw/types/index.ts` | 重新构建镜像 | 总览展示完整 LLM 报告,每个表格卡片展开后显示对应 AI 核查结果Markdown |
| FE-5 | RVW 新增临床专业评估 Tab + Agent 选择项 | `ClinicalReport.tsx`(新), `AgentModal.tsx`, `TaskDetail.tsx`, `rvw/types/index.ts` | 重新构建镜像 | 共 4 个 Tab稿约规范性/方法学/数据验证/临床评估Word 导出包含临床评估章节 |
| FE-6 | RVW 前端支持 partial_completed 状态(部分完成) | `TaskDetail.tsx`, `TaskTable.tsx`, `rvw/types/index.ts` | 重新构建镜像 | 琥珀色警告横幅展示失败模块详情,列表页显示"部分完成"标签,支持查看已完成模块的报告 |
### Python 微服务变更
@@ -45,7 +58,7 @@
| # | 变更内容 | 服务 | 变量名 | 备注 |
|---|---------|------|--------|------|
| — | *暂无* | | | |
| ENV-1 | Unifuncs 模型版本控制(可选,不配置则默认 s3 | nodejs-backend | `UNIFUNCS_MODEL=s3` | 降级时改为 `s2`,无需重新部署代码 |
### 基础设施变更

View File

@@ -0,0 +1,197 @@
# 2026年3月5日部署完成总结
> **部署日期**2026-03-05
> **部署范围**数据库数据更新1项 + Node.js后端 + 前端Nginx + R统计引擎待部署
> **部署状态**:✅ 后端/前端已完成
> **文档日期**2026-03-05
---
## 部署成果一览
### 服务版本对比
| 服务 | 部署前 | 部署后 | 变更类型 |
|------|--------|--------|---------|
| PostgreSQLRDS | 86 表 | 86 表(数据更新) | modules 表 seed 更新 |
| Node.js后端 | v2.4 | **v2.6** | 登录踢人 + 权限体系 + SSA双通道 |
| 前端Nginx | v2.0 | **v2.3** | UI优化 + 权限适配 + 批量导入重构 |
### 内网地址变更
| 服务 | 部署前地址 | 部署后地址 | 状态 |
|------|-----------|-----------|------|
| Node.js后端 | `172.17.197.32:3001` | `172.17.197.36:3001` | ✅ 已变更 |
| 前端Nginx | `172.17.197.32:80` | `172.17.173.104:80` | ✅ 已变更 |
---
## 一、数据库更新
### 1.1 DB-1modules 表数据更新
通过 `node scripts/seed-modules.js` 连接 RDS 外网执行upsert 幂等操作):
| 操作 | 模块代码 | 名称 | 说明 |
|------|---------|------|------|
| 新增 | `RM` | 研究管理 | 排序 9 |
| 新增 | `AIA_PROTOCOL` | 全流程研究方案制定 | 排序 100 |
| 更新 | `IIT` | CRA质控 | 原名 IIT Manager |
执行结果modules 表共 10 个模块,全部上线 ✅
### 1.2 未执行项
| 项目 | 原因 |
|------|------|
| DB-2RVW Prompt 更新 | 用户指定不执行 |
| DB-3SSA 双通道表结构 | 待后续部署 |
---
## 二、Node.js后端更新v2.4 → v2.6
### 2.1 主要变更9 项)
| 类别 | 变更内容 |
|------|---------|
| 登录安全 | 同一手机号登录踢人机制JWT tokenVersion + 缓存校验) |
| 权限体系 | `/me/modules` API 尊重 user_modules 精细化配置 |
| 权限体系 | 用户模块配置校验放宽(模块代码存在即可,不限租户订阅) |
| 权限体系 | user_modules 独立生效(如 AIA_PROTOCOL 可单独配给用户) |
| 模块名称 | getModuleName 补充 RM、AIA_PROTOCOL、IIT→CRA质控 |
| RVW | 稿约 Prompt 源文件期刊名称修正 |
| Seed | 内部租户补充 RM、AIA_PROTOCOL 模块 |
| SSA | 双通道架构Agent 模式 4 服务 + ChatHandler 分流 |
| 用户管理 | 批量导入增加 autoInheritModules + 模块校验 |
### 2.2 镜像信息
| 项目 | 值 |
|------|---|
| ACR 仓库 | `backend-service` |
| 镜像版本 | v2.4 → **v2.6** |
| Digest | `sha256:17dc3b3b6171bad891b0d366a22e1b52d79db7fc9caccedf816a7feab4cea449` |
| 内网地址 | `http://172.17.197.36:3001` |
---
## 三、前端Nginx更新v2.0 → v2.3
### 3.1 主要变更11 项)
| 类别 | 变更内容 |
|------|---------|
| ASL | 隐藏数据源/年限/篇数 + 去掉研究方案生成/文献管理 + 默认进入智能文献检索 |
| AIA | 删除「已接入DeepSeek」和搜索框 + Protocol Agent 按权限动态显示 |
| AIA | 数据评价与预处理/智能统计分析链接修正 |
| 首页 | 重定向到 `/ai-qa`,不再显示模块卡片首页 |
| PKB | 创建知识库时隐藏科室选择,默认 General |
| 安全 | 被踢出时提示「账号已在其他设备登录」 |
| 运营端 | 用户模块权限弹窗显示所有模块(含未订阅标注) |
| 运营端 | 批量导入用户重构为 4 步流程 + 自动继承租户模块 |
### 3.2 镜像信息
| 项目 | 值 |
|------|---|
| ACR 仓库 | `ai-clinical_frontend-nginx` |
| 镜像版本 | v2.0 → **v2.3** |
| Digest | `sha256:db031053d8ac50d8f2ce39a8406534743d974a5506b7d4af5a944dd145ce8589` |
| 内网地址 | `http://172.17.173.104:80` |
---
## 四、环境变量联动更新
| 服务 | 环境变量 | 旧值 | 新值 |
|------|---------|------|------|
| frontend-nginx-service | `BACKEND_SERVICE_HOST` | `172.17.197.32` | `172.17.197.36` |
> CLB 负载均衡器由阿里云自动更新,无需手动操作。
---
## 五、待后续部署项
| 项目 | 说明 |
|------|------|
| DB-3 | SSA 双通道表结构ssa_sessions + ssa_agent_executions |
| R-1 | R 统计引擎新增 execute-code 端点(需重构 R 镜像) |
---
## 六、当前系统配置速查
### 服务内网地址
```
R统计引擎: http://172.17.173.101:8080 (不变)
Python: http://172.17.173.102:8000 (不变)
后端: http://172.17.197.36:3001 (更新)
前端: http://172.17.173.104:80 (更新)
```
### ACR 镜像版本
| 仓库 | 版本 |
|------|-----|
| `ssa-r-statistics` | v1.0.1 |
| `python-extraction` | v1.2 |
| `backend-service` | **v2.6** |
| `ai-clinical_frontend-nginx` | **v2.3** |
### 公网访问
```
CLB: http://8.140.53.236/
域名: https://iit.xunzhengyixue.com/
```
---
---
## 七、二次热修部署(同日)
### 7.1 触发原因
- SSA 智能统计分析上传文件报错:`The column execution_mode does not exist in the current database`
- 前端/后端其他 bug 修复(基于测试反馈)
### 7.2 数据库热修
| 操作 | 内容 | 状态 |
|------|------|------|
| ALTER TABLE | `ssa_sessions` 新增 `execution_mode` 列(默认 `'qper'` | ✅ |
| CREATE TABLE | `ssa_agent_executions` 表 + 索引 + 外键 | ✅ |
| INSERT | Prisma 迁移记录同步注册 | ✅ |
### 7.3 镜像重构与部署
| 服务 | 版本变化 | 镜像 Digest | IP 变化 |
|------|---------|-------------|---------|
| Node.js 后端 | v2.6 → **v2.7** | `sha256:bfb7d3e6ce39...` | `172.17.197.36``172.17.197.37` |
| 前端 Nginx | v2.3 → **v2.4** | `sha256:03fab06fb1a2...` | `172.17.173.104``172.17.173.105` |
### 7.4 最终系统配置
```
后端: http://172.17.197.37:3001
前端: http://172.17.173.105:80
R统计引擎: http://172.17.173.101:8080
Python: http://172.17.173.102:8000
```
| ACR 仓库 | 最终版本 |
|----------|---------|
| `backend-service` | **v2.7** |
| `ai-clinical_frontend-nginx` | **v2.4** |
| `ssa-r-statistics` | v1.0.1 |
| `python-extraction` | v1.2 |
---
> **文档版本**v1.1
> **最后更新**2026-03-05
> **维护人员**:开发团队