Skip to content

v0.1.6

Choose a tag to compare

@github-actions github-actions released this 10 Oct 12:00
· 26 commits to main since this release

本次更新(0.1.6 发布后的全部工作)

Added

  • 每日签到,三家风(Trae CN / Qoder / MiniMax Code) — 一个注册表(CHECKIN_TOOLS)同时驱动三处:引擎的宿主退出豁免(高级 → 自动签到,开启后关着 IDE 也不错过当天——领取是对账号的 HTTPS 调用,不是读宿主状态)、面板的工具头按钮、底栏的控件渲染;按钮只把真实领取变成"已签到"徽标,排队中、未到点都保持按钮并把原因写明。Qoder 的读取按客户端实测定案:请求头带无横线的 Cosy-ClientType 才返回活动列表(横线版服务端答空列表,曾把"有活动"误读成"无活动");每日行 10:00 开门,开门前按钮置灰写明时刻;领取有客户端设备指纹门槛(第三方面部伪造应答 503 SAME_PERSON),TokenMe 把状态读对、把"需在 Qoder 内领取"说清楚——用户在 Qoder 里领完,下一次轮询面板即亮已签到。MiniMax Code 的协议取自厂商自己的开源客户端(MiniMax-AI/minimax-code)逐字实现:7 天面板的 status 2 即可领日、claim 幂等,请求带厂商公开定义为 wire 常量的归因签名头(授权仍是面板本就代为续期的 bearer token),账号 id 按客户端同款三级阶梯解析(运行时快照 → 指纹钉住的 CLI 身份文件 → 账号端点)。Trae CN 的积分包把已用份额真正送进仪表(原先恒 0%),签到包 id 带日期作用域——上周的积分包不再把"今日已签到"点亮;匿名积分包各持唯一 id,不再互相串状态。
  • CodeBuddy CN 桌面版 加入适配器注册表——会话流与配额照注册表契约一并接入。
  • Windows 宠物皮肤可配置 — 桌面宠物从内置注册表选择模型与皮肤(PetModel 组件 + 皮肤注册表),配独立的一致性校验脚本进构建门禁。
  • Windows 面板与托盘的稳定化 — "第一次点击就有页面":WebView2 控制器的可见性在每次显示时重申(Runtime 154 起不再免费合成不可见控制器)、显示先于锚定、无托盘锚点改读光标;托盘面板可见性恢复;托盘字形按任务栏实际绘制尺寸取图;应用图标铺满 Windows 期望的画布。

Fixed

  • DSH 的跨天会话不再把历史用量搬进今天 — v4 会话原先按"终身累计 + 最后使用时刻"计账:昨天花 12M 的会话今天继续用 1M,今天就读成 13M、昨天清零(用户实测报告)。实拆 v4 流发现逐次计费事件俱在(152 次调用之和与投影累计分毫不差),现在 v3/v4 一律逐次计费、各归各的一天;投影缓存退役为精度审计的厂商侧台账;一次性迁移把存量累计行退役、全部流按日真归因重读(总量分毫不差,只挪归属)。
  • 配额门禁的承诺在无缓存时也成立 — 被关闭或宿主暂停的探测,在缓存目录缺失时不再漏出去真连厂商;对签到工具这意味着不会替用户已关闭的工具偷偷发领取请求。
  • 启动 Loading 的双环终于转起来了,颜色也跟随主题 — 守卫询问的两个 selector 名(一个拼错、一个张冠李戴)自始就不存在于 AppKit,静默降级让双环从上线首日起就只画一条字;现在换上真 selector、拒绝按名落日志,文字与双环按设置钉住/系统外观解析主题色(浅色 #66748a/#0d8f74 · 深色 #8a99ad/#30d9aa,与页面加载卡同源),浅色底不再把字和内环洗没。
  • 额度横幅的两次误响 — 100% 横幅每个窗口实例只发一次(不再随样本抖动反复横跳),重发加冷却;配额缓存带版本换挡,同类窗口的碰撞按版本作废而不是互相污染。
  • 签到标记行不再冒充数据 — 今日已签到 / 未到签到时间 / 需在客户端内领取是状态不是窗口,不再作为一行插在真实仪表之间;徽标与按钮仍从原始报告读取它们。
  • 全平台 lint 干净,scripts/check-no-window-spawns.py 等门禁随之覆盖新代码面。

Changed

  • 公开交付成为一条命令 — scripts/public-deliver.sh:全量门禁(CLEAN 否则不动)→ github 分支跟随 main 并在 forge 落台 → 新 tag 先行(release 工作流必须先于 main 启动)→ 公共远端 main 以 force-with-lease 收尾;README hero 横幅随树自动重渲(版本号、工具数、徽章布局、双主题 2x PNG)。
  • CI 的三个打包 job 各自带齐 collector — macos / macos-universal / windows-panel 在构建前从 cli-linux 工件取两枚 musl 二进制(资源是未跟踪的构建输入,镜像检出没有它们)。
  • README 的 Windows 更新段与适配器表落到与实物一致;scan-public.py 喂 cat-file --batch 改为独立线程(范围过千 blob 时同管道先写后读会两头死锁)。

[0.1.6] — 2026-10-08

Added

  • Quota banners at 80 % and 100 % — the panel now posts a native notification the moment a vendor-metered window first crosses 80 % used ("已用 83%(剩余 17%)", titled Qoder · 5 小时) and once more when it runs out ("额度已用完(100%)"; the language follows the panel's). The real-quota sources get banners — the strip's windows and credit wallets — while §5 budgets stay the panel's own business. Edge-triggered, not level-triggered: each line fires once per window instance and the fact is persisted beside the index (notify-state.json, 0600, tmp + fsync + rename, 30-day retention), so a restart replays nothing and the 60 s quota-pass TTL can re-publish the same sample all it likes — the machine is idempotent. A posted banner is what advances the tier: a delivery the system refused is retried on a later pass (one attempt per window per 60 s). A window renewal — its reset instant jumped past the few seconds of probe jitter (> 5 min) — re-baselines silently; a tier that falls back re-arms without a word (the path that also covers credit top-ups, whose resets_at never moves); and a window first seen already past 80 % is baselined quietly, because the banner marks the crossing, not the state — only an already-exhausted first sight announces itself. The permission prompt is raised lazily, by the first banner actually due (once per run, on its own thread, so a prompt nobody answers cannot wedge the worker) — a user who never crosses a line is never asked — and a denial logs the System Settings path on first occurrence instead of silently swallowing banners.
  • Delivery is notify-rust's UNUserNotificationCenter backend; the Tauri notification plugin was tried and dropped — on desktop that plugin's permission API is an empty stub (permission_state answers Granted unconditionally), its show posts through the deprecated NSUserNotification path and discards the delivery error in a fire-and-forget task, and in dev it would post as com.apple.Terminal. The threshold machine needs the real results at both ends — the permission verdict and whether a banner actually posted — so delivery goes through notify-rust with its preview-macos-un feature, the UNC path the spike proved posts from an ad-hoc-signed bundle; its Windows side rides the same crate's WinRT toast backend under the app's AppUserModelID once installed. TOKENME_TEST_NOTIFY=1 posts a test banner through the same gate (permission request included) for QA of the permission flow, and the Windows backend is cross-checked compiling for x86_64-pc-windows-gnu. The honest boundary: a bare cargo run has no bundle identifier, so desktop banners are off there by nature and the worker says so in the log — the .app is the real surface.
  • The pointer to the OS toggle lives in the panel — while the system would not deliver a banner (permission denied, or never asked) and there is a vendor window to warn about, the quota section carries a one-line guide above its list: 开启系统通知后,额度跨过 80% 或耗尽时会自动弹出提醒 — with an 开启通知 button when the permission is undecided (it raises the system prompt, which is macOS's own decision UI; the state is polled afterwards so the row retires the moment the answer lands) or 去开启 after a denial (opens System Settings' notification pane, deep-linked straight to TokenMe's row). It is dismissible (忽略) and the dismissal is remembered per state — observing the permission change re-arms it, so a grant now never suppresses a later denial — and it renders nothing at all once banners can post, so a granted machine never sees it. The status is a new read-only pair of commands (notify_status / notify_enable), QA-pinned by TOKENME_NOTIFY_STATE and ?notify=. In the same batch: the TOKENME_TEST_NOTIFY test post now fires after the run loop is up — fired at setup it could not deliver ("Mainthread not running"), a failure a denial had been masking.
  • Settings became a four-tab sheet, and every probe is now something you can turn off — the one long scroll is now 通用 / 提醒 / 高级 / 关于 at a fixed 65 % of the panel height, with the body scrolling inside the selected tab and 版本 · 检查更新 · 退出 pinned in the footer so the escape hatch never scrolls away; tabs carry no entry counts. Two rows open a list inside the same sheet (‹ 返回 back, › a page here, ↗ another surface — 服务器与同步 borrows the existing server sheet rather than reimplementing it). What the old sheet could not do was stop polling: 高级 → 自动探测配额 is the master gate and 逐工具探测 the per-vendor list, wired through usage_quota::collect_gated(budget, gate). A gated probe sends no request but its cache is still read, so the row keeps its last answer on screen instead of going blank, and while the gate is closed the quota section says 已停止探测 · 数据停在 N 分钟前 — quiet numbers and stale numbers must not look identical. Moving either switch deliberately does not fire a refresh: a forced pass would delete the answer the user just chose to keep, and the panel's own 60 s pass picks it back up. The list defaults to the probes this machine has actually answered and offers 显示全部 for the ones it never has; the row set comes from usage_quota::inventory() — the registry describing itself (which probes exist, which have a host-process gate, which have answered here) rather than a front-end copy that drifts — pinned by a test that names the probes no host gate can reach. The four with no mapping (Copilot, Gemini CLI, Kimi Code, MiniMax Code) are tagged 无退出门, because that list is the only way to stop them. 提醒 grew the choice the banners were missing: 提醒档位 (80 % 与 100 % / 仅 100 % / 关闭) and 静默工具, which mutes selected windows while their numbers keep refreshing — notify::Gate filters delivery and still advances the window's recorded tier, because a tier left frozen at its old value would post a stale banner the instant the user changed their mind. A list under a closed gate or a denied notification permission greys its switches and says why instead of showing toggles that look live, and 退出后暂停 moved into the per-tool page where the host gate belongs — which is also what keeps all four tabs inside the 65 % box. The tray display-mode chip and 桌面气泡 (Windows-only) keep their places; the quota section's guide line is unchanged.
  • scripts/check-i18n-parity.py — the two dictionaries and their call sites are now gated, not eyeballed — TypeScript pins the key sets against each other, but it cannot see a placeholder that diverges between the two sides ({n} in zh against {count} in en renders the raw token), a call site handing t() the wrong name ({ lab } against a string that says {label} typechecks and prints {label} on screen), or dead copy left behind when a control is deleted — and t() answers undefined for a miss, so every one of those symptoms is a single blank or raw row. The gate checks the key sets, the placeholder sets per key, that each literal t("key", { … }) hands exactly the names its string asks for, and that no key in the dictionary is unreachable; it refuses to skip a dictionary line it cannot parse, and it is wired into build-macos.sh beside the theme-parity gate so it runs even with --fast. Controls, because a gate nobody has falsified is decoration: four broken copies (an English key removed, a placeholder renamed on one side, an orphan key added, a value written unquoted) each fail, and a call site renamed to { lab } fails with the file and line. The first run that read the file correctly (two parser holes earlier: a value written across two lines, and a key reached through a `set.tab.${k}` template) found six keys nothing referenced — read.hour.tail, set.quit.btn, srv.a11y, srv.copy.fail, srv.install.fail, srv.next — left behind by earlier UI iterations; they are deleted, and the quit button now says 退出程序 with 关闭面板并退出托盘进程 as its tooltip, which is where the two set.quit* strings went instead of being dropped.

Fixed

  • No console window on Windows when a quota probe shells out — the Copilot probe reads its credential through the user's own gh auth token, and gh is a console binary: started from a GUI process (the tray app owns no console), Windows allocates one for the child and the default terminal paints it — a black window that took focus once per probe TTL on every machine that has GitHub CLI installed, whether or not the user runs Copilot at all. The spawn now carries CREATE_NO_WINDOW, the flag the Antigravity CLI and atomcode probes already carried for exactly this reason, and the panel's own link buttons (cmd /c start behind 发邮件 / Releases / 日志) are quietened the same way. The CLI is unaffected: a console child started from a terminal inherits that terminal and never opens a second window. Because the defect is invisible in review and loud on the user's screen, the batch adds scripts/check-no-window-spawns.py — every Command::new reachable from a Windows build must either name a program that cannot allocate a console (a GUI-subsystem binary, or one Windows cannot resolve) or carry the flag within 16 lines. Controls, as any gate needs them: the pre-fix copilot.rs taken from HEAD fires exactly one finding at line 127, and the fixed tree answers CLEAN over 142 files. Both spawn shapes are cross-checked compiling for x86_64-pc-windows-gnu.

  • The hairline under a page's last row is gone, on all four pages — every page section carried its own border-bottom, and a divider's only claim is that it separates two blocks: under a page's last section there is nothing left to separate, so the 工具 page holding one tool painted a full-width line over a screenful of blank panel and it read as a rendering defect rather than as design (measured 2026-10-08 over the fixture at 400×1500: one stranded line per page — 概览 y=580, 工具 y=489, 排行 y=979, 明细 y=811 — each with 620–950 px of blank beneath it, and zero after the fix). The separator changed owner instead of getting a per-page exception: .sec + .sec { border-top } means a line exists exactly where two sections meet and nowhere else, so the stranded line is no longer expressible — .page > .sec:last-child { border-bottom: none } would have quieted today's four pages and any new page or reordered section would have brought the line back. The lines that do separate are untouched (概览 365 / 明细 587 / 排行 534 + 757 still measured). Section's divider switch, which existed only to hide this line, is gone; the quota section's waiting state now declares what it actually does — fill, take the list's footprint so the spinner sits in the middle of the rows the list would have had (.sec-nodiv → .sec-fill). scripts/check-section-dividers.py gates the shape: a section that owns a bottom edge fails, a divider nobody draws fails, the same rule hidden inside an @media block fails with its at-rule named, and .sec-hd / .sec-fill do not trip it; it runs in build-macos.sh beside the theme-parity gate, so even --fast pays for it.

  • Check-for-updates now only checks — the About tab's 立即检查 button was wired straight to the download-and-install flow (runUpdate): one click fetched the latest release, streamed the bundle, verified its sha256, swapped it in and relaunched the panel — with no version comparison anywhere on that path, so an install already running 0.1.5 against the 0.1.5 release downloaded and reinstalled itself. The button now calls check_update and nothing else; the answer lands in the footer (已是最新版本 vX / 发现新版本 vX), and 下载并安装 / 重启到新版本 stay behind the footer buttons that only a genuinely newer version raises. download_update grew the gate too, rather than trusting its caller: it re-fetches the release and answers 已是最新版本 instead of downloading when that version is not newer than the running one — a re-pointed tag, or a caller that skipped the check, would otherwise quit and restart the panel for a swap that changed nothing. The same pass fixed is_newer's missing precedence: the flat a[0]>b[0] || a[1]>b[1] || a[2]>b[2] let the patch digit alone decide, counting 0.1.6 as newer than 0.2.0; it is a componentwise tuple comparison now, with tests pinning 0.1.6 vs 0.2.0, 0.1.10 vs 0.2.0 and double-digit patches comparing numerically. A failed check also stopped answering an empty message: background paths (the boot check, the toggle-on check) filter by phase and stay silent as before, while a manual click shows 检查失败:无法连接更新源 instead of a button that visibly did nothing.

Full Changelog: https://github.com/Bencibr/tokenme/commits/v0.1.6