Skip to content

Releases: deltrivx/AutoBuddy

v0.9.30

Choose a tag to compare

@deltrivx deltrivx released this 05 Oct 15:10

AutoBuddy v0.9.30

修复账号卡片读停更旧 JSON 导致新账号模型不显示、429 响应体进日志、×N 重复打印。

1. 新账号有调用次数,模型却不显示

现象

用户反馈:新加的账号调用次数是 11 次,但模型没体现出来。

实测

账号 17521565831(16acb0ee)在用量明细里确实有 18 条记录:

deepseek-v4.1-flash   1 次
glm-5.3               8 次
hy4-preview-f         9 次

但 /api/account-models 返回它的 used / gatewayCalls 全为 None。

根因:v0.9.29 迁移引入的回归

/api/account-models 仍直接读 token_stats_logs.json:

tracker_file = data_dir / "token_stats_logs.json"
with open(tracker_file, "r", encoding="utf-8") as f:
    tracker_logs = _json.load(f) or []

而这个文件在 10-02 迁移到 SQLite 后就再没被写入过 ——
mtime 与最新记录都停在 10-02 14:06。

新账号的调用只存在于 SQLite,旧 JSON 里根本没有,于是看起来
就像「新账号没被调用过」。

修复

新增 token_tracker.load_tracker_records():读取明细时自动走当前
存储后端
(SQLite 或 JSON)。调用方不该再直接读那个 JSON 文件 ——
否则换后端时又会出现同样的双份数据源问题。

account-models 改用它;新路径失败时退回旧文件兜底,绝不整页空白。

2. 上游非 200 的响应体现在会记进日志

排查 429 时发现一个阻碍定性的缺口:日志里只有状态码,
rate limit / quota / 上游错误体一条都没有。

于是同一个 429 无法区分:

可能 处理
速率限流 等一会儿就好
额度耗尽 得充值或换号

历史上额度耗尽正是 HTTP 429 + code=14018 Credits exhausted ——
两者都是 429,没有响应体就只能靠猜。

新增 _log_upstream_error(),挂在 _looks_like_model_unavailable()
这个两条转发路径(流式 / 非流式)的共同判定入口上,覆盖每一次
上游错误。写入前抹掉 requestId / traceId(每次都不同,不抹掉
就是一条一条刷屏),并截断到 400 字符。

3. 周期汇报的 ×N 不再每个周期重复打印

实测数据

2727 条 ×N 行里,835 条与上一条一字不差;/health 更是
连续 27 条 ×1 完全相同。

三个成因,两个要修

① ×N 字符本身是正确的(不用改)

实测 200 ×6 的字节为 c39736,即 U+00D7 × + 6 ——
符号与数字各一个,不存在「重复」。用户看到的「重复」是整行重复。

② 周期汇报绕过了黑名单(修)

周期汇报是直接 print,不走 logging filter,所以 NOISY_PATHS
黑名单对它无效 —— /health 这类探活接口每 60s 稳定产出一条,
独占 1561 条。新增 _is_noisy_access(),复用同一份黑名单静音它们。

③ 同内容同次数 = 没有新信息(修)

与上一周期同内容且同次数的汇报不再打印
(_ACCESS_DEDUP_LAST_PRINTED 记录上次打印值)。

测试

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

升级说明

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

  • 无数据库结构变更;
  • 本次修复的账号卡片问题,升级后立即生效(不再依赖停更的旧 JSON);
  • 旧 JSON 文件保留在原地不删,但已无任何读取方依赖它。

v0.9.29

Choose a tag to compare

@deltrivx deltrivx released this 02 Oct 06:05

AutoBuddy v0.9.29

用量明细迁到 SQLite(增量幂等写入 + 索引),聚合逻辑与输出契约一行未改。

为什么要迁

明细原先是单个 JSON 文件,每次记录都要:

读整个文件 → append → 写整个文件

窗口写满 3000 条时约 1.28MB 一轮 I/O,请求一密就把磁盘打满。
v0.9.27 的攒批只降低了频率,单次落盘仍是整文件重写,成本没变。

sub2api(43k★)的做法是明细落到数据库表,写入为增量 INSERT 且幂等
(ON CONFLICT ... DO NOTHING),并建复合索引服务聚合查询。

关键取舍:只换存储,不改聚合

这是本次最重要的一条设计决定。

聚合逻辑与输出契约( daily / models / projects / dailyByModel /
requests / sessions / summary )一行未改,换的只是
「明细从哪来、往哪写」。

好处:

  • 前端零改动;
  • 现有回归测试继续验证语义,迁移风险可被测试守住;
  • 真正的瓶颈在写入,换成 INSERT 后即解决 —— 不做多余的重构。

新增 gateway/token_store.py:

能力 实现
写入 INSERT OR IGNORE(等价 sub2api 的 ON CONFLICT DO NOTHING)
幂等 同 request_id 重复写/重放/并发提交都不重复计数
聚合折算 UPSERT 累加,避免读-改-写竞态
并发 WAL 模式:读写互不阻塞

索引对齐 sub2api 的复合索引思路(列顺序 = 查询条件顺序):

idx_usage_logs_date          (date)
idx_usage_logs_model_date    (model, date)
idx_usage_logs_account_date  (account_id, date)
idx_usage_logs_variant_date  (variant, date)
idx_usage_logs_ts            (ts DESC)

迁移过程中修掉的三处真实缺陷

这三处都是迁移本身引入的风险,不是原有问题,逐一修掉:

① 迁移会把超期旧记录两头落空

旧 JSON 里必然混有超期记录(线上 3000 条里就有)。若一股脑 INSERT 进
明细表,读取时会被时间窗口排除,而它们又没被折算进聚合 ——
明细里没有、聚合里也没有,等于静默丢数据。

现按保留期分流:新鲜的进明细,超期的直接折算进聚合。

② 兜底折算写错了后端

队列撑不住时的降级路径固定写 JSON 聚合文件;SQLite 模式下读取侧从
SQLite 读,这批数据落到没人看的地方 —— 同样是静默丢失。
现按 TOKEN_STORE 分发到当前后端。

③ 保留策略每次落盘都扫全表

明细超期是以天为单位的,每秒扫一次纯属浪费。改为按
_RETENTION_CHECK_SEC(默认 3600s)节流。

注意:不能只在启动时跑一次 —— 长跑进程跨过午夜后日期就变了,
必须周期性重判。

一处会直接崩的导入问题(自查发现)

gateway/ 没有 __init__.py,且容器内以顶层模块方式运行
(sys.path 含 /app/gateway)。

写成硬 from gateway import token_store 会 ModuleNotFoundError,
网关一启动就挂。

现改为两种模式都试,与 main.py / web_proxy.py 的既有约定一致:

try:
    from gateway import token_store
except ImportError:
    import token_store

兼容与回滚

  • 默认 sqlite;设 AB_TOKEN_STORE=json 即回到旧行为。
  • 首次读取时自动把旧 JSON 明细与聚合迁入 SQLite,幂等
    (meta 标记 + INSERT OR IGNORE),线上历史数据不丢。
  • 超期旧记录在迁移时会被折算进聚合,不会两头落空。
  • 明细默认 7 天(AB_TOKEN_DETAIL_DAYS),聚合 90 天
    (AB_TOKEN_ROLLUP_DAYS),与 v0.9.28 语义一致。

测试

新增 _test_token_store_sqlite.py 26 项:

[1] 幂等写入:同 request_id 重复写不重复计数
[2] 超期明细折算进聚合,input/output/records 全部保住
[3] 迁移:旧 JSON 超期记录不两头落空,重复迁移不翻倍
[4] 输出契约与 JSON 后端一致(结构不变)
[5] 默认后端必须是 sqlite,且保留 AB_TOKEN_STORE 回滚开关

三个既有测试改为显式钉住 TOKEN_STORE = "json" —— 它们断言的是
JSON 文件行为(原子写、.tmp 残留、直接读 TRACKER_FILE)。
钉住后端既保留回归覆盖,也顺带守住了可回滚路径。

全量 19 个测试文件               ✅ 全绿
五模块 AST                       ✅ 通过
两种导入模式(顶层 / 包)        ✅ 均通过

升级说明

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

  • 新增数据文件 /data/.autobuddy/token_stats.db(WAL 模式下还会有
    -wal / -shm 两个伴生文件,属正常现象,勿手动删除)。
  • 旧 JSON 文件保留在原地不删,迁移完成后不再写入;
    确认运行稳定后可自行清理。
  • 无环境变量强制要求;AB_TOKEN_STORE 仅在需要回滚时设置。

v0.9.28

Choose a tag to compare

@deltrivx deltrivx released this 02 Oct 05:30

AutoBuddy v0.9.28

补上 v0.9.27 遗留的两处缺口:队列撑不住时不再静默丢弃、明细改为按时间保留。

背景

v0.9.27 参照 sub2api / CLIProxyAPI 重构了统计存储。sub2api 的完整调研
报告随后送达,回看发现有两处没做到位,本版补齐。

1. 队列撑不住时不再静默丢弃

sub2api 的原则写得很硬 —— submitMandatoryUsageRecordTask 永不静默丢弃:
它的有界 worker 池(128 worker / 16384 队列)在队列满时提交方内联执行,
关停窗口的丢弃也会降级同步,保证计费一条不丢。

我 v0.9.27 的兜底却写着:

if len(_PENDING) < _PENDING_MAX_BACKLOG:
    _PENDING[:0] = batch
else:
    print(f"... dropping {len(batch)}")   # ← 悄悄扔掉

这正是「悄悄少数据」,与 sub2api 的做法背道而驰。

现在改成:队列真的撑不住时,把这一批折算进聚合(fold_into_rollup)
再丢弃明细。

明细 汇总数字
旧行为 丢失 随之丢失
新行为 丢失 完整保住(token 总数、调用次数、按天/模型/账号分布)

统计页的数字仍然正确,只是查不到单次调用的详情。
「悄悄少数据」和「降级保汇总」是两件事,后者才是可接受的兜底。

2. 明细改为按时间保留

sub2api 明确提醒:

别用滑动窗口。你的「3000 条」本质是行数上限,改成时间维度保留
(usage_logs_days),历史跨度就不受数据量绑架。

v0.9.27 只把聚合行改成了按天保留,明细仍是 MAX_DETAIL = 3000 的
行数上限。而实测网关每天约 2000 条调用 —— 忙的时候只能看两天,
闲的时候能看一个月,能回溯多久是不确定的。

现在主维度改为时间:

  • 新增 DETAIL_MAX_DAYS(默认 7 天,AB_TOKEN_DETAIL_DAYS 可调)
  • 新增 _split_expired() 按天裁剪,超期明细先折算进聚合再移除
  • MAX_DETAIL 降级为文件体积护栏,只在时间维度不够用时兜底

一个易漏的细节:没有 date 字段的记录一律保留 ——
宁可多留,也不能把数据误判成过期删掉。

为什么明细是 7 天而不是 sub2api 的 90 天

sub2api 明细留 90 天,是因为它在 PostgreSQL 里有分区 + 按月 DROP,
存得起。本网关明细是单个 JSON 文件,每请求都要读写一次,
7 天 × 约 2000 条/天 ≈ 1.4 万条会让文件膨胀到数 MB,得不偿失。

所以这里做了取舍:明细 7 天(可查单次调用),聚合 90 天(可查长期趋势)。
趋势图本来就读聚合,长期回溯不受影响。

关于 sub2api 的第 5 条(上游数据刷新)

sub2api 建议:上游额度用惰性读 + 分级 TTL(成功 3min / 错误 1min
负缓存 / 探测 10min)+ singleflight + jitter,并把快照写回账号记录。

本项目的 officialUsage 由官方闭源引擎采集,实测无法触发重写 ——
该路径用不上这套机制,v0.9.27 已改为暴露陈旧度
(officialUsageFreshness 的 stale / ageDays / reason)如实告知。

我们自己能控的上游调用(如签到状态)本来就有 60s TTL,与此一致。

测试

_test_token_storage.py 从 16 项扩到 24 项:

[5] 明细按时间保留
    超期明细被划为待折算
    近期与今天的明细保留
    无日期字段的记录一律保留
    条数远未达上限时仍按时间裁剪(证明主维度是时间)

[6] 队列撑不住时不得静默丢弃
    源码中不再出现 dropping 分支
    降级为折算保汇总
    折算后 token 总数被保住
全量 18 个测试文件               ✅ 全绿
五模块 AST                       ✅ 通过

升级说明

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

新增可选环境变量:

变量 默认 含义
AB_TOKEN_DETAIL_DAYS 7 明细保留天数(0 = 不按时间裁剪)

无数据库结构变更;现有明细与聚合文件会被就地沿用。
升级后首次落盘会把超过 7 天的明细折算进聚合 —— 这是预期行为,
长期趋势数据不会丢。

v0.9.27

Choose a tag to compare

@deltrivx deltrivx released this 02 Oct 02:52

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 会被就地沿用,无需手工迁移。

v0.9.26

Choose a tag to compare

@deltrivx deltrivx released this 02 Oct 02:15

AutoBuddy v0.9.26

修复工具调用透传丢失 tool_calls、日志重复计数改为行尾 ×N、词元统计只剩两天。

1. 工具调用透传损坏(严重)

现象

用户 2026-10-02 反馈:「所有模型都返回 finish_reason=tool_calls 却不携带 tool_calls 数组」。

实测确认:

finish_reason : tool_calls
message       : {"role": "assistant", "content": ""}
tool_calls    : null          ← 没有

危险之处在于它不报错:模型「说要调工具」,但调用内容一个字都没传下去,
Agent 侧的 function calling 链路整条断掉,而空 content 看起来像一条正常回复,
排查时极易误判成「模型不支持工具」。

根因:非流式路径只挑了 content

网关把上游流式帧重组成 assistant message 时:

delta = choices[0].get("delta", {})
if "content" in delta and delta["content"]:
    collected_content += delta["content"]      # ← 只挑了 content

而 tool calling 恰恰不走 content,它走 delta["tool_calls"]。
于是那一半消息被静默丢弃,最终拼出的 message 只剩 role/content。

修复

新增 accumulate_delta(),把整块 delta 增量合并进 assistant 消息:

字段 处理
content / reasoning_content 字符串累加
tool_calls 按 index 定位同一条,再拼接 arguments
role 只在首帧携带,取到就写、取不到不动

⚠️ arguments 必须拼接而非覆盖 —— 上游是把 JSON 参数逐片下发的:

{"tz"  →  :"Asia/  →  Shanghai"}

每片都 append 一条新 tool_call 会变成一串残缺调用,必须按 index 归位。

组装时不再只塞 content,而是把累积结果整体放进 message(真·透传)。

2. 日志重复计数改为行尾 ×N

原先数数是另起一行,等于又多打了一条几乎重复的记录,与「防刷屏」初衷相悖:

改前:  [pool]  msg
       [pool]  (上一条重复 4 次)      ← 多一行
改后:  [pool]  msg ×4                ← 挂在行尾

三处一并改:lprint 通用日志、access log 周期汇报(main + web_proxy)。

3. 词元统计只剩最近两天

根因

明细日志是滑动窗口,上限 3000 条,而实测网关每天产生约 2000 条调用:

按天分布: 10-01: 2025 条, 10-02: 975 条   ← 不到两天写满
最密时段: 00 时 418 条 / 21 时 358 条

窗口写满后最老记录被直接丢掉,页面永远只剩一两天。

为什么不能简单调大窗口

明细每请求都要整文件读+写一次,3000 条已是 1.28MB 一轮 I/O,
放大到几万条会把网关拖垮。

修复:明细保近期、历史转聚合

滑出窗口的记录按「天 x 模型 x 账号 x variant」折算成极小一行存
token_stats_rollup.json;趋势图与汇总把明细与 rollup 一起算,
请求明细仍只列真实调用记录。

每请求的成本从此与「累积了多久」无关。

伴随修正的四处计数缺陷(均由新测试暴露)

  • 各维度桶的 records 一律 +1 → 改为按 _rec_count() 累加
    (聚合行代表 N 次调用,算 1 会把历史量算少);
  • requests / sessions 误把 rollup 聚合行混进去,变成一堆没有 id 的
    req-0 假记录 → 改为只遍历真实明细;
  • 分流子集(国内/国际版)同源问题一并修。

关于「积分统计页最近更新停在 9-24」

这一项网关侧无法修复,如实说明:

official_usage_cache.json 的 collectedAt 停在 09-23 04:44、
rangeEnd 停在 2026-09-24。它是官方闭源底层
(autobuddy-engine)的采集产物 —— 引擎对该环节只字不提(静默跳过),
直连 :57890/api/credits/stats 也不触发重写。

Web 代理层已做的日程校正(_reconcile_official_usage)不受影响,
顶层 daily 仍是新鲜的(最新 2026-10-02,今日消耗 586.64)。

测试

新增 2 个测试文件共 37 项:

_test_tool_call_passthrough.py   16 项   逐片拼接、按 index 归位、真透传
_test_token_stats_history.py     21 项   历史折算、records 计数、文件损坏容错

_test_log_dedup.py 增补第 6 组断言:强制「行尾 ×N」写法,
禁止再出现独立成行的重复计数。

全量 17 个测试文件               ✅ 全绿
五模块 AST                       ✅ 通过

升级说明

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

  • 新增可选环境变量 AB_TOKEN_DETAIL_MAX(默认 3000,明细窗口上限);
  • 新增数据文件 /data/.autobuddy/token_stats_rollup.json(历史聚合,
    首次写入时自动生成,无需手工初始化);
  • 无数据库结构变更;
  • 行为变化:非流式 chat 响应在模型要求调工具时会带上 tool_calls,
    Agent 的 function calling 链路恢复。

v0.9.25

Choose a tag to compare

@deltrivx deltrivx released this 30 Sep 03:17

AutoBuddy v0.9.25

一致性自检页补上「额度用尽」的显示档位,不再与「未检测」混淆。

背景

v0.9.24 已把额度耗尽正确判为 quota_exhausted。但界面上仍看不到——
它显示为「没检测出结果」。

先说定位结论:判定是对的

实测(对额度耗尽的账号发一次探测请求):

HTTP 429
{"error":{"data":{"code":14018,"msg":"Credits exhausted. ..."}}}

code      = 14018
semantic  = quota_exhausted
verdict   = quota_exhausted
state     = quota_exhausted

对照旧代码:14018 不在语义表里 → 429 属 TRANSIENT_STATUS
→ transient → 健康映射 "transient": "unknown"。

所以界面显示的正是「没测出结果」 —— 与你看到的现象完全吻合。
这不是定位错误,是判定修好后显示层还差一档。

修复:补 audit 页的显示档位

检测接口 /api/account-health/probe 现在返回正确:

{
  "verdict": "quota_exhausted",
  "state": "quota_exhausted",
  "message": "额度已用尽",
  "userMessage": "额度已用尽,已自动停用",
  "action": "充值或等待额度重置",
  "code": 14018
}

但一致性自检页(audit)的状态着色只认三档:

if (st === "invalid") cls += " wb-audit-bad";
else if (st === "restricted") cls += " wb-audit-warn";
else if (st === "valid") cls += " wb-audit-ok";

quota_exhausted 一档不落 → 掉进「默认无着色」→
与「未检测」在视觉上无法区分。

现补上:

else if (st === "quota_exhausted") cls += " wb-audit-warn";

配色与 restricted 同为琥珀:两者都不是凭据坏了,
用红色会让人跑去重新登录,而重登对这两种情况毫无帮助。

一处说明:「检测账号」按钮无需改动

那条路径(/api/account-health/probe 的渲染,2654 行)本来就是三分支:

var tone = state === "valid" ? "ok"
         : (state === "invalid" ? "error" : "warn");

quota_exhausted 落在琥珀 warn 档,已经是对的。
本次只补 audit 页。

验证

  • 容器实测接口返回:state=quota_exhausted、
    userMessage=额度已用尽,已自动停用、code=14018
  • 对照正常账号:HTTP 400 code=11102(模型不存在)→
    verdict=available → state=valid,确认未误伤
15 项单测                      ✅ 全绿
五模块 AST                     ✅ 通过
注入 JS node --check           ✅ 通过

升级说明

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

  • 无新增或删除环境变量;
  • 无数据库结构变更;
  • 纯前端显示修复,无判定逻辑变更。

v0.9.24

Choose a tag to compare

@deltrivx deltrivx released this 30 Sep 03:06

AutoBuddy v0.9.24

额度耗尽的账号由巡检自动停用,不再只提醒用户手动删除。

问题

额度用尽的账号一直留在轮询里持续失败,系统本该自己把它停掉,
却只能反复提醒用户去手动删除。

根因:额度耗尽根本不在自动禁用的触发条件里

账号级自动禁用只认两类:

if verdict not in ("auth_failed", "restricted"):
    continue

而额度耗尽的上游响应是:

HTTP 402  {"msg":"Insufficient Balance"}              code=ACCOUNT_QUOTA
HTTP 429  {"data":{"code":14018,"msg":"Credits exhausted..."}}

两条都掉进 transient:

响应 归类路径 结果
HTTP 402 不在 UNAVAILABLE_STATUS{400,401,403,404,422,500,502,503,504},也不在 TRANSIENT_STATUS{408,425,429} → 走兜底 transient
HTTP 429 明确属于 TRANSIENT_STATUS transient

而 transient 的语义就是「临时状态,不参与自动禁用」——
它本是为「网络抖动、上游 5xx」设计的一次性状态。

额度耗尽是持续状态(不充值 / 不重置不会自己好),
把它当一次性抖动,是这次故障的根源。

修复

新增 quota_exhausted 这一 verdict,并纳入自动禁用:

if verdict not in ("auth_failed", "restricted", "quota_exhausted"):
    continue

判定覆盖三条路,任一命中即可:

1. 语义表 —— 14018 → quota_exhausted

实测证据(2026-09-28 Mac dsh 会话日志):

429 {"data":{"code":14018,"msg":"Credits exhausted. Please visit
     the link below to purchase add-on packs..."}}

2. HTTP 402 —— 按状态码判定

402 Payment Required 语义唯一,不需要依赖错误码。

3. 文字兜底 —— QUOTA_EXHAUSTED_HINTS

用于响应体取不到错误码的情形,覆盖
insufficient balance / credits exhausted / 额度不足 等。

⚠️ 一处刻意的取舍

没有把 10001 写进语义表。

同一个码在签到接口上是「今天已签到」
(workbuddy2api-panel 的 alreadyCheckinMarkers 实测),
含义冲突 —— 写成额度耗尽会把正常的重复签到误判成没钱。

因此 402 的额度耗尽改由状态码判定,判据更可靠、不会误伤。

界面文案

额度耗尽与「账号被拦截」区分开 —— 后者只能等,前者可以充值:

字段 文案
label 额度已用尽
action 充值或等待额度重置
userMessage 额度已用尽,已自动停用

自愈

无需额外改动:自动启用只放开 verdict == "available" 的账号,
额度耗尽不等于 available,不会被误放回;
充值 / 重置后探测恢复正常,巡检会自己把它放回来。

测试

_test_model_health.py 新增 7 项(231 → 238):

[ok] 14018(Credits exhausted)归类为额度用尽
[ok] HTTP 402 归类为额度用尽(不依赖错误码,避免 10001 语义冲突)
[ok] 无错误码但文案命中,同样归类为额度用尽
[ok] 额度用尽**不是** transient(transient 不参与自动禁用)
[ok] 额度用尽的账号被自动停用
[ok] 额度用尽写入账号策略且来源为 auto
[ok] 同轮正常账号不被误停
15 项单测                      ✅ 全绿
五模块 AST                     ✅ 通过

升级说明

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

  • 无新增或删除环境变量;
  • 无数据库结构变更;
  • 行为变化:巡检发现账号额度耗尽时,会自动将其停用(来源 auto),
    而不是继续让它参与轮询。充值后巡检自动放回。

相关:v0.9.23 已修的两项

  • 新加的账号不被调用(能力矩阵收窄排除未知账号)
  • 巡检探测账号池外的账号(现与日常调用同源)

v0.9.23

Choose a tag to compare

@deltrivx deltrivx released this 29 Sep 16:57

AutoBuddy v0.9.23

修复新加的账号永远不被调用,以及巡检探测账号池外的账号。

修复一:新账号一次都没被调用

现象

新加进账号池的账号,token 有效、确实在 enabledAccountIds 里,
但一次都没被调用过。

selection_logs.json 实测(09-29 23:43 ~ 09-30 00:36,200 条选号记录):

palmesewooters371@gmail.com   28 次
尘星途                        28 次
Mr.Chen                       27 次
13035639603                   26 次
18981122864                   26 次
18011628363                   25 次
13558746529                   24 次
13666135560                   16 次
----------------------------------------
15573657770                    0 次   ← 新账号
13111877992                    0 次
15228718439                    0 次
15198254808                    0 次

根因:收窄逻辑把「未知」当成了「不支持」

选号时有一层「正向能力矩阵」收窄:

known_yes = [a for a in candidates
             if capability_state(_account_id(a), model) == "yes"]
if known_yes:
    candidates = known_yes      # ← 只留"明确支持"的

capability_state() 对从未探测过的账号返回 None,
于是新账号和"明确不支持"一样被排除。

配合巡检的冷启动缺口,形成死循环:

新账号 0 调用
   ↓
巡检 onlyUsedModels=true → used_models 为空
   ↓
只探 BASE_MODELS 基模型清单
   ↓
清单里没有自动发现的新模型(如 hy4-preview-f)
   ↓
能力矩阵永远缺这个键 → state 永远是 None
   ↓
收窄时被排除
   ↓
又是 0 调用  ← 回到起点

对比数据印证了机制:

账号 hy4-preview-f 键 结果
6 个老账号 yes 进候选,24~28 次
4 个新账号 键不存在 出局,0 次

为什么 13666135560 没键也能被调用?

它 261 次调用全是 deepseek-v4.1-flash(不是 hy4-preview-f)。
收窄是按当前请求的模型查键的 —— 这恰好反证:
不是账号被禁,而是「该账号 × 该模型」这个组合没被探过。

修复

只排除明确不支持,未知保留参与:

not_no = [a for a in candidates
          if capability_state(_account_id(a), model) != "no"]
if not_no:
    candidates = not_no

两处收窄点均已修正:主选号 _pick_account、换号重试 _retry_candidates。

真不支持的账号仍会被 _learn_model_unavailable 即时学习成 no,
下一轮自然出局 —— 不牺牲原有选号质量。

保留 if not_no: 守卫:全部明确 no 时不覆盖候选集,
让上游返回真实错误,而不在网关层编一个「无账号」。

修复二:巡检探测账号池外的账号

现象

账号已经移出账号池,巡检还在探它、还在报错。

实测:一轮巡检探了 15 个账号,远多于池内数量;
model_health_last.json 里出现了已移出池的账号。

根因:两套账号集合

路径 账号来源
日常调用 /v1/chat/completions _enabled_accounts()(白名单 + 停用策略)
模型巡检 _load_accounts()(两份账号文件全量合并)

巡检那条完全没过池白名单。

修复

raw = model_health.run_round(
    accounts=_enabled_accounts(_load_accounts(), _load_pool_config()),
    ...
)

与日常调用同源。这符合项目既有设计原则 ——
build_account_store() 的注释写着:

账号来源与账号池其它功能保持同一个出处,
避免「账号页看得到、每日任务里没有」

界面手动「只探这几个账号」(overrides.accountIds)时,
仍由 model_health.select_targets 按该清单收窄,不受影响。

测试

  • 新增 _test_pool_selection.py(11 项):锁定「不得排除未知账号」契约,
    防止日后又改回 state == "yes";同时校验巡检的池过滤。
  • 修正 _test_model_health.py:该测试把 run_model_health_check
    切片到隔离命名空间执行,巡检新增的两个依赖需补桩
    (_load_pool_config / _enabled_accounts)。
    生产代码没有问题,是测试桩缺失。
15 项单测                      ✅ 全绿
四模块 AST                     ✅ 通过
注入 JS node --check           ✅ 通过

升级说明

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

  • 无新增或删除环境变量;
  • 无数据库结构变更;
  • 行为变化:
    • 新账号加入池后可立即参与轮转(原先需先被巡检探到该模型);
    • 巡检只探池内账号,探测次数下降,不再报池外账号的错。

遗留事项(非本次范围)

  • 国际版账号额度耗尽时会被正常选中但调用失败,建议移出账号池
    (本次未改:这是账号状态问题,不是选号逻辑问题)。
  • 上游 429 的响应体未落盘,无法区分 6004(模型级限流)与
    14018(额度耗尽)。如需精确诊断,可另加日志。

v0.9.22

Choose a tag to compare

@deltrivx deltrivx released this 28 Sep 14:32

AutoBuddy v0.9.22

修复每日任务页顶部四项数据框在深色主题下不变色。

问题

深色主题下,每日任务页顶部那块「今日积分 / 签到积分 / 任务积分 / 总积分」
的四项数据框仍是一块白底,和整体暗色页面格格不入。

根因:背景色被写死

.wb-dl-credit-card 里:

background: rgba(255,255,255,.7);   /* 硬编码白色 */

讽刺的是,同一处代码的注释里明明记着官方规格是 bg-card/70
(跟随主题变量)—— 实现与注释自相矛盾。

实测证据

1280px 视口、深色主题下取 computed style:

项目 取值 判定
--card oklch(27.9% .041 260.031) ✅ 主题变量正常
body 背景 oklch(0.208 0.042 265.755) ✅ 已变暗
本框背景 rgba(255,255,255,0.7) ❌ 没跟上
文字色 oklch(0.929 0.013 255.508) ✅ 跟随了
边框色 oklch(1 0 0 / 0.1) ✅ 跟随了

也就是说:文字和边框都正常跟随主题,唯独背景是写死的 ——
所以现象精确地表现为「深色页面里嵌一块白底卡片」,而不是整体都不对。

修复

background: color-mix(in oklab, var(--card, #ffffff) 70%, transparent);

与官方积分统计页的 bg-card/70 完全对齐。

⚠️ 一个容易踩的坑

--card 是 oklch 格式(oklch(27.9% .041 260.031)),不是 hex。

所以不能用项目里常见的那种老写法:

color-mix(in srgb, var(--card, #0f172a) 92%, transparent)  /* ❌ 兜底是 hex */

srgb 空间 + hex 兜底在 oklch 变量下语义不对。这里必须指定
in oklab,且变量缺失时退回中性半透明色,而不是又变回固定白。

顺带做的检查

全量复查了页面内其它硬编码浅色,确认都不是同一类问题:

位置 内容 判定
.wb-modal-btn 蓝色按钮 #3b82f6 + 白字 按钮主色,非背景
toast color: #ffffff 深色浮层上的前景白字

这两处保持原样。

验证

四模块 AST                    ✅ 通过
注入 JS node --check          ✅ 通过
15 项单元测试                 ✅ 全绿
版本一致性自检                ✅ 通过
部署后实测                    深色主题下背景跟随 --card 变化

升级说明

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

  • 无新增或删除环境变量;
  • 无数据库结构变更;
  • 本次为纯前端样式调整,无后端逻辑变更。

v0.9.21

Choose a tag to compare

@deltrivx deltrivx released this 27 Sep 10:21

AutoBuddy v0.9.21

修正 v0.9.20 日志去重的两个缺陷:重复次数被悄悄吞掉、去重字典无上限。

背景

v0.9.20 上线后我做了一次部署后对照实测,发现去重虽然生效,
但有两个我自己引入的缺陷。

缺陷 1:重复次数被悄悄吞掉(观测断档)

原设计

「内容变化时,先汇报上一条被吞了多少次」:

repeat = _ACCESS_DEDUP_COUNT.get(key, 0)
if repeat > 0:
    print(f"(上一条重复 {repeat} 次) {key}")

为什么永远不会触发

为了抗交错轮询(A、B 交替),去重是按内容各自独立记录的:

c3 累积的次数 → 记在 c3 名下
d4 出现时     → 查的是 d4 的计数(= 0)

所以那行交代永远不会打印。

实测确认

场景 结果 判定
发 3 个不同内容 出现 3 行 ✅ 没被静音
再发 4 次相同内容 0 行 ✅ 去重生效
换成新内容 只出现新行,无交代行 ❌ 观测断档

修复

改为按时间周期汇报:默认每 60s 把各条内容累积的待报次数
统一打出并清零。

INFO:     (重复 N 次) <内容>

可用环境变量 ACCESS_DEDUP_FLUSH_SEC 调整周期(测试时可设成几秒)。

这样既保留可观测性,又不破坏「按内容各自去重」的正确性。

缺陷 2:去重字典无上限(内存泄漏)

_ACCESS_DEDUP_LAST 每遇到一种新内容就加一个键,从不清理。

access log 的内容种类理论上无界 —— URL 里带动态 id 时尤甚 ——
长时间运行就是一条缓慢的内存泄漏。

现已加上限 _ACCESS_DEDUP_MAX_KEYS = 500,超出时淘汰最早写入的键
(dict 保持插入顺序,取前 N 个即可)。

回归测试补强

_test_log_dedup.py 新增两条校验:

  • 必须存在 _flush_access_dedup(否则重复次数被悄悄吞掉)
  • 必须存在容量上限(否则内存泄漏)

这两条都是实测踩出来的,不是凭空加的。

验证

四模块 AST                    ✅ 通过
15 项单元测试                 ✅ 全绿(含新增两条校验)
版本一致性自检                ✅ 通过
部署后对照实测                ✅ 去重生效 + 不同内容不误杀

升级说明

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

  • 新增可选环境变量 ACCESS_DEDUP_FLUSH_SEC(默认 60,不设即用默认值);
  • 无数据库结构变更;
  • 本次为日志输出层调整,不影响任何接口行为。