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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-ai",
3
- "version": "1.0.75",
3
+ "version": "1.0.76",
4
4
  "description": "Zero Ecotope AI",
5
5
  "main": "src/ai.js",
6
6
  "bin": {
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "executor": "executeExApi",
3
- "description": "对已开发 API 进行授权(数据库 + Excel),需 .r2mo/task/command/ex-api.yaml",
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":" ex-crud.yaml 从模板生成 CRUD Excel 及 RBAC 授权(RBAC_CRUD + falcon),需 .r2mo/task/command/ex-crud.yaml","command":"ex-crud","options":[{"name":"skip","alias":"s","type":"boolean","description":"仅生成 CRUD 文件,跳过数据库与角色选择","default":false}]}
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 CONFIG_PATH = ".r2mo/task/command/ex-api.yaml";
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
- module.exports = async (options) => {
179
- const cwd = process.cwd();
180
-
181
- // 1. 前置:配置文件必须存在;缺失时在配置路径中写入模板
182
- const configFullPath = path.resolve(cwd, CONFIG_PATH);
183
- if (!fs.existsSync(configFullPath)) {
184
- const configDir = path.dirname(configFullPath);
185
- if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
186
- const template = `# ai ex-api 使用此配置,请按项目修改
187
- # 参数格式:-r "<METHOD> <uri>" uri 必须以 /api 为前缀
188
- metadata:
189
- identifier: "核心标识符"
190
- brief: "接口描述"
191
- resource: "resource.ambient"
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
- // 2. 环境变量:先加载 app.env 再检查
210
- const appEnvPath = resolveAppEnvPath(cwd);
211
- if (!appEnvPath) {
212
- Ec.error(".r2mo/app.env 不存在;DPA 下也未找到 {id}-api/.r2mo/app.env");
213
- process.exit(1);
214
- }
215
- loadAppEnv(appEnvPath);
216
- Ec.info("已加载环境变量:" + appEnvPath);
217
- checkEnv(REQUIRED_ENV_DB, "数据库环境变量");
218
- checkEnv(REQUIRED_ENV_APP, "应用环境变量(Z_APP_ID / Z_TENANT / Z_SIGMA)");
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
- // 3. 解析 ex-api.yaml(不校验格式)
221
- let config;
222
- try {
223
- config = yaml.load(fs.readFileSync(configFullPath, "utf-8"));
224
- } catch (e) {
225
- Ec.error("ex-api.yaml 解析失败:" + e.message);
226
- process.exit(1);
227
- }
228
- if (!config || !config.metadata) {
229
- Ec.error("ex-api.yaml 需包含 metadata 节点");
230
- process.exit(1);
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
- Ec.error("缺少必需参数:请求方法与 URI");
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
- Ec.error("参数 uri 必须以 /api 为前缀。");
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
- const appId = process.env.Z_APP_ID;
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
- if (err && err.code) Ec.error("[ex-api] 错误码:" + err.code);
805
- if (err && err.sqlMessage) Ec.error("[ex-api] SQL:" + err.sqlMessage);
806
- if (err && err.sql) Ec.error("[ex-api] 语句:" + (typeof err.sql === "string" ? err.sql : err.sql.join(" ")));
807
- if (err && err.stack) Ec.error("[ex-api] 堆栈:" + err.stack);
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 CONFIG_PATH = ".r2mo/task/command/ex-crud.yaml";
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
- /** 解析 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;
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 configFullPath = resolveExCrudConfigPath(cwd);
278
- if (!fs.existsSync(configFullPath)) {
279
- const configDir = path.dirname(configFullPath);
280
- if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
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(configFullPath, template, "utf-8");
294
- Ec.info("配置文件缺失,已在下列路径写入模板:" + configFullPath);
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
- 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);
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
- 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;
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
- 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
- }
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
- await copyTemplateWithReplace(templateDir, rbacCrudDir, meta, ["ex-crud.yaml", "README.md", "template-RBAC_ROLE.xlsx", ".DS_Store"]);
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-" + (meta.identifier || "default").replace(/[^a-zA-Z0-9._-]/g, "_") + ".xlsx";
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;