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,545 +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-crud";
|
|
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 metadata:至少 keyword、identifier 非空 */
|
|
265
|
-
function validateExCrudMetadata(config) {
|
|
266
|
-
if (!config || !config.metadata || typeof config.metadata !== "object") return { valid: false, error: "缺少 metadata" };
|
|
267
|
-
const m = config.metadata;
|
|
268
|
-
const keyword = m.keyword != null ? String(m.keyword).trim() : "";
|
|
269
|
-
const identifier = m.identifier != null ? String(m.identifier).trim() : "";
|
|
270
|
-
if (!keyword) return { valid: false, error: "metadata.keyword 为空" };
|
|
271
|
-
if (!identifier) return { valid: false, error: "metadata.identifier 为空" };
|
|
272
|
-
if (!/^[a-zA-Z0-9._-]+$/.test(identifier)) return { valid: false, error: "metadata.identifier 仅允许字母数字、点、下划线、横线" };
|
|
273
|
-
return { valid: true };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** 解析 ex-crud 配置目录:cwd / 上级 / 上上级 */
|
|
277
|
-
function resolveExCrudConfigDir(cwd) {
|
|
278
|
-
const primary = path.resolve(cwd, CONFIG_DIR);
|
|
279
|
-
if (fs.existsSync(primary) && fs.statSync(primary).isDirectory()) return primary;
|
|
280
|
-
const parent = path.resolve(cwd, "..", CONFIG_DIR);
|
|
281
|
-
if (fs.existsSync(parent) && fs.statSync(parent).isDirectory()) return parent;
|
|
282
|
-
const grand = path.resolve(cwd, "..", "..", CONFIG_DIR);
|
|
283
|
-
if (fs.existsSync(grand) && fs.statSync(grand).isDirectory()) return grand;
|
|
284
|
-
return primary;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
module.exports = async (options) => {
|
|
288
|
-
const cwd = process.cwd();
|
|
289
|
-
const configDir = resolveExCrudConfigDir(cwd);
|
|
290
|
-
if (!fs.existsSync(configDir)) {
|
|
291
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
292
|
-
const templatePath = path.join(configDir, "ex-crud.yaml");
|
|
293
|
-
const template = `# ai ex-crud 使用此配置,请按项目修改
|
|
294
|
-
metadata:
|
|
295
|
-
keyword: "log"
|
|
296
|
-
identifier: "x.log"
|
|
297
|
-
actor: "x-log"
|
|
298
|
-
name: "日志"
|
|
299
|
-
type: "resource.ambient"
|
|
300
|
-
# target 可选;与 ex-api 一致,存在时需 ZERO_MODULE 与 zero-exmodule-{module}
|
|
301
|
-
# target:
|
|
302
|
-
# root: "ZERO_MODULE"
|
|
303
|
-
# module: "ambient"
|
|
304
|
-
`;
|
|
305
|
-
fs.writeFileSync(templatePath, template, "utf-8");
|
|
306
|
-
Ec.info("配置目录缺失,已创建并写入模板:" + templatePath);
|
|
307
|
-
Ec.info("请编辑后重新执行: ai ex-crud");
|
|
308
|
-
process.exit(1);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const backupDir = path.join(configDir, "backup");
|
|
312
|
-
const allEntries = fs.readdirSync(configDir, { withFileTypes: true });
|
|
313
|
-
const yamlFiles = allEntries.filter((e) => !e.isDirectory() && e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml")));
|
|
314
|
-
const entries = [];
|
|
315
|
-
for (const e of yamlFiles) {
|
|
316
|
-
const f = e.name;
|
|
317
|
-
const full = path.join(configDir, f);
|
|
318
|
-
try {
|
|
319
|
-
const config = yaml.load(fs.readFileSync(full, "utf-8"));
|
|
320
|
-
const valid = validateExCrudMetadata(config);
|
|
321
|
-
if (!valid.valid) {
|
|
322
|
-
Ec.info("[ex-crud] 警告(metadata 不合法,已跳过):" + f + "," + (valid.error || ""));
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
const label = (config.metadata.identifier || f) + " | " + (config.metadata.keyword || "") + (config.metadata.name ? " " + config.metadata.name : "");
|
|
326
|
-
entries.push({ path: full, config, label });
|
|
327
|
-
} catch (err) {
|
|
328
|
-
Ec.info("[ex-crud] 跳过(解析失败):" + f + "," + (err && err.message));
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (entries.length === 0) {
|
|
333
|
-
Ec.error("[ex-crud] 无有效配置:请在 " + configDir + " 下添加含合法 metadata(keyword、identifier)的 yaml");
|
|
334
|
-
process.exit(1);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const answer = await inquirer.prompt([
|
|
338
|
-
{ type: "checkbox", name: "selected", message: "选择要执行的 CRUD 配置(多选)", choices: entries.map((e) => ({ name: e.label, value: e.path })) }
|
|
339
|
-
]);
|
|
340
|
-
const selectedPaths = answer && answer.selected && Array.isArray(answer.selected) ? answer.selected : [];
|
|
341
|
-
if (selectedPaths.length === 0) {
|
|
342
|
-
Ec.info("未选择任何项,退出");
|
|
343
|
-
process.exit(0);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const parsed = Ut.parseArgument(options);
|
|
347
|
-
const skip = parsed.skip === true || process.argv.includes("-s") || process.argv.includes("--skip");
|
|
348
|
-
|
|
349
|
-
if (!skip) {
|
|
350
|
-
const appEnvPath = resolveAppEnvPath(cwd);
|
|
351
|
-
if (!appEnvPath) {
|
|
352
|
-
Ec.error(".r2mo/app.env 不存在;DPA 下也未找到 {id}-api/.r2mo/app.env");
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
355
|
-
loadAppEnv(appEnvPath);
|
|
356
|
-
checkEnv(REQUIRED_ENV_DB, "数据库环境变量");
|
|
357
|
-
checkEnv(REQUIRED_ENV_APP, "应用环境变量");
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
Ec.execute("ai ex-crud:配置已加载。");
|
|
361
|
-
|
|
362
|
-
const templateDir = path.resolve(__dirname, "..", "_template", "EXCEL", "ex-crud");
|
|
363
|
-
const loadedConfigs = selectedPaths.map((p) => ({ path: p, config: yaml.load(fs.readFileSync(p, "utf-8")) }));
|
|
364
|
-
const first = loadedConfigs[0];
|
|
365
|
-
let target = first.config && first.config.target && typeof first.config.target === "object"
|
|
366
|
-
? (first.config.target.root && first.config.target.module ? { root: String(first.config.target.root).trim(), module: String(first.config.target.module).trim() } : null)
|
|
367
|
-
: null;
|
|
368
|
-
|
|
369
|
-
if (target) {
|
|
370
|
-
const zeroModule = process.env.ZERO_MODULE;
|
|
371
|
-
if (!zeroModule || !String(zeroModule).trim()) {
|
|
372
|
-
Ec.error("存在 target 配置时,环境变量 ZERO_MODULE 必须已设置");
|
|
373
|
-
process.exit(1);
|
|
374
|
-
}
|
|
375
|
-
const dpaRoot = path.resolve(zeroModule || "", `zero-exmodule-${target.module}`);
|
|
376
|
-
if (!fs.existsSync(dpaRoot) || !isDpaRoot(dpaRoot)) {
|
|
377
|
-
Ec.error(`ZERO_MODULE 下 DPA 目录不是标准架构:${dpaRoot}`);
|
|
378
|
-
Ec.info("需存在 pom.xml 且包含 xxx-api、xxx-domain 子目录");
|
|
379
|
-
process.exit(1);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const excelRoot = resolveExcelRoot(cwd, target);
|
|
384
|
-
const domainName = target && target.module ? `zero-exmodule-${target.module}-domain` : null;
|
|
385
|
-
const pluginsBase = domainName
|
|
386
|
-
? path.join(excelRoot, domainName, "src", "main", "resources", "plugins")
|
|
387
|
-
: path.join(excelRoot, "src", "main", "resources", "plugins");
|
|
388
|
-
const pluginId = domainName ? `zero-exmodule-${target.module}` : "zero-launcher-configuration";
|
|
389
|
-
const rbacCrudDir = path.join(pluginsBase, pluginId, "security", "RBAC_CRUD");
|
|
390
|
-
const rbacRoleDir = path.join(pluginsBase, pluginId, "security", "RBAC_ROLE", "ADMIN.SUPER");
|
|
391
|
-
|
|
392
|
-
if (!fs.existsSync(rbacCrudDir)) fs.mkdirSync(rbacCrudDir, { recursive: true });
|
|
393
|
-
|
|
394
|
-
Ec.info("[ex-crud] 模板目录:" + templateDir);
|
|
395
|
-
Ec.info("[ex-crud] excelRoot=" + excelRoot + ",pluginId=" + pluginId + (domainName ? "(target 分流)" : ""));
|
|
396
|
-
Ec.info("[ex-crud] RBAC_CRUD:" + rbacCrudDir);
|
|
397
|
-
Ec.info("[ex-crud] RBAC_ROLE :" + rbacRoleDir);
|
|
398
|
-
|
|
399
|
-
for (const { path: configPath, config } of loadedConfigs) {
|
|
400
|
-
const valid = validateExCrudMetadata(config);
|
|
401
|
-
if (!valid.valid) {
|
|
402
|
-
Ec.info("[ex-crud] 警告(metadata 不合法,跳过执行):" + path.basename(configPath) + "," + (valid.error || ""));
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
const metadata = config.metadata;
|
|
406
|
-
const meta = {
|
|
407
|
-
keyword: metadata.keyword != null ? String(metadata.keyword).trim() : "",
|
|
408
|
-
identifier: metadata.identifier != null ? String(metadata.identifier).trim() : "",
|
|
409
|
-
actor: metadata.actor != null ? String(metadata.actor).trim() : "",
|
|
410
|
-
name: metadata.name != null ? String(metadata.name).trim() : "",
|
|
411
|
-
type: metadata.type != null ? String(metadata.type).trim() : ""
|
|
412
|
-
};
|
|
413
|
-
Ec.info("[ex-crud] 处理:" + (metadata.identifier || path.basename(configPath)));
|
|
414
|
-
try {
|
|
415
|
-
await copyTemplateWithReplace(templateDir, rbacCrudDir, meta, ["ex-crud.yaml", "README.md", "template-RBAC_ROLE.xlsx", ".DS_Store"]);
|
|
416
|
-
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
417
|
-
const bakPath = path.join(backupDir, path.basename(configPath) + ".bak");
|
|
418
|
-
fs.renameSync(configPath, bakPath);
|
|
419
|
-
Ec.info("[ex-crud] 已备份:" + path.basename(configPath) + " -> backup/" + path.basename(configPath) + ".bak");
|
|
420
|
-
} catch (err) {
|
|
421
|
-
Ec.info("[ex-crud] 生成或备份失败(未备份):" + path.basename(configPath) + "," + (err && err.message));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
Ec.info("[ex-crud] 已生成 CRUD 文件到 RBAC_CRUD");
|
|
426
|
-
|
|
427
|
-
// 2. 从生成的 CRUD 中收集 S_PERMISSION 表的所有 UUID(即 falcon 要关联的权限,不可能在库中已存在)
|
|
428
|
-
const permissionIds = await collectPermissionIdsFromCrudDir(rbacCrudDir);
|
|
429
|
-
Ec.info("[ex-crud] 从 CRUD 中收集到 S_PERMISSION UUID 数:" + permissionIds.length);
|
|
430
|
-
|
|
431
|
-
// 3. 若不 skip:连接数据库仅查角色,用户选择(角色不可为空,未选则取默认一条),写 falcon 角色权限表到 RBAC_ROLE
|
|
432
|
-
let roleIds = [];
|
|
433
|
-
if (!skip) {
|
|
434
|
-
const mysql = require("mysql2/promise");
|
|
435
|
-
const dbConfig = {
|
|
436
|
-
host: process.env.Z_DB_HOST || "localhost",
|
|
437
|
-
port: parseInt(process.env.Z_DB_PORT || "3306", 10),
|
|
438
|
-
user: process.env.Z_DB_APP_USER,
|
|
439
|
-
password: process.env.Z_DB_APP_PASS,
|
|
440
|
-
database: process.env.Z_DBS_INSTANCE
|
|
441
|
-
};
|
|
442
|
-
let conn;
|
|
443
|
-
try {
|
|
444
|
-
conn = await mysql.createConnection(dbConfig);
|
|
445
|
-
Ec.info("[ex-crud] 数据库已连接,查询角色");
|
|
446
|
-
|
|
447
|
-
const [roleRows] = await conn.execute("SELECT ID, NAME, CODE FROM S_ROLE ORDER BY NAME");
|
|
448
|
-
if (!roleRows || roleRows.length === 0) {
|
|
449
|
-
Ec.info("[ex-crud] S_ROLE 中无角色,跳过 falcon");
|
|
450
|
-
} else {
|
|
451
|
-
const answer = await inquirer.prompt([
|
|
452
|
-
{
|
|
453
|
-
type: "checkbox",
|
|
454
|
-
name: "selectedRoles",
|
|
455
|
-
message: "选择要授权当前 CRUD 的角色(可多选)",
|
|
456
|
-
choices: roleRows.map((r) => ({ name: `${r.NAME || r.CODE} (${r.ID})`, value: String(r.ID) }))
|
|
457
|
-
}
|
|
458
|
-
]);
|
|
459
|
-
const raw = answer && answer.selectedRoles;
|
|
460
|
-
if (Array.isArray(raw) && raw.length > 0) roleIds = raw.map((id) => (id != null ? String(id) : "")).filter((id) => id !== "");
|
|
461
|
-
else if (raw != null && raw !== "") roleIds = [String(raw)];
|
|
462
|
-
|
|
463
|
-
if (roleIds.length === 0) {
|
|
464
|
-
let [oneRole] = await conn.execute(
|
|
465
|
-
"SELECT ID FROM S_ROLE WHERE NAME = ? OR CODE = ? OR CODE = ? LIMIT 1",
|
|
466
|
-
["超级管理员", "ADMIN.SUPER", "ADMIN_SUPER"]
|
|
467
|
-
);
|
|
468
|
-
if (!oneRole || !oneRole[0]) {
|
|
469
|
-
[oneRole] = await conn.execute("SELECT ID FROM S_ROLE ORDER BY NAME LIMIT 1", []);
|
|
470
|
-
}
|
|
471
|
-
if (oneRole && oneRole[0]) {
|
|
472
|
-
const rid = oneRole[0].ID != null ? String(oneRole[0].ID) : String(oneRole[0].id);
|
|
473
|
-
roleIds = [rid];
|
|
474
|
-
Ec.info("[ex-crud] 未选角色,已用 S_ROLE 补一条(ROLE_ID=" + rid + ")");
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (permissionIds.length > 0 && roleIds.length > 0) {
|
|
480
|
-
const rolePermsToWrite = roleIds.flatMap((rid) => permissionIds.map((pid) => ({ ROLE_ID: rid, PERM_ID: pid })));
|
|
481
|
-
if (!fs.existsSync(rbacRoleDir)) fs.mkdirSync(rbacRoleDir, { recursive: true });
|
|
482
|
-
const ExcelJS = require("exceljs");
|
|
483
|
-
const roleFileName = "falcon-crud-" + (first.config.metadata && first.config.metadata.identifier ? String(first.config.metadata.identifier) : "batch").replace(/[^a-zA-Z0-9._-]/g, "_") + ".xlsx";
|
|
484
|
-
const outRolePath = path.join(rbacRoleDir, roleFileName);
|
|
485
|
-
const templatePath = path.resolve(__dirname, "..", "_template", "EXCEL", "ex-crud", "template-RBAC_ROLE.xlsx");
|
|
486
|
-
let roleWorkbook;
|
|
487
|
-
if (fs.existsSync(templatePath)) {
|
|
488
|
-
roleWorkbook = await new ExcelJS.Workbook().xlsx.readFile(templatePath);
|
|
489
|
-
const wsRole = roleWorkbook.getWorksheet("DATA-PERM") || roleWorkbook.worksheets[0];
|
|
490
|
-
if (wsRole) {
|
|
491
|
-
const tableNameRole = "R_ROLE_PERM";
|
|
492
|
-
let dataStartRow = 1;
|
|
493
|
-
let colRole = 1;
|
|
494
|
-
let colPerm = 2;
|
|
495
|
-
for (let r = 1; r <= 100; r++) {
|
|
496
|
-
const first = wsRole.getRow(r).getCell(1).value;
|
|
497
|
-
const v = first != null ? String(first).trim() : "";
|
|
498
|
-
if (v === "{TABLE}") {
|
|
499
|
-
const t2 = wsRole.getRow(r).getCell(2).value;
|
|
500
|
-
const tname = t2 != null ? String(t2).trim() : "";
|
|
501
|
-
if (tname === tableNameRole) {
|
|
502
|
-
dataStartRow = r + 3;
|
|
503
|
-
const enRow = wsRole.getRow(r + 2);
|
|
504
|
-
enRow.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
|
505
|
-
const val = cell && cell.value != null ? String(cell.value).trim() : "";
|
|
506
|
-
if (val === "roleId" || val === "ROLE_ID") colRole = colNumber;
|
|
507
|
-
if (val === "permId" || val === "PERM_ID") colPerm = colNumber;
|
|
508
|
-
});
|
|
509
|
-
break;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
rolePermsToWrite.forEach((pair, idx) => {
|
|
514
|
-
const row = wsRole.getRow(dataStartRow + idx);
|
|
515
|
-
row.getCell(colRole).value = pair.ROLE_ID;
|
|
516
|
-
row.getCell(colPerm).value = pair.PERM_ID;
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
} else {
|
|
520
|
-
roleWorkbook = new ExcelJS.Workbook();
|
|
521
|
-
const wsRole = roleWorkbook.addWorksheet("DATA-PERM");
|
|
522
|
-
wsRole.addRow([]);
|
|
523
|
-
wsRole.addRow([]);
|
|
524
|
-
wsRole.addRow(["{TABLE}", "R_ROLE_PERM", "角色和权限关系", "", ""]);
|
|
525
|
-
wsRole.addRow(["角色ID", "权限ID"]);
|
|
526
|
-
wsRole.addRow(["roleId", "permId"]);
|
|
527
|
-
rolePermsToWrite.forEach((p) => wsRole.addRow([p.ROLE_ID, p.PERM_ID]));
|
|
528
|
-
}
|
|
529
|
-
await roleWorkbook.xlsx.writeFile(outRolePath);
|
|
530
|
-
Ec.info("[ex-crud] 已写入 RBAC_ROLE/ADMIN.SUPER:" + outRolePath);
|
|
531
|
-
}
|
|
532
|
-
} catch (err) {
|
|
533
|
-
Ec.error("[ex-crud] 数据库或 falcon 写入失败:" + (err && err.message));
|
|
534
|
-
if (err && err.stack) Ec.info(err.stack);
|
|
535
|
-
} finally {
|
|
536
|
-
if (conn) await conn.end();
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
Ec.info("[ex-crud] ✅ 执行完成");
|
|
541
|
-
Ec.info("[ex-crud] 📋 汇总:");
|
|
542
|
-
Ec.info("[ex-crud] 📁 RBAC_CRUD = " + rbacCrudDir);
|
|
543
|
-
if (permissionIds.length > 0) Ec.info("[ex-crud] 🔑 S_PERMISSION UUID 数 = " + permissionIds.length);
|
|
544
|
-
if (roleIds.length > 0) Ec.info("[ex-crud] 👥 授权角色数 = " + roleIds.length);
|
|
545
|
-
};
|