67 lines
2.6 KiB
Python
67 lines
2.6 KiB
Python
"""任务启动点数预检 + 运行中熔断(用户反馈:没点数了还在跑)。
|
|||
|
|
|
||
|
|
预检(readiness 阶段):按成本预估与当前余额比较,余额必须覆盖预估 + 20% 缓冲。
|
||
|
|
熔断(runner 每段结束后):点数/余额类错误说明继续跑只会白跑,立刻停止整个任务。
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from sqlmodel import Session
|
||
|
|
|
||
|
|
from .billing import point_state
|
||
|
|
|
||
|
|
# 启动缓冲:预估之外还要留 20%,防止输出超长导致中途断粮
|
||
|
|
BALANCE_BUFFER_RATIO = 0.2
|
||
|
|
|
||
|
|
|
||
|
|
def points_preflight(session: Session, campaign, estimated_points: int) -> dict:
|
||
|
|
"""启动前点数校验。返回 {ok, blocker, info}。
|
||
|
|
|
||
|
|
ok=False 时 blocker 是可直接展示的中文原因。
|
||
|
|
"""
|
||
|
|
owner_id = int(campaign.owner_user_id or 0)
|
||
|
|
org_id = int(campaign.organization_id or 1)
|
||
|
|
if not owner_id:
|
||
|
|
return {"ok": True, "blocker": "", "info": None}
|
||
|
|
state = point_state(session, owner_id, org_id)
|
||
|
|
balance = int(state.get("points") or 0)
|
||
|
|
need = int(estimated_points or 0)
|
||
|
|
required = int(need * (1 + BALANCE_BUFFER_RATIO))
|
||
|
|
info = {"balance": balance, "estimated_points": need,
|
||
|
|
"required_points": required, "buffer_ratio": BALANCE_BUFFER_RATIO}
|
||
|
|
if balance < required:
|
||
|
|
gap = required - balance
|
||
|
|
return {
|
||
|
|
"ok": False,
|
||
|
|
"blocker": (f"点数不足:本次预估需要约 {need:,} 点数(含 20% 缓冲共 {required:,}),"
|
||
|
|
f"当前余额 {balance:,},还差 {gap:,}。请联系管理员在「用量与账单」增加点数后重试"),
|
||
|
|
"info": info,
|
||
|
|
}
|
||
|
|
return {"ok": True, "blocker": "", "info": info}
|
||
|
|
|
||
|
|
|
||
|
|
# 段级失败里属于「继续跑没有意义」的错误类型:钱的问题不会自愈。
|
||
|
|
CIRCUIT_BREAK_FAILURE_TYPES = {"billing", "balance", "auth"}
|
||
|
|
|
||
|
|
|
||
|
|
def should_break_run(failure: dict | None) -> tuple[bool, str]:
|
||
|
|
"""根据 execute_segment 返回的 failure 判断是否熔断整个任务。
|
||
|
|
|
||
|
|
返回 (是否熔断, 原因)。
|
||
|
|
"""
|
||
|
|
if not failure:
|
||
|
|
return False, ""
|
||
|
|
ftype = str(failure.get("type") or "")
|
||
|
|
detail = str(failure.get("detail") or "")
|
||
|
|
if ftype in CIRCUIT_BREAK_FAILURE_TYPES:
|
||
|
|
return True, detail[:160] or "模型服务拒绝(余额/鉴权问题)"
|
||
|
|
low = detail.lower()
|
||
|
|
if ("点数余额不足" in detail or "余额不足" in detail
|
||
|
|
or "insufficient" in low and ("balance" in low or "quota" in low)
|
||
|
|
or "invalid api key" in low):
|
||
|
|
return True, detail[:160]
|
||
|
|
return False, ""
|
||
|
|
|
||
|
|
|
||
|
|
__all__ = ["points_preflight", "should_break_run", "BALANCE_BUFFER_RATIO",
|
||
|
|
"CIRCUIT_BREAK_FAILURE_TYPES"]
|