@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
@@ -7,7 +7,7 @@
7
7
  ```bash
8
8
  pnpm install
9
9
  pnpm dev # 开发:前端 devServer + oj 后端(api/src 保存即热更)
10
- pnpm build # 构建:oj build + 前端全站合并到 web/dist
10
+ pnpm build # 构建:oj build + 前端全站合并到 dist/web
11
11
  pnpm preview # 预览:oj migrate + server(release 产物)
12
12
  ```
13
13
 
@@ -35,9 +35,9 @@ pnpm exec ojm api # 重新生成 web/src/*/__client(dev
35
35
  ```
36
36
  bin/ oj 二进制(init 解压,不入库)
37
37
  api/src/ 后端模块源码(目录镜像路由:api/src/<模块>/<路径>/api.ts)
38
- api/dist/ oj build 产物
38
+ dist/api/ oj build 产物
39
39
  web/src/ 前端模块源码
40
- web/dist/ 构建产物(完整站点)
40
+ dist/web/ 构建产物(完整站点)
41
41
  ```
42
42
 
43
43
  ## 注意
@@ -1,8 +1,8 @@
1
1
  # cdn/ —— 本地模拟市场源
2
2
 
3
- 本目录是**本地模拟 CDN**:`ojm market publish` 把分发包落在这里,站点通过 `api/config.yaml` 里的挂载
4
- (`- prefix: /cdn` → `web: ../cdn`)把它直出成分发源,市场页按系统设置 `framework.market_cdn_url`
5
- 指向的地址读取。
3
+ 本目录是**本地模拟 CDN**:`ojm market publish` 把分发包落在这里,站点通过工程根 `config.yaml` 里的挂载
4
+ (`- prefix: /cdn` → `web: ./cdn`)把它直出成分发源。市场按系统设置 `framework.market_cdn_url` 读取,
5
+ **seed 缺省值即相对路径 `/cdn`(本站点自供),新工程免 `ojm market setup`**;指向外部真 CDN 时才改设置。
6
6
 
7
7
  > **目录缺失会让站点起不来**:`oj serve` 启动即校验挂载目录,缺失就报错退出。
8
8
  > 所以本 README 进模板、进发布包、进新工程——它的存在就是目录存在的理由,**不要删**。
@@ -23,14 +23,14 @@
23
23
 
24
24
  ## 生产怎么切出去
25
25
 
26
- 1. 删掉 `api/config.yaml` 的 `mounts` 里这条(它就是本地挂载的全部):
26
+ 1. 删掉工程根 `config.yaml` 的 `mounts` 里这条(它就是本地挂载的全部):
27
27
 
28
28
  ```yaml
29
29
  - prefix: /cdn
30
- web: ../cdn
30
+ web: ./cdn
31
31
  ```
32
32
 
33
- 2. 把真 CDN(对象存储 / nginx)的地址写进系统设置 `framework.market_cdn_url`;
33
+ 2. 把真 CDN(对象存储 / nginx)的**绝对地址**写进系统设置 `framework.market_cdn_url`;
34
34
  命令行等价写法是 `ojm market setup --url <地址>`。
35
35
  3. 索引(`bundles.yaml` / `manifests.yaml`)由 CDN 侧维护:`ojm market build-index` 在本地产出后自行上传。
36
36
 
@@ -30,19 +30,20 @@
30
30
  # **这里没有 api 挂载是有意的**:本工程是 `src/<name>/{api,web}` 布局,模块分散在
31
31
  # `src/<name>/api` 下,而 mounts 的 api 项按 `<目录>/<模块>/manifest.yaml` 单层解析、
32
32
  # 不会递归(实测报 `module 'x' missing manifest.yaml`)。源码形态由 `ojm dev` 用
33
- # `--module-root src` 供给,产物形态由 `ojm preview` 用 `--api-path api/dist` 供给;
33
+ # `--module-root src` 供给,产物形态由 `ojm preview` 用 `--api-path dist/api` 供给;
34
34
  # 两者与 mounts 的 api 项同时出现还会报「module declared in multiple places」。
35
35
  # 手写部署(不经 ojm)时才需要自己加回一条 `- prefix: /api` + `api: dist`。
36
36
  mounts:
37
37
  # 本地模拟 CDN:直出工程根 cdn/(内容见 cdn/README.md)。
38
- # 红线:生产请删掉这一条挂载,并把 system 设置 framework.market_cdn_url 指向真 CDN。
38
+ # seed 的 framework.market_cdn_url 缺省即 /cdn(本站点自供),新工程免 market setup。
39
+ # 红线:生产请删掉这一条挂载,并把 system 设置 framework.market_cdn_url 指向真 CDN(绝对地址)。
39
40
  # 不得给此挂载设 spa——缺文件必须返回 404,回落 index.html 会让市场解析崩。
40
41
  - prefix: /cdn
41
42
  web: ./cdn
42
43
  # 生产若由 oj 直出前端(前后端同进程,即 ojm preview 的生产形态),
43
- # 取消下一行注释并先 build 出 web/dist(preview 会自动注入等价挂载,无需手改):
44
+ # 取消下一行注释并先 build 出 dist/web(preview 会自动注入等价挂载,无需手改):
44
45
  # - prefix: "/"
45
- # web: "./web/dist" # 相对 config.yaml → 工程根/web/dist
46
+ # web: "./dist/web" # 相对 config.yaml → 工程根/dist/web
46
47
  # spa: true
47
48
  # 嵌套 handler 组(oj v0.1.64)无需配置开关,组织方式见 api.ts 顶部注释。
48
49
 
@@ -8,7 +8,7 @@
8
8
  # 2. 模板自运行专属文件——templates 本身也是一个可 `pnpm dev` 的工程,
9
9
  # 它的 package.json / node_modules / bin / lock 绝不能泄漏进用户工程;
10
10
  # package.json 由 init 现场生成(钉死真实版本,不带 workspace:/catalog:)。
11
- # 3. 构建与运行期产物——api/dist、web/dist、logs、签发的私钥与 sqlite;
11
+ # 3. 构建与运行期产物——dist/api、dist/web、logs、签发的私钥与 sqlite;
12
12
  # 模板跑过 build/dev 后会留在本地,既不进用户工程,更不该让所有工程
13
13
  # 共享同一把私钥。
14
14
  # 4. 模板自用元数据——本清单自身。
@@ -9,7 +9,9 @@
9
9
  "preview": "ojm preview",
10
10
  "info": "ojm info",
11
11
  "typecheck": "tsc --noEmit -p tsconfig.json",
12
- "typecheck:api": "tsc --noEmit -p src/tsconfig.json"
12
+ "typecheck:api": "tsc --noEmit -p src/tsconfig.json",
13
+ "bundle": "ojm build --bundle",
14
+ "new": "ojm new"
13
15
  },
14
16
  "devDependencies": {
15
17
  "@ant-design/cssinjs": "catalog:",
@@ -1,5 +1,6 @@
1
1
  -- 系统设置默认值(PRD §4.3;注释内禁英文分号)
2
2
  INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('framework.site_title', 'framework', 'general', 'string', 'React Antd Admin', 'React Antd Admin', 'system:setting.site_title', 0, 0);
3
- INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '', '', 'system:setting.market_cdn_url', 0, 0);
3
+ -- 系统设置默认值(PRD §4.3;注释内禁英文分号;framework.market_cdn_url 缺省 /cdn = 本站点自供,免 setup)
4
+ INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '/cdn', '/cdn', 'system:setting.market_cdn_url', 0, 0);
4
5
  INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('ldap.enabled', 'ldap', 'login', 'bool', '0', '0', 'system:setting.ldap_enabled', 0, 0);
5
6
  INSERT IGNORE INTO sys_setting (key, module, `group`, type, value, default_value, label_key, secret, update_time) VALUES ('sso.enabled', 'sso', 'login', 'bool', '0', '0', 'system:setting.sso_enabled', 0, 0);
@@ -1,5 +1,6 @@
1
1
  -- 系统设置默认值(PRD §4.3;注释内禁英文分号)
2
2
  INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.site_title', 'framework', 'general', 'string', 'React Antd Admin', 'React Antd Admin', 'system:setting.site_title', 0, 0) ON CONFLICT DO NOTHING;
3
- INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '', '', 'system:setting.market_cdn_url', 0, 0) ON CONFLICT DO NOTHING;
3
+ -- 系统设置默认值(PRD §4.3;注释内禁英文分号;framework.market_cdn_url 缺省 /cdn = 本站点自供,免 setup)
4
+ INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '/cdn', '/cdn', 'system:setting.market_cdn_url', 0, 0) ON CONFLICT DO NOTHING;
4
5
  INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap.enabled', 'ldap', 'login', 'bool', '0', '0', 'system:setting.ldap_enabled', 0, 0) ON CONFLICT DO NOTHING;
5
6
  INSERT INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('sso.enabled', 'sso', 'login', 'bool', '0', '0', 'system:setting.sso_enabled', 0, 0) ON CONFLICT DO NOTHING;
@@ -1,10 +1,9 @@
1
1
  -- L1 测试账号(仅 `oj test` 经 fixtures 灌入,不进 release 产物)。
2
2
  --
3
- -- 为什么不能用 seed 的 admin 跑 L1 登录:
4
- -- ① seed 的 admin 是 must_change=1 首登改密态(D12 生产语义),且初始口令 123456
5
- -- 只是开发便利——用户在 dev 库改过口令后(产品主动引导),admin 口令即漂移,
6
- -- L1 若绑 admin/123456 会随机 401(本地实测踩过:dev 库口令被改成 12345678)。
7
- -- ② 判权类端点(currentAccess/assertPermission)对 must_change=1 恒 403。
3
+ -- 为什么不能用 seed 的开发账号(admin/superadmin 等)跑 L1 登录:
4
+ -- ① seed 账号初始口令 123456 只是开发便利——用户在 dev 库改过口令后,口令即漂移,
5
+ -- L1 若绑 seed 账号会随机 401(本地实测踩过:dev 库口令被改成 12345678)。
6
+ -- ② 不用 must_change 解释:seed 账号现均为 0(2026-10-22 口径);真正的原因是 ①。
8
7
  -- 故 additive 建一个专用账号:INSERT OR IGNORE 幂等,反复重放终态一致,
9
8
  -- 不碰 admin 既有行(用户的口令/状态不受 `oj test` 影响)。
10
9
  -- 按分号朴素切分(S006):语句内不得含分号字面量。
@@ -87,7 +87,7 @@ export interface MarketEnv {
87
87
 
88
88
  /**
89
89
  * 宿主 versions.json 布局推导(B-11):与 manifests.yaml 同层(release 根 = `sharedRoot`),
90
- * 不是站点产物根——两处根在 `src/<name>/{api,web}` 布局下已分流(`api/dist` vs `web/dist`)。
90
+ * 不是站点产物根——两处根在 `src/<name>/{api,web}` 布局下已分流(`dist/api` vs `dist/web`)。
91
91
  */
92
92
  export function versionsJsonPathOf(releaseRoot: string): string {
93
93
  const trimmed = releaseRoot.replace(/\/+$/, "");
@@ -132,21 +132,22 @@ async function hostVersionsOf(env: MarketEnv): Promise<Record<string, string> |
132
132
  /**
133
133
  * oj serve release 布局生产缺省(P1-16 B3/B4;全字段可覆盖)。
134
134
  *
135
- * 两处根在 `src/<name>/{api,web}` 布局下分流,且都相对 fs jail 根(= config.yaml 所在目录):
136
- * release 根 `api/dist` —— 版本目录 / manifests.yaml / versions.json / migrations(后端产物)
137
- * 站点根 `web/dist` —— modules/ + modules.json(前端产物,安装器 upsert 的落点)
135
+ * 两处根在 `src/<name>/{api,web}` 布局下分流(2026-10-22 起收进工程根 dist/),
136
+ * 且都相对 fs jail 根(= config.yaml 所在目录):
137
+ * release 根 `dist/api` —— 版本目录 / manifests.yaml / versions.json / migrations(后端产物)
138
+ * 站点根 `dist/web` —— modules/ + modules.json(前端产物,安装器 upsert 的落点)
138
139
  * 暂存 / 任务 / 状态 / 信任库 / db 仍在 jail 根下:它们是安装器私有面,不属任何产物根。
139
140
  */
140
141
  export function marketEnv(): MarketEnv {
141
142
  return {
142
- sharedRoot: "api/dist",
143
+ sharedRoot: "dist/api",
143
144
  staging: "modules/.staging",
144
145
  taskDir: "modules/.install-tasks",
145
146
  statePath: "modules/.solutions/state.yaml",
146
147
  trustPath: ".solutions/trusted-keys.yaml",
147
148
  dbPath: "db.sqlite",
148
- siteModulesDir: "web/dist/modules",
149
- modulesJsonPath: "web/dist/modules.json",
149
+ siteModulesDir: "dist/web/modules",
150
+ modulesJsonPath: "dist/web/modules.json",
150
151
  apiPrefix: "/api",
151
152
  ledgerHeadOf: journalHeadOf,
152
153
  };
@@ -162,7 +163,9 @@ function fail(status: number, msg: string): never {
162
163
  }
163
164
 
164
165
  function fetchOf(env: MarketEnv): FetchInitFace {
165
- return env.fetch ?? (globalThis as { fetch?: FetchInitFace }).fetch as FetchInitFace;
166
+ // 相对基址(feature:/cdn = 本站点自供)按请求源补全同源绝对 URL;绝对 http(s) 直通。
167
+ // 无请求上下文(无 Host)不猜原点、原样透传;runInstall 侧的 withSiteOrigin 包裹幂等。
168
+ return withSiteOrigin(env.fetch ?? (globalThis as { fetch?: FetchInitFace }).fetch as FetchInitFace);
166
169
  }
167
170
 
168
171
  function execOf(env: MarketEnv): ExecFace {
@@ -1,8 +1,16 @@
1
1
  -- _platform 种子数据 mysql 方言覆盖(方言覆盖文件必胜;INSERT OR IGNORE → INSERT IGNORE)。
2
- -- 语句分号切分与注释纪律同基线 seed.sql。
3
- INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 1);
2
+ -- 语句分号切分与注释纪律同基线 seed.sql;账号口径同基线(账号名=角色名,must_change=0)。
3
+ INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('superadmin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 0);
4
+ INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 0);
5
+ INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('editor', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 0);
6
+ INSERT IGNORE INTO users (username, password_hash, must_change) VALUES ('viewer', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 0);
7
+ DELETE FROM sys_user_role WHERE role_name = 'superadmin' AND user_id = (SELECT id FROM users WHERE username = 'admin');
8
+ UPDATE users SET must_change = 0 WHERE username IN ('superadmin', 'admin', 'editor', 'viewer');
4
9
  INSERT IGNORE INTO sys_role (name, module, title, builtin) VALUES ('superadmin', 'framework', '超级管理员', 1);
5
10
  INSERT IGNORE INTO sys_role (name, module, title, builtin) VALUES ('admin', 'framework', '系统管理员', 1);
6
11
  INSERT IGNORE INTO sys_role (name, module, title, builtin) VALUES ('editor', 'framework', '编辑', 1);
7
12
  INSERT IGNORE INTO sys_role (name, module, title, builtin) VALUES ('viewer', 'framework', '访客', 1);
8
- INSERT IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = 'admin';
13
+ INSERT IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = 'superadmin';
14
+ INSERT IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'admin' FROM users WHERE username = 'admin';
15
+ INSERT IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'editor' FROM users WHERE username = 'editor';
16
+ INSERT IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'viewer' FROM users WHERE username = 'viewer';
@@ -1,8 +1,16 @@
1
1
  -- _platform 种子数据 pg 方言覆盖(方言覆盖文件必胜;INSERT OR IGNORE → ON CONFLICT DO NOTHING)。
2
- -- 语句分号切分与注释纪律同基线 seed.sql。
3
- INSERT INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 1) ON CONFLICT DO NOTHING;
2
+ -- 语句分号切分与注释纪律同基线 seed.sql;账号口径同基线(账号名=角色名,must_change=0)。
3
+ INSERT INTO users (username, password_hash, must_change) VALUES ('superadmin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 0) ON CONFLICT DO NOTHING;
4
+ INSERT INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 0) ON CONFLICT DO NOTHING;
5
+ INSERT INTO users (username, password_hash, must_change) VALUES ('editor', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 0) ON CONFLICT DO NOTHING;
6
+ INSERT INTO users (username, password_hash, must_change) VALUES ('viewer', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 0) ON CONFLICT DO NOTHING;
7
+ DELETE FROM sys_user_role WHERE role_name = 'superadmin' AND user_id = (SELECT id FROM users WHERE username = 'admin');
8
+ UPDATE users SET must_change = 0 WHERE username IN ('superadmin', 'admin', 'editor', 'viewer');
4
9
  INSERT INTO sys_role (name, module, title, builtin) VALUES ('superadmin', 'framework', '超级管理员', 1) ON CONFLICT DO NOTHING;
5
10
  INSERT INTO sys_role (name, module, title, builtin) VALUES ('admin', 'framework', '系统管理员', 1) ON CONFLICT DO NOTHING;
6
11
  INSERT INTO sys_role (name, module, title, builtin) VALUES ('editor', 'framework', '编辑', 1) ON CONFLICT DO NOTHING;
7
12
  INSERT INTO sys_role (name, module, title, builtin) VALUES ('viewer', 'framework', '访客', 1) ON CONFLICT DO NOTHING;
8
- INSERT INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = 'admin' ON CONFLICT DO NOTHING;
13
+ INSERT INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = 'superadmin' ON CONFLICT DO NOTHING;
14
+ INSERT INTO sys_user_role (user_id, role_name) SELECT id, 'admin' FROM users WHERE username = 'admin' ON CONFLICT DO NOTHING;
15
+ INSERT INTO sys_user_role (user_id, role_name) SELECT id, 'editor' FROM users WHERE username = 'editor' ON CONFLICT DO NOTHING;
16
+ INSERT INTO sys_user_role (user_id, role_name) SELECT id, 'viewer' FROM users WHERE username = 'viewer' ON CONFLICT DO NOTHING;
@@ -1,19 +1,31 @@
1
1
  -- _platform 种子数据(只放数据,DDL 归 migrations/,S006 纪律)。
2
2
  -- 方言覆盖:seed.mysql.sql / seed.pg.sql(INSERT OR IGNORE 为 sqlite 专属语法,方言文件必胜,同迁移约定)。
3
3
  -- 注意:seed 按英文分号朴素切分语句,注释里也不要出现英文分号。
4
- -- 账号生命周期 D12:admin 为 seed 超管(superadmin),must_change=1 首登强制改密——
5
- -- 初始口令 123456 仅为开发便利,must_change 闭环兜底默认凭据窗口(口令不随机生成的取舍见计划 A2 阶段小结)
6
- INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 1);
4
+ -- 账号生命周期 D12(2026-10-22 调整):四个内置角色各一个开发账号,**账号名=角色名**
5
+ -- (superadmin/admin/editor/viewer,避免心智负担),口令均 123456,must_change=0 免首登改密——
6
+ -- 纯开发便利(默认凭据不随机生成的取舍见计划 A2 阶段小结),生产部署须改密或删除
7
+ INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('superadmin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 0);
8
+ INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('admin', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 0);
9
+ INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('editor', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 0);
10
+ INSERT OR IGNORE INTO users (username, password_hash, must_change) VALUES ('viewer', '$2y$10$ylE2NdjnYiuEcq54W/KxH.bhvGaQQg3rbmYI3mcEHX6LdQEAqixeC', 0);
11
+ -- 历史 seed 把 admin 绑在 superadmin 上;收敛为账号名=角色名(幂等 DELETE,每次重放生效)。
12
+ -- 已存在库中 admin 行可能残留 must_change=1,UPDATE 统一收敛为 0(四个均为 seed 自有开发账号)
13
+ DELETE FROM sys_user_role WHERE role_name = 'superadmin' AND user_id = (SELECT id FROM users WHERE username = 'admin');
14
+ UPDATE users SET must_change = 0 WHERE username IN ('superadmin', 'admin', 'editor', 'viewer');
7
15
  -- 框架内置角色(ROLES 单一真相:superadmin 安全敏感项 / admin 日常管理 / editor、viewer 页面门)
8
16
  INSERT OR IGNORE INTO sys_role (name, module, title, builtin) VALUES ('superadmin', 'framework', '超级管理员', 1);
9
17
  INSERT OR IGNORE INTO sys_role (name, module, title, builtin) VALUES ('admin', 'framework', '系统管理员', 1);
10
18
  INSERT OR IGNORE INTO sys_role (name, module, title, builtin) VALUES ('editor', 'framework', '编辑', 1);
11
19
  INSERT OR IGNORE INTO sys_role (name, module, title, builtin) VALUES ('viewer', 'framework', '访客', 1);
12
- -- seed 超管绑定(sys_user_role 唯一权威源,D10)
13
- INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = 'admin';
20
+ -- 角色绑定(sys_user_role 唯一权威源,D10):账号名=角色名一一对应
21
+ INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = 'superadmin';
22
+ INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'admin' FROM users WHERE username = 'admin';
23
+ INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'editor' FROM users WHERE username = 'editor';
24
+ INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'viewer' FROM users WHERE username = 'viewer';
14
25
  -- 系统设置默认值(PRD §4.3;C4 设置页 / C5 登录方式发现的数据源;framework.market_cdn_url 安全组仅 superadmin 可改)
26
+ -- framework.market_cdn_url 缺省 /cdn = 本站点自供(config.yaml 的 /cdn 挂载直出工程根 cdn/),脚手架免 setup;外部真 CDN 才改指绝对地址
15
27
  INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.site_title', 'framework', 'general', 'string', 'React Antd Admin', 'React Antd Admin', 'system:setting.site_title', 0, 0);
16
- INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '', '', 'system:setting.market_cdn_url', 0, 0);
28
+ INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('framework.market_cdn_url', 'framework', 'security', 'string', '/cdn', '/cdn', 'system:setting.market_cdn_url', 0, 0);
17
29
  INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap.enabled', 'ldap', 'login', 'bool', '0', '0', 'system:setting.ldap_enabled', 0, 0);
18
30
  INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('sso.enabled', 'sso', 'login', 'bool', '0', '0', 'system:setting.sso_enabled', 0, 0);
19
31
  INSERT OR IGNORE INTO sys_setting (key, module, "group", type, value, default_value, label_key, secret, update_time) VALUES ('ldap_base_dn', 'ldap', 'ldap', 'string', '', '', 'system:setting.ldap_base_dn', 0, 0);
@@ -37,9 +37,9 @@ export type { SolutionRouteRow } from "../_lifecycle/verify-checks";
37
37
  */
38
38
  export function solutionsEnv(): SolutionsEnv {
39
39
  return {
40
- sharedRoot: "api/dist",
40
+ sharedRoot: "dist/api",
41
41
  statePath: "modules/.solutions/state.yaml",
42
- modulesJsonPath: "web/dist/modules.json",
42
+ modulesJsonPath: "dist/web/modules.json",
43
43
  apiPrefix: "/api",
44
44
  };
45
45
  }
@@ -1,5 +1,5 @@
1
1
  import type { MarketClient } from "./client";
2
- import { createContext, useContext } from "react";
2
+ import { createContext, use } from "react";
3
3
  import { httpMarketClient } from "./defaultClient";
4
4
 
5
5
  /** 市场客户端上下文(依赖注入位;默认 = 运行时 HTTP 实现,测试可替换为内存 fake) */
@@ -7,5 +7,5 @@ export const MarketClientContext = createContext<MarketClient>(httpMarketClient)
7
7
 
8
8
  /** 组件/hooks 取客户端(默认注入运行时实现) */
9
9
  export function useMarketClient(): MarketClient {
10
- return useContext(MarketClientContext);
10
+ return use(MarketClientContext);
11
11
  }
@@ -44,7 +44,7 @@ export function InstallDialog({ row, open, onClose, onDone, version, mode }: Ins
44
44
  // onDone 对同一任务只通告一次:父级每次渲染都会换 onDone 身份,若只靠 deps 触发,
45
45
  // 终态后 onDone → index.retry() → 父级重渲染 → onDone 身份变 → 再触发,会形成
46
46
  // 无限请求循环(浏览器侧表现为 ERR_INSUFFICIENT_RESOURCES)。
47
- const notifiedTaskId = useRef<string | null>(null);
47
+ const notifiedTaskIdRef = useRef<string | null>(null);
48
48
 
49
49
  const targetVersion = version ?? row?.latest ?? null;
50
50
  const title
@@ -54,9 +54,13 @@ export function InstallDialog({ row, open, onClose, onDone, version, mode }: Ins
54
54
  ? t("oj-market:card:upgrade")
55
55
  : t("oj-market:task:installing");
56
56
 
57
- // 打开即触发安装(version 缺省取 latest;已装视为升级;显式 version = 回滚降级安装)
57
+ // 打开即触发安装(version 缺省取 latest;已装视为升级;显式 version = 回滚降级安装)。
58
+ // 注:下面的同步 set 均为「弹窗打开/关闭生命周期」的有意重置与触发守卫;
59
+ // 曾尝试渲染期派生重置,但同组件 notifier effect 里的 onDone?.(record) 会触发
60
+ // eslint-react set-state-in-effect 的误判(prop 回调被解析回组件函数),故保留原结构。
58
61
  useEffect(() => {
59
62
  if (open && row && !started) {
63
+ // eslint-disable-next-line react/set-state-in-effect -- 有意:打开弹窗时置「已触发」守卫,保证安装只发起一次
60
64
  setStarted(true);
61
65
  client
62
66
  .install({ name: row.name, version: targetVersion, source: "cdn" })
@@ -64,19 +68,22 @@ export function InstallDialog({ row, open, onClose, onDone, version, mode }: Ins
64
68
  .catch(e => setTaskId(`__error__${e instanceof Error ? e.message : String(e)}`));
65
69
  }
66
70
  if (!open) {
71
+ // eslint-disable-next-line react/set-state-in-effect -- 有意:关闭弹窗时清空任务,下次打开是全新一次安装
67
72
  setTaskId(null);
73
+ // eslint-disable-next-line react/set-state-in-effect -- 有意:关闭时复位触发守卫,配合上行清场
68
74
  setStarted(false);
75
+ // eslint-disable-next-line react/set-state-in-effect -- 有意:关闭时清掉信任错误,避免残留到下次打开
69
76
  setTrustError(null);
70
- notifiedTaskId.current = null;
77
+ notifiedTaskIdRef.current = null;
71
78
  }
72
79
  }, [open, row, started, client, targetVersion]);
73
80
 
74
81
  useEffect(() => {
75
82
  if (!record || !isTerminal(record) || !done)
76
83
  return;
77
- if (notifiedTaskId.current === record.id)
84
+ if (notifiedTaskIdRef.current === record.id)
78
85
  return;
79
- notifiedTaskId.current = record.id;
86
+ notifiedTaskIdRef.current = record.id;
80
87
  onDone?.(record);
81
88
  }, [record, done, onDone]);
82
89
 
@@ -88,7 +95,7 @@ export function InstallDialog({ row, open, onClose, onDone, version, mode }: Ins
88
95
  setTrustError(null);
89
96
  try {
90
97
  await client.trust(taskId);
91
- notifiedTaskId.current = null;
98
+ notifiedTaskIdRef.current = null;
92
99
  setTaskId(null);
93
100
  setStarted(false);
94
101
  }
@@ -1,6 +1,6 @@
1
1
  import type { IndexRow } from "../types";
2
2
  import { Alert, Modal, Radio, Space, Spin, Typography } from "antd";
3
- import React, { useEffect, useMemo, useState } from "react";
3
+ import React, { useMemo, useState } from "react";
4
4
  import { useTranslation } from "react-i18next";
5
5
  import { useMarketHistory } from "../hooks/useMarket";
6
6
  import { compareVersions } from "../utils/semver";
@@ -34,9 +34,13 @@ export function RollbackDialog({ row, open, onClose, onRollback }: RollbackDialo
34
34
  return [...versions].sort((a, b) => compareVersions(b, a)); // 较新的旧版在前
35
35
  }, [row, status, data]);
36
36
 
37
- useEffect(() => {
37
+ // 候选集变化(历史加载完成/切换方案)时默认选中最新旧版;
38
+ // 渲染期同步派生重置,替代 effect 内同步 set(react/set-state-in-effect)
39
+ const [prevCandidates, setPrevCandidates] = useState(candidates);
40
+ if (prevCandidates !== candidates) {
41
+ setPrevCandidates(candidates);
38
42
  setSelected(candidates.length ? candidates[0] : null);
39
- }, [candidates]);
43
+ }
40
44
 
41
45
  const loading = status === "loading";
42
46
 
@@ -19,22 +19,33 @@ export function UninstallConfirm({ row, open, onClose, onConfirmed }: UninstallC
19
19
  const { t } = useTranslation();
20
20
  const client = useMarketClient();
21
21
  const [impact, setImpact] = useState<UninstallImpact | null>(null);
22
- const [loading, setLoading] = useState(false);
23
22
  const [error, setError] = useState<string | null>(null);
24
23
 
24
+ // 打开(或切换方案)即清场上一轮结果;渲染期同步派生重置,替代 effect 内同步 set
25
+ const rowKey = open && row ? row.name : null;
26
+ const [prevRowKey, setPrevRowKey] = useState(rowKey);
27
+ if (prevRowKey !== rowKey) {
28
+ setPrevRowKey(rowKey);
29
+ setImpact(null);
30
+ setError(null);
31
+ }
32
+
25
33
  useEffect(() => {
26
34
  if (!open || !row)
27
35
  return;
28
- setLoading(true);
29
- setError(null);
30
- setImpact(null);
36
+ let active = true;
31
37
  client
32
38
  .uninstallImpact(row.name)
33
- .then(setImpact)
34
- .catch(e => setError(e instanceof Error ? e.message : String(e)))
35
- .finally(() => setLoading(false));
39
+ .then(i => active && setImpact(i))
40
+ .catch(e => active && setError(e instanceof Error ? e.message : String(e)));
41
+ return () => {
42
+ active = false;
43
+ };
36
44
  }, [open, row, client]);
37
45
 
46
+ // 加载态派生:已打开且尚无结果/错误即视为预检中(替代原 effect 同步 setLoading)
47
+ const loading = rowKey !== null && impact === null && error === null;
48
+
38
49
  return (
39
50
  <Modal
40
51
  title={t("oj-market:confirm:uninstallTitle")}
@@ -12,10 +12,12 @@ export type LoadState<T> = { status: "loading", data?: undefined } | { status: "
12
12
  export function useMarketIndex(): LoadState<IndexRow[]> & { retry: () => void } {
13
13
  const client = useMarketClient();
14
14
  const [state, setState] = useState<LoadState<IndexRow[]>>({ status: "loading" });
15
+ // 重试不复跑「先同步 set loading 再拉取」的加载函数(effect 内同步 set 触发
16
+ // react/set-state-in-effect),改为事件里置 loading + 递增 nonce 驱动 effect 重跑。
17
+ const [nonce, setNonce] = useState(0);
15
18
 
16
- const load = useCallback(() => {
19
+ useEffect(() => {
17
20
  let active = true;
18
- setState({ status: "loading" });
19
21
  client
20
22
  .index()
21
23
  .then(data => active && setState({ status: "ok", data }))
@@ -23,11 +25,14 @@ export function useMarketIndex(): LoadState<IndexRow[]> & { retry: () => void }
23
25
  return () => {
24
26
  active = false;
25
27
  };
26
- }, [client]);
28
+ }, [client, nonce]);
27
29
 
28
- useEffect(() => load(), [load]);
30
+ const retry = useCallback(() => {
31
+ setState({ status: "loading" });
32
+ setNonce(n => n + 1);
33
+ }, []);
29
34
 
30
- return { ...state, retry: load };
35
+ return { ...state, retry };
31
36
  }
32
37
 
33
38
  /** 任务轮询:给定 task_id,800ms 轮询至终态(done/failed/partial);超时不抛,返回当前值 */
@@ -39,17 +44,20 @@ export function useInstallTask(taskId: string | null): {
39
44
  const [record, setRecord] = useState<TaskRecord | null>(null);
40
45
  const [done, setDone] = useState(false);
41
46
 
47
+ // 无任务 id / 切换任务 = 尚无任务:清空上一次任务的记录,避免「重新安装/信任后重试」时
48
+ // 旧终态(可能是 needs_trust)残留在弹窗里被误读成本次结果。
49
+ // (渲染期同步派生重置,替代 effect 内同步 set,规避 react/set-state-in-effect)
50
+ const [prevTaskId, setPrevTaskId] = useState(taskId);
51
+ if (prevTaskId !== taskId) {
52
+ setPrevTaskId(taskId);
53
+ setRecord(null);
54
+ setDone(false);
55
+ }
56
+
42
57
  useEffect(() => {
43
- // 无任务 id = 尚无任务:清空上一次任务的记录,避免「重新安装/信任后重试」时
44
- // 旧终态(可能是 needs_trust)残留在弹窗里被误读成本次结果。
45
- if (!taskId) {
46
- setRecord(null);
47
- setDone(false);
58
+ if (!taskId)
48
59
  return;
49
- }
50
60
  let active = true;
51
- setRecord(null);
52
- setDone(false);
53
61
  poll(
54
62
  () => client.task(taskId),
55
63
  r => r.status !== "running",
@@ -80,10 +88,11 @@ export function useInstallTask(taskId: string | null): {
80
88
  export function useMarketHistory(): LoadState<MarketHistory[]> & { retry: () => void } {
81
89
  const client = useMarketClient();
82
90
  const [state, setState] = useState<LoadState<MarketHistory[]>>({ status: "loading" });
91
+ // 同 useMarketIndex:重试走 nonce 驱动,不在 effect 链路里同步 set
92
+ const [nonce, setNonce] = useState(0);
83
93
 
84
- const load = useCallback(() => {
94
+ useEffect(() => {
85
95
  let active = true;
86
- setState({ status: "loading" });
87
96
  client
88
97
  .history()
89
98
  .then(data => active && setState({ status: "ok", data }))
@@ -91,9 +100,12 @@ export function useMarketHistory(): LoadState<MarketHistory[]> & { retry: () =>
91
100
  return () => {
92
101
  active = false;
93
102
  };
94
- }, [client]);
103
+ }, [client, nonce]);
95
104
 
96
- useEffect(() => load(), [load]);
105
+ const retry = useCallback(() => {
106
+ setState({ status: "loading" });
107
+ setNonce(n => n + 1);
108
+ }, []);
97
109
 
98
- return { ...state, retry: load };
110
+ return { ...state, retry };
99
111
  }
@@ -30,7 +30,7 @@
30
30
  "displayName": "Display Name",
31
31
  "email": "Email",
32
32
  "roles": "Roles",
33
- "status": "Status",
33
+ "status": "Account status (forbidden / password change)",
34
34
  "actions": "Actions"
35
35
  }
36
36
  },
@@ -30,7 +30,7 @@
30
30
  "displayName": "显示名",
31
31
  "email": "邮箱",
32
32
  "roles": "角色",
33
- "status": "状态",
33
+ "status": "账号状态(禁用/首登改密)",
34
34
  "actions": "操作"
35
35
  }
36
36
  },
@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
7
7
  import { useNavigate, useParams } from "react-router";
8
8
 
9
9
  import { systemRolesGet, systemUsersGet, systemUsersRolesPut } from "../../__client/api";
10
- import { isForbidden } from "../errors";
10
+ import { explainError, isForbidden } from "../errors";
11
11
  import { ForbiddenPanel } from "../forbidden";
12
12
 
13
13
  /**
@@ -36,6 +36,8 @@ export default function UserRolesPage() {
36
36
  queryClient.invalidateQueries({ queryKey: ["system", "users"] });
37
37
  navigate("/system/users");
38
38
  },
39
+ // 403 全局 toast 已被请求层抑制,页面须自行反馈(admin 非 superadmin 手输 URL 的场景)
40
+ onError: e => message.error(explainError(e, t)),
39
41
  });
40
42
 
41
43
  const user = usersQuery.data?.find(u => String(u.id) === uid);
@@ -11,8 +11,8 @@
11
11
  // ③ notification:join ON 值条件 + UNION ALL + 字面量列 {lit,as} 的真渲染(场景 41 同款)。
12
12
  //
13
13
  // 登录账号:fixtures 的 l1tester(src/_platform/fixtures/l1-test-user.sql,仅 oj test 灌入)。
14
- // 不用 seed 的 admin:其 must_change=1(判权端点恒 403)且初始口令 123456 会随用户
15
- // 首登改密漂移——L1 绑 admin 曾实测 401。l1tester 为 additive 专用账号(must_change=0)。
14
+ // 不用 seed 的 admin 等开发账号:初始口令 123456 会随用户在 dev 库改密漂移——
15
+ // L1 绑 seed 账号曾实测 401。l1tester 为 additive 专用账号(must_change=0)。
16
16
  //
17
17
  // 运行:cd packages/cli/templates/api && ~/.local/share/oj/oj test -c config.yaml -d src -t tests
18
18