Files
iboard/README.md
T
jif2.zhang afbb9bc603
Docker Build and Push / build-image (push) Successful in 2m37s
feat(宏观): ifinance 分类动态挂载为宏观 tab, 报警迁入监控, 下线油价/本地经济数据/策略研究
- 宏观只保留本地 新闻/经济文章, 外接 ifinance categories[] 动态成为 tab (纯前端直连, CORS/_headers, https)
- 报警从宏观迁到监控第 4 个 tab (保留 Alert 模块, 供未来新增报警源)
- 彻底删除油价/经济数据/策略研究: 前端组件+api、后端 apizero/data 路由、cron、fetchers、种子数据
- 新增迁移 DROP Indicator/IndicatorPoint/Strategy/OilPriceHistory; 重新生成 Prisma Client
- CSP 放开 *.honor3.com (含端口通配), 避免新增子域反复重装外壳
2026-10-30 17:17:10 +08:00

317 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# iBoard
跨平台个人看板 (Tauri + React + Express + MariaDB)。用于收集和整理经济数据,但是以策略为主体重新组织。
- 宏观:新闻 / 经济文章(本地),以及外接 ifinance 模块的分类(经济数据 / 策略研究 / 看板,运行时动态挂载为 tab)
- 数据与策略由外接 ifinance 模块承担(静态包,见 `../ifinance/readme.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/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-<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](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/<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. **生成签名密钥** (一次性):
```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 <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/便携包、也不必重装。
```bash
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 二进制。