2026-11-09 16:25:49 +08:00
2026-08-03 18:09:08 +08:00
2026-08-07 17:10:07 +08:00
2026-04-25 19:48:19 +08:00
2026-08-07 14:36:57 +08:00
2026-08-07 14:36:57 +08:00
2026-08-03 18:09:08 +08:00

iBoard

跨平台个人看板 (Tauri + React + Express + MariaDB)。用于收集和整理经济数据,但是以策略为主体重新组织。

  • 宏观:新闻 / 经济文章(本地),以及外接 ifinance 模块的分类(经济数据 / 策略研究 / 看板,运行时动态挂载为 tab)
  • 数据与策略由外接 ifinance 模块承担(静态包,见 ../ifinance/readme.md)
  • 报警并入「监控」;油价模块已下线
  • AI 接入:内置 MCP 服务,个人 Token 授权后可查询 / 新增 / 修改 / 删除 / 排序项目、任务、清单和打卡(见 docs/MCP.md)

宏观

宏观页固定「新闻 / 经济文章」两个本地 tab,并动态挂载外接 ifinance info.json 的 categories[] 作为额外 tab:

  • 新闻:每日夜间 3 点通过妙想 MCP 获取国内外最新经济政治新闻,按影响大小排序 10 条,存入数据库;列表按时间倒序无限滚动。
  • 经济文章:由 content/articles/*.md 承载的长文。
  • 经济数据 / 策略研究 / …:交给外接 ifinance 模块,宿主只读取 info.json 并用 iframe 打开条目页(纯前端直连,见 ifinance/readme.md §9)。iBoard 本地不再保存指标 / 策略数据。

监控

监控分为主机监控 / 服务监控 / 环境监控 / 报警四个 tab。主机监控对接 komari:https://komari.honor3.com/;报警 tab 展示跨源数据差异与定时任务异常,按时间倒序。


分支规则

分支类型与职责

分支 职责 部署 合并方向
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
  1. 日常开发:从 dev-* 拉 tmp-*,改完合回 dev-*,tmp-* 用完即删。
  2. 上线:dev-* 验收通过后合并到 master,推送 master 才部署生产。
  3. 打 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.json version 第三位),否则一直沿用。
  • 每次自增在 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。

首次部署

  1. 生成签名密钥 (一次性):
    # 安装 minisign: Windows (scoop) / macOS (brew) / Linux (apt)
    node scripts/ota-genkey.mjs
    # 产物: ota-package/hotswap.{key, pub}
    
  2. 把公钥写入 src-tauri/tauri.conf.json 的 plugins.hotswap.pubkey 字段(脚本会打印)。
  3. 配置 endpoint (src-tauri/tauri.conf.json):
    "hotswap": {
      "endpoint": "https://your-server/api/ota/check/{{current_sequence}}",
      "pubkey": "RWR...",
      "require_https": true
    }
    
  4. 服务端环境变量 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 粘贴即可。

使用流程:

  1. 在 Gitea Actions 页面选 OTA Frontend Build & Sign -> Run workflow
  2. 填 sequence(可选,不填用时间戳)和 notes(可选)
  3. CI 跑完会在 artifact 里看到 ota-package/iboard-ota.tar.gz + latest.json + iboard-ota.tar.gz.minisig
  4. 下载 artifact,scp 到服务器 OTA_PACKAGE_DIR 目录
  5. 客户端"我"页 [检查更新] 即可触发升级

注意:

  • 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 二进制。
S
Description
No description provided
Readme
26 MiB
Languages
TypeScript 68.6%
JavaScript 25.6%
CSS 2.7%
Rust 1.7%
Shell 0.9%
Other 0.4%