e10-data 0.1.9 → 0.1.12

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 +33 -2
  2. package/bin/e10-data.mjs +239 -68
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -9,7 +9,7 @@ npm i -g e10-data # 全局安装
9
9
  e10-data --version # 验证
10
10
  ```
11
11
 
12
- > 依赖:认证复用 `weaver-e10-login` 的登录态。**认证单一权威源 = `~/.e10-cli/profiles/<active>/auth`**(e10-login 统一认证,与所有专家/工具共享);CLI 进程内读取并解密(AES-256-GCM,seed 与 e10-login 一致),随后自动回写同步明文镜像 `~/.workbuddy/e10-login-cache.md`(仅供查看/兜底)。请先安装并登录 weaver-e10-login(`e10-login oidc`),登录态失效时 CLI 会提示重新登录。
12
+ > 依赖:认证复用 `weaver-e10-login` 的登录态。**加解密全部由 `e10-login` 负责,本 CLI 不实现任何加解密**(auth 文件由 e10-login 自己的密钥体系加密:1.5.0 起为「每机随机密钥 + OS keychain」,早期版本为固定 seed,只有 e10-login 解得开)。CLI 通过 e10-login 的命令出口取明文凭据,优先级:**① `e10-login auth --json`**(推荐的正式契约)→ **② `e10-login whoami --json`**(其 JSON 带 `cookies` 时)→ **③ `e10-login whoami`**(文本解析,当前实际路径)→ **④ 明文镜像 `~/.workbuddy/e10-login-cache.md`**(兜底,命中时打印告警)。请先安装并登录 weaver-e10-login(`e10-login oidc`)。
13
13
 
14
14
  ## 命令总览
15
15
 
@@ -29,7 +29,7 @@ e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-atte
29
29
  - `--json`:原样输出接口 JSON(供脚本/Agent 消费);默认人读输出。
30
30
  - 复杂 JSON/中文参数:推荐 `@文件` 承载(如 `--dsl @q.json`、`--where @cond.json`)。
31
31
  - ⚠️ 大整数精度(自动处理):E10 的 ID 多为 19 位,超过 JS 安全整数(2^53-1≈9e15)。CLI 解析 JSON 时**自动把超范围裸整数转成字符串**再发请求/输出(实测后端接受字符串做 =/IN/BETWEEN 数值比较),无需手动加引号,也不误伤字符串正文。
32
- - 认证:优先读权威源 `~/.e10-cli/profiles/<active>/auth`(进程内 AES-256-GCM 解密)并自动同步明文镜像 `~/.workbuddy/e10-login-cache.md`;两者皆无提示先执行 `e10-login oidc`。
32
+ - 认证:**加解密由 `e10-login` 负责(本 CLI 不参与)**。凭据出口优先级:`e10-login auth --json` → `e10-login whoami --json` → `e10-login whoami`(文本)→ 明文镜像 `~/.workbuddy/e10-login-cache.md`(兜底,命中时告警)。命中的出口会记入 `~/.e10-cli/.e10-data-auth-mode`(只存模式名、不存凭据),下次优先复用。环境变量:`E10_DATA_AUTH_PROBE=1` 强制重新探测、`E10_DATA_AUTH_MODE=<模式名>` 手动指定、`E10_DATA_QUIET=1` 抑制告警(告警只走 stderr)。
33
33
  - 版本更新:`npm i -g e10-data@latest`;查看源上最新 `npm view e10-data version`。
34
34
 
35
35
  ## 用法示例
@@ -112,6 +112,37 @@ e10-data conf delete GET_WORKFLOW_LIST --type api --group workflow
112
112
  - 同一 `confKey` 可存在于多个 `confGroup`;`conf batch` / `batch-file` 的返回映射在带分组时以 `组|键` 为键,不会互相覆盖。
113
113
  - `--json` 输出:`conf group` 返回配置数组(带 `--key` 时返回单条),`conf batch` 返回映射对象。
114
114
 
115
+ ## ds appId 自动解析(v0.1.11 新增)
116
+
117
+ ChatBI 数据源取数接口(`getListDatas` / `getChartDatas` / `getStatDatas` / `getCountData`)对**部分 EB 应用(独立部署)**的数据集,必须在 `queryDto` 中携带 `appId`,否则后端一律返回 `code=500 请求失败`(实测:小餐厅应用 `840360791696547845` 名下 4 张表不传全 500、传即 200;低代码大赛等应用不传也正常)。
118
+
119
+ `e10-data ds list|chart|stat|count` 现**默认自动解析并带上 appId**,解析顺序:
120
+
121
+ 1. `--app-id <id>` 显式指定(最高优先,0 额外请求);
122
+ 2. 本地缓存 `~/.e10-cli/.e10-data-ds-appid.json`(key = `sourceType|groupId|tableId`,TTL 7 天);
123
+ 3. `ebdf_obj`(DSL 查 `ec_edcapp.ebdf_obj`,`id = tableId` → `app_id`)—— EB 表单权威来源,请求最小;
124
+ 4. `ds tables`(`listByPage`)按 id 匹配,命中后把**整组** `id→appId` 映射一次性写入缓存;
125
+ 5. `ds fields` 扫字段 `config.appId`(Ebuilder 类字段携带);
126
+ 6. 均无 → 不带 appId 发送(兼容旧行为)。
127
+
128
+ ```bash
129
+ # 默认:自动解析(推荐)
130
+ e10-data ds count --group weaver-ebuilder-form-service --table 840360795890851856
131
+
132
+ # 显式指定(跳过所有解析,最快)
133
+ e10-data ds count --group weaver-ebuilder-form-service --table 840360795890851856 --app-id 840360791696547845
134
+
135
+ # 完全跳过自动解析(明确不需要 appId 的数据集,省一次解析往返)
136
+ e10-data ds count --group weaver-ebuilder-form-service --table 1081139888007225347 --no-app-id
137
+
138
+ # 查看某张表的 appId:ds tables 输出新增 appId 列;ds fields 命中时在表头打印 appId=…
139
+ e10-data ds tables --group weaver-ebuilder-form-service --name 低代码大赛
140
+ e10-data ds fields --group weaver-ebuilder-form-service --table 840360795890851856
141
+ ```
142
+
143
+ > ⚠️ `ds tables`(`listByPage`)**不覆盖全部数据集**:实测「小餐厅」应用的 4 张表在该清单(共 997 条)里查不到,其 appId 由第 3 步 `ebdf_obj` 兜底拿到——故解析顺序把 `ebdf_obj` 置于 `ds tables` 之前。
144
+ > 实测对照(带 appId 与 DSL 口径一致):`ds count` + `ydlc isNotNull` = **6470**,与 DSL 同口径结果一致;`ds chart` 按区域分组 = 上海 5123 / 北京 165 / 区域空 1182。
145
+
115
146
  ## ds --where 过滤条件(v0.1.8 起支持树形 OR)
116
147
 
117
148
  `e10-data ds list|chart|stat|count --where` 接收**条件数组**或**完整条件树**,均可递归嵌套表达 AND/OR:
package/bin/e10-data.mjs CHANGED
@@ -6,6 +6,9 @@
6
6
  * 覆盖(数据查询链路全部家族;登录/HRM 复用 e10-login / hrm-cli,checkEbModulePermission 已废弃不提供):
7
7
  * e10-data query DSL 查询 executeQueryDsl(conn/model/schema/tableId/dslQuery/debug)
8
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 跳过(最快)
9
12
  * e10-data doc count|list 文档查询 documentListCount / documentList
10
13
  * e10-data noun list 词库 getChatDocsBySkillKey
11
14
  * e10-data field id2name|name2id 关联字段翻译 testRemoteBrowser / complete
@@ -15,7 +18,9 @@
15
18
  * e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-attendance 病假/事假,2026-09-07)
16
19
  *
17
20
  * 通用:--json 原样输出;复杂 JSON 参数支持 @文件 或 内联(中文走 @文件 更稳)
18
- * 认证:进程内读取权威源 ~/.e10-cli/profiles/<active>/auth(e10-login 统一认证,AES-256-GCM seed 硬编码纯 JS 解密,v0.1.7 起),并自动回写同步明文镜像 ~/.workbuddy/e10-login-cache.md;两者皆无时提示先 e10-login
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 登录
19
24
  *
20
25
  * 用法示例:
21
26
  * node e10-data.mjs query --conn 1144919996804022272 --model dw --schema dw --dsl @q.json
@@ -29,6 +34,7 @@ import fs from "node:fs";
29
34
  import os from "node:os";
30
35
  import path from "node:path";
31
36
  import crypto from "node:crypto";
37
+ import { spawnSync } from "node:child_process";
32
38
  import { fileURLToPath } from "node:url";
33
39
 
34
40
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -40,70 +46,148 @@ function cliVersion() {
40
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";
41
47
  const EB_BIZ_ID = "10000000000000000";
42
48
 
43
- // ---------------- 认证 ----------------
44
- // 认证单一权威源:~/.e10-cli/profiles/<active>/auth(e10-login / e10-cli 统一认证,与所有专家共享,无需各自存登录信息)。
45
- // AES-256-GCM 加密,seed 与 e10-login 一致(ccw_eteams_auth_v1_8a3f2c1b9e4d → sha256),纯 JS 进程内可解、零外部依赖。
46
- // 明文镜像:~/.workbuddy/e10-login-cache.md(历史遗留,仅供查看/兜底;读到权威源后自动回写同步,不再需要手工维护)。
47
- const E10_AUTH_SEED = "ccw_eteams_auth_v1_8a3f2c1b9e4d";
48
- function decryptAuthWrapper(wrapper) {
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";
49
67
  try {
50
- const combined = Buffer.from(wrapper.data, "base64");
51
- const IV = 12, TAG = 16;
52
- if (combined.length < IV + TAG + 1) return null;
53
- const key = crypto.createHash("sha256").update(E10_AUTH_SEED).digest();
54
- const decipher = crypto.createDecipheriv("aes-256-gcm", key, combined.subarray(0, IV), { authTagLength: TAG });
55
- decipher.setAuthTag(combined.subarray(combined.length - TAG));
56
- return JSON.parse(Buffer.concat([decipher.update(combined.subarray(IV, combined.length - TAG)), decipher.final()]).toString("utf-8"));
57
- } catch { return null; }
58
- }
59
- function parseAuthPayload(raw) {
60
- try { // 兼容两种形态:加密 wrapper({enc:true,data:base64})与明文 JSON({cookies,userId,...})
61
- const obj = JSON.parse(raw);
62
- if (obj && obj.enc === true && typeof obj.data === "string") return decryptAuthWrapper(obj);
63
- if (obj && obj.cookies && (obj.userId || obj.cookies.ETEAMSID)) return obj;
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; }
64
70
  } catch {}
65
- return null;
66
- }
67
- function cookieFromAuthData(data) { // 组装完整 cookie 串:cookies.raw 优先,逐项补齐 langType/agentType/isAgent
68
- const eteamsid = (data && data.cookies && data.cookies.ETEAMSID) || "";
69
- const agent = (data && (data.agentType || data.agent_type)) || "workbuddy";
70
- let c = (data && data.cookies && data.cookies.raw) || "";
71
- if (c && !/langType=/.test(c)) c = "langType=zh_CN; " + c;
72
- if (c && agent && !/agentType=/.test(c)) c += "; agentType=" + agent;
73
- if (c && !/isAgent=/.test(c)) c += "; isAgent=true";
74
- if (!c && eteamsid) c = "langType=zh_CN; ETEAMSID=" + eteamsid + "; agentType=" + agent + "; isAgent=true";
75
- return { eteamsid, cookie: c };
76
- }
77
- function authFromE10Cli() { // ① 权威源:~/.e10-cli/profiles/<active>/auth
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) {
78
77
  try {
79
- const home = os.homedir();
80
- let profile = "default";
81
- try {
82
- const activeFile = path.join(home, ".e10-cli", "profile");
83
- if (fs.existsSync(activeFile)) { const n = fs.readFileSync(activeFile, "utf-8").trim(); if (n) profile = n; }
84
- } catch {}
85
- const p = path.join(home, ".e10-cli", "profiles", profile, "auth");
86
- if (!fs.existsSync(p)) return null;
87
- const data = parseAuthPayload(fs.readFileSync(p, "utf-8"));
88
- if (!data || !data.cookies || !data.cookies.ETEAMSID || !data.baseUrl) return null;
89
- const { eteamsid, cookie } = cookieFromAuthData(data);
90
- return { baseUrl: data.baseUrl, eteamsid, cookie, userId: data.userId || "", tenantKey: data.tenantKey || "", profile };
91
- } catch { return null; }
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
+ });
92
149
  }
93
- function authFromCacheMd() { // ② 兜底:历史明文缓存 ~/.workbuddy/e10-login-cache.md
150
+
151
+ /** ④ 明文镜像(最后兜底:不涉及任何解密,但可能已过期 → 命中时告警) */
152
+ function providerCacheMd() {
94
153
  const p = path.join(os.homedir(), ".workbuddy", "e10-login-cache.md");
95
154
  if (!fs.existsSync(p)) return null;
96
155
  const txt = fs.readFileSync(p, "utf-8");
97
156
  const get = (key) => {
98
- const m = txt.match(new RegExp("\\*\\*" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\*\\*\\s*[::]\\s*`?([^`\\n]+)`?"));
99
- return m ? m[1].trim() : "";
157
+ const mm = txt.match(new RegExp("\\*\\*" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\*\\*\\s*[::]\\s*`?([^`\\n]+)`?"));
158
+ return mm ? mm[1].trim() : "";
100
159
  };
101
- const auth = { baseUrl: get("baseUrl"), eteamsid: get("ETEAMSID"), cookie: get("完整 Cookie 串"), userId: get("userId") || get("employeeId"), tenantKey: get("tenantKey") };
102
- if (!auth.baseUrl || !auth.eteamsid) return null;
103
- if (!auth.cookie) auth.cookie = "langType=zh_CN; ETEAMSID=" + auth.eteamsid + "; agentType=workbuddy; isAgent=true";
104
- return auth;
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 {}
105
187
  }
106
- function syncCacheMd(auth) { // 读到权威源后自动回写明文镜像(内容无变化不写盘),根治"重登后手工同步"问题
188
+
189
+ /** 凭据取自 e10-login 时,顺手把明文镜像刷新到最新(保持镜像可用;镜像不参与正常取数) */
190
+ function syncCacheMd(auth) {
107
191
  try {
108
192
  const p = path.join(os.homedir(), ".workbuddy", "e10-login-cache.md");
109
193
  if (!fs.existsSync(p) || !/🔐 登录信息缓存/.test(fs.readFileSync(p, "utf-8"))) return; // 非标准缓存文件不动它
@@ -117,16 +201,47 @@ function syncCacheMd(auth) { // 读到权威源后自动回写明文镜像(内
117
201
  txt = n;
118
202
  };
119
203
  rep("baseUrl", auth.baseUrl); rep("ETEAMSID", auth.eteamsid); rep("完整 Cookie 串", auth.cookie); rep("userId", auth.userId || ""); rep("tenantKey", auth.tenantKey || "");
120
- rep("缓存记录时间", ts + "(e10-data 自动同步自 ~/.e10-cli/profiles/" + (auth.profile || "") + "/auth)");
204
+ rep("缓存记录时间", ts + "(e10-data 自动同步,凭据来源:" + (auth.source || "") + ")");
121
205
  if (changed) fs.writeFileSync(p, txt, "utf-8");
122
206
  } catch {}
123
207
  }
208
+
209
+ /** 取凭据:按优先级依次尝试各出口;命中的模式会被记住(下次优先尝试它,省一次进程调用) */
124
210
  function loadAuth() {
125
- const cli = authFromE10Cli(); // ① 权威源优先(e10-login 统一认证,多专家共享)
126
- if (cli) { syncCacheMd(cli); return cli; }
127
- const cached = authFromCacheMd(); // ② 权威源缺失时回退旧明文缓存
128
- if (cached) return cached;
129
- throw new Error("未找到登录认证(~/.e10-cli/profiles 与 ~/.workbuddy/e10-login-cache.md 均无),请先执行 e10-login oidc / set 登录");
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
+ );
130
245
  }
131
246
  const auth = loadAuth();
132
247
  const BASE = auth.baseUrl.replace(/\/+$/, "");
@@ -135,7 +250,11 @@ const USER = { id: auth.userId || "", tenantKey: auth.tenantKey || "" };
135
250
  // ---------------- HTTP ----------------
136
251
  function checkAuthFail(resp, text, j) {
137
252
  if (resp.status === 401 || (j && String(j.code) === "-1" && /token|登录|失效/i.test(String(j.msg || "")))) {
138
- throw new Error("登录态失效(" + (j.msg || resp.status) + "),请重新 e10-login oidc 登录");
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);
139
258
  }
140
259
  }
141
260
  async function postJson(endpoint, bodyObj, extraHeaders = {}) {
@@ -377,17 +496,66 @@ function parseOrder(val) {
377
496
  return { fieldId: f, fieldName: f, objId: "", isAsc: (dir || "asc").toLowerCase() !== "desc", mainField: true };
378
497
  });
379
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
+ }
380
546
  async function cmdTables(args, json) {
381
547
  const gid = need("group", args);
382
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"];
383
550
  const r = await postMultipart("/api/ebuilder/common/ds/listByPage", form);
384
551
  if (json) return r;
385
552
  const recs = ((r.data || {}).records) || [];
386
- printHuman("分组表清单 (total=" + ((r.data || {}).total) + ")", recs, ["id", "name", "tableText", "detailTable", "usage"]);
553
+ printHuman("分组表清单 (total=" + ((r.data || {}).total) + ")", recs, ["id", "name", "appId", "tableText", "detailTable", "usage"]);
387
554
  }
388
555
  async function cmdFields(args, json) {
389
556
  const gid = need("group", args), tid = need("table", args);
390
- const form = { groupId: gid, sourceId: tid, sourceType: args["source-type"] || "LOGIC", detailFieldsGroup: "true", enableMerge: "true", needTableName: "true", params: "[]" };
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"];
391
559
  const r = await postMultipart("/api/ebuilder/common/ds/fields", form);
392
560
  if (json) return r;
393
561
  for (const d of (r.data || [])) {
@@ -398,7 +566,7 @@ async function cmdFields(args, json) {
398
566
  async function cmdList(args, json) {
399
567
  const gid = need("group", args), tid = need("table", args), fld = need("fields", args);
400
568
  const fields = fld.split(",").map((f) => ({ fieldId: f, fieldName: f, objId: String(tid), mainField: true }));
401
- const dto = baseDto(tid, gid, parseInt(args.page || "1", 10), parseInt(args.size || "20", 10), "getListData");
569
+ const dto = await dsDto(args, tid, gid, parseInt(args.page || "1", 10), parseInt(args.size || "20", 10), "getListData");
402
570
  dto.queryFields = fields;
403
571
  dto.orderFields = parseOrder(args.order);
404
572
  const ct = parseWhere(args.where); if (ct) dto.conditionTrees = ct;
@@ -411,7 +579,7 @@ async function cmdList(args, json) {
411
579
  }
412
580
  async function cmdChart(args, json) {
413
581
  const gid = need("group", args), tid = need("table", args), gb = need("group-by", args);
414
- const dto = baseDto(tid, gid, parseInt(args.page || "1", 10), parseInt(args.size || "100", 10), "getChartData");
582
+ const dto = await dsDto(args, tid, gid, parseInt(args.page || "1", 10), parseInt(args.size || "100", 10), "getChartData");
415
583
  dto.groupFields = gb.split(",").map((f) => ({ fieldId: f, fieldName: f, objId: String(tid), mainField: true }));
416
584
  dto.groupStatFields = parseStat(need("stat", args)).map((s) => ({ ...s, objId: String(tid) }));
417
585
  const ct = parseWhere(args.where); if (ct) dto.conditionTrees = ct;
@@ -421,7 +589,7 @@ async function cmdChart(args, json) {
421
589
  }
422
590
  async function cmdStat(args, json) {
423
591
  const gid = need("group", args), tid = need("table", args);
424
- const dto = baseDto(tid, gid, 1, 10, "getStatData");
592
+ const dto = await dsDto(args, tid, gid, 1, 10, "getStatData");
425
593
  dto.statFields = parseStat(need("stat", args)).map((s) => ({ ...s, objId: String(tid) }));
426
594
  const ct = parseWhere(args.where); if (ct) dto.conditionTrees = ct;
427
595
  const r = await postMultipart("/api/dw/newetl/chatBi/ds/getStatDatas", { sourceType: args["source-type"] || "LOGIC", queryDto: dto });
@@ -432,7 +600,7 @@ async function cmdStat(args, json) {
432
600
  }
433
601
  async function cmdCount(args, json) {
434
602
  const gid = need("group", args), tid = need("table", args);
435
- const dto = baseDto(tid, gid, 1, 10, "getCountData");
603
+ const dto = await dsDto(args, tid, gid, 1, 10, "getCountData");
436
604
  const ct = parseWhere(args.where); if (ct) dto.conditionTrees = ct;
437
605
  const r = await postMultipart("/api/dw/newetl/chatBi/ds/getCountData", { sourceType: args["source-type"] || "LOGIC", queryDto: dto });
438
606
  if (json) return r;
@@ -1242,6 +1410,9 @@ ds 数据源(业务模块)取数与元数据
1242
1410
  ds stat --group <gid> --table <tid> --stat '[...]' [--where ...]
1243
1411
  ds count --group <gid> --table <tid> [--where ...]
1244
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)
1245
1416
  --where 过滤条件(v0.1.8 起支持树形 OR,递归):
1246
1417
  数组 = AND 根: [{"fieldId":"f1","conditionType":"Date","compareType":"range","conditionValue":"2026-09~2026-10"},{"fieldId":"f2",...}]
1247
1418
  对象 = 完整树: {"relationship":"0","conditionList":[{"fieldId":"f1",...},{"fieldId":"f2",...}]} # "0"=OR、"1"=AND,缺省 AND
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "e10-data",
3
- "version": "0.1.9",
4
- "description": "泛微 E10 数据接口统一 CLI(data-assistant 专家配套):DSL 查询/数据源/文档/词库/关联字段翻译/环境参数配置中心(conf,支持 confGroup 分组)/专家包版本管理,认证复用 weaver-e10-login 登录态",
3
+ "version": "0.1.12",
4
+ "description": "泛微 E10 数据接口统一 CLI(data-assistant 专家配套):DSL 查询/数据源/文档/词库/关联字段翻译/环境参数配置中心(conf,支持 confGroup 分组)/专家包版本管理;ds 取数(list/chart/stat/count)自动解析并携带 queryDto.appId(v0.1.12,chat_bi_eb_config_map → ebdf_obj;--app-id 显式/--no-app-id 跳过);认证加解密全部交由 weaver-e10-login(本 CLI 不再自行解密,通过 e10-login 命令出口获取凭据)",
5
5
  "author": "e10-dw",
6
6
  "license": "MIT",
7
7
  "type": "module",