zero-ai 1.0.74 → 1.0.75
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/.r2mo/task/command/ex-api.yaml.example +13 -0
- package/.r2mo/task/task-001.md +136 -4
- package/.r2mo/task/task-002.md +4 -0
- package/.r2mo/task/task-003.md +4 -0
- package/package.json +4 -3
- package/script/clear-excel-template-data.js +128 -0
- package/script/create-ex-api-templates.js +39 -0
- package/script/inspect-excel-templates.js +102 -0
- package/script/read-ex-api-templates.js +51 -0
- package/script/scan-rbac-schema.js +102 -0
- package/src/_template/EXCEL/ex-api/README.md +13 -0
- package/src/_template/EXCEL/ex-api/template-RBAC_RESOURCE.xlsx +0 -0
- package/src/_template/EXCEL/ex-api/template-RBAC_ROLE.xlsx +0 -0
- package/src/_template/EXCEL/ex-api/template-def.json +21 -0
- package/src/_template/EXCEL/ex-crud/README.md +28 -0
- package/src/_template/EXCEL/ex-crud/ex-crud.yaml +6 -0
- package/src/_template/EXCEL/ex-crud/template-RBAC_ROLE.xlsx +0 -0
- package/src/_template/EXCEL/ex-crud/x.log.xlsx +0 -0
- package/src/commander/ex-api.json +9 -0
- package/src/commander/ex-crud.json +1 -0
- package/src/commander-ai/fn.ex.api.js +812 -0
- package/src/commander-ai/fn.ex.crud.js +501 -0
- package/src/commander-ai/fn.ex.perm.js +18 -3
- package/src/commander-ai/index.js +4 -0
|
@@ -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"
|
package/.r2mo/task/task-001.md
CHANGED
|
@@ -1,12 +1,144 @@
|
|
|
1
1
|
---
|
|
2
2
|
runAt: 2026-02-12.22-47-18
|
|
3
|
-
title: 开发 ai ex-
|
|
3
|
+
title: 开发 ai ex-api 命令
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 任务
|
|
7
7
|
|
|
8
|
-
开发 `ai ex-
|
|
8
|
+
开发 `ai ex-api` 命令对已经开发好的 API 进行授权,此命令为固定命令,要完成全套执行操作,系统重启后要保证权限生效,且命令必须拥有幂等性。
|
|
9
9
|
|
|
10
|
-
###
|
|
10
|
+
### 前置条件
|
|
11
11
|
|
|
12
|
-
|
|
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 语句、堆栈。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-ai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.75",
|
|
4
4
|
"description": "Zero Ecotope AI",
|
|
5
5
|
"main": "src/ai.js",
|
|
6
6
|
"bin": {
|
|
@@ -38,16 +38,17 @@
|
|
|
38
38
|
"i": "^0.3.7",
|
|
39
39
|
"immutable": "^4.3.0",
|
|
40
40
|
"inquirer": "^8.2.5",
|
|
41
|
+
"js-yaml": "^4.1.1",
|
|
41
42
|
"linebyline": "^1.3.0",
|
|
42
43
|
"lodash": "^4.17.21",
|
|
43
44
|
"mkdirp": "^3.0.1",
|
|
44
45
|
"mockjs": "^1.1.0",
|
|
46
|
+
"mysql2": "^3.11.0",
|
|
45
47
|
"random-js": "^2.1.0",
|
|
46
48
|
"superagent": "^8.0.9",
|
|
47
49
|
"taffydb": "^2.7.3",
|
|
48
50
|
"underscore": "^1.13.6",
|
|
49
|
-
"uuid": "^9.0.0"
|
|
50
|
-
"mysql2": "^3.11.0"
|
|
51
|
+
"uuid": "^9.0.0"
|
|
51
52
|
},
|
|
52
53
|
"homepage": "http://www.vertxai.cn",
|
|
53
54
|
"github": "https://github.com/silentbalanceyh/vertx-ai.git"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 清空 src/_template/EXCEL/ex-api 下两个模板 xlsx 中所有 {TABLE} 区域的数据行,只保留表头。
|
|
4
|
+
* 执行一次后,ai ex-api 只需直接填充数据,无需运行时清除。
|
|
5
|
+
* 运行:node script/clear-excel-template-data.js
|
|
6
|
+
*/
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const ExcelJS = require("exceljs");
|
|
10
|
+
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const templateDir = path.resolve(cwd, "src", "_template", "EXCEL", "ex-api");
|
|
13
|
+
const maxCols = 50;
|
|
14
|
+
const limit = 5000;
|
|
15
|
+
|
|
16
|
+
function scanTableRegions(ws) {
|
|
17
|
+
if (!ws) return [];
|
|
18
|
+
const regions = [];
|
|
19
|
+
let i = 1;
|
|
20
|
+
while (i <= limit) {
|
|
21
|
+
const row = ws.getRow(i);
|
|
22
|
+
const first = row.getCell(1).value;
|
|
23
|
+
const v = first != null ? String(first).trim() : "";
|
|
24
|
+
if (v === "{TABLE}") {
|
|
25
|
+
const tableNameCell = row.getCell(2).value;
|
|
26
|
+
const tableName = tableNameCell != null ? String(tableNameCell).trim() : "";
|
|
27
|
+
const headerRowCount = 2;
|
|
28
|
+
const dataStartRow = i + 1 + headerRowCount;
|
|
29
|
+
let dataEndRow = dataStartRow - 1;
|
|
30
|
+
let j = i + 1;
|
|
31
|
+
while (j <= limit) {
|
|
32
|
+
const nextRow = ws.getRow(j);
|
|
33
|
+
const nextFirst = nextRow.getCell(1).value;
|
|
34
|
+
const nv = nextFirst != null ? String(nextFirst).trim() : "";
|
|
35
|
+
if (nv === "{TABLE}") {
|
|
36
|
+
dataEndRow = j - 1;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
dataEndRow = j;
|
|
40
|
+
j++;
|
|
41
|
+
}
|
|
42
|
+
regions.push({ tableName, tableStartRow: i, dataStartRow, dataEndRow });
|
|
43
|
+
i = dataEndRow + 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
return regions;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function clearRegion(ws, dataStartRow, dataEndRow) {
|
|
52
|
+
for (let r = dataStartRow; r <= dataEndRow; r++) {
|
|
53
|
+
const row = ws.getRow(r);
|
|
54
|
+
for (let c = 1; c <= maxCols; c++) row.getCell(c).value = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 清除指定行的背景与边框(表间两行空白用) */
|
|
59
|
+
function clearBlankRowFormat(ws, startRow, endRow, maxCols) {
|
|
60
|
+
if (!ws || endRow < startRow) return;
|
|
61
|
+
const cols = maxCols || 20;
|
|
62
|
+
const noFill = { type: "pattern", pattern: "none" };
|
|
63
|
+
const noBorder = { top: { style: "none" }, left: { style: "none" }, bottom: { style: "none" }, right: { style: "none" } };
|
|
64
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
65
|
+
const row = ws.getRow(r);
|
|
66
|
+
for (let c = 1; c <= cols; c++) {
|
|
67
|
+
const cell = row.getCell(c);
|
|
68
|
+
if (cell.fill) cell.fill = noFill;
|
|
69
|
+
if (cell.border) cell.border = noBorder;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 表间只保留 2 行空白:删除 (dataStartRow+3) 到 (下一表 tableStartRow-1) 的多余行,从后往前删避免行号变化 */
|
|
75
|
+
function compressTableGaps(ws, regions) {
|
|
76
|
+
if (!regions || regions.length < 2) return;
|
|
77
|
+
const BLANK_ROWS = 2;
|
|
78
|
+
const deletions = [];
|
|
79
|
+
for (let i = 0; i < regions.length - 1; i++) {
|
|
80
|
+
const start = regions[i].dataStartRow + 1 + BLANK_ROWS; // 保留 1 行数据位 + 2 空行
|
|
81
|
+
const end = regions[i + 1].tableStartRow - 1;
|
|
82
|
+
if (end >= start) {
|
|
83
|
+
const count = end - start + 1;
|
|
84
|
+
deletions.push({ start, count });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
deletions.sort((a, b) => b.start - a.start);
|
|
88
|
+
deletions.forEach(({ start, count }) => {
|
|
89
|
+
try {
|
|
90
|
+
ws.spliceRows(start, count);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.warn("spliceRows(" + start + "," + count + ") 跳过: " + e.message);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function processFile(fileName) {
|
|
98
|
+
const filePath = path.join(templateDir, fileName);
|
|
99
|
+
if (!fs.existsSync(filePath)) {
|
|
100
|
+
console.log("跳过(不存在): " + filePath);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const wb = await new ExcelJS.Workbook().xlsx.readFile(filePath);
|
|
104
|
+
let cleared = 0;
|
|
105
|
+
wb.worksheets.forEach((ws) => {
|
|
106
|
+
const regions = scanTableRegions(ws);
|
|
107
|
+
regions.forEach((reg) => {
|
|
108
|
+
clearRegion(ws, reg.dataStartRow, reg.dataEndRow);
|
|
109
|
+
cleared += Math.max(0, reg.dataEndRow - reg.dataStartRow + 1);
|
|
110
|
+
clearBlankRowFormat(ws, reg.dataStartRow + 1, reg.dataStartRow + 2, maxCols);
|
|
111
|
+
});
|
|
112
|
+
compressTableGaps(ws, regions);
|
|
113
|
+
});
|
|
114
|
+
await wb.xlsx.writeFile(filePath);
|
|
115
|
+
console.log("已清空、压缩并写回: " + fileName + "(清除 " + cleared + " 行,表间仅留 2 行空行)");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function main() {
|
|
119
|
+
console.log("模板目录: " + templateDir);
|
|
120
|
+
await processFile("template-RBAC_RESOURCE.xlsx");
|
|
121
|
+
await processFile("template-RBAC_ROLE.xlsx");
|
|
122
|
+
console.log("完成。模板仅保留表头,ai ex-api 将直接填充数据。");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main().catch((e) => {
|
|
126
|
+
console.error(e);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 生成 ai ex-api 使用的 Excel 模板,写入 src/_template/ex-api/
|
|
4
|
+
* 可与 ZERO_MODULE 下已有 RBAC Excel 格式对齐后,将彼处文件复制到 src/_template/ex-api/ 覆盖本脚本生成的标准模板。
|
|
5
|
+
* 运行:node script/create-ex-api-templates.js
|
|
6
|
+
*/
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const ExcelJS = require("exceljs");
|
|
10
|
+
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const templateDir = path.resolve(cwd, "src", "_template", "ex-api");
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(templateDir)) fs.mkdirSync(templateDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const wbRes = new ExcelJS.Workbook();
|
|
18
|
+
wbRes.creator = "ai ex-api template";
|
|
19
|
+
const wsRes = wbRes.addWorksheet("DATA-RES", { views: [{ state: "frozen", ySplit: 1 }] });
|
|
20
|
+
wsRes.addRow(["ID", "CODE", "NAME", "IDENTIFIER", "TYPE", "LEVEL", "MODE_ROLE"]);
|
|
21
|
+
|
|
22
|
+
const wbRole = new ExcelJS.Workbook();
|
|
23
|
+
wbRole.creator = "ai ex-api template";
|
|
24
|
+
const wsRole = wbRole.addWorksheet("DATA-PERM", { views: [{ state: "frozen", ySplit: 1 }] });
|
|
25
|
+
wsRole.addRow(["ROLE_ID", "PERM_ID"]);
|
|
26
|
+
|
|
27
|
+
const outRes = path.join(templateDir, "template-RBAC_RESOURCE.xlsx");
|
|
28
|
+
const outRole = path.join(templateDir, "template-RBAC_ROLE.xlsx");
|
|
29
|
+
await wbRes.xlsx.writeFile(outRes);
|
|
30
|
+
await wbRole.xlsx.writeFile(outRole);
|
|
31
|
+
console.log("已生成模板:");
|
|
32
|
+
console.log(" " + outRes);
|
|
33
|
+
console.log(" " + outRole);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
main().catch((e) => {
|
|
37
|
+
console.error(e);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 检查 src/_template/EXCEL/ex-api 下 template-RBAC_ROLE.xlsx 与 template-RBAC_RESOURCE.xlsx 的详细格式。
|
|
4
|
+
* 输出:工作表名、行/列数、前若干行单元格内容,便于按模板格式填充。
|
|
5
|
+
* 运行:node script/inspect-excel-templates.js
|
|
6
|
+
*/
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const ExcelJS = require("exceljs");
|
|
10
|
+
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const templateDir = path.resolve(cwd, "src", "_template", "EXCEL", "ex-api");
|
|
13
|
+
const maxRows = 15;
|
|
14
|
+
const maxCols = 20;
|
|
15
|
+
|
|
16
|
+
async function inspectFile(fileName) {
|
|
17
|
+
const filePath = path.join(templateDir, fileName);
|
|
18
|
+
if (!fs.existsSync(filePath)) {
|
|
19
|
+
console.log("\n文件不存在: " + filePath);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const wb = await new ExcelJS.Workbook().xlsx.readFile(filePath);
|
|
23
|
+
const result = { file: fileName, sheetCount: wb.worksheets.length, sheets: [] };
|
|
24
|
+
for (let i = 0; i < wb.worksheets.length; i++) {
|
|
25
|
+
const ws = wb.worksheets[i];
|
|
26
|
+
const sheetInfo = {
|
|
27
|
+
name: ws.name,
|
|
28
|
+
id: ws.id,
|
|
29
|
+
rowCount: ws.rowCount,
|
|
30
|
+
columnCount: ws.columnCount,
|
|
31
|
+
rows: []
|
|
32
|
+
};
|
|
33
|
+
const rowCount = Math.min(ws.rowCount || 0, maxRows);
|
|
34
|
+
const colCount = Math.min((ws.columnCount || 0) + 5, maxCols);
|
|
35
|
+
for (let r = 1; r <= rowCount; r++) {
|
|
36
|
+
const row = ws.getRow(r);
|
|
37
|
+
const cells = [];
|
|
38
|
+
row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
|
39
|
+
if (colNumber <= colCount) {
|
|
40
|
+
let v = cell && cell.value != null ? cell.value : "";
|
|
41
|
+
if (v && typeof v === "object" && v.text !== undefined) v = v.text;
|
|
42
|
+
if (v && typeof v === "object" && v.result !== undefined) v = v.result;
|
|
43
|
+
cells.push(v);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
for (let c = cells.length; c < colCount; c++) cells.push("");
|
|
47
|
+
sheetInfo.rows.push(cells);
|
|
48
|
+
}
|
|
49
|
+
if (sheetInfo.rows.length === 0 && (ws.columnCount || ws.rowCount)) {
|
|
50
|
+
for (let r = 1; r <= Math.min(5, ws.rowCount || 5); r++) {
|
|
51
|
+
const row = ws.getRow(r);
|
|
52
|
+
const cells = [];
|
|
53
|
+
row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
|
54
|
+
if (colNumber <= maxCols) cells.push(cell && cell.value != null ? cell.value : "");
|
|
55
|
+
});
|
|
56
|
+
sheetInfo.rows.push(cells);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
result.sheets.push(sheetInfo);
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printResult(obj) {
|
|
65
|
+
if (!obj) return;
|
|
66
|
+
console.log("\n========== " + obj.file + " ==========");
|
|
67
|
+
console.log("工作表数量:", obj.sheetCount);
|
|
68
|
+
obj.sheets.forEach((sh, idx) => {
|
|
69
|
+
console.log("\n--- Sheet[" + idx + "] 名称: " + sh.name + " ---");
|
|
70
|
+
console.log(" 行数(约):", sh.rowCount, " 列数(约):", sh.columnCount);
|
|
71
|
+
sh.rows.forEach((row, rIdx) => {
|
|
72
|
+
const line = row.map((c) => (c == null ? "" : String(c).slice(0, 30)));
|
|
73
|
+
console.log(" 行" + (rIdx + 1) + ":", JSON.stringify(line));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function main() {
|
|
79
|
+
console.log("模板目录:", templateDir);
|
|
80
|
+
const a = await inspectFile("template-RBAC_RESOURCE.xlsx");
|
|
81
|
+
const b = await inspectFile("template-RBAC_ROLE.xlsx");
|
|
82
|
+
printResult(a);
|
|
83
|
+
printResult(b);
|
|
84
|
+
if (a) {
|
|
85
|
+
const defPath = path.join(templateDir, "template-def.json");
|
|
86
|
+
const def = {
|
|
87
|
+
RBAC_RESOURCE: a.sheets[0] ? { sheetName: a.sheets[0].name, columns: (a.sheets[0].rows[0] || []).map((c) => String(c || "").trim() || null).filter(Boolean) } : null,
|
|
88
|
+
RBAC_ROLE: b && b.sheets[0] ? { sheetName: b.sheets[0].name, columns: (b.sheets[0].rows[0] || []).map((c) => String(c || "").trim() || null).filter(Boolean) } : null
|
|
89
|
+
};
|
|
90
|
+
if (def.RBAC_RESOURCE && def.RBAC_RESOURCE.columns.length) def.RBAC_RESOURCE.columns = def.RBAC_RESOURCE.columns.filter((c) => c);
|
|
91
|
+
if (def.RBAC_ROLE && def.RBAC_ROLE.columns.length) def.RBAC_ROLE.columns = def.RBAC_ROLE.columns.filter((c) => c);
|
|
92
|
+
if (!def.RBAC_RESOURCE || !def.RBAC_RESOURCE.columns.length) def.RBAC_RESOURCE = { sheetName: "DATA-RES", columns: ["ID", "CODE", "NAME", "IDENTIFIER", "TYPE", "LEVEL", "MODE_ROLE"] };
|
|
93
|
+
if (!def.RBAC_ROLE || !def.RBAC_ROLE.columns.length) def.RBAC_ROLE = { sheetName: "DATA-PERM", columns: ["ROLE_ID", "PERM_ID"] };
|
|
94
|
+
fs.writeFileSync(defPath, JSON.stringify(def, null, 2), "utf-8");
|
|
95
|
+
console.log("\n已写入 template-def.json(表头从首行解析)");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main().catch((e) => {
|
|
100
|
+
console.error(e);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 解读 src/_template/ex-api 下两个 xlsx,提取工作表名与首行表头,生成 template-def.json 供 ai ex-api 按模版生成。
|
|
4
|
+
* 运行:node script/read-ex-api-templates.js
|
|
5
|
+
*/
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const ExcelJS = require("exceljs");
|
|
9
|
+
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
const templateDir = path.resolve(cwd, "src", "_template", "ex-api");
|
|
12
|
+
const resPath = path.join(templateDir, "template-RBAC_RESOURCE.xlsx");
|
|
13
|
+
const rolePath = path.join(templateDir, "template-RBAC_ROLE.xlsx");
|
|
14
|
+
|
|
15
|
+
async function readSheetDef(filePath, key) {
|
|
16
|
+
if (!fs.existsSync(filePath)) {
|
|
17
|
+
console.warn("文件不存在,跳过: " + filePath);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const wb = await new ExcelJS.Workbook().xlsx.readFile(filePath);
|
|
21
|
+
const ws = wb.worksheets[0];
|
|
22
|
+
if (!ws) return null;
|
|
23
|
+
const sheetName = ws.name;
|
|
24
|
+
const firstRow = ws.getRow(1);
|
|
25
|
+
const columns = [];
|
|
26
|
+
firstRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
|
27
|
+
const v = cell && cell.value != null ? String(cell.value).trim() : "";
|
|
28
|
+
columns.push(v || "Column" + colNumber);
|
|
29
|
+
});
|
|
30
|
+
return { sheetName, columns };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function main() {
|
|
34
|
+
const RBAC_RESOURCE = await readSheetDef(resPath, "RBAC_RESOURCE");
|
|
35
|
+
const RBAC_ROLE = await readSheetDef(rolePath, "RBAC_ROLE");
|
|
36
|
+
|
|
37
|
+
const def = {
|
|
38
|
+
RBAC_RESOURCE: RBAC_RESOURCE || { sheetName: "DATA-RES", columns: ["ID", "CODE", "NAME", "IDENTIFIER", "TYPE", "LEVEL", "MODE_ROLE"] },
|
|
39
|
+
RBAC_ROLE: RBAC_ROLE || { sheetName: "DATA-PERM", columns: ["ROLE_ID", "PERM_ID"] }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const outPath = path.join(templateDir, "template-def.json");
|
|
43
|
+
fs.writeFileSync(outPath, JSON.stringify(def, null, 2), "utf-8");
|
|
44
|
+
console.log("已解读 xlsx 并写入模版定义:" + outPath);
|
|
45
|
+
console.log(JSON.stringify(def, null, 2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
main().catch((e) => {
|
|
49
|
+
console.error(e);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 扫描 RBAC 相关表结构,输出列信息,用于对齐 ai ex-api 等命令的 SQL 列名。
|
|
4
|
+
* 使用前请 source .r2mo/app.env 或设置环境变量:
|
|
5
|
+
* Z_DB_HOST, Z_DB_PORT, Z_DB_APP_USER, Z_DB_APP_PASS, Z_DBS_INSTANCE
|
|
6
|
+
* 运行:node script/scan-rbac-schema.js
|
|
7
|
+
* 可选:--write 将结果写入 .r2mo/task/rbac-schema.txt
|
|
8
|
+
*/
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
|
|
14
|
+
function getArtifactIdFromPom(dir) {
|
|
15
|
+
const pomPath = path.resolve(dir, "pom.xml");
|
|
16
|
+
if (!fs.existsSync(pomPath)) return null;
|
|
17
|
+
let content = fs.readFileSync(pomPath, "utf-8");
|
|
18
|
+
content = content.replace(/<parent>[\s\S]*?<\/parent>/i, "");
|
|
19
|
+
const m = content.match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
20
|
+
return m ? m[1].trim() : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveAppEnvPath(dir) {
|
|
24
|
+
const primary = path.resolve(dir, ".r2mo", "app.env");
|
|
25
|
+
if (fs.existsSync(primary)) return primary;
|
|
26
|
+
const artifactId = getArtifactIdFromPom(dir) || path.basename(dir);
|
|
27
|
+
if (artifactId && artifactId !== ".") {
|
|
28
|
+
const apiEnv = path.resolve(dir, artifactId + "-api", ".r2mo", "app.env");
|
|
29
|
+
if (fs.existsSync(apiEnv)) return apiEnv;
|
|
30
|
+
}
|
|
31
|
+
return primary;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const appEnvPath = resolveAppEnvPath(cwd);
|
|
35
|
+
if (fs.existsSync(appEnvPath)) {
|
|
36
|
+
const content = fs.readFileSync(appEnvPath, "utf-8");
|
|
37
|
+
content.split(/\r?\n/).forEach((line) => {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
if (trimmed.startsWith("#") || !trimmed.startsWith("export ")) return;
|
|
40
|
+
const match = trimmed.match(/^export\s+([A-Za-z0-9_]+)=["']?([^"'\n]*)["']?/);
|
|
41
|
+
if (match) process.env[match[1]] = match[2].trim();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const required = ["Z_DB_APP_USER", "Z_DB_APP_PASS", "Z_DBS_INSTANCE"];
|
|
46
|
+
const missing = required.filter((k) => !process.env[k] || !String(process.env[k]).trim());
|
|
47
|
+
if (missing.length > 0) {
|
|
48
|
+
console.error("缺少环境变量:" + missing.join(", "));
|
|
49
|
+
console.error("请 source .r2mo/app.env 或设置 Z_DB_APP_USER, Z_DB_APP_PASS, Z_DBS_INSTANCE");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const mysql = require("mysql2/promise");
|
|
54
|
+
const dbConfig = {
|
|
55
|
+
host: process.env.Z_DB_HOST || "127.0.0.1",
|
|
56
|
+
port: parseInt(process.env.Z_DB_PORT || "3306", 10),
|
|
57
|
+
user: process.env.Z_DB_APP_USER,
|
|
58
|
+
password: process.env.Z_DB_APP_PASS,
|
|
59
|
+
database: process.env.Z_DBS_INSTANCE
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const TABLES = ["S_RESOURCE", "S_ACTION", "S_PERMISSION", "S_PERM_SET", "R_ROLE_PERM"];
|
|
63
|
+
|
|
64
|
+
async function main() {
|
|
65
|
+
const conn = await mysql.createConnection(dbConfig);
|
|
66
|
+
const lines = [];
|
|
67
|
+
try {
|
|
68
|
+
const header = "数据库:" + dbConfig.database + " @ " + dbConfig.host + ":" + dbConfig.port;
|
|
69
|
+
console.log(header);
|
|
70
|
+
lines.push(header);
|
|
71
|
+
lines.push("(以下列名用于对齐 ai ex-api / ex-perm 等命令的 SQL,请勿手写列名,以扫描为准)");
|
|
72
|
+
lines.push("---");
|
|
73
|
+
for (const table of TABLES) {
|
|
74
|
+
try {
|
|
75
|
+
const [rows] = await conn.execute("SHOW COLUMNS FROM `" + table.replace(/`/g, "") + "`");
|
|
76
|
+
const columns = rows.map((r) => r.Field);
|
|
77
|
+
const line = table + " 列:" + columns.join(", ");
|
|
78
|
+
console.log(line);
|
|
79
|
+
lines.push(line);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
const msg = table + ":表不存在或无权限 - " + e.message;
|
|
82
|
+
console.log(msg);
|
|
83
|
+
lines.push(msg);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
await conn.end();
|
|
88
|
+
}
|
|
89
|
+
const writeOut = process.argv.includes("--write");
|
|
90
|
+
if (writeOut && lines.length > 0) {
|
|
91
|
+
const outPath = path.resolve(cwd, ".r2mo", "task", "rbac-schema.txt");
|
|
92
|
+
const dir = path.dirname(outPath);
|
|
93
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
94
|
+
fs.writeFileSync(outPath, lines.join("\n") + "\n", "utf-8");
|
|
95
|
+
console.log("已写入:" + outPath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main().catch((e) => {
|
|
100
|
+
console.error(e.message);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# ai ex-api Excel 模板(R2MO-INIT 默认模板)
|
|
2
|
+
|
|
3
|
+
模板固定位于 **R2MO-INIT** 项目下的 `src/_template/EXCEL/ex-api`,与执行命令所在项目无关:任意项目执行 `ai ex-api` 时,均使用本目录下的默认模板驱动生成 RBAC_RESOURCE / RBAC_ROLE 的 xlsx,保证输出风格与模板一致。
|
|
4
|
+
|
|
5
|
+
- **template-RBAC_RESOURCE.xlsx**:RBAC 资源/权限集(S_PERM_SET 等 {TABLE} 区域)。
|
|
6
|
+
- **template-RBAC_ROLE.xlsx**:RBAC 角色权限(R_ROLE_PERM 等 {TABLE} 区域)。
|
|
7
|
+
|
|
8
|
+
模板仅保留表头结构(含 `{TABLE}`、中文表头、英文驼峰列名),**数据行已预清空**。ai ex-api 输出时**只写入数据单元格的 value**,除此以外不修改任何 style(格式、颜色、边框、合并单元格等),不删行、不清格式,输出与模板完全一致。
|
|
9
|
+
|
|
10
|
+
- **清空并压缩模板(一次性)**:在 r2mo-init 根目录执行
|
|
11
|
+
`node script/clear-excel-template-data.js`
|
|
12
|
+
会清除上述两个 xlsx 中所有 {TABLE} 区域的数据行,并将表间空隙压缩为仅 2 行空白,写回后 ai ex-api 仅填充。
|
|
13
|
+
- 若从 ZERO_MODULE 复制了新 xlsx 覆盖本目录模板,需重新执行一次上述脚本再使用。
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"RBAC_RESOURCE": {
|
|
3
|
+
"sheetName": "DATA-RES",
|
|
4
|
+
"columns": [
|
|
5
|
+
"ID",
|
|
6
|
+
"CODE",
|
|
7
|
+
"NAME",
|
|
8
|
+
"IDENTIFIER",
|
|
9
|
+
"TYPE",
|
|
10
|
+
"LEVEL",
|
|
11
|
+
"MODE_ROLE"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"RBAC_ROLE": {
|
|
15
|
+
"sheetName": "DATA-PERM",
|
|
16
|
+
"columns": [
|
|
17
|
+
"ROLE_ID",
|
|
18
|
+
"PERM_ID"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
}
|