轻量拓展分析支持上游样本继承:同job内成功运行产物自动预填下游输入槽

- 新增 upstream.py:枚举 workspace/<job>/extension_runs/*/runtime_*/outputs 的成功产物,
  按 basename 精确匹配优先、role 关键词辅助(多命中取最新 run)收敛为每槽一个候选
- 新增 GET /api/lightweight-extension-stages/{id}/upstream-candidates?job_id= 端点,
  返回 workspace 相对形式的候选与来源信息;仅对契约 patch_targets 可补丁槽位启用改名匹配,
  不可补丁槽位留空由用户手填,避免 PATCH_TARGET_UNAVAILABLE
- 前端运行向导:LLM slots 之后对空槽填充上游候选并显示绿色来源标记(可修改),
  用户手动上传/导入时清除来源标记
- runtime_binding 支持目录输入:_resolve_inputs/_job_path 放宽目录,
  物化走 copy_tree 整树拷贝 + _sha256_tree 目录摘要校验,isolate 跳过独立副本
- nl_intent prompt 规则 4 追加 extension_runs 上游产物引用指引
- 新增 20+ 单测覆盖上游发现/端点/目录输入/前端契约

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
potaser
2026-09-01 20:26:00 +08:00
co-authored by Claude Fable 5
parent abdd486ed8
commit cb3732dc71
13 changed files with 1095 additions and 18 deletions
+27 -2
View File
@@ -1761,7 +1761,10 @@
<div v-if="lightweightNlRun.step === 1" style="display:flex;flex-direction:column;gap:10px;">
<div v-for="input in lightweightNlRun.inputs" :key="input.key" style="display:grid;grid-template-columns:150px 1fr;gap:8px;align-items:center;">
<span style="font-size:12px;color:#606266;word-break:break-all;">{{ input.key }}</span>
<el-input v-model="input.value" size="small" placeholder="当前 job 的 input/ 或 workspace/ 中的文件路径"></el-input>
<div style="display:flex;flex-direction:column;gap:2px;">
<el-input v-model="input.value" size="small" placeholder="当前 job 的 input/ 或 workspace/ 中的文件路径"></el-input>
<span v-if="input.source" style="font-size:11px;color:#67c23a;">来源:{{ input.source }}(可修改)</span>
</div>
</div>
<div style="border:1px dashed #c0c4cc;border-radius:4px;padding:14px;text-align:center;color:#909399;font-size:12px;"
@dragover.prevent @drop.prevent="handleNlRunFileDrop">
@@ -20443,6 +20446,16 @@ compare: [[flesh, peel]]`;
if (!response.ok) throw new Error(await extensionApiError(response, '读取拓展分析说明失败'));
return await response.json();
};
const loadUpstreamCandidates = async (extensionId) => {
const jobId = String(jobInfo.job_id || '').trim();
if (!jobId || jobId === 'default_job') return {};
try {
const response = await extensionApiFetch(`/api/lightweight-extension-stages/${encodeURIComponent(extensionId)}/upstream-candidates?job_id=${encodeURIComponent(jobId)}`);
if (!response.ok) return {};
const data = await response.json();
return (data && data.candidates) || {};
} catch (_) { return {}; }
};
const isLightweightExtensionId = (extensionId) => (lightweightExtensions.value || []).some(item => item.extension_id === extensionId);
const lightweightRunById = (runs, runId) => (runs || []).find(item => item && item.run_id === runId) || {};
const lightweightResumeNext = (action) => ({
@@ -20880,11 +20893,13 @@ compare: [[flesh, peel]]`;
});
if (hitIndex >= 0) {
input.value = remaining.splice(hitIndex, 1)[0];
input.source = '';
}
}
const emptyInputs = lightweightNlRun.inputs.filter(item => !String(item.value || '').trim());
for (let i = 0; i < remaining.length && i < emptyInputs.length; i++) {
emptyInputs[i].value = remaining[i];
emptyInputs[i].source = '';
}
const matched = uploaded.length - remaining.length;
ElMessage.success(`已上传 ${uploaded.length} 个文件${matched ? `,已自动匹配 ${matched} 个输入槽` : ''}`);
@@ -20911,6 +20926,7 @@ compare: [[flesh, peel]]`;
const targetInput = lightweightNlRun.inputs.find(item => !String(item.value || '').trim()) || lightweightNlRun.inputs[0];
if (targetInput) {
targetInput.value = Object.values(imported)[0] || '';
targetInput.source = '';
}
ElMessage.success(`已导入 ${Object.keys(imported).length} 个路径`);
} catch (err) {
@@ -20923,7 +20939,7 @@ compare: [[flesh, peel]]`;
if (!extensionId) return null;
try {
const help = await lightweightHelp(extensionId);
const inputs = (help.inputs || []).filter(item => item && item.replaceable).map(item => ({ key: item.path, value: '' }));
const inputs = (help.inputs || []).filter(item => item && item.replaceable).map(item => ({ key: item.path, value: '', source: '' }));
const parameters = (help.parameters || []).map(item => ({ key: item.fact_id, flag: item.flag || item.fact_id, display_name: item.display_name || '', value: item.value || '' }));
const slots = (intentParams && intentParams.llm_slots) || {};
const slotInputs = slots.input_overrides || {};
@@ -20932,6 +20948,15 @@ compare: [[flesh, peel]]`;
const value = slotInputs[item.key];
if (value) item.value = value;
});
const upstreamCandidates = await loadUpstreamCandidates(extensionId);
inputs.forEach(item => {
if (String(item.value || '').trim()) return;
const candidate = upstreamCandidates[item.key];
if (candidate && candidate.path) {
item.value = candidate.path;
item.source = `上游样本 ${candidate.source_extension_id}/${candidate.run_id}`;
}
});
parameters.forEach(item => {
const value = slotParameters[item.key];
if (value) item.value = value;