Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/updates-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,13 @@ M1 之后 M2 → M3 与 M4 并行;M5.1、M5.2 与 tinyui 0.3.0(npm 三包 +
| 5 | 签名私钥 | GitHub secret + `HarlonWang/secrets` 备份一份,本机生成后即删 |
| 6 | `installId` | 复用 TrendingAI 的 `getOrCreateInstallId()`,`Updates` 事件接进埋点 |
| 7 | 命名 | 包名 `trendingai`(页面 `trendingai/subscription`),线上 app id `trendingai`;宿主层面标识页面(深链、埋点页面名、日志)用 `tinyui:trendingai/subscription`,`navigation.push` 仍用不带前缀的 |
| 8 | JS 侧填错目标宿主版本 | 发布前核对(updates.md §1.3):`tinyui build` 把每页用到的能力名与宿主组件名写进 manifest 的 `requires`;`bundle` 从热下发服务读目标版本的宿主快照比对,不通过即拒绝出包。快照由宿主 CI 在发 App 版本时上传,每版本只写一次。铁律:`host.call` 的名字必须是字面量、宿主组件必须能静态解析(build-chain.md §5.1)。设备侧核对暂不做 |
| 8 | JS 侧填错目标宿主版本 | 发布前核对(updates.md §1.3):`tinyui build` 把每页用到的能力名与宿主组件名写进 manifest 的 `requires`;`publish` 上传前从热下发服务读目标版本的宿主快照比对,不通过即拒绝发布(`bundle` 保持离线)。快照由宿主 CI 在发 App 版本时上传,每版本只写一次。铁律:`host.call` 的名字必须是字面量、宿主组件必须能静态解析(build-chain.md §5.1)。设备侧核对暂不做 |
| 9 | 服务端凭据分层 | 在实例 `ADMIN_TOKEN` 与包的发布 token 之间补 app token:一个 app 的完整管理权(注册包、公钥、签发发布 token、上传宿主快照),暂不细分权限范围;宿主 CI 用它上传快照;本 app 的发布 token 可读快照。权限与 URL 挂在 app(一个宿主 App)上,不挂在租户(`org`,可改可转让)上(updates.md §6) |

交付:

- **tinyui**:`tinyui build` 静态找出每页用到的能力与宿主组件写进 manifest 的 `requires`,违反 build-chain.md §5.1 即失败;`tinyui bundle` 从服务端读目标宿主版本的快照核对(updates.md §1.3);宿主契约快照——导出宿主组件 schema、能力名、tinyui 版本为稳定排序的文本,存 `tinyui-host/<hostVersion>.txt`;检查任务(当前宿主与快照不符即失败)与生成任务(已随发版带出的版本拒绝覆盖),任务名做时定。前置:宿主能力能集中枚举(TrendingAI 现在散在各 Screen 里注册)。`tinyui:` 前缀约定写进 app-model.md
- **trendingai-tinyui**:先恢复可构建——依赖从 `link:` 换成 npm 的 0.3.0,加 `tinyui.config.json`(`name: "trendingai"`,公钥),模块名 `pages/subscription` → `trendingai/subscription`;`keys generate`,私钥写进 GitHub secret 与 `HarlonWang/secrets` 的 `tinyui-updates/signing/trendingai/trendingai.pem`;CI:合 main → build → `bundle --host-version <值>` → `publish --channel staging`,`hostVersion` 取 workflow 里的一个变量(抄宿主仓当前值;抄错时由 `bundle` 对照服务端的宿主快照拒绝出包,见拍板第 8 点);`production` environment 设人工批准,手动 workflow 跑 `releases promote / rollback / rollout`
- **tinyui**:`tinyui build` 静态找出每页用到的能力与宿主组件写进 manifest 的 `requires`,违反 build-chain.md §5.1 即失败;`tinyui publish` 上传前读目标宿主版本的快照核对(updates.md §1.3,快照格式 §6.4);宿主契约快照——导出宿主组件 schema、能力名、tinyui 版本为稳定排序的文本,存 `tinyui-host/<hostVersion>.txt`;检查任务(当前宿主与快照不符即失败)与生成任务(已随发版带出的版本拒绝覆盖),任务名做时定。前置:宿主能力能集中枚举(TrendingAI 现在散在各 Screen 里注册)。`tinyui:` 前缀约定写进 app-model.md
- **trendingai-tinyui**:先恢复可构建——依赖从 `link:` 换成 npm 的 0.3.0,加 `tinyui.config.json`(`name: "trendingai"`,公钥),模块名 `pages/subscription` → `trendingai/subscription`;`keys generate`,私钥写进 GitHub secret 与 `HarlonWang/secrets` 的 `tinyui-updates/signing/trendingai/trendingai.pem`;CI:合 main → build → `bundle --host-version <值>` → `publish --channel staging`,`hostVersion` 取 workflow 里的一个变量(抄宿主仓当前值;抄错时由 `publish` 对照服务端的宿主快照拒绝发布,见拍板第 8 点);`production` environment 设人工批准,手动 workflow 跑 `releases promote / rollback / rollout`
- **服务端代码**:app token 的签发与吊销、各管理端点改为认 app token(updates.md §6.3);宿主快照的上传与读取端点(§6.4,每个 (app, hostVersion) 只写一次);CLI 配套 `apps tokens`、`hosts upload`
- **服务端(线上,不可逆,执行前列命令确认)**:`apps create trendingai --name TrendingAI`(`org` 留空);签一枚 app token 给 TrendingAI 宿主 CI 上传快照;`packages create trendingai --app trendingai`(登记公钥);两枚发布 token,CI 用的限 `staging`,production environment 用的限 `production`
- **TrendingAI**:依赖升到 tinyui 0.3.0 并加 `tinyui-updates`;`TinyUIHost` 改用 `Updates(listOf(embedded), hostVersion = "1", dir, installId = getOrCreateInstallId(), fetch = ktor)`,base URL `https://updates.tinyui.app/trendingai/<channel>`;设置页隐藏的 channel 开关(持久化,切回即恢复 `production`);订阅页改用 `UpdatesPage`,`PAGE = "trendingai/subscription"`;App 启动即 `check()`(先于任何 TinyUI 页挂载);`onEvent` 接埋点;宿主能力集中注册,`shared/tinyui-host/1.txt` 与检查任务进 CI,发版时 CI 把快照上传到热下发服务;内置包改为发 App 时从服务端拉当时 `production` 的那一版进 `composeResources/files/tinyui/trendingai/`(`pnpm sync` 只再生成 `HostSchemas.kt`),脚本还是 CLI 子命令做时定
Expand All @@ -139,5 +139,5 @@ M1 之后 M2 → M3 与 M4 并行;M5.1、M5.2 与 tinyui 0.3.0(npm 三包 +
| M5.1 改名 `hostVersion` | 已完成(2026-09-23,tinyui PR #17、tinyui-updates-server PR #2,服务端已部署):manifest 字段、Kotlin API、CLI `--host-version`、服务端路由与校验、文档全部改名;CLI、`Updates` 构造、服务端路径与 manifest 五处限定正整数;共用签名夹具换钥重签,Android host 与 iOS 模拟器两端验签通过 |
| M5.2 `qjsc-kmp` 平台包 | 已完成(2026-09-23,quickjs-kmp PR #13 与 0.1.2、tinyui PR #18):入口包 `qjsc-kmp` + `@qjsc-kmp/{darwin-arm64,darwin-x64,linux-x64,linux-arm64}`,Linux 为 musl 全静态;首版本地发布,之后随 quickjs-kmp 的 tag 经 OIDC 发布;`tinyui-cli` 依赖它并由测试钉住与 `quickjsKmp` 同版本,CI 不再现编 `qjsc-kmp` |
| tinyui 0.3.0 | 已发(2026-09-23):npm `tinyui-core` / `tinyui-native` / `tinyui-cli`,Maven `app.tinyui:tinyui` 与首发的 `app.tinyui:tinyui-updates`;`tinyui-cli` 带 `qjsc-kmp@0.1.2` |
| M6 TrendingAI 接入 | 进行中:拍板八点(2026-09-23);`tinyui build` 写 `requires` 已完成(PR #19,TrendingAI 订阅页实测 5 个能力、2 个宿主组件);下一步服务端快照端点与宿主 CI 凭据,再做 `bundle` 核对 |
| M6 TrendingAI 接入 | 进行中:拍板八点(2026-09-23);`tinyui build` 写 `requires` 已完成(PR #19,TrendingAI 订阅页实测 5 个能力、2 个宿主组件);服务端 app token 与快照端点、CLI 的 `apps tokens` / `hosts upload` 与 `publish` 核对进行中 |
| M7 第二个 App | 待开 |
32 changes: 24 additions & 8 deletions docs/updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ pages/**/*.bin
| `engine` | `tinyui build` | 字节码文件头里的引擎 commit(40 位 hex,所有 `.bin` 一致,取第一个) |
| `protocol` | `tinyui build` | 所含 `tinyui-core` 的 `PROTOCOL`,读自子路径导出 `tinyui-core/protocol`(只含常量,不在 Node 里执行运行时模块) |
| `hashes` | `tinyui build` | 模块名 → 该模块 `.bin` 的 sha256 hex,键与 `files` 一致 |
| `requires` | `tinyui build` | 页面模块名 → `{ components, capabilities }`:该页用到的带点宿主组件名与 `host.call` 能力名,各自排序;内置组件不列。`bundle` 据此核对目标宿主版本(§1.3),客户端不读 |
| `tinyui` | `tinyui build` | 运行时模块取自的 `tinyui-core` 版本,决定页面能用哪些内置组件;`publish` 核对它等于目标宿主的 tinyui 版本(§1.3),客户端不读 |
| `requires` | `tinyui build` | 页面模块名 → `{ components, capabilities }`:该页用到的带点宿主组件名与 `host.call` 能力名,各自排序;内置组件不列。`publish` 据此核对目标宿主版本(§1.3),客户端不读 |
| `hostVersion` | `tinyui bundle` | 发布目标,等于宿主声明值;写入后文件定稿,签名覆盖它的原始字节(§7) |

签名值与 `rollout` 不在 manifest 里,在指针文件 `current.json`(§1.2)。内置包的 manifest 没有 `hostVersion`,也没有指针文件——宿主知道自己的 hostVersion,内置包不参与灰度、不验签。
Expand Down Expand Up @@ -65,16 +66,16 @@ dist/ota/<pkg>/<hostVersion>/<version>/pages/**/*.bin

`--host-version` 是发布方手填的。填成一个仍有设备在用的旧值(例:main 已为宿主版本 3 用上新能力 `coupon.apply`,却以 `--host-version 2` 发出一个修文案的包),旧宿主会照常装上,页面调用它没有的能力只得到 `E_UNSUPPORTED`、用到它没有的组件只渲染占位——都不崩溃,§4.5 的回退接不住。宿主侧的构建检查(§4.1)只管宿主漏 bump,管不到发布方填错。

所以 `bundle` 在签名前核对:**这个包用到的宿主东西,目标宿主版本都提供**。
所以 `tinyui publish` 在上传前核对:**这个包用到的宿主东西,目标宿主版本都提供**。放在 `publish` 而不是 `bundle`:`bundle` 是纯离线命令(签名、写目录,静态托管就用它的产物),`publish` 本来就带着 app、实例地址与 token 在和服务端说话,快照也在服务端上;静态托管没有快照,也就没有这层核对。

| 输入 | 来源 |
|---|---|
| 包用到了什么 | `tinyui build` 从每页打包产物里静态找出,写进 manifest 的 `requires`(页 → `{ components, capabilities }`):`host.call` 的能力名、带点的宿主组件名(`ta.Icon`);内置组件不列,随 tinyui 版本走(规则见 build-chain.md §5.1) |
| 目标宿主版本提供了什么 | 宿主快照 `tinyui-host/<hostVersion>.txt`(宿主组件、能力名、tinyui 版本)。宿主发 App 版本时由其 CI 上传到热下发服务,每个 (app, hostVersion) 只能写一次,与已随发版冻结的快照一致;`bundle` 从服务端读 |
| 目标宿主版本提供了什么 | 宿主快照 `tinyui-host/<hostVersion>.txt`(宿主组件、能力名、tinyui 版本)。宿主发 App 版本时由其 CI 上传到热下发服务,每个 (app, hostVersion) 只能写一次,与已随发版冻结的快照一致;`publish` 从服务端读 |

不通过即拒绝出包,报出哪一页缺了什么:页面用到的能力或宿主组件不在快照里;或打包用的 `tinyui-cli` 版本不等于快照里的 tinyui 版本(新版 tinyui 的内置组件,旧宿主没有)。目标宿主版本还没有快照(宿主没上传过)也拒绝。
不通过即拒绝发布(一个请求都不上传),报出哪一页缺了什么:页面用到的能力或宿主组件不在快照里;或 manifest 的 `tinyui` 不等于快照里的 tinyui 版本(新版 tinyui 的内置组件,旧宿主没有)。目标宿主版本还没有快照(宿主没上传过)也拒绝。

核对的是名字,覆盖不到已有能力的参数形状与行为,那部分仍按 §4.1 的 bump 规则由人判断。绕过 `bundle` 直接调发布端点的,目前拦不住;`requires` 进 manifest 并受签名覆盖,日后服务端在 `PUT current.json` 时同样可以核对。快照的上传与读取见 §6.4。
核对的是名字,覆盖不到已有能力的参数形状与行为,那部分仍按 §4.1 的 bump 规则由人判断。绕过 `publish` 直接调发布端点的,目前拦不住;`requires` 进 manifest 并受签名覆盖,日后服务端在 `PUT current.json` 时同样可以核对。快照的上传与读取见 §6.4。

## 2. 投递协议

Expand Down Expand Up @@ -146,7 +147,7 @@ class Updates(

验签公钥不由宿主传:每个包的信任锚是它内置 manifest 里的 `publicKey`(§1.1、§7)。单包宿主写 `Updates(listOf(embedded), …)`。

**`hostVersion` 的口径**(作用同 Expo 的 `runtimeVersion`,但只取正整数、数的是宿主的变化;不沿用那个名字,是因为 tinyui 里 runtime 已指运行时模块与引擎):宿主仓持有的递增正整数字符串(`"1"`、`"2"`……;`1.2.0` 这类 App 版本号在 CLI、`Updates`、服务端三处都被拒),语义是"这个值下发布的任何包都能在本宿主上跑";一个宿主一个值,挂在它上面的所有包共用。宿主给页面的东西变了就加 1:宿主组件增删或改 schema;宿主能力增删,或改已有能力的参数与行为;升级 tinyui。只改宿主内部实现、其他原生页面、修崩溃,不加。包的 JS 工程发布时由 `tinyui bundle --host-version` 取这个值,不自行推导。JS 侧取错分两个方向:取成不存在或更新的值,包只是送不到(`releases list` 与客户端事件可见);取成一个仍有设备在用的旧值,旧宿主会照常装上,而页面可能用到它没有的东西——宿主构建的检查拦不住这个方向,由 `bundle` 发布前核对(§1.3)。
**`hostVersion` 的口径**(作用同 Expo 的 `runtimeVersion`,但只取正整数、数的是宿主的变化;不沿用那个名字,是因为 tinyui 里 runtime 已指运行时模块与引擎):宿主仓持有的递增正整数字符串(`"1"`、`"2"`……;`1.2.0` 这类 App 版本号在 CLI、`Updates`、服务端三处都被拒),语义是"这个值下发布的任何包都能在本宿主上跑";一个宿主一个值,挂在它上面的所有包共用。宿主给页面的东西变了就加 1:宿主组件增删或改 schema;宿主能力增删,或改已有能力的参数与行为;升级 tinyui。只改宿主内部实现、其他原生页面、修崩溃,不加。包的 JS 工程发布时由 `tinyui bundle --host-version` 取这个值,不自行推导。JS 侧取错分两个方向:取成不存在或更新的值,包只是送不到(`releases list` 与客户端事件可见);取成一个仍有设备在用的旧值,旧宿主会照常装上,而页面可能用到它没有的东西——宿主构建的检查拦不住这个方向,由 `publish` 发布前核对(§1.3)。

漏加的后果是旧宿主收到跑不了的页面(不崩溃的错误不会触发 §4.5 回退),多加的后果是更早的宿主从此收不到更新。漏加只可能发生在宿主仓,由宿主构建拦:宿主快照 `tinyui-host/<hostVersion>.txt`(宿主组件 schema、能力名、tinyui 版本)每个版本一份入库,当前宿主与当前版本的快照不符即构建失败;已随发版带出去的版本,其快照冻结不可改。快照覆盖不到已有能力的参数与行为变化,这部分按上面的规则由人判断。为什么是精确匹配而不是 `>=` 范围,见 ADR-006 §2.2。

Expand Down Expand Up @@ -312,12 +313,27 @@ CLI:`tinyui apps create` / `tinyui apps tokens create|revoke` / `tinyui packag

### 6.4 宿主快照

宿主快照(§1.3、§4.1)是 `tinyui-host/<hostVersion>.txt` 的原始字节:该宿主版本提供的宿主组件、能力名与 tinyui 版本。宿主发 App 版本时由其 CI 上传,`bundle` 发布前读取核对。
宿主快照(§1.3、§4.1)是 `tinyui-host/<hostVersion>.txt` 的原始字节:该宿主版本提供的宿主组件、能力名与 tinyui 版本。宿主发 App 版本时由其 CI 上传,`publish` 发布前读取核对。格式是宿主侧生成工具与 CLI 之间的契约:

```
hostVersion 2
tinyui 0.3.0

components
ta.Icon name: string, tint?: color, size?: dp
ta.Loading color?: color, size?: dp

capabilities
checkout.start
coupon.apply
```

前两行固定;`components` 与 `capabilities` 两段按此顺序各出现一次,没有条目也要写段名(缺段会被读成"什么都不提供",所以一律拒绝);每项缩进两格、一行一项、按名排序。`tinyui hosts upload` 先解析并核对首行的 `hostVersion` 与 `--host-version` 相符再上传——服务端只收第一份。**每行第一个词是名字**,其后供人阅读、供宿主侧检查判断 schema 是否变了,CLI 核对只看名字。宿主组件只列带点的,内置组件随 `tinyui` 版本。

| 请求 | 凭据 | 语义 |
|---|---|---|
| `PUT /apps/<app>/hosts/<hostVersion>`,body 为快照字节 | `ADMIN_TOKEN` 或本 app 的 app token | 每个 (app, hostVersion) 只写一次:同字节重传返回已存在,不同字节 → 409——已随发版带出的宿主版本,其快照冻结 |
| `GET /apps/<app>/hosts/<hostVersion>` | 以上两种,或本 app 任一包的发布 token | 原字节;没上传过 → 404,`bundle` 据此拒绝出包 |
| `GET /apps/<app>/hosts/<hostVersion>` | 以上两种,或本 app 任一包的发布 token | 原字节;没上传过 → 404,`publish` 据此拒绝发布 |

CLI:`tinyui hosts upload <file> --host-version <n> [--app <app>]`(宿主 CI 用)。

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

[TinyUI](https://github.com/tiny-ui/tinyui) 的构建工具:`tinyui build` 把 `tinyui.config.json` 描述的包(`src/pages/**/*.tsx`)编成每页一个 ESM 模块字节码(含运行时模块与 `manifest.json`),`tinyui bundle` 把一次 build 签名成可上传 / 可静态托管的热下发目录,`tinyui keys generate` 生成签名密钥对,`tinyui schema` 从组件 schema 生成 TS 类型与 Kotlin 侧代码。

`tinyui publish` 把 `bundle` 的产物传到热下发服务并切换 channel 指针,`tinyui apps` / `packages` / `tokens` / `releases` 是这套服务的管理面(建 app、登记包的验签公钥、签发与吊销发布 token、列 release 与回滚 / 晋级 / 改灰度)。实例地址取 `--url`、`$TINYUI_UPDATES_URL`、`https://updates.tinyui.app` 三者之一;凭据只从环境变量读,不接受命令行参数:`$TINYUI_TOKEN` 是发布 token,`$TINYUI_ADMIN_TOKEN` 是实例管理 token。`--app` 可用 `$TINYUI_APP` 代替,`--pkg` 缺省取当前目录 `tinyui.config.json` 的 `name`,`--channel` 必须每次显式写。
`tinyui publish` 先对照目标宿主版本的快照核对包用到的宿主能力与组件,再把 `bundle` 的产物传到热下发服务并切换 channel 指针;`tinyui apps` / `packages` / `tokens` / `releases` / `hosts` 是这套服务的管理面(建 app 与签发 app token、登记包的验签公钥、签发与吊销发布 token、列 release 与回滚 / 晋级 / 改灰度、宿主 CI 上传快照)。实例地址取 `--url`、`$TINYUI_UPDATES_URL`、`https://updates.tinyui.app` 三者之一;凭据只从环境变量读,不接受命令行参数:`$TINYUI_TOKEN` 是发布 token,`$TINYUI_ADMIN_TOKEN` 放实例的管理 token 或某个 app 的 app token。`--app` 可用 `$TINYUI_APP` 代替,`--pkg` 缺省取当前目录 `tinyui.config.json` 的 `name`,`--channel` 必须每次显式写。

字节码编译用 quickjs-kmp 的宿主工具 `qjsc-kmp`:依赖里的 npm 包 `qjsc-kmp` 带 macOS 与 Linux 的预编译版,装完即用;其他平台用 `--qjsc` 或 `TINYUI_QJSC` 指向自己编的那份。`qjsc-kmp` 的版本钉在与 tinyui 所用 quickjs-kmp 相同的版本上,编出的字节码才认 App 里的引擎。用法见仓库 `docs/build-chain.md` 与 `docs/updates.md`;与 `tinyui-core` 同版本号。
Loading
Loading