@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.
- package/devkit/manual.md +100 -12
- package/devkit/module-dev-and-cdn-playground.md +10 -10
- package/package.json +4 -4
- package/shell-dist/assets/runtime.js +61 -35
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +13 -13
- package/src/__tests__/cli.test.ts +225 -0
- package/src/__tests__/init-packagejson.test.ts +29 -0
- package/src/__tests__/layout.test.ts +8 -5
- package/src/__tests__/new-scaffold.test.ts +59 -0
- package/src/__tests__/usage.test.ts +45 -47
- package/src/build.ts +4 -4
- package/src/builtin.ts +93 -0
- package/src/bundle.ts +237 -33
- package/src/cli.ts +265 -0
- package/src/contract/__tests__/market-client.integration.test.ts +12 -12
- package/src/contract/check.ts +4 -4
- package/src/dev.ts +75 -2
- package/src/index.ts +31 -191
- package/src/info.ts +49 -7
- package/src/init.ts +25 -11
- package/src/keygen.ts +0 -19
- package/src/layout.ts +11 -11
- package/src/lifecycle/__tests__/authoring.test.ts +3 -14
- package/src/lifecycle/__tests__/builtin.test.ts +97 -0
- package/src/lifecycle/__tests__/bundle.test.ts +188 -25
- package/src/lifecycle/__tests__/install.test.ts +0 -23
- package/src/lifecycle/__tests__/new.test.ts +129 -0
- package/src/lifecycle/__tests__/smoke.test.ts +4 -3
- package/src/manifest-contract.ts +16 -3
- package/src/market/__tests__/bootstrap.test.ts +24 -0
- package/src/market/__tests__/setup.test.ts +8 -1
- package/src/market/bootstrap.ts +10 -8
- package/src/market/cdn-layout.ts +3 -3
- package/src/market/command.ts +0 -23
- package/src/market/publish.ts +10 -2
- package/src/market/setup.ts +14 -2
- package/src/market/trial.ts +3 -3
- package/src/market/walkthrough.ts +7 -6
- package/src/new.ts +226 -0
- package/src/preview-config.ts +2 -2
- package/src/preview.ts +5 -4
- package/src/publish.ts +0 -22
- package/src/sign.ts +0 -22
- package/src/usage.ts +482 -198
- package/templates/README.md +3 -3
- package/templates/cdn/README.md +6 -6
- package/templates/config.yaml +5 -4
- package/templates/init.include.yaml +1 -1
- package/templates/package.json +3 -1
- package/templates/seed.mysql.sql +2 -1
- package/templates/seed.pg.sql +2 -1
- package/templates/src/_platform/api/fixtures/l1-test-user.sql +4 -5
- package/templates/src/_platform/api/market/api.ts +11 -8
- package/templates/src/_platform/api/seed.mysql.sql +11 -3
- package/templates/src/_platform/api/seed.pg.sql +11 -3
- package/templates/src/_platform/api/seed.sql +18 -6
- package/templates/src/_platform/api/solutions/api.ts +2 -2
- package/templates/src/oj-market/web/api/context.ts +2 -2
- package/templates/src/oj-market/web/components/InstallDialog.tsx +13 -6
- package/templates/src/oj-market/web/components/RollbackDialog.tsx +7 -3
- package/templates/src/oj-market/web/components/UninstallConfirm.tsx +18 -7
- package/templates/src/oj-market/web/hooks/useMarket.ts +30 -18
- package/templates/src/system/web/locales/en-US.json +1 -1
- package/templates/src/system/web/locales/zh-CN.json +1 -1
- package/templates/src/system/web/pages/users/roles.tsx +3 -1
- package/templates/tests/l1-builder.test.ts +2 -2
- package/templates/tests/lifecycle/market-api.spec.ts +35 -5
- package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
- package/templates/tests/lifecycle/verify-report.spec.ts +1 -1
- package/templates/tests/rbac-seed.spec.ts +12 -10
- package/templates/tests/seed-role-accounts.spec.ts +87 -0
- package/vendor/host-versions.json +3 -3
- package/src/args.ts +0 -122
package/templates/README.md
CHANGED
|
@@ -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
|
|
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/
|
|
38
|
+
dist/api/ oj build 产物
|
|
39
39
|
web/src/ 前端模块源码
|
|
40
|
-
web/
|
|
40
|
+
dist/web/ 构建产物(完整站点)
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## 注意
|
package/templates/cdn/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# cdn/ —— 本地模拟市场源
|
|
2
2
|
|
|
3
|
-
本目录是**本地模拟 CDN**:`ojm market publish`
|
|
4
|
-
(`- prefix: /cdn` → `web:
|
|
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.
|
|
26
|
+
1. 删掉工程根 `config.yaml` 的 `mounts` 里这条(它就是本地挂载的全部):
|
|
27
27
|
|
|
28
28
|
```yaml
|
|
29
29
|
- prefix: /cdn
|
|
30
|
-
web:
|
|
30
|
+
web: ./cdn
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
2. 把真 CDN(对象存储 / nginx
|
|
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
|
|
package/templates/config.yaml
CHANGED
|
@@ -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
|
|
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
|
-
#
|
|
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
|
|
44
|
+
# 取消下一行注释并先 build 出 dist/web(preview 会自动注入等价挂载,无需手改):
|
|
44
45
|
# - prefix: "/"
|
|
45
|
-
# web: "./web
|
|
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/
|
|
11
|
+
# 3. 构建与运行期产物——dist/api、dist/web、logs、签发的私钥与 sqlite;
|
|
12
12
|
# 模板跑过 build/dev 后会留在本地,既不进用户工程,更不该让所有工程
|
|
13
13
|
# 共享同一把私钥。
|
|
14
14
|
# 4. 模板自用元数据——本清单自身。
|
package/templates/package.json
CHANGED
|
@@ -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:",
|
package/templates/seed.mysql.sql
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/templates/seed.pg.sql
CHANGED
|
@@ -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
|
-
|
|
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
|
|
4
|
-
-- ① seed
|
|
5
|
-
--
|
|
6
|
-
--
|
|
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
|
|
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}`
|
|
136
|
-
*
|
|
137
|
-
*
|
|
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
|
|
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/
|
|
149
|
-
modulesJsonPath: "web/
|
|
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
|
-
|
|
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 ('
|
|
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 = '
|
|
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 ('
|
|
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 = '
|
|
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
|
|
5
|
-
--
|
|
6
|
-
|
|
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
|
-
--
|
|
13
|
-
INSERT OR IGNORE INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = '
|
|
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
|
|
40
|
+
sharedRoot: "dist/api",
|
|
41
41
|
statePath: "modules/.solutions/state.yaml",
|
|
42
|
-
modulesJsonPath: "web/
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
84
|
+
if (notifiedTaskIdRef.current === record.id)
|
|
78
85
|
return;
|
|
79
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
+
const retry = useCallback(() => {
|
|
31
|
+
setState({ status: "loading" });
|
|
32
|
+
setNonce(n => n + 1);
|
|
33
|
+
}, []);
|
|
29
34
|
|
|
30
|
-
return { ...state, retry
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
+
const retry = useCallback(() => {
|
|
106
|
+
setState({ status: "loading" });
|
|
107
|
+
setNonce(n => n + 1);
|
|
108
|
+
}, []);
|
|
97
109
|
|
|
98
|
-
return { ...state, retry
|
|
110
|
+
return { ...state, retry };
|
|
99
111
|
}
|
|
@@ -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
|
|
15
|
-
//
|
|
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
|
|