Releases: Amdeo/TokenMeter
Release list
TokenMeter 0.4.0
ad-hoc 签名的 universal 构建(arm64 + x86_64):没有 Developer ID,也没有公证,首次打开仍会被 Gatekeeper 拦截,步骤见 README 的「安装 → 已发布版本」。附 zip(旁有同名 .sha256 可校验)与 DMG 两种下载。
中文
应用内更新(Sparkle)
- 应用能自己更新了:后台按
SUScheduledCheckInterval(两小时)查一次新版本,找到后弹的是 Sparkle 自己的窗口,下载、验签、安装、重启一并做完。 - 手动检查有两个入口:菜单栏图标右键菜单的「检查更新…」,以及设置窗口「应用」页版本号右边的「检查更新…」按钮(在测试宿主等不能检查的进程里,按钮留着但按不动)。
- 更新源是
main上的appcast.xml,条目指向 GitHub Release 里的 zip;地址由Info.plist的SUFeedURL固定,信任由SUPublicEDKey固定。 - 更新包的完整性由 Sparkle 的 EdDSA 签名保证,与 Apple 账号无关:feed 里带签名,app 里带公钥,签名对不上的包一律不装。自动检查开着、
SUAutomaticallyUpdate关着——查到新版本仍由用户确认再装。
发布产物新增 DMG
- 每个版本除了 zip 与同名
.sha256,多一个TokenMeter-<version>-macos-universal.dmg:卷名是「TokenMeter 」,卷里只有 app 和指向/Applications的符号链接,把它拖过去就算装完。 - 由
scripts/dmg.sh <app 路径> [输出目录]打出(输出目录默认dist/),全程不碰 GUI:不挂载镜像、不跑 AppleScript、不经过 Finder,因此没有桌面会话的 CI runner 也能跑。可重复执行,失败会把 staging 目录与半个 DMG 一并清掉。 - 版本号读自 app 自己的
CFBundleShortVersionString,DMG 名字与卷名不会和 app 本体各说各话。
发版流水线:ad-hoc 签名与自动更新 appcast
- 构建完成后由内向外 ad-hoc 签名(
Updater.app/Downloader.xpc/Installer.xpc→Sparkle.framework→ app,codesign --sign -),签完用codesign --verify --deep --strict自检:Sparkle 的 XPC 服务在未签名的 bundle 里不会加载。 - 发布成功后多一步「Offer it to Sparkle」:切回
main,用scripts/appcast.py <version> <zip 路径> <下载 URL>给 zip 签名(私钥来自仓库 secretSPARKLE_PRIVATE_KEY)并把<item>插到appcast.xml最前,以github-actions[bot]提交Offer <version> to Sparkle推回main,最后回读远端确认——推不上去就报::error::让这次发布显性失败,而不是悄悄没人能更新。 - feed 条目带
sparkle:version/sparkle:shortVersionString/sparkle:minimumSystemVersion(14.0) /sparkle:edSignature/length,更新说明由 CHANGELOG 该版本一节转成 HTML 随 feed 下发。脚本只改appcast.xml、不做任何 git 操作,同一个版本重复跑不会插出两条。 - 校验和与签名并存:
.sha256供人手工核对下载,EdDSA 签名由 Sparkle 自动验。 - 版本号带
-的预发布在 GitHub 上会标成 pre-release,但 Sparkle 的 feed 不区分渠道——装了的人一样会收到。
签名、公证与首次打开
- app 与 DMG 都没有 Developer ID 签名、也没有公证,因为维护者不是苹果开发者:首次打开仍会被 Gatekeeper 拦下(「无法验证开发者」或「已损坏」),放行要用户自己动手——右键 app →「打开」,或到「系统设置 → 隐私与安全性」点「仍要打开」。zip 下载同样如此。
- 这是有意的取舍,不是打包漏了一步:本机 ad-hoc 签名只为了让 Sparkle 的 XPC 能被加载,更新包的完整性由 EdDSA 签名保证,与有没有 Apple 账号无关。
English
In-app updates (Sparkle)
- The app updates itself now: it checks for a new version in the background every two hours (
SUScheduledCheckInterval), and Sparkle's own window takes it from there — download, signature check, install and relaunch in one go. - Two ways to check by hand: 检查更新… in the menu-bar item's right-click menu, and 检查更新… next to the version on the settings window's 应用 page (kept visible but disabled in a process that cannot check, such as the test host).
- The feed is
appcast.xmlonmain, whose items point at the ZIP on the GitHub Release;SUFeedURLinInfo.plistfixes the address andSUPublicEDKeyfixes the trust. - An update's integrity rests on Sparkle's EdDSA signature and owes nothing to an Apple account: the feed carries the signature, the app carries the public half, and an archive that does not match is never installed. Automatic checks are on and
SUAutomaticallyUpdateis off, so installing a version that was found is still the user's call.
A DMG among the release assets
- Each release now ships
TokenMeter-<version>-macos-universal.dmgalongside the ZIP and its matching.sha256: the volume is named TokenMeter and holds the app plus a symlink to /Applications, so dragging it across is the whole install. - Built by
scripts/dmg.sh <app path> [output directory](the output directory defaults todist/), which stays clear of the GUI — no mounted image, no AppleScript, no Finder — so a CI runner with no desktop session is enough. Repeatable, and a failure takes its staging directory and the half-written DMG with it. - The file name and the volume name read the version from the app's own
CFBundleShortVersionString, so the artifact cannot disagree with the app it carries.
The release pipeline: ad-hoc signing and a self-updating appcast
- The app is now ad-hoc signed from the inside out once it is built (Updater.app / Downloader.xpc / Installer.xpc → Sparkle.framework → the app, with
codesign --sign -), and the result is checked withcodesign --verify --deep --strict: Sparkle's XPC services will not load out of an unsigned bundle. - A new step offers the ZIP to Sparkle once the release is published: it checks out
main, runsscripts/appcast.py <version> <zip path> <download URL>to sign the archive (private key from theSPARKLE_PRIVATE_KEYrepository secret) and insert the<item>at the top ofappcast.xml, commits that asgithub-actions[bot]under the subjectOffer <version> to Sparkle, pushes it tomain, and reads the feed back off the remote — a push that did not land fails the release loudly instead of quietly leaving every installed copy on the old version. - The item carries
sparkle:version,sparkle:shortVersionString,sparkle:minimumSystemVersion(14.0),sparkle:edSignatureandlength, with the release notes rendered from this version's CHANGELOG entry and delivered inside the feed. The script writesappcast.xmland nothing else — no git operations — and running it twice for one version adds no second item. - Checksums and signatures sit side by side on purpose: the
.sha256file is for a human checking a download, the EdDSA signature is what Sparkle verifies. - A version number carrying
-is marked pre-release on GitHub, but Sparkle's feed has no channels — installed copies are offered it just the same.
Signing, notarization and the first launch
- Neither the app nor the DMG carries a Developer ID signature, and neither is notarized, because the maintainer is not an Apple developer: Gatekeeper still stops the first launch of a download (「无法验证开发者」 or 「已损坏」), and letting it through is a manual step — right-click the app → 打开, or 系统设置 → 隐私与安全性 → 仍要打开. The ZIP download is the same.
- That is a deliberate trade rather than a missing packaging step: the local ad-hoc signature exists only so that Sparkle's XPC services load at all, and an update's integrity is guaranteed by the EdDSA signature, with no Apple account involved either way.
TokenMeter 0.3.0
未签名 universal 构建(arm64 + x86_64)。首次打开会被 Gatekeeper 拦截,步骤见 README 的「安装 → 已发布版本」;下载后用同名 .sha256 文件校验。
中文
悬浮条(TM-07)
- 新增屏幕边缘的用量悬浮条:静止时是一条细带,鼠标划过展开成一排环,悬停看详情卡片、点击刷新该订阅、拖动可贴到屏幕边缘或自由悬浮。默认关闭,一个环都没有时自动隐藏。
- 每条订阅自己带悬浮条设置(是否上条、环追踪哪个额度、环的颜色),与卡片样式、进度条配色存在同一层:删除订阅会一并带走,不会在
UserDefaults里留下孤儿键。「谁在条上」只在RailEntryBuilder一处判定。 - 显示设置分成「显示 / 位置 / 形状 / 环上画什么」四组:环间距与圆角端、倒数、数字位置、百分比、稍细的第二圈、窗口时钟弧、取数中的活动动画、收起细条的告警色。
- 悬浮条有自己的配色(深色 / 浅色 / 跟随主题,默认深色),与 app 主题互不影响;开着玻璃特效时不强钉外观。
- 环上的品牌标记用单色模板图,按用量状态染色:七个 Lobe Icons SVG,加 APIKEY.FUN 的单色 PNG 兜底;其余三个中转站的渐变 logo 在 18pt 下会糊成一团,仍用 SF Symbols。
- 悬浮条右键菜单独立成可测的
RailContextMenu,贴边时多一个「常显示」;条上的菜单只管条自己,不再有「打开 TokenMeter」。 - 详情卡片补上总使用量一行,金额改用紧凑写法(
Quota.compactText,与面板的 Siyu 卡片共用);卡片轮廓改成一条连续路径,根部不再有两道竖缝。 - 尺寸预算(
RailMetrics)与环上画什么(RailRingOptions)分成两层:窗口 frame、命中区与绘制读同一份,不再出现「窗口按 A 算、绘制按 B 画」。
独立设置窗口(TM-02 / TM-03 / TM-04 / TM-05)
- 设置从面板搬进独立窗口:左侧 source list 分「应用 / 订阅 / 其他」,右侧一次一页;「订阅」组只列用户真的添加过的订阅,按自己的顺序排列。
- 三个入口:面板头部齿轮、菜单栏图标右键菜单、悬浮条右键菜单。
- 窗口用
SettingsGroup+SettingsRow卡片脚手架搭出来,沿用面板的 TM 配色而不是系统色。 - 订阅编辑页新增「悬浮条」组(是否上条、环追踪哪个额度、环的颜色)与「显示顺序」组(箭头逐格调整,拖放一次跨多格)。
- 编辑页底栏钉在自己底部(只有这一页自己滚),主按钮只留「保存」,删除挪到另一头并保留二次确认;卡片里不放控件的裸文本补上统一内边距。
- Debug 的状态预览(TM-06)随设置一起搬走,改挂到菜单栏右键菜单。
面板收敛成单页(TM-01)
- 菜单面板只留概览一页,二级页、编辑草稿与按页高度记忆一并删掉;以前手动拖出的高度仍经
panel.overviewHeight生效。 - 「添加订阅」与点订阅卡片改为打开设置窗口的对应页;数据迁移成为窗口里的一页。
- 只为「第二个宿主」存在的旋钮(
heightRoute、showsBackButton、showsCancel、页面 chrome 常量、供应商选择页的返回按钮)随第二个宿主一起删掉。
卡片与外观
- 订阅卡片样式可选择(标准 / 紧凑),外观页带实时轮播预览;进度条配色与余额配色按样式分开保存,解码到未知样式时回退标准样式,旧订阅数据不丢。
- 外观页可配置余额颜色;颜色目标只要有内容就追加「默认颜色」兜底行,纯余额卡片也能设置。
- Kimi 月额度成为独立的额度行,带进度条与重置时间,状态判定也把它算进去;OpenCode Go 卡片显示重置时间;长按卡片显示额度重置倒计时(计时改由可取消的任务驱动,指针离开或位移超过 10pt 即作废)。
- 紧凑样式抽成共享的
CompactUsageCard(Kimi、OpenCode Go、DeepSeek 与各中转站的余额卡片共用),供应商只组装数据行;紧凑余额卡压成一行(图标 + 名称 + 金额)。 - 切换卡片样式不改变行高与面板高度;卡片头部去掉状态点,取值旁边冗余的「已用」字样也去掉了。
面板与菜单栏修复
- 菜单栏图标可设为「点击不弹面板」,普通左击改弹右键那个菜单,关掉时把已弹出的面板收掉;点图标的判定抽成
StatusItemClick。 - 面板窗口尺寸收敛到单一来源(
navigation.displayedSize),并加屏幕限高:高度不超过锚定屏幕visibleFrame,且限高只影响显示、不写回记忆高度。 - 宿主视图改成四边约束钉在容器上、清空
NSHostingView的sizingOptions,并在窗口 frame 变化后显式推动一次布局求解;修掉内容贴底、顶部露出空白条、首尾被裁(返回按钮看不到也点不到)、以及设置页脚注异步到达后高度错位。 - 面板支持在每一页拖拽调整大小;概览的订阅行卡片自己读快照,快照写入不再触发整块面板重建。
- 右击菜单加退出确认,退出按钮改成亮红;额度配色行默认折叠;菜单头部控件收紧,头部「添加订阅」按钮去掉常态背景。
发版流程与工具
- 版本号收敛到根目录
VERSION单一来源:scripts/version.py把它写进 Xcode 工程的 Debug 与 Release 两个配置(MARKETING_VERSION取版本号,CURRENT_PROJECT_VERSION取major*10000 + minor*100 + patch),--check给 CI 与发版门禁校验。 - 发版即推 tag:tag 形如
v0.3.0且必须等于v$(cat VERSION),发布提交标题为TokenMeter 0.3.0;tag 触发 release workflow,跑测试、出 universal 包、附 SHA-256 校验和并建 GitHub Release。发版流程与门禁见docs/releasing.md。 CHANGELOG.md条目改为中英双语,Release 说明直接取对应版本这一节。- 中转站开发 skill 增加本地取证工具:通过 Chrome CDP(9222 端口)探测中转站站点,并先用 HTML 预览卡片再写 Swift。
AGENTS.md更新页面索引(TM-02 / TM-06 的归属变化与新增的 TM-07)、四个文件系统同步组,以及悬浮条「谁在条上」只有一个判定处、悬浮条呈现分两层这两条容易踩错的约定。
English
Rail (TM-07)
- Added a screen-edge usage rail: a thin strip at rest that expands into one ring per subscription, with a detail card on hover, a refresh of that subscription on click, and drag-to-dock. Off by default, and hidden when nothing is left on it.
- Gave each subscription its own rail settings (whether it appears, which quota the ring tracks, the ring's colour), stored on the subscription next to its card style and quota colours, so deleting one takes its settings with it instead of leaving orphaned defaults keys; "who is on the rail" is decided in
RailEntryBuilderalone. - Grouped the rail's display settings into 显示 / 位置 / 形状 / 环上画什么: ring spacing and round ends, counting down, figure placement, percentages, a thinner second ring, a window-clock arc, a fetching animation, and the collapsed strip's alert colour.
- Let the rail pick its own colour scheme (dark / light / follow the theme, dark by default) independently of the app's theme, and stopped pinning the appearance while the glass material is on.
- Shipped monochrome template marks for the rings, tinted by usage state: seven Lobe Icons SVGs plus a monochrome PNG fallback for APIKEY.FUN; the three remaining relay providers keep their SF Symbols because their gradient logos turn to mush at the rail's 18pt mark size.
- Moved the rail's context menu into
RailContextMenuso it is testable, with a 常显示 toggle while docked; the rail's menu no longer offers 打开 TokenMeter. - Added the overall usage row to the detail card with compact amounts (
Quota.compactText, shared with the panel's Siyu card), and drew the card's outline as one continuous path so the pointer no longer leaves two vertical seams. - Split the rail's presentation into two layers — the size budget (
RailMetrics) and what each ring draws (RailRingOptions) — so the window frame, the hit areas and the drawing all read the same answer.
A standalone settings window (TM-02 / TM-03 / TM-04 / TM-05)
- Moved settings out of the panel into a standalone window: a source list on the left grouped into 应用 / 订阅 / 其他, one pane at a time on the right, with the 订阅 group listing only subscriptions the user has actually added, in their own order.
- Kept three ways in: the gear in the panel header, the menu-bar item's right-click menu, and the rail's context menu.
- Built the panes from
SettingsGroup+SettingsRow, keeping the app's TM colours rather than system ones. - Added a 悬浮条 group (show on the rail, which quota the ring tracks, the ring's colour) and a 显示顺序 group (arrow buttons for exact steps, drag-and-drop for long moves) to the subscription editor.
- Pinned the editor's footer (only that page scrolls), left 保存 as its only primary button, moved delete to the other end behind a second confirmation, and gave bare text inside the cards a shared inset.
- Moved the Debug status preview (TM-06) out of the panel's settings route and onto the menu-bar right-click menu.
The menu panel as a single overview page (TM-01)
- Reduced the menu panel to its overview page, dropping the routes, the editor draft and the per-route height memory; a height dragged out before still applies through
panel.overviewHeight. - Pointed 添加订阅 and the subscription cards at the matching settings-window pane, and made the migration flow a page of that window.
- Removed everything that existed only to serve a second host:
heightRoute,showsBackButton,showsCancel, the page-chrome constants and the provider picker's back button.
Cards and appearance
- Added selectable subscription card styles (standard / compact) with a live carousel preview, storing progress and balance colours per style and falling back to standard for unknown decoded values so existing subscriptions survive.
- Added an appearance page with configurable balance colours, appending a 默认颜色 fallback row whenever there is something to colour, so pure-balance cards can be themed too.
- Made Kimi's monthly quota a proper quota row with a progress bar and reset time (and counted it towards the status); showed OpenCode Go reset times on the card; revealed a quota reset countdown while long-pressing a card, tracked by a cancellable task that is abandoned once the pointer leaves or moves more than 10pt.
- Extracted the compact style into the shared
CompactUsageCard(used by Kimi, OpenCode Go and the balance cards of DeepSeek and every relay), with providers assembling only the data rows; the compact balance card is a single line — icon + name + amount. - Kept the row height and panel height unchanged across card styles, dropped the card header's status dot, and removed the redundant 已用 prefix from card values.
Panel and menu-bar fixes
- Added a menu-bar setting to skip the panel (a plain left click then opens the menu) and pulled the click rule into
StatusItemClick. - Gave the panel window a single size source (
navigation.displayedSize) plus a screen limit that never writes back to the remembered height. - Pinned the hosted content view to its container with four-edge constraints, cleared
NSHostingView.sizingOptions, and forced one layout solve after the window frame changes — fixing content stuck to the bottom, a blank strip at the top, clipped headers (a back button you could neither see nor click), and the settings footer's late arrival shifting the height. - Supported panel resizing on every page, and moved the snapshot read into the overview row card so a snapshot write no longer rebuilds the whole panel.
- Added a quit confirmation to the right-click menu with a bright-red quit button, collapsed the quota colour rows by default, and tightened the menu header controls.
Release process and tooling
- Made
VERSIONat the repository root the single source of the version:scripts/version.pywrites it into the project's Debug and Release configurations (MARKETING_VERSIONfrom the version,CURRENT_PROJECT_VERSIONasmajor*10000 + minor*100 + patch), and--checkgates both CI and the release workflow. - Made releasing a tag push: the tag (e.g.
v0.3.0) must equalv$(cat VERSION), the release commit is titledTokenMeter 0.3.0, and the tag runs the release workflow that tests, builds the universal archive, writes the SHA-256 checksum and publishes the GitHub Release. The process and its gates are documented indocs/releasing.md. - Turned
CHANGELOG.mdentries bilingual, with the Release notes taken from the matching version's entry. - Gave the add-relay-provider skill a local evidence path: probe relay sites through Chrome CDP (port 9222), and preview cards in HTML before writing Swift.
- Updated
AGENTS.md: the page index (TM-02 / TM-06 moved, TM-07 added), the four file-system synchronized groups, and the two conventions that are easy to get wrong later — that "which subscriptions are on the rail" is decided in one place, and that the rail's presentation is two layers.
TokenMeter 0.2.0
未签名 universal 构建(arm64 + x86_64)。首次打开会被 Gatekeeper 拦截,步骤见 README 的「安装 → 已发布版本」;下载后用同名 .sha256 文件校验。
Added
- Added Claude (OAuth authorization code, Pro/Max plan quota windows) and OpenAI Codex (device OAuth, Codex quota windows) providers.
- Added the Siyu API relay provider with balance and daily/weekly/monthly plan windows.
- Made providers a folder-based extension point: each provider owns its stable IDs, login site, authorization handlers, card renderer, and icon under
Providers/Extensions/<id>/, so adding one takes a new folder plus a single line inProviderCatalog; shipped a copy-ready template, the provider development guide, and an add-relay skill. - Published releases from a tag-driven GitHub Actions workflow (
Release) that runs the test suite, builds the universal unsigned archive, writes the SHA-256 checksum, and attaches both to the GitHub Release; signed/notarized packages are still built locally.
Changed
- Merged the relay providers into one site-driven implementation; the shared machinery now lives in
Providers/Common/instead of the app-levelServices/layer. - Restored subscription drag-to-reorder with animated drop placement, and balanced the overview header icon spacing and card insets.
- Release archives are now named
TokenMeter-<version>-macos-universal.zipand published with a matching.sha256file. - Moved add-subscription, settings, and quit into a right-click menu on the menu-bar icon; the overview's bottom action row is gone and the synchronization status now sits next to the service count in the header.
- Showed Siyu API plans as daily, weekly, and monthly quota windows with reset hints instead of a single monthly row, and listed Siyu in the README provider tables.
- Added an opt-in frosted-glass panel background for light appearance; the light panel is plain white by default.
- Requested notification authorization once at launch instead of prompting in the overview panel, and only when alerts are enabled and the system has not decided yet; the settings panel still reports status and refusal reasons.
- Clarified installation paths, including the published unsigned v0.1.0 archive and the distinction between that release and
main. - Corrected the public clone URL and documented the Xcode 26+ source-build requirement.
- Documented the current notification scope, Chinese-only UI, local persistence, plaintext credential storage, persistent WebKit site data, and public issue workflow.
- Added security-reporting guidance, contribution guidance, and public issue/pull-request templates that prohibit sharing credentials.
- Added pinned CI dependencies, verified Gitleaks scanning, universal release packaging, and an original application icon.
- Clarified used-quota percentages, actual synchronization state, notification permission, and local plaintext storage in the UI; added app version and issue links.
Fixed
- Grouped Siyu API plan windows by an explicit key instead of the displayed plan name, so two plans with the same name (or no name) stay separate sections, and omitted windows the API does not cap instead of rendering them as exhausted
$0.00 / $0.00rows. - Surfaced the system's reason when notification authorization is refused (unsigned builds), instead of leaving the allow-notifications button apparently inert.
- Prevented the test host from initializing production stores, windows, or background refreshes.
- Derived OAuth form state from authorization data rather than status-message wording and cleared stale state when switching methods.
- Prevented cancelled refresh completions from overwriting newer state or clearing a newer refresh task.
- Preserved corrupt subscription metadata, blocked configuration mutations until recovery, and exposed persistence errors with a reload action.
- Refused to repackage an existing release version and printed the source revision at package time, so unreleased
maincontent cannot be shipped under a released version number. - Corrected stale
PONYTAIL-DEBT.mdline references after the editor and store refactors. - Documented the concrete first-launch steps for the unsigned build, including quarantine removal after checksum verification.
Full Changelog: v0.1.0...v0.2.0
TokenMeter 0.1.0
TokenMeter 0.1.0
macOS 菜单栏 AI 用量监控工具的首个公开版本。
功能特性
- 多供应商支持:OpenAI、Kimi、CCBus、APIKeyFun、NowCoding,以及通用的 API 中转站
- 菜单栏概览:一键查看所有订阅的用量、余额与刷新状态
- 浏览器凭证导入:从 Chrome 会话导入登录凭证,自动刷新过期会话(Kimi / CCBus / APIKeyFun)
- Kimi OAuth 登录:内嵌浏览器登录与多账号切换
- 订阅管理:添加、编辑、删除订阅,支持设置用量/余额阈值提醒
- 通知提醒:余额不足、认证失败等状态变化即时通知
- 数据迁移:旧版凭证数据自动迁移到新存储格式
安装说明
- 下载
TokenMeter-0.1.0-macOS.zip并解压 - 将
TokenMeter.app拖入「应用程序」文件夹 - 首次打开时在 Finder 中右键 App →「打开」(未签名构建,macOS 会拦截 Gatekeeper)
应用为未签名构建,凭据仅保存在本机
Application Support/TokenMeter/credentials.json。
验证
- Release 构建通过
- 159 个测试全部通过(13 个套件)