Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4891a69
Windows portable: module skeletons and startup order
fylorn Oct 3, 2026
4645c8e
Windows portable: data beside the exe, self-update from a zip, new as…
fylorn Oct 3, 2026
5cc7c1f
Windows portable: system dialogs, HKCU registrations, one instance
fylorn Oct 3, 2026
f605da7
Windows portable path C: uninstall cleanup, other-instance clients, W…
fylorn Oct 3, 2026
b8ed465
Merge portable/b-system: dialogs, HKCU registrations, single instance
fylorn Oct 3, 2026
c7fed67
Merge remote-tracking branch 'origin/feat/windows-portable' into port…
fylorn Oct 3, 2026
ac63538
NSIS: keep another copy's launch-at-login entry through uninstall
fylorn Oct 3, 2026
9212f4b
Merge portable/a-core: portable data dir, self-update, release assets
fylorn Oct 3, 2026
c7b5e4c
README: new download names, the Windows portable copy, uninstalling
fylorn Oct 3, 2026
79ad381
Merge remote-tracking branch 'origin/feat/windows-portable' into port…
fylorn Oct 3, 2026
8066add
Uninstall: recognise the kept webview folder whatever its spelling
fylorn Oct 3, 2026
955f25d
Release: run the Windows portable and installed copies end to end bef…
fylorn Oct 3, 2026
330e385
e2e: process variables no longer shadow the folder paths (PowerShell …
fylorn Oct 3, 2026
6490532
Merge portable/c-uninstall: uninstall cleanup, NSIS hooks, WebView2 c…
fylorn Oct 3, 2026
10b7514
e2e: find dialog buttons by name whatever their control type; log the…
fylorn Oct 3, 2026
5a59acd
Merge remote-tracking branch 'origin/feat/windows-portable' into port…
fylorn Oct 3, 2026
1c93436
NSIS: say so when the uninstall cleanup did not finish
fylorn Oct 3, 2026
a879f9b
e2e: check 7 always runs now that uninstall cleanup is in; click dial…
fylorn Oct 3, 2026
8eec427
e2e: create the Run key when the runner has none
fylorn Oct 3, 2026
8f6bc39
Merge portable/c2-cleanup-notice: warn when the uninstall cleanup did…
fylorn Oct 3, 2026
092c7bc
Windows portable: review fixes for dialogs, data dir, switching
fylorn Oct 3, 2026
b07e385
Merge dev into feat/windows-portable (core v0.60.0, two probe modes)
fylorn Oct 3, 2026
e526f5e
single: do not build a Handle around a null pointer
fylorn Oct 3, 2026
a992339
Merge remote-tracking branch 'origin/feat/windows-portable' into port…
fylorn Oct 3, 2026
b03abee
Merge origin/feat/windows-portable into portable/f-review-fixes
fylorn Oct 3, 2026
db0f99c
Tests: portable swaps on FAT32, exFAT and SMB; steady supervisor timings
fylorn Oct 3, 2026
e1b944b
Merge portable/f-review-fixes: review findings on the portable build
fylorn Oct 3, 2026
a24b2a4
Merge remote-tracking branch 'origin/feat/windows-portable' into port…
fylorn Oct 3, 2026
3b596cf
Merge remote-tracking branch 'origin/feat/windows-portable' into port…
fylorn Oct 3, 2026
22d364f
Windows: only the release pipeline's build counts as the portable copy
fylorn Oct 3, 2026
9ecddf4
Windows portable: the app in the zip is "ThinkWatch Lite.exe"
fylorn Oct 3, 2026
bcdc823
Merge portable/g-fs-flaky: rename tests on FAT32/exFAT/SMB, steadier …
fylorn Oct 3, 2026
4aed9e6
ci: cancel superseded runs on the same branch or PR
fylorn Oct 3, 2026
9645054
Merge remote-tracking branch 'origin/feat/windows-portable' into port…
fylorn Oct 3, 2026
dac6037
Release: rehearse/windows/ builds only Windows; a new push cancels th…
fylorn Oct 3, 2026
e69b319
Tests: remove their temporary directories, passed or failed
fylorn Oct 3, 2026
3a10ad8
Merge portable/g2-test-tempdirs: tests remove their temporary directo…
fylorn Oct 3, 2026
ae52c8a
Test: the D-Bus round trip stops its notifier tasks before removing i…
fylorn Oct 3, 2026
7be685d
Merge portable/e-e2e: Windows end-to-end checks, official-build marke…
fylorn Oct 3, 2026
4d3a355
ci: drop the temporary trigger for the portable branches
fylorn Oct 3, 2026
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
62 changes: 62 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ on:
pull_request:
branches: [main, dev]

# **同一个分支、同一个 PR 上新的一次推送,把还在跑的旧一次取消掉。**结果只看最新的那次,
# 旧的跑完也没人看,只会占着并发名额(免费组织同时最多 20 个任务、其中 5 个 macOS)。
# main 和 dev 上的不取消:那里每一笔都要有自己的结果。
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/dev' }}

env:
CARGO_TERM_COLOR: always

Expand Down Expand Up @@ -138,6 +145,61 @@ jobs:
- name: Test
run: cargo test --manifest-path src-tauri/Cargo.toml

# **绿色版会被放在 U 盘和网络共享上。**它的自更新靠「正在运行的 exe 能改名、
# 腾出来的名字上能写新的」(`portable::swap_in`),上一步只在 NTFS 上证明过。
# 这里建两块小虚拟盘、格式化成 FAT32 和 exFAT,再开一个本机的 SMB 共享,把
# portable 那一组测试(含改名正在运行的 exe 那一条)在这三处各跑一遍:
# `TW_PORTABLE_TEST_DIR` 指定测试文件夹建在哪。
#
# diskpart、New-SmbShare 要管理员身份。GitHub 的 Windows 机器上步骤本来就以
# 管理员身份运行(UAC 关着),先核实一遍,不是的话直接报错,不让它悄悄跳过。
# 测试程序上一步已经编好,这里不重编
- name: Portable tests on FAT32, exFAT and an SMB share
shell: pwsh
run: |
$me = [Security.Principal.WindowsIdentity]::GetCurrent()
if (-not ([Security.Principal.WindowsPrincipal]$me).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw "This step needs an elevated shell"
}

$dirs = @()
foreach ($v in @(@{ fs = 'fat32'; letter = 'R' }, @{ fs = 'exfat'; letter = 'S' })) {
$vhd = Join-Path $env:RUNNER_TEMP "tw-$($v.fs).vhdx"
$script = Join-Path $env:RUNNER_TEMP "tw-$($v.fs).diskpart"
@(
"create vdisk file=""$vhd"" maximum=2048 type=expandable"
"select vdisk file=""$vhd"""
"attach vdisk"
"convert mbr"
"create partition primary"
"format fs=$($v.fs) quick label=TW$($v.fs.ToUpper())"
"assign letter=$($v.letter)"
) | Set-Content -Encoding ascii $script
diskpart /s $script
if ($LASTEXITCODE -ne 0) { throw "diskpart could not make the $($v.fs) volume" }
$vol = Get-Volume -DriveLetter $v.letter
if ($vol.FileSystem -ne $v.fs) { throw "$($v.letter): is $($vol.FileSystem), not $($v.fs)" }
$dirs += "$($v.letter):\"
}

$share = Join-Path $env:RUNNER_TEMP 'tw-share'
New-Item -ItemType Directory $share | Out-Null
New-SmbShare -Name tw-share -Path $share -FullAccess $me.Name | Out-Null
$dirs += '\\localhost\tw-share'

Get-Volume -DriveLetter R, S | Format-Table DriveLetter, FileSystem, FileSystemLabel, Size
Get-SmbShare -Name tw-share | Format-Table Name, Path

$failed = @()
foreach ($dir in $dirs) {
Write-Host "::group::portable tests in $dir"
$env:TW_PORTABLE_TEST_DIR = $dir
cargo test --manifest-path src-tauri/Cargo.toml --lib -- portable::tests --nocapture
if ($LASTEXITCODE -ne 0) { $failed += $dir }
Write-Host "::endgroup::"
}
if ($failed) { throw "portable tests failed in: $($failed -join ', ')" }

# 理由同 Windows 那一档:一个 `#[cfg(target_os = "linux")]` 分支「检查通过」
# 和「根本没人编过它」长得一模一样。跟随系统的深浅色(portal)、登录 shell
# 的环境、系统通知,这些只在这里编、只在这里测。
Expand Down
245 changes: 213 additions & 32 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

39 changes: 35 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ before the client runs them.

| Platform | Install |
|---|---|
| macOS 12 or later, Apple silicon | `brew install --cask thinkwatchproject/tap/thinkwatch-lite`, or [`ThinkWatch-Lite-<version>-arm64.dmg`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| Windows 10 21H2 or later, x64 | [`ThinkWatch-Lite-<version>-x64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| Windows 10 21H2 or later, ARM64 | [`ThinkWatch-Lite-<version>-arm64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| Linux, x86_64 or aarch64 | `curl -fsSL https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest/download/install.sh \| sh`, or [`ThinkWatch-Lite-<version>-<arch>.AppImage`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| macOS 12 or later, Apple silicon | `brew install --cask thinkwatchproject/tap/thinkwatch-lite`, or [`ThinkWatch-Lite-<version>-darwin-arm64.dmg`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| Windows 10 21H2 or later, x64 | Installer [`ThinkWatch-Lite-<version>-windows-x64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest), or portable [`ThinkWatch-Lite-<version>-windows-x64-portable.zip`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| Windows 10 21H2 or later, ARM64 | Installer [`ThinkWatch-Lite-<version>-windows-arm64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest), or portable [`ThinkWatch-Lite-<version>-windows-arm64-portable.zip`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| Linux, x86_64 or aarch64 | `curl -fsSL https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest/download/install.sh \| sh`, or [`ThinkWatch-Lite-<version>-linux-<arch>.AppImage`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |

The gateway, [ThinkWatch Core](https://github.com/ThinkWatchProject/ThinkWatch-Core),
ships inside the app. The app is not signed by Apple or Microsoft, so the
Expand All @@ -97,6 +97,37 @@ covers it, along with how updates arrive. The interface is in English and
Simplified Chinese, and the app updates itself (a Homebrew installation
updates through Homebrew).

### Windows portable

Unzip and run `ThinkWatch Lite.exe`: no installation and no administrator
rights. Configuration, keys and request history stay in the `data\` folder next
to the program, apart from the installed copy's data, so the two keep separate
settings. The folder has to be writable.

Only one of the installed and portable copies runs at a time; opening the other
one offers to stop the running one and start it instead. `thinkwatch://` links
and launch at login always point to the copy that is running.

The portable copy has no system uninstaller. Uninstall it in Settings › Full
uninstall first, which restores connected clients and removes its launch at
login and other registry entries, then delete the whole folder.

### Uninstall

Uninstalling in Settings › Full uninstall restores connected clients, turns off
launch at login and can delete the data directory as well. Afterwards, move the
app to the Trash on macOS, or delete the AppImage file on Linux.

The installed copy on Windows can also be uninstalled through the system: the
uninstaller closes ThinkWatch Lite and does the same restoring and cleanup
first. With "Also delete data (configuration, API keys, request history)"
ticked it deletes `%APPDATA%\ThinkWatch` too, except when a client could not be
restored, in which case the data directory, with that client's backup, is kept.
The uninstaller runs as an administrator: when a standard account uninstalls
with an administrator's password, it restores the administrator's clients and
deletes the administrator's data instead, so uninstall in the app first in that
case.

## Documentation

- [Features](https://thinkwat.ch/docs/lite/features): every page, in detail
Expand Down
22 changes: 18 additions & 4 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,27 @@ Claude Code、Codex 等 AI 客户端的本地网关,支持 macOS、Windows 与

| 平台 | 安装 |
|---|---|
| macOS 12 及以上,Apple 芯片 | `brew install --cask thinkwatchproject/tap/thinkwatch-lite`,或 [`ThinkWatch-Lite-<版本>-arm64.dmg`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| Windows 10 21H2 及以上,x64 | [`ThinkWatch-Lite-<版本>-x64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| Windows 10 21H2 及以上,ARM64 | [`ThinkWatch-Lite-<版本>-arm64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| Linux,x86_64 或 aarch64 | `curl -fsSL https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest/download/install.sh \| sh`,或 [`ThinkWatch-Lite-<版本>-<架构>.AppImage`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| macOS 12 及以上,Apple 芯片 | `brew install --cask thinkwatchproject/tap/thinkwatch-lite`,或 [`ThinkWatch-Lite-<版本>-darwin-arm64.dmg`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| Windows 10 21H2 及以上,x64 | 安装版 [`ThinkWatch-Lite-<版本>-windows-x64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest),或绿色版 [`ThinkWatch-Lite-<版本>-windows-x64-portable.zip`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| Windows 10 21H2 及以上,ARM64 | 安装版 [`ThinkWatch-Lite-<版本>-windows-arm64-setup.exe`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest),或绿色版 [`ThinkWatch-Lite-<版本>-windows-arm64-portable.zip`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |
| Linux,x86_64 或 aarch64 | `curl -fsSL https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest/download/install.sh \| sh`,或 [`ThinkWatch-Lite-<版本>-linux-<架构>.AppImage`](https://github.com/ThinkWatchProject/ThinkWatch-Lite/releases/latest) |

网关 [ThinkWatch Core](https://github.com/ThinkWatchProject/ThinkWatch-Core) 随应用一同安装。应用未经 Apple 与 Microsoft 签名,首次打开需要多一步操作,见[安装与更新](https://thinkwat.ch/zh-CN/docs/lite/install),其中也说明了各种安装方式如何更新。界面提供英文与简体中文,应用自动更新(通过 Homebrew 安装的随 Homebrew 更新)。

### Windows 绿色版

解压后运行其中的 `ThinkWatch Lite.exe` 即可,无需安装,也不需要管理员权限。配置、密钥与请求记录保存在程序旁边的 `data\` 文件夹中,与安装版的数据互不相干,是两套独立的设置。程序所在的文件夹需要可以写入。

安装版与绿色版同一时间只运行一个,打开另一个时可以停止正在运行的那一个、改为启动它。`thinkwatch://` 链接与开机启动始终指向正在运行的那一个。

绿色版不经过系统卸载:先在应用的「设置 › 完全卸载」中完成卸载,还原已接管的客户端、移除开机启动等注册项,再删除整个文件夹。

### 卸载

在应用的「设置 › 完全卸载」中卸载,会还原已接管的客户端、取消开机启动,并可同时删除数据目录。之后在 macOS 上将应用移到废纸篓,在 Linux 上删除 AppImage 文件。

Windows 安装版也可以直接通过系统卸载:卸载程序先关闭 ThinkWatch Lite,完成同样的还原与清理。勾选「同时删除数据(配置、API 密钥、请求记录)」时一并删除 `%APPDATA%\ThinkWatch`;有客户端未能还原时数据目录保留,它的备份仍在其中。卸载程序以管理员身份运行,标准账户输入管理员密码卸载时,还原与删除的是管理员账户的客户端和数据,这种情况应先在应用内卸载。

## 文档

- [功能详解](https://thinkwat.ch/zh-CN/docs/lite/features):逐页说明
Expand Down
33 changes: 22 additions & 11 deletions scripts/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

应用去问「有没有新版本」时读的就是这一份清单,下载的是它指向的那个文件
—— 和网页上给人下载的是同一个:macOS 上是 DMG(应用怎么从 DMG 更新自己,
见 `src-tauri/src/dmg.rs`),Windows 上是 NSIS 安装程序,更新器直接跑它;
见 `src-tauri/src/dmg.rs`),Windows 上安装版是 NSIS 安装程序,更新器直接跑它,
绿色版是 zip,应用自己换掉文件夹里的两个 exe(见 `src-tauri/src/portable.rs`);
Linux 上只发 AppImage,它原地换掉自己(见 `src-tauri/src/update.rs`);
`scripts/install.sh` 也从这一份里找本架构的 AppImage。

Expand Down Expand Up @@ -33,12 +34,22 @@
# 到 `<系统>-<架构>`(插件的 `get_urls`);安装方式来自打包时写进二进制的标记。
# **只给带 `-appimage` 的那个。**AppImage 第一下就找到它;不带后缀的键是所有
# 构建的退路,给了它,一个自己打出来的 deb 或 rpm 也会被递上 AppImage 的字节。
#
# Windows 的绿色版**自己点名要 `-portable` 那个键**(`portable::updater_target`):
# 它的 exe 就是安装程序里的那一个,打包标记是 NSIS,按标记找会找到安装程序。安装版
# 照旧找 `windows-<架构>-nsis`、再退到 `windows-<架构>` —— 已经装着的旧版本也是这样
# 找的,新加的两个键它们看不见,文件改名也不影响它们:地址只从这一份清单里读。
#
# 每个键指向的文件**以什么结尾**也定死:键和文件对错了(x64 的键指着 arm64 的包、
# 绿色版的键指着安装程序),装下去就是一个起不来的应用。
PLATFORMS = {
"darwin-aarch64",
"windows-x86_64",
"windows-aarch64",
"linux-x86_64-appimage",
"linux-aarch64-appimage",
"darwin-aarch64": "-darwin-arm64.dmg",
"windows-x86_64": "-windows-x64-setup.exe",
"windows-aarch64": "-windows-arm64-setup.exe",
"windows-x86_64-portable": "-windows-x64-portable.zip",
"windows-aarch64-portable": "-windows-arm64-portable.zip",
"linux-x86_64-appimage": "-linux-x86_64.AppImage",
"linux-aarch64-appimage": "-linux-aarch64.AppImage",
}


Expand Down Expand Up @@ -93,13 +104,13 @@ def main() -> None:
sys.exit(f"不认识的平台或写法:{pair}(要的是 <平台>=<文件>,平台是 {sorted(PLATFORMS)} 之一)")
if platform in platforms:
sys.exit(f"{platform} 给了两次")
# AppImage 会被原地换成这个文件:换成别的东西就再也起不来了
if platform.endswith("-appimage") and not file.endswith(".AppImage"):
sys.exit(f"{platform} 要的是 .AppImage,给的是 {file}")
# AppImage、绿色版的 exe 会被原地换成这个文件里的东西:换成别的就再也起不来了
if not file.endswith(PLATFORMS[platform]):
sys.exit(f"{platform} 要的是 …{PLATFORMS[platform]},给的是 {file}")
platforms[platform] = entry(pathlib.Path(file), version, want)
# **少一个平台就不发。**缺掉的那个平台上,已经装好的每一份都会停在旧版本,
# 而发布页上看起来一切正常
missing = PLATFORMS - platforms.keys()
missing = PLATFORMS.keys() - platforms.keys()
if missing:
sys.exit(f"这一版缺了这些平台:{sorted(missing)}")

Expand All @@ -118,7 +129,7 @@ def main() -> None:
out = pathlib.Path(pairs[0].partition("=")[2]).parent / "latest.json"
out.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
for p, e in sorted(platforms.items()):
print(f"{p:23} {e['url'].rsplit('/', 1)[1]}")
print(f"{p:25} {e['url'].rsplit('/', 1)[1]}")
print(f"签名密钥 {want.hex()},和应用里的公钥是同一把")


Expand Down
29 changes: 20 additions & 9 deletions scripts/release_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
正文是英文的,依次是:

1. `release-notes/<版本>.md`:这一版的说明。**可以没有**,没有就从下载表开始。
2. 下载表:每个平台一行,文件名和 release.yml 挂上去的一字不差;再给 Homebrew
和 Linux 安装脚本的命令。
2. 下载表:每个文件一行(Windows 每个架构两行:安装程序、绿色版 zip),文件名和
release.yml 挂上去的一字不差;再给 Homebrew 和 Linux 安装脚本的命令、绿色版的
用法。
3. 怎样用 `.sha256` 核对下载的文件。
4. 第二个参数的内容:GitHub 按上一版以来合并的 PR 生成的「What's Changed」,由
调用方用 `gh api repos/<仓库>/releases/generate-notes` 取来。拆成参数传进来,
Expand All @@ -34,11 +35,13 @@
# 每个平台发的那一个文件。**和 release.yml 起的名字一字不差** ——
# release_notes_test.py 拿 release.yml 核对这张表,对不上的话发布页上的链接就是 404。
DOWNLOADS = [
("macOS, Apple silicon", "ThinkWatch-Lite-{v}-arm64.dmg"),
("Windows, x64", "ThinkWatch-Lite-{v}-x64-setup.exe"),
("Windows, ARM64", "ThinkWatch-Lite-{v}-arm64-setup.exe"),
("Linux, x86_64", "ThinkWatch-Lite-{v}-x86_64.AppImage"),
("Linux, aarch64", "ThinkWatch-Lite-{v}-aarch64.AppImage"),
("macOS, Apple silicon", "ThinkWatch-Lite-{v}-darwin-arm64.dmg"),
("Windows, x64, installer", "ThinkWatch-Lite-{v}-windows-x64-setup.exe"),
("Windows, x64, portable", "ThinkWatch-Lite-{v}-windows-x64-portable.zip"),
("Windows, ARM64, installer", "ThinkWatch-Lite-{v}-windows-arm64-setup.exe"),
("Windows, ARM64, portable", "ThinkWatch-Lite-{v}-windows-arm64-portable.zip"),
("Linux, x86_64", "ThinkWatch-Lite-{v}-linux-x86_64.AppImage"),
("Linux, aarch64", "ThinkWatch-Lite-{v}-linux-aarch64.AppImage"),
]

BREW = "brew install --cask thinkwatchproject/tap/thinkwatch-lite"
Expand Down Expand Up @@ -81,8 +84,14 @@ def render(version: str, changes: str, summary_text: str | None) -> str:
raise NotesError(f"版本号的写法不对:{version}(要的是 2026.9.16 这样的)")
base = f"https://github.com/{REPO}/releases/download/v{version}"
files = [(platform, name.format(v=version)) for platform, name in DOWNLOADS]
dmg, x64 = files[0][1], files[1][1]
appimage = files[3][1]

def named(suffix: str) -> str:
"""下载表里以这一段结尾的那个文件。按名字找,表里加一行、换个顺序都不跟着错"""
(name,) = [n for _, n in files if n.endswith(suffix)]
return name

dmg, x64 = named("-darwin-arm64.dmg"), named("-windows-x64-setup.exe")
appimage = named("-linux-x86_64.AppImage")

parts = []
if summary_text:
Expand All @@ -98,6 +107,8 @@ def render(version: str, changes: str, summary_text: str | None) -> str:

Each file is published with a `.sha256` file beside it. `latest.json` is the manifest for in-app updates, and `install.sh` is the Linux install script.

On Windows, the portable ZIP runs without installation or administrator rights: extract it to any writable folder and start `ThinkWatch Lite.exe`. Its settings stay in the `data` folder beside it, separate from an installed copy, and it updates itself in place.

On macOS, Homebrew installs the same disk image:

```sh
Expand Down
Loading
Loading