zero-ai 1.0.75 → 1.0.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commander/ex-api.json +1 -2
- package/src/commander/ex-crud.json +1 -1
- package/src/commander-ai/fn.ex.api.js +192 -103
- package/src/commander-ai/fn.ex.crud.js +102 -58
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"executor": "executeExApi",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "从 ex-api 目录加载多份 yaml(metadata.r),多选后执行授权(数据库+Excel),处理成功后移至 backup 并加 .bak",
|
|
4
4
|
"command": "ex-api",
|
|
5
5
|
"options": [
|
|
6
|
-
{"name": "request", "alias": "r", "description": "请求方法与 URI,格式:\"<METHOD> <uri>\",如 \"GET /api/ambient\""},
|
|
7
6
|
{"name": "skip", "alias": "s", "type": "boolean", "description": "仅生成 Excel,跳过去重等检查", "default": false}
|
|
8
7
|
]
|
|
9
8
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"executor":"executeExCrud","description":"
|
|
1
|
+
{"executor":"executeExCrud","description":"从 ex-crud 目录加载多份 yaml,多选后按 metadata 从模板生成 CRUD Excel 及 RBAC 授权,处理成功后移至 backup 并加 .bak","command":"ex-crud","options":[{"name":"skip","alias":"s","type":"boolean","description":"仅生成 CRUD 文件,跳过数据库与角色选择","default":false}]}
|
|
@@ -8,7 +8,7 @@ const yaml = require("js-yaml");
|
|
|
8
8
|
const inquirer = require("inquirer");
|
|
9
9
|
const { v4: uuidv4 } = require("uuid");
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const CONFIG_DIR = ".r2mo/task/command/ex-api";
|
|
12
12
|
const REQUIRED_ENV_DB = ["Z_DB_TYPE", "Z_DB_HOST", "Z_DB_PORT", "Z_DBS_INSTANCE", "Z_DB_APP_USER", "Z_DB_APP_PASS"];
|
|
13
13
|
const REQUIRED_ENV_APP = ["Z_APP_ID", "Z_TENANT", "Z_SIGMA"];
|
|
14
14
|
const R2_BY_UUID = "9a0d5018-33ad-4c64-80bf-8ae7947c482f";
|
|
@@ -175,124 +175,73 @@ function isDpaRoot(dir) {
|
|
|
175
175
|
return fs.existsSync(apiDir) && fs.existsSync(domainDir);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
level: 1
|
|
193
|
-
ptype: "权限集 S_PERM_SET 类型"
|
|
194
|
-
pname: "权限集 S_PERM_SET 名称"
|
|
195
|
-
# keyword 可选;若存在则编码为 res.\${keyword} / act.\${keyword} / perm.\${keyword},否则按规则计算
|
|
196
|
-
# keyword: "app.test.data"
|
|
197
|
-
# target 可选;存在时需配置 ZERO_MODULE 且 DPA 目录 zero-exmodule-{module} 存在
|
|
198
|
-
# target:
|
|
199
|
-
# root: "ZERO_MODULE"
|
|
200
|
-
# module: "ambient"
|
|
201
|
-
`;
|
|
202
|
-
fs.writeFileSync(configFullPath, template, "utf-8");
|
|
203
|
-
Ec.info("配置文件缺失,已在下列路径写入模板:" + configFullPath);
|
|
204
|
-
Ec.info("请编辑后重新执行。参数格式: -r \"<METHOD> <uri>\" (uri 必须以 /api 为前缀)");
|
|
205
|
-
Ec.info("示例: ai ex-api -r \"GET /api/ambient\"");
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
178
|
+
/** 校验 metadata.r:须为 "<METHOD> <uri>",METHOD 常见动词,uri 以 /api 开头 */
|
|
179
|
+
function validateExApiR(r) {
|
|
180
|
+
if (!r || typeof r !== "string") return { valid: false, error: "r 为空" };
|
|
181
|
+
const s = String(r).trim();
|
|
182
|
+
if (!s) return { valid: false, error: "r 为空" };
|
|
183
|
+
const parts = s.split(/\s+/);
|
|
184
|
+
if (parts.length < 2) return { valid: false, error: "r 须为 \"<METHOD> <uri>\" 两段" };
|
|
185
|
+
const method = (parts[0] || "").toUpperCase();
|
|
186
|
+
const uri = parts.slice(1).join(" ").trim();
|
|
187
|
+
const allowed = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
188
|
+
if (!allowed.includes(method)) return { valid: false, error: "METHOD 须为 " + allowed.join("/") };
|
|
189
|
+
if (!uri.startsWith("/api")) return { valid: false, error: "uri 须以 /api 为前缀" };
|
|
190
|
+
return { valid: true };
|
|
191
|
+
}
|
|
208
192
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
193
|
+
/** 解析 ex-api 配置目录:cwd / 上级 / 上上级 */
|
|
194
|
+
function resolveExApiConfigDir(cwd) {
|
|
195
|
+
const primary = path.resolve(cwd, CONFIG_DIR);
|
|
196
|
+
if (fs.existsSync(primary) && fs.statSync(primary).isDirectory()) return primary;
|
|
197
|
+
const parent = path.resolve(cwd, "..", CONFIG_DIR);
|
|
198
|
+
if (fs.existsSync(parent) && fs.statSync(parent).isDirectory()) return parent;
|
|
199
|
+
const grand = path.resolve(cwd, "..", "..", CONFIG_DIR);
|
|
200
|
+
if (fs.existsSync(grand) && fs.statSync(grand).isDirectory()) return grand;
|
|
201
|
+
return primary;
|
|
202
|
+
}
|
|
219
203
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
204
|
+
/** 表格化打印 ex-api 汇总 */
|
|
205
|
+
function printExApiTable(results) {
|
|
206
|
+
const rows = results.map((r) => ({
|
|
207
|
+
identifier: r.identifier || "—",
|
|
208
|
+
request: r.request || "—",
|
|
209
|
+
ok: r.ok ? "✓" : "✗",
|
|
210
|
+
error: r.error || "—"
|
|
211
|
+
}));
|
|
212
|
+
const col = (arr, key) => arr.map((x) => String(x[key] != null ? x[key] : ""));
|
|
213
|
+
const max = (arr) => Math.max(2, ...arr.map((s) => (s && s.length) || 0));
|
|
214
|
+
const wId = max(col(rows, "identifier"));
|
|
215
|
+
const wReq = Math.min(max(col(rows, "request")), 48);
|
|
216
|
+
const wErr = Math.min(max(col(rows, "error")), 32);
|
|
217
|
+
const sep = " | ";
|
|
218
|
+
Ec.info("[ex-api] 汇总:");
|
|
219
|
+
Ec.info(" " + "identifier".padEnd(wId) + sep + "request".padEnd(wReq) + sep + "ok" + sep + "error".padEnd(wErr));
|
|
220
|
+
rows.forEach((r) => Ec.info(" " + (r.identifier + "").padEnd(wId) + sep + (r.request + "").slice(0, wReq).padEnd(wReq) + sep + r.ok + sep + (r.error + "").slice(0, wErr)));
|
|
221
|
+
}
|
|
232
222
|
|
|
223
|
+
/** 单条 API 执行:使用 config.metadata.r 作为 request,执行 DB + Excel,返回汇总 */
|
|
224
|
+
async function runOneExApi(cwd, conn, config, requestRaw, skip) {
|
|
233
225
|
const metadata = config.metadata;
|
|
234
226
|
const target = config.target;
|
|
235
|
-
if (target && target.root && target.module) {
|
|
236
|
-
const zeroModule = process.env.ZERO_MODULE;
|
|
237
|
-
if (!zeroModule || !zeroModule.trim()) {
|
|
238
|
-
Ec.error("存在 target 配置时,环境变量 ZERO_MODULE 必须已设置");
|
|
239
|
-
process.exit(1);
|
|
240
|
-
}
|
|
241
|
-
const dpaRoot = path.resolve(zeroModule, `zero-exmodule-${target.module}`);
|
|
242
|
-
if (!fs.existsSync(dpaRoot) || !isDpaRoot(dpaRoot)) {
|
|
243
|
-
Ec.error(`ZERO_MODULE 下 DPA 目录不是标准架构:${dpaRoot}`);
|
|
244
|
-
Ec.info("需存在 pom.xml 且包含 xxx-api、xxx-domain 子目录");
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// 4. 参数:-r "<METHOD> <uri>" ,-s 可选
|
|
250
|
-
const parsed = Ut.parseArgument(options);
|
|
251
|
-
const skip = parsed.skip === true || process.argv.includes("-s") || process.argv.includes("--skip");
|
|
252
227
|
let method = "";
|
|
253
228
|
let uri = "";
|
|
254
|
-
const requestRaw = parsed.request;
|
|
255
229
|
if (requestRaw && String(requestRaw).trim()) {
|
|
256
230
|
const parts = String(requestRaw).trim().split(/\s+/);
|
|
257
231
|
if (parts.length >= 2) {
|
|
258
232
|
method = parts[0].toUpperCase();
|
|
259
233
|
uri = parts.slice(1).join(" ").trim();
|
|
260
|
-
} else if (parts.length === 1) {
|
|
261
|
-
Ec.error("参数 -r 格式应为 \"<METHOD> <uri>\",例如 \"GET /api/ambient\"");
|
|
262
|
-
process.exit(1);
|
|
263
234
|
}
|
|
264
235
|
}
|
|
265
236
|
if (!skip && (!method || !uri)) {
|
|
266
|
-
|
|
267
|
-
Ec.info("参数格式: -r \"<METHOD> <uri>\" 或 --request \"<METHOD> <uri>\"");
|
|
268
|
-
Ec.info("示例: ai ex-api -r \"GET /api/ambient\"(uri 必须以 /api 为前缀)");
|
|
269
|
-
process.exit(1);
|
|
237
|
+
return { identifier: metadata.identifier || "—", request: requestRaw || "—", ok: false, error: "缺少 metadata.r 或格式非 \"<METHOD> <uri>\"" };
|
|
270
238
|
}
|
|
271
239
|
if (!skip && uri && !uri.trim().startsWith("/api")) {
|
|
272
|
-
|
|
273
|
-
Ec.info("当前 uri:" + (uri || ""));
|
|
274
|
-
Ec.info("示例: ai ex-api -r \"GET /api/ambient\"");
|
|
275
|
-
process.exit(1);
|
|
240
|
+
return { identifier: metadata.identifier || "—", request: requestRaw || "—", ok: false, error: "uri 必须以 /api 为前缀" };
|
|
276
241
|
}
|
|
277
242
|
|
|
278
|
-
Ec.execute("ai ex-api:配置已加载,环境与参数检查通过。");
|
|
279
|
-
|
|
280
|
-
const dbConfig = {
|
|
281
|
-
host: process.env.Z_DB_HOST || "localhost",
|
|
282
|
-
port: parseInt(process.env.Z_DB_PORT || "3306", 10),
|
|
283
|
-
user: process.env.Z_DB_APP_USER,
|
|
284
|
-
password: process.env.Z_DB_APP_PASS,
|
|
285
|
-
database: process.env.Z_DBS_INSTANCE
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
const mysql = require("mysql2/promise");
|
|
289
|
-
Ec.info("[ex-api] 配置:" + CONFIG_PATH + " | request=" + (requestRaw || "") + " | skip=" + skip);
|
|
290
|
-
let conn = await mysql.createConnection(dbConfig);
|
|
291
|
-
Ec.info("[ex-api] 数据库已连接:" + (dbConfig.database || "") + " @" + (dbConfig.host || "") + ":" + (dbConfig.port || ""));
|
|
292
|
-
|
|
293
|
-
// 约定:执行时数据表已存在且结构固定,仅执行 DML(SELECT/INSERT/INSERT IGNORE),禁止表扫描、DDL、元数据查询(如 SHOW COLUMNS)。
|
|
294
243
|
try {
|
|
295
|
-
|
|
244
|
+
const appId = process.env.Z_APP_ID;
|
|
296
245
|
const tenantId = process.env.Z_TENANT;
|
|
297
246
|
const sigma = process.env.Z_SIGMA;
|
|
298
247
|
|
|
@@ -799,13 +748,153 @@ metadata:
|
|
|
799
748
|
Ec.info("[ex-api] 📦 R_ROLE_PERM 本次写入(ROLE_ID, PERM_ID):");
|
|
800
749
|
insertedRolePerms.forEach((r, i) => Ec.info("[ex-api] [" + (i + 1) + "] " + r.ROLE_ID + ", " + r.PERM_ID));
|
|
801
750
|
}
|
|
751
|
+
return {
|
|
752
|
+
identifier: metadata.identifier || "—",
|
|
753
|
+
request: requestRaw || "—",
|
|
754
|
+
actionId: actionId || "—",
|
|
755
|
+
resourceId: resourceId || "—",
|
|
756
|
+
permissionId: permissionId || "—",
|
|
757
|
+
roleCount: roleIds ? roleIds.length : 0,
|
|
758
|
+
outResPath: outResPath || "—",
|
|
759
|
+
outRolePath: outRolePath || "—",
|
|
760
|
+
ok: true
|
|
761
|
+
};
|
|
802
762
|
} catch (err) {
|
|
803
763
|
Ec.error("[ex-api] 执行失败:" + (err && err.message));
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
764
|
+
return {
|
|
765
|
+
identifier: (metadata && metadata.identifier) || "—",
|
|
766
|
+
request: requestRaw || "—",
|
|
767
|
+
actionId: "—",
|
|
768
|
+
resourceId: "—",
|
|
769
|
+
permissionId: "—",
|
|
770
|
+
roleCount: 0,
|
|
771
|
+
outResPath: "—",
|
|
772
|
+
outRolePath: "—",
|
|
773
|
+
ok: false,
|
|
774
|
+
error: (err && err.message) || String(err)
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
module.exports = async (options) => {
|
|
780
|
+
const cwd = process.cwd();
|
|
781
|
+
const configDir = resolveExApiConfigDir(cwd);
|
|
782
|
+
if (!fs.existsSync(configDir)) {
|
|
783
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
784
|
+
const templatePath = path.join(configDir, "ex-api.yaml");
|
|
785
|
+
const template = `# ai ex-api 使用此配置,请按项目修改
|
|
786
|
+
metadata:
|
|
787
|
+
r: "GET /api/ambient"
|
|
788
|
+
identifier: "核心标识符"
|
|
789
|
+
brief: "接口描述"
|
|
790
|
+
resource: "resource.ambient"
|
|
791
|
+
level: 1
|
|
792
|
+
ptype: "权限集 S_PERM_SET 类型"
|
|
793
|
+
pname: "权限集 S_PERM_SET 名称"
|
|
794
|
+
# keyword 可选
|
|
795
|
+
# target 可选;存在时需 ZERO_MODULE 与 zero-exmodule-{module}
|
|
796
|
+
# target:
|
|
797
|
+
# root: "ZERO_MODULE"
|
|
798
|
+
# module: "ambient"
|
|
799
|
+
`;
|
|
800
|
+
fs.writeFileSync(templatePath, template, "utf-8");
|
|
801
|
+
Ec.info("配置目录缺失,已创建并写入模板:" + templatePath);
|
|
802
|
+
Ec.info("请编辑后重新执行: ai ex-api");
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const backupDir = path.join(configDir, "backup");
|
|
807
|
+
const allEntries = fs.readdirSync(configDir, { withFileTypes: true });
|
|
808
|
+
const yamlFiles = allEntries.filter((e) => !e.isDirectory() && e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml")));
|
|
809
|
+
const entries = [];
|
|
810
|
+
for (const e of yamlFiles) {
|
|
811
|
+
const f = e.name;
|
|
812
|
+
const full = path.join(configDir, f);
|
|
813
|
+
try {
|
|
814
|
+
const config = yaml.load(fs.readFileSync(full, "utf-8"));
|
|
815
|
+
if (!config || !config.metadata) continue;
|
|
816
|
+
const r = config.metadata.r != null ? String(config.metadata.r).trim() : "";
|
|
817
|
+
if (!r) {
|
|
818
|
+
Ec.info("[ex-api] 跳过(无 metadata.r):" + f);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const valid = validateExApiR(r);
|
|
822
|
+
if (!valid.valid) {
|
|
823
|
+
Ec.info("[ex-api] 警告(r 不合法,已跳过):" + f + "," + (valid.error || ""));
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
const label = (config.metadata.identifier || f) + " | " + (config.metadata.brief || r);
|
|
827
|
+
entries.push({ path: full, config, label });
|
|
828
|
+
} catch (_) {}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (entries.length === 0) {
|
|
832
|
+
Ec.error("[ex-api] 无有效配置:请在 " + configDir + " 下添加含 metadata.r 的 yaml");
|
|
808
833
|
process.exit(1);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const answer = await inquirer.prompt([
|
|
837
|
+
{ type: "checkbox", name: "selected", message: "选择要执行的 API(多选)", choices: entries.map((e) => ({ name: e.label, value: e.path })) }
|
|
838
|
+
]);
|
|
839
|
+
const selectedPaths = answer && answer.selected && Array.isArray(answer.selected) ? answer.selected : [];
|
|
840
|
+
if (selectedPaths.length === 0) {
|
|
841
|
+
Ec.info("未选择任何项,退出");
|
|
842
|
+
process.exit(0);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const appEnvPath = resolveAppEnvPath(cwd);
|
|
846
|
+
if (!appEnvPath) {
|
|
847
|
+
Ec.error(".r2mo/app.env 不存在;DPA 下也未找到 {id}-api/.r2mo/app.env");
|
|
848
|
+
process.exit(1);
|
|
849
|
+
}
|
|
850
|
+
loadAppEnv(appEnvPath);
|
|
851
|
+
checkEnv(REQUIRED_ENV_DB, "数据库环境变量");
|
|
852
|
+
checkEnv(REQUIRED_ENV_APP, "应用环境变量(Z_APP_ID / Z_TENANT / Z_SIGMA)");
|
|
853
|
+
|
|
854
|
+
const dbConfig = {
|
|
855
|
+
host: process.env.Z_DB_HOST || "localhost",
|
|
856
|
+
port: parseInt(process.env.Z_DB_PORT || "3306", 10),
|
|
857
|
+
user: process.env.Z_DB_APP_USER,
|
|
858
|
+
password: process.env.Z_DB_APP_PASS,
|
|
859
|
+
database: process.env.Z_DBS_INSTANCE
|
|
860
|
+
};
|
|
861
|
+
const parsed = Ut.parseArgument(options);
|
|
862
|
+
const skip = parsed.skip === true || process.argv.includes("-s") || process.argv.includes("--skip");
|
|
863
|
+
|
|
864
|
+
const mysql = require("mysql2/promise");
|
|
865
|
+
let conn;
|
|
866
|
+
try {
|
|
867
|
+
conn = await mysql.createConnection(dbConfig);
|
|
868
|
+
Ec.info("[ex-api] 数据库已连接,执行 " + selectedPaths.length + " 条 API");
|
|
869
|
+
const results = [];
|
|
870
|
+
for (const configPath of selectedPaths) {
|
|
871
|
+
const config = yaml.load(fs.readFileSync(configPath, "utf-8"));
|
|
872
|
+
const requestRaw = config.metadata && config.metadata.r != null ? String(config.metadata.r).trim() : "";
|
|
873
|
+
if (!requestRaw) {
|
|
874
|
+
results.push({ identifier: config.metadata?.identifier || "—", request: "—", ok: false, error: "无 metadata.r" });
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const valid = validateExApiR(requestRaw);
|
|
878
|
+
if (!valid.valid) {
|
|
879
|
+
Ec.info("[ex-api] 警告(r 不合法,跳过执行):" + path.basename(configPath) + "," + (valid.error || ""));
|
|
880
|
+
results.push({ identifier: config.metadata?.identifier || "—", request: requestRaw, ok: false, error: valid.error || "r 不合法" });
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
Ec.info("[ex-api] 处理:" + (config.metadata.identifier || path.basename(configPath)) + " (" + requestRaw + ")");
|
|
884
|
+
const one = await runOneExApi(cwd, conn, config, requestRaw, skip);
|
|
885
|
+
results.push(one);
|
|
886
|
+
if (one.ok) {
|
|
887
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
888
|
+
const bakPath = path.join(backupDir, path.basename(configPath) + ".bak");
|
|
889
|
+
try {
|
|
890
|
+
fs.renameSync(configPath, bakPath);
|
|
891
|
+
Ec.info("[ex-api] 已备份:" + path.basename(configPath) + " -> backup/" + path.basename(configPath) + ".bak");
|
|
892
|
+
} catch (errBak) {
|
|
893
|
+
Ec.info("[ex-api] 备份失败(已忽略):" + configPath + "," + (errBak && errBak.message));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
printExApiTable(results);
|
|
809
898
|
} finally {
|
|
810
899
|
if (conn) await conn.end();
|
|
811
900
|
}
|
|
@@ -8,7 +8,7 @@ const yaml = require("js-yaml");
|
|
|
8
8
|
const inquirer = require("inquirer");
|
|
9
9
|
const { v4: uuidv4 } = require("uuid");
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const CONFIG_DIR = ".r2mo/task/command/ex-crud";
|
|
12
12
|
const REQUIRED_ENV_DB = ["Z_DB_TYPE", "Z_DB_HOST", "Z_DB_PORT", "Z_DBS_INSTANCE", "Z_DB_APP_USER", "Z_DB_APP_PASS"];
|
|
13
13
|
const REQUIRED_ENV_APP = ["Z_APP_ID", "Z_TENANT", "Z_SIGMA"];
|
|
14
14
|
|
|
@@ -261,23 +261,35 @@ async function copyTemplateWithReplace(templateDir, destDir, meta, skipNames) {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
/**
|
|
265
|
-
function
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (
|
|
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;
|
|
272
284
|
return primary;
|
|
273
285
|
}
|
|
274
286
|
|
|
275
287
|
module.exports = async (options) => {
|
|
276
288
|
const cwd = process.cwd();
|
|
277
|
-
const
|
|
278
|
-
if (!fs.existsSync(
|
|
279
|
-
|
|
280
|
-
|
|
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");
|
|
281
293
|
const template = `# ai ex-crud 使用此配置,请按项目修改
|
|
282
294
|
metadata:
|
|
283
295
|
keyword: "log"
|
|
@@ -290,56 +302,45 @@ metadata:
|
|
|
290
302
|
# root: "ZERO_MODULE"
|
|
291
303
|
# module: "ambient"
|
|
292
304
|
`;
|
|
293
|
-
fs.writeFileSync(
|
|
294
|
-
Ec.info("
|
|
305
|
+
fs.writeFileSync(templatePath, template, "utf-8");
|
|
306
|
+
Ec.info("配置目录缺失,已创建并写入模板:" + templatePath);
|
|
295
307
|
Ec.info("请编辑后重新执行: ai ex-crud");
|
|
296
308
|
process.exit(1);
|
|
297
309
|
}
|
|
298
310
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
+
}
|
|
309
330
|
}
|
|
310
331
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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;
|
|
332
|
+
if (entries.length === 0) {
|
|
333
|
+
Ec.error("[ex-crud] 无有效配置:请在 " + configDir + " 下添加含合法 metadata(keyword、identifier)的 yaml");
|
|
334
|
+
process.exit(1);
|
|
321
335
|
}
|
|
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
336
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
}
|
|
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);
|
|
343
344
|
}
|
|
344
345
|
|
|
345
346
|
const parsed = Ut.parseArgument(options);
|
|
@@ -358,8 +359,27 @@ metadata:
|
|
|
358
359
|
|
|
359
360
|
Ec.execute("ai ex-crud:配置已加载。");
|
|
360
361
|
|
|
361
|
-
// 1. 模板目录(R2MO-INIT 包内)与输出目录(与 ex-api 一致:有 target 时分流到 zero-exmodule-{module},无 target 时为 zero-launcher-configuration)
|
|
362
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
|
+
|
|
363
383
|
const excelRoot = resolveExcelRoot(cwd, target);
|
|
364
384
|
const domainName = target && target.module ? `zero-exmodule-${target.module}-domain` : null;
|
|
365
385
|
const pluginsBase = domainName
|
|
@@ -376,7 +396,31 @@ metadata:
|
|
|
376
396
|
Ec.info("[ex-crud] RBAC_CRUD:" + rbacCrudDir);
|
|
377
397
|
Ec.info("[ex-crud] RBAC_ROLE :" + rbacRoleDir);
|
|
378
398
|
|
|
379
|
-
|
|
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
|
+
}
|
|
380
424
|
|
|
381
425
|
Ec.info("[ex-crud] 已生成 CRUD 文件到 RBAC_CRUD");
|
|
382
426
|
|
|
@@ -436,7 +480,7 @@ metadata:
|
|
|
436
480
|
const rolePermsToWrite = roleIds.flatMap((rid) => permissionIds.map((pid) => ({ ROLE_ID: rid, PERM_ID: pid })));
|
|
437
481
|
if (!fs.existsSync(rbacRoleDir)) fs.mkdirSync(rbacRoleDir, { recursive: true });
|
|
438
482
|
const ExcelJS = require("exceljs");
|
|
439
|
-
const roleFileName = "falcon-crud-" + (
|
|
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";
|
|
440
484
|
const outRolePath = path.join(rbacRoleDir, roleFileName);
|
|
441
485
|
const templatePath = path.resolve(__dirname, "..", "_template", "EXCEL", "ex-crud", "template-RBAC_ROLE.xlsx");
|
|
442
486
|
let roleWorkbook;
|