e10-data 0.1.12 → 0.1.13

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.
Files changed (3) hide show
  1. package/README.md +207 -210
  2. package/bin/e10-data.mjs +1519 -1519
  3. package/package.json +29 -29
package/bin/e10-data.mjs CHANGED
@@ -1,1519 +1,1519 @@
1
- #!/usr/bin/env node
2
- // -*- coding: utf-8 -*-
3
- /**
4
- * e10-data CLI —— data-assistant 专家统一数据接口命令行(2026-09-03 扩展)
5
- *
6
- * 覆盖(数据查询链路全部家族;登录/HRM 复用 e10-login / hrm-cli,checkEbModulePermission 已废弃不提供):
7
- * e10-data query DSL 查询 executeQueryDsl(conn/model/schema/tableId/dslQuery/debug)
8
- * e10-data ds tables|fields|list|chart|stat|count 数据源(业务模块)取数与元数据
9
- * 取数(list/chart/stat/count)自动解析并携带 queryDto.appId(v0.1.12):
10
- * 顺序 chat_bi_eb_config_map(dw,按 obj_id) → ebdf_obj(ec_edcapp,按 id),实时查、不缓存;
11
- * --app-id <id> 显式指定 / --app-id auto 强制自动 / --no-app-id 跳过(最快)
12
- * e10-data doc count|list 文档查询 documentListCount / documentList
13
- * e10-data noun list 词库 getChatDocsBySkillKey
14
- * e10-data field id2name|name2id 关联字段翻译 testRemoteBrowser / complete
15
- * e10-data conf get|batch|batch-file|group|group-file|resolve|resolve-file|add|add-file|update|delete
16
- * 环境参数配置中心(chatBi config map,替代 config-resolve 脚本 URL 直连;
17
- * confType=api 时支持 confGroup 分组二级定位,见 --group)
18
- * e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-attendance 病假/事假,2026-09-07)
19
- *
20
- * 通用:--json 原样输出;复杂 JSON 参数支持 @文件 或 内联(中文走 @文件 更稳)
21
- * 认证:加解密全部由 e10-login 负责(本 CLI 不实现任何加解密)。凭据出口优先级:
22
- * ① e10-login auth --json → ② e10-login whoami --json → ③ e10-login whoami(文本解析)
23
- * → ④ 明文镜像 ~/.workbuddy/e10-login-cache.md(兜底,命中时告警);均不可用时提示先 e10-login oidc 登录
24
- *
25
- * 用法示例:
26
- * node e10-data.mjs query --conn 1144919996804022272 --model dw --schema dw --dsl @q.json
27
- * node e10-data.mjs ds count --group weaver-project-servicetask --table 715738355537494016
28
- * node e10-data.mjs doc count --name 测试
29
- * node e10-data.mjs noun list --skill rd-department
30
- * node e10-data.mjs field id2name --module ebuilder/form --type ebuilder \
31
- * --ids 3194494277170470571 --form-param '{"fieldId":"966849910772850766","module":"ebuilderform"}'
32
- */
33
- import fs from "node:fs";
34
- import os from "node:os";
35
- import path from "node:path";
36
- import crypto from "node:crypto";
37
- import { spawnSync } from "node:child_process";
38
- import { fileURLToPath } from "node:url";
39
-
40
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
41
- function cliVersion() {
42
- try { return JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")).version; }
43
- catch { return "dev"; }
44
- }
45
-
46
- const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36";
47
- const EB_BIZ_ID = "10000000000000000";
48
-
49
- // ---------------- 认证(加解密全部交由 e10-login,本 CLI 不再实现任何加解密)----------------
50
- // 背景:auth 文件由 e10-login 自己的密钥体系加密 —— e10-login 1.5.0 起为「每机随机密钥 + OS keychain
51
- // (@napi-rs/keyring,service=e10-cli / account=auth-key;keychain 不可用才落 ~/.e10-cli/.key)」,
52
- // 早期版本为固定 seed。**只有 e10-login 解得开**,所以本 CLI 只负责「向 e10-login 要明文凭据」,
53
- // 不再自行解密 —— e10-login 以后更换密钥体系 / 算法 / profile 机制,本 CLI 都无需改动。
54
- // 凭据出口优先级(命中即返回;命中的"模式名"会记入 ~/.e10-cli/.e10-data-auth-mode,只存模式、不存凭据):
55
- // ① e10-login auth --json 正式契约(推荐 e10-login 侧新增;一旦可用自动优先)
56
- // ② e10-login whoami --json 其 JSON 输出带 cookies 字段时可用
57
- // ③ e10-login whoami 文本输出解析(当前 e10-login 1.5.0 的实际可用路径)
58
- // ④ ~/.workbuddy/e10-login-cache.md 明文镜像 —— 最后兜底,命中时打印告警(该文件可能已过期)
59
- // 环境变量:E10_DATA_AUTH_PROBE=1 强制重新探测优先级;E10_DATA_AUTH_MODE=<模式名> 手动指定;
60
- // E10_DATA_QUIET=1 抑制告警输出(告警一律走 stderr,不污染 --json 的 stdout)。
61
- const AUTH_MODE_FILE = path.join(os.homedir(), ".e10-cli", ".e10-data-auth-mode");
62
-
63
- /** 定位当前 profile 的 auth 文件(仅用于"是否已登录"的判断与报错提示,本 CLI 不读其内容做解密) */
64
- function authFilePath() {
65
- const home = os.homedir();
66
- let profile = "default";
67
- try {
68
- const activeFile = path.join(home, ".e10-cli", "profile");
69
- if (fs.existsSync(activeFile)) { const n = fs.readFileSync(activeFile, "utf-8").trim(); if (n) profile = n; }
70
- } catch {}
71
- return { profile, file: path.join(home, ".e10-cli", "profiles", profile, "auth") };
72
- }
73
-
74
- let lastLoginDiag = "";
75
- /** 调用 e10-login(凭据的唯一来源);失败返回 null,并把诊断信息留在 lastLoginDiag */
76
- function runLoginCli(args) {
77
- try {
78
- const r = spawnSync("e10-login", args, { shell: true, encoding: "utf-8", timeout: 20000, windowsHide: true });
79
- const out = (r.stdout || "").trim();
80
- if (r.error || r.status !== 0) {
81
- lastLoginDiag = ((r.stderr || "") + out).trim().split(/\r?\n/).slice(0, 2).join(" / ") || (r.error ? String(r.error.message) : "exit=" + r.status);
82
- return null;
83
- }
84
- return out || null;
85
- } catch (e) { lastLoginDiag = String((e && e.message) || e); return null; }
86
- }
87
-
88
- /** 归一化 + 校验(缺 baseUrl / eteamsid / cookie 一律视为该出口不可用,继续下一个出口) */
89
- function normalizeAuth(o) {
90
- if (!o) return null;
91
- const baseUrl = String(o.baseUrl || "").trim().replace(/\/+$/, "");
92
- const eteamsid = String(o.eteamsid || "").trim();
93
- const cookie = String(o.cookie || "").trim();
94
- if (!baseUrl || !eteamsid || !cookie) return null;
95
- if (/^\((未知|空)\)$/.test(eteamsid) || /^\((未知|空)\)$/.test(cookie)) return null;
96
- return { baseUrl, eteamsid, cookie, userId: String(o.userId || "").trim(), tenantKey: String(o.tenantKey || "").trim(), source: o.source };
97
- }
98
-
99
- /** ① e10-login auth --json(正式契约:{v,baseUrl,eteamsId,cookie,userAgent,userId,tenantKey,agentType,profile}) */
100
- function providerAuthJson() {
101
- const out = runLoginCli(["auth", "--json"]);
102
- if (!out) return null;
103
- let j; try { j = JSON.parse(out); } catch { return null; }
104
- if (!j || typeof j !== "object" || Array.isArray(j)) return null;
105
- return normalizeAuth({
106
- baseUrl: j.baseUrl || j.base_url,
107
- eteamsid: j.eteamsId || j.eteamsid,
108
- cookie: typeof j.cookie === "string" ? j.cookie : "",
109
- userId: j.userId || j.employeeId,
110
- tenantKey: j.tenantKey,
111
- source: "e10-login auth --json",
112
- });
113
- }
114
-
115
- /** ② e10-login whoami --json(需其 JSON 带 cookies 字符串;当前 1.5.0 未带 → 自动落到 ③) */
116
- function providerWhoamiJson() {
117
- const out = runLoginCli(["whoami", "--json"]);
118
- if (!out) return null;
119
- let j; try { j = JSON.parse(out); } catch { return null; }
120
- if (!j || typeof j !== "object" || Array.isArray(j)) return null;
121
- const cookie = typeof j.cookies === "string" ? j.cookies : (typeof j.cookie === "string" ? j.cookie : "");
122
- return normalizeAuth({
123
- baseUrl: j.baseUrl,
124
- eteamsid: j.eteamsId || j.eteamsid,
125
- cookie,
126
- userId: j.employeeId || j.userId,
127
- tenantKey: j.tenantKey,
128
- source: "e10-login whoami --json",
129
- });
130
- }
131
-
132
- /** ③ e10-login whoami(文本输出解析:employeeId / tenantKey / baseUrl / ETEAMSID / cookies / User-Agent) */
133
- function providerWhoamiText() {
134
- const out = runLoginCli(["whoami"]);
135
- if (!out) return null;
136
- const m = {};
137
- for (const line of out.split(/\r?\n/)) {
138
- const x = line.match(/^\s*([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.+?)\s*$/);
139
- if (x) m[x[1].toLowerCase()] = x[2];
140
- }
141
- return normalizeAuth({
142
- baseUrl: m["baseurl"],
143
- eteamsid: m["eteamsid"],
144
- cookie: m["cookies"],
145
- userId: m["employeeid"] || m["userid"],
146
- tenantKey: m["tenantkey"],
147
- source: "e10-login whoami(文本)",
148
- });
149
- }
150
-
151
- /** ④ 明文镜像(最后兜底:不涉及任何解密,但可能已过期 → 命中时告警) */
152
- function providerCacheMd() {
153
- const p = path.join(os.homedir(), ".workbuddy", "e10-login-cache.md");
154
- if (!fs.existsSync(p)) return null;
155
- const txt = fs.readFileSync(p, "utf-8");
156
- const get = (key) => {
157
- const mm = txt.match(new RegExp("\\*\\*" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\*\\*\\s*[::]\\s*`?([^`\\n]+)`?"));
158
- return mm ? mm[1].trim() : "";
159
- };
160
- const eteamsid = get("ETEAMSID");
161
- let cookie = get("完整 Cookie 串");
162
- if (!cookie && eteamsid) cookie = "langType=zh_CN; ETEAMSID=" + eteamsid + "; agentType=workbuddy; isAgent=true";
163
- return normalizeAuth({
164
- baseUrl: get("baseUrl"), eteamsid, cookie,
165
- userId: get("userId") || get("employeeId"), tenantKey: get("tenantKey"),
166
- source: "明文镜像 ~/.workbuddy/e10-login-cache.md(可能已过期)",
167
- });
168
- }
169
-
170
- const AUTH_PROVIDERS = [
171
- ["auth-json", providerAuthJson],
172
- ["whoami-json", providerWhoamiJson],
173
- ["whoami-text", providerWhoamiText],
174
- ["cache-md", providerCacheMd],
175
- ];
176
- /** 兜底出口不进记忆:否则 e10-login 偶发不可用一次,就会把"用镜像"永久设为首选、次次告警 */
177
- const AUTH_FALLBACK_PROVIDERS = new Set(["cache-md"]);
178
-
179
- function readAuthModeMemo() {
180
- try {
181
- const s = fs.readFileSync(AUTH_MODE_FILE, "utf-8").trim();
182
- return AUTH_PROVIDERS.some(([n]) => n === s) ? s : null;
183
- } catch { return null; }
184
- }
185
- function writeAuthModeMemo(name) {
186
- try { fs.mkdirSync(path.dirname(AUTH_MODE_FILE), { recursive: true }); fs.writeFileSync(AUTH_MODE_FILE, name, "utf-8"); } catch {}
187
- }
188
-
189
- /** 凭据取自 e10-login 时,顺手把明文镜像刷新到最新(保持镜像可用;镜像不参与正常取数) */
190
- function syncCacheMd(auth) {
191
- try {
192
- const p = path.join(os.homedir(), ".workbuddy", "e10-login-cache.md");
193
- if (!fs.existsSync(p) || !/🔐 登录信息缓存/.test(fs.readFileSync(p, "utf-8"))) return; // 非标准缓存文件不动它
194
- const d = new Date();
195
- const ts = d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0") + " " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
196
- let txt = fs.readFileSync(p, "utf-8");
197
- let changed = false;
198
- const rep = (key, val) => {
199
- const re = new RegExp("(\\*\\*" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\*\\*\\s*[::]\\s*`?)([^`\\n]*)(`?)");
200
- const n = txt.replace(re, (m, a, b, c) => { if (b === val && c === "`") return m; changed = true; return a + val + (c || "`"); });
201
- txt = n;
202
- };
203
- rep("baseUrl", auth.baseUrl); rep("ETEAMSID", auth.eteamsid); rep("完整 Cookie 串", auth.cookie); rep("userId", auth.userId || ""); rep("tenantKey", auth.tenantKey || "");
204
- rep("缓存记录时间", ts + "(e10-data 自动同步,凭据来源:" + (auth.source || "") + ")");
205
- if (changed) fs.writeFileSync(p, txt, "utf-8");
206
- } catch {}
207
- }
208
-
209
- /** 取凭据:按优先级依次尝试各出口;命中的模式会被记住(下次优先尝试它,省一次进程调用) */
210
- function loadAuth() {
211
- const { profile, file } = authFilePath();
212
- const override = (process.env.E10_DATA_AUTH_MODE || "").trim();
213
- const memo = override || (process.env.E10_DATA_AUTH_PROBE ? null : readAuthModeMemo());
214
- const order = memo
215
- ? AUTH_PROVIDERS.filter(([n]) => n === memo).concat(AUTH_PROVIDERS.filter(([n]) => n !== memo))
216
- : AUTH_PROVIDERS;
217
-
218
- const diag = [];
219
- for (const [name, fn] of order) {
220
- let got = null;
221
- lastLoginDiag = "";
222
- try { got = fn(); } catch (e) { diag.push(name + ": " + ((e && e.message) || e)); }
223
- if (got) {
224
- if (name !== memo && !AUTH_FALLBACK_PROVIDERS.has(name)) writeAuthModeMemo(name);
225
- got.profile = profile;
226
- if (name === "cache-md") {
227
- if (!process.env.E10_DATA_QUIET) {
228
- console.error("[e10-data][warn] 凭据来源=" + got.source + ";正常应由 e10-login 提供凭据,请检查 `e10-login whoami` 是否能输出 cookies。");
229
- }
230
- } else {
231
- syncCacheMd(got); // 凭据来自 e10-login → 顺便把镜像刷新到最新
232
- }
233
- return got;
234
- }
235
- if (lastLoginDiag) diag.push(name + ": " + lastLoginDiag);
236
- }
237
-
238
- throw new Error(
239
- "未取得登录凭据(解密由 e10-login 负责,本 CLI 不参与)。请依次检查:\n" +
240
- " ① e10-login 是否可用:`e10-login --version`(缺失 → `npm i -g e10-login`);`e10-login whoami` 应能输出 cookies\n" +
241
- " ② 是否已登录 / 登录态是否失效:`e10-login oidc` 重新登录(当前 profile: " + profile + ")\n" +
242
- " ③ auth 文件:" + (fs.existsSync(file) ? "存在" : "不存在") + "(" + file + ")—— 其加解密由 e10-login 负责\n" +
243
- (diag.length ? " 探测诊断:" + diag.join(" | ") + "\n" : "")
244
- );
245
- }
246
- const auth = loadAuth();
247
- const BASE = auth.baseUrl.replace(/\/+$/, "");
248
- const USER = { id: auth.userId || "", tenantKey: auth.tenantKey || "" };
249
-
250
- // ---------------- HTTP ----------------
251
- function checkAuthFail(resp, text, j) {
252
- if (resp.status === 401 || (j && String(j.code) === "-1" && /token|登录|失效/i.test(String(j.msg || "")))) {
253
- const from = (auth && auth.source) || "未知";
254
- const hint = /镜像/.test(from)
255
- ? "当前用的是明文镜像兜底凭据(可能已过期),请先确认 `e10-login whoami` 能正常导出 cookies,或执行 `e10-login oidc` 重新登录"
256
- : "凭据由 e10-login 提供,请执行 `e10-login oidc` 重新登录";
257
- throw new Error("登录态失效(" + (j.msg || resp.status) + "):凭据来源=" + from + ";" + hint);
258
- }
259
- }
260
- async function postJson(endpoint, bodyObj, extraHeaders = {}) {
261
- const resp = await fetch(BASE + endpoint, {
262
- method: "POST",
263
- headers: {
264
- "Content-Type": "application/json;charset=UTF-8",
265
- "accept": "application/json, text/plain, */*",
266
- "langType": "zh_CN",
267
- "cookie": auth.cookie,
268
- "eteamsid": auth.eteamsid,
269
- "ebbusinessid": EB_BIZ_ID,
270
- "user-agent": UA,
271
- "timezoneoffset": "-480",
272
- ...extraHeaders,
273
- },
274
- body: JSON.stringify(bodyObj),
275
- });
276
- const text = await resp.text();
277
- let j; try { j = parseJsonSafe(text); } catch { throw new Error("HTTP " + resp.status + " 非JSON响应: " + text.slice(0, 300)); }
278
- checkAuthFail(resp, text, j);
279
- return j;
280
- }
281
- async function getUrl(url) {
282
- const resp = await fetch(BASE + url, {
283
- method: "GET",
284
- headers: {
285
- "accept": "application/json, text/plain, */*",
286
- "langType": "zh_CN",
287
- "cookie": auth.cookie,
288
- "eteamsid": auth.eteamsid,
289
- "ebbusinessid": EB_BIZ_ID,
290
- "user-agent": UA,
291
- },
292
- });
293
- const text = await resp.text();
294
- let j; try { j = parseJsonSafe(text); } catch { throw new Error("HTTP " + resp.status + " 非JSON响应: " + text.slice(0, 300)); }
295
- checkAuthFail(resp, text, j);
296
- return j;
297
- }
298
- async function postMultipart(endpoint, formObj) {
299
- const fd = new FormData();
300
- for (const [k, v] of Object.entries(formObj)) fd.append(k, typeof v === "string" ? v : JSON.stringify(v));
301
- const resp = await fetch(BASE + endpoint, {
302
- method: "POST",
303
- headers: {
304
- "accept": "application/json, text/plain, */*",
305
- "langType": "zh_CN",
306
- "cookie": auth.cookie,
307
- "eteamsid": auth.eteamsid,
308
- "ebbusinessid": EB_BIZ_ID,
309
- "user-agent": UA,
310
- "timezoneoffset": "-480",
311
- "referer": BASE + "/sp/ebddesigner/1282331905881874436",
312
- },
313
- body: fd,
314
- });
315
- const text = await resp.text();
316
- let j; try { j = parseJsonSafe(text); } catch { throw new Error("HTTP " + resp.status + " 非JSON响应: " + text.slice(0, 300)); }
317
- checkAuthFail(resp, text, j);
318
- return j;
319
- }
320
-
321
- // ---------------- 参数解析 ----------------
322
- function parseArgs(argv) {
323
- const args = {};
324
- const pos = [];
325
- for (let i = 0; i < argv.length; i++) {
326
- const a = argv[i];
327
- if (a.startsWith("--")) {
328
- const key = a.slice(2);
329
- const v = argv[i + 1];
330
- if (v === undefined || v.startsWith("--")) { args[key] = true; }
331
- else { args[key] = v; i++; }
332
- } else pos.push(a);
333
- }
334
- return { args, pos };
335
- }
336
- // ⚠️ 大整数精度保护(2026-09-03):E10 主键/ID 多为 19 位大整数,超过 JS 安全整数 2^53-1
337
- // (9007199254740991)。JSON.parse 会将其舍入为近似 double(如 1297901170990161977 → ...1920),
338
- // 导致 DSL 条件查不到数据(静默空结果)或返回 ID 失真。此处把 JSON 中 >2^53 的裸数字字面量
339
- // 加引号转字符串(后端实测接受字符串形式做 = / IN / BETWEEN 数值比较),请求与响应两侧统一保护。
340
- function protectBigInts(raw) {
341
- // 逐字符扫描:跟踪 JSON 字符串状态(含转义),仅在引号外的结构层把 >2^53 的裸整数加引号,
342
- // 避免误伤字符串内容(如 avatar JSON、文档正文里含 ",大数字" 片段)。
343
- let out = "", inStr = false, esc = false;
344
- for (let i = 0; i < raw.length; i++) {
345
- const ch = raw[i];
346
- if (inStr) {
347
- out += ch;
348
- if (esc) esc = false;
349
- else if (ch === "\\") esc = true;
350
- else if (ch === '"') inStr = false;
351
- continue;
352
- }
353
- if (ch === '"') { inStr = true; out += ch; continue; }
354
- if (ch === "-" || (ch >= "0" && ch <= "9")) {
355
- let j = i + 1;
356
- while (j < raw.length && raw[j] >= "0" && raw[j] <= "9") j++; // 纯整数 token(无小数点/指数)
357
- let k = i - 1;
358
- while (k >= 0 && /\s/.test(raw[k])) k--;
359
- const prev = k < 0 ? "" : raw[k];
360
- let n = j;
361
- while (n < raw.length && /\s/.test(raw[n])) n++;
362
- const next = n < raw.length ? raw[n] : "";
363
- const token = raw.slice(i, j);
364
- if ((prev === ":" || prev === "," || prev === "[") && (next === "," || next === "}" || next === "]")) {
365
- try {
366
- const bi = BigInt(token);
367
- if (bi > 9007199254740991n || bi < -9007199254740991n) { out += '"' + token + '"'; i = j - 1; continue; }
368
- } catch { /* 非纯整数不转换 */ }
369
- }
370
- out += raw.slice(i, j);
371
- i = j - 1;
372
- continue;
373
- }
374
- out += ch;
375
- }
376
- return out;
377
- }
378
- function parseJsonSafe(raw, label) {
379
- try { return JSON.parse(protectBigInts(raw)); }
380
- catch (e) { throw new Error((label || "JSON") + " 解析失败: " + e.message); }
381
- }
382
- function readJsonArg(val, label) {
383
- let raw = val;
384
- if (val.startsWith("@")) raw = fs.readFileSync(val.slice(1), "utf-8");
385
- return parseJsonSafe(raw, label);
386
- }
387
- function need(arg, args) { if (!args[arg]) throw new Error("缺少必填参数 --" + arg); return args[arg]; }
388
- // 文件/JSON 参数取值:兼容 --flag <v> 与位置参数 <v>(如 conf add-file @items.json / --add-file @items.json)
389
- function flagOrPos(args, flag, keys) {
390
- if (args[flag] !== undefined) return args[flag];
391
- return keys && keys.length ? keys[0] : undefined;
392
- }
393
-
394
- // ---------------- 展示 ----------------
395
- function fmt(v) {
396
- if (v === null || v === undefined) return "";
397
- if (typeof v === "object") { const s = JSON.stringify(v); return s.length > 80 ? s.slice(0, 80) + "…" : s; }
398
- return String(v);
399
- }
400
- function printHuman(title, rows, cols) {
401
- if (title) console.log("== " + title + " ==");
402
- if (!rows || !rows.length) { console.log("(空)"); return; }
403
- if (cols) {
404
- console.log(cols.join("\t"));
405
- for (const r of rows) console.log(cols.map((c) => fmt(r[c])).join("\t"));
406
- } else {
407
- for (const r of rows.slice(0, 100)) console.log(JSON.stringify(r));
408
- if (rows.length > 100) console.log("... 共 " + rows.length + " 行(--json 取全量)");
409
- }
410
- }
411
-
412
- // ---------------- query:DSL 查询 ----------------
413
- async function cmdQuery(args, json) {
414
- const conn = need("conn", args), model = need("model", args), schema = need("schema", args);
415
- const tableId = args["table-id"] || "";
416
- const dslVal = need("dsl", args);
417
- const dslQuery = dslVal.startsWith("@") ? readJsonArg(dslVal, "--dsl") : parseJsonSafe(dslVal, "--dsl");
418
- const body = { connId: conn, model, schema, tableId, dslQuery };
419
- if (args.debug !== undefined) { // --debug true|false,与 conn/model 同层级
420
- const dbg = String(args.debug).toLowerCase();
421
- if (dbg === "true" || dbg === "1") body.debug = true;
422
- else if (dbg === "false" || dbg === "0") body.debug = false;
423
- else throw new Error("--debug 值必须为 true 或 false(当前: " + args.debug + ")");
424
- }
425
- const r = await postJson("/api/dw/dsl/executeQueryDsl", body);
426
- if (json) return r;
427
- if (String(r.code) !== "200" && r.code !== undefined && String(r.code) !== "0") {
428
- console.log("code=" + r.code + " msg=" + (r.msg || ""));
429
- if (/没有权限|无权限|权限不足|未授权|无权访问|无查询权限/i.test(String(r.msg || ""))) {
430
- throw new Error("当前用户对该数据无查询权限,请检查 ETEAMSID 是否有效或联系管理员开通");
431
- }
432
- return;
433
- }
434
- const d = r.data;
435
- // debug=true 时 data 为 { sql, records };否则为行数组
436
- const rows = Array.isArray(d) ? d : (d && Array.isArray(d.records) ? d.records : []);
437
- if (d && !Array.isArray(d) && d.sql) console.log("SQL: " + d.sql);
438
- if (!rows.length) { console.log("(空:0 行)"); return; }
439
- printHuman("结果 " + rows.length + " 行", rows);
440
- }
441
-
442
- // ---------------- ds:数据源 ----------------
443
- function dyn() {
444
- return { eteamsId: auth.eteamsid, userId: auth.userId || "", tenantKey: auth.tenantKey || "", langId: 7, formatDate: "yyyy-MM-dd HH:mm:ss", isPublic: false };
445
- }
446
- function baseDto(tableId, groupId, pageNo, pageSize, queryType) {
447
- return { tableId: String(tableId), groupId, pageNo, pageSize, queryType, containDetailTable: false, needFormat: true, dynamicParams: dyn() };
448
- }
449
- // 条件树(conditionTrees)协议:根/组节点 { key, relationship("1"=AND|"0"=OR), isParent:true, parentNode, conditionList:[...] }
450
- // 叶子节点 { key, relationship:"1", isParent:false, parentNode, conditionId, conditionType, compareType, conditionValue, ... }
451
- // v0.1.8 起 parseWhere 支持递归树(OR);向后兼容:数组=AND 根、叶子键 fieldId 优先(conditionId 兼容兜底)
452
- function normRel(r) { return (r === "0" || r === 0 || r === false) ? "0" : "1"; }
453
- function isCondGroup(c) { return !!c && Array.isArray(c.conditionList) && c.conditionList.length > 0; }
454
- function buildCondNode(c, key, parentKey) {
455
- if (isCondGroup(c)) {
456
- return {
457
- key, relationship: normRel(c.relationship), isParent: true, parentNode: parentKey,
458
- mainField: c.mainField !== false,
459
- conditionList: c.conditionList.map((k, i) => buildCondNode(k, key + "_" + i, key)),
460
- };
461
- }
462
- const fid = (c && (c.fieldId || c.conditionId)) || "";
463
- if (!fid) throw new Error("--where 条件缺少字段:叶子须提供 fieldId(或 conditionId),条件组须提供非空 conditionList");
464
- return {
465
- key, relationship: "1", isParent: false, parentNode: parentKey,
466
- objId: c.objId || "", conditionId: fid, fieldName: c.fieldName || fid,
467
- conditionType: c.conditionType || "", compareType: c.compareType, conditionValue: c.conditionValue ?? "",
468
- fieldValue: c.fieldValue || "", mainField: c.mainField !== false, ignoreCaseMark: c.ignoreCaseMark || false,
469
- };
470
- }
471
- function parseWhere(val) {
472
- if (!val) return null;
473
- const raw = readJsonArg(val, "--where");
474
- if (!raw) return null;
475
- const ts = Date.now(), rootKey = "root_" + ts;
476
- if (Array.isArray(raw)) { // 数组 → AND 根(元素可为叶子或内嵌条件组,组自带 relationship)
477
- const kids = [];
478
- raw.forEach((c, i) => { if (c && typeof c === "object") kids.push(buildCondNode(c, rootKey + "_" + i, rootKey)); });
479
- if (!kids.length) return null;
480
- return { key: rootKey, relationship: "1", isParent: true, parentNode: "", mainField: true, conditionList: kids };
481
- }
482
- if (typeof raw !== "object") throw new Error("--where 须为条件数组或条件树对象");
483
- // 对象形态 = 完整条件树({relationship, conditionList:[...]},缺省 AND);无 conditionList 视为无条件
484
- if (!Array.isArray(raw.conditionList) || !raw.conditionList.length) return null;
485
- return buildCondNode(raw, rootKey, "");
486
- }
487
- function parseStat(val) {
488
- const arr = readJsonArg(val, "--stat");
489
- const list = Array.isArray(arr) ? arr : [arr];
490
- return list.map((s) => ({ fieldId: s.fieldId, fieldName: s.fieldId, objId: "", mainField: true, aggregator: s.aggregator || "SUM", ...(s.fieldAlias ? { fieldAlias: s.fieldAlias } : {}) }));
491
- }
492
- function parseOrder(val) {
493
- if (!val) return [];
494
- return val.split(",").map((s) => {
495
- const [f, dir] = s.trim().split(/\s+/);
496
- return { fieldId: f, fieldName: f, objId: "", isAsc: (dir || "asc").toLowerCase() !== "desc", mainField: true };
497
- });
498
- }
499
- // ---------------- ds appId 自动解析(v0.1.12)----------------
500
- // 背景:ChatBI 数据源取数接口(getListDatas / getChartDatas / getStatDatas / getCountData)
501
- // 对部分 EB 应用(独立部署)的数据集必须携带 queryDto.appId,否则后端一律返回 code=500。
502
- // 解析顺序(无缓存,每次实时查):--app-id <id> 显式 > chat_bi_eb_config_map(dw 权限配置表,按 obj_id,主来源)
503
- // > ebdf_obj(ec_edcapp,按 id,兜底) > 不带 appId 发送(兼容旧行为)。
504
- // 关闭自动解析:--no-app-id。
505
- const APPID_SOURCES = [
506
- { confKey: "CONN_DW", model: "dw", schema: "dw", table: "chat_bi_eb_config_map", idField: "obj_id" },
507
- { confKey: "CONN_EC_EDCAPP", model: "ebuilder", schema: "ec_edcapp", table: "ebdf_obj", idField: "id" },
508
- ];
509
- function dsFieldsForm(gid, tid, st) {
510
- return { groupId: gid, sourceId: tid, sourceType: st, detailFieldsGroup: "true", enableMerge: "true", needTableName: "true", params: "[]" };
511
- }
512
- async function appIdByDsl(src, tid) {
513
- const conn = await confGetData("conn", src.confKey, true);
514
- const connId = (conn && conn.confValue) ? String(conn.confValue) : "";
515
- if (!connId) return "";
516
- const dslQuery = {
517
- select: { columns: [{ field: "t1.app_id", alias: "app_id" }] },
518
- from: { table: src.table, alias: "t1" },
519
- where: { logic: "AND", conditions: [{ field: "t1." + src.idField, op: "=", value: String(tid) }, { field: "t1.delete_type", op: "=", value: 0 }] },
520
- limit: { size: 1, offset: 0 },
521
- };
522
- const r = await postJson("/api/dw/dsl/executeQueryDsl", { connId, model: src.model, schema: src.schema, tableId: "", dslQuery });
523
- const rows = Array.isArray(r && r.data) ? r.data : [];
524
- return (rows[0] && rows[0].app_id) ? String(rows[0].app_id) : "";
525
- }
526
- async function resolveDsAppId(tid) {
527
- for (const src of APPID_SOURCES) {
528
- let appId = "";
529
- try { appId = await appIdByDsl(src, tid); } catch { appId = ""; }
530
- if (appId) return appId;
531
- }
532
- return "";
533
- }
534
- // ds 取数统一入口:--app-id <id> 显式 / --app-id auto 或省略=自动解析 / --no-app-id 跳过
535
- async function dsDto(args, tid, gid, pageNo, pageSize, queryType) {
536
- const dto = baseDto(tid, gid, pageNo, pageSize, queryType);
537
- const explicit = args["app-id"];
538
- if (typeof explicit === "string" && explicit !== "" && explicit !== "auto") {
539
- dto.appId = explicit;
540
- } else if (!args["no-app-id"]) {
541
- const appId = await resolveDsAppId(tid);
542
- if (appId) dto.appId = String(appId);
543
- }
544
- return dto;
545
- }
546
- async function cmdTables(args, json) {
547
- const gid = need("group", args);
548
- const form = { name: args.name || "", pageNo: args.page || "1", pageSize: args.size || "100", groupId: gid, sourceType: args["source-type"] || "LOGIC", dataUsage: "" };
549
- if (typeof args["app-id"] === "string" && args["app-id"] !== "" && args["app-id"] !== "auto") form.appId = args["app-id"];
550
- const r = await postMultipart("/api/ebuilder/common/ds/listByPage", form);
551
- if (json) return r;
552
- const recs = ((r.data || {}).records) || [];
553
- printHuman("分组表清单 (total=" + ((r.data || {}).total) + ")", recs, ["id", "name", "appId", "tableText", "detailTable", "usage"]);
554
- }
555
- async function cmdFields(args, json) {
556
- const gid = need("group", args), tid = need("table", args);
557
- const form = dsFieldsForm(gid, tid, args["source-type"] || "LOGIC");
558
- if (typeof args["app-id"] === "string" && args["app-id"] !== "" && args["app-id"] !== "auto") form.appId = args["app-id"];
559
- const r = await postMultipart("/api/ebuilder/common/ds/fields", form);
560
- if (json) return r;
561
- for (const d of (r.data || [])) {
562
- console.log("== 表: " + d.name + " (id=" + d.id + ") 字段 " + (d.fields || []).length + " 个 ==");
563
- for (const f of (d.fields || [])) console.log([f.name, f.text, f.type, f.mainPrimaryKey ? "PK" : ""].filter(Boolean).join("\t"));
564
- }
565
- }
566
- async function cmdList(args, json) {
567
- const gid = need("group", args), tid = need("table", args), fld = need("fields", args);
568
- const fields = fld.split(",").map((f) => ({ fieldId: f, fieldName: f, objId: String(tid), mainField: true }));
569
- const dto = await dsDto(args, tid, gid, parseInt(args.page || "1", 10), parseInt(args.size || "20", 10), "getListData");
570
- dto.queryFields = fields;
571
- dto.orderFields = parseOrder(args.order);
572
- const ct = parseWhere(args.where); if (ct) dto.conditionTrees = ct;
573
- dto.queryCount = true;
574
- const r = await postMultipart("/api/dw/newetl/chatBi/ds/getListDatas", { sourceType: args["source-type"] || "LOGIC", queryDto: dto });
575
- if (json) return r;
576
- const data = r.data || {};
577
- console.log("== 数据列表 count=" + data.count + " 行=" + (data.list || []).length + " ==");
578
- printHuman("", data.list || [], fields.map((f) => f.fieldId));
579
- }
580
- async function cmdChart(args, json) {
581
- const gid = need("group", args), tid = need("table", args), gb = need("group-by", args);
582
- const dto = await dsDto(args, tid, gid, parseInt(args.page || "1", 10), parseInt(args.size || "100", 10), "getChartData");
583
- dto.groupFields = gb.split(",").map((f) => ({ fieldId: f, fieldName: f, objId: String(tid), mainField: true }));
584
- dto.groupStatFields = parseStat(need("stat", args)).map((s) => ({ ...s, objId: String(tid) }));
585
- const ct = parseWhere(args.where); if (ct) dto.conditionTrees = ct;
586
- const r = await postMultipart("/api/dw/newetl/chatBi/ds/getChartDatas", { sourceType: args["source-type"] || "LOGIC", queryDto: dto });
587
- if (json) return r;
588
- printHuman("分组聚合 rows=" + (r.data || []).length, r.data || []);
589
- }
590
- async function cmdStat(args, json) {
591
- const gid = need("group", args), tid = need("table", args);
592
- const dto = await dsDto(args, tid, gid, 1, 10, "getStatData");
593
- dto.statFields = parseStat(need("stat", args)).map((s) => ({ ...s, objId: String(tid) }));
594
- const ct = parseWhere(args.where); if (ct) dto.conditionTrees = ct;
595
- const r = await postMultipart("/api/dw/newetl/chatBi/ds/getStatDatas", { sourceType: args["source-type"] || "LOGIC", queryDto: dto });
596
- if (json) return r;
597
- const data = r.data || {};
598
- if (data.fields && data.fields.length) { for (const f of data.fields) console.log(JSON.stringify(f)); }
599
- else console.log("(无统计结果)");
600
- }
601
- async function cmdCount(args, json) {
602
- const gid = need("group", args), tid = need("table", args);
603
- const dto = await dsDto(args, tid, gid, 1, 10, "getCountData");
604
- const ct = parseWhere(args.where); if (ct) dto.conditionTrees = ct;
605
- const r = await postMultipart("/api/dw/newetl/chatBi/ds/getCountData", { sourceType: args["source-type"] || "LOGIC", queryDto: dto });
606
- if (json) return r;
607
- const data = r.data || {};
608
- console.log("totalCount = " + (data.totalCount ?? "-"));
609
- }
610
-
611
- // ---------------- doc:文档查询 ----------------
612
- function docBaseBody(pageNo, pageSize) {
613
- return {
614
- curMenuType: "", limitedFolderIncludeSub: "", limitedFolderIds: "", docAppConsole: "",
615
- viewType: "DOC_PAGINATION", pageNo, pageSize, morePageId: "", dimension: "ALL", includeBelongFolders: true,
616
- order: { property: "default", direction: "DESC" }, searchType: "all", conditions: [], customConditions: [],
617
- userId: auth.userId || "", current: pageNo,
618
- columnOrders: [{ orderKey: "default", orderType: "" }], getColumn: false, quickSearchType: "normal",
619
- withFullSearch: false, fetchFullSearchParam: true,
620
- };
621
- }
622
- function buildDocConditions(args) {
623
- const conds = [];
624
- if (args.name) conds.push({ id: "name", type: "string", value: args.name });
625
- if (args.creator) conds.push({ id: "creator", operate: args["creator-neq"] ? "neq" : "eq", type: "array", value: args.creator });
626
- if (args.author) conds.push({ id: "author", operate: args["author-neq"] ? "neq" : "eq", type: "array", value: args.author });
627
- if (args["content-type"]) conds.push({ id: "contentType", operate: "eq", type: "array", value: args["content-type"] });
628
- if (args.time) conds.push({ id: "create_time", type: "date", value: args.time });
629
- if (args["last-time"]) conds.push({ id: "last_update_time", type: "date", value: args["last-time"] });
630
- if (args.folder) conds.push({ id: "folder", operate: "eq", type: "folder", value: args.folder });
631
- if (args["folder-neq"]) conds.push({ id: "folder", operate: "neq", type: "folder", value: args["folder-neq"] });
632
- return conds;
633
- }
634
- async function cmdDocCount(args, json) {
635
- const body = docBaseBody(1, parseInt(args.size || "20", 10));
636
- body.conditions = buildDocConditions(args);
637
- const r = await postJson("/api/doc/documents/documentListCount", body);
638
- if (json) return r;
639
- const data = r.data || {};
640
- console.log("total = " + (data.total ?? "-"));
641
- }
642
- async function cmdDocList(args, json) {
643
- const pageSize = parseInt(args.size || "100", 10);
644
- const body = docBaseBody(1, pageSize);
645
- body.conditions = buildDocConditions(args);
646
- const r1 = await postJson("/api/doc/documents/documentListCount", body);
647
- if (json) return r1;
648
- const total = (r1.data || {}).total || 0;
649
- if (!total) { console.log("total = 0(未查询到符合条件的文档)"); return; }
650
- const totalPages = Math.ceil(total / pageSize);
651
- const maxPages = args.all ? totalPages : Math.min(parseInt(args.page || "1", 10), totalPages);
652
- const startPage = args.all ? 1 : parseInt(args.page || "1", 10);
653
- const allRows = [];
654
- for (let pno = startPage; pno <= maxPages; pno++) {
655
- const b = docBaseBody(pno, pageSize);
656
- b.conditions = buildDocConditions(args);
657
- const rr = await postJson("/api/doc/documents/documentList", b);
658
- const rows = ((rr.data || {}).data) || [];
659
- for (const row of rows) if (!allRows.some((x) => x.id === row.id)) allRows.push(row);
660
- if (!args.all && pno === maxPages) break;
661
- }
662
- console.log("== 文档列表 total=" + total + " 本批=" + allRows.length + " 行 ==");
663
- printHuman("", allRows, ["id", "name", "docCreatorName", "createTime", "folder"]);
664
- }
665
-
666
- // ---------------- noun:词库 ----------------
667
- async function cmdNoun(args, json) {
668
- const skill = need("skill", args);
669
- const q = new URLSearchParams({ skillKey: skill });
670
- if (args["source-type"]) q.append("sourceType", args["source-type"]);
671
- if (args["source-group"]) q.append("sourceGroupId", args["source-group"]);
672
- if (args["source-table"]) q.append("sourceTableId", args["source-table"]);
673
- const r = await getUrl("/api/dw/newetl/chatBi/skill/getChatDocsBySkillKey?" + q.toString());
674
- if (json) return r;
675
- const data = r.data;
676
- if (!data || !data.nouns) {
677
- console.log("(该 skill 词库不存在或为空) msg=" + (r.msg || ""));
678
- return;
679
- }
680
- console.log("== 名词解释 " + data.nouns.length + " 条 (skill=" + skill + ") ==");
681
- for (const n of data.nouns) {
682
- console.log("【" + (n.keyword || "") + "】" + (n.synonyms ? " 同义:" + n.synonyms : ""));
683
- if (n.wordExplain) console.log(" " + String(n.wordExplain).replace(/\n/g, " "));
684
- }
685
- }
686
-
687
- // ---------------- field:关联字段翻译 ----------------
688
- function customParamFromArgs(args) {
689
- if (args["form-param"]) return { formParam: args["form-param"] };
690
- if (args["eb-browser-param"]) return { ebBrowserForEditParam: readJsonArg(args["eb-browser-param"], "--eb-browser-param") };
691
- if (args["property-type"]) return { propertyType: args["property-type"] };
692
- return {};
693
- }
694
- function extractRemoteRows(r, browserType) {
695
- const remote = r.remote || {};
696
- const keys = Object.keys(remote);
697
- const out = [];
698
- for (const k of keys) {
699
- if (Array.isArray(remote[k])) for (const it of remote[k]) out.push({ key: k, id: it.id, content: it.content ?? it.name ?? "" });
700
- }
701
- return out;
702
- }
703
- async function cmdFieldId2name(args, json) {
704
- const module = need("module", args), type = need("type", args), idsVal = need("ids", args);
705
- const dataIds = idsVal.split(",").map((s) => s.trim()).filter(Boolean);
706
- const body = [{ module, browserType: type, dataIds, user: USER, customParam: customParamFromArgs(args) }];
707
- const r = await postJson("/api/bcw/test/testRemoteBrowser", body);
708
- if (json) return r;
709
- const rows = extractRemoteRows(r, type);
710
- if (!rows.length) {
711
- const nf = (r.notFound || {});
712
- const list = nf && nf[module + "_" + type] && nf[module + "_" + type].ids ? nf[module + "_" + type].ids : [];
713
- console.log("(未翻译到名称) notFound ids: " + (list.join(",") || "全部"));
714
- return;
715
- }
716
- printHuman("id→name (" + rows.length + "/" + dataIds.length + ")", rows, ["id", "content"]);
717
- }
718
- async function cmdFieldName2id(args, json) {
719
- const module = need("module", args), type = need("type", args), name = need("name", args);
720
- const body = { newly: 1, searchValue: name, ...customParamFromArgs(args) };
721
- const url = "/api/" + module + "/common/browser/complete/" + type;
722
- const r = await postJson(url, body);
723
- if (json) return r;
724
- const rows = ((r.data || {}).data) || [];
725
- if (!rows.length) { console.log("(未匹配到名称对应 ID)"); return; }
726
- printHuman("name→id 候选 " + rows.length + " 条", rows.map((x) => ({ id: x.id, name: x.name ?? x.title ?? "" })), ["id", "name"]);
727
- }
728
-
729
- // ---------------- conf:环境参数配置中心(chatBi config map) ----------------
730
- // 覆盖 config-resolve 原 resolve_config.py 的 URL 直连调用:get/batch/batch-file/group/group-file/resolve/resolve-file
731
- // 与管理 add/add-file/update/delete。认证复用进程内登录缓存(完整 Cookie + eteamsid,同 query/ds/doc),
732
- // 无内置令牌;baseUrl 取登录缓存(与 executeQueryDsl 同域)。confType 默认由 key 前缀推导
733
- // (CONN_→conn / DS_→ds / WF_→wf / API_→api),可用 --type 显式指定任意大类(如 data-source-query / system-config)。
734
- //
735
- // v0.1.9 confGroup(配置分组,可选):
736
- // 后端在 confType=api 时支持按分组二次分层(如 api/workflow、api/hr),定位维度变为
737
- // 「confType + confGroup + confKey」。本 CLI 统一用 --group 传分组:
738
- // · confType 为 api(或显式传了 --group)时,新增/更新/删除/单查 才下发 confGroup 字段,
739
- // 其他 confType 一律不带该字段,避免污染既有(无分组列)的配置类型。
740
- // · conf group <confType> <confGroup> 走 batchGetChatBiConfigMapByTypeAndGroup 按类型+分组批量取回。
741
- const CONF_PREFIX_TYPE = [["CONN_", "conn"], ["DS_", "ds"], ["WF_", "wf"], ["API_", "api"]];
742
- const CONF_GET_PATH = "/api/dw/newetl/chatBi/getChatBiConfigMap";
743
- const CONF_BATCH_PATH = "/api/dw/newetl/chatBi/batchGetChatBiConfigMap";
744
- const CONF_BATCH_GROUP_PATH = "/api/dw/newetl/chatBi/batchGetChatBiConfigMapByTypeAndGroup";
745
- const CONF_ADD_PATH = "/api/dw/newetl/chatBi/batchAddChatBiConfigMap";
746
- const CONF_UPDATE_PATH = "/api/dw/newetl/chatBi/updateChatBiConfigMap";
747
- const CONF_DELETE_PATH = "/api/dw/newetl/chatBi/deleteChatBiConfigMap";
748
- const CONF_TOKEN_RE = /#\{([A-Z][A-Z0-9_]{2,})\}/g;
749
- function confKeyType(key, explicitType) {
750
- if (explicitType) return explicitType;
751
- for (const [p, t] of CONF_PREFIX_TYPE) if (key.startsWith(p)) return t;
752
- throw new Error("无法由 confKey 推导 confType(需 CONN_/DS_/WF_/API_ 前缀或显式 --type): " + key);
753
- }
754
- // confGroup 仅在 confType=api 或调用方显式指定时下发(见上方说明)
755
- function confUseGroup(ctype, group) {
756
- return ctype === "api" || (group !== undefined && group !== null && group !== "");
757
- }
758
- function confPutGroup(body, ctype, group) {
759
- if (confUseGroup(ctype, group)) body.confGroup = group === undefined || group === null ? "" : String(group);
760
- return body;
761
- }
762
- function confGroupSuffix(ctype, group) { // GET/DELETE 用的 query 后缀
763
- return confUseGroup(ctype, group) ? "&confGroup=" + encodeURIComponent(group === undefined || group === null ? "" : String(group)) : "";
764
- }
765
- function confItemId(it) { // 同 confKey 可分布在多个 confGroup,带分组时用「组|键」做映射键,避免互相覆盖
766
- const g = it && it.confGroup;
767
- return g ? g + "|" + (it.confKey || "") : ((it && it.confKey) || "");
768
- }
769
- function confLookup(map, it) { // 兼容后端回包是否带 confGroup:先按「组|键」取,再退化到 confKey
770
- return map[confItemId(it)] || map[it.confKey];
771
- }
772
- function confFail(j, url) {
773
- const m = (j && (j.msg || (j.actionMsg && j.actionMsg.message))) || "";
774
- throw new Error("配置服务请求失败 [" + url + "]" + (m ? ": " + m : ""));
775
- }
776
- async function confRequest(method, url, bodyObj) {
777
- const resp = await fetch(BASE + url, {
778
- method,
779
- headers: {
780
- "accept": "application/json, text/plain, */*",
781
- "langType": "zh_CN",
782
- "cookie": auth.cookie,
783
- "eteamsid": auth.eteamsid,
784
- "ebbusinessid": EB_BIZ_ID,
785
- "user-agent": UA,
786
- ...(bodyObj !== undefined ? { "Content-Type": "application/json;charset=UTF-8" } : {}),
787
- },
788
- ...(bodyObj !== undefined ? { body: JSON.stringify(bodyObj) } : {}),
789
- });
790
- const text = await resp.text();
791
- let j; try { j = parseJsonSafe(text); } catch { throw new Error("HTTP " + resp.status + " 非JSON响应: " + text.slice(0, 300)); }
792
- checkAuthFail(resp, text, j);
793
- return j;
794
- }
795
- async function confGetData(ctype, key, silent, group) {
796
- const url = CONF_GET_PATH + "?confType=" + encodeURIComponent(ctype) + "&confKey=" + encodeURIComponent(key) + confGroupSuffix(ctype, group);
797
- let r;
798
- try { r = await confRequest("GET", url); }
799
- catch (e) { if (silent) return null; throw e; }
800
- const data = (r && r.data) || null;
801
- if (!silent && (!data || data.confValue === undefined || data.confValue === null || data.confValue === "")) {
802
- throw new Error("配置不存在或为空: confType=" + ctype + (group ? " confGroup=" + group : "") + " confKey=" + key);
803
- }
804
- return data;
805
- }
806
- function confNormalizeBatch(payload) {
807
- const data = (payload && payload.data) || null;
808
- const out = {};
809
- const put = (row, fallbackKey) => {
810
- if (!row || typeof row !== "object") return;
811
- const k = row.confKey || row.conf_key || fallbackKey;
812
- if (!k) return;
813
- if (row.confGroup === undefined) { const g = row.conf_group; if (g !== undefined && g !== null && g !== "") row.confGroup = g; }
814
- out[confItemId({ confGroup: row.confGroup, confKey: k })] = row;
815
- };
816
- if (Array.isArray(data)) for (const row of data) put(row);
817
- else if (data && typeof data === "object") for (const [k, v] of Object.entries(data)) put(v, k);
818
- return out;
819
- }
820
- // 按类型+分组批量取回(batchGetChatBiConfigMapByTypeAndGroup),payload = [{confType, confGroup}...]
821
- async function confBatchByTypeAndGroup(pairs) {
822
- const payload = pairs.map((p) => ({ confType: p.confType, confGroup: p.confGroup === undefined || p.confGroup === null ? "" : String(p.confGroup) }));
823
- const r = await confRequest("POST", CONF_BATCH_GROUP_PATH, payload);
824
- return confNormalizeBatch(r);
825
- }
826
- async function cmdConfGet(args, json, keys) {
827
- const key = keys[0];
828
- if (!key) throw new Error("conf get 缺少 confKey(用法: conf get <key> [--type t] [--group g])");
829
- const ctype = confKeyType(key, args.type);
830
- let data;
831
- try {
832
- data = await confGetData(ctype, key, false, args.group);
833
- } catch (e) {
834
- if (ctype === "api" && !args.group) {
835
- throw new Error(e.message + "(confType=api 建议用 --group 指定分组,如 --group workflow;或用 conf group api <分组> 查看该分组下全部配置)");
836
- }
837
- throw e;
838
- }
839
- if (json) return data;
840
- console.log(JSON.stringify(data, null, 1));
841
- }
842
- async function cmdConfBatch(args, json, keys) {
843
- if (!keys.length) throw new Error("conf batch 缺少 confKey 列表(用法: conf batch <key1> [key2 ...] [--type t] [--group g])");
844
- const items = keys.map((k) => { const t = confKeyType(k, args.type); return confPutGroup({ confType: t, confKey: k }, t, args.group); });
845
- let map = {};
846
- try {
847
- const r = await confRequest("POST", CONF_BATCH_PATH, items);
848
- map = confNormalizeBatch(r);
849
- for (const it of items) {
850
- const c = confLookup(map, it);
851
- if (!c || c.confValue === undefined || c.confValue === null || c.confValue === "") {
852
- throw new Error("批量响应缺失 confKey=" + it.confKey);
853
- }
854
- }
855
- } catch (e) {
856
- if (String(e.message).indexOf("批量响应缺失") === 0) throw e;
857
- console.error("[config-resolve] 批量接口不可用,降级逐条单查: " + e.message);
858
- map = {};
859
- for (const it of items) map[confItemId(it)] = await confGetData(it.confType, it.confKey, false, it.confGroup);
860
- }
861
- if (json) return map;
862
- printHuman("配置 " + items.length + " 条", keys.map((k) => {
863
- const it = items.find((x) => x.confKey === k);
864
- const c = confLookup(map, it) || {};
865
- return { key: k, confType: it.confType, confGroup: it.confGroup ?? "", confValue: c.confValue ?? "", confName: c.confName ?? "" };
866
- }), ["key", "confType", "confGroup", "confValue", "confName"]);
867
- }
868
- async function cmdConfBatchFile(args, json, keys) {
869
- const val = flagOrPos(args, "batch-file", keys);
870
- if (val === undefined) throw new Error("conf batch-file 缺少参数(用法: conf batch-file @items.json)");
871
- const items = readJsonArg(String(val), "--batch-file");
872
- const list = Array.isArray(items) ? items : [items];
873
- const map = {};
874
- const meta = {};
875
- for (const it of list) {
876
- if (!it.confKey || !it.confType) continue;
877
- const id = confItemId(it);
878
- map[id] = await confGetData(it.confType, it.confKey, false, it.confGroup);
879
- meta[id] = it;
880
- }
881
- if (json) return map;
882
- printHuman("配置 " + Object.keys(map).length + " 条", Object.keys(map).map((id) => ({ key: meta[id].confKey, confGroup: meta[id].confGroup ?? "", confValue: (map[id] || {}).confValue ?? "", confName: (map[id] || {}).confName ?? "" })), ["key", "confGroup", "confValue", "confName"]);
883
- }
884
- // 按类型+分组批量查(batchGetChatBiConfigMapByTypeAndGroup):pairs = [{confType, confGroup}...]
885
- async function cmdConfGroup(args, json, keys) {
886
- const ctype = keys[0] || args.type;
887
- const group = keys[1] !== undefined ? keys[1] : args.group;
888
- if (!ctype) throw new Error("conf group 缺少 confType(用法: conf group <confType> <confGroup> | --type api --group workflow)");
889
- if (group === undefined) throw new Error("conf group 缺少 confGroup(用法: conf group <confType> <confGroup>)");
890
- const map = await confBatchByTypeAndGroup([{ confType: ctype, confGroup: group }]);
891
- const rows = Object.values(map);
892
- const onlyKey = args.key;
893
- const filtered = onlyKey ? rows.filter((r) => (r.confKey || r.conf_key) === onlyKey) : rows;
894
- if (json) return onlyKey ? (filtered[0] ?? null) : filtered;
895
- if (onlyKey && !filtered.length) throw new Error("配置不存在: confType=" + ctype + " confGroup=" + group + " confKey=" + onlyKey);
896
- printHuman("confType=" + ctype + " confGroup=" + group + " 配置 " + filtered.length + " 条",
897
- filtered.map((r) => ({ confKey: r.confKey || r.conf_key || "", confValue: r.confValue ?? "", confName: r.confName ?? "", confAttrs: r.confAttrs ?? "" })),
898
- ["confKey", "confValue", "confName", "confAttrs"]);
899
- return filtered;
900
- }
901
- async function cmdConfGroupFile(args, json, keys) {
902
- const val = flagOrPos(args, "group-file", keys);
903
- if (val === undefined) throw new Error("conf group-file 缺少参数(用法: conf group-file @items.json,形如 [{\"confType\":\"api\",\"confGroup\":\"workflow\"}])");
904
- const pairs = readJsonArg(String(val), "--group-file");
905
- const list = (Array.isArray(pairs) ? pairs : [pairs]).filter((p) => p && p.confType);
906
- if (!list.length) throw new Error("conf group-file 内容为空或缺少 confType");
907
- const map = await confBatchByTypeAndGroup(list);
908
- const rows = Object.values(map);
909
- if (json) return map;
910
- printHuman("批量分组配置 " + rows.length + " 条",
911
- rows.map((r) => ({ confType: r.confType ?? "", confGroup: r.confGroup ?? "", confKey: r.confKey || r.conf_key || "", confValue: r.confValue ?? "", confName: r.confName ?? "" })),
912
- ["confType", "confGroup", "confKey", "confValue", "confName"]);
913
- return map;
914
- }
915
- async function confResolveAll(keys, group) {
916
- // 多占位符:先批量预取一轮(失败降级逐条单查),返回 {key: data}
917
- const items = keys.map((k) => confPutGroup({ confType: confKeyType(k, ""), confKey: k }, confKeyType(k, ""), group));
918
- const map = {};
919
- try {
920
- const r = await confRequest("POST", CONF_BATCH_PATH, items);
921
- const got = confNormalizeBatch(r);
922
- for (const it of items) {
923
- const c = confLookup(got, it);
924
- if (!c || c.confValue === undefined || c.confValue === null || c.confValue === "") throw new Error("缺失 " + it.confKey);
925
- map[it.confKey] = c;
926
- }
927
- } catch {
928
- for (const it of items) map[it.confKey] = await confGetData(it.confType, it.confKey, false, it.confGroup);
929
- }
930
- return map;
931
- }
932
- async function confResolveText(text, json, group) {
933
- const found = [...new Set([...text.matchAll(CONF_TOKEN_RE)].map((m) => m[1]))];
934
- const map = found.length ? await confResolveAll(found, group) : {};
935
- const out = text.replace(CONF_TOKEN_RE, (m0, k) => {
936
- const cfg = map[k];
937
- if (!cfg) throw new Error("配置不存在或为空: " + k);
938
- return cfg.confValue ?? "";
939
- });
940
- if (json) return { input: text, output: out, resolved: Object.fromEntries(Object.entries(map).map(([k, v]) => [k, v.confValue ?? ""])) };
941
- return out;
942
- }
943
- async function cmdConfResolve(args, json, keys) {
944
- let text = keys[0];
945
- if (!text) throw new Error("conf resolve 缺少文本(用法: conf resolve '<含#{KEY}文本>' | @file.txt [--group g])");
946
- if (text.startsWith("@")) text = fs.readFileSync(text.slice(1), "utf-8");
947
- const out = await confResolveText(text, json, args.group);
948
- if (json) { console.log(JSON.stringify(out, null, 1)); return out; }
949
- process.stdout.write(out + "\n");
950
- }
951
- async function cmdConfResolveFile(args, json, keys) {
952
- const p = flagOrPos(args, "resolve-file", keys);
953
- if (!p) throw new Error("conf resolve-file 缺少文件路径(用法: conf resolve-file <文件路径> [--group g])");
954
- if (!fs.existsSync(p)) throw new Error("文件不存在: " + p);
955
- const text = fs.readFileSync(p, "utf-8");
956
- const out = await confResolveText(text, json, args.group);
957
- if (json) { console.log(JSON.stringify(out, null, 1)); return out; }
958
- process.stdout.write(out + "\n");
959
- }
960
- async function cmdConfAdd(args, json, keys) {
961
- const key = keys[0];
962
- if (!key || args.value === undefined) throw new Error("conf add 缺少 confKey/confValue(用法: conf add <key> --value <真实值> [--name] [--attrs] [--type] [--group] [--force])");
963
- const ctype = confKeyType(key, args.type);
964
- if (!args.force && await confGetData(ctype, key, true, args.group)) {
965
- console.error("[config-resolve] 配置已存在,跳过新增(如需修改请用 conf update;确需重建可先 conf delete): " + key);
966
- return null;
967
- }
968
- const item = confPutGroup({ confType: ctype, confKey: key, confValue: String(args.value), confAttrs: args.attrs || "", confName: args.name || "" }, ctype, args.group);
969
- const r = await confRequest("POST", CONF_ADD_PATH, [item]);
970
- if (json) return r;
971
- console.log("新增成功: " + (item.confGroup ? item.confGroup + "/" : "") + key + " = " + item.confValue);
972
- }
973
- async function cmdConfAddFile(args, json, keys) {
974
- const val = flagOrPos(args, "add-file", keys);
975
- if (val === undefined) throw new Error("conf add-file 缺少参数(用法: conf add-file @items.json,元素形如 {confType,confGroup,confKey,confValue,confName,confAttrs})");
976
- const items = readJsonArg(String(val), "--add-file");
977
- const list = Array.isArray(items) ? items : [items];
978
- const toAdd = [];
979
- for (const it of list) {
980
- if (!it.confKey || it.confValue === undefined) continue;
981
- const ctype = it.confType || confKeyType(it.confKey, args.type);
982
- const group = it.confGroup !== undefined ? it.confGroup : args.group;
983
- const dup = await confGetData(ctype, it.confKey, true, group);
984
- if (dup) { console.error("[config-resolve] 已存在跳过: " + it.confKey); continue; }
985
- toAdd.push(confPutGroup({ confType: ctype, confKey: it.confKey, confValue: String(it.confValue), confAttrs: it.confAttrs || "", confName: it.confName || "" }, ctype, group));
986
- }
987
- if (!toAdd.length) { console.log("全部已存在,无新增"); return null; }
988
- const r = await confRequest("POST", CONF_ADD_PATH, toAdd);
989
- if (json) return r;
990
- console.log("批量新增 " + toAdd.length + " 条成功");
991
- }
992
- async function cmdConfUpdate(args, json, keys) {
993
- const key = keys[0];
994
- if (!key) throw new Error("conf update 缺少 confKey(用法: conf update <key> [--value v] [--name n] [--attrs a] [--type t] [--group g])");
995
- if (args.value === undefined && args.name === undefined && args.attrs === undefined) {
996
- throw new Error("conf update 至少需提供 --value / --name / --attrs 之一");
997
- }
998
- const ctype = confKeyType(key, args.type);
999
- const body = confPutGroup({ confType: ctype, confKey: key }, ctype, args.group);
1000
- if (args.value !== undefined) body.confValue = String(args.value);
1001
- if (args.name !== undefined) body.confName = args.name;
1002
- if (args.attrs !== undefined) body.confAttrs = args.attrs;
1003
- const r = await confRequest("POST", CONF_UPDATE_PATH, body);
1004
- if (json) return r;
1005
- console.log("更新成功: " + (body.confGroup ? body.confGroup + "/" : "") + key);
1006
- }
1007
- async function cmdConfDelete(args, json, keys) {
1008
- const key = keys[0];
1009
- if (!key) throw new Error("conf delete 缺少 confKey(用法: conf delete <key> [--type t] [--group g])");
1010
- const ctype = confKeyType(key, args.type);
1011
- const url = CONF_DELETE_PATH + "?confType=" + encodeURIComponent(ctype) + "&confKey=" + encodeURIComponent(key) + confGroupSuffix(ctype, args.group);
1012
- const r = await confRequest("DELETE", url);
1013
- if (json) return r;
1014
- console.log("删除成功: " + (confUseGroup(ctype, args.group) && args.group ? args.group + "/" : "") + key);
1015
- }
1016
- async function cmdConf(sub, args, json, pos) {
1017
- const keys = pos.slice(2);
1018
- switch (sub) {
1019
- case "get": return cmdConfGet(args, json, keys);
1020
- case "batch": return cmdConfBatch(args, json, keys);
1021
- case "batch-file": return cmdConfBatchFile(args, json, keys);
1022
- case "group": return cmdConfGroup(args, json, keys);
1023
- case "group-file": return cmdConfGroupFile(args, json, keys);
1024
- case "resolve": return cmdConfResolve(args, json, keys);
1025
- case "resolve-file": return cmdConfResolveFile(args, json, keys);
1026
- case "add": return cmdConfAdd(args, json, keys);
1027
- case "add-file": return cmdConfAddFile(args, json, keys);
1028
- case "update": return cmdConfUpdate(args, json, keys);
1029
- case "delete": return cmdConfDelete(args, json, keys);
1030
- default: usage(); process.exit(1);
1031
- }
1032
- }
1033
-
1034
- // ---------------- attend:考勤 REST 接口 ----------------
1035
- // 2026-09-07:封装 /api/attend/web/attendInfoV2/getAttendInfoStatis(假期余额统计,hr-attendance 场景六病假/事假)。
1036
- // 请求参数 {beginDate, endDate, userId};返回 data.vacationBalanceList[]:{id,title,data:[{name,value,key}]},
1037
- // key 含 lastRemaining(历年可请)/remaining(今年可请)/total(总计可请)。
1038
- function defaultMonthRange() {
1039
- const now = new Date();
1040
- const y = now.getFullYear();
1041
- const m = now.getMonth() + 1;
1042
- const lastDay = new Date(y, m, 0).getDate();
1043
- const p = (n) => String(n).padStart(2, "0");
1044
- return { begin: y + "-" + p(m) + "-01", end: y + "-" + p(m) + "-" + p(lastDay) };
1045
- }
1046
- async function cmdAttendStatis(args, json) {
1047
- const user = args.user || auth.userId || "";
1048
- if (!user) throw new Error("缺少查询人员:--user <人员ID>(查本人可不传,默认当前登录用户)");
1049
- const def = defaultMonthRange();
1050
- const body = { beginDate: args.begin || def.begin, endDate: args.end || def.end, userId: user };
1051
- const r = await postJson("/api/attend/web/attendInfoV2/getAttendInfoStatis", body);
1052
- if (json) return r;
1053
- if (String(r.code) !== "200") {
1054
- console.log("code=" + r.code + " msg=" + (r.msg || ""));
1055
- if (/没有权限|无权限|权限不足|未授权|无权访问|无查询权限/i.test(String(r.msg || ""))) {
1056
- throw new Error("当前用户对该人员考勤数据无查询权限,请检查 ETEAMSID 或联系管理员开通");
1057
- }
1058
- return;
1059
- }
1060
- const d = r.data || {};
1061
- const list = Array.isArray(d.vacationBalanceList) ? d.vacationBalanceList : [];
1062
- if (!list.length) { console.log("(空:该人员无假期余额数据) userId=" + user + " " + body.beginDate + "~" + body.endDate); return; }
1063
- console.log("== 考勤假期余额 userId=" + user + "(" + body.beginDate + " ~ " + body.endDate + ")==");
1064
- for (const item of list) {
1065
- console.log((item.title || item.id || "?") + (item.id ? " [" + item.id + "]" : ""));
1066
- if (Array.isArray(item.data) && item.data.length) {
1067
- console.log(" " + item.data.map((x) => (x.name || x.key) + "=" + (x.value ?? "")).join(" "));
1068
- }
1069
- }
1070
- }
1071
-
1072
- // ---------------- exp:专家包版本管理(expert-repository) ----------------
1073
- async function expGet(url) {
1074
- // 返回 {raw, j}:raw 用于正则提取大整数 id(commitId 19 位超 JS Number 精度,JSON.parse 会丢精度!)
1075
- const resp = await fetch(BASE + url, {
1076
- method: "GET",
1077
- headers: { "accept": "application/json, text/plain, */*", "langType": "zh_CN", "cookie": auth.cookie, "eteamsid": auth.eteamsid, "ebbusinessid": EB_BIZ_ID, "user-agent": UA },
1078
- });
1079
- const raw = await resp.text();
1080
- let j; try { j = parseJsonSafe(raw); } catch { throw new Error("HTTP " + resp.status + " 非JSON响应: " + raw.slice(0, 300)); }
1081
- if (resp.status === 401 || (j && String(j.code) === "-1" && /token|登录|失效/i.test(String(j.msg || "")))) throw new Error("登录态失效,请重新 e10-login oidc 登录");
1082
- return { raw, j };
1083
- }
1084
- function rawNum(raw, key) {
1085
- // 从原始 JSON 文本提取整数字段为精确字符串(避免 JSON.parse 大数丢精度,如 commitId)
1086
- const m = raw.match(new RegExp('"' + key + '":\\s*(\\d+)'));
1087
- return m ? m[1] : "";
1088
- }
1089
- function rawNumSeq(raw, key) {
1090
- // 提取某 key 全部整数序列(按出现顺序),如提交历史里的 commitId
1091
- const re = new RegExp('"' + key + '":\\s*(\\d+)', "g");
1092
- const out = []; let m;
1093
- while ((m = re.exec(raw)) !== null) out.push(m[1]);
1094
- return out;
1095
- }
1096
- const EXP_ALLOWED_DIRS = [".codebuddy-plugin", "agents", "avatars", "skills"];
1097
- const EXP_SKIP_DIRS = [".git", ".workbuddy", "node_modules", "__pycache__"];
1098
- const EXP_SKIP_FILES = [".sync-state.json"]; // 本地同步状态元文件(含基线 sha),永不随包提交
1099
- function sha256(buf) { return crypto.createHash("sha256").update(buf).digest("hex"); }
1100
- function walkDir(dir, root) {
1101
- const out = [];
1102
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1103
- const abs = path.join(dir, entry.name);
1104
- const rel = path.relative(root, abs).split(path.sep).join("/");
1105
- if (entry.isDirectory()) {
1106
- if (EXP_SKIP_DIRS.includes(entry.name)) continue;
1107
- const top = rel.split("/")[0];
1108
- if (rel.includes("/") && !EXP_ALLOWED_DIRS.includes(top)) continue; // 仅允许根文件+四大目录
1109
- walkDir(abs, root).forEach((x) => out.push(x));
1110
- } else if (entry.isFile()) {
1111
- if (EXP_SKIP_FILES.includes(entry.name)) continue; // .sync-state.json 不入包
1112
- const top = rel.split("/")[0];
1113
- if (!rel.includes("/") || EXP_ALLOWED_DIRS.includes(top)) out.push({ rel, abs });
1114
- }
1115
- }
1116
- return out;
1117
- }
1118
- function collectPackage(dir) {
1119
- if (!fs.existsSync(dir)) throw new Error("本地专家包目录不存在: " + dir);
1120
- return walkDir(dir, dir).map(({ rel, abs }) => {
1121
- const buf = fs.readFileSync(abs);
1122
- if (buf.length > 10 * 1024 * 1024) throw new Error("文件超 10MB 上限: " + rel);
1123
- return { relativePath: rel, contentBase64: buf.toString("base64"), sha256: sha256(buf), abs };
1124
- });
1125
- }
1126
- function localShaMap(dir) {
1127
- const map = {};
1128
- for (const f of walkDir(dir, dir)) map[f.rel] = sha256(fs.readFileSync(f.abs));
1129
- return map;
1130
- }
1131
- function expOk(r) {
1132
- if (!r) return false;
1133
- if (r.actionMsg) return String(r.actionMsg.code) === "0";
1134
- const code = r.code;
1135
- if (code === undefined) return !r.fail;
1136
- return String(code) === "0" || String(code) === "200";
1137
- }
1138
- function expFailMsg(r) {
1139
- if (r && r.actionMsg && String(r.actionMsg.code) !== "0") return r.actionMsg.message;
1140
- if (r && r.msg) return r.msg;
1141
- return "请求失败";
1142
- }
1143
- function printExpData(r, title) {
1144
- if (!expOk(r)) throw new Error(expFailMsg(r));
1145
- if (title) console.log("== " + title + " ==");
1146
- console.log(JSON.stringify(r.data ?? {}, null, 1));
1147
- }
1148
- async function rawGet(url) {
1149
- const resp = await fetch(BASE + url, {
1150
- method: "GET",
1151
- headers: { "accept": "*/*", "langType": "zh_CN", "cookie": auth.cookie, "eteamsid": auth.eteamsid, "ebbusinessid": EB_BIZ_ID, "user-agent": UA },
1152
- });
1153
- if (resp.status === 401) throw new Error("登录态失效,请重新 e10-login oidc 登录");
1154
- return Buffer.from(await resp.arrayBuffer());
1155
- }
1156
- async function expStatus(args, json) {
1157
- const expert = need("expert", args);
1158
- const { raw, j: r } = await expGet("/api/dw/expert-repository/v1/repositories/status?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
1159
- if (json) return r;
1160
- if (!expOk(r)) throw new Error(expFailMsg(r));
1161
- const d = r.data || {};
1162
- console.log("== 仓库状态 " + expert + " ==");
1163
- console.log("expertCode = " + (d.expertCode ?? ""));
1164
- console.log("displayName = " + (d.displayName ?? ""));
1165
- console.log("branchName = " + (d.branchName ?? ""));
1166
- console.log("headCommitId = " + (rawNum(raw, "headCommitId") || d.headCommitId));
1167
- console.log("headRevision = " + (d.headRevision ?? ""));
1168
- for (const k of ["headCommitHash", "headCommitMessage", "headCommitTime", "headAuthor", "protected"]) if (d[k] !== undefined) console.log(k + " = " + d[k]);
1169
- if (Array.isArray(d.locks) && d.locks.length) { console.log("有效锁:"); for (const l of d.locks) console.log(" " + JSON.stringify(l)); }
1170
- }
1171
- async function expManifest(args, json) {
1172
- const expert = need("expert", args);
1173
- let url = "/api/dw/expert-repository/v1/repositories/manifest?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main");
1174
- if (args.commit) url += "&commitId=" + encodeURIComponent(args.commit);
1175
- const r = await getUrl(url);
1176
- if (json) return r;
1177
- if (!expOk(r)) throw new Error(expFailMsg(r));
1178
- const items = r.data || [];
1179
- console.log("== 文件清单 " + items.length + " 项 ==");
1180
- printHuman("", items, ["relativePath", "sha256", "size", "action"]);
1181
- }
1182
- async function expCommits(args, json) {
1183
- const expert = need("expert", args);
1184
- const { raw, j: r } = await expGet("/api/dw/expert-repository/v1/repositories/commits?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
1185
- if (json) return r;
1186
- if (!expOk(r)) throw new Error(expFailMsg(r));
1187
- const rows = (r.data || []).map((x, i) => ({ ...x, commitId: (rawNumSeq(raw, "commitId")[i] || x.commitId) }));
1188
- console.log("== 提交历史 " + rows.length + " 条(commitId 为精确值,可复制用于 file/release/rollback)==");
1189
- printHuman("", rows, ["commitId", "submitMode", "author", "createTime", "message"]);
1190
- }
1191
- async function expFile(args, json) {
1192
- const expert = need("expert", args), commit = need("commit", args), rp = need("path", args);
1193
- const buf = await rawGet("/api/dw/expert-repository/v1/repositories/file?expertCode=" + encodeURIComponent(expert) + "&commitId=" + encodeURIComponent(commit) + "&relativePath=" + encodeURIComponent(rp));
1194
- if (args.out) { fs.writeFileSync(args.out, buf); console.log("已下载 " + buf.length + " 字节 -> " + args.out); return; }
1195
- if (json) { console.log(JSON.stringify({ relativePath: rp, size: buf.length, base64: buf.toString("base64") })); return; }
1196
- process.stdout.write(buf.toString("utf-8"));
1197
- }
1198
- // ---- exp 智能拉取:基线状态文件(三方差异保护,2026-09-04 实现)----
1199
- // .sync-state.json 记录上次成功同步的 commitId/revision 与每文件 sha256(基线)。
1200
- // pull 逐文件三方对比:local(当前本地) vs base(上次同步基线) vs remote(远端 manifest),
1201
- // 决策:仅远端改→覆盖;仅本地改→保留本地;双方都改→备份本地副本后覆盖(备份目录保留本地版);
1202
- // 无基线文件/基线无记录→保守备份后覆盖。拉取成功后把 .sync-state.json 更新为新 head。
1203
- function syncStatePath(dir) { return path.join(dir, ".sync-state.json"); }
1204
- function readSyncState(dir) {
1205
- const p = syncStatePath(dir);
1206
- if (!fs.existsSync(p)) return null;
1207
- try {
1208
- const j = JSON.parse(fs.readFileSync(p, "utf-8"));
1209
- const files = {};
1210
- for (const [rel, v] of Object.entries(j.files || {})) files[rel] = (typeof v === "string") ? v : (v.sha256 || "");
1211
- return { commitId: String(j.commitId || ""), headRevision: j.headRevision, files };
1212
- } catch { return null; }
1213
- }
1214
- function writeSyncState(dir, commitId, headRevision, fileShaMap) {
1215
- const files = {};
1216
- for (const [rel, sha] of Object.entries(fileShaMap)) files[rel] = { sha256: sha };
1217
- const obj = { commitId: String(commitId), headRevision, files, updateTime: new Date().toISOString() };
1218
- fs.writeFileSync(syncStatePath(dir), JSON.stringify(obj, null, 1), "utf-8");
1219
- }
1220
- async function expPull(args, json) {
1221
- const expert = need("expert", args), dir = need("dir", args);
1222
- const dry = !!args["dry-run"];
1223
- const force = !!args["force"]; // --force = 一律以远端覆盖(放弃本地修改)
1224
- const st = await expGet("/api/dw/expert-repository/v1/repositories/status?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
1225
- if (!expOk(st.j)) throw new Error(expFailMsg(st.j));
1226
- const headId = rawNum(st.raw, "headCommitId") || String((st.j.data || {}).headCommitId);
1227
- const headRev = (st.j.data || {}).headRevision;
1228
- const mf = await expGet("/api/dw/expert-repository/v1/repositories/manifest?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
1229
- if (!expOk(mf.j)) throw new Error(expFailMsg(mf.j));
1230
- const items = (mf.j.data || []).filter((f) => f.relativePath && f.relativePath !== "DELETE");
1231
- const local = fs.existsSync(dir) ? localShaMap(dir) : {};
1232
- const base = readSyncState(dir); // 基线:上次同步后的文件 sha(无则 null)
1233
- const baseCommit = (base && base.commitId) || "";
1234
- // 基线有效判定:上次同步 commitId 须在提交历史上(取不到历史就保守按无基线处理)
1235
- let backupRoot = "";
1236
- let downloaded = 0, unchanged = 0, preserved = 0, backed = 0, missing = 0;
1237
- const plan = []; // {rel, action, reason}
1238
- const mkBackupRoot = () => {
1239
- if (backupRoot) return backupRoot;
1240
- const absDir = path.resolve(dir);
1241
- const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1242
- backupRoot = path.join(path.dirname(absDir), path.basename(absDir) + ".pull-backup-" + ts);
1243
- fs.mkdirSync(backupRoot, { recursive: true });
1244
- return backupRoot;
1245
- };
1246
- for (const it of items) {
1247
- const lp = path.join(dir, it.relativePath.split("/").join(path.sep));
1248
- const ls = local[it.relativePath];
1249
- const baseSha = (base && base.files) ? base.files[it.relativePath] : undefined;
1250
- if (ls === it.sha256) { unchanged++; continue; }
1251
- if (ls === undefined) { // 本地缺失 → 直接下载(新增/恢复)
1252
- missing++; plan.push({ rel: it.relativePath, action: dry ? "待下载" : "已下载", reason: "本地缺失" });
1253
- } else if (!force && baseSha !== undefined && ls === baseSha && baseSha !== it.sha256) {
1254
- // 仅远端改(本地未动,与基线一致)→ 覆盖
1255
- plan.push({ rel: it.relativePath, action: dry ? "待下载" : "已下载", reason: "远端更新(本地未动)" });
1256
- } else if (!force && baseSha !== undefined && baseSha === it.sha256) {
1257
- // 仅本地改(远端=基线,本地≠基线)→ 保留本地,不覆盖
1258
- preserved++; plan.push({ rel: it.relativePath, action: "保留本地", reason: "本地修改(远端未动)" });
1259
- continue;
1260
- } else { // 双方都改 / 无基线可判 / --force → 先备份本地副本再覆盖
1261
- backed++;
1262
- if (!dry && !force) {
1263
- const bdir = path.join(mkBackupRoot(), it.relativePath.split("/").join(path.sep));
1264
- fs.mkdirSync(path.dirname(bdir), { recursive: true });
1265
- fs.copyFileSync(lp, bdir);
1266
- }
1267
- plan.push({ rel: it.relativePath, action: dry ? "待下载(将备份本地)" : (force ? "已下载(force覆盖)" : "已下载(本地已备份)"), reason: (force ? "force 覆盖" : (baseSha === undefined ? "无基线记录" : "双方都改")) });
1268
- }
1269
- if (dry) continue;
1270
- fs.mkdirSync(path.dirname(lp), { recursive: true });
1271
- const buf = await rawGet("/api/dw/expert-repository/v1/repositories/file?expertCode=" + encodeURIComponent(expert) + "&commitId=" + encodeURIComponent(headId) + "&relativePath=" + encodeURIComponent(it.relativePath));
1272
- fs.writeFileSync(lp, buf);
1273
- downloaded++;
1274
- }
1275
- // 拉取成功后更新基线状态文件(记录新 head + 本地最终文件 sha)
1276
- if (!dry) {
1277
- const finalLocal = localShaMap(dir);
1278
- writeSyncState(dir, headId, headRev, finalLocal);
1279
- }
1280
- const out = { expert, headCommitId: headId, headRevision: headRev, total: items.length, unchanged, downloaded, preserved, backed, missing, dryRun: !!dry, backupDir: backupRoot || undefined };
1281
- if (json) { console.log(JSON.stringify(out)); return; }
1282
- console.log("== pull " + expert + "@" + headId + (dry ? " (dry-run)" : "") + " ==");
1283
- console.log("清单 " + out.total + " 项 | 未变 " + unchanged + " | 已下载 " + downloaded + " | 保留本地 " + preserved + " | 覆盖前备份 " + backed + " | 缺失 " + missing);
1284
- for (const p of plan.slice(0, 40)) console.log(" [" + p.action + "] " + p.rel + (p.reason ? "(" + p.reason + ")" : ""));
1285
- if (plan.length > 40) console.log(" ... 共 " + plan.length + " 个文件");
1286
- if (backupRoot && !dry) console.log("本地修改副本备份于: " + backupRoot);
1287
- if (preserved > 0 && !dry) console.log("⚠️ " + preserved + " 个文件因含本地修改被保留(远端未改);如需强制覆盖请加 --force");
1288
- }
1289
- async function expInit(args, json) {
1290
- const expert = need("expert", args), dir = need("dir", args);
1291
- const files = collectPackage(dir);
1292
- const body = { expertCode: expert, displayName: args["display-name"] || expert, branchName: args.branch || "main",
1293
- description: args.description || "", initialCommitMessage: args.message || "初始化导入", files };
1294
- const r = await postJson("/api/dw/expert-repository/v1/repositories", body);
1295
- if (json) return r;
1296
- if (!expOk(r)) throw new Error(expFailMsg(r));
1297
- console.log("== 仓库已创建 =="); console.log(JSON.stringify(r.data ?? {}, null, 1));
1298
- }
1299
- async function expCommit(args, json) {
1300
- const expert = need("expert", args), dir = need("dir", args), msg = need("message", args);
1301
- const mode = (args.mode || "FULL_SNAPSHOT").toUpperCase();
1302
- const st = await expGet("/api/dw/expert-repository/v1/repositories/status?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
1303
- if (!expOk(st.j)) throw new Error("无法获取基线: " + expFailMsg(st.j));
1304
- const base = st.j.data || {};
1305
- const body = {
1306
- branchName: base.branchName || "main",
1307
- baseCommitId: rawNum(st.raw, "headCommitId") || String(base.headCommitId), // 精确字符串,避免大数丢精度
1308
- baseRevision: base.headRevision,
1309
- commitMessage: msg,
1310
- };
1311
- if (mode === "FILE_PATCH") {
1312
- const mf = await expGet("/api/dw/expert-repository/v1/repositories/manifest?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
1313
- if (!expOk(mf.j)) throw new Error("无法获取 manifest: " + expFailMsg(mf.j));
1314
- const remoteSha = {}; for (const f of (mf.j.data || [])) remoteSha[f.relativePath] = f.sha256;
1315
- const local = collectPackage(dir);
1316
- const localSha = {}; for (const f of local) localSha[f.relativePath] = f.sha256;
1317
- const changes = [];
1318
- for (const f of local) {
1319
- const rs = remoteSha[f.relativePath];
1320
- if (rs === undefined) changes.push({ operation: "ADD", relativePath: f.relativePath, contentBase64: f.contentBase64, sha256: f.sha256, baseFileSha256: "" });
1321
- else if (rs !== f.sha256) changes.push({ operation: "MODIFY", relativePath: f.relativePath, contentBase64: f.contentBase64, sha256: f.sha256, baseFileSha256: rs });
1322
- }
1323
- for (const rp of Object.keys(remoteSha)) if (localSha[rp] === undefined) changes.push({ operation: "DELETE", relativePath: rp, baseFileSha256: remoteSha[rp] });
1324
- if (!changes.length) throw new Error("本地与远端一致,无变更可提交");
1325
- body.submitMode = "FILE_PATCH"; body.changes = changes;
1326
- } else {
1327
- body.submitMode = "FULL_SNAPSHOT"; body.files = collectPackage(dir);
1328
- }
1329
- const r = await postJson("/api/dw/expert-repository/v1/repositories/commits?expertCode=" + encodeURIComponent(expert), body);
1330
- if (json) return r;
1331
- if (!expOk(r)) {
1332
- const m = expFailMsg(r);
1333
- if (/基线|过期|revision|冲突/i.test(m)) throw new Error("提交冲突: " + m + " —— 请先 exp pull 拉取最新并合并后重试");
1334
- throw new Error("提交失败: " + m);
1335
- }
1336
- // 提交成功后把本地 .sync-state.json 基线刷新到新 head(与本地文件 sha 一致),保证后续 pull 三方判定准确
1337
- const rj = r.data || {};
1338
- const newHead = rawNum(JSON.stringify(r), "headCommitId") || rawNum(JSON.stringify(r), "commitId") || rj.headCommitId || rj.commitId || "";
1339
- if (newHead && fs.existsSync(dir)) {
1340
- try { writeSyncState(dir, newHead, rj.headRevision ?? rj.revision ?? 0, localShaMap(dir)); } catch {}
1341
- }
1342
- console.log("== 提交成功 =="); console.log(JSON.stringify(r.data ?? {}, null, 1));
1343
- }
1344
- async function expLock(args, json) {
1345
- const expert = need("expert", args);
1346
- const act = (args.action || "").toLowerCase();
1347
- const branch = args.branch || "main";
1348
- if (act === "query") {
1349
- const r = await getUrl("/api/dw/expert-repository/v1/repositories/locks?expertCode=" + encodeURIComponent(expert) + "&branchName=" + encodeURIComponent(branch));
1350
- if (json) return r;
1351
- if (!expOk(r)) throw new Error(expFailMsg(r));
1352
- printExpData(r, "有效锁");
1353
- return;
1354
- }
1355
- if (act === "heartbeat") {
1356
- const token = need("lock-token", args);
1357
- const r = await postJson("/api/dw/expert-repository/v1/repositories/locks/heartbeat?lockToken=" + encodeURIComponent(token), {});
1358
- if (json) return r;
1359
- if (!expOk(r)) throw new Error(expFailMsg(r));
1360
- console.log("锁已续期: " + JSON.stringify(r.data ?? {}));
1361
- return;
1362
- }
1363
- if (act === "release") {
1364
- const token = need("lock-token", args);
1365
- const req = new Request(BASE + "/api/dw/expert-repository/v1/repositories/locks?lockToken=" + encodeURIComponent(token), { method: "DELETE" });
1366
- const resp = await fetch(req, { headers: { "cookie": auth.cookie, "eteamsid": auth.eteamsid, "accept": "application/json" } });
1367
- const text = await resp.text();
1368
- let r; try { r = JSON.parse(text); } catch { r = { data: { raw: text } }; }
1369
- if (json) return r;
1370
- if (!expOk(r)) throw new Error(expFailMsg(r));
1371
- console.log("锁已释放");
1372
- return;
1373
- }
1374
- // acquire(默认)
1375
- const body = { branchName: branch, componentKey: args.component || "ROOT", ttlMinutes: parseInt(args.ttl || "30", 10) };
1376
- const r = await postJson("/api/dw/expert-repository/v1/repositories/locks?expertCode=" + encodeURIComponent(expert), body);
1377
- if (json) return r;
1378
- if (!expOk(r)) throw new Error(expFailMsg(r));
1379
- console.log("== 锁已获取(记录 lockToken 供 heartbeat/release/commit 使用)==");
1380
- console.log(JSON.stringify(r.data ?? {}, null, 1));
1381
- }
1382
- async function expRelease(args, json) {
1383
- const expert = need("expert", args);
1384
- const r = await postJson("/api/dw/expert-repository/v1/repositories/releases?expertCode=" + encodeURIComponent(expert),
1385
- { commitId: need("commit", args), versionTag: need("version", args), releaseNote: args.note || "" });
1386
- if (json) return r;
1387
- if (!expOk(r)) throw new Error(expFailMsg(r));
1388
- console.log("== 发布成功 =="); console.log(JSON.stringify(r.data ?? {}, null, 1));
1389
- }
1390
- async function expRollback(args, json) {
1391
- const expert = need("expert", args);
1392
- const r = await postJson("/api/dw/expert-repository/v1/repositories/releases/rollback?expertCode=" + encodeURIComponent(expert) + "&versionTag=" + encodeURIComponent(need("version", args)), {});
1393
- if (json) return r;
1394
- if (!expOk(r)) throw new Error(expFailMsg(r));
1395
- console.log("== 回滚成功 =="); console.log(JSON.stringify(r.data ?? {}, null, 1));
1396
- }
1397
-
1398
- // ---------------- main ----------------
1399
- function usage() {
1400
- console.log(`e10-data CLI(data-assistant 数据接口统一命令行,2026-09-03 扩展)
1401
- 用法: node e10-data.mjs <族> <子命令> [--参数] 通用 --json 原样输出
1402
-
1403
- query DSL 数据查询 executeQueryDsl
1404
- query --conn <connId> --model <ebuilder|flow|eteams|attend|hr|document|dw|blog> --schema <数据库> [--table-id <流程ID>] [--debug true|false] --dsl @file.json|'{...}'
1405
- ds 数据源(业务模块)取数与元数据
1406
- ds tables --group <gid> [--name 关键词]
1407
- ds fields --group <gid> --table <tid>
1408
- ds list --group <gid> --table <tid> --fields a,b [--where @f|json] [--order "f asc"] [--page N] [--size N]
1409
- ds chart --group <gid> --table <tid> --group-by g1,g2 --stat '[{"fieldId":"x","aggregator":"SUM"}]' [--where ...]
1410
- ds stat --group <gid> --table <tid> --stat '[...]' [--where ...]
1411
- ds count --group <gid> --table <tid> [--where ...]
1412
- (ds 通用 --source-type LOGIC|ETEAMS,默认 LOGIC)
1413
- (ds 取数 list|chart|stat|count 的 appId:默认自动解析并带上——顺序 chat_bi_eb_config_map(dw 权限配置表,按 obj_id)
1414
- → ebdf_obj(ec_edcapp,按 id),每次实时查、不缓存;也可 --app-id <id> 显式指定、--app-id auto 强制自动、
1415
- --no-app-id 完全跳过自动解析(最快,适用于无需 appId 的数据集);ds tables/fields 也接受 --app-id)
1416
- --where 过滤条件(v0.1.8 起支持树形 OR,递归):
1417
- 数组 = AND 根: [{"fieldId":"f1","conditionType":"Date","compareType":"range","conditionValue":"2026-09~2026-10"},{"fieldId":"f2",...}]
1418
- 对象 = 完整树: {"relationship":"0","conditionList":[{"fieldId":"f1",...},{"fieldId":"f2",...}]} # "0"=OR、"1"=AND,缺省 AND
1419
- 数组元素可内嵌条件组 {"relationship":"0","conditionList":[...]} 实现 (A AND B) OR (C AND D)
1420
- 叶子键 fieldId/conditionId 均可(fieldId 优先);缺少字段的叶子会直接报错(不再静默当全表)
1421
- doc 文档查询
1422
- doc count [--name 关键词] [--creator id1,id2] [--author id1,id2] [--content-type office] [--time "起~止"] [--last-time "起~止"] [--folder id,id|--folder-neq id,id]
1423
- doc list [同上条件] [--all 翻页到底] [--page N] [--size 100]
1424
- noun 词库查询
1425
- noun list --skill <skillKey> [--source-type LOGIC] [--source-group gid] [--source-table tid](数据源场景带三参数)
1426
- field 关联字段翻译
1427
- field id2name --module <m> --type <t> --ids id1,id2 [--form-param '{"fieldId":"..","module":"ebuilderform"}'|--eb-browser-param '{..}'|--property-type x]
1428
- field name2id --module <m> --type <t> --name 名称 [同上附加参数]
1429
- conf 环境参数配置中心(chatBi config map,替代 config-resolve 脚本 URL 直连;confType 默认由 CONN_/DS_/WF_/API_ 前缀推导,可 --type 显式)
1430
- confGroup(配置分组,v0.1.9 新增,可选):confType=api 时后端支持二级分层(如 api/workflow),
1431
- 定位维度 = confType+confGroup+confKey;仅 confType=api(或显式传 --group)时才下发 confGroup,其他类型不带该字段
1432
- conf get <key> [--type t] [--group g] # 单查(confValue/confAttrs/confName 全量 JSON)
1433
- conf batch <key1> [key2 ...] [--type t] [--group g] # 批量查(批量接口异常自动降级逐条单查)
1434
- conf batch-file @items.json # 按 [{confType,confGroup,confKey}..] 显式批量查(支持任意 confType)
1435
- conf group <confType> <confGroup> [--key k] # 按类型+分组批量查(batchGetChatBiConfigMapByTypeAndGroup)
1436
- conf group-file @items.json # 按 [{"confType":"api","confGroup":"workflow"}..] 多分组批量查
1437
- conf resolve '<含#{KEY}文本>'|@file.txt [--group g] # 文本占位符替换为真实值(含中文建议 @file)
1438
- conf resolve-file <文件路径> [--group g] # 读文件替换全部占位符并打印(不改写原文件)
1439
- conf add <key> --value <真实值> [--name 说明] [--attrs '{"request":..,"response":..}'] [--type t] [--group g] [--force] # 新增(默认判重,已存在跳过)
1440
- conf add-file @items.json # JSON 数组批量新增(逐条判重,元素可带 confGroup)
1441
- conf update <key> [--value v] [--name n] [--attrs a] [--type t] [--group g] # 更新(confType+confGroup+confKey 定位,传哪个字段更哪个)
1442
- conf delete <key> [--type t] [--group g] # 删除
1443
- 示例: conf group api workflow | conf add GET_WORKFLOW_LIST --type api --group workflow --value /api/workflow/getList --name 查询流程列表
1444
- exp 专家包版本管理(expert-repository/v1,响应以 data.actionMsg.code=0 判成功)
1445
- exp status --expert <code> [--branch main] # 仓库状态(headCommitId/headRevision=提交基线)
1446
- exp manifest --expert <code> [--commit <id>] # 文件清单(sha256)
1447
- exp commits --expert <code> # 提交历史
1448
- exp file --expert <code> --commit <id> --path <相对路径> [--out 本地文件]
1449
- exp pull --expert <code> --dir <本地专家包目录> [--dry-run] [--force] # 智能拉取:基线三方对比(仅远端改→覆盖/仅本地改→保留/双方都改→备份后覆盖;--force 一律覆盖),成功后更新 .sync-state.json 基线
1450
- exp init --expert <code> --dir <本地目录> [--display-name X] [--message 初始化导入]
1451
- exp commit --expert <code> --dir <本地目录> --message "..." [--mode FULL_SNAPSHOT|FILE_PATCH]
1452
- exp lock --expert <code> --action query|acquire(默认)|heartbeat|release --component ROOT --ttl 30 --lock-token <token>
1453
- exp release --expert <code> --commit <id> --version vX.Y.Z [--note 说明]
1454
- exp rollback --expert <code> --version vX.Y.Z
1455
- attend 考勤 REST 接口(2026-09-07 新增,hr-attendance 病假/事假等假期余额)
1456
- attend statis [--user <人员ID>] [--begin 2026-09-01] [--end 2026-09-30] # getAttendInfoStatis 假期余额统计;--user 默认当前登录用户,--begin/--end 默认当月
1457
- 认证: 优先读权威源 ~/.e10-cli/profiles/<active>/auth(自动同步镜像 ~/.workbuddy/e10-login-cache.md);皆无请先 e10-login oidc`);
1458
- }
1459
-
1460
- async function main() {
1461
- const rawArgv = process.argv.slice(2);
1462
- // 仅当顶层显式 -v/--version(无子命令)时打印版本;exp release/rollback 的 --version 是版本号参数、conf 文本可能含 version 字样,一律放行
1463
- if (rawArgv.length === 1 && (rawArgv[0] === "--version" || rawArgv[0] === "-v")) {
1464
- console.log("e10-data " + cliVersion());
1465
- process.exit(0);
1466
- }
1467
- const { args, pos } = parseArgs(rawArgv);
1468
- if (!pos.length) { usage(); process.exit(1); }
1469
- const family = pos[0], sub = pos[1], json = !!args.json;
1470
- let result;
1471
- if (family === "ds") {
1472
- if (!sub) { usage(); process.exit(1); }
1473
- switch (sub) {
1474
- case "tables": result = await cmdTables(args, json); break;
1475
- case "fields": result = await cmdFields(args, json); break;
1476
- case "list": result = await cmdList(args, json); break;
1477
- case "chart": result = await cmdChart(args, json); break;
1478
- case "stat": result = await cmdStat(args, json); break;
1479
- case "count": result = await cmdCount(args, json); break;
1480
- default: usage(); process.exit(1);
1481
- }
1482
- } else if (family === "query") {
1483
- result = await cmdQuery(args, json);
1484
- } else if (family === "doc") {
1485
- if (sub === "count") result = await cmdDocCount(args, json);
1486
- else if (sub === "list") result = await cmdDocList(args, json);
1487
- else { usage(); process.exit(1); }
1488
- } else if (family === "noun") {
1489
- if (sub === "list") result = await cmdNoun(args, json);
1490
- else { usage(); process.exit(1); }
1491
- } else if (family === "field") {
1492
- if (sub === "id2name") result = await cmdFieldId2name(args, json);
1493
- else if (sub === "name2id") result = await cmdFieldName2id(args, json);
1494
- else { usage(); process.exit(1); }
1495
- } else if (family === "conf") {
1496
- if (!sub) { usage(); process.exit(1); }
1497
- result = await cmdConf(sub, args, json, pos);
1498
- } else if (family === "exp") {
1499
- if (!sub) { usage(); process.exit(1); }
1500
- switch (sub) {
1501
- case "status": result = await expStatus(args, json); break;
1502
- case "manifest": result = await expManifest(args, json); break;
1503
- case "commits": result = await expCommits(args, json); break;
1504
- case "file": result = await expFile(args, json); break;
1505
- case "pull": result = await expPull(args, json); break;
1506
- case "init": result = await expInit(args, json); break;
1507
- case "commit": result = await expCommit(args, json); break;
1508
- case "lock": result = await expLock(args, json); break;
1509
- case "release": result = await expRelease(args, json); break;
1510
- case "rollback": result = await expRollback(args, json); break;
1511
- default: usage(); process.exit(1);
1512
- }
1513
- } else if (family === "attend") {
1514
- if (sub === "statis") result = await cmdAttendStatis(args, json);
1515
- else { usage(); process.exit(1); }
1516
- } else { usage(); process.exit(1); }
1517
- if (json && result !== undefined) console.log(JSON.stringify(result, null, 1));
1518
- }
1519
- main().catch((e) => { console.error("ERROR: " + e.message); process.exit(1); });
1
+ #!/usr/bin/env node
2
+ // -*- coding: utf-8 -*-
3
+ /**
4
+ * e10-data CLI —— data-assistant 专家统一数据接口命令行(2026-09-03 扩展)
5
+ *
6
+ * 覆盖(数据查询链路全部家族;登录/HRM 复用 e10-login / hrm-cli,checkEbModulePermission 已废弃不提供):
7
+ * e10-data query DSL 查询 executeQueryDsl(conn/model/schema/tableId/dslQuery/debug)
8
+ * e10-data ds tables|fields|list|chart|stat|count 数据源(业务模块)取数与元数据
9
+ * 取数(list/chart/stat/count)自动解析并携带 queryDto.appId(v0.1.13):
10
+ * 顺序 chat_bi_eb_config_map(dw,按 obj_id) → ebdf_obj(ec_edcapp,按 id),实时查、不缓存;
11
+ * --app-id <id> 显式指定 / --app-id auto 强制自动 / --no-app-id 跳过(最快)
12
+ * e10-data doc count|list 文档查询 documentListCount / documentList
13
+ * e10-data noun list 词库 getChatDocsBySkillKey
14
+ * e10-data field id2name|name2id 关联字段翻译 testRemoteBrowser / complete
15
+ * e10-data conf get|batch|batch-file|group|group-file|resolve|resolve-file|add|add-file|update|delete
16
+ * 环境参数配置中心(chatBi config map,替代 config-resolve 脚本 URL 直连;
17
+ * confType=api 时支持 confGroup 分组二级定位,见 --group)
18
+ * e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-attendance 病假/事假,2026-09-07)
19
+ *
20
+ * 通用:--json 原样输出;复杂 JSON 参数支持 @文件 或 内联(中文走 @文件 更稳)
21
+ * 认证:加解密全部由 e10-login 负责(本 CLI 不实现任何加解密)。凭据出口优先级:
22
+ * ① e10-login auth --json → ② e10-login whoami --json → ③ e10-login whoami(文本解析)
23
+ * → ④ 明文镜像 ~/.workbuddy/e10-login-cache.md(兜底,命中时告警);均不可用时提示先 e10-login oidc 登录
24
+ *
25
+ * 用法示例:
26
+ * node e10-data.mjs query --conn 1144919996804022272 --model dw --schema dw --dsl @q.json
27
+ * node e10-data.mjs ds count --group weaver-project-servicetask --table 715738355537494016
28
+ * node e10-data.mjs doc count --name 测试
29
+ * node e10-data.mjs noun list --skill rd-department
30
+ * node e10-data.mjs field id2name --module ebuilder/form --type ebuilder \
31
+ * --ids 3194494277170470571 --form-param '{"fieldId":"966849910772850766","module":"ebuilderform"}'
32
+ */
33
+ import fs from "node:fs";
34
+ import os from "node:os";
35
+ import path from "node:path";
36
+ import crypto from "node:crypto";
37
+ import { spawnSync } from "node:child_process";
38
+ import { fileURLToPath } from "node:url";
39
+
40
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
41
+ function cliVersion() {
42
+ try { return JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")).version; }
43
+ catch { return "dev"; }
44
+ }
45
+
46
+ const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36";
47
+ const EB_BIZ_ID = "10000000000000000";
48
+
49
+ // ---------------- 认证(加解密全部交由 e10-login,本 CLI 不再实现任何加解密)----------------
50
+ // 背景:auth 文件由 e10-login 自己的密钥体系加密 —— e10-login 1.5.0 起为「每机随机密钥 + OS keychain
51
+ // (@napi-rs/keyring,service=e10-cli / account=auth-key;keychain 不可用才落 ~/.e10-cli/.key)」,
52
+ // 早期版本为固定 seed。**只有 e10-login 解得开**,所以本 CLI 只负责「向 e10-login 要明文凭据」,
53
+ // 不再自行解密 —— e10-login 以后更换密钥体系 / 算法 / profile 机制,本 CLI 都无需改动。
54
+ // 凭据出口优先级(命中即返回;命中的"模式名"会记入 ~/.e10-cli/.e10-data-auth-mode,只存模式、不存凭据):
55
+ // ① e10-login auth --json 正式契约(推荐 e10-login 侧新增;一旦可用自动优先)
56
+ // ② e10-login whoami --json 其 JSON 输出带 cookies 字段时可用
57
+ // ③ e10-login whoami 文本输出解析(当前 e10-login 1.5.0 的实际可用路径)
58
+ // ④ ~/.workbuddy/e10-login-cache.md 明文镜像 —— 最后兜底,命中时打印告警(该文件可能已过期)
59
+ // 环境变量:E10_DATA_AUTH_PROBE=1 强制重新探测优先级;E10_DATA_AUTH_MODE=<模式名> 手动指定;
60
+ // E10_DATA_QUIET=1 抑制告警输出(告警一律走 stderr,不污染 --json 的 stdout)。
61
+ const AUTH_MODE_FILE = path.join(os.homedir(), ".e10-cli", ".e10-data-auth-mode");
62
+
63
+ /** 定位当前 profile 的 auth 文件(仅用于"是否已登录"的判断与报错提示,本 CLI 不读其内容做解密) */
64
+ function authFilePath() {
65
+ const home = os.homedir();
66
+ let profile = "default";
67
+ try {
68
+ const activeFile = path.join(home, ".e10-cli", "profile");
69
+ if (fs.existsSync(activeFile)) { const n = fs.readFileSync(activeFile, "utf-8").trim(); if (n) profile = n; }
70
+ } catch {}
71
+ return { profile, file: path.join(home, ".e10-cli", "profiles", profile, "auth") };
72
+ }
73
+
74
+ let lastLoginDiag = "";
75
+ /** 调用 e10-login(凭据的唯一来源);失败返回 null,并把诊断信息留在 lastLoginDiag */
76
+ function runLoginCli(args) {
77
+ try {
78
+ const r = spawnSync("e10-login", args, { shell: true, encoding: "utf-8", timeout: 20000, windowsHide: true });
79
+ const out = (r.stdout || "").trim();
80
+ if (r.error || r.status !== 0) {
81
+ lastLoginDiag = ((r.stderr || "") + out).trim().split(/\r?\n/).slice(0, 2).join(" / ") || (r.error ? String(r.error.message) : "exit=" + r.status);
82
+ return null;
83
+ }
84
+ return out || null;
85
+ } catch (e) { lastLoginDiag = String((e && e.message) || e); return null; }
86
+ }
87
+
88
+ /** 归一化 + 校验(缺 baseUrl / eteamsid / cookie 一律视为该出口不可用,继续下一个出口) */
89
+ function normalizeAuth(o) {
90
+ if (!o) return null;
91
+ const baseUrl = String(o.baseUrl || "").trim().replace(/\/+$/, "");
92
+ const eteamsid = String(o.eteamsid || "").trim();
93
+ const cookie = String(o.cookie || "").trim();
94
+ if (!baseUrl || !eteamsid || !cookie) return null;
95
+ if (/^\((未知|空)\)$/.test(eteamsid) || /^\((未知|空)\)$/.test(cookie)) return null;
96
+ return { baseUrl, eteamsid, cookie, userId: String(o.userId || "").trim(), tenantKey: String(o.tenantKey || "").trim(), source: o.source };
97
+ }
98
+
99
+ /** ① e10-login auth --json(正式契约:{v,baseUrl,eteamsId,cookie,userAgent,userId,tenantKey,agentType,profile}) */
100
+ function providerAuthJson() {
101
+ const out = runLoginCli(["auth", "--json"]);
102
+ if (!out) return null;
103
+ let j; try { j = JSON.parse(out); } catch { return null; }
104
+ if (!j || typeof j !== "object" || Array.isArray(j)) return null;
105
+ return normalizeAuth({
106
+ baseUrl: j.baseUrl || j.base_url,
107
+ eteamsid: j.eteamsId || j.eteamsid,
108
+ cookie: typeof j.cookie === "string" ? j.cookie : "",
109
+ userId: j.userId || j.employeeId,
110
+ tenantKey: j.tenantKey,
111
+ source: "e10-login auth --json",
112
+ });
113
+ }
114
+
115
+ /** ② e10-login whoami --json(需其 JSON 带 cookies 字符串;当前 1.5.0 未带 → 自动落到 ③) */
116
+ function providerWhoamiJson() {
117
+ const out = runLoginCli(["whoami", "--json"]);
118
+ if (!out) return null;
119
+ let j; try { j = JSON.parse(out); } catch { return null; }
120
+ if (!j || typeof j !== "object" || Array.isArray(j)) return null;
121
+ const cookie = typeof j.cookies === "string" ? j.cookies : (typeof j.cookie === "string" ? j.cookie : "");
122
+ return normalizeAuth({
123
+ baseUrl: j.baseUrl,
124
+ eteamsid: j.eteamsId || j.eteamsid,
125
+ cookie,
126
+ userId: j.employeeId || j.userId,
127
+ tenantKey: j.tenantKey,
128
+ source: "e10-login whoami --json",
129
+ });
130
+ }
131
+
132
+ /** ③ e10-login whoami(文本输出解析:employeeId / tenantKey / baseUrl / ETEAMSID / cookies / User-Agent) */
133
+ function providerWhoamiText() {
134
+ const out = runLoginCli(["whoami"]);
135
+ if (!out) return null;
136
+ const m = {};
137
+ for (const line of out.split(/\r?\n/)) {
138
+ const x = line.match(/^\s*([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.+?)\s*$/);
139
+ if (x) m[x[1].toLowerCase()] = x[2];
140
+ }
141
+ return normalizeAuth({
142
+ baseUrl: m["baseurl"],
143
+ eteamsid: m["eteamsid"],
144
+ cookie: m["cookies"],
145
+ userId: m["employeeid"] || m["userid"],
146
+ tenantKey: m["tenantkey"],
147
+ source: "e10-login whoami(文本)",
148
+ });
149
+ }
150
+
151
+ /** ④ 明文镜像(最后兜底:不涉及任何解密,但可能已过期 → 命中时告警) */
152
+ function providerCacheMd() {
153
+ const p = path.join(os.homedir(), ".workbuddy", "e10-login-cache.md");
154
+ if (!fs.existsSync(p)) return null;
155
+ const txt = fs.readFileSync(p, "utf-8");
156
+ const get = (key) => {
157
+ const mm = txt.match(new RegExp("\\*\\*" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\*\\*\\s*[::]\\s*`?([^`\\n]+)`?"));
158
+ return mm ? mm[1].trim() : "";
159
+ };
160
+ const eteamsid = get("ETEAMSID");
161
+ let cookie = get("完整 Cookie 串");
162
+ if (!cookie && eteamsid) cookie = "langType=zh_CN; ETEAMSID=" + eteamsid + "; agentType=workbuddy; isAgent=true";
163
+ return normalizeAuth({
164
+ baseUrl: get("baseUrl"), eteamsid, cookie,
165
+ userId: get("userId") || get("employeeId"), tenantKey: get("tenantKey"),
166
+ source: "明文镜像 ~/.workbuddy/e10-login-cache.md(可能已过期)",
167
+ });
168
+ }
169
+
170
+ const AUTH_PROVIDERS = [
171
+ ["auth-json", providerAuthJson],
172
+ ["whoami-json", providerWhoamiJson],
173
+ ["whoami-text", providerWhoamiText],
174
+ ["cache-md", providerCacheMd],
175
+ ];
176
+ /** 兜底出口不进记忆:否则 e10-login 偶发不可用一次,就会把"用镜像"永久设为首选、次次告警 */
177
+ const AUTH_FALLBACK_PROVIDERS = new Set(["cache-md"]);
178
+
179
+ function readAuthModeMemo() {
180
+ try {
181
+ const s = fs.readFileSync(AUTH_MODE_FILE, "utf-8").trim();
182
+ return AUTH_PROVIDERS.some(([n]) => n === s) ? s : null;
183
+ } catch { return null; }
184
+ }
185
+ function writeAuthModeMemo(name) {
186
+ try { fs.mkdirSync(path.dirname(AUTH_MODE_FILE), { recursive: true }); fs.writeFileSync(AUTH_MODE_FILE, name, "utf-8"); } catch {}
187
+ }
188
+
189
+ /** 凭据取自 e10-login 时,顺手把明文镜像刷新到最新(保持镜像可用;镜像不参与正常取数) */
190
+ function syncCacheMd(auth) {
191
+ try {
192
+ const p = path.join(os.homedir(), ".workbuddy", "e10-login-cache.md");
193
+ if (!fs.existsSync(p) || !/🔐 登录信息缓存/.test(fs.readFileSync(p, "utf-8"))) return; // 非标准缓存文件不动它
194
+ const d = new Date();
195
+ const ts = d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0") + " " + String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
196
+ let txt = fs.readFileSync(p, "utf-8");
197
+ let changed = false;
198
+ const rep = (key, val) => {
199
+ const re = new RegExp("(\\*\\*" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\*\\*\\s*[::]\\s*`?)([^`\\n]*)(`?)");
200
+ const n = txt.replace(re, (m, a, b, c) => { if (b === val && c === "`") return m; changed = true; return a + val + (c || "`"); });
201
+ txt = n;
202
+ };
203
+ rep("baseUrl", auth.baseUrl); rep("ETEAMSID", auth.eteamsid); rep("完整 Cookie 串", auth.cookie); rep("userId", auth.userId || ""); rep("tenantKey", auth.tenantKey || "");
204
+ rep("缓存记录时间", ts + "(e10-data 自动同步,凭据来源:" + (auth.source || "") + ")");
205
+ if (changed) fs.writeFileSync(p, txt, "utf-8");
206
+ } catch {}
207
+ }
208
+
209
+ /** 取凭据:按优先级依次尝试各出口;命中的模式会被记住(下次优先尝试它,省一次进程调用) */
210
+ function loadAuth() {
211
+ const { profile, file } = authFilePath();
212
+ const override = (process.env.E10_DATA_AUTH_MODE || "").trim();
213
+ const memo = override || (process.env.E10_DATA_AUTH_PROBE ? null : readAuthModeMemo());
214
+ const order = memo
215
+ ? AUTH_PROVIDERS.filter(([n]) => n === memo).concat(AUTH_PROVIDERS.filter(([n]) => n !== memo))
216
+ : AUTH_PROVIDERS;
217
+
218
+ const diag = [];
219
+ for (const [name, fn] of order) {
220
+ let got = null;
221
+ lastLoginDiag = "";
222
+ try { got = fn(); } catch (e) { diag.push(name + ": " + ((e && e.message) || e)); }
223
+ if (got) {
224
+ if (name !== memo && !AUTH_FALLBACK_PROVIDERS.has(name)) writeAuthModeMemo(name);
225
+ got.profile = profile;
226
+ if (name === "cache-md") {
227
+ if (!process.env.E10_DATA_QUIET) {
228
+ console.error("[e10-data][warn] 凭据来源=" + got.source + ";正常应由 e10-login 提供凭据,请检查 `e10-login whoami` 是否能输出 cookies。");
229
+ }
230
+ } else {
231
+ syncCacheMd(got); // 凭据来自 e10-login → 顺便把镜像刷新到最新
232
+ }
233
+ return got;
234
+ }
235
+ if (lastLoginDiag) diag.push(name + ": " + lastLoginDiag);
236
+ }
237
+
238
+ throw new Error(
239
+ "未取得登录凭据(解密由 e10-login 负责,本 CLI 不参与)。请依次检查:\n" +
240
+ " ① e10-login 是否可用:`e10-login --version`(缺失 → `npm i -g e10-login`);`e10-login whoami` 应能输出 cookies\n" +
241
+ " ② 是否已登录 / 登录态是否失效:`e10-login oidc` 重新登录(当前 profile: " + profile + ")\n" +
242
+ " ③ auth 文件:" + (fs.existsSync(file) ? "存在" : "不存在") + "(" + file + ")—— 其加解密由 e10-login 负责\n" +
243
+ (diag.length ? " 探测诊断:" + diag.join(" | ") + "\n" : "")
244
+ );
245
+ }
246
+ const auth = loadAuth();
247
+ const BASE = auth.baseUrl.replace(/\/+$/, "");
248
+ const USER = { id: auth.userId || "", tenantKey: auth.tenantKey || "" };
249
+
250
+ // ---------------- HTTP ----------------
251
+ function checkAuthFail(resp, text, j) {
252
+ if (resp.status === 401 || (j && String(j.code) === "-1" && /token|登录|失效/i.test(String(j.msg || "")))) {
253
+ const from = (auth && auth.source) || "未知";
254
+ const hint = /镜像/.test(from)
255
+ ? "当前用的是明文镜像兜底凭据(可能已过期),请先确认 `e10-login whoami` 能正常导出 cookies,或执行 `e10-login oidc` 重新登录"
256
+ : "凭据由 e10-login 提供,请执行 `e10-login oidc` 重新登录";
257
+ throw new Error("登录态失效(" + (j.msg || resp.status) + "):凭据来源=" + from + ";" + hint);
258
+ }
259
+ }
260
+ async function postJson(endpoint, bodyObj, extraHeaders = {}) {
261
+ const resp = await fetch(BASE + endpoint, {
262
+ method: "POST",
263
+ headers: {
264
+ "Content-Type": "application/json;charset=UTF-8",
265
+ "accept": "application/json, text/plain, */*",
266
+ "langType": "zh_CN",
267
+ "cookie": auth.cookie,
268
+ "eteamsid": auth.eteamsid,
269
+ "ebbusinessid": EB_BIZ_ID,
270
+ "user-agent": UA,
271
+ "timezoneoffset": "-480",
272
+ ...extraHeaders,
273
+ },
274
+ body: JSON.stringify(bodyObj),
275
+ });
276
+ const text = await resp.text();
277
+ let j; try { j = parseJsonSafe(text); } catch { throw new Error("HTTP " + resp.status + " 非JSON响应: " + text.slice(0, 300)); }
278
+ checkAuthFail(resp, text, j);
279
+ return j;
280
+ }
281
+ async function getUrl(url) {
282
+ const resp = await fetch(BASE + url, {
283
+ method: "GET",
284
+ headers: {
285
+ "accept": "application/json, text/plain, */*",
286
+ "langType": "zh_CN",
287
+ "cookie": auth.cookie,
288
+ "eteamsid": auth.eteamsid,
289
+ "ebbusinessid": EB_BIZ_ID,
290
+ "user-agent": UA,
291
+ },
292
+ });
293
+ const text = await resp.text();
294
+ let j; try { j = parseJsonSafe(text); } catch { throw new Error("HTTP " + resp.status + " 非JSON响应: " + text.slice(0, 300)); }
295
+ checkAuthFail(resp, text, j);
296
+ return j;
297
+ }
298
+ async function postMultipart(endpoint, formObj) {
299
+ const fd = new FormData();
300
+ for (const [k, v] of Object.entries(formObj)) fd.append(k, typeof v === "string" ? v : JSON.stringify(v));
301
+ const resp = await fetch(BASE + endpoint, {
302
+ method: "POST",
303
+ headers: {
304
+ "accept": "application/json, text/plain, */*",
305
+ "langType": "zh_CN",
306
+ "cookie": auth.cookie,
307
+ "eteamsid": auth.eteamsid,
308
+ "ebbusinessid": EB_BIZ_ID,
309
+ "user-agent": UA,
310
+ "timezoneoffset": "-480",
311
+ "referer": BASE + "/sp/ebddesigner/1282331905881874436",
312
+ },
313
+ body: fd,
314
+ });
315
+ const text = await resp.text();
316
+ let j; try { j = parseJsonSafe(text); } catch { throw new Error("HTTP " + resp.status + " 非JSON响应: " + text.slice(0, 300)); }
317
+ checkAuthFail(resp, text, j);
318
+ return j;
319
+ }
320
+
321
+ // ---------------- 参数解析 ----------------
322
+ function parseArgs(argv) {
323
+ const args = {};
324
+ const pos = [];
325
+ for (let i = 0; i < argv.length; i++) {
326
+ const a = argv[i];
327
+ if (a.startsWith("--")) {
328
+ const key = a.slice(2);
329
+ const v = argv[i + 1];
330
+ if (v === undefined || v.startsWith("--")) { args[key] = true; }
331
+ else { args[key] = v; i++; }
332
+ } else pos.push(a);
333
+ }
334
+ return { args, pos };
335
+ }
336
+ // ⚠️ 大整数精度保护(2026-09-03):E10 主键/ID 多为 19 位大整数,超过 JS 安全整数 2^53-1
337
+ // (9007199254740991)。JSON.parse 会将其舍入为近似 double(如 1297901170990161977 → ...1920),
338
+ // 导致 DSL 条件查不到数据(静默空结果)或返回 ID 失真。此处把 JSON 中 >2^53 的裸数字字面量
339
+ // 加引号转字符串(后端实测接受字符串形式做 = / IN / BETWEEN 数值比较),请求与响应两侧统一保护。
340
+ function protectBigInts(raw) {
341
+ // 逐字符扫描:跟踪 JSON 字符串状态(含转义),仅在引号外的结构层把 >2^53 的裸整数加引号,
342
+ // 避免误伤字符串内容(如 avatar JSON、文档正文里含 ",大数字" 片段)。
343
+ let out = "", inStr = false, esc = false;
344
+ for (let i = 0; i < raw.length; i++) {
345
+ const ch = raw[i];
346
+ if (inStr) {
347
+ out += ch;
348
+ if (esc) esc = false;
349
+ else if (ch === "\\") esc = true;
350
+ else if (ch === '"') inStr = false;
351
+ continue;
352
+ }
353
+ if (ch === '"') { inStr = true; out += ch; continue; }
354
+ if (ch === "-" || (ch >= "0" && ch <= "9")) {
355
+ let j = i + 1;
356
+ while (j < raw.length && raw[j] >= "0" && raw[j] <= "9") j++; // 纯整数 token(无小数点/指数)
357
+ let k = i - 1;
358
+ while (k >= 0 && /\s/.test(raw[k])) k--;
359
+ const prev = k < 0 ? "" : raw[k];
360
+ let n = j;
361
+ while (n < raw.length && /\s/.test(raw[n])) n++;
362
+ const next = n < raw.length ? raw[n] : "";
363
+ const token = raw.slice(i, j);
364
+ if ((prev === ":" || prev === "," || prev === "[") && (next === "," || next === "}" || next === "]")) {
365
+ try {
366
+ const bi = BigInt(token);
367
+ if (bi > 9007199254740991n || bi < -9007199254740991n) { out += '"' + token + '"'; i = j - 1; continue; }
368
+ } catch { /* 非纯整数不转换 */ }
369
+ }
370
+ out += raw.slice(i, j);
371
+ i = j - 1;
372
+ continue;
373
+ }
374
+ out += ch;
375
+ }
376
+ return out;
377
+ }
378
+ function parseJsonSafe(raw, label) {
379
+ try { return JSON.parse(protectBigInts(raw)); }
380
+ catch (e) { throw new Error((label || "JSON") + " 解析失败: " + e.message); }
381
+ }
382
+ function readJsonArg(val, label) {
383
+ let raw = val;
384
+ if (val.startsWith("@")) raw = fs.readFileSync(val.slice(1), "utf-8");
385
+ return parseJsonSafe(raw, label);
386
+ }
387
+ function need(arg, args) { if (!args[arg]) throw new Error("缺少必填参数 --" + arg); return args[arg]; }
388
+ // 文件/JSON 参数取值:兼容 --flag <v> 与位置参数 <v>(如 conf add-file @items.json / --add-file @items.json)
389
+ function flagOrPos(args, flag, keys) {
390
+ if (args[flag] !== undefined) return args[flag];
391
+ return keys && keys.length ? keys[0] : undefined;
392
+ }
393
+
394
+ // ---------------- 展示 ----------------
395
+ function fmt(v) {
396
+ if (v === null || v === undefined) return "";
397
+ if (typeof v === "object") { const s = JSON.stringify(v); return s.length > 80 ? s.slice(0, 80) + "…" : s; }
398
+ return String(v);
399
+ }
400
+ function printHuman(title, rows, cols) {
401
+ if (title) console.log("== " + title + " ==");
402
+ if (!rows || !rows.length) { console.log("(空)"); return; }
403
+ if (cols) {
404
+ console.log(cols.join("\t"));
405
+ for (const r of rows) console.log(cols.map((c) => fmt(r[c])).join("\t"));
406
+ } else {
407
+ for (const r of rows.slice(0, 100)) console.log(JSON.stringify(r));
408
+ if (rows.length > 100) console.log("... 共 " + rows.length + " 行(--json 取全量)");
409
+ }
410
+ }
411
+
412
+ // ---------------- query:DSL 查询 ----------------
413
+ async function cmdQuery(args, json) {
414
+ const conn = need("conn", args), model = need("model", args), schema = need("schema", args);
415
+ const tableId = args["table-id"] || "";
416
+ const dslVal = need("dsl", args);
417
+ const dslQuery = dslVal.startsWith("@") ? readJsonArg(dslVal, "--dsl") : parseJsonSafe(dslVal, "--dsl");
418
+ const body = { connId: conn, model, schema, tableId, dslQuery };
419
+ if (args.debug !== undefined) { // --debug true|false,与 conn/model 同层级
420
+ const dbg = String(args.debug).toLowerCase();
421
+ if (dbg === "true" || dbg === "1") body.debug = true;
422
+ else if (dbg === "false" || dbg === "0") body.debug = false;
423
+ else throw new Error("--debug 值必须为 true 或 false(当前: " + args.debug + ")");
424
+ }
425
+ const r = await postJson("/api/dw/dsl/executeQueryDsl", body);
426
+ if (json) return r;
427
+ if (String(r.code) !== "200" && r.code !== undefined && String(r.code) !== "0") {
428
+ console.log("code=" + r.code + " msg=" + (r.msg || ""));
429
+ if (/没有权限|无权限|权限不足|未授权|无权访问|无查询权限/i.test(String(r.msg || ""))) {
430
+ throw new Error("当前用户对该数据无查询权限,请检查 ETEAMSID 是否有效或联系管理员开通");
431
+ }
432
+ return;
433
+ }
434
+ const d = r.data;
435
+ // debug=true 时 data 为 { sql, records };否则为行数组
436
+ const rows = Array.isArray(d) ? d : (d && Array.isArray(d.records) ? d.records : []);
437
+ if (d && !Array.isArray(d) && d.sql) console.log("SQL: " + d.sql);
438
+ if (!rows.length) { console.log("(空:0 行)"); return; }
439
+ printHuman("结果 " + rows.length + " 行", rows);
440
+ }
441
+
442
+ // ---------------- ds:数据源 ----------------
443
+ function dyn() {
444
+ return { eteamsId: auth.eteamsid, userId: auth.userId || "", tenantKey: auth.tenantKey || "", langId: 7, formatDate: "yyyy-MM-dd HH:mm:ss", isPublic: false };
445
+ }
446
+ function baseDto(tableId, groupId, pageNo, pageSize, queryType) {
447
+ return { tableId: String(tableId), groupId, pageNo, pageSize, queryType, containDetailTable: false, needFormat: true, dynamicParams: dyn() };
448
+ }
449
+ // 条件树(conditionTrees)协议:根/组节点 { key, relationship("1"=AND|"0"=OR), isParent:true, parentNode, conditionList:[...] }
450
+ // 叶子节点 { key, relationship:"1", isParent:false, parentNode, conditionId, conditionType, compareType, conditionValue, ... }
451
+ // v0.1.8 起 parseWhere 支持递归树(OR);向后兼容:数组=AND 根、叶子键 fieldId 优先(conditionId 兼容兜底)
452
+ function normRel(r) { return (r === "0" || r === 0 || r === false) ? "0" : "1"; }
453
+ function isCondGroup(c) { return !!c && Array.isArray(c.conditionList) && c.conditionList.length > 0; }
454
+ function buildCondNode(c, key, parentKey) {
455
+ if (isCondGroup(c)) {
456
+ return {
457
+ key, relationship: normRel(c.relationship), isParent: true, parentNode: parentKey,
458
+ mainField: c.mainField !== false,
459
+ conditionList: c.conditionList.map((k, i) => buildCondNode(k, key + "_" + i, key)),
460
+ };
461
+ }
462
+ const fid = (c && (c.fieldId || c.conditionId)) || "";
463
+ if (!fid) throw new Error("--where 条件缺少字段:叶子须提供 fieldId(或 conditionId),条件组须提供非空 conditionList");
464
+ return {
465
+ key, relationship: "1", isParent: false, parentNode: parentKey,
466
+ objId: c.objId || "", conditionId: fid, fieldName: c.fieldName || fid,
467
+ conditionType: c.conditionType || "", compareType: c.compareType, conditionValue: c.conditionValue ?? "",
468
+ fieldValue: c.fieldValue || "", mainField: c.mainField !== false, ignoreCaseMark: c.ignoreCaseMark || false,
469
+ };
470
+ }
471
+ function parseWhere(val) {
472
+ if (!val) return null;
473
+ const raw = readJsonArg(val, "--where");
474
+ if (!raw) return null;
475
+ const ts = Date.now(), rootKey = "root_" + ts;
476
+ if (Array.isArray(raw)) { // 数组 → AND 根(元素可为叶子或内嵌条件组,组自带 relationship)
477
+ const kids = [];
478
+ raw.forEach((c, i) => { if (c && typeof c === "object") kids.push(buildCondNode(c, rootKey + "_" + i, rootKey)); });
479
+ if (!kids.length) return null;
480
+ return { key: rootKey, relationship: "1", isParent: true, parentNode: "", mainField: true, conditionList: kids };
481
+ }
482
+ if (typeof raw !== "object") throw new Error("--where 须为条件数组或条件树对象");
483
+ // 对象形态 = 完整条件树({relationship, conditionList:[...]},缺省 AND);无 conditionList 视为无条件
484
+ if (!Array.isArray(raw.conditionList) || !raw.conditionList.length) return null;
485
+ return buildCondNode(raw, rootKey, "");
486
+ }
487
+ function parseStat(val) {
488
+ const arr = readJsonArg(val, "--stat");
489
+ const list = Array.isArray(arr) ? arr : [arr];
490
+ return list.map((s) => ({ fieldId: s.fieldId, fieldName: s.fieldId, objId: "", mainField: true, aggregator: s.aggregator || "SUM", ...(s.fieldAlias ? { fieldAlias: s.fieldAlias } : {}) }));
491
+ }
492
+ function parseOrder(val) {
493
+ if (!val) return [];
494
+ return val.split(",").map((s) => {
495
+ const [f, dir] = s.trim().split(/\s+/);
496
+ return { fieldId: f, fieldName: f, objId: "", isAsc: (dir || "asc").toLowerCase() !== "desc", mainField: true };
497
+ });
498
+ }
499
+ // ---------------- ds appId 自动解析(v0.1.13)----------------
500
+ // 背景:ChatBI 数据源取数接口(getListDatas / getChartDatas / getStatDatas / getCountData)
501
+ // 对部分 EB 应用(独立部署)的数据集必须携带 queryDto.appId,否则后端一律返回 code=500。
502
+ // 解析顺序(无缓存,每次实时查):--app-id <id> 显式 > chat_bi_eb_config_map(dw 权限配置表,按 obj_id,主来源)
503
+ // > ebdf_obj(ec_edcapp,按 id,兜底) > 不带 appId 发送(兼容旧行为)。
504
+ // 关闭自动解析:--no-app-id。
505
+ const APPID_SOURCES = [
506
+ { confKey: "CONN_DW", model: "dw", schema: "dw", table: "chat_bi_eb_config_map", idField: "obj_id" },
507
+ { confKey: "CONN_EC_EDCAPP", model: "ebuilder", schema: "ec_edcapp", table: "ebdf_obj", idField: "id" },
508
+ ];
509
+ function dsFieldsForm(gid, tid, st) {
510
+ return { groupId: gid, sourceId: tid, sourceType: st, detailFieldsGroup: "true", enableMerge: "true", needTableName: "true", params: "[]" };
511
+ }
512
+ async function appIdByDsl(src, tid) {
513
+ const conn = await confGetData("conn", src.confKey, true);
514
+ const connId = (conn && conn.confValue) ? String(conn.confValue) : "";
515
+ if (!connId) return "";
516
+ const dslQuery = {
517
+ select: { columns: [{ field: "t1.app_id", alias: "app_id" }] },
518
+ from: { table: src.table, alias: "t1" },
519
+ where: { logic: "AND", conditions: [{ field: "t1." + src.idField, op: "=", value: String(tid) }, { field: "t1.delete_type", op: "=", value: 0 }] },
520
+ limit: { size: 1, offset: 0 },
521
+ };
522
+ const r = await postJson("/api/dw/dsl/executeQueryDsl", { connId, model: src.model, schema: src.schema, tableId: "", dslQuery });
523
+ const rows = Array.isArray(r && r.data) ? r.data : [];
524
+ return (rows[0] && rows[0].app_id) ? String(rows[0].app_id) : "";
525
+ }
526
+ async function resolveDsAppId(tid) {
527
+ for (const src of APPID_SOURCES) {
528
+ let appId = "";
529
+ try { appId = await appIdByDsl(src, tid); } catch { appId = ""; }
530
+ if (appId) return appId;
531
+ }
532
+ return "";
533
+ }
534
+ // ds 取数统一入口:--app-id <id> 显式 / --app-id auto 或省略=自动解析 / --no-app-id 跳过
535
+ async function dsDto(args, tid, gid, pageNo, pageSize, queryType) {
536
+ const dto = baseDto(tid, gid, pageNo, pageSize, queryType);
537
+ const explicit = args["app-id"];
538
+ if (typeof explicit === "string" && explicit !== "" && explicit !== "auto") {
539
+ dto.appId = explicit;
540
+ } else if (!args["no-app-id"]) {
541
+ const appId = await resolveDsAppId(tid);
542
+ if (appId) dto.appId = String(appId);
543
+ }
544
+ return dto;
545
+ }
546
+ async function cmdTables(args, json) {
547
+ const gid = need("group", args);
548
+ const form = { name: args.name || "", pageNo: args.page || "1", pageSize: args.size || "100", groupId: gid, sourceType: args["source-type"] || "LOGIC", dataUsage: "" };
549
+ if (typeof args["app-id"] === "string" && args["app-id"] !== "" && args["app-id"] !== "auto") form.appId = args["app-id"];
550
+ const r = await postMultipart("/api/ebuilder/common/ds/listByPage", form);
551
+ if (json) return r;
552
+ const recs = ((r.data || {}).records) || [];
553
+ printHuman("分组表清单 (total=" + ((r.data || {}).total) + ")", recs, ["id", "name", "appId", "tableText", "detailTable", "usage"]);
554
+ }
555
+ async function cmdFields(args, json) {
556
+ const gid = need("group", args), tid = need("table", args);
557
+ const form = dsFieldsForm(gid, tid, args["source-type"] || "LOGIC");
558
+ if (typeof args["app-id"] === "string" && args["app-id"] !== "" && args["app-id"] !== "auto") form.appId = args["app-id"];
559
+ const r = await postMultipart("/api/ebuilder/common/ds/fields", form);
560
+ if (json) return r;
561
+ for (const d of (r.data || [])) {
562
+ console.log("== 表: " + d.name + " (id=" + d.id + ") 字段 " + (d.fields || []).length + " 个 ==");
563
+ for (const f of (d.fields || [])) console.log([f.name, f.text, f.type, f.mainPrimaryKey ? "PK" : ""].filter(Boolean).join("\t"));
564
+ }
565
+ }
566
+ async function cmdList(args, json) {
567
+ const gid = need("group", args), tid = need("table", args), fld = need("fields", args);
568
+ const fields = fld.split(",").map((f) => ({ fieldId: f, fieldName: f, objId: String(tid), mainField: true }));
569
+ const dto = await dsDto(args, tid, gid, parseInt(args.page || "1", 10), parseInt(args.size || "20", 10), "getListData");
570
+ dto.queryFields = fields;
571
+ dto.orderFields = parseOrder(args.order);
572
+ const ct = parseWhere(args.where); if (ct) dto.conditionTrees = ct;
573
+ dto.queryCount = true;
574
+ const r = await postMultipart("/api/dw/newetl/chatBi/ds/getListDatas", { sourceType: args["source-type"] || "LOGIC", queryDto: dto });
575
+ if (json) return r;
576
+ const data = r.data || {};
577
+ console.log("== 数据列表 count=" + data.count + " 行=" + (data.list || []).length + " ==");
578
+ printHuman("", data.list || [], fields.map((f) => f.fieldId));
579
+ }
580
+ async function cmdChart(args, json) {
581
+ const gid = need("group", args), tid = need("table", args), gb = need("group-by", args);
582
+ const dto = await dsDto(args, tid, gid, parseInt(args.page || "1", 10), parseInt(args.size || "100", 10), "getChartData");
583
+ dto.groupFields = gb.split(",").map((f) => ({ fieldId: f, fieldName: f, objId: String(tid), mainField: true }));
584
+ dto.groupStatFields = parseStat(need("stat", args)).map((s) => ({ ...s, objId: String(tid) }));
585
+ const ct = parseWhere(args.where); if (ct) dto.conditionTrees = ct;
586
+ const r = await postMultipart("/api/dw/newetl/chatBi/ds/getChartDatas", { sourceType: args["source-type"] || "LOGIC", queryDto: dto });
587
+ if (json) return r;
588
+ printHuman("分组聚合 rows=" + (r.data || []).length, r.data || []);
589
+ }
590
+ async function cmdStat(args, json) {
591
+ const gid = need("group", args), tid = need("table", args);
592
+ const dto = await dsDto(args, tid, gid, 1, 10, "getStatData");
593
+ dto.statFields = parseStat(need("stat", args)).map((s) => ({ ...s, objId: String(tid) }));
594
+ const ct = parseWhere(args.where); if (ct) dto.conditionTrees = ct;
595
+ const r = await postMultipart("/api/dw/newetl/chatBi/ds/getStatDatas", { sourceType: args["source-type"] || "LOGIC", queryDto: dto });
596
+ if (json) return r;
597
+ const data = r.data || {};
598
+ if (data.fields && data.fields.length) { for (const f of data.fields) console.log(JSON.stringify(f)); }
599
+ else console.log("(无统计结果)");
600
+ }
601
+ async function cmdCount(args, json) {
602
+ const gid = need("group", args), tid = need("table", args);
603
+ const dto = await dsDto(args, tid, gid, 1, 10, "getCountData");
604
+ const ct = parseWhere(args.where); if (ct) dto.conditionTrees = ct;
605
+ const r = await postMultipart("/api/dw/newetl/chatBi/ds/getCountData", { sourceType: args["source-type"] || "LOGIC", queryDto: dto });
606
+ if (json) return r;
607
+ const data = r.data || {};
608
+ console.log("totalCount = " + (data.totalCount ?? "-"));
609
+ }
610
+
611
+ // ---------------- doc:文档查询 ----------------
612
+ function docBaseBody(pageNo, pageSize) {
613
+ return {
614
+ curMenuType: "", limitedFolderIncludeSub: "", limitedFolderIds: "", docAppConsole: "",
615
+ viewType: "DOC_PAGINATION", pageNo, pageSize, morePageId: "", dimension: "ALL", includeBelongFolders: true,
616
+ order: { property: "default", direction: "DESC" }, searchType: "all", conditions: [], customConditions: [],
617
+ userId: auth.userId || "", current: pageNo,
618
+ columnOrders: [{ orderKey: "default", orderType: "" }], getColumn: false, quickSearchType: "normal",
619
+ withFullSearch: false, fetchFullSearchParam: true,
620
+ };
621
+ }
622
+ function buildDocConditions(args) {
623
+ const conds = [];
624
+ if (args.name) conds.push({ id: "name", type: "string", value: args.name });
625
+ if (args.creator) conds.push({ id: "creator", operate: args["creator-neq"] ? "neq" : "eq", type: "array", value: args.creator });
626
+ if (args.author) conds.push({ id: "author", operate: args["author-neq"] ? "neq" : "eq", type: "array", value: args.author });
627
+ if (args["content-type"]) conds.push({ id: "contentType", operate: "eq", type: "array", value: args["content-type"] });
628
+ if (args.time) conds.push({ id: "create_time", type: "date", value: args.time });
629
+ if (args["last-time"]) conds.push({ id: "last_update_time", type: "date", value: args["last-time"] });
630
+ if (args.folder) conds.push({ id: "folder", operate: "eq", type: "folder", value: args.folder });
631
+ if (args["folder-neq"]) conds.push({ id: "folder", operate: "neq", type: "folder", value: args["folder-neq"] });
632
+ return conds;
633
+ }
634
+ async function cmdDocCount(args, json) {
635
+ const body = docBaseBody(1, parseInt(args.size || "20", 10));
636
+ body.conditions = buildDocConditions(args);
637
+ const r = await postJson("/api/doc/documents/documentListCount", body);
638
+ if (json) return r;
639
+ const data = r.data || {};
640
+ console.log("total = " + (data.total ?? "-"));
641
+ }
642
+ async function cmdDocList(args, json) {
643
+ const pageSize = parseInt(args.size || "100", 10);
644
+ const body = docBaseBody(1, pageSize);
645
+ body.conditions = buildDocConditions(args);
646
+ const r1 = await postJson("/api/doc/documents/documentListCount", body);
647
+ if (json) return r1;
648
+ const total = (r1.data || {}).total || 0;
649
+ if (!total) { console.log("total = 0(未查询到符合条件的文档)"); return; }
650
+ const totalPages = Math.ceil(total / pageSize);
651
+ const maxPages = args.all ? totalPages : Math.min(parseInt(args.page || "1", 10), totalPages);
652
+ const startPage = args.all ? 1 : parseInt(args.page || "1", 10);
653
+ const allRows = [];
654
+ for (let pno = startPage; pno <= maxPages; pno++) {
655
+ const b = docBaseBody(pno, pageSize);
656
+ b.conditions = buildDocConditions(args);
657
+ const rr = await postJson("/api/doc/documents/documentList", b);
658
+ const rows = ((rr.data || {}).data) || [];
659
+ for (const row of rows) if (!allRows.some((x) => x.id === row.id)) allRows.push(row);
660
+ if (!args.all && pno === maxPages) break;
661
+ }
662
+ console.log("== 文档列表 total=" + total + " 本批=" + allRows.length + " 行 ==");
663
+ printHuman("", allRows, ["id", "name", "docCreatorName", "createTime", "folder"]);
664
+ }
665
+
666
+ // ---------------- noun:词库 ----------------
667
+ async function cmdNoun(args, json) {
668
+ const skill = need("skill", args);
669
+ const q = new URLSearchParams({ skillKey: skill });
670
+ if (args["source-type"]) q.append("sourceType", args["source-type"]);
671
+ if (args["source-group"]) q.append("sourceGroupId", args["source-group"]);
672
+ if (args["source-table"]) q.append("sourceTableId", args["source-table"]);
673
+ const r = await getUrl("/api/dw/newetl/chatBi/skill/getChatDocsBySkillKey?" + q.toString());
674
+ if (json) return r;
675
+ const data = r.data;
676
+ if (!data || !data.nouns) {
677
+ console.log("(该 skill 词库不存在或为空) msg=" + (r.msg || ""));
678
+ return;
679
+ }
680
+ console.log("== 名词解释 " + data.nouns.length + " 条 (skill=" + skill + ") ==");
681
+ for (const n of data.nouns) {
682
+ console.log("【" + (n.keyword || "") + "】" + (n.synonyms ? " 同义:" + n.synonyms : ""));
683
+ if (n.wordExplain) console.log(" " + String(n.wordExplain).replace(/\n/g, " "));
684
+ }
685
+ }
686
+
687
+ // ---------------- field:关联字段翻译 ----------------
688
+ function customParamFromArgs(args) {
689
+ if (args["form-param"]) return { formParam: args["form-param"] };
690
+ if (args["eb-browser-param"]) return { ebBrowserForEditParam: readJsonArg(args["eb-browser-param"], "--eb-browser-param") };
691
+ if (args["property-type"]) return { propertyType: args["property-type"] };
692
+ return {};
693
+ }
694
+ function extractRemoteRows(r, browserType) {
695
+ const remote = r.remote || {};
696
+ const keys = Object.keys(remote);
697
+ const out = [];
698
+ for (const k of keys) {
699
+ if (Array.isArray(remote[k])) for (const it of remote[k]) out.push({ key: k, id: it.id, content: it.content ?? it.name ?? "" });
700
+ }
701
+ return out;
702
+ }
703
+ async function cmdFieldId2name(args, json) {
704
+ const module = need("module", args), type = need("type", args), idsVal = need("ids", args);
705
+ const dataIds = idsVal.split(",").map((s) => s.trim()).filter(Boolean);
706
+ const body = [{ module, browserType: type, dataIds, user: USER, customParam: customParamFromArgs(args) }];
707
+ const r = await postJson("/api/bcw/test/testRemoteBrowser", body);
708
+ if (json) return r;
709
+ const rows = extractRemoteRows(r, type);
710
+ if (!rows.length) {
711
+ const nf = (r.notFound || {});
712
+ const list = nf && nf[module + "_" + type] && nf[module + "_" + type].ids ? nf[module + "_" + type].ids : [];
713
+ console.log("(未翻译到名称) notFound ids: " + (list.join(",") || "全部"));
714
+ return;
715
+ }
716
+ printHuman("id→name (" + rows.length + "/" + dataIds.length + ")", rows, ["id", "content"]);
717
+ }
718
+ async function cmdFieldName2id(args, json) {
719
+ const module = need("module", args), type = need("type", args), name = need("name", args);
720
+ const body = { newly: 1, searchValue: name, ...customParamFromArgs(args) };
721
+ const url = "/api/" + module + "/common/browser/complete/" + type;
722
+ const r = await postJson(url, body);
723
+ if (json) return r;
724
+ const rows = ((r.data || {}).data) || [];
725
+ if (!rows.length) { console.log("(未匹配到名称对应 ID)"); return; }
726
+ printHuman("name→id 候选 " + rows.length + " 条", rows.map((x) => ({ id: x.id, name: x.name ?? x.title ?? "" })), ["id", "name"]);
727
+ }
728
+
729
+ // ---------------- conf:环境参数配置中心(chatBi config map) ----------------
730
+ // 覆盖 config-resolve 原 resolve_config.py 的 URL 直连调用:get/batch/batch-file/group/group-file/resolve/resolve-file
731
+ // 与管理 add/add-file/update/delete。认证复用进程内登录缓存(完整 Cookie + eteamsid,同 query/ds/doc),
732
+ // 无内置令牌;baseUrl 取登录缓存(与 executeQueryDsl 同域)。confType 默认由 key 前缀推导
733
+ // (CONN_→conn / DS_→ds / WF_→wf / API_→api),可用 --type 显式指定任意大类(如 data-source-query / system-config)。
734
+ //
735
+ // v0.1.9 confGroup(配置分组,可选):
736
+ // 后端在 confType=api 时支持按分组二次分层(如 api/workflow、api/hr),定位维度变为
737
+ // 「confType + confGroup + confKey」。本 CLI 统一用 --group 传分组:
738
+ // · confType 为 api(或显式传了 --group)时,新增/更新/删除/单查 才下发 confGroup 字段,
739
+ // 其他 confType 一律不带该字段,避免污染既有(无分组列)的配置类型。
740
+ // · conf group <confType> <confGroup> 走 batchGetChatBiConfigMapByTypeAndGroup 按类型+分组批量取回。
741
+ const CONF_PREFIX_TYPE = [["CONN_", "conn"], ["DS_", "ds"], ["WF_", "wf"], ["API_", "api"]];
742
+ const CONF_GET_PATH = "/api/dw/newetl/chatBi/getChatBiConfigMap";
743
+ const CONF_BATCH_PATH = "/api/dw/newetl/chatBi/batchGetChatBiConfigMap";
744
+ const CONF_BATCH_GROUP_PATH = "/api/dw/newetl/chatBi/batchGetChatBiConfigMapByTypeAndGroup";
745
+ const CONF_ADD_PATH = "/api/dw/newetl/chatBi/batchAddChatBiConfigMap";
746
+ const CONF_UPDATE_PATH = "/api/dw/newetl/chatBi/updateChatBiConfigMap";
747
+ const CONF_DELETE_PATH = "/api/dw/newetl/chatBi/deleteChatBiConfigMap";
748
+ const CONF_TOKEN_RE = /#\{([A-Z][A-Z0-9_]{2,})\}/g;
749
+ function confKeyType(key, explicitType) {
750
+ if (explicitType) return explicitType;
751
+ for (const [p, t] of CONF_PREFIX_TYPE) if (key.startsWith(p)) return t;
752
+ throw new Error("无法由 confKey 推导 confType(需 CONN_/DS_/WF_/API_ 前缀或显式 --type): " + key);
753
+ }
754
+ // confGroup 仅在 confType=api 或调用方显式指定时下发(见上方说明)
755
+ function confUseGroup(ctype, group) {
756
+ return ctype === "api" || (group !== undefined && group !== null && group !== "");
757
+ }
758
+ function confPutGroup(body, ctype, group) {
759
+ if (confUseGroup(ctype, group)) body.confGroup = group === undefined || group === null ? "" : String(group);
760
+ return body;
761
+ }
762
+ function confGroupSuffix(ctype, group) { // GET/DELETE 用的 query 后缀
763
+ return confUseGroup(ctype, group) ? "&confGroup=" + encodeURIComponent(group === undefined || group === null ? "" : String(group)) : "";
764
+ }
765
+ function confItemId(it) { // 同 confKey 可分布在多个 confGroup,带分组时用「组|键」做映射键,避免互相覆盖
766
+ const g = it && it.confGroup;
767
+ return g ? g + "|" + (it.confKey || "") : ((it && it.confKey) || "");
768
+ }
769
+ function confLookup(map, it) { // 兼容后端回包是否带 confGroup:先按「组|键」取,再退化到 confKey
770
+ return map[confItemId(it)] || map[it.confKey];
771
+ }
772
+ function confFail(j, url) {
773
+ const m = (j && (j.msg || (j.actionMsg && j.actionMsg.message))) || "";
774
+ throw new Error("配置服务请求失败 [" + url + "]" + (m ? ": " + m : ""));
775
+ }
776
+ async function confRequest(method, url, bodyObj) {
777
+ const resp = await fetch(BASE + url, {
778
+ method,
779
+ headers: {
780
+ "accept": "application/json, text/plain, */*",
781
+ "langType": "zh_CN",
782
+ "cookie": auth.cookie,
783
+ "eteamsid": auth.eteamsid,
784
+ "ebbusinessid": EB_BIZ_ID,
785
+ "user-agent": UA,
786
+ ...(bodyObj !== undefined ? { "Content-Type": "application/json;charset=UTF-8" } : {}),
787
+ },
788
+ ...(bodyObj !== undefined ? { body: JSON.stringify(bodyObj) } : {}),
789
+ });
790
+ const text = await resp.text();
791
+ let j; try { j = parseJsonSafe(text); } catch { throw new Error("HTTP " + resp.status + " 非JSON响应: " + text.slice(0, 300)); }
792
+ checkAuthFail(resp, text, j);
793
+ return j;
794
+ }
795
+ async function confGetData(ctype, key, silent, group) {
796
+ const url = CONF_GET_PATH + "?confType=" + encodeURIComponent(ctype) + "&confKey=" + encodeURIComponent(key) + confGroupSuffix(ctype, group);
797
+ let r;
798
+ try { r = await confRequest("GET", url); }
799
+ catch (e) { if (silent) return null; throw e; }
800
+ const data = (r && r.data) || null;
801
+ if (!silent && (!data || data.confValue === undefined || data.confValue === null || data.confValue === "")) {
802
+ throw new Error("配置不存在或为空: confType=" + ctype + (group ? " confGroup=" + group : "") + " confKey=" + key);
803
+ }
804
+ return data;
805
+ }
806
+ function confNormalizeBatch(payload) {
807
+ const data = (payload && payload.data) || null;
808
+ const out = {};
809
+ const put = (row, fallbackKey) => {
810
+ if (!row || typeof row !== "object") return;
811
+ const k = row.confKey || row.conf_key || fallbackKey;
812
+ if (!k) return;
813
+ if (row.confGroup === undefined) { const g = row.conf_group; if (g !== undefined && g !== null && g !== "") row.confGroup = g; }
814
+ out[confItemId({ confGroup: row.confGroup, confKey: k })] = row;
815
+ };
816
+ if (Array.isArray(data)) for (const row of data) put(row);
817
+ else if (data && typeof data === "object") for (const [k, v] of Object.entries(data)) put(v, k);
818
+ return out;
819
+ }
820
+ // 按类型+分组批量取回(batchGetChatBiConfigMapByTypeAndGroup),payload = [{confType, confGroup}...]
821
+ async function confBatchByTypeAndGroup(pairs) {
822
+ const payload = pairs.map((p) => ({ confType: p.confType, confGroup: p.confGroup === undefined || p.confGroup === null ? "" : String(p.confGroup) }));
823
+ const r = await confRequest("POST", CONF_BATCH_GROUP_PATH, payload);
824
+ return confNormalizeBatch(r);
825
+ }
826
+ async function cmdConfGet(args, json, keys) {
827
+ const key = keys[0];
828
+ if (!key) throw new Error("conf get 缺少 confKey(用法: conf get <key> [--type t] [--group g])");
829
+ const ctype = confKeyType(key, args.type);
830
+ let data;
831
+ try {
832
+ data = await confGetData(ctype, key, false, args.group);
833
+ } catch (e) {
834
+ if (ctype === "api" && !args.group) {
835
+ throw new Error(e.message + "(confType=api 建议用 --group 指定分组,如 --group workflow;或用 conf group api <分组> 查看该分组下全部配置)");
836
+ }
837
+ throw e;
838
+ }
839
+ if (json) return data;
840
+ console.log(JSON.stringify(data, null, 1));
841
+ }
842
+ async function cmdConfBatch(args, json, keys) {
843
+ if (!keys.length) throw new Error("conf batch 缺少 confKey 列表(用法: conf batch <key1> [key2 ...] [--type t] [--group g])");
844
+ const items = keys.map((k) => { const t = confKeyType(k, args.type); return confPutGroup({ confType: t, confKey: k }, t, args.group); });
845
+ let map = {};
846
+ try {
847
+ const r = await confRequest("POST", CONF_BATCH_PATH, items);
848
+ map = confNormalizeBatch(r);
849
+ for (const it of items) {
850
+ const c = confLookup(map, it);
851
+ if (!c || c.confValue === undefined || c.confValue === null || c.confValue === "") {
852
+ throw new Error("批量响应缺失 confKey=" + it.confKey);
853
+ }
854
+ }
855
+ } catch (e) {
856
+ if (String(e.message).indexOf("批量响应缺失") === 0) throw e;
857
+ console.error("[config-resolve] 批量接口不可用,降级逐条单查: " + e.message);
858
+ map = {};
859
+ for (const it of items) map[confItemId(it)] = await confGetData(it.confType, it.confKey, false, it.confGroup);
860
+ }
861
+ if (json) return map;
862
+ printHuman("配置 " + items.length + " 条", keys.map((k) => {
863
+ const it = items.find((x) => x.confKey === k);
864
+ const c = confLookup(map, it) || {};
865
+ return { key: k, confType: it.confType, confGroup: it.confGroup ?? "", confValue: c.confValue ?? "", confName: c.confName ?? "" };
866
+ }), ["key", "confType", "confGroup", "confValue", "confName"]);
867
+ }
868
+ async function cmdConfBatchFile(args, json, keys) {
869
+ const val = flagOrPos(args, "batch-file", keys);
870
+ if (val === undefined) throw new Error("conf batch-file 缺少参数(用法: conf batch-file @items.json)");
871
+ const items = readJsonArg(String(val), "--batch-file");
872
+ const list = Array.isArray(items) ? items : [items];
873
+ const map = {};
874
+ const meta = {};
875
+ for (const it of list) {
876
+ if (!it.confKey || !it.confType) continue;
877
+ const id = confItemId(it);
878
+ map[id] = await confGetData(it.confType, it.confKey, false, it.confGroup);
879
+ meta[id] = it;
880
+ }
881
+ if (json) return map;
882
+ printHuman("配置 " + Object.keys(map).length + " 条", Object.keys(map).map((id) => ({ key: meta[id].confKey, confGroup: meta[id].confGroup ?? "", confValue: (map[id] || {}).confValue ?? "", confName: (map[id] || {}).confName ?? "" })), ["key", "confGroup", "confValue", "confName"]);
883
+ }
884
+ // 按类型+分组批量查(batchGetChatBiConfigMapByTypeAndGroup):pairs = [{confType, confGroup}...]
885
+ async function cmdConfGroup(args, json, keys) {
886
+ const ctype = keys[0] || args.type;
887
+ const group = keys[1] !== undefined ? keys[1] : args.group;
888
+ if (!ctype) throw new Error("conf group 缺少 confType(用法: conf group <confType> <confGroup> | --type api --group workflow)");
889
+ if (group === undefined) throw new Error("conf group 缺少 confGroup(用法: conf group <confType> <confGroup>)");
890
+ const map = await confBatchByTypeAndGroup([{ confType: ctype, confGroup: group }]);
891
+ const rows = Object.values(map);
892
+ const onlyKey = args.key;
893
+ const filtered = onlyKey ? rows.filter((r) => (r.confKey || r.conf_key) === onlyKey) : rows;
894
+ if (json) return onlyKey ? (filtered[0] ?? null) : filtered;
895
+ if (onlyKey && !filtered.length) throw new Error("配置不存在: confType=" + ctype + " confGroup=" + group + " confKey=" + onlyKey);
896
+ printHuman("confType=" + ctype + " confGroup=" + group + " 配置 " + filtered.length + " 条",
897
+ filtered.map((r) => ({ confKey: r.confKey || r.conf_key || "", confValue: r.confValue ?? "", confName: r.confName ?? "", confAttrs: r.confAttrs ?? "" })),
898
+ ["confKey", "confValue", "confName", "confAttrs"]);
899
+ return filtered;
900
+ }
901
+ async function cmdConfGroupFile(args, json, keys) {
902
+ const val = flagOrPos(args, "group-file", keys);
903
+ if (val === undefined) throw new Error("conf group-file 缺少参数(用法: conf group-file @items.json,形如 [{\"confType\":\"api\",\"confGroup\":\"workflow\"}])");
904
+ const pairs = readJsonArg(String(val), "--group-file");
905
+ const list = (Array.isArray(pairs) ? pairs : [pairs]).filter((p) => p && p.confType);
906
+ if (!list.length) throw new Error("conf group-file 内容为空或缺少 confType");
907
+ const map = await confBatchByTypeAndGroup(list);
908
+ const rows = Object.values(map);
909
+ if (json) return map;
910
+ printHuman("批量分组配置 " + rows.length + " 条",
911
+ rows.map((r) => ({ confType: r.confType ?? "", confGroup: r.confGroup ?? "", confKey: r.confKey || r.conf_key || "", confValue: r.confValue ?? "", confName: r.confName ?? "" })),
912
+ ["confType", "confGroup", "confKey", "confValue", "confName"]);
913
+ return map;
914
+ }
915
+ async function confResolveAll(keys, group) {
916
+ // 多占位符:先批量预取一轮(失败降级逐条单查),返回 {key: data}
917
+ const items = keys.map((k) => confPutGroup({ confType: confKeyType(k, ""), confKey: k }, confKeyType(k, ""), group));
918
+ const map = {};
919
+ try {
920
+ const r = await confRequest("POST", CONF_BATCH_PATH, items);
921
+ const got = confNormalizeBatch(r);
922
+ for (const it of items) {
923
+ const c = confLookup(got, it);
924
+ if (!c || c.confValue === undefined || c.confValue === null || c.confValue === "") throw new Error("缺失 " + it.confKey);
925
+ map[it.confKey] = c;
926
+ }
927
+ } catch {
928
+ for (const it of items) map[it.confKey] = await confGetData(it.confType, it.confKey, false, it.confGroup);
929
+ }
930
+ return map;
931
+ }
932
+ async function confResolveText(text, json, group) {
933
+ const found = [...new Set([...text.matchAll(CONF_TOKEN_RE)].map((m) => m[1]))];
934
+ const map = found.length ? await confResolveAll(found, group) : {};
935
+ const out = text.replace(CONF_TOKEN_RE, (m0, k) => {
936
+ const cfg = map[k];
937
+ if (!cfg) throw new Error("配置不存在或为空: " + k);
938
+ return cfg.confValue ?? "";
939
+ });
940
+ if (json) return { input: text, output: out, resolved: Object.fromEntries(Object.entries(map).map(([k, v]) => [k, v.confValue ?? ""])) };
941
+ return out;
942
+ }
943
+ async function cmdConfResolve(args, json, keys) {
944
+ let text = keys[0];
945
+ if (!text) throw new Error("conf resolve 缺少文本(用法: conf resolve '<含#{KEY}文本>' | @file.txt [--group g])");
946
+ if (text.startsWith("@")) text = fs.readFileSync(text.slice(1), "utf-8");
947
+ const out = await confResolveText(text, json, args.group);
948
+ if (json) { console.log(JSON.stringify(out, null, 1)); return out; }
949
+ process.stdout.write(out + "\n");
950
+ }
951
+ async function cmdConfResolveFile(args, json, keys) {
952
+ const p = flagOrPos(args, "resolve-file", keys);
953
+ if (!p) throw new Error("conf resolve-file 缺少文件路径(用法: conf resolve-file <文件路径> [--group g])");
954
+ if (!fs.existsSync(p)) throw new Error("文件不存在: " + p);
955
+ const text = fs.readFileSync(p, "utf-8");
956
+ const out = await confResolveText(text, json, args.group);
957
+ if (json) { console.log(JSON.stringify(out, null, 1)); return out; }
958
+ process.stdout.write(out + "\n");
959
+ }
960
+ async function cmdConfAdd(args, json, keys) {
961
+ const key = keys[0];
962
+ if (!key || args.value === undefined) throw new Error("conf add 缺少 confKey/confValue(用法: conf add <key> --value <真实值> [--name] [--attrs] [--type] [--group] [--force])");
963
+ const ctype = confKeyType(key, args.type);
964
+ if (!args.force && await confGetData(ctype, key, true, args.group)) {
965
+ console.error("[config-resolve] 配置已存在,跳过新增(如需修改请用 conf update;确需重建可先 conf delete): " + key);
966
+ return null;
967
+ }
968
+ const item = confPutGroup({ confType: ctype, confKey: key, confValue: String(args.value), confAttrs: args.attrs || "", confName: args.name || "" }, ctype, args.group);
969
+ const r = await confRequest("POST", CONF_ADD_PATH, [item]);
970
+ if (json) return r;
971
+ console.log("新增成功: " + (item.confGroup ? item.confGroup + "/" : "") + key + " = " + item.confValue);
972
+ }
973
+ async function cmdConfAddFile(args, json, keys) {
974
+ const val = flagOrPos(args, "add-file", keys);
975
+ if (val === undefined) throw new Error("conf add-file 缺少参数(用法: conf add-file @items.json,元素形如 {confType,confGroup,confKey,confValue,confName,confAttrs})");
976
+ const items = readJsonArg(String(val), "--add-file");
977
+ const list = Array.isArray(items) ? items : [items];
978
+ const toAdd = [];
979
+ for (const it of list) {
980
+ if (!it.confKey || it.confValue === undefined) continue;
981
+ const ctype = it.confType || confKeyType(it.confKey, args.type);
982
+ const group = it.confGroup !== undefined ? it.confGroup : args.group;
983
+ const dup = await confGetData(ctype, it.confKey, true, group);
984
+ if (dup) { console.error("[config-resolve] 已存在跳过: " + it.confKey); continue; }
985
+ toAdd.push(confPutGroup({ confType: ctype, confKey: it.confKey, confValue: String(it.confValue), confAttrs: it.confAttrs || "", confName: it.confName || "" }, ctype, group));
986
+ }
987
+ if (!toAdd.length) { console.log("全部已存在,无新增"); return null; }
988
+ const r = await confRequest("POST", CONF_ADD_PATH, toAdd);
989
+ if (json) return r;
990
+ console.log("批量新增 " + toAdd.length + " 条成功");
991
+ }
992
+ async function cmdConfUpdate(args, json, keys) {
993
+ const key = keys[0];
994
+ if (!key) throw new Error("conf update 缺少 confKey(用法: conf update <key> [--value v] [--name n] [--attrs a] [--type t] [--group g])");
995
+ if (args.value === undefined && args.name === undefined && args.attrs === undefined) {
996
+ throw new Error("conf update 至少需提供 --value / --name / --attrs 之一");
997
+ }
998
+ const ctype = confKeyType(key, args.type);
999
+ const body = confPutGroup({ confType: ctype, confKey: key }, ctype, args.group);
1000
+ if (args.value !== undefined) body.confValue = String(args.value);
1001
+ if (args.name !== undefined) body.confName = args.name;
1002
+ if (args.attrs !== undefined) body.confAttrs = args.attrs;
1003
+ const r = await confRequest("POST", CONF_UPDATE_PATH, body);
1004
+ if (json) return r;
1005
+ console.log("更新成功: " + (body.confGroup ? body.confGroup + "/" : "") + key);
1006
+ }
1007
+ async function cmdConfDelete(args, json, keys) {
1008
+ const key = keys[0];
1009
+ if (!key) throw new Error("conf delete 缺少 confKey(用法: conf delete <key> [--type t] [--group g])");
1010
+ const ctype = confKeyType(key, args.type);
1011
+ const url = CONF_DELETE_PATH + "?confType=" + encodeURIComponent(ctype) + "&confKey=" + encodeURIComponent(key) + confGroupSuffix(ctype, args.group);
1012
+ const r = await confRequest("DELETE", url);
1013
+ if (json) return r;
1014
+ console.log("删除成功: " + (confUseGroup(ctype, args.group) && args.group ? args.group + "/" : "") + key);
1015
+ }
1016
+ async function cmdConf(sub, args, json, pos) {
1017
+ const keys = pos.slice(2);
1018
+ switch (sub) {
1019
+ case "get": return cmdConfGet(args, json, keys);
1020
+ case "batch": return cmdConfBatch(args, json, keys);
1021
+ case "batch-file": return cmdConfBatchFile(args, json, keys);
1022
+ case "group": return cmdConfGroup(args, json, keys);
1023
+ case "group-file": return cmdConfGroupFile(args, json, keys);
1024
+ case "resolve": return cmdConfResolve(args, json, keys);
1025
+ case "resolve-file": return cmdConfResolveFile(args, json, keys);
1026
+ case "add": return cmdConfAdd(args, json, keys);
1027
+ case "add-file": return cmdConfAddFile(args, json, keys);
1028
+ case "update": return cmdConfUpdate(args, json, keys);
1029
+ case "delete": return cmdConfDelete(args, json, keys);
1030
+ default: usage(); process.exit(1);
1031
+ }
1032
+ }
1033
+
1034
+ // ---------------- attend:考勤 REST 接口 ----------------
1035
+ // 2026-09-07:封装 /api/attend/web/attendInfoV2/getAttendInfoStatis(假期余额统计,hr-attendance 场景六病假/事假)。
1036
+ // 请求参数 {beginDate, endDate, userId};返回 data.vacationBalanceList[]:{id,title,data:[{name,value,key}]},
1037
+ // key 含 lastRemaining(历年可请)/remaining(今年可请)/total(总计可请)。
1038
+ function defaultMonthRange() {
1039
+ const now = new Date();
1040
+ const y = now.getFullYear();
1041
+ const m = now.getMonth() + 1;
1042
+ const lastDay = new Date(y, m, 0).getDate();
1043
+ const p = (n) => String(n).padStart(2, "0");
1044
+ return { begin: y + "-" + p(m) + "-01", end: y + "-" + p(m) + "-" + p(lastDay) };
1045
+ }
1046
+ async function cmdAttendStatis(args, json) {
1047
+ const user = args.user || auth.userId || "";
1048
+ if (!user) throw new Error("缺少查询人员:--user <人员ID>(查本人可不传,默认当前登录用户)");
1049
+ const def = defaultMonthRange();
1050
+ const body = { beginDate: args.begin || def.begin, endDate: args.end || def.end, userId: user };
1051
+ const r = await postJson("/api/attend/web/attendInfoV2/getAttendInfoStatis", body);
1052
+ if (json) return r;
1053
+ if (String(r.code) !== "200") {
1054
+ console.log("code=" + r.code + " msg=" + (r.msg || ""));
1055
+ if (/没有权限|无权限|权限不足|未授权|无权访问|无查询权限/i.test(String(r.msg || ""))) {
1056
+ throw new Error("当前用户对该人员考勤数据无查询权限,请检查 ETEAMSID 或联系管理员开通");
1057
+ }
1058
+ return;
1059
+ }
1060
+ const d = r.data || {};
1061
+ const list = Array.isArray(d.vacationBalanceList) ? d.vacationBalanceList : [];
1062
+ if (!list.length) { console.log("(空:该人员无假期余额数据) userId=" + user + " " + body.beginDate + "~" + body.endDate); return; }
1063
+ console.log("== 考勤假期余额 userId=" + user + "(" + body.beginDate + " ~ " + body.endDate + ")==");
1064
+ for (const item of list) {
1065
+ console.log((item.title || item.id || "?") + (item.id ? " [" + item.id + "]" : ""));
1066
+ if (Array.isArray(item.data) && item.data.length) {
1067
+ console.log(" " + item.data.map((x) => (x.name || x.key) + "=" + (x.value ?? "")).join(" "));
1068
+ }
1069
+ }
1070
+ }
1071
+
1072
+ // ---------------- exp:专家包版本管理(expert-repository) ----------------
1073
+ async function expGet(url) {
1074
+ // 返回 {raw, j}:raw 用于正则提取大整数 id(commitId 19 位超 JS Number 精度,JSON.parse 会丢精度!)
1075
+ const resp = await fetch(BASE + url, {
1076
+ method: "GET",
1077
+ headers: { "accept": "application/json, text/plain, */*", "langType": "zh_CN", "cookie": auth.cookie, "eteamsid": auth.eteamsid, "ebbusinessid": EB_BIZ_ID, "user-agent": UA },
1078
+ });
1079
+ const raw = await resp.text();
1080
+ let j; try { j = parseJsonSafe(raw); } catch { throw new Error("HTTP " + resp.status + " 非JSON响应: " + raw.slice(0, 300)); }
1081
+ if (resp.status === 401 || (j && String(j.code) === "-1" && /token|登录|失效/i.test(String(j.msg || "")))) throw new Error("登录态失效,请重新 e10-login oidc 登录");
1082
+ return { raw, j };
1083
+ }
1084
+ function rawNum(raw, key) {
1085
+ // 从原始 JSON 文本提取整数字段为精确字符串(避免 JSON.parse 大数丢精度,如 commitId)
1086
+ const m = raw.match(new RegExp('"' + key + '":\\s*(\\d+)'));
1087
+ return m ? m[1] : "";
1088
+ }
1089
+ function rawNumSeq(raw, key) {
1090
+ // 提取某 key 全部整数序列(按出现顺序),如提交历史里的 commitId
1091
+ const re = new RegExp('"' + key + '":\\s*(\\d+)', "g");
1092
+ const out = []; let m;
1093
+ while ((m = re.exec(raw)) !== null) out.push(m[1]);
1094
+ return out;
1095
+ }
1096
+ const EXP_ALLOWED_DIRS = [".codebuddy-plugin", "agents", "avatars", "skills"];
1097
+ const EXP_SKIP_DIRS = [".git", ".workbuddy", "node_modules", "__pycache__"];
1098
+ const EXP_SKIP_FILES = [".sync-state.json"]; // 本地同步状态元文件(含基线 sha),永不随包提交
1099
+ function sha256(buf) { return crypto.createHash("sha256").update(buf).digest("hex"); }
1100
+ function walkDir(dir, root) {
1101
+ const out = [];
1102
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1103
+ const abs = path.join(dir, entry.name);
1104
+ const rel = path.relative(root, abs).split(path.sep).join("/");
1105
+ if (entry.isDirectory()) {
1106
+ if (EXP_SKIP_DIRS.includes(entry.name)) continue;
1107
+ const top = rel.split("/")[0];
1108
+ if (rel.includes("/") && !EXP_ALLOWED_DIRS.includes(top)) continue; // 仅允许根文件+四大目录
1109
+ walkDir(abs, root).forEach((x) => out.push(x));
1110
+ } else if (entry.isFile()) {
1111
+ if (EXP_SKIP_FILES.includes(entry.name)) continue; // .sync-state.json 不入包
1112
+ const top = rel.split("/")[0];
1113
+ if (!rel.includes("/") || EXP_ALLOWED_DIRS.includes(top)) out.push({ rel, abs });
1114
+ }
1115
+ }
1116
+ return out;
1117
+ }
1118
+ function collectPackage(dir) {
1119
+ if (!fs.existsSync(dir)) throw new Error("本地专家包目录不存在: " + dir);
1120
+ return walkDir(dir, dir).map(({ rel, abs }) => {
1121
+ const buf = fs.readFileSync(abs);
1122
+ if (buf.length > 10 * 1024 * 1024) throw new Error("文件超 10MB 上限: " + rel);
1123
+ return { relativePath: rel, contentBase64: buf.toString("base64"), sha256: sha256(buf), abs };
1124
+ });
1125
+ }
1126
+ function localShaMap(dir) {
1127
+ const map = {};
1128
+ for (const f of walkDir(dir, dir)) map[f.rel] = sha256(fs.readFileSync(f.abs));
1129
+ return map;
1130
+ }
1131
+ function expOk(r) {
1132
+ if (!r) return false;
1133
+ if (r.actionMsg) return String(r.actionMsg.code) === "0";
1134
+ const code = r.code;
1135
+ if (code === undefined) return !r.fail;
1136
+ return String(code) === "0" || String(code) === "200";
1137
+ }
1138
+ function expFailMsg(r) {
1139
+ if (r && r.actionMsg && String(r.actionMsg.code) !== "0") return r.actionMsg.message;
1140
+ if (r && r.msg) return r.msg;
1141
+ return "请求失败";
1142
+ }
1143
+ function printExpData(r, title) {
1144
+ if (!expOk(r)) throw new Error(expFailMsg(r));
1145
+ if (title) console.log("== " + title + " ==");
1146
+ console.log(JSON.stringify(r.data ?? {}, null, 1));
1147
+ }
1148
+ async function rawGet(url) {
1149
+ const resp = await fetch(BASE + url, {
1150
+ method: "GET",
1151
+ headers: { "accept": "*/*", "langType": "zh_CN", "cookie": auth.cookie, "eteamsid": auth.eteamsid, "ebbusinessid": EB_BIZ_ID, "user-agent": UA },
1152
+ });
1153
+ if (resp.status === 401) throw new Error("登录态失效,请重新 e10-login oidc 登录");
1154
+ return Buffer.from(await resp.arrayBuffer());
1155
+ }
1156
+ async function expStatus(args, json) {
1157
+ const expert = need("expert", args);
1158
+ const { raw, j: r } = await expGet("/api/dw/expert-repository/v1/repositories/status?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
1159
+ if (json) return r;
1160
+ if (!expOk(r)) throw new Error(expFailMsg(r));
1161
+ const d = r.data || {};
1162
+ console.log("== 仓库状态 " + expert + " ==");
1163
+ console.log("expertCode = " + (d.expertCode ?? ""));
1164
+ console.log("displayName = " + (d.displayName ?? ""));
1165
+ console.log("branchName = " + (d.branchName ?? ""));
1166
+ console.log("headCommitId = " + (rawNum(raw, "headCommitId") || d.headCommitId));
1167
+ console.log("headRevision = " + (d.headRevision ?? ""));
1168
+ for (const k of ["headCommitHash", "headCommitMessage", "headCommitTime", "headAuthor", "protected"]) if (d[k] !== undefined) console.log(k + " = " + d[k]);
1169
+ if (Array.isArray(d.locks) && d.locks.length) { console.log("有效锁:"); for (const l of d.locks) console.log(" " + JSON.stringify(l)); }
1170
+ }
1171
+ async function expManifest(args, json) {
1172
+ const expert = need("expert", args);
1173
+ let url = "/api/dw/expert-repository/v1/repositories/manifest?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main");
1174
+ if (args.commit) url += "&commitId=" + encodeURIComponent(args.commit);
1175
+ const r = await getUrl(url);
1176
+ if (json) return r;
1177
+ if (!expOk(r)) throw new Error(expFailMsg(r));
1178
+ const items = r.data || [];
1179
+ console.log("== 文件清单 " + items.length + " 项 ==");
1180
+ printHuman("", items, ["relativePath", "sha256", "size", "action"]);
1181
+ }
1182
+ async function expCommits(args, json) {
1183
+ const expert = need("expert", args);
1184
+ const { raw, j: r } = await expGet("/api/dw/expert-repository/v1/repositories/commits?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
1185
+ if (json) return r;
1186
+ if (!expOk(r)) throw new Error(expFailMsg(r));
1187
+ const rows = (r.data || []).map((x, i) => ({ ...x, commitId: (rawNumSeq(raw, "commitId")[i] || x.commitId) }));
1188
+ console.log("== 提交历史 " + rows.length + " 条(commitId 为精确值,可复制用于 file/release/rollback)==");
1189
+ printHuman("", rows, ["commitId", "submitMode", "author", "createTime", "message"]);
1190
+ }
1191
+ async function expFile(args, json) {
1192
+ const expert = need("expert", args), commit = need("commit", args), rp = need("path", args);
1193
+ const buf = await rawGet("/api/dw/expert-repository/v1/repositories/file?expertCode=" + encodeURIComponent(expert) + "&commitId=" + encodeURIComponent(commit) + "&relativePath=" + encodeURIComponent(rp));
1194
+ if (args.out) { fs.writeFileSync(args.out, buf); console.log("已下载 " + buf.length + " 字节 -> " + args.out); return; }
1195
+ if (json) { console.log(JSON.stringify({ relativePath: rp, size: buf.length, base64: buf.toString("base64") })); return; }
1196
+ process.stdout.write(buf.toString("utf-8"));
1197
+ }
1198
+ // ---- exp 智能拉取:基线状态文件(三方差异保护,2026-09-04 实现)----
1199
+ // .sync-state.json 记录上次成功同步的 commitId/revision 与每文件 sha256(基线)。
1200
+ // pull 逐文件三方对比:local(当前本地) vs base(上次同步基线) vs remote(远端 manifest),
1201
+ // 决策:仅远端改→覆盖;仅本地改→保留本地;双方都改→备份本地副本后覆盖(备份目录保留本地版);
1202
+ // 无基线文件/基线无记录→保守备份后覆盖。拉取成功后把 .sync-state.json 更新为新 head。
1203
+ function syncStatePath(dir) { return path.join(dir, ".sync-state.json"); }
1204
+ function readSyncState(dir) {
1205
+ const p = syncStatePath(dir);
1206
+ if (!fs.existsSync(p)) return null;
1207
+ try {
1208
+ const j = JSON.parse(fs.readFileSync(p, "utf-8"));
1209
+ const files = {};
1210
+ for (const [rel, v] of Object.entries(j.files || {})) files[rel] = (typeof v === "string") ? v : (v.sha256 || "");
1211
+ return { commitId: String(j.commitId || ""), headRevision: j.headRevision, files };
1212
+ } catch { return null; }
1213
+ }
1214
+ function writeSyncState(dir, commitId, headRevision, fileShaMap) {
1215
+ const files = {};
1216
+ for (const [rel, sha] of Object.entries(fileShaMap)) files[rel] = { sha256: sha };
1217
+ const obj = { commitId: String(commitId), headRevision, files, updateTime: new Date().toISOString() };
1218
+ fs.writeFileSync(syncStatePath(dir), JSON.stringify(obj, null, 1), "utf-8");
1219
+ }
1220
+ async function expPull(args, json) {
1221
+ const expert = need("expert", args), dir = need("dir", args);
1222
+ const dry = !!args["dry-run"];
1223
+ const force = !!args["force"]; // --force = 一律以远端覆盖(放弃本地修改)
1224
+ const st = await expGet("/api/dw/expert-repository/v1/repositories/status?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
1225
+ if (!expOk(st.j)) throw new Error(expFailMsg(st.j));
1226
+ const headId = rawNum(st.raw, "headCommitId") || String((st.j.data || {}).headCommitId);
1227
+ const headRev = (st.j.data || {}).headRevision;
1228
+ const mf = await expGet("/api/dw/expert-repository/v1/repositories/manifest?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
1229
+ if (!expOk(mf.j)) throw new Error(expFailMsg(mf.j));
1230
+ const items = (mf.j.data || []).filter((f) => f.relativePath && f.relativePath !== "DELETE");
1231
+ const local = fs.existsSync(dir) ? localShaMap(dir) : {};
1232
+ const base = readSyncState(dir); // 基线:上次同步后的文件 sha(无则 null)
1233
+ const baseCommit = (base && base.commitId) || "";
1234
+ // 基线有效判定:上次同步 commitId 须在提交历史上(取不到历史就保守按无基线处理)
1235
+ let backupRoot = "";
1236
+ let downloaded = 0, unchanged = 0, preserved = 0, backed = 0, missing = 0;
1237
+ const plan = []; // {rel, action, reason}
1238
+ const mkBackupRoot = () => {
1239
+ if (backupRoot) return backupRoot;
1240
+ const absDir = path.resolve(dir);
1241
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1242
+ backupRoot = path.join(path.dirname(absDir), path.basename(absDir) + ".pull-backup-" + ts);
1243
+ fs.mkdirSync(backupRoot, { recursive: true });
1244
+ return backupRoot;
1245
+ };
1246
+ for (const it of items) {
1247
+ const lp = path.join(dir, it.relativePath.split("/").join(path.sep));
1248
+ const ls = local[it.relativePath];
1249
+ const baseSha = (base && base.files) ? base.files[it.relativePath] : undefined;
1250
+ if (ls === it.sha256) { unchanged++; continue; }
1251
+ if (ls === undefined) { // 本地缺失 → 直接下载(新增/恢复)
1252
+ missing++; plan.push({ rel: it.relativePath, action: dry ? "待下载" : "已下载", reason: "本地缺失" });
1253
+ } else if (!force && baseSha !== undefined && ls === baseSha && baseSha !== it.sha256) {
1254
+ // 仅远端改(本地未动,与基线一致)→ 覆盖
1255
+ plan.push({ rel: it.relativePath, action: dry ? "待下载" : "已下载", reason: "远端更新(本地未动)" });
1256
+ } else if (!force && baseSha !== undefined && baseSha === it.sha256) {
1257
+ // 仅本地改(远端=基线,本地≠基线)→ 保留本地,不覆盖
1258
+ preserved++; plan.push({ rel: it.relativePath, action: "保留本地", reason: "本地修改(远端未动)" });
1259
+ continue;
1260
+ } else { // 双方都改 / 无基线可判 / --force → 先备份本地副本再覆盖
1261
+ backed++;
1262
+ if (!dry && !force) {
1263
+ const bdir = path.join(mkBackupRoot(), it.relativePath.split("/").join(path.sep));
1264
+ fs.mkdirSync(path.dirname(bdir), { recursive: true });
1265
+ fs.copyFileSync(lp, bdir);
1266
+ }
1267
+ plan.push({ rel: it.relativePath, action: dry ? "待下载(将备份本地)" : (force ? "已下载(force覆盖)" : "已下载(本地已备份)"), reason: (force ? "force 覆盖" : (baseSha === undefined ? "无基线记录" : "双方都改")) });
1268
+ }
1269
+ if (dry) continue;
1270
+ fs.mkdirSync(path.dirname(lp), { recursive: true });
1271
+ const buf = await rawGet("/api/dw/expert-repository/v1/repositories/file?expertCode=" + encodeURIComponent(expert) + "&commitId=" + encodeURIComponent(headId) + "&relativePath=" + encodeURIComponent(it.relativePath));
1272
+ fs.writeFileSync(lp, buf);
1273
+ downloaded++;
1274
+ }
1275
+ // 拉取成功后更新基线状态文件(记录新 head + 本地最终文件 sha)
1276
+ if (!dry) {
1277
+ const finalLocal = localShaMap(dir);
1278
+ writeSyncState(dir, headId, headRev, finalLocal);
1279
+ }
1280
+ const out = { expert, headCommitId: headId, headRevision: headRev, total: items.length, unchanged, downloaded, preserved, backed, missing, dryRun: !!dry, backupDir: backupRoot || undefined };
1281
+ if (json) { console.log(JSON.stringify(out)); return; }
1282
+ console.log("== pull " + expert + "@" + headId + (dry ? " (dry-run)" : "") + " ==");
1283
+ console.log("清单 " + out.total + " 项 | 未变 " + unchanged + " | 已下载 " + downloaded + " | 保留本地 " + preserved + " | 覆盖前备份 " + backed + " | 缺失 " + missing);
1284
+ for (const p of plan.slice(0, 40)) console.log(" [" + p.action + "] " + p.rel + (p.reason ? "(" + p.reason + ")" : ""));
1285
+ if (plan.length > 40) console.log(" ... 共 " + plan.length + " 个文件");
1286
+ if (backupRoot && !dry) console.log("本地修改副本备份于: " + backupRoot);
1287
+ if (preserved > 0 && !dry) console.log("⚠️ " + preserved + " 个文件因含本地修改被保留(远端未改);如需强制覆盖请加 --force");
1288
+ }
1289
+ async function expInit(args, json) {
1290
+ const expert = need("expert", args), dir = need("dir", args);
1291
+ const files = collectPackage(dir);
1292
+ const body = { expertCode: expert, displayName: args["display-name"] || expert, branchName: args.branch || "main",
1293
+ description: args.description || "", initialCommitMessage: args.message || "初始化导入", files };
1294
+ const r = await postJson("/api/dw/expert-repository/v1/repositories", body);
1295
+ if (json) return r;
1296
+ if (!expOk(r)) throw new Error(expFailMsg(r));
1297
+ console.log("== 仓库已创建 =="); console.log(JSON.stringify(r.data ?? {}, null, 1));
1298
+ }
1299
+ async function expCommit(args, json) {
1300
+ const expert = need("expert", args), dir = need("dir", args), msg = need("message", args);
1301
+ const mode = (args.mode || "FULL_SNAPSHOT").toUpperCase();
1302
+ const st = await expGet("/api/dw/expert-repository/v1/repositories/status?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
1303
+ if (!expOk(st.j)) throw new Error("无法获取基线: " + expFailMsg(st.j));
1304
+ const base = st.j.data || {};
1305
+ const body = {
1306
+ branchName: base.branchName || "main",
1307
+ baseCommitId: rawNum(st.raw, "headCommitId") || String(base.headCommitId), // 精确字符串,避免大数丢精度
1308
+ baseRevision: base.headRevision,
1309
+ commitMessage: msg,
1310
+ };
1311
+ if (mode === "FILE_PATCH") {
1312
+ const mf = await expGet("/api/dw/expert-repository/v1/repositories/manifest?expertCode=" + encodeURIComponent(expert) + "&branchName=" + (args.branch || "main"));
1313
+ if (!expOk(mf.j)) throw new Error("无法获取 manifest: " + expFailMsg(mf.j));
1314
+ const remoteSha = {}; for (const f of (mf.j.data || [])) remoteSha[f.relativePath] = f.sha256;
1315
+ const local = collectPackage(dir);
1316
+ const localSha = {}; for (const f of local) localSha[f.relativePath] = f.sha256;
1317
+ const changes = [];
1318
+ for (const f of local) {
1319
+ const rs = remoteSha[f.relativePath];
1320
+ if (rs === undefined) changes.push({ operation: "ADD", relativePath: f.relativePath, contentBase64: f.contentBase64, sha256: f.sha256, baseFileSha256: "" });
1321
+ else if (rs !== f.sha256) changes.push({ operation: "MODIFY", relativePath: f.relativePath, contentBase64: f.contentBase64, sha256: f.sha256, baseFileSha256: rs });
1322
+ }
1323
+ for (const rp of Object.keys(remoteSha)) if (localSha[rp] === undefined) changes.push({ operation: "DELETE", relativePath: rp, baseFileSha256: remoteSha[rp] });
1324
+ if (!changes.length) throw new Error("本地与远端一致,无变更可提交");
1325
+ body.submitMode = "FILE_PATCH"; body.changes = changes;
1326
+ } else {
1327
+ body.submitMode = "FULL_SNAPSHOT"; body.files = collectPackage(dir);
1328
+ }
1329
+ const r = await postJson("/api/dw/expert-repository/v1/repositories/commits?expertCode=" + encodeURIComponent(expert), body);
1330
+ if (json) return r;
1331
+ if (!expOk(r)) {
1332
+ const m = expFailMsg(r);
1333
+ if (/基线|过期|revision|冲突/i.test(m)) throw new Error("提交冲突: " + m + " —— 请先 exp pull 拉取最新并合并后重试");
1334
+ throw new Error("提交失败: " + m);
1335
+ }
1336
+ // 提交成功后把本地 .sync-state.json 基线刷新到新 head(与本地文件 sha 一致),保证后续 pull 三方判定准确
1337
+ const rj = r.data || {};
1338
+ const newHead = rawNum(JSON.stringify(r), "headCommitId") || rawNum(JSON.stringify(r), "commitId") || rj.headCommitId || rj.commitId || "";
1339
+ if (newHead && fs.existsSync(dir)) {
1340
+ try { writeSyncState(dir, newHead, rj.headRevision ?? rj.revision ?? 0, localShaMap(dir)); } catch {}
1341
+ }
1342
+ console.log("== 提交成功 =="); console.log(JSON.stringify(r.data ?? {}, null, 1));
1343
+ }
1344
+ async function expLock(args, json) {
1345
+ const expert = need("expert", args);
1346
+ const act = (args.action || "").toLowerCase();
1347
+ const branch = args.branch || "main";
1348
+ if (act === "query") {
1349
+ const r = await getUrl("/api/dw/expert-repository/v1/repositories/locks?expertCode=" + encodeURIComponent(expert) + "&branchName=" + encodeURIComponent(branch));
1350
+ if (json) return r;
1351
+ if (!expOk(r)) throw new Error(expFailMsg(r));
1352
+ printExpData(r, "有效锁");
1353
+ return;
1354
+ }
1355
+ if (act === "heartbeat") {
1356
+ const token = need("lock-token", args);
1357
+ const r = await postJson("/api/dw/expert-repository/v1/repositories/locks/heartbeat?lockToken=" + encodeURIComponent(token), {});
1358
+ if (json) return r;
1359
+ if (!expOk(r)) throw new Error(expFailMsg(r));
1360
+ console.log("锁已续期: " + JSON.stringify(r.data ?? {}));
1361
+ return;
1362
+ }
1363
+ if (act === "release") {
1364
+ const token = need("lock-token", args);
1365
+ const req = new Request(BASE + "/api/dw/expert-repository/v1/repositories/locks?lockToken=" + encodeURIComponent(token), { method: "DELETE" });
1366
+ const resp = await fetch(req, { headers: { "cookie": auth.cookie, "eteamsid": auth.eteamsid, "accept": "application/json" } });
1367
+ const text = await resp.text();
1368
+ let r; try { r = JSON.parse(text); } catch { r = { data: { raw: text } }; }
1369
+ if (json) return r;
1370
+ if (!expOk(r)) throw new Error(expFailMsg(r));
1371
+ console.log("锁已释放");
1372
+ return;
1373
+ }
1374
+ // acquire(默认)
1375
+ const body = { branchName: branch, componentKey: args.component || "ROOT", ttlMinutes: parseInt(args.ttl || "30", 10) };
1376
+ const r = await postJson("/api/dw/expert-repository/v1/repositories/locks?expertCode=" + encodeURIComponent(expert), body);
1377
+ if (json) return r;
1378
+ if (!expOk(r)) throw new Error(expFailMsg(r));
1379
+ console.log("== 锁已获取(记录 lockToken 供 heartbeat/release/commit 使用)==");
1380
+ console.log(JSON.stringify(r.data ?? {}, null, 1));
1381
+ }
1382
+ async function expRelease(args, json) {
1383
+ const expert = need("expert", args);
1384
+ const r = await postJson("/api/dw/expert-repository/v1/repositories/releases?expertCode=" + encodeURIComponent(expert),
1385
+ { commitId: need("commit", args), versionTag: need("version", args), releaseNote: args.note || "" });
1386
+ if (json) return r;
1387
+ if (!expOk(r)) throw new Error(expFailMsg(r));
1388
+ console.log("== 发布成功 =="); console.log(JSON.stringify(r.data ?? {}, null, 1));
1389
+ }
1390
+ async function expRollback(args, json) {
1391
+ const expert = need("expert", args);
1392
+ const r = await postJson("/api/dw/expert-repository/v1/repositories/releases/rollback?expertCode=" + encodeURIComponent(expert) + "&versionTag=" + encodeURIComponent(need("version", args)), {});
1393
+ if (json) return r;
1394
+ if (!expOk(r)) throw new Error(expFailMsg(r));
1395
+ console.log("== 回滚成功 =="); console.log(JSON.stringify(r.data ?? {}, null, 1));
1396
+ }
1397
+
1398
+ // ---------------- main ----------------
1399
+ function usage() {
1400
+ console.log(`e10-data CLI(data-assistant 数据接口统一命令行,2026-09-03 扩展)
1401
+ 用法: node e10-data.mjs <族> <子命令> [--参数] 通用 --json 原样输出
1402
+
1403
+ query DSL 数据查询 executeQueryDsl
1404
+ query --conn <connId> --model <ebuilder|flow|eteams|attend|hr|document|dw|blog> --schema <数据库> [--table-id <流程ID>] [--debug true|false] --dsl @file.json|'{...}'
1405
+ ds 数据源(业务模块)取数与元数据
1406
+ ds tables --group <gid> [--name 关键词]
1407
+ ds fields --group <gid> --table <tid>
1408
+ ds list --group <gid> --table <tid> --fields a,b [--where @f|json] [--order "f asc"] [--page N] [--size N]
1409
+ ds chart --group <gid> --table <tid> --group-by g1,g2 --stat '[{"fieldId":"x","aggregator":"SUM"}]' [--where ...]
1410
+ ds stat --group <gid> --table <tid> --stat '[...]' [--where ...]
1411
+ ds count --group <gid> --table <tid> [--where ...]
1412
+ (ds 通用 --source-type LOGIC|ETEAMS,默认 LOGIC)
1413
+ (ds 取数 list|chart|stat|count 的 appId:默认自动解析并带上——顺序 chat_bi_eb_config_map(dw 权限配置表,按 obj_id)
1414
+ → ebdf_obj(ec_edcapp,按 id),每次实时查、不缓存;也可 --app-id <id> 显式指定、--app-id auto 强制自动、
1415
+ --no-app-id 完全跳过自动解析(最快,适用于无需 appId 的数据集);ds tables/fields 也接受 --app-id)
1416
+ --where 过滤条件(v0.1.8 起支持树形 OR,递归):
1417
+ 数组 = AND 根: [{"fieldId":"f1","conditionType":"Date","compareType":"range","conditionValue":"2026-09~2026-10"},{"fieldId":"f2",...}]
1418
+ 对象 = 完整树: {"relationship":"0","conditionList":[{"fieldId":"f1",...},{"fieldId":"f2",...}]} # "0"=OR、"1"=AND,缺省 AND
1419
+ 数组元素可内嵌条件组 {"relationship":"0","conditionList":[...]} 实现 (A AND B) OR (C AND D)
1420
+ 叶子键 fieldId/conditionId 均可(fieldId 优先);缺少字段的叶子会直接报错(不再静默当全表)
1421
+ doc 文档查询
1422
+ doc count [--name 关键词] [--creator id1,id2] [--author id1,id2] [--content-type office] [--time "起~止"] [--last-time "起~止"] [--folder id,id|--folder-neq id,id]
1423
+ doc list [同上条件] [--all 翻页到底] [--page N] [--size 100]
1424
+ noun 词库查询
1425
+ noun list --skill <skillKey> [--source-type LOGIC] [--source-group gid] [--source-table tid](数据源场景带三参数)
1426
+ field 关联字段翻译
1427
+ field id2name --module <m> --type <t> --ids id1,id2 [--form-param '{"fieldId":"..","module":"ebuilderform"}'|--eb-browser-param '{..}'|--property-type x]
1428
+ field name2id --module <m> --type <t> --name 名称 [同上附加参数]
1429
+ conf 环境参数配置中心(chatBi config map,替代 config-resolve 脚本 URL 直连;confType 默认由 CONN_/DS_/WF_/API_ 前缀推导,可 --type 显式)
1430
+ confGroup(配置分组,v0.1.9 新增,可选):confType=api 时后端支持二级分层(如 api/workflow),
1431
+ 定位维度 = confType+confGroup+confKey;仅 confType=api(或显式传 --group)时才下发 confGroup,其他类型不带该字段
1432
+ conf get <key> [--type t] [--group g] # 单查(confValue/confAttrs/confName 全量 JSON)
1433
+ conf batch <key1> [key2 ...] [--type t] [--group g] # 批量查(批量接口异常自动降级逐条单查)
1434
+ conf batch-file @items.json # 按 [{confType,confGroup,confKey}..] 显式批量查(支持任意 confType)
1435
+ conf group <confType> <confGroup> [--key k] # 按类型+分组批量查(batchGetChatBiConfigMapByTypeAndGroup)
1436
+ conf group-file @items.json # 按 [{"confType":"api","confGroup":"workflow"}..] 多分组批量查
1437
+ conf resolve '<含#{KEY}文本>'|@file.txt [--group g] # 文本占位符替换为真实值(含中文建议 @file)
1438
+ conf resolve-file <文件路径> [--group g] # 读文件替换全部占位符并打印(不改写原文件)
1439
+ conf add <key> --value <真实值> [--name 说明] [--attrs '{"request":..,"response":..}'] [--type t] [--group g] [--force] # 新增(默认判重,已存在跳过)
1440
+ conf add-file @items.json # JSON 数组批量新增(逐条判重,元素可带 confGroup)
1441
+ conf update <key> [--value v] [--name n] [--attrs a] [--type t] [--group g] # 更新(confType+confGroup+confKey 定位,传哪个字段更哪个)
1442
+ conf delete <key> [--type t] [--group g] # 删除
1443
+ 示例: conf group api workflow | conf add GET_WORKFLOW_LIST --type api --group workflow --value /api/workflow/getList --name 查询流程列表
1444
+ exp 专家包版本管理(expert-repository/v1,响应以 data.actionMsg.code=0 判成功)
1445
+ exp status --expert <code> [--branch main] # 仓库状态(headCommitId/headRevision=提交基线)
1446
+ exp manifest --expert <code> [--commit <id>] # 文件清单(sha256)
1447
+ exp commits --expert <code> # 提交历史
1448
+ exp file --expert <code> --commit <id> --path <相对路径> [--out 本地文件]
1449
+ exp pull --expert <code> --dir <本地专家包目录> [--dry-run] [--force] # 智能拉取:基线三方对比(仅远端改→覆盖/仅本地改→保留/双方都改→备份后覆盖;--force 一律覆盖),成功后更新 .sync-state.json 基线
1450
+ exp init --expert <code> --dir <本地目录> [--display-name X] [--message 初始化导入]
1451
+ exp commit --expert <code> --dir <本地目录> --message "..." [--mode FULL_SNAPSHOT|FILE_PATCH]
1452
+ exp lock --expert <code> --action query|acquire(默认)|heartbeat|release --component ROOT --ttl 30 --lock-token <token>
1453
+ exp release --expert <code> --commit <id> --version vX.Y.Z [--note 说明]
1454
+ exp rollback --expert <code> --version vX.Y.Z
1455
+ attend 考勤 REST 接口(2026-09-07 新增,hr-attendance 病假/事假等假期余额)
1456
+ attend statis [--user <人员ID>] [--begin 2026-09-01] [--end 2026-09-30] # getAttendInfoStatis 假期余额统计;--user 默认当前登录用户,--begin/--end 默认当月
1457
+ 认证: 优先读权威源 ~/.e10-cli/profiles/<active>/auth(自动同步镜像 ~/.workbuddy/e10-login-cache.md);皆无请先 e10-login oidc`);
1458
+ }
1459
+
1460
+ async function main() {
1461
+ const rawArgv = process.argv.slice(2);
1462
+ // 仅当顶层显式 -v/--version(无子命令)时打印版本;exp release/rollback 的 --version 是版本号参数、conf 文本可能含 version 字样,一律放行
1463
+ if (rawArgv.length === 1 && (rawArgv[0] === "--version" || rawArgv[0] === "-v")) {
1464
+ console.log("e10-data " + cliVersion());
1465
+ process.exit(0);
1466
+ }
1467
+ const { args, pos } = parseArgs(rawArgv);
1468
+ if (!pos.length) { usage(); process.exit(1); }
1469
+ const family = pos[0], sub = pos[1], json = !!args.json;
1470
+ let result;
1471
+ if (family === "ds") {
1472
+ if (!sub) { usage(); process.exit(1); }
1473
+ switch (sub) {
1474
+ case "tables": result = await cmdTables(args, json); break;
1475
+ case "fields": result = await cmdFields(args, json); break;
1476
+ case "list": result = await cmdList(args, json); break;
1477
+ case "chart": result = await cmdChart(args, json); break;
1478
+ case "stat": result = await cmdStat(args, json); break;
1479
+ case "count": result = await cmdCount(args, json); break;
1480
+ default: usage(); process.exit(1);
1481
+ }
1482
+ } else if (family === "query") {
1483
+ result = await cmdQuery(args, json);
1484
+ } else if (family === "doc") {
1485
+ if (sub === "count") result = await cmdDocCount(args, json);
1486
+ else if (sub === "list") result = await cmdDocList(args, json);
1487
+ else { usage(); process.exit(1); }
1488
+ } else if (family === "noun") {
1489
+ if (sub === "list") result = await cmdNoun(args, json);
1490
+ else { usage(); process.exit(1); }
1491
+ } else if (family === "field") {
1492
+ if (sub === "id2name") result = await cmdFieldId2name(args, json);
1493
+ else if (sub === "name2id") result = await cmdFieldName2id(args, json);
1494
+ else { usage(); process.exit(1); }
1495
+ } else if (family === "conf") {
1496
+ if (!sub) { usage(); process.exit(1); }
1497
+ result = await cmdConf(sub, args, json, pos);
1498
+ } else if (family === "exp") {
1499
+ if (!sub) { usage(); process.exit(1); }
1500
+ switch (sub) {
1501
+ case "status": result = await expStatus(args, json); break;
1502
+ case "manifest": result = await expManifest(args, json); break;
1503
+ case "commits": result = await expCommits(args, json); break;
1504
+ case "file": result = await expFile(args, json); break;
1505
+ case "pull": result = await expPull(args, json); break;
1506
+ case "init": result = await expInit(args, json); break;
1507
+ case "commit": result = await expCommit(args, json); break;
1508
+ case "lock": result = await expLock(args, json); break;
1509
+ case "release": result = await expRelease(args, json); break;
1510
+ case "rollback": result = await expRollback(args, json); break;
1511
+ default: usage(); process.exit(1);
1512
+ }
1513
+ } else if (family === "attend") {
1514
+ if (sub === "statis") result = await cmdAttendStatis(args, json);
1515
+ else { usage(); process.exit(1); }
1516
+ } else { usage(); process.exit(1); }
1517
+ if (json && result !== undefined) console.log(JSON.stringify(result, null, 1));
1518
+ }
1519
+ main().catch((e) => { console.error("ERROR: " + e.message); process.exit(1); });