mirror of
https://github.com/MemeMeow-Studio/MemeMeow.git
synced 2026-10-16 20:31:33 +08:00
89 lines
3.6 KiB
Python
89 lines
3.6 KiB
Python
# Executor 从任务工作区读取绑定主 session 的累计分析金额。
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import sqlite3
|
|
from contextlib import closing
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
from decimal import Decimal, InvalidOperation
|
|
from pathlib import Path
|
|
|
|
|
|
class AnalysisUsageError(RuntimeError):
|
|
"""保留稳定错误码及可定位读取阶段的原因。"""
|
|
|
|
code = "agent_analysis_usage_unavailable"
|
|
|
|
def __init__(self, reason: str):
|
|
"""接收不含业务数据的原因,用于 Executor 终止诊断。"""
|
|
self.reason = reason
|
|
super().__init__(reason)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AnalysisUsage:
|
|
"""一次主 session 只读快照,包含美元累计值及 UTC 检查时间。"""
|
|
|
|
observed_cost: Decimal
|
|
checked_at: str
|
|
|
|
|
|
def _validated_usage(*, observed_cost: object, checked_at: object, minimum_cost: Decimal) -> AnalysisUsage:
|
|
"""校验公共金额快照,拒绝无效金额、无时区时间和金额倒退。"""
|
|
if not isinstance(observed_cost, (str, int, float)) or isinstance(observed_cost, bool):
|
|
raise AnalysisUsageError("session_cost_invalid")
|
|
try:
|
|
cost = Decimal(str(observed_cost))
|
|
except (InvalidOperation, ValueError) as exc:
|
|
raise AnalysisUsageError("session_cost_invalid") from exc
|
|
if not cost.is_finite() or cost < 0:
|
|
raise AnalysisUsageError("session_cost_invalid")
|
|
if cost < minimum_cost:
|
|
raise AnalysisUsageError("session_cost_decreased")
|
|
if not isinstance(checked_at, str):
|
|
raise AnalysisUsageError("usage_checked_at_invalid")
|
|
try:
|
|
parsed = datetime.fromisoformat(checked_at.replace("Z", "+00:00"))
|
|
except ValueError as exc:
|
|
raise AnalysisUsageError("usage_checked_at_invalid") from exc
|
|
if parsed.tzinfo is None:
|
|
raise AnalysisUsageError("usage_checked_at_invalid")
|
|
return AnalysisUsage(cost, parsed.astimezone(timezone.utc).isoformat())
|
|
|
|
|
|
def read_analysis_usage(
|
|
database: Path, *, session_id: str, directory: Path,
|
|
minimum_cost: Decimal = Decimal(0),
|
|
) -> AnalysisUsage:
|
|
"""从任务 OPENCODE_DB 读取已绑定主 session;缺失、冲突及金额减少立即报错。"""
|
|
if not session_id:
|
|
raise AnalysisUsageError("session_binding_missing")
|
|
if not database.is_file():
|
|
raise AnalysisUsageError("database_missing")
|
|
try:
|
|
with closing(sqlite3.connect(database.absolute().as_uri() + "?mode=ro", uri=True, timeout=0.25)) as connection:
|
|
connection.execute("PRAGMA query_only = ON")
|
|
columns = {row[1] for row in connection.execute("PRAGMA table_info(session)")}
|
|
if not {"id", "parent_id", "directory", "cost"}.issubset(columns):
|
|
raise AnalysisUsageError("session_schema_incompatible")
|
|
row = connection.execute(
|
|
"SELECT parent_id, directory, cost FROM session WHERE id = ?", (session_id,),
|
|
).fetchone()
|
|
except sqlite3.Error as exc:
|
|
reason = getattr(exc, "sqlite_errorname", type(exc).__name__)
|
|
raise AnalysisUsageError(f"database_read_error:{reason}") from exc
|
|
if row is None:
|
|
raise AnalysisUsageError("session_missing")
|
|
if row[0] is not None or row[1] != str(directory):
|
|
raise AnalysisUsageError("session_binding_mismatch")
|
|
value = row[2]
|
|
if not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0:
|
|
raise AnalysisUsageError("session_cost_invalid")
|
|
return _validated_usage(
|
|
observed_cost=value,
|
|
checked_at=datetime.now(timezone.utc).isoformat(),
|
|
minimum_cost=minimum_cost,
|
|
)
|