feat: 模型限额台账 + CLI 切换即时生效(先关进程再写状态 / 轮换存活门控) - #57
Merged
Merged
Conversation
从本机 WorkBuddy 日志还原「哪个账号的哪个模型被限流、官方何时恢复」, 新增命令 get_rate_limits(Tauri)与 GET /api/rate-limits(WebUI), 一次返回全部账号的当前受限状态。数据全部来自本地日志,不新增网络请求。 - 窗口固定 2 天(最近 2 个日期目录),两档位各扫一遍 - 字节级粗筛(Boyer–Moore–Horspool)后才解码,2 天窗口下扫描 < 200ms - 中英双语限额文案;重置时刻直接采用原文值,不自建窗口模型 - 模型归因三级回退:requestId → 该会话被限前的 resolved model → 未知(不猜) - 两步去重(会话+重置时刻;同重置时刻且相差 ≤15s),实测 14 行 → 2 事件 - 账号归因走 sessionId → sessions.user_id 批量查询;归因失败丢弃而非错归
在账号卡片右上角、旅行图标旁加一个限额状态图标,悬停显示受限模型与 恢复倒计时;不新增页面,也不改侧边栏导航。 - 该账号无受限模型时不渲染;受限模型数 >1 时图标带数量角标 - 悬停按恢复时间升序列出全部受限模型,归因失败显示「未知模型」 - 本地每秒按 resetAt 过滤,跨过官方重置时刻图标自动消失,不依赖后端刷新 - 随账号页既有节奏每 60s 重扫一次以捕获新事件 - 补齐四方接线:api.ts 的 ROUTES 与 DEMO_READ_COMMANDS、演示 mock (1 个单模型受限 + 1 个多模型受限账号) - 命令缺失或失败时按「无受限」处理,不影响账号页其它功能
- 新增三个限额来源:CLI(~/.codebuddy/logs)与 CodeBuddy / CodeBuddy CN 两个 IDE 的扩展日志(按插件目录过滤 + 文件 mtime 收窗) - 解析格式参数化(WorkBuddy / Ide):IDE 新增时间戳、conversationId 事件键 与 [ModelSelection] 模型归因;超长错误行只做定长窗口嗅探 - 账号归因改走日志内鉴权 uid(CLI [AuthDoInitProbe] / IDE Auth session changed),回落状态文件 activeAccountId 且要求 updatedAt <= 事件时刻, 仍不可用则丢弃;WorkBuddy 两档位继续走 sessions 表(行为不变) - 不扫 CodeBuddyExtension 共享宿主根:与 CN IDE 真身重复记录同一次 429 - payload 契约不变;新增 18 个单测(304 passed)
- 每行改为「模型 · 倒计时(恢复时刻)」:保留「2h14m 后恢复」, 括号补充官方恢复时刻(当天 17:59、跨天 明天 09:00 / 9/18 09:00) - tooltip 多模型多行时字号收至 11px(全局 TooltipContent 为 text-xs)
- 原按「最近 N 个日期目录」收窗:当天创建的文件会写到次日凌晨(实测 2026-09-15/ 的两个文件写到 9/16 07:59),跨天那批记录随整目录被排除; 改为「日期目录形态校验 + 文件 mtime >= now-2d」 - 目录名只用于排除 sdk/migration/memwatch 等非日志目录,不参与窗口判定 - 五个来源收窗口径统一(两档位与 CLI 共用同一套枚举) - 实测:候选 96 到 121MB、命中解码 44 到 50MB,release 稳定约 130ms - 收窗测试替换为「旧目录里的新文件要收、新目录里的旧文件要排除」
- 新增 useVisibleInterval:仅主窗口可见时按间隔执行,隐藏时暂停、 恢复可见时立即执行一次(复用 credit/status 的 visibilitychange + Tauri main-window-visible 双来源) - 旅行与限额轮询接入门控;两者间隔(均 1 分钟)提为命名常量 - 轻量模式销毁窗口时前端卸载、定时器随 cleanup 清理,退出后重建立即刷新
CLI 与 WorkBuddy 两档位的 429 由 Stop hook 当轮上报,后端消费信号文件后实时入账 (秒级);两个 CodeBuddy IDE 维持日志扫描,降频为 5 分钟兜底。 - 新增 rate_limit_hook:生成平台原生 hook 脚本,在三处客户端 settings.json (~/.codebuddy、~/.workbuddy、~/.workbuddy-ai)注册 Stop / FinalStop;幂等安装、 写前备份、卸载可逐字节还原,保留用户已有的其它 hooks - 新增 rate_limit_events:mtime 轮询消费 ~/.wb-switch/hook-events.jsonl,按 QUOTA_MARKERS + parse_reset_at 识别限额行,实时归因(CLI 读轮换状态、 WorkBuddy 走登录态 uid → sessions 兜底),状态由后端持有(轻量模式不丢) - limits:hook 健康时只扫 IDE 两源,未安装/被删时全量五源兜底;扫描按 5 分钟 节流并缓存,装/卸 hook 时作废缓存并强制全量一次 - 前端:监听 rate-limits-updated 立即拉取,兜底轮询改 5 分钟并以 scannedAt 门闩; 设置页新增「限额监听」开关(关闭后不扫日志、不渲染 chip)
- 启动时后台自动接入 hook(幂等、非阻塞、失败静默);设置页「卸载 hook」记下 用户拒绝(hookOptOut),之后不再自动装回,「接入 hook」恢复默认接入 - 客户端存在性(数据根 is_dir)成为安装与扫描的共同前提:不为未安装的客户端创建 目录 / 空 settings.json;扫描改为逐来源判定——存在且该处没接上 hook 才扫日志, 某处配置写坏或条目被手删只让那一处回退,不再整体退化 - ScanCache 由 ide_only 改为 scope 位图,命中要求「缓存 scope ⊇ 请求 scope」; 装/卸后仍强制全量扫一次 - 设置页文案区分未检测到客户端 / 已接入 / 已卸载 / 未接入;装/卸后回读配置, 避免本地标记过期被总开关写回 - 移除已无调用方的 is_installed(扫描范围改由 needs_log_scan 判定)
IDE 的 429 不触发任何 hook 事件,日志是它唯一的数据源;关闭后不再扫描两个 IDE 的 日志,CLI / WorkBuddy 的实时上报与未接 hook 时的日志兜底不受影响。 - 配置新增 scanIdeLogs(默认 true,与 enabled / hookOptOut 同为白名单字段) - scan_scope 增加入参:关闭时两个 IDE 不入范围;装/卸 hook 触发的强制全量上限收窄为 HOOK_SOURCES,不会把已关掉的 IDE 带回来 - 缓存失效分语义:开关类变更只清缓存(新增私有 clear_scan_cache),不置 FORCE_FULL_SCAN,避免刚关掉的 IDE 又被扫一次 - 设置页新增开关(整份提交配置);IDE 相关状态文案跟随该开关,不再说「仍按日志扫描」 - 移除 config::save_rate_limit_config 死代码(两个宿主统一走 limits 的同名入口)
CLI 的 apiKeyHelper key 是进程级快照(切换只对新进程生效),原按「事件
时刻的 state.json」归因,会把切换后老会话的 429 记到新账号。
- 新增 cli_switch_history.jsonl:切换成功且账号变化时追加 {at, accountId},
90 天 / 2000 条裁剪,读取容忍坏行
- 归因链:会话进程 startedAt(sessions/<pid>.json,兜底 transcript 首行)
→ snap_at ≥ state.json mtime 用当前账号 → 否则查历史 → 证据不足丢弃
- 不新增依赖;cargo test -p wb-switch-core 355 passed,fmt/clippy 零新增
- checkin_account 入口统一短路:自动周期、一键签到、单账号签到共用 - run_checkin_cycle 只遍历支持签到的档位 - 账号页不再查询签到状态;刷新按钮对国际版只刷新积分(文案相应调整) - counts_as_pending_checkin 更名 supports_checkin:同时门控请求与待签到集合
模型按消耗降序全部保留(不再截断 top5),新增 4 个配色 (orange/pink/cyan/slate)配套。
CLI 的 key 是进程级快照:改 state.json 不会给正在运行的会话换号。原先 "手动切换仅在跨站时关进程"与"轮换不关进程"都会留下持旧 key 的进程,限额 归因只能靠时间区间推理(上一版为此引入了 cli_switch_history 查表)。 改为先建立不变式:活着的 CLI 进程持有的 key == 当前默认账号。 - switch_active_account:去掉 close_running_cli 参数,固定"校验 → 关闭正在 运行的 CLI → 写 state.json";写失败时明示"已关闭但账号未写入",不回滚关闭 - rotate:删除按 transcript mtime 的 30 分钟活跃保护(管不住开着但闲置的 会话,且每次检查全量扫 ~/.codebuddy/projects),改为读 ~/.codebuddy/sessions/*.json 的心跳(120s,对齐客户端超时常量)做存活门控 - 被门控跳过且"放开门控后本会切换"时投递系统通知(每日上限 5 次,文案单点 构造);无头 server 只返回 notify 字段、不投递 - 归因简化为 snap_at ≥ state.json mtime → 当前账号,否则丢弃并告警;删除 cli_switch_history 模块与其写入 - active_guard_minutes / closeRunningCli 保留读取兼容,不再参与决策 cargo test -p wb-switch-core 357 passed;fmt 通过;clippy 零新增
- 切换确认弹窗不再区分是否跨站:统一写明确认后会关闭正在运行的 CodeBuddy CLI,按钮改为"关闭 CLI 并切换";移除已废弃的 closeRunningCli 传参 - 设置页移除已失效的「活跃保护」控件,切换时机说明改为存活门控 + 每日最多 5 次推迟提示;active_guard_minutes 字段保留仅为兼容旧配置 - 顺带修正 CLI 卡片里与实现矛盾的文案("当前运行会话不会切换") - api.ts 的 runRotate 补 notify 字段类型(桌面端宿主投递、无头 server 只返回) npx tsc --noEmit 通过
…-ledger # Conflicts: # README.md
系统通知在这条链上不可靠:插件在开发态会把通知登记到「终端」名下,macOS 后端仍是 已废弃的 NSUserNotificationCenter,且投递失败无法观测(插件的 show() 恒返回 Ok)。 因此窗口开着时以应用内提示为准,系统通知降级为尽力而为: - 桌面端 deliver_rotate_notify 先 emit rotate-deferred,再调用系统通知 - 新增 useRotateDeferredNotice 并在 Layout 挂载:收到事件弹 warning toast - webui 没有事件通道:手动检查读 run_rotate 返回的 notify 弹一次(isWebui 守卫, 避免桌面端同一件事弹两次) - README 与 spec 记录两路渠道,并明确"不要把日志里的『已提示』当成 OS 通知已送达" npx tsc --noEmit / cargo fmt --check / cargo check(src-tauri) 均通过;clippy 无新增
StrictMode 开发态会「挂载 → 卸载 → 再挂载」,而 listen() 的 Promise 在清理之后才 resolve,原写法(cleanup 里只 unlisten?.())会漏掉第一次注册的监听,导致同一事件 弹两条 toast。改为用 disposed 标记:Promise 晚到时立即自我退订。 同一竞态在 switch-account-dialog 与 use-workbuddy-status-refresh 里也存在(表现为 重复处理事件,非用户可见),本次不扩范围。
changexbc
added a commit
that referenced
this pull request
Sep 18, 2026
同步各 package.json、tauri.conf.json、三个 Cargo.toml 与 Cargo.lock 的版本号。 本次发布内容(PR #57): - 模型限额台账:新增「本机日志扫描 + 客户端 hook 事件」两条通路,来源覆盖 CodeBuddy CLI、WorkBuddy 两档、CodeBuddy IDE(含 CN) - 账号卡片展示模型限额 chip(含恢复时刻倒计时),设置页新增「限额监听」与 「扫描 CodeBuddy IDE 日志」开关;hook 默认自动接入,按客户端存在性收窄 - CLI 切换即时生效:手动切换固定「关闭运行中的 CLI → 写默认账号」;自动轮换 改为按 ~/.codebuddy/sessions 心跳做存活门控,被跳过时每日最多 5 次提示 (应用内 toast 兜底,系统通知尽力而为) - 限额归因收敛:snap_at >= state.json mtime → 当前账号,否则丢弃并告警
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
本分支一次性交付模型限额台账(本机日志 + hook 事件两条通路)与配套的账号切换语义收敛:
关键设计:不变式
CLI 的
apiKeyHelperkey 是进程级快照(改state.json不给运行中的会话换号),因此:state.json"(用例含顺序倒置实验)~/.codebuddy/sessions/*.json心跳(120s,对齐客户端WORKER_HEARTBEAT_TIMEOUT_MS)做存活门控,只在没有 CLI 会话运行时切由此归因简化为:
snap_at ≥ state.json mtime → 当前账号,否则丢弃并告警(删除上一版的cli_switch_history查表机制)。主要改动
限额台账
wb-switch-core:新增limits(日志扫描/解析/去重/归因)、rate_limit_events(hook 事件通路 + 去重入账)、rate_limit_hook(三处 settings.json 安装/卸载/状态,幂等 + 备份 + 逐字节还原)CLI 切换与轮换
switch_active_account去掉close_running_cli参数,固定先关后写;写失败时明示"已关闭但账号未切换"~/.codebuddy/projects),改用心跳存活门控notify字段不投递归因简化
attribute_cli:snap_at ≥ mtime → 当前账号,否则丢弃 + 告警;删除cli_switch_history模块(-241 行)与其写入兼容说明
closeRunningCli(HTTP/Tauri)保留接受但忽略,标 deprecatedactive_guard_minutes配置键保留读取(不报错),不再参与决策,设置页不再展示~/.wb-switch/cli_switch_history.jsonl留在原处不清理验证
cargo test -p wb-switch-corecargo fmt --all --checknpx tsc --noEmitcargo clippy --all-targets -- -D warnings冒烟清单(发布前人工执行)
state.json在关闭后写入已知残余
state.json时,旧进程的事件会被丢弃(保守,宁少不错)通知渠道说明(补充)
轮换推迟提示的可见性有三层:
emit("rotate-deferred"),前端useRotateDeferredNotice(挂 Layout)弹 toast;webui 无事件通道,手动检查时从run_rotate返回的notify取值弹出(isWebui()守卫,避免桌面端重复弹)。tauri-plugin-notification在 macOS 上走notify-rust的NSUserNotificationCenter(苹果已废弃)后端,开发态下通知还会被登记到「终端」名下,且show()恒返回成功、投递失败无法观测;实测本机通知注册表(com.apple.ncprefs)中没有本应用条目。因此不要把轮换日志里的"(已提示)"当成系统通知已送达。若以后要修这条链,可选择:打开notify-rust的preview-macos-un现代后端,或 macOS 分支改用mac-usernotifications(可check_bundle/request_auth并拿到错误原因)——本次不做。