Files
AIclinicalresearch/frontend-v2/src/modules/rvw/pages/Dashboard.tsx
HaHafeng 61cdc97eeb feat(platform): Fix pg-boss queue conflict and add safety standards
Summary:
- Fix pg-boss queue conflict (duplicate key violation on queue_pkey)
- Add global error listener to prevent process crash
- Reduce connection pool from 10 to 4
- Add graceful shutdown handling (SIGTERM/SIGINT)
- Fix researchWorker recursive call bug in catch block
- Make screeningWorker idempotent using upsert

Security Standards (v1.1):
- Prohibit recursive retry in Worker catch blocks
- Prohibit payload bloat (only store fileKey/ID in job.data)
- Require Worker idempotency (upsert + unique constraint)
- Recommend task-specific expireInSeconds settings
- Document graceful shutdown pattern

New Features:
- PKB signed URL endpoint for document preview/download
- pg_bigm installation guide for Docker
- Dockerfile.postgres-with-extensions for pgvector + pg_bigm

Documentation:
- Update Postgres-Only async task processing guide (v1.1)
- Add troubleshooting SQL queries
- Update safety checklist

Tested: Local verification passed
2026-01-23 22:07:26 +08:00

305 lines
9.0 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审稿系统 - 主Dashboard页面
*/
import { useState, useEffect, useCallback } from 'react';
import { message } from 'antd';
import {
Sidebar,
Header,
FilterChips,
TaskTable,
BatchToolbar,
AgentModal,
ReportDetail,
TaskDetail,
} from '../components';
import * as api from '../api';
import type { ReviewTask, ReviewReport, TaskFilters, AgentType } from '../types';
import '../styles/index.css';
export default function Dashboard() {
// ==================== State ====================
const [currentView, setCurrentView] = useState<'dashboard' | 'archive'>('dashboard');
const [tasks, setTasks] = useState<ReviewTask[]>([]);
const [loading, setLoading] = useState(true);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [filters, setFilters] = useState<TaskFilters>({ status: 'all', timeRange: 'all' });
const [agentModalVisible, setAgentModalVisible] = useState(false);
const [pendingTaskForRun, setPendingTaskForRun] = useState<ReviewTask | null>(null);
// 报告详情
const [reportDetail, setReportDetail] = useState<ReviewReport | null>(null);
// 任务详情(支持进度显示)
const [viewingTask, setViewingTask] = useState<ReviewTask | null>(null);
const [currentJobId, setCurrentJobId] = useState<string | null>(null);
// ==================== 数据加载 ====================
const loadTasks = useCallback(async () => {
try {
setLoading(true);
const data = await api.getTasks(filters.status !== 'all' ? filters.status : undefined);
// 时间筛选
let filtered = data;
if (filters.timeRange === 'today') {
const today = new Date().toDateString();
filtered = data.filter(t => new Date(t.createdAt).toDateString() === today);
} else if (filters.timeRange === 'week') {
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
filtered = data.filter(t => new Date(t.createdAt).getTime() > weekAgo);
}
setTasks(filtered);
} catch (error) {
console.error('加载任务失败:', error);
message.error('加载任务列表失败');
} finally {
setLoading(false);
}
}, [filters]);
useEffect(() => {
loadTasks();
}, [loadTasks]);
// 轮询更新进行中的任务
useEffect(() => {
const processingTasks = tasks.filter(t =>
t.status === 'extracting' || t.status === 'reviewing'
);
if (processingTasks.length === 0) return;
const interval = setInterval(async () => {
for (const task of processingTasks) {
try {
const updated = await api.getTask(task.id);
setTasks(prev => prev.map(t => t.id === updated.id ? updated : t));
} catch (error) {
console.error('更新任务状态失败:', error);
}
}
}, 3000);
return () => clearInterval(interval);
}, [tasks]);
// ==================== 统计数据 ====================
const counts = {
all: tasks.length,
pending: tasks.filter(t => t.status === 'pending').length,
completed: tasks.filter(t => t.status === 'completed').length,
};
// ==================== 事件处理 ====================
const handleUpload = async (files: FileList) => {
const uploadPromises = Array.from(files).map(async (file) => {
try {
message.loading({ content: `正在上传 ${file.name}...`, key: file.name });
await api.uploadManuscript(file);
message.success({ content: `${file.name} 上传成功`, key: file.name, duration: 2 });
} catch (error: any) {
message.error({ content: `${file.name} 上传失败: ${error.message}`, key: file.name, duration: 3 });
}
});
await Promise.all(uploadPromises);
loadTasks();
};
const handleRunTask = (task: ReviewTask) => {
setPendingTaskForRun(task);
setAgentModalVisible(true);
};
const handleRunBatch = () => {
setPendingTaskForRun(null); // 批量模式
setAgentModalVisible(true);
};
const handleConfirmRun = async (agents: AgentType[]) => {
// 🔥 保存到局部变量避免onClose后丢失
const taskToRun = pendingTaskForRun;
// 立即关闭弹窗
setAgentModalVisible(false);
setPendingTaskForRun(null);
try {
if (taskToRun) {
// 单个任务 - 启动后跳转到详情页显示进度
message.loading({ content: '正在启动审查...', key: 'run' });
const { jobId } = await api.runTask(taskToRun.id, agents);
message.success({ content: '审查已启动', key: 'run', duration: 2 });
// 更新任务状态后跳转到详情页传递jobId
const updatedTask = await api.getTask(taskToRun.id);
setCurrentJobId(jobId);
setViewingTask(updatedTask);
return;
} else {
// 批量任务
const pendingIds = selectedIds.filter(id => {
const task = tasks.find(t => t.id === id);
return task && task.status === 'pending';
});
if (pendingIds.length === 0) {
message.warning('没有待处理的任务');
return;
}
message.loading({ content: `正在启动 ${pendingIds.length} 个任务...`, key: 'run' });
await api.batchRunTasks(pendingIds, agents);
message.success({ content: `${pendingIds.length} 个任务已启动`, key: 'run', duration: 2 });
setSelectedIds([]);
}
loadTasks();
} catch (error: any) {
message.error({ content: error.message || '启动失败', key: 'run', duration: 3 });
}
};
const handleViewReport = async (task: ReviewTask) => {
// 直接使用TaskDetail视图支持进度和报告
setViewingTask(task);
};
const handleDeleteTask = async (task: ReviewTask) => {
if (!window.confirm(`确定要删除 "${task.fileName}" 吗?`)) {
return;
}
try {
message.loading({ content: '正在删除...', key: 'delete' });
await api.deleteTask(task.id);
message.success({ content: '删除成功', key: 'delete', duration: 2 });
loadTasks();
} catch (error: any) {
message.error({ content: error.message || '删除失败', key: 'delete', duration: 3 });
}
};
const handleBackToList = () => {
setReportDetail(null);
};
// 返回列表并刷新
const handleBackFromDetail = () => {
setViewingTask(null);
setCurrentJobId(null);
loadTasks();
};
// ==================== 渲染 ====================
// 任务详情视图(支持进度显示)
if (viewingTask) {
return (
<div className="h-full flex overflow-hidden bg-slate-50">
<Sidebar
currentView={currentView}
onViewChange={setCurrentView}
/>
<TaskDetail task={viewingTask} jobId={currentJobId} onBack={handleBackFromDetail} />
</div>
);
}
// 报告详情视图(旧版,保留兼容)
if (reportDetail) {
return (
<div className="h-full flex overflow-hidden bg-slate-50">
<Sidebar
currentView={currentView}
onViewChange={setCurrentView}
/>
<ReportDetail report={reportDetail} onBack={handleBackToList} />
</div>
);
}
// 主仪表盘视图
return (
<div className="h-full flex overflow-hidden bg-slate-50">
<Sidebar
currentView={currentView}
onViewChange={setCurrentView}
/>
<main className="flex-1 flex flex-col min-w-0 bg-white">
<div className="flex-1 flex flex-col h-full relative fade-in">
{/* 顶部操作区 */}
<header className="bg-white px-8 pt-6 pb-4 border-b border-gray-100 flex-shrink-0 z-10">
<Header onUpload={handleUpload} />
<FilterChips
filters={filters}
counts={counts}
onFilterChange={setFilters}
/>
</header>
{/* 列表区域 */}
<div className="flex-1 overflow-auto bg-slate-50/50 p-6">
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600" />
</div>
) : (
<TaskTable
tasks={tasks}
selectedIds={selectedIds}
onSelectChange={setSelectedIds}
onViewReport={handleViewReport}
onRunTask={handleRunTask}
onDeleteTask={handleDeleteTask}
/>
)}
</div>
</div>
</main>
{/* 批量操作工具栏 */}
<BatchToolbar
selectedCount={selectedIds.length}
onRunBatch={handleRunBatch}
onClearSelection={() => setSelectedIds([])}
/>
{/* 智能体选择弹窗 */}
<AgentModal
visible={agentModalVisible}
taskCount={pendingTaskForRun ? 1 : selectedIds.length}
onClose={() => {
setAgentModalVisible(false);
setPendingTaskForRun(null);
}}
onConfirm={handleConfirmRun}
/>
</div>
);
}