e10-data 0.1.6 → 0.1.8

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 +41 -3
  2. package/bin/e10-data.mjs +118 -17
  3. package/package.json +1 -1
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` 的登录态(用户级缓存 `~/.workbuddy/e10-login-cache.md`)。请先安装并登录 weaver-e10-login(`e10-login oidc`),登录态失效时 CLI 会提示重新登录。
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 会提示重新登录。
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
- - 认证:进程内读取 `~/.workbuddy/e10-login-cache.md`;失效提示重新执行 `e10-login oidc`。
32
+ - 认证:优先读权威源 `~/.e10-cli/profiles/<active>/auth`(进程内 AES-256-GCM 解密)并自动同步明文镜像 `~/.workbuddy/e10-login-cache.md`;两者皆无提示先执行 `e10-login oidc`。
33
33
  - 版本更新:`npm i -g e10-data@latest`;查看源上最新 `npm view e10-data version`。
34
34
 
35
35
  ## 用法示例
@@ -53,6 +53,44 @@ e10-data attend statis # 查当前登录用户当月假
53
53
  e10-data attend statis --user 6264843388578732287 --begin 2026-09-01 --end 2026-09-30
54
54
  ```
55
55
 
56
+ ## ds --where 过滤条件(v0.1.8 起支持树形 OR)
57
+
58
+ `e10-data ds list|chart|stat|count --where` 接收**条件数组**或**完整条件树**,均可递归嵌套表达 AND/OR:
59
+
60
+ ```bash
61
+ # ① 数组 = AND 根(旧写法,行为不变):满足全部条件的记录
62
+ e10-data ds count --group <gid> --table <tid> \
63
+ --where '[{"fieldId":"fieldA","compareType":"range","conditionValue":"2026-09~2026-10"},{"fieldId":"fieldB","compareType":"eq","conditionValue":"1"}]'
64
+
65
+ # ② 对象 = 完整条件树:relationship "0"=OR、"1"=AND(缺省 AND),conditionList 内可再嵌套条件组
66
+ e10-data ds count --group <gid> --table <tid> \
67
+ --where '{"relationship":"0","conditionList":[{"fieldId":"fieldA","compareType":"range","conditionValue":"2026-09~2026-10"},{"fieldId":"fieldB","compareType":"range","conditionValue":"2026-09~2026-10"}]}'
68
+
69
+ # ③ 数组元素内嵌条件组 → 复杂组合,如 (A AND B) OR (C AND D)
70
+ e10-data ds list --group <gid> --table <tid> --fields a,b \
71
+ --where @cond.json # 内容见下
72
+ ```
73
+
74
+ ```json
75
+ // cond.json —— 组合条件示例
76
+ [
77
+ { "relationship": "1", "conditionList": [
78
+ { "fieldId": "A", "compareType": "eq", "conditionValue": "x" },
79
+ { "fieldId": "B", "compareType": "eq", "conditionValue": "y" } ] },
80
+ { "relationship": "0", "conditionList": [
81
+ { "fieldId": "C", "compareType": "eq", "conditionValue": "m" },
82
+ { "fieldId": "D", "compareType": "eq", "conditionValue": "n" } ] }
83
+ ]
84
+ ```
85
+
86
+ 要点:
87
+
88
+ - **叶子字段键**:`fieldId` 与 `conditionId` 均可(fieldId 优先),旧树形 `conditionId` 写法兼容;
89
+ - **relationship**:`"0"`/`0`/`false` = OR,其余 = AND;子节点关系作用于其自身 `conditionList`;
90
+ - 数组顶层恒为 AND 根,需要整体 OR 请用对象形态 `{"relationship":"0","conditionList":[...]}`;
91
+ - 叶子缺少字段会**直接报错**(v0.1.8 起,不再静默产生空条件导致误返回全表);
92
+ - 其余条件属性(conditionType/compareType/conditionValue/objId/mainField/ignoreCaseMark 等)照旧透传。
93
+
56
94
  ## exp pull 智能拉取(v0.1.4 新增,防本地修改被覆盖)`exp pull` 不再盲目覆盖。CLI 在专家包本地目录维护基线文件 `.sync-state.json`(记录上次同步的 commitId/headRevision 与每文件 sha256),拉取时逐文件三方对比:
57
95
 
58
96
  | 场景 | 判定 | 处理 |
@@ -69,7 +107,7 @@ e10-data attend statis --user 6264843388578732287 --begin 2026-09-01 --end 2026-
69
107
 
70
108
  ## attend statis 考勤假期余额(v0.1.5 新增)
71
109
 
72
- 封装 E10 考勤 REST 接口 `POST /api/attend/web/attendInfoV2/getAttendInfoStatis`(hr-attendance 场景六:病假/事假/年假余额统计,替代原手工 curl)。认证/绕代理由 CLI 进程内自动完成(读 `~/.workbuddy/e10-login-cache.md`)。
110
+ 封装 E10 考勤 REST 接口 `POST /api/attend/web/attendInfoV2/getAttendInfoStatis`(hr-attendance 场景六:病假/事假/年假余额统计,替代原手工 curl)。认证/绕代理由 CLI 进程内自动完成(权威源 `~/.e10-cli/profiles/<active>/auth`)。
73
111
 
74
112
  ```bash
75
113
  e10-data attend statis [--user <人员ID>] [--begin 2026-09-01] [--end 2026-09-30]
package/bin/e10-data.mjs CHANGED
@@ -14,7 +14,7 @@
14
14
  * e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-attendance 病假/事假,2026-09-07)
15
15
  *
16
16
  * 通用:--json 原样输出;复杂 JSON 参数支持 @文件 或 内联(中文走 @文件 更稳)
17
- * 认证:进程内读取用户级登录缓存 ~/.workbuddy/e10-login-cache.md;不存在/失效提示先 e10-login
17
+ * 认证:进程内读取权威源 ~/.e10-cli/profiles/<active>/auth(e10-login 统一认证,AES-256-GCM seed 硬编码纯 JS 解密,v0.1.7 起),并自动回写同步明文镜像 ~/.workbuddy/e10-login-cache.md;两者皆无时提示先 e10-login
18
18
  *
19
19
  * 用法示例:
20
20
  * node e10-data.mjs query --conn 1144919996804022272 --model dw --schema dw --dsl @q.json
@@ -40,19 +40,93 @@ const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
40
40
  const EB_BIZ_ID = "10000000000000000";
41
41
 
42
42
  // ---------------- 认证 ----------------
43
- function loadAuth() {
43
+ // 认证单一权威源:~/.e10-cli/profiles/<active>/auth(e10-login / e10-cli 统一认证,与所有专家共享,无需各自存登录信息)。
44
+ // AES-256-GCM 加密,seed 与 e10-login 一致(ccw_eteams_auth_v1_8a3f2c1b9e4d → sha256),纯 JS 进程内可解、零外部依赖。
45
+ // 明文镜像:~/.workbuddy/e10-login-cache.md(历史遗留,仅供查看/兜底;读到权威源后自动回写同步,不再需要手工维护)。
46
+ const E10_AUTH_SEED = "ccw_eteams_auth_v1_8a3f2c1b9e4d";
47
+ function decryptAuthWrapper(wrapper) {
48
+ try {
49
+ const combined = Buffer.from(wrapper.data, "base64");
50
+ const IV = 12, TAG = 16;
51
+ if (combined.length < IV + TAG + 1) return null;
52
+ const key = crypto.createHash("sha256").update(E10_AUTH_SEED).digest();
53
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, combined.subarray(0, IV), { authTagLength: TAG });
54
+ decipher.setAuthTag(combined.subarray(combined.length - TAG));
55
+ return JSON.parse(Buffer.concat([decipher.update(combined.subarray(IV, combined.length - TAG)), decipher.final()]).toString("utf-8"));
56
+ } catch { return null; }
57
+ }
58
+ function parseAuthPayload(raw) {
59
+ try { // 兼容两种形态:加密 wrapper({enc:true,data:base64})与明文 JSON({cookies,userId,...})
60
+ const obj = JSON.parse(raw);
61
+ if (obj && obj.enc === true && typeof obj.data === "string") return decryptAuthWrapper(obj);
62
+ if (obj && obj.cookies && (obj.userId || obj.cookies.ETEAMSID)) return obj;
63
+ } catch {}
64
+ return null;
65
+ }
66
+ function cookieFromAuthData(data) { // 组装完整 cookie 串:cookies.raw 优先,逐项补齐 langType/agentType/isAgent
67
+ const eteamsid = (data && data.cookies && data.cookies.ETEAMSID) || "";
68
+ const agent = (data && (data.agentType || data.agent_type)) || "workbuddy";
69
+ let c = (data && data.cookies && data.cookies.raw) || "";
70
+ if (c && !/langType=/.test(c)) c = "langType=zh_CN; " + c;
71
+ if (c && agent && !/agentType=/.test(c)) c += "; agentType=" + agent;
72
+ if (c && !/isAgent=/.test(c)) c += "; isAgent=true";
73
+ if (!c && eteamsid) c = "langType=zh_CN; ETEAMSID=" + eteamsid + "; agentType=" + agent + "; isAgent=true";
74
+ return { eteamsid, cookie: c };
75
+ }
76
+ function authFromE10Cli() { // ① 权威源:~/.e10-cli/profiles/<active>/auth
77
+ try {
78
+ const home = os.homedir();
79
+ let profile = "default";
80
+ try {
81
+ const activeFile = path.join(home, ".e10-cli", "profile");
82
+ if (fs.existsSync(activeFile)) { const n = fs.readFileSync(activeFile, "utf-8").trim(); if (n) profile = n; }
83
+ } catch {}
84
+ const p = path.join(home, ".e10-cli", "profiles", profile, "auth");
85
+ if (!fs.existsSync(p)) return null;
86
+ const data = parseAuthPayload(fs.readFileSync(p, "utf-8"));
87
+ if (!data || !data.cookies || !data.cookies.ETEAMSID || !data.baseUrl) return null;
88
+ const { eteamsid, cookie } = cookieFromAuthData(data);
89
+ return { baseUrl: data.baseUrl, eteamsid, cookie, userId: data.userId || "", tenantKey: data.tenantKey || "", profile };
90
+ } catch { return null; }
91
+ }
92
+ function authFromCacheMd() { // ② 兜底:历史明文缓存 ~/.workbuddy/e10-login-cache.md
44
93
  const p = path.join(os.homedir(), ".workbuddy", "e10-login-cache.md");
45
- if (!fs.existsSync(p)) throw new Error("未找到登录缓存 " + p + ",请先执行 e10-login oidc / set 登录");
94
+ if (!fs.existsSync(p)) return null;
46
95
  const txt = fs.readFileSync(p, "utf-8");
47
96
  const get = (key) => {
48
97
  const m = txt.match(new RegExp("\\*\\*" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\*\\*\\s*[::]\\s*`?([^`\\n]+)`?"));
49
98
  return m ? m[1].trim() : "";
50
99
  };
51
100
  const auth = { baseUrl: get("baseUrl"), eteamsid: get("ETEAMSID"), cookie: get("完整 Cookie 串"), userId: get("userId") || get("employeeId"), tenantKey: get("tenantKey") };
52
- if (!auth.baseUrl || !auth.eteamsid) throw new Error("登录缓存缺少 baseUrl/ETEAMSID,请重新登录(e10-login oidc)");
101
+ if (!auth.baseUrl || !auth.eteamsid) return null;
53
102
  if (!auth.cookie) auth.cookie = "langType=zh_CN; ETEAMSID=" + auth.eteamsid + "; agentType=workbuddy; isAgent=true";
54
103
  return auth;
55
104
  }
105
+ function syncCacheMd(auth) { // 读到权威源后自动回写明文镜像(内容无变化不写盘),根治"重登后手工同步"问题
106
+ try {
107
+ const p = path.join(os.homedir(), ".workbuddy", "e10-login-cache.md");
108
+ if (!fs.existsSync(p) || !/🔐 登录信息缓存/.test(fs.readFileSync(p, "utf-8"))) return; // 非标准缓存文件不动它
109
+ const d = new Date();
110
+ 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");
111
+ let txt = fs.readFileSync(p, "utf-8");
112
+ let changed = false;
113
+ const rep = (key, val) => {
114
+ const re = new RegExp("(\\*\\*" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\*\\*\\s*[::]\\s*`?)([^`\\n]*)(`?)");
115
+ const n = txt.replace(re, (m, a, b, c) => { if (b === val && c === "`") return m; changed = true; return a + val + (c || "`"); });
116
+ txt = n;
117
+ };
118
+ rep("baseUrl", auth.baseUrl); rep("ETEAMSID", auth.eteamsid); rep("完整 Cookie 串", auth.cookie); rep("userId", auth.userId || ""); rep("tenantKey", auth.tenantKey || "");
119
+ rep("缓存记录时间", ts + "(e10-data 自动同步自 ~/.e10-cli/profiles/" + (auth.profile || "") + "/auth)");
120
+ if (changed) fs.writeFileSync(p, txt, "utf-8");
121
+ } catch {}
122
+ }
123
+ function loadAuth() {
124
+ const cli = authFromE10Cli(); // ① 权威源优先(e10-login 统一认证,多专家共享)
125
+ if (cli) { syncCacheMd(cli); return cli; }
126
+ const cached = authFromCacheMd(); // ② 权威源缺失时回退旧明文缓存
127
+ if (cached) return cached;
128
+ throw new Error("未找到登录认证(~/.e10-cli/profiles 与 ~/.workbuddy/e10-login-cache.md 均无),请先执行 e10-login oidc / set 登录");
129
+ }
56
130
  const auth = loadAuth();
57
131
  const BASE = auth.baseUrl.replace(/\/+$/, "");
58
132
  const USER = { id: auth.userId || "", tenantKey: auth.tenantKey || "" };
@@ -247,21 +321,43 @@ function dyn() {
247
321
  function baseDto(tableId, groupId, pageNo, pageSize, queryType) {
248
322
  return { tableId: String(tableId), groupId, pageNo, pageSize, queryType, containDetailTable: false, needFormat: true, dynamicParams: dyn() };
249
323
  }
324
+ // 条件树(conditionTrees)协议:根/组节点 { key, relationship("1"=AND|"0"=OR), isParent:true, parentNode, conditionList:[...] }
325
+ // 叶子节点 { key, relationship:"1", isParent:false, parentNode, conditionId, conditionType, compareType, conditionValue, ... }
326
+ // v0.1.8 起 parseWhere 支持递归树(OR);向后兼容:数组=AND 根、叶子键 fieldId 优先(conditionId 兼容兜底)
327
+ function normRel(r) { return (r === "0" || r === 0 || r === false) ? "0" : "1"; }
328
+ function isCondGroup(c) { return !!c && Array.isArray(c.conditionList) && c.conditionList.length > 0; }
329
+ function buildCondNode(c, key, parentKey) {
330
+ if (isCondGroup(c)) {
331
+ return {
332
+ key, relationship: normRel(c.relationship), isParent: true, parentNode: parentKey,
333
+ mainField: c.mainField !== false,
334
+ conditionList: c.conditionList.map((k, i) => buildCondNode(k, key + "_" + i, key)),
335
+ };
336
+ }
337
+ const fid = (c && (c.fieldId || c.conditionId)) || "";
338
+ if (!fid) throw new Error("--where 条件缺少字段:叶子须提供 fieldId(或 conditionId),条件组须提供非空 conditionList");
339
+ return {
340
+ key, relationship: "1", isParent: false, parentNode: parentKey,
341
+ objId: c.objId || "", conditionId: fid, fieldName: c.fieldName || fid,
342
+ conditionType: c.conditionType || "", compareType: c.compareType, conditionValue: c.conditionValue ?? "",
343
+ fieldValue: c.fieldValue || "", mainField: c.mainField !== false, ignoreCaseMark: c.ignoreCaseMark || false,
344
+ };
345
+ }
250
346
  function parseWhere(val) {
251
347
  if (!val) return null;
252
- const arr = readJsonArg(val, "--where");
253
- const list = Array.isArray(arr) ? arr : (arr.conditionList || []);
254
- if (!list.length) return null;
348
+ const raw = readJsonArg(val, "--where");
349
+ if (!raw) return null;
255
350
  const ts = Date.now(), rootKey = "root_" + ts;
256
- return {
257
- key: rootKey, relationship: "1", isParent: true, parentNode: "", mainField: true,
258
- conditionList: list.map((c, i) => ({
259
- key: rootKey + "_" + i, relationship: "1", isParent: false, parentNode: rootKey,
260
- objId: c.objId || "", conditionId: c.fieldId, fieldName: c.fieldName || c.fieldId,
261
- conditionType: c.conditionType || "", compareType: c.compareType, conditionValue: c.conditionValue ?? "",
262
- fieldValue: c.fieldValue || "", mainField: c.mainField !== false, ignoreCaseMark: c.ignoreCaseMark || false,
263
- })),
264
- };
351
+ if (Array.isArray(raw)) { // 数组 → AND 根(元素可为叶子或内嵌条件组,组自带 relationship)
352
+ const kids = [];
353
+ raw.forEach((c, i) => { if (c && typeof c === "object") kids.push(buildCondNode(c, rootKey + "_" + i, rootKey)); });
354
+ if (!kids.length) return null;
355
+ return { key: rootKey, relationship: "1", isParent: true, parentNode: "", mainField: true, conditionList: kids };
356
+ }
357
+ if (typeof raw !== "object") throw new Error("--where 须为条件数组或条件树对象");
358
+ // 对象形态 = 完整条件树({relationship, conditionList:[...]},缺省 AND);无 conditionList 视为无条件
359
+ if (!Array.isArray(raw.conditionList) || !raw.conditionList.length) return null;
360
+ return buildCondNode(raw, rootKey, "");
265
361
  }
266
362
  function parseStat(val) {
267
363
  const arr = readJsonArg(val, "--stat");
@@ -1059,6 +1155,11 @@ ds 数据源(业务模块)取数与元数据
1059
1155
  ds stat --group <gid> --table <tid> --stat '[...]' [--where ...]
1060
1156
  ds count --group <gid> --table <tid> [--where ...]
1061
1157
  (ds 通用 --source-type LOGIC|ETEAMS,默认 LOGIC)
1158
+ --where 过滤条件(v0.1.8 起支持树形 OR,递归):
1159
+ 数组 = AND 根: [{"fieldId":"f1","conditionType":"Date","compareType":"range","conditionValue":"2026-09~2026-10"},{"fieldId":"f2",...}]
1160
+ 对象 = 完整树: {"relationship":"0","conditionList":[{"fieldId":"f1",...},{"fieldId":"f2",...}]} # "0"=OR、"1"=AND,缺省 AND
1161
+ 数组元素可内嵌条件组 {"relationship":"0","conditionList":[...]} 实现 (A AND B) OR (C AND D)
1162
+ 叶子键 fieldId/conditionId 均可(fieldId 优先);缺少字段的叶子会直接报错(不再静默当全表)
1062
1163
  doc 文档查询
1063
1164
  doc count [--name 关键词] [--creator id1,id2] [--author id1,id2] [--content-type office] [--time "起~止"] [--last-time "起~止"] [--folder id,id|--folder-neq id,id]
1064
1165
  doc list [同上条件] [--all 翻页到底] [--page N] [--size 100]
@@ -1090,7 +1191,7 @@ exp 专家包版本管理(expert-repository/v1,响应以 data.actionMsg.
1090
1191
  exp rollback --expert <code> --version vX.Y.Z
1091
1192
  attend 考勤 REST 接口(2026-09-07 新增,hr-attendance 病假/事假等假期余额)
1092
1193
  attend statis [--user <人员ID>] [--begin 2026-09-01] [--end 2026-09-30] # getAttendInfoStatis 假期余额统计;--user 默认当前登录用户,--begin/--end 默认当月
1093
- 认证: 读 ~/.workbuddy/e10-login-cache.md;失效请先 e10-login oidc`);
1194
+ 认证: 优先读权威源 ~/.e10-cli/profiles/<active>/auth(自动同步镜像 ~/.workbuddy/e10-login-cache.md);皆无请先 e10-login oidc`);
1094
1195
  }
1095
1196
 
1096
1197
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "e10-data",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "泛微 E10 数据接口统一 CLI(data-assistant 专家配套):DSL 查询/数据源/文档/词库/关联字段翻译/环境参数配置中心(conf)/专家包版本管理,认证复用 weaver-e10-login 登录态",
5
5
  "author": "e10-dw",
6
6
  "license": "MIT",