diff --git a/README.md b/README.md index 11defc9..3864ead 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ TokenStep 是一个 macOS 菜单栏 App,用来本地统计你在 Codex、Claud 下载最新版 DMG,打开后把 `TokenStep.app` 拖进「应用程序」即可使用: -[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.13.dmg) +[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.14.dmg) 也可以从 Release 页面查看所有版本: @@ -32,6 +32,10 @@ TokenStep 已使用 Developer ID 签名并通过 Apple 公证。首次打开时 Windows版本由十七做了移植,欢迎大家前往使用:https://github.com/canyexuanfan/TokenStep-Windows/releases +## 0.2.14 更新:恢复 AgentWork 榜单关联 + +兼容 Token Rank 新版状态文件中的账号信息,同时保留旧版格式支持。已绑定的用户升级后可重新识别身份,无需删除状态文件或重新绑定。详见 [0.2.14 发布说明](docs/RELEASE_NOTES_0.2.14.md)。 + ## 0.2.13 更新:修复自动更新的 Apple 公证门禁 0.2.13 修复了 0.2.12 因发布包遗漏 Apple 公证票据而无法自动安装的问题。新版已经过 Developer ID 签名、Apple 公证、App/DMG 票据装订、macOS 分发策略检查和隔离更新安装验证;不需要关闭 Gatekeeper 或更改系统安全设置。发布流程也改为“先草稿上传、回下载验收,再公开”,未公证产物无法进入公开 Release。完整说明见 [0.2.13 发布说明](docs/RELEASE_NOTES_0.2.13.md)。 @@ -150,7 +154,7 @@ TokenStep 第一次从“更换配色”升级为完整的**主题皮肤包系 - 新增奥德赛弓箭阶梯 Logo;用量采用骨金、冷金或余烬橙,绿色只保留给同步成功等状态反馈。 - 关闭排行榜后浮层会自动收短,多来源额度则使用紧凑布局完整展示。 -打开 `设置 → 通用 → 主题皮肤包` 即可切换。完整说明见 [0.2.4 发布说明](docs/RELEASE_NOTES_0.2.4.md),或直接[下载已签名并通过 Apple 公证的最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.13.dmg)。 +打开 `设置 → 通用 → 主题皮肤包` 即可切换。完整说明见 [0.2.4 发布说明](docs/RELEASE_NOTES_0.2.4.md),或直接[下载已签名并通过 Apple 公证的最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.14.dmg)。 ## TokenStep 适合谁? @@ -209,7 +213,7 @@ TokenStep 默认只做本地统计。 ## 安装方式 -1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.13.dmg)。 +1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.14.dmg)。 2. 打开 DMG。 3. 把 `TokenStep.app` 拖到「应用程序」。 4. 启动 TokenStep。 @@ -269,7 +273,7 @@ TokenStepSwift/dist/TokenStep.app 公开发布强制执行 Developer ID 签名、Apple 公证、票据装订、系统分发检查和隔离安装验证。不再生成可被误上传的“仅签名、未公证”发布包: ```bash -TOKENSTEP_VERSION=0.2.13 \ +TOKENSTEP_VERSION=0.2.14 \ CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \ TOKENSTEP_NOTARY_PROFILE="tokenstep-notary" \ ./script/package_release.sh --notarize diff --git a/TokenStepSwift/Sources/TokenStepSwift/Services/TokenRankService.swift b/TokenStepSwift/Sources/TokenStepSwift/Services/TokenRankService.swift index b282f99..0f83ea8 100644 --- a/TokenStepSwift/Sources/TokenStepSwift/Services/TokenRankService.swift +++ b/TokenStepSwift/Sources/TokenStepSwift/Services/TokenRankService.swift @@ -65,7 +65,7 @@ enum AgentWorkRankService { .appendingPathComponent(".token-rank/client-state.json") ) -> AgentWorkRankIdentity? { guard let data = try? Data(contentsOf: clientStateURL), - let state = try? JSONDecoder().decode(LocalClientState.self, from: data), + let state = try? JSONDecoder().decode(LocalClientStateDocument.self, from: data).state, let user = state.user, user.id > 0 else { @@ -91,6 +91,34 @@ enum AgentWorkRankService { } } +// Read a public identity projection only. Credential and checksum validation +// remain the uploader's responsibility; this reader never writes its state. +private struct LocalClientStateDocument: Decodable { + let state: LocalClientState + + enum CodingKeys: String, CodingKey { + case schemaVersion = "schema_version" + case payload + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if container.contains(.schemaVersion) || container.contains(.payload) { + let version = try container.decode(Int.self, forKey: .schemaVersion) + guard version == 2 || version == 3 else { + throw DecodingError.dataCorruptedError( + forKey: .schemaVersion, + in: container, + debugDescription: "Unsupported Token Rank state format" + ) + } + state = try container.decode(LocalClientState.self, forKey: .payload) + } else { + state = try LocalClientState(from: decoder) + } + } +} + private struct LocalClientState: Decodable { var user: LocalClientUser? var lastSuccessfulSyncAt: String? diff --git a/TokenStepSwift/Tests/TokenStepSwiftTests/AgentWorkRankServiceTests.swift b/TokenStepSwift/Tests/TokenStepSwiftTests/AgentWorkRankServiceTests.swift index 7063eae..6eb9d08 100644 --- a/TokenStepSwift/Tests/TokenStepSwiftTests/AgentWorkRankServiceTests.swift +++ b/TokenStepSwift/Tests/TokenStepSwiftTests/AgentWorkRankServiceTests.swift @@ -77,6 +77,53 @@ final class AgentWorkRankServiceTests: XCTestCase { XCTAssertNotNil(identity.lastSyncedAt) } + private func identity(from json: String) throws -> AgentWorkRankIdentity? { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("rank-state-\(UUID().uuidString).json") + defer { try? FileManager.default.removeItem(at: url) } + try Data(json.utf8).write(to: url) + return AgentWorkRankService.loadLocalIdentity(clientStateURL: url) + } + + func testVersionedStatesReadPayloadIdentityAndSyncTime() throws { + for version in [2, 3] { + let result = try XCTUnwrap(identity(from: """ + { + "schema_version": \(version), "state_revision": 7, + "checksum": "ignored-by-public-identity-reader", + "user": {"id": 99, "name": "Stale root identity"}, + "payload": { + "device_token": "never-decoded", + "last_successful_sync_at": "2026-09-05T08:00:00Z", + "user": {"id": 4, "name": "Agent User", "avatar_url": "https://example.com/a.png"} + } + } + """)) + XCTAssertEqual(result.id, 4) + XCTAssertEqual(result.name, "Agent User") + XCTAssertEqual(result.avatarURL, "https://example.com/a.png") + XCTAssertEqual(result.lastSyncedAt, ISO8601DateFormatter().date(from: "2026-09-05T08:00:00Z")) + } + } + + func testInvalidEnvelopesNeverFallBackToRootIdentity() throws { + let rootUser = "\"user\":{\"id\":99,\"name\":\"Stale\"}" + for fields in [ + "\"schema_version\":4,\"payload\":{}", + "\"schema_version\":3", + "\"schema_version\":3,\"payload\":null", + "\"schema_version\":3,\"payload\":[]", + "\"schema_version\":3,\"payload\":{}", + "\"schema_version\":null,\"payload\":{}", + "\"payload\":{}", + "\"schema_version\":3,\"payload\":{\"user\":{\"id\":0,\"name\":\"Invalid\"}}" + ] { + XCTAssertNil(try identity(from: "{\(rootUser),\(fields)}"), fields) + } + XCTAssertNil(try identity(from: "not json")) + XCTAssertNil(try identity(from: "{}")) + } + func testLegacyShengcaiSettingsMigrateToAutomaticAgentWorkRank() throws { let data = Data(""" { diff --git a/docs/RELEASE_NOTES_0.2.14.md b/docs/RELEASE_NOTES_0.2.14.md new file mode 100644 index 0000000..2d20a42 --- /dev/null +++ b/docs/RELEASE_NOTES_0.2.14.md @@ -0,0 +1,9 @@ +# TokenStep 0.2.14 + +修复升级 Token Rank 后,TokenStep 显示“尚未关联”或自动隐藏 AgentWork 榜单的问题。 + +- 同时兼容旧版平面状态与 v2/v3 封装状态,正确读取账号及同步时间。 +- 遇到无效或未知版本的封装,不回退使用顶层残留账号。 +- 保留榜单三种显示模式;不修改采集器状态,不读取凭据用于联网请求。 + +已绑定用户无需重新绑定,安装新版后刷新即可。此更新修复身份识别,不改变采集计量与上传逻辑。 diff --git a/script/build_swiftui_and_run.sh b/script/build_swiftui_and_run.sh index dfbccf2..f020ca0 100755 --- a/script/build_swiftui_and_run.sh +++ b/script/build_swiftui_and_run.sh @@ -23,7 +23,7 @@ HELPER_EXECUTABLE="$BUILD_DIR/$HELPER_NAME" ICON_FILE="$ROOT_DIR/TokenUsageMenuApp/assets/TokenStepIcon.icns" ODYSSEY_ASSET_DIR="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey" INTERSTELLAR_ASSET_DIR="$ROOT_DIR/TokenUsageMenuApp/assets/interstellar" -VERSION="${TOKENSTEP_VERSION:-0.2.13}" +VERSION="${TOKENSTEP_VERSION:-0.2.14}" LAUNCH=true VERIFY=false