Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ jobs:
run: ./script/test_claude_incremental_collector.sh
- name: Run settings Codable checks
run: ./script/test_settings_codable.sh
- name: Run compact popover checks
run: ./script/test_compact_popover.sh
- name: Build TokenStep
run: ./script/build_swiftui_and_run.sh --no-launch
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

TokenStep 各版本的更新内容,新版本在前。每个版本的完整说明在 [docs/RELEASE_NOTES_<版本号>.md](docs/)。

## 0.2.16 更新:紧凑面板,额度刷新更及时

菜单栏面板改为一列 340pt,只放额度、今日 Token、今日排名,13 寸屏幕上不再被截掉;刘海旁的 Token Island 打开同一个面板。额度在面板打开时最多每 5 分钟刷新,刷新按钮会一并刷新额度;只用 Claude Code CLI 时也显示 Claude 图标。详见 [0.2.16 发布说明](docs/RELEASE_NOTES_0.2.16.md)。

## 0.2.15 更新:按本地时区统计,采集更稳更省电

每天的用量改为按 Mac 的系统时区切分,更换时区后自动重新统计;修复 Codex 从本地数据库回退统计时可能卡住的问题;Claude Code 长会话只读取新增内容,读取本地数据库不再启动额外进程;设置中填写的密钥不再出现在进程列表里;隐私页列出每一个联网请求。详见 [0.2.15 发布说明](docs/RELEASE_NOTES_0.2.15.md)。
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ TokenStep 是一个 macOS 菜单栏 App,用来本地统计你在 Codex、Claud

下载最新版 DMG,打开后把 `TokenStep.app` 拖进「应用程序」即可使用:

[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.15.dmg)
[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.16.dmg)

也可以从 Release 页面查看所有版本:

Expand All @@ -32,9 +32,9 @@ TokenStep 已使用 Developer ID 签名并通过 Apple 公证。首次打开时

Windows版本由十七做了移植,欢迎大家前往使用:https://github.com/canyexuanfan/TokenStep-Windows/releases

## 最新版本:0.2.15
## 最新版本:0.2.16

按 Mac 的系统时区统计每天的用量,修复 Codex 回退统计时可能卡住的问题,后台采集更省电,隐私页列出每一个联网请求。详见 [0.2.15 发布说明](docs/RELEASE_NOTES_0.2.15.md)。
菜单栏面板改为一列紧凑面板,13 寸屏幕上不再被截掉,刘海旁的 Token Island 打开同一个面板;额度在面板打开时最多每 5 分钟刷新,刷新按钮会一并刷新额度。详见 [0.2.16 发布说明](docs/RELEASE_NOTES_0.2.16.md)。

历次更新(主题包、引力动效、自动更新等)见 [CHANGELOG.md](CHANGELOG.md)。

Expand Down Expand Up @@ -95,7 +95,7 @@ TokenStep 默认只做本地统计。

## 安装方式

1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.15.dmg)。
1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.16.dmg)。
2. 打开 DMG。
3. 把 `TokenStep.app` 拖到「应用程序」。
4. 启动 TokenStep。
Expand Down Expand Up @@ -166,7 +166,7 @@ TokenStepSwift/dist/TokenStep.app
公开发布强制执行 Developer ID 签名、Apple 公证、票据装订、系统分发检查和隔离安装验证。不再生成可被误上传的“仅签名、未公证”发布包:

```bash
TOKENSTEP_VERSION=0.2.15 \
TOKENSTEP_VERSION=0.2.16 \
CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \
TOKENSTEP_NOTARY_PROFILE="tokenstep-notary" \
./script/package_release.sh --notarize
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ struct TokenStepApp: App {
.commands {
CommandMenu("TokenStep") {
Button(L("刷新")) {
appState.refresh()
appState.refreshNow()
}
.keyboardShortcut("r", modifiers: [.command])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import Foundation

enum ClaudeQuotaService {
private static let keychainServices = ["Claude Code-credentials"]
private static let cacheTTL: TimeInterval = 10 * 60
// Shorter than EnergyRefreshPolicy.quotaTTL so the file cache only spares
// repeat calls across relaunches and never adds to the app's own interval.
private static let cacheTTL: TimeInterval = 4 * 60
private static let endpoint = URL(string: "https://api.anthropic.com/api/oauth/usage")!

static func read() throws -> CodexQuotaSnapshot {
if let cached = readFreshCache() {
static func read(useCache: Bool = true) throws -> CodexQuotaSnapshot {
if useCache, let cached = readFreshCache() {
return cached
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ enum CursorQuotaService {
static var databaseURL: URL = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Application Support/Cursor/User/globalStorage/state.vscdb")

static func read() throws -> ProviderQuota {
if let cached = readFreshCache() {
static func read(useCache: Bool = true) throws -> ProviderQuota {
if useCache, let cached = readFreshCache() {
return cached
}
let token = try readAccessToken()
Expand Down Expand Up @@ -221,7 +221,8 @@ enum CursorQuotaService {
private static func readFreshCache(now: Date = Date()) -> ProviderQuota? {
guard let data = try? Data(contentsOf: AppPaths.cursorQuotaCacheJSON),
let cache = try? JSONDecoder().decode(ProviderQuotaCache.self, from: data),
now.timeIntervalSince(cache.fetchedAt) <= 10 * 60
// Shorter than EnergyRefreshPolicy.quotaTTL; see ClaudeQuotaService.
now.timeIntervalSince(cache.fetchedAt) <= 4 * 60
else { return nil }
let quota = cache.quota
guard quota.isAvailable else { return nil }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import Foundation

enum QuotaRefreshCoordinator {
static func fetch(providers: Set<QuotaProviderID>) -> [QuotaProviderID: ProviderQuota] {
/// `bypassCache` skips provider file caches, for a refresh the user asked for.
static func fetch(providers: Set<QuotaProviderID>, bypassCache: Bool = false) -> [QuotaProviderID: ProviderQuota] {
var result: [QuotaProviderID: ProviderQuota] = [:]
let lock = NSLock()
let group = DispatchGroup()
for provider in providers {
group.enter()
DispatchQueue.global(qos: .utility).async {
let quota = read(provider)
let quota = read(provider, bypassCache: bypassCache)
lock.lock()
result[provider] = quota
lock.unlock()
Expand All @@ -23,15 +24,15 @@ enum QuotaRefreshCoordinator {
return result
}

static func read(_ provider: QuotaProviderID) -> ProviderQuota {
static func read(_ provider: QuotaProviderID, bypassCache: Bool = false) -> ProviderQuota {
do {
switch provider {
case .codex:
return try CodexQuotaService.read().asProviderQuota(.codex)
case .claude:
return try ClaudeQuotaService.read().asProviderQuota(.claude)
return try ClaudeQuotaService.read(useCache: !bypassCache).asProviderQuota(.claude)
case .cursor:
return try CursorQuotaService.read()
return try CursorQuotaService.read(useCache: !bypassCache)
case .glm:
return try GLMQuotaService.read()
case .kimi:
Expand Down
17 changes: 15 additions & 2 deletions TokenStepSwift/Sources/TokenStepSwift/Stores/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ final class AppState: ObservableObject {
autostartEnabled = AutostartService.isEnabled
}

/// The refresh button and ⌘R: usage plus quotas, skipping quota caches.
func refreshNow() {
refresh()
refreshCodexQuota(force: true, bypassCache: true)
}

func refresh(forceCollection: Bool = true) {
guard !isRefreshing else {
if forceCollection {
Expand Down Expand Up @@ -280,7 +286,7 @@ final class AppState: ObservableObject {
configureForegroundTimer()
}

func refreshCodexQuota(force: Bool = false, now: Date = Date()) {
func refreshCodexQuota(force: Bool = false, bypassCache: Bool = false, now: Date = Date()) {
refreshCursorOfficialUsage(force: force, now: now)
let providers = settings.enabledQuotaProviders
guard !providers.isEmpty else {
Expand All @@ -301,10 +307,17 @@ final class AppState: ObservableObject {
isRefreshingCodexQuota = true
Task {
let fetched = await Task.detached(priority: .utility) {
QuotaRefreshCoordinator.fetch(providers: providers)
QuotaRefreshCoordinator.fetch(providers: providers, bypassCache: bypassCache)
}.value
for provider in providers {
if let quota = fetched[provider] {
// A transient failure (rate limit, network) keeps the last
// good reading; its "N minutes ago" shows how old it is.
// Login and key problems still replace it.
if !quota.isAvailable, quota.status == .unavailable,
quotas[provider]?.isAvailable == true {
continue
}
quotas[provider] = quota
} else if quotas[provider]?.isAvailable != true {
quotas[provider] = .unavailable(provider)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ enum TokenStepPowerSource: Equatable {
enum EnergyRefreshPolicy {
static let acBackgroundFloorSeconds = 15 * 60
static let batteryBackgroundFloorSeconds = 30 * 60
static let quotaTTL: TimeInterval = 15 * 60
/// Quotas move fast during heavy use, and the calls are small, so an open
/// popover checks them every 5 minutes. Background refresh still follows
/// the 15/30-minute floors above.
static let quotaTTL: TimeInterval = 5 * 60
static let rankTTL: TimeInterval = 30 * 60
static let minimumAutomaticRetryTTL: TimeInterval = 60
static let maximumForegroundTickSeconds = 60
Expand Down
70 changes: 70 additions & 0 deletions TokenStepSwift/Sources/TokenStepSwift/Support/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,41 @@ enum TokenStepLocalization {
"历史范围": "History range",
"%d 天": "%d days",
"联网项(除检查更新外默认关闭)": "Network items (off by default except update checks)",
"%@(你)": "%@ (you)",
"%d 分钟后": "in %d min",
"%d 小时 %d 分": "%d h %d min",
"/ %d 人": "/ %d people",
"你是今日第一": "You are #1 today",
"再跑": "Run",
"分享": "Share",
"另有 %d 项正常:%@": "%d more, all normal: %@",
"差 %@": "%@ ahead",
"已用": "Used",
"已用 · %@": "Used · %@",
"引力动效": "Gravity motion",
"按最近 1 小时的速度(约 %@/时),约 %@": "At the last hour's pace (about %@/h), about %@",
"收起": "Collapse",
"明天 %@": "Tomorrow %@",
"更多": "More",
"更多:分享、截图、退出": "More: share, screenshot, quit",
"最近 1 小时没有用量": "No usage in the last hour",
"未进入前 %d 名": "Not in the top %d",
"未上榜": "Not ranked",
"今日还没有你的上传": "No uploads from you today yet",
"本地比榜单多 %@,同步后预计升到 #%d": "%@ more locally than on the board; about #%d after sync",
"本地比榜单多 %@,等待同步": "%@ more locally than on the board, waiting for sync",
"榜上 %@": "On board %@",
"榜单 %@": "Board %@",
"照此速度会提前用完": "Runs out early at this pace",
"已用 %@": "%@ used",
"竖线是时间进度:已过 %@,已用 %@": "The tick marks time passed: %@ of the window gone, %@ used",
"。用得比时间快,照此速度会在重置前用完": ". Usage is ahead of time and runs out before the reset at this pace",
"第 %d 名今日 %@": "#%d today: %@",
"触发坠落脉冲": "Trigger plunge pulse",
"超过 #%d %@": "to pass #%d %@",
"进入前 %d 名": "to enter the top %d",
"退出 TokenStep": "Quit TokenStep",
"领先第二名 %@": "%@ ahead of #2",
"近似 · %d 会话": "Approx. · %d sessions",
"近似值(按会话开始日汇总)": "Approximate (totals by session start day)",
"版本": "Ver",
Expand Down Expand Up @@ -1150,6 +1185,41 @@ enum TokenStepLocalization {
"不开代理 · 不按字数估算 token": "不開代理 · 不按字數估算 token",
"不上传用量与内容。Token 统计全部来自本机日志。": "不上傳用量與內容。Token 統計全部來自本機日誌。",
"联网项(除检查更新外默认关闭)": "連網項(除檢查更新外預設關閉)",
"%@(你)": "%@(你)",
"%d 分钟后": "%d 分鐘後",
"%d 小时 %d 分": "%d 小時 %d 分",
"/ %d 人": "/ %d 人",
"你是今日第一": "你是今日第一",
"再跑": "再跑",
"分享": "分享",
"另有 %d 项正常:%@": "另有 %d 項正常:%@",
"差 %@": "差 %@",
"已用": "已用",
"已用 · %@": "已用 · %@",
"引力动效": "引力動效",
"按最近 1 小时的速度(约 %@/时),约 %@": "按最近 1 小時的速度(約 %@/時),約 %@",
"收起": "收起",
"明天 %@": "明天 %@",
"更多": "更多",
"更多:分享、截图、退出": "更多:分享、截圖、退出",
"最近 1 小时没有用量": "最近 1 小時沒有用量",
"未进入前 %d 名": "未進入前 %d 名",
"未上榜": "未上榜",
"今日还没有你的上传": "今日還沒有你的上傳",
"本地比榜单多 %@,同步后预计升到 #%d": "本機比榜單多 %@,同步後預計升到 #%d",
"本地比榜单多 %@,等待同步": "本機比榜單多 %@,等待同步",
"榜上 %@": "榜上 %@",
"榜单 %@": "榜單 %@",
"照此速度会提前用完": "照此速度會提前用完",
"已用 %@": "已用 %@",
"竖线是时间进度:已过 %@,已用 %@": "豎線是時間進度:已過 %@,已用 %@",
"。用得比时间快,照此速度会在重置前用完": "。用得比時間快,照此速度會在重置前用完",
"第 %d 名今日 %@": "第 %d 名今日 %@",
"触发坠落脉冲": "觸發墜落脈衝",
"超过 #%d %@": "超過 #%d %@",
"进入前 %d 名": "進入前 %d 名",
"退出 TokenStep": "結束 TokenStep",
"领先第二名 %@": "領先第二名 %@",
"近似 · %d 会话": "近似 · %d 會話",
"近似值(按会话开始日汇总)": "近似值(按會話開始日彙總)",
"版本": "版本",
Expand Down
Loading
Loading