From fe5f7b9d6ee32e3b1dab8fda5e0dcc1b01e68641 Mon Sep 17 00:00:00 2001 From: potaser Date: Mon, 10 Aug 2026 00:31:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E8=AF=AD=E8=A8=80=E6=95=A3=E8=90=BD?= =?UTF-8?q?=E8=A1=A5=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/vue3/index_ml.html | 286 +++++++++++++++--------------- apps/web/vue3/main.py | 2 + apps/web/vue3/static/i18n_en.js | 176 +++++++++++++++++- gfa_core/mas/agents/diagnosis.py | 44 +++-- gfa_core/mas/core/orchestrator.py | 102 +++++++++-- 5 files changed, 434 insertions(+), 176 deletions(-) diff --git a/apps/web/vue3/index_ml.html b/apps/web/vue3/index_ml.html index adc74f6de..8f2cd4ec7 100644 --- a/apps/web/vue3/index_ml.html +++ b/apps/web/vue3/index_ml.html @@ -7724,7 +7724,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 if (!conv) return; currentConversationId.value = conv.conversation_id; currentConversation.value = conv; - ElementPlus.ElMessage.info('切换到: ' + conv.title); + ElementPlus.ElMessage.info(tp('switched-to', { title: conv.title })); // 恢复关联的 job_id if (conv.job_id) { @@ -7789,9 +7789,9 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 if (!conv || !conv.conversation_id) return; try { await ElementPlus.ElMessageBox.confirm( - `确定删除会话「${conv.title || conv.conversation_id}」吗?该操作会从列表中移除,但保留后台记录。`, - '删除会话', - { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' } + tp('delete-conversation-msg', { title: conv.title || conv.conversation_id }), + t('delete-conversation-title'), + { confirmButtonText: t('delete'), cancelButtonText: t('cancel'), type: 'warning' } ); } catch (e) { return; @@ -8080,7 +8080,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 logContent.value = ''; ElMessage.success(t('错误日志已清空')); }; - const handleDownloadExample = () => ElMessage.info('示例下载功能当前不可用'); + const handleDownloadExample = () => ElMessage.info(t('example-download-unavailable')); const autofillRowMatchesBulkFilter = (row) => { const query = String(autofillState.bulkFilter || '').trim().toLowerCase(); if (!query) return true; @@ -8272,12 +8272,12 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 const edits = currentAutofillBulkEdits(); const editedFields = Object.entries(edits).filter(([, value]) => value !== '').map(([key]) => key); if (!editedFields.length) { - ElMessage.warning('请至少填写一个要预览修改的字段'); + ElMessage.warning(t('autofill-preview-required')); return; } const rows = selectedAutofillBulkRows(); if (!rows.length) { - ElMessage.warning('没有匹配的草稿行'); + ElMessage.warning(t('autofill-no-matching-rows')); return; } (autofillState.rows || []).forEach(row => { delete row.__bulkEdited; }); @@ -8289,12 +8289,12 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 pushAutofillChangedRow(row); }); autofillState.bulkPreviewActive = true; - autofillState.bulkLastSummary = `预览 ${rows.length} 行:${editedFields.join(', ')}。确认无误后点击 Apply。`; + autofillState.bulkLastSummary = t('autofill-bulk-rows-preview', { rows: rows.length, fields: editedFields.join(', ') }); ElMessage.success(autofillState.bulkLastSummary); }; const applyAutofillBulkPreview = () => { if (!autofillState.bulkPreviewActive) { - ElMessage.warning('请先预览要应用的修改'); + ElMessage.warning(t('autofill-preview-first')); return; } let count = 0; @@ -8307,14 +8307,14 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 delete row.__bulkPreviewed; }); autofillState.bulkPreviewActive = false; - autofillState.bulkLastSummary = `已应用 ${count} 行修改。`; + autofillState.bulkLastSummary = t('autofill-bulk-rows-applied', { rows: count }); ElMessage.success(autofillState.bulkLastSummary); }; const cancelAutofillBulkPreview = () => { if (!autofillState.bulkPreviewActive) return; restoreAutofillBulkPreview(); autofillState.bulkChangedRows = []; - autofillState.bulkLastSummary = '已取消预览,表格已恢复到预览前。'; + autofillState.bulkLastSummary = t('autofill-bulk-preview-cancelled'); ElMessage.info(autofillState.bulkLastSummary); }; const applyAutofillBulkEdit = previewAutofillBulkEdit; @@ -8557,7 +8557,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 }; const confirmAutofillDraft = async () => { if (!autofillState.draftPath) { - ElMessage.warning('请先生成或加载草稿'); + ElMessage.warning(t('autofill-generate-draft-first')); return; } if (autofillState.bulkPreviewActive) { @@ -8567,20 +8567,20 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 const missing = (autofillState.rows || []).filter(row => !String(row.library_strandness || '').trim()); if (missing.length) { const examples = missing.slice(0, 5).map(row => row.sample_id || row.run || '未命名样本').join(', '); - ElMessage.warning(`链特异性为必填:还有 ${missing.length} 个样本未填写(${examples})`); + ElMessage.warning(tp('autofill-strandness-required', { count: missing.length, examples: examples })); return; } const review = await requestSamplesDesignReview({ quiet: true }); if (review?.status !== 'ready') { - ElMessage.warning(`设计提示:${review?.error || '未生成高级设计'};将保留原流程的兼容处理。`); + ElMessage.warning(tp('autofill-design-tip', { error: review?.error || t('autofill-design-not-generated') })); } const comparisonText = (review?.comparisons || []).map(item => item.display).join(';') || '沿用原流程兼容设计'; const interactionText = review?.interaction?.enabled ? `;${review.interaction.display}(LRT + Wald)` : ''; try { await ElMessageBox.confirm( `${review?.module || 'M1'} ${review?.module_name || '兼容模式'}\n${comparisonText}${interactionText}`, - '确认 DE 分析设计', - { confirmButtonText: '确认设计并写入', cancelButtonText: '返回修改', type: 'info' } + t('confirm-de-design-title'), + { confirmButtonText: t('confirm-design-write'), cancelButtonText: t('return-modify'), type: 'info' } ); } catch (_err) { return; @@ -8614,10 +8614,10 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 }); const data = await response.json(); if (!data.success) { - ElMessage.error(data.detail || data.message || '确认失败'); + ElMessage.error(data.detail || data.message || t('confirm-failed')); return; } - ElMessage.success(autofillState.type === 'config' ? '已写入正式 config.xlsx' : '已写入正式 samples.yaml'); + ElMessage.success(autofillState.type === 'config' ? t('autofill-config-written') : t('autofill-samples-written')); autofillDialogVisible.value = false; if (autofillState.type === 'config' && data.resume_goal) { await addAgentMessage('assistant', t('config.xlsx 已确认,继续当前目标:') + data.resume_goal); @@ -8625,7 +8625,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 }; const loadFromPath = async (options = {}) => { if (!inputPath.value) { - ElMessage.warning('请输入文件路径'); + ElMessage.warning(t('enter-file-path')); return { success: false }; } const response = await fetch('/api/load_from_path', { @@ -8642,7 +8642,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 const data = await response.json(); const hasDraftRows = (draft) => Array.isArray(draft?.rows) && draft.rows.length > 0; if (data.success) { - ElMessage.success(`已加载 ${data.count || 0} files`); + ElMessage.success(tp('files-loaded', { count: data.count || 0 })); const hasSamplesDraft = !!data.samples_draft_created && hasDraftRows(data.samples_draft); const hasConfigDraft = !!data.config_draft_created && hasDraftRows(data.config_draft); if (!options.suppressAutofill && hasSamplesDraft) { @@ -8662,7 +8662,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 } inputPath.value = ''; } else { - ElMessage.error(data.message || '加载失败'); + ElMessage.error(data.message || t('load-failed')); } return data; }; @@ -9328,7 +9328,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓 sessionId.value = 'session_' + Date.now(); localStorage.setItem('bbav_session_id', sessionId.value); ragHistory.value = []; - ElMessage.success('已Clear当前会话'); + ElMessage.success(t('cleared-current-session')); }; // 参数确认对话框 @@ -9560,7 +9560,7 @@ compare: [[flesh, peel]]`; currentInterpretStage.value = { id: stageId, name: stageName, identity }; } catch (err) { console.error('[深度解读] 请求失败:', err); - ElMessage.error(err.message || '请求解读服务失败'); + ElMessage.error(err.message || t('interpret-request-failed')); throw err; } }; @@ -9591,9 +9591,9 @@ compare: [[flesh, peel]]`; useRagSupport: true, refreshRag: true }); - ElMessage.success('RAG解读已刷新'); + ElMessage.success(t('rag-interpretation-refreshed')); } catch (err) { - ElMessage.error(err.message || 'RAG解读刷新失败'); + ElMessage.error(err.message || t('rag-interpretation-refresh-failed')); } finally { interpretationRagRefreshing.value = false; } @@ -9715,14 +9715,14 @@ compare: [[flesh, peel]]`; const res = await fetch(`/api/agent/report/${session}/download?format=${format}&stage=${stage.id || ''}`); if (format === 'json') { const data = await res.json(); - if (!res.ok || data.success === false) throw new Error(data.message || '下载报告失败'); + if (!res.ok || data.success === false) throw new Error(data.message || t('download-report-failed')); console.log('报告数据:', data); - ElMessage.success('JSON数据已加载到控制台'); + ElMessage.success(t('json-loaded-to-console')); return; } if (!res.ok) { const errData = await res.json().catch(() => ({})); - throw new Error(errData.message || '下载报告失败'); + throw new Error(errData.message || t('download-report-failed')); } const blob = await res.blob(); const url = window.URL.createObjectURL(blob); @@ -9744,8 +9744,8 @@ compare: [[flesh, peel]]`; if (!interpretationReport.value) return; const content = interpretationReport.value.biological_story || JSON.stringify(interpretationReport.value, null, 2); navigator.clipboard.writeText(content).then(() => { - ElMessage.success('解读内容已复制'); - }).catch(() => ElMessage.error('复制失败')); + ElMessage.success(t('interpretation-copied')); + }).catch(() => ElMessage.error(t('copy-failed'))); }; // 依赖驱动的并行执行架构 @@ -10479,9 +10479,9 @@ compare: [[flesh, peel]]`; nodeTypes: Object.keys(nodeColors).length }; initKgChart(); - ElMessage.success('知识图谱加载完成'); + ElMessage.success(t('kg-loaded')); } catch (error) { - ElMessage.error('加载图谱失败: ' + error.message); + ElMessage.error(t('kg-load-failed', { message: error.message })); } finally { kgLoading.value = false; } @@ -10519,9 +10519,9 @@ compare: [[flesh, peel]]`; if (node) { kgChart.value.dispatchAction({ type: 'highlight', seriesIndex: 0, name: node.id }); kgSelectedNode.value = node; - ElMessage.success(`找到节点: ${node.name}`); + ElMessage.success(tp('kg-node-found', { name: node.name })); } else { - ElMessage.warning('未找到匹配的节点'); + ElMessage.warning(t('kg-node-not-found')); } }; @@ -10534,7 +10534,7 @@ compare: [[flesh, peel]]`; if (kgChart.value) kgChart.value.resize(); }); }); - ElMessage.success('视图已重置'); + ElMessage.success(t('kg-view-reset')); }; const reloadKnowledgeGraph = () => { @@ -10548,7 +10548,7 @@ compare: [[flesh, peel]]`; const openKgFullscreen = () => { if (kgData.value.nodes.length === 0) { - ElMessage.warning('请先通过问答加载子图,或点击「重新加载」'); + ElMessage.warning(t('kg-load-subgraph-first')); return; } kgFullscreenVisible.value = true; @@ -10582,9 +10582,9 @@ compare: [[flesh, peel]]`; if (node) { kgFullscreenChart.value.dispatchAction({ type: 'highlight', seriesIndex: 0, name: node.id }); kgSelectedNode.value = node; - ElMessage.success(`找到节点: ${node.name}`); + ElMessage.success(tp('kg-node-found', { name: node.name })); } else { - ElMessage.warning('未找到匹配的节点'); + ElMessage.warning(t('kg-node-not-found')); } }; @@ -10731,7 +10731,7 @@ compare: [[flesh, peel]]`; } catch (err) { tablePreviewRows.value = []; tablePreviewColumns.value = []; - ElMessage.error('读取表格失败: ' + (err.message || err)); + ElMessage.error(t('table-read-failed', { reason: err.message || err })); } finally { tablePreviewLoading.value = false; } @@ -10765,7 +10765,7 @@ compare: [[flesh, peel]]`; const openPpiResetDialog = async (stage = null) => { if (!jobInfo.job_id) { - ElMessage.warning('当前没有可用 job_id'); + ElMessage.warning(t('no-job-id-available')); return; } ppiResetDialogVisible.value = true; @@ -10795,7 +10795,7 @@ compare: [[flesh, peel]]`; const geneId = String(ppiResetNewId.value || '').trim(); if (!geneId) return; if (ppiResetRows.value.some(row => row.gene_id === geneId)) { - ElMessage.warning('该 ID 已存在'); + ElMessage.warning(t('ppi-id-exists')); return; } ppiResetRows.value.push({ gene_id: geneId, keep: true, source: 'manual_added' }); @@ -10809,7 +10809,7 @@ compare: [[flesh, peel]]`; .map(row => String(row.gene_id || '').trim()) .filter(Boolean); if (!keptIds.length) { - ElMessage.warning('至少保留一个 gene ID'); + ElMessage.warning(t('ppi-keep-at-least-one')); return; } ppiResetSubmitting.value = true; @@ -10830,7 +10830,7 @@ compare: [[flesh, peel]]`; }); const data = await res.json(); if (!res.ok || !data.success) throw new Error(data.detail || data.message || 'PPI Reset 失败'); - ElMessage.success(`PPI Reset 已完成: ${data.reset_id}`); + ElMessage.success(tp('ppi-reset-done', { reset_id: data.reset_id })); ppiResetDialogVisible.value = false; await refreshResults(); } catch (err) { @@ -10931,10 +10931,10 @@ compare: [[flesh, peel]]`; } } // 如果没找到 URL,显示文件内容 - ElMessage.info('STRING 链接文件内容:' + text.substring(0, 200)); + ElMessage.info(tp('string-link-content', { preview: text.substring(0, 200) })); }) .catch(err => { - ElMessage.error('读取文件失败: ' + err.message); + ElMessage.error(tp('file-read-failed', { message: err.message })); }); return; } @@ -11227,7 +11227,7 @@ compare: [[flesh, peel]]`; const openSelectedImageReplot = async () => { if (!selectedImageUrl.value) { - ElMessage.warning('请先选择一张图片'); + ElMessage.warning(t('select-image-first')); return; } if (isComboImageName(selectedImage.value)) { @@ -11384,7 +11384,7 @@ compare: [[flesh, peel]]`; // 显示任务详情 const showTaskDetail = (task) => { - ElMessage.info(`任务: ${task.message}\n状态: ${task.status}\n进度: ${task.progress}%`); + ElMessage.info(tp('task-detail', { message: task.message, status: task.status, progress: task.progress })); }; // 定时RefreshAgent Tasks @@ -11509,7 +11509,7 @@ compare: [[flesh, peel]]`; } } catch (e) { console.error('[会话] 消息持久化失败:', e); - ElMessage.warning('当前消息未成功写入会话历史,刷新后可能丢失'); + ElMessage.warning(t('message-persist-failed')); } } }; @@ -11528,9 +11528,9 @@ compare: [[flesh, peel]]`; const copyPath = async (path) => { try { await navigator.clipboard.writeText(path); - ElMessage.success('路径已复制'); + ElMessage.success(t('path-copied')); } catch (err) { - ElMessage.error('复制失败'); + ElMessage.error(t('copy-failed')); } }; @@ -11588,10 +11588,10 @@ compare: [[flesh, peel]]`; } } - ElMessage.success('已创建新Working Directory: ' + data.job_id); + ElMessage.success(tp('wd-created', { job_id: data.job_id })); } } catch (err) { - ElMessage.error('创建失败: ' + err.message); + ElMessage.error(tp('wd-create-failed', { message: err.message })); } }; @@ -11610,7 +11610,7 @@ compare: [[flesh, peel]]`; jobSelectorVisible.value = true; } } catch (err) { - ElMessage.error('获取Working Directory列表失败: ' + err.message); + ElMessage.error(tp('wd-list-failed', { message: err.message })); } }; @@ -11657,14 +11657,14 @@ compare: [[flesh, peel]]`; } } - ElMessage.success('已切换到Working Directory: ' + data.job_id); + ElMessage.success(tp('wd-switched', { job_id: data.job_id })); // Refresh结果展示 await refreshResults(); } else { - ElMessage.error(data.error || '切换失败'); + ElMessage.error(data.error || t('wd-switch-failed')); } } catch (err) { - ElMessage.error('切换失败: ' + err.message); + ElMessage.error(tp('wd-switch-failed-reason', { message: err.message })); } }; @@ -12958,7 +12958,7 @@ compare: [[flesh, peel]]`; const showConfirm = async () => { console.log('[DEBUG] showConfirm called, selectedStage:', selectedStage.value); if (!selectedStage.value) { - ElMessage.warning('请先SelectAnalysis Stage'); + ElMessage.warning(t('select-analysis-stage')); console.log('[DEBUG] showConfirm: no stage selected, returning'); return; } @@ -13065,13 +13065,13 @@ compare: [[flesh, peel]]`; const autoAnnotationBackgroundTitle = (payload = {}) => { const required = Array.isArray(payload.required_backgrounds) ? payload.required_backgrounds : []; const enrichType = String(payload.enrich_type || '').toLowerCase(); - if (required.includes('gene2go') && required.includes('gene2ko')) return 'GO/KEGG 富集背景缺失'; - if (required.includes('gene2ko') || enrichType === 'kegg') return 'KEGG 富集背景缺失'; - if (required.includes('gene2go') || enrichType === 'go') return 'GO 富集背景缺失'; - return 'RNA-seq 富集背景缺失'; + if (required.includes('gene2go') && required.includes('gene2ko')) return t('go-kegg-enrich-bg-missing'); + if (required.includes('gene2ko') || enrichType === 'kegg') return t('kegg-enrich-bg-missing'); + if (required.includes('gene2go') || enrichType === 'go') return t('go-enrich-bg-missing'); + return t('rnaseq-enrich-bg-missing'); }; - const autoAnnotationDialogTitle = (payload = {}) => `${autoAnnotationBackgroundTitle(payload)},需要自动注释`; + const autoAnnotationDialogTitle = (payload = {}) => t('enrich-bg-missing-auto-annotation', { title: autoAnnotationBackgroundTitle(payload) }); const autoAnnotationDialogMessage = (payload = {}) => { const required = Array.isArray(payload.required_backgrounds) ? payload.required_backgrounds : []; @@ -13145,7 +13145,7 @@ compare: [[flesh, peel]]`; : (issues.length ? issues[0] : {}); const firstIssue = primaryIssue; if (!Object.keys(firstIssue).length && !fixData.force_show) { - ElMessage.info(fixData.reason || '诊断未发现明确问题,请查看阶段错误日志。'); + ElMessage.info(fixData.reason || t('diagnosis-no-issue-found')); return; } const firstIssueCode = firstIssue.code || ''; @@ -13175,7 +13175,7 @@ compare: [[flesh, peel]]`; const explicitAction = fixData.fix_action || fixData.action || ''; const hasExecutableFix = safeCandidates.length > 0 || ['prepare_job_patch', 'execute_job_patch'].includes(explicitAction); if (!fixData.force_show && !hasExecutableFix && firstIssue.severity !== 'blocking' && firstIssue.severity !== 'error') { - ElMessage.info(firstIssue.recommendation || fixData.reason || '诊断仅提示信息,无需确认修复。'); + ElMessage.info(firstIssue.recommendation || fixData.reason || t('diagnosis-info-only')); return; } const hasConfirmActions = repairActions.some(action => action.policy === 'confirm'); @@ -13261,13 +13261,13 @@ compare: [[flesh, peel]]`; }); if (response.ok) { - ElMessage.success('✅ 已应用修复方案,继续执行...'); + ElMessage.success(t('fix-applied')); } else { - ElMessage.error('❌ 应用修复失败'); + ElMessage.error(t('fix-apply-failed')); } } catch (error) { console.error('[Diagnosis Confirm] Error:', error); - ElMessage.error('❌ 网络错误,请重试'); + ElMessage.error(t('network-error-retry')); } }; @@ -13289,9 +13289,9 @@ compare: [[flesh, peel]]`; const shouldPause = diagnosisFixData.value.blocksDownstream === true; if (shouldPause) { shouldStopBatchExecute.value = true; - ElMessage.warning('⏸ 已跳过修复;该问题会阻断下游,后续阶段已暂停。'); + ElMessage.warning(t('fix-skipped-paused')); } else { - ElMessage.info('已关闭诊断提示。'); + ElMessage.info(t('diagnosis-closed')); } } catch (error) { console.error('[Diagnosis Reject] Error:', error); @@ -13354,11 +13354,11 @@ compare: [[flesh, peel]]`; executeFn, confirmFn = (message) => ElMessageBox.confirm( message, - 'GATK 执行失败', + t('gatk-failed-title'), { type: 'warning', - confirmButtonText: '使用 bcftools 重试', - cancelButtonText: '取消', + confirmButtonText: t('retry-bcftools'), + cancelButtonText: t('cancel'), } ) ) => { @@ -13615,14 +13615,14 @@ compare: [[flesh, peel]]`; try { await ElMessageBox.confirm( [ - '目标物种不在 STRING 固定快照中。请确认是否采用排序第一的近源物种:', + t('string-close-species-line1'), '', ...candidateLines, '', - '确认后才会下载参考 PEP、生成 Ortholog,并执行 PPI。' + t('string-close-species-line2') ].join('\n'), - '确认 PPI 的 STRING 近源参考物种', - { type: 'warning', confirmButtonText: '确认第一候选', cancelButtonText: '取消 PPI' } + t('string-close-species-title'), + { type: 'warning', confirmButtonText: t('string-confirm-first-candidate'), cancelButtonText: t('string-cancel-ppi') } ); } catch (_err) { return null; @@ -15036,7 +15036,7 @@ compare: [[flesh, peel]]`; if (gwasPrefixMissingFor({ stage: 'gwas_fmt', prefix })) { clearBatchExecuteTimer(); batchExecuteCountdown.value = 0; - ElMessage.warning('请填写 GWAS 物种简称(用于结果前缀)'); + ElMessage.warning(t('gwas-prefix-required')); return; } lockGwasBatchPrefix(prefix); @@ -16022,15 +16022,15 @@ compare: [[flesh, peel]]`; const categories = Array.isArray(confirmParams.promoterCategories) ? confirmParams.promoterCategories : []; const plotModes = Array.isArray(confirmParams.promoterPlotModes) ? confirmParams.promoterPlotModes : []; if (!prefixValue) { - ElMessage.error('无法确定启动子输出前缀。'); + ElMessage.error(t('promoter-prefix-unknown')); return null; } if (!elements.length && !categories.length) { - ElMessage.error('请至少选择一个启动子元件或类别。'); + ElMessage.error(t('promoter-select-element')); return null; } if (!plotModes.length) { - ElMessage.error('请至少选择一种图类型。'); + ElMessage.error(t('promoter-select-plot-type')); return null; } const requestBody = { @@ -16086,7 +16086,7 @@ compare: [[flesh, peel]]`; const submitSelectedTreeReplot = async () => { const prefixValue = confirmParams.prefix || inferTreePrefixFromImageName(selectedImage.value); if (!prefixValue) { - ElMessage.error('无法确定进化树输出标签,请先选择树图或填写输出标签。'); + ElMessage.error(t('tree-output-label-unknown')); return null; } const requestBody = { @@ -16144,12 +16144,12 @@ compare: [[flesh, peel]]`; : outputStem + '.png'; const outputPath = '/workspace/' + jobInfo.job_id + '/final/' + outputName; selectImageFile(displayStage, outputPath); - ElMessage.success('进化树图重绘完成,已切换到新图'); + ElMessage.success(t('tree-replot-done')); return data; } catch (err) { console.error('[Tree Replot] submit failed:', err); const msg = err && err.message ? err.message : String(err); - ElMessage.error('进化树图重绘失败:' + msg); + ElMessage.error(tp('tree-replot-failed-reason', { message: msg })); addAgentMessage('assistant', t('进化树图重绘失败:') + msg); return null; } finally { @@ -16160,7 +16160,7 @@ compare: [[flesh, peel]]`; const submitSelectedSyntenyReplot = async () => { const prefixValue = confirmParams.prefix || inferSyntenyPrefixFromImageName(selectedImage.value); if (!prefixValue) { - ElMessage.error('无法确定种内共线性输出标签,请先选择 Circos 图。'); + ElMessage.error(t('synteny-output-label-unknown')); return null; } const outputLabel = confirmParams.syntenyOutputLabel || 'synteny_replot_circos'; @@ -16219,12 +16219,12 @@ compare: [[flesh, peel]]`; const submitSelectedComboReplot = async () => { const prefixValue = confirmParams.prefix || inferComboPrefixFromImageName(selectedImage.value); if (!prefixValue) { - ElMessage.error('无法确定 combo 输出标签,请先选择 combo 图。'); + ElMessage.error(t('combo-output-label-unknown')); return null; } const panels = Array.isArray(confirmParams.comboPanels) ? [...new Set(confirmParams.comboPanels)] : []; if (panels.length < 2) { - ElMessage.error('Combo 修订至少选择两个类型。'); + ElMessage.error(t('combo-needs-two-types')); return null; } const comboOutputLabel = makeComboOutputLabel(panels); @@ -16268,12 +16268,12 @@ compare: [[flesh, peel]]`; selectImageFile('combo', outputPath); activeStages.value = 'combo'; rightPanelTab.value = 'results'; - ElMessage.success('Combo 图修订完成,已切换到新图'); + ElMessage.success(t('combo-replot-done')); return data; } catch (err) { console.error('[Combo Replot] submit failed:', err); const msg = err && err.message ? err.message : String(err); - ElMessage.error('Combo 图修订失败:' + msg); + ElMessage.error(tp('combo-replot-failed-reason', { message: msg })); addAgentMessage('assistant', t('Combo 图修订失败:') + msg); return null; } finally { @@ -16285,7 +16285,7 @@ compare: [[flesh, peel]]`; const inferredPrefix = inferPrefixFromImageName(selectedImage.value); const prefixValue = inferredPrefix || confirmParams.prefix; if (!prefixValue) { - ElMessage.error('无法确定MSA输出标签,请先选择对应图片或填写输出标签。'); + ElMessage.error(t('msa-output-label-unknown')); return null; } const requestBody = { @@ -16306,11 +16306,11 @@ compare: [[flesh, peel]]`; msaSourceImage: confirmParams.msaSourceImage || selectedImageContext.path || stripUrlQuery(selectedImageUrl.value) || '' }; if (confirmParams.msaRegionMode === 'manual' && (!requestBody.msaStartCol || !requestBody.msaEndCol)) { - ElMessage.error('请填写MSA alignment 起始列和终止列,或选择其他模式。'); + ElMessage.error(t('msa-alignment-required')); return null; } if (confirmParams.msaRegionMode === 'reference' && (!requestBody.msaReferenceId || !requestBody.msaReferenceStartResidue || !requestBody.msaReferenceEndResidue)) { - ElMessage.error('请填写参考基因及其起止残基编号。'); + ElMessage.error(t('msa-reference-required')); return null; } try { @@ -16367,7 +16367,7 @@ compare: [[flesh, peel]]`; const confirmStageExecute = async () => { if (gwasPrefixMissingFor(confirmParams)) { - ElMessage.warning('请填写 GWAS 物种简称(用于结果前缀)'); + ElMessage.warning(t('gwas-prefix-required')); return; } if (isGwasStage(confirmParams.stage)) { @@ -16379,7 +16379,7 @@ compare: [[flesh, peel]]`; } } if (gwasConfirmationBlockedFor(confirmParams)) { - ElMessage.warning('请先选择有效 VCF'); + ElMessage.warning(t('select-valid-vcf')); return; } clearStageConfirmTimer(); @@ -17587,11 +17587,11 @@ compare: [[flesh, peel]]`; ? ['gene2ko'] : ['gene2go']; await ElMessageBox.confirm( - `${text}\n\n是否切换为“自动注释 + 继续富集”?`, + `${text}\n\n${t('auto-annotation-switch-msg')}`, autoAnnotationBackgroundTitle({ required_backgrounds: fallbackRequiredBackgrounds }), { - confirmButtonText: '自动注释并继续', - cancelButtonText: '我先去上传背景文件', + confirmButtonText: t('auto-annotate-continue'), + cancelButtonText: t('upload-bg-first'), type: 'warning', distinguishCancelAndClose: true } @@ -18285,7 +18285,7 @@ compare: [[flesh, peel]]`; const applyDomainReviewIds = async (ids, source) => { clearDomainReviewTimer(); if (!ids || !ids.length) { - ElMessage.warning('当前筛选没有成员,未重建 final_members。'); + ElMessage.warning(t('domain-review-no-members')); return; } domainReviewApplying.value = true; @@ -18397,14 +18397,14 @@ compare: [[flesh, peel]]`; return openedAny; } if (mode === 'per_species' && expectedSpecies.length && (!Array.isArray(perSpeciesResults) || perSpeciesResults.length < expectedSpecies.length)) { - ElMessage.warning('当前选择为按物种分别分析,但后端没有返回完整的分物种亚家族结果;将尝试打开可用的联合结果。'); + ElMessage.warning(t('subfamily-per-species-incomplete')); } const prefixes = collectSubfamilyReviewPrefixes(result, stageParams); for (let idx = 0; idx < prefixes.length; idx += 1) { const opened = await maybeShowSubfamilyReview('reference_classification', idx === prefixes.length - 1, prefixes[idx], false); if (opened) return true; } - ElMessage.warning('亚家族预测已完成,但未能自动打开确认表;请发送“调出亚家族确认表”手动打开。'); + ElMessage.warning(t('subfamily-auto-open-failed')); return false; }; @@ -18510,7 +18510,7 @@ compare: [[flesh, peel]]`; const applySubfamilyReview = async () => { clearSubfamilyReviewTimer(); if (!subfamilyReviewRows.value.length) { - ElMessage.warning('没有可确认的亚家族标签。'); + ElMessage.warning(t('subfamily-no-labels')); return; } subfamilyReviewApplying.value = true; @@ -18863,9 +18863,9 @@ compare: [[flesh, peel]]`; isStopping.value = true; try { await stopLightweightOperation(); - ElMessage.success('已请求停止当前拓展分析;状态仍会继续轮询直到进程退出。'); + ElMessage.success(t('extension-stop-requested')); } catch (err) { - ElMessage.error(`停止拓展分析失败:${err.message || err}`); + ElMessage.error(tp('extension-stop-failed', { message: err.message || err })); } finally { isStopping.value = false; } @@ -18894,7 +18894,7 @@ compare: [[flesh, peel]]`; if (idSet.size === 0) { if (!stoppedBatch) { - ElMessage.warning('No running task to stop'); + ElMessage.warning(t('no-running-task-to-stop')); } return; } @@ -18914,7 +18914,7 @@ compare: [[flesh, peel]]`; } } - ElMessage.success('Task(s) stopped (backend + Celery when available)'); + ElMessage.success(t('tasks-stopped')); addMessage('assistant', '⏹ Task(s) stopped'); if (pollTimer) { @@ -19005,7 +19005,7 @@ compare: [[flesh, peel]]`; console.log('[DEBUG] doExecute called, closing dialog'); if (gwasPrefixMissingFor(confirmParams)) { clearAutoExecuteTimer(); - ElMessage.warning('请填写 GWAS 物种简称(用于结果前缀)'); + ElMessage.warning(t('gwas-prefix-required')); return; } if (isGwasStage(confirmParams.stage)) { @@ -19309,9 +19309,9 @@ compare: [[flesh, peel]]`; const handleUploadSuccess = async (response) => { if (response && response.config_normalized) { const warningText = (response.warnings || []).join(' '); - ElMessage.success(`Upload complete. Latest Excel is now used as config.xlsx.${warningText ? ' ' + warningText : ''}`); + ElMessage.success(tp('upload-config-applied', { warningText: warningText ? ' ' + warningText : '' })); } else { - ElMessage.success('File uploaded successfully'); + ElMessage.success(t('file-uploaded-success')); } refreshInfo(); const opened = await queueAutofillFromPayload(response, 'upload'); @@ -19346,19 +19346,19 @@ compare: [[flesh, peel]]`; const onExtensionCodeFilesChange = (event) => { const selected = Array.from(event.target.files || []); extensionBuild.codeFiles = appendExtensionFiles(extensionBuild.codeFiles, selected); - if (selected.length) ElMessage.success(`已选择 ${selected.length} 个原始代码文件`); + if (selected.length) ElMessage.success(tp('code-files-selected', { count: selected.length })); event.target.value = ''; }; const onExtensionDataFilesChange = (event) => { const selected = Array.from(event.target.files || []); extensionBuild.dataFiles = appendExtensionFiles(extensionBuild.dataFiles, selected); - if (selected.length) ElMessage.success(`已选择 ${selected.length} 个测试数据文件`); + if (selected.length) ElMessage.success(tp('data-files-selected', { count: selected.length })); event.target.value = ''; }; const handleLightweightExtensionFolderDrop = (event) => { const droppedFiles = Array.from(event?.dataTransfer?.files || []); if (!droppedFiles.length) { - ElMessage.warning('未检测到可用文件'); + ElMessage.warning(t('no-usable-files-detected')); return; } @@ -19400,7 +19400,7 @@ compare: [[flesh, peel]]`; } } - ElMessage.success(`已拖入 ${normalized.length} 个文件,已自动填充拓展包`); + ElMessage.success(tp('files-dropped-extension', { count: normalized.length })); }; const removeExtensionFile = (kind, file) => { const key = extensionFileKey(file); @@ -19495,7 +19495,7 @@ compare: [[flesh, peel]]`; }; const runExtensionPrePublishGates = async () => { if (!extensionBuild.extensionId) { - ElMessage.warning('请先生成拓展分析阶段。'); + ElMessage.warning(t('extension-generate-first')); return; } if (!jobInfo.job_id) { @@ -19556,7 +19556,7 @@ compare: [[flesh, peel]]`; extensionBuild.prePublishReady = true; await loadExtensionStages(true); pushExtensionStatus('发布前验收已全部通过,可以发布。', 'gate', true); - ElMessage.success('发布前验收已通过'); + ElMessage.success(t('pre-publish-passed')); } catch (err) { extensionBuild.prePublishReady = false; extensionBuild.prePublishError = err.message || '发布前验收失败'; @@ -20061,7 +20061,7 @@ compare: [[flesh, peel]]`; }; const buildExtensionStageLegacy = async () => { if (!extensionBuild.displayName || !extensionBuild.codeFiles.length) { - ElMessage.warning('请填写名称,并上传原始代码。'); + ElMessage.warning(t('extension-name-code-required')); return; } extensionBuild.confirmDialogVisible = false; @@ -20133,7 +20133,7 @@ compare: [[flesh, peel]]`; if (!publishResponse.ok) throw new Error(await extensionApiError(publishResponse, '发布失败')); await loadExtensionStages(); await extensionProgressMessage(`已发布 ${builtExtensionId},现在可以直接在对话框中用自然语言执行。`, 'publish'); - ElMessage.success('拓展分析阶段已生成并发布'); + ElMessage.success(t('extension-built-published')); } catch (err) { const message = err.message || '拓展分析阶段生成失败'; pushExtensionStatus(message, 'error', false); @@ -20222,7 +20222,7 @@ compare: [[flesh, peel]]`; if (!response.ok) throw new Error(await extensionApiError(response, '确认失败')); pushExtensionStatus('已确认参数和输出,请继续验证', 'confirm'); extensionBuild.confirmDialogVisible = false; - ElMessage.success('确认已保存'); + ElMessage.success(t('confirm-saved')); await loadExtensionStages(true); } catch (err) { ElMessage.error(err.message || '确认失败'); @@ -20239,7 +20239,7 @@ compare: [[flesh, peel]]`; if (!response.ok) throw new Error(await extensionApiError(response, '验证失败')); const data = await response.json(); pushExtensionStatus(data.success ? '验证成功,可发布' : '验证失败,请查看后端日志', data.success ? 'validate' : 'error', !data.success); - data.success ? ElMessage.success('验证成功') : ElMessage.warning('验证未通过'); + data.success ? ElMessage.success(t('validation-success')) : ElMessage.warning(t('validation-not-passed')); await loadExtensionStages(true); } catch (err) { pushExtensionStatus(err.message || '验证失败', 'error', true); @@ -20256,7 +20256,7 @@ compare: [[flesh, peel]]`; const response = await extensionApiFetch(`/api/extension-stages/${encodeURIComponent(extensionBuild.extensionId)}/publish`, { method: 'POST' }); if (!response.ok) throw new Error(await extensionApiError(response, '发布失败')); pushExtensionStatus('已发布,可执行', 'publish'); - ElMessage.success('拓展分析阶段已发布'); + ElMessage.success(t('extension-published')); await loadExtensionStages(); } catch (err) { pushExtensionStatus(err.message || '发布失败', 'error', true); @@ -20385,8 +20385,8 @@ compare: [[flesh, peel]]`; const data = await response.json(); const run = data.run || {}; extensionRun.statusText = `执行完成: ${run.status || 'unknown'}`; - if (run.status === 'success') ElMessage.success('拓展分析阶段执行完成'); - else ElMessage.warning('拓展分析阶段执行未成功,请查看结果日志'); + if (run.status === 'success') ElMessage.success(t('extension-run-success')); + else ElMessage.warning(t('extension-run-incomplete')); await loadExtensionRuns(); extensionRun.activeRun = run.run_id || extensionRun.activeRun; if (fromIntent) { @@ -20568,7 +20568,7 @@ compare: [[flesh, peel]]`; return stageContext === candidateContext && stageRoute === candidateRoute; }); if (exists) { - ElMessage.info('该阶段已在正式列表中'); + ElMessage.info(t('stage-already-in-list')); return; } const stage = { @@ -20580,7 +20580,7 @@ compare: [[flesh, peel]]`; }; methodologyConfig.value.stages = [...(methodologyConfig.value.stages || []), stage]; methodologyConfig.value.figure_only_candidate_stages = methodologyFigureCandidates(methodologyConfig.value).filter(item => item.observation_id !== candidate.observation_id); - ElMessage.success('已加入正式阶段列表,并默认纳入本次执行'); + ElMessage.success(t('stage-added-to-list')); }; const methodologyStageRecordKey = (stage) => { @@ -21209,7 +21209,7 @@ compare: [[flesh, peel]]`; const triggerMethodologyExtraction = async () => { if (!methodologyPaperFile.value) { - ElMessage.warning('请先选择 PDF 或 MinerU content_list JSON 文献文件'); + ElMessage.warning(t('methodology-select-pdf-first')); return; } methodologyLoading.value = true; @@ -21232,7 +21232,7 @@ compare: [[flesh, peel]]`; await pollMethodologyExtraction(); } catch (err) { methodologyLoading.value = false; - ElMessage.error(err.message || '方法学提取启动失败'); + ElMessage.error(err.message || t('methodology-extract-failed')); } }; @@ -21253,7 +21253,7 @@ compare: [[flesh, peel]]`; methodologySandboxRoot.value = data.sandbox_root; } } catch (err) { - ElMessage.error(err.message || '方法学 sandbox 示例读取失败'); + ElMessage.error(err.message || t('methodology-sandbox-examples-failed')); } finally { methodologySandboxExamplesLoading.value = false; } @@ -21261,7 +21261,7 @@ compare: [[flesh, peel]]`; const buildMethodologySandbox = async () => { if (!methodologySandboxDir.value) { - ElMessage.warning('请先选择或输入 methodology_dir'); + ElMessage.warning(t('methodology-select-dir-first')); return; } methodologySandboxLoading.value = true; @@ -21288,7 +21288,7 @@ compare: [[flesh, peel]]`; methodologySandboxRegistryPath.value = report.registry_path || ''; methodologySandboxDirBuilt.value = registry.sandbox_dir || report.sandbox_dir || ''; methodologyStatusText.value = `Sandbox 已构建:阶段 ${report.stage_card_count || 0},图表 ${report.figure_table_stage_card_count || 0},关系 ${report.relation_count || 0}`; - ElMessage.success('方法学 sandbox 已构建'); + ElMessage.success(t('methodology-sandbox-built')); } catch (err) { ElMessage.error(err.message || '方法学 sandbox 构建失败'); } finally { @@ -21298,11 +21298,11 @@ compare: [[flesh, peel]]`; const askMethodologySandbox = async () => { if (!methodologySandboxRegistryPath.value && !methodologySandboxDirBuilt.value) { - ElMessage.warning('请先构建临时库'); + ElMessage.warning(t('methodology-build-first')); return; } if (!methodologySandboxQuestion.value) { - ElMessage.warning('请输入方法学问题'); + ElMessage.warning(t('methodology-enter-question')); return; } methodologySandboxAnswerLoading.value = true; @@ -21326,7 +21326,7 @@ compare: [[flesh, peel]]`; methodologySandboxAnswer.value += `\n\n来源 ${sourceCount} 条`; } } catch (err) { - ElMessage.error(err.message || '方法学 sandbox 查询失败'); + ElMessage.error(err.message || t('methodology-sandbox-query-failed')); } finally { methodologySandboxAnswerLoading.value = false; } @@ -21387,13 +21387,13 @@ compare: [[flesh, peel]]`; } methodologyStatusText.value = `方法学方案已保存到当前 job:${bindData.plan_path || 'methodology_plan.json'}`; if (!silent) { - ElMessage.success('方法学方案已保存,可先准备数据,稍后再按该方案执行'); + ElMessage.success(t('methodology-plan-saved')); await addAgentMessage('assistant', t('已保存方法学方案到当前 job。下一步请上传/填写目标物种和参考物种数据,并确认 config.xlsx 后再执行方法学流程。')); } return bindData; } catch (err) { if (silent) throw err; - ElMessage.error(err.message || '方法学方案保存失败'); + ElMessage.error(err.message || t('methodology-plan-save-failed')); methodologyStatusText.value = err.message || '方法学方案保存失败'; return null; } finally { @@ -21423,7 +21423,7 @@ compare: [[flesh, peel]]`; try { if (isBatchExecuting.value) { methodologyExecuting.value = false; - ElMessage.warning('当前已有批量任务在执行,请等待完成后再提交方法学流程'); + ElMessage.warning(t('methodology-batch-running')); return; } const readiness = await methodologyBindAndCheckReadiness(); @@ -21441,7 +21441,7 @@ compare: [[flesh, peel]]`; const selectedStages = methodologyExecutionSeedStages(methodologyConfig.value); if (!selectedStages.length) { methodologyExecuting.value = false; - ElMessage.warning('请至少选择一个可执行的 bbav5 阶段'); + ElMessage.warning(t('methodology-select-stage')); return; } @@ -21531,7 +21531,7 @@ compare: [[flesh, peel]]`; }, completed_stages: [] }; - ElMessage.warning('亚家族参考物种未写入当前 job 配置,已生成方法学本地计划,请在批量确认页核对参考物种'); + ElMessage.warning(t('methodology-subfamily-species-not-saved')); } if (!planData.success || !planData.plan) { throw new Error(planData.message || '多智能体规划方法学执行顺序失败'); @@ -21622,7 +21622,7 @@ compare: [[flesh, peel]]`; startBatchExecuteTimer(); } catch (err) { methodologyExecuting.value = false; - ElMessage.error(err.message || '方法学执行规划失败'); + ElMessage.error(err.message || t('methodology-plan-failed')); addAgentMessage('assistant', t('❌ 方法学执行规划失败:') + (err.message || 'Unknown error'), { taskType: 'error' }); @@ -21651,7 +21651,7 @@ compare: [[flesh, peel]]`; await runPlanForIntent(syntenyChoiceQuery.value || option.label, selectedIntent); } catch (err) { console.error('[DEBUG] Synteny choice plan error:', err); - ElMessage.error(`❌ ${err.message || '规划请求失败'}`); + ElMessage.error(tp('plan-request-failed-reason', { message: err.message || t('plan-request-failed') })); addAgentMessage('assistant', t('❌ 服务暂时不可用:') + (err.message || 'Planning request failed') + '\n\n' + t('请稍后重试。'), { taskType: 'error' }); @@ -22295,7 +22295,7 @@ compare: [[flesh, peel]]`; // 处理自然语言Input const handleNlInput = async () => { if (!nlInput.value.trim()) { - ElMessage.warning('请Input内容'); + ElMessage.warning(t('input-required')); return; } @@ -22333,7 +22333,7 @@ compare: [[flesh, peel]]`; if (wantsIdentifyMemberReviewEarly) { try { if (!jobInfo.job_id || jobInfo.job_id === 'default_job') { - ElMessage.warning('请先选择或创建 Working Directory'); + ElMessage.warning(t('select-or-create-wd')); nlLoading.value = false; return; } @@ -22407,7 +22407,7 @@ compare: [[flesh, peel]]`; try { // 检查是否有绑定的 job if (!jobInfo.job_id || jobInfo.job_id === 'default_job') { - ElMessage.warning('请先选择或创建 Working Directory'); + ElMessage.warning(t('select-or-create-wd')); nlLoading.value = false; return; } @@ -22771,7 +22771,7 @@ compare: [[flesh, peel]]`; // 规划失败(如PreFlight验证失败),显示错误并停止 const errorMsg = planData.message || "规划失败"; console.log('[DEBUG] Plan API failed:', errorMsg); - ElMessage.error(`❌ ${errorMsg}`); + ElMessage.error(tp('plan-request-failed-reason', { message: errorMsg })); let planHint = /缺少|输入文件|upload|上传|file/i.test(errorMsg) ? '\n\n请检查必要输入文件是否已上传。' : '\n\n规划阶段未能生成可执行流程,请查看后端日志中的具体原因。'; @@ -22809,7 +22809,7 @@ compare: [[flesh, peel]]`; if (err?.code === 'user_cancelled') { addAgentMessage('assistant', t('已停止等待,本次未继续获取回答。')); } else { - ElMessage.error('请求失败: ' + err.message); + ElMessage.error(tp('request-failed-reason', { message: err.message })); addAgentMessage('assistant', t('抱歉,服务暂时不可用,请稍后重试。')); } } finally { @@ -23195,7 +23195,7 @@ compare: [[flesh, peel]]`; // 重新绘图 const redrawImage = () => { - ElMessage.info('重新绘图功能开发中...'); + ElMessage.info(t('replot-in-development')); }; // 组件卸载时清理定时器 diff --git a/apps/web/vue3/main.py b/apps/web/vue3/main.py index bf6513b73..3af4b63ae 100644 --- a/apps/web/vue3/main.py +++ b/apps/web/vue3/main.py @@ -22947,6 +22947,7 @@ class AgentRunRequest(BaseModel): job_id: str = "" intent: dict = {} # 前端可传入已有的intent stages: list = [] # 要执行的阶段列表 + language: str = DEFAULT_AGENT_LANG # "zh" or "en" @app.post("/api/agent/run") @@ -22980,6 +22981,7 @@ async def agent_run(request: AgentRunRequest): ) # 添加额外的运行时字段 + initial_state["language"] = request.language initial_state["status"] = "running" initial_state["intent_result"] = request.intent or {} # 传入已有的 intent initial_state["completed_stages"] = [] diff --git a/apps/web/vue3/static/i18n_en.js b/apps/web/vue3/static/i18n_en.js index 41947fdea..cc294239a 100644 --- a/apps/web/vue3/static/i18n_en.js +++ b/apps/web/vue3/static/i18n_en.js @@ -712,6 +712,180 @@ window.__I18N_EN__ = { " 已暂停依赖 confirmed_subfamily.tsv": " has paused stages depending on confirmed_subfamily.tsv", ";": ";", " 个分析阶段": " analysis stages", - " 个阶段": " stages" + " 个阶段": " stages", + + // ===== 阶段 7:ElMessage/ElMessageBox i18n keys ===== + // 注意:key 为中文原文(zh 模式直接返回 key),value 为 English + // tp() 类 key 需要同时注册变量 placeholder 版本和 static 文案 + + // ElMessageBox + "删除会话": "Delete Session", + "取消 PPI": "Cancel PPI", + "自动注释并继续": "Auto-annotate & Continue", + "我先去上传背景文件": "Upload Background Files First", + "确认设计并写入": "Confirm Design & Write", + "返回修改": "Return to Modify", + "确认第一候选": "Confirm First Candidate", + "使用 bcftools 重试": "Retry with bcftools", + "GATK 执行失败": "GATK Execution Failed", + "确认 DE 分析设计": "Confirm DE Analysis Design", + "确认 PPI 的 STRING 近源参考物种": "Confirm STRING Close Reference Species for PPI", + + // autoAnnotation 标题 + "GO/KEGG 富集背景缺失": "GO/KEGG Enrichment Background Missing", + "KEGG 富集背景缺失": "KEGG Enrichment Background Missing", + "GO 富集背景缺失": "GO Enrichment Background Missing", + "RNA-seq 富集背景缺失": "RNA-seq Enrichment Background Missing", + + // 会话管理 + "示例下载功能当前不可用": "Example download is currently unavailable", + "已Clear当前会话": "Current session cleared", + + // autoFill + "请至少填写一个要预览修改的字段": "Please fill at least one field to preview modifications", + "没有匹配的草稿行": "No matching draft rows", + "请先预览要应用的修改": "Please preview modifications before applying", + "请先生成或加载草稿": "Please generate or load a draft first", + "生成草稿失败": "Failed to generate draft", + "确认失败": "Confirmation failed", + "已写入正式 config.xlsx": "Formal config.xlsx written", + "已写入正式 samples.yaml": "Formal samples.yaml written", + "请输入文件路径": "Please enter file path", + "加载失败": "Load failed", + + // RAG 解读 + "请求解读服务失败": "Interpretation service request failed", + "RAG解读已刷新": "RAG interpretation refreshed", + "RAG解读刷新失败": "RAG interpretation refresh failed", + "下载报告失败": "Failed to download report", + "JSON数据已加载到控制台": "JSON data loaded to console", + "解读内容已复制": "Interpretation content copied", + "复制失败": "Copy failed", + + // 知识图谱 + "知识图谱加载完成": "Knowledge graph loaded", + "未找到匹配的节点": "No matching node found", + "视图已重置": "View reset", + "请先通过问答加载子图,或点击「重新加载」": "Please load subgraph via Q&A first, or click Reload", + + // PPI/STRING + "当前没有可用 job_id": "No job_id currently available", + "该 ID 已存在": "This ID already exists", + "至少保留一个 gene ID": "Keep at least one gene ID", + + // Working Directory + "切换失败": "Switch failed", + + // 图片/可视化 + "请先选择一张图片": "Please select an image first", + "无法确定启动子输出前缀。": "Cannot determine promoter output prefix.", + "请至少选择一个启动子元件或类别。": "Please select at least one promoter element or category.", + "请至少选择一种图类型。": "Please select at least one plot type.", + "无法确定进化树输出标签,请先选择树图或填写输出标签。": "Cannot determine phylogeny tree output label. Please select a tree image or fill in the output label first.", + "进化树图重绘完成,已切换到新图": "Phylogeny tree replot complete; switched to new image", + "无法确定种内共线性输出标签,请先选择 Circos 图。": "Cannot determine intraspecies synteny output label. Please select a Circos image first.", + "无法确定 combo 输出标签,请先选择 combo 图。": "Cannot determine combo output label. Please select a combo image first.", + "Combo 修订至少选择两个类型。": "Combo revision requires at least two panel types.", + "Combo 图修订完成,已切换到新图": "Combo plot revision complete; switched to new image", + "无法确定MSA输出标签,请先选择对应图片或填写输出标签。": "Cannot determine MSA output label. Please select corresponding image or fill in output label.", + "请填写MSA alignment 起始列和终止列,或选择其他模式。": "Please fill in MSA alignment start/end columns or choose another mode.", + "请填写参考基因及其起止残基编号。": "Please fill in reference gene and its start/end residue numbers.", + + // GWAS/诊断 + "请填写 GWAS 物种简称(用于结果前缀)": "Please fill in GWAS species abbreviation (for result prefix)", + "请先选择有效 VCF": "Please select a valid VCF first", + "请先SelectAnalysis Stage": "Please select an analysis stage first", + "诊断未发现明确问题,请查看阶段错误日志。": "Diagnosis found no clear issues. Please check stage error logs.", + "诊断仅提示信息,无需确认修复。": "Diagnosis is informational only; no fix confirmation needed.", + "✅ 已应用修复方案,继续执行...": "✅ Fix applied; continuing execution...", + "❌ 应用修复失败": "❌ Fix application failed", + "❌ 网络错误,请重试": "❌ Network error; please retry", + "⏸ 已跳过修复;该问题会阻断下游,后续阶段已暂停。": "⏸ Fix skipped; issue blocks downstream; subsequent stages paused.", + "已关闭诊断提示。": "Diagnosis prompt closed.", + + // 任务/消息 + "当前消息未成功写入会话历史,刷新后可能丢失": "Message not persisted to session history; may be lost on refresh", + "路径已复制": "Path copied", + + // 拓展分析 + "当前筛选没有成员,未重建 final_members。": "No members matching current filter; final_members not rebuilt.", + "当前选择为按物种分别分析,但后端没有返回完整的分物种亚家族结果;将尝试打开可用的联合结果。": "Per-species analysis selected but backend returned incomplete results; attempting to open available joint results.", + "亚家族预测已完成,但未能自动打开确认表;请发送\"调出亚家族确认表\"手动打开。": "Subfamily prediction completed but confirmation table could not open automatically; please send \"show subfamily review table\" manually.", + "没有可确认的亚家族标签。": "No subfamily labels to confirm.", + "已请求停止当前拓展分析;状态仍会继续轮询直到进程退出。": "Stop requested for current extension; polling will continue until process exits.", + "No running task to stop": "No running task to stop", + "Task(s) stopped (backend + Celery when available)": "Task(s) stopped (backend + Celery when available)", + "File uploaded successfully": "File uploaded successfully", + "未检测到可用文件": "No usable files detected", + "请先生成拓展分析阶段。": "Please generate the extension stage first.", + "发布前验收已通过": "Pre-publish validation passed", + "请填写名称,并上传原始代码。": "Please provide name and upload source code.", + "拓展分析阶段已生成并发布": "Extension stage generated and published", + "确认已保存": "Confirmation saved", + "验证成功": "Validation successful", + "验证未通过": "Validation not passed", + "拓展分析阶段已发布": "Extension stage published", + "拓展分析阶段执行完成": "Extension stage execution complete", + "拓展分析阶段执行未成功,请查看结果日志": "Extension stage execution incomplete; please check result logs", + "该阶段已在正式列表中": "This stage is already in the formal list", + "已加入正式阶段列表,并默认纳入本次执行": "Added to formal stage list and included in this execution", + + // 方法学 + "请先选择 PDF 或 MinerU content_list JSON 文献文件": "Please select a PDF or MinerU content_list JSON literature file first", + "方法学提取启动失败": "Methodology extraction failed to start", + "方法学 sandbox 示例读取失败": "Failed to read methodology sandbox examples", + "请先选择或输入 methodology_dir": "Please select or enter methodology_dir first", + "方法学 sandbox 已构建": "Methodology sandbox built", + "请先构建临时库": "Please build the sandbox first", + "请输入方法学问题": "Please enter methodology question", + "方法学 sandbox 查询失败": "Methodology sandbox query failed", + "方法学方案已保存,可先准备数据,稍后再按该方案执行": "Methodology plan saved; you may prepare data first and execute later", + "方法学方案保存失败": "Failed to save methodology plan", + "当前已有批量任务在执行,请等待完成后再提交方法学流程": "A batch task is currently running; please wait for completion before submitting methodology workflow", + "请至少选择一个可执行的 bbav5 阶段": "Please select at least one executable bbav5 stage", + "亚家族参考物种未写入当前 job 配置,已生成方法学本地计划,请在批量确认页核对参考物种": "Subfamily reference species not written to current job config; local methodology plan generated. Please verify reference species on batch confirmation page.", + "方法学执行规划失败": "Methodology execution planning failed", + + // Other + "请Input内容": "Please enter content", + "请先选择或创建 Working Directory": "Please select or create a Working Directory first", + "规划请求失败": "Plan request failed", + "重新绘图功能开发中...": "Replot feature is under development...", + + // ===== tp() keys — 使用完整中文模板作为 key ===== + "确定删除会话「{title}」吗?该操作会从列表中移除,但保留后台记录。": "Delete session \"{title}\"? This removes it from the list but keeps backend records.", + "切换到: {title}": "Switched to: {title}", + "预览 {rows} 行:{fields}。确认无误后点击 Apply。": "Preview {rows} rows: {fields}. Click Apply once confirmed.", + "已应用 {rows} 行修改。": "Applied {rows} row modifications.", + "已取消预览,表格已恢复到预览前。": "Preview cancelled, table restored.", + "链特异性为必填:还有 {count} 个样本未填写({examples})": "Library strandness required: {count} sample(s) unfilled ({examples})", + "设计提示:{error};将保留原流程的兼容处理。": "Design tip: {error}; keeping original compatible handling.", + "已加载 {count} files": "Loaded {count} files", + "加载图谱失败: {message}": "Graph load failed: {message}", + "找到节点: {name}": "Node found: {name}", + "读取表格失败: {reason}": "Table read failed: {reason}", + "PPI Reset 已完成: {reset_id}": "PPI Reset complete: {reset_id}", + "STRING 链接文件内容:{preview}": "STRING link file content: {preview}", + "读取文件失败: {message}": "File read failed: {message}", + "已创建新Working Directory: {job_id}": "New Working Directory created: {job_id}", + "创建失败: {message}": "Creation failed: {message}", + "获取Working Directory列表失败: {message}": "Failed to get Working Directory list: {message}", + "已切换到Working Directory: {job_id}": "Switched to Working Directory: {job_id}", + "切换失败: {message}": "Switch failed: {message}", + "进化树图重绘失败:{message}": "Phylogeny tree replot failed: {message}", + "Combo 图修订失败:{message}": "Combo plot revision failed: {message}", + "任务: {message}\n状态: {status}\n进度: {progress}%": "Task: {message}\nStatus: {status}\nProgress: {progress}%", + "停止拓展分析失败:{message}": "Failed to stop extension: {message}", + "已选择 {count} 个原始代码文件": "{count} code file(s) selected", + "已选择 {count} 个测试数据文件": "{count} test data file(s) selected", + "已拖入 {count} 个文件,已自动填充拓展包": "{count} file(s) dropped; extension package auto-filled", + "❌ {message}": "❌ {message}", + "Upload complete. Latest Excel is now used as config.xlsx.{warningText}": "Upload complete. Latest Excel is now used as config.xlsx.{warningText}", + "{title},需要自动注释": "{title}, Auto-annotation Required", + + // String close species (tp with multi-line) + "目标物种不在 STRING 固定快照中。请确认是否采用排序第一的近源物种:": "Target species not in STRING fixed snapshot. Confirm using the top-ranked close species:", + "确认后才会下载参考 PEP、生成 Ortholog,并执行 PPI。": "Reference PEP will be downloaded, Ortholog generated, and PPI executed only after confirmation.", + "是否切换为\"自动注释 + 继续富集\"?": "Switch to \"Auto-annotation + Continue Enrichment\"?" }; diff --git a/gfa_core/mas/agents/diagnosis.py b/gfa_core/mas/agents/diagnosis.py index 98575181d..0376de93e 100755 --- a/gfa_core/mas/agents/diagnosis.py +++ b/gfa_core/mas/agents/diagnosis.py @@ -390,48 +390,57 @@ class DiagnosisAgent(BaseAgent): ERROR_PATTERNS = { "missing_input_file": { "patterns": [r"文件不存在", r"FileNotFoundError", r"No such file", r"找不到文件"], - "suggestion": "检查输入文件路径是否正确,确保文件已上传到input目录", + "suggestion_zh": "检查输入文件路径是否正确,确保文件已上传到input目录", + "suggestion_en": "Check if input file path is correct; ensure files are uploaded to the input directory", "auto_fix": False }, "hmm_not_found": { "patterns": [r"required_pfams is empty", r"No HMM file", r"找不到HMM"], - "suggestion": "请上传HMM模型文件或指定--required-pfams参数", + "suggestion_zh": "请上传HMM模型文件或指定--required-pfams参数", + "suggestion_en": "Please upload HMM model file or specify --required-pfams parameter", "auto_fix": False }, "blast_db_error": { "patterns": [r"BLAST Database error", r"makeblastdb failed"], - "suggestion": "Identify BLAST 固定用目标物种全蛋白 FASTA 建库;请优先检查目标全蛋白 FASTA 是否存在、非空且为有效蛋白FASTA格式,再检查参考家族蛋白 query 文件。", + "suggestion_zh": "Identify BLAST 固定用目标物种全蛋白 FASTA 建库;请优先检查目标全蛋白 FASTA 是否存在、非空且为有效蛋白FASTA格式,再检查参考家族蛋白 query 文件。", + "suggestion_en": "Identify BLAST uses target species full protein FASTA for database; check that target FASTA exists, is non-empty and valid, then check reference family protein query file.", "auto_fix": False }, "memory_error": { "patterns": [r"MemoryError", r"out of memory", r"内存不足"], - "suggestion": "减少--cpu参数值或分批处理,建议使用--cpu 4", + "suggestion_zh": "减少--cpu参数值或分批处理,建议使用--cpu 4", + "suggestion_en": "Reduce --cpu parameter or use batch processing; recommend --cpu 4", "auto_fix": True, "fix_action": "reduce_cpu" }, "rnaseq_library_strandness_error": { "patterns": [r"library_strandness", r"链特异性", r"rna-strandness", r"featureCounts.*-s"], - "suggestion": "请在 samples.yaml 的 rnaseq.library_strandness 或每个 samples..library_strandness 中明确填写 non_stranded / forward / reverse;混合链特异性样本当前不能用单次 featureCounts 定量。", + "suggestion_zh": "请在 samples.yaml 的 rnaseq.library_strandness 或每个 samples..library_strandness 中明确填写 non_stranded / forward / reverse;混合链特异性样本当前不能用单次 featureCounts 定量。", + "suggestion_en": "Please specify non_stranded / forward / reverse in samples.yaml rnaseq.library_strandness or each samples..library_strandness; mixed strand-specific samples cannot be quantified in a single featureCounts run.", "auto_fix": False }, "timeout_error": { "patterns": [r"Timeout", r"timed out", r"超时"], - "suggestion": "增加超时时间或检查输入数据大小", + "suggestion_zh": "增加超时时间或检查输入数据大小", + "suggestion_en": "Increase timeout or check input data size", "auto_fix": False }, "conda_env_error": { "patterns": [r"conda", r"Environment", r"找不到命令"], - "suggestion": "检查conda环境是否正确激活,确保工具已安装", + "suggestion_zh": "检查conda环境是否正确激活,确保工具已安装", + "suggestion_en": "Check that conda environment is activated and tools are installed", "auto_fix": False }, "permission_error": { "patterns": [r"Permission denied", r"权限不足"], - "suggestion": "检查文件权限,确保有读写权限", + "suggestion_zh": "检查文件权限,确保有读写权限", + "suggestion_en": "Check file permissions; ensure read/write access", "auto_fix": False }, "invalid_parameter": { "patterns": [r"unrecognized arguments", r"invalid parameter", r"参数错误"], - "suggestion": "检查参数名称是否正确,注意使用短横线而非下划线", + "suggestion_zh": "检查参数名称是否正确,注意使用短横线而非下划线", + "suggestion_en": "Check parameter name; use hyphens instead of underscores", "auto_fix": True, "fix_action": "fix_parameter_format" }, @@ -448,7 +457,8 @@ class DiagnosisAgent(BaseAgent): r"RuntimeError: MAFFT失败.*No such file", r"RuntimeError:.*失败.*No such file or directory" ], - "suggestion": "生物信息学工具未安装或未添加到PATH。建议:1) 激活conda环境: conda activate test4py311 2) 安装缺失工具: conda install -c bioconda mafft blast iqtree hmmer", + "suggestion_zh": "生物信息学工具未安装或未添加到PATH。建议:1) 激活conda环境: conda activate test4py311 2) 安装缺失工具: conda install -c bioconda mafft blast iqtree hmmer", + "suggestion_en": "Bioinformatics tools not installed or not in PATH. Suggestions: 1) activate conda environment: conda activate test4py311 2) install missing tools: conda install -c bioconda mafft blast iqtree hmmer", "auto_fix": False, "severity": "high" }, @@ -461,7 +471,8 @@ class DiagnosisAgent(BaseAgent): r"requires.*stage.*not completed", r"dependency.*not satisfied" ], - "suggestion": "前置分析阶段未完成,系统将自动先执行依赖阶段", + "suggestion_zh": "前置分析阶段未完成,系统将自动先执行依赖阶段", + "suggestion_en": "Prerequisite analysis stage not completed; system will execute dependency stage first automatically", "auto_fix": True, "fix_action": "execute_dependencies", "severity": "high" @@ -601,6 +612,7 @@ class DiagnosisAgent(BaseAgent): prepare_patch_workspace: bool = False, scope_id: str = "", params: Optional[Dict[str, Any]] = None, + language: str = "zh", ) -> Dict[str, Any]: """只读审计某个 job/stage 的产物、manifest、日志和输入配置。 @@ -930,6 +942,7 @@ class DiagnosisAgent(BaseAgent): gate: str = "preflight", project_root: Path = PROJECT_ROOT, params: Optional[Dict[str, Any]] = None, + language: str = "zh", ) -> Dict[str, Any]: """Global QA gate for stage preflight/postflight/failure. @@ -4490,10 +4503,10 @@ class DiagnosisAgent(BaseAgent): except Exception as exc: print(f"[DiagnosisAgent] Failed to remember LLM diagnosis case: {exc}") - def _match_error_pattern(self, error_text: str) -> Optional[Dict]: + def _match_error_pattern(self, error_text: str, language: str = "zh") -> Optional[Dict]: """匹配错误模式""" error_lower = error_text.lower() - + priority_order = [ "tool_not_found", "dependency_failed", @@ -4507,15 +4520,16 @@ class DiagnosisAgent(BaseAgent): "permission_error", "timeout_error" ] - + for error_type in priority_order: if error_type in self.ERROR_PATTERNS: pattern_info = self.ERROR_PATTERNS[error_type] for pattern in pattern_info["patterns"]: if re.search(pattern, error_text, re.IGNORECASE): + suggestion_key = "suggestion_en" if language == "en" else "suggestion_zh" return { "error_type": error_type, - "suggestion": pattern_info["suggestion"], + "suggestion": pattern_info.get(suggestion_key, pattern_info.get("suggestion_zh", "")), "auto_fix": pattern_info.get("auto_fix", False), "fix_action": pattern_info.get("fix_action"), "severity": pattern_info.get("severity", "medium") diff --git a/gfa_core/mas/core/orchestrator.py b/gfa_core/mas/core/orchestrator.py index 680fd7ded..3361df9d3 100755 --- a/gfa_core/mas/core/orchestrator.py +++ b/gfa_core/mas/core/orchestrator.py @@ -29,6 +29,74 @@ WORKSPACE_DIR = WORKSPACE_ROOT class Orchestrator: + # 双语消息映射表:中文 → English(用于用户可见消息) + _MSG = { + # analysis_node — response 消息 + "✅ 所有分析阶段已完成!": "✅ All analysis stages completed!", + "📋 自动规划剩余分析阶段": "📋 Auto-planned remaining analysis stages", + "**已完成**": "**Completed**", + "无": "None", + "**待执行**": "**Pending**", + "个阶段)": "stage(s))", + "请确认是否按顺序执行以上分析阶段?": "Confirm to execute the above analysis stages in order?", + "我将帮您执行RNA-seq": "I will help you execute RNA-seq", + "分析。请确认参数后开始。": "analysis. Please confirm parameters to start.", + "RNA-seq分析将通过前端异步执行。": "RNA-seq analysis will be executed asynchronously via frontend.", + " 分析。PlanningAgentV2 已根据当前状态生成完整执行计划。": " analysis. PlanningAgentV2 has generated a complete execution plan based on current state.", + "我将帮您执行": "I will help you execute", + "分析。": "analysis.", + "请提供基因家族蛋白序列文件(.fa/.fasta),用于MEME保守基序预测。": "Please provide gene family protein sequence file (.fa/.fasta) for MEME conserved motif prediction.", + "请提供config.xlsx及基因组/GFF/CDS/PEP等原始文件,用于完整数据准备。": "Please provide config.xlsx and genome/GFF/CDS/PEP files for complete data preparation.", + "请提供基因家族蛋白序列文件,用于Pfam/InterPro结构域注释。": "Please provide gene family protein sequence file for Pfam/InterPro domain annotation.", + "请提供基因家族的GFF注释文件和基因组序列。": "Please provide gene family GFF annotation file and genome sequence.", + "请提供基因家族成员的启动子序列(上游2000bp)或全基因组序列+GFF。": "Please provide gene family promoter sequences (upstream 2000bp) or whole genome + GFF.", + "请提供物种基因组序列(.fasta)和GFF注释文件。": "Please provide species genome sequence (.fasta) and GFF annotation file.", + "请提供多个物种的基因组序列和GFF注释文件,以及物种间的同源关系。": "Please provide genome sequences and GFF annotation files for multiple species, along with inter-species orthology.", + "请提供染色体长度信息和基因位置信息。": "Please provide chromosome length and gene position information.", + "请提供拟南芥同源基因映射文件(ortholog_pairs.txt,格式:拟南芥ID\t苦荞ID),用于STRING数据库互作预测。如未提供,请先运行同源基因识别分析。": "Please provide Arabidopsis ortholog mapping file (ortholog_pairs.txt, format: AtID\tTargetID) for STRING interaction prediction. If unavailable, run ortholog identification first.", + "请提供目标物种和参考物种(如拟南芥At)的蛋白序列文件,用于同源基因识别。": "Please provide target and reference species (e.g., Arabidopsis At) protein FASTA for ortholog identification.", + "请提供物种间同源基因对(ortholog pairs,格式:基因A\t基因B)或共线性分析结果。": "Please provide inter-species ortholog pairs (format: GeneA\tGeneB) or synteny analysis results.", + "请提供RNA-seq表达矩阵(FPKM/TPM/Count)或原始测序数据。": "Please provide RNA-seq expression matrix (FPKM/TPM/Count) or raw sequencing data.", + "请提供差异表达基因列表(DEGs)或目标基因集。": "Please provide differentially expressed gene list (DEGs) or target gene set.", + "请提供基因家族蛋白序列文件,用于理化性质分析。": "Please provide gene family protein sequence file for physicochemical property analysis.", + "请提供基因家族HMM文件和蛋白序列文件,将自动执行标准11阶段分析。": "Please provide gene family HMM file and protein FASTA; standard 11-stage analysis will be executed automatically.", + "请提供RNA-seq原始测序数据(FASTQ格式)。": "Please provide RNA-seq raw sequencing data (FASTQ format).", + "请提供质控后的clean reads(FASTQ格式)和参考基因组。": "Please provide QC-filtered clean reads (FASTQ format) and reference genome.", + "请提供比对后的BAM文件或参考基因组注释GFF。": "Please provide aligned BAM file or reference genome annotation GFF.", + "请提供featureCounts计数矩阵和样本分组表,用于VST、PCA、样本相关性和聚类。": "Please provide featureCounts count matrix and sample grouping table for VST, PCA, sample correlation and clustering.", + "请提供多时相表达矩阵(如不同发育阶段/处理时间点)。": "Please provide multi-temporal expression matrix (e.g., different developmental stages/treatment time points).", + "请提供VST/log2表达矩阵或count矩阵和样本性状表,用于共表达网络构建。": "Please provide VST/log2 expression matrix or count matrix and sample trait table for co-expression network construction.", + "请提供差异表达结果和gene2go背景文件,或先执行背景注释。": "Please provide differential expression results and gene2go background file, or run background annotation first.", + "请提供差异表达结果和gene2ko背景文件,或先执行背景注释。": "Please provide differential expression results and gene2ko background file, or run background annotation first.", + "请提供差异表达排序统计量和gene2ko/KEGG背景,用于fgsea排序富集。": "Please provide differential expression ranking statistics and gene2ko/KEGG background for fgsea ranked enrichment.", + "请提供差异表达结果和目标物种蛋白FASTA;若已有InterProScan TSV会优先复用。": "Please provide differential expression results and target species protein FASTA; existing InterProScan TSV will be reused if available.", + "请提供表达矩阵(样本数≥6),用于共表达网络构建。": "Please provide expression matrix (samples ≥ 6) for co-expression network construction.", + # knowledge_node — system prompt / response 消息 + "你是BotanicalBaseAgent基因家族分析系统的知识助手。基于提供的文献资料,生成专业、详细的回答。": "You are a knowledge assistant for the BotanicalBaseAgent gene family analysis system. Generate professional, detailed answers based on provided literature.", + "📚 基于知识库的回答:": "📚 Knowledge base answer:", + "**参考文献**:": "**References**:", + "(来源: 本地LLM回答 + RAG辅助检索)": "(Source: local LLM answer + RAG-assisted retrieval)", + "⚠️ (本地LLM回答,未使用RAG文献检索)": "⚠️ (local LLM answer, RAG retrieval not used)", + "找到": "Found", + "条相关文献,正在生成本地LLM回答...": " related references; generating local LLM answer...", + "未找到相关文献,生成一般回答": "No related references found; generating general answer", + "你是一个专业的基因家族分析意图识别器。": "You are a professional gene family analysis intent recognizer.", + "你是一个基因家族分析专家。用户想要进行基因家族分析。": "You are a gene family analysis expert. The user wants to perform gene family analysis.", + } + + @classmethod + def _l(cls, zh_text: str, language: str = "zh") -> str: + """Return language-appropriate text from the bilingual map.""" + if language == "zh": + return zh_text + en = cls._MSG.get(zh_text) + if en is not None: + return en + # fallback: try partial match for template strings + for zh_key, en_val in cls._MSG.items(): + if zh_key in zh_text: + return zh_text.replace(zh_key, en_val) + return zh_text def __init__( self, llm_manager: LLMManager, @@ -313,6 +381,7 @@ class Orchestrator: async def _router_node(self, state: GraphState) -> dict: query = state["query"] + language = state.get("language", "zh") # 使用 RouterAgent 进行意图路由 print(f"[RouterNode] Using RouterAgent to classify query: {query[:50]}...") @@ -378,6 +447,7 @@ class Orchestrator: async def _knowledge_node(self, state: GraphState) -> dict: query = state["query"] + language = state.get("language", "zh") try: # 优先调用当前 Dual-KB QA 服务 @@ -578,7 +648,7 @@ class Orchestrator: "status": "completed", "mode": "analysis", "final_output": { - "response": f"✅ 所有分析阶段已完成!\n\n已完成: {', '.join(completed_stages)}", + "response": self._l("✅ 所有分析阶段已完成!", language) + "\n\n" + self._l("**已完成**", language) + ": " + ", ".join(completed_stages), "analysis_plan": { "gene_family": gene_family, "species": species, @@ -595,14 +665,7 @@ class Orchestrator: "status": "completed", "mode": "analysis", "final_output": { - "response": f"""📋 自动规划剩余分析阶段 - -**已完成**: {', '.join(completed_stages) if completed_stages else '无'} - -**待执行** ({len(remaining_stages)}个阶段): -{stages_text} - -请确认是否按顺序执行以上分析阶段?""", + "response": f"{self._l('📋 自动规划剩余分析阶段', language)}\n\n{self._l('**已完成**', language)}: {', '.join(completed_stages) if completed_stages else self._l('无', language)}\n\n{self._l('**待执行**', language)} ({len(remaining_stages)}{self._l('个阶段)', language)}:\n{stages_text}\n\n{self._l('请确认是否按顺序执行以上分析阶段?', language)}", "analysis_plan": { "gene_family": gene_family, "species": species, @@ -630,6 +693,7 @@ class Orchestrator: async def _analysis_node(self, state: GraphState) -> dict: query = state["query"] files = state.get("files", []) + language = state.get("language", "zh") print(f"[_analysis_node] Entered with query: {query[:50]}...", flush=True) try: @@ -821,7 +885,7 @@ class Orchestrator: "status": "completed", "mode": "analysis", "final_output": { - "response": f"我将帮您执行RNA-seq {analysis_type}分析。请确认参数后开始。", + "response": self._l("我将帮您执行RNA-seq", language) + " " + analysis_type + self._l("分析。请确认参数后开始。", language), "analysis_plan": { "gene_family": gene_family, "species": species, @@ -831,7 +895,7 @@ class Orchestrator: "prefix": f"{species}.{gene_family}", "cpu": 8 }, - "guidance": "RNA-seq分析将通过前端异步执行。" + "guidance": self._l("RNA-seq分析将通过前端异步执行。", language) } } } @@ -878,7 +942,7 @@ class Orchestrator: "status": "completed", "mode": "analysis", "final_output": { - "response": f"我将帮您执行RNA-seq {analysis_type} 分析。PlanningAgentV2 已根据当前状态生成完整执行计划。", + "response": self._l("我将帮您执行RNA-seq", language) + " " + analysis_type + self._l(" 分析。PlanningAgentV2 已根据当前状态生成完整执行计划。", language), "analysis_plan": plan_result.get("final_output", {}).get("analysis_plan", {}) } } @@ -889,7 +953,7 @@ class Orchestrator: "status": "completed", "mode": "analysis", "final_output": { - "response": f"我将帮您执行RNA-seq {analysis_type} 分析。请确认参数后开始。", + "response": self._l("我将帮您执行RNA-seq", language) + " " + analysis_type + self._l(" 分析。请确认参数后开始。", language), "analysis_plan": { "gene_family": gene_family, "species": species, @@ -900,21 +964,25 @@ class Orchestrator: "prefix": f"{species}.{gene_family}", "cpu": 8 }, - "guidance": f"RNA-seq {analysis_type} 分析将通过前端异步执行。" + "guidance": self._l("RNA-seq " + analysis_type + " 分析将通过前端异步执行。", language) } } } # Ka/Ks分析:走通用规划流程(返回analysis_plan让前端执行) # 注意:不再直接调用_execute_kaks,而是通过PlanningAgentV2规划完整流程 - guidance = guidance_map.get(analysis_type, f"请上传进行{analysis_type}分析所需的文件,或确认使用系统内置数据。") - + guidance = guidance_map.get(analysis_type) + if guidance: + guidance = self._l(guidance, language) + else: + guidance = self._l("请上传进行", language) + analysis_type + self._l("分析所需的文件,或确认使用系统内置数据。", language) + return { "current_agent": "analysis", "status": "completed", "mode": "analysis", "final_output": { - "response": f"我将帮您执行{analysis_type}分析。", + "response": self._l("我将帮您执行", language) + analysis_type + self._l("分析。", language), "analysis_plan": { "gene_family": gene_family, "species": species,