实现侧: - 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。 - 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。 - 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。 - 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。 - 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。 - 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。 - 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。 - 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。 用例侧: - 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存; - 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
712 lines
30 KiB
Python
712 lines
30 KiB
Python
"""版本化额度与逐调用预算账本;预留、取消和结算都在短事务中完成。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from collections.abc import Callable, Iterator
|
|
from contextlib import contextmanager
|
|
from dataclasses import asdict, dataclass, replace
|
|
from datetime import datetime, timedelta
|
|
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
|
|
from typing import Any, LiteralString
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import psycopg
|
|
from psycopg import sql
|
|
from psycopg.rows import dict_row
|
|
from psycopg.types.json import Jsonb
|
|
|
|
from muse.任务运行.任务领取 import 校验领取
|
|
from muse.任务运行.存储 import 任务存储
|
|
from muse.任务运行.模型 import 内容哈希, 领取凭证
|
|
from muse.任务运行.角色策略 import 角色身份
|
|
from muse.共享.调用身份 import 用途
|
|
from muse.共享.错误 import Muse错误
|
|
from muse.基础设施.数据库.连接 import 数据库工厂
|
|
|
|
_金额精度 = Decimal("0.000001")
|
|
|
|
|
|
class 预算错误(Muse错误):
|
|
错误码 = "MUSE_BUDGET"
|
|
|
|
|
|
class 预算不足(预算错误):
|
|
错误码 = "MUSE_BUDGET_EXHAUSTED"
|
|
|
|
|
|
class 预算状态冲突(预算错误):
|
|
错误码 = "MUSE_BUDGET_STATE"
|
|
|
|
|
|
def 金额(值: Decimal | str | int) -> Decimal:
|
|
try:
|
|
if isinstance(值, bool):
|
|
raise ValueError
|
|
结果 = Decimal(str(值))
|
|
if not 结果.is_finite() or 结果 < 0:
|
|
raise ValueError
|
|
return 结果.quantize(_金额精度, rounding=ROUND_HALF_UP)
|
|
except (InvalidOperation, ValueError):
|
|
raise 预算错误("金额必须是明确的非负有限数") from None
|
|
|
|
|
|
def _时区时间(值: datetime) -> datetime:
|
|
if 值.tzinfo is None or 值.utcoffset() is None:
|
|
raise 预算错误("预算时间必须包含时区")
|
|
return 值
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class 额度窗口:
|
|
起点: datetime
|
|
终点: datetime
|
|
|
|
|
|
def 归属窗口(当前: datetime, 时区: str = "Asia/Shanghai") -> 额度窗口:
|
|
"""沿用每日 00/05/10/15/20 边界;末窗到午夜,避免默改既有额度合同。"""
|
|
本地 = _时区时间(当前).astimezone(ZoneInfo(时区))
|
|
起点 = 本地.replace(hour=(本地.hour // 5) * 5, minute=0, second=0, microsecond=0)
|
|
终点 = (
|
|
起点 + timedelta(hours=5) if 起点.hour < 20 else (起点 + timedelta(days=1)).replace(hour=0)
|
|
)
|
|
return 额度窗口(起点, 终点)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class 额度策略:
|
|
账户ID: str
|
|
版本: str
|
|
窗口金额上限: Decimal
|
|
窗口调用上限: int
|
|
时区: str = "Asia/Shanghai"
|
|
|
|
def __post_init__(self) -> None:
|
|
object.__setattr__(self, "窗口金额上限", 金额(self.窗口金额上限))
|
|
if not self.账户ID or not self.版本 or self.窗口金额上限 <= 0:
|
|
raise 预算错误("额度策略需要身份、版本和正数金额上限")
|
|
if type(self.窗口调用上限) is not int or self.窗口调用上限 < 1:
|
|
raise 预算错误("窗口调用上限必须是正整数")
|
|
ZoneInfo(self.时区)
|
|
|
|
def 冻结(self) -> dict:
|
|
return {**asdict(self), "窗口金额上限": str(self.窗口金额上限)}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class 角色预算:
|
|
角色: str
|
|
计划次数: int
|
|
最多次数: int
|
|
单次上限: Decimal
|
|
|
|
def __post_init__(self) -> None:
|
|
object.__setattr__(self, "单次上限", 金额(self.单次上限))
|
|
if self.角色 not in 角色身份 or self.单次上限 <= 0:
|
|
raise 预算错误("角色预算需要已登记角色和正数单次上限")
|
|
if any(type(n) is not int or n < 1 for n in (self.计划次数, self.最多次数)):
|
|
raise 预算错误("调用计划与安全容量必须为正整数")
|
|
if self.计划次数 > self.最多次数:
|
|
raise 预算错误("计划调用次数不能超过安全容量")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class 任务预算计划:
|
|
总金额: Decimal
|
|
角色: tuple[角色预算, ...]
|
|
批准引用: str
|
|
截止时间: datetime
|
|
|
|
def __post_init__(self) -> None:
|
|
object.__setattr__(self, "总金额", 金额(self.总金额))
|
|
_时区时间(self.截止时间)
|
|
if not self.批准引用 or not self.角色 or len({r.角色 for r in self.角色}) != len(self.角色):
|
|
raise 预算错误("任务预算需要批准引用和不重复的完整角色计划")
|
|
if self.总金额 < self.最坏预留:
|
|
raise 预算不足("任务总预算不能覆盖计划调用的最坏预留")
|
|
|
|
@property
|
|
def 最坏预留(self) -> Decimal:
|
|
return sum((r.单次上限 * r.计划次数 for r in self.角色), Decimal(0))
|
|
|
|
def 冻结(self) -> dict:
|
|
return {
|
|
"总金额": str(self.总金额),
|
|
"批准引用": self.批准引用,
|
|
"截止时间": self.截止时间.isoformat(),
|
|
"角色": [{**asdict(r), "单次上限": str(r.单次上限)} for r in self.角色],
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class 预算预留:
|
|
调用ID: str
|
|
任务ID: str
|
|
尝试ID: str
|
|
角色: str
|
|
状态: str
|
|
预留金额: Decimal
|
|
实际金额: Decimal | None
|
|
到期时间: datetime
|
|
结算回执: str | None
|
|
超预算: bool
|
|
允许外发: bool = False
|
|
|
|
|
|
class 预算管理:
|
|
def __init__(self, 数据库: 数据库工厂, 账户ID: str) -> None:
|
|
self.数据库 = 数据库
|
|
self.账户ID = 账户ID
|
|
self._schema = "evaluation" if 数据库.用途 is 用途.评测 else "public"
|
|
|
|
def _查询(self, 连: psycopg.Connection, 语句: LiteralString, 参数: tuple = ()) -> Any:
|
|
return 连.cursor(row_factory=dict_row).execute(
|
|
sql.SQL(语句).format(s=sql.Identifier(self._schema)), 参数
|
|
)
|
|
|
|
@contextmanager
|
|
def _事务(self) -> Iterator[psycopg.Connection]:
|
|
with self.数据库.连接() as 连, 连.transaction():
|
|
yield 连
|
|
|
|
def _锁账户(self, 连: psycopg.Connection) -> dict:
|
|
行 = self._查询(
|
|
连,
|
|
"SELECT * FROM {s}.muse_budget_account "
|
|
"WHERE run_purpose=%s AND account_id=%s FOR UPDATE",
|
|
(self.数据库.用途.value, self.账户ID),
|
|
).fetchone()
|
|
if 行 is None:
|
|
raise 预算错误("额度账户尚未登记")
|
|
return 行["policy"]
|
|
|
|
def 登记策略(self, 策略: 额度策略) -> None:
|
|
if 策略.账户ID != self.账户ID:
|
|
raise 预算错误("额度策略与账户不匹配")
|
|
内容 = 策略.冻结()
|
|
with self._事务() as 连:
|
|
self._查询(
|
|
连,
|
|
"INSERT INTO {s}.muse_budget_account "
|
|
"(run_purpose,account_id,policy_version,policy,policy_hash) VALUES (%s,%s,%s,"
|
|
"%s,%s) "
|
|
"ON CONFLICT DO NOTHING",
|
|
(self.数据库.用途.value, self.账户ID, 策略.版本, Jsonb(内容), 内容哈希(内容)),
|
|
)
|
|
if self._锁账户(连) != 内容:
|
|
raise 预算状态冲突("账户已有冻结额度策略;不能就地改变在途预算")
|
|
|
|
def 登记任务预算(self, 任务ID: str, 计划: 任务预算计划) -> None:
|
|
with self._事务() as 连:
|
|
self.登记任务预算于(连, 任务ID, 计划)
|
|
|
|
def 登记任务预算于(self, 连, 任务ID: str, 计划: 任务预算计划) -> None:
|
|
"""沿用调用方事务,使任务、配置和预算同时可见或一起回滚。"""
|
|
内容 = 计划.冻结()
|
|
任务存储(连, self.数据库.用途).任务行(任务ID, 锁定=True)
|
|
self._锁账户(连)
|
|
self._查询(
|
|
连,
|
|
"INSERT INTO {s}.muse_task_budget (task_id,account_id,plan,plan_hash) "
|
|
"VALUES (%s,%s,%s,%s) ON CONFLICT DO NOTHING",
|
|
(任务ID, self.账户ID, Jsonb(内容), 内容哈希(内容)),
|
|
)
|
|
既有 = self._任务预算(连, 任务ID)
|
|
if 既有["plan_hash"] != 内容哈希(内容):
|
|
raise 预算状态冲突("任务预算已冻结,重复登记不能改变批准金额与计划")
|
|
|
|
def _任务预算(self, 连: psycopg.Connection, 任务ID: str) -> dict:
|
|
行 = self._查询(
|
|
连,
|
|
"SELECT * FROM {s}.muse_task_budget WHERE task_id=%s AND account_id=%s",
|
|
(任务ID, self.账户ID),
|
|
).fetchone()
|
|
if 行 is None:
|
|
raise 预算错误("任务尚未登记本账户的预算计划")
|
|
return 行
|
|
|
|
def 核对任务预算(self, 任务ID: str, 角色: str) -> dict:
|
|
"""领取模型步骤前核对已批准的任务包络。
|
|
|
|
这是只读预检,不预留金额、不延长截止时间,也不代替发送前的
|
|
账户、窗口与逐调用原子复检。
|
|
"""
|
|
|
|
if 角色 not in 角色身份:
|
|
raise 预算错误("任务预算角色未登记")
|
|
with self.数据库.连接(只读=True) as 连:
|
|
登记 = self._任务预算(连, 任务ID)
|
|
计划 = 登记["plan"]
|
|
当前 = self._查询(连, "SELECT clock_timestamp() AS current_time").fetchone()[
|
|
"current_time"
|
|
]
|
|
if 登记["stopped"] or datetime.fromisoformat(计划["截止时间"]) <= 当前:
|
|
raise 预算不足("任务预算已停止或超过总期限")
|
|
角色计划 = next((项 for 项 in 计划["角色"] if 项["角色"] == 角色), None)
|
|
if 角色计划 is None:
|
|
raise 预算错误("当前角色未纳入已批准的任务调用计划")
|
|
return {
|
|
"账户ID": self.账户ID,
|
|
"批准引用": 计划["批准引用"],
|
|
"截止时间": 计划["截止时间"],
|
|
"角色": 角色,
|
|
}
|
|
|
|
def 读取任务预算面(self, 任务ID: str) -> dict:
|
|
with self.数据库.连接(只读=True) as 连:
|
|
account = self._查询(
|
|
连,
|
|
"SELECT policy,policy_hash FROM {s}.muse_budget_account "
|
|
"WHERE run_purpose=%s AND account_id=%s",
|
|
(self.数据库.用途.value, self.账户ID),
|
|
).fetchone()
|
|
row = self._查询(
|
|
连,
|
|
"SELECT plan,plan_hash,stopped FROM {s}.muse_task_budget "
|
|
"WHERE task_id=%s AND account_id=%s",
|
|
(任务ID, self.账户ID),
|
|
).fetchone()
|
|
if account and 内容哈希(account["policy"]) != account["policy_hash"]:
|
|
raise 预算错误("账户额度策略哈希不一致")
|
|
if row and 内容哈希(row["plan"]) != row["plan_hash"]:
|
|
raise 预算错误("任务预算哈希不一致")
|
|
return {
|
|
"account_id": self.账户ID,
|
|
"account_policy": account["policy"] if account else None,
|
|
"plan": row["plan"] if row else None,
|
|
"stopped": row["stopped"] if row else False,
|
|
}
|
|
|
|
def 读取任务费用于(self, 连, 任务ID: str) -> dict:
|
|
"""查询完整尝试账本;失败调用不因没有业务产物而消失。"""
|
|
任务存储(连, self.数据库.用途).任务行(任务ID)
|
|
行集 = self._查询(
|
|
连,
|
|
"SELECT call_id,attempt_id,role_id,state,actual_amount,reserved_amount,over_budget "
|
|
"FROM {s}.muse_budget_reservation WHERE run_purpose=%s AND account_id=%s "
|
|
"AND task_id=%s ORDER BY call_id",
|
|
(self.数据库.用途.value, self.账户ID, 任务ID),
|
|
).fetchall()
|
|
known = sum(
|
|
(r["actual_amount"] for r in 行集 if r["actual_amount"] is not None), Decimal(0)
|
|
)
|
|
unknown = any(r["state"] in {"in_flight", "unknown"} for r in 行集)
|
|
pending = any(r["state"] == "reserved" for r in 行集)
|
|
return {
|
|
"known_total_usd": str(known),
|
|
"total_usd": None if unknown or pending else str(known),
|
|
"has_unknown": unknown,
|
|
"has_pending": pending,
|
|
"over_budget": any(r["over_budget"] for r in 行集),
|
|
"calls": [
|
|
{
|
|
**r,
|
|
"attempt_id": str(r["attempt_id"]),
|
|
"actual_amount": str(r["actual_amount"])
|
|
if r["actual_amount"] is not None
|
|
else None,
|
|
"reserved_amount": str(r["reserved_amount"]),
|
|
}
|
|
for r in 行集
|
|
],
|
|
}
|
|
|
|
def 回收过期(self) -> dict[str, int]:
|
|
"""未发送预留可释放;已发送过期只能转未知,不能自动抹掉成本。"""
|
|
with self._事务() as 连:
|
|
self._锁账户(连)
|
|
条件 = (self.数据库.用途.value, self.账户ID)
|
|
释放 = self._查询(
|
|
连,
|
|
"UPDATE {s}.muse_budget_reservation SET state='released',reason='expired' "
|
|
"WHERE run_purpose=%s AND account_id=%s AND state='reserved' "
|
|
"AND expires_at<=clock_timestamp()",
|
|
条件,
|
|
).rowcount
|
|
未知 = self._查询(
|
|
连,
|
|
"UPDATE {s}.muse_budget_reservation SET state='unknown',reason='expired' "
|
|
"WHERE run_purpose=%s AND account_id=%s AND state='in_flight' "
|
|
"AND expires_at<=clock_timestamp()",
|
|
条件,
|
|
).rowcount
|
|
return {"释放": 释放, "未知": 未知}
|
|
|
|
def _调用行(self, 连: psycopg.Connection, 调用ID: str) -> dict | None:
|
|
return self._查询(
|
|
连,
|
|
"SELECT * FROM {s}.muse_budget_reservation "
|
|
"WHERE run_purpose=%s AND account_id=%s AND call_id=%s",
|
|
(self.数据库.用途.value, self.账户ID, 调用ID),
|
|
).fetchone()
|
|
|
|
def _所有调用(self, 连: psycopg.Connection) -> list[dict]:
|
|
return self._查询(
|
|
连,
|
|
"SELECT * FROM {s}.muse_budget_reservation "
|
|
"WHERE run_purpose=%s AND account_id=%s AND state<>'released'",
|
|
(self.数据库.用途.value, self.账户ID),
|
|
).fetchall()
|
|
|
|
def 预留(
|
|
self,
|
|
领取: 领取凭证,
|
|
调用ID: str,
|
|
角色: str,
|
|
*,
|
|
有效秒: float = 60,
|
|
允许未发重绑: bool = False,
|
|
) -> 预算预留:
|
|
if not 调用ID or not math.isfinite(有效秒) or 有效秒 <= 0:
|
|
raise 预算错误("调用身份和正数预留期限必须提供")
|
|
self.回收过期()
|
|
with self._事务() as 连:
|
|
校验领取(任务存储(连, self.数据库.用途), 领取)
|
|
策略 = self._锁账户(连)
|
|
请求哈希 = 内容哈希(
|
|
{
|
|
"task": 领取.任务ID,
|
|
"attempt": 领取.尝试ID,
|
|
"role": 角色,
|
|
"account": self.账户ID,
|
|
"ttl": 有效秒,
|
|
}
|
|
)
|
|
既有 = self._调用行(连, 调用ID)
|
|
可重绑 = bool(
|
|
允许未发重绑
|
|
and 既有
|
|
and 既有["state"] == "released"
|
|
and 既有["sent_at"] is None
|
|
and str(既有["task_id"]) == 领取.任务ID
|
|
and 既有["role_id"] == 角色
|
|
)
|
|
if 既有 and not 可重绑:
|
|
if 既有["request_hash"] != 请求哈希:
|
|
raise 预算状态冲突("同一调用身份不能重复绑定不同预算请求")
|
|
return self._预留(既有)
|
|
当前 = self._查询(连, "SELECT clock_timestamp() AS current_time").fetchone()[
|
|
"current_time"
|
|
]
|
|
窗口 = 归属窗口(当前, 策略["时区"])
|
|
登记 = self._任务预算(连, 领取.任务ID)
|
|
计划 = 登记["plan"]
|
|
if 登记["stopped"] or datetime.fromisoformat(计划["截止时间"]) <= 当前:
|
|
raise 预算不足("任务预算已停止或超过总期限")
|
|
所有 = self._所有调用(连)
|
|
if any(r["state"] == "unknown" for r in 所有):
|
|
raise 预算不足("账户存在未知成本,必须先对账")
|
|
本窗 = [r for r in 所有 if r["window_start"] == 窗口.起点]
|
|
已占 = sum((self._占用(r) for r in 本窗), Decimal(0))
|
|
角色表 = {r["角色"]: r for r in 计划["角色"]}
|
|
if 角色 not in 角色表:
|
|
raise 预算错误("当前角色未纳入已批准的任务调用计划")
|
|
每次 = Decimal(角色表[角色]["单次上限"])
|
|
if len(本窗) >= 策略["窗口调用上限"] or 已占 + 每次 > Decimal(策略["窗口金额上限"]):
|
|
raise 预算不足(
|
|
"当前窗口的金额或调用次数不足", 上下文={"下个窗口": 窗口.终点.isoformat()}
|
|
)
|
|
本任务 = [r for r in 所有 if str(r["task_id"]) == 领取.任务ID]
|
|
用量 = {role: sum(r["role_id"] == role for r in 本任务) for role in 角色表}
|
|
目标 = 角色表[角色]
|
|
if 用量[角色] >= min(目标["计划次数"], 目标["最多次数"]):
|
|
raise 预算不足("当前角色的计划调用次数已耗尽")
|
|
剩余 = sum(
|
|
(
|
|
Decimal(r["单次上限"]) * max(r["计划次数"] - 用量[role], 0)
|
|
for role, r in 角色表.items()
|
|
),
|
|
Decimal(0),
|
|
)
|
|
if sum((self._占用(r) for r in 本任务), Decimal(0)) + 剩余 > Decimal(计划["总金额"]):
|
|
raise 预算不足("实际成本与剩余计划预留超过任务总预算")
|
|
到期 = min(
|
|
当前 + timedelta(seconds=有效秒),
|
|
窗口.终点,
|
|
datetime.fromisoformat(计划["截止时间"]),
|
|
)
|
|
if 可重绑:
|
|
行 = self._查询(
|
|
连,
|
|
"UPDATE {s}.muse_budget_reservation SET attempt_id=%s,request_hash=%s,"
|
|
"state='reserved',reserved_amount=%s,window_start=%s,window_end=%s,"
|
|
"expires_at=%s,reason=NULL WHERE run_purpose=%s AND account_id=%s "
|
|
"AND call_id=%s AND state='released' AND sent_at IS NULL RETURNING *",
|
|
(
|
|
领取.尝试ID,
|
|
请求哈希,
|
|
每次,
|
|
窗口.起点,
|
|
窗口.终点,
|
|
到期,
|
|
self.数据库.用途.value,
|
|
self.账户ID,
|
|
调用ID,
|
|
),
|
|
).fetchone()
|
|
return self._预留(行)
|
|
行 = self._查询(
|
|
连,
|
|
"INSERT INTO {s}.muse_budget_reservation "
|
|
"(run_purpose,call_id,account_id,task_id,attempt_id,role_id,request_hash,state,"
|
|
"reserved_amount,window_start,window_end,expires_at) "
|
|
"VALUES (%s,%s,%s,%s,%s,%s,%s,'reserved',%s,%s,%s,%s) RETURNING *",
|
|
(
|
|
self.数据库.用途.value,
|
|
调用ID,
|
|
self.账户ID,
|
|
领取.任务ID,
|
|
领取.尝试ID,
|
|
角色,
|
|
请求哈希,
|
|
每次,
|
|
窗口.起点,
|
|
窗口.终点,
|
|
到期,
|
|
),
|
|
).fetchone()
|
|
return self._预留(行)
|
|
|
|
def 标记已发送(
|
|
self,
|
|
领取: 领取凭证,
|
|
调用ID: str,
|
|
*,
|
|
最长秒: float,
|
|
登记尝试: bool = False,
|
|
发送复检: Callable[[psycopg.Connection], None] | None = None,
|
|
) -> 预算预留:
|
|
if not math.isfinite(最长秒) or 最长秒 <= 0:
|
|
raise 预算错误("调用总期限必须大于零")
|
|
self.回收过期()
|
|
with self._事务() as 连:
|
|
存储 = 任务存储(连, self.数据库.用途)
|
|
_, 尝试 = 校验领取(存储, 领取)
|
|
self._锁账户(连)
|
|
行 = self._要求调用(连, 调用ID)
|
|
if (str(行["task_id"]), str(行["attempt_id"])) != (领取.任务ID, 领取.尝试ID):
|
|
raise 预算状态冲突("调用预留不属于当前尝试")
|
|
if 行["state"] == "in_flight":
|
|
return self._预留(行)
|
|
# 用例授权检查和发送状态共用事务;回调不能做模型调用或另开写事务。
|
|
if 发送复检 is not None:
|
|
发送复检(连)
|
|
校验领取(存储, 领取)
|
|
# 锁等待也消耗租期与预算有效期,不能沿用复检前的时间。
|
|
当前 = self._查询(连, "SELECT clock_timestamp() AS current_time").fetchone()[
|
|
"current_time"
|
|
]
|
|
if 行["state"] != "reserved" or 行["expires_at"] <= 当前:
|
|
raise 预算状态冲突("只有有效且尚未发送的预留可以外发")
|
|
登记 = self._任务预算(连, 领取.任务ID)
|
|
if 登记["stopped"]:
|
|
raise 预算不足("任务预算已经停止")
|
|
if any(r["state"] == "unknown" for r in self._所有调用(连)):
|
|
raise 预算不足("账户存在未知成本,必须先对账")
|
|
到期 = min(
|
|
当前 + timedelta(seconds=最长秒), datetime.fromisoformat(登记["plan"]["截止时间"])
|
|
)
|
|
if 到期 <= 当前:
|
|
raise 预算不足("调用已经超过任务总期限")
|
|
if 登记尝试:
|
|
if 尝试["call_state"] not in {"not_sent", "saved"}:
|
|
raise 预算状态冲突("本次尝试的上一调用仍未完成证据对账")
|
|
存储.登记调用(领取, 调用ID)
|
|
self._查询(
|
|
连,
|
|
"UPDATE {s}.muse_budget_reservation SET state='in_flight',sent_at=%s,expires_at=%s "
|
|
"WHERE run_purpose=%s AND call_id=%s",
|
|
(当前, 到期, self.数据库.用途.value, 调用ID),
|
|
)
|
|
return replace(self._预留(self._要求调用(连, 调用ID)), 允许外发=True)
|
|
|
|
def 结算(self, 调用ID: str, 实际金额: Decimal | None, *, 回执ID: str) -> 预算预留:
|
|
if not 回执ID:
|
|
raise 预算错误("结算需要实际回执身份")
|
|
成本 = None if 实际金额 is None else 金额(实际金额)
|
|
with self._事务() as 连:
|
|
return self.结算于(连, 调用ID, 成本, 回执ID=回执ID)
|
|
|
|
def 结算于(self, 连, 调用ID: str, 实际金额: Decimal | None, *, 回执ID: str) -> 预算预留:
|
|
"""调用事实与作者结算命令共用事务,不改变任务执行状态。"""
|
|
if not 回执ID:
|
|
raise 预算错误("结算需要实际回执身份")
|
|
成本 = None if 实际金额 is None else 金额(实际金额)
|
|
所属 = self._要求调用(连, 调用ID)
|
|
任务存储(连, self.数据库.用途).任务行(str(所属["task_id"]), 锁定=True)
|
|
self._锁账户(连)
|
|
行 = self._要求调用(连, 调用ID)
|
|
if 行["state"] == "settled":
|
|
if 行["receipt_id"] != 回执ID or 行["actual_amount"] != 成本:
|
|
raise 预算状态冲突("调用已经结算,不能改写成本或回执")
|
|
return self._预留(行)
|
|
if 行["state"] not in ("in_flight", "unknown"):
|
|
raise 预算状态冲突("未发送的调用不能记作实际成本")
|
|
超额 = 成本 is not None and 成本 > 行["reserved_amount"]
|
|
self._查询(
|
|
连,
|
|
"UPDATE {s}.muse_budget_reservation SET state=%s,actual_amount=%s,"
|
|
"receipt_id=%s,over_budget=%s WHERE run_purpose=%s AND call_id=%s",
|
|
(
|
|
"unknown" if 成本 is None else "settled",
|
|
成本,
|
|
回执ID,
|
|
超额,
|
|
self.数据库.用途.value,
|
|
调用ID,
|
|
),
|
|
)
|
|
self._查询(
|
|
连,
|
|
"UPDATE {s}.muse_task SET accounting_revision=accounting_revision+1 WHERE task_id=%s",
|
|
(行["task_id"],),
|
|
)
|
|
if 超额:
|
|
self._查询(
|
|
连,
|
|
"UPDATE {s}.muse_task_budget SET stopped=true WHERE task_id=%s",
|
|
(行["task_id"],),
|
|
)
|
|
return self._预留(self._要求调用(连, 调用ID))
|
|
|
|
def 取消预留(self, 调用ID: str) -> 预算预留:
|
|
with self._事务() as 连:
|
|
self._锁账户(连)
|
|
行 = self._要求调用(连, 调用ID)
|
|
self._取消行(连, 行)
|
|
return self._预留(self._要求调用(连, 调用ID))
|
|
|
|
def 关闭任务预算(self, 任务ID: str) -> None:
|
|
with self._事务() as 连:
|
|
存储 = 任务存储(连, self.数据库.用途)
|
|
存储.任务行(任务ID, 锁定=True)
|
|
停止任务预算(存储, 任务ID)
|
|
|
|
def _取消行(self, 连: psycopg.Connection, 行: dict) -> None:
|
|
if 行["state"] in ("reserved", "in_flight"):
|
|
self._查询(
|
|
连,
|
|
"UPDATE {s}.muse_budget_reservation SET state=%s,reason='cancelled' "
|
|
"WHERE run_purpose=%s AND call_id=%s",
|
|
(
|
|
"released" if 行["state"] == "reserved" else "unknown",
|
|
self.数据库.用途.value,
|
|
行["call_id"],
|
|
),
|
|
)
|
|
|
|
def 读取(self, 调用ID: str) -> 预算预留:
|
|
with self._事务() as 连:
|
|
return self.读取于(连, 调用ID)
|
|
|
|
def 读取于(self, 连, 调用ID: str) -> 预算预留:
|
|
"""在调用方快照核验实际结算;不锁账户或改变状态。"""
|
|
return self._预留(self._要求调用(连, 调用ID))
|
|
|
|
def 窗口余额(self) -> dict:
|
|
self.回收过期()
|
|
with self._事务() as 连:
|
|
策略 = self._锁账户(连)
|
|
当前 = self._查询(连, "SELECT clock_timestamp() AS current_time").fetchone()[
|
|
"current_time"
|
|
]
|
|
窗口 = 归属窗口(当前, 策略["时区"])
|
|
所有 = self._所有调用(连)
|
|
本窗 = [r for r in 所有 if r["window_start"] == 窗口.起点]
|
|
已知 = sum((r["actual_amount"] for r in 本窗 if r["state"] == "settled"), Decimal(0))
|
|
在途 = sum((r["reserved_amount"] for r in 本窗 if r["state"] != "settled"), Decimal(0))
|
|
return {
|
|
"已知成本": 已知,
|
|
"在途预留": 在途,
|
|
"已占次数": len(本窗),
|
|
"未知调用数": sum(r["state"] == "unknown" for r in 所有),
|
|
"可用金额": max(Decimal(0), Decimal(策略["窗口金额上限"]) - 已知 - 在途),
|
|
"窗口起点": 窗口.起点,
|
|
"窗口终点": 窗口.终点,
|
|
}
|
|
|
|
def _要求调用(self, 连: psycopg.Connection, 调用ID: str) -> dict:
|
|
行 = self._调用行(连, 调用ID)
|
|
if 行 is None:
|
|
raise 预算状态冲突("调用预算不存在或不属于本账户用途")
|
|
return 行
|
|
|
|
@staticmethod
|
|
def _占用(行: dict) -> Decimal:
|
|
return 行["actual_amount"] if 行["state"] == "settled" else 行["reserved_amount"]
|
|
|
|
@staticmethod
|
|
def _预留(行: dict) -> 预算预留:
|
|
return 预算预留(
|
|
行["call_id"],
|
|
str(行["task_id"]),
|
|
str(行["attempt_id"]),
|
|
行["role_id"],
|
|
行["state"],
|
|
行["reserved_amount"],
|
|
行["actual_amount"],
|
|
行["expires_at"],
|
|
行["receipt_id"],
|
|
行["over_budget"],
|
|
)
|
|
|
|
|
|
def 恢复任务预算(存储: 任务存储, 任务ID: str) -> None:
|
|
"""作者恢复在原批准范围内重新开放剩余额度,不重置已使用次数和费用。"""
|
|
预算 = 存储.查询(
|
|
"SELECT account_id,plan FROM {s}.muse_task_budget WHERE task_id=%s", (任务ID,)
|
|
).fetchone()
|
|
if 预算 is None:
|
|
return
|
|
存储.查询(
|
|
"SELECT account_id FROM {s}.muse_budget_account "
|
|
"WHERE run_purpose=%s AND account_id=%s FOR UPDATE",
|
|
(存储.用途.value, 预算["account_id"]),
|
|
)
|
|
当前 = 存储.查询("SELECT clock_timestamp() AS now").fetchone()["now"]
|
|
if datetime.fromisoformat(预算["plan"]["截止时间"]) <= 当前:
|
|
raise 预算不足("原任务预算已经截止")
|
|
if 存储.查询(
|
|
"SELECT call_id FROM {s}.muse_budget_reservation WHERE task_id=%s "
|
|
"AND state IN ('unknown','in_flight') LIMIT 1",
|
|
(任务ID,),
|
|
).fetchone():
|
|
raise 预算不足("恢复前必须对账所有在途调用")
|
|
存储.查询("UPDATE {s}.muse_task_budget SET stopped=false WHERE task_id=%s", (任务ID,))
|
|
|
|
|
|
def 停止任务预算(存储: 任务存储, 任务ID: str) -> None:
|
|
"""与任务终止共用短事务;按任务、账户顺序锁定,不丢失已发出的成本。"""
|
|
预算 = 存储.查询(
|
|
"SELECT account_id FROM {s}.muse_task_budget WHERE task_id=%s", (任务ID,)
|
|
).fetchone()
|
|
if 预算 is None:
|
|
return
|
|
存储.查询(
|
|
"SELECT account_id FROM {s}.muse_budget_account "
|
|
"WHERE run_purpose=%s AND account_id=%s FOR UPDATE",
|
|
(存储.用途.value, 预算["account_id"]),
|
|
)
|
|
存储.查询("UPDATE {s}.muse_task_budget SET stopped=true WHERE task_id=%s", (任务ID,))
|
|
存储.查询(
|
|
"UPDATE {s}.muse_budget_reservation SET state=CASE WHEN state='reserved' "
|
|
"THEN 'released' ELSE 'unknown' END,reason='task_stopped' "
|
|
"WHERE task_id=%s AND state IN ('reserved','in_flight')",
|
|
(任务ID,),
|
|
)
|
|
|
|
|
|
__all__ = [
|
|
"额度策略",
|
|
"额度窗口",
|
|
"归属窗口",
|
|
"角色预算",
|
|
"任务预算计划",
|
|
"预算预留",
|
|
"预算管理",
|
|
"预算错误",
|
|
"预算不足",
|
|
"预算状态冲突",
|
|
"金额",
|
|
]
|