@oj-module/cli 0.2.20 → 0.2.25

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 (75) hide show
  1. package/devkit/manual.md +100 -12
  2. package/devkit/module-dev-and-cdn-playground.md +10 -10
  3. package/package.json +4 -4
  4. package/shell-dist/assets/runtime.js +61 -35
  5. package/shell-dist/index.html +2 -2
  6. package/shell-dist/versions.json +3 -3
  7. package/src/__tests__/build-backend.test.ts +13 -13
  8. package/src/__tests__/cli.test.ts +225 -0
  9. package/src/__tests__/init-packagejson.test.ts +29 -0
  10. package/src/__tests__/layout.test.ts +8 -5
  11. package/src/__tests__/new-scaffold.test.ts +59 -0
  12. package/src/__tests__/usage.test.ts +45 -47
  13. package/src/build.ts +4 -4
  14. package/src/builtin.ts +93 -0
  15. package/src/bundle.ts +237 -33
  16. package/src/cli.ts +265 -0
  17. package/src/contract/__tests__/market-client.integration.test.ts +12 -12
  18. package/src/contract/check.ts +4 -4
  19. package/src/dev.ts +75 -2
  20. package/src/index.ts +31 -191
  21. package/src/info.ts +49 -7
  22. package/src/init.ts +25 -11
  23. package/src/keygen.ts +0 -19
  24. package/src/layout.ts +11 -11
  25. package/src/lifecycle/__tests__/authoring.test.ts +3 -14
  26. package/src/lifecycle/__tests__/builtin.test.ts +97 -0
  27. package/src/lifecycle/__tests__/bundle.test.ts +188 -25
  28. package/src/lifecycle/__tests__/install.test.ts +0 -23
  29. package/src/lifecycle/__tests__/new.test.ts +129 -0
  30. package/src/lifecycle/__tests__/smoke.test.ts +4 -3
  31. package/src/manifest-contract.ts +16 -3
  32. package/src/market/__tests__/bootstrap.test.ts +24 -0
  33. package/src/market/__tests__/setup.test.ts +8 -1
  34. package/src/market/bootstrap.ts +10 -8
  35. package/src/market/cdn-layout.ts +3 -3
  36. package/src/market/command.ts +0 -23
  37. package/src/market/publish.ts +10 -2
  38. package/src/market/setup.ts +14 -2
  39. package/src/market/trial.ts +3 -3
  40. package/src/market/walkthrough.ts +7 -6
  41. package/src/new.ts +226 -0
  42. package/src/preview-config.ts +2 -2
  43. package/src/preview.ts +5 -4
  44. package/src/publish.ts +0 -22
  45. package/src/sign.ts +0 -22
  46. package/src/usage.ts +482 -198
  47. package/templates/README.md +3 -3
  48. package/templates/cdn/README.md +6 -6
  49. package/templates/config.yaml +5 -4
  50. package/templates/init.include.yaml +1 -1
  51. package/templates/package.json +3 -1
  52. package/templates/seed.mysql.sql +2 -1
  53. package/templates/seed.pg.sql +2 -1
  54. package/templates/src/_platform/api/fixtures/l1-test-user.sql +4 -5
  55. package/templates/src/_platform/api/market/api.ts +11 -8
  56. package/templates/src/_platform/api/seed.mysql.sql +11 -3
  57. package/templates/src/_platform/api/seed.pg.sql +11 -3
  58. package/templates/src/_platform/api/seed.sql +18 -6
  59. package/templates/src/_platform/api/solutions/api.ts +2 -2
  60. package/templates/src/oj-market/web/api/context.ts +2 -2
  61. package/templates/src/oj-market/web/components/InstallDialog.tsx +13 -6
  62. package/templates/src/oj-market/web/components/RollbackDialog.tsx +7 -3
  63. package/templates/src/oj-market/web/components/UninstallConfirm.tsx +18 -7
  64. package/templates/src/oj-market/web/hooks/useMarket.ts +30 -18
  65. package/templates/src/system/web/locales/en-US.json +1 -1
  66. package/templates/src/system/web/locales/zh-CN.json +1 -1
  67. package/templates/src/system/web/pages/users/roles.tsx +3 -1
  68. package/templates/tests/l1-builder.test.ts +2 -2
  69. package/templates/tests/lifecycle/market-api.spec.ts +35 -5
  70. package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
  71. package/templates/tests/lifecycle/verify-report.spec.ts +1 -1
  72. package/templates/tests/rbac-seed.spec.ts +12 -10
  73. package/templates/tests/seed-role-accounts.spec.ts +87 -0
  74. package/vendor/host-versions.json +3 -3
  75. package/src/args.ts +0 -122
package/devkit/manual.md CHANGED
@@ -39,6 +39,7 @@
39
39
  ├── tests/ # 后端 BDD 测试(node:sqlite 内存库 + 真实迁移建表)
40
40
  ├── src/
41
41
  │ ├── tsconfig.json # 后端 typecheck 工程(extends 上两者,exclude */web)
42
+ │ ├── .ojm-builtin.json # 框架自带方案名单(init 派生,可手改/删除;打包排除 + info 折叠,见 §11)
42
43
  │ ├── styles.css
43
44
  │ ├── _platform/ # 框架级:users 表 / rbac / 市场编排 / 生命周期
44
45
  │ │ ├── api/ # + _shared/(respond、service/、site-facade)
@@ -74,7 +75,19 @@
74
75
  > **历史布局仍被识别**:`web/src/<name>`(uni-dev)与 `web/<name>`(框架根仓)照旧可用,
75
76
  > 只是 `ojm init` 不再产出。两套源码树**互斥**——同时存在时 `ojm` 直接报错,而不是静默选一个。
76
77
 
77
- 登录账号 `admin / 123456`。端口:前端 devServer **5174**,oj 后端 **9778**
78
+ 登录账号:**账号名=内置角色名**一一对应,口令均 `123456`,且全部**免首登改密**(must_change=0,纯开发便利,勿用于生产):
79
+
80
+ | 账号 | 角色 | 能干什么 |
81
+ |---|---|---|
82
+ | `superadmin` | superadmin | 全部能力(含安全组设置、成员资格管理) |
83
+ | `admin` | admin | 日常管理(不含 superadmin 专属项) |
84
+ | `editor` | editor | 页面门编辑级 |
85
+ | `viewer` | viewer | 页面门访客级 |
86
+
87
+ > 旧脚手架库(admin 绑 superadmin / must_change=1)**无需手工迁移**:seed 每次启动幂等重放,
88
+ > 自动收敛为账号名=角色名并清零 must_change(`DELETE` 旧绑定 + `UPDATE`,见 `_platform/api/seed.sql`)。
89
+
90
+ 端口:前端 devServer **5174**,oj 后端 **9778**
78
91
  (以工程根 `config.yaml` 的 `server.port` 为准)。
79
92
 
80
93
  ### 1.2 两个模块体系,同名不同物
@@ -93,6 +106,33 @@
93
106
  历史布局下两侧分居 `web/src/<模块>/` 与 `api/src/<模块>/`——**同一个名字、两个目录**,
94
107
  是布局迁移时最容易对不上的地方。
95
108
 
109
+ #### manifest.yaml 与 solution.yaml:两种身份,别混淆
110
+
111
+ 工程里会出现两个名字里带 "manifest" 的文件,用途**完全不同**,混用会出构建/运行期怪问题:
112
+
113
+ | 文件 | 层级 | 身份 | 谁读它 | 写什么 |
114
+ |---|---|---|---|---|
115
+ | `src/<模块>/api/manifest.yaml` | **模块级**(每个后端模块一份) | oj **原生七键**清单(`name`/`desc`/`version`/`tables`/…) | **oj 运行时**——按 `manifest.yaml` 枚举发现并校验 api 后端模块 | 模块名、版本、表清单(与 `schema.yaml` 双向一致,§1.5) |
116
+ | `solution.yaml`(`src/<模块>/` 或工程根) | **方案级**(整个方案一份) | 方案包契约(身份 / 路由 / api 段 / web 段 / 权限 / …) | **`ojm build --bundle`**——打包入口与契约校验 | 方案名、版本、`api` 段(模块+前缀+tables+deps+anonymous)、`web` 段(entry/css/integrity)、`routes`、`health` |
117
+
118
+ 关键区别与红线:
119
+
120
+ - **模块级 `manifest.yaml` 是 oj 发现 api 后端的唯一真源**:`ojm dev/build` 递归扫描
121
+ `src/<模块>/api/manifest.yaml` 认领后端模块;oj 启动也用它做强校验(`name ==` 目录名、版本同源)。
122
+ 它**只能有 oj 原生七键**,写任何方案级字段(`routes` / `permissions` / …)都会被拒启。
123
+ - **方案级契约以前也叫 `manifest.yaml`(工程根)**,但它会和上面的「oj 按 `manifest.yaml` 枚举 api 模块」
124
+ 撞名:**工程根再放一份 `manifest.yaml` 当 bundle 契约,会被 oj 的搜索枚举误当成 api 模块**,污染后端发现。
125
+ 所以解决方案统一改名为 **`solution.yaml`**,`ojm build --bundle` 优先认它(`manifest.yaml` 仅作旧方案回退)——
126
+ **后端按 `manifest.yaml` 枚举 api、打包按 `solution.yaml` 取契约**,两条枚举互不干扰。
127
+ **位置**:单模块方案推荐放 `src/<模块>/solution.yaml`(契约与所打包模块同位);`ojm build --bundle`
128
+ 也兼容工程根 `solution.yaml`,以及两处的 `manifest.yaml` 回退。**模块级契约只认 `src/<模块>/` 根下、
129
+ 绝不进 `<模块>/api/`**——否则照样会和 oj 的 api 枚举撞车。
130
+ - 进包后文件名固定回到 `manifest.yaml`(安装侧只认这个名字):`ojm build --bundle` 把工程根或
131
+ `src/<模块>/` 下的 `solution.yaml`(或旧 `manifest.yaml`)拷贝进 tgz 内的 `<name>-<version>/manifest.yaml`,安装器/市场仍按老名字消费。
132
+
133
+ > 一句话:**`manifest.yaml` 是给 oj 认模块用的(模块级、七键、oj 枚举);`solution.yaml` 是给 `ojm build --bundle`
134
+ > 打包用的(方案级、工程根)。两者文件名不同、层级不同、读者不同,别写错层。**
135
+
96
136
  ### 1.3 后端路由 = 目录镜像
97
137
 
98
138
  ```
@@ -1377,12 +1417,15 @@ useEffect(() => {
1377
1417
  pnpm dev # 开发:devServer + oj 后端(src/<模块>/api 保存即热更)
1378
1418
  # 交互快捷键(仅 TTY 终端启用,类 Vite):
1379
1419
  # r 强制重建并热更新 h 显示帮助 c 清屏 o 浏览器打开 q 退出(oj 一并终止)
1420
+ # 提示行常驻终端最末行(滚动区),日志在其上方滚动不会被顶走(ojm ≥ 0.2.22)
1380
1421
  # Ctrl+C(SIGINT)/ kill(SIGTERM)终止 dev 时,前端与后端 oj 一起终止:
1381
1422
  # 先关 SSE 通道 → 停 oj 子进程 → 打印「后端 oj 已随前端一起终止」,不留 oj 孤儿占端口
1382
1423
  pnpm build # = ojm build:oj build(生成 routes.js)+ 前端全站合并到 web/dist
1383
1424
  pnpm preview # = ojm preview:oj migrate(verify 闸)→ oj serve 单服务(API + 静态 SPA 同进程)
1384
1425
  pnpm typecheck # = tsc --noEmit(前端工程)
1385
1426
  pnpm typecheck:api # = tsc --noEmit -p src/tsconfig.json(后端工程,见 §6.5)
1427
+ pnpm new # = ojm new <name>:脚手架内生成用户方案骨架(见 §10.1)
1428
+ pnpm bundle # = ojm build --bundle:打包全部用户方案(自带方案自动排除,见 §11)
1386
1429
  pnpm exec ojm info # 版本矩阵 + 模块清单(报障用)
1387
1430
  pnpm exec ojm vendor # 下载/重装 oj 二进制
1388
1431
  ```
@@ -1616,6 +1659,28 @@ export default async (path: string, html: string) => {
1616
1659
  > release 下只有 `routes.js` 里的路由存在,目录镜像路由不生效——所以参数路由要先
1617
1660
  > `ojm build` 再测(见 §7)。
1618
1661
 
1662
+ #### `ojm build --bundle [name]`
1663
+
1664
+ 打包**方案包**(solution bundle,可分发产物;语义细节见 §11)。只组装不构建,先 `ojm build` 再打包。
1665
+
1666
+ - `--bundle` 无参:工程根有 `solution.yaml` 则只打根契约(旧单方案行为零变化);
1667
+ 否则收集 `src/*/` 下全部**用户方案**契约逐个打包(框架自带方案自动排除,日志注明「框架自带,跳过」)。
1668
+ - `--bundle <name>` / `--bundle=<name>`:只打 `src/<name>/solution.yaml` 这一份(manifest.yaml 回退)。
1669
+ - 两段式:全部契约校验通过才开始写盘;零用户方案时报错指路 `ojm new`。
1670
+
1671
+ #### `ojm new <name> [--layout container|none] [--api]`
1672
+
1673
+ 脚手架内生成一份用户方案骨架(module-root 布局):`src/<name>/solution.yaml` +
1674
+ `web/{entry.ts,pages/,locales/}`(`--api` 再加 `api/{manifest.yaml,api.ts}`)。
1675
+ 生成即被扫描收录,补产物后 `ojm build --bundle <name>` 直接可打(round-trip)。
1676
+ 非 module-root 布局拒绝(会先要求迁移);名字撞内置方案 / 保留名 / 已存在目录都会拒绝。
1677
+ ```bash
1678
+ ojm new crm --layout container # 建 src/crm/ 骨架
1679
+ pnpm dev # 看效果
1680
+ ojm build --bundle crm # 打包
1681
+ ```
1682
+
1683
+
1619
1684
  #### `ojm preview [port]`
1620
1685
 
1621
1686
  生产形态预览(设计 §6,2026-10-19 单服务化):
@@ -1647,9 +1712,11 @@ export default async (path: string, html: string) => {
1647
1712
 
1648
1713
  生成物重跑即覆盖(eslint 已忽略);唯一例外是带 `// ojm-api:stub` 指纹的 stub,人碰过则工具永不写删。
1649
1714
 
1650
- #### `ojm info`
1715
+ #### `ojm info [--all]`
1651
1716
 
1652
1717
  输出版本矩阵(`@oj-module/cli` / `@oj-module/runtime` / 钉版 `@plane/*` / `oj` 二进制)与模块清单。
1718
+ module-root 下数据源是「扫描 + `web.config.ts` 登记」并集(未登记的用户模块也可见)。
1719
+ 框架自带方案默认折叠为一行计数(`--all` 展开平铺)。
1653
1720
  **报障用**:拿不准「工程里到底装了什么版本、哪些模块被登记」时,先 `ojm info` 贴出来。
1654
1721
 
1655
1722
  #### `ojm vendor [tag] [--force]`
@@ -1697,20 +1764,22 @@ export default async (path: string, html: string) => {
1697
1764
  ### 10.3 本地市场链路:`ojm market` 子命令
1698
1765
 
1699
1766
  `market` 是一组**本地模拟 CDN + 市场链路**工具,专供作者在本机验证「方案包能不能发、能不能装」,
1700
- **不触碰真实生产 CDN**。所有产物落在工程根 `cdn/`(由 `config.yaml` 的 `/cdn` 挂载直出),
1701
- 首跑用 `market setup` 把系统设置 `framework.market_cdn_url` 指过去即可在本地市场页看到。
1767
+ **不触碰真实生产 CDN**。所有产物落在工程根 `cdn/`(由 `config.yaml` 的 `/cdn` 挂载直出)。
1768
+ **脚手架 seed 的 `framework.market_cdn_url` 缺省即相对路径 `/cdn`(本站点自供),起站即市场可用,
1769
+ 无需 setup**;只有指向外部真 CDN 时才需要 `market setup` 改写设置。
1702
1770
 
1703
- > 跑任何子命令前先 `ojm market setup` 一次(把模拟 CDN 地址写进站点配置)。
1771
+ > `market setup` 已非必需(见上);它只剩「指向外部真 CDN」一个场景。
1704
1772
  > 想看「这个包到底能不能装」而不污染工程,用 `ojm market trial`(一次性隔离站点,跑完即毁)。
1705
1773
  > 每个子命令都能 `ojm market <sub> --help` 单独看帮助。
1706
1774
 
1707
1775
  | 子命令 | 一句话 |
1708
1776
  |---|---|
1709
- | `ojm market setup [--url <站点>] [--user <u> --pass <p>] [--new-pass <p>]` | 把 `framework.market_cdn_url` 指向本地 `cdn/`;首跑若账号处于首次改密期会先自动改密再写入 |
1777
+ | `ojm market setup [--url <站点>] [--user <u> --pass <p>] [--new-pass <p>]` | **外部真 CDN 场景才需要**:把 `framework.market_cdn_url` 改写为绝对地址;账号须 **superadmin**(该设置安全组仅 superadmin 可写,admin 角色会被 403);首跑若账号处于首次改密期会先自动改密再写入 |
1710
1778
  | `ojm market publish [--fixture <name>@<version>] [--real <方案工程>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]` | 把方案包落进 `cdn/` 并做包级 detached 签名;**不碰索引**(索引由 `build-index` 重建,避免多人发布互相覆盖) |
1711
1779
  | `ojm market build-index [--id <key>] [--key <pem>] [--check]` | 扫描 `cdn/*.tgz`、提取包内 manifest,全量重建 `bundles.yaml` + `manifests.yaml` 并签名;结果只取决于目录内容,连续执行字节一致 |
1712
1780
  | `ojm market verify [--file <tgz>] [--rebuild-index]` | 离线验包(不起站点、不装、不要凭据):sha256 完整性 / 包级 Ed25519 验签 / 索引验签 / manifest 结构分列输出;`--rebuild-index` 额外比对重建索引与盘上索引的漂移 |
1713
1781
  | `ojm market trial [--fixture <name>@<version>] [--keep]` | 在一次性隔离站点里跑完整安装,跑完即毁;`--keep` 保留现场供排查 |
1782
+ | `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` 单跑验原子性 |
1714
1783
 
1715
1784
  - `--fixture <name>@<version>`:用内置合成包(无需先 build);`--real <方案工程>`:先 `ojm build --bundle` 再拿真实产物。
1716
1785
  - `--id <key>` / `--key <pem>`:包级签名身份(与 `ojm sign` 同体系,默认取 `~/.ojm/keys`)。
@@ -1722,25 +1791,44 @@ export default async (path: string, html: string) => {
1722
1791
  ## §11 方案包:打包与分发
1723
1792
 
1724
1793
  「站点级构建」见 §7;本节讲**方案包**(solution bundle)——单方案的可分发产物与到达站点的方式。
1794
+ 一个脚手架可放多份用户方案(`ojm new` 逐个生成,各自一份 `src/<name>/solution.yaml`),
1795
+ `ojm build --bundle` 无参时全部打包、逐个校验独立出包;框架自带方案自动排除(名单见
1796
+ `src/.ojm-builtin.json`,`ojm info` 默认折叠自带模块)。
1797
+ **从开发到发布的完整过程视角(含签名与私钥保存纪律)见**
1798
+ [`docs/guides/solution-dev-build-publish.md`](../guides/solution-dev-build-publish.md)。
1725
1799
 
1726
1800
  ### 11.1 打包:`ojm build --bundle`
1727
1801
 
1728
1802
  ```bash
1729
1803
  ojm build --bundle # --out <dir> 缺省工程根 dist/(方案包输出目录,与后端产物 api/dist 无关)
1730
- # [ojm] 方案包已生成 → dist/<name>-<version>.tgz
1804
+ ojm build --bundle crm # 只打 src/crm/ 这一份;--bundle=crm 等价
1805
+ # [ojm] 方案包已生成 → dist/<name>-<version>.tgz(多份用户方案时逐份输出,共打包 N 份)
1731
1806
  # [ojm] sha256:…(确定性打包:同源同字节)
1807
+ # 没有可打包的用户方案时:报错指向 ojm new(不静默产出空批次)
1732
1808
  ```
1733
1809
 
1734
1810
  > `--bundle` 只**组装**,不重跑构建:入包内容取自 `api/dist/<name>-<version>/` 与
1735
1811
  > `web/dist/modules/<name>/`——先 `ojm build` 再打包。
1736
1812
 
1737
- - 打包即校验(fail-fast,坏清单出不了包):方案 manifest 契约校验(字段齐全 / 类型白名单 /
1738
- 保留名 / 键空间 → 可读违规清单)→ 文件面校验(`api.tables` ↔ `schema.yaml`、locales 双语齐备)。
1813
+ - **契约入口(按优先级)**:`src/<模块>/solution.yaml` ≻ 工程根 `solution.yaml` ≻
1814
+ `src/<模块>/manifest.yaml` ≻ 工程根 `manifest.yaml`(方案级;`manifest.yaml` 为旧方案回退)。
1815
+ **单模块方案推荐把契约放在 `src/<模块>/solution.yaml`**——契约与它所打包的模块同位,最直观。
1816
+ 文件名刻意避开 `manifest.yaml`:oj 按 `manifest.yaml` 枚举发现 api 后端模块(§1.2),工程根若放一份
1817
+ bundle 契约会污染那条搜索枚举;故用 `solution.yaml` 隔开两条枚举——**后端按 `manifest.yaml` 枚举 api、
1818
+ 打包按 `solution.yaml` 取契约**,不再冲突。模块级契约同样**只认 `src/<模块>/` 根下、绝不进 `<模块>/api/`**,
1819
+ 否则会与 oj 的 api 枚举撞车。tgz 内固定写回 `manifest.yaml`(安装侧只认这个名字)。
1820
+ - **两段式**:先对全部目标契约跑完校验(纯读零写盘),全绿才开始逐个写 tgz——任一校验失败
1821
+ dist/ 零增量,报错指名失败方案(发布者不会把半截批次扫进 CDN 索引)。校验项(fail-fast,
1822
+ 坏清单出不了包):方案 solution 契约(字段齐全 / 类型白名单 / 保留名 / 键空间 → 可读违规清单)
1823
+ → 文件面(`api.tables` ↔ `schema.yaml`、locales 双语齐备)→ 模块级契约 `name` 必须 === 目录名。
1824
+ - 框架自带方案不参与打包:`src/.ojm-builtin.json` 名单内的模块契约被跳过(逐条日志);
1825
+ 该文件由 `ojm init` 从模板白名单派生,**可手改/删除**(删名 = 恢复参与打包),重跑 init 缺失才补写。
1826
+ 它只是打包排除 / info 折叠提示,**不是安全边界**:文件永不入包,安装侧信任链(签名 + 信任库 + TOFU)与它无关。
1739
1827
  - **只出包不签名**——签名见 §12(关注点分离:作者签名是显式动作,不是构建副产品)。
1740
1828
  - 产物内 `manifest.yaml` 的 `archive` 段是 **Form A**(仅 `file`);`sha256`/`size` 由
1741
1829
  file 源上传时点补齐(宿主安装器)或发布通道 Form B 自带。
1742
- - **入包红线**:`*.pem` / `*.key` / `.env` / `id_ed25519` / `id_rsa` 等私钥形态文件禁入包
1743
- (与 `modules.json` 禁入同款自检)——私钥入包即信任根基烧毁。
1830
+ - **入包红线**:`*.pem` / `*.key` / `*.p12` / `*.pfx` / `.env` 系 / `id_ed25519` / `id_rsa`
1831
+ 等私钥形态文件禁入包(与 `modules.json` 禁入同款自检,0.2.23 起扩面)——私钥入包即信任根基烧毁。
1744
1832
 
1745
1833
  ### 11.2 两种分发源
1746
1834
 
@@ -1850,7 +1938,7 @@ ojm keygen --id <publisher>.<key-name>
1850
1938
  ojm sign dist/<name>-<version>.tgz --id <publisher>.<key-name> --check
1851
1939
 
1852
1940
  # ③ 安装/分发:.sig 自动探测随包上传
1853
- ojm install dist/<name>-<version>.tgz --url <站点> --user admin --pass …
1941
+ ojm install dist/<name>-<version>.tgz --url <站点> --user superadmin --pass …
1854
1942
  ```
1855
1943
 
1856
1944
  宿主验签六码(失败文案双语,`--locale` 切换):`sig_missing`(缺签)/ `sig_alg`(算法
@@ -68,15 +68,15 @@
68
68
 
69
69
  模板自带一个**本地模拟 CDN**(工程根 `cdn/` + 工程根 `config.yaml` 里的 `/cdn` 挂载),配上 `ojm market` 四条命令,就能在本地复刻「发包 → 上架 → 索引 → 安装」全链路。不需要外部服务。
70
70
 
71
- ### 4.1 全流程(五步)
71
+ ### 4.1 全流程(五步,step 1 已省)
72
72
 
73
73
  ```bash
74
- # 0) 起站点(dev 或 serve 均可;市场地址必须指向 oj 子进程的端口,见 §5.1)
74
+ # 0) 起站点(dev 或 serve 均可)
75
75
  ojm dev
76
76
 
77
- # 1) 打开市场地址:把 framework.market_cdn_url 指向 http://127.0.0.1:9778/cdn
78
- # 站点管理员首次登录处于改密悬挂期时会自动改密换全权 token
79
- ojm market setup --user admin --pass 123456 --new-pass <你的口令>
77
+ # 1) 【免 setup】脚手架 seed 的 framework.market_cdn_url 缺省即 /cdn(本站点自供,
78
+ # dev 形态 ojm 已把 /cdn 反代给 oj 子进程)。只有指向外部真 CDN 时才需要:
79
+ # ojm market setup --user superadmin --pass 123456 --new-pass <你的口令>
80
80
 
81
81
  # 2) 发包:合成 fixture(或 --real 走 ojm build --bundle)→ 确定性 tgz → 包级签名
82
82
  # --rebuild-index 顺带重建索引
@@ -93,7 +93,7 @@ ojm market verify --rebuild-index
93
93
 
94
94
  | 命令 | 做什么 | **不**做什么 |
95
95
  |---|---|---|
96
- | `market setup` | 写系统设置 `framework.market_cdn_url` | 不改写工程根 `config.yaml`(`/cdn` 挂载由模板自带) |
96
+ | `market setup` | 写系统设置 `framework.market_cdn_url`(**外部真 CDN 场景才需要**;脚手架缺省 /cdn 免 setup) | 不改写工程根 `config.yaml`(`/cdn` 挂载由模板自带) |
97
97
  | `market publish` | 落 `<name>-<version>.tgz` + 包级 `.tgz.sig` | **不碰索引**(索引是共享单文件,多人发布会互相覆盖) |
98
98
  | `market build-index` | 扫 `cdn/*.tgz` 提取包内 manifest,全量重建两份索引并签名 | 不落包 |
99
99
  | `market verify` | 完整性 / 包级签 / 索引签 / manifest 结构四类校验 + 漂移检出 | 不起站点、不装、不访问任何远端 |
@@ -111,9 +111,9 @@ ojm market verify --rebuild-index
111
111
 
112
112
  ## 5. 两个已经踩过的坑(别再踩)
113
113
 
114
- ### 5.1 dev 形态下,`/cdn` 不在 dev server 上
114
+ ### 5.1 dev 形态下,`/cdn` 由 oj 子进程直出(ojm 已代为反代)
115
115
 
116
- `ojm dev` 的 Node 服务在 **5174**,只把 `/api` 反代给 oj 子进程;`/cdn` 是 **oj 的挂载**,只在 oj 子进程(`config.yaml` 的 `server.port`,默认 9778)上成立。所以市场地址**必须写绝对 URL** 且端口取自 config——写成相对 `/cdn` 的话,dev 下取到的是 index.html,报出来是难懂的 YAML 解析错,不是干净 404。`market setup` 已按 config 推导,别手改。
116
+ `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`)依旧兼容可用。
117
117
 
118
118
  ### 5.2 CDN 里摆了 `.tgz.sig` 侧车,不等于能装
119
119
 
@@ -165,7 +165,7 @@ node "$CLI_PKG/bin/ojm.mjs" market verify --rebuild-index
165
165
  node "$CLI_PKG/bin/ojm.mjs" build # 生成 api/dist + web/dist(含 api/dist/versions.json)
166
166
  # config:platform.root 改成 api/dist(安装目标恒为 release 部署,root 必须与 --api-path 同源)
167
167
  (bin/oj serve -c config.yaml -b /api --api-path api/dist --console-log &)
168
- node "$CLI_PKG/bin/ojm.mjs" market setup --url http://127.0.0.1:9778 --user admin --pass 123456 --new-pass <新口令>
168
+ node "$CLI_PKG/bin/ojm.mjs" market setup --url http://127.0.0.1:9778 --user superadmin --pass 123456 --new-pass <新口令>
169
169
  # 市场页点安装 → 首次停在「待信任」→ 核对指纹 → ojm trust confirm --task <id> → 重试 → done
170
170
  ```
171
171
 
@@ -174,7 +174,7 @@ node "$CLI_PKG/bin/ojm.mjs" market setup --url http://127.0.0.1:9778 --user admi
174
174
  | # | 现象 | 根因 / 正确做法 |
175
175
  |---|---|---|
176
176
  | ① | `oj build` 报 `tables 声明 "x" 不在 schema.yaml` | oj 用 `schema.yaml` 做表结构真源,`manifest.tables` 只是引用面;补 `schema.yaml`(`tables: {<t>: {pk, columns}}`),与迁移语句对齐 |
177
- | ② | `oj build` 成功但 `routes.js` 是空表、dist 里没有 handler | **只编译 `.ts` handler**;写成 `ping/api.js` 会被静默丢弃 → 改 `api.ts` |
177
+ | ② | `oj build` 成功但 `routes.js` 是空表、dist 里没有 handler | **只编译 `.ts` handler**;写成 `ping/api.js` 会被静默丢弃 → 改 `api.ts`。另一同形坑(构建也零报错):`api.ts` 写成裸具名导出(`export async function ping`)——oj 内省只读 **default export** 且链尾动词须在 METHODS 白名单(get/post/put/del/patch/head/options),裸导出的函数根本不在表里 → 改成 `const ping = { async get() {...} }; export default { ping }`(`ojm new --api` 骨架即此形状) |
178
178
  | ③ | `build --bundle` 报 `缺少 src/<name>/web/locales/…`(历史布局是 `web/src/<name>/locales/…`) | manifest 一旦引用 `title_key`/`label_key` 就必须双语 locale(后端方案也要);且 key 必须**嵌套** `{"menu":{"main":"…"}}`,平铺 `"menu.main"` 查不到 |
179
179
  | ④ | 安装 POST 返回 200,但 `market/task` 恒 404 | 宿主 config 缺 `fs:` 段 → 安装器落盘被 jail 拒、失败发生在任务文件写入前;0.2.13 起模板自带,旧工程手补 |
180
180
  | ⑤ | 安装卡 `preflight`:`无法读取宿主 runtime 版本` | 安装器读 `api/dist/versions.json`;0.2.13 起 `ojm build` 双落点(旧版手抄 `web/dist/versions.json`) |
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@oj-module/cli",
3
3
  "type": "module",
4
- "version": "0.2.20",
5
- "description": "React Antd Admin 模块工程命令行工具(含预构建宿主 shell-dist)",
4
+ "version": "0.2.25",
5
+ "description": "React Antd 模块工程命令行工具(含预构建宿主 shell-dist)",
6
6
  "license": "MIT",
7
7
  "exports": {
8
8
  ".": {
@@ -49,9 +49,10 @@
49
49
  ],
50
50
  "dependencies": {
51
51
  "@faker-js/faker": "^10.4.0",
52
- "@oj-module/runtime": "0.2.20",
52
+ "@oj-module/runtime": "0.2.25",
53
53
  "@redocly/cli": "^2.51.0",
54
54
  "@tailwindcss/vite": "^4.3.0",
55
+ "cac": "^6.7.14",
55
56
  "esbuild": "^0.28.2",
56
57
  "tailwindcss": "^4.3.0",
57
58
  "tsx": "^4.23.12",
@@ -97,7 +98,6 @@
97
98
  "zustand": "^5.0.13"
98
99
  },
99
100
  "publishConfig": {
100
- "registry": "https://registry.npmjs.org/",
101
101
  "access": "public"
102
102
  },
103
103
  "scripts": {
@@ -204,6 +204,7 @@ var init_site_base = __esmMin((() => {
204
204
  * 子路径部署(/oj/wiki/...)下直接与 loginPath 比较恒为 false,401 时会重复 goLogin。
205
205
  * React 组件内请用 `useLocation()`(react-router 已剥离 basename),无需本函数。
206
206
  *
207
+ * @param pathname 原始路径(含部署 base,如 `location.pathname`)
207
208
  * @param base 部署 base,恒以 `/` 结尾;缺省取 `resolveSiteBase()`(部署期注入优先,
208
209
  * 见 utils/site-base.ts —— runtime 产物里的 `import.meta.env.BASE_URL` 是构建期
209
210
  * 内联的 `/`,子路径部署下直接用它恒错)
@@ -303,10 +304,10 @@ function getAppInfo() {
303
304
  return {
304
305
  "pkg": {
305
306
  "name": "@oj-module/runtime",
306
- "version": "0.2.20",
307
+ "version": "0.2.25",
307
308
  "license": "MIT"
308
309
  },
309
- "lastBuildTime": "2026-10-22 12:17:23"
310
+ "lastBuildTime": "2026-10-23 15:39:15"
310
311
  };
311
312
  }
312
313
  var init_get_app_info = __esmMin((() => {}));
@@ -429,7 +430,7 @@ function PageError({ error, resetErrorBoundary }) {
429
430
  };
430
431
  useEffect(() => {
431
432
  if (enableDynamicTitle) document.title = t("exception.pageErrorTitle");
432
- }, [enableDynamicTitle]);
433
+ }, [enableDynamicTitle, t]);
433
434
  return /* @__PURE__ */ jsx(Result, {
434
435
  status: "error",
435
436
  icon: /* @__PURE__ */ jsx("div", {
@@ -3495,7 +3496,7 @@ function useCurrentRoute() {
3495
3496
  const matches = useMatches();
3496
3497
  return useMemo(() => {
3497
3498
  return matches[matches.length - 1];
3498
- }, [matches, location]);
3499
+ }, [matches]);
3499
3500
  }
3500
3501
  var init_use_current_route = __esmMin((() => {}));
3501
3502
  //#endregion
@@ -3595,7 +3596,7 @@ function useCssVar(name, root = defaultRoot, options = {}) {
3595
3596
  }), [name, root]);
3596
3597
  useEffect(() => {
3597
3598
  if (options?.initialValue) controls.set(options.initialValue);
3598
- }, [options?.initialValue]);
3599
+ }, [options?.initialValue, controls]);
3599
3600
  useEffect(() => {
3600
3601
  return () => controls.remove();
3601
3602
  }, [controls]);
@@ -3656,7 +3657,7 @@ var init_dom = __esmMin((() => {}));
3656
3657
  * @en Get the style of the layout content area
3657
3658
  */
3658
3659
  function useLayoutContentStyle() {
3659
- const contentElement = useRef(null);
3660
+ const contentElementRef = useRef(null);
3660
3661
  const [visibleDomRect, setVisibleDomRect] = useState(null);
3661
3662
  const resizeObserverRef = useRef(null);
3662
3663
  const contentHeightControls = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
@@ -3673,7 +3674,7 @@ function useLayoutContentStyle() {
3673
3674
  };
3674
3675
  }, [visibleDomRect]);
3675
3676
  const debouncedCalcHeight = useDebounceFn((_entries) => {
3676
- const rect = getElementVisibleRect(contentElement.current);
3677
+ const rect = getElementVisibleRect(contentElementRef.current);
3677
3678
  setVisibleDomRect(rect);
3678
3679
  if (rect) {
3679
3680
  contentHeightControls.set(`${rect.height}px`);
@@ -3681,10 +3682,10 @@ function useLayoutContentStyle() {
3681
3682
  }
3682
3683
  }, { wait: 16 });
3683
3684
  useEffect(() => {
3684
- if (contentElement.current && !resizeObserverRef.current) {
3685
+ if (contentElementRef.current && !resizeObserverRef.current) {
3685
3686
  const resizeObserver = new ResizeObserver(debouncedCalcHeight.run);
3686
3687
  resizeObserverRef.current = resizeObserver;
3687
- resizeObserver.observe(contentElement.current);
3688
+ resizeObserver.observe(contentElementRef.current);
3688
3689
  }
3689
3690
  return () => {
3690
3691
  resizeObserverRef.current?.disconnect();
@@ -3692,7 +3693,7 @@ function useLayoutContentStyle() {
3692
3693
  };
3693
3694
  }, [debouncedCalcHeight]);
3694
3695
  return {
3695
- contentElement,
3696
+ contentElementRef,
3696
3697
  overlayStyle,
3697
3698
  visibleDomRect
3698
3699
  };
@@ -4173,7 +4174,7 @@ function KeepAliveLayer() {
4173
4174
  (aliveRef.current?.getCacheNodes?.())?.forEach((node) => {
4174
4175
  if (!openTabs.has(node.cacheKey)) aliveRef.current?.destroy(node.cacheKey);
4175
4176
  });
4176
- }, [openTabs]);
4177
+ }, [openTabs, aliveRef]);
4177
4178
  /**
4178
4179
  * 关闭多 tab 功能时,清空所有非当前页面缓存
4179
4180
  */
@@ -4181,10 +4182,18 @@ function KeepAliveLayer() {
4181
4182
  if (!tabbarEnable) (aliveRef.current?.getCacheNodes?.())?.forEach((node) => {
4182
4183
  if (node.cacheKey !== cacheKey) aliveRef.current?.destroy(node.cacheKey);
4183
4184
  });
4184
- }, [tabbarEnable]);
4185
+ }, [
4186
+ tabbarEnable,
4187
+ aliveRef,
4188
+ cacheKey
4189
+ ]);
4185
4190
  useEffect(() => {
4186
4191
  if (tabbarEnable && isRefresh) aliveRef.current?.refresh();
4187
- }, [isRefresh]);
4192
+ }, [
4193
+ isRefresh,
4194
+ aliveRef,
4195
+ tabbarEnable
4196
+ ]);
4188
4197
  const keepAliveExclude = useMemo(() => {
4189
4198
  /**
4190
4199
  * 不开启多 tab 时不需要 KeepAlive,把所有路由放进 exclude 仅保留切换动画
@@ -4212,7 +4221,7 @@ var init_keep_alive_layer = __esmMin((() => {
4212
4221
  //#region src/layout/layout-content/index.tsx
4213
4222
  function LayoutContent() {
4214
4223
  const { token: { colorBgLayout } } = theme.useToken();
4215
- const { contentElement } = useLayoutContentStyle();
4224
+ const { contentElementRef } = useLayoutContentStyle();
4216
4225
  const enableFooter = usePreferencesStore((state) => state.enableFooter);
4217
4226
  const fixedFooter = usePreferencesStore((state) => state.fixedFooter);
4218
4227
  /**
@@ -4221,7 +4230,7 @@ function LayoutContent() {
4221
4230
  */
4222
4231
  return /* @__PURE__ */ jsx("main", {
4223
4232
  id: ELEMENT_ID_MAIN_CONTENT,
4224
- ref: contentElement,
4233
+ ref: contentElementRef,
4225
4234
  className: "relative overflow-y-auto overflow-x-hidden grow",
4226
4235
  style: { backgroundColor: colorBgLayout },
4227
4236
  children: /* @__PURE__ */ jsx(Scrollbar, { children: /* @__PURE__ */ jsx(GlobalSpin, { children: /* @__PURE__ */ jsxs("div", {
@@ -4447,7 +4456,11 @@ function GlobalSearch() {
4447
4456
  useKeyPress("downarrow", handleDown);
4448
4457
  useEffect(() => {
4449
4458
  if (!keyword.length && Array.isArray(searchHistory)) setResultOptions(searchMenuList.filter((item) => searchHistory?.includes(item.key)));
4450
- }, [keyword, searchHistory]);
4459
+ }, [
4460
+ keyword,
4461
+ searchHistory,
4462
+ searchMenuList
4463
+ ]);
4451
4464
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
4452
4465
  onClick: () => setOpen((open) => !open),
4453
4466
  className: "group flex justify-center items-center gap-2 md:bg-colorBgLayout px-3 py-1.5 rounded-full cursor-pointer text-colorTextSecondary hover:text-colorText md:mr-2.5",
@@ -5113,18 +5126,18 @@ function Sidebar() {
5113
5126
  name: item.name,
5114
5127
  checked: item.value,
5115
5128
  onChange: handleChange,
5116
- children: item.label,
5117
- disabled: item.disabled
5129
+ disabled: item.disabled,
5130
+ children: item.label
5118
5131
  }, item.name);
5119
5132
  }),
5120
5133
  /* @__PURE__ */ jsx(SwitchItem, {
5121
5134
  name: "sidebarTheme",
5122
5135
  checked: sidebarTheme === "light",
5123
5136
  onChange: (name, value) => setPreferences(name, value ? "light" : "dark"),
5124
- children: t("preferences.sidebar.sidebarTheme"),
5125
5137
  disabled: !sidebarEnable,
5126
5138
  unCheckedChildren: /* @__PURE__ */ jsx(RiContrastFill, {}),
5127
- checkedChildren: /* @__PURE__ */ jsx(RiMoonIcon, {})
5139
+ checkedChildren: /* @__PURE__ */ jsx(RiMoonIcon, {}),
5140
+ children: t("preferences.sidebar.sidebarTheme")
5128
5141
  }),
5129
5142
  /* @__PURE__ */ jsx(NumberInputSpinner, {
5130
5143
  min: 40,
@@ -5702,7 +5715,7 @@ function UserMenu({ ...restProps }) {
5702
5715
  }
5703
5716
  if (key === "personal-center") navigate("/personal-center/my-profile");
5704
5717
  };
5705
- const altView = useMemo(() => isWindowsOs() ? "Alt" : "⌥", [isWindowsOs]);
5718
+ const altView = useMemo(() => isWindowsOs() ? "Alt" : "⌥", []);
5706
5719
  const items = [{
5707
5720
  label: t("personal-center:menu.personalCenter"),
5708
5721
  key: "personal-center",
@@ -5973,7 +5986,7 @@ function LayoutMenu({ mode = "inline", autoExpandCurrentMenu, handleMenuSelect,
5973
5986
  isMobile,
5974
5987
  sidebarCollapsed
5975
5988
  ]);
5976
- const handleOpenChange = (keys) => {
5989
+ const handleOpenChange = useCallback((keys) => {
5977
5990
  /**
5978
5991
  * 1. 手风琴模式,点击菜单项,自动关闭其他菜单
5979
5992
  * 2. 非手风琴模式且菜单是收起的,鼠标悬浮菜单自动关闭其他菜单
@@ -5992,7 +6005,12 @@ function LayoutMenu({ mode = "inline", autoExpandCurrentMenu, handleMenuSelect,
5992
6005
  if (currentCloseKey) setOpenKeys(menuParentKeys[currentCloseKey]);
5993
6006
  }
5994
6007
  } else setOpenKeys(keys);
5995
- };
6008
+ }, [
6009
+ accordion,
6010
+ sidebarCollapsed,
6011
+ openKeys,
6012
+ menuParentKeys
6013
+ ]);
5996
6014
  const menuOpenProps = useMemo(() => {
5997
6015
  if (autoExpandCurrentMenu) return {
5998
6016
  openKeys,
@@ -6019,7 +6037,8 @@ function LayoutMenu({ mode = "inline", autoExpandCurrentMenu, handleMenuSelect,
6019
6037
  }, [
6020
6038
  matches,
6021
6039
  sidebarCollapsed,
6022
- getSelectedKeys
6040
+ getSelectedKeys,
6041
+ accordion
6023
6042
  ]);
6024
6043
  return /* @__PURE__ */ jsx(Menu, {
6025
6044
  /**
@@ -6552,7 +6571,6 @@ function useDropdownMenu() {
6552
6571
  }, [
6553
6572
  t,
6554
6573
  activeKey,
6555
- homePath,
6556
6574
  openTabs
6557
6575
  ]);
6558
6576
  /**
@@ -6569,6 +6587,8 @@ function useDropdownMenu() {
6569
6587
  [TabActionKeys.CLOSE_OTHERS]: closeOtherTabs,
6570
6588
  [TabActionKeys.CLOSE_ALL]: closeAllTabs
6571
6589
  }), [
6590
+ refresh,
6591
+ setIsRefresh,
6572
6592
  removeTab,
6573
6593
  closeRightTabs,
6574
6594
  closeLeftTabs,
@@ -6814,7 +6834,7 @@ function LayoutTabbar() {
6814
6834
  hash: ""
6815
6835
  };
6816
6836
  navigate(key + historyState.search + historyState.hash);
6817
- }, [openTabs]);
6837
+ }, [openTabs, navigate]);
6818
6838
  /**
6819
6839
  * 处理标签页编辑(关闭)
6820
6840
  * @param {React.MouseEvent | React.KeyboardEvent | string} key - 被编辑的标签页的key
@@ -6941,7 +6961,8 @@ function LayoutTabbar() {
6941
6961
  location,
6942
6962
  currentRoute,
6943
6963
  setActiveKey,
6944
- addTab
6964
+ addTab,
6965
+ activeKey
6945
6966
  ]);
6946
6967
  return /* @__PURE__ */ jsx("div", {
6947
6968
  className: classes.tabsContainer,
@@ -7023,7 +7044,7 @@ function ContainerLayout() {
7023
7044
  if (screens.lg && !screens.xl) setPreferences("sidebarCollapsed", true);
7024
7045
  else if (screens.xl) setPreferences("sidebarCollapsed", false);
7025
7046
  else if (screens.xs || screens.sm && !screens.md) setPreferences("sidebarCollapsed", false);
7026
- }, [screens]);
7047
+ }, [screens, setPreferences]);
7027
7048
  const sidebarEnableState = useMemo(() => !isTopNav && sidebarEnable, [isTopNav, sidebarEnable]);
7028
7049
  const computedSidebarWidth = useMemo(() => {
7029
7050
  if (isMaximize || isMobile) return 0;
@@ -7049,7 +7070,7 @@ function ContainerLayout() {
7049
7070
  let height = 48;
7050
7071
  if (tabbarEnable) height += 35;
7051
7072
  return height;
7052
- }, [tabbarEnable, 35]);
7073
+ }, [tabbarEnable]);
7053
7074
  useEffect(() => {
7054
7075
  setLayoutHeaderHeight(isMaximize ? 35 : headerWrapperHeight);
7055
7076
  }, [headerWrapperHeight, isMaximize]);
@@ -7934,7 +7955,7 @@ async function generateRoutesFromBackend(backendRoutes) {
7934
7955
  /**
7935
7956
  * 转换路由配置
7936
7957
  * @param route 原始路由配置
7937
- * @param parentPath 父级路径(用于嵌套路由)
7958
+ * @param parentComponentPath 父级组件路径(用于嵌套路由)
7938
7959
  * @returns 转换后的路由配置
7939
7960
  */
7940
7961
  const transformRoute = async (route, parentComponentPath) => {
@@ -8853,9 +8874,10 @@ async function handleErrorResponse(response) {
8853
8874
  openMustChangeModal();
8854
8875
  return response;
8855
8876
  }
8877
+ if (response.status === 403) return response;
8856
8878
  const json = data;
8857
8879
  message$1.error(json.msg || response.statusText);
8858
- } else message$1.error(response.statusText);
8880
+ } else if (response.status !== 403) message$1.error(response.statusText);
8859
8881
  return response;
8860
8882
  }
8861
8883
  var init_error_response = __esmMin((() => {
@@ -8918,8 +8940,6 @@ var init_remember_route = __esmMin((() => {
8918
8940
  * 必须带上部署 base(`resolveSiteBase()`):子路径部署下 `location.href = "/login"`
8919
8941
  * 会跳到宿主机根,而不是 `/oj/login`。不能用 `import.meta.env.BASE_URL`——预构建
8920
8942
  * 产物里它是构建期内联的 `/`。
8921
- *
8922
- * @returns 无返回值
8923
8943
  */
8924
8944
  function goLogin() {
8925
8945
  useAuthStore.getState().reset();
@@ -9022,6 +9042,8 @@ function onRefreshFailed(error) {
9022
9042
  * 订阅者对象应包含 resolve 和 reject 方法。
9023
9043
  *
9024
9044
  * @param subscriber 订阅者对象,包含 resolve 和 reject 方法
9045
+ * @param subscriber.resolve 当 token 刷新成功时调用的函数,传入新的 token
9046
+ * @param subscriber.reject 当 token 刷新失败时调用的函数,传入错误信息
9025
9047
  */
9026
9048
  function addRefreshSubscriber(subscriber) {
9027
9049
  refreshSubscribers.push(subscriber);
@@ -9447,6 +9469,10 @@ function useAccess() {
9447
9469
  * 权限验证组件
9448
9470
  *
9449
9471
  * @param AccessControlProps 权限验证组件的属性
9472
+ * @param AccessControlProps.type 权限类型,默认为 code
9473
+ * @param AccessControlProps.codes 权限值,可以是字符串或字符串数组
9474
+ * @param AccessControlProps.children 有权限时渲染的子组件
9475
+ * @param AccessControlProps.fallback 无权限时显示的内容
9450
9476
  * @returns 若子组件存在,并且传入的权限值有效,则返回子组件;否则返回 null
9451
9477
  */
9452
9478
  function AccessControl({ type = "code", codes, children, fallback }) {
@@ -9800,7 +9826,7 @@ function BasicTable(props) {
9800
9826
  if (enableFooter && fixedFooter) return 40;
9801
9827
  return 0;
9802
9828
  }, [enableFooter, fixedFooter]);
9803
- const getPaginationProps = () => {
9829
+ const getPaginationProps = useCallback(() => {
9804
9830
  if (props.pagination === false) return false;
9805
9831
  return {
9806
9832
  placement: ["bottomStart"],
@@ -9810,7 +9836,7 @@ function BasicTable(props) {
9810
9836
  showTotal: (total) => t("common.pagination", { total }),
9811
9837
  ...props.pagination
9812
9838
  };
9813
- };
9839
+ }, [props.pagination, t]);
9814
9840
  /**
9815
9841
  * @description 计算分页器的高度
9816
9842
  * 如果分页器被禁用,则返回 0,否则根据分页器的大小返回相应的高度
@@ -5,9 +5,9 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" href="/favicon.ico" />
7
7
  <title>React Antd Admin · Shell</title>
8
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-oCauQheXzDS2E/yd7smCtg=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
8
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-rkh8kQ0W+uFw3wTdIkSSUA=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
9
9
 
10
- <script type="importmap" nonce="oCauQheXzDS2E/yd7smCtg==">{
10
+ <script type="importmap" nonce="rkh8kQ0W+uFw3wTdIkSSUA==">{
11
11
  "imports": {
12
12
  "react": "/assets/react.js",
13
13
  "react/jsx-runtime": "/assets/jsx-runtime.js",