发布更新
概述
Knowledge Base 桌面应用采用 CI 构建 + Claude 全自动后处理 + R2 CDN 模式:
本地:更新版本号 → 提交 → 打 Tag → 推送(触发 CI)
↓ Claude ScheduleWakeup 轮询 CI(不推送任何内容到 release 仓库)
CI:构建 Windows + macOS + Linux 安装包 → 上传到 GitHub Release(草稿)
↓ CI 完成后,Claude 用 GitHub API 自动下载草稿 release 的 13 个 asset 到 release 仓库
本地:上传产物到 R2 CDN + 更新 README + 生成 update.json → 推送到 release 仓库 → publish Release → 触发文档站重建
关键原则:CI 构建完成前,不要推送任何内容到 release 仓库。 README 更新、产物下载、update.json 生成在获得产物后一次性完成并推送。
本地不需要执行
pnpm tauri build。CI 负责构建和签名。 CI 构建完成后,Claude 通过 GitHub API 自动下载 draft release 的 assets(路线 A 走 Sigil,路线 B 走gh), 全程不需要用户手动下载(v1.8.1 起验证可行)。
支持平台
本项目 CI 构建 Windows + macOS + Linux 三平台。
🔴 Windows 可按需从 CI 摘掉:仓库变量
RELEASE_WINDOWS=skip时 CI 不起 windows runner (Windows 安装包改由发版机本地构建提供,省 ~12 分钟额度)。详见步骤 5a。
| 平台 | Runner | Bundle 参数 |
|---|---|---|
| Windows x64 | windows-latest |
--bundles nsis(RELEASE_WINDOWS=skip 时不构建) |
| macOS Apple Silicon | macos-latest + target aarch64-apple-darwin |
--bundles app,dmg |
| macOS Intel | macos-latest + target x86_64-apple-darwin |
--bundles app,dmg |
| Linux x64 | ubuntu-22.04 |
--bundles deb,appimage |
Linux runner 在构建前需安装 webkit2gtk-4.1 / soup-3.0 / ayatana-appindicator3 等系统包 (工作流里的 "Install Linux system dependencies" 步骤已覆盖,只在
matrix.platform == 'ubuntu-22.04'时触发)。
三级分发策略
| 用途 | 平台 | 角色 | 原因 |
|---|---|---|---|
| 源码托管 | GitHub 私有 bkywksj/knowledge-base |
— | 代码管理 + CI 构建 |
| CI 构建 | GitHub Actions | — | 跨平台构建 + 签名 |
| 安装包下载 + 自动更新(主) | Cloudflare R2 CDN | 主源 | 国内快,零流量费,全球 CDN |
| 自动更新备源 + 海外存档 | GitHub 公开 bkywksj/knowledge-base-release |
备源 1 | R2 不通时兜底,含签名产物 |
| 自动更新兜底 + 国内镜像 | Gitee 公开 bkywksj/knowledge-base-release |
备源 2 | 国内免代理访问,raw URL 作为 update.json 发现兜底 |
Gitee 仓库的 update.json 与 GitHub 版内容完全一致(url 都指向 GitHub raw)。 Gitee endpoint 主要作用是"读取 update.json"时的兜底(在 R2 和 GitHub 都挂的场景下至少能发现新版本)。 完整下载链路走 R2(首选)或 GitHub raw(R2 不通时)。
为什么不让 CI 推送到 release 仓库?
GitHub Actions 在美国服务器运行,推送二进制产物到国内通常超时。 改为用户本地下载产物后,由 Claude 在本地完成推送,速度更快且更可控。
🔴 发布架构选型(首发必读)
上面的"三级分发策略"是老路线(公开 release 仓 + Gitee raw 端点 + rclone)。 实战(cup_watch 首发)证明:当本机装了 Sigil 凭据保险库时,有一条更省事、更安全、零公开仓的路线,应作为默认首选。 两条路线都要支持——没装 Sigil 时用 B 路线的等价命令,流程与产物完全一致。
路线 A(推荐):R2-only 分发 + 全私有仓 + Sigil 注入
CI(私有源码仓) 构建 .exe + .sig → GitHub draft release(私有,仅 token 可读)
↓ Sigil github_download_release_asset(带 token 下私有 draft,AI 不碰 token 明文)
本地拿到 .exe + .sig
↓ Sigil r2_object_upload(bucket=downloads, key=<prefix>/releases/vX/...)
R2 downloads/<prefix>/... + update.json ← 公开 r2.dev,用户下载 + 自动更新都走这
- 下载源 + 自动更新端点 = R2 唯一(
pub-….r2.dev,公开、零流量费、无 CORS 问题)。 - GitHub / Gitee 仓全部保持私有 —— 只做源码 + CI + 可选存档,不对外服务。
- 不需要"把 release 仓改公开"(Sigil 的
repo_update也明确禁止设公开,别再往那撞)。 - tauri.conf 的
endpoints只留 R2 一个(删掉需要公开仓的 Gitee raw 端点)。
路线 B:不装 / 不用 Sigil 时的等价做法
Sigil 只是"凭据注入器"。没有它时,每一步都有等价的本地命令(gh CLI + 系统 git credential + rclone):
| 步骤 | 路线 A(Sigil) | 路线 B(无 Sigil 等价) |
|---|---|---|
| 建源码 / release 仓 | mcp__sigil__github_repo_create(private:true) |
gh repo create <owner>/<repo> --private |
| 推源码 / tag | mcp__sigil__git_push |
git push <remote> <branch> / git push <remote> vX.Y.Z(系统 credential helper 注入) |
| 启用 Actions | mcp__sigil__github_actions_enable |
gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=true |
| 配签名 Secret | github_repo_secret_set(secret_name=TAURI_SIGNING_PRIVATE_KEY, value_from_file=src-tauri/keys/tauri-updater.key) ← Sigil 自己读 .key 注入、私钥不进对话也免导金库,用此参数会弹一次桌面端确认(旧的 value_credential_name 路径仍可用,但需先把私钥导入金库,已不推荐) |
gh secret set TAURI_SIGNING_PRIVATE_KEY --repo <owner>/<repo> < src-tauri/keys/tauri-updater.key ← 无 Sigil 时用它(见第 3 节) |
| 下私有 draft 产物 | mcp__sigil__github_download_release_asset |
gh release download vX.Y.Z --repo <owner>/<repo> --dir <dir>(draft 也能下,gh 自带 token) |
| 上传 R2 | mcp__sigil__r2_object_upload(bucket=downloads,key=…) |
rclone copyto <file> r2:downloads/<prefix>/…(rclone remote 须能写 downloads 桶) |
| 监听 CI | mcp__sigil__github_run_get |
gh run list --repo <owner>/<repo> / gh run watch |
检测:本会话能
ToolSearch到mcp__sigil__*→ 走 A;查不到 → 自动走 B,不报错不卡流程。 两条路线产出完全一致(同样的 R2 对象 + update.json),只是"谁来注入凭据"不同。
🔴 R2 桶真相(写死,别再每次重新摸索)
cup_watch 首发在这上面烧了约 10 轮反复误判。事实固定如下,直接照用:
- 真实桶名 =
downloads(一个桶,全项目共用);reeve/cupwatch/sigil等是桶内前缀,不是独立桶。 - 公开域名(形如
https://pub-xxxxx.r2.dev)是downloads桶的共享域名,所有项目共用、不用改;对象 URL =<publicUrl>/<prefix>/...。 - 这把 R2 access key 是 scoped key:不能 ListBuckets、不能建桶(
rclone lsd r2:报 403 /mkdir静默失败)。所以别用 rclone 去"自省桶结构"或"建桶"——会把你带进沟里。 - 首选:用 Sigil
r2_object_upload(bucket=downloads, key=<prefix>/...)一传即通(它知道 default_bucket)。 - 无 Sigil 时 rclone 正确写法是
r2:downloads/<prefix>/...(第一段必须是桶名downloads;写成r2:<prefix>/...会报NoSuchBucket)。 - 新项目只需选一个前缀(如
myapp),无需在 Cloudflare 控制台建任何东西。
🔴 首发踩坑速查(血泪,cup_watch v0.1.0 实录)
| 坑(现象) | 真因 | 正解 |
|---|---|---|
R2 反复 NoSuchBucket / 403 / 误判"reeve 是桶" |
scoped key 不能列/建桶;桶名其实是 downloads、项目是前缀 |
见上「R2 桶真相」:用 Sigil 传,或 rclone 写 r2:downloads/<prefix>/ |
| 卡在"要把 release 仓改公开",Sigil 拒绝 | 老架构靠公开仓 raw;撞"仓库必须私有"红线 | 走路线 A:R2-only + 全私有,根本不需要改公开 |
| 签名 Secret 配置 | 私钥不能进对话(红线),又不想手动导金库 | 首选 github_repo_secret_set(value_from_file=src-tauri/keys/tauri-updater.key):Sigil 自读私钥注入、免导金库、弹一次确认;无 Sigil 时用 gh secret set < keyfile |
| tag 推了但 0 个 workflow run | 新建私有仓 Actions 默认禁用 | 推 tag 前先 github_actions_enable(或 gh api … permissions -F enabled=true) |
| 启用 Actions 后老 tag 不触发,又不能手动 dispatch | 启用不回溯已推 tag;workflow 没声明 workflow_dispatch |
删远端 tag 重推;workflow 模板加 workflow_dispatch(见 CI 章节)以后可手动重触发 |
| CI 秒级 failure:所有 job 都 failure 但无任何失败 step(job 没分到 runner,实测非「无 job」而是有 job 空跑) | 私有仓 Actions 免费分钟耗尽 | 切备用账号(见「多 CI 仓库 fallback」),整套:建仓+remote+独立 secret+启用 Actions+推。Sigil 的 github_run_jobs 会自动标 quota_exhausted_suspect=true 并就地提示换号(github_billing_actions 判不出:免费号耗尽不计费、净费用停在 |
关键配置
| 项目 | 值 |
|---|---|
| 应用名(productName) | Knowledge Base(CI 产物前缀会变成 Knowledge.Base_,空格→点) |
| 签名私钥 | GitHub Secrets TAURI_SIGNING_PRIVATE_KEY(已配置) |
| 签名公钥 | src-tauri/tauri.conf.json → plugins.updater.pubkey |
| 更新端点 1(R2 主) | https://pub-9d9e6c0cb6934fb0a0c505e3c64f39b2.r2.dev/knowledge-base/update.json |
| 更新端点 2(GitHub raw 备) | https://github.com/bkywksj/knowledge-base-release/raw/main/update.json |
| 更新端点 3(Gitee raw 兜底) | https://gitee.com/bkywksj/knowledge-base-release/raw/master/update.json |
| R2 CDN 公开地址 | https://pub-9d9e6c0cb6934fb0a0c505e3c64f39b2.r2.dev/knowledge-base |
| R2 rclone remote | r2:downloads/knowledge-base/(~/.config/rclone/rclone.conf) |
| R2 rclone 程序 | ~/bin/rclone.exe |
| 源码仓库本地路径 | E:/my/桌面软件tauri/knowledge_base |
| 源码仓库分支 | master(GitHub remote 名为 github,Gitee remote 名为 origin) |
| Release 仓库(GitHub 公开) | https://github.com/bkywksj/knowledge-base-release(remote: origin) |
| Release 仓库(Gitee 公开) | https://gitee.com/bkywksj/knowledge-base-release(remote: gitee) |
| 本地 Release 仓库路径 | E:/my/桌面软件tauri/knowledge-base-release |
| Release 仓库分支 | 本地 main;GitHub 远端 main;Gitee 远端 master(推 Gitee 时用 refspec main:master) |
| 默认下载目录 | D:/download/download/(浏览器下载保存位置) |
| GitHub Actions 工作流 | .github/workflows/release.yml |
| 平台配置 | .claude/release-config.json(含所有自动化参数) |
多 GitHub CI 仓库(额度耗尽时切换)
| 名称 | 仓库地址 | Git Remote | Token 文件 | 用途 |
|---|---|---|---|---|
| GitHub 主仓库 | https://github.com/bkywksj/knowledge-base |
github |
已存 git credential(默认凭证) | CI 构建(主) |
| GitHub 备用 1 | git@github.com:allebamala/knowledge-base.git(SSH) |
github2 |
~/.gh_token_allebamala + 本机 SSH key 已绑 |
bkywksj 额度耗尽时切换 |
| GitHub 备用 2 | https://github.com/elginbolds-cell/knowledge-base.git(HTTPS + token URL) |
github3 |
~/.gh_token_elginbolds |
bkywksj + allebamala 都耗尽时切换 |
| GitHub 备用 3+ | 按需新建,如 https://github.com/shdidnjfreezing-del/knowledge-base(github12,v1.31.0 用) |
github5 / github7 / github12 … |
Sigil 凭据(mcp__sigil__list_credentials(kind=github_token) 查真实 name → account) |
前面都耗尽时切换 |
🔴 新 CI 仓一次性开通(4 步,全走 Sigil,凭据不落地):
1. mcp__sigil__github_repo_create(credential_name=<githubN>, name=knowledge-base) # 强制 private 2. mcp__sigil__github_actions_enable(credential_name=<githubN>, repo=<owner>/knowledge-base) ↑ 新建私有仓 Actions 默认禁用;必须在推 tag 前开,否则 tag 推了 0 个 run, 且启用不回溯已推 tag(要删远端 tag 重推) 3. mcp__sigil__github_repo_secret_set(credential_name=<githubN>, repo=…, secret_name=TAURI_SIGNING_PRIVATE_KEY, value_from_file=src-tauri/keys/tauri-updater.key) ↑ 桌面端弹一次确认;私钥不进对话。桌面 release.yml 只需这一个 secret (PASSWORD 在 workflow 里写死空串)。要跑 android.yml 才需另配 4 个 ANDROID_* secret 4. git remote add <githubN> <https url> → mcp__sigil__git_push(credential_name=<githubN>, …) tag 用 mcp__sigil__workspace_register + workspace_push_tag(git_push 只推分支不推 tag)别名对照:Sigil 凭据
github1=bkywksj(remotegithub)、github2=allebamala、github3=elginbolds、github12=shdidnjfreezing-del。不要凭印象猜,用list_credentials查。
发布前必须用 AskUserQuestion 询问:本次用哪个 GitHub 仓库跑 CI? 代码推到选定 CI 仓库,tag 也只推到选定的 CI 仓库(避免重复构建浪费配额)。 切换前先
git push <ci-remote> master把历史同步上去,再git push <ci-remote> v$VERSION触发。github3 推送注意:elginbolds-cell 账号的凭证不要存进 git credential(会覆盖 github.com 默认凭证 bkywksj,导致推 release 仓库时认证错乱)。github3 的 remote URL 已内嵌 token,正常
git push github3 ...即可。github3 Actions Secrets:首次切到 github3 前,必须在
elginbolds-cell/knowledge-base仓库的 Settings → Secrets and variables → Actions 手动配齐TAURI_SIGNING_PRIVATE_KEY、TAURI_SIGNING_PRIVATE_KEY_PASSWORD(空)、ANDROID_KEYSTORE_BASE64、ANDROID_KEYSTORE_PASSWORD、ANDROID_KEY_ALIAS、ANDROID_KEY_PASSWORD(与 github / github2 一致)。未配齐 CI 会失败。API 调用切换:CI 监控 / 产物下载脚本里的 owner/repo 路径需要跟着改:
bkywksj/knowledge-base→allebamala/knowledge-base→elginbolds-cell/knowledge-base;token 也要跟着切到对应的~/.gh_token*文件。
版本号位置(三处必须同步)
| 文件 | 字段 |
|---|---|
src-tauri/tauri.conf.json |
"version": "x.y.z" |
src-tauri/Cargo.toml |
version = "x.y.z" |
package.json |
"version": "x.y.z" |
完整发布流程
步骤 1:本地 tsc 自检 + 询问版本号和更新说明
# 避免 CI 因 unused import 失败
cd "E:/my/桌面软件tauri/knowledge_base" && npx tsc --noEmit
使用 AskUserQuestion 询问:
- 新版本号(当前版本读取自
tauri.conf.json) - 更新说明(将写入 release 仓库 README 版本历史)
步骤 2:更新三处版本号
Edit src-tauri/tauri.conf.json # "version": "x.y.z"
Edit src-tauri/Cargo.toml # version = "x.y.z"
Edit package.json # "version": "x.y.z"
步骤 3:更新 release 仓库 README.md
更新 3 处:
- 顶部 "最新版本: vx.y.z" 下载表格(文件名用
Knowledge.Base_x.y.z_...,注意带点) - "版本历史" 添加 v_x.y.z_ 条目
- "项目结构" 树中添加 v_x.y.z_ 目录
下载表格模板(Windows + macOS,所有文件名带点):
### 最新版本: vx.y.z
| 平台 | 下载链接 |
|------|---------|
| Windows x64 | [Knowledge.Base_x.y.z_x64-setup.exe](releases/vx.y.z/Knowledge.Base_x.y.z_x64-setup.exe) |
| macOS Apple Silicon | [Knowledge.Base_x.y.z_aarch64.dmg](releases/vx.y.z/Knowledge.Base_x.y.z_aarch64.dmg) |
| macOS Intel | [Knowledge.Base_x.y.z_x64.dmg](releases/vx.y.z/Knowledge.Base_x.y.z_x64.dmg) |
步骤 4:提交并推送 release 仓库 README
推送前必须先
git pull --rebase origin main(上次 CI 可能已推)。
cd "E:/my/桌面软件tauri/knowledge-base-release"
git add README.md
git commit -m "docs: 更新 README 至 vx.y.z
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
git pull --rebase origin main
git push origin main
步骤 5:提交源码仓库并打 Tag 触发 CI
⚠️ commit message 不能含
[skip ci]—— 被 tag 的那个 commit 带[skip ci]的话, tag push 触发的 workflow 也会被一并跳过(v1.9.0-mobile 时踩过这个坑)。
cd "E:/my/桌面软件tauri/knowledge_base"
git add src-tauri/tauri.conf.json src-tauri/Cargo.toml src-tauri/Cargo.lock package.json
# 如有其他变更一起 add
git commit -m "release: vx.y.z
<更新说明摘要>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"
git push <CI_REMOTE> master
git tag "vx.y.z"
git push <CI_REMOTE> "vx.y.z"
💡 版本号改动会让
src-tauri/Cargo.lock里的knowledge_base版本失配, 用cargo update -p knowledge_base --offline --manifest-path src-tauri/Cargo.toml同步后一起提交(一条命令,不重编)。
🔴 5a. 推 tag 前:设 CI 仓变量 RELEASE_WINDOWS(决定 CI 起不起 Windows runner)
.github/workflows/release.yml 的 prepare-release job 读仓库变量 RELEASE_WINDOWS 动态生成构建矩阵:
| 场景 | RELEASE_WINDOWS 设为 |
CI 行为 |
|---|---|---|
| 常规发布(Windows 也走 CI) | build(或不设 / 删除该变量) |
构建 Win + macOS×2 + Linux,draft release 共 17 个 asset(取其中 13 个) |
| Windows 由发版机本地构建(本地代码签名档) | skip |
不起 windows runner,省 ~12 分钟额度;draft release 只有 13 个 asset(无 Windows 4 个),Windows 3 个产物由本地构建补 |
# Sigil(推荐):对准“当次选定的 CI 仓”,幂等设置
mcp__sigil__github_repo_variable_set(
credential_name=<CI仓凭据>, repo=<owner/repo>, name=RELEASE_WINDOWS, value=skip|build)
# 无 Sigil
gh variable set RELEASE_WINDOWS --repo <owner/repo> -b skip
🔴 每次发版都显式设一次(幂等),即使这次要
build也要显式设 —— 否则上次的skip残留会让这次悄悄漏建 Windows,直到步骤 7 清点产物才发现。 🔴 变量是"每个 CI 仓各自一份":切到备用 CI 仓(github2/github3/…)时要在新仓重新设, 新建的 CI 仓默认没有该变量(=含 Windows)。本机是否属于"本地构建档"由
.claude/signing.local.json是否存在判定;存在时按.claude/EVSIGN-LOCAL.md(git-excluded,仅本机可见)执行本地构建,并把RELEASE_WINDOWS设为skip。
步骤 6:用 ScheduleWakeup 监控 CI 构建(15–30 分钟,零干预)
CI 构建期间不要 sleep / 不要让用户手动等。用 ScheduleWakeup 自醒:每 ~5 分钟检查一次 workflow run 状态,未完成就再睡,完成就推进到步骤 7。
首次检查脚本(直接调用,立刻返回当前状态):
# poll_ci.py —— 通过 GitHub API 查询 v_X.Y.Z_ 的 workflow run
import urllib.request, json, subprocess
import os
# 🔴 严禁 git credential fill —— 本机没存 github.com 凭据时会弹 GCM 登录框并阻塞
# 路线 A:会话里有 mcp__sigil__* → 直接用 Sigil 能力,根本不用 token
# 路线 B:用 gh 自带 token(不碰系统 credential helper,任何机器都不弹窗)
token = (os.environ.get('GH_TOKEN') or os.environ.get('GITHUB_TOKEN')
or subprocess.run(['gh', 'auth', 'token'], capture_output=True, text=True).stdout.strip())
if not token:
raise SystemExit('❌ 拿不到 token:先 gh auth login 或 export GH_TOKEN=<PAT>')
req = urllib.request.Request('https://api.github.com/repos/bkywksj/knowledge-base/actions/runs?per_page=10')
req.add_header('Authorization', f'Bearer {token}')
req.add_header('Accept', 'application/vnd.github+json')
runs = json.loads(urllib.request.urlopen(req).read())
target = next((r for r in runs['workflow_runs'] if r['head_branch'] == 'vX.Y.Z'), None)
if target:
print(f"status={target['status']} conclusion={target['conclusion']} url={target['html_url']}")
else:
print('未找到 vX.Y.Z 对应的 run')
判定逻辑:
status="queued"或"in_progress"→ 用 ScheduleWakeup 睡 270 秒(保持缓存温热),继续轮询status="completed"+conclusion="success"→ 推进到步骤 7conclusion="failure"→ 提示用户去 Actions 页面看 log,停止流程
ScheduleWakeup 调用模板(在自己 prompt 里):
delaySeconds: 270 (在 5 分钟缓存窗口内,避免缓存失效)
prompt: 继续 v1.8.1 发布流程:再查一次 CI workflow 状态,未完成则继续睡 270s,完成则推进到步骤 7(API 下载产物)
⚠️ 不要用
sleep命令阻塞 Bash——会把上下文锁死且无法被中断。 ⚠️ 不要主动让用户去 Actions 页面看——除非 CI 失败需要定位原因。
步骤 7:用 GitHub API 自动下载 13 个产物到 release 仓库
CI 完成后产物在 draft Release 上(tag_name = vX.Y.Z)。取 token 见下(🔴 严禁 git credential fill:本机没存 github.com 凭据时会弹 Git Credential Manager 登录框并阻塞;有 Sigil 就直接用 mcp__sigil__github_release_get / github_download_release_asset,无 Sigil 用 gh),然后用 Accept: application/octet-stream 通过 assets API 直接下到 release 仓库目录。全程零浏览器、零用户操作。
7a. 列产物清单(核对 13 个文件齐全)
# list_assets.py
import urllib.request, json, subprocess
import os
# 🔴 严禁 git credential fill —— 本机没存 github.com 凭据时会弹 GCM 登录框并阻塞
# 路线 A:会话里有 mcp__sigil__* → 直接用 Sigil 能力,根本不用 token
# 路线 B:用 gh 自带 token(不碰系统 credential helper,任何机器都不弹窗)
token = (os.environ.get('GH_TOKEN') or os.environ.get('GITHUB_TOKEN')
or subprocess.run(['gh', 'auth', 'token'], capture_output=True, text=True).stdout.strip())
if not token:
raise SystemExit('❌ 拿不到 token:先 gh auth login 或 export GH_TOKEN=<PAT>')
req = urllib.request.Request('https://api.github.com/repos/bkywksj/knowledge-base/releases?per_page=5')
req.add_header('Authorization', f'Bearer {token}')
req.add_header('Accept', 'application/vnd.github+json')
releases = json.loads(urllib.request.urlopen(req).read())
target = next(r for r in releases if r['tag_name'] == 'vX.Y.Z')
print(f"draft={target['draft']} assets={len(target['assets'])}")
for a in target['assets']:
print(f" {a['name']:60} {a['size']/1024/1024:>8.2f} MB")
CI 实际会上传 17 个 asset(含 latest.json + 几个额外 .sig),但只需要拿其中 13 个:
# Windows (3)
Knowledge.Base_x.y.z_x64-setup.exe
Knowledge.Base_x.y.z_x64-setup.exe.sig
Knowledge.Base_x.y.z_x64-setup.nsis.zip
# macOS (6)
Knowledge.Base_x.y.z_aarch64.dmg
Knowledge.Base_x.y.z_x64.dmg
Knowledge.Base_aarch64.app.tar.gz
Knowledge.Base_aarch64.app.tar.gz.sig
Knowledge.Base_x64.app.tar.gz
Knowledge.Base_x64.app.tar.gz.sig
# Linux (4) —— v1.8.1 起前缀已统一为 Knowledge.Base_(与 Win/macOS 一致)
Knowledge.Base_x.y.z_amd64.deb
Knowledge.Base_x.y.z_amd64.AppImage
Knowledge.Base_x.y.z_amd64.AppImage.tar.gz
Knowledge.Base_x.y.z_amd64.AppImage.tar.gz.sig
⚠️ dmg 带版本号 / macOS app.tar.gz 不带版本号 / Linux AppImage.tar.gz 带版本号(tauri-action 约定)
🔴
RELEASE_WINDOWS=skip档(Windows 本地构建)下清点规则不同: CI 的 draft release 只有 13 个 asset(无上面 Windows 那 3 个 + 无.nsis.zip.sig), 只下载 mac 6 + Linux 4 = 10 个;Windows 3 个由本地构建产物改名补进releases/vX.Y.Z/(本地文件名带空格Knowledge Base_…,必须改成带点的Knowledge.Base_…与其它平台一致;minisign 只验内容不验文件名,改名不影响自动更新校验)。 凑齐后同样是 13 个文件再进入步骤 7c。另外:GitHub Release 页面上挂的 Windows asset 也要换成本地构建版本,否则从 Release 页 下载到的是 CI 的那份。
skip档天然没有这个问题;若这次是build档但仍用本地产物覆盖, 需先删掉 CI 的 4 个 Windows asset 再上传本地版:mcp__sigil__http_request(credential_name=<CI仓凭据>, method=DELETE, injection=bearer,url=https://api.github.com/repos/<owner>/<repo>/releases/assets/<asset_id>)然后mcp__sigil__github_release_asset_upload(...)传本地文件。
7b. 用 API 下载所有 13 个产物到 release 仓库目录
🔴 必须使用
PYTHONIOENCODING=utf-8前缀,否则 Windows GBK 控制台遇到 ✓ 等 unicode 字符会UnicodeEncodeError中断下载。 🔴 不要绕路D:/download/download/——直接下到releases/vX.Y.Z/,省去 cp 步骤。
mkdir -p "E:/my/桌面软件tauri/knowledge-base-release/releases/vX.Y.Z"
cd "E:/my/桌面软件tauri/knowledge_base" && PYTHONIOENCODING=utf-8 python -c "
import urllib.request, json, subprocess, os, sys
import os
# 🔴 严禁 git credential fill —— 本机没存 github.com 凭据时会弹 GCM 登录框并阻塞
# 路线 A:会话里有 mcp__sigil__* → 直接用 Sigil 能力,根本不用 token
# 路线 B:用 gh 自带 token(不碰系统 credential helper,任何机器都不弹窗)
token = (os.environ.get('GH_TOKEN') or os.environ.get('GITHUB_TOKEN')
or subprocess.run(['gh', 'auth', 'token'], capture_output=True, text=True).stdout.strip())
if not token:
raise SystemExit('❌ 拿不到 token:先 gh auth login 或 export GH_TOKEN=<PAT>')
VERSION = 'X.Y.Z'
TARGET = f'E:/my/桌面软件tauri/knowledge-base-release/releases/v{VERSION}'
WANT = [
f'Knowledge.Base_{VERSION}_x64-setup.exe',
f'Knowledge.Base_{VERSION}_x64-setup.exe.sig',
f'Knowledge.Base_{VERSION}_x64-setup.nsis.zip',
f'Knowledge.Base_{VERSION}_aarch64.dmg',
f'Knowledge.Base_{VERSION}_x64.dmg',
'Knowledge.Base_aarch64.app.tar.gz',
'Knowledge.Base_aarch64.app.tar.gz.sig',
'Knowledge.Base_x64.app.tar.gz',
'Knowledge.Base_x64.app.tar.gz.sig',
f'Knowledge.Base_{VERSION}_amd64.deb',
f'Knowledge.Base_{VERSION}_amd64.AppImage',
f'Knowledge.Base_{VERSION}_amd64.AppImage.tar.gz',
f'Knowledge.Base_{VERSION}_amd64.AppImage.tar.gz.sig',
]
req = urllib.request.Request('https://api.github.com/repos/bkywksj/knowledge-base/releases?per_page=5')
req.add_header('Authorization', f'Bearer {token}')
req.add_header('Accept', 'application/vnd.github+json')
releases = json.loads(urllib.request.urlopen(req).read())
target_release = next(r for r in releases if r['tag_name'] == f'v{VERSION}')
assets = {a['name']: a for a in target_release['assets']}
total = len(WANT)
for i, name in enumerate(WANT, 1):
if name not in assets:
print(f'[{i}/{total}] MISSING: {name}', flush=True)
sys.exit(1)
a = assets[name]
out = os.path.join(TARGET, name)
if os.path.exists(out) and os.path.getsize(out) == a['size']:
print(f'[{i}/{total}] skip cached {name}', flush=True)
continue
url = f\"https://api.github.com/repos/bkywksj/knowledge-base/releases/assets/{a['id']}\"
dr = urllib.request.Request(url)
dr.add_header('Authorization', f'Bearer {token}')
dr.add_header('Accept', 'application/octet-stream')
print(f'[{i}/{total}] downloading {name} ({a[\"size\"]/1024/1024:.1f} MB)...', flush=True)
with urllib.request.urlopen(dr) as resp, open(out, 'wb') as f:
while True:
chunk = resp.read(1024 * 256)
if not chunk: break
f.write(chunk)
print(f' OK {name}', flush=True)
print('all done')"
下载约 30–60 秒(取决于网速;Linux AppImage 单文件 ~92 MB)。完成后 releases/vX.Y.Z/ 下应有 13 个文件。
7c. 上传产物到 R2
~/bin/rclone.exe copy "E:/my/桌面软件tauri/knowledge-base-release/releases/vX.Y.Z/" \
"r2:downloads/knowledge-base/vX.Y.Z/" --progress --stats 5s
总大小 ~290 MB,上传约 60 秒(rclone 并发)。
步骤 8:生成两份 update.json(GitHub 版 + R2 版)
🔴 签名注入规则(违反会导致应用内更新签名验证失败)
- 必须用脚本从
.sig文件直接读内容(去掉末尾换行),禁止手动复制粘贴- 三份 JSON(R2/GitHub)只有 URL 的
BASE不同,签名完全相同- 生成后必须验证 JSON 中的签名与 .sig 原始内容完全一致
用 Python 生成(放在 $GITHUB_DIR 执行):
import json
from datetime import datetime, timezone
VERSION = 'x.y.z'
NOTES = '<更新说明>'
GITHUB_BASE = f'https://github.com/bkywksj/knowledge-base-release/raw/main/releases/v{VERSION}'
R2_BASE = f'https://pub-9d9e6c0cb6934fb0a0c505e3c64f39b2.r2.dev/knowledge-base/v{VERSION}'
RELEASE_DIR = f'releases/v{VERSION}'
def read_sig(path):
with open(path, 'r', encoding='utf-8') as f:
return f.read().strip()
def build(base):
return {
'version': VERSION,
'notes': NOTES,
'pub_date': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
'platforms': {
'windows-x86_64': {
'url': f'{base}/Knowledge.Base_{VERSION}_x64-setup.exe',
'signature': read_sig(f'{RELEASE_DIR}/Knowledge.Base_{VERSION}_x64-setup.exe.sig'),
},
'darwin-aarch64': {
'url': f'{base}/Knowledge.Base_aarch64.app.tar.gz',
'signature': read_sig(f'{RELEASE_DIR}/Knowledge.Base_aarch64.app.tar.gz.sig'),
},
'darwin-x86_64': {
'url': f'{base}/Knowledge.Base_x64.app.tar.gz',
'signature': read_sig(f'{RELEASE_DIR}/Knowledge.Base_x64.app.tar.gz.sig'),
},
# Linux x86_64(v1.8.1 起前缀统一为 Knowledge.Base_)
'linux-x86_64': {
'url': f'{base}/Knowledge.Base_{VERSION}_amd64.AppImage.tar.gz',
'signature': read_sig(f'{RELEASE_DIR}/Knowledge.Base_{VERSION}_amd64.AppImage.tar.gz.sig'),
},
# 注意:这里 **没有** android —— 移动端走独立版本线 + 独立的 update-mobile.json,
# 见下方「移动端(Android)独立发布」章节。桌面 update.json 只管 Win/macOS/Linux。
},
}
# GitHub 版 → git 仓库 update.json
with open('update.json', 'w', encoding='utf-8') as f:
json.dump(build(GITHUB_BASE), f, ensure_ascii=False, indent=2)
# R2 版 → 备档到 git(方便排查),同时上传到 R2
with open('update-r2.json', 'w', encoding='utf-8') as f:
json.dump(build(R2_BASE), f, ensure_ascii=False, indent=2)
print('✅ 两份 update.json 已生成')
步骤 9:上传 R2 版 update.json 覆盖 R2 根目录
$RCLONE copy "$GITHUB_DIR/update-r2.json" r2:downloads/knowledge-base/
$RCLONE moveto r2:downloads/knowledge-base/update-r2.json r2:downloads/knowledge-base/update.json
# 验证 R2 可访问
curl -s -o /dev/null -w "R2 update.json HTTP %{http_code}\n" \
"https://pub-9d9e6c0cb6934fb0a0c505e3c64f39b2.r2.dev/knowledge-base/update.json"
步骤 9.5:更新 R2 versions.json(文档站下载页依赖此文件)
文档站 knowledge-base-docs 的 DownloadSection.vue 在构建时(config.ts 顶层 await)
从 R2 拉取 versions.json 并嵌入 bundle(浏览器直连 R2 受 CORS 限制,运行时 fetch 会失败)。
所以每次发布新版本后,必须更新 R2 的 versions.json,文档站重建时才能拿到最新快照。
versions.json 格式(对象数组,含 notes/pub_date):
{
"versions": [
{ "version": "v0.2.1", "notes": "本次更新说明", "pub_date": "2026-04-21T12:34:56Z" },
{ "version": "v0.2.0", "notes": "上次更新说明", "pub_date": "..." }
]
}
# 🔴 Windows Git Bash 下 /tmp 解析为 E:\tmp(不存在),必须用 $TEMP
TMPV=$(cygpath -w "$TEMP" 2>/dev/null || echo "C:/Users/$USERNAME/AppData/Local/Temp")
OLD="$TMPV/versions-old.json"
NEW="$TMPV/versions.json"
# NOTES 与步骤 1 询问用户时的"更新说明"保持一致(可多行,用 \n 分隔)
RELEASE_NOTES="<本次发布说明>"
PUB_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
R2_PUBLIC="https://pub-9d9e6c0cb6934fb0a0c505e3c64f39b2.r2.dev/knowledge-base"
# 下载当前 versions.json(不存在则初始化空数组)
curl -s "${R2_PUBLIC}/versions.json" -o "$OLD" 2>/dev/null \
|| echo '{"versions":[]}' > "$OLD"
# 用 Node 脚本:把旧格式(字符串)规整为对象格式,去重后在头部插入新版本
OLDPATH="$OLD" NEWPATH="$NEW" NOTES="$RELEASE_NOTES" PUB="$PUB_DATE" VER="v${VERSION}" node -e "
const fs = require('fs');
const old = JSON.parse(fs.readFileSync(process.env.OLDPATH, 'utf8'));
const existing = (old.versions || [])
.map(v => typeof v === 'string' ? { version: v } : v)
.filter(v => v.version && v.version !== process.env.VER);
const next = { versions: [
{ version: process.env.VER, notes: process.env.NOTES, pub_date: process.env.PUB },
...existing
] };
fs.writeFileSync(process.env.NEWPATH, JSON.stringify(next, null, 2));
"
# 上传覆盖 R2
~/bin/rclone.exe copyto "$NEW" r2:downloads/knowledge-base/versions.json --progress
# 验证
curl -s "${R2_PUBLIC}/versions.json" | head -c 500
步骤 10:推送 release 仓库(产物 + update.json + update-r2.json)到 GitHub + Gitee
cd "$GITHUB_DIR"
git add -A
git commit -m "release: vx.y.z
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
git pull --rebase origin main
git push origin main # GitHub(remote 名 origin,远端分支 main)
git push gitee main:master # Gitee(远端分支 master;本地 main → 远端 master 的 refspec 映射)
# 如果任一 push 超时(SSL_ERROR_SYSCALL 等),不要重试,提示用户手动执行:
# cd "E:/my/桌面软件tauri/knowledge-base-release" && git push origin main
# cd "E:/my/桌面软件tauri/knowledge-base-release" && git push gitee main:master
Gitee push 比 GitHub 快(国内直连,~10 秒)。如果 GitHub 超时而 Gitee 成功,国内用户的自动更新仍然能从 Gitee endpoint 读取 update.json 并通过 update.json 里的 GitHub/R2 url 下载。
步骤 11:发布 GitHub Release(从 draft → published)
用 GitHub API patch bkywksj token,找到 vX.Y.Z 的 draft Release 并发布:
import urllib.request, json, subprocess
import os
# 🔴 严禁 git credential fill —— 本机没存 github.com 凭据时会弹 GCM 登录框并阻塞
# 路线 A:会话里有 mcp__sigil__* → 直接用 Sigil 能力,根本不用 token
# 路线 B:用 gh 自带 token(不碰系统 credential helper,任何机器都不弹窗)
token = (os.environ.get('GH_TOKEN') or os.environ.get('GITHUB_TOKEN')
or subprocess.run(['gh', 'auth', 'token'], capture_output=True, text=True).stdout.strip())
if not token:
raise SystemExit('❌ 拿不到 token:先 gh auth login 或 export GH_TOKEN=<PAT>')
req = urllib.request.Request('https://api.github.com/repos/bkywksj/knowledge-base/releases')
req.add_header('Authorization', f'Bearer {token}')
releases = json.loads(urllib.request.urlopen(req).read())
target = next((r for r in releases if r['tag_name'] == 'vX.Y.Z'), None)
if target and target['draft']:
body = json.dumps({
'draft': False,
'name': f'Knowledge Base vX.Y.Z',
'body': '<更新说明>\n\n自动更新:R2 CDN(主)+ GitHub raw(备)'
}).encode()
r = urllib.request.Request(f'https://api.github.com/repos/bkywksj/knowledge-base/releases/{target["id"]}',
data=body, method='PATCH')
r.add_header('Authorization', f'Bearer {token}')
r.add_header('Accept', 'application/vnd.github+json')
urllib.request.urlopen(r)
print('✅ Release 已 publish')
步骤 11.5:触发文档站重建(同步 R2 versions.json 到下载页)
为什么需要这步:文档站
knowledge-base-docs的DownloadSection.vue在构建时 从 R2 拉取versions.json并嵌入 bundle(避免运行时 CORS + GitHub 限流/国内慢)。 所以每次发布新版本更新 R2 后,必须触发文档站重新构建(腾讯 EdgeOne Pages 会在 git push 检测到 diff 时自动重建),让下载页拿到最新快照。
⚠️ 文档站仓库 remote 名是 gitee + github(没有 origin),两个都要推。
用 Edit 工具改 .last-release.json(用 cat > ... << EOF 在 Git Bash 也行,但 heredoc 容易踩 cd 持久化坑):
DOCS_DIR="E:/my/桌面软件tauri/knowledge-base-docs"
# 1. 用 Edit 工具把 docs/public/.last-release.json 的 version 改成 vX.Y.Z,published_at 改成当前 UTC
# 2. 提交并双推
cd "$DOCS_DIR" && git add docs/public/.last-release.json && \
git commit -m "chore: 同步 v${VERSION} 发布(触发下载页快照重建)" && \
git push gitee master && git push github master
# 如果任一 push 超时,不要重试,让用户手动执行:
# cd "E:/my/桌面软件tauri/knowledge-base-docs" && git push gitee master
# cd "E:/my/桌面软件tauri/knowledge-base-docs" && git push github master
推送后腾讯 EdgeOne Pages 会自动重新构建文档站,config.ts 里的顶层 await 重新拉取
R2 versions.json,新版本会作为构建时快照嵌入到 download.html。用户打开下载页
一瞬间就能看到最新版本(无需等待运行时 fetch,也不受 CORS 限制)。
步骤 12:完成报告
## v_X.Y.Z_ 发布完成
| 项目 | 值 |
|------|-----|
| 版本 | vX.Y.Z |
| 源码 commit | <hash> (master) |
| Tag | vX.Y.Z |
| CI 状态 | ✅ 成功(Windows + macOS ARM + macOS Intel + Linux x64)|
| GitHub Release | https://github.com/bkywksj/knowledge-base/releases/tag/vX.Y.Z |
| Release 仓库 | <hash> (main) — 13 个产物 + 2 个 update.json |
| R2 CDN | ✅ 13 个产物 + update.json + versions.json 已上传 |
| 文档站(knowledge-base-docs) | ✅ 已推 .last-release.json 触发 EdgeOne Pages 重建,下载页将同步最新版本 |
| 应用内自动更新 | R2 主 + GitHub raw 备,双端点已生效 |
CI 构建说明
工作流文件
.github/workflows/release.yml
触发方式
推送 v*.*.* 格式的 Git Tag 自动触发:
git push github vx.y.z
构建矩阵
矩阵不是写死的,由 prepare-release job 按仓库变量 RELEASE_WINDOWS 动态生成
(skip = 去掉 Windows 那一行,见步骤 5a):
| 平台 | Runner | Bundle 参数 | Updater 产物 | 安装包 | skip 档 |
|---|---|---|---|---|---|
| Windows | windows-latest |
--bundles nsis |
.exe + .exe.sig |
.exe (NSIS) |
❌ 不构建 |
| macOS ARM | macos-latest |
--bundles app,dmg |
.app.tar.gz + .sig |
.dmg (aarch64) |
✅ |
| macOS Intel | macos-latest |
--bundles app,dmg |
.app.tar.gz + .sig |
.dmg (x86_64) |
✅ |
| Linux x64 | ubuntu-22.04 |
--bundles deb,appimage |
.AppImage.tar.gz + .sig |
.deb + .AppImage |
✅ |
macOS 必须
--bundles app,dmg(dmg 单独不产出 updater) Linux Runner 需在 tauri-action 前安装 webkit2gtk-4.1 等系统包(工作流已包含 Install Linux system dependencies 步骤) Linux updater 产物.AppImage.tar.gz仅在createUpdaterArtifacts: "v1Compatible"模式下产生(本项目已配置)
签名说明
- CI 自动用
TAURI_SIGNING_PRIVATE_KEY签名 .sig文件包含在产物中,无需本地签名- Claude 读取
.sig直接注入update.json
便携版(Portable Zip)— 发布时可选附带
本节在每次桌面发布的步骤 7 之后、步骤 8 之前执行,作为可选附加产物。 不影响主流程(R2 + GitHub Release + update.json 还是正常的 NSIS 安装包)。 便携版面向不想装到 Program Files / 想绑死安装目录 / 想 U 盘携带的用户。
触发条件(不强制每次都做)
- 用户明确说"这次顺手出个 portable.zip"
- 大版本(1.x.0)发布
- 默认:小版本(patch)不出 portable.zip,节省时间
portable.zip 的构造原理
应用启动时 resolver 优先级链 env > portable > pointer > default,发现 exe 同级有 portable.txt
就走便携模式:数据写到 <exe同级>/data/(空内容)或 portable.txt 里指定的路径。
所以"做一个便携版" = 把官方 NSIS 安装出来的目录原样打包 + 加一个空的 portable.txt:
KnowledgeBase-Portable-vX.Y.Z/
├── KnowledgeBase.exe
├── kb-mcp.exe
├── resources/
├── ... (NSIS 安装目录里的所有文件)
└── portable.txt ← 空文件,触发便携模式
用户解压到任意目录 → 双击 exe → 数据全部写到
data/子目录,不碰 C 盘 AppData。
制作步骤(每次发布手动操作 ~3 分钟)
在 CI 完成、产物已下载到 releases/vX.Y.Z/ 之后:
# 1. 准备工作目录
PORTABLE_DIR="E:/my/桌面软件tauri/knowledge-base-release/releases/vX.Y.Z/portable"
mkdir -p "$PORTABLE_DIR"
# 2. 用 NSIS 静默安装 + 抽出文件(推荐:直接从已有产物抽,不要装到本机注册表)
# NSIS exe 支持 /D=<dir> 静默装到指定目录,但会写注册表/快捷方式 → 不推荐
# 更干净:直接用 7z 解压 NSIS exe(NSIS 安装包本质是 7z 压缩)
# (需要本机装 7-Zip:scoop install 7zip 或 winget install 7zip)
TMPEXTRACT="$PORTABLE_DIR/_extract"
mkdir -p "$TMPEXTRACT"
"C:/Program Files/7-Zip/7z.exe" x \
"E:/my/桌面软件tauri/knowledge-base-release/releases/vX.Y.Z/Knowledge.Base_X.Y.Z_x64-setup.exe" \
-o"$TMPEXTRACT" -y
# 3. NSIS 7z 内一般有 $PLUGINSDIR / $TEMP 等头部目录,实际程序在 $INSTDIR 对应的子目录
# 抽取后查看实际结构(首次需要人眼对一下;之后稳定后可写死路径)
ls "$TMPEXTRACT"
# 4. 拷贝程序文件 + 加 portable.txt
# (假设抽出来的程序文件在 $TMPEXTRACT 根目录或某子目录,根据 ls 结果调整)
APP_FILES="$TMPEXTRACT" # 或具体子目录
KB_PORTABLE="$PORTABLE_DIR/KnowledgeBase-Portable-vX.Y.Z"
mkdir -p "$KB_PORTABLE"
cp -r "$APP_FILES"/* "$KB_PORTABLE/"
# 删掉 NSIS 卸载器(便携版不需要)
rm -f "$KB_PORTABLE/uninstall.exe"
# 加哨兵文件(空文件 = 数据写到 <exe同级>/data/)
touch "$KB_PORTABLE/portable.txt"
# 5. 打 zip
cd "$PORTABLE_DIR"
"C:/Program Files/7-Zip/7z.exe" a -tzip \
"KnowledgeBase-Portable-vX.Y.Z.zip" \
"KnowledgeBase-Portable-vX.Y.Z/" -mx=9
# 6. 清理临时目录
rm -rf "$TMPEXTRACT" "$KB_PORTABLE"
# 7. 上传 R2(与官方安装包同目录)
~/bin/rclone.exe copy \
"$PORTABLE_DIR/KnowledgeBase-Portable-vX.Y.Z.zip" \
"r2:downloads/knowledge-base/vX.Y.Z/" --progress
# 8. 验证可下载
curl -I "https://pub-9d9e6c0cb6934fb0a0c505e3c64f39b2.r2.dev/knowledge-base/vX.Y.Z/KnowledgeBase-Portable-vX.Y.Z.zip"
README 下载表格补充
在 release 仓库 README 的下载表格里加一行(Windows 平台下):
| Windows x64 (便携版 ⭐) | [KnowledgeBase-Portable-X.Y.Z.zip](releases/vX.Y.Z/KnowledgeBase-Portable-vX.Y.Z.zip) — 解压即用,数据在安装目录 |
注意事项
- ✅ 跟自动更新无关:portable.zip 不接 updater;用户要升级请重新下载 zip 解压覆盖。后续可考虑在 portable 模式下禁用 updater 弹窗(目前 updater 不会自动检测便携模式)
- ✅ 签名/公钥与官方一致:抽 NSIS 出来的 exe 是 CI 签过的,便携包里 exe 哈希与官方一致
- ⚠️ 不要把 portable.zip 推到 GitHub Release:它体积比官方 exe 大(~200MB),GitHub 上传慢;只放 R2
- ⚠️ 不要 push 到 release 仓库:portable.zip 是辅助产物,从 R2 直接分发即可,release 仓库 push 二进制本来就慢
不实现"安装时自动写 portable.txt"
之前讨论过让 NSIS POSTINSTALL 钩子检测旧数据后自动决定是否写 portable.txt—— 已放弃:风险 > 收益。改 installMode 会让升级路径变复杂,需要在虚拟机里跑全套 升级测试。当前的 portable.zip 分发足够覆盖"想绑安装目录"的用户诉求。
移动端(Android)独立发布
桌面和移动端是两条独立的版本线、两条独立的发布管道,互不影响。 桌面 =
tauri.conf.json的版本(1.x)+v*.*.*tag +release.yml+update.json。 移动端 =src-tauri/tauri.android.conf.json的version(从 0.1.0 起)+mobile-v*.*.*tag
android.yml+update-mobile.json。tauri android build时 Tauri 2 自动把tauri.android.conf.json合并覆盖到tauri.conf.json,所以 APK 的 versionName 和编译进去的package_info().version都是这个独立的移动版本号。
移动端发布流程(独立于桌面 /release)
| 步骤 | 操作 |
|---|---|
| M1 本地自检 + 询问版本号/说明 | npx tsc --noEmit;问新移动版本号(读 src-tauri/tauri.android.conf.json 的 version)+ 更新说明 |
| M2 改版本号 | Edit src-tauri/tauri.android.conf.json 的 "version": "x.y.z"(只改这一处,桌面三处不动) |
| M3 提交 + 推 | git add src-tauri/tauri.android.conf.json && git commit -m "release(mobile): vx.y.z ..."(⚠️ commit message 不能含 [skip ci],否则 tag push 也会被跳过);git push origin master && git push github master && git push github2 master |
| M4 打 tag 触发 CI | git tag mobile-vx.y.z && git push <CI 远端> mobile-vx.y.z(CI 远端 = github 或配额耗尽时 github2)→ 触发 android.yml release 路径(正式签名 APK + AAB) |
| M5 ScheduleWakeup 轮询 CI | 同桌面步骤 6,盯 Android Build run(head_branch=mobile-vx.y.z),~20–30 分钟 |
| M6 下载产物 | CI 完成后产物在 GitHub Release 草稿(tag = mobile-vx.y.z)+ Actions Artifact(名 knowledge-base-android-release-vx.y.z)。从 Release assets 或 Artifact zip 取 Knowledge.Base_x.y.z_android-arm64.apk + .aab,下到 releases/mobile-vx.y.z/(release 仓)。校验 APK 是 release 体积(~20–120MB,不是 ~345MB 的 debug) |
| M7 上传 R2 | ~/bin/rclone.exe copy "releases/mobile-vx.y.z/Knowledge.Base_x.y.z_android-arm64.apk" r2:downloads/knowledge-base/mobile-vx.y.z/;.aab 同样 |
M8 生成 update-mobile.json + update-mobile-r2.json |
扁平结构(无 minisign signature,Android 自己验 APK 签名):{"version":"x.y.z","notes":"...","pub_date":"...","url":"<base>/Knowledge.Base_x.y.z_android-arm64.apk"}。两份只有 url 的 base 不同:GitHub 版 base = https://github.com/bkywksj/knowledge-base-release/raw/main/releases/mobile-vx.y.z,R2 版 base = https://pub-...r2.dev/knowledge-base/mobile-vx.y.z。写到 release 仓根目录 |
M9 上传 R2 update-mobile.json |
~/bin/rclone.exe copy update-mobile-r2.json r2:downloads/knowledge-base/ → ~/bin/rclone.exe moveto r2:downloads/knowledge-base/update-mobile-r2.json r2:downloads/knowledge-base/update-mobile.json;curl 验证 200 |
M9.5 更新 R2 mobile-versions.json |
文档站下载页的 📱 banner 读这个(构建时由 docs 的 config.ts 拉,类似桌面 versions.json)。curl 拉 https://pub-...r2.dev/knowledge-base/mobile-versions.json(不存在则初始化 {"versions":[]})→ 去重后头部插入 {version:"mobile-vx.y.z", notes:..., pub_date:now UTC, apk_url:".../mobile-vx.y.z/Knowledge.Base_x.y.z_android-arm64.apk", aab_url:".../...aab"}(apk_url/aab_url 用 R2 base)→ ~/bin/rclone.exe copyto 覆盖 r2:downloads/knowledge-base/mobile-versions.json;顺手 ~/bin/rclone.exe copyto 把 APK 也覆盖到 r2:downloads/knowledge-base/mobile-latest.apk(稳定链接,mobile.md 指向它) |
| M10 更新 README + 推 release 仓 | 在 README 的「移动端(Android)」区块加下载行 + 移动端版本历史条目;git add -A && git commit -m "release(mobile): vx.y.z" && git pull --rebase origin main && git push origin main && git push gitee main:master(Gitee 失败跳过,已知 fork 分叉) |
| M11 publish GitHub Release | 把 mobile-vx.y.z 的 draft Release 改 draft:false(PATCH,name=知识库 移动端 vx.y.z,body=更新说明)。注意 CI 在哪个仓就 publish 哪个仓的 |
| M12 触发文档站重建 | 改 docs 仓 docs/public/.last-release-mobile.json({version:"mobile-vx.y.z", published_at:now UTC})→ git add docs/public/.last-release-mobile.json && git commit -m "chore: 同步移动端 vx.y.z" && git push gitee master && git push github master(docs 仓 remote 只有 gitee+github),EdgeOne 自动重建 → config.ts 重新拉 R2 mobile-versions.json → 📱 banner 显示新版本 |
| M13 完成报告 | 移动版本 / 源码 commit / tag / CI / release 仓 commit / R2(含 mobile-versions.json + mobile-latest.apk)/ 自动更新(update-mobile.json)/ 文档站 |
移动端在 R2 上的 3 个关键文件:
update-mobile.json(App 内检查更新读它,单条最新)/mobile-versions.json(文档站下载页 📱 banner 读它,历史数组带 apk_url/aab_url)/mobile-latest.apk(稳定下载链接,mobile.md 指向它)。三个每发一次移动端都要更新。
配合 App 内"检查更新"
App 的 check_mobile_update 命令(src-tauri/src/commands/mobile_update.rs,#[cfg(mobile)])
按 R2 → Gitee → GitHub 顺序拉 update-mobile.json,比对 version 字段与 package_info().version,
有新版本就把 url 给前端 → openUrl 浏览器下载 APK → 用户点一下进系统安装器(首次需在系统里允许"安装未知应用")。
🔴 签名一致性铁律:同一条移动版本线的所有 APK 必须用同一个
kb-release.jks签名, 否则用户"检查更新→装新 APK"会因签名不匹配失败(INSTALL_FAILED_UPDATE_INCOMPATIBLE,只能卸载重装丢数据)。 debug APK(SDK 自带 keystore)与 release APK 签名不同;首次从 debug 换 release 需先卸载再装,之后 release→release 才一致。
versionCode 注意
Tauri 把 Android versionCode 从 version 推导(major*1000000 + minor*1000 + patch,例 0.1.0→1000、0.2.0→2000)。
只要移动版本号一直往上走就没问题。永远不要把移动版本号往回调(versionCode 倒退 → 用户装不上,报 INSTALL_FAILED_VERSION_DOWNGRADE)。
密钥管理
当前配置
- 私钥:
src-tauri/keys/tauri-updater.key(已 gitignore) - 公钥:
src-tauri/tauri.conf.json的plugins.updater.pubkey - GitHub Secrets:
TAURI_SIGNING_PRIVATE_KEY已配置
重新生成密钥(仅在泄露时)
pnpm tauri signer generate -w src-tauri/keys/tauri-updater.key
# 密码提示时直接按两次回车(空密码)
重新生成后必须:
- 更新
tauri.conf.json的pubkey(从.key.pub读) - 更新 GitHub Secrets 的
TAURI_SIGNING_PRIVATE_KEY(从.key读) - 重新构建发布新版本(旧签名对新公钥无效,但已安装用户仍能升级)
安全提醒
- 私钥绝不能进公开仓库,
src-tauri/keys/已 gitignore - R2 credentials 在
~/.config/rclone/rclone.conf,不要放入项目
Cloudflare R2 CDN 配置
当前配置
| 项 | 值 |
|---|---|
| Bucket | downloads(APAC 区域) |
| 公开地址 | https://pub-9d9e6c0cb6934fb0a0c505e3c64f39b2.r2.dev |
| 目录结构 | downloads/knowledge-base/vX.Y.Z/ + downloads/knowledge-base/update.json |
| rclone remote | r2:downloads/ |
R2 目录规划(Bucket 共享)
downloads/ ← Bucket 根(与 aicoder 共享)
├── aicoder/ ← 智码 AICoder(其他项目)
└── knowledge-base/ ← 本项目
├── vX.Y.Z/ ← 版本产物
│ ├── Knowledge.Base_X.Y.Z_x64-setup.exe
│ ├── ...
└── update.json ← Tauri 自动更新端点
R2 成本(永久免费额度内)
| 项目 | 免费额度 | 实际用量 |
|---|---|---|
| 存储 | 10 GB/月 | ~300MB(20 版本) |
| 出站流量 |
…(truncated)