zero-ai 1.0.84 → 1.0.86
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/.obsidian/plugins/terminal/manifest.json +14 -0
- package/.r2mo/.obsidian/plugins/terminal/styles.css +32 -0
- package/.r2mo/task/2026-03-07/2026-03-07.12-26-02-TASK@/345/274/200/345/217/221/346/226/260/345/221/275/344/273/244 ai ex-menu.md" +310 -0
- package/.r2mo/task/task-001.md +28 -294
- package/package.json +1 -1
- package/src/commander/auth.json +6 -0
- package/src/commander-ai/fn.ex.auth.js +136 -0
- package/src/commander-ai/index.js +2 -6
- package/.r2mo/.obsidian/themes/Comfort/manifest.json +0 -11
- package/.r2mo/.obsidian/themes/Comfort/theme.css +0 -218
- package/.r2mo/.obsidian/themes/Serenity/manifest.json +0 -7
- package/.r2mo/.obsidian/themes/Serenity/theme.css +0 -7258
- package/.r2mo/.obsidian/themes/W95/manifest.json +0 -8
- package/.r2mo/.obsidian/themes/W95/theme.css +0 -768
- package/src/commander/ex-api.json +0 -8
- package/src/commander/ex-crud.json +0 -1
- package/src/commander/ex-perm.json +0 -8
- package/src/commander-ai/fn.ex.api.js +0 -999
- package/src/commander-ai/fn.ex.crud.js +0 -545
- package/src/commander-ai/fn.ex.perm.js +0 -207
|
@@ -1,999 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const Ec = require("../epic");
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
const path = require("path");
|
|
6
|
-
const Ut = require("../commander-shared");
|
|
7
|
-
const yaml = require("js-yaml");
|
|
8
|
-
const inquirer = require("inquirer");
|
|
9
|
-
const { v4: uuidv4 } = require("uuid");
|
|
10
|
-
|
|
11
|
-
const CONFIG_DIR = ".r2mo/task/command/ex-api";
|
|
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
|
-
const REQUIRED_ENV_APP = ["Z_APP_ID", "Z_TENANT", "Z_SIGMA"];
|
|
14
|
-
const R2_BY_UUID = "9a0d5018-33ad-4c64-80bf-8ae7947c482f";
|
|
15
|
-
const REFERENCE_ROLE_ID = "e501b47a-c08b-4c83-b12b-95ad82873e96";
|
|
16
|
-
|
|
17
|
-
/** 全局列(开发时按 RBAC Flyway 建表固定,执行时仅 DML,不查元数据):S_RESOURCE/S_ACTION/S_PERMISSION 写入,Excel 不写入 */
|
|
18
|
-
const GLOBAL_COLUMNS = [
|
|
19
|
-
{ name: "SIGMA", value: () => process.env.Z_SIGMA || "" },
|
|
20
|
-
{ name: "APP_ID", value: () => process.env.Z_APP_ID || "" },
|
|
21
|
-
{ name: "TENANT_ID", value: () => process.env.Z_TENANT || "" },
|
|
22
|
-
{ name: "CREATED_BY", value: () => R2_BY_UUID },
|
|
23
|
-
{ name: "UPDATED_BY", value: () => R2_BY_UUID },
|
|
24
|
-
{ name: "CREATED_AT", value: () => new Date().toISOString().slice(0, 19).replace("T", " ") },
|
|
25
|
-
{ name: "UPDATED_AT", value: () => new Date().toISOString().slice(0, 19).replace("T", " ") }
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
function getGlobalColsAndVals() {
|
|
29
|
-
const cols = GLOBAL_COLUMNS.map((c) => c.name);
|
|
30
|
-
const vals = GLOBAL_COLUMNS.map((c) => c.value());
|
|
31
|
-
return { cols, vals };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 扫描 sheet 中所有 {TABLE} 区域:某行首格为 "{TABLE}" 则开启一个表,下一格为表名;紧跟 2 行为表头(中文、英文),之后为数据区;下一处 {TABLE} 或 sheet 末为数据区结束。
|
|
36
|
-
* 返回 [{ tableName, tableStartRow, dataStartRow, dataEndRow, columnNames: [name], columnIndex: { name: colNum } }, ...]
|
|
37
|
-
*/
|
|
38
|
-
function scanTableRegions(ws, maxScanRows) {
|
|
39
|
-
if (!ws) return [];
|
|
40
|
-
const regions = [];
|
|
41
|
-
const limit = maxScanRows || 5000;
|
|
42
|
-
let i = 1;
|
|
43
|
-
while (i <= limit) {
|
|
44
|
-
const row = ws.getRow(i);
|
|
45
|
-
const first = row.getCell(1).value;
|
|
46
|
-
const v = first != null ? String(first).trim() : "";
|
|
47
|
-
if (v === "{TABLE}") {
|
|
48
|
-
const tableNameCell = row.getCell(2).value;
|
|
49
|
-
const tableName = tableNameCell != null ? String(tableNameCell).trim() : "";
|
|
50
|
-
const headerRowCount = 2;
|
|
51
|
-
const dataStartRow = i + 1 + headerRowCount;
|
|
52
|
-
let dataEndRow = dataStartRow - 1;
|
|
53
|
-
let j = i + 1;
|
|
54
|
-
while (j <= limit) {
|
|
55
|
-
const nextRow = ws.getRow(j);
|
|
56
|
-
const nextFirst = nextRow.getCell(1).value;
|
|
57
|
-
const nv = nextFirst != null ? String(nextFirst).trim() : "";
|
|
58
|
-
if (nv === "{TABLE}") {
|
|
59
|
-
dataEndRow = j - 1;
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
dataEndRow = j;
|
|
63
|
-
j++;
|
|
64
|
-
}
|
|
65
|
-
const enHeaderRow = ws.getRow(i + 2);
|
|
66
|
-
const columnNames = [];
|
|
67
|
-
const columnIndex = {};
|
|
68
|
-
enHeaderRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
|
69
|
-
const val = cell && cell.value != null ? String(cell.value).trim() : "";
|
|
70
|
-
if (val) {
|
|
71
|
-
columnNames.push(val);
|
|
72
|
-
columnIndex[val] = colNumber;
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
regions.push({
|
|
76
|
-
tableName,
|
|
77
|
-
tableStartRow: i,
|
|
78
|
-
dataStartRow,
|
|
79
|
-
dataEndRow,
|
|
80
|
-
columnNames,
|
|
81
|
-
columnIndex
|
|
82
|
-
});
|
|
83
|
-
i = dataEndRow + 1;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
i++;
|
|
87
|
-
}
|
|
88
|
-
return regions;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** 在指定 TABLE 区域内找最后一行有数据的行号(按首列非空判断),返回下一行用于追加;若无则返回 dataStartRow */
|
|
92
|
-
function findAppendRowInRegion(ws, dataStartRow, dataEndRow) {
|
|
93
|
-
let last = dataStartRow - 1;
|
|
94
|
-
for (let r = dataStartRow; r <= dataEndRow; r++) {
|
|
95
|
-
const cell = ws.getRow(r).getCell(1).value;
|
|
96
|
-
if (cell != null && String(cell).trim() !== "") last = r;
|
|
97
|
-
}
|
|
98
|
-
return last + 1;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** 将 uri 转为文件名安全片段:/ -> -,去首尾 -,长度限制 */
|
|
102
|
-
function uriToFileNameSlug(uri) {
|
|
103
|
-
if (!uri || !String(uri).trim()) return "default";
|
|
104
|
-
return String(uri)
|
|
105
|
-
.trim()
|
|
106
|
-
.replace(/\//g, "-")
|
|
107
|
-
.replace(/-+/g, "-")
|
|
108
|
-
.replace(/^-|-$/g, "")
|
|
109
|
-
.slice(0, 120);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** 无 target 时解析到 -api 项目目录(当前为父目录时取 xxx-api,已在 -api 内则用 cwd) */
|
|
113
|
-
function resolveExcelRoot(cwd, target) {
|
|
114
|
-
if (target && target.root && target.module) {
|
|
115
|
-
const zeroModule = process.env.ZERO_MODULE;
|
|
116
|
-
return path.resolve(zeroModule, `zero-exmodule-${target.module}`);
|
|
117
|
-
}
|
|
118
|
-
const artifactId = getArtifactIdFromPom(cwd);
|
|
119
|
-
const apiDir = artifactId ? path.resolve(cwd, artifactId + "-api") : null;
|
|
120
|
-
if (apiDir && fs.existsSync(apiDir)) return apiDir;
|
|
121
|
-
return cwd;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function getArtifactIdFromPom(cwd) {
|
|
125
|
-
const pomPath = path.resolve(cwd, "pom.xml");
|
|
126
|
-
if (!fs.existsSync(pomPath)) return null;
|
|
127
|
-
let content = fs.readFileSync(pomPath, "utf-8");
|
|
128
|
-
content = content.replace(/<parent>[\s\S]*?<\/parent>/i, "");
|
|
129
|
-
const m = content.match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
130
|
-
return m ? m[1].trim() : null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function loadAppEnv(filePath) {
|
|
134
|
-
if (!fs.existsSync(filePath)) return false;
|
|
135
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
136
|
-
content.split(/\r?\n/).forEach((line) => {
|
|
137
|
-
const trimmed = line.trim();
|
|
138
|
-
if (trimmed.startsWith("#") || !trimmed.startsWith("export ")) return;
|
|
139
|
-
const match = trimmed.match(/^export\s+([A-Za-z0-9_]+)=["']?([^"'\n]*)["']?/);
|
|
140
|
-
if (match) process.env[match[1]] = match[2].trim();
|
|
141
|
-
});
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function resolveAppEnvPath(cwd) {
|
|
146
|
-
const primary = path.resolve(cwd, ".r2mo", "app.env");
|
|
147
|
-
if (fs.existsSync(primary)) return primary;
|
|
148
|
-
let artifactId = getArtifactIdFromPom(cwd);
|
|
149
|
-
if (!artifactId) artifactId = path.basename(cwd);
|
|
150
|
-
if (artifactId && artifactId !== ".") {
|
|
151
|
-
const apiDir = `${artifactId}-api`;
|
|
152
|
-
const nested = path.resolve(cwd, apiDir, ".r2mo", "app.env");
|
|
153
|
-
if (fs.existsSync(nested)) return nested;
|
|
154
|
-
const sibling = path.resolve(cwd, "..", apiDir, ".r2mo", "app.env");
|
|
155
|
-
if (fs.existsSync(sibling)) return sibling;
|
|
156
|
-
}
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function checkEnv(keys, label) {
|
|
161
|
-
const missing = keys.filter((k) => !process.env[k] || !String(process.env[k]).trim());
|
|
162
|
-
if (missing.length > 0) {
|
|
163
|
-
Ec.error(`${label}:以下环境变量必须全部已设置。`);
|
|
164
|
-
Ec.info("当前缺失:" + missing.join(", "));
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function isDpaRoot(dir) {
|
|
170
|
-
const pom = path.join(dir, "pom.xml");
|
|
171
|
-
if (!fs.existsSync(pom)) return false;
|
|
172
|
-
const id = getArtifactIdFromPom(dir);
|
|
173
|
-
if (!id) return false;
|
|
174
|
-
const apiDir = path.join(dir, `${id}-api`);
|
|
175
|
-
const domainDir = path.join(dir, `${id}-domain`);
|
|
176
|
-
return fs.existsSync(apiDir) && fs.existsSync(domainDir);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** 校验 metadata.r:须为 "<METHOD> <uri>",METHOD 常见动词,uri 以 /api 开头 */
|
|
180
|
-
function validateExApiR(r) {
|
|
181
|
-
if (!r || typeof r !== "string") return { valid: false, error: "r 为空" };
|
|
182
|
-
const s = String(r).trim();
|
|
183
|
-
if (!s) return { valid: false, error: "r 为空" };
|
|
184
|
-
const parts = s.split(/\s+/);
|
|
185
|
-
if (parts.length < 2) return { valid: false, error: "r 须为 \"<METHOD> <uri>\" 两段" };
|
|
186
|
-
const method = (parts[0] || "").toUpperCase();
|
|
187
|
-
const uri = parts.slice(1).join(" ").trim();
|
|
188
|
-
const allowed = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
189
|
-
if (!allowed.includes(method)) return { valid: false, error: "METHOD 须为 " + allowed.join("/") };
|
|
190
|
-
if (!uri.startsWith("/api")) return { valid: false, error: "uri 须以 /api 为前缀" };
|
|
191
|
-
return { valid: true };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** 解析 ex-api 配置目录:cwd / 上级 / 上上级 */
|
|
195
|
-
function resolveExApiConfigDir(cwd) {
|
|
196
|
-
const primary = path.resolve(cwd, CONFIG_DIR);
|
|
197
|
-
if (fs.existsSync(primary) && fs.statSync(primary).isDirectory()) return primary;
|
|
198
|
-
const parent = path.resolve(cwd, "..", CONFIG_DIR);
|
|
199
|
-
if (fs.existsSync(parent) && fs.statSync(parent).isDirectory()) return parent;
|
|
200
|
-
const grand = path.resolve(cwd, "..", "..", CONFIG_DIR);
|
|
201
|
-
if (fs.existsSync(grand) && fs.statSync(grand).isDirectory()) return grand;
|
|
202
|
-
return primary;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** 表格化打印 ex-api 汇总 */
|
|
206
|
-
function printExApiTable(results) {
|
|
207
|
-
const rows = results.map((r) => ({
|
|
208
|
-
identifier: r.identifier || "—",
|
|
209
|
-
request: r.request || "—",
|
|
210
|
-
ok: r.ok ? "✓" : "✗",
|
|
211
|
-
error: r.error || "—"
|
|
212
|
-
}));
|
|
213
|
-
const col = (arr, key) => arr.map((x) => String(x[key] != null ? x[key] : ""));
|
|
214
|
-
const max = (arr) => Math.max(2, ...arr.map((s) => (s && s.length) || 0));
|
|
215
|
-
const wId = max(col(rows, "identifier"));
|
|
216
|
-
const wReq = Math.min(max(col(rows, "request")), 48);
|
|
217
|
-
const wErr = Math.min(max(col(rows, "error")), 32);
|
|
218
|
-
const sep = " | ";
|
|
219
|
-
Ec.info("[ex-api] 汇总:");
|
|
220
|
-
Ec.info(" " + "identifier".padEnd(wId) + sep + "request".padEnd(wReq) + sep + "ok" + sep + "error".padEnd(wErr));
|
|
221
|
-
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)));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/** 单条 API 执行:使用 config.metadata.r 作为 request,执行 DB + Excel,返回汇总 */
|
|
225
|
-
async function runOneExApi(cwd, conn, config, requestRaw, skip) {
|
|
226
|
-
const metadata = config.metadata;
|
|
227
|
-
const target = config.target;
|
|
228
|
-
let method = "";
|
|
229
|
-
let uri = "";
|
|
230
|
-
if (requestRaw && String(requestRaw).trim()) {
|
|
231
|
-
const parts = String(requestRaw).trim().split(/\s+/);
|
|
232
|
-
if (parts.length >= 2) {
|
|
233
|
-
method = parts[0].toUpperCase();
|
|
234
|
-
uri = parts.slice(1).join(" ").trim();
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (!skip && (!method || !uri)) {
|
|
238
|
-
return { identifier: metadata.identifier || "—", request: requestRaw || "—", ok: false, error: "缺少 metadata.r 或格式非 \"<METHOD> <uri>\"" };
|
|
239
|
-
}
|
|
240
|
-
if (!skip && uri && !uri.trim().startsWith("/api")) {
|
|
241
|
-
return { identifier: metadata.identifier || "—", request: requestRaw || "—", ok: false, error: "uri 必须以 /api 为前缀" };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
const appId = process.env.Z_APP_ID;
|
|
246
|
-
const tenantId = process.env.Z_TENANT;
|
|
247
|
-
const sigma = process.env.Z_SIGMA;
|
|
248
|
-
|
|
249
|
-
let actionId = null;
|
|
250
|
-
let resourceId = null;
|
|
251
|
-
let insertedResource = null;
|
|
252
|
-
let insertedAction = null;
|
|
253
|
-
let insertedPermission = null;
|
|
254
|
-
const insertedRolePerms = [];
|
|
255
|
-
|
|
256
|
-
if (!skip && method && uri) {
|
|
257
|
-
const level = metadata.level != null ? metadata.level : 1;
|
|
258
|
-
const brief = metadata.brief || "";
|
|
259
|
-
const identifier = metadata.identifier || "default";
|
|
260
|
-
const keyword = metadata.keyword && String(metadata.keyword).trim();
|
|
261
|
-
const resourceCode = keyword
|
|
262
|
-
? "res." + keyword
|
|
263
|
-
: ("res_" + (metadata.resource || "api").replace(/\./g, "_") + "_" + method + "_" + uri.replace(/\//g, "_").replace(/^\s+|\s+$/g, "").slice(0, 48) || "res_api");
|
|
264
|
-
const actionCode = keyword ? "act." + keyword : "act_" + (metadata.resource || "api").replace(/\./g, "_");
|
|
265
|
-
|
|
266
|
-
// 实体表按 CODE 去重:先按 METHOD+URI 查,再按 CODE+SIGMA 查,存在则不插入
|
|
267
|
-
let [rows] = await conn.execute(
|
|
268
|
-
"SELECT ID, RESOURCE_ID FROM S_ACTION WHERE METHOD = ? AND URI = ? LIMIT 2",
|
|
269
|
-
[method, uri]
|
|
270
|
-
);
|
|
271
|
-
if (rows && rows.length > 1) {
|
|
272
|
-
[rows] = await conn.execute(
|
|
273
|
-
"SELECT ID, RESOURCE_ID FROM S_ACTION WHERE METHOD = ? AND URI = ? AND SIGMA = ? AND APP_ID = ? AND TENANT_ID = ? LIMIT 1",
|
|
274
|
-
[method, uri, sigma, appId, tenantId]
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
if (rows && rows.length > 0) {
|
|
278
|
-
actionId = rows[0].ID;
|
|
279
|
-
resourceId = rows[0].RESOURCE_ID;
|
|
280
|
-
Ec.info("[ex-api] 已存在 S_ACTION(METHOD+URI),ID=" + actionId);
|
|
281
|
-
} else {
|
|
282
|
-
const [actByCode] = await conn.execute("SELECT ID, RESOURCE_ID FROM S_ACTION WHERE CODE = ? AND SIGMA = ? LIMIT 1", [actionCode, sigma]);
|
|
283
|
-
if (actByCode && actByCode.length > 0) {
|
|
284
|
-
actionId = actByCode[0].ID;
|
|
285
|
-
resourceId = actByCode[0].RESOURCE_ID;
|
|
286
|
-
Ec.info("[ex-api] 已存在 S_ACTION(CODE 去重),ID=" + actionId);
|
|
287
|
-
} else {
|
|
288
|
-
const [resRows] = await conn.execute("SELECT ID FROM S_RESOURCE WHERE CODE = ? AND SIGMA = ? LIMIT 1", [resourceCode, sigma]);
|
|
289
|
-
if (resRows && resRows[0]) {
|
|
290
|
-
resourceId = resRows[0].ID;
|
|
291
|
-
Ec.info("[ex-api] 已存在 S_RESOURCE(CODE 去重),ID=" + resourceId);
|
|
292
|
-
} else {
|
|
293
|
-
resourceId = uuidv4();
|
|
294
|
-
const resBaseCols = ["ID", "NAME", "CODE", "IDENTIFIER", "TYPE", "LEVEL", "MODE_ROLE"];
|
|
295
|
-
const resBaseVals = [resourceId, brief, resourceCode, identifier, metadata.resource || "resource.ambient", level, "UNION"];
|
|
296
|
-
const resGlobal = getGlobalColsAndVals();
|
|
297
|
-
const resCols = resBaseCols.concat(resGlobal.cols);
|
|
298
|
-
const resVals = resBaseVals.concat(resGlobal.vals);
|
|
299
|
-
const resPlaceholders = resCols.map(() => "?").join(", ");
|
|
300
|
-
await conn.execute(
|
|
301
|
-
"INSERT INTO S_RESOURCE (" + resCols.join(", ") + ") VALUES (" + resPlaceholders + ")",
|
|
302
|
-
resVals
|
|
303
|
-
);
|
|
304
|
-
insertedResource = { CODE: resourceCode, NAME: brief, IDENTIFIER: identifier, TYPE: metadata.resource || "resource.ambient", LEVEL: level, MODE_ROLE: "UNION" };
|
|
305
|
-
Ec.info("[ex-api] 已插入 S_RESOURCE,ID=" + resourceId);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
actionId = uuidv4();
|
|
309
|
-
const actBaseCols = ["ID", "CODE", "NAME", "RESOURCE_ID", "METHOD", "URI", "LEVEL"];
|
|
310
|
-
const actBaseVals = [actionId, actionCode, brief, resourceId, method, uri, level];
|
|
311
|
-
const actGlobal = getGlobalColsAndVals();
|
|
312
|
-
const actCols = actBaseCols.concat(actGlobal.cols);
|
|
313
|
-
const actVals = actBaseVals.concat(actGlobal.vals);
|
|
314
|
-
const actPlaceholders = actCols.map(() => "?").join(", ");
|
|
315
|
-
await conn.execute(
|
|
316
|
-
"INSERT INTO S_ACTION (" + actCols.join(", ") + ") VALUES (" + actPlaceholders + ")",
|
|
317
|
-
actVals
|
|
318
|
-
);
|
|
319
|
-
insertedAction = { CODE: actionCode, NAME: brief, RESOURCE_ID: resourceId, METHOD: method, URI: uri, LEVEL: level };
|
|
320
|
-
Ec.info("[ex-api] 已插入 S_ACTION,ID=" + actionId);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
let permissionId = null;
|
|
326
|
-
const permIdentifier = metadata.identifier || "default";
|
|
327
|
-
let permissionMode = "new";
|
|
328
|
-
|
|
329
|
-
if (!skip) {
|
|
330
|
-
const ans = await inquirer.prompt([
|
|
331
|
-
{
|
|
332
|
-
type: "list",
|
|
333
|
-
name: "permissionMode",
|
|
334
|
-
message: "追加新权限 / 使用已有权限?",
|
|
335
|
-
choices: [
|
|
336
|
-
{ name: "追加新权限", value: "new" },
|
|
337
|
-
{ name: "使用已有权限(按 identifier 选择)", value: "existing" }
|
|
338
|
-
]
|
|
339
|
-
}
|
|
340
|
-
]);
|
|
341
|
-
permissionMode = ans.permissionMode;
|
|
342
|
-
|
|
343
|
-
if (permissionMode === "existing") {
|
|
344
|
-
const [permRows] = await conn.execute("SELECT ID, CODE, NAME FROM S_PERMISSION WHERE IDENTIFIER = ?", [permIdentifier]);
|
|
345
|
-
if (!permRows || permRows.length === 0) {
|
|
346
|
-
Ec.info("当前 identifier 下无已有权限,将按新权限创建");
|
|
347
|
-
permissionMode = "new";
|
|
348
|
-
} else {
|
|
349
|
-
const { selectedPerm } = await inquirer.prompt([
|
|
350
|
-
{
|
|
351
|
-
type: "list",
|
|
352
|
-
name: "selectedPerm",
|
|
353
|
-
message: "选择要追加到的权限",
|
|
354
|
-
choices: permRows.map((r) => ({ name: `${r.NAME} (${r.CODE})`, value: r.ID }))
|
|
355
|
-
}
|
|
356
|
-
]);
|
|
357
|
-
permissionId = selectedPerm;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
if (permissionMode === "new" || !permissionId) {
|
|
361
|
-
const brief = metadata.brief || "";
|
|
362
|
-
const keyword = metadata.keyword && String(metadata.keyword).trim();
|
|
363
|
-
const permCode = keyword ? "perm." + keyword : ("perm_" + (metadata.resource || "api").replace(/\./g, "_").slice(0, 64));
|
|
364
|
-
const [ex] = await conn.execute("SELECT ID FROM S_PERMISSION WHERE CODE = ? AND SIGMA = ? LIMIT 1", [permCode, sigma]);
|
|
365
|
-
if (ex && ex[0]) {
|
|
366
|
-
permissionId = ex[0].ID;
|
|
367
|
-
Ec.info("[ex-api] 已存在 S_PERMISSION(CODE 去重),ID=" + permissionId);
|
|
368
|
-
} else {
|
|
369
|
-
permissionId = uuidv4();
|
|
370
|
-
const permBaseCols = ["ID", "CODE", "NAME", "IDENTIFIER", "COMMENT"];
|
|
371
|
-
const permBaseVals = [permissionId, permCode, brief, permIdentifier, brief];
|
|
372
|
-
const permGlobal = getGlobalColsAndVals();
|
|
373
|
-
const permCols = permBaseCols.concat(permGlobal.cols);
|
|
374
|
-
const permVals = permBaseVals.concat(permGlobal.vals);
|
|
375
|
-
const permPlaceholders = permCols.map(() => "?").join(", ");
|
|
376
|
-
await conn.execute(
|
|
377
|
-
"INSERT INTO S_PERMISSION (" + permCols.join(", ") + ") VALUES (" + permPlaceholders + ")",
|
|
378
|
-
permVals
|
|
379
|
-
);
|
|
380
|
-
insertedPermission = { CODE: permCode, NAME: brief, IDENTIFIER: permIdentifier, COMMENT: brief };
|
|
381
|
-
Ec.info("[ex-api] 已插入 S_PERMISSION,ID=" + permissionId);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
let roleIds = [];
|
|
387
|
-
let roleIdToCode = {}; // roleId -> CODE,供输出路径分流使用
|
|
388
|
-
let roleIdToName = {}; // roleId -> NAME,供汇总输出使用
|
|
389
|
-
if (!skip) {
|
|
390
|
-
const [roleRows] = await conn.execute("SELECT ID, NAME, CODE FROM S_ROLE ORDER BY NAME");
|
|
391
|
-
if (!roleRows || roleRows.length === 0) {
|
|
392
|
-
Ec.info("[ex-api] S_ROLE 中无角色,跳过授权");
|
|
393
|
-
} else {
|
|
394
|
-
// 建立 roleId -> CODE/NAME 映射(所有角色)
|
|
395
|
-
roleRows.forEach((r) => {
|
|
396
|
-
const id = String(r.ID != null ? r.ID : r.id);
|
|
397
|
-
roleIdToCode[id] = r.CODE || "";
|
|
398
|
-
roleIdToName[id] = r.NAME || "";
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// 过滤掉超级管理员角色(固定输出,不需要用户选择)
|
|
402
|
-
const selectableRoles = roleRows.filter((r) => {
|
|
403
|
-
const id = String(r.ID != null ? r.ID : r.id);
|
|
404
|
-
return id !== REFERENCE_ROLE_ID;
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
if (selectableRoles.length > 0) {
|
|
408
|
-
const answer = await inquirer.prompt([
|
|
409
|
-
{
|
|
410
|
-
type: "checkbox",
|
|
411
|
-
name: "selectedRoles",
|
|
412
|
-
message: "选择要授权当前 API 的角色(可多选,超级管理员已自动包含)",
|
|
413
|
-
choices: selectableRoles.map((r) => ({ name: `${r.NAME || r.CODE} (${r.CODE || '-'})`, value: String(r.ID != null ? r.ID : r.id) })),
|
|
414
|
-
pageSize: 999
|
|
415
|
-
}
|
|
416
|
-
]);
|
|
417
|
-
const raw = answer.selectedRoles;
|
|
418
|
-
if (Array.isArray(raw)) {
|
|
419
|
-
roleIds = raw.map((id) => (id != null ? String(id) : id));
|
|
420
|
-
} else if (raw != null && raw !== "") {
|
|
421
|
-
roleIds = [String(raw)];
|
|
422
|
-
} else {
|
|
423
|
-
roleIds = [];
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
Ec.info("[ex-api] 已选角色数(不含超级管理员):" + roleIds.length + (roleIds.length > 0 ? ",ID=" + roleIds.slice(0, 5).join(",") + (roleIds.length > 5 ? "..." : "") : ""));
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (!skip && permissionId && roleIds.length > 0) {
|
|
431
|
-
for (const roleId of roleIds) {
|
|
432
|
-
await conn.execute(
|
|
433
|
-
"INSERT IGNORE INTO R_ROLE_PERM (ROLE_ID, PERM_ID) VALUES (?, ?)",
|
|
434
|
-
[roleId, permissionId]
|
|
435
|
-
);
|
|
436
|
-
insertedRolePerms.push({ ROLE_ID: roleId, PERM_ID: permissionId });
|
|
437
|
-
}
|
|
438
|
-
Ec.info("[ex-api] 已同步 R_ROLE_PERM,角色数:" + roleIds.length);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// 汇总与 Excel 写入前:查询已有记录,供 Excel 填充(本次未插入时也写出当前 resource/role-perm)
|
|
442
|
-
let existingResource = null;
|
|
443
|
-
let existingAction = null;
|
|
444
|
-
let existingPermission = null;
|
|
445
|
-
if (resourceId && !insertedResource) {
|
|
446
|
-
const [rows] = await conn.execute("SELECT ID, CODE, NAME, IDENTIFIER, TYPE, LEVEL, MODE_ROLE FROM S_RESOURCE WHERE ID = ? LIMIT 1", [resourceId]);
|
|
447
|
-
if (rows && rows[0]) existingResource = rows[0];
|
|
448
|
-
}
|
|
449
|
-
if (actionId && !insertedAction) {
|
|
450
|
-
const [rows] = await conn.execute("SELECT ID, CODE, NAME, RESOURCE_ID, METHOD, URI, LEVEL FROM S_ACTION WHERE ID = ? LIMIT 1", [actionId]);
|
|
451
|
-
if (rows && rows[0]) existingAction = rows[0];
|
|
452
|
-
}
|
|
453
|
-
if (permissionId && !insertedPermission) {
|
|
454
|
-
const [rows] = await conn.execute("SELECT ID, CODE, NAME, IDENTIFIER, COMMENT FROM S_PERMISSION WHERE ID = ? LIMIT 1", [permissionId]);
|
|
455
|
-
if (rows && rows[0]) existingPermission = rows[0];
|
|
456
|
-
}
|
|
457
|
-
let existingRolePerms = [];
|
|
458
|
-
if (permissionId && insertedRolePerms.length === 0 && (!roleIds || roleIds.length === 0)) {
|
|
459
|
-
try {
|
|
460
|
-
const [rpRows] = await conn.execute("SELECT ROLE_ID, PERM_ID FROM R_ROLE_PERM WHERE PERM_ID = ?", [permissionId]);
|
|
461
|
-
if (rpRows && rpRows.length > 0) {
|
|
462
|
-
Ec.info("[ex-api] R_ROLE_PERM 从库中提取 " + rpRows.length + " 条(PERM_ID=" + permissionId + ")");
|
|
463
|
-
existingRolePerms = rpRows.map((r) => ({
|
|
464
|
-
ROLE_ID: r.ROLE_ID != null ? r.ROLE_ID : r.role_id,
|
|
465
|
-
PERM_ID: r.PERM_ID != null ? r.PERM_ID : r.perm_id
|
|
466
|
-
}));
|
|
467
|
-
}
|
|
468
|
-
} catch (e) {
|
|
469
|
-
Ec.info("[ex-api] R_ROLE_PERM 查询失败: " + e.message);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
Ec.info("[ex-api] R_ROLE_PERM 查询失败: " + e.message);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
// 四张表行数据(Excel 列名与模板英文表头一致);S_PERM_SET 的 name/type 来自配置 pname/ptype
|
|
477
|
-
const resRow = resourceId && (insertedResource || existingResource) ? (insertedResource || existingResource) : null;
|
|
478
|
-
const actRow = actionId && (insertedAction || existingAction) ? (insertedAction || existingAction) : null;
|
|
479
|
-
const permRow = permissionId && (insertedPermission || existingPermission) ? (insertedPermission || existingPermission) : null;
|
|
480
|
-
const rowS_RESOURCE = resRow
|
|
481
|
-
? {
|
|
482
|
-
key: resourceId,
|
|
483
|
-
name: resRow.NAME,
|
|
484
|
-
modeRole: resRow.MODE_ROLE || "UNION",
|
|
485
|
-
code: resRow.CODE,
|
|
486
|
-
identifier: resRow.IDENTIFIER,
|
|
487
|
-
type: resRow.TYPE,
|
|
488
|
-
level: resRow.LEVEL,
|
|
489
|
-
modeGroup: "",
|
|
490
|
-
modeTree: ""
|
|
491
|
-
}
|
|
492
|
-
: null;
|
|
493
|
-
const rowS_ACTION = actRow && resourceId && permissionId
|
|
494
|
-
? {
|
|
495
|
-
key: actionId,
|
|
496
|
-
resourceId,
|
|
497
|
-
permissionId,
|
|
498
|
-
code: actRow.CODE,
|
|
499
|
-
method: actRow.METHOD,
|
|
500
|
-
uri: actRow.URI,
|
|
501
|
-
name: actRow.NAME,
|
|
502
|
-
level: actRow.LEVEL,
|
|
503
|
-
renewalCredit: ""
|
|
504
|
-
}
|
|
505
|
-
: null;
|
|
506
|
-
const rowS_PERMISSION = permRow
|
|
507
|
-
? {
|
|
508
|
-
key: permissionId,
|
|
509
|
-
name: permRow.NAME,
|
|
510
|
-
comment: permRow.COMMENT,
|
|
511
|
-
code: permRow.CODE,
|
|
512
|
-
identifier: permRow.IDENTIFIER
|
|
513
|
-
}
|
|
514
|
-
: null;
|
|
515
|
-
// S_PERM_SET:name/type 必须从配置 pname/ptype 提取,key 与资源一致,code 与权限一致
|
|
516
|
-
const rowS_PERM_SET = resourceId && resRow && permRow
|
|
517
|
-
? {
|
|
518
|
-
key: resourceId,
|
|
519
|
-
code: permRow.CODE,
|
|
520
|
-
name: (metadata.pname != null && metadata.pname !== "") ? metadata.pname : resRow.NAME,
|
|
521
|
-
type: (metadata.ptype != null && metadata.ptype !== "") ? metadata.ptype : resRow.TYPE
|
|
522
|
-
}
|
|
523
|
-
: null;
|
|
524
|
-
// 写入 Excel 的数据(仅用于写 Excel,不写库):
|
|
525
|
-
// 1. 超级管理员角色(REFERENCE_ROLE_ID)始终包含(如果有 permissionId)
|
|
526
|
-
// 2. 用户选中的其他角色
|
|
527
|
-
let rolePermsToWrite = [];
|
|
528
|
-
|
|
529
|
-
// 始终添加超级管理员角色
|
|
530
|
-
if (permissionId) {
|
|
531
|
-
rolePermsToWrite.push({ ROLE_ID: REFERENCE_ROLE_ID, PERM_ID: permissionId });
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// 添加用户选中的角色
|
|
535
|
-
if (permissionId && roleIds && roleIds.length > 0) {
|
|
536
|
-
roleIds.forEach((rid) => {
|
|
537
|
-
rolePermsToWrite.push({ ROLE_ID: rid, PERM_ID: permissionId });
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// 如果本次有插入记录(R_ROLE_PERM),也合并进来(去重)
|
|
542
|
-
if (insertedRolePerms.length > 0) {
|
|
543
|
-
insertedRolePerms.forEach((rp) => {
|
|
544
|
-
const rid = rp.ROLE_ID != null ? String(rp.ROLE_ID) : String(rp.role_id);
|
|
545
|
-
const pid = rp.PERM_ID != null ? String(rp.PERM_ID) : String(rp.perm_id);
|
|
546
|
-
const exists = rolePermsToWrite.some((p) => String(p.ROLE_ID) === rid && String(p.PERM_ID) === pid);
|
|
547
|
-
if (!exists) {
|
|
548
|
-
rolePermsToWrite.push({ ROLE_ID: rid, PERM_ID: pid });
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// 如果库中已有记录(existingRolePerms),也合并进来(去重)
|
|
554
|
-
if (existingRolePerms.length > 0) {
|
|
555
|
-
existingRolePerms.forEach((rp) => {
|
|
556
|
-
const rid = rp.ROLE_ID != null ? String(rp.ROLE_ID) : String(rp.role_id);
|
|
557
|
-
const pid = rp.PERM_ID != null ? String(rp.PERM_ID) : String(rp.perm_id);
|
|
558
|
-
const exists = rolePermsToWrite.some((p) => String(p.ROLE_ID) === rid && String(p.PERM_ID) === pid);
|
|
559
|
-
if (!exists) {
|
|
560
|
-
rolePermsToWrite.push({ ROLE_ID: rid, PERM_ID: pid });
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
rolePermsToWrite = rolePermsToWrite.map((p) => ({
|
|
566
|
-
ROLE_ID: p.ROLE_ID != null ? p.ROLE_ID : p.role_id,
|
|
567
|
-
PERM_ID: p.PERM_ID != null ? p.PERM_ID : p.perm_id
|
|
568
|
-
}));
|
|
569
|
-
|
|
570
|
-
Ec.info("[ex-api] 📋 R_ROLE_PERM 写入前数据:");
|
|
571
|
-
Ec.info("[ex-api] insertedRolePerms.length = " + insertedRolePerms.length);
|
|
572
|
-
Ec.info("[ex-api] existingRolePerms.length = " + existingRolePerms.length);
|
|
573
|
-
Ec.info("[ex-api] permissionId = " + (permissionId || "—"));
|
|
574
|
-
Ec.info("[ex-api] roleIds.length = " + (roleIds ? roleIds.length : 0));
|
|
575
|
-
Ec.info("[ex-api] rolePermsToWrite.length = " + rolePermsToWrite.length);
|
|
576
|
-
if (rolePermsToWrite.length > 0) {
|
|
577
|
-
rolePermsToWrite.slice(0, 10).forEach((p, i) => {
|
|
578
|
-
Ec.info("[ex-api] rolePermsToWrite[" + i + "] = { ROLE_ID: " + (p.ROLE_ID != null ? p.ROLE_ID : "undefined") + ", PERM_ID: " + (p.PERM_ID != null ? p.PERM_ID : "undefined") + " }");
|
|
579
|
-
});
|
|
580
|
-
if (rolePermsToWrite.length > 10) Ec.info("[ex-api] ... 共 " + rolePermsToWrite.length + " 条");
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Excel 输出:有 target 时为 DPA zero-exmodule-{module};无 target 时输出到 -api 项目;文件名固化 identifier-method-uri
|
|
584
|
-
const excelRoot = resolveExcelRoot(cwd, target);
|
|
585
|
-
const domainName = target && target.module ? `zero-exmodule-${target.module}-domain` : null;
|
|
586
|
-
const pluginsBase = domainName
|
|
587
|
-
? path.join(excelRoot, domainName, "src", "main", "resources", "plugins")
|
|
588
|
-
: path.join(excelRoot, "src", "main", "resources", "plugins");
|
|
589
|
-
const pluginId = domainName ? `zero-exmodule-${target.module}` : "zero-launcher-configuration";
|
|
590
|
-
const rbacResourceDir = path.join(pluginsBase, pluginId, "security", "RBAC_RESOURCE");
|
|
591
|
-
const rbacRoleDir = path.join(pluginsBase, pluginId, "security", "RBAC_ROLE", "ADMIN.SUPER");
|
|
592
|
-
|
|
593
|
-
if (!fs.existsSync(rbacResourceDir)) fs.mkdirSync(rbacResourceDir, { recursive: true });
|
|
594
|
-
if (!fs.existsSync(rbacRoleDir)) fs.mkdirSync(rbacRoleDir, { recursive: true });
|
|
595
|
-
|
|
596
|
-
const identifierSlug = (metadata.identifier || "api").replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
597
|
-
const methodSlug = (method || "GET").toUpperCase();
|
|
598
|
-
const uriSlug = uriToFileNameSlug(uri);
|
|
599
|
-
const defaultFileName = `${identifierSlug}-${methodSlug}-${uriSlug}.xlsx`;
|
|
600
|
-
|
|
601
|
-
const fileName = defaultFileName;
|
|
602
|
-
const ExcelJS = require("exceljs");
|
|
603
|
-
// 模板目录取自 r2mo-init 包内(__dirname),非当前项目 cwd,保证任意项目执行 ai ex-api 都能找到模板
|
|
604
|
-
const templateDir = path.resolve(__dirname, "..", "_template", "EXCEL", "ex-api");
|
|
605
|
-
const templateDefPath = path.join(templateDir, "template-def.json");
|
|
606
|
-
let templateDef = {
|
|
607
|
-
RBAC_RESOURCE: { templateFile: "template-RBAC_RESOURCE.xlsx", sheetName: "DATA-PERM", tableName: "S_PERM_SET", columns: ["key", "code", "name", "type"] },
|
|
608
|
-
RBAC_ROLE: { templateFile: "template-RBAC_ROLE.xlsx", sheetName: "DATA-PERM", tableName: "R_ROLE_PERM", columns: ["roleId", "permId"] }
|
|
609
|
-
};
|
|
610
|
-
if (fs.existsSync(templateDefPath)) {
|
|
611
|
-
try {
|
|
612
|
-
const defJson = JSON.parse(fs.readFileSync(templateDefPath, "utf-8"));
|
|
613
|
-
if (defJson.RBAC_RESOURCE) templateDef.RBAC_RESOURCE = { ...templateDef.RBAC_RESOURCE, ...defJson.RBAC_RESOURCE };
|
|
614
|
-
if (defJson.RBAC_ROLE) templateDef.RBAC_ROLE = { ...templateDef.RBAC_ROLE, ...defJson.RBAC_ROLE };
|
|
615
|
-
} catch (_) {
|
|
616
|
-
Ec.info("[ex-api] 模版定义解析失败,使用内置格式");
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const defRes = templateDef.RBAC_RESOURCE;
|
|
621
|
-
const defRole = templateDef.RBAC_ROLE;
|
|
622
|
-
const tableNameRole = defRole.tableName || "R_ROLE_PERM";
|
|
623
|
-
|
|
624
|
-
const templateResPath = path.join(templateDir, defRes.templateFile || "template-RBAC_RESOURCE.xlsx");
|
|
625
|
-
const templateRolePath = path.join(templateDir, defRole.templateFile || "template-RBAC_ROLE.xlsx");
|
|
626
|
-
|
|
627
|
-
// 四张表行数据 keyed by 模板中的 tableName(与 scanTableRegions 返回一致)
|
|
628
|
-
const tableRowData = {
|
|
629
|
-
S_RESOURCE: rowS_RESOURCE,
|
|
630
|
-
S_ACTION: rowS_ACTION,
|
|
631
|
-
S_PERMISSION: rowS_PERMISSION,
|
|
632
|
-
S_PERM_SET: rowS_PERM_SET
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
let workbook;
|
|
636
|
-
if (fs.existsSync(templateResPath)) {
|
|
637
|
-
workbook = await new ExcelJS.Workbook().xlsx.readFile(templateResPath);
|
|
638
|
-
const wsRes = workbook.getWorksheet(defRes.sheetName || "DATA-PERM") || workbook.worksheets[0];
|
|
639
|
-
if (wsRes) {
|
|
640
|
-
const regions = scanTableRegions(wsRes);
|
|
641
|
-
regions.forEach((region) => {
|
|
642
|
-
const dataRow = tableRowData[region.tableName];
|
|
643
|
-
if (!dataRow || !region.columnIndex) return;
|
|
644
|
-
const row = wsRes.getRow(region.dataStartRow);
|
|
645
|
-
Object.keys(dataRow).forEach((col) => {
|
|
646
|
-
const colNum = region.columnIndex[col];
|
|
647
|
-
if (colNum != null && dataRow[col] != null && dataRow[col] !== "") {
|
|
648
|
-
row.getCell(colNum).value = dataRow[col];
|
|
649
|
-
}
|
|
650
|
-
});
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
} else {
|
|
654
|
-
Ec.info("[ex-api] 未找到模板 " + templateResPath + ",使用固定表头格式(可被解析)");
|
|
655
|
-
workbook = new ExcelJS.Workbook();
|
|
656
|
-
const wsRes = workbook.addWorksheet(defRes.sheetName || "DATA-PERM");
|
|
657
|
-
wsRes.addRow([]);
|
|
658
|
-
wsRes.addRow([]);
|
|
659
|
-
["S_PERM_SET", "S_PERMISSION", "S_ACTION", "S_RESOURCE"].forEach((tname) => {
|
|
660
|
-
const data = tableRowData[tname];
|
|
661
|
-
if (tname === "S_PERM_SET" && data) {
|
|
662
|
-
wsRes.addRow(["{TABLE}", tname, "", "", ""]);
|
|
663
|
-
wsRes.addRow(["权限集主键", "权限代码", "权限集名称", "权限集类型"]);
|
|
664
|
-
wsRes.addRow(["key", "code", "name", "type"]);
|
|
665
|
-
wsRes.addRow([data.key, data.code, data.name, data.type]);
|
|
666
|
-
} else if (tname === "S_RESOURCE" && data) {
|
|
667
|
-
wsRes.addRow(["{TABLE}", tname, "", "", ""]);
|
|
668
|
-
wsRes.addRow(["主键", "名称", "MODE_ROLE", "CODE", "IDENTIFIER", "TYPE", "LEVEL"]);
|
|
669
|
-
wsRes.addRow(["key", "name", "modeRole", "code", "identifier", "type", "level"]);
|
|
670
|
-
wsRes.addRow([data.key, data.name, data.modeRole, data.code, data.identifier, data.type, data.level]);
|
|
671
|
-
} else if (tname === "S_ACTION" && data) {
|
|
672
|
-
wsRes.addRow(["{TABLE}", tname, "", "", ""]);
|
|
673
|
-
wsRes.addRow(["主键", "RESOURCE_ID", "PERMISSION_ID", "CODE", "METHOD", "URI", "NAME", "LEVEL"]);
|
|
674
|
-
wsRes.addRow(["key", "resourceId", "permissionId", "code", "method", "uri", "name", "level"]);
|
|
675
|
-
wsRes.addRow([data.key, data.resourceId, data.permissionId, data.code, data.method, data.uri, data.name, data.level]);
|
|
676
|
-
} else if (tname === "S_PERMISSION" && data) {
|
|
677
|
-
wsRes.addRow(["{TABLE}", tname, "", "", ""]);
|
|
678
|
-
wsRes.addRow(["主键", "名称", "备注", "CODE", "IDENTIFIER"]);
|
|
679
|
-
wsRes.addRow(["key", "name", "comment", "code", "identifier"]);
|
|
680
|
-
wsRes.addRow([data.key, data.name, data.comment, data.code, data.identifier]);
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
const outResPath = path.join(rbacResourceDir, fileName);
|
|
685
|
-
await workbook.xlsx.writeFile(outResPath);
|
|
686
|
-
Ec.info("[ex-api] 已写入 RBAC_RESOURCE:" + outResPath);
|
|
687
|
-
|
|
688
|
-
const roleFileName = "falcon-" + fileName;
|
|
689
|
-
const roleWritePaths = [];
|
|
690
|
-
|
|
691
|
-
// 固定参考角色输出:维持原目录规则
|
|
692
|
-
const hasReferenceRole = rolePermsToWrite.some((p) => String(p.ROLE_ID) === REFERENCE_ROLE_ID);
|
|
693
|
-
if (hasReferenceRole) {
|
|
694
|
-
const refPerms = rolePermsToWrite.filter((p) => String(p.ROLE_ID) === REFERENCE_ROLE_ID);
|
|
695
|
-
if (refPerms.length > 0) {
|
|
696
|
-
let refWorkbook;
|
|
697
|
-
if (fs.existsSync(templateRolePath)) {
|
|
698
|
-
refWorkbook = await new ExcelJS.Workbook().xlsx.readFile(templateRolePath);
|
|
699
|
-
const wsRole = refWorkbook.getWorksheet(defRole.sheetName || "DATA-PERM") || refWorkbook.worksheets[0];
|
|
700
|
-
if (wsRole) {
|
|
701
|
-
const regionsRole = scanTableRegions(wsRole);
|
|
702
|
-
const region = regionsRole.find((r) => String(r.tableName).trim() === String(tableNameRole).trim());
|
|
703
|
-
if (region) {
|
|
704
|
-
const colRole = region.columnIndex["roleId"] || region.columnIndex["ROLE_ID"] || 1;
|
|
705
|
-
const colPerm = region.columnIndex["permId"] || region.columnIndex["PERM_ID"] || 2;
|
|
706
|
-
refPerms.forEach((pair, idx) => {
|
|
707
|
-
const row = wsRole.getRow(region.dataStartRow + idx);
|
|
708
|
-
row.getCell(colRole).value = pair.ROLE_ID;
|
|
709
|
-
row.getCell(colPerm).value = pair.PERM_ID;
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
} else {
|
|
714
|
-
refWorkbook = new ExcelJS.Workbook();
|
|
715
|
-
const wsRole = refWorkbook.addWorksheet(defRole.sheetName || "DATA-PERM");
|
|
716
|
-
wsRole.addRow([]);
|
|
717
|
-
wsRole.addRow([]);
|
|
718
|
-
wsRole.addRow(["{TABLE}", tableNameRole, "角色和权限关系", "", ""]);
|
|
719
|
-
wsRole.addRow(["角色ID", "权限ID"]);
|
|
720
|
-
wsRole.addRow(["roleId", "permId"]);
|
|
721
|
-
refPerms.forEach((p) => wsRole.addRow([p.ROLE_ID, p.PERM_ID]));
|
|
722
|
-
}
|
|
723
|
-
const outRefRolePath = path.join(rbacRoleDir, roleFileName);
|
|
724
|
-
await refWorkbook.xlsx.writeFile(outRefRolePath);
|
|
725
|
-
roleWritePaths.push(outRefRolePath);
|
|
726
|
-
Ec.info("[ex-api] 已写入 RBAC_ROLE/ADMIN.SUPER(参考角色):" + outRefRolePath);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// 其他角色输出:当前工作目录下 src/main/resources/init/oob/role/{CODE}/ 目录
|
|
731
|
-
const nonReferenceRoleIds = roleIds.filter((rid) => String(rid) !== REFERENCE_ROLE_ID);
|
|
732
|
-
for (const rid of nonReferenceRoleIds) {
|
|
733
|
-
const code = roleIdToCode[String(rid)] || "";
|
|
734
|
-
if (!code) {
|
|
735
|
-
Ec.info("[ex-api] ⚠️ 警告:角色 ID=" + rid + " 缺少 CODE,跳过输出");
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
738
|
-
const targetRoleDir = path.join(cwd, "src", "main", "resources", "init", "oob", "role", code);
|
|
739
|
-
if (!fs.existsSync(targetRoleDir)) {
|
|
740
|
-
Ec.info("[ex-api] ⚠️ 警告:未找到输出目录 " + targetRoleDir + ",跳过输出");
|
|
741
|
-
continue;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
const oneRolePerms = rolePermsToWrite.filter((p) => String(p.ROLE_ID) === String(rid));
|
|
745
|
-
if (oneRolePerms.length === 0) continue;
|
|
746
|
-
|
|
747
|
-
let oneRoleWorkbook;
|
|
748
|
-
if (fs.existsSync(templateRolePath)) {
|
|
749
|
-
oneRoleWorkbook = await new ExcelJS.Workbook().xlsx.readFile(templateRolePath);
|
|
750
|
-
const wsRole = oneRoleWorkbook.getWorksheet(defRole.sheetName || "DATA-PERM") || oneRoleWorkbook.worksheets[0];
|
|
751
|
-
if (wsRole) {
|
|
752
|
-
const regionsRole = scanTableRegions(wsRole);
|
|
753
|
-
const region = regionsRole.find((r) => String(r.tableName).trim() === String(tableNameRole).trim());
|
|
754
|
-
if (region) {
|
|
755
|
-
const colRole = region.columnIndex["roleId"] || region.columnIndex["ROLE_ID"] || 1;
|
|
756
|
-
const colPerm = region.columnIndex["permId"] || region.columnIndex["PERM_ID"] || 2;
|
|
757
|
-
oneRolePerms.forEach((pair, idx) => {
|
|
758
|
-
const row = wsRole.getRow(region.dataStartRow + idx);
|
|
759
|
-
row.getCell(colRole).value = pair.ROLE_ID;
|
|
760
|
-
row.getCell(colPerm).value = pair.PERM_ID;
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
} else {
|
|
765
|
-
oneRoleWorkbook = new ExcelJS.Workbook();
|
|
766
|
-
const wsRole = oneRoleWorkbook.addWorksheet(defRole.sheetName || "DATA-PERM");
|
|
767
|
-
wsRole.addRow([]);
|
|
768
|
-
wsRole.addRow([]);
|
|
769
|
-
wsRole.addRow(["{TABLE}", tableNameRole, "角色和权限关系", "", ""]);
|
|
770
|
-
wsRole.addRow(["角色ID", "权限ID"]);
|
|
771
|
-
wsRole.addRow(["roleId", "permId"]);
|
|
772
|
-
oneRolePerms.forEach((p) => wsRole.addRow([p.ROLE_ID, p.PERM_ID]));
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const outOneRolePath = path.join(targetRoleDir, roleFileName);
|
|
776
|
-
await oneRoleWorkbook.xlsx.writeFile(outOneRolePath);
|
|
777
|
-
roleWritePaths.push(outOneRolePath);
|
|
778
|
-
Ec.info("[ex-api] 已写入角色目录(" + code + "):" + outOneRolePath);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (roleWritePaths.length === 0) {
|
|
782
|
-
Ec.info("[ex-api] RBAC_ROLE 未输出任何文件(可能角色未选中或目标目录不存在)");
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
Ec.info("[ex-api] ✅ 执行完成(幂等)");
|
|
786
|
-
Ec.info("[ex-api] 📋 汇总:");
|
|
787
|
-
Ec.info("[ex-api] 🔑 ACTION_ID = " + (actionId || "—"));
|
|
788
|
-
Ec.info("[ex-api] 🔑 RESOURCE_ID = " + (resourceId || "—"));
|
|
789
|
-
Ec.info("[ex-api] 🔑 PERMISSION_ID = " + (permissionId || "—"));
|
|
790
|
-
Ec.info("[ex-api] 👥 授权角色总数 = " + (rolePermsToWrite.length > 0 ? rolePermsToWrite.length : 0));
|
|
791
|
-
|
|
792
|
-
// 固定输出:超级管理员角色
|
|
793
|
-
const refRoleName = roleIdToName[REFERENCE_ROLE_ID] || "超级管理员";
|
|
794
|
-
const refRoleCode = roleIdToCode[REFERENCE_ROLE_ID] || "ADMIN.SUPER";
|
|
795
|
-
Ec.info("[ex-api] 🔒 固定输出角色:");
|
|
796
|
-
Ec.info("[ex-api] [1] " + refRoleName + " (CODE: " + refRoleCode + ", ID: " + REFERENCE_ROLE_ID + ")");
|
|
797
|
-
|
|
798
|
-
// 用户选择的角色
|
|
799
|
-
if (roleIds && roleIds.length > 0) {
|
|
800
|
-
Ec.info("[ex-api] 👤 用户选择角色:");
|
|
801
|
-
roleIds.forEach((rid, idx) => {
|
|
802
|
-
const code = roleIdToCode[rid] || "—";
|
|
803
|
-
const name = roleIdToName[rid] || "—";
|
|
804
|
-
Ec.info("[ex-api] [" + (idx + 1) + "] " + name + " (CODE: " + code + ", ID: " + rid + ")");
|
|
805
|
-
});
|
|
806
|
-
} else {
|
|
807
|
-
Ec.info("[ex-api] 👤 用户选择角色:无");
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
Ec.info("[ex-api] 📁 RBAC_RESOURCE = " + outResPath);
|
|
811
|
-
roleWritePaths.forEach((p) => Ec.info("[ex-api] 📁 RBAC_ROLE = " + p));
|
|
812
|
-
if (insertedResource) {
|
|
813
|
-
Ec.info("[ex-api] 📦 S_RESOURCE 本次插入字段:");
|
|
814
|
-
Object.keys(insertedResource).forEach((k) => Ec.info("[ex-api] " + k + " = " + (insertedResource[k] != null ? insertedResource[k] : "—")));
|
|
815
|
-
}
|
|
816
|
-
if (existingResource) {
|
|
817
|
-
Ec.info("[ex-api] 📄 S_RESOURCE 已有记录(有值属性):");
|
|
818
|
-
Object.keys(existingResource).forEach((k) => {
|
|
819
|
-
const v = existingResource[k];
|
|
820
|
-
if (v != null && v !== "") Ec.info("[ex-api] " + k + " = " + v);
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
if (insertedAction) {
|
|
824
|
-
Ec.info("[ex-api] 📦 S_ACTION 本次插入字段:");
|
|
825
|
-
Object.keys(insertedAction).forEach((k) => Ec.info("[ex-api] " + k + " = " + (insertedAction[k] != null ? insertedAction[k] : "—")));
|
|
826
|
-
}
|
|
827
|
-
if (existingAction) {
|
|
828
|
-
Ec.info("[ex-api] 📄 S_ACTION 已有记录(有值属性):");
|
|
829
|
-
Object.keys(existingAction).forEach((k) => {
|
|
830
|
-
const v = existingAction[k];
|
|
831
|
-
if (v != null && v !== "") Ec.info("[ex-api] " + k + " = " + v);
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
if (insertedPermission) {
|
|
835
|
-
Ec.info("[ex-api] 📦 S_PERMISSION 本次插入字段:");
|
|
836
|
-
Object.keys(insertedPermission).forEach((k) => Ec.info("[ex-api] " + k + " = " + (insertedPermission[k] != null ? insertedPermission[k] : "—")));
|
|
837
|
-
}
|
|
838
|
-
if (existingPermission) {
|
|
839
|
-
Ec.info("[ex-api] 📄 S_PERMISSION 已有记录(有值属性):");
|
|
840
|
-
Object.keys(existingPermission).forEach((k) => {
|
|
841
|
-
const v = existingPermission[k];
|
|
842
|
-
if (v != null && v !== "") Ec.info("[ex-api] " + k + " = " + v);
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
|
-
if (insertedRolePerms.length > 0) {
|
|
846
|
-
Ec.info("[ex-api] 📦 R_ROLE_PERM 本次写入(ROLE_ID, PERM_ID):");
|
|
847
|
-
insertedRolePerms.forEach((r, i) => Ec.info("[ex-api] [" + (i + 1) + "] " + r.ROLE_ID + ", " + r.PERM_ID));
|
|
848
|
-
}
|
|
849
|
-
return {
|
|
850
|
-
identifier: metadata.identifier || "—",
|
|
851
|
-
request: requestRaw || "—",
|
|
852
|
-
actionId: actionId || "—",
|
|
853
|
-
resourceId: resourceId || "—",
|
|
854
|
-
permissionId: permissionId || "—",
|
|
855
|
-
roleCount: roleIds ? roleIds.length : 0,
|
|
856
|
-
outResPath: outResPath || "—",
|
|
857
|
-
outRolePaths: roleWritePaths || [],
|
|
858
|
-
ok: true
|
|
859
|
-
};
|
|
860
|
-
} catch (err) {
|
|
861
|
-
Ec.error("[ex-api] 执行失败:" + (err && err.message));
|
|
862
|
-
return {
|
|
863
|
-
identifier: (metadata && metadata.identifier) || "—",
|
|
864
|
-
request: requestRaw || "—",
|
|
865
|
-
actionId: "—",
|
|
866
|
-
resourceId: "—",
|
|
867
|
-
permissionId: "—",
|
|
868
|
-
roleCount: 0,
|
|
869
|
-
outResPath: "—",
|
|
870
|
-
outRolePath: "—",
|
|
871
|
-
ok: false,
|
|
872
|
-
error: (err && err.message) || String(err)
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
module.exports = async (options) => {
|
|
878
|
-
const cwd = process.cwd();
|
|
879
|
-
const configDir = resolveExApiConfigDir(cwd);
|
|
880
|
-
if (!fs.existsSync(configDir)) {
|
|
881
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
882
|
-
const templatePath = path.join(configDir, "ex-api.yaml");
|
|
883
|
-
const template = `# ai ex-api 使用此配置,请按项目修改
|
|
884
|
-
metadata:
|
|
885
|
-
r: "GET /api/ambient"
|
|
886
|
-
identifier: "核心标识符"
|
|
887
|
-
brief: "接口描述"
|
|
888
|
-
resource: "resource.ambient"
|
|
889
|
-
level: 1
|
|
890
|
-
ptype: "权限集 S_PERM_SET 类型"
|
|
891
|
-
pname: "权限集 S_PERM_SET 名称"
|
|
892
|
-
# keyword 可选
|
|
893
|
-
# target 可选;存在时需 ZERO_MODULE 与 zero-exmodule-{module}
|
|
894
|
-
# target:
|
|
895
|
-
# root: "ZERO_MODULE"
|
|
896
|
-
# module: "ambient"
|
|
897
|
-
`;
|
|
898
|
-
fs.writeFileSync(templatePath, template, "utf-8");
|
|
899
|
-
Ec.info("配置目录缺失,已创建并写入模板:" + templatePath);
|
|
900
|
-
Ec.info("请编辑后重新执行: ai ex-api");
|
|
901
|
-
process.exit(1);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
const backupDir = path.join(configDir, "backup");
|
|
905
|
-
const allEntries = fs.readdirSync(configDir, { withFileTypes: true });
|
|
906
|
-
const yamlFiles = allEntries.filter((e) => !e.isDirectory() && e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml")));
|
|
907
|
-
const entries = [];
|
|
908
|
-
for (const e of yamlFiles) {
|
|
909
|
-
const f = e.name;
|
|
910
|
-
const full = path.join(configDir, f);
|
|
911
|
-
try {
|
|
912
|
-
const config = yaml.load(fs.readFileSync(full, "utf-8"));
|
|
913
|
-
if (!config || !config.metadata) continue;
|
|
914
|
-
const r = config.metadata.r != null ? String(config.metadata.r).trim() : "";
|
|
915
|
-
if (!r) {
|
|
916
|
-
Ec.info("[ex-api] 跳过(无 metadata.r):" + f);
|
|
917
|
-
continue;
|
|
918
|
-
}
|
|
919
|
-
const valid = validateExApiR(r);
|
|
920
|
-
if (!valid.valid) {
|
|
921
|
-
Ec.info("[ex-api] 警告(r 不合法,已跳过):" + f + "," + (valid.error || ""));
|
|
922
|
-
continue;
|
|
923
|
-
}
|
|
924
|
-
const label = (config.metadata.identifier || f) + " | " + (config.metadata.brief || r);
|
|
925
|
-
entries.push({ path: full, config, label });
|
|
926
|
-
} catch (_) {}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
if (entries.length === 0) {
|
|
930
|
-
Ec.error("[ex-api] 无有效配置:请在 " + configDir + " 下添加含 metadata.r 的 yaml");
|
|
931
|
-
process.exit(1);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const answer = await inquirer.prompt([
|
|
935
|
-
{ type: "checkbox", name: "selected", message: "选择要执行的 API(多选)", choices: entries.map((e) => ({ name: e.label, value: e.path })) }
|
|
936
|
-
]);
|
|
937
|
-
const selectedPaths = answer && answer.selected && Array.isArray(answer.selected) ? answer.selected : [];
|
|
938
|
-
if (selectedPaths.length === 0) {
|
|
939
|
-
Ec.info("未选择任何项,退出");
|
|
940
|
-
process.exit(0);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
const appEnvPath = resolveAppEnvPath(cwd);
|
|
944
|
-
if (!appEnvPath) {
|
|
945
|
-
Ec.error(".r2mo/app.env 不存在;DPA 下也未找到 {id}-api/.r2mo/app.env");
|
|
946
|
-
process.exit(1);
|
|
947
|
-
}
|
|
948
|
-
loadAppEnv(appEnvPath);
|
|
949
|
-
checkEnv(REQUIRED_ENV_DB, "数据库环境变量");
|
|
950
|
-
checkEnv(REQUIRED_ENV_APP, "应用环境变量(Z_APP_ID / Z_TENANT / Z_SIGMA)");
|
|
951
|
-
|
|
952
|
-
const dbConfig = {
|
|
953
|
-
host: process.env.Z_DB_HOST || "localhost",
|
|
954
|
-
port: parseInt(process.env.Z_DB_PORT || "3306", 10),
|
|
955
|
-
user: process.env.Z_DB_APP_USER,
|
|
956
|
-
password: process.env.Z_DB_APP_PASS,
|
|
957
|
-
database: process.env.Z_DBS_INSTANCE
|
|
958
|
-
};
|
|
959
|
-
const parsed = Ut.parseArgument(options);
|
|
960
|
-
const skip = parsed.skip === true || process.argv.includes("-s") || process.argv.includes("--skip");
|
|
961
|
-
|
|
962
|
-
const mysql = require("mysql2/promise");
|
|
963
|
-
let conn;
|
|
964
|
-
try {
|
|
965
|
-
conn = await mysql.createConnection(dbConfig);
|
|
966
|
-
Ec.info("[ex-api] 数据库已连接,执行 " + selectedPaths.length + " 条 API");
|
|
967
|
-
const results = [];
|
|
968
|
-
for (const configPath of selectedPaths) {
|
|
969
|
-
const config = yaml.load(fs.readFileSync(configPath, "utf-8"));
|
|
970
|
-
const requestRaw = config.metadata && config.metadata.r != null ? String(config.metadata.r).trim() : "";
|
|
971
|
-
if (!requestRaw) {
|
|
972
|
-
results.push({ identifier: config.metadata?.identifier || "—", request: "—", ok: false, error: "无 metadata.r" });
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
const valid = validateExApiR(requestRaw);
|
|
976
|
-
if (!valid.valid) {
|
|
977
|
-
Ec.info("[ex-api] 警告(r 不合法,跳过执行):" + path.basename(configPath) + "," + (valid.error || ""));
|
|
978
|
-
results.push({ identifier: config.metadata?.identifier || "—", request: requestRaw, ok: false, error: valid.error || "r 不合法" });
|
|
979
|
-
continue;
|
|
980
|
-
}
|
|
981
|
-
Ec.info("[ex-api] 处理:" + (config.metadata.identifier || path.basename(configPath)) + " (" + requestRaw + ")");
|
|
982
|
-
const one = await runOneExApi(cwd, conn, config, requestRaw, skip);
|
|
983
|
-
results.push(one);
|
|
984
|
-
if (one.ok) {
|
|
985
|
-
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
986
|
-
const bakPath = path.join(backupDir, path.basename(configPath) + ".bak");
|
|
987
|
-
try {
|
|
988
|
-
fs.renameSync(configPath, bakPath);
|
|
989
|
-
Ec.info("[ex-api] 已备份:" + path.basename(configPath) + " -> backup/" + path.basename(configPath) + ".bak");
|
|
990
|
-
} catch (errBak) {
|
|
991
|
-
Ec.info("[ex-api] 备份失败(已忽略):" + configPath + "," + (errBak && errBak.message));
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
printExApiTable(results);
|
|
996
|
-
} finally {
|
|
997
|
-
if (conn) await conn.end();
|
|
998
|
-
}
|
|
999
|
-
};
|