现象: 项目设置 -> 归档设置 -> 隐藏的任务自动归档, 选完档位提示「已保存」,
重新打开仍是「永不」, 且不会产生任何归档。
根因 (读写两条路都断了, 缺一不可):
1. 读: projectSummary() 没有回传 autoArchive, 而 ProjectManage 读的是
project.autoArchive -> 永远是 undefined -> 界面恒显示默认值「永不」;
2. 写: projectApi.update() 的字段白名单里没有 autoArchive 分支
(只有 name/code/taskKinds/icon/archived/notes), 传进来直接被忽略,
data 为空 -> 一条 SQL 都不执行 -> 但函数正常返回, 调用方照常弹「已保存」。
服务端一直是对的: powersync 上传分支处理了 autoArchive, REST 也有校验,
所以不是同步丢字段。
修复:
- projectSummary 增加 autoArchive: p.autoArchive || 'never';
- projectApi.update 增加 autoArchive 分支, 校验取值与 AUTO_ARCHIVE_OPTIONS 同口径。
验证 (真实 Windows 外壳, 临时暴露 API 直调被修改的函数):
- update({autoArchive:'3d'}) 返回 3d; 重新 list/detail 仍为 3d (已落库);
- 进项目设置, 该行显示「一周后 ›」(设成 1w 后), 不再恒为「永不」;
- 还原为 never 后界面同步回「永不」。
- 顺带确认「同样隐藏已完成」的列级设置 retention 读写都正常:
Done 列 retention=3d, 服务端 26 条已完成中 9 条超 3 天,
App 实际只返回 14 条 -> 过滤本身是生效的, 此前不生效的只有自动归档档位。
- 控制台无异常; 验证用的临时钩子已移除。
iBoard
跨平台个人看板 (Tauri + React + Express + MariaDB)。用于收集和整理经济数据,但是以策略为主体重新组织。
- 首页主要是放经济文章
- 数据主要是放各种经济数据和图表
- 策略是放按照经济理论组织的相关数据和图表的对比
首页
首页显示新闻模块和报警模块的数据,按照时间最新优先,无限滚动,直到数据库中没有数据为止。
新闻模块
增加一个新闻模块,数据来源是每日夜间 3 点,通过妙想 MCP 获取国内外最新经济政治新闻,按影响大小排序 10 条。存入数据库。
报警模块
World Bank 中的数据只用来对比妙想 MCP 拉取的数据正确性。当同一时间的同一项数据不同时,记录一个报警。
监控
监控分为服务器监控和服务监控两部分。服务器监控对接 komari,地址:https://komari.honor3.com/
分支规则
分支类型与职责
| 分支 | 职责 | 部署 | 合并方向 |
|---|---|---|---|
master |
主分支(生产分支) | ✅ 唯一部署出口,push 即发布生产 | — |
dev-* |
开发集成分支,如 dev-0.2 |
❌ 不部署(只跑 CI 校验) | 合到 master 后由 master 部署 |
tmp-* |
临时分支,针对单个问题的修改 | ❌ 不部署 | 合到对应 dev-* 后删除 |
android-* |
Android 外壳构建分支 | ❌ 不部署,只触发 APK 构建 | 构建前须与 master 同步 |
关于
main:团队约定里的「main 主分支」在本仓库落地为master(仓库默认分支即master,不另建main)。 下文所有「main 主分支」均指master。
流程
tmp-xxx ──合并──▶ dev-0.2 ──合并──▶ master ──push──▶ 自动部署生产
│ │
│ └──push──▶ 只跑 CI(版本一致性校验),不部署
│
└── 问题解决后删除
android-1.0 ── 从 master 同步代码后 push ──▶ 只触发 APK 构建 / 发布到 Packages
- 日常开发:从
dev-*拉tmp-*,改完合回dev-*,tmp-*用完即删。 - 上线:
dev-*验收通过后合并到master,推送master才部署生产。 - 打 Android 包:
android-*与master同步(git merge master或git reset --hard origin/master)后推送, 触发 android-build.yml,产物发布到 Gitea Generic Packages。
CI 触发范围
| 工作流 | 触发分支 | 动作 |
|---|---|---|
| docker-build.yml | 仅 master |
构建前端 + 生成签名 OTA 包 + 构建镜像 + 迁移数据库 + 部署生产 |
| android-build.yml | android-* |
构建并签名 release APK,上传到 Packages |
| ota-build.yml | 仅手动 workflow_dispatch |
单独构建/签发 OTA 包 |
| sync-registry.yml | 见文件 | 同步注册中心 |
⚠️ 重要变更(2026-10):
dev-*分支已取消 push 自动部署。 此前dev-*与master共用同一套生产环境,推送开发分支等于直接发布,容易把半成品带上线。 现在开发分支的提交只有合并进master并推送后才会部署。
日常操作规则(工作目录约定)
-
主工作目录(
iboard/)默认停留在dev-0.2,所有修改都在dev-0.2上提交;dev-0.2可随时 push(只跑 CI 校验,不部署)。 -
独立功能开发使用独立分支 + 独立工作目录(如离线编辑在
iboard-dev-0.2.1/的tmp-offline-edit分支),不占用dev-0.2。 -
发布流程(仅在明确需要发布时执行,结束后立刻回到
dev-0.2):git switch master git merge --ff-only dev-0.2 git push origin master # push 即部署生产 git switch dev-0.2
版本管理
版本号构成与口径(2026-10-25 决议)
V MAJOR.MINOR.PATCH 例: V0.2.0
│ └── PATCH 小版本号: 仅在明确提出时自增, 否则沿用 (记录见 CHANGELOG.md)
└── MINOR 大版本迭代, 对应一个开发分支 (V0.2 ↔ dev-0.2)
前端与外壳解绑,独立维护、独立发版(2026-10-25 决议):
| 组 | 版本 | 管理点 | 产物 | 发版节奏 |
|---|---|---|---|---|
| 前端 (Web/OTA) | 0.2.0 |
package.json 的 version(唯一手动管理点,只写裸版本号,不允许带 -ota) |
前端运行时 APP_VERSION = 0.2.0;OTA 包 version = 0.2.0-ota |
随 master push 高频发版 |
| 外壳 | 0.2.0 |
src-tauri/tauri.conf.json 的 version(须与 Cargo.toml/Cargo.lock 组内一致) |
APK versionName、桌面安装包版本;后续 Windows 外壳同理 | 独立分支(如 android-*)低频发版 |
- 两组版本号不要求相等,CI(
scripts/ci/check-version.mjs)只校验组内一致 + 前端版本与dev-<major>.<minor>分支前缀对应。 - 分支口径:dev-0.2 开发,发布一律走 master。
build 标识
- 格式统一为
YYYYMMDDHHmmss(北京时间):- 前端
BUILD_ID:vite 编译时生成(vite.config.js#makeBuildId),无需手动维护; - OTA 包
build_id:打包时按同一格式生成(scripts/build-ota.mjs)。
- 前端
- OTA 包的
sequence(Unix 毫秒,单调递增)保留,仅用于判新与热插拔协议,不再用于展示。
PATCH 小版本号
- 仅在明确提出时自增(改
package.jsonversion 第三位),否则一直沿用。 - 每次自增在 CHANGELOG.md 追加一行(patch 号 / 日期 / commit / 要点)。
改版本号时要同步的位置
| 组 | 文件 | 字段 | 说明 |
|---|---|---|---|
| 前端 | package.json |
version |
前端唯一源,src/version.js 与 OTA 打包脚本均读取此值 |
| 外壳 | src-tauri/tauri.conf.json |
version |
APK 的 versionName、桌面安装包版本 |
| 外壳 | src-tauri/Cargo.toml |
[package].version |
Rust 包版本,须与 tauri.conf.json 一致 |
| 外壳 | src-tauri/Cargo.lock |
name = "iboard" 条目 |
随 Cargo.toml 一起改 |
APK 版本号是怎么来的
tauri android init 会按 src-tauri/tauri.conf.json 的 version 生成
src-tauri/gen/android/app/tauri.properties(该目录被 gitignore,CI 每次 android init 重新生成):
tauri.android.versionName = 0.2.0 # 人类可见版本,与 tauri.conf.json 一致
tauri.android.versionCode = 2000 # 整数,升级判定用 = major*1000000 + minor*1000 + patch
注意:
versionCode必须单调递增才能覆盖安装,所以外壳版本号只能往上走(0.2.0→ 2000 >0.1.0→ 1000)。- 本地如果 APK 装完显示旧版本号,是
src-tauri/gen/android缓存没刷新, 删掉该目录重新npx tauri android init即可。
其它
src/version.js从package.json取值,前端运行时与 OTA 检查接口展示的即此版本,不要另写死版本号。min_binary_version与这里的版本号是两件事:它是 OTA 包要求的最低原生二进制版本 (CI 里由OTA_MIN_BINARY显式指定,当前0.1.0)。前端版本升到 0.2.0 时它不应跟着抬, 否则装了 0.1.0 APK 的设备会直接收不到热更。ota-package/latest.json为运行时产物,不纳入版本管理。
"我的"页版本展示口径
| 行 | 展示 |
|---|---|
| 当前版本 | v0.2.0 · build 20261025204300(前端 APP_VERSION + BUILD_ID 原始格式) |
| 运行资源 | OTA 覆盖包版本 / 内置 |
| 新版本 | v0.2.0-ota · build 20261025210000(OTA 包 build_id) |
| 发布说明 | 触发构建的 commit message |
| 提交 | commit sha 前 10 位 |
| 页面底部 | 外壳 v0.2.0(Tauri API 读取;Web 端显示 —) |
前端 OTA 升级
只升级前端资源 (dist/),不触碰 Tauri 框架/原生层,不重打 APK。
底层使用 tauri-plugin-hotswap
(签名/回滚/资源替换全部由插件处理)。
工作流
开发者 服务端
npm run build ──┐
scripts/build-ota.mjs │ 生成 iboard-ota.tar.gz + minisign 签名
│ 输出 latest.json (sequence/version/...)
▼
ota-package/ ── 部署到服务器 ──▶ /srv/iboard/ota-package
│
▼
GET /api/ota/check/<seq>
客户端 (Tauri) │
启动 ─▶ notifyReady() ▼
我页 ─▶ checkUpdate() ◀──── 200 + manifest (含 url/signature) ─────┘
applyUpdate() ─▶ 下载 tar.gz ─▶ minisign 校验 ─▶ 解压到 AppData/ota-dist
location.reload() ─▶ 后续资源从 tauri://localhost 优先命中覆盖包
发布说明与提交号(2026-10-25 起)
- CI(docker-build.yml)把触发构建的 commit message(标题+正文,去空行,截断 400 字)作为
notes写入 latest.json; - commit sha 经
--commit参数单独写入commit字段; - "我的"页:发布说明展示 commit message,「提交」行展示短 sha。
首次部署
- 生成签名密钥 (一次性):
# 安装 minisign: Windows (scoop) / macOS (brew) / Linux (apt) node scripts/ota-genkey.mjs # 产物: ota-package/hotswap.{key, pub} - 把公钥写入
src-tauri/tauri.conf.json的plugins.hotswap.pubkey字段(脚本会打印)。 - 配置 endpoint (
src-tauri/tauri.conf.json):"hotswap": { "endpoint": "https://your-server/api/ota/check/{{current_sequence}}", "pubkey": "RWR...", "require_https": true } - 服务端环境变量
OTA_PACKAGE_DIR指向ota-package/所在目录。
发布新版前端
npm run build # 生成 dist/
node scripts/build-ota.mjs # 打成 tar.gz + 签名 + latest.json
# sequence 默认 = Date.now(), 也可 --sequence 43 手动指定
# 也可加 --notes "修复登录按钮" --commit <sha> --min-binary 0.1.0
然后把 ota-package/ 下两个文件 (iboard-ota.tar.gz + latest.json) 同步到
服务端 OTA_PACKAGE_DIR 指向的目录。sequence 单调递增即可。
客户端使用
打开 App 的"我"页,点 [检查更新] → [立即升级] 即可。失败会自动回滚到内置 dist。
注意: Tauri 2 的 dev 模式 (
tauri dev) 会代理到 Vite dev server,OTA 不生效。 OTA 仅在 release/打包构建中可用,测试请用tauri build产物。
客户端判新口径(V0.2)
"我"页 OTA 组内有一个 开发版本 开关(默认关闭),切换"怎么算新版本":
| 开关 | 判新依据 | 适用 |
|---|---|---|
| 关(默认) | 版本号逐字段比大小(0.2.0-ota > 0.1.0 才算新) |
生产。旧分支被重建(sequence 变大但版本号更小)时不会把设备拉回旧版 |
| 开 | build sequence(服务端 sequence 比本地大就算新) | 开发联调。每次构建都能推到设备 |
- 开关值持久化在设备端
localStorage.iboard_ota_dev_mode('1'/'0'),默认'0'。 - 版本比较在
src/utils/version.js(纯函数,src/api/ota.js统一封装两种模式)。 min_binary_version与应用版本是两件事:前者是前端 bundle 要求的最低原生二进制版本, 当前 APK 原生层仍是0.1.0,所以 CI 里固定传--min-binary 0.1.0(见docker-build.yml的OTA_MIN_BINARY)。只改前端版本号时不要跟着抬这个值,否则旧 APK 会收不到更新。
Web 端
Web 浏览器访问 (同源) 时,服务端返回简化版 manifest,仅显示"有新版本请刷新"。 Web 端不会自动下载包,也不会替换前端代码。
CI 自动签名 (Gitea Actions)
.gitea/workflows/ota-build.yml 是手动触发的 OTA 打包工作流(每次发版前点一次)。
配置 secret (Gitea 仓库 -> Settings -> Actions -> Secrets):
- MINISIGN_PRIVATE_KEY: 完整 .key 文件内容,整段(含首行
untrusted comment: minisign encrypted secret key和 base64 行),整段作为多行 secret 粘贴即可。
使用流程:
- 在 Gitea Actions 页面选 OTA Frontend Build & Sign -> Run workflow
- 填 sequence(可选,不填用时间戳)和 notes(可选)
- CI 跑完会在 artifact 里看到 ota-package/iboard-ota.tar.gz + latest.json + iboard-ota.tar.gz.minisig
- 下载 artifact,scp 到服务器 OTA_PACKAGE_DIR 目录
- 客户端"我"页 [检查更新] 即可触发升级
注意:
- CI 用的私钥必须与
tauri.conf.json里 plugins.hotswap.pubkey 配对;换密钥时需要同时更新 CI secret 和 conf。 - 不要把私钥 commit 到 git,整个 ota-package/hotswap.key 已被 .gitignore 排除。
数据库升级约定
- 唯一入口是
prisma/migrations/下的迁移文件,由npx prisma migrate deploy在启新容器之前执行 (幂等,无新迁移时为 no-op);迁移失败会中断 workflow,不会用新镜像顶掉仍在运行的旧容器。 - 由于只有
master会部署,迁移真正作用到生产库的唯一时机就是 master 发布; 开发分支上的迁移不会被自动应用,请在合并到master前确认可回滚。 - 本地改了
prisma/schema.prisma后:写迁移 SQL →npx prisma migrate deploy验证 → 提交迁移目录。
Windows 本地实时预览(不打包)
改完前端 UI 想立刻在 Windows 外壳里看效果,不必 tauri build、不必打 NSIS/便携包、也不必重装。
npm run win:dev # 构建(shell 模式) + 挂载 + 重启外壳
npm run win:dev -- --no-build # 复用现有 dist, 只挂载+重启 (最快, 约 2 秒)
npm run win:dev -- --no-launch # 只挂载, 自己手动启动外壳
npm run win:dev -- --exe "D:\path\iBoard.exe"
npm run win:dev -- --clear # 卸载覆盖, 外壳回到内置前端
原理
外壳用 tauri-plugin-hotswap,启动时先看覆盖目录有没有生效的前端包,没有才回退到 exe 内置的 dist。
这和线上 OTA 走的是同一条路径,所以只要把新构建的 dist 写进覆盖目录再重启外壳即可:
%APPDATA%\com.iboard.app\hotswap\
current ← 文本文件, 内容为生效的目录名 (如 seq-1793068631298)
seq-1793068631298\ ← 本次前端 (dist 内容) + hotswap-meta.json
hotswap-meta.json 里有两个字段是硬约束,改错会导致覆盖包被丢弃或立刻回滚:
| 字段 | 取值 | 原因 |
|---|---|---|
min_binary_version |
必须等于外壳版本(当前 0.2.1) |
默认策略 DiscardOnUpgrade 在 binary > min 时丢弃缓存;安全线又要求 binary >= min,故只能取等号 |
confirmed |
必须为 true |
默认策略 SingleLaunch 对未确认版本会在启动时立即回滚 |
另外 sequence 取 Date.now()(大值),让 OTA 检查认为"已是最新",避免线上更新把本地版本顶掉。
注意
- 覆盖目录就是外壳真实的 OTA 落地目录,挂载期间"我的"页会显示本地版本(
0.2.1-local); 测完建议npm run win:dev -- --clear复原,避免影响后续正常 OTA。 - 脚本按
src-tauri/tauri.conf.json的identifier推导覆盖目录,换 identifier 后无需改脚本。 - 这只适用于前端改动。改了
src-tauri/(Rust/原生层、权限、CSP)必须重新打包外壳。 - 想要带 HMR 的完整开发循环(改代码即时刷新、免手动重启)用
npm run tauri:dev, 代价是首次要编译一次 debug 版 Rust 二进制。