diff --git a/backend/src/modules/rvw/controllers/reviewController.ts b/backend/src/modules/rvw/controllers/reviewController.ts index 8c3c6257..e822cfea 100644 --- a/backend/src/modules/rvw/controllers/reviewController.ts +++ b/backend/src/modules/rvw/controllers/reviewController.ts @@ -315,6 +315,11 @@ export async function getTaskDetail( logger.info('[RVW:Controller] 获取任务详情', { taskId }); const task = await reviewService.getTaskDetail(userId, taskId); + const contextData = task.contextData as { + forensicsResult?: unknown; + clinicalReview?: unknown; + skillProgress?: Record; + } | null; // 🆕 直接使用新字段 return reply.send({ @@ -336,6 +341,11 @@ export async function getTaskDetail( durationSeconds: task.durationSeconds, errorMessage: task.errorMessage, errorDetails: task.errorDetails ?? undefined, + editorialReview: task.editorialReview ?? undefined, + methodologyReview: task.methodologyReview ?? undefined, + forensicsResult: contextData?.forensicsResult ?? undefined, + clinicalReview: contextData?.clinicalReview ?? undefined, + reviewProgress: contextData?.skillProgress ?? undefined, }, }); } catch (error) { diff --git a/backend/src/modules/rvw/skills/core/executor.ts b/backend/src/modules/rvw/skills/core/executor.ts index 430a1c4c..cc13bbd5 100644 --- a/backend/src/modules/rvw/skills/core/executor.ts +++ b/backend/src/modules/rvw/skills/core/executor.ts @@ -125,45 +125,49 @@ export class SkillExecutor { taskId: context.taskId, skillIds: stage.map(s => s.skillId), }); + const stageConcurrency = Math.max(1, profile.globalConfig?.maxConcurrency ?? stage.length); + for (let offset = 0; offset < stage.length; offset += stageConcurrency) { + const chunk = stage.slice(offset, offset + stageConcurrency); + const promises = chunk.map(item => this.executePipelineItem(item, context, profile)); + const settled = await Promise.allSettled(promises); - const promises = stage.map(item => this.executePipelineItem(item, context, profile)); - const settled = await Promise.allSettled(promises); - - for (let i = 0; i < stage.length; i++) { - const outcome = settled[i]; - if (outcome.status === 'fulfilled') { - const result = outcome.value; - if (result) { - results.push(result); - context.previousResults.push(result); - const skill = SkillRegistry.get(stage[i].skillId); - if (skill) this.updateContextWithResult(context, skill, result); + for (let i = 0; i < chunk.length; i++) { + const outcome = settled[i]; + const currentItem = chunk[i]; + if (outcome.status === 'fulfilled') { + const result = outcome.value; + if (result) { + results.push(result); + context.previousResults.push(result); + const skill = SkillRegistry.get(currentItem.skillId); + if (skill) this.updateContextWithResult(context, skill, result); + } + } else { + // Promise 本身 rejected — 极端情况下的兜底 + const errorMessage = outcome.reason instanceof Error + ? outcome.reason.message + : String(outcome.reason); + logger.error('[SkillExecutor] Parallel skill promise rejected (uncaught)', { + skillId: currentItem.skillId, + taskId: context.taskId, + error: errorMessage, + }); + const now = new Date(); + results.push({ + skillId: currentItem.skillId, + skillName: currentItem.skillId, + status: 'error', + issues: [{ + severity: 'ERROR', + type: SkillErrorCodes.SKILL_EXECUTION_ERROR, + message: `${currentItem.skillId} 执行异常: ${errorMessage}`, + }], + error: errorMessage, + executionTime: 0, + startedAt: now, + completedAt: now, + }); } - } else { - // Promise 本身 rejected — 极端情况下的兜底 - const errorMessage = outcome.reason instanceof Error - ? outcome.reason.message - : String(outcome.reason); - logger.error('[SkillExecutor] Parallel skill promise rejected (uncaught)', { - skillId: stage[i].skillId, - taskId: context.taskId, - error: errorMessage, - }); - const now = new Date(); - results.push({ - skillId: stage[i].skillId, - skillName: stage[i].skillId, - status: 'error', - issues: [{ - severity: 'ERROR', - type: SkillErrorCodes.SKILL_EXECUTION_ERROR, - message: `${stage[i].skillId} 执行异常: ${errorMessage}`, - }], - error: errorMessage, - executionTime: 0, - startedAt: now, - completedAt: now, - }); } } } diff --git a/backend/src/modules/rvw/skills/core/profile.ts b/backend/src/modules/rvw/skills/core/profile.ts index 17869ba5..7164db45 100644 --- a/backend/src/modules/rvw/skills/core/profile.ts +++ b/backend/src/modules/rvw/skills/core/profile.ts @@ -29,6 +29,7 @@ export const DEFAULT_PROFILE: JournalProfile = { tolerancePercent: 0.1, }, timeout: 300000, // 5min: Python + LLM核查(内部180s超时降级) + 长文档余量 + parallelGroup: 'llm-review', // 与其余模块并行,缩短总时长 }, { skillId: 'EditorialSkill', @@ -57,6 +58,7 @@ export const DEFAULT_PROFILE: JournalProfile = { strictness: 'STANDARD', continueOnError: true, timeoutMultiplier: 1.0, + maxConcurrency: 4, // 受控并行:默认同时跑4个模块 }, }; diff --git a/backend/src/modules/rvw/workers/reviewWorker.ts b/backend/src/modules/rvw/workers/reviewWorker.ts index 2c6f2f7a..892d75ea 100644 --- a/backend/src/modules/rvw/workers/reviewWorker.ts +++ b/backend/src/modules/rvw/workers/reviewWorker.ts @@ -34,6 +34,7 @@ import { createPartialContextFromTask, registerBuiltinSkills, ExecutionSummary, + SkillResult, } from '../skills/index.js'; /** @@ -307,6 +308,7 @@ export async function registerReviewWorker() { }, forensicsResult: skillsSummary.results.find(r => r.skillId === 'DataForensicsSkill')?.data, clinicalReview: clinicalResult, + skillProgress: buildSkillProgressMap(skillsSummary.results), } : null; @@ -439,6 +441,19 @@ function buildErrorDetails(summary: ExecutionSummary): Record { }; } +function buildSkillProgressMap(results: SkillResult[]): Record { + const progress: Record = {}; + for (const item of results) { + progress[item.skillId] = { + status: item.status, + executionTime: item.executionTime, + completedAt: item.completedAt.toISOString(), + error: item.error || null, + }; + } + return progress; +} + /** * 使用 V2.0 Skills 架构执行审查 */ @@ -480,9 +495,73 @@ async function executeWithSkills( fileSize, }); + const currentTask = await prisma.reviewTask.findUnique({ + where: { id: taskId }, + select: { contextData: true }, + }); + const incrementalContext = + ((currentTask?.contextData as Record | null) || {}); + const runningContext: Record = { ...incrementalContext }; + const skillProgress = + ((incrementalContext.skillProgress as Record | undefined) || {}); + let persistQueue: Promise = Promise.resolve(); + + const persistSkillResult = (result: SkillResult) => { + persistQueue = persistQueue.then(async () => { + skillProgress[result.skillId] = { + status: result.status, + executionTime: result.executionTime, + completedAt: result.completedAt.toISOString(), + error: result.error || null, + }; + + const nextContext: Record = { + ...runningContext, + skillProgress: { ...skillProgress }, + }; + + const updateData: Record = { + contextData: nextContext as Prisma.InputJsonValue, + }; + + if (result.skillId === 'DataForensicsSkill' && result.data) { + nextContext.forensicsResult = result.data; + } + if (result.skillId === 'ClinicalAssessmentSkill' && result.data) { + nextContext.clinicalReview = result.data; + } + if (result.skillId === 'EditorialSkill' && result.data) { + const editorialData = result.data as EditorialReview; + updateData.editorialReview = editorialData as unknown as Prisma.InputJsonValue; + updateData.editorialScore = editorialData.overall_score ?? null; + } + if (result.skillId === 'MethodologySkill' && result.data) { + const methodologyData = result.data as MethodologyReview; + updateData.methodologyReview = methodologyData as unknown as Prisma.InputJsonValue; + updateData.methodologyScore = methodologyData.overall_score ?? null; + updateData.methodologyStatus = getMethodologyStatus(methodologyData); + } + + updateData.contextData = nextContext as Prisma.InputJsonValue; + Object.assign(runningContext, nextContext); + + await prisma.reviewTask.update({ + where: { id: taskId }, + data: updateData as Prisma.ReviewTaskUpdateInput, + }); + }); + + return persistQueue; + }; + // 执行 Pipeline - const executor = new SkillExecutor(); + const executor = new SkillExecutor({ + onSkillComplete: async (_skillId, result) => { + await persistSkillResult(result); + }, + }); const summary = await executor.execute(profile, partialContext); + await persistQueue; // 输出执行结果 console.log(`\n 📊 Skills 执行结果:`); diff --git a/docs/03-业务模块/RVW-稿件审查系统/00-模块当前状态与开发指南.md b/docs/03-业务模块/RVW-稿件审查系统/00-模块当前状态与开发指南.md index ff0f509c..886df168 100644 --- a/docs/03-业务模块/RVW-稿件审查系统/00-模块当前状态与开发指南.md +++ b/docs/03-业务模块/RVW-稿件审查系统/00-模块当前状态与开发指南.md @@ -1,10 +1,10 @@ # RVW稿件审查模块 - 当前状态与开发指南 -> **文档版本:** v6.0 +> **文档版本:** v6.1 > **创建日期:** 2026-01-07 -> **最后更新:** 2026-03-07 +> **最后更新:** 2026-03-10 > **维护者:** 开发团队 -> **当前状态:** 🚀 **V3.0 "智能审稿增强" 完成(LLM数据核查 + 临床评估 + 稳定性增强)** +> **当前状态:** 🚀 **V3.0.1 "性能与体验增强" 完成(4模块并行 + 增量展示 + 导出补全)** > **文档目的:** 快速了解RVW模块状态,为新AI助手提供上下文 > > **🎉 V3.0 进展(2026-03-07):** @@ -14,6 +14,12 @@ > - ✅ **稳定性增强**:SkillExecutor 使用 Promise.allSettled 实现并行故障隔离 > - ✅ **部分完成支持**:新增 `partial_completed` 状态 + `errorDetails` 字段,部分模块失败仍展示成功结果 > - ✅ **前端 4 Tab 报告**:稿约规范性 / 方法学 / 数据验证 / 临床评估,Word 导出全覆盖 +> +> **⚡ V3.0.1 增强(2026-03-10):** +> - ✅ **4模块受控并行**:DataForensics 与 Editorial/Methodology/Clinical 同组并行,`maxConcurrency=4` 控制并发上限 +> - ✅ **增量结果持久化**:每个 Skill 完成即写入任务中间结果,`getTaskDetail` 返回模块级 `reviewProgress` +> - ✅ **先出先看**:TaskDetail 在审查中即可展示已完成模块(无需等待全流程结束) +> - ✅ **Word 导出修复**:补齐“数据验证”章节,导出汇总 + 表格明细 + 该表问题列表 > > **V2.0 进展回顾:** > - ✅ L1 算术验证 + L2 统计验证 + L2.5 一致性取证 @@ -34,7 +40,7 @@ | **商业价值** | ⭐⭐⭐⭐⭐ 极高 | | **独立性** | ⭐⭐⭐⭐⭐ 极高(用户群完全不同) | | **目标用户** | 期刊初审编辑 | -| **开发状态** | ✅ **V3.0 完成:4维审查(规范性+方法学+数据验证+临床评估)+ 稳定性增强 + Word导出** | +| **开发状态** | ✅ **V3.0.1 完成:4维审查并行提速 + 增量结果展示 + Word导出补全** | ### 核心目标 @@ -105,7 +111,7 @@ backend/src/modules/rvw/ │ ├── clinicalService.ts # 🆕 V3.0 临床专业评估服务 │ └── utils.ts # 工具函数 ├── workers/ -│ └── reviewWorker.ts # pg-boss异步任务处理(V2.0 Skills集成 + V3.0 partial_completed) +│ └── reviewWorker.ts # pg-boss异步任务处理(V2.0 Skills集成 + V3.0 partial_completed + V3.0.1 增量落库) ├── skills/ # V2.0 Skills 架构 │ ├── core/ # 核心框架(types, registry, executor[allSettled]等) │ ├── library/ # Skill 实现(Forensics[+LLM], Editorial, Methodology, 🆕Clinical) @@ -125,7 +131,7 @@ frontend-v2/src/modules/rvw/ ├── Header.tsx # 页头(上传按钮) ├── Sidebar.tsx # 侧边栏导航 ├── TaskTable.tsx # 任务列表表格(支持 partial_completed 状态) - ├── TaskDetail.tsx # 任务详情(进度条+报告+Word导出+部分完成警告) + ├── TaskDetail.tsx # 任务详情(进度条+增量展示+Word导出+部分完成警告) ├── EditorialReport.tsx # 稿约规范性报告 ├── MethodologyReport.tsx # 方法学评估报告 ├── ForensicsReport.tsx # 数据验证报告(含 LLM 核查结果) @@ -446,6 +452,17 @@ Content-Type: multipart/form-data | 前端 partial_completed UI | ✅ 已完成 | 琥珀色警告横幅 + 列表"部分完成"标签 | | Word 导出覆盖临床评估 | ✅ 已完成 | 导出报告包含临床专业评估章节 | +### ⚡ V3.0.1 "性能与体验增强" 开发进度(2026-03-10) + +| 任务 | 状态 | 说明 | +|------|------|------| +| DataForensics 并入并行组 | ✅ 已完成 | 与 Editorial/Methodology/Clinical 同组并行执行 | +| 并发上限控制 | ✅ 已完成 | Executor 支持按 `maxConcurrency` 分批并行 | +| 模块完成即持久化 | ✅ 已完成 | Worker 通过 `onSkillComplete` 增量写入 `contextData` | +| 任务详情增量返回 | ✅ 已完成 | `getTaskDetail` 返回模块结果与 `reviewProgress` | +| 前端先出先看 | ✅ 已完成 | 审查过程中实时展示已完成 Tab | +| Word 导出补齐数据验证 | ✅ 已完成 | 导出包含数据验证汇总、表格明细、该表问题列表 | + ### 后续版本(V3.1+) - [ ] 全面移除评分机制(只列问题,不打分) @@ -462,7 +479,7 @@ Content-Type: multipart/form-data --- -**文档版本:** v6.0 -**最后更新:** 2026-03-07 -**当前状态:** 🚀 V3.0 "智能审稿增强" 完成(LLM数据核查 + 临床评估 + 稳定性增强) +**文档版本:** v6.1 +**最后更新:** 2026-03-10 +**当前状态:** 🚀 V3.0.1 "性能与体验增强" 完成(4模块并行 + 增量展示 + 导出补全) **下一步:** V3.1 移除评分机制 + 单模块重试 diff --git a/frontend-v2/src/modules/rvw/components/TaskDetail.tsx b/frontend-v2/src/modules/rvw/components/TaskDetail.tsx index 5c4a5b69..c295c36a 100644 --- a/frontend-v2/src/modules/rvw/components/TaskDetail.tsx +++ b/frontend-v2/src/modules/rvw/components/TaskDetail.tsx @@ -39,6 +39,7 @@ const getProgressSteps = (selectedAgents: string[]) => { const steps = [ { key: 'upload', label: '上传文档' }, { key: 'extract', label: '文本提取' }, + { key: 'forensics', label: '数据验证' }, ]; if (selectedAgents.includes('editorial')) { @@ -67,6 +68,17 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet const isCompleted = task.status === 'completed' || task.status === 'partial_completed'; const isPartial = task.status === 'partial_completed'; const isFailed = task.status === 'failed'; + const hasIncrementalResult = + !!task.editorialReview || !!task.methodologyReview || !!task.forensicsResult || !!task.clinicalReview; + const displayReport: ReviewReport | null = report || (hasIncrementalResult + ? { + ...task, + editorialReview: task.editorialReview, + methodologyReview: task.methodologyReview, + forensicsResult: task.forensicsResult, + clinicalReview: task.clinicalReview, + } + : null); // 轮询任务状态 useEffect(() => { @@ -110,20 +122,20 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet } }, [isCompleted, task.id, report]); - // 报告加载后自动设置正确的 Tab + // 报告(含增量结果)加载后自动设置可用 Tab useEffect(() => { - if (report) { - if (report.editorialReview) { + if (displayReport) { + if (displayReport.editorialReview) { setActiveTab('editorial'); - } else if (report.methodologyReview) { + } else if (displayReport.methodologyReview) { setActiveTab('methodology'); - } else if (report.forensicsResult) { + } else if (displayReport.forensicsResult) { setActiveTab('forensics'); - } else if (report.clinicalReview) { + } else if (displayReport.clinicalReview) { setActiveTab('clinical'); } } - }, [report]); + }, [displayReport]); // 动态获取进度步骤 const progressSteps = getProgressSteps(task.selectedAgents || ['editorial', 'methodology']); @@ -132,6 +144,21 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet const getStepStatus = (stepKey: string): 'completed' | 'active' | 'pending' => { const hasEditorial = task.selectedAgents?.includes('editorial'); const hasMethodology = task.selectedAgents?.includes('methodology'); + const hasClinical = task.selectedAgents?.includes('clinical'); + const skillMap: Record = { + forensics: 'DataForensicsSkill', + editorial: 'EditorialSkill', + methodology: 'MethodologySkill', + clinical: 'ClinicalAssessmentSkill', + }; + const currentSkill = stepKey in skillMap ? task.reviewProgress?.[skillMap[stepKey]] : undefined; + + if (currentSkill?.status && ['success', 'warning', 'skipped'].includes(currentSkill.status)) { + return 'completed'; + } + if (currentSkill?.status && ['error', 'timeout'].includes(currentSkill.status)) { + return 'completed'; + } if (task.status === 'pending') { return stepKey === 'upload' ? 'completed' : 'pending'; @@ -143,7 +170,10 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet } if (task.status === 'reviewing' || task.status === 'reviewing_editorial') { if (['upload', 'extract'].includes(stepKey)) return 'completed'; + if (stepKey === 'forensics') return 'active'; if (stepKey === 'editorial' && hasEditorial) return 'active'; + if (stepKey === 'methodology' && hasMethodology) return 'active'; + if (stepKey === 'clinical' && hasClinical) return 'active'; return 'pending'; } if (task.status === 'reviewing_methodology') { @@ -245,12 +275,20 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet ); children.push(new Paragraph({ spacing: { after: 300 } })); + + let sectionIndex = 1; + const sectionNumbers = ['一', '二', '三', '四', '五', '六']; + const nextSectionTitle = (title: string) => { + const current = sectionNumbers[sectionIndex - 1] || `${sectionIndex}`; + sectionIndex += 1; + return `${current}、${title}`; + }; // 稿约规范性评估 if (report.editorialReview) { children.push( new Paragraph({ - text: '一、稿约规范性评估', + text: nextSectionTitle('稿约规范性评估'), heading: HeadingLevel.HEADING_1, spacing: { before: 400, after: 200 }, }) @@ -313,7 +351,7 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet if (report.methodologyReview) { children.push( new Paragraph({ - text: '二、方法学评估', + text: nextSectionTitle('方法学评估'), heading: HeadingLevel.HEADING_1, spacing: { before: 400, after: 200 }, }) @@ -374,13 +412,152 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet } }); } + + // 数据验证 + if (report.forensicsResult) { + children.push( + new Paragraph({ + text: nextSectionTitle('数据验证'), + heading: HeadingLevel.HEADING_1, + spacing: { before: 400, after: 200 }, + }) + ); + + const summary = report.forensicsResult.summary; + children.push( + new Paragraph({ + text: `共识别表格 ${summary.totalTables} 个;发现问题 ${summary.totalIssues} 个(错误 ${summary.errorCount},警告 ${summary.warningCount})。`, + spacing: { after: 160 }, + }) + ); + + // 导出表格明细(按 forensicsResult.tables 渲染) + if (report.forensicsResult.tables.length > 0) { + report.forensicsResult.tables.slice(0, 20).forEach((tableItem, tableIndex) => { + children.push( + new Paragraph({ + text: `表${tableIndex + 1}:${tableItem.caption || tableItem.id || '未命名表格'}`, + heading: HeadingLevel.HEADING_2, + spacing: { before: 180, after: 100 }, + }) + ); + + const rows: TableRow[] = []; + const headers = tableItem.headers || []; + const tableData = tableItem.data || []; + + if (headers.length > 0) { + rows.push( + new TableRow({ + children: headers.map((header) => new TableCell({ + children: [new Paragraph({ children: [new TextRun({ text: String(header ?? ''), bold: true })] })], + })), + }) + ); + } + + // 限制导出行数,避免超大表格导致文档膨胀 + tableData.slice(0, 200).forEach((row) => { + rows.push( + new TableRow({ + children: row.map((cell) => new TableCell({ + children: [new Paragraph(String(cell ?? ''))], + })), + }) + ); + }); + + if (rows.length > 0) { + children.push( + new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + rows, + }) + ); + } else { + children.push( + new Paragraph({ + text: '(该表未提取到可导出的结构化数据)', + spacing: { after: 80 }, + }) + ); + } + + const tableIssues = (tableItem.issues || []).slice(0, 50); + if (tableIssues.length > 0) { + children.push( + new Paragraph({ + children: [new TextRun({ text: '该表问题:', bold: true })], + spacing: { before: 80, after: 40 }, + }) + ); + tableIssues.forEach((issue) => { + const level = issue.severity === 'ERROR' ? '错误' : issue.severity === 'WARNING' ? '警告' : '提示'; + children.push( + new Paragraph({ + text: `• [${level}] ${issue.message}`, + indent: { left: 720 }, + spacing: { after: 40 }, + }) + ); + }); + } + }); + + if (report.forensicsResult.tables.length > 20) { + children.push( + new Paragraph({ + text: `(其余 ${report.forensicsResult.tables.length - 20} 张表格已省略)`, + spacing: { before: 80, after: 80 }, + }) + ); + } + } + + if (report.forensicsResult.issues.length > 0) { + children.push( + new Paragraph({ + children: [new TextRun({ text: '问题清单:', bold: true })], + spacing: { after: 80 }, + }) + ); + + report.forensicsResult.issues.slice(0, 120).forEach((issue) => { + const level = issue.severity === 'ERROR' ? '错误' : issue.severity === 'WARNING' ? '警告' : '提示'; + children.push( + new Paragraph({ + text: `• [${level}] ${issue.message}`, + indent: { left: 720 }, + spacing: { after: 50 }, + }) + ); + }); + + if (report.forensicsResult.issues.length > 120) { + children.push( + new Paragraph({ + text: `(其余 ${report.forensicsResult.issues.length - 120} 条问题已省略)`, + indent: { left: 720 }, + spacing: { after: 100 }, + }) + ); + } + } else { + children.push( + new Paragraph({ + children: [new TextRun({ text: '✓ 未发现数据一致性问题', color: '006600' })], + indent: { left: 720 }, + spacing: { after: 100 }, + }) + ); + } + } // 临床专业评估 if (report.clinicalReview) { - const sectionNum = report.methodologyReview ? '三' : '二'; children.push( new Paragraph({ - text: `${sectionNum}、临床专业评估`, + text: nextSectionTitle('临床专业评估'), heading: HeadingLevel.HEADING_1, spacing: { before: 400, after: 200 }, }) @@ -552,7 +729,7 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet {/* 提示信息 */}

AI 正在分析您的稿件,这可能需要 1-3 分钟

-

请耐心等待,完成后将自动显示结果

+

模块完成后会实时显示,无需等待全部结束

)} @@ -594,8 +771,8 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet )} - {/* 完成状态 - 显示报告 */} - {isCompleted && report && ( + {/* 增量报告:处理中也可查看已完成模块 */} + {displayReport && (isProcessing || isCompleted || isPartial) && ( <> {/* 审查完成信息 */}
@@ -603,10 +780,10 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet
-

审查完成

+

{isCompleted ? '审查完成' : '已完成模块结果(实时更新)'}

- 审查用时 {report.durationSeconds ? formatTime(report.durationSeconds) : '-'} - {report.completedAt && ` · ${new Date(report.completedAt).toLocaleString('zh-CN')}`} + 审查用时 {displayReport.durationSeconds ? formatTime(displayReport.durationSeconds) : '-'} + {displayReport.completedAt && ` · ${new Date(displayReport.completedAt).toLocaleString('zh-CN')}`}

@@ -615,7 +792,7 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet {/* Tab切换 */}
- {report.editorialReview && ( + {displayReport.editorialReview && ( )} - {report.methodologyReview && ( + {displayReport.methodologyReview && ( )} - {report.forensicsResult && ( + {displayReport.forensicsResult && ( )} - {report.clinicalReview && ( + {displayReport.clinicalReview && (
{/* 非 docx 文件无数据验证提示 */} - {!report.forensicsResult && (report.editorialReview || report.methodologyReview) && (() => { + {!displayReport.forensicsResult && (displayReport.editorialReview || displayReport.methodologyReview) && (() => { const fileName = task.fileName || ''; const isPdf = fileName.toLowerCase().endsWith('.pdf'); const isDoc = fileName.toLowerCase().endsWith('.doc'); @@ -686,17 +863,17 @@ export default function TaskDetail({ task: initialTask, jobId, onBack }: TaskDet })()} {/* 报告内容 */} - {activeTab === 'editorial' && report.editorialReview && ( - + {activeTab === 'editorial' && displayReport.editorialReview && ( + )} - {activeTab === 'methodology' && report.methodologyReview && ( - + {activeTab === 'methodology' && displayReport.methodologyReview && ( + )} - {activeTab === 'forensics' && report.forensicsResult && ( - + {activeTab === 'forensics' && displayReport.forensicsResult && ( + )} - {activeTab === 'clinical' && report.clinicalReview && ( - + {activeTab === 'clinical' && displayReport.clinicalReview && ( + )} )} diff --git a/frontend-v2/src/modules/rvw/types/index.ts b/frontend-v2/src/modules/rvw/types/index.ts index e010534d..73ba35b6 100644 --- a/frontend-v2/src/modules/rvw/types/index.ts +++ b/frontend-v2/src/modules/rvw/types/index.ts @@ -42,6 +42,16 @@ export interface ReviewTask { timeoutCount: number; totalSkills: number; }; + editorialReview?: EditorialReviewResult; + methodologyReview?: MethodologyReviewResult; + forensicsResult?: ForensicsResult; + clinicalReview?: ClinicalReviewResult; + reviewProgress?: Record; createdAt: string; completedAt?: string; durationSeconds?: number;