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
19 changes: 14 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,17 @@ jobs:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
run: |
gh release create "v$VERSION" \
"release/TokenStep-$VERSION.dmg" \
"release/TokenStep-$VERSION.zip" \
--title "TokenStep $VERSION" \
--notes "TokenStep $VERSION"
NOTES_FILE="docs/RELEASE_NOTES_$VERSION.md"
if [[ -f "$NOTES_FILE" ]]; then
gh release create "v$VERSION" \
"release/TokenStep-$VERSION.dmg" \
"release/TokenStep-$VERSION.zip" \
--title "TokenStep $VERSION" \
--notes-file "$NOTES_FILE"
else
gh release create "v$VERSION" \
"release/TokenStep-$VERSION.dmg" \
"release/TokenStep-$VERSION.zip" \
--title "TokenStep $VERSION" \
--notes "TokenStep $VERSION"
fi
24 changes: 18 additions & 6 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.5.dmg)
[下载 TokenStep 最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.6.dmg)

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

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

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

## 0.2.6 重大更新:检查更新后立即进入安装闭环

0.2.6 修复了从浮层主动检查更新时“查到了,但没有继续弹出更新窗口”的断点,并把下载、验证、替换与自动重启做成可追踪的完整链路。

- **发现新版立即弹窗**:点击浮层或主窗口的更新按钮后,若检测到新版,会立刻打开独立更新窗口。
- **更新窗口确保可见**:菜单栏瞬时浮层自动收起,更新窗口切到当前桌面并保持在前台,不再被浮层遮住。
- **一次点击完成升级**:点击“安装并重启”后自动下载 DMG、校验签名、公证与版本,备份旧 App、替换 `/Applications/TokenStep.app` 并重新打开。
- **全过程可诊断**:检查、弹窗、安装启动和失败都会写入生命周期日志;新增隔离安装验证脚本,避免把“能下载”等同于“能升级”。
- **不改变自动检查策略**:后台检查仍采用非打扰提醒;只有用户主动点击检查并发现新版时才强制弹出窗口。

本次不改变 Token、金额、额度、排行榜及本地采集口径。完整说明见 [0.2.6 发布说明](docs/RELEASE_NOTES_0.2.6.md)。

## 0.2.5 重大更新:动态特洛伊火焰、模型用量与更新提醒

0.2.5 让奥德赛主题从静态电影画面进入动态状态,同时把菜单栏浮层和更新体验补得更完整。
Expand Down Expand Up @@ -73,7 +85,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.5.dmg)。
打开 `设置 → 通用 → 主题皮肤包` 即可切换。完整说明见 [0.2.4 发布说明](docs/RELEASE_NOTES_0.2.4.md),或直接[下载已签名并通过 Apple 公证的最新版](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.6.dmg)。

## TokenStep 适合谁?

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

## 安装方式

1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.5.dmg)。
1. 下载 [TokenStep 最新版 DMG](https://github.com/Backtthefuture/TokenStep/releases/latest/download/TokenStep-0.2.6.dmg)。
2. 打开 DMG。
3. 把 `TokenStep.app` 拖到「应用程序」。
4. 启动 TokenStep。
Expand Down Expand Up @@ -192,15 +204,15 @@ TokenStepSwift/dist/TokenStep.app
Developer ID 签名:

```bash
TOKENSTEP_VERSION=0.2.5 \
TOKENSTEP_VERSION=0.2.6 \
CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \
./script/package_release.sh
```

签名 + Apple 公证:

```bash
TOKENSTEP_VERSION=0.2.5 \
TOKENSTEP_VERSION=0.2.6 \
CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" \
TOKENSTEP_NOTARY_PROFILE="tokenstep-notary" \
./script/package_release.sh --notarize
Expand All @@ -215,7 +227,7 @@ release/TokenStep-<version>.dmg

维护者说明见 [docs/RELEASE.md](docs/RELEASE.md)。

0.2.5 发布说明见 [docs/RELEASE_NOTES_0.2.5.md](docs/RELEASE_NOTES_0.2.5.md);0.2.4 主题包实现与验收说明见 [docs/ODYSSEY_THEME_PACK_0.2.4.md](docs/ODYSSEY_THEME_PACK_0.2.4.md)。
0.2.6 发布说明见 [docs/RELEASE_NOTES_0.2.6.md](docs/RELEASE_NOTES_0.2.6.md);0.2.5 动效、模型用量与更新提醒说明见 [docs/RELEASE_NOTES_0.2.5.md](docs/RELEASE_NOTES_0.2.5.md);0.2.4 主题包实现与验收说明见 [docs/ODYSSEY_THEME_PACK_0.2.4.md](docs/ODYSSEY_THEME_PACK_0.2.4.md)。

## 开源协议

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ enum UpdateCheckPolicy {
}
}

enum UpdatePresentationPolicy {
static func shouldPresentWindow(
trigger: UpdateCheckTrigger,
update: AvailableUpdate?
) -> Bool {
trigger.isManual && update != nil
}
}

struct UpdateCheckGate {
private(set) var isChecking = false
private(set) var lastAttemptAt: Date?
Expand Down
17 changes: 16 additions & 1 deletion TokenStepSwift/Sources/TokenStepSwift/Stores/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,13 @@ final class AppState: ObservableObject {
LifecycleLogger.log(
"Update check success trigger=\(trigger.rawValue) result=available version=\(visibleUpdate.version)."
)
if trigger.isManual {
if UpdatePresentationPolicy.shouldPresentWindow(
trigger: trigger,
update: visibleUpdate
) {
LifecycleLogger.log(
"Presenting update window trigger=\(trigger.rawValue) version=\(visibleUpdate.version)."
)
UpdateWindowPresenter.shared.show(appState: self, update: visibleUpdate)
}
} else {
Expand Down Expand Up @@ -859,6 +865,9 @@ final class AppState: ObservableObject {

func installAvailableUpdate() {
guard let update = availableUpdate, !isDownloadingUpdate else { return }
LifecycleLogger.log(
"Update install requested version=\(update.version) asset=\(update.assetName)."
)
isDownloadingUpdate = true
updateDownloadProgress = 0
updateInstallStatus = L("正在下载")
Expand All @@ -875,10 +884,16 @@ final class AppState: ObservableObject {
updateDownloadedURL = url
updateDownloadProgress = 1
updateInstallStatus = L("正在安装并重启")
LifecycleLogger.log(
"Update installer launched version=\(update.version) dmg=\(url.path)."
)
} catch {
lastError = error.localizedDescription
updateInstallStatus = L("更新失败")
isDownloadingUpdate = false
LifecycleLogger.log(
"Update install failed version=\(update.version) error=\(error.localizedDescription)."
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ final class UpdateWindowPresenter {
private var window: NSWindow?

func show(appState: AppState, update: AvailableUpdate) {
window?.close()
let window = makeWindow(appState: appState, update: update)
self.window = window

NSApp.activate(ignoringOtherApps: true)
closeTransientPanels(except: window)
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
}
Expand All @@ -34,9 +36,18 @@ final class UpdateWindowPresenter {
window.titlebarAppearsTransparent = true
window.titlebarSeparatorStyle = .none
window.isReleasedWhenClosed = false
window.level = .floating
window.hidesOnDeactivate = false
window.collectionBehavior.insert(.moveToActiveSpace)
window.setContentSize(NSSize(width: 560, height: 500))
window.minSize = NSSize(width: 520, height: 460)
window.center()
return window
}

private func closeTransientPanels(except updateWindow: NSWindow) {
for candidate in NSApp.windows where candidate !== updateWindow && candidate.title.isEmpty {
candidate.close()
}
}
}
29 changes: 29 additions & 0 deletions TokenStepSwift/Tests/TokenStepSwiftTests/UpdateServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,35 @@ final class UpdateServiceTests: XCTestCase {
)
}

func testManualAvailableUpdateRequestsImmediateWindowPresentation() {
let update = makeUpdate(version: "0.2.6")

XCTAssertTrue(
UpdatePresentationPolicy.shouldPresentWindow(
trigger: .manual,
update: update
)
)
XCTAssertFalse(
UpdatePresentationPolicy.shouldPresentWindow(
trigger: .manual,
update: nil
)
)
XCTAssertFalse(
UpdatePresentationPolicy.shouldPresentWindow(
trigger: .startup,
update: update
)
)
XCTAssertFalse(
UpdatePresentationPolicy.shouldPresentWindow(
trigger: .timer,
update: update
)
)
}

func testAutomaticCheckHonorsToggleSixHourThrottleAndSkippedVersion() {
let now = Date(timeIntervalSince1970: 100_000)
XCTAssertFalse(
Expand Down
39 changes: 39 additions & 0 deletions docs/RELEASE_NOTES_0.2.6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# TokenStep 0.2.6 — 更新检查、下载安装与自动重启闭环

0.2.6 聚焦一件事:用户主动点击更新按钮后,如果存在新版,TokenStep 必须立即进入可见、可完成、可诊断的更新流程。

## 发现新版后立即打开更新窗口

- 菜单栏浮层、主窗口和设置中的手动检查继续共用同一更新状态。
- 手动检查发现新版后,立即打开独立更新窗口,不再只改变按钮图标或等待用户再次点击。
- 手动检查仍会忽略自动检查开关和“跳过此版本”状态,方便用户主动重新查看新版。
- 后台启动、定时和回到前台检查保持非打扰模式,只显示已有更新标记,不强制弹窗。

## 更新窗口不会被浮层遮挡

- 打开更新窗口时自动关闭菜单栏瞬时浮层。
- 更新窗口移动到当前桌面、激活 TokenStep 并置于前台。
- 重复点击更新入口只保留一个新的更新窗口,避免多个窗口叠加。

## 一次点击完成下载、验证、替换和重启

- 点击“安装并重启”后显示真实下载进度。
- 下载完成后挂载 DMG,并校验 Developer ID、Gatekeeper、公证状态和 App 版本。
- 安装 helper 先备份现有 `/Applications/TokenStep.app`,复制失败时恢复备份。
- 安装成功后删除临时备份,重新打开新版本并确认进程已启动。
- 检查、窗口展示、安装请求、helper 启动与失败均写入生命周期或安装日志。

## 验证

- 全量 Swift 测试:100 项通过,0 项失败。
- 新增手动发现更新后立即请求窗口展示的策略测试。
- 使用官方 0.2.5 DMG 和旧版 helper 完成隔离安装验证:签名、挂载、复制及版本检查均通过。
- 在真实 `/Applications` 环境完成 0.2.4 → 0.2.5 的备份、替换和自动重开验证。
- 新增 `script/verify_update_installer.sh`,可在不覆盖已安装 App 的情况下验证发布 DMG。

## 安全、隐私与兼容性

- 更新源仍为本仓库的 GitHub Releases,不新增更新服务器或遥测。
- 不做静默强制安装;下载和替换必须由用户点击“安装并重启”。
- 不改变 Token、金额、额度、排行榜、主题和本地数据采集口径。
- 支持 macOS 14 及以上的 Apple Silicon Mac。
2 changes: 1 addition & 1 deletion script/build_swiftui_and_run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ EXECUTABLE="$BUILD_DIR/$PRODUCT_NAME"
HELPER_EXECUTABLE="$BUILD_DIR/$HELPER_NAME"
ICON_FILE="$ROOT_DIR/TokenUsageMenuApp/assets/TokenStepIcon.icns"
ODYSSEY_ASSET_DIR="$ROOT_DIR/TokenUsageMenuApp/assets/odyssey"
VERSION="${TOKENSTEP_VERSION:-0.2.5}"
VERSION="${TOKENSTEP_VERSION:-0.2.6}"
LAUNCH=true
VERIFY=false

Expand Down
4 changes: 2 additions & 2 deletions script/package_release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ SWIFT_DIR="$ROOT_DIR/TokenStepSwift"
BUILT_APP_BUNDLE="$SWIFT_DIR/dist/$APP_NAME.app"
APP_BUNDLE="$BUILT_APP_BUNDLE"
RELEASE_DIR="$ROOT_DIR/release"
VERSION="${TOKENSTEP_VERSION:-0.2.5}"
VERSION="${TOKENSTEP_VERSION:-0.2.6}"
IDENTITY="${CODE_SIGN_IDENTITY:-}"
NOTARIZE=false

usage() {
cat <<'USAGE'
Usage:
TOKENSTEP_VERSION=0.1.48 CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" ./script/package_release.sh [--notarize]
TOKENSTEP_VERSION=0.2.6 CODE_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" ./script/package_release.sh [--notarize]

Notarization credentials, choose one:
TOKENSTEP_NOTARY_PROFILE="notarytool-profile"
Expand Down
58 changes: 58 additions & 0 deletions script/verify_update_installer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/bin/bash
set -euo pipefail

if [[ $# -ne 3 ]]; then
echo "Usage: $0 /path/to/TokenStep.dmg EXPECTED_VERSION /path/to/TokenStepHelper" >&2
exit 2
fi

DMG_PATH="$1"
EXPECTED_VERSION="$2"
HELPER_SOURCE="$3"

if [[ ! -f "$DMG_PATH" ]]; then
echo "DMG not found: $DMG_PATH" >&2
exit 2
fi
if [[ ! -x "$HELPER_SOURCE" ]]; then
echo "Helper is not executable: $HELPER_SOURCE" >&2
exit 2
fi

VERIFY_ROOT="$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/tokenstep-update-verify.XXXXXX")"
VERIFY_DESTINATION="$VERIFY_ROOT/Applications/TokenStep.app"
VERIFY_HELPER="$VERIFY_ROOT/TokenStepHelper"
VERIFY_LOG="$VERIFY_ROOT/update-install.log"

cleanup() {
if [[ -n "${VERIFY_ROOT:-}" && "$VERIFY_ROOT" == *tokenstep-update-verify.* ]]; then
/bin/rm -rf "$VERIFY_ROOT"
fi
}
trap cleanup EXIT

/bin/mkdir -p "$VERIFY_ROOT/Applications"
/bin/cp "$HELPER_SOURCE" "$VERIFY_HELPER"
/bin/chmod 755 "$VERIFY_HELPER"

"$VERIFY_HELPER" install \
--dmg "$DMG_PATH" \
--version "$EXPECTED_VERSION" \
--current-pid 0 \
--require-verified 1 \
--log "$VERIFY_LOG" \
--helper-path "$VERIFY_HELPER" \
--destination "$VERIFY_DESTINATION" \
--skip-relaunch 1 \
--skip-stop 1

INSTALLED_VERSION="$(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "$VERIFY_DESTINATION/Contents/Info.plist")"
if [[ "$INSTALLED_VERSION" != "$EXPECTED_VERSION" ]]; then
echo "Installed version mismatch: expected $EXPECTED_VERSION, got $INSTALLED_VERSION" >&2
exit 1
fi

/usr/bin/codesign --verify --deep --strict "$VERIFY_DESTINATION"
/usr/sbin/spctl --assess --type execute "$VERIFY_DESTINATION"
/bin/cat "$VERIFY_LOG"
echo "Verified TokenStep $INSTALLED_VERSION update installation in an isolated destination."
Loading