tbphp
1a6e454475
feat(subscription): 接入 Codex/Claude 被动额度信号观察 (#487)
* feat(subscription): 接入 Codex/Claude 被动额度信号观察
从 Codex/Claude 订阅请求的响应 Header 中安全采集额度信号,归一化为与主动观察一致的账号级窗口,
经内存 pending map 合并后由 RequestLog worker 复用现有 quotaWake 异步落库,
自动刷新账号卡的额度窗口与同步时间;额度仅用于管理面展示,不参与路由、调度或健康判定。
同时修复 recordCredentialObservationFailure / invalidateCredentialObservationAfterReset
整行 Save 覆盖并发写入 snapshot_json 的既有问题,改为按列更新。
* fix(subscription): 修正被动额度合并的两处数据错误
丢弃比已存记录更旧的被动采样:手动刷新在 pending 落库前完成时,
旧采样会让 observed_at_ms 倒退并用旧额度覆盖新值;updated_at_ms
条件更新只能拦截读取之后的写入,拦不住这种时间倒退。
MergeQuotaWindow 不再用空字符串覆盖 Label/LabelKey/Scope/Unit:
这些是快照必填字段,被清空后 ApplyQuotaWindows 会跳过该窗口并
清除管理面低额度提示。
* fix(subscription): 落库确认后清空 pending 窗口缓存
ack 此前只清 dirty 标记,不清 entry.windows;下一次响应即使只带
部分字段,也会把已经落库的陈旧字段当作当前观察重新合并进去,
可能覆盖同一时间发生的主动刷新写入的更新值。
现在版本匹配时一并清空窗口缓存,配合已有的 MergeQuotaWindow
字段级保护,落空字段的响应不会再用零值或陈旧值覆盖数据库现值。
* fix(subscription): 通用化 Codex 被动额度解析并修正三处数据错误
Codex 被动解析改为从 Header 自动发现限额命名空间,不再写死
primary/secondary 两个账号级窗口。带 Limit-Name 的附加限额
(如 GPT 5.3 Codex Spark)现在也能被动更新,且窗口 ID 与主动
JSON 路径一致;将来新增的限额类型无需改代码即可识别。缺少
Limit-Name 的命名空间跳过,因为无法生成可匹配的稳定 ID。
同时修正:
- 越界 used-percent、非正或溢出的 reset/window 一律丢弃,
不再用 0 或回绕值覆盖快照中的有效字段;
- 被动不再输出 Label/LabelKey,避免只带使用率的响应把 7d 窗口
的标签改写成 Session,与 window_seconds 自相矛盾;
- 主动成功的 observed_at_ms/updated_at_ms 改用响应完成时间
(last_attempt_at_ms 仍为尝试开始时间),慢速手动刷新不再被
刷新期间捕获的被动样本反向覆盖,CAS token 也不再可能不变;
- 窗口值未变化但确实观察到时推进 observed_at_ms,低频账号不再
长期显示数小时前同步。
* refactor(subscription): pending 只保留最新一次响应,不再跨响应合并
pending 条目此前会把多次响应的窗口字段按 ID 合并,却只记录一个
观察时间。混在一起的字段因此可能带着较晚的时间戳落库,反过来
盖掉这期间提交的主动刷新结果。上一轮给 ack 加清缓存只堵住了
"已落库后残留"这一半,尚未落库前合并的那一半仍然存在。
改为每次记录整体替换:一个 pending 条目只承载一次响应,窗口与
观察时间始终描述同一时刻,这类时序覆盖从根上不再可能,ack 也
不需要再清缓存。被丢弃的旧响应不构成数据丢失——快照中该窗口
保持原值,下一次带该窗口的响应会刷新它。
生产代码净减 19 行。
* fix(subscription): 用 observation_version 做被动写入的并发令牌
时间戳不能充当 CAS 令牌:两次写入可能落在同一毫秒,updated_at_ms
数值不变会让已过期的被动写入通过条件更新,盖掉刚提交的主动刷新。
改用 observation_version(主动成功本就递增),并保留 updated_at_ms
比对以覆盖主动失败与重置这两条只改元数据、不动版本的路径。
同时收敛三处局部合同:
- 绝对 reset 无效时继续尝试同一响应中的相对 reset,不再因一个坏
Header 丢掉可用值;
- 被动输出不再携带 Scope / Unit / IsPrimary(此前只清了 Label),
贯彻「自动更新只改额度数值与状态」,Codex 与 Claude 两侧一致;
- pending 深拷贝窗口的指针字段,调用方后续修改不会改变待落库内容。
* fix(subscription): ACK 时移除 pending 条目并让同毫秒判给主动结果
ACK 此前只清 dirty 标记,条目连同克隆的窗口一直留在 map 里。凭据
删除后也不会释放,长期运行且凭据有增删的实例会持续增长。改为版本
匹配时直接移除条目——凭据删除本就走「registry 查不到 → ACK」这条
路径,因此不需要再给删除流程挂额外的清理钩子。
被动样本与行内 observed_at_ms 相同时改为丢弃(原为严格小于才丢)。
被动 Header 先捕获、主动刷新稍后完成,两者截断到同一毫秒时,旧的
被动结果会盖掉权威的主动结果,且此时 CAS 不冲突。相同毫秒判给主动
结果即可,不引入更高精度时间戳。该判断收紧后,落库处基于
alreadyObservedAt 的分支成为死代码,一并删除。
顺带移除无生产调用方的 AckPassiveQuotaObservation 导出方法。
2026-08-28 18:26:00 +08:00
..
2026-08-20 17:19:32 +08:00
2026-08-20 17:19:32 +08:00
2026-08-16 12:00:07 +08:00
2026-08-17 15:00:14 +08:00
2026-08-17 16:12:07 +08:00
2026-08-22 02:00:23 +08:00
2026-08-22 02:00:23 +08:00
2026-08-20 17:19:32 +08:00
2026-08-10 08:22:15 +08:00
2026-08-12 21:05:21 +08:00
2026-08-24 23:26:22 +08:00
2026-08-24 23:26:22 +08:00
2026-08-28 18:26:00 +08:00
2026-08-10 08:22:15 +08:00
2026-08-03 08:37:16 +08:00
2026-08-10 08:22:15 +08:00
2026-08-10 08:22:15 +08:00
2026-08-24 23:26:22 +08:00
2026-08-24 23:26:22 +08:00
2026-08-17 10:52:33 +08:00
2026-08-17 10:52:33 +08:00
2026-08-11 16:32:43 +08:00
2026-08-28 18:26:00 +08:00
2026-08-10 08:22:15 +08:00
2026-08-24 23:26:22 +08:00
2026-08-03 08:37:16 +08:00
2026-08-12 22:46:08 +08:00
2026-08-17 10:52:33 +08:00
2026-08-17 15:00:14 +08:00
2026-08-28 18:26:00 +08:00