zero-ai 1.0.75 → 1.0.79
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/package.json +1 -1
- package/r2mo-init/.obsidian/app.json +1 -0
- package/r2mo-init/.obsidian/appearance.json +10 -0
- package/r2mo-init/.obsidian/community-plugins.json +7 -0
- package/r2mo-init/.obsidian/core-plugins.json +33 -0
- package/r2mo-init/.obsidian/plugins/dataview/main.js +20876 -0
- package/r2mo-init/.obsidian/plugins/dataview/manifest.json +11 -0
- package/r2mo-init/.obsidian/plugins/dataview/styles.css +141 -0
- package/r2mo-init/.obsidian/plugins/obsidian-excalidraw-plugin/data.json +815 -0
- package/r2mo-init/.obsidian/plugins/obsidian-excalidraw-plugin/main.js +10 -0
- package/r2mo-init/.obsidian/plugins/obsidian-excalidraw-plugin/manifest.json +12 -0
- package/r2mo-init/.obsidian/plugins/obsidian-excalidraw-plugin/styles.css +1 -0
- package/r2mo-init/.obsidian/plugins/obsidian-kanban/main.js +153 -0
- package/r2mo-init/.obsidian/plugins/obsidian-kanban/manifest.json +11 -0
- package/r2mo-init/.obsidian/plugins/obsidian-kanban/styles.css +1 -0
- package/r2mo-init/.obsidian/plugins/obsidian-plantuml/main.js +7732 -0
- package/r2mo-init/.obsidian/plugins/obsidian-plantuml/manifest.json +10 -0
- package/r2mo-init/.obsidian/plugins/obsidian-plantuml/styles.css +38 -0
- package/r2mo-init/.obsidian/plugins/obsidian-tasks-plugin/main.js +504 -0
- package/r2mo-init/.obsidian/plugins/obsidian-tasks-plugin/manifest.json +12 -0
- package/r2mo-init/.obsidian/plugins/obsidian-tasks-plugin/styles.css +1 -0
- package/r2mo-init/.obsidian/snippets/body-font.css +9 -0
- package/r2mo-init/.obsidian/themes/Comfort/manifest.json +11 -0
- package/r2mo-init/.obsidian/themes/Comfort/theme.css +218 -0
- package/r2mo-init/.obsidian/themes/Primary/manifest.json +9 -0
- package/r2mo-init/.obsidian/themes/Primary/theme.css +3878 -0
- package/r2mo-init/.obsidian/themes/Retro Windows/manifest.json +7 -0
- package/r2mo-init/.obsidian/themes/Retro Windows/theme.css +582 -0
- package/r2mo-init/.obsidian/themes/RetroOS 98/manifest.json +9 -0
- package/r2mo-init/.obsidian/themes/RetroOS 98/theme.css +2566 -0
- package/r2mo-init/.obsidian/themes/Serenity/manifest.json +7 -0
- package/r2mo-init/.obsidian/themes/Serenity/theme.css +7258 -0
- package/r2mo-init/.obsidian/themes/W95/manifest.json +8 -0
- package/r2mo-init/.obsidian/themes/W95/theme.css +768 -0
- package/r2mo-init/.obsidian/types.json +28 -0
- package/r2mo-init/task/command/ex-api.yaml.example +13 -0
- package/r2mo-init/task/task-001.md +144 -0
- package/r2mo-init/task/task-002.md +4 -0
- package/r2mo-init/task/task-003.md +4 -0
- package/r2mo-init/task/thread +1 -0
- package/src/commander/ex-api.json +1 -2
- package/src/commander/ex-crud.json +1 -1
- package/src/commander-ai/fn.ex.api.js +192 -103
- package/src/commander-ai/fn.ex.crud.js +102 -58
- package/.cursor/rules/test.mdc +0 -4
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"types": {
|
|
3
|
+
"aliases": "aliases",
|
|
4
|
+
"cssclasses": "multitext",
|
|
5
|
+
"tags": "tags",
|
|
6
|
+
"TQ_explain": "checkbox",
|
|
7
|
+
"TQ_extra_instructions": "text",
|
|
8
|
+
"TQ_short_mode": "checkbox",
|
|
9
|
+
"TQ_show_backlink": "checkbox",
|
|
10
|
+
"TQ_show_cancelled_date": "checkbox",
|
|
11
|
+
"TQ_show_created_date": "checkbox",
|
|
12
|
+
"TQ_show_depends_on": "checkbox",
|
|
13
|
+
"TQ_show_done_date": "checkbox",
|
|
14
|
+
"TQ_show_due_date": "checkbox",
|
|
15
|
+
"TQ_show_edit_button": "checkbox",
|
|
16
|
+
"TQ_show_id": "checkbox",
|
|
17
|
+
"TQ_show_on_completion": "checkbox",
|
|
18
|
+
"TQ_show_postpone_button": "checkbox",
|
|
19
|
+
"TQ_show_priority": "checkbox",
|
|
20
|
+
"TQ_show_recurrence_rule": "checkbox",
|
|
21
|
+
"TQ_show_scheduled_date": "checkbox",
|
|
22
|
+
"TQ_show_start_date": "checkbox",
|
|
23
|
+
"TQ_show_tags": "checkbox",
|
|
24
|
+
"TQ_show_task_count": "checkbox",
|
|
25
|
+
"TQ_show_tree": "checkbox",
|
|
26
|
+
"TQ_show_urgency": "checkbox"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 复制为 ex-api.yaml 后按项目修改
|
|
2
|
+
# ai ex-api 使用 .r2mo/task/command/ex-api.yaml
|
|
3
|
+
metadata:
|
|
4
|
+
identifier: "核心标识符"
|
|
5
|
+
brief: "接口描述"
|
|
6
|
+
resource: "resource.ambient"
|
|
7
|
+
level: 1
|
|
8
|
+
ptype: "权限集 S_PERM_SET 类型"
|
|
9
|
+
pname: "权限集 S_PERM_SET 名称"
|
|
10
|
+
# target 可选;存在时需配置 ZERO_MODULE 且 DPA 目录 zero-exmodule-{module} 存在
|
|
11
|
+
# target:
|
|
12
|
+
# root: "ZERO_MODULE"
|
|
13
|
+
# module: "ambient"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
runAt: 2026-02-12.22-47-18
|
|
3
|
+
title: 开发 ai ex-api 命令
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 任务
|
|
7
|
+
|
|
8
|
+
开发 `ai ex-api` 命令对已经开发好的 API 进行授权,此命令为固定命令,要完成全套执行操作,系统重启后要保证权限生效,且命令必须拥有幂等性。
|
|
9
|
+
|
|
10
|
+
### 前置条件
|
|
11
|
+
|
|
12
|
+
1. 当前项目中 `.r2mo/task/command/ex-api.yaml` 配置文件必须存在,不存在则直接退出,并且提示用户配置文件缺失。
|
|
13
|
+
2. 环境变量必须,检查这些环境变量必须存在,不存在则跳出
|
|
14
|
+
```bash
|
|
15
|
+
export Z_DB_TYPE="MYSQL" # 数据库类型: MYSQL / SQLSERVER / ORACLE / POSTGRESQL
|
|
16
|
+
export Z_DB_HOST="127.0.0.1"
|
|
17
|
+
export Z_DB_PORT="3306"
|
|
18
|
+
export Z_DBS_INSTANCE=""
|
|
19
|
+
export Z_DB_APP_USER="r2admin"
|
|
20
|
+
export Z_DB_APP_PASS="r2admin"
|
|
21
|
+
```
|
|
22
|
+
3. 环境变量检查(这个步骤必须)
|
|
23
|
+
```bash
|
|
24
|
+
export Z_APP_ID=???
|
|
25
|
+
export Z_TENANT=???
|
|
26
|
+
export Z_SIGMA=???
|
|
27
|
+
```
|
|
28
|
+
4. `ex-api.yaml` 的基本格式
|
|
29
|
+
```yaml
|
|
30
|
+
metadata:
|
|
31
|
+
identifier: "核心标识符"
|
|
32
|
+
brief: "接口描述"
|
|
33
|
+
resource: "resource.ambient"
|
|
34
|
+
level: ???
|
|
35
|
+
ptype: "权限集 S_PERM_SET 类型"
|
|
36
|
+
pname: "权限集 S_PERM_SET 名称"
|
|
37
|
+
keyword: "app.test.data"
|
|
38
|
+
target:
|
|
39
|
+
root: "ZERO_MODULE"
|
|
40
|
+
module: "ambient"
|
|
41
|
+
```
|
|
42
|
+
5. 存在 target 配置多检查一个环境变量 ZERO_MODULE,并且检查`{ZERO_MODULE}/zero-exmodule-{module}` 是否一个标准的DPA架构,若不是则命令退出
|
|
43
|
+
|
|
44
|
+
### 执行逻辑
|
|
45
|
+
|
|
46
|
+
1. 检查前置条件是否满足,不检查 `ex-api.yaml` 的格式问题
|
|
47
|
+
2. 参数检查:
|
|
48
|
+
- `-r "<METHOD> <uri>"` ,其中 `r` 的全称是 `request`
|
|
49
|
+
- `-s`,其中 `s` 的全称是 `skip`,若如此则只生成 Excel,可能最终不完美(没做去重检查),但交给开发人员处理
|
|
50
|
+
3. 定义表信息
|
|
51
|
+
- `S_RESOURCE`:资源定义
|
|
52
|
+
- `S_ACTION`:操作定义
|
|
53
|
+
- `S_PERMISSION`:权限定义
|
|
54
|
+
- `S_PERM_SET`:权限集定义
|
|
55
|
+
- `R_ROLE_PERM`:角色权限定义
|
|
56
|
+
4. 先检查 `S_ACTION` 中是否已经存在当前参数,按列名开发(`METHOD, URI`;不唯一时追加 `SIGMA, APP_ID, TENANT_ID`),只有当 `METHOD, URI` 无法提取唯一记录时追加后续三个查询条件
|
|
57
|
+
- 若存在不创建数据记录,提取 `S_ACTION` 中的 ID
|
|
58
|
+
- 若不存在则创建数据记录 / `S_RESOURCE` 需要同步创建(因为 `S_RESOURCE` 资源是新的)
|
|
59
|
+
5. 询问用户:追加新权限 / 执行已有权限,若执行已有权限,按 `S_PERMISSION` 中的 identifier 进行查询让用户选择追加到那个权限(追加到已有权限则直接跳过额外的数据库操作)
|
|
60
|
+
6. 提取数据库中已有的 `S_ROLE` 角色信息,可多选,让用户选择当前 API 授权给哪些角色
|
|
61
|
+
|
|
62
|
+
## 输出说明
|
|
63
|
+
|
|
64
|
+
### 数据库输出
|
|
65
|
+
|
|
66
|
+
- 同步五张表的信息,注意要幂等性,不引起异常
|
|
67
|
+
|
|
68
|
+
### Excel输出
|
|
69
|
+
|
|
70
|
+
1. 如果存在 `target` 的配置,则输出项目根目录应该是 `${ZERO_MODULE}/zero-exmodule-{module}`,它必须是 DPA 架构,生成的 `excel` 的信息应该位于 `${ZERO_MODULE}/zero-exmodule-{module}/zero-exmodule-{module}-domain` 的资源目录下,开发时查看一下,有一个 `plugins/***/security/RBAC_RESOURCE` 目录。
|
|
71
|
+
2. Excel 直接覆盖,不提示选择文件;文件名固化为 `identifier-method-uri.xlsx`(uri 中 `/` 转 `-`);无 target 时输出到 **-api** 项目下 `plugins/.../security/`。
|
|
72
|
+
3. `plugins/***/security/RBAC_ROLE/ADMIN.SUPER/`(固定)写入带 **falcon** 前缀的同名文件(`falcon-identifier-method-uri.xlsx`);若没有 `target` 则写入当前/ -api 项目下的 `plugins/zero-launcher-configuration/security/` 目录下。
|
|
73
|
+
4. `plugins` 都是从 maven 项目的 src/main/resources 开始计算
|
|
74
|
+
5. 在目标路径下生成对应的 `*.xlsx` 或更改现有的内容
|
|
75
|
+
|
|
76
|
+
### Excel 样式约定
|
|
77
|
+
|
|
78
|
+
- **默认模板**:模板位于 R2MO-INIT 项目固定路径 `src/_template/EXCEL/ex-api`,与执行命令所在项目无关;任意项目执行 ai ex-api 均使用该默认模板驱动。
|
|
79
|
+
- **仅填数据,样式与模板完全一致**:除写入的数据单元格 value 外,不修改任何 style(格式、颜色、边框、合并单元格、行数等)。不删行、不清格式、不填色、不设边框,输出与模板在视觉和结构上一致。
|
|
80
|
+
|
|
81
|
+
### 属性规则
|
|
82
|
+
|
|
83
|
+
- `S_RESOURCE`
|
|
84
|
+
- 资源名称,从 `ex-api.yaml` 中提取
|
|
85
|
+
- `modelRole` -> UNION 固定值
|
|
86
|
+
- 资源编码 `res` 前缀加 `S_ACTION` 的 code
|
|
87
|
+
- `identifier / type / level` 这些配置文件中有
|
|
88
|
+
- `S_ACTION`
|
|
89
|
+
- 操作编码,读取当前 `identifier` 下的核心操作编码追加 `act` 前缀
|
|
90
|
+
- 操作名称同 `brief`(和资源一样)
|
|
91
|
+
- 操作级别同 `S_RESOURCE`
|
|
92
|
+
- `S_PERMISSION`
|
|
93
|
+
- 权限名称同 `brief`(备注也想通)
|
|
94
|
+
- 权限码 `perm` 前缀
|
|
95
|
+
- 所属模型 `identifier`
|
|
96
|
+
- `S_PERM_SET`:参考前边规则提取,配置文件中有
|
|
97
|
+
- 如果出现 `keyword`,则所有编码名称直接追加,不计算,比如
|
|
98
|
+
- `res.${keyword}`
|
|
99
|
+
- `act.${keyword}`
|
|
100
|
+
- `perm.${keyword}`
|
|
101
|
+
|
|
102
|
+
### 核心输出
|
|
103
|
+
|
|
104
|
+
- 数据库中建立关系
|
|
105
|
+
- Excel中建立管理
|
|
106
|
+
- Excel中的UUID值固化
|
|
107
|
+
|
|
108
|
+
### 全局列补充(仅数据库,不写入 Excel)
|
|
109
|
+
|
|
110
|
+
仅**实体表**写入全局列:S_RESOURCE、S_ACTION、S_PERMISSION。若表中包含以下列则一并写入;Excel 中不输出这些列。关系表 R_ROLE_PERM 只有 ROLE_ID、**PERM_ID** 两列,无全局列。
|
|
111
|
+
|
|
112
|
+
| 列名 | 取值来源 |
|
|
113
|
+
|------|----------|
|
|
114
|
+
| `sigma` | 环境变量 `Z_SIGMA` |
|
|
115
|
+
| `appId` | 环境变量 `Z_APP_ID` |
|
|
116
|
+
| `tenantId` | 环境变量 `Z_TENANT` |
|
|
117
|
+
| `scope` | 环境变量 `Z_APP` |
|
|
118
|
+
| `createdBy` | 固定值 `9a0d5018-33ad-4c64-80bf-8ae7947c482f`(R2_BY) |
|
|
119
|
+
| `updatedBy` | 固定值 `9a0d5018-33ad-4c64-80bf-8ae7947c482f`(R2_BY) |
|
|
120
|
+
| `createdAt` | 当前时间(R2_NOW) |
|
|
121
|
+
| `updatedAt` | 当前时间(R2_NOW) |
|
|
122
|
+
|
|
123
|
+
- 全局列在开发时按建表固定写好,执行时不查元数据;Excel 输出中不包含上述全局列,仅做数据库同步。
|
|
124
|
+
|
|
125
|
+
### 表列信息(开发时对齐,执行时不查元数据)
|
|
126
|
+
|
|
127
|
+
**所有 SQL 与 Excel 表头列名在开发时按 RBAC 建表(如 zero-exmodule-rbac Flyway)固定写好;ai ex-api 执行时只执行 DML,不访问数据库元数据(不执行 SHOW COLUMNS 等)。**
|
|
128
|
+
|
|
129
|
+
- **R_ROLE_PERM**:关系表,仅两列 **ROLE_ID**、**PERM_ID**(无 `PERMISSION_ID`)。
|
|
130
|
+
- S_RESOURCE、S_ACTION、S_PERMISSION 实体表列名与 Flyway 一致:S_ACTION 用 **SIGMA/APP_ID/TENANT_ID**(非 Z_*),S_PERMISSION 用 **COMMENT**(非 REMARK);唯一性查询带 SIGMA 以保证幂等。全局列(SIGMA/APP_ID/TENANT_ID/CREATED_BY/UPDATED_BY/CREATED_AT/UPDATED_AT)在代码中写死,不运行时查表。
|
|
131
|
+
- 开发阶段可用 `node script/scan-rbac-schema.js`(可选 `--write`)核对库表列名与脚本一致;不修改 RBAC 的 Flyway 配置。
|
|
132
|
+
|
|
133
|
+
### 执行约定(仅 DML,不查元数据)
|
|
134
|
+
|
|
135
|
+
- ai ex-api 执行时**只执行 DML**(SELECT/INSERT/INSERT IGNORE),**不访问数据库元数据**(如 SHOW COLUMNS)。
|
|
136
|
+
- 表列名在**开发时**按 RBAC 建表(如 Flyway)固定写好,运行时不再查询表结构。
|
|
137
|
+
- **假设**:执行时数据表已存在且结构固定;**禁止**表扫描、DDL、元数据提取(已知表结构,无需运行时获取)。
|
|
138
|
+
|
|
139
|
+
### 完整打印(成功与失败)
|
|
140
|
+
|
|
141
|
+
- **启动**:配置路径、request 参数、skip、数据库连接信息。
|
|
142
|
+
- **步骤**:已存在/已创建 S_RESOURCE、S_ACTION、S_PERMISSION,R_ROLE_PERM 同步角色数,Excel 写出路径。
|
|
143
|
+
- **成功**:`[ex-api] 执行完成(幂等)` 及汇总(ACTION_ID、RESOURCE_ID、PERMISSION_ID、授权角色数、Excel 路径)。
|
|
144
|
+
- **失败**:`[ex-api] 执行失败` + 错误信息、错误码、sqlMessage、sql 语句、堆栈。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
5
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"executor": "executeExApi",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "从 ex-api 目录加载多份 yaml(metadata.r),多选后执行授权(数据库+Excel),处理成功后移至 backup 并加 .bak",
|
|
4
4
|
"command": "ex-api",
|
|
5
5
|
"options": [
|
|
6
|
-
{"name": "request", "alias": "r", "description": "请求方法与 URI,格式:\"<METHOD> <uri>\",如 \"GET /api/ambient\""},
|
|
7
6
|
{"name": "skip", "alias": "s", "type": "boolean", "description": "仅生成 Excel,跳过去重等检查", "default": false}
|
|
8
7
|
]
|
|
9
8
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"executor":"executeExCrud","description":"
|
|
1
|
+
{"executor":"executeExCrud","description":"从 ex-crud 目录加载多份 yaml,多选后按 metadata 从模板生成 CRUD Excel 及 RBAC 授权,处理成功后移至 backup 并加 .bak","command":"ex-crud","options":[{"name":"skip","alias":"s","type":"boolean","description":"仅生成 CRUD 文件,跳过数据库与角色选择","default":false}]}
|
|
@@ -8,7 +8,7 @@ const yaml = require("js-yaml");
|
|
|
8
8
|
const inquirer = require("inquirer");
|
|
9
9
|
const { v4: uuidv4 } = require("uuid");
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const CONFIG_DIR = ".r2mo/task/command/ex-api";
|
|
12
12
|
const REQUIRED_ENV_DB = ["Z_DB_TYPE", "Z_DB_HOST", "Z_DB_PORT", "Z_DBS_INSTANCE", "Z_DB_APP_USER", "Z_DB_APP_PASS"];
|
|
13
13
|
const REQUIRED_ENV_APP = ["Z_APP_ID", "Z_TENANT", "Z_SIGMA"];
|
|
14
14
|
const R2_BY_UUID = "9a0d5018-33ad-4c64-80bf-8ae7947c482f";
|
|
@@ -175,124 +175,73 @@ function isDpaRoot(dir) {
|
|
|
175
175
|
return fs.existsSync(apiDir) && fs.existsSync(domainDir);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
level: 1
|
|
193
|
-
ptype: "权限集 S_PERM_SET 类型"
|
|
194
|
-
pname: "权限集 S_PERM_SET 名称"
|
|
195
|
-
# keyword 可选;若存在则编码为 res.\${keyword} / act.\${keyword} / perm.\${keyword},否则按规则计算
|
|
196
|
-
# keyword: "app.test.data"
|
|
197
|
-
# target 可选;存在时需配置 ZERO_MODULE 且 DPA 目录 zero-exmodule-{module} 存在
|
|
198
|
-
# target:
|
|
199
|
-
# root: "ZERO_MODULE"
|
|
200
|
-
# module: "ambient"
|
|
201
|
-
`;
|
|
202
|
-
fs.writeFileSync(configFullPath, template, "utf-8");
|
|
203
|
-
Ec.info("配置文件缺失,已在下列路径写入模板:" + configFullPath);
|
|
204
|
-
Ec.info("请编辑后重新执行。参数格式: -r \"<METHOD> <uri>\" (uri 必须以 /api 为前缀)");
|
|
205
|
-
Ec.info("示例: ai ex-api -r \"GET /api/ambient\"");
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
178
|
+
/** 校验 metadata.r:须为 "<METHOD> <uri>",METHOD 常见动词,uri 以 /api 开头 */
|
|
179
|
+
function validateExApiR(r) {
|
|
180
|
+
if (!r || typeof r !== "string") return { valid: false, error: "r 为空" };
|
|
181
|
+
const s = String(r).trim();
|
|
182
|
+
if (!s) return { valid: false, error: "r 为空" };
|
|
183
|
+
const parts = s.split(/\s+/);
|
|
184
|
+
if (parts.length < 2) return { valid: false, error: "r 须为 \"<METHOD> <uri>\" 两段" };
|
|
185
|
+
const method = (parts[0] || "").toUpperCase();
|
|
186
|
+
const uri = parts.slice(1).join(" ").trim();
|
|
187
|
+
const allowed = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
188
|
+
if (!allowed.includes(method)) return { valid: false, error: "METHOD 须为 " + allowed.join("/") };
|
|
189
|
+
if (!uri.startsWith("/api")) return { valid: false, error: "uri 须以 /api 为前缀" };
|
|
190
|
+
return { valid: true };
|
|
191
|
+
}
|
|
208
192
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
193
|
+
/** 解析 ex-api 配置目录:cwd / 上级 / 上上级 */
|
|
194
|
+
function resolveExApiConfigDir(cwd) {
|
|
195
|
+
const primary = path.resolve(cwd, CONFIG_DIR);
|
|
196
|
+
if (fs.existsSync(primary) && fs.statSync(primary).isDirectory()) return primary;
|
|
197
|
+
const parent = path.resolve(cwd, "..", CONFIG_DIR);
|
|
198
|
+
if (fs.existsSync(parent) && fs.statSync(parent).isDirectory()) return parent;
|
|
199
|
+
const grand = path.resolve(cwd, "..", "..", CONFIG_DIR);
|
|
200
|
+
if (fs.existsSync(grand) && fs.statSync(grand).isDirectory()) return grand;
|
|
201
|
+
return primary;
|
|
202
|
+
}
|
|
219
203
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
204
|
+
/** 表格化打印 ex-api 汇总 */
|
|
205
|
+
function printExApiTable(results) {
|
|
206
|
+
const rows = results.map((r) => ({
|
|
207
|
+
identifier: r.identifier || "—",
|
|
208
|
+
request: r.request || "—",
|
|
209
|
+
ok: r.ok ? "✓" : "✗",
|
|
210
|
+
error: r.error || "—"
|
|
211
|
+
}));
|
|
212
|
+
const col = (arr, key) => arr.map((x) => String(x[key] != null ? x[key] : ""));
|
|
213
|
+
const max = (arr) => Math.max(2, ...arr.map((s) => (s && s.length) || 0));
|
|
214
|
+
const wId = max(col(rows, "identifier"));
|
|
215
|
+
const wReq = Math.min(max(col(rows, "request")), 48);
|
|
216
|
+
const wErr = Math.min(max(col(rows, "error")), 32);
|
|
217
|
+
const sep = " | ";
|
|
218
|
+
Ec.info("[ex-api] 汇总:");
|
|
219
|
+
Ec.info(" " + "identifier".padEnd(wId) + sep + "request".padEnd(wReq) + sep + "ok" + sep + "error".padEnd(wErr));
|
|
220
|
+
rows.forEach((r) => Ec.info(" " + (r.identifier + "").padEnd(wId) + sep + (r.request + "").slice(0, wReq).padEnd(wReq) + sep + r.ok + sep + (r.error + "").slice(0, wErr)));
|
|
221
|
+
}
|
|
232
222
|
|
|
223
|
+
/** 单条 API 执行:使用 config.metadata.r 作为 request,执行 DB + Excel,返回汇总 */
|
|
224
|
+
async function runOneExApi(cwd, conn, config, requestRaw, skip) {
|
|
233
225
|
const metadata = config.metadata;
|
|
234
226
|
const target = config.target;
|
|
235
|
-
if (target && target.root && target.module) {
|
|
236
|
-
const zeroModule = process.env.ZERO_MODULE;
|
|
237
|
-
if (!zeroModule || !zeroModule.trim()) {
|
|
238
|
-
Ec.error("存在 target 配置时,环境变量 ZERO_MODULE 必须已设置");
|
|
239
|
-
process.exit(1);
|
|
240
|
-
}
|
|
241
|
-
const dpaRoot = path.resolve(zeroModule, `zero-exmodule-${target.module}`);
|
|
242
|
-
if (!fs.existsSync(dpaRoot) || !isDpaRoot(dpaRoot)) {
|
|
243
|
-
Ec.error(`ZERO_MODULE 下 DPA 目录不是标准架构:${dpaRoot}`);
|
|
244
|
-
Ec.info("需存在 pom.xml 且包含 xxx-api、xxx-domain 子目录");
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// 4. 参数:-r "<METHOD> <uri>" ,-s 可选
|
|
250
|
-
const parsed = Ut.parseArgument(options);
|
|
251
|
-
const skip = parsed.skip === true || process.argv.includes("-s") || process.argv.includes("--skip");
|
|
252
227
|
let method = "";
|
|
253
228
|
let uri = "";
|
|
254
|
-
const requestRaw = parsed.request;
|
|
255
229
|
if (requestRaw && String(requestRaw).trim()) {
|
|
256
230
|
const parts = String(requestRaw).trim().split(/\s+/);
|
|
257
231
|
if (parts.length >= 2) {
|
|
258
232
|
method = parts[0].toUpperCase();
|
|
259
233
|
uri = parts.slice(1).join(" ").trim();
|
|
260
|
-
} else if (parts.length === 1) {
|
|
261
|
-
Ec.error("参数 -r 格式应为 \"<METHOD> <uri>\",例如 \"GET /api/ambient\"");
|
|
262
|
-
process.exit(1);
|
|
263
234
|
}
|
|
264
235
|
}
|
|
265
236
|
if (!skip && (!method || !uri)) {
|
|
266
|
-
|
|
267
|
-
Ec.info("参数格式: -r \"<METHOD> <uri>\" 或 --request \"<METHOD> <uri>\"");
|
|
268
|
-
Ec.info("示例: ai ex-api -r \"GET /api/ambient\"(uri 必须以 /api 为前缀)");
|
|
269
|
-
process.exit(1);
|
|
237
|
+
return { identifier: metadata.identifier || "—", request: requestRaw || "—", ok: false, error: "缺少 metadata.r 或格式非 \"<METHOD> <uri>\"" };
|
|
270
238
|
}
|
|
271
239
|
if (!skip && uri && !uri.trim().startsWith("/api")) {
|
|
272
|
-
|
|
273
|
-
Ec.info("当前 uri:" + (uri || ""));
|
|
274
|
-
Ec.info("示例: ai ex-api -r \"GET /api/ambient\"");
|
|
275
|
-
process.exit(1);
|
|
240
|
+
return { identifier: metadata.identifier || "—", request: requestRaw || "—", ok: false, error: "uri 必须以 /api 为前缀" };
|
|
276
241
|
}
|
|
277
242
|
|
|
278
|
-
Ec.execute("ai ex-api:配置已加载,环境与参数检查通过。");
|
|
279
|
-
|
|
280
|
-
const dbConfig = {
|
|
281
|
-
host: process.env.Z_DB_HOST || "localhost",
|
|
282
|
-
port: parseInt(process.env.Z_DB_PORT || "3306", 10),
|
|
283
|
-
user: process.env.Z_DB_APP_USER,
|
|
284
|
-
password: process.env.Z_DB_APP_PASS,
|
|
285
|
-
database: process.env.Z_DBS_INSTANCE
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
const mysql = require("mysql2/promise");
|
|
289
|
-
Ec.info("[ex-api] 配置:" + CONFIG_PATH + " | request=" + (requestRaw || "") + " | skip=" + skip);
|
|
290
|
-
let conn = await mysql.createConnection(dbConfig);
|
|
291
|
-
Ec.info("[ex-api] 数据库已连接:" + (dbConfig.database || "") + " @" + (dbConfig.host || "") + ":" + (dbConfig.port || ""));
|
|
292
|
-
|
|
293
|
-
// 约定:执行时数据表已存在且结构固定,仅执行 DML(SELECT/INSERT/INSERT IGNORE),禁止表扫描、DDL、元数据查询(如 SHOW COLUMNS)。
|
|
294
243
|
try {
|
|
295
|
-
|
|
244
|
+
const appId = process.env.Z_APP_ID;
|
|
296
245
|
const tenantId = process.env.Z_TENANT;
|
|
297
246
|
const sigma = process.env.Z_SIGMA;
|
|
298
247
|
|
|
@@ -799,13 +748,153 @@ metadata:
|
|
|
799
748
|
Ec.info("[ex-api] 📦 R_ROLE_PERM 本次写入(ROLE_ID, PERM_ID):");
|
|
800
749
|
insertedRolePerms.forEach((r, i) => Ec.info("[ex-api] [" + (i + 1) + "] " + r.ROLE_ID + ", " + r.PERM_ID));
|
|
801
750
|
}
|
|
751
|
+
return {
|
|
752
|
+
identifier: metadata.identifier || "—",
|
|
753
|
+
request: requestRaw || "—",
|
|
754
|
+
actionId: actionId || "—",
|
|
755
|
+
resourceId: resourceId || "—",
|
|
756
|
+
permissionId: permissionId || "—",
|
|
757
|
+
roleCount: roleIds ? roleIds.length : 0,
|
|
758
|
+
outResPath: outResPath || "—",
|
|
759
|
+
outRolePath: outRolePath || "—",
|
|
760
|
+
ok: true
|
|
761
|
+
};
|
|
802
762
|
} catch (err) {
|
|
803
763
|
Ec.error("[ex-api] 执行失败:" + (err && err.message));
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
764
|
+
return {
|
|
765
|
+
identifier: (metadata && metadata.identifier) || "—",
|
|
766
|
+
request: requestRaw || "—",
|
|
767
|
+
actionId: "—",
|
|
768
|
+
resourceId: "—",
|
|
769
|
+
permissionId: "—",
|
|
770
|
+
roleCount: 0,
|
|
771
|
+
outResPath: "—",
|
|
772
|
+
outRolePath: "—",
|
|
773
|
+
ok: false,
|
|
774
|
+
error: (err && err.message) || String(err)
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
module.exports = async (options) => {
|
|
780
|
+
const cwd = process.cwd();
|
|
781
|
+
const configDir = resolveExApiConfigDir(cwd);
|
|
782
|
+
if (!fs.existsSync(configDir)) {
|
|
783
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
784
|
+
const templatePath = path.join(configDir, "ex-api.yaml");
|
|
785
|
+
const template = `# ai ex-api 使用此配置,请按项目修改
|
|
786
|
+
metadata:
|
|
787
|
+
r: "GET /api/ambient"
|
|
788
|
+
identifier: "核心标识符"
|
|
789
|
+
brief: "接口描述"
|
|
790
|
+
resource: "resource.ambient"
|
|
791
|
+
level: 1
|
|
792
|
+
ptype: "权限集 S_PERM_SET 类型"
|
|
793
|
+
pname: "权限集 S_PERM_SET 名称"
|
|
794
|
+
# keyword 可选
|
|
795
|
+
# target 可选;存在时需 ZERO_MODULE 与 zero-exmodule-{module}
|
|
796
|
+
# target:
|
|
797
|
+
# root: "ZERO_MODULE"
|
|
798
|
+
# module: "ambient"
|
|
799
|
+
`;
|
|
800
|
+
fs.writeFileSync(templatePath, template, "utf-8");
|
|
801
|
+
Ec.info("配置目录缺失,已创建并写入模板:" + templatePath);
|
|
802
|
+
Ec.info("请编辑后重新执行: ai ex-api");
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const backupDir = path.join(configDir, "backup");
|
|
807
|
+
const allEntries = fs.readdirSync(configDir, { withFileTypes: true });
|
|
808
|
+
const yamlFiles = allEntries.filter((e) => !e.isDirectory() && e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml")));
|
|
809
|
+
const entries = [];
|
|
810
|
+
for (const e of yamlFiles) {
|
|
811
|
+
const f = e.name;
|
|
812
|
+
const full = path.join(configDir, f);
|
|
813
|
+
try {
|
|
814
|
+
const config = yaml.load(fs.readFileSync(full, "utf-8"));
|
|
815
|
+
if (!config || !config.metadata) continue;
|
|
816
|
+
const r = config.metadata.r != null ? String(config.metadata.r).trim() : "";
|
|
817
|
+
if (!r) {
|
|
818
|
+
Ec.info("[ex-api] 跳过(无 metadata.r):" + f);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const valid = validateExApiR(r);
|
|
822
|
+
if (!valid.valid) {
|
|
823
|
+
Ec.info("[ex-api] 警告(r 不合法,已跳过):" + f + "," + (valid.error || ""));
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
const label = (config.metadata.identifier || f) + " | " + (config.metadata.brief || r);
|
|
827
|
+
entries.push({ path: full, config, label });
|
|
828
|
+
} catch (_) {}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (entries.length === 0) {
|
|
832
|
+
Ec.error("[ex-api] 无有效配置:请在 " + configDir + " 下添加含 metadata.r 的 yaml");
|
|
808
833
|
process.exit(1);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const answer = await inquirer.prompt([
|
|
837
|
+
{ type: "checkbox", name: "selected", message: "选择要执行的 API(多选)", choices: entries.map((e) => ({ name: e.label, value: e.path })) }
|
|
838
|
+
]);
|
|
839
|
+
const selectedPaths = answer && answer.selected && Array.isArray(answer.selected) ? answer.selected : [];
|
|
840
|
+
if (selectedPaths.length === 0) {
|
|
841
|
+
Ec.info("未选择任何项,退出");
|
|
842
|
+
process.exit(0);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const appEnvPath = resolveAppEnvPath(cwd);
|
|
846
|
+
if (!appEnvPath) {
|
|
847
|
+
Ec.error(".r2mo/app.env 不存在;DPA 下也未找到 {id}-api/.r2mo/app.env");
|
|
848
|
+
process.exit(1);
|
|
849
|
+
}
|
|
850
|
+
loadAppEnv(appEnvPath);
|
|
851
|
+
checkEnv(REQUIRED_ENV_DB, "数据库环境变量");
|
|
852
|
+
checkEnv(REQUIRED_ENV_APP, "应用环境变量(Z_APP_ID / Z_TENANT / Z_SIGMA)");
|
|
853
|
+
|
|
854
|
+
const dbConfig = {
|
|
855
|
+
host: process.env.Z_DB_HOST || "localhost",
|
|
856
|
+
port: parseInt(process.env.Z_DB_PORT || "3306", 10),
|
|
857
|
+
user: process.env.Z_DB_APP_USER,
|
|
858
|
+
password: process.env.Z_DB_APP_PASS,
|
|
859
|
+
database: process.env.Z_DBS_INSTANCE
|
|
860
|
+
};
|
|
861
|
+
const parsed = Ut.parseArgument(options);
|
|
862
|
+
const skip = parsed.skip === true || process.argv.includes("-s") || process.argv.includes("--skip");
|
|
863
|
+
|
|
864
|
+
const mysql = require("mysql2/promise");
|
|
865
|
+
let conn;
|
|
866
|
+
try {
|
|
867
|
+
conn = await mysql.createConnection(dbConfig);
|
|
868
|
+
Ec.info("[ex-api] 数据库已连接,执行 " + selectedPaths.length + " 条 API");
|
|
869
|
+
const results = [];
|
|
870
|
+
for (const configPath of selectedPaths) {
|
|
871
|
+
const config = yaml.load(fs.readFileSync(configPath, "utf-8"));
|
|
872
|
+
const requestRaw = config.metadata && config.metadata.r != null ? String(config.metadata.r).trim() : "";
|
|
873
|
+
if (!requestRaw) {
|
|
874
|
+
results.push({ identifier: config.metadata?.identifier || "—", request: "—", ok: false, error: "无 metadata.r" });
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const valid = validateExApiR(requestRaw);
|
|
878
|
+
if (!valid.valid) {
|
|
879
|
+
Ec.info("[ex-api] 警告(r 不合法,跳过执行):" + path.basename(configPath) + "," + (valid.error || ""));
|
|
880
|
+
results.push({ identifier: config.metadata?.identifier || "—", request: requestRaw, ok: false, error: valid.error || "r 不合法" });
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
Ec.info("[ex-api] 处理:" + (config.metadata.identifier || path.basename(configPath)) + " (" + requestRaw + ")");
|
|
884
|
+
const one = await runOneExApi(cwd, conn, config, requestRaw, skip);
|
|
885
|
+
results.push(one);
|
|
886
|
+
if (one.ok) {
|
|
887
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
888
|
+
const bakPath = path.join(backupDir, path.basename(configPath) + ".bak");
|
|
889
|
+
try {
|
|
890
|
+
fs.renameSync(configPath, bakPath);
|
|
891
|
+
Ec.info("[ex-api] 已备份:" + path.basename(configPath) + " -> backup/" + path.basename(configPath) + ".bak");
|
|
892
|
+
} catch (errBak) {
|
|
893
|
+
Ec.info("[ex-api] 备份失败(已忽略):" + configPath + "," + (errBak && errBak.message));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
printExApiTable(results);
|
|
809
898
|
} finally {
|
|
810
899
|
if (conn) await conn.end();
|
|
811
900
|
}
|