Commit b5bd5e7127738a3f4c56bc464590d15a2adf67dd
1 parent
a77934e7
feat(graph-console): 加「追加预算」按钮
run 撞墙钟/调用次数/token 上限 blocked 时,之前界面上没有入口, 只能开发者工具手搓 fetch 调 POST /:id/budget。加一个按钮 + 小弹窗, 三维增量(留空不追加)+ 追加后立即启动,复用现有的 act() 提交入口 与 confirmAuto 同款 modal 样式。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U7xYyvv3pzuV9diMVUH5QL
Showing
1 changed file
with
59 additions
and
0 deletions
web/src/components/graph-console/GraphRunPane.vue
| ... | ... | @@ -28,6 +28,7 @@ |
| 28 | 28 | <button class="btn" :disabled="busy || !can('pause')" @click="act('pause')">⏸ 暂停</button> |
| 29 | 29 | <button class="btn" :disabled="busy || !can('stop')" @click="act('stop')">⏹ 停止</button> |
| 30 | 30 | <button class="btn retry" :disabled="busy || !canRetry" @click="onRetry">🔁 重试</button> |
| 31 | + <button class="btn budget" :disabled="busy || !canExtendBudget" @click="openBudgetModal">💰 追加预算</button> | |
| 31 | 32 | <span v-if="nextNodes.length === 1" class="next-hint">下一步将执行 <code>{{ nextNode.name || nextNode.defNodeId }}</code></span> |
| 32 | 33 | <span v-else-if="nextNodes.length > 1" class="next-hint">自动模式下一批最多并发 <code>{{ nextNodes.length }}</code> 个: |
| 33 | 34 | <code v-for="n in nextNodes" :key="n.defNodeId">{{ n.name || n.defNodeId }}</code></span> |
| ... | ... | @@ -67,6 +68,35 @@ |
| 67 | 68 | </div> |
| 68 | 69 | </div> |
| 69 | 70 | </div> |
| 71 | + | |
| 72 | + <!-- 追加预算:撞顶(尤其是墙钟)之前只能开发者工具手搓 fetch 调 POST /:id/budget, | |
| 73 | + 界面上没有入口。三维各自是**增量**(叠加在当前上限上,不是替换), | |
| 74 | + 留空的维度不追加——后端 extendBudget 本来就是这个语义(§四.C)。 --> | |
| 75 | + <div v-if="showBudgetModal" class="modal-back" @click.self="showBudgetModal = false"> | |
| 76 | + <div class="modal"> | |
| 77 | + <h3>追加预算</h3> | |
| 78 | + <p class="lead">这条 run 已经 blocked。三个维度都是往当前上限上加,留空 = 不追加那一维。</p> | |
| 79 | + <ul class="est" v-if="budget"> | |
| 80 | + <li>当前剩余:<b>{{ remain.calls }}</b> 次调用 · <b>{{ fmtNum(remain.tokens) }}</b> tokens</li> | |
| 81 | + </ul> | |
| 82 | + <label class="budget-field">墙钟时长(小时) | |
| 83 | + <input type="number" min="0" step="1" v-model.number="budgetForm.hours" placeholder="例如 8" /> | |
| 84 | + </label> | |
| 85 | + <label class="budget-field">调用次数 | |
| 86 | + <input type="number" min="0" step="1" v-model.number="budgetForm.calls" placeholder="留空不追加" /> | |
| 87 | + </label> | |
| 88 | + <label class="budget-field">tokens | |
| 89 | + <input type="number" min="0" step="1" v-model.number="budgetForm.tokens" placeholder="留空不追加" /> | |
| 90 | + </label> | |
| 91 | + <label class="budget-resume"> | |
| 92 | + <input type="checkbox" v-model="budgetForm.resume" /> 追加后立即启动(沿用当前的 手动/自动 选择) | |
| 93 | + </label> | |
| 94 | + <div class="modal-btns"> | |
| 95 | + <button class="btn" @click="showBudgetModal = false">取消</button> | |
| 96 | + <button class="btn go" :disabled="!budgetHasAnyDelta" @click="submitBudget">追加</button> | |
| 97 | + </div> | |
| 98 | + </div> | |
| 99 | + </div> | |
| 70 | 100 | </div> |
| 71 | 101 | </template> |
| 72 | 102 | |
| ... | ... | @@ -116,6 +146,8 @@ const err = ref(''); |
| 116 | 146 | const notice = ref(''); |
| 117 | 147 | const mode = ref('manual'); |
| 118 | 148 | const confirmAuto = ref(false); |
| 149 | +const showBudgetModal = ref(false); | |
| 150 | +const budgetForm = ref({ hours: 8, calls: '', tokens: '', resume: true }); | |
| 119 | 151 | const sseStatus = ref('idle'); |
| 120 | 152 | |
| 121 | 153 | let es = null; |
| ... | ... | @@ -138,6 +170,14 @@ const nextNodes = computed(() => { |
| 138 | 170 | const nextNode = computed(() => nextNodes.value[0] || null); // 手动模式「下一步」提示仍用单数 |
| 139 | 171 | const selectedNode = computed(() => nodes.value.find((n) => String(n.defNodeId) === selectedDefId.value) || null); |
| 140 | 172 | const canRetry = computed(() => selectedNode.value && ['fail', 'blocked'].includes(selectedNode.value.status)); |
| 173 | +// 与后端 ensureStatus 同一张表(§四.C):/budget 只在 run_status='blocked' 时可用; | |
| 174 | +// budget 为 null 说明这一层不是挂预算的 root run(扣减与熔断只在 root run 上生效), | |
| 175 | +// 追加了也没有意义,按钮直接不亮 | |
| 176 | +const canExtendBudget = computed(() => !!budget.value && run.value?.run_status === 'blocked'); | |
| 177 | +const budgetHasAnyDelta = computed(() => { | |
| 178 | + const f = budgetForm.value; | |
| 179 | + return [f.hours, f.calls, f.tokens].some((v) => Number(v) > 0); | |
| 180 | +}); | |
| 141 | 181 | |
| 142 | 182 | const remain = computed(() => { |
| 143 | 183 | const b = budget.value || {}; |
| ... | ... | @@ -277,6 +317,21 @@ function onStart() { |
| 277 | 317 | } |
| 278 | 318 | function onRetry() { if (canRetry.value) act('retry'); } |
| 279 | 319 | |
| 320 | +function openBudgetModal() { | |
| 321 | + budgetForm.value = { hours: 8, calls: '', tokens: '', resume: true }; | |
| 322 | + showBudgetModal.value = true; | |
| 323 | +} | |
| 324 | +function submitBudget() { | |
| 325 | + const f = budgetForm.value; | |
| 326 | + const patch = {}; | |
| 327 | + if (Number(f.hours) > 0) patch.wallclock_ms = Number(f.hours) * 3600 * 1000; | |
| 328 | + if (Number(f.calls) > 0) patch.total_agent_calls = Number(f.calls); | |
| 329 | + if (Number(f.tokens) > 0) patch.total_tokens = Number(f.tokens); | |
| 330 | + patch.resume = !!f.resume; | |
| 331 | + showBudgetModal.value = false; | |
| 332 | + act('budget', patch); | |
| 333 | +} | |
| 334 | + | |
| 280 | 335 | function onSelect(node) { |
| 281 | 336 | selectedDefId.value = String(node.defNodeId); |
| 282 | 337 | emit('select', node); |
| ... | ... | @@ -374,6 +429,7 @@ function fmtNum(n) { return (n || 0).toLocaleString(); } |
| 374 | 429 | .btn:disabled { opacity:.4; cursor:not-allowed; } |
| 375 | 430 | .btn.go { background:#1a7f37; border-color:#1a7f37; color:#fff; } |
| 376 | 431 | .btn.retry { border-color:#c96a00; color:#c96a00; } |
| 432 | +.btn.budget { border-color:#8a5cd6; color:#8a5cd6; } | |
| 377 | 433 | .next-hint { font-size:11px; color:#666; } |
| 378 | 434 | .next-hint code { background:#eef4ff; padding:1px 5px; border-radius:3px; } |
| 379 | 435 | .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(); } |
| 389 | 445 | .est { font-size:12.5px; color:#444; padding-left:18px; margin:0 0 16px; } |
| 390 | 446 | .est li { margin-bottom:4px; } |
| 391 | 447 | .modal-btns { display:flex; justify-content:flex-end; gap:8px; } |
| 448 | +.budget-field { display:flex; align-items:center; justify-content:space-between; gap:10px; font-size:12.5px; color:#444; margin-bottom:8px; } | |
| 449 | +.budget-field input { width:140px; font-size:12.5px; padding:3px 6px; border:1px solid #ddd; border-radius:4px; } | |
| 450 | +.budget-resume { display:flex; align-items:center; gap:6px; font-size:12.5px; color:#444; margin:4px 0 14px; } | |
| 392 | 451 | </style> | ... | ... |