diff --git a/web/src/components/graph-console/GraphRunPane.vue b/web/src/components/graph-console/GraphRunPane.vue index 975c0c0..ef4b046 100644 --- a/web/src/components/graph-console/GraphRunPane.vue +++ b/web/src/components/graph-console/GraphRunPane.vue @@ -28,6 +28,7 @@ + 下一步将执行 {{ nextNode.name || nextNode.defNodeId }} 自动模式下一批最多并发 {{ nextNodes.length }} 个: {{ n.name || n.defNodeId }} @@ -67,6 +68,35 @@ + + + @@ -116,6 +146,8 @@ const err = ref(''); const notice = ref(''); const mode = ref('manual'); const confirmAuto = ref(false); +const showBudgetModal = ref(false); +const budgetForm = ref({ hours: 8, calls: '', tokens: '', resume: true }); const sseStatus = ref('idle'); let es = null; @@ -138,6 +170,14 @@ const nextNodes = computed(() => { const nextNode = computed(() => nextNodes.value[0] || null); // 手动模式「下一步」提示仍用单数 const selectedNode = computed(() => nodes.value.find((n) => String(n.defNodeId) === selectedDefId.value) || null); const canRetry = computed(() => selectedNode.value && ['fail', 'blocked'].includes(selectedNode.value.status)); +// 与后端 ensureStatus 同一张表(§四.C):/budget 只在 run_status='blocked' 时可用; +// budget 为 null 说明这一层不是挂预算的 root run(扣减与熔断只在 root run 上生效), +// 追加了也没有意义,按钮直接不亮 +const canExtendBudget = computed(() => !!budget.value && run.value?.run_status === 'blocked'); +const budgetHasAnyDelta = computed(() => { + const f = budgetForm.value; + return [f.hours, f.calls, f.tokens].some((v) => Number(v) > 0); +}); const remain = computed(() => { const b = budget.value || {}; @@ -277,6 +317,21 @@ function onStart() { } function onRetry() { if (canRetry.value) act('retry'); } +function openBudgetModal() { + budgetForm.value = { hours: 8, calls: '', tokens: '', resume: true }; + showBudgetModal.value = true; +} +function submitBudget() { + const f = budgetForm.value; + const patch = {}; + if (Number(f.hours) > 0) patch.wallclock_ms = Number(f.hours) * 3600 * 1000; + if (Number(f.calls) > 0) patch.total_agent_calls = Number(f.calls); + if (Number(f.tokens) > 0) patch.total_tokens = Number(f.tokens); + patch.resume = !!f.resume; + showBudgetModal.value = false; + act('budget', patch); +} + function onSelect(node) { selectedDefId.value = String(node.defNodeId); emit('select', node); @@ -374,6 +429,7 @@ function fmtNum(n) { return (n || 0).toLocaleString(); } .btn:disabled { opacity:.4; cursor:not-allowed; } .btn.go { background:#1a7f37; border-color:#1a7f37; color:#fff; } .btn.retry { border-color:#c96a00; color:#c96a00; } +.btn.budget { border-color:#8a5cd6; color:#8a5cd6; } .next-hint { font-size:11px; color:#666; } .next-hint code { background:#eef4ff; padding:1px 5px; border-radius:3px; } .notice { color: var(--text-secondary); background: var(--bg-overlay); border: 1px dashed var(--border-primary); @@ -389,4 +445,7 @@ function fmtNum(n) { return (n || 0).toLocaleString(); } .est { font-size:12.5px; color:#444; padding-left:18px; margin:0 0 16px; } .est li { margin-bottom:4px; } .modal-btns { display:flex; justify-content:flex-end; gap:8px; } +.budget-field { display:flex; align-items:center; justify-content:space-between; gap:10px; font-size:12.5px; color:#444; margin-bottom:8px; } +.budget-field input { width:140px; font-size:12.5px; padding:3px 6px; border:1px solid #ddd; border-radius:4px; } +.budget-resume { display:flex; align-items:center; gap:6px; font-size:12.5px; color:#444; margin:4px 0 14px; }