diff --git a/apps/web/vue3/index_ml.html b/apps/web/vue3/index_ml.html
index ef98e8dcd..0f5967946 100644
--- a/apps/web/vue3/index_ml.html
+++ b/apps/web/vue3/index_ml.html
@@ -7216,7 +7216,8 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓
session_id: this.sessionId,
job_id: window.parentJobInfo ? window.parentJobInfo.job_id : 'default_job',
prefix: normalizedPrefix(window.parentParams ? window.parentParams.prefix : ''),
- model: 'kimi'
+ model: 'kimi',
+ language: window.__CROPAI_LANG__ || 'zh'
})
});
@@ -8619,7 +8620,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓
ElMessage.success(autofillState.type === 'config' ? '已写入正式 config.xlsx' : '已写入正式 samples.yaml');
autofillDialogVisible.value = false;
if (autofillState.type === 'config' && data.resume_goal) {
- await addAgentMessage('assistant', `config.xlsx 已确认,继续当前目标:${data.resume_goal}`);
+ await addAgentMessage('assistant', t('config.xlsx 已确认,继续当前目标:') + data.resume_goal);
}
};
const loadFromPath = async (options = {}) => {
@@ -9548,7 +9549,8 @@ compare: [[flesh, peel]]`;
include_previous: true,
mode: 'comprehensive',
use_rag_support: Boolean(options.useRagSupport),
- refresh_rag: Boolean(options.refreshRag)
+ refresh_rag: Boolean(options.refreshRag),
+ language: currentLang.value
})
});
const data = await res.json();
@@ -12350,14 +12352,14 @@ compare: [[flesh, peel]]`;
};
const executePerSpeciesEvidenceStage = async (stageId, baseParams = {}, executor) => {
const speciesList = subfamilyEvidenceFanoutSpecies(baseParams);
- addAgentMessage('assistant', `按 per_species 拆分 ${getStageLabel(stageId)}:${speciesList.join(', ')}`);
+ addAgentMessage('assistant', t('按 per_species 拆分 ') + getStageLabel(stageId) + ':' + speciesList.join(', '));
const results = [];
for (const species of speciesList) {
if (shouldStopBatchExecute.value) {
throw new Error('Batch execution stopped');
}
const speciesParams = perSpeciesEvidenceParams(stageId, baseParams, species);
- addAgentMessage('assistant', `⚙️ ${getStageLabel(stageId)} (${species})...`);
+ addAgentMessage('assistant', '⚙️ ' + getStageLabel(stageId) + ' (' + species + ')...');
const result = await executor(stageId, speciesParams);
results.push({ species, result });
}
@@ -13110,7 +13112,7 @@ compare: [[flesh, peel]]`;
autoAnnotationConfirmData.value = payload || {};
autoAnnotationCountdown.value = 40;
autoAnnotationDialogVisible.value = true;
- addAgentMessage('assistant', `⚠️ 检测到 ${autoAnnotationBackgroundTitle(payload)},将在确认后启动本地自动注释。`);
+ addAgentMessage('assistant', t('⚠️ 检测到 ') + autoAnnotationBackgroundTitle(payload) + t(',将在确认后启动本地自动注释。'));
return new Promise((resolve) => {
autoAnnotationResolve = resolve;
autoAnnotationTimer = setInterval(() => {
@@ -13796,13 +13798,13 @@ compare: [[flesh, peel]]`;
stageExecutionStatus.value[stageId] = 'pending';
});
if (isBatchExecuting.value) {
- addAgentMessage('assistant', `🔄 ${reason || '参考数据已就绪'};Ortholog/PPI/KaKs 已恢复为待执行,将随当前批次继续调度。`);
+ addAgentMessage('assistant', '🔄 ' + (reason || t('参考数据已就绪')) + t(';Ortholog/PPI/KaKs 已恢复为待执行,将随当前批次继续调度。'));
return;
}
isBatchExecuting.value = true;
shouldStopBatchExecute.value = false;
try {
- addAgentMessage('assistant', `🔄 ${reason || '参考数据已就绪'};开始续跑 Ortholog/PPI/KaKs。`);
+ addAgentMessage('assistant', '🔄 ' + (reason || t('参考数据已就绪')) + t(';开始续跑 Ortholog/PPI/KaKs。'));
await executeStagesWithDependencies(retryStages, new Set(retryStages.map(stage => getStageId(stage)).filter(Boolean)));
} finally {
isBatchExecuting.value = false;
@@ -13814,7 +13816,7 @@ compare: [[flesh, peel]]`;
const decision = await requestPpiDownloadFallbackDecision(err, resolution, recommendation, requestedStages);
if (decision === 'ath') {
applyArabidopsisFallbackForPpiKaks(requestedStages, err?.message || String(err || 'reference_bundle_download_failed'));
- addAgentMessage('assistant', '✅ 已确认使用拟南芥作为 PPI/KaKs 的显式兜底参考;该选择只写入 Ortholog/PPI/KaKs 专用参数,其它常规阶段不受影响。');
+ addAgentMessage('assistant', t('✅ 已确认使用拟南芥作为 PPI/KaKs 的显式兜底参考;该选择只写入 Ortholog/PPI/KaKs 专用参数,其它常规阶段不受影响。'));
if (requestedStages.includes('ppi')) {
const requestBody = buildPpiKaksDependencyRequest(['ppi']);
await orchestratePpiOwnerAfterReferenceReady({
@@ -14083,7 +14085,7 @@ compare: [[flesh, peel]]`;
mergePpiReferenceSpeciesIntoBatch(refCode, ['kaks']);
}
clearReferenceBundleDownloadProgress();
- addAgentMessage('assistant', `✅ 推荐参考数据下载完成:${recommendation.species_name || result.species_name || refCode} (${refCode || 'reference'})。已写入 Ortholog/PPI/Ka/Ks 专用参考物种。`);
+ addAgentMessage('assistant', t('✅ 推荐参考数据下载完成:') + (recommendation.species_name || result.species_name || refCode) + ' (' + (refCode || 'reference') + t(')。已写入 Ortholog/PPI/Ka/Ks 专用参考物种。'));
if (requestedStages.includes('ppi')) {
const requestBody = buildPpiKaksDependencyRequest(['ppi']);
await orchestratePpiOwnerAfterReferenceReady({
@@ -14370,11 +14372,11 @@ compare: [[flesh, peel]]`;
const stageIds = stages.map(stage => getStageId(stage)).filter(Boolean);
const recoveredReferenceApplied = applyResolvedPpiReferenceToBatch(resolution, requestedStages, requestBody);
if (recoveredReferenceApplied) {
- addAgentMessage('assistant', `✅ PPI/KaKs 前置数据检查通过:检测到已下载近源参考 ${resolution.reference_species},将用该参考先生成 Ortholog mapping。`);
+ addAgentMessage('assistant', t('✅ PPI/KaKs 前置数据检查通过:检测到已下载近源参考 ') + resolution.reference_species + t(',将用该参考先生成 Ortholog mapping。'));
}
const mappingLabel = ppiOrthologMappingLabel(resolution);
if (mappingLabel && resolution?.status !== 'needs_ortholog') {
- addAgentMessage('assistant', `✅ PPI/KaKs 前置数据检查通过:检测到已有 Ortholog 映射(${mappingLabel}),将复用该映射执行 ${requestedStages.join('、')}。`);
+ addAgentMessage('assistant', t('✅ PPI/KaKs 前置数据检查通过:检测到已有 Ortholog 映射(') + mappingLabel + t('),将复用该映射执行 ') + requestedStages.join('、') + '。');
return true;
}
attachOrthologDependencyToOwners(requestedStages);
@@ -14392,7 +14394,7 @@ compare: [[flesh, peel]]`;
: stage
));
}
- addAgentMessage('assistant', '✅ PPI/KaKs 前置数据检查通过:当前批次已有 Ortholog,将先生成同源映射再继续下游分析。');
+ addAgentMessage('assistant', t('✅ PPI/KaKs 前置数据检查通过:当前批次已有 Ortholog,将先生成同源映射再继续下游分析。'));
return true;
}
const insertAt = Math.max(0, stages.findIndex(stage => ppiKaksDependencyStages.has(getStageId(stage))));
@@ -14422,7 +14424,7 @@ compare: [[flesh, peel]]`;
const reason = resolution?.issue_code === 'stale_ortholog_reference_conflict'
? '已有 Ortholog 映射与当前 PPI/KaKs 参考物种不一致'
: '缺少可用同源映射';
- addAgentMessage('assistant', `✅ PPI/KaKs 前置数据检查通过:${reason},已自动插入 Ortholog,将先生成 mapping 再执行 ${requestedStages.join('、')}。`);
+ addAgentMessage('assistant', t('✅ PPI/KaKs 前置数据检查通过:') + reason + t(',已自动插入 Ortholog,将先生成 mapping 再执行 ') + requestedStages.join('、') + '。');
return true;
};
@@ -14484,12 +14486,12 @@ compare: [[flesh, peel]]`;
return { ...base, params: { ...(base.params || {}), ppiSpecies: explicitPpiSpecies } };
});
clearPpiKaksReferencePause(requestedStages);
- addAgentMessage('assistant', `✅ 已选择项目参考物种 ${projectReference};将按该参考检查或生成 Ortholog,并执行 ${requestedStages.join('、')}。`);
+ addAgentMessage('assistant', t('✅ 已选择项目参考物种 ') + projectReference + t(';将按该参考检查或生成 Ortholog,并执行 ') + requestedStages.join('、') + '。');
return true;
}
if (decision === 'download') {
try {
- addAgentMessage('assistant', '⬇️ 正在提交推荐近源参考物种注释包后台下载;Ortholog/PPI/KaKs 将等待参考数据,其它阶段继续执行。');
+ addAgentMessage('assistant', t('⬇️ 正在提交推荐近源参考物种注释包后台下载;Ortholog/PPI/KaKs 将等待参考数据,其它阶段继续执行。'));
const download = await downloadRecommendedPpiReferenceBundle(preflight.resolution, requestedStages);
if (download.task?.task_id) {
const recommendation = download.recommendation || {};
@@ -14503,7 +14505,7 @@ compare: [[flesh, peel]]`;
reference_download_task_id: download.task.task_id
}, pauseMessage);
monitorPpiReferenceBundleDownload(download.task.task_id, recommendation, requestedStages, preflight.resolution);
- addAgentMessage('assistant', `⏳ 推荐参考数据下载已转入后台:${recommendation.species_name || recommendation.species_code || 'reference'},task_id=${download.task.task_id}。`);
+ addAgentMessage('assistant', t('⏳ 推荐参考数据下载已转入后台:') + (recommendation.species_name || recommendation.species_code || 'reference') + t(',task_id=') + download.task.task_id + '。');
return true;
}
const recommendation = download.recommendation || {};
@@ -14516,7 +14518,7 @@ compare: [[flesh, peel]]`;
if (ownerStageId === 'kaks' && refCode) {
mergePpiReferenceSpeciesIntoBatch(refCode, ['kaks']);
}
- addAgentMessage('assistant', `✅ 推荐参考数据下载完成:${recommendation.species_name || refCode} (${refCode})。已写入 Ortholog/PPI/Ka/Ks 专用参考物种。`);
+ addAgentMessage('assistant', t('✅ 推荐参考数据下载完成:') + (recommendation.species_name || refCode) + ' (' + refCode + t(')。已写入 Ortholog/PPI/Ka/Ks 专用参考物种。'));
if (ownerStageId === 'ppi') {
await orchestratePpiOwnerAfterReferenceReady({
requestBody,
@@ -14547,7 +14549,7 @@ compare: [[flesh, peel]]`;
}
const pauseMessage = formatPpiSpeciesResolutionBlockMessage(preflight.resolution);
storePpiKaksReferencePause(requestedStages, preflight.resolution, pauseMessage);
- addAgentMessage('assistant', `⏸️ 已按选择先暂停 Ortholog/PPI/KaKs,其它阶段继续执行。\n${pauseMessage}`);
+ addAgentMessage('assistant', t('⏸️ 已按选择先暂停 Ortholog/PPI/KaKs,其它阶段继续执行。') + '\n' + pauseMessage);
return true;
}
if (!preflight.allowed) return false;
@@ -14556,7 +14558,7 @@ compare: [[flesh, peel]]`;
if (preflight.resolution?.status !== 'needs_ortholog') {
const mappingLabel = ppiOrthologMappingLabel(preflight.resolution);
if (preflight.resolution?.status === 'ready' && mappingLabel && !stageIds.includes('ortholog')) {
- addAgentMessage('assistant', `✅ PPI/KaKs 前置数据检查通过:检测到已有 Ortholog 映射(${mappingLabel}),将直接执行 ${requestedStages.join('、')}。`);
+ addAgentMessage('assistant', t('✅ PPI/KaKs 前置数据检查通过:检测到已有 Ortholog 映射(') + mappingLabel + t('),将直接执行 ') + requestedStages.join('、') + '。');
}
return true;
}
@@ -14637,10 +14639,10 @@ compare: [[flesh, peel]]`;
: (stageId === 'rnaseq_kegg_enrich' ? 'gene2ko' : 'gene2go');
const accepted = await requestAutoAnnotationConfirmation(annotationPayload);
if (!accepted) throw createStructuredExecuteError({ ...data, message: `已取消自动注释,${getStageLabel(stageId)} 未提交。` });
- addAgentMessage('assistant', `✅ 已确认自动注释,正在重新提交 ${getStageLabel(stageId)} 到后端;系统会先生成 ${backgroundLabel},再继续富集分析。`);
+ addAgentMessage('assistant', t('✅ 已确认自动注释,正在重新提交 ') + getStageLabel(stageId) + t(' 到后端;系统会先生成 ') + backgroundLabel + t(',再继续富集分析。'));
data = await submit({ ...requestBody, enrichBackgroundSource: 'annotation', confirmedAutoAnnotation: true });
if (data.success && data.task_id) {
- addAgentMessage('assistant', `🧬 自动背景注释与 ${getStageLabel(stageId)} 已提交后端,task_id=${data.task_id}`);
+ addAgentMessage('assistant', t('🧬 自动背景注释与 ') + getStageLabel(stageId) + t(' 已提交后端,task_id=') + data.task_id);
}
}
return data;
@@ -14816,7 +14818,7 @@ compare: [[flesh, peel]]`;
return true;
} catch (err) {
console.error(`[${errorPrefix}] failed:`, err);
- addAgentMessage('assistant', `⚠️ ${errorPrefix}:${err.message || err}`);
+ addAgentMessage('assistant', '⚠️ ' + errorPrefix + ':' + (err.message || err));
return false;
}
};
@@ -14865,7 +14867,7 @@ compare: [[flesh, peel]]`;
|| previousPlanParams.postConfirmMessage
|| fallbackMessage;
try {
- addAgentMessage('assistant', '🔁 亚家族标签已确认,正在重新规划确认后常规基因家族分析...');
+ addAgentMessage('assistant', t('🔁 亚家族标签已确认,正在重新规划确认后常规基因家族分析...'));
const response = await fetch('/api/agent/plan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -14893,7 +14895,7 @@ compare: [[flesh, peel]]`;
}
const workflowPhase = String(plan.workflowPhase || plan.params?.workflowPhase || planData.intent?.workflowPhase || '');
if (workflowPhase && workflowPhase !== 'post_subfamily_confirm' && workflowPhase !== 'custom_scope') {
- addAgentMessage('assistant', `亚家族确认已完成,但后端当前 workflowPhase 为 ${workflowPhase},未自动打开常规分析。`);
+ addAgentMessage('assistant', t('亚家族确认已完成,但后端当前 workflowPhase 为 ') + workflowPhase + t(',未自动打开常规分析。'));
return false;
}
const completedSet = new Set(planData.completed_stages || []);
@@ -14906,7 +14908,7 @@ compare: [[flesh, peel]]`;
return stageId && !completedSet.has(stageId) && !blocked;
});
if (!activeExecutionStages.length) {
- addAgentMessage('assistant', '确认后常规分析没有新的可执行阶段。');
+ addAgentMessage('assistant', t('确认后常规分析没有新的可执行阶段。'));
return false;
}
@@ -14936,12 +14938,12 @@ compare: [[flesh, peel]]`;
await loadInterspeciesSpeciesOptions();
}
batchExecuteDialogVisible.value = true;
- addAgentMessage('assistant', `⚙️ 亚家族确认后已规划 ${activeExecutionStages.length} 个常规分析阶段,请确认参数`);
+ addAgentMessage('assistant', t('⚙️ 亚家族确认后已规划 ') + activeExecutionStages.length + t(' 个常规分析阶段,请确认参数'));
startBatchExecuteTimer();
return true;
} catch (err) {
console.error('[contextual post-confirm continuation] failed:', err);
- addAgentMessage('assistant', `⚠️ 亚家族确认后自动续跑规划失败:${err.message || err}`);
+ addAgentMessage('assistant', t('⚠️ 亚家族确认后自动续跑规划失败:') + (err.message || err));
return false;
}
};
@@ -15054,7 +15056,7 @@ compare: [[flesh, peel]]`;
normalizeBatchExecutionSpeciesForStages();
const dependencyPreflightOk = await ensureOrthologProducerForPpiKaks();
if (!dependencyPreflightOk) {
- addAgentMessage('assistant', '⏸️ PPI/KaKs 前置数据检查未通过,已暂停批量执行;请补齐参考注释包或确认参考物种后重新执行。');
+ addAgentMessage('assistant', t('⏸️ PPI/KaKs 前置数据检查未通过,已暂停批量执行;请补齐参考注释包或确认参考物种后重新执行。'));
return;
}
isBatchExecuting.value = true;
@@ -15063,7 +15065,7 @@ compare: [[flesh, peel]]`;
const stages = batchExecuteStages.value;
const plannedStageIds = new Set(stages.map(stage => getStageId(stage)).filter(Boolean));
- addAgentMessage('assistant', `🚀 准备提交依赖驱动执行,共 ${stages.length} 个阶段...`);
+ addAgentMessage('assistant', t('🚀 准备提交依赖驱动执行,共 ') + stages.length + t(' 个阶段...'));
// 初始化阶段状态
stageExecutionStatus.value = {};
@@ -15135,11 +15137,11 @@ compare: [[flesh, peel]]`;
const totalCount = stages.length;
if (failedCount > 0) {
- addAgentMessage('assistant', `⚠️ 批量执行完成:成功 ${completedCount}/${totalCount},失败 ${failedCount}/${totalCount}`);
+ addAgentMessage('assistant', t('⚠️ 批量执行完成:成功 ') + completedCount + '/' + totalCount + t(',失败 ') + failedCount + '/' + totalCount);
} else if (pausedCount > 0) {
addAgentMessage('assistant', pausedStageSummaryText(stages, completedCount, totalCount));
} else {
- addAgentMessage('assistant', `🎉 批量执行完成!成功: ${completedCount}/${totalCount}`);
+ addAgentMessage('assistant', t('🎉 批量执行完成!成功: ') + completedCount + '/' + totalCount);
if (isMethodologyDataPreparationContinuationCandidate(stages, completedCount, totalCount, failedCount, pausedCount)) {
await openMethodologyDataPreparationContinuation();
} else if (isMethodologyPostConfirmContinuationCandidate(stages, completedCount, totalCount, failedCount, pausedCount)) {
@@ -15268,7 +15270,7 @@ compare: [[flesh, peel]]`;
if (pausedDeps.length > 0) {
const pausedNames = pausedDeps.map(s => typeof s === 'object' ? (s.name || getStageLabel(getStageAction(s))) : s).join(', ');
- addAgentMessage('assistant', `⏸️ 以下阶段等待上游暂停项处理后继续: ${pausedNames}`);
+ addAgentMessage('assistant', t('⏸️ 以下阶段等待上游暂停项处理后继续: ') + pausedNames);
pausedDeps.forEach(stage => {
const stageId = typeof stage === 'object' ? stage.id : stage;
stageExecutionStatus.value[stageId] = 'paused';
@@ -15297,7 +15299,7 @@ compare: [[flesh, peel]]`;
if (failedDeps.length > 0) {
const failedNames = failedDeps.map(s => typeof s === 'object' ? (s.name || getStageLabel(getStageAction(s))) : s).join(', ');
- addAgentMessage('assistant', `⚠️ 以下阶段因依赖失败被跳过: ${failedNames}`);
+ addAgentMessage('assistant', t('⚠️ 以下阶段因依赖失败被跳过: ') + failedNames);
failedDeps.forEach(stage => {
const stageId = typeof stage === 'object' ? stage.id : stage;
stageExecutionStatus.value[stageId] = 'failed';
@@ -15329,7 +15331,7 @@ compare: [[flesh, peel]]`;
stageExecutionStatus.value[stageId] = "failed";
}
});
- addAgentMessage("assistant", "亚家族标签确认被跳过,已停止依赖 confirmed_subfamily.tsv 的后续阶段;如需普通无亚家族标签分析,请重新发起并明确说明“不需要亚家族”。");
+ addAgentMessage("assistant", t('亚家族标签确认被跳过,已停止依赖 confirmed_subfamily.tsv 的后续阶段;如需普通无亚家族标签分析,请重新发起并明确说明"不需要亚家族"。'));
};
const confirmedStageParamsForExecution = (stageId, stageParams = {}) => {
@@ -15454,7 +15456,7 @@ compare: [[flesh, peel]]`;
if (!shouldExecute) {
console.log(`[DEBUG-STAGE] ${stageId}: user skipped`);
- addAgentMessage('assistant', `⏭️ 跳过阶段: ${stageName}`);
+ addAgentMessage('assistant', t('⏭️ 跳过阶段: ') + stageName);
if (stageAction === "subfamily_confirm") {
stopBatchAfterSkippedSubfamilyConfirm();
stageExecutionStatus.value[stageId] = "failed";
@@ -15474,7 +15476,7 @@ compare: [[flesh, peel]]`;
return;
}
- addAgentMessage('assistant', `⚙️ 提交阶段: ${stageName}...`);
+ addAgentMessage('assistant', t('⚙️ 提交阶段: ') + stageName + '...');
// 执行阶段
console.log(`[DEBUG-STAGE] ${stageId}: calling executeSingleStageWithParams`);
@@ -15535,7 +15537,7 @@ compare: [[flesh, peel]]`;
reason: 'methodology_member_review_required',
message: 'identify 已完成,但方法学链路需要先确认基因家族成员表'
});
- addAgentMessage('assistant', '⏸️ identify 已完成,但方法学链路需要先确认基因家族成员表;保存成员确认后再继续 phylogeny。');
+ addAgentMessage('assistant', t('⏸️ identify 已完成,但方法学链路需要先确认基因家族成员表;保存成员确认后再继续 phylogeny。'));
return;
}
clearStagePause(stageId);
@@ -15557,7 +15559,7 @@ compare: [[flesh, peel]]`;
reason: 'confirmed_subfamily_missing',
message: err.message || '亚家族标签尚未确认'
});
- addAgentMessage('assistant', `⏸️ ${err.message} 已暂停依赖 confirmed_subfamily.tsv 的后续阶段;请在亚家族确认表中保存后重新拉取规划继续执行。`);
+ addAgentMessage('assistant', '⏸️ ' + err.message + t(' 已暂停依赖 confirmed_subfamily.tsv 的后续阶段;请在亚家族确认表中保存后重新拉取规划继续执行。'));
return;
}
if (await openIdentifyMemberReviewFromBlockedStage(
@@ -15581,14 +15583,14 @@ compare: [[flesh, peel]]`;
message: pauseMessage,
resolution
});
- addAgentMessage('assistant', `⏸️ ${stageName} 暂停:需要先准备参考物种 PEP/CDS 或 Ortholog 映射。\n${pauseMessage}`);
+ addAgentMessage('assistant', t('⏸️ ') + stageName + t(' 暂停:需要先准备参考物种 PEP/CDS 或 Ortholog 映射。') + '\n' + pauseMessage);
return;
}
if (err.status === 'preflight_failed') {
shouldStopBatchExecute.value = true;
stageExecutionStatus.value[stageId] = 'failed';
addAgentMessage('assistant', await showFailureDiagnosis(stageAction, err, { fallbackMessage: err.message }));
- addAgentMessage('assistant', '⏹️ 后端预检失败,当前批次已停止;请重新拉取规划后继续。');
+ addAgentMessage('assistant', t('⏹️ 后端预检失败,当前批次已停止;请重新拉取规划后继续。'));
console.error('[ERROR] Stage preflight failed:', stageId, err);
return;
}
@@ -15601,13 +15603,13 @@ compare: [[flesh, peel]]`;
if (wgsFallbackResult) {
stageExecutionStatus.value[stageId] = 'completed';
clearStagePause(stageId);
- addAgentMessage('assistant', `✅ 阶段 ${stageName} 已按用户确认使用 bcftools 完成`);
+ addAgentMessage('assistant', t('✅ 阶段 ') + stageName + t(' 已按用户确认使用 bcftools 完成'));
return;
}
const shouldRetryWithFallback = await maybePromptEnrichBackgroundFallback(err.message, stageAction);
if (shouldRetryWithFallback) {
try {
- addAgentMessage('assistant', `🔄 阶段 ${getStageLabel(stageAction)} 重新提交(自动注释背景)...`);
+ addAgentMessage('assistant', t('🔄 阶段 ') + getStageLabel(stageAction) + t(' 重新提交(自动注释背景)...'));
const retryParams = confirmedStageParamsForExecution(stageAction, typeof stage === 'object' ? (stage.params || {}) : {});
const retryResult = await executeSingleStageWithParams(stageAction, retryParams);
addInterpretationButton(stageAction, stageName, {
@@ -15625,7 +15627,7 @@ compare: [[flesh, peel]]`;
reason: 'methodology_member_review_required',
message: 'identify 已完成,但方法学链路需要先确认基因家族成员表'
});
- addAgentMessage('assistant', '⏸️ identify 已完成,但方法学链路需要先确认基因家族成员表;保存成员确认后再继续 phylogeny。');
+ addAgentMessage('assistant', t('⏸️ identify 已完成,但方法学链路需要先确认基因家族成员表;保存成员确认后再继续 phylogeny。'));
return;
}
clearStagePause(stageId);
@@ -16124,7 +16126,7 @@ compare: [[flesh, peel]]`;
if (!data.success) {
throw createStructuredExecuteError(data);
}
- addAgentMessage('assistant', '进化树图重绘已提交:task_id=' + (data.task_id || 'sync'));
+ addAgentMessage('assistant', t('进化树图重绘已提交:task_id=') + (data.task_id || 'sync'));
if (data.task_id) {
activePipelineTaskIds.value = [...activePipelineTaskIds.value, data.task_id];
try {
@@ -16148,7 +16150,7 @@ compare: [[flesh, peel]]`;
console.error('[Tree Replot] submit failed:', err);
const msg = err && err.message ? err.message : String(err);
ElMessage.error('进化树图重绘失败:' + msg);
- addAgentMessage('assistant', '进化树图重绘失败:' + msg);
+ addAgentMessage('assistant', t('进化树图重绘失败:') + msg);
return null;
} finally {
isExecuting.value = false;
@@ -16251,7 +16253,7 @@ compare: [[flesh, peel]]`;
if (!data.success) {
throw createStructuredExecuteError(data);
}
- addAgentMessage('assistant', `Combo 图修订已提交:task_id=${data.task_id || 'sync'};panels=${panels.join(',')}`);
+ addAgentMessage('assistant', t('Combo 图修订已提交:task_id=') + (data.task_id || 'sync') + ';panels=' + panels.join(','));
if (data.task_id) {
activePipelineTaskIds.value = [...activePipelineTaskIds.value, data.task_id];
try {
@@ -16272,7 +16274,7 @@ compare: [[flesh, peel]]`;
console.error('[Combo Replot] submit failed:', err);
const msg = err && err.message ? err.message : String(err);
ElMessage.error('Combo 图修订失败:' + msg);
- addAgentMessage('assistant', 'Combo 图修订失败:' + msg);
+ addAgentMessage('assistant', t('Combo 图修订失败:') + msg);
return null;
} finally {
isExecuting.value = false;
@@ -16452,7 +16454,7 @@ compare: [[flesh, peel]]`;
batchExecutionUserConfirmed.value = false;
batchExecuteDialogVisible.value = false;
isBatchExecuting.value = false;
- addAgentMessage('assistant', '❌ 批量执行已取消');
+ addAgentMessage('assistant', t('❌ 批量执行已取消'));
};
const prepareGwasConfirmation = (params = {}) => {
@@ -17484,7 +17486,7 @@ compare: [[flesh, peel]]`;
if (data.task_id) {
// 异步任务,轮询状态。只有拿到 task_id 后才认为后端已接收。
console.log(`[DEBUG-EXEC] ${stageName}: async task, task_id=${data.task_id}`);
- addAgentMessage('assistant', `🚀 阶段 ${getStageLabel(stageName)} 已提交后端,task_id=${data.task_id}`);
+ addAgentMessage('assistant', t('🚀 阶段 ') + getStageLabel(stageName) + t(' 已提交后端,task_id=') + data.task_id);
activePipelineTaskIds.value = [...activePipelineTaskIds.value, data.task_id];
let result;
try {
@@ -17914,7 +17916,7 @@ compare: [[flesh, peel]]`;
if (!recoveryNoticeShown) {
recoveryNoticeShown = true;
console.warn(`[DEBUG-POLL] taskId=${taskId} polling interrupted, switching to manifest recovery:`, err);
- addAgentMessage('assistant', `⚠️ ${getStageLabel(stageName)} 前端连接短暂中断,正在按后端产物状态恢复,不会立即判定阶段失败`);
+ addAgentMessage('assistant', '⚠️ ' + getStageLabel(stageName) + t(' 前端连接短暂中断,正在按后端产物状态恢复,不会立即判定阶段失败'));
}
setTimeout(checkStatus, 5000);
};
@@ -18389,7 +18391,7 @@ compare: [[flesh, peel]]`;
const confirmed = await maybeShowSubfamilyReview('reference_classification', true, prefix, true);
openedAny = openedAny || confirmed;
if (!confirmed) {
- addAgentMessage('assistant', `${prefix} 亚家族分类已完成预测,但确认表尚未保存;后续会继续显示为待确认。`);
+ addAgentMessage('assistant', prefix + t(' 亚家族分类已完成预测,但确认表尚未保存;后续会继续显示为待确认。'));
}
}
return openedAny;
@@ -18430,7 +18432,7 @@ compare: [[flesh, peel]]`;
reason: 'curated_members_missing',
message: '亚家族证据阶段需要先保存 identify 成员初筛确认表'
});
- addAgentMessage('assistant', '⏸️ 已打开 identify 成员初筛确认表;保存后会生成 curated_members,再继续亚家族证据阶段。');
+ addAgentMessage('assistant', t('⏸️ 已打开 identify 成员初筛确认表;保存后会生成 curated_members,再继续亚家族证据阶段。'));
return true;
};
@@ -19314,9 +19316,9 @@ compare: [[flesh, peel]]`;
refreshInfo();
const opened = await queueAutofillFromPayload(response, 'upload');
if (opened === 'samples') {
- addAgentMessage('assistant', '已根据上传文件生成 samples.yaml 草稿。请在弹窗中核对 run、sample_id、condition、replicate、链特异性和分组角色,确认后再写入正式 samples.yaml。');
+ addAgentMessage('assistant', t('已根据上传文件生成 samples.yaml 草稿。请在弹窗中核对 run、sample_id、condition、replicate、链特异性和分组角色,确认后再写入正式 samples.yaml。'));
} else if (opened === 'config') {
- addAgentMessage('assistant', '已根据上传文件生成 config.xlsx 草稿。请在弹窗中核对物种代码、文件类型、物种角色和 Gene_Family,确认后再写入正式 config.xlsx。');
+ addAgentMessage('assistant', t('已根据上传文件生成 config.xlsx 草稿。请在弹窗中核对物种代码、文件类型、物种角色和 Gene_Family,确认后再写入正式 config.xlsx。'));
}
};
const handleUploadError = (err) => {
@@ -19451,7 +19453,7 @@ compare: [[flesh, peel]]`;
};
const extensionProgressMessage = async (message, type = 'info') => {
pushExtensionStatus(message, type, false);
- await addAgentMessage('assistant', `拓展分析阶段:${message}`);
+ await addAgentMessage('assistant', t('拓展分析阶段:') + message);
};
const summarizeExtensionRunError = (run, fallback) => {
const error = String(run?.error || run?.stderr || fallback || '执行失败').trim();
@@ -19595,12 +19597,12 @@ compare: [[flesh, peel]]`;
const lightweightErrorText = (view, fallback) => {
const error = view && view.error || {};
const phase = error.phase || view?.action || 'operation';
- const message = error.message || fallback || '操作失败';
- return `阶段:${phase}\n原因:${message}`;
+ const message = error.message || fallback || t('操作失败');
+ return t('阶段:') + phase + '\n' + t('原因:') + message;
};
const lightweightHelp = async (extensionId) => {
const response = await extensionApiFetch(`/api/lightweight-extension-stages/${encodeURIComponent(extensionId)}/help`);
- if (!response.ok) throw new Error(await extensionApiError(response, '读取拓展分析说明失败'));
+ if (!response.ok) throw new Error(await extensionApiError(response, t('读取拓展分析说明失败')));
return await response.json();
};
const isLightweightExtensionId = (extensionId) => (lightweightExtensions.value || []).some(item => item.extension_id === extensionId);
@@ -19610,8 +19612,8 @@ compare: [[flesh, peel]]`;
review: 'publish', publish: 'list', run: 'runtime_result'
})[action] || '';
const lightweightCandidateConfirmationSummary = (card) => card.revision_summary
- ? `✓ 已确认候选|已修订:${card.revision_summary}`
- : '✓ 已确认候选|未作修订';
+ ? t('✓ 已确认候选|已修订:') + card.revision_summary
+ : t('✓ 已确认候选|未作修订');
const lightweightParameterText = (item) => {
const label = item.flag || item.fact_id || '';
const value = item.value ?? item.default_value ?? item.default ?? '';
@@ -19621,10 +19623,10 @@ compare: [[flesh, peel]]`;
const projection = run.projection || {};
const stdout = projection.stdout || {};
return [
- { item: '实际命令', value: run.command || run.execution_command || '见执行日志' },
- { item: '输出文件', value: (projection.files || []).map(item => item.path).join(';') || '(无文件输出)' },
- { item: '标准输出', value: stdout.present ? (stdout.text || '') : '(无)' },
- { item: '日志', value: Object.values(run.logs || {}).join(';') || '(无)' }
+ { item: t('实际命令'), value: run.command || run.execution_command || t('见执行日志') },
+ { item: t('输出文件'), value: (projection.files || []).map(item => item.path).join(';') || t('(无文件输出)') },
+ { item: t('标准输出'), value: stdout.present ? (stdout.text || '') : t('(无)') },
+ { item: t('日志'), value: Object.values(run.logs || {}).join(';') || t('(无)') }
];
};
const lightweightRows = (help) => {
@@ -19632,11 +19634,11 @@ compare: [[flesh, peel]]`;
const parameters = (help.parameters || []).map(lightweightParameterText).filter(Boolean);
const outputFiles = ((help.outputs || {}).files || []).map(item => item.path).filter(Boolean);
return [
- { item: '功能', value: help.function_summary || '-' },
- { item: '原始命令', value: help.original_command || '-' },
- { item: '输入文件', value: inputs.join(';') || '(无)' },
- { item: '可改参数', value: parameters.join(';') || '(无)' },
- { item: '输出', value: outputFiles.join(';') || (((help.outputs || {}).stdout || {}).present ? '标准输出(直接展示)' : '(无)') }
+ { item: t('功能'), value: help.function_summary || '-' },
+ { item: t('原始命令'), value: help.original_command || '-' },
+ { item: t('输入文件'), value: inputs.join(';') || t('(无)') },
+ { item: t('可改参数'), value: parameters.join(';') || t('(无)') },
+ { item: t('输出'), value: outputFiles.join(';') || (((help.outputs || {}).stdout || {}).present ? t('标准输出(直接展示)') : t('(无)')) }
];
};
const setLightweightRunning = (extensionId, operationToken) => {
@@ -19684,7 +19686,7 @@ compare: [[flesh, peel]]`;
const poll = async () => {
try {
const response = await extensionApiFetch(`/api/lightweight-extension-stages/operations/${encodeURIComponent(operationToken)}`);
- if (!response.ok) throw new Error(await extensionApiError(response, '读取拓展分析状态失败'));
+ if (!response.ok) throw new Error(await extensionApiError(response, t('读取拓展分析状态失败')));
const view = await response.json();
const terminal = ['completed', 'failed'].includes(String(view.state || ''));
if (!terminal) {
@@ -19698,14 +19700,14 @@ compare: [[flesh, peel]]`;
card.loading = false;
if (view.outcome === 'failed' || view.error || view.state === 'failed') {
card.status = 'failed';
- card.status_label = '失败';
+ card.status_label = t('失败');
card.type = 'danger';
card.error = lightweightErrorText(view);
card.logs = view.logs || (view.error && view.error.logs) || [];
if (String(view?.error?.phase || '') === 'semantic_candidate') {
- card.title = '拓展分析说明生成失败';
- card.purpose = '原始命令、测试数据和执行事实已经保留;这里只重新生成说明,不会重新运行原始命令。';
- card.summary = '可以直接重试,或取消后检查错误原因。';
+ card.title = t('拓展分析说明生成失败');
+ card.purpose = t('原始命令、测试数据和执行事实已经保留;这里只重新生成说明,不会重新运行原始命令。');
+ card.summary = t('可以直接重试,或取消后检查错误原因。');
card.actions = { retry_semantic: true, cancel: true };
}
resolve(view);
@@ -19719,9 +19721,9 @@ compare: [[flesh, peel]]`;
forgetLightweightOperation(operationToken);
card.loading = false;
card.status = 'failed';
- card.status_label = '失败';
+ card.status_label = t('失败');
card.type = 'danger';
- card.error = `阶段:前端轮询\n原因:${err.message || '无法读取操作状态'}`;
+ card.error = t('阶段:') + 'Frontend Polling\n' + t('原因:') + (err.message || t('无法读取操作状态'));
resolve({ state: 'failed', error: { phase: 'frontend_poll', message: err.message } });
}
};
@@ -19735,7 +19737,7 @@ compare: [[flesh, peel]]`;
const operationToken = lightweightOperationToken(action);
card.loading = true;
card.status = 'running';
- card.status_label = '运行中';
+ card.status_label = t('运行中');
card.type = 'primary';
try {
const response = await extensionApiFetch(endpoint, {
@@ -19743,7 +19745,7 @@ compare: [[flesh, peel]]`;
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ operation_token: operationToken, ...payload })
});
- if (!response.ok) throw new Error(await extensionApiError(response, `${action} 提交失败`));
+ if (!response.ok) throw new Error(await extensionApiError(response, action + t(' 提交失败')));
rememberLightweightOperation(extensionId, action, operationToken, card.card_id);
setLightweightRunning(extensionId, operationToken);
return await pollLightweightOperation(extensionId, operationToken, card, onComplete);
@@ -19751,20 +19753,20 @@ compare: [[flesh, peel]]`;
clearLightweightRunning(operationToken);
card.loading = false;
card.status = 'failed';
- card.status_label = '失败';
+ card.status_label = t('失败');
card.type = 'danger';
- card.error = `阶段:${action}\n原因:${err.message || '提交失败'}`;
+ card.error = t('阶段:') + action + '\n' + t('原因:') + (err.message || t('提交失败'));
return null;
}
};
- const showLightweightCandidate = async (extensionId, summary = '执行入口已在隔离环境运行。请核对系统记录的事实与说明;确认后才会验证。', revisionSummary = '') => {
+ const showLightweightCandidate = async (extensionId, summary = t('执行入口已在隔离环境运行。请核对系统记录的事实与说明;确认后才会验证。'), revisionSummary = '') => {
const help = await lightweightHelp(extensionId);
addLightweightCard({
card_id: `${extensionId}:candidate:${Date.now()}`,
status: 'pending',
- status_label: '待确认',
- title: '拓展分析候选核对',
- purpose: '这张卡用于核对系统从真实执行中锁定的命令、输入、参数和输出。',
+ status_label: t('待确认'),
+ title: t('拓展分析候选核对'),
+ purpose: t('这张卡用于核对系统从真实执行中锁定的命令、输入、参数和输出。'),
summary,
rows: lightweightRows(help),
extension_id: extensionId,
@@ -19777,16 +19779,16 @@ compare: [[flesh, peel]]`;
const hasCode = extensionBuild.codeFiles.length > 0;
const hasDataPaths = Boolean(String(extensionBuild.dataPaths || '').trim());
if (!extensionBuild.displayName || (!extensionBuild.dataFiles.length && !hasDataPaths) || (!hasCommand && !hasCode)) {
- ElMessage.warning('请填写名称并提供测试数据文件或服务器路径;原始命令和原始代码至少提供一项。');
+ ElMessage.warning(t('请填写名称并提供测试数据文件或服务器路径;原始命令和原始代码至少提供一项。'));
return;
}
const extensionId = String(extensionBuild.displayName).trim().toLowerCase()
.replace(/[^a-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '') || `extension_${Date.now()}`;
const token = lightweightOperationToken('upload');
const card = addLightweightCard({
- card_id: `${extensionId}:upload`, status: 'running', status_label: '运行中', type: 'primary',
- title: '正在生成拓展分析', purpose: '系统正在确定执行入口,并运行上传的代码和测试数据,提取不可修改的执行事实。',
- summary: '原始包可复现检查中…', extension_id: extensionId
+ card_id: `${extensionId}:upload`, status: 'running', status_label: t('运行中'), type: 'primary',
+ title: t('正在生成拓展分析'), purpose: t('系统正在确定执行入口,并运行上传的代码和测试数据,提取不可修改的执行事实。'),
+ summary: t('原始包可复现检查中…'), extension_id: extensionId
});
if (!card) return;
extensionBuild.loading = true;
@@ -19799,12 +19801,12 @@ compare: [[flesh, peel]]`;
extensionBuild.dataFiles.forEach(file => form.append('raw_data_files', file, extensionUploadPath(file)));
if (hasDataPaths) form.append('raw_data_paths', extensionBuild.dataPaths);
const response = await extensionApiFetch('/api/lightweight-extension-stages/upload', { method: 'POST', body: form });
- if (!response.ok) throw new Error(await extensionApiError(response, '原始包可复现检查提交失败'));
- ElMessage.success('文件上传成功,已开始生成并验收');
+ if (!response.ok) throw new Error(await extensionApiError(response, t('原始包可复现检查提交失败')));
+ ElMessage.success(t('文件上传成功,已开始生成并验收'));
rememberLightweightOperation(extensionId, 'upload', token, card.card_id);
setLightweightRunning(extensionId, token);
await pollLightweightOperation(extensionId, token, card, async () => {
- finishLightweightCard(card, '✓ 执行入口已成功复现;候选信息已生成。');
+ finishLightweightCard(card, t('✓ 执行入口已成功复现;候选信息已生成。'));
extensionBuild.extensionId = extensionId;
await loadLightweightExtensionStages();
await showLightweightCandidate(extensionId);
@@ -19812,9 +19814,9 @@ compare: [[flesh, peel]]`;
} catch (err) {
clearLightweightRunning(token);
card.status = 'failed';
- card.status_label = '失败';
+ card.status_label = t('失败');
card.type = 'danger';
- card.error = `阶段:upload\n原因:${err.message || '原始包可复现检查提交失败'}`;
+ card.error = t('阶段:upload') + '\n' + t('原因:') + (err.message || t('原始包可复现检查提交失败'));
} finally {
extensionBuild.loading = false;
}
@@ -19823,9 +19825,9 @@ compare: [[flesh, peel]]`;
await submitLightweightOperation(card.extension_id, 'confirm', `/api/lightweight-extension-stages/${encodeURIComponent(card.extension_id)}/confirm`, {}, card, async () => {
finishLightweightCard(card, lightweightCandidateConfirmationSummary(card));
addLightweightCard({
- card_id: `${card.extension_id}:validate`, status: 'pending', status_label: '待执行',
- title: '开始验证', purpose: '这张卡用于用同一份测试数据执行已确认的工具,并生成可核对的结果。',
- summary: '确认无误后点击“开始验证”。', extension_id: card.extension_id,
+ card_id: `${card.extension_id}:validate`, status: 'pending', status_label: t('待执行'),
+ title: t('开始验证'), purpose: t('这张卡用于用同一份测试数据执行已确认的工具,并生成可核对的结果。'),
+ summary: t('确认无误后点击“开始验证”。'), extension_id: card.extension_id,
actions: { validate: true }
});
});
@@ -19835,25 +19837,25 @@ compare: [[flesh, peel]]`;
lightweightExtensionConversation.pendingRevisionCardId = '';
}
card.status = 'cancelled';
- card.status_label = '已取消';
- finishLightweightCard(card, '已取消;没有提交后续执行。');
+ card.status_label = t('已取消');
+ finishLightweightCard(card, t('已取消;没有提交后续执行。'));
};
const beginLightweightRevision = (card) => {
lightweightExtensionConversation.pendingRevisionCardId = card.card_id;
- card.summary = '请在页面下方的主对话输入框说明需要修订的内容,然后发送。命令和真实执行事实不会被修改。';
+ card.summary = t('请在页面下方的主对话输入框说明需要修订的内容,然后发送。命令和真实执行事实不会被修改。');
};
const reviseLightweightCandidate = async (card, feedback) => {
const normalizedFeedback = String(feedback || '').trim();
if (!normalizedFeedback) {
- ElMessage.warning('请写明修订意见。');
+ ElMessage.warning(t('请写明修订意见。'));
return;
}
return await submitLightweightOperation(card.extension_id, 'revise', `/api/lightweight-extension-stages/${encodeURIComponent(card.extension_id)}/revise`, {
feedback: normalizedFeedback,
revision_target: 'semantic'
}, card, async () => {
- const summary = `已修订:${normalizedFeedback}`;
- finishLightweightCard(card, `✓ ${summary}`);
+ const summary = t('已修订:') + normalizedFeedback;
+ finishLightweightCard(card, '✓ ' + summary);
await showLightweightCandidate(card.extension_id, summary, normalizedFeedback);
});
};
@@ -19865,23 +19867,23 @@ compare: [[flesh, peel]]`;
{},
card,
async () => {
- finishLightweightCard(card, '✓ 语义说明已重新生成;执行命令未重新运行。');
+ finishLightweightCard(card, t('✓ 语义说明已重新生成;执行命令未重新运行。'));
await showLightweightCandidate(card.extension_id);
}
);
};
const validationRows = async (extensionId, runId) => {
const response = await extensionApiFetch(`/api/lightweight-extension-stages/${encodeURIComponent(extensionId)}/results`);
- if (!response.ok) throw new Error(await extensionApiError(response, '读取验证结果失败'));
+ if (!response.ok) throw new Error(await extensionApiError(response, t('读取验证结果失败')));
const data = await response.json();
return lightweightResultRows(lightweightRunById(data.validation_runs, runId));
};
const validateLightweightExtension = async (card) => {
await submitLightweightOperation(card.extension_id, 'validate', `/api/lightweight-extension-stages/${encodeURIComponent(card.extension_id)}/validate`, {}, card, async (view) => {
- finishLightweightCard(card, '✓ 验证已完成;结果核对卡已生成。');
+ finishLightweightCard(card, t('✓ 验证已完成;结果核对卡已生成。'));
addLightweightCard({
- card_id: `${card.extension_id}:result_review:${view.run_id}`, status: 'pending', status_label: '待核对',
- title: '运行结果核对', purpose: '这张卡用于查看实际命令、输出与日志;核对无误后才允许授权发布。',
+ card_id: `${card.extension_id}:result_review:${view.run_id}`, status: 'pending', status_label: t('待核对'),
+ title: t('运行结果核对'), purpose: t('这张卡用于查看实际命令、输出与日志;核对无误后才允许授权发布。'),
rows: await validationRows(card.extension_id, view.run_id), extension_id: card.extension_id,
actions: { review: true }
});
@@ -19889,18 +19891,18 @@ compare: [[flesh, peel]]`;
};
const reviewLightweightResult = async (card) => {
await submitLightweightOperation(card.extension_id, 'review', `/api/lightweight-extension-stages/${encodeURIComponent(card.extension_id)}/review`, {}, card, async () => {
- finishLightweightCard(card, '✓ 已核对运行结果。');
+ finishLightweightCard(card, t('✓ 已核对运行结果。'));
addLightweightCard({
- card_id: `${card.extension_id}:publish`, status: 'pending', status_label: '待授权',
- title: '授权发布', purpose: '这张卡用于确认将已经验证的工具发布为可复用的拓展分析。',
- summary: '确认后才会发布;不会自动发布。', extension_id: card.extension_id,
+ card_id: `${card.extension_id}:publish`, status: 'pending', status_label: t('待授权'),
+ title: t('授权发布'), purpose: t('这张卡用于确认将已经验证的工具发布为可复用的拓展分析。'),
+ summary: t('确认后才会发布;不会自动发布。'), extension_id: card.extension_id,
actions: { publish: true, cancel: true }
});
});
};
const publishLightweightExtension = async (card) => {
await submitLightweightOperation(card.extension_id, 'publish', `/api/lightweight-extension-stages/${encodeURIComponent(card.extension_id)}/publish`, {}, card, async () => {
- finishLightweightCard(card, '✓ 已显式授权并发布。');
+ finishLightweightCard(card, t('✓ 已显式授权并发布。'));
await loadLightweightExtensionStages();
});
};
@@ -19910,15 +19912,15 @@ compare: [[flesh, peel]]`;
try {
const help = await lightweightHelp(extensionId);
addLightweightCard({
- card_id: `${extensionId}:runtime:${Date.now()}`, status: 'pending', status_label: '待确认', kind: 'runtime',
- title: '输入文件和参数', purpose: '这张卡用于确认本次新数据运行的输入文件和原始命令已有参数;确认后立即执行,不会再次弹出确认。',
+ card_id: `${extensionId}:runtime:${Date.now()}`, status: 'pending', status_label: t('待确认'), kind: 'runtime',
+ title: t('输入文件和参数'), purpose: t('这张卡用于确认本次新数据运行的输入文件和原始命令已有参数;确认后立即执行,不会再次弹出确认。'),
extension_id: extensionId,
inputs: (help.inputs || []).filter(item => item && item.replaceable).map(item => ({ key: item.path, value: '' })),
parameters: (help.parameters || []).map(item => ({ key: item.fact_id, label: item.flag || item.fact_id, value: item.value || item.flag || '' })),
explicit_prefix: '', actions: { run: true, cancel: true }
});
} catch (err) {
- ElMessage.error(err.message || '读取运行说明失败');
+ ElMessage.error(err.message || t('读取运行说明失败'));
}
};
const runtimeInputPath = (jobId, value) => {
@@ -19930,12 +19932,12 @@ compare: [[flesh, peel]]`;
const runLightweightExtension = async (card) => {
const jobId = String(jobInfo.job_id || '').trim();
if (!jobId || jobId === 'default_job') {
- ElMessage.warning('请先选择实际运行 Job。');
+ ElMessage.warning(t('请先选择实际运行 Job。'));
return;
}
const inputOverrides = Object.fromEntries((card.inputs || []).map(item => [item.key, runtimeInputPath(jobId, item.value)]));
if (Object.values(inputOverrides).some(value => !value)) {
- ElMessage.warning('请填写每个输入文件。');
+ ElMessage.warning(t('请填写每个输入文件。'));
return;
}
const parameterOverrides = Object.fromEntries((card.parameters || []).map(item => [item.key, item.value]));
@@ -19945,13 +19947,13 @@ compare: [[flesh, peel]]`;
parameter_overrides: parameterOverrides,
explicit_prefix: String(card.explicit_prefix || '').trim() || null
}, card, async (view) => {
- finishLightweightCard(card, '✓ 已确认输入与参数;本次运行已完成。');
+ finishLightweightCard(card, t('✓ 已确认输入与参数;本次运行已完成。'));
const response = await extensionApiFetch(`/api/lightweight-extension-stages/${encodeURIComponent(card.extension_id)}/results`);
const data = response.ok ? await response.json() : {};
const run = lightweightRunById(data.runtime_runs, view.run_id);
addLightweightCard({
- card_id: `${card.extension_id}:runtime_result:${view.run_id}`, status: 'completed', status_label: '已完成', type: 'success',
- title: '运行结果', purpose: '这张卡展示本次新数据运行生成的文件、标准输出和日志。',
+ card_id: `${card.extension_id}:runtime_result:${view.run_id}`, status: 'completed', status_label: t('已完成'), type: 'success',
+ title: t('运行结果'), purpose: t('这张卡展示本次新数据运行生成的文件、标准输出和日志。'),
extension_id: card.extension_id,
rows: lightweightResultRows(run)
});
@@ -19967,61 +19969,61 @@ compare: [[flesh, peel]]`;
if (!saved || !saved.extensionId || !saved.operationToken || !saved.action) return;
const card = addLightweightCard({
card_id: saved.cardId || `${saved.extensionId}:${saved.action}`,
- status: 'running', status_label: '运行中', type: 'primary', extension_id: saved.extensionId,
- title: '正在恢复拓展分析状态', purpose: '页面已恢复;系统继续轮询同一个操作,不会重复提交。',
- summary: `正在恢复 ${saved.action}…`
+ status: 'running', status_label: t('运行中'), type: 'primary', extension_id: saved.extensionId,
+ title: t('正在恢复拓展分析状态'), purpose: t('页面已恢复;系统继续轮询同一个操作,不会重复提交。'),
+ summary: t('正在恢复 ') + saved.action + '…'
});
if (!card) return;
await pollLightweightOperation(saved.extensionId, saved.operationToken, card, async (view) => {
const next = lightweightResumeNext(saved.action);
switch (next) {
case 'candidate':
- finishLightweightCard(card, '✓ 已恢复并完成;候选核对卡已生成。');
+ finishLightweightCard(card, t('✓ 已恢复并完成;候选核对卡已生成。'));
extensionBuild.extensionId = saved.extensionId;
await loadLightweightExtensionStages();
- await showLightweightCandidate(saved.extensionId, saved.action === 'revise' ? '已恢复修订;请核对最新候选。' : undefined);
+ await showLightweightCandidate(saved.extensionId, saved.action === 'revise' ? t('已恢复修订;请核对最新候选。') : undefined);
break;
case 'validate':
- finishLightweightCard(card, '✓ 已恢复确认;可以开始验证。');
+ finishLightweightCard(card, t('✓ 已恢复确认;可以开始验证。'));
addLightweightCard({
- card_id: `${saved.extensionId}:validate`, status: 'pending', status_label: '待执行',
- title: '开始验证', purpose: '这张卡用于用同一份测试数据执行已确认的工具,并生成可核对的结果。',
- summary: '确认无误后点击“开始验证”。', extension_id: saved.extensionId, actions: { validate: true }
+ card_id: `${saved.extensionId}:validate`, status: 'pending', status_label: t('待执行'),
+ title: t('开始验证'), purpose: t('这张卡用于用同一份测试数据执行已确认的工具,并生成可核对的结果。'),
+ summary: t('确认无误后点击“开始验证”。'), extension_id: saved.extensionId, actions: { validate: true }
});
break;
case 'review':
- finishLightweightCard(card, '✓ 已恢复验证;结果核对卡已生成。');
+ finishLightweightCard(card, t('✓ 已恢复验证;结果核对卡已生成。'));
addLightweightCard({
- card_id: `${saved.extensionId}:result_review:${view.run_id}`, status: 'pending', status_label: '待核对',
- title: '运行结果核对', purpose: '这张卡用于查看实际命令、输出与日志;核对无误后才允许授权发布。',
+ card_id: `${saved.extensionId}:result_review:${view.run_id}`, status: 'pending', status_label: t('待核对'),
+ title: t('运行结果核对'), purpose: t('这张卡用于查看实际命令、输出与日志;核对无误后才允许授权发布。'),
rows: await validationRows(saved.extensionId, view.run_id), extension_id: saved.extensionId, actions: { review: true }
});
break;
case 'publish':
- finishLightweightCard(card, '✓ 已恢复结果核对;可以授权发布。');
+ finishLightweightCard(card, t('✓ 已恢复结果核对;可以授权发布。'));
addLightweightCard({
- card_id: `${saved.extensionId}:publish`, status: 'pending', status_label: '待授权',
- title: '授权发布', purpose: '这张卡用于确认将已经验证的工具发布为可复用的拓展分析。',
- summary: '确认后才会发布;不会自动发布。', extension_id: saved.extensionId, actions: { publish: true, cancel: true }
+ card_id: `${saved.extensionId}:publish`, status: 'pending', status_label: t('待授权'),
+ title: t('授权发布'), purpose: t('这张卡用于确认将已经验证的工具发布为可复用的拓展分析。'),
+ summary: t('确认后才会发布;不会自动发布。'), extension_id: saved.extensionId, actions: { publish: true, cancel: true }
});
break;
case 'list':
- finishLightweightCard(card, '✓ 已恢复并发布。');
+ finishLightweightCard(card, t('✓ 已恢复并发布。'));
await loadLightweightExtensionStages();
break;
case 'runtime_result': {
- finishLightweightCard(card, '✓ 已恢复运行;本次结果卡已生成。');
+ finishLightweightCard(card, t('✓ 已恢复运行;本次结果卡已生成。'));
const response = await extensionApiFetch(`/api/lightweight-extension-stages/${encodeURIComponent(saved.extensionId)}/results`);
const data = response.ok ? await response.json() : {};
addLightweightCard({
- card_id: `${saved.extensionId}:runtime_result:${view.run_id}`, status: 'completed', status_label: '已完成', type: 'success',
- title: '运行结果', purpose: '这张卡展示本次新数据运行生成的文件、标准输出和日志。',
+ card_id: `${saved.extensionId}:runtime_result:${view.run_id}`, status: 'completed', status_label: t('已完成'), type: 'success',
+ title: t('运行结果'), purpose: t('这张卡展示本次新数据运行生成的文件、标准输出和日志。'),
extension_id: saved.extensionId, rows: lightweightResultRows(lightweightRunById(data.runtime_runs, view.run_id))
});
break;
}
default:
- finishLightweightCard(card, '✓ 已恢复并完成。');
+ finishLightweightCard(card, t('✓ 已恢复并完成。'));
}
});
};
@@ -20050,11 +20052,11 @@ compare: [[flesh, peel]]`;
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: query })
});
const data = response.ok ? await response.json() : {};
- await addAgentMessage('assistant', data.usage || '上传可执行代码、测试数据和原始命令,核对候选后验证并授权发布;发布后可指定输入和原命令已有参数运行。');
+ await addAgentMessage('assistant', data.usage || t('上传可执行代码、测试数据和原始命令,核对候选后验证并授权发布;发布后可指定输入和原命令已有参数运行。'));
return true;
}
const help = await lightweightHelp(matched.extension_id);
- await addAgentMessage('assistant', lightweightRows(help).map(row => `${row.item}:${row.value}`).join('\n'));
+ await addAgentMessage('assistant', lightweightRows(help).map(row => row.item + t(':') + row.value).join('\n'));
return true;
};
const buildExtensionStageLegacy = async () => {
@@ -20135,7 +20137,7 @@ compare: [[flesh, peel]]`;
} catch (err) {
const message = err.message || '拓展分析阶段生成失败';
pushExtensionStatus(message, 'error', false);
- await addAgentMessage('assistant', `拓展分析阶段失败:${message}`);
+ await addAgentMessage('assistant', t('拓展分析阶段失败:') + message);
ElMessage.error(message.split('\n')[0]);
} finally {
extensionBuild.loading = false;
@@ -20394,7 +20396,7 @@ compare: [[flesh, peel]]`;
throw new Error(diagnosis || run.error || '拓展分析阶段执行未成功,请查看 stderr.log');
}
const outputNames = (run.outputs || []).map(out => out.name).join('、') || '未扫描到输出文件';
- addAgentMessage('assistant', `已执行拓展分析阶段:${params.extensionName || extensionId}。结果已写入当前 job workspace,输出:${outputNames}。`, {
+ addAgentMessage('assistant', t('已执行拓展分析阶段:') + (params.extensionName || extensionId) + t('。结果已写入当前 job workspace,输出:') + outputNames + '。', {
stage: 'extension_stage',
taskType: 'extension_stage',
params,
@@ -20405,7 +20407,7 @@ compare: [[flesh, peel]]`;
} catch (err) {
extensionRun.statusText = err.message || '执行失败';
if (fromIntent) {
- addAgentMessage('assistant', `拓展分析阶段执行失败:${extensionRun.statusText}`, {
+ addAgentMessage('assistant', t('拓展分析阶段执行失败:') + extensionRun.statusText, {
stage: 'extension_stage',
taskType: 'extension_stage',
params,
@@ -21386,7 +21388,7 @@ compare: [[flesh, peel]]`;
methodologyStatusText.value = `方法学方案已保存到当前 job:${bindData.plan_path || 'methodology_plan.json'}`;
if (!silent) {
ElMessage.success('方法学方案已保存,可先准备数据,稍后再按该方案执行');
- await addAgentMessage('assistant', `已保存方法学方案到当前 job。下一步请上传/填写目标物种和参考物种数据,并确认 config.xlsx 后再执行方法学流程。`);
+ await addAgentMessage('assistant', t('已保存方法学方案到当前 job。下一步请上传/填写目标物种和参考物种数据,并确认 config.xlsx 后再执行方法学流程。'));
}
return bindData;
} catch (err) {
@@ -21431,7 +21433,7 @@ compare: [[flesh, peel]]`;
methodologyStatusText.value = message;
ElMessage.warning(message);
await openMethodologyDataPreparation();
- await addAgentMessage('assistant', `已保存方法学方案,但暂不执行:${message}。请在 config.xlsx 自动识别弹窗中核对/补齐数据,确认写入正式配置后再点击“确认并执行”。`, { taskType: 'methodology_data_preparation' });
+ await addAgentMessage('assistant', t('已保存方法学方案,但暂不执行:') + message + t('。请在 config.xlsx 自动识别弹窗中核对/补齐数据,确认写入正式配置后再点击”确认并执行”。'), { taskType: 'methodology_data_preparation' });
methodologyExecuting.value = false;
return;
}
@@ -21621,7 +21623,7 @@ compare: [[flesh, peel]]`;
} catch (err) {
methodologyExecuting.value = false;
ElMessage.error(err.message || '方法学执行规划失败');
- addAgentMessage('assistant', '❌ 方法学执行规划失败:' + (err.message || '未知错误'), {
+ addAgentMessage('assistant', t('❌ 方法学执行规划失败:') + (err.message || 'Unknown error'), {
taskType: 'error'
});
}
@@ -21643,14 +21645,14 @@ compare: [[flesh, peel]]`;
taskType: 'analysis'
};
- addAgentMessage('assistant', `已选择:${option.label}`);
+ addAgentMessage('assistant', t('已选择:') + option.label);
try {
await runPlanForIntent(syntenyChoiceQuery.value || option.label, selectedIntent);
} catch (err) {
console.error('[DEBUG] Synteny choice plan error:', err);
ElMessage.error(`❌ ${err.message || '规划请求失败'}`);
- addAgentMessage('assistant', `❌ 服务暂时不可用:${err.message || '规划请求失败'}\n\n请稍后重试。`, {
+ addAgentMessage('assistant', t('❌ 服务暂时不可用:') + (err.message || 'Planning request failed') + '\n\n' + t('请稍后重试。'), {
taskType: 'error'
});
} finally {
@@ -21922,7 +21924,7 @@ compare: [[flesh, peel]]`;
const localStatus = {};
const prefixValue = normalizedPrefix(plan.prefix || params.prefix);
- addAgentMessage('assistant', `📌 当前已有前台批次执行中,新任务已转为后台执行:${stageIds.map(getStageLabel).join(', ')}`, {
+ addAgentMessage('assistant', t('📌 当前已有前台批次执行中,新任务已转为后台执行:') + stageIds.map(getStageLabel).join(', '), {
taskType: 'background_batch',
batchId,
stages: stageIds
@@ -21972,11 +21974,11 @@ compare: [[flesh, peel]]`;
localStatus[stageId] = 'failed';
failed++;
});
- addAgentMessage('assistant', `⚠️ 后台批次中以下阶段因依赖失败被跳过: ${blocked.map(getStageLabel).join(', ')}`);
+ addAgentMessage('assistant', t('⚠️ 后台批次中以下阶段因依赖失败被跳过: ') + blocked.map(getStageLabel).join(', '));
continue;
}
- addAgentMessage('assistant', `⚠️ 后台批次无法继续:${pending.map(getStageLabel).join(', ')} 的依赖尚未完成`);
+ addAgentMessage('assistant', '⚠️ ' + t('后台批次无法继续:') + pending.map(getStageLabel).join(', ') + t(' 的依赖尚未完成'));
pending.forEach(stageId => {
localStatus[stageId] = 'failed';
failed++;
@@ -21988,14 +21990,14 @@ compare: [[flesh, peel]]`;
localStatus[stageId] = 'running';
const stage = stageMap[stageId] || stageId;
const stageName = getStageName(stage);
- addAgentMessage('assistant', `⚙️ 后台执行阶段: ${stageName}...`);
+ addAgentMessage('assistant', t('⚙️ 后台执行阶段: ') + stageName + '...');
try {
const stageParams = (plan.stages_detail || []).find(s => s.id === stageId)?.params || plan.params || {};
await executeStageDirect(stageId, stageParams);
localStatus[stageId] = 'completed';
completed++;
- addAgentMessage('assistant', `✅ 后台阶段 ${stageName} 完成`);
+ addAgentMessage('assistant', t('✅ 后台阶段 ') + stageName + t(' 完成'));
} catch (err) {
localStatus[stageId] = 'failed';
failed++;
@@ -22004,7 +22006,7 @@ compare: [[flesh, peel]]`;
}));
}
- addAgentMessage('assistant', `📦 后台批次完成:成功 ${completed}/${stageIds.length},失败 ${failed}/${stageIds.length}`);
+ addAgentMessage('assistant', t('📦 后台批次完成:成功 ') + completed + '/' + stageIds.length + t(',失败 ') + failed + '/' + stageIds.length);
await refreshResults();
};
@@ -22437,9 +22439,9 @@ compare: [[flesh, peel]]`;
const reviewPrefix = requestedSpecies ? `${requestedSpecies}.${familyForReview}` : '';
const opened = await maybeShowSubfamilyReview('reference_classification', true, reviewPrefix);
if (opened) {
- addAgentMessage('assistant', `已调出${requestedSpecies ? requestedSpecies + ' ' : ''}亚家族分类确认表。你可以在弹窗中修订 confirmed_subfamily,点击“确认亚家族标签”后保存。`);
+ addAgentMessage('assistant', t('已调出') + (requestedSpecies ? requestedSpecies + ' ' : '') + t('亚家族分类确认表。你可以在弹窗中修订 confirmed_subfamily,点击”确认亚家族标签”后保存。'));
} else {
- addAgentMessage('assistant', `${requestedSpecies ? requestedSpecies + ' ' : ''}亚家族分类确认表没有打开:未找到可确认的分类结果。请先确认该物种已完成 reference_classification。`);
+ addAgentMessage('assistant', (requestedSpecies ? requestedSpecies + ' ' : '') + t('亚家族分类确认表没有打开:未找到可确认的分类结果。请先确认该物种已完成 reference_classification。'));
}
nlInput.value = '';
nlLoading.value = false;
@@ -22456,7 +22458,7 @@ compare: [[flesh, peel]]`;
try {
await answerLightweightExtensionHelp(userInput);
} catch (err) {
- await addAgentMessage('assistant', `拓展分析帮助暂时不可用:${err.message || err}`);
+ await addAgentMessage('assistant', t('拓展分析帮助暂时不可用:') + (err.message || err));
} finally {
nlInput.value = '';
nlLoading.value = false;
@@ -22478,8 +22480,8 @@ compare: [[flesh, peel]]`;
keepCurrentRows: !wantsAutofillRegenerate,
});
addAgentMessage('assistant', wantsAutofillRegenerate
- ? '已重新识别并生成 config.xlsx 草稿。请在弹窗中核对并修订,确认后才会写入正式 config.xlsx。'
- : '已打开 config.xlsx 自动识别表。你可以直接修订单行,或用批量修订区筛选、预览、Apply。');
+ ? t('已重新识别并生成 config.xlsx 草稿。请在弹窗中核对并修订,确认后才会写入正式 config.xlsx。')
+ : t('已打开 config.xlsx 自动识别表。你可以直接修订单行,或用批量修订区筛选、预览、Apply。'));
nlInput.value = '';
nlLoading.value = false;
return;
@@ -22495,8 +22497,8 @@ compare: [[flesh, peel]]`;
keepCurrentRows: !wantsAutofillRegenerate,
});
addAgentMessage('assistant', wantsAutofillRegenerate
- ? '已重新识别并生成 samples.yaml 草稿。请确认 run、sample_id、condition、replicate 和 control_group 后再写入正式 samples.yaml。'
- : '已打开 samples.yaml 自动识别表。你可以直接修订样本、分组、重复和对照组。');
+ ? t('已重新识别并生成 samples.yaml 草稿。请确认 run、sample_id、condition、replicate 和 control_group 后再写入正式 samples.yaml。')
+ : t('已打开 samples.yaml 自动识别表。你可以直接修订样本、分组、重复和对照组。'));
nlInput.value = '';
nlLoading.value = false;
return;
@@ -22525,9 +22527,9 @@ compare: [[flesh, peel]]`;
if (autofillAutoQueue.value.length > beforeQueueLength || autofillDialogVisible.value || hasSamplesDraft || hasConfigDraft) {
const msg = hasSamplesDraft
? (hasConfigDraft
- ? '已识别到 RNA-seq FASTQ 和参考资源,优先打开 samples.yaml 草稿供你核对样本、分组和对照;config.xlsx 草稿也已生成。'
- : '已识别到 RNA-seq/FASTQ 数据并生成 samples.yaml 草稿。请在弹窗中核对 run、sample_id、condition、replicate、control_group 后再写入正式 samples.yaml。')
- : '已把目录中的可识别参考资源导入当前 job,并生成 config.xlsx 草稿。请在弹窗中核对和修订,点击“确认并写入正式配置”后再执行正式分析。';
+ ? t('已识别到 RNA-seq FASTQ 和参考资源,优先打开 samples.yaml 草稿供你核对样本、分组和对照;config.xlsx 草稿也已生成。')
+ : t('已识别到 RNA-seq/FASTQ 数据并生成 samples.yaml 草稿。请在弹窗中核对 run、sample_id、condition、replicate、control_group 后再写入正式 samples.yaml。'))
+ : t('已把目录中的可识别参考资源导入当前 job,并生成 config.xlsx 草稿。请在弹窗中核对和修订,点击”确认并写入正式配置”后再执行正式分析。');
addAgentMessage('assistant', msg);
nlInput.value = '';
nlLoading.value = false;
@@ -22548,7 +22550,8 @@ compare: [[flesh, peel]]`;
model: selectedModel.value,
session_id: currentConversationId.value || sessionId.value,
job_id: jobInfo.job_id,
- history: agentSession.messages.slice(-10)
+ history: agentSession.messages.slice(-10),
+ language: currentLang.value
});
if (data.success && data.intent) {
@@ -22557,7 +22560,7 @@ compare: [[flesh, peel]]`;
if (data.action === 'prepare_inputs') {
inputPath.value = (data.input_paths || []).join(',');
- addAgentMessage('assistant', data.intent.message || '请先核对 config.xlsx 草稿。', {
+ addAgentMessage('assistant', data.intent.message || t('请先核对 config.xlsx 草稿。'), {
stage: null,
taskType: 'input_preparation',
params: { input_paths: data.input_paths || [] }
@@ -22585,7 +22588,7 @@ compare: [[flesh, peel]]`;
return;
}
if (data.action === 'reply' || data.action === 'clarify') {
- addAgentMessage('assistant', data.intent.message || '请补充你的需求。', {
+ addAgentMessage('assistant', data.intent.message || t('请补充你的需求。'), {
stage: null,
taskType: data.intent.taskType || (data.action === 'clarify' ? 'clarification' : 'faq'),
params: {},
@@ -22606,7 +22609,7 @@ compare: [[flesh, peel]]`;
if (data.intent.taskType === 'result_view') {
autoDetectedMode.value = 'analysis';
if (jobInfo.job_id) await refreshResults(jobInfo.job_id);
- addAgentMessage('assistant', data.intent.message || '没有找到可展示的结果。', {
+ addAgentMessage('assistant', data.intent.message || t('没有找到可展示的结果。'), {
stage: null,
taskType: 'result_view',
params: data.intent.params || {},
@@ -22647,6 +22650,7 @@ compare: [[flesh, peel]]`;
job_id: jobInfo.job_id, // 传递当前 job_id 用于查询已完成阶段
prefix: normalizedPrefix(params.prefix || ''),
history: agentSession.messages.slice(-10),
+ language: currentLang.value,
intent: data.intent // 传递 chat 返回的 intent,避免重复调用 IntentAgentV2
})
}, 180000, '多智能体规划');
@@ -22680,12 +22684,12 @@ compare: [[flesh, peel]]`;
setResultStageFilter(requestedCompletedStage);
await refreshResults(jobInfo.job_id, requestedCompletedStage);
const completedLabel = getStageLabel(requestedCompletedStage);
- addAgentMessage('assistant', `${completedLabel} 已有同一分析范围的有效结果,无需重复执行。`, {
+ addAgentMessage('assistant', completedLabel + t(' 已有同一分析范围的有效结果,无需重复执行。'), {
taskType: 'analysis_completed',
plan: plan
});
} else {
- addAgentMessage('assistant', `未生成可执行阶段。未检测到当前 scope 的完成结果时,不再把空计划解释为已完成;请检查阶段名称或前置依赖。`, {
+ addAgentMessage('assistant', t('未生成可执行阶段。未检测到当前 scope 的完成结果时,不再把空计划解释为已完成;请检查阶段名称或前置依赖。'), {
taskType: 'analysis_plan_empty',
plan: plan,
completed_stages: [...completedSet]
@@ -22728,7 +22732,7 @@ compare: [[flesh, peel]]`;
String(getStageId(stage) || '').startsWith('wgs_')
);
if (containsWgsStage) {
- addAgentMessage('assistant', '⏸️ 当前已有前台批次执行中;WGS 规划已保留,请在当前批次结束后重新执行。');
+ addAgentMessage('assistant', t('⏸️ 当前已有前台批次执行中;WGS 规划已保留,请在当前批次结束后重新执行。'));
nlLoading.value = false;
nlInput.value = '';
return;
@@ -22758,7 +22762,7 @@ compare: [[flesh, peel]]`;
await loadInterspeciesSpeciesOptions();
}
batchExecuteDialogVisible.value = true;
- addAgentMessage('assistant', `⚙️ 准备执行 ${activeExecutionStages.length} 个分析阶段,请确认参数`);
+ addAgentMessage('assistant', t('⚙️ 准备执行 ') + activeExecutionStages.length + t(' 个分析阶段,请确认参数'));
// 展示规划与参数弹窗,等待用户手动确认。
startBatchExecuteTimer();
@@ -22777,7 +22781,7 @@ compare: [[flesh, peel]]`;
const suggested = (planData.suggested_stages || []).map(getStageLabel).join('、');
planHint = `${issueText ? '\n\n检测结果:\n' + issueText : ''}${suggested ? '\n\n建议先执行:' + suggested : ''}${planData.plain_phylogeny_hint ? '\n\n' + planData.plain_phylogeny_hint : ''}`;
}
- addAgentMessage('assistant', `❌ 无法执行分析:${errorMsg}${planHint}`, {
+ addAgentMessage('assistant', t('❌ 无法执行分析:') + errorMsg + planHint, {
taskType: subfamilyGateStatuses.includes(planData.status) ? 'subfamily_decision' : 'error',
diagnosis: planData.context_gate || null,
suggestedStages: planData.suggested_stages || []
@@ -22791,7 +22795,7 @@ compare: [[flesh, peel]]`;
// API调用异常,显示错误并停止
const errorMsg = planErr.message || "规划请求失败";
ElMessage.error(`❌ ${errorMsg}`);
- addAgentMessage('assistant', `❌ 服务暂时不可用:${errorMsg}\n\n请稍后重试。`, {
+ addAgentMessage('assistant', t('❌ 服务暂时不可用:') + errorMsg + '\n\n' + t('请稍后重试。'), {
taskType: 'error'
});
nlLoading.value = false;
@@ -22799,14 +22803,14 @@ compare: [[flesh, peel]]`;
return;
}
} else {
- addAgentMessage('assistant', data.message || '抱歉,我没有理解你的意思。请尝试更详细地描述。');
+ addAgentMessage('assistant', data.message || t('抱歉,我没有理解你的意思。请尝试更详细地描述。'));
}
} catch (err) {
if (err?.code === 'user_cancelled') {
- addAgentMessage('assistant', '已停止等待,本次未继续获取回答。');
+ addAgentMessage('assistant', t('已停止等待,本次未继续获取回答。'));
} else {
ElMessage.error('请求失败: ' + err.message);
- addAgentMessage('assistant', '抱歉,服务暂时不可用,请稍后重试。');
+ addAgentMessage('assistant', t('抱歉,服务暂时不可用,请稍后重试。'));
}
} finally {
nlInput.value = '';
diff --git a/apps/web/vue3/main.py b/apps/web/vue3/main.py
index f1f63a684..77e5596a2 100644
--- a/apps/web/vue3/main.py
+++ b/apps/web/vue3/main.py
@@ -56,6 +56,8 @@ GF_MAS_ROOT = Path(os.environ.get("GF_MAS_ROOT", "/home/xzq/gene-family-mas-v2")
if GF_MAS_ROOT.exists() and str(GF_MAS_ROOT) not in sys.path:
sys.path.insert(0, str(GF_MAS_ROOT))
PLANTAI_RAG_ROOT = Path(os.environ.get("PLANTAI_RAG_ROOT", "/home/xzq/PlantAI-rag")).expanduser()
+
+DEFAULT_AGENT_LANG = "zh"
if PLANTAI_RAG_ROOT.exists() and str(PLANTAI_RAG_ROOT) not in sys.path:
sys.path.insert(0, str(PLANTAI_RAG_ROOT))
@@ -15352,6 +15354,10 @@ async def get_results(
if group_stage not in visible_stage_names:
result_groups.pop(group_name, None)
+ gene_family_report_group = result_groups.get("gene_family_report")
+ if isinstance(gene_family_report_group, dict):
+ stages["gene_family_report"] = gene_family_report_group
+
stages["_result_groups"] = result_groups
stages["_scope"] = {
"rnaseq_scope_id": active_rnaseq_scope_id,
@@ -16427,6 +16433,7 @@ class AgentChatRequest(BaseModel):
session_id: str = ""
job_id: str = "" # 当前 job_id,用于查询已完成 stages
history: list = []
+ language: str = DEFAULT_AGENT_LANG # "zh" or "en"
methodology_sandbox_registry_cache: Dict[str, str] = {}
@@ -17139,6 +17146,11 @@ async def _run_agent_chat(
executes analysis stages here.
"""
session_id = request.session_id or f"session_{int(time.time())}"
+ # Inject language instruction for non-Chinese languages
+ if request.language and request.language != DEFAULT_AGENT_LANG:
+ lang_map = {"en": "Please respond in English."}
+ lang_instruction = lang_map.get(request.language, f"Please respond in {request.language}.")
+ request.message = f"{lang_instruction}\n\n{request.message}"
completed_stages: list[str] = []
if request.job_id:
final_dir = PROJECT_ROOT / "workspace" / request.job_id / "final"
@@ -17444,6 +17456,7 @@ async def _run_agent_chat(
"history": list(request.history or []),
"session_id": session_id,
"job_id": request.job_id,
+ "language": request.language,
}
if router_goal_ledger is not None:
payload["goal_ledger"] = router_goal_ledger
@@ -17684,25 +17697,47 @@ async def agent_chat(request: AgentChatRequest):
AGENT_CHAT_HEARTBEAT_SECONDS = 5.0
_AGENT_CHAT_TOOL_PROGRESS = {
- "inspect_project_context": ("正在读取当前 job 的项目状态……", "当前 job 状态读取完成,主 Agent 正在继续处理……"),
- "inspect_analysis_results": ("正在读取当前 job 的分析结果……", "分析结果读取完成,主 Agent 正在继续处理……"),
- "inspect_job_file": ("正在读取当前 job 中的文件……", "文件读取完成,主 Agent 正在继续处理……"),
- "query_plantai_rag": ("正在检索知识库……", "知识库检索完成,主 Agent 正在继续处理……"),
- "recognize_analysis_intent": ("正在识别分析需求……", "分析需求识别完成,主 Agent 正在继续处理……"),
- "recognize_utility_intent": ("正在识别工具需求……", "工具需求识别完成,主 Agent 正在继续处理……"),
+ "inspect_project_context": {
+ "zh": ("正在读取当前 job 的项目状态……", "当前 job 状态读取完成,主 Agent 正在继续处理……"),
+ "en": ("Reading current job project status...", "Job status read complete; agent continuing..."),
+ },
+ "inspect_analysis_results": {
+ "zh": ("正在读取当前 job 的分析结果……", "分析结果读取完成,主 Agent 正在继续处理……"),
+ "en": ("Reading current job analysis results...", "Analysis results read complete; agent continuing..."),
+ },
+ "inspect_job_file": {
+ "zh": ("正在读取当前 job 中的文件……", "文件读取完成,主 Agent 正在继续处理……"),
+ "en": ("Reading current job files...", "File read complete; agent continuing..."),
+ },
+ "query_plantai_rag": {
+ "zh": ("正在检索知识库……", "知识库检索完成,主 Agent 正在继续处理……"),
+ "en": ("Searching knowledge base...", "Knowledge base search complete; agent continuing..."),
+ },
+ "recognize_analysis_intent": {
+ "zh": ("正在识别分析需求……", "分析需求识别完成,主 Agent 正在继续处理……"),
+ "en": ("Recognizing analysis intent...", "Analysis intent recognized; agent continuing..."),
+ },
+ "recognize_utility_intent": {
+ "zh": ("正在识别工具需求……", "工具需求识别完成,主 Agent 正在继续处理……"),
+ "en": ("Recognizing utility intent...", "Utility intent recognized; agent continuing..."),
+ },
}
-def _agent_chat_progress_payload(event: dict, elapsed_seconds: int) -> dict:
+def _agent_chat_progress_payload(event: dict, elapsed_seconds: int, language: str = "zh") -> dict:
stage = str(event.get("stage") or "")
tool = str(event.get("tool") or "")
- messages = _AGENT_CHAT_TOOL_PROGRESS.get(tool)
+ pair = _AGENT_CHAT_TOOL_PROGRESS.get(tool, {})
+ messages = pair.get(language) or pair.get("zh")
if stage == "tool_started" and messages:
message = messages[0]
elif stage == "tool_completed" and messages:
message = messages[1]
else:
- message = "主 Agent 正在处理请求……"
+ message = {
+ "zh": "主 Agent 正在处理请求……",
+ "en": "Main Agent is processing your request...",
+ }.get(language, "主 Agent 正在处理请求……")
return {
"stage": stage or "agent_running",
"tool": tool,
@@ -17717,6 +17752,8 @@ def _agent_chat_sse(event_name: str, payload: dict) -> str:
@app.post("/api/agent/chat/stream")
async def agent_chat_stream(request: AgentChatRequest, http_request: Request):
+ lang = request.language or DEFAULT_AGENT_LANG
+
async def event_stream():
started_at = time.monotonic()
progress_queue: asyncio.Queue[dict] = asyncio.Queue()
@@ -17731,7 +17768,10 @@ async def agent_chat_stream(request: AgentChatRequest, http_request: Request):
"progress",
{
"stage": "agent_started",
- "message": "正在理解你的问题并确定所需信息……",
+ "message": {
+ "zh": "正在理解你的问题并确定所需信息……",
+ "en": "Understanding your question and determining required information...",
+ }.get(lang, "正在理解你的问题并确定所需信息……"),
"elapsed_seconds": 0,
},
)
@@ -17751,7 +17791,10 @@ async def agent_chat_stream(request: AgentChatRequest, http_request: Request):
"heartbeat",
{
"stage": "agent_running",
- "message": f"主 Agent 仍在处理,已用时 {elapsed} 秒……",
+ "message": {
+ "zh": f"主 Agent 仍在处理,已用时 {elapsed} 秒……",
+ "en": f"Main Agent is still processing, {elapsed} s elapsed...",
+ }.get(lang, f"主 Agent 仍在处理,已用时 {elapsed} 秒……"),
"elapsed_seconds": elapsed,
},
)
@@ -17759,7 +17802,7 @@ async def agent_chat_stream(request: AgentChatRequest, http_request: Request):
elapsed = int(time.monotonic() - started_at)
yield _agent_chat_sse(
"progress",
- _agent_chat_progress_payload(event, elapsed),
+ _agent_chat_progress_payload(event, elapsed, language=lang),
)
result = await runner
@@ -18204,6 +18247,7 @@ class AgentExecuteRequest(BaseModel):
model: str = "kimi"
history: list = []
auto_execute: bool = True # 是否自动执行分析
+ language: str = DEFAULT_AGENT_LANG # "zh" or "en"
class AgentStatusResponse(BaseModel):
"""Agent状态响应"""
@@ -18223,12 +18267,20 @@ async def agent_execute(request: AgentExecuteRequest, background_tasks: Backgrou
与/agent/chat不同,此接口会实际执行分析流程
"""
task_id = f"agent_task_{int(time.time())}_{uuid.uuid4().hex[:8]}"
-
+
+ # Inject language instruction for non-Chinese languages
+ message = request.message
+ lang = getattr(request, "language", DEFAULT_AGENT_LANG)
+ if lang and lang != DEFAULT_AGENT_LANG:
+ lang_map = {"en": "Please respond in English."}
+ lang_instruction = lang_map.get(lang, f"Please respond in {lang}.")
+ message = f"{lang_instruction}\n\n{request.message}"
+
# 初始化任务状态
agent_tasks[task_id] = {
"task_id": task_id,
"status": "pending",
- "message": request.message,
+ "message": message,
"job_id": request.job_id,
"prefix": request.prefix,
"current_stage": "",
@@ -18267,7 +18319,7 @@ async def agent_execute(request: AgentExecuteRequest, background_tasks: Backgrou
# 提交任务到Celery队列
celery_task = execute_agent_task.delay(
task_id=task_id,
- message=request.message,
+ message=message,
session_id=request.session_id or task_id,
job_id=request.job_id, # 传入选中的 job_id
history=request.history if request.history else [],
@@ -18494,6 +18546,7 @@ class AgentPlanRequest(BaseModel):
prefix: str = ""
history: list = []
intent: dict = {} # 前端可传入已有的intent,避免重复调用IntentAgentV2
+ language: str = DEFAULT_AGENT_LANG # "zh" or "en"
SUBFAMILY_PREDICTION_PLAN_STAGES = [
@@ -20485,6 +20538,11 @@ async def agent_plan(request: AgentPlanRequest):
"""
try:
session_id = request.session_id or f"session_{int(time.time())}"
+ # Inject language instruction for non-Chinese languages
+ if request.language and request.language != DEFAULT_AGENT_LANG:
+ lang_map = {"en": "Please respond in English."}
+ lang_instruction = lang_map.get(request.language, f"Please respond in {request.language}.")
+ request.message = f"{lang_instruction}\n\n{request.message}"
methodology_plan, methodology_intent, methodology_params = _load_methodology_plan_for_agent_request(request)
if methodology_plan == {}:
return {
@@ -23746,6 +23804,7 @@ class GenerateInterpretationRequest(BaseModel):
strict_identity: bool = True
use_rag_support: bool = False
refresh_rag: bool = False
+ language: str = DEFAULT_AGENT_LANG # "zh" or "en"
@app.post("/api/agent/interpret")
async def generate_interpretation(request: GenerateInterpretationRequest):
diff --git a/apps/web/vue3/static/i18n_en.js b/apps/web/vue3/static/i18n_en.js
index 416f57655..41947fdea 100644
--- a/apps/web/vue3/static/i18n_en.js
+++ b/apps/web/vue3/static/i18n_en.js
@@ -1,3 +1,7 @@
+// ===== Agent 多语言:卡片 + Agent 消息英文翻译 =====
+// key 为中文原文,value 为英文翻译
+// 使用 t(key) 获取翻译,tp(key, params) 处理动态占位符
+
window.__I18N_EN__ = {
"Agent状态": "AgentStatus",
"Self-RAG反思": "Self-RAG Reflection",
@@ -375,6 +379,339 @@ window.__I18N_EN__ = {
"亚家族确认 - Reference Classification": "Subfamily Confirmation - Reference Classification",
"这会停止当前 profile 的 Celery worker 并重新启动。若有任务正在运行,后端会默认拒绝执行。": "This will stop and restart the Celery worker for the current profile. Running tasks will be rejected by default.",
"Celery 已重启并完成队列检查": "Celery restarted and queue check complete",
- "Celery 重启未完成": "Celery restart incomplete"
+ "Celery 重启未完成": "Celery restart incomplete",
+ // ---- Card status_label (8) ----
+ "失败": "Failed",
+ "运行中": "Running",
+ "待确认": "Pending",
+ "待执行": "Pending",
+ "待核对": "Review",
+ "待授权": "Authorize",
+ "已取消": "Cancelled",
+ // ---- Card title (10) ----
+ "拓展分析候选核对": "Extension Candidate Review",
+ "拓展分析说明生成失败": "Description Generation Failed",
+ "正在生成拓展分析": "Generating Extension",
+ "运行结果核对": "Result Review",
+ "输入文件和参数": "Input Files & Parameters",
+ "正在恢复拓展分析状态": "Restoring Extension State",
+ // ---- Card purpose/summary (15) ----
+ "原始命令、测试数据和执行事实已经保留;这里只重新生成说明,不会重新运行原始命令。": "Raw command, test data and execution facts are preserved; only regenerate description without re-running.",
+ "可以直接重试,或取消后检查错误原因。": "Retry or cancel and check error cause.",
+ "这张卡用于核对系统从真实执行中锁定的命令、输入、参数和输出。": "Review system-locked command, inputs, parameters and outputs from real execution.",
+ "系统正在确定执行入口,并运行上传的代码和测试数据,提取不可修改的执行事实。": "Determining entry point, running uploaded code/test data, extracting immutable execution facts.",
+ "原始包可复现检查中…": "Checking reproducibility of original package…",
+ "这张卡用于用同一份测试数据执行已确认的工具,并生成可核对的结果。": "Run confirmed tool with same test data; generate verifiable results.",
+ "确认无误后点击“开始验证”。": "Click \"Start Validation\" after confirming.",
+ "这张卡用于查看实际命令、输出与日志;核对无误后才允许授权发布。": "View actual command, output and logs; publication requires review confirmation.",
+ "这张卡用于确认将已经验证的工具发布为可复用的拓展分析。": "Confirm publishing validated tool as reusable extension.",
+ "确认后才会发布;不会自动发布。": "Publication requires explicit confirmation; not automatic.",
+ "这张卡用于确认本次新数据运行的输入文件和原始命令已有参数;确认后立即执行,不会再次弹出确认。": "Confirm input files and parameters for this run; executes immediately without re-confirmation.",
+ "这张卡展示本次新数据运行生成的文件、标准输出和日志。": "Files, stdout and logs generated by this run.",
+ "页面已恢复;系统继续轮询同一个操作,不会重复提交。": "Page restored; continuing to poll the same operation without re-submitting.",
+ "请在页面下方的主对话输入框说明需要修订的内容,然后发送。命令和真实执行事实不会被修改。": "Describe revisions in the main chat input below. Commands and execution facts are immutable.",
+ "已取消;没有提交后续执行。": "Cancelled; no further execution submitted.",
+ // ---- Card rows labels (13) ----
+ "实际命令": "Actual Command",
+ "输出文件": "Output Files",
+ "标准输出": "Standard Output",
+ "见执行日志": "See execution log",
+ "功能": "Function",
+ "输入文件": "Input Files",
+ "可改参数": "Configurable Parameters",
+ "标准输出(直接展示)": "Stdout (direct display)",
+ "(无文件输出)": "(No file output)",
+ "(无)": "(None)",
+ // ---- Card error/misc (25+) ----
+ "操作失败": "Operation failed",
+ "读取拓展分析说明失败": "Failed to read extension help",
+ "读取拓展分析状态失败": "Failed to read extension status",
+ "阶段:": "Phase: ",
+ "原因:": "Cause: ",
+ "无法读取操作状态": "Cannot read operation status",
+ "提交失败": "Submission failed",
+ "原始包可复现检查提交失败": "Reproducibility check submission failed",
+ "阶段:upload": "Phase: Upload",
+ "已恢复修订;请核对最新候选。": "Revision restored; please review latest candidate.",
+ "✓ 语义说明已重新生成;执行命令未重新运行。": "✓ Description regenerated; command not re-run.",
+ "✓ 执行入口已成功复现;候选信息已生成。": "✓ Entry point reproduced successfully; candidate info generated.",
+ "✓ 验证已完成;结果核对卡已生成。": "✓ Validation complete; result review card generated.",
+ "✓ 已核对运行结果。": "✓ Results reviewed.",
+ "✓ 已显式授权并发布。": "✓ Explicitly authorized and published.",
+ "✓ 已确认输入与参数;本次运行已完成。": "✓ Inputs and parameters confirmed; run completed.",
+ "✓ 已恢复并完成;候选核对卡已生成。": "✓ Restored and completed; candidate review card generated.",
+ "✓ 已恢复确认;可以开始验证。": "✓ Confirmation restored; ready to validate.",
+ "✓ 已恢复验证;结果核对卡已生成。": "✓ Validation restored; result review card generated.",
+ "✓ 已恢复结果核对;可以授权发布。": "✓ Review restored; ready to publish.",
+ "✓ 已恢复并发布。": "✓ Restored and published.",
+ "✓ 已恢复运行;本次结果卡已生成。": "✓ Run restored; result card generated.",
+ "✓ 已确认候选|未作修订": "✓ Candidate confirmed | no revision",
+ "✓ 已确认候选|已修订:": "✓ Candidate confirmed | revised: ",
+ "文件上传成功,已开始生成并验收": "Upload successful; generation and validation started",
+ "请填写名称并提供测试数据文件或服务器路径;原始命令和原始代码至少提供一项。": "Please provide name and test data files or server path; at least command or code is required.",
+ "请写明修订意见。": "Please specify revision feedback.",
+ "读取验证结果失败": "Failed to read validation results",
+ "读取运行说明失败": "Failed to read run help",
+ "请先选择实际运行 Job。": "Please select a job first.",
+ "请填写每个输入文件。": "Please fill in each input file.",
+ "已修订:": "Revised: ",
+ "正在恢复 ": "Restoring ",
+ "…": "…",
+ " 提交失败": " submission failed",
+ " 原始包可复现检查提交失败": " Reproducibility check submission failed",
+ "执行入口已在隔离环境运行。请核对系统记录的事实与说明;确认后才会验证。": "Entry point ran in isolated environment. Review system-recorded facts and descriptions; validation proceeds after confirmation.",
+ // ---- Agent message: status/confirm/cancel (10) ----
+ "config.xlsx 已确认,继续当前目标:": "config.xlsx confirmed; continuing current goal: ",
+ "✅ 已确认使用拟南芥作为 PPI/KaKs 的显式兜底参考;该选择只写入 Ortholog/PPI/KaKs 专用参数,其它常规阶段不受影响。": "✅ Arabidopsis confirmed as PPI/KaKs explicit fallback reference; affects only Ortholog/PPI/KaKs parameters.",
+ "✅ 已确认自动注释,正在重新提交": "✅ Auto-annotation confirmed; re-submitting",
+ "✅ 已选择项目参考物种": "✅ Project reference species selected",
+ "已停止等待,本次未继续获取回答。": "Stopped waiting; no further response.",
+ "抱歉,我没有理解你的意思。请尝试更详细地描述。": "Sorry, I didn't understand. Please try describing in more detail.",
+ "抱歉,服务暂时不可用,请稍后重试。": "Sorry, service temporarily unavailable. Please try again later.",
+ // ---- Agent message: pause/block/skip (13) ----
+ "⏸️ PPI/KaKs 前置数据检查未通过,已暂停批量执行;请补齐参考注释包或确认参考物种后重新执行。": "⏸️ PPI/KaKs prerequisite check failed; batch paused. Add reference annotation or confirm reference species.",
+ "⏸️ identify 已完成,但方法学链路需要先确认基因家族成员表;保存成员确认后再继续 phylogeny。": "⏸️ identify completed; methodology chain requires confirmed gene family members before continuing phylogeny.",
+ "⏸️ 已打开 identify 成员初筛确认表;保存后会生成 curated_members,再继续亚家族证据阶段。": "⏸️ identify member confirmation table opened; save to generate curated_members, then continue subfamily evidence stages.",
+ "⏸️ 当前已有前台批次执行中;WGS 规划已保留,请在当前批次结束后重新执行。": "⏸️ Foreground batch in progress; WGS plan saved. Re-run after current batch finishes.",
+ ",其它阶段继续执行。": ", other stages continue.",
+ "⏸️ 已按选择先暂停 Ortholog/PPI/KaKs,其它阶段继续执行。": "⏸️ Paused Ortholog/PPI/KaKs as selected; other stages continue.",
+ "⏹️ 后端预检失败,当前批次已停止;请重新拉取规划后继续。": "⏹️ Backend pre-check failed; batch stopped. Re-pull plan to continue.",
+ // ---- Agent message: replan/continue (5) ----
+ "🔁 亚家族标签已确认,正在重新规划确认后常规基因家族分析...": "🔁 Subfamily labels confirmed; re-planning post-confirmation gene family analysis...",
+ "确认后常规分析没有新的可执行阶段。": "No new executable stages after confirmation.",
+ // ---- Agent message: submit/execute (7) ----
+ "⚙️ 提交阶段: ": "⚙️ Submitting stage: ",
+ "按 per_species 拆分 ": "Split by per_species ",
+ // ---- Agent message: complete/success (7) ----
+ "✅ PPI/KaKs 前置数据检查通过:当前批次已有 Ortholog,将先生成同源映射再继续下游分析。": "✅ PPI/KaKs check passed: Ortholog exists in current batch; generating mapping first.",
+ // ---- Agent message: download/background (5) ----
+ "⬇️ 正在提交推荐近源参考物种注释包后台下载;Ortholog/PPI/KaKs 将等待参考数据,其它阶段继续执行。": "⬇️ Submitting reference annotation download; Ortholog/PPI/KaKs wait for data, other stages continue.",
+ // ---- Agent message: autofill (2) ----
+ "已根据上传文件生成 samples.yaml 草稿。请在弹窗中核对 run、sample_id、condition、replicate、链特异性和分组角色,确认后再写入正式 samples.yaml。": "samples.yaml draft generated. Review run, sample_id, condition, replicate, strand specificity and group roles in the dialog, then confirm to write.",
+ "已根据上传文件生成 config.xlsx 草稿。请在弹窗中核对物种代码、文件类型、物种角色和 Gene_Family,确认后再写入正式 config.xlsx。": "config.xlsx draft generated. Review species code, file type, species role and Gene_Family in the dialog, then confirm to write.",
+ // ---- Agent message: methodology (4) ----
+ "已保存方法学方案到当前 job。下一步请上传/填写目标物种和参考物种数据,并确认 config.xlsx 后再执行方法学流程。": "Methodology plan saved. Next: upload target/reference species data, confirm config.xlsx, then execute.",
+ "❌ 方法学执行规划失败:": "❌ Methodology execution planning failed: ",
+ // ---- Agent message: replot/revision (4 to fix) ----
+ "进化树图重绘已提交:": "Phylogeny tree redraw submitted: ",
+ "进化树图重绘失败:": "Phylogeny tree redraw failed: ",
+ "Combo 图修订已提交:": "Combo plot revision submitted: ",
+ "Combo 图修订失败:": "Combo plot revision failed: ",
+ // ---- Agent message: failure/interrupt (10) ----
+ "无法执行分析:": "Cannot execute analysis: ",
+ "服务暂时不可用:": "Service temporarily unavailable: ",
+ "⚠️ 批量执行完成:成功 ": "⚠️ Batch complete: success ",
+ "⚠️ 以下阶段因依赖失败被跳过: ": "⚠️ Stages skipped due to dependency failure: ",
+ "⚠️ 以下阶段等待上游暂停项处理后继续: ": "⚠️ Stages waiting for upstream paused items: ",
+ "未生成可执行阶段。未检测到当前 scope 的完成结果时,不再把空计划解释为已完成;请检查阶段名称或前置依赖。": "No executable stages generated. Empty plan is not interpreted as completed. Check stage names or dependencies.",
+ // ---- Agent message: extension analysis (6) ----
+ "拓展分析阶段:": "Extension stage: ",
+ "拓展分析阶段失败:": "Extension stage failed: ",
+ "已执行拓展分析阶段:": "Extension stage executed: ",
+ "拓展分析阶段执行失败:": "Extension stage execution failed: ",
+ "拓展分析帮助暂时不可用:": "Extension help temporarily unavailable: ",
+ "上传可执行代码、测试数据和原始命令,核对候选后验证并授权发布;发布后可指定输入和原命令已有参数运行。": "Upload executable code, test data and original command; review candidate, validate, then authorize publishing.",
+ // ---- Agent message: other (5) ----
+ "📌 当前已有前台批次执行中,新任务已转为后台执行:": "📌 Foreground batch running; new task moved to background: ",
+ "已识别到 RNA-seq/FASTQ 和参考资源": "RNA-seq/FASTQ and reference resources detected",
+ "已把目录中的可识别参考资源导入当前 job": "Reference resources imported into current job",
+ "我识别到你是在提供数据路径,但该路径下没有发现": "Data path detected but no usable files found",
+ "已选择:": "Selected: ",
+ // ---- Agent message: remaining dynamic patterns ----
+ "❌ 批量执行已取消": "❌ Batch execution cancelled",
+ "⏭️ 跳过阶段: ": "⏭️ Skipped stage: ",
+ "🚀 准备提交依赖驱动执行,共 ": "🚀 Preparing dependency-driven execution: ",
+ "🎉 批量执行完成!成功: ": "🎉 Batch complete! Success: ",
+ ",失败 ": ", failed ",
+ "跳过阶段: ": "Skipped stage: ",
+ " 暂停:需要先准备参考物种 PEP/CDS 或 Ortholog 映射。": " paused: prepare reference species PEP/CDS or Ortholog mapping first.",
+ " 已暂停依赖 confirmed_subfamily.tsv 的后续阶段;请在亚家族确认表中保存后重新拉取规划继续执行。": " paused subsequent stages depending on confirmed_subfamily.tsv; save the subfamily confirmation table and re-pull the plan.",
+ "参考数据已就绪": "Reference data ready",
+ " 已恢复为待执行,将随当前批次继续调度。": " restored as pending; will continue with current batch.",
+ " 开始续跑 Ortholog/PPI/KaKs。": " resuming Ortholog/PPI/KaKs.",
+ " 重新提交(自动注释背景)": " re-submit (auto-annotation background)",
+ " 已提交后端,task_id=": " submitted to backend, task_id=",
+ " 准备执行 ": " preparing to execute ",
+ " 个阶段,请确认参数": " stages; please confirm parameters",
+ " 个阶段...": " stages...",
+ " 个分析阶段,请确认参数": " analysis stages; please confirm parameters",
+ "亚家族确认后已规划 ": "Post-subfamily-confirmation plan: ",
+ " 个常规分析阶段": " regular analysis stages",
+ "PPI/KaKs 前置数据检查通过:检测到已下载近源参考 ": "PPI/KaKs check passed: downloaded reference ",
+ ",将用该参考先生成 Ortholog mapping。": ", generating Ortholog mapping first.",
+ "PPI/KaKs 前置数据检查通过:检测到已有 Ortholog 映射(": "PPI/KaKs check passed: Ortholog mapping exists (",
+ "),将复用该映射执行 ": "), reusing mapping for ",
+ "),将直接执行 ": "), executing directly ",
+ "),已自动插入 Ortholog,将先生成 mapping 再执行 ": "), Ortholog auto-inserted; generating mapping then executing ",
+ ",已自动插入 Ortholog": ", Ortholog auto-inserted",
+ "推荐参考数据下载完成:": "Reference download complete: ",
+ ")。已写入 Ortholog/PPI/Ka/Ks 专用参考物种。": "). Written to Ortholog/PPI/Ka/Ks specific reference species.",
+ " 已按用户确认使用 bcftools 完成": " completed with bcftools as confirmed by user",
+ " 前端连接短暂中断,正在按后端产物状态恢复": " frontend connection interrupted; recovering from backend state",
+ "亚家族确认已完成,但后端当前 workflowPhase 为 ": "Subfamily confirmation completed; backend workflowPhase is ",
+ ",未自动打开常规分析。": "; no auto-analysis opened.",
+ "亚家族分类已完成预测,但确认表尚未保存;": "Subfamily classification predicted; confirmation table not yet saved; ",
+ "已调出": "Opened ",
+ "亚家族分类确认表。你可以在弹窗中修订 confirmed_subfamily,点击\"确认亚家族标签\"后保存。": " subfamily classification confirmation table. Revise confirmed_subfamily in the dialog, then click \"Confirm Subfamily Labels\" to save.",
+ "亚家族分类确认表没有打开:未找到可确认的分类结果。请先确认该物种已完成 reference_classification。": " subfamily classification confirmation table not opened: no confirmable results found. Confirm the species has completed reference_classification.",
+ "确认后常规分析没有新的可执行阶段。": "No new executable stages after confirmation.",
+ "⚠️ 检测到 ": "⚠️ Detected ",
+ ",将在确认后启动本地自动注释。": "; auto-annotation will start after confirmation.",
+ "🧬 自动背景注释与 ": "🧬 Auto-background annotation and ",
+ " 已提交后端,task_id=": " submitted to backend, task_id=",
+ "⚠️ 亚家族确认后自动续跑规划失败:": "⚠️ Post-subfamily-confirm auto-plan failed: ",
+ "; Ortholog/PPI/KaKs ": "; Ortholog/PPI/KaKs ",
+ "推荐参考数据下载已转入后台:": "Reference download moved to background: ",
+ ",task_id=": ", task_id=",
+ "后台批次完成:成功 ": "Background batch complete: success ",
+ "后台执行阶段: ": "Background stage: ",
+ "后台阶段 ": "Background stage ",
+ " 完成": " done",
+ "已保存方法学方案,但暂不执行:": "Methodology plan saved, not executing: ",
+ "。请在 config.xlsx 自动识别弹窗中核对/补齐数据,确认写入正式配置后再点击\"确认并执行\"。": ". Review/fill data in the config.xlsx auto-detect dialog, confirm to write then click \"Confirm & Execute\".",
+ "当前已有前台批次执行中;WGS 规划已保留,请在当前批次结束后重新执行。": "Foreground batch in progress; WGS plan saved. Re-run after current batch finishes.",
+ "拓展分析阶段执行失败:": "Extension stage execution failed: ",
+ "已把路径中的 ": "Soft-linked ",
+ " 个可识别文件软链接到当前 job": " recognizable files to current job",
+ " 个可识别文件软链接到当前 job,但没有发现需要自动填写 config.xlsx 或 samples.yaml 的文件类型;请检查目录或继续补充文件。": " recognizable files linked to current job; no files found for config.xlsx/samples.yaml auto-fill. Check directory or add more files.",
+ "前": " ago",
+ "秒前": "sec ago",
+ "分钟前": "min ago",
+ "小时前": "hr ago",
+ "天前": "d ago",
+ "待机": "Idle",
+ "分析中": "Analyzing",
+ "检索中": "Retrieving",
+ "反思中": "Reflecting",
+ "生成中": "Generating",
+ "完成": "Complete",
+ "错误": "Error",
+ "处理中...": "Processing...",
+ "发送": "Send",
+ "任务列表": "Tasks",
+ " 个常规分析阶段,请确认参数": " regular analysis stages; please confirm parameters",
+ " 亚家族分类已完成预测,但确认表尚未保存;后续会继续显示为待确认。": " subfamily classification predicted; confirmation table not yet saved; will show as pending.",
+ " 到后端;系统会先生成 ": " to backend; system will generate ",
+ " 前端连接短暂中断,正在按后端产物状态恢复,不会立即判定阶段失败": " frontend connection interrupted; recovering from backend artifact state. Stage will not be marked as failed.",
+ " 已有同一分析范围的有效结果,无需重复执行。": " already has valid results for the same analysis scope. No need to re-execute.",
+ " 的依赖尚未完成": " dependency not yet completed",
+ " 重新提交(自动注释背景)...": " re-submitting (auto-annotation background)...",
+ " 提交失败": " submission failed",
+ " 原始包可复现检查提交失败": " reproducibility check submission failed",
+ "Combo 图修订已提交:task_id=": "Combo plot revision submitted: task_id=",
+ "⏳ 推荐参考数据下载已转入后台:": "⏳ Reference download moved to background: ",
+ "⏸️ 以下阶段等待上游暂停项处理后继续: ": "⏸️ Stages waiting for upstream paused items: ",
+ "⚙️ 亚家族确认后已规划 ": "⚙️ Post-subfamily-confirm plan: ",
+ "⚙️ 准备执行 ": "⚙️ Preparing to execute ",
+ "⚙️ 后台执行阶段: ": "⚙️ Background stage: ",
+ "⚠️ 后台批次中以下阶段因依赖失败被跳过: ": "⚠️ Background batch stages skipped due to dependency failure: ",
+ "✅ PPI/KaKs 前置数据检查通过:检测到已下载近源参考 ": "✅ PPI/KaKs check passed: downloaded reference ",
+ "✅ PPI/KaKs 前置数据检查通过:检测到已有 Ortholog 映射(": "✅ PPI/KaKs check passed: Ortholog mapping found (",
+ "✅ 后台阶段 ": "✅ Background stage ",
+ "✅ 已确认自动注释,正在重新提交 ": "✅ Auto-annotation confirmed; re-submitting ",
+ "✅ 已选择项目参考物种 ": "✅ Selected project reference species: ",
+ "✅ 推荐参考数据下载完成:": "✅ Reference download complete: ",
+ "✅ 阶段 ": "✅ Stage ",
+ "☒ 已恢复并完成。": "☒ Restored and completed.",
+ "❌ 无法执行分析:": "❌ Cannot execute analysis: ",
+ "❌ 服务暂时不可用:": "❌ Service temporarily unavailable: ",
+ "。结果已写入当前 job workspace,输出:": ". Results written to current job workspace, output: ",
+ "。请在 config.xlsx 自动识别弹窗中核对/补齐数据,确认写入正式配置后再点击“确认并执行”。": ". Review/fill data in the config.xlsx auto-detect dialog; confirm to write then click \"Confirm & Execute\".",
+ "亚家族分类确认表。你可以在弹窗中修订 confirmed_subfamily,点击“确认亚家族标签”后保存。": "subfamily classification confirmation table. Revise confirmed_subfamily in the dialog, then click \"Confirm Subfamily Labels\" to save.",
+ "亚家族标签确认被跳过,已停止依赖 confirmed_subfamily.tsv 的后续阶段;如需普通无亚家族标签分析,请重新发起并明确说明\"不需要亚家族\"。": "Subfamily label confirmation skipped; subsequent stages depending on confirmed_subfamily.tsv have been stopped. For regular analysis without subfamily labels, please restart and explicitly state \"no subfamily\".",
+ "发送\"按已保存的方法学方案继续执行\"": "Send \"Continue with saved methodology plan\"",
+ "后台批次无法继续:": "Background batch cannot continue: ",
+ "已打开 config.xlsx 自动识别表。你可以直接修订单行,或用批量修订区筛选、预览、Apply。": "Opened config.xlsx auto-identification table. You can edit individual rows, or use the batch revision area to filter, preview, and Apply.",
+ "已打开 samples.yaml 自动识别表。你可以直接修订样本、分组、重复和对照组。": "Opened samples.yaml auto-identification table. You can revise samples, groups, replicates and controls directly.",
+ "已把目录中的可识别参考资源导入当前 job,并生成 config.xlsx 草稿。请在弹窗中核对和修订,点击“确认并写入正式配置”后再执行正式分析。": "Imported recognizable reference resources into current job and generated config.xlsx draft. Please review and revise in the dialog, then click \"Confirm & Write to Official Config\" before executing.",
+ "已识别到 RNA-seq FASTQ 和参考资源,优先打开 samples.yaml 草稿供你核对样本、分组和对照;config.xlsx 草稿也已生成。": "RNA-seq FASTQ and reference resources detected; opening samples.yaml draft for sample/group/control review. config.xlsx draft also generated.",
+ "已识别到 RNA-seq/FASTQ 数据并生成 samples.yaml 草稿。请在弹窗中核对 run、sample_id、condition、replicate、control_group 后再写入正式 samples.yaml。": "RNA-seq/FASTQ data detected and samples.yaml draft generated. Review run, sample_id, condition, replicate, control_group in the dialog, then confirm to write.",
+ "已重新识别并生成 config.xlsx 草稿。请在弹窗中核对并修订,确认后才会写入正式 config.xlsx。": "Re-identified and generated config.xlsx draft. Review and revise in the dialog; writes to official config.xlsx only after confirmation.",
+ "已重新识别并生成 samples.yaml 草稿。请确认 run、sample_id、condition、replicate 和 control_group 后再写入正式 samples.yaml。": "Re-identified and generated samples.yaml draft. Confirm run, sample_id, condition, replicate, and control_group before writing to official samples.yaml.",
+ "没有找到可展示的结果。": "No displayable results found.",
+ "点击\"浏览\"上传文献": "Click \"Browse\" to upload paper",
+ "点击\"知识图谱\"查看关系": "Click \"Knowledge Graph\" to view relations",
+ "请先核对 config.xlsx 草稿。": "Please review the config.xlsx draft first.",
+ "请补充你的需求。": "Please clarify your requirements.",
+ "进化树图重绘已提交:task_id=": "Phylogeny tree redraw submitted: task_id=",
+ "选择输入框上方\"Analysis\"": "Select \"Analysis\" above the input box",
+ "选择输入框上方\"RAG\"": "Select \"RAG\" above the input box",
+ ",再继续富集分析。": ", then continue enrichment analysis.",
+ ",将用该参考先生成 Ortholog mapping。": ", will generate Ortholog mapping from this reference first.",
+ ";Ortholog/PPI/KaKs 已恢复为待执行,将随当前批次继续调度。": "; Ortholog/PPI/KaKs restored as pending; will continue with current batch.",
+ ",将按该参考检查或生成 Ortholog,并执行 ": "; will check or generate Ortholog from this reference, and execute ",
+ ";开始续跑 Ortholog/PPI/KaKs。": "; resuming Ortholog/PPI/KaKs.",
+ "),将复用该映射执行 ": "), will reuse this mapping and execute ",
+ "),将直接执行 ": "), will execute directly ",
+ ",已自动插入 Ortholog,将先生成 mapping 再执行 ": ", Ortholog auto-inserted; generating mapping first then executing ",
+ ")。已写入 Ortholog/PPI/Ka/Ks 专用参考物种。": "). Written to Ortholog/PPI/Ka/Ks specific reference species.",
+ " 已按用户确认使用 bcftools 完成": " completed with bcftools as user confirmed",
+ " 已暂停依赖 confirmed_subfamily.tsv 的后续阶段;请在亚家族确认表中保存后重新拉取规划继续执行。": " has paused subsequent stages depending on confirmed_subfamily.tsv; save the subfamily confirmation table and re-pull the plan.",
+ " 暂停:需要先准备参考物种 PEP/CDS 或 Ortholog 映射。": "paused: prepare reference species PEP/CDS or Ortholog mapping first.",
+ " 个分析阶段,请确认参数": " analysis stages, please confirm parameters",
+ "📦 后台批次完成:成功 ": "📦 Background batch complete: success ",
+ "🔄 阶段 ": "🔄 Stage ",
+ "🚀 阶段 ": "🚀 Stage ",
+ "config.xlsx 已确认,继续当前目标:": "config.xlsx confirmed; continuing current goal: ",
+ "按 per_species 拆分 ": "Split by per_species ",
+ "⚠️ 检测到 ": "⚠️ Detected ",
+ ",将在确认后启动本地自动注释。": "; auto-annotation will start after confirmation.",
+ "。已写入 Ortholog/PPI/Ka/Ks 专用参考物种。": ". Written to Ortholog/PPI/Ka/Ks specific reference species.",
+ "🚀 准备提交依赖驱动执行,共 ": "🚀 Preparing dependency-driven execution: ",
+ " 个阶段...": " stages...",
+ "⚠️ 批量执行完成:成功 ": "⚠️ Batch complete: success ",
+ ",失败 ": ", failed ",
+ "🎉 批量执行完成!成功: ": "🎉 Batch complete! Success: ",
+ "⏭️ 跳过阶段: ": "⏭️ Skipped stage: ",
+ "⚙️ 提交阶段: ": "⚙️ Submitting stage: ",
+ "⏭️ ": "⏭️ ",
+ " 已提交后端,task_id=": " submitted to backend, task_id=",
+ " 完成": " complete",
+ ",task_id=": ", task_id=",
+ "⚠️ 亚家族确认后自动续跑规划失败:": "⚠️ Post-subfamily-confirm auto-plan failed: ",
+ "亚家族确认已完成,但后端当前 workflowPhase 为 ": "Subfamily confirmation complete; backend workflowPhase is ",
+ ",未自动打开常规分析。": "; no auto-analysis opened.",
+ "已调出": "Opened: ",
+ "亚家族分类确认表没有打开:未找到可确认的分类结果。请先确认该物种已完成 reference_classification。": "Subfamily classification confirmation table not opened: no confirmable results found. Confirm the species has completed reference_classification.",
+ "拓展分析帮助暂时不可用:": "Extension help temporarily unavailable: ",
+ "已保存方法学方案,但暂不执行:": "Methodology plan saved; not executing: ",
+ "🧬 自动背景注释与 ": "🧬 Auto-background annotation and ",
+ "参考数据已就绪": "Reference data ready",
+ "Combo 图修订失败:": "Combo plot revision failed: ",
+ "进化树图重绘失败:": "Phylogeny tree redraw failed: ",
+ ",其它阶段继续执行。": ", other stages continue.",
+ " 个阶段,请确认参数": " stages, please confirm parameters",
+ "—": "—",
+ "✓ 已恢复并完成。": "✓ Restored and completed.",
+ "后台批次完成:成功 ": "Background batch complete: success ",
+ "后台执行阶段: ": "Background stage: ",
+ "拓展分析阶段执行失败:": "Extension stage execution failed: ",
+ "拓展分析阶段:": "Extension stage: ",
+ "已执行拓展分析阶段:": "Extension stage executed: ",
+ "拓展分析阶段失败:": "Extension stage failed: ",
+ "MSA区间重绘已提交": "MSA replot submitted",
+ "MSA区间重绘失败": "MSA replot failed",
+ "已选择:": "Selected: ",
+ "📌 当前已有前台批次执行中,新任务已转为后台执行:": "📌 Foreground batch running; new task moved to background: ",
+ ",已自动插入 Ortholog,将先生成 mapping 再执行": ", Ortholog auto-inserted; generating mapping first then executing",
+ "推荐参考数据下载完成:": "Reference download complete: ",
+ "ABORTED": "ABORTED",
+ "的依赖尚未完成": "dependency not yet completed",
+ "亚家族标签确认被跳过,已停止依赖 confirmed_subfamily.tsv 的后续阶段;如需普通无亚家族标签分析,请重新发起并明确说明": "Subfamily label confirmation skipped; stages depending on confirmed_subfamily.tsv stopped. For regular analysis without subfamily labels, restart and explicitly state: ",
+ "不需要亚家族": "no subfamily",
+ "。": ".",
+ ",将复用该映射执行 ": ", will reuse this mapping and execute ",
+ ";将按该参考检查或生成 Ortholog,并执行 ": "; will check or generate Ortholog from this reference, and execute ",
+ "⚠️ 以下阶段因依赖失败被跳过: ": "⚠️ Stages skipped due to dependency failure: ",
+ "⚠️ 以下阶段等待上游暂停项处理后继续: ": "⚠️ Stages waiting for upstream paused items: ",
+ "启动子自定义绘图已提交": "Promoter custom plot submitted",
+ "启动子自定义绘图失败": "Promoter custom plot failed",
+ "种内共线性 Circos 重绘已提交": "Intraspecies Circos replot submitted",
+ "种内共线性 Circos 重绘失败": "Intraspecies Circos replot failed",
+ " 重新提交(自动注释背景)": " re-submit (auto-annotation background)",
+ " 已暂停依赖 confirmed_subfamily.tsv": " has paused stages depending on confirmed_subfamily.tsv",
+ ";": ";",
+ " 个分析阶段": " analysis stages",
+ " 个阶段": " stages"
};
diff --git a/gfa_core/gene_family_report.py b/gfa_core/gene_family_report.py
index 5fd145909..becc12d30 100644
--- a/gfa_core/gene_family_report.py
+++ b/gfa_core/gene_family_report.py
@@ -14,6 +14,12 @@ from gfa_core.gene_family_report_data import (
ReportSection,
build_report_data,
)
+from gfa_core.gene_family_report_html import (
+ REPORT_RENDERER_VERSION,
+ collect_stage_figures,
+ markdown_to_html,
+ render_stage_figures,
+)
from gfa_core.mas.agents.interpretation_evidence import load_stage_manifests
@@ -40,6 +46,7 @@ class GeneFamilyReportResult:
generation_mode: str
included_stages: tuple[str, ...]
reused: bool = False
+ html_path: Path | None = None
def _normalized_stages(stages: Iterable[str] | None) -> list[str]:
@@ -185,12 +192,32 @@ def _arranged_content(section: ReportSection, arranged: Mapping[str, Any]) -> st
return "\n\n".join(paragraphs)
+def _render_completion_table(report_data: ReportData) -> str:
+ supported_stages = {section.stage for section in report_data.sections if section.stage}
+ included = set(report_data.included)
+ status_labels = {"success": "成功", "partial_success": "部分完成", "failed": "失败", "missing": "未完成"}
+ lines = [
+ f"本次计划 {len(report_data.planned)} 个阶段,纳入解读 {len(report_data.included)} 个,未纳入 {len(report_data.planned) - len(report_data.included)} 个。",
+ "",
+ "| 分析阶段 | 运行状态 | 模板支持 | 纳入报告 |",
+ "|---|---|---|---|",
+ ]
+ for stage in report_data.planned:
+ status = str(report_data.statuses.get(stage, "missing"))
+ lines.append(
+ f"| `{stage}` | {status_labels.get(status, status)} | "
+ f"{'是' if stage in supported_stages else '否'} | {'是' if stage in included else '否'} |"
+ )
+ return "\n".join(lines)
+
+
def _render_markdown(
report_data: ReportData,
*,
job_id: str,
generation_mode: str,
arrangement: Mapping[str, Any],
+ figures_by_stage: Mapping[str, list[dict[str, str]]],
) -> str:
section_data = {section.section_id: section for section in report_data.sections}
arranged = {section["section_id"]: section for section in arrangement["sections"]}
@@ -198,7 +225,15 @@ def _render_markdown(
for section_id, block in _template_blocks():
if section_id not in section_data:
continue
- rendered.append(block.replace("{{SECTION_CONTENT}}", _arranged_content(section_data[section_id], arranged[section_id])))
+ content = (
+ _render_completion_table(report_data)
+ if section_id == "common.completeness"
+ else _arranged_content(section_data[section_id], arranged[section_id])
+ )
+ stage = section_id.removeprefix("stage.") if section_id.startswith("stage.") else ""
+ if stage and figures_by_stage.get(stage):
+ content += render_stage_figures(stage, figures_by_stage[stage])
+ rendered.append(block.replace("{{SECTION_CONTENT}}", content))
markdown = "\n\n".join(rendered)
fallback_notice = (
"> 生成模式:候补脚本。模型 API 不可用或输出越过语言编排边界;报告事实仍来自确定性结果数据。"
@@ -237,9 +272,14 @@ def generate_gene_family_report(
raise ValueError("action must be 'generate' or 'open'")
final_dir = Path(workspace).expanduser().resolve() / "final"
report_path = final_dir / f"{prefix}.gene_family_report.md"
+ html_path = final_dir / f"{prefix}.gene_family_report.html"
manifest_path = final_dir / f"{prefix}.gene_family_report.manifest.json"
if action == "open" and report_path.is_file() and manifest_path.is_file():
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
+ if not html_path.is_file():
+ html_path.write_text(markdown_to_html(report_path.read_text(encoding="utf-8"), report_path, f"{prefix} 基因家族分析结果解读报告"), encoding="utf-8")
+ manifest.setdefault("artifacts", {})["gene_family_report_html"] = {"path": f"final/{html_path.name}", "full_path": str(html_path), "type": "html"}
+ manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
metrics = manifest.get("metrics") or {}
return GeneFamilyReportResult(
report_path,
@@ -248,6 +288,7 @@ def generate_gene_family_report(
str(metrics.get("generation_mode") or "fallback_script"),
tuple(metrics.get("included_stages") or []),
reused=True,
+ html_path=html_path,
)
planned = _normalized_stages(expected_stages)
@@ -283,14 +324,17 @@ def generate_gene_family_report(
"reason": "api_unavailable_or_boundary_violation",
}
+ figures_by_stage = collect_stage_figures(final_dir, report_data.included, manifests)
markdown = _render_markdown(
report_data,
job_id=job_id,
generation_mode=generation_mode,
arrangement=arrangement,
+ figures_by_stage=figures_by_stage,
)
final_dir.mkdir(parents=True, exist_ok=True)
report_path.write_text(markdown, encoding="utf-8")
+ html_path.write_text(markdown_to_html(markdown, report_path, f"{prefix} 基因家族分析结果解读报告"), encoding="utf-8")
manifest = {
"manifest_version": "1.0",
"stage": REPORT_STAGE,
@@ -308,6 +352,8 @@ def generate_gene_family_report(
"included_stages": list(report_data.included),
"unsupported_stages": list(report_data.unsupported_stages),
"failed_or_missing_stages": [stage for stage in planned if stage not in report_data.included],
+ "report_renderer_version": REPORT_RENDERER_VERSION,
+ "embedded_figure_count": sum(len(items) for items in figures_by_stage.values()),
},
"model": model_metadata,
"artifacts": {
@@ -316,6 +362,9 @@ def generate_gene_family_report(
"full_path": str(report_path),
"type": "markdown",
},
+ "gene_family_report_html": {
+ "path": f"final/{html_path.name}", "full_path": str(html_path), "type": "html",
+ },
"gene_family_report_manifest": {
"path": f"final/{manifest_path.name}",
"full_path": str(manifest_path),
@@ -330,4 +379,5 @@ def generate_gene_family_report(
report_data.report_status,
generation_mode,
report_data.included,
+ html_path=html_path,
)
diff --git a/gfa_core/gene_family_report_data.py b/gfa_core/gene_family_report_data.py
index 968765ac8..108a94ae9 100644
--- a/gfa_core/gene_family_report_data.py
+++ b/gfa_core/gene_family_report_data.py
@@ -719,13 +719,7 @@ def build_report_data(
)
for index, sentence in enumerate(integrated_observations, start=1)
),
- interpretations=(
- _sentence(
- "common.integrated.boundary",
- "综合结论只并列上述已提取事实;当前源结果不足以无偏地指定候选基因,也不据此建立功能因果。",
- "interpretation",
- ),
- ),
+ interpretations=(),
),
ReportSection(
"common.limitations",
diff --git a/gfa_core/gene_family_report_html.py b/gfa_core/gene_family_report_html.py
new file mode 100644
index 000000000..e4cfc712b
--- /dev/null
+++ b/gfa_core/gene_family_report_html.py
@@ -0,0 +1,122 @@
+# HTML rendering and representative-figure selection for gene-family reports.
+from __future__ import annotations
+import base64
+import html
+import mimetypes
+from pathlib import Path
+import re
+from typing import Any, Iterable, Mapping
+
+REPORT_RENDERER_VERSION = "gene_family_report_html_v4"
+IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".webp", ".svg"}
+STAGE_FIGURE_TEXT = {
+ "phylogeny": ("系统发育图展示目标成员与参考成员之间的聚类关系。", "结合分支拓扑、节点支持率和成员标签阅读,不把邻近关系直接等同于功能一致。", "目标与参考序列的聚类关系见图,具体分支以图中节点和标签为准。"),
+ "reference_classification": ("参考分类图展示目标成员与已知参考亚家族的相对位置。", "结合参考成员标签、目标成员所在分支及支持率判断分类线索。", "目标成员的参考辅助分类位置见图,分类结论以本节确定性结果为准。"),
+ "motif": ("Motif图展示各成员短保守片段的组成、顺序和相对位置。", "按成员逐行比较Motif颜色、编号、数量和排列,不把Motif相似直接写成功能相同。", "本次成员的Motif组成与排列见图,数量统计以本节结果数据为准。"),
+ "domain": ("Domain图展示蛋白主要功能区域的类型和位置。", "结合结构域名称、覆盖区间和成员间差异阅读。", "各成员的结构域组成见图,覆盖数量和比例以本节结果数据为准。"),
+ "structure": ("基因结构图展示外显子、内含子和基因长度的相对关系。", "结合图例比较外显子数量、结构排列和异常长基因。", "目标成员的基因结构差异见图,具体统计以本节结果数据为准。"),
+ "combo": ("综合组合图并列展示系统发育、Motif、Domain和基因结构信息。", "沿同一成员横向比较多个证据面板,关注一致模式与明显差异。", "多类结构证据的对应关系见图,不据此单独确认功能改变。"),
+ "promoter": ("启动子图展示顺式作用元件的类型、数量或位置分布。", "结合元件图例、位置坐标和成员标签比较,不把预测位点写成已验证调控。", "目标成员启动子元件的分布见图,数量和类别统计以本节结果数据为准。"),
+ "synteny": ("共线性图展示染色体区段和基因对之间的连线关系。", "结合染色体标签、连线和家族成员标记阅读,区分全基因组背景与家族证据。", "本次共线性背景见图,家族专属关系以本节可追溯结果为准。"),
+ "karyotype": ("染色体分布图展示家族成员在染色体上的物理位置。", "结合染色体编号、位置刻度和基因标签查看分布范围。", "目标家族成员的染色体位置见图,成员数和染色体数以本节结果数据为准。"),
+ "interspecies_synteny": ("种间共线性图展示目标物种与参考物种之间的保守区块和基因连线。", "结合物种、染色体、标记连线和成员标签阅读,不直接移植参考物种功能。", "跨物种共线性关系见图,目标与参考家族配对数以本节结果数据为准。"),
+ "pep": ("蛋白特征图展示理化性质、跨膜区域或亚细胞定位预测结果。", "根据具体图例比较成员分布,区分不同预测子任务。", "本次蛋白预测结果见图,具体数量与分类以本节结果数据为准。"),
+ "tfbs": ("Motif统计图展示不同基因的位点计数、全部Motif计数和高频Motif。", "结合基因标签、颜色图例、计数刻度和Motif名称比较分布。", "本次gene、all和top motif统计结果见图,具体计数以图例和源结果为准。"),
+}
+
+def _artifact_image(final_dir: Path, artifact: Any) -> Path | None:
+ if not isinstance(artifact, Mapping): return None
+ raw = str(artifact.get("full_path") or artifact.get("path") or "").strip()
+ target = final_dir / Path(raw).name if raw else None
+ return target if target and target.is_file() and target.suffix.lower() in IMAGE_SUFFIXES else None
+
+def _kind(role: str, filename: str) -> str:
+ value = f"{role} {filename}".lower()
+ groups = (("gene_motif_count", ("gene_motif_heatmap",)), ("all_motif_count", ("all_motif_count_heatmap",)), ("top_motif", ("top_motifs_barplot",)), ("tree", ("tree",)), ("msa", ("msa_plot", "conserved_region")), ("synteny", ("synteny", "circos")), ("motif", ("motif",)), ("domain", ("domain",)), ("structure", ("structure",)), ("combo", ("combo",)), ("promoter_combined", ("promoter_v6_combined",)), ("promoter_position", ("promoter_position_distribution",)), ("promoter", ("promoter",)), ("karyotype", ("karyotype",)), ("tmhmm", ("tmhmm",)), ("subcellular", ("subcellular",)), ("properties", ("properties",)))
+ return next((kind for kind, tokens in groups if any(token in value for token in tokens)), role.lower())
+
+def _rank(role: str, path: Path) -> tuple[int, int, str]:
+ value = f"{role} {path.name}".lower()
+ role_lower = role.lower()
+ preferred = -5 if role_lower == "synteny_marked_png" else -4 if "circular" in value else -2 if "marked" in value else -1 if "summary" in value else 0
+ return (preferred, 0, path.name)
+
+def collect_stage_figures(final_dir: Path, stages: Iterable[str], manifests: Mapping[str, Mapping[str, Any]]) -> dict[str, list[dict[str, str]]]:
+ result = {}
+ for stage in stages:
+ selected = {}; seen = set()
+ for role, artifact in ((manifests.get(stage) or {}).get("artifacts") or {}).items():
+ target = _artifact_image(final_dir, artifact)
+ if target is None or target.name in seen: continue
+ seen.add(target.name); kind = _kind(str(role), target.name)
+ candidate = (_rank(str(role), target), {"role": str(role), "filename": target.name})
+ if kind not in selected or candidate[0] < selected[kind][0]: selected[kind] = candidate
+ if stage == "tfbs":
+ for role, pattern in (
+ ("gene_motif_heatmap", "*gene_motif_heatmap.png"),
+ ("all_motif_count_heatmap", "*all_motif_count_heatmap.png"),
+ ("top_motifs_barplot", "*top_motifs_barplot.png"),
+ ):
+ matches = sorted(final_dir.glob(pattern))
+ if not matches:
+ continue
+ target = matches[0]
+ kind = _kind(role, target.name)
+ selected[kind] = (_rank(role, target), {"role": role, "filename": target.name})
+ figures = [entry[1] for _, entry in sorted(selected.items(), key=lambda item: (item[1][0], item[0]))][:4]
+ if figures: result[stage] = figures
+ return result
+
+def render_stage_figures(stage: str, figures: list[dict[str, str]]) -> str:
+ meaning, reading, result = STAGE_FIGURE_TEXT.get(stage, ("结果图展示该阶段的主要计算产物。", "结合图题、坐标轴、图例和成员标签阅读。", "本阶段代表结果见图,具体事实以本节确定性数据为准。"))
+ lines = ["", "### 结果图"]
+ for index, figure in enumerate(figures, 1):
+ title = figure["role"]
+ lines.extend(["", f"#### 图 {index}. {title}", "", f"", "", f"图形含义:{meaning}", "", f"阅读方法:{reading}", "", f"本次结果:{result}"])
+ return "\n".join(lines)
+
+def _inline(value: str, report_path: Path) -> str:
+ tokens = []
+ def stash(content):
+ token = f"@@GFAHTML{len(tokens)}@@"; tokens.append(content); return token
+ def image(match):
+ alt = match.group(1).strip() or "基因家族结果图"; target = report_path.parent / Path(match.group(2).strip().replace("\\", "/")).name
+ source = target.name; mime = mimetypes.guess_type(target.name)[0] or ""
+ if target.is_file() and mime.startswith("image/"): source = f"data:{mime};base64,{base64.b64encode(target.read_bytes()).decode('ascii')}"
+ return stash(f'
| {_inline(c, report_path)} | " for c in rows[0]) + '
|---|
| {_inline(c, report_path)} | " for c in row) + '
{_inline(' | '.join(row), report_path)}
" for row in rows) + for raw in markdown.splitlines(): + line = raw.strip() + if line.startswith("|") and line.endswith("|"): close_list(); table_rows.append([c.strip() for c in line.strip("|").split("|")]); continue + flush_table() + if not line: close_list(); continue + heading = re.match(r"^(#{1,6})\s+(.+)$", line) + if heading: close_list(); level=len(heading.group(1)); output.append(f"{_inline(line[2:], report_path)}"); continue + item = re.match(r"^([-*]|\d+\.)\s+(.+)$", line) + if item: + tag = "ol" if item.group(1)[0].isdigit() else "ul" + if list_tag != tag: close_list(); list_tag=tag; output.append(f"<{tag}>") + output.append(f"
{rendered}
") + close_list(); flush_table(); body="\n".join(output); safe_title=html.escape(title) + return f'''| {_markdown_inline(cell, report_path)} | " for cell in header) + "
|---|
| {_markdown_inline(cell, report_path)} | " for cell in row) + "
{html.escape(chr(10).join(code_lines))}")
+ code_lines = []
+ in_code = False
+ else:
+ in_code = True
+ continue
+ if in_code:
+ code_lines.append(line)
+ continue
+ if line.startswith("|") and line.endswith("|"):
+ close_list()
+ table_rows.append([cell.strip() for cell in line.strip("|").split("|")])
+ continue
+ flush_table()
+ if not line.strip():
+ close_list()
+ continue
+ heading = re.match(r"^(#{1,6})\s+(.+)$", line)
+ if heading:
+ close_list()
+ level = len(heading.group(1))
+ output.append(f"{_markdown_inline(line[2:], report_path)}") + continue + bullet = re.match(r"^-\s+(.+)$", line) + ordered = re.match(r"^\d+\.\s+(.+)$", line) + if bullet or ordered: + wanted = "ul" if bullet else "ol" + if list_tag != wanted: + close_list() + list_tag = wanted + output.append(f"<{list_tag}>") + item = bullet.group(1) if bullet else ordered.group(1) + output.append(f"
{_markdown_inline(line, report_path)}
") + close_list() + flush_table() + if in_code: + output.append(f"{html.escape(chr(10).join(code_lines))}")
+
+ safe_title = html.escape(title)
+ body = "\n".join(output)
+ return f"""
+
+
+
+
+