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,501 @@
|
|
|
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_PATH = ".r2mo/task/command/ex-crud.yaml";
|
|
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
|
+
|
|
15
|
+
/** 占位符替换顺序:先长后短,避免 "log" 把 "x-log" / "x.log" 破坏。literal 为模板参考值,替换为 meta 中对应字段。 */
|
|
16
|
+
const REPLACE_ORDER = [
|
|
17
|
+
["x.log", "identifier"],
|
|
18
|
+
["x-log", "actor"],
|
|
19
|
+
["log", "keyword"],
|
|
20
|
+
["日志", "name"],
|
|
21
|
+
["resource.ambient", "type"]
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
|
|
25
|
+
|
|
26
|
+
/** 扫描 sheet 中 {TABLE} 区域,返回 [{ tableName, dataStartRow, dataEndRow, columnIndex }, ...] */
|
|
27
|
+
function scanTableRegions(ws, maxScanRows) {
|
|
28
|
+
if (!ws) return [];
|
|
29
|
+
const regions = [];
|
|
30
|
+
const limit = maxScanRows || 5000;
|
|
31
|
+
let i = 1;
|
|
32
|
+
while (i <= limit) {
|
|
33
|
+
const row = ws.getRow(i);
|
|
34
|
+
const first = row.getCell(1).value;
|
|
35
|
+
const v = first != null ? String(first).trim() : "";
|
|
36
|
+
if (v === "{TABLE}") {
|
|
37
|
+
const tableNameCell = row.getCell(2).value;
|
|
38
|
+
const tableName = tableNameCell != null ? String(tableNameCell).trim() : "";
|
|
39
|
+
const headerRowCount = 2;
|
|
40
|
+
const dataStartRow = i + 1 + headerRowCount;
|
|
41
|
+
let dataEndRow = dataStartRow - 1;
|
|
42
|
+
let j = i + 1;
|
|
43
|
+
while (j <= limit) {
|
|
44
|
+
const nextRow = ws.getRow(j);
|
|
45
|
+
const nextFirst = nextRow.getCell(1).value;
|
|
46
|
+
const nv = nextFirst != null ? String(nextFirst).trim() : "";
|
|
47
|
+
if (nv === "{TABLE}") {
|
|
48
|
+
dataEndRow = j - 1;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
dataEndRow = j;
|
|
52
|
+
j++;
|
|
53
|
+
}
|
|
54
|
+
const enHeaderRow = ws.getRow(i + 2);
|
|
55
|
+
const columnIndex = {};
|
|
56
|
+
enHeaderRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
|
57
|
+
const val = cell && cell.value != null ? String(cell.value).trim() : "";
|
|
58
|
+
if (val) columnIndex[val] = colNumber;
|
|
59
|
+
});
|
|
60
|
+
regions.push({ tableName, tableStartRow: i, dataStartRow, dataEndRow, columnIndex });
|
|
61
|
+
i = dataEndRow + 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
return regions;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 从已生成的 RBAC_CRUD 目录下所有 xlsx 中收集 S_PERMISSION 表的 UUID(key/ID 列) */
|
|
70
|
+
async function collectPermissionIdsFromCrudDir(rbacCrudDir) {
|
|
71
|
+
const ExcelJS = require("exceljs");
|
|
72
|
+
const ids = [];
|
|
73
|
+
const readXlsx = async (filePath) => {
|
|
74
|
+
const workbook = new ExcelJS.Workbook();
|
|
75
|
+
await workbook.xlsx.readFile(filePath);
|
|
76
|
+
workbook.eachSheet((ws) => {
|
|
77
|
+
const regions = scanTableRegions(ws);
|
|
78
|
+
regions.forEach((reg) => {
|
|
79
|
+
if (reg.tableName !== "S_PERMISSION") return;
|
|
80
|
+
const colKey = reg.columnIndex.key || reg.columnIndex.ID || reg.columnIndex.id;
|
|
81
|
+
if (colKey == null) return;
|
|
82
|
+
for (let r = reg.dataStartRow; r <= reg.dataEndRow; r++) {
|
|
83
|
+
const val = ws.getRow(r).getCell(colKey).value;
|
|
84
|
+
if (val != null && String(val).trim() !== "") ids.push(String(val).trim());
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
const walk = (dir) => {
|
|
90
|
+
if (!fs.existsSync(dir)) return;
|
|
91
|
+
fs.readdirSync(dir, { withFileTypes: true }).forEach((ent) => {
|
|
92
|
+
const full = path.join(dir, ent.name);
|
|
93
|
+
if (ent.isDirectory()) walk(full);
|
|
94
|
+
else if (ent.name.toLowerCase().endsWith(".xlsx")) queue.push(full);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
const queue = [];
|
|
98
|
+
walk(rbacCrudDir);
|
|
99
|
+
for (const filePath of queue) {
|
|
100
|
+
try {
|
|
101
|
+
await readXlsx(filePath);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
Ec.info("[ex-crud] 读取 xlsx 跳过:" + path.relative(rbacCrudDir, filePath) + "," + (e && e.message));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return ids;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getArtifactIdFromPom(cwd) {
|
|
110
|
+
const pomPath = path.resolve(cwd, "pom.xml");
|
|
111
|
+
if (!fs.existsSync(pomPath)) return null;
|
|
112
|
+
let content = fs.readFileSync(pomPath, "utf-8");
|
|
113
|
+
content = content.replace(/<parent>[\s\S]*?<\/parent>/i, "");
|
|
114
|
+
const m = content.match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
115
|
+
return m ? m[1].trim() : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveExcelRoot(cwd, target) {
|
|
119
|
+
if (target && target.module) {
|
|
120
|
+
const zeroModule = process.env.ZERO_MODULE || "";
|
|
121
|
+
return path.resolve(zeroModule, `zero-exmodule-${target.module}`);
|
|
122
|
+
}
|
|
123
|
+
const artifactId = getArtifactIdFromPom(cwd);
|
|
124
|
+
const apiDir = artifactId ? path.resolve(cwd, artifactId + "-api") : null;
|
|
125
|
+
if (apiDir && fs.existsSync(apiDir)) return apiDir;
|
|
126
|
+
return cwd;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function loadAppEnv(filePath) {
|
|
130
|
+
if (!fs.existsSync(filePath)) return false;
|
|
131
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
132
|
+
content.split(/\r?\n/).forEach((line) => {
|
|
133
|
+
const trimmed = line.trim();
|
|
134
|
+
if (trimmed.startsWith("#") || !trimmed.startsWith("export ")) return;
|
|
135
|
+
const match = trimmed.match(/^export\s+([A-Za-z0-9_]+)=["']?([^"'\n]*)["']?/);
|
|
136
|
+
if (match) process.env[match[1]] = match[2].trim();
|
|
137
|
+
});
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveAppEnvPath(cwd) {
|
|
142
|
+
const primary = path.resolve(cwd, ".r2mo", "app.env");
|
|
143
|
+
if (fs.existsSync(primary)) return primary;
|
|
144
|
+
let artifactId = getArtifactIdFromPom(cwd);
|
|
145
|
+
if (!artifactId) artifactId = path.basename(cwd);
|
|
146
|
+
if (artifactId && artifactId !== ".") {
|
|
147
|
+
const apiDir = `${artifactId}-api`;
|
|
148
|
+
const nested = path.resolve(cwd, apiDir, ".r2mo", "app.env");
|
|
149
|
+
if (fs.existsSync(nested)) return nested;
|
|
150
|
+
const sibling = path.resolve(cwd, "..", apiDir, ".r2mo", "app.env");
|
|
151
|
+
if (fs.existsSync(sibling)) return sibling;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function checkEnv(keys, label) {
|
|
157
|
+
const missing = keys.filter((k) => !process.env[k] || !String(process.env[k]).trim());
|
|
158
|
+
if (missing.length > 0) {
|
|
159
|
+
Ec.error(`${label}:以下环境变量必须全部已设置。`);
|
|
160
|
+
Ec.info("当前缺失:" + missing.join(", "));
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isDpaRoot(dir) {
|
|
166
|
+
const pom = path.join(dir, "pom.xml");
|
|
167
|
+
if (!fs.existsSync(pom)) return false;
|
|
168
|
+
const id = getArtifactIdFromPom(dir);
|
|
169
|
+
if (!id) return false;
|
|
170
|
+
const apiDir = path.join(dir, `${id}-api`);
|
|
171
|
+
const domainDir = path.join(dir, `${id}-domain`);
|
|
172
|
+
return fs.existsSync(apiDir) && fs.existsSync(domainDir);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 对路径片段做替换(先长后短),用于目录/文件名 */
|
|
176
|
+
function replacePathSegment(seg, meta) {
|
|
177
|
+
let s = seg;
|
|
178
|
+
for (const [literal, key] of REPLACE_ORDER) {
|
|
179
|
+
const val = meta[key];
|
|
180
|
+
if (val != null && String(val).trim() !== "") s = s.split(literal).join(String(meta[key]));
|
|
181
|
+
}
|
|
182
|
+
// 支持占位符 {{key}}
|
|
183
|
+
s = s.replace(/\{\{identifier\}\}/g, meta.identifier != null ? meta.identifier : "");
|
|
184
|
+
s = s.replace(/\{\{actor\}\}/g, meta.actor != null ? meta.actor : "");
|
|
185
|
+
s = s.replace(/\{\{keyword\}\}/g, meta.keyword != null ? meta.keyword : "");
|
|
186
|
+
s = s.replace(/\{\{name\}\}/g, meta.name != null ? meta.name : "");
|
|
187
|
+
s = s.replace(/\{\{type\}\}/g, meta.type != null ? meta.type : "");
|
|
188
|
+
return s;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 对文件内容做替换(先长后短),再替换所有 UUID */
|
|
192
|
+
function replaceContent(content, meta, isBinary) {
|
|
193
|
+
if (isBinary) return content;
|
|
194
|
+
if (typeof content !== "string") content = String(content);
|
|
195
|
+
let s = content;
|
|
196
|
+
for (const [literal, key] of REPLACE_ORDER) {
|
|
197
|
+
const val = meta[key];
|
|
198
|
+
if (val != null && String(val).trim() !== "") s = s.split(literal).join(String(meta[key]));
|
|
199
|
+
}
|
|
200
|
+
s = s.replace(/\{\{identifier\}\}/g, meta.identifier != null ? meta.identifier : "");
|
|
201
|
+
s = s.replace(/\{\{actor\}\}/g, meta.actor != null ? meta.actor : "");
|
|
202
|
+
s = s.replace(/\{\{keyword\}\}/g, meta.keyword != null ? meta.keyword : "");
|
|
203
|
+
s = s.replace(/\{\{name\}\}/g, meta.name != null ? meta.name : "");
|
|
204
|
+
s = s.replace(/\{\{type\}\}/g, meta.type != null ? meta.type : "");
|
|
205
|
+
return replaceAllUuids(s);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function replaceAllUuids(str) {
|
|
209
|
+
return str.replace(UUID_REGEX, () => uuidv4());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 对单个单元格 value 做占位符与 UUID 替换;仅当含占位符或 UUID 时才替换,避免破坏格式 */
|
|
213
|
+
function replaceCellValue(val, meta) {
|
|
214
|
+
if (val == null) return val;
|
|
215
|
+
if (typeof val !== "string") return val;
|
|
216
|
+
const next = replaceContent(val, meta, false);
|
|
217
|
+
return next !== val ? next : val;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** 递归复制模板目录到目标,跳过 ex-crud.yaml、README.md;路径片段与文本内容按 meta 替换,内容中 UUID 重新生成;.xlsx 用 ExcelJS 按单元格替换后写回 */
|
|
221
|
+
async function copyTemplateWithReplace(templateDir, destDir, meta, skipNames) {
|
|
222
|
+
if (!fs.existsSync(templateDir)) return;
|
|
223
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
224
|
+
const skipSet = new Set(Array.isArray(skipNames) ? skipNames : (skipNames ? [skipNames] : []));
|
|
225
|
+
const entries = fs.readdirSync(templateDir, { withFileTypes: true });
|
|
226
|
+
const ExcelJS = require("exceljs");
|
|
227
|
+
for (const ent of entries) {
|
|
228
|
+
const srcPath = path.join(templateDir, ent.name);
|
|
229
|
+
const segReplaced = replacePathSegment(ent.name, meta);
|
|
230
|
+
const destPath = path.join(destDir, segReplaced);
|
|
231
|
+
if (skipSet.has(ent.name) || ent.name === ".DS_Store") continue;
|
|
232
|
+
if (ent.isDirectory()) {
|
|
233
|
+
await copyTemplateWithReplace(srcPath, destPath, meta, []);
|
|
234
|
+
} else {
|
|
235
|
+
const ext = path.extname(ent.name).toLowerCase();
|
|
236
|
+
if (ext === ".xlsx" || ext === ".xls") {
|
|
237
|
+
try {
|
|
238
|
+
const workbook = await new ExcelJS.Workbook().xlsx.readFile(srcPath);
|
|
239
|
+
workbook.eachSheet((ws) => {
|
|
240
|
+
ws.eachRow((row) => {
|
|
241
|
+
if (!row) return;
|
|
242
|
+
row.eachCell((cell) => {
|
|
243
|
+
if (cell && cell.value != null) cell.value = replaceCellValue(cell.value, meta);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
await workbook.xlsx.writeFile(destPath);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
Ec.info("[ex-crud] 跳过 xlsx 占位符替换,直接复制:" + segReplaced + "," + (e && e.message));
|
|
250
|
+
fs.copyFileSync(srcPath, destPath);
|
|
251
|
+
}
|
|
252
|
+
Ec.info("[ex-crud] 生成:" + path.relative(destDir, destPath));
|
|
253
|
+
} else {
|
|
254
|
+
const isBinary = [".png", ".jpg", ".jpeg", ".gif", ".ico", ".pdf"].includes(ext);
|
|
255
|
+
let content = fs.readFileSync(srcPath, isBinary ? null : "utf-8");
|
|
256
|
+
if (!isBinary) content = replaceContent(content, meta, false);
|
|
257
|
+
fs.writeFileSync(destPath, content, isBinary ? null : "utf-8");
|
|
258
|
+
Ec.info("[ex-crud] 生成:" + path.relative(destDir, destPath));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 解析 ex-crud.yaml 路径:先 cwd,再上级目录(与 ex-api 一致,便于在 -api 子目录执行时找到带 target 的配置) */
|
|
265
|
+
function resolveExCrudConfigPath(cwd) {
|
|
266
|
+
const primary = path.resolve(cwd, CONFIG_PATH);
|
|
267
|
+
if (fs.existsSync(primary)) return primary;
|
|
268
|
+
const parent = path.resolve(cwd, "..", CONFIG_PATH);
|
|
269
|
+
if (fs.existsSync(parent)) return parent;
|
|
270
|
+
const grand = path.resolve(cwd, "..", "..", CONFIG_PATH);
|
|
271
|
+
if (fs.existsSync(grand)) return grand;
|
|
272
|
+
return primary;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = async (options) => {
|
|
276
|
+
const cwd = process.cwd();
|
|
277
|
+
const configFullPath = resolveExCrudConfigPath(cwd);
|
|
278
|
+
if (!fs.existsSync(configFullPath)) {
|
|
279
|
+
const configDir = path.dirname(configFullPath);
|
|
280
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
281
|
+
const template = `# ai ex-crud 使用此配置,请按项目修改
|
|
282
|
+
metadata:
|
|
283
|
+
keyword: "log"
|
|
284
|
+
identifier: "x.log"
|
|
285
|
+
actor: "x-log"
|
|
286
|
+
name: "日志"
|
|
287
|
+
type: "resource.ambient"
|
|
288
|
+
# target 可选;与 ex-api 一致,存在时需 ZERO_MODULE 与 zero-exmodule-{module}
|
|
289
|
+
# target:
|
|
290
|
+
# root: "ZERO_MODULE"
|
|
291
|
+
# module: "ambient"
|
|
292
|
+
`;
|
|
293
|
+
fs.writeFileSync(configFullPath, template, "utf-8");
|
|
294
|
+
Ec.info("配置文件缺失,已在下列路径写入模板:" + configFullPath);
|
|
295
|
+
Ec.info("请编辑后重新执行: ai ex-crud");
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let config;
|
|
300
|
+
try {
|
|
301
|
+
config = yaml.load(fs.readFileSync(configFullPath, "utf-8"));
|
|
302
|
+
} catch (e) {
|
|
303
|
+
Ec.error("ex-crud.yaml 解析失败:" + e.message);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
if (!config || !config.metadata) {
|
|
307
|
+
Ec.error("ex-crud.yaml 需包含 metadata 节点");
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const metadata = config.metadata;
|
|
312
|
+
// target 与 ex-api 一致:root + module 存在时分流到 zero-exmodule-{module}
|
|
313
|
+
let target = config.target;
|
|
314
|
+
if (target && typeof target === "object") {
|
|
315
|
+
const root = target.root != null ? String(target.root).trim() : "";
|
|
316
|
+
const moduleName = target.module != null ? String(target.module).trim() : "";
|
|
317
|
+
if (root && moduleName) target = { root, module: moduleName };
|
|
318
|
+
else target = null;
|
|
319
|
+
} else {
|
|
320
|
+
target = null;
|
|
321
|
+
}
|
|
322
|
+
Ec.info("[ex-crud] 配置:" + configFullPath + (target ? ",target=" + target.module : ",无 target"));
|
|
323
|
+
const meta = {
|
|
324
|
+
keyword: metadata.keyword != null ? String(metadata.keyword).trim() : "",
|
|
325
|
+
identifier: metadata.identifier != null ? String(metadata.identifier).trim() : "",
|
|
326
|
+
actor: metadata.actor != null ? String(metadata.actor).trim() : "",
|
|
327
|
+
name: metadata.name != null ? String(metadata.name).trim() : "",
|
|
328
|
+
type: metadata.type != null ? String(metadata.type).trim() : ""
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
if (target) {
|
|
332
|
+
const zeroModule = process.env.ZERO_MODULE;
|
|
333
|
+
if (!zeroModule || !String(zeroModule).trim()) {
|
|
334
|
+
Ec.error("存在 target 配置时,环境变量 ZERO_MODULE 必须已设置");
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
const dpaRoot = path.resolve(zeroModule || "", `zero-exmodule-${target.module}`);
|
|
338
|
+
if (!fs.existsSync(dpaRoot) || !isDpaRoot(dpaRoot)) {
|
|
339
|
+
Ec.error(`ZERO_MODULE 下 DPA 目录不是标准架构:${dpaRoot}`);
|
|
340
|
+
Ec.info("需存在 pom.xml 且包含 xxx-api、xxx-domain 子目录");
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const parsed = Ut.parseArgument(options);
|
|
346
|
+
const skip = parsed.skip === true || process.argv.includes("-s") || process.argv.includes("--skip");
|
|
347
|
+
|
|
348
|
+
if (!skip) {
|
|
349
|
+
const appEnvPath = resolveAppEnvPath(cwd);
|
|
350
|
+
if (!appEnvPath) {
|
|
351
|
+
Ec.error(".r2mo/app.env 不存在;DPA 下也未找到 {id}-api/.r2mo/app.env");
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
loadAppEnv(appEnvPath);
|
|
355
|
+
checkEnv(REQUIRED_ENV_DB, "数据库环境变量");
|
|
356
|
+
checkEnv(REQUIRED_ENV_APP, "应用环境变量");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
Ec.execute("ai ex-crud:配置已加载。");
|
|
360
|
+
|
|
361
|
+
// 1. 模板目录(R2MO-INIT 包内)与输出目录(与 ex-api 一致:有 target 时分流到 zero-exmodule-{module},无 target 时为 zero-launcher-configuration)
|
|
362
|
+
const templateDir = path.resolve(__dirname, "..", "_template", "EXCEL", "ex-crud");
|
|
363
|
+
const excelRoot = resolveExcelRoot(cwd, target);
|
|
364
|
+
const domainName = target && target.module ? `zero-exmodule-${target.module}-domain` : null;
|
|
365
|
+
const pluginsBase = domainName
|
|
366
|
+
? path.join(excelRoot, domainName, "src", "main", "resources", "plugins")
|
|
367
|
+
: path.join(excelRoot, "src", "main", "resources", "plugins");
|
|
368
|
+
const pluginId = domainName ? `zero-exmodule-${target.module}` : "zero-launcher-configuration";
|
|
369
|
+
const rbacCrudDir = path.join(pluginsBase, pluginId, "security", "RBAC_CRUD");
|
|
370
|
+
const rbacRoleDir = path.join(pluginsBase, pluginId, "security", "RBAC_ROLE", "ADMIN.SUPER");
|
|
371
|
+
|
|
372
|
+
if (!fs.existsSync(rbacCrudDir)) fs.mkdirSync(rbacCrudDir, { recursive: true });
|
|
373
|
+
|
|
374
|
+
Ec.info("[ex-crud] 模板目录:" + templateDir);
|
|
375
|
+
Ec.info("[ex-crud] excelRoot=" + excelRoot + ",pluginId=" + pluginId + (domainName ? "(target 分流)" : ""));
|
|
376
|
+
Ec.info("[ex-crud] RBAC_CRUD:" + rbacCrudDir);
|
|
377
|
+
Ec.info("[ex-crud] RBAC_ROLE :" + rbacRoleDir);
|
|
378
|
+
|
|
379
|
+
await copyTemplateWithReplace(templateDir, rbacCrudDir, meta, ["ex-crud.yaml", "README.md", "template-RBAC_ROLE.xlsx", ".DS_Store"]);
|
|
380
|
+
|
|
381
|
+
Ec.info("[ex-crud] 已生成 CRUD 文件到 RBAC_CRUD");
|
|
382
|
+
|
|
383
|
+
// 2. 从生成的 CRUD 中收集 S_PERMISSION 表的所有 UUID(即 falcon 要关联的权限,不可能在库中已存在)
|
|
384
|
+
const permissionIds = await collectPermissionIdsFromCrudDir(rbacCrudDir);
|
|
385
|
+
Ec.info("[ex-crud] 从 CRUD 中收集到 S_PERMISSION UUID 数:" + permissionIds.length);
|
|
386
|
+
|
|
387
|
+
// 3. 若不 skip:连接数据库仅查角色,用户选择(角色不可为空,未选则取默认一条),写 falcon 角色权限表到 RBAC_ROLE
|
|
388
|
+
let roleIds = [];
|
|
389
|
+
if (!skip) {
|
|
390
|
+
const mysql = require("mysql2/promise");
|
|
391
|
+
const dbConfig = {
|
|
392
|
+
host: process.env.Z_DB_HOST || "localhost",
|
|
393
|
+
port: parseInt(process.env.Z_DB_PORT || "3306", 10),
|
|
394
|
+
user: process.env.Z_DB_APP_USER,
|
|
395
|
+
password: process.env.Z_DB_APP_PASS,
|
|
396
|
+
database: process.env.Z_DBS_INSTANCE
|
|
397
|
+
};
|
|
398
|
+
let conn;
|
|
399
|
+
try {
|
|
400
|
+
conn = await mysql.createConnection(dbConfig);
|
|
401
|
+
Ec.info("[ex-crud] 数据库已连接,查询角色");
|
|
402
|
+
|
|
403
|
+
const [roleRows] = await conn.execute("SELECT ID, NAME, CODE FROM S_ROLE ORDER BY NAME");
|
|
404
|
+
if (!roleRows || roleRows.length === 0) {
|
|
405
|
+
Ec.info("[ex-crud] S_ROLE 中无角色,跳过 falcon");
|
|
406
|
+
} else {
|
|
407
|
+
const answer = await inquirer.prompt([
|
|
408
|
+
{
|
|
409
|
+
type: "checkbox",
|
|
410
|
+
name: "selectedRoles",
|
|
411
|
+
message: "选择要授权当前 CRUD 的角色(可多选)",
|
|
412
|
+
choices: roleRows.map((r) => ({ name: `${r.NAME || r.CODE} (${r.ID})`, value: String(r.ID) }))
|
|
413
|
+
}
|
|
414
|
+
]);
|
|
415
|
+
const raw = answer && answer.selectedRoles;
|
|
416
|
+
if (Array.isArray(raw) && raw.length > 0) roleIds = raw.map((id) => (id != null ? String(id) : "")).filter((id) => id !== "");
|
|
417
|
+
else if (raw != null && raw !== "") roleIds = [String(raw)];
|
|
418
|
+
|
|
419
|
+
if (roleIds.length === 0) {
|
|
420
|
+
let [oneRole] = await conn.execute(
|
|
421
|
+
"SELECT ID FROM S_ROLE WHERE NAME = ? OR CODE = ? OR CODE = ? LIMIT 1",
|
|
422
|
+
["超级管理员", "ADMIN.SUPER", "ADMIN_SUPER"]
|
|
423
|
+
);
|
|
424
|
+
if (!oneRole || !oneRole[0]) {
|
|
425
|
+
[oneRole] = await conn.execute("SELECT ID FROM S_ROLE ORDER BY NAME LIMIT 1", []);
|
|
426
|
+
}
|
|
427
|
+
if (oneRole && oneRole[0]) {
|
|
428
|
+
const rid = oneRole[0].ID != null ? String(oneRole[0].ID) : String(oneRole[0].id);
|
|
429
|
+
roleIds = [rid];
|
|
430
|
+
Ec.info("[ex-crud] 未选角色,已用 S_ROLE 补一条(ROLE_ID=" + rid + ")");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (permissionIds.length > 0 && roleIds.length > 0) {
|
|
436
|
+
const rolePermsToWrite = roleIds.flatMap((rid) => permissionIds.map((pid) => ({ ROLE_ID: rid, PERM_ID: pid })));
|
|
437
|
+
if (!fs.existsSync(rbacRoleDir)) fs.mkdirSync(rbacRoleDir, { recursive: true });
|
|
438
|
+
const ExcelJS = require("exceljs");
|
|
439
|
+
const roleFileName = "falcon-crud-" + (meta.identifier || "default").replace(/[^a-zA-Z0-9._-]/g, "_") + ".xlsx";
|
|
440
|
+
const outRolePath = path.join(rbacRoleDir, roleFileName);
|
|
441
|
+
const templatePath = path.resolve(__dirname, "..", "_template", "EXCEL", "ex-crud", "template-RBAC_ROLE.xlsx");
|
|
442
|
+
let roleWorkbook;
|
|
443
|
+
if (fs.existsSync(templatePath)) {
|
|
444
|
+
roleWorkbook = await new ExcelJS.Workbook().xlsx.readFile(templatePath);
|
|
445
|
+
const wsRole = roleWorkbook.getWorksheet("DATA-PERM") || roleWorkbook.worksheets[0];
|
|
446
|
+
if (wsRole) {
|
|
447
|
+
const tableNameRole = "R_ROLE_PERM";
|
|
448
|
+
let dataStartRow = 1;
|
|
449
|
+
let colRole = 1;
|
|
450
|
+
let colPerm = 2;
|
|
451
|
+
for (let r = 1; r <= 100; r++) {
|
|
452
|
+
const first = wsRole.getRow(r).getCell(1).value;
|
|
453
|
+
const v = first != null ? String(first).trim() : "";
|
|
454
|
+
if (v === "{TABLE}") {
|
|
455
|
+
const t2 = wsRole.getRow(r).getCell(2).value;
|
|
456
|
+
const tname = t2 != null ? String(t2).trim() : "";
|
|
457
|
+
if (tname === tableNameRole) {
|
|
458
|
+
dataStartRow = r + 3;
|
|
459
|
+
const enRow = wsRole.getRow(r + 2);
|
|
460
|
+
enRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
|
461
|
+
const val = cell && cell.value != null ? String(cell.value).trim() : "";
|
|
462
|
+
if (val === "roleId" || val === "ROLE_ID") colRole = colNumber;
|
|
463
|
+
if (val === "permId" || val === "PERM_ID") colPerm = colNumber;
|
|
464
|
+
});
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
rolePermsToWrite.forEach((pair, idx) => {
|
|
470
|
+
const row = wsRole.getRow(dataStartRow + idx);
|
|
471
|
+
row.getCell(colRole).value = pair.ROLE_ID;
|
|
472
|
+
row.getCell(colPerm).value = pair.PERM_ID;
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
roleWorkbook = new ExcelJS.Workbook();
|
|
477
|
+
const wsRole = roleWorkbook.addWorksheet("DATA-PERM");
|
|
478
|
+
wsRole.addRow([]);
|
|
479
|
+
wsRole.addRow([]);
|
|
480
|
+
wsRole.addRow(["{TABLE}", "R_ROLE_PERM", "角色和权限关系", "", ""]);
|
|
481
|
+
wsRole.addRow(["角色ID", "权限ID"]);
|
|
482
|
+
wsRole.addRow(["roleId", "permId"]);
|
|
483
|
+
rolePermsToWrite.forEach((p) => wsRole.addRow([p.ROLE_ID, p.PERM_ID]));
|
|
484
|
+
}
|
|
485
|
+
await roleWorkbook.xlsx.writeFile(outRolePath);
|
|
486
|
+
Ec.info("[ex-crud] 已写入 RBAC_ROLE/ADMIN.SUPER:" + outRolePath);
|
|
487
|
+
}
|
|
488
|
+
} catch (err) {
|
|
489
|
+
Ec.error("[ex-crud] 数据库或 falcon 写入失败:" + (err && err.message));
|
|
490
|
+
if (err && err.stack) Ec.info(err.stack);
|
|
491
|
+
} finally {
|
|
492
|
+
if (conn) await conn.end();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
Ec.info("[ex-crud] ✅ 执行完成");
|
|
497
|
+
Ec.info("[ex-crud] 📋 汇总:");
|
|
498
|
+
Ec.info("[ex-crud] 📁 RBAC_CRUD = " + rbacCrudDir);
|
|
499
|
+
if (permissionIds.length > 0) Ec.info("[ex-crud] 🔑 S_PERMISSION UUID 数 = " + permissionIds.length);
|
|
500
|
+
if (roleIds.length > 0) Ec.info("[ex-crud] 👥 授权角色数 = " + roleIds.length);
|
|
501
|
+
};
|
|
@@ -4,7 +4,14 @@ const path = require("path");
|
|
|
4
4
|
const Ut = require("../commander-shared");
|
|
5
5
|
|
|
6
6
|
const REF_ROLE_ID = "e501b47a-c08b-4c83-b12b-95ad82873e96";
|
|
7
|
-
const REQUIRED_ENV_KEYS = [
|
|
7
|
+
const REQUIRED_ENV_KEYS = [
|
|
8
|
+
"Z_DB_TYPE", // 数据库类型: MYSQL / SQLSERVER / ORACLE / POSTGRESQL
|
|
9
|
+
"Z_DB_HOST", // 数据库主机
|
|
10
|
+
"Z_DB_PORT", // 数据库端口
|
|
11
|
+
"Z_DBS_INSTANCE", // 业务数据库实例名
|
|
12
|
+
"Z_DB_APP_USER", // 数据库用户
|
|
13
|
+
"Z_DB_APP_PASS" // 数据库密码
|
|
14
|
+
];
|
|
8
15
|
|
|
9
16
|
/**
|
|
10
17
|
* 从 pom.xml 读取当前项目的 artifactId(排除 <parent> 内的)
|
|
@@ -93,10 +100,18 @@ module.exports = async (options) => {
|
|
|
93
100
|
loadAppEnv(appEnvPath);
|
|
94
101
|
Ec.info(`已加载环境变量:${appEnvPath}`);
|
|
95
102
|
|
|
103
|
+
// 直接检查环境变量(不依赖文件内容;可来自 .r2mo/app.env 或当前 shell 已 export)
|
|
96
104
|
const missing = REQUIRED_ENV_KEYS.filter((k) => !process.env[k] || !String(process.env[k]).trim());
|
|
97
105
|
if (missing.length > 0) {
|
|
98
|
-
Ec.error(
|
|
99
|
-
Ec.info("
|
|
106
|
+
Ec.error("环境变量不齐,以下前置条件必须全部已设置,否则不执行。");
|
|
107
|
+
Ec.info("当前缺失的环境变量:" + missing.join(", "));
|
|
108
|
+
Ec.info("请确保以下环境变量已设置(可在 .r2mo/app.env 中 export,或在当前 shell 中 export):");
|
|
109
|
+
Ec.info(" Z_DB_TYPE # 数据库类型: MYSQL / SQLSERVER / ORACLE / POSTGRESQL");
|
|
110
|
+
Ec.info(" Z_DB_HOST # 数据库主机,如 127.0.0.1");
|
|
111
|
+
Ec.info(" Z_DB_PORT # 数据库端口,如 3306");
|
|
112
|
+
Ec.info(" Z_DBS_INSTANCE # 业务数据库实例名");
|
|
113
|
+
Ec.info(" Z_DB_APP_USER # 数据库用户");
|
|
114
|
+
Ec.info(" Z_DB_APP_PASS # 数据库密码");
|
|
100
115
|
process.exit(1);
|
|
101
116
|
}
|
|
102
117
|
|
|
@@ -9,6 +9,8 @@ const executeSpring = require('./fn.source.spring');
|
|
|
9
9
|
const executeZero = require('./fn.source.zero');
|
|
10
10
|
const executeApply = require('./fn.source.apply');
|
|
11
11
|
const executeExPerm = require('./fn.ex.perm');
|
|
12
|
+
const executeExApi = require('./fn.ex.api');
|
|
13
|
+
const executeExCrud = require('./fn.ex.crud');
|
|
12
14
|
const exported = {
|
|
13
15
|
executeUuid, // ai uuid
|
|
14
16
|
executeString, // ai str
|
|
@@ -23,6 +25,8 @@ const exported = {
|
|
|
23
25
|
// Cursor 规则安装
|
|
24
26
|
executeApply, // ai apply
|
|
25
27
|
executeExPerm, // ai ex-perm
|
|
28
|
+
executeExApi, // ai ex-api
|
|
29
|
+
executeExCrud, // ai ex-crud
|
|
26
30
|
};
|
|
27
31
|
module.exports = exported;
|
|
28
32
|
/**
|