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.
- package/README.md +41 -3
- package/bin/e10-data.mjs +118 -17
- 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`
|
|
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
|
-
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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))
|
|
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)
|
|
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
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
认证:
|
|
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