# iBoard 跨平台个人看板 (Tauri + React + Express + MariaDB)。用于收集和整理经济数据,但是以策略为主体重新组织。 - 首页主要是放经济文章 - 数据主要是放各种经济数据和图表 - 策略是放按照经济理论组织的相关数据和图表的对比 ## 首页 首页显示新闻模块和报警模块的数据,按照时间最新优先,无限滚动,直到数据库中没有数据为止。 ### 新闻模块 增加一个新闻模块,数据来源是每日夜间 3 点,通过妙想 MCP 获取国内外最新经济政治新闻,按影响大小排序 10 条。存入数据库。 ### 报警模块 World Bank 中的数据只用来对比妙想 MCP 拉取的数据正确性。当同一时间的同一项数据不同时,记录一个报警。 ## 监控 监控分为服务器监控和服务监控两部分。服务器监控对接 komari,地址: --- ## 分支规则 ### 分支类型与职责 | 分支 | 职责 | 部署 | 合并方向 | | --- | --- | --- | --- | | `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/workflows/android-build.yml),产物发布到 Gitea Generic Packages。 ### CI 触发范围 | 工作流 | 触发分支 | 动作 | | --- | --- | --- | | [docker-build.yml](.gitea/workflows/docker-build.yml) | **仅 `master`** | 构建前端 + 生成签名 OTA 包 + 构建镜像 + 迁移数据库 + 部署生产 | | [android-build.yml](.gitea/workflows/android-build.yml) | `android-*` | 构建并签名 release APK,上传到 Packages | | [ota-build.yml](.gitea/workflows/ota-build.yml) | 仅手动 `workflow_dispatch` | 单独构建/签发 OTA 包 | | [sync-registry.yml](.gitea/workflows/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`)**: ```bash 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-.` 分支前缀对应。 - 分支口径:**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](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`](https://github.com/denniskribl/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/ 客户端 (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. **生成签名密钥** (一次性): ```bash # 安装 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`): ```json "hotswap": { "endpoint": "https://your-server/api/ota/check/{{current_sequence}}", "pubkey": "RWR...", "require_https": true } ``` 4. **服务端环境变量** `OTA_PACKAGE_DIR` 指向 `ota-package/` 所在目录。 ### 发布新版前端 ```bash npm run build # 生成 dist/ node scripts/build-ota.mjs # 打成 tar.gz + 签名 + latest.json # sequence 默认 = Date.now(), 也可 --sequence 43 手动指定 # 也可加 --notes "修复登录按钮" --commit --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` 验证 → 提交迁移目录。