@oj-module/cli 0.2.15 → 0.2.18

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.
@@ -113,6 +113,7 @@ pnpm test tests/shell # 宿主全量
113
113
  | 内置异常后备页 | 框架内置 `/exception/403\|404\|500` 后备页;exception 模块仅为可选覆盖 |
114
114
  | 宿主 runtime 版本锁等 | prepack 断言宿主 runtime 版本 == `packages/runtime` 版本 |
115
115
  | merge 合并多团队清单 | `ojm merge` 合并多团队清单 |
116
+ | 站点 base 单点 | 部署前缀只从 `resolveSiteBase()`(`window.__OJ_BASE__` → `BASE_URL`)取;runtime 里禁止再读 `import.meta.env.BASE_URL` |
116
117
 
117
118
  ### 4.5 守卫测试索引(改动后知道哪个会红)
118
119
 
@@ -124,6 +125,10 @@ pnpm test tests/shell # 宿主全量
124
125
  | `tests/runtime/scoped-request.test.ts` | 请求前缀收敛(scoped client) |
125
126
  | `tests/shell/no-fake-in-dist.test.ts` | 生产产物不含 fake |
126
127
  | `tests/shell/shell-importmap.test.ts` | 门禁一致性 + runtime.js 与包 dist 一致 + 深路径回归 |
128
+ | `tests/runtime/site-base.test.ts` | 站点部署 base 单点:`import.meta.env.BASE_URL` 只许出现在 `utils/site-base.ts` |
129
+ | `tests/cli/site-base.test.ts` | 三份 base 实现(runtime / shell / cli)逐条对撞 + `index.html` 重写与 `__OJ_BASE__` 注入 |
130
+ | `tests/shell/site-urls.test.ts` | 宿主清单 entry/css/chunks 补部署前缀(子路径部署不白屏) |
131
+ | `scripts/verify-subpath.mjs`(`pnpm verify:subpath`) | 子路径部署 **e2e**:真构建 + 真 `oj serve`,核对 importmap/`__OJ_BASE__`/modules.json/深链刷新/零回归字节一致 |
127
132
  | `tests/shell/host-version-drift.test.ts`、`version-gate.test.ts` | 共享依赖三处同步 |
128
133
  | `tests/cli/release-manifest.test.ts` | 真跑 `pnpm pack`,守护 `workspace:*` → 精确版本改写 |
129
134
  | `tests/cli/__snapshots__` | 契约代码生成快照(改 `.schema` 发射规则需同步更新) |
package/devkit/manual.md CHANGED
@@ -180,7 +180,8 @@ web/src/<模块名>/
180
180
  ```ts
181
181
  // web.config.ts
182
182
  export default {
183
- baseUrl: "",
183
+ baseUrl: "", // 模块产物 URL 前缀(可填 CDN 绝对域);与站点部署位置无关
184
+ // siteBase: "/oj", // 站点挂在 https://host/oj/ 下时才需要(默认根路径),见 §13 的路径空间一节
184
185
  modules: [
185
186
  // home 必须保留且尽量靠前:shell 预构建把 "/" → VITE_BASE_HOME_PATH(=/home)
186
187
  // 重定向,缺 /home 路由会让登录回跳 / 点 logo 落错误边界。
@@ -1465,6 +1466,9 @@ export default async (path: string, html: string) => {
1465
1466
 
1466
1467
  `ojm` 暴露两类命令,务必分清(这也是本手册 §7 反复强调的「oj 透传」指的是什么):
1467
1468
 
1469
+ > 每个命令都支持 **`ojm <cmd> --help`**(或 `ojm help <cmd>`)打印该命令的用法与全部选项——
1470
+ > 记不住参数时直接加 `--help`,不必翻本节。
1471
+
1468
1472
  | 类别 | 命令 | 谁实现 | 一句话 |
1469
1473
  |---|---|---|---|
1470
1474
  | **ojm 原生** | `init` / `dev` / `build` / `preview` / `api` / `info` / `vendor` / `merge` / `install` / `enable` / `disable` / `uninstall` / `trust` / `keygen` / `sign` / `publish` | `ojm` 自己 | 前后端一体工程的脚手架、开发、构建、契约生成、版本/清单、oj 分发、清单合并;方案包安装面与签名信任(install/enable/disable/uninstall 见 §11,trust/keygen/sign 见 §12,publish 见 §11.2) |
@@ -1595,6 +1599,29 @@ export default async (path: string, html: string) => {
1595
1599
 
1596
1600
  `secret`(`keygen` / `seal` 等)**不在透传集合内**——它不需要 config,需直接 `./bin/oj secret ...`。
1597
1601
 
1602
+ ### 10.3 本地市场链路:`ojm market` 子命令
1603
+
1604
+ `market` 是一组**本地模拟 CDN + 市场链路**工具,专供作者在本机验证「方案包能不能发、能不能装」,
1605
+ **不触碰真实生产 CDN**。所有产物落在工程根 `cdn/`(由 `api/config.yaml` 的 `/cdn` 挂载直出),
1606
+ 首跑用 `market setup` 把系统设置 `framework.market_cdn_url` 指过去即可在本地市场页看到。
1607
+
1608
+ > 跑任何子命令前先 `ojm market setup` 一次(把模拟 CDN 地址写进站点配置)。
1609
+ > 想看「这个包到底能不能装」而不污染工程,用 `ojm market trial`(一次性隔离站点,跑完即毁)。
1610
+ > 每个子命令都能 `ojm market <sub> --help` 单独看帮助。
1611
+
1612
+ | 子命令 | 一句话 |
1613
+ |---|---|
1614
+ | `ojm market setup [--url <站点>] [--user <u> --pass <p>] [--new-pass <p>]` | 把 `framework.market_cdn_url` 指向本地 `cdn/`;首跑若账号处于首次改密期会先自动改密再写入 |
1615
+ | `ojm market publish [--fixture <name>@<version>] [--real <方案工程>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]` | 把方案包落进 `cdn/` 并做包级 detached 签名;**不碰索引**(索引由 `build-index` 重建,避免多人发布互相覆盖) |
1616
+ | `ojm market build-index [--id <key>] [--key <pem>] [--check]` | 扫描 `cdn/*.tgz`、提取包内 manifest,全量重建 `bundles.yaml` + `manifests.yaml` 并签名;结果只取决于目录内容,连续执行字节一致 |
1617
+ | `ojm market verify [--file <tgz>] [--rebuild-index]` | 离线验包(不起站点、不装、不要凭据):sha256 完整性 / 包级 Ed25519 验签 / 索引验签 / manifest 结构分列输出;`--rebuild-index` 额外比对重建索引与盘上索引的漂移 |
1618
+ | `ojm market trial [--fixture <name>@<version>] [--keep]` | 在一次性隔离站点里跑完整安装,跑完即毁;`--keep` 保留现场供排查 |
1619
+
1620
+ - `--fixture <name>@<version>`:用内置合成包(无需先 build);`--real <方案工程>`:先 `ojm build --bundle` 再拿真实产物。
1621
+ - `--id <key>` / `--key <pem>`:包级签名身份(与 `ojm sign` 同体系,默认取 `~/.ojm/keys`)。
1622
+ - `--check`:离线自检签名(不必靠装失败发现签错)。
1623
+ - `verify` 的边界声明保留:**校验通过只说明来源与完整性,不构成内容安全性背书**。
1624
+
1598
1625
  ---
1599
1626
 
1600
1627
  ## §11 方案包:打包与分发
@@ -1665,6 +1692,26 @@ flowchart TD
1665
1692
 
1666
1693
  - **needs_trust 确认后重试同一安装**即可续跑;一切消费面须把它当**非成功态**渲染。
1667
1694
 
1695
+ ### 11.4 生命周期命令与站点寻址约定
1696
+
1697
+ `install` / `enable` / `disable` / `uninstall` / `trust` 都是**瘦客户端**——它们把请求发到目标站点
1698
+ (需 superadmin),本地只做「登录 → 提交 → 轮询任务至终态」并打印结果。三处寻址约定完全一致:
1699
+
1700
+ - `--url <站点地址>`:缺省 `http://127.0.0.1:9778`;
1701
+ - `--user <账号>` / `--pass <密码>`:站点凭据;
1702
+ - 等价环境变量 `OJM_URL` / `OJM_USER` / `OJM_PASS`(命令行缺省时回退读取,**凭据只走内存不落盘**);
1703
+ - `--locale <zh-CN|en-US>`:`install` 专用,切换失败文案双语(默认按站点 locale)。
1704
+
1705
+ | 命令 | 作用 | 注意 |
1706
+ |---|---|---|
1707
+ | `ojm install <file.tgz \| name[@version]> [--name <n>] [--version <v>]` | 安装方案包(file 直传 / cdn 拉取,见 §11.2),十阶段状态机见 §11.3 | `<name>-<version>.tgz` 文件名推导身份,可用 `--name`/`--version` 覆盖 |
1708
+ | `ojm enable <name>` / `ojm disable <name>` | 启用 / 禁用已装方案 | 四处同写:锁 / sidecar / 路由清单 / 模块清单 |
1709
+ | `ojm uninstall <name>` | 卸载已装方案 | 四处同删 + 权限级联清理 + 版本目录移除;**站点至少保留一个已锁模块** |
1710
+ | `ojm trust ls\|add\|revoke\|confirm` | 站点信任库管理(§12.2) | `confirm --task <id>` 把 needs_trust 待确认 key 入库 |
1711
+
1712
+ > `enable`/`disable`/`uninstall` 失败多半是「站点地址/凭据没给对」或「站点没这个方案」——
1713
+ > 先 `ojm info` 贴出版本矩阵与模块清单,确认目标站点与方案名无误再重试。
1714
+
1668
1715
  ---
1669
1716
 
1670
1717
  ## §12 签名与信任
@@ -1745,59 +1792,71 @@ ojm trust revoke <key_id> # 吊销:阻断新装/升级,不动已装
1745
1792
  ## §13 方案菜单归组规则
1746
1793
 
1747
1794
  > 适用:写方案(前后端一体模块)时让路由菜单按模块自动分组。归组是**纯视觉层**——
1748
- > 开发态(`ojm dev` 直接扫源码)不归组;装进宿主且方案路由行就绪后才归组,开发形态不变。
1795
+ > 归组元数据(`category` / `order` / `module`)的真源是 **`entry.ts` 路由 handle**,因此
1796
+ > **连 dev 形态(`sys_solution_route` 为空)也会归组**,不再依赖方案路由行就绪。
1797
+ > 完整心智模型与流水线见 [`menu-rules.md`](./menu-rules.md)。
1749
1798
 
1750
- ### 13.1 归组规则
1799
+ > **2026-10 起设计已变**:取消「`main` ≥2 条按模块收合成父菜单」的旧归组;改为**单根模块约定**
1800
+ > (一个方案只有一个根路由,它本身就是第一级菜单),admin 走**路由层真重写**到
1801
+ > `/system/<模块 root path>/*` 再视觉归组,管理子组与模块根菜单共用 `<模块>:menu.main` 一个 key。
1802
+ > 完整规则、流水线、`currentActiveMenu`、模块内路径助手见 [`menu-rules.md`](./menu-rules.md)(自包含)。
1803
+ > **路径写法另见同文件 §7「两个路径空间」**:模块内只产出**路由空间**路径(`/wiki/...`),
1804
+ > 禁止拿 `window.location.pathname` 做匹配、禁止读 `import.meta.env.BASE_URL`;站点要挂到
1805
+ > 非根前缀(`/oj`)时改 `web.config.ts` 的 `siteBase` 一项即可,用法与原理也在那一节
1806
+ > (配好后用 `pnpm verify:subpath` 一键验收:真构建 + 真 `oj serve` + 深链刷新)。
1751
1807
 
1752
- 方案路由分 `main` / `admin` / `hidden` 三类。归组只动 `main` 和 `admin`:
1808
+ ### 13.1 单根模块约定(第一级菜单 = 模块根路由)
1753
1809
 
1754
- - **`main` 路由 ≥ 2 条(同模块)** → 收进一个合成父菜单:
1755
- - 父项 key:`solgroup:main:<模块名>`
1756
- - 父项标题:裸 i18n key `<方案名>:menu.group`(翻译归渲染层,本仓只存裸 key)
1757
- - 子项按各自 `order` 升序;只有 1 条 `main` 路由时不归组,原样平铺
1758
- - **`admin` 路由** → 挂进宿主底部「系统设置」(`/system`)菜单的 children 末尾,同样按模块
1759
- 归成 `solgroup:admin:<模块名>` 父项
1760
- - 若宿主没有 `/system`(极少数自定义宿主):`admin` 路由不归组、回落平铺,并告警一次
1761
- (warnOnce,同消息不重复)
1810
+ 一个方案 `entry.ts` `routes` 只有**一个根路由**(如 `path: "wiki"`),它直接渲染为侧栏第一级菜单项;
1811
+ 其余页面是其 `children`。根路由 `handle.title` 用 `<模块>:menu.main`。不要为归组另写分组根路由。
1812
+
1813
+ ```ts
1814
+ export default defineModule({
1815
+ name: "wiki",
1816
+ routes: [{
1817
+ path: "wiki",
1818
+ handle: { title: "wiki:menu.main", layout: "container" },
1819
+ children: [
1820
+ { path: "space", handle: { title: "wiki:menu.space", layout: "parent" } },
1821
+ { path: "admin", handle: { title: "wiki:menu.admin", category: "admin", layout: "container" } },
1822
+ { path: "status/detail", handle: { title: "wiki:menu.statusDetail", category: "hidden" } },
1823
+ ],
1824
+ }],
1825
+ });
1826
+ ```
1762
1827
 
1763
- > `/system` 是框架侧断言,不是方案 manifest 能声明或验证的。方案只需把 `admin` 路由标
1764
- > `category: "admin"`,挂载点交给框架。
1828
+ ### 13.2 category 三值语义
1765
1829
 
1766
- ### 13.2 组标题 i18n key 约定(最容易踩的坑)
1830
+ `handle.category`:
1767
1831
 
1768
- 归组父项标题是 `<方案名>:menu.group`(`<方案名>` = 你的方案 manifest 的 `name`)。这条 key
1769
- **必须**存在于双语 locale 文件,且必须是**嵌套**结构:
1832
+ - 省略 / `""` / 旧 `"main"` → 普通可见菜单项。**`"main"` 不再产生合成父菜单**(扁平 main 归组已取消),新代码直接省略。
1833
+ - `"hidden"` → 有效路由但**不进菜单**(等价于 `hideInMenu: true`),用于详情页等。
1834
+ - `"admin"` → 路由层被 `rewriteAdminPaths` 真重写为 `/system/<模块 root path>/*`(运行时 URL 真正归到系统设置),菜单层再被 `groupSolutionMenus` 收进 `/system` 子组 `solgroup:admin:<模块>`。
1770
1835
 
1771
- ```jsonc
1772
- // web/src/<模块>/locales/zh-CN.json
1773
- { "menu": { "group": "Wiki" } } // 命中 <方案名>:menu.group
1836
+ > `/system` 是框架侧断言,方案只标 `category: "admin"`,挂载点交给框架;宿主缺 `/system` 时告警一次。
1774
1837
 
1775
- // web/src/<模块>/locales/en-US.json
1776
- { "menu": { "group": "Wiki" } }
1777
- ```
1838
+ ### 13.3 menu.main 统一 + 路径助手
1778
1839
 
1779
- - 只声明一条就够:无论多少模块、多少 `main`/`admin` 路由,框架只认一个 `<方案名>:menu.group`(按 name 去重)
1780
- - 必须嵌套在 `menu.group`,不能平铺成 `"<方案名>:menu.group": "..."`(i18next 点路径取值)
1781
- - 缺这条 key → 构建期契约校验直接 fail(构建期 fail-fast)
1840
+ - 模块根菜单标题与 /system 下的管理子组标题**共用** `<模块>:menu.main` 一个 i18n key(嵌套在 `menu.main`,是模块级标题的唯一真源,缺则构建期 fail-fast)。
1841
+ - 模块内所有前端路径——`navigate` / `Link to` / `useMatch`·`matchPath` / `currentActiveMenu` / SEO 正则——统一从模块基路径助手(如 `paths.ts` 的 `wikiPath(...)` / `wikiBase`)派生,不散落 `/wiki/...` 字面量;`currentActiveMenu` 必须写绝对路径(指向真实菜单 key)。`tree/bus.ts` 的 room 名是后端 WS 契约,非前端路由,不动。
1782
1842
 
1783
- ### 13.3 开发者的该做 / 别做
1843
+ ### 13.4 开发者的该做 / 别做
1784
1844
 
1785
1845
  该做(DO):
1786
1846
 
1787
- - 路由 `handle.title` 写裸 key:`{ title: "wiki:menu.space", order: 200, layout: "container" }`
1788
- - manifest `routes[]` 每条标 `category`(main/admin/hidden)+ `order` 整数 + `title_key`
1789
- (main/admin 必填,须匹配 `<方案名>:menu.*`)
1790
- - 在 `locales/{zh-CN,en-US}.json` 里补 `menu.group`
1791
- - 多个 `main` 路由想归到一组 → 让它们 `module` 字段相同、`category: "main"`
1792
- - `admin` 路由统一 `category: "admin"`,框架自动塞进 `/system`
1847
+ - 在 `entry.ts` 路由 handle 直接声明归组/菜单元数据(**真源**):
1848
+ `{ title: "wiki:menu.space", layout: "parent" }`、
1849
+ `{ title: "wiki:menu.admin", category: "admin", layout: "container" }`
1850
+ - 模块只设一个根路由,根 `title` 用 `<模块>:menu.main`;`manifest.yaml` 的 `routes[]` 不必手写(构建期从 handle 派生覆盖)。
1851
+ - 在 `locales/{zh-CN,en-US}.json` 里补嵌套的 `menu.main`。
1852
+ - `admin` 路由统一 `category: "admin"`;模块内路径走 `paths.ts` 助手,`currentActiveMenu` 写绝对路径且从助手派生。
1793
1853
 
1794
1854
  别做(DON'T):
1795
1855
 
1796
- - 为归组父项写路由 / 配 handle —— 父项是合成的,没有真实路由
1797
- - 直接写中文 `label` / `title` —— 必须走 i18n key,否则切语言不动
1798
- - 在方案里假设 `/system` 的 key 或结构 —— 那是框架断言,自定义宿主会回落平铺
1799
- - 给隐藏(hidden)或禁用(`enabled: false`)路由指望归组 —— 它们不参与
1800
- - 一个模块只挂 1 条 `main` 路由还想要父菜单 —— 规则门槛是 ≥2,单条保持平铺
1856
+ - 为归组父菜单手写额外分组根路由;用 `category:"main"` 想「归到一组」(扁平 main 归组已取消);
1857
+ - 直接写中文 `label`/`title`(必须 i18n key);假设 `/system` 的 key/结构;给 hidden/禁用路由指望进菜单;
1858
+ - 把 `currentActiveMenu` / `Link to` / `useMatch` 写成散落全路径字面量(统一走模块基路径助手);
1859
+ - 手工双写 `manifest.yaml` routes[] 与 entry.ts handle(单一真源是 entry.ts);改 `tree/bus.ts` room 名当路由。
1801
1860
 
1802
1861
  ---
1803
1862
 
@@ -1814,7 +1873,7 @@ ojm trust revoke <key_id> # 吊销:阻断新装/升级,不动已装
1814
1873
  | §7 构建与发布 | build / preview、htmlTransform、verify 闸 | 发布前 |
1815
1874
  | §8 排查 | 五步排查法、症状速查表 | 任何「没生效」 |
1816
1875
  | §9 参考 | 可抄的工程内文件、oj-api-dev 手册指引 | 找范本 |
1817
- | §10 CLI 子命令参考 | ojm 原生命令 vs oj 透传命令 | 查命令用法 |
1876
+ | §10 CLI 子命令参考 | ojm 原生命令 vs oj 透传命令;`market` 子命令(本地模拟 CDN) | 查命令用法;`ojm <cmd> --help` 看单命令帮助 |
1818
1877
  | §11 方案包:打包与分发 | build --bundle、Form A/B、分发源、索引规范 | 发布方案包 |
1819
1878
  | §12 签名与信任 | keygen/sign/install 发布流程、信任库、TOFU、bootstrap key | 首次签名发布;管理信任 |
1820
- | §13 方案菜单归组规则 | main ≥2 按模块归父菜单、admin 挂进系统设置、组标题 i18n key `<方案名>:menu.group` 约定、开发者 DO/DON'T | 写方案要让菜单按模块分组 |
1879
+ | §13 方案菜单归组规则 | 单根模块约定(根路由=第一级菜单)、category 三值(普通/hidden/admin)、admin 真重写 `/system/<模块 root>/*` + 视觉归组、menu.main 统一、模块内路径助手、开发者 DO/DON'T | 写方案要让菜单按模块分组、admin 归系统设置 |
@@ -0,0 +1,374 @@
1
+ # 菜单规则(方案路由 → 菜单的投影与归组)
2
+
3
+ > 读者:写**方案**(前后端一体模块)的开发者,以及改 `packages/runtime/`、`packages/cli/` 的框架贡献者。
4
+ > 本文件只讲「规则是什么、为什么、你怎么遵守」,不重复实现细节。规则真源在
5
+ > `packages/runtime/src/router/utils/rewrite-admin-paths.ts`(admin 真重写)与
6
+ > `packages/runtime/src/router/utils/group-solution-menus.ts`(admin 视觉归组)及其上下游纯函数。
7
+ > 本文件自包含:读懂它不需要再翻别的文档。
8
+
9
+ ---
10
+
11
+ ## 先建立心智模型:菜单是路由的「视觉投影」
12
+
13
+ 侧栏菜单**不是**手写出来的,它是路由树经一串纯函数算出来的:
14
+
15
+ ```
16
+ AppRouteRecordRaw[] (路由树,带 handle.order / handle.category / handle.module / handle.title)
17
+ │
18
+ ├─ mergeSolutionCategories() 仅 enabled=false → hideInMenu(管理禁用覆写;category/order 不再由行注写)
19
+ ├─ ascending() 双段序轴(负 order 整体在后)
20
+ ├─ splitMenuRoutes() 按首个负 order 切主区(top) / 底部固定区(bottom)
21
+ ├─ rewriteAdminPaths() ★ admin 真重写:category==="admin" 子树路径改写为 /system/<模块 root>/* 并 hoist 到顶层
22
+ ├─ generateMenuItemsFromRoutes() → MenuItemType[] (key/label/children/icon/disabled/module/category/order)
23
+ ├─ groupSolutionMenus() ★ admin 视觉归组:按 module 把 admin 项收进 /system 子组 solgroup:admin:<module>
24
+ │
25
+ ▼
26
+ wholeMenus / bottomMenus (zustand access store)
27
+ │
28
+ └─ translateMenus(t) 渲染期按当前语言把 label 裸 key 翻成文案
29
+ ```
30
+
31
+ 几个关键事实(写错的根因都在这):
32
+
33
+ - **`label` 是裸 i18n key,不是文案**。`generateMenuItemsFromRoutes` 把 `handle.title` 原样塞进 `label`;真正的翻译发生在渲染期 `translateMenus`(每次切语言重跑)。所以菜单项**永远不要**直接写中文 `label: "设置"`,要写 `label: "wiki:menu.settings"`。
34
+ - **归组元数据(category / order / module)的真源是 `entry.ts` 的路由 handle**,不再依赖宿主的方案路由表(`sys_solution_route`)。dev 模式下该表为空,归组照样靠 handle 进行。
35
+ - 归组是**纯视觉层**:`groupSolutionMenus` 只重组菜单的呈现(把 admin 挂进系统设置),**不动路由树、不触碰 i18n 翻译、不 mutate 入参**。而 admin 的**路径**由 `rewriteAdminPaths` 在路由注入前就真改写掉了——那是路由层改动,不是视觉层。
36
+ - **「单根模块约定」**:一个方案的 `entry.ts` `routes` 只有一个根路由(如 `path: "wiki"`),它本身就是第一级菜单;其余页面是其 children。这取代了旧的「把 ≥2 条 `main` 路由收进一个合成父菜单」的做法(见 §3)。
37
+
38
+ ---
39
+
40
+ ## 单根模块约定(第一级菜单 = 模块根路由)
41
+
42
+ 方案 `entry.ts` 这样组织(**真源**在路由 handle):
43
+
44
+ ```ts
45
+ export default defineModule({
46
+ name: "wiki",
47
+ routes: [
48
+ {
49
+ // 唯一根路由 = 第一级菜单项;其标题 key 用 <module>:menu.main(§4 统一)
50
+ path: "wiki",
51
+ handle: { title: "wiki:menu.main", layout: "container" },
52
+ children: [
53
+ { path: "space", handle: { title: "wiki:menu.space", layout: "parent" } },
54
+ { path: "status", handle: { title: "wiki:menu.status", layout: "parent" } },
55
+ { path: "collections", handle: { title: "wiki:menu.collections", layout: "parent" } },
56
+ // admin 页面:标 category:"admin",路径会被真重写为 /system/wiki/...(§2)
57
+ { path: "admin", handle: { title: "wiki:menu.admin", category: "admin", layout: "container" } },
58
+ // 仅可达、不进菜单的页面:category:"hidden"
59
+ { path: "status/detail", handle: { title: "wiki:menu.statusDetail", category: "hidden" } },
60
+ ],
61
+ },
62
+ ],
63
+ });
64
+ ```
65
+
66
+ 约定要点:
67
+
68
+ - 模块**只有一个根路由**,它渲染成侧栏第一级菜单项。根路由下的页面是其 `children`,按 `category` 决定是否在菜单出现(见 §1)。
69
+ - 根路由的 `handle.title` 用 `<模块名>:menu.main`(这是「模块标题」与「该模块在 /system 下的管理子组标题」共用的同一个 key,见 §4)。
70
+ - `layout` 语义:`container` = 全站外壳(顶栏/侧栏/页签/页脚);`parent` = 仅 Outlet 包裹(分组路由,无额外外壳);深嵌套分组路由必须用 `parent`,否则会叠加出双层外壳(双击 chrome)。根路由与 admin 路由保留 `container`。
71
+ - `module` 由 module-loader 在 `getRoutes()` 注册时按 `definition.name` 自动注入,**你不用写**;旧 bundle 也带标。
72
+ - 不要为了归组父菜单去手写一个额外的「分组根路由」——第一级菜单就是模块根路由本身。
73
+
74
+ > 不要再在 `manifest.yaml` 的 `routes[]` 里手工维护 category/order:`ojm build --bundle` 构建期会**从 entry.ts handle 自动派生**并覆盖 `manifest.routes`。手工那份与 handle 漂移时,构建会警告一次并以 entry.ts 为准。手工维持双份只会增加漂移风险。
75
+
76
+ ---
77
+
78
+ ## §1 category 三值语义(hidden / admin / 普通)
79
+
80
+ 路由 `handle.category` 取值与行为:
81
+
82
+ | `category` | 是否在菜单 | 路由是否可达 | 行为 |
83
+ |---|---|---|---|
84
+ | 省略 / `""` / `"main"`(旧写法) | ✅ 普通菜单项 | ✅ | 普通可见项。**`"main"` 不再触发任何归组**(旧设计把 ≥2 条 `main` 收进合成父菜单,现已取消);推荐新代码直接省略 category |
85
+ | `"hidden"` | ❌ | ✅ | 有效路由但**不进菜单**(等价于 `hideInMenu: true`);用于详情页、弹窗页等 |
86
+ | `"admin"` | ✅(挂在 `/system` 下) | ✅(路径变为 `/system/<模块 root>/...`) | 管理/设置页,走 §2 真重写 + §3 视觉归组 |
87
+
88
+ - `hidden` 项不进菜单,但路由仍可达(深链、`navigate` 都能命中)。
89
+ - `category:"admin"` 项先在**路由层**被 `rewriteAdminPaths` 真重写为 `/system/<模块 root path>/*`,再在**菜单层**被 `groupSolutionMenus` 收进 `/system` 子组。两者配合,URL 与菜单都归到系统设置下。
90
+ - `module` 自动注入,归组按它分桶,你无需关心。
91
+
92
+ > `/system` 是框架侧断言,不是方案 manifest 能声明或验证的。方案只需把 admin 路由标
93
+ > `category: "admin"`,挂载点交给框架。若宿主没有 `/system`(极少数自定义宿主):admin 项已被
94
+ > 从原位置摘除却无处挂载,仅告警一次(`warnOnce`,同消息不重复),不会静默丢失为「出现在错误位置」。
95
+
96
+ ---
97
+
98
+ ## §2 admin 真重写(路由层,不是视觉把戏)
99
+
100
+ `rewriteAdminPaths`(`access.ts` 注入路由前调用)对 `category==="admin"` 的**路由子树**做真改写:
101
+
102
+ - 把该子树从原父树中摘除,hoist 到顶层(与模块根路由平级),使其运行时 URL 真正变成
103
+ `/system/<模块 root path>/...`——而不是仍挂 `/wiki` 下、仅菜单挪到 /system 的「视觉假象」。
104
+ - `<模块 root path>` 取该模块根路由的**绝对路径**(单根约定下即顶层路由 path,如 `wiki`),
105
+ admin 路径后缀(root 之后的部分)原样保留,故无论 admin 嵌套多深都能正确重写。
106
+ - **非突变**:返回新路由数组,未变动的分支复用原引用(react-router `patchRoutes` 接收)。
107
+
108
+ 例:上节 `wiki` 模块的 `admin` 子路由,重写后路径为 `/system/wiki/admin`,运行时访问即
109
+ `/system/wiki/admin`。菜单层(`groupSolutionMenus`)随后把它视觉归进 `/system` 子组。
110
+
111
+ ```ts
112
+ // rewriteAdminPaths 语义(非突变)
113
+ "/wiki/admin" (category:"admin") ──► hoist + 重写 ──► "/system/wiki/admin"
114
+ ```
115
+
116
+ > 真重写 vs 旧视觉归组:旧版只在菜单层把 admin 挪到 /system,路由 URL 仍是 `/wiki/admin`,
117
+ > 深链与 SEO 都错位。新版在路由层就改掉路径,URL 与菜单一致。
118
+
119
+ ---
120
+
121
+ ## §3 admin 视觉归组(菜单层)
122
+
123
+ `groupSolutionMenus` 在菜单生成后、渲染前做**纯视觉层**归组(只收已生成的菜单项,不动路由树):
124
+
125
+ - 递归收集 `category==="admin"` 的菜单项(handle 优先,宿主方案路由行仅作 `admin` 的向后兼容回退)。
126
+ - 按 `module` 分桶,每组挂进宿主 `/system` 菜单的 children 末尾:
127
+ - 子组 key:`solgroup:admin:<模块名>`
128
+ - 子组标题:裸 i18n key `<模块名>:menu.main`(与模块根菜单标题共用同一 key,见 §4)
129
+ - 组内压平 admin 项自身的层级(「管理」中间级消失,子项直挂子组下),按各自 `order` 升序
130
+ - 仅当有 admin 组要挂入时才改写 `/system`(无 admin 项时不强加空 children);宿主缺 `/system` 时告警一次。
131
+ - 非突变、无副作用(未变动子树复用原引用)。
132
+
133
+ > 旧的「`main` 路由 ≥2 条按模块收进合成父菜单 `solgroup:main:<模块>`」**已取消**。模块第一级菜单
134
+ > 现在直接是模块根路由(§0 单根约定),`category:"main"` 退回为普通可见项,不再产生合成父菜单。
135
+
136
+ ---
137
+
138
+ ## §4 menu.main 统一(模块标题与管理子组标题共用一个 key)
139
+
140
+ 模块根菜单标题与它在 /system 下的管理子组标题**用同一个 i18n key**:`<模块名>:menu.main`。
141
+
142
+ - 模块根路由:`handle.title: "<模块名>:menu.main"` → 渲染为侧栏第一级菜单项文案。
143
+ - admin 子组:`groupSolutionMenus` 生成的 `solgroup:admin:<模块>` 标题也取 `<模块名>:menu.main`。
144
+ - 因此你只需在双语 locale 里声明**一条** `<模块名>:menu.main`:
145
+
146
+ ```jsonc
147
+ // web/src/<模块>/locales/zh-CN.json
148
+ { "menu": { "main": "Wiki" } } // 命中 <模块名>:menu.main(根菜单 + 系统设置里的管理子组都读它)
149
+
150
+ // web/src/<模块>/locales/en-US.json
151
+ { "menu": { "main": "Wiki" } }
152
+ ```
153
+
154
+ 约定:
155
+
156
+ - 必须**嵌套**在 `menu.main`,不能平铺成 `"<模块名>:menu.main": "..."`(i18next 点路径取值)。
157
+ - 缺这条 key → 构建期契约校验直接 fail(构建期 fail-fast)。
158
+ - 普通子项(space / status / collections 等)的 `title` 仍各自匹配 `<模块名>:menu.*`(如 `wiki:menu.space`)。
159
+
160
+ ---
161
+
162
+ ## §5 currentActiveMenu(高亮哪一个菜单项)
163
+
164
+ `handle.currentActiveMenu` 是一个字符串,声明「当前路由应当高亮哪个菜单项」,值是该菜单项的
165
+ **绝对路径 key**(即菜单项的 `key`,等于路由绝对路径)。用于混合导航 / 双列导航下定位要展开
166
+ 的第一级菜单。
167
+
168
+ 规则与坑:
169
+
170
+ - 值必须是**绝对路径**,且指向一个真实存在的菜单 key(通常是模块根或某个子路由,如 `/wiki/space`)。
171
+ 写成相对值会被按当前路由错误解析(如 `/system/dept` 下点 `detail` → `/system/detail`,落 404)。
172
+ - **不要硬编码散落的全路径字面量**。模块内访问路径应统一从「模块基路径助手」派生(见 §6),
173
+ 这样模块挂载根(`wiki`)改名时,所有引用一次性跟着变,不会因前缀写死而出错。
174
+ - 例(推荐写法,依赖模块基路径助手):
175
+
176
+ ```ts
177
+ import { wikiPath } from "./paths";
178
+ // 在 /wiki/space/:id/doc 路由上,希望高亮「空间」菜单项:
179
+ handle: { currentActiveMenu: wikiPath("space") } // → "/wiki/space",随模块根改名自动更新
180
+ ```
181
+
182
+ ---
183
+
184
+ ## §6 模块内路径一律走「模块基路径助手」(相对/模块基,不散落绝对路径)
185
+
186
+ 方案内部的所有前端路径——`navigate(...)`、`Link to`、`useMatch` / `matchPath` 的匹配模式、
187
+ `currentActiveMenu`、SEO 正则——都应从一个**单一来源的模块基路径助手**派生,不要散落写
188
+ `/wiki/...` 字面量。
189
+
190
+ 推荐在模块内放一个 `paths.ts`:
191
+
192
+ ```ts
193
+ // web/src/<模块>/paths.ts
194
+ export const WIKI_ROOT = "wiki"; // 模块挂载根(唯一定义处)
195
+ export const wikiBase = `/${WIKI_ROOT}`;
196
+ export const wikiPath = (...segments: Array<string | number>): string =>
197
+ `/${WIKI_ROOT}/${segments.join("/")}`; // wikiPath("space", 1, "doc") → /wiki/space/1/doc
198
+ ```
199
+
200
+ 为什么不该用 react-router 的 location-relative 导航:
201
+
202
+ - 深嵌套路由(如 `/wiki/space/:id/doc/:docId`)里用 `navigate("doc/...")` 这类相对跳转,
203
+ 相对基准随当前层级变化,极易跳错;统一从 `wikiPath(...)` 拼绝对路径最稳。
204
+ - 模块根改名(如 `wiki` → `knowledge`)时,只改 `WIKI_ROOT` 一处,全模块引用自动收敛;
205
+ 散落字面量则每个都要手工改,且容易漏。
206
+
207
+ > 例外(**不要**改):`tree/bus.ts` 里的 `treeRoom("/space/:id")` 之类是**后端 WebSocket 房间名**
208
+ > (后端契约),不是前端路由,故意不参与前端路径重写。
209
+
210
+ `manifest.yaml` 的 `routes[].path` 同样是**带模块根的绝对路由路径**(`/wiki/space`,不是 `/space`):
211
+ 安装器按它注册菜单,漏掉模块根前缀会让菜单指到别的模块或 404。构建期从 `entry.ts` 派生与
212
+ 手写两种形态都遵循这一条;`health` 也必须是 `routes[]` 里的路由或其子路径。
213
+
214
+ ---
215
+
216
+ ## §7 两个路径空间:URL 空间 vs 路由空间
217
+
218
+ **同一串字符,在浏览器和 React Router 里不是同一个东西**:
219
+
220
+ ```
221
+ URL 空间(地址栏,含部署 base)
222
+ https://host/oj/wiki/space/1/doc/2
223
+ └ basename ┘└──── 路由空间 ────┘
224
+ 路由空间(Router 已剥 basename)
225
+ /wiki/space/1/doc/2
226
+ ```
227
+
228
+ 开发/预览通常部署在根路径(base = `/`),两个空间**恰好重合**——把 URL 空间的值喂给路由空间
229
+ 的 API 也能「碰巧正确」。换到子路径部署(`/oj/`、`/app/`)立刻分叉。
230
+
231
+ | 空间 | 谁负责加/剥 base | 典型 API |
232
+ |---|---|---|
233
+ | URL 空间 | 浏览器 + 宿主 | `window.location.pathname`、`document.baseURI`、复制分享链接、OAuth 回跳 |
234
+ | 路由空间 | React Router(`basename`) | `<Link to>`、`navigate()`、`useMatch()`、`useLocation()`、`matchPath()` 的模式、`currentActiveMenu`、`manifest.routes`、`html_meta` 键 |
235
+
236
+ ### 三条硬规则
237
+
238
+ 1. **导航与匹配只走路由空间**:`navigate`/`Link to`/`useMatch` 一律用 §6 的路径助手产出。
239
+ 需要「当前是不是某个页面」时用 `useMatch(pattern)` / `useLocation()`,**不要**
240
+ `matchPath(pattern, window.location.pathname)`。
241
+ 2. **模块内禁止读 `import.meta.env.BASE_URL`**:模块是**独立构建**的,这个值在构建期就被替换成
242
+ *方案工程自己的* base(通常 `/`),不是宿主部署 base。同理,模块也不该自己拼 `location.origin`。
243
+ 3. **真要 URL 空间的值,用 `useHref(path)`**(Router 负责补 base),不要手工字符串拼接。
244
+ 典型场景:复制分享链接、第三方回跳地址、`document.title` 之类的外部消费。
245
+
246
+ ### 为什么这些错误在本地看不出来
247
+
248
+ 根路径部署下 `window.location.pathname` 与 `useLocation().pathname` 相同,上面第 1 条的错误
249
+ 写法**恰好**正确。所以必须用测试把「非根 base」变成常规路径:
250
+
251
+ ```tsx
252
+ // 部署 base 基线:basename 明确设为 /oj,initialEntries 是 URL 空间
253
+ const router = createMemoryRouter([...], {
254
+ basename: "/oj",
255
+ initialEntries: ["/oj/wiki/space/3/doc/12"], // location.pathname = /oj/wiki/...
256
+ });
257
+ // 断言:useMatch("/wiki/space/:id/doc/:docId/*") 命中;<Link to="/wiki/..."> 的 href 带 /oj
258
+ ```
259
+
260
+ 再配一条源码扫描(成本极低、收益很高):模块目录里禁止出现 `window.location.pathname`、
261
+ `import.meta.env.BASE_URL`、裸 `"/<模块根>"` 字面量,白名单只留 `paths.ts`(与「API 前缀」
262
+ 这类非路由常量所在的文件)。
263
+
264
+ ### 子路径部署怎么开(`web.config.ts` 的 `siteBase`)
265
+
266
+ 模块侧按本节写只是必要条件,**能不能子路径部署由宿主决定**。从 `0.2.17` 起宿主侧支持了,
267
+ 全套动作只需一个配置项:
268
+
269
+ ```ts
270
+ // web.config.ts —— 站点挂在 https://host/oj/ 下
271
+ export default {
272
+ siteBase: "/oj", // 留空 = 站点根路径(默认,行为与旧版一致)
273
+ modules: [...],
274
+ };
275
+ ```
276
+
277
+ `ojm build` 会按它做三件事(都发生在**构建/部署期**,运行期只读结果):
278
+
279
+ | 位置 | 动作 | 不配的后果 |
280
+ |---|---|---|
281
+ | `<dist>/index.html` | importmap 映射值、`<script src>`、`<link href>` 统一加 `/oj` 前缀 | HTML 指向 `/assets/react.js`,在 `/oj` 挂载下 404 → 白屏 |
282
+ | `<dist>/index.html` | `<head>` 注入 `<script nonce>window.__OJ_BASE__="/oj/"</script>`(复用 importmap 的 nonce,否则被 CSP 拦掉) | 运行期拿不到真实部署前缀 |
283
+ | 运行期 Router | runtime 与宿主 shell 都经 `resolveSiteBase()` 取 basename,即 `window.__OJ_BASE__` | Router 不剥 `/oj`,`/oj/wiki/...` 匹配不到 `/wiki/...`(深链刷新落 404 页) |
284
+
285
+ 为什么必须运行期读全局:runtime 是**预构建 npm 产物**,`import.meta.env.BASE_URL` 在包构建期
286
+ 就被内联成 `/`(`packages/runtime/vite.config.ts` 的 `define`);shell 的 importmap 同样是在包
287
+ 构建期写死。部署前缀只有拿到实际 config 的人才知道,所以只能由 `ojm build` 在站点产物上注入。
288
+
289
+ 由此推出**两条模块侧纪律**:
290
+
291
+ 1. **禁止读 `import.meta.env.BASE_URL`**(§7 第 2 条)——它在你的模块构建期就被钉死成方案工程
292
+ 自己的 base,永远不等于宿主部署 base。
293
+ 2. 需要「站内绝对 URL」(下载直链、`window.open`、`<a href>` 到静态资源)时,用 runtime 出口
294
+ `withSiteBase("/files/a.pdf")`,不要自己拼 `location.origin` 或写死 `/files/...`。
295
+ **路由跳转仍然不要用它**:`navigate`/`<Link>` 走路由空间,basename 已由 Router 剥离,
296
+ 再拼一次会得到 `/oj/oj/wiki`。
297
+
298
+ 副产物:`ojm preview` 会把站点挂到同一前缀(`http://localhost:<port>/oj`),所以子路径部署
299
+ 在本地就能验证,不必等上线。
300
+
301
+ 根路径部署(`siteBase` 留空)时产物与旧版**逐字节一致**,无需任何迁移。
302
+
303
+ 实测依据(2026-10-21,站点以 `mounts: [{prefix: /oj, web: web/dist, spa: true}]` 挂起):
304
+
305
+ | 请求 | 结果 | 说明 |
306
+ |---|---|---|
307
+ | `GET /oj/wiki/space/3/doc/12` | `200 text/html` | 深链回落本身是通的,前提是该挂载显式 `spa: true` |
308
+ | `GET /wiki/space/3/doc/12` | `404` | 没有根挂载,命中不了别的前缀 |
309
+ | `GET /oj/assets/react.js` | `200` | 静态服务正常 |
310
+ | `GET /assets/react.js`(旧产物 HTML 指向的地址) | `404` | 旧版缺的正是「HTML 与 Router 的前缀」,不是服务端回落 |
311
+
312
+ 这套结论已固化成可重复的验收脚本 `pnpm verify:subpath`(`scripts/verify-subpath.mjs`):
313
+ 它造一个最小模块工程 → `ojm build` 配 `siteBase: "/oj"` → 逐项核对产物(importmap 全量带
314
+ 前缀、注入脚本复用 nonce 且在 importmap 之前、modules.json 不带前缀、资产文件真的存在)→
315
+ 再以 `siteBase` 留空重建,核对与 `shell-dist` **逐字节一致** → 起真 `oj serve` 跑上表矩阵 →
316
+ headless chromium 核对「`/` 重定向到 `/oj/<首个模块路由>`」与「深链刷新命中模块路由」。
317
+ 改到 base 相关代码后跑它,比手搓 curl 可靠(删掉注入即 6 项红,正是修复前的形态)。
318
+
319
+ ### per-route meta 的生效矩阵(深链标题/OG)
320
+
321
+ `web.config.ts` 的 `htmlTransform` **只在 `ojm dev` 的静态层生效**:
322
+
323
+ | 链路 | 谁产出 HTML | 深链标题 |
324
+ |---|---|---|
325
+ | `ojm dev` | CLI 静态层,跑 `htmlTransform` | 生效 |
326
+ | `ojm preview`(oj 单服务) | oj 直出 | **不生效**(CLI 启动时 WARN) |
327
+ | 生产 `oj serve` | oj 直出 | 需 `server.html_meta`(构建期 JSON 目录)或 `server.html_meta_handler`(GET handler) |
328
+
329
+ 配 `html_meta`/`html_meta_handler` 时注意:**它们的 path 是「已剥 web 挂载前缀」的站内路径**,
330
+ 也就是**路由空间**——子路径部署时键仍写 `/wiki/space/3/doc/12`,不要带 `/oj`。这一点与第 1 条
331
+ 规则同源:只认路由空间。
332
+
333
+ ---
334
+
335
+ ## 开发者的该做 / 别做
336
+
337
+ 该做(DO):
338
+
339
+ - 在 `entry.ts` 路由 handle 直接声明归组/菜单元数据(**真源**):
340
+ `{ title: "wiki:menu.space", layout: "parent" }`、
341
+ `{ title: "wiki:menu.admin", category: "admin", layout: "container" }`
342
+ - 模块只设**一个根路由**,其余页面作其 `children`;根路由 `title` 用 `<模块>:menu.main`。
343
+ - `manifest.yaml` 的 `routes[]` **不必手写**:构建期从 entry.ts handle 自动派生并覆盖;漂移时警告一次并以 entry.ts 为准。
344
+ - 在 `locales/{zh-CN,en-US}.json` 里补嵌套的 `menu.main`(根菜单标题 + 系统设置管理子组标题共用)。
345
+ - `admin` 路由统一 `category: "admin"`,框架自动真重写进 `/system` 并归组。
346
+ - 模块内所有前端路径走 `paths.ts` 的 `wikiPath(...)` / `wikiBase`,不散落绝对路径字面量。
347
+ - `currentActiveMenu` 写绝对路径,且从模块基路径助手派生。
348
+ - 判断「当前是不是某个页面」用 `useMatch(pattern)` / `useLocation()`(路由空间);
349
+ 需要把路由路径变回真实 URL(分享链接、外部回跳)用 `useHref(path)`。
350
+ - 给路径助手配两条测试:①路由表展平后与助手产出逐条对撞(含反例);②`createMemoryRouter`
351
+ 显式 `basename: "/oj"` 的部署基线(`useMatch` 命中 + `Link` 的 href 带 base)。
352
+ - 站点要挂到非根前缀时,在 `web.config.ts` 配 `siteBase: "/oj"`,别去动模块代码(见 §7)。
353
+ - 拼静态资源直链用 runtime 出口的 `withSiteBase("/files/a.pdf")`。
354
+
355
+ 别做(DON'T):
356
+
357
+ - 为归组父菜单手写一个额外「分组根路由」——第一级菜单就是模块根路由本身(单根约定)。
358
+ - 直接写中文 `label` / `title` —— 必须走 i18n key,否则切语言不动。
359
+ - 用 `category: "main"` 想「归到一组」—— 扁平 main 归组已取消,`main` 等同普通项,不再产生合成父菜单。
360
+ - 在方案里假设 `/system` 的 key 或结构 —— 那是框架断言,自定义宿主会告警并回落。
361
+ - 给隐藏(`hidden`)或禁用(`enabled: false`)路由指望进菜单 —— 它们不参与。
362
+ - 把 `currentActiveMenu` / `Link to` / `useMatch` 写成散落的全路径字面量 —— 统一走模块基路径助手(§6)。
363
+ - 用 `window.location.pathname` 去和路由模式 `matchPath` —— 那是 URL 空间,子路径部署下恒不匹配
364
+ (根部署时两者重合,测试也难发现;见 §7)。
365
+ - 在模块里读 `import.meta.env.BASE_URL` —— 模块独立构建,这个值是**方案工程自己的** base,
366
+ 不是宿主部署 base。
367
+ - 用 `withSiteBase()` 拼 `navigate` / `<Link to>` 的目标 —— 那是路由空间,会得到 `/oj/oj/wiki`。
368
+ - 子路径部署时去改模块里的路径字面量 —— 该改的是 `web.config.ts` 的 `siteBase`。
369
+ - 在模块目录里散落 `"/<模块根>/..."` 字符串 —— 源码扫描应零命中(白名单只留 `paths.ts`、
370
+ API 前缀常量所在文件)。
371
+ - 在 `manifest.yaml` 的 `routes[]` 里写 `/<子段>`(如 `/space`)当绝对路径 —— 必须带模块根
372
+ (`/wiki/space`),否则安装器注册的菜单指错地方。
373
+ - 手工维持 `manifest.yaml` 的 `routes[]` 与 entry.ts handle 双份 —— 单一真源是 entry.ts,手工那份会被派生结果覆盖(并警告),多写只会增加漂移风险。
374
+ - 改动 `tree/bus.ts` 的 room 名当路由改 —— 那是后端 WS 契约,非前端路由。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oj-module/cli",
3
3
  "type": "module",
4
- "version": "0.2.15",
4
+ "version": "0.2.18",
5
5
  "description": "React Antd Admin 模块工程命令行工具(含预构建宿主 shell-dist)",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -49,7 +49,7 @@
49
49
  ],
50
50
  "dependencies": {
51
51
  "@faker-js/faker": "^10.4.0",
52
- "@oj-module/runtime": "0.2.15",
52
+ "@oj-module/runtime": "0.2.18",
53
53
  "@redocly/cli": "^2.51.0",
54
54
  "@tailwindcss/vite": "^4.3.0",
55
55
  "esbuild": "^0.28.2",