多语言散落补全
This commit is contained in:
+143
-143
@@ -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'));
|
||||
};
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
|
||||
@@ -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"] = []
|
||||
|
||||
@@ -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\"?"
|
||||
};
|
||||
|
||||
|
||||
@@ -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.<sample>.library_strandness 中明确填写 non_stranded / forward / reverse;混合链特异性样本当前不能用单次 featureCounts 定量。",
|
||||
"suggestion_zh": "请在 samples.yaml 的 rnaseq.library_strandness 或每个 samples.<sample>.library_strandness 中明确填写 non_stranded / forward / reverse;混合链特异性样本当前不能用单次 featureCounts 定量。",
|
||||
"suggestion_en": "Please specify non_stranded / forward / reverse in samples.yaml rnaseq.library_strandness or each samples.<sample>.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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user