Skip to content

v0.9.27

Choose a tag to compare

@deltrivx deltrivx released this 02 Oct 02:52
· 9 commits to main since this release

AutoBuddy v0.9.27

参照 sub2api / CLIProxyAPI 重构统计存储(攒批 + 原子写 + 分级保留),并让官方数据陈旧可见。

为什么要改

v0.9.26 修好了「词元统计只剩两天」,但暴露了更根本的问题:
统计每记录一次就要重写整个明细文件,窗口写满时是 1.28MB 一轮 I/O。

于是对照两个同类中转项目的成熟做法做了一轮重构:

项目 规模 参考价值
sub2api 43k★ 用量攒批写入、分级保留、DB 侧 GROUP BY 聚合
CLIProxyAPI 53.7k★ 缓存显式 TTL、ticker 自动刷新、防抖、fsnotify 热加载

1. 写入改为攒批

sub2api 的做法(usage_log_repo_insert.go):

usageLogCreateBatchMaxSize  = 64
usageLogCreateBatchWindow   = 3 * time.Millisecond
usageLogBestEffortBatchMaxSize  = 256
usageLogBestEffortBatchWindow   = 20 * time.Millisecond

原先本网关是「读整个 JSON → append → 写整个 JSON」,每请求一次。
现在只入内存队列,满 32 条或 1 秒才真正落盘
(AB_TOKEN_PENDING_MAX / AB_TOKEN_PENDING_SEC 可调)。

两个必须配套的细节

攒批会引入新问题,缺一个就出错:

① 读侧强制 flush —— get_aggregated_token_stats() 读取前先把队列落盘。
攒批是为了省写入,但「刚发生的调用在统计页看不到」不能接受。

② 后台线程驱动时间窗口 —— sub2api 的窗口是后台 goroutine 的 ticker,
不依赖新请求到来。这里同样必须有:否则最后一批不足 32 条的记录
会一直留在内存,进程被 SIGKILL(容器重建很常见,atexit 根本不执行)
就彻底丢了。已补 daemon 线程按周期落盘,并保留 atexit 兜底。

2. 原子写

原先 open(w) 直接写,写到一半进程被杀会留下半截文件,
下次读取整个统计页就炸。改成写临时文件 + os.replace()
(同一分区内是原子操作),明细与 rollup 两处都改。

3. 聚合行分级保留

sub2api 按粒度分档保留(1m 明细 3 天 → 1d 汇总 90 天),
原则是越粗的档保留越久 —— 既能回溯长期趋势,又不会让文件无限膨胀。

新增 _prune_rollup(),默认保留 90 天(AB_TOKEN_ROLLUP_DAYS 可调),
每次折算时顺带裁剪。

4. 官方数据陈旧:修不了,就让它可见

CLIProxyAPI 对所有缓存显式设 TTL(如 AntigravityReasoningReplayCacheTTL = 1h),
3h ticker 自动刷新 + 30s 最小间隔防抖。核心思想:
陈旧是可知的,不是默认透明的。

本项目的 officialUsage 由官方闭源引擎 autobuddy-engine 采集。
实测直连 :57890 调 credits/stats 也不触发重写(POST 返回 405、
文件 mtime 不变)—— 该环节在闭源二进制内,网关侧无法修复。

既然修不了上游,就必须把陈旧如实暴露。过去页面显示
「最近更新 9-24」却不说原因,用户只能以为是网关坏了。

现在 /api/credits/stats 一并下发 officialUsageFreshness:

{"collectedAt": 1790179275670,
 "rangeEnd": "2026-09-24",
 "ageDays": 8.6,
 "stale": true,
 "reason": "官方用量数据由闭源底层引擎采集,当前未再刷新:最近一次采集距今约 8.6 天,数据截止 2026-09-24。该采集环节在官方二进制内部,网关侧无法触发重写;本页的今日消耗已改用实时数据校准,不受此影响。"}

默认超过 2 天标为陈旧(AB_OFFICIAL_USAGE_STALE_DAYS 可调)。

测试

新增 _test_token_storage.py 16 项,锁定四条契约:

攒批:不足一批也会在时间窗口内自动落盘(后台线程驱动)
原子写:不留 .tmp 临时文件、始终是完整 JSON
分级保留:超期聚合行被裁剪,近期保留且能继续累加
陈旧可见:过期给 stale/reason,缺失/无时间戳也各有说明,不静默

已有两个测试按攒批后的真实读取路径补了 flush_pending(force=True) ——
它们原先直接读文件,攒批后读不到,这正是「读侧必须 flush」这条设计的由来。

全量 18 个测试文件               ✅ 全绿
五模块 AST                       ✅ 通过
注入 JS node --check             ✅ 通过

升级说明

重建容器即可,/data 数据不受影响。

新增可选环境变量(都有默认值,无需配置):

变量 默认 含义
AB_TOKEN_PENDING_MAX 32 攒够多少条落盘
AB_TOKEN_PENDING_SEC 1.0 攒批时间窗口(秒)
AB_TOKEN_ROLLUP_DAYS 90 聚合行保留天数
AB_OFFICIAL_USAGE_STALE_DAYS 2 官方数据超过几天标陈旧

无数据库结构变更;现有 token_stats_logs.json 与
token_stats_rollup.json 会被就地沿用,无需手工迁移。