@oj-module/cli 0.2.68 → 0.2.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/devkit/README.md +10 -8
  2. package/devkit/framework-dev.md +1 -1
  3. package/devkit/manual.md +45 -19
  4. package/devkit/module-dev-and-cdn-playground.md +20 -14
  5. package/devkit/publish-walkthrough.md +132 -43
  6. package/devkit/wecom-customer-reauth-guide.md +110 -0
  7. package/package.json +2 -2
  8. package/shell-dist/assets/{runtime.1xzahckt10.js → runtime.7x99hmm020.js} +2 -2
  9. package/shell-dist/bundles/oj-market-0.2.0.tgz +0 -0
  10. package/shell-dist/bundles/oj-market-0.2.0.tgz.sig +8 -0
  11. package/shell-dist/index.html +4 -4
  12. package/shell-dist/versions.json +3 -3
  13. package/src/__tests__/list.test.ts +7 -7
  14. package/src/__tests__/oj-health-host.test.ts +19 -0
  15. package/src/__tests__/site-tenant-header.test.ts +2 -2
  16. package/src/__tests__/templates-builtin-list.test.ts +47 -0
  17. package/src/__tests__/templates-schema-columns.test.ts +11 -2
  18. package/src/__tests__/templates-tenant-scope.test.ts +2 -3
  19. package/src/__tests__/users-roles-display.test.ts +84 -0
  20. package/src/bundle.ts +1 -1
  21. package/src/contract/__tests__/market-client.integration.test.ts +23 -22
  22. package/src/enable.ts +2 -2
  23. package/src/init.ts +45 -0
  24. package/src/install.ts +5 -5
  25. package/src/lifecycle/__tests__/install.test.ts +29 -29
  26. package/src/lifecycle/__tests__/market-bundle.test.ts +87 -0
  27. package/src/lifecycle/__tests__/publish.test.ts +8 -10
  28. package/src/lifecycle/__tests__/trust.test.ts +14 -14
  29. package/src/lifecycle/__tests__/uninstall.test.ts +4 -4
  30. package/src/list.ts +3 -3
  31. package/src/market/__tests__/bootstrap.test.ts +24 -7
  32. package/src/market/__tests__/failure-playbook.test.ts +0 -1
  33. package/src/market/__tests__/remote-push.test.ts +228 -0
  34. package/src/market/__tests__/verify.test.ts +45 -207
  35. package/src/market/bootstrap.ts +12 -7
  36. package/src/market/command.ts +33 -35
  37. package/src/market/db-seed.ts +129 -0
  38. package/src/market/failure-playbook.ts +3 -9
  39. package/src/market/fixture.ts +2 -6
  40. package/src/market/publish.ts +145 -89
  41. package/src/market/remote-push.ts +236 -0
  42. package/src/market/trial.ts +2 -2
  43. package/src/market/verify.ts +17 -270
  44. package/src/market/walkthrough.ts +38 -63
  45. package/src/oj.ts +13 -2
  46. package/src/preview.ts +5 -3
  47. package/src/publish.ts +3 -2
  48. package/src/scaffold-guard.ts +4 -0
  49. package/src/site.ts +1 -1
  50. package/src/trust.ts +8 -8
  51. package/src/uninstall.ts +2 -2
  52. package/src/usage.ts +23 -21
  53. package/templates/config.yaml +22 -3
  54. package/templates/global.d.ts +6 -4
  55. package/templates/init.include.yaml +16 -11
  56. package/templates/openapi.json +3971 -3663
  57. package/templates/src/.ojm-builtin.json +13 -0
  58. package/templates/src/README.md +5 -2
  59. package/templates/src/app/api/_platform/README.md +3 -2
  60. package/templates/src/app/api/_platform/_lifecycle/atomic.ts +7 -2
  61. package/templates/src/app/api/_platform/_lifecycle/bundle-converge.ts +158 -0
  62. package/templates/src/app/api/_platform/_lifecycle/history.ts +3 -2
  63. package/templates/src/app/api/_platform/_lifecycle/installer.ts +25 -5
  64. package/templates/src/app/api/_platform/_lifecycle/local-archive.ts +61 -0
  65. package/templates/src/app/api/_platform/_lifecycle/manifest-of-upload.ts +62 -0
  66. package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +12 -2
  67. package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +4 -1
  68. package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +8 -2
  69. package/templates/src/app/api/_platform/_lifecycle/types.ts +1 -1
  70. package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +14 -5
  71. package/templates/src/app/api/_platform/_shared/service/domain/users.ts +7 -1
  72. package/templates/src/app/api/_platform/_shared/site-facade.ts +31 -0
  73. package/templates/src/app/api/_platform/manifest.yaml +1 -2
  74. package/templates/src/app/api/_platform/migrations/0003__market_catalog.mysql.sql +6 -0
  75. package/templates/src/app/api/_platform/migrations/0003__market_catalog.pg.sql +6 -0
  76. package/templates/src/app/api/_platform/migrations/0003__market_catalog.sql +18 -0
  77. package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.mysql.sql +6 -0
  78. package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.pg.sql +6 -0
  79. package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.sql +6 -0
  80. package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.mysql.sql +8 -0
  81. package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.pg.sql +8 -0
  82. package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.sql +8 -0
  83. package/templates/src/app/api/_platform/schema.yaml +8 -11
  84. package/templates/src/app/api/_platform/seed.mysql.sql +1 -1
  85. package/templates/src/app/api/_platform/seed.pg.sql +1 -1
  86. package/templates/src/app/api/_platform/seed.sql +1 -1
  87. package/templates/src/app/api/auth/_shared/wecom/sync.ts +6 -0
  88. package/templates/src/app/api/system/users/api.ts +3 -0
  89. package/templates/src/app/web/_platform/__client/api.schemas.ts +0 -113
  90. package/templates/src/app/web/_platform/__client/index.ts +1 -333
  91. package/templates/src/app/web/system/__client/api.schemas.ts +2 -0
  92. package/templates/src/app/web/system/locales/en-US.json +15 -1
  93. package/templates/src/app/web/system/locales/zh-CN.json +15 -1
  94. package/templates/src/app/web/system/pages/departments/sync-bar.tsx +9 -3
  95. package/templates/src/app/web/system/pages/roles/index.tsx +4 -8
  96. package/templates/src/app/web/system/pages/roles/role-label.ts +26 -0
  97. package/templates/src/app/web/system/pages/solutions/index.tsx +110 -0
  98. package/templates/src/app/web/system/pages/solutions/market-entry.ts +46 -0
  99. package/templates/src/app/web/system/pages/users/index.tsx +12 -2
  100. package/templates/src/app/web/system/pages/users/match-keyword.ts +19 -0
  101. package/templates/src/app/web/system/pages/users/roles.tsx +43 -14
  102. package/templates/src/app/web/system/routes.ts +18 -1
  103. package/templates/src/{app/api/_platform/market → oj-market/api}/api.ts +475 -592
  104. package/templates/src/oj-market/api/backfill.ts +221 -0
  105. package/templates/src/{app/api/_platform/market → oj-market/api}/bootstrap.ts +2 -2
  106. package/templates/src/oj-market/api/cdn/api.ts +312 -0
  107. package/templates/src/oj-market/api/cdn/shared.ts +104 -0
  108. package/templates/src/oj-market/api/cdn/signed-auth.ts +75 -0
  109. package/templates/src/oj-market/api/manifest.yaml +8 -0
  110. package/templates/src/oj-market/solution.yaml +21 -0
  111. package/templates/src/oj-market/web/__client/api.schemas.ts +167 -0
  112. package/templates/src/oj-market/web/__client/index.ts +509 -0
  113. package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +6 -6
  114. package/templates/src/oj-market/web/api/client.ts +6 -6
  115. package/templates/src/oj-market/web/api/defaultClient.ts +31 -25
  116. package/templates/src/oj-market/web/entry.ts +5 -3
  117. package/templates/src/oj-market/web/hooks/useMarket.ts +6 -6
  118. package/templates/src/oj-market/web/pages/MarketApp.tsx +20 -3
  119. package/templates/src/oj-market/web/publish/locales/en-US.json +1 -1
  120. package/templates/src/oj-market/web/publish/locales/zh-CN.json +1 -1
  121. package/templates/src/oj-market/web/publish/pages/history.tsx +2 -11
  122. package/templates/src/oj-market/web/routes.ts +12 -0
  123. package/templates/src/oj-market/web/types.ts +14 -8
  124. package/templates/tests/lifecycle/anonymous-wiring.spec.ts +65 -0
  125. package/templates/tests/lifecycle/backfill.spec.ts +392 -0
  126. package/templates/tests/lifecycle/bootstrap.spec.ts +1 -1
  127. package/templates/tests/lifecycle/bundle-converge.spec.ts +264 -0
  128. package/templates/tests/lifecycle/cdn-upload.spec.ts +661 -0
  129. package/templates/tests/lifecycle/deploy-smoke.spec.ts +25 -0
  130. package/templates/tests/lifecycle/exec-grant.spec.ts +38 -0
  131. package/templates/tests/lifecycle/installer.spec.ts +36 -0
  132. package/templates/tests/lifecycle/market-api.spec.ts +448 -803
  133. package/templates/tests/lifecycle/preflight.spec.ts +18 -0
  134. package/templates/tests/lifecycle/trust.spec.ts +2 -2
  135. package/templates/tests/lifecycle/uninstall.spec.ts +36 -2
  136. package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
  137. package/templates/tests/rbac-users.spec.ts +8 -0
  138. package/templates/tests/solutions-entry.spec.ts +55 -0
  139. package/templates/tsconfig.paths.json +1 -0
  140. package/templates/types/stream-probes/stream-handlers.ts +34 -0
  141. package/templates/web.config.ts +6 -6
  142. package/vendor/host-versions.json +3 -3
  143. package/bin/oj +0 -4
  144. package/src/market/__tests__/build-index.test.ts +0 -641
  145. package/src/market/__tests__/sharding.test.ts +0 -90
  146. package/src/market/__tests__/yank-ledger.test.ts +0 -60
  147. package/src/market/build-index.ts +0 -526
  148. package/src/market/index-builder.ts +0 -149
  149. package/src/market/sharding.ts +0 -94
  150. package/src/market/yank-ledger.ts +0 -91
  151. package/templates/src/app/api/_platform/_lifecycle/source-seq.ts +0 -88
  152. package/templates/src/app/api/_platform/market/index-source.ts +0 -112
  153. package/templates/tests/lifecycle/source-seq.spec.ts +0 -55
package/devkit/README.md CHANGED
@@ -9,14 +9,14 @@
9
9
 
10
10
  面向用 `ojm` 开发前后端一体工程的开发者与 AI agent。本目录是**源文件**,
11
11
  发布时由 cli 的 `prepack`(`packages/cli/scripts/sync-devkit.mjs`)把其中
12
- **ojm 侧的 11 个文件**同步进 `packages/cli/devkit/`,随 `@oj-module/cli`
12
+ **ojm 侧的 12 个文件**同步进 `packages/cli/devkit/`,随 `@oj-module/cli`
13
13
  发布,再由 `ojm init` 分发到工程内。
14
14
 
15
15
  整棵树覆盖前后端两侧,但仍是**两套独立分发的 skill**(同名不同物,别混):
16
16
 
17
17
  | 侧 | 本仓位置 | 用户工程里的落点 | 分发链 |
18
18
  |---|---|---|---|
19
- | ojm 模块开发(前端模块、契约、前后端对接) | 本目录顶层 11 个文件 | `.claude/skills/ojm-module-dev/` | `@oj-module/cli` 包(全部 11 个)→ `ojm init` 第 4b 步(只分发其中 6 个,见下) |
19
+ | ojm 模块开发(前端模块、契约、前后端对接) | 本目录顶层 12 个文件 | `.claude/skills/ojm-module-dev/` | `@oj-module/cli` 包(全部 11 个)→ `ojm init` 第 4b 步(只分发其中 6 个,见下) |
20
20
  | oj API 开发(后端 handler、全局对象、配置、测试) | `oj-api-dev/` 子目录(快照) | `.claude/skills/oj-api-dev/` | **oj 二进制自带**(`bin/devkit/`)→ `ojm init` 第 4 步 / `ojm vendor` 装·升后同步(0.2.57 起共用 `syncOjDevkit`,全量 `.md` **覆盖**——文档始终与已装 oj 版本一致) |
21
21
 
22
22
  > **`oj-api-dev/` 子目录是快照,不是本仓维护的文档**:它与本机 oj 的
@@ -32,20 +32,22 @@
32
32
  |---|---|
33
33
  | `SKILL.md` | Claude Code 等 agent 的 skill 入口:工作流、红线、新模块 checklist、陷阱速查,按章节号引用手册 |
34
34
  | `manual.md` | 模块开发手册(14 章):工程结构(module-root 单树 + 两处产物根;含 §1.7 handler 四红线——oj 运行时实测钉出,后端必读;§1.8 `#` 别名与 TS 投影)、写模块、契约与代码生成(含 §3.10 旧 contract.ts 工程迁移指南)、provider 接管(覆盖框架内置功能)、布局、i18n、构建发布、排查、CLI 子命令参考(ojm 原生命令 vs oj 透传命令)、方案包打包与分发(含 §11.1.1 引用式方案)、签名与信任、§13.1.1 web.route_prefix 自定义根路径、**§14 组织架构与通讯录**(内置部门/成员底座 + 共享读面 7 端点 + `scope` 两级门禁 + 归属权四条规则 + 源适配器接点,选人与按部门算人都读它) |
35
- | `publish-walkthrough.md` | 方案包发布全流程(**新手照做版**):构建打包 → **拿发布身份(「发布管理」申请凭证 → `ojm keygen --credential`;无站点才用离线 `--id`)** → 签名 → `market publish` 落 CDN → build-index → verify/trial 验证 → install + TOFU,含多人各自独立发包纪律与失败速查(手册 §11/§12 的过程版) |
35
+ | `publish-walkthrough.md` | 方案包发布全流程(**新手照做版**):构建打包 → **拿发布身份(「发布管理」申请凭证 → `ojm keygen --credential`;无站点才用离线 `--id`)** → 签名 → `market publish` 远端签名推送 → verify/trial 验证 → install + TOFU,含多人并发推送纪律、yank 下线与失败速查(手册 §11/§12 的过程版) |
36
36
  | `framework-dev.md` | 框架贡献者路线(**仅框架仓库**):改 `packages/runtime`、`packages/cli` 时的入口与红线 |
37
- | `module-dev-and-cdn-playground.md` | 教学文档:开发态 ≠ 分发态、「怎么安装自己的模块 / 会不会重复」、本地 CDN 市场链路五步跑通(setup → publish → build-index → verify → 安装) |
37
+ | `module-dev-and-cdn-playground.md` | 教学文档:开发态 ≠ 分发态、「怎么安装自己的模块 / 会不会重复」、本地 CDN 市场链路跑通(setup → publish → verify → 安装) |
38
38
  | `sql-builder-missing-capabilities-pr.md` | 构造器能力缺口 PR 参考(**仅框架仓库**):7 项诉求的闭环记录 |
39
39
  | `menu-rules.md` | 方案菜单归组规则专题:单根模块约定、自定义根路径 `web.route_prefix`、`category` 三值语义、admin 重写与视觉归组(手册 §13 的展开版) |
40
40
  | `wecom-daikaifa.md` | 企业微信代开发集成:**凭据清单**(8 项 `vars`,按 Tier A/B/C 分层的来源与必填时序 + 可信域名/可信 IP 备案)、首次开通顺序(含 `suite_ticket` 依赖与引导期死锁的破法)、**用户 / 开发 / 部署三角色手册**、错误文案速查、与自建应用共存的语义变化 |
41
41
  | `wecom-internal-app.md` | 企业微信**企业内部应用(自建应用)**集成:5 项 `wecom_corp_*` 凭据 + 2 项复用、回调只填「通讯录同步」一处、「验证并接入」四步自检、可见范围与可信 IP/域名、单企业私有化的边界(自建应用收不到成员变更事件)、共存配置、故障速查 |
42
42
  | `wecom-multi-corp-links.md` | **平台运维 / 服务商运营**:一套部署服务 N 家企业的**专属登录链接运维清单**(方案一,运维零代码改动)——前置检查、逐企业接入 6 步、链接格式与参数、员工四入口、逐企业验收打勾、故障速查、日常运维;含企微工作台内早跳透传 `corp`/`agent` 的修复说明 |
43
+ | `wecom-customer-reauth-guide.md` | **客户企业微信管理员(可直接转发)**:站点重新部署后的**重新授权与数据恢复操作单**——服务商填写信息区、重新授权(管理员授权 / 可见范围全公司)、首次登录改密、组织架构同步、员工重新登录、角色权限重配、验收打勾、故障速查 |
43
44
 
44
- > 上表 10 项 + 本文件 `README.md` = 顶层 11 个文件,全部随 cli 包发货。
45
+ > 上表 11 项 + 本文件 `README.md` = 顶层 12 个文件,全部随 cli 包发货。
45
46
  > `ojm init` 第 4b 步**只分发其中 6 个**给业务工程:`SKILL.md` / `manual.md` / `README.md` /
46
47
  > `wecom-daikaifa.md` / `wecom-internal-app.md` / `publish-walkthrough.md`。
47
- > 其余 5 个(`framework-dev.md`、`menu-rules.md`、`module-dev-and-cdn-playground.md`、
48
- > `sql-builder-missing-capabilities-pr.md`、`wecom-multi-corp-links.md`)是框架贡献者或专题材料,只在 cli 包内可取。
48
+ > 其余 6 个(`framework-dev.md`、`menu-rules.md`、`module-dev-and-cdn-playground.md`、
49
+ > `sql-builder-missing-capabilities-pr.md`、`wecom-multi-corp-links.md`、
50
+ > `wecom-customer-reauth-guide.md`)是框架贡献者或专题材料,只在 cli 包内可取。
49
51
 
50
52
  后端 handler 本身的写法(`json` 信封、`db` 查询、`manifest.yaml`、`oj test`)、
51
53
  站点挂载(`mounts`)、密钥信封(`secrets`)、vars、cors、schema 校验、broker/tasks/oidc/ws 等
@@ -129,6 +131,6 @@ flowchart LR
129
131
  框架贡献者要把本仓快照同步到新版:从 `~/.local/share/oj/devkit/` 整目录覆盖
130
132
  `docs/devkit/oj-api-dev/`(oj v0.1.116 起 12 个文件,保持逐字节一致以便对账)。
131
133
 
132
- > 本目录顶层 11 个文件是**自包含**的:业务工程拿到即可读懂,不需要工程仓库里的
134
+ > 本目录顶层 12 个文件是**自包含**的:业务工程拿到即可读懂,不需要工程仓库里的
133
135
  > 其它文档。`oj-api-dev/` 子目录是 oj 侧快照,同样自包含(其自身 README 即索引),
134
136
  > 但不随 cli 包发货、仅本仓参考。维护者视角的内容来源整理在 `framework-dev.md`(仅本仓)。
@@ -197,7 +197,7 @@ runtime → cli → registry 复核;`--dry-run` 只演练)。下面的手动
197
197
 
198
198
  ## 附:本 devkit 内容来源(维护者看)
199
199
 
200
- `docs/devkit/` 是**自包含**的交付文档(顶层 11 个文件,全部随 cli 包发货,其中 6 个随
200
+ `docs/devkit/` 是**自包含**的交付文档(顶层 12 个文件,全部随 cli 包发货,其中 6 个随
201
201
  `ojm init` 进业务工程;含 `sql-builder-missing-capabilities-pr.md` 这份构造器能力缺口 PR 参考),
202
202
  业务工程拿到即可读懂,不依赖工程仓库里的其它文档。
203
203
  另有 `oj-api-dev/` 子目录是 oj 侧 devkit 的快照(oj v0.1.116 起 12 文件,仅本仓参考、不发货),
package/devkit/manual.md CHANGED
@@ -61,9 +61,11 @@
61
61
  │ │ ├── auth/ # 仅 __client(无页面)
62
62
  │ │ ├── _platform/ # 仅 __client(oj-market 引用)
63
63
  │ │ └── web/ # 仅 __client
64
- │ ├── oj-market/web/ # 解决方案市场(独立模块,enabled: false 下线;单根 /oj-market 挂三个子页)
65
- │ │ ├── entry.ts # routes 聚合:/oj-market 根(方案清单 index)+ 发布管理/发布历史子路由
66
- │ │ └── publish/ # 发布管理 + 发布历史(/oj-market/publish、/oj-market/history:routes + pages + locales + __client)
64
+ │ ├── oj-market/ # 解决方案市场(Phase B 起「出厂未装」分层)
65
+ │ │ ├── api/ # 编排面随脚手架内置:/api/oj-market/*(安装/目录/下线/信任/回填;第一方专用)
66
+ │ │ └── web/__client/ # 生成物落点(ojm api --check 对账;页面源码随第一方方案包分发,不在脚手架)
67
+ │ ├── (方案包装后) # 市场 UI = 纯 web 第一方方案包:superadmin 经 system 域「方案安装」首装;
68
+ │ │ # ojm init --yes 落位的随站 bundle 由首个市场请求自动收敛(上拉不下拉)
67
69
  ├── dist/api/ # 后端产物(release 根:<name>-<ver>/ + manifests.yaml + versions.json)—构建后才有
68
70
  ├── dist/web/ # 站点产物(shell + modules/ + modules.json)—构建后才有
69
71
  ├── bin/ # oj 二进制 + plugins/ + devkit/(不入库)
@@ -1932,8 +1934,10 @@ module-root 下数据源是「扫描 + `web.config.ts` 登记」并集(未登
1932
1934
 
1933
1935
  ### 10.3 本地市场链路:`ojm market` 子命令
1934
1936
 
1935
- `market` 是一组**本地模拟 CDN + 市场链路**工具,专供作者在本机验证「方案包能不能发、能不能装」,
1936
- **不触碰真实生产 CDN**。所有产物落在工程根 `cdn/`(由 `config.yaml` 的 `/cdn` 挂载直出)。
1937
+ `market` 围绕两条链路:**发布**(`publish`/`yank`,缺省远端签名推送到目标站点的市场上传端点)
1938
+ 与**本机演练**(`verify`/`trial`/`walkthrough`,隔离站点跑完整安装,跑完即毁)。
1939
+ 演练产物落在工程根 `cdn/`(本地模拟),发布不经账号口令(签名头 + 信任库 + Ed25519),
1940
+ CLI 没账号也能发。目标站点的市场目录由站点自己的库承载——不再有索引文件这层人工面。
1937
1941
  **脚手架 seed 的 `framework.market_cdn_url` 缺省即相对路径 `/cdn`(本站点自供),起站即市场可用,
1938
1942
  无需 setup**;只有指向外部真 CDN 时才需要 `market setup` 改写设置。
1939
1943
 
@@ -1944,9 +1948,9 @@ module-root 下数据源是「扫描 + `web.config.ts` 登记」并集(未登
1944
1948
  | 子命令 | 一句话 |
1945
1949
  |---|---|
1946
1950
  | `ojm market setup [--site-url <站点>] [--user <u> --pass <p>] [--new-pass <p>]` | **外部真 CDN 场景才需要**:把 `framework.market_cdn_url` 改写为绝对地址;账号须 **superadmin**(该设置安全组仅 superadmin 可写,admin 角色会被 403);首跑若账号处于首次改密期会先自动改密再写入 |
1947
- | `ojm market publish [--fixture <name>@<version>] [--real <方案工程>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]` | 把方案包落进 `cdn/` 并做包级 detached 签名;**不碰索引**(索引由 `build-index` 重建,避免多人发布互相覆盖) |
1948
- | `ojm market build-index [--id <key>] [--key <pem>] [--check]` | 扫描 `cdn/*.tgz`、提取包内 manifest,全量重建 `bundles.yaml` + `manifests.yaml` 并签名;结果只取决于目录内容,连续执行字节一致 |
1949
- | `ojm market verify [--file <tgz>] [--rebuild-index]` | 离线验包(不起站点、不装、不要凭据):sha256 完整性 / 包级 Ed25519 验签 / 索引验签 / manifest 结构分列输出;`--rebuild-index` 额外比对重建索引与盘上索引的漂移 |
1951
+ | `ojm market publish [--fixture <name>@<version>] [--real <方案工程>] [--site-url <站点>] [--id <key>] [--key <pem>] [--registry-id <身份>] [--check]` | **缺省链路 = 远端签名推送**:构建 + 包级签名后把 `.sig`/`.tgz` 按序推到站点 `/api/oj-market/cdn/upload`(pair 约束:旁车先行);不经账号口令(`X-OJM-Key-Id`/`X-OJM-Signature` 头 + 信任库 + Ed25519);同字节重传幂等,半成功重跑补齐;`--site-url` 缺省 `OJM_SITE_URL` |
1952
+ | `ojm market yank <name> <version> [--site-url <站点>] [--id <key>] [--reason <r>] [--by <人>] [--restore]` | 远端下线/恢复目录版本(签名鉴权,canonical `market-yank:<站点 registry>:<产品>:<版本>`;仅发布者本人的 key 可操作);yank 不动已装方案,只摘除目录 + 精确版本安装 409;重复操作幂等 |
1953
+ | `ojm market verify --file <tgz>` | 离线验**单个包**(不起站点、不装、不要凭据):manifest 结构 / 包级 Ed25519 验签 / 契约-迁移一致性分列输出 |
1950
1954
  | `ojm market trial [--fixture <name>@<version>] [--keep]` | 在一次性隔离站点里跑完整安装,跑完即毁;`--keep` 保留现场供排查 |
1951
1955
  | `ojm market walkthrough [--fixture <name>@<version>] [--only <op>] [--keep] [--strict]` | 隔离站点上 14 项全套自检(主链 9:index/install/reinstall/enable/verify/history/upgrade/rollback/uninstall;负向 5:tamper/nosig/badindex/stranger/evil——预期失败口径,防线没拦住才算 FAIL);AC 表 + 失败文案脱敏落盘 `logs/walkthrough-<ts>.md`;`--only` 单跑验原子性 |
1952
1956
 
@@ -2023,25 +2027,28 @@ ojm build --bundle crm # 只打 src/crm/ 这一份;--bundle=crm 等价
2023
2027
  | 源 | 形态 | 说明 |
2024
2028
  |---|---|---|
2025
2029
  | **file** | `ojm install dist/<name>-<version>.tgz` | multipart 直传(同目录 `<tgz>.sig` 自动探测随包上传,§12) |
2026
- | **cdn** | `ojm install <name>[@version]` | 站点从 CDN 拉 Form B 索引:`bundles.yaml`(版本清单)+ `manifests.yaml`(聚合清单)+ 分发包 |
2030
+ | **cdn** | `ojm install <name>[@version]` | 站点从自己的**目录库**解析(`market_release` 表:product+version 唯一行,含 sha256/size/签名四列/内嵌 manifest),再拉 `cdn/` 字节 |
2027
2031
 
2028
- Form B 索引规范(宿主侧强制,防索引投毒):`versions[].file` 必须逐字
2029
- `<name>-<version>.tgz`(禁 `../`、绝对路径、异名、异版本);`archive.size` 在下载段立即
2030
- 比对;索引拉取恒 `Cache-Control: no-cache`。索引本身未签名(签名只保护分发包本身,
2031
- `.sig` 侧车随包上传)。
2032
+ 目录库是服务端在**签名推送完成时直写**的(发布方 `ojm market publish` 把 `.sig` 旁车与
2033
+ `.tgz` 按序推到 `POST /api/oj-market/cdn/upload`,pair 约束:旁车先行、配对成功才写行)——
2034
+ 不再有索引文件这层人工面,也就没有「索引投毒/索引重建竞态」问题。同 `name@version`
2035
+ 同字节重传幂等、异字节 409、方案名绑定首发 key。存量包与灾备恢复由站点
2036
+ `POST /api/oj-market/backfill` 扫 `cdn/` 自愈(superadmin,幂等可重入);`.sig` 缺失的
2037
+ 行签名列为 NULL,安装验签 `sig_missing` 硬拒。目录分页读 `GET /api/oj-market/catalog`
2038
+ (匿名可达),单方案全版本读 `GET /api/oj-market/catalog/detail`(含 yanked 标记)。
2032
2039
 
2033
- **作者侧**:索引的产出/重签用 `ojm publish <发布目录>`——对 `bundles.yaml` +
2034
- `manifests.yaml` 重算(确定性 stringify)→ detached 签名(`.sig` 侧车)→ 写回
2035
- (本地文件目标;CDN 推送走同一 resolveCdn 面)。发布方 key 与 `ojm sign` 同身份体系
2036
- (`~/.ojm/keys`)。
2040
+ **作者侧**:发布 = `ojm market publish --real <方案工程> --site-url <站点> --id <key>
2041
+ --registry-id <站点 registry_id>`(不经账号口令:签名请求头 + 站点信任库 + Ed25519);
2042
+ 下线/恢复 = `ojm market yank <name> <version> --site-url <站点> --id <key> [--restore]`。
2043
+ 发布方 key 与 `ojm sign` 同身份体系(`~/.ojm/keys`)。
2037
2044
 
2038
2045
  ### 11.3 安装面速览
2039
2046
 
2040
- `ojm install`(file/cdn 双源)→ 站点编排端点 `POST /api/_platform/market/install`
2047
+ `ojm install`(file/cdn 双源)→ 站点编排端点 `POST /api/oj-market/install`
2041
2048
  (需 superadmin)→ 十阶段状态机(§7 verify 闸语义的方案版,失败语义见下图)→
2042
2049
  任务记录 `.install-tasks/<id>.json`(保留 10 条,800ms 轮询消费面)。市场页族端点
2043
2050
  (install/task/index/meta/trust/uninstall…)已全量登记 openapi.json,
2044
- 前端经 `src/_platform/web/__client` 消费。
2051
+ 前端经 `src/oj-market/web/__client` 消费。
2045
2052
 
2046
2053
  十阶段(阶段名如下)+ 失败语义:
2047
2054
 
@@ -2074,6 +2081,25 @@ flowchart TD
2074
2081
  - **preflight 含 RBAC 声明校验**:`permissions[]` / `roles[]` 的命名前缀、`title_key`
2075
2082
  必填、悬空引用双端拒绝——形状细则与注册/管理面见 §11.5。
2076
2083
 
2084
+ **随站 bundle 自动收敛(对账即服务)运维口径**(第一方随站 bundle 场景,发布方流程见发布
2085
+ walkthrough §7.5):
2086
+
2087
+ - 站点首个市场编排请求(如 superadmin 打开 system 域「方案安装」)触发收敛:bundle 比已装
2088
+ 新→升级、bundle ≤ 已装→跳过;**永不自动降级**(降级 = 站点产物回滚)。
2089
+ - 收敛失败落 `market_history` 审计(operator 记 `bundle-converge`)+ 10 分钟冷却文件
2090
+ `modules/.install-tasks/.bundle-converge-cooldown.json`;处置完(如推信任 key)删冷却文件
2091
+ 即立即重试。
2092
+ - 卸载第一方市场包会摘掉 `cdn/bundles.json` 意图条目(墓碑)——收敛不再自动装回,
2093
+ 重装走「方案安装」手动入口。
2094
+ - 旧工程注意:市场编排模块需双授权——exec 白名单有它的条目(与 `_platform` 逐字相同)、
2095
+ `platform.modules` 列表含它。模板自带(脚手架 0.2.71 起);旧 config 需手工补,否则
2096
+ 收敛/安装卡 reloading 段 NotCapable。
2097
+ - 安装验证的起站形态:release 站点二选一——config 加 api 挂载(`platform.root: dist/api`
2098
+ + `- prefix: /api / api: dist/api`,零 CLI 旗标)或 CLI 旗标(`-b /api --api-path dist/api`,
2099
+ 此形态 web 挂载必须绝对路径)。**不要**用 `--module-root`/dev 形态验证安装:十阶段可全
2100
+ done 但重启后方案 api/页面 404(src 树供给不含 dist 产物,装而不达)。例行 `ojm build`
2101
+ 会冲掉已装方案的 web 登记(菜单消失,安装态完好)——backfill 补目录 + 重装同版本即愈合。
2102
+
2077
2103
  ### 11.4 生命周期命令与站点寻址约定
2078
2104
 
2079
2105
  `install` / `enable` / `disable` / `uninstall` / `trust` 都是**瘦客户端**——它们把请求发到目标站点
@@ -91,11 +91,11 @@ ojm dev
91
91
  # ojm market setup --user superadmin --pass aA123456 --new-pass <你的口令>
92
92
 
93
93
  # 2) 发包:合成 fixture(或 --real 走 ojm build --bundle)→ 确定性 tgz → 包级签名
94
- # --rebuild-index 顺带重建索引
95
- ojm market publish --fixture demo-solution@0.1.0 --id <发布方id> --rebuild-index
94
+ # 本机演练走本地落盘面;对真站点发布加 --site-url 即远端签名推送
95
+ ojm market publish --fixture demo-solution@0.1.0 --id <发布方id>
96
96
 
97
97
  # 3) 离线验包:不起站点、不装、不要凭据
98
- ojm market verify --rebuild-index
98
+ ojm market verify --file cdn/demo-solution-0.1.0.tgz
99
99
 
100
100
  # 4) 浏览器打开市场页 → 看到卡片 → 点安装
101
101
  # 首次遇陌生发布方会停在「待信任」→ ojm trust confirm --task <id> → 重试即成
@@ -106,13 +106,16 @@ ojm market verify --rebuild-index
106
106
  | 命令 | 做什么 | **不**做什么 |
107
107
  |---|---|---|
108
108
  | `market setup` | 写系统设置 `framework.market_cdn_url`(**外部真 CDN 场景才需要**;脚手架缺省 /cdn 免 setup) | 不改写工程根 `config.yaml`(`/cdn` 挂载由模板自带) |
109
- | `market publish` | 落 `<name>-<version>.tgz` + 包级 `.tgz.sig` | **不碰索引**(索引是共享单文件,多人发布会互相覆盖) |
110
- | `market build-index` | 扫 `cdn/*.tgz` 提取包内 manifest,全量重建两份索引并签名 | 不落包 |
111
- | `market verify` | 完整性 / 包级签 / 索引签 / manifest 结构四类校验 + 漂移检出 | 不起站点、不装、不访问任何远端 |
109
+ | `market publish` | 本机演练:落 `<name>-<version>.tgz` + 包级 `.tgz.sig`;加 `--site-url` 则远端签名推送(`.sig`/`.tgz` 按序推,pair 约束) | 不经账号口令(签名头 + 信任库);同字节重传幂等 |
110
+ | `market verify` | 单包校验:manifest 结构 / 包级签 / 契约-迁移一致性 | 不起站点、不装、不访问任何远端 |
112
111
 
113
- ### 4.2 索引为什么是「扫描重建」而不是「增量合并」
112
+ ### 4.2 为什么目录归站点库而不是共享索引文件
114
113
 
115
- 因为索引签名对不上多人协作:如果 publish 时读-改-写共享索引,后发者会把前者的签名覆盖掉,且存在竞态。而索引签名在运行期只是**完整性层**(防目录被改),不是信任锚——那就干脆把它降级成派生产物:**谁都能重建,重建结果只取决于目录里有什么**。这也是 `publish` 与 `build-index` 必须分成两个命令的原因。
114
+ 文件索引时代的教训:publish 读-改-写共享索引,后发者会覆盖前者的签名且有竞态;索引签名
115
+ 只是完整性层不是信任锚。现在的答案是把目录降级成站点的**派生数据**:签名推送完成时由
116
+ 服务端直写目录库(`market_release` 行),安装解析直读该库——没有共享可变文件,天然支持
117
+ 多人并发,重放同字节幂等;存量包/灾备恢复由站点 `POST /api/oj-market/backfill` 扫描
118
+ `cdn/` 自愈。
116
119
 
117
120
  ### 4.3 信任模型(30 秒版)
118
121
 
@@ -127,9 +130,12 @@ ojm market verify --rebuild-index
127
130
 
128
131
  `ojm dev` 的 Node 服务在 **5174**,把 `/api` 与 `/cdn`(config 有 `/cdn` 挂载时)一并反代给 oj 子进程;`/cdn` 是 **oj 的挂载**,直出工程根 `cdn/`。`framework.market_cdn_url` 写成相对路径 `/cdn`(seed 缺省值)时,服务端按**请求源**补全同源绝对 URL 再拉取——dev 下请求走 5174,反代链路内透传,拿到的是真索引。2026-10-23 之前 dev 只反代 `/api`,相对基址会拿到静态层回落的 index.html(难懂的 YAML 解析错)——该坑已由 dev 反代 + 消费侧 `withSiteOrigin` 补全闭环。历史上下文:`market setup` 写绝对地址(`http://<host>:<config server.port>/cdn`)依旧兼容可用。
129
132
 
130
- ### 5.2 CDN 里摆了 `.tgz.sig` 侧车,不等于能装
133
+ ### 5.2 安装验签的数据源是站点目录行
131
134
 
132
- 安装器的验签段只认 `manifests.yaml` 里的 `archive.signature`。侧车文件在目录里摆着**不够**,索引不带过去就是 `sig_missing` 硬拒。`market build-index` 已负责把侧车读进索引——所以规范动作永远是 **publish → build-index**,两个都跑。
135
+ 安装器的验签段认目录库行里的签名四列(由签名推送完成时服务端从 `.sig` 旁车写入)。
136
+ CDN 目录里只摆 `.tgz.sig` 侧车、库里没有对应行,就是 `sig_missing` 硬拒——所以规范动作是
137
+ **publish 把 pair 推完整**(`.sig` 先行 `.tgz` 后至,服务端配对成功才写行);
138
+ 存量包/灾备恢复由站点 `POST /api/oj-market/backfill` 扫描 `cdn/` 把旁车读进库补齐。
133
139
 
134
140
  > 附注:曾有一条「包内 `api/<mod>/manifest.yaml` 的 desc 必须逐字等于方案 summary」的校验,已于 2026-10-21 移除——那是构建期映射被当成安装期契约,而 desc 只是模块描述文本、非安全边界。现存相关校验只有:`summary` 必填、`desc` 必填非空。
135
141
 
@@ -171,8 +177,8 @@ node "$CLI_PKG/bin/ojm.mjs" sign dist/<name>-<version>.tgz --id demo.publisher -
171
177
 
172
178
  ```bash
173
179
  cd <host>
174
- node "$CLI_PKG/bin/ojm.mjs" market publish --real ../solution-demo --id demo.publisher --key ../keys/private.pem --rebuild-index
175
- node "$CLI_PKG/bin/ojm.mjs" market verify --rebuild-index
180
+ node "$CLI_PKG/bin/ojm.mjs" market publish --real ../solution-demo --site-url http://127.0.0.1:9778 --id demo.publisher --registry-id <站点 registry_id>
181
+ node "$CLI_PKG/bin/ojm.mjs" market verify --file cdn/<name>-<version>.tgz
176
182
 
177
183
  node "$CLI_PKG/bin/ojm.mjs" build # 生成 api/dist + web/dist(含 api/dist/versions.json)
178
184
  # config:platform.root 改成 api/dist(安装目标恒为 release 部署,root 必须与 --api-path 同源)
@@ -207,9 +213,9 @@ ls api/dist/<name>-<version>/ # 安装产物目录
207
213
  写模块 → 源码直跑(ojm dev),不要装
208
214
  验证能装 → ojm market trial(隔离站点,12s),或 ojm install <tgz> 到另一个工程
209
215
  重复? → 不会。注册面全 upsert;同名再装被前置校验直接拒
210
- 市场链路 → setup → publish → build-index → verify → 市场页安装(release 形态:--api-path api/dist)
216
+ 市场链路 → setup(外部真 CDN 才需要)→ publish(远端签名推送)→ verify → 市场页安装(release 形态:--api-path api/dist)
211
217
  绝对 URL → 市场地址端口取自 config(9778),不是 dev server 的 5174
212
- 信任 → 包级签=锚,索引签=完整性,TOFU 管陌生发布方,verify 不背书内容安全
218
+ 信任 → 包级签=锚,目录行签名四列=完整性,TOFU 管陌生发布方,verify 不背书内容安全
213
219
  ```
214
220
 
215
221
  ## 8. 延伸阅读
@@ -1,6 +1,6 @@
1
1
  # 方案包发布全流程——新手照做版
2
2
 
3
- 适用 ojm ≥ 0.2.22(写作时 0.2.66)。本文自包含:从「代码写完了」到「装进站点」一条命令链照着敲,每步附期望输出与通过判据。
3
+ 适用 ojm ≥ 0.2.22(0.3.0 改写:发布改远端签名推送,索引机制退役)。本文自包含:从「代码写完了」到「装进站点」一条命令链照着敲,每步附期望输出与通过判据。
4
4
  字段规范与错误码全口径见同目录 `manual.md` §11/§12——**本文不引用包外文件**,照着读完即可完成发布。
5
5
 
6
6
  ---
@@ -12,10 +12,9 @@ flowchart LR
12
12
  A["① 构建 ojm build"] --> B["② 打包 ojm build --bundle"]
13
13
  B --> C["③ 身份 方案市场→发布管理 申请凭证 → ojm keygen --credential"]
14
14
  C --> D["④ 签名 ojm sign --check"]
15
- D --> E["⑤ 发布 ojm market publish"]
16
- E --> F["⑥ 重建索引 ojm market build-index"]
17
- F --> G["⑦ 验证 market verify / trial"]
18
- G --> H["⑧ 安装 ojm install + TOFU"]
15
+ D --> E["⑤ 发布 ojm market publish(签名推送远端站点)"]
16
+ E --> G["⑥ 验证 market verify / trial"]
17
+ G --> H["⑦ 安装 ojm install + TOFU"]
19
18
  ```
20
19
 
21
20
  信任链硬 fail-closed:未签、签错、key 不被信任的包一律拒装,没有「先装了再说」的开关。
@@ -94,8 +93,8 @@ OJM_PUBLISH_CREDENTIAL=<token> ojm keygen # 或走环境变量,避免令
94
93
  不是「信任免检」。
95
94
  - 兑换完成后,「方案市场 → 发布管理」该条记录变为**已兑换**并显示 key_id。
96
95
  - 发版之后,「方案市场 → 发布历史」(`/oj-market/history`)按发布时间倒序列出所有产品/版本:
97
- 数据由站点从签名索引对账落库,**不需要 CLI 额外上报**;发布时间优先取分发包落 CDN 的
98
- `Last-Modified`,源不提供时回落索引生成时间并标「索引时间」标签。
96
+ 数据由站点在签名推送完成时直写目录库,**不需要 CLI 额外上报**;经站点回填命令补录的存量包
97
+ `published_at` 取回填时刻并标「回填时间」标签(与推送直写的行区分)。
99
98
 
100
99
  ### 2.1 没有站点时:离线自建身份(旁路)
101
100
 
@@ -140,22 +139,34 @@ ojm sign dist/<name>-<version>.tgz \
140
139
 
141
140
  ---
142
141
 
143
- ## 4. 发布到 CDN
142
+ ## 4. 发布到站点(远端签名推送)
144
143
 
145
- CDN 就是**一个装着固定命名文件的目录**:`<name>-<version>.tgz`(+ `.sig`)与索引。发布 = 把文件放进去 + 重建索引。推到远端真 CDN(OSS/S3)只是目录同步(rsync/工具上传),包文件名含版本、内容不可变,多人并发推不冲突。
144
+ 发布 = 把 `.sig` 旁车 + `.tgz` 包体**按序推到目标站点的市场上传端点**
145
+ (`POST /api/oj-market/cdn/upload`)。不经账号口令:身份与完整性全靠
146
+ `X-OJM-Key-Id` / `X-OJM-Signature` 请求头(信任库 + Ed25519)——站点信任库认这个 key,
147
+ 推送即被接受。CLI 可能根本没账号(CI/企微用户),推送链路为此设计。
146
148
 
147
149
  ### 4.1 单方案工程(一条命令)
148
150
 
149
151
  ```bash
150
152
  ojm market publish --real . \
153
+ --site-url https://your-site \
151
154
  --id <key_id> \
152
- --registry-id oj-registry \
153
- --rebuild-index
155
+ --registry-id <站点 registry_id>
154
156
  ```
155
157
 
156
- 期望输出:`[ojm] 包已落 CDN → cdn/<name>-<version>.tgz(N 字节)` + 发布方指纹。
157
- `cdn/` 是**三级索引**布局:`catalog.yaml`(+`.sig`)、`shards/<n>.yaml`(+`.sig`)、
158
- `<name>-<version>.tgz`(+`.tgz.sig`);分片数随方案数增长,不要按固定文件数核对。
158
+ 期望输出:`[ojm] 发布完成:<name>@<version> 已进 https://your-site 市场目录(DB 目录行 + cdn/ 字节)`。
159
+
160
+ 命令内部四步:构建 → 包级签名(同 §3)→ 目录探测(读站点 `registry_id`)→ 先推 `.sig` 再推 `.tgz`。
161
+ **pair 约束**:服务端只有配对的 `.sig` 在手才收 `.tgz`——顺序由命令保证,无须人工干预。
162
+
163
+ - `--site-url` 缺省读环境变量 `OJM_SITE_URL`;两个都没有 → 人话报错(不猜默认站点)。
164
+ - 站点版本探测:推送前 CLI 先匿名 GET 目录 API——旧站点(文件索引时代)无此 API → 404 →
165
+ 「站点版本过旧」人话 + 迁移指南指向;站点未配 `registry_id` → 明确指向 `config.yaml vars` 设置项。
166
+ - **半成功自愈**:`.sig` 已推、`.tgz` 被拒(如网络中断)——报错会提示「重跑本命令幂等补齐」:
167
+ 同字节重传幂等,pair 自动完成,无人工清场负担。
168
+ - **目标站点要先信任你的 key**(`ojm trust add` 或市场页信任确认)——否则推送 403,
169
+ 与安装的 `needs_trust` 同一信任库。
159
170
 
160
171
  > **`--real` 仅限单方案工程**(`src/` 下只有一份 `solution.yaml`);`ojm new` 生成的方案长在宿主工程里
161
172
  > (宿主还自带 `app`/`oj-market`),多方案工程走 §4.2 显式四步。
@@ -172,32 +183,44 @@ ojm market publish --real . \
172
183
  ```bash
173
184
  ojm build --bundle crm # ① 打包指定方案
174
185
  ojm sign dist/crm-1.0.0.tgz --id <key_id> \
175
- --registry-id oj-registry --check # ② 签名(私钥缺省取 ~/.ojm/keys/<key_id>/)
176
- cp dist/crm-1.0.0.tgz dist/crm-1.0.0.tgz.sig cdn/ # ③ tgz + .sig 放进 cdn/
177
- ojm market build-index --id <维护者key> --check # ④ 重建索引(见 §4.3)
186
+ --registry-id <站点 registry_id> --check # ② 签名(私钥缺省取 ~/.ojm/keys/<key_id>/)
187
+ ojm market publish --real . --site-url https://your-site \
188
+ --id <key_id> --registry-id <站点 registry_id> # ③ 远端签名推送(同 §4.1)
178
189
  ```
179
190
 
180
- ### 4.3 多人协作纪律(同一仓库、不同开发者、不同环境各自发包)
191
+ ### 4.3 多人协作纪律(不同开发者、不同环境各自发包)
181
192
 
182
- `market publish` 的设计就是为多人并发而做的:**每人只写自己的 `<name>-<version>.tgz`**,没有共享可变文件,互不干扰。四条纪律:
193
+ 上传端点天然支持多人并发:**每人推自己的 `<name>-<version>`**,服务端按库仲裁。三条纪律:
183
194
 
184
- 1. **落包并发安全**:谁都可以随时 `market publish` 落自己的包,不用排队。
185
- 2. **索引重建要中心化**:publish 落包后**索引不自动重建**(日志会提示「多人场景请由维护者跑 `ojm market build-index`」)。包落了但没重建索引 = 站点装不到。约定一个触发点:维护者定期跑,或 CI 钩子。**不要两人同时对同一 cdn/ 跑 build-index**(重建本身设计为单执行者)。
186
- 3. **同版本号字节冲突会被拒**:`crm@1.0.0` 已存在且字节不一致时 publish 直接报错——内容有变就升 version 重发,同一版本号全宇宙只允许一份字节。同源重发字节一致则幂等通过。
187
- 4. **同名方案两把 key 是治理问题**:技术上可行(索引按名字聚合版本、信任按 key),但「这个方案名归谁」机制不裁决——不同人发**不同方案**随便;发**同一方案名**需团队事先约定(推荐:方案名归属唯一发布方,他人在该方案上协作走代码仓库而非另起 key 发布)。
195
+ 1. **并发安全**:谁都可以随时推自己的包;同一 `name@version` 同字节重传幂等通过。
196
+ 2. **同版本号字节冲突会被拒(409)**:`crm@1.0.0` 已存在且字节不一致 → 内容有变就升 version 重发,
197
+ 同一版本号全宇宙只允许一份字节。
198
+ 3. **方案名所有权绑定首发 key(409)**:`crm` 的第一个上传者把名字绑定到自己的 key_id,
199
+ 别的 key 推 `crm@*` 会被拒——换主是站点 superadmin 操作(端点不提供)。
188
200
 
189
- ---
201
+ ### 4.4 下线与恢复(yank)
202
+
203
+ ```bash
204
+ ojm market yank crm 1.0.0 --site-url https://your-site --id <key_id> # 下线(仅发布者本人)
205
+ ojm market yank crm 1.0.0 --site-url https://your-site --id <key_id> --restore # 恢复
206
+ ```
207
+
208
+ - 签名鉴权同推送(canonical = `market-yank:<站点 registry_id>:<产品>:<版本>`),
209
+ **只有行内记录的发布 key 可下线/恢复**(key 不符 403);重复 yank/restore 幂等 200。
210
+ - yank **不动已装站点**:只把版本从目录摘除(详情标 `yanked:true`),已装的方案照常运行;
211
+ 精确版本安装被 409 人话拒。`--reason`/`--by` 留审计痕(发布历史可见)。
190
212
 
191
213
  ## 5. 验证发布(不起站点)
192
214
 
193
215
  ```bash
194
- ojm market verify --rebuild-index # 逐项验:完整性 / 包级签 / manifest 结构 / 索引签(catalog+分片)/ 索引同代 / 索引时效 / 迁移一致性
195
- ojm market trial --fixture <name>@<version> # 隔离站点跑完整安装,跑完即毁,不污染工程
196
- ojm market walkthrough --fixture <name>@0.1.0 # 14 项 AC:主链 9 + 负向 5;报告落 logs/walkthrough-<ts>.md
216
+ ojm market verify --file dist/<name>-<version>.tgz # 包级验证:结构 / 包级签 / 迁移一致性
217
+ ojm market trial --fixture <name>@<version> # 隔离站点跑完整安装,跑完即毁,不污染工程
218
+ ojm market walkthrough --fixture <name>@0.1.0 # 14 项 AC:主链 9 + 负向 5;报告落 logs/walkthrough-<ts>.md
197
219
  ```
198
220
 
199
- - `verify` 的检查项数随包数与分片数变化,判据是**末行「全部校验通过(N 项)」且无未通过项**,
200
- 不要按固定条数核对。
221
+ - `verify --file` 验**单个包**(发布前自检):manifest 结构、包级 detached 签名、
222
+ 契约-迁移一致性;末行「全部校验通过(N 项)」。市场目录的一致性由站点目录库自保证,
223
+ 不再有「索引文件」这层需要人工重建/校验的东西。
201
224
  - `walkthrough` 的 fixture **必须是 `@0.1.0`**:主链的 upgrade/rollback 硬编码 0.1.0→0.2.0,
202
225
  给别的版本这两项会 FAIL(是 fixture 选错,不是框架缺陷)。
203
226
 
@@ -211,12 +234,25 @@ walkthrough 期望 `14 PASS / 0 FAIL`。负向 5 项是**预期失败口径**:
211
234
  # ⓪ 先构建:release 形态服务的是产物,不先 build 会缺 dist/api(或装到旧产物)
212
235
  ojm build
213
236
 
214
- # ① 站点以 release 形态起(dev/preview 完不成安装的热更段)
215
- # 先把 config.yaml 的 platform.root 从 src 改成 dist/api——必须与 --api-path 同源,
216
- # 否则 oj serve 直接拒启:platform.root (...) must be mounted as an api mount
217
- # 再配 vars.canonical_origin(本地演练填 http://127.0.0.1:9778):市场基址 /cdn 是相对路径,
218
- # 服务端要靠它补全成绝对 URL;缺了安装直接 503(REG-CS6 不读 Host 头,不会替你猜)
219
- oj serve -c config.yaml -b /api --api-path dist/api
237
+ # ① 站点以 release 形态起(dev/preview 完不成安装的热更段)。两种等价形态,**首选 A**:
238
+ # A. config 挂载形态(零 CLI 旗标;2026-11-07 实证重启持久可达):config.yaml 做两处改动——
239
+ # platform.root: dist/api # 从 src 改过来,必须与 api 供给同源
240
+ # mounts: 加一条 - prefix: /api
241
+ # api: dist/api # 锁驱动装配:已装方案的版本目录也进路由表
242
+ # 然后:oj serve -c config.yaml # API 基座 = 挂载前缀 /api
243
+ # B. CLI 旗标形态(config 不加 api 挂载):oj serve -c config.yaml -b /api --api-path dist/api
244
+ # 此形态下 "/" 的 web 挂载路径必须写绝对路径(web: /abs/.../dist/web)——相对路径 + -b
245
+ # 组合会让静态文件全 404(SPA 回落正常,极具迷惑性)。
246
+ # 两形态共同红线:platform.root 与 api 供给不同源,oj serve 直接拒启
247
+ # (platform.root (...) must be mounted as an api mount)。
248
+ # 注意 platform.root 是 dev/release 开关:改成 dist/api 后 `pnpm dev` 会同样拒启——
249
+ # dev 从 src 树供给 api;要开发就切回 src,要 release 起站就保持 dist/api(两形态互斥,
250
+ # 同一份 config 不能同时喂 dev 和 release)。且 dev/例行 build 的模块重建都会冲掉
251
+ # 已装方案的 web 登记(见 §7 速查「例行 build 后菜单消失」行)。
252
+ # 另配 vars.canonical_origin(本地演练填 http://127.0.0.1:9778):市场基址 /cdn 是相对路径,
253
+ # 服务端要靠它补全成绝对 URL;缺了 cdn 源安装直接 503(不读 Host 头,不会替你猜)。
254
+ # file 直传安装不读 canonical_origin——只有按名从市场目录拉才需要。
255
+ oj serve -c config.yaml # 形态 A(config 已加 api 挂载时);或形态 B 的旗标命令见上
220
256
 
221
257
  # ② 市场地址:本地自供 /cdn 是模板默认,**不需要 setup**;
222
258
  # 只有指向外部真 CDN 才要执行。注意 setup 不一定改密:账号不在「必须改密」悬挂期时
@@ -224,9 +260,11 @@ oj serve -c config.yaml -b /api --api-path dist/api
224
260
  # 2026-11-06 演练实踩:setup 后拿新口令登录得 401。真触发了改密闭环才用新口令:
225
261
  # ojm market setup --site-url http://127.0.0.1:9778 --user superadmin --pass <当前口令> --new-pass <新口令>
226
262
 
227
- # ③ 安装(从 CDN 拉:name[@version];直传文件则给 tgz 路径,.sig 同目录自动随包)
228
- # 只对 release 站点装;别对着 `ojm dev` 装——dev 的文件监听会在安装器 reload 之前
229
- # 先吃掉 config 改动,安装器自己再 reload 时得到 unchanged,判 partial(reloading)
263
+ # ③ 安装(从 CDN 拉:name[@version];直传文件则给 tgz 路径,.sig 同目录自动随包)。
264
+ # 只对 release 站点装;别对着 `ojm dev` 或 --module-root 形态装——src 树供给不含 dist 产物,
265
+ # 十阶段可以全 done 但重启后 /api/<name>/ping 与方案页面全 404(装而不达,2026-11-07 实证);
266
+ # 也别对 `ojm dev` 装第二个理由:dev 的文件监听会在安装器 reload 之前先吃掉 config 改动,
267
+ # 安装器再 reload 时得到 unchanged,判 partial(reloading)
230
268
  ojm install crm@1.0.0 --site-url http://127.0.0.1:9778 --user superadmin --pass <口令>
231
269
 
232
270
  # ④ 首装陌生 key 必停在 needs_trust:响应 trust.fingerprint 与 §2 记下的指纹【逐字】核对
@@ -268,13 +306,24 @@ TOFU 没有免检通道:**只有模板内置的 oj-market(第一方包)用
268
306
  | 安装拒「包内原生清单含白名单外键 `permissions、roles`…」(extracting 段,不可自动重试) | 角色/权限声明写进了 `api/manifest.yaml`(oj 原生七键清单) | 声明挪到方案契约 `solution.yaml`(上级目录那一份);`api/manifest.yaml` 只留 oj 原生七键(2026-11-06 演练实踩) |
269
307
  | 安装 preflight 拒 RBAC 声明 | 声明形状不符:权限 code / 角色 name 没带 `<name>.` 前缀、`title_key` 缺失或 ns 不符、角色引用了未声明的权限(悬空) | 按报错条款改 `solution.yaml` 的 `permissions[]` / `roles[]`(字段细则见 devkit 手册 §11.5) |
270
308
  | 装完 `curl /api/<name>/ping` 返回 400 `missing tenant header` | 少了 `X-TENANT-ID` 头 | 加 `-H 'X-TENANT-ID: default'` 再探 |
309
+ | 安装/收敛卡在 reloading 段 `NotCapable`(市场编排端点) | 站点 config 缺市场编排模块授权:`platform.exec` 没有该模块的白名单条目,或 `platform.modules` 列表缺它(安装器热更 config / 写信任库需要这份特权) | 给市场编排模块配与 `_platform` 逐字相同的 exec 白名单并加入 `platform.modules`(新工程模板自带;**旧工程升级脚手架不会自动补**,需手工改 config 后重启宿主) |
310
+ | `-b /api` 起站后 API 正常但静态文件全 404(`/modules/<name>/<ver>/entry.js` 返回 API 信封 `no route matched`;SPA 回落却正常) | web 挂载用了**相对路径**:`-b` 基座覆盖与相对路径组合时静态层解析落空(极具迷惑性) | web 挂载写绝对路径(`web: /abs/path/dist/web`);或改用 config 挂载形态起站(§6 ① 形态 A,零 CLI 旗标、无此坑) |
311
+ | `pnpm dev` 启动即退:`platform.root (...) must be mounted as an api mount` | config 处于 release 形态(platform.root 改成了 dist/api,见 §6 ①)——dev 从 src 树供给 api,与 release 互斥 | 要开发:platform.root 切回 `src` 再 `pnpm dev`;要 release 起站:保持 dist/api 用 §6 ① 的 serve 命令(别用 pnpm dev) |
312
+ | 例行 `pnpm build`(`ojm build`)/跑过 `pnpm dev` 后,已装方案的市场菜单/页面全消失(`dist/web/modules.json` 只剩 app) | 两者都会重生成 `dist/web/` 且只收录**源码树模块**,安装器登记的方案 web 面被整体冲掉;安装态本身完好(方案状态/锁/api 版本目录都在) | 愈合两步:① `POST /api/oj-market/backfill` 扫 `cdn/` 补市场目录行(从市场目录装的方案需要);② 逐方案 `ojm install <name>[@version]` 重装同名同版本——web-only 包 api 锁零变更,重装只补 web 面 |
313
+ | 随站 bundle 反复装不上(打开「方案安装」没反应;审计表有收敛失败行) | 信任空窗(bootstrap key 未回填且信任库无发布 key),或刚失败过仍在收敛冷却期 | 先推发布公钥进信任库,再删冷却文件 `modules/.install-tasks/.bundle-converge-cooldown.json` 立即重试(冷却默认 10 分钟,自愈等待亦可) |
271
314
  | `sig_alg` / `sig_format` | 签名算法/.sig 结构不符 | 用当前 ojm 重签(旧版本工具产物) |
272
315
  | `needs_trust`(第五终态,非成功) | 站点第一次见这个 key | 与发布方逐字核对指纹 → `trust confirm --task <id>` → 重跑 install |
273
- | publish 报「包冲突:…字节不一致」 | 同 name@version 已有字节不同的包 | **升 version 重发**;确需覆盖先删 CDN 旧 tgz |
316
+ | publish 报「包冲突:…字节不一致」(409) | 同 name@version 已有字节不同的包 | **升 version 重发**;确需覆盖须站点侧删行(superadmin) |
317
+ | publish 报「name 所有权:已由 key 首发绑定」(409) | 方案名绑定过别的发布 key | 换方案名;或站点 superadmin 换主 |
318
+ | publish 报「站点版本过旧(无 /api/oj-market/catalog)」 | 目标站点是文件索引时代的旧版 | 站点先升级到 DB 目录版本(迁移指南见 docs/migration/ 市场远程发布篇) |
319
+ | publish 报「站点未配置 registry_id」(503) | 站点 config.yaml 的 vars 缺 registry_id | 站点管理员补 `registry_id: <站点身份>` 后重启 |
320
+ | 推送 401(签名不符) | 换字节/跨站重放,或 canonical 与站点重算不一致 | 原样重跑本命令(同字节幂等);改过包则重签重推 |
321
+ | 推送 403(key 不在信任库) | 站点还没信任你的发布 key | 站点 superadmin `ojm trust add` 或市场页信任确认 |
274
322
  | publish 报「一次只发布一份方案」 | 多方案工程走了自动链路 | §4.2 显式四步 |
323
+ | yank 403 | 不是该版本的发布 key(行 key_id 仲裁) | 用首发 key 操作;换主是 superadmin 面 |
275
324
  | 打包报「缺少本方案的站点产物」 | 没先 `ojm build`;或**卸载方案时把 `dist/api/<name>-<ver>` 一并移除了**(2026-11-06 实测:卸载四同删含版本目录,重发同版本前必须重建);或前端入口不在 `src/<模块>/web/entry.ts` | 先 `ojm build` 再 `--bundle`;补 entry |
276
325
  | 打包报「缺少 bundle 级契约」 | 没有 `solution.yaml` | 补契约(§0 前提 1) |
277
- | 装不到刚发的包 | 索引没重建 | `ojm market build-index`(§4.3 纪律 2),确认 `cdn/` 的 catalog + 分片 + tgz(+sig) 已同步 |
326
+ | 装不到刚发的包 | 推送未完成(半成功)或 .sig/.tgz 顺序颠倒 | 重跑 publish(同字节幂等补齐);站点 superadmin 可跑 `POST /api/oj-market/backfill` 对账自愈 |
278
327
  | 装成后 404 | 方案路由没注册 | 重启 `oj serve`(§6 通过判据) |
279
328
  | `oj serve` 报 `platform.root (...) must be mounted as an api mount` | release 起站了,但 config 的 `platform.root` 还是 `src` | 改成 `dist/api`(与 `--api-path dist/api` 同源)后重启 |
280
329
  | `oj serve` 报 `lock points to missing version dir: dist/api/<name>-<ver>` | 锁文件记着某个已装版本,但那个版本目录不在了(清过 dist / 换过机器) | 先 `ojm build`;锁与目录对不上时按站点安装记录重装该方案,别手改锁 |
@@ -282,13 +331,53 @@ TOFU 没有免检通道:**只有模板内置的 oj-market(第一方包)用
282
331
 
283
332
  ---
284
333
 
334
+ ## 7.5 第一方随站 bundle(oj-market 自身)发布流程
335
+
336
+ > 仅框架发布人需要;业务方案发布用 §1–§6 常规流程。oj-market 市场 UI 是第一方
337
+ > **纯 web 方案包**(契约 `templates/src/oj-market/solution.yaml`,第一方专用,
338
+ > 第三方方案不得仿照 `/api/oj-market/*` 调用面),**出厂未装**:
339
+
340
+ - **打包**:templates 工程 `pnpm build` → `ojm build --bundle oj-market`
341
+ (产 `templates/dist/oj-market-<ver>.tgz`,版本基线 0.2.0 起;双轨版本:包版本随 entry.ts,平台 api 版本随脚手架);
342
+ - **签名**:`ojm sign <tgz> --id <publisher>.<key> --registry-id <registry>` 产 .sig 旁车
343
+ (私钥永不出 `~/.ojm/keys/`,手工动作);
344
+ - **收编**:`pnpm prepack` 的 `scripts/sync-bundles.mjs` 把 templates/dist 里**已签名**
345
+ 的 tgz+.sig 收进 `shell-dist/bundles/` 随 cli 包发布(未签名不收;dist 未构建则告警跳过);
346
+ - **信任根基**:把签名公钥(64 hex)+ key_id 回填 `templates/src/oj-market/api/bootstrap.ts`
347
+ 的 `BOOTSTRAP_KEY`(模板内只有公钥;不回填则首装落 needs_trust 走 TOFU);
348
+ - **分发**:`ojm init --yes` 把 bundle 拷入新工程 `cdn/` 并写 `bundles.json` 意图标记——
349
+ 站点首个市场编排请求(如 superadmin 打开 system 域「方案安装」)自动收敛安装;
350
+ **上拉不下拉**:未装→装、bundle 新→升级、bundle ≤ 已装(市场轨升过)→ 跳过不降级。
351
+ 交互式 init 零动作(保持手动口径);bundle 缺失告警不失败。
352
+
353
+ **运维须知(真机演练钉出的口径)**:
354
+
355
+ - **部署面双授权,旧工程要手工补**:站点 config 里市场编排模块要有两样——exec 白名单里
356
+ 有它的条目(与 `_platform` 逐字相同的解 tar / 跑迁移命令),`platform.modules` 列表同时
357
+ 含它(安装器热更 config、写信任库需要这份特权)。新工程随 init 的模板 config 已自带;
358
+ **旧工程升级脚手架不会自动补**,收敛/安装会卡在 reloading 段报 NotCapable,补上重启即可。
359
+ - **收敛失败可观测、有冷却**:自动收敛失败不静默——审计落 `market_history`(operator 记
360
+ `bundle-converge`),并写 10 分钟冷却文件 `modules/.install-tasks/.bundle-converge-cooldown.json`
361
+ 防风暴。典型是信任空窗(bootstrap key 未回填 + 信任库无发布 key):推公钥后删冷却文件,
362
+ 下一次编排请求立即重试。
363
+ - **卸载是墓碑不是暂离**:卸载第一方市场包会把 `cdn/bundles.json` 里的意图条目一并摘掉——
364
+ 收敛不会再把它装回来;重装走 system 域「方案安装」页手动入口。
365
+ - **纯 web 包的安装语义**:随站 bundle 是 web-only 包——安装时 api 锁零变更(版本目录整体
366
+ 就位承载前端产物),市场内不提供降级入口(降级 = 站点产物回滚)。它与平台内建编排模块
367
+ 同名(双轨同名):api 锁/版本目录归平台轨,方案 sidecar 归方案轨,安装/卸载第一方包
368
+ 都不碰内建面。
369
+ - **例行构建会冲掉已装方案的 web 面**:站点跑 `ojm build` 后 `dist/web/modules.json` 与
370
+ `dist/web/modules/` 只含源码树模块,已装方案的市场菜单/页面消失(安装态完好)。愈合 =
371
+ backfill 补目录行 + 逐方案重装同版本(见 §7 速查表对应行)。
372
+ - 端到端随站收敛演练步骤见框架仓《框架验证演练手册》§7.6(市场线)。
373
+
285
374
  ## 8. 发布前 checklist
286
375
 
287
376
  - [ ] `solution.yaml` 的 `version` 已升,与文件名一致;
288
377
  - [ ] `ojm build` + `ojm build --bundle` 均通过,sha256 记进发布单;
289
378
  - [ ] `ojm sign … --check` 通过,`.sig` 与 tgz 同目录;
290
379
  - [ ] 发布身份来自站点凭证(有发布管理页时),「方案市场 → 发布管理」页该条已变**已兑换**;
291
- - [ ] 发版后「方案市场 → 发布历史」能看到这次的 产品 / 版本 / 发布时间(时间来源为索引兜底时会标「索引时间」);
380
+ - [ ] 发版后「方案市场 → 发布历史」能看到这次的 产品 / 版本 / 发布时间(回填补录的存量包标「回填时间」);
292
381
  - [ ] 新 key 首次发布:指纹已提前交给运维(免得卡在 needs_trust);
293
- - [ ] `ojm market verify` 末行「全部校验通过(N 项)」,无未通过项;
294
- - [ ] 多人团队:索引重建触发点已约定(维护者或 CI)。
382
+ - [ ] `ojm market verify --file dist/<name>-<version>.tgz` 末行「全部校验通过(N 项)」,无未通过项;
383
+ - [ ] 目标站点已在信任库(`ojm trust add` 或市场页确认),推送 403 预案已知。