feat: 成本预算与失败通知第一批(G1/G2/G5/G6/无价上报)

- G1:修正 _check_quota docstring(Local popen 30 秒轮询掐断、
  Docker/同步路径只有边界检查),README 配额节同步;followup
  创建不查配额是已声明设计,保持不动。
- G2:notify_failure 对插值 shlex.quote,模板错误记日志不静默
  吞,命令返回值/超时/异常记 host-run.log;dispatch 前置预算门
  与 goal 预算中断补 budget-exceeded 通知(没配命令时只写日志)。
- G5/G6:重试与续跑的配额检查按任务创建者查(费用记创建者名下),
  维护者替他人操作不再用错人的额度;续跑 API 侧补查,超限回
  402 且不改状态,不再先 200 排队再悄悄 interrupted。
- 无价上报:配额自查/管理端点返回增加 unpriced_actual_tokens
  (actual 且 usd_micros 为 NULL,与 CostStore 口径一致),只读
  上报、不计价、不拦任务;不给 gpt-5.6-terra 编造价格。
测试:host 68、cloud 760(4 跳过)、deploy 19 全过。
This commit is contained in:
host committed 2026-10-27 10:13:51 +00:00
1 parent c2f82da5e0
commit e4b5545a24
5 files changed
+304 -38

No files matched your search

+60 -8
View File
@@ -48,6 +48,7 @@ import json
import os
import queue
import re
import shlex
import subprocess
import sys
import threading
@@ -1375,20 +1376,59 @@ def budget_exceeded(state, cfg):
def notify_failure(state_path, cfg, task_name, result):
"""失败通知:先写 host-run.log(一定有),再跑用户配置的
notify_command(本地 shell 命令,只来自用户自己的配置,
不是外部输入;模板里可用 {task} {status})。"""
text = (result or {}).get("text", "") or ""
msg = f"FAILED {task_name}: {result.get('status')} {text[:200]}"
不是外部输入;模板里可用 {task} {status} {text})。
插进命令串的值一律 shlex.quote:模型文本可能含 shell 元
字符(引号、分号、$() 等),不加 quote 就等于把模型输出
当命令执行。模板本身有问题(未知占位符、未成对的大括号)
时不再静默吞掉:记一条 template error 到 host-run.log 并
且不跑这条命令。命令的返回值 / 超时 / 异常同样记日志,
否则通知丢了都无从发现(旧行为是异常全吞、无任何记录)。
没有配 notify_command 时只写 host-run.log,不外发。"""
result = result or {}
text = result.get("text", "") or ""
status = result.get("status", "") or ""
msg = f"FAILED {task_name}: {status} {text[:200]}"
try:
log_event(state_path, msg)
except Exception:
pass
cmd = (cfg or {}).get("notify_command")
if cmd:
if not cmd:
return msg
try:
formatted = cmd.format(task=shlex.quote(str(task_name)),
status=shlex.quote(str(status)),
text=shlex.quote(str(text[:200])))
except (KeyError, IndexError, ValueError) as exc:
# 模板写错(例如 {unknown} 或单个 {)时 .format 抛错:
# 明确记日志、不跑命令、不让调用方以为通知已发出。
try:
subprocess.run(cmd.format(task=task_name,
status=result.get("status", ""),
text=text[:200]),
shell=True, timeout=30)
log_event(state_path,
"notify command template error: "
f"{type(exc).__name__}: {exc}")
except Exception:
pass
return msg
try:
proc = subprocess.run(formatted, shell=True, timeout=30)
try:
log_event(state_path,
f"notify command rc={proc.returncode} "
f"task={task_name}")
except Exception:
pass
except subprocess.TimeoutExpired:
try:
log_event(state_path,
f"notify command timeout task={task_name}")
except Exception:
pass
except Exception as exc:
try:
log_event(state_path,
"notify command failed: "
f"{type(exc).__name__} task={task_name}")
except Exception:
pass
return msg
@@ -1969,6 +2009,11 @@ def run_goal_loop(server, state, state_path, task, goal_text, verify,
if budget_stopped:
task["status"] = "interrupted"
task["goal_stopped_reason"] = "budget exceeded: max_daily_turns"
# goal 预算中断以前只打印:与 dispatch 前置门同款,
# 补一次 budget-exceeded 通知(没配命令时只写日志)。
notify_failure(state_path, cfg, task.get("name") or "goal",
{"status": "budget-exceeded",
"text": "budget exceeded: max_daily_turns"})
else:
task["status"] = "goal-met" if ok else "goal-unmet"
if task["status"] == "goal-unmet":
@@ -2423,6 +2468,13 @@ def main():
if args.cmd == "dispatch":
if budget_exceeded(state, cfg):
print("预算已用完(max_daily_turns),本次不派发,见 doctor 的 usage")
# 前置预算门以前只打印:配了 notify_command 的
# 用户收不到任何通知。补一次 budget-exceeded
# 通知;没配命令时 notify_failure 只写日志,
# 不会无配置刷屏。
notify_failure(args.state, cfg, args.name,
{"status": "budget-exceeded",
"text": "budget exceeded: max_daily_turns"})
return
deps = [d.strip() for d in args.depends_on.split(",")
if d.strip()] if args.depends_on else []