e10-data 0.1.20 → 0.1.21

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/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.1.21(2026-09-28)—— 新增 `module call`:模块配置接口调用(配置中心驱动)
4
+
5
+ ### 1. 新命令族 `module`(子命令 `call`)
6
+
7
+ **用途**:**已确定「哪个模块的哪条具体接口」之后**,用本命令一条命令完成取数 ——
8
+ 定位模块 → 拉接口清单 → 选接口 → **组参调用** 的前三步仍由技能侧完成,第四步「调用」收敛到本命令。
9
+
10
+ ```bash
11
+ e10-data module call --conf-key <confKey> --group <模块> \
12
+ [--args '<JSON>'|--args @params.json] [--query "k=v&k2=v2"] \
13
+ [--method GET|POST|FORM] [--header 'K: V'|--headers '<JSON>'|@file] \
14
+ [--limit 50] [--fields a,b] [--raw] [--show-params] [--dry-run] [--timeout 60] [--json]
15
+ ```
16
+
17
+ 内部一次完成:**精取该条 `confAttrs`**(`confType=api`,按 `confGroup` 二级定位)
18
+ → 按 `http_method` / `input_params` 组参(GET 拼 query;POST 发 JSON body,`FORM` /
19
+ `api_definition` 含 `x-www-form-urlencoded` 时发表单)
20
+ → 发请求(**自动禁代理** + `Cookie` / `eteamsid` / `User-Agent: AgentType=…,IsAgent=true` 三件套)
21
+ → 按外层结构(`datajson` / `data` / `records` / 裸数组)提取精简结果(`rows` / `total` / `data`)。
22
+
23
+ ### 2. 与技能自带 `script/call_module.py` 的等价关系
24
+
25
+ 参数名、退出码、提取口径**全对齐**,可互为替换(`--args` 语义一致、`--query` 多对改用 `&` 连接、
26
+ `--header` 单条 + `--headers` JSON 多条)。差异只有两点:
27
+
28
+ 1. **复用本 CLI 的凭据缓存与 `conf` 通道** —— 不额外起 python、不重复取凭据(少 1~2 次进程);
29
+ 2. **零临时文件** —— body 由内存直发,不落盘。
30
+
31
+ ### 3. 退出码(照 `call_module.py` 语义,`main().catch` 按 `e.exitCode` 退出)
32
+
33
+ | 码 | 含义 |
34
+ |---|---|
35
+ | 0 | 正常(**空结果也算正常**:0 行 / 计数 0 直接交付) |
36
+ | 2 | 未登录 / 凭据失效(HTTP 401 / 凭据取不到)→ 重登 |
37
+ | 3 | 非 JSON / `confAttrs` 解析失败 / 配置不存在 |
38
+ | 4 | 权限受限(`hasSearchRight=false`;终端态:立即终止,不重试、不换参、不交付为「无数据」) |
39
+ | 6 | 命中 HRM 路径黑名单(`/api/hrm/`、`/api/bs/hrm/`、`/api/app/hrm/` → 拒绝调用) |
40
+ | 7 | 请求失败(网络 / 超时 / HTTP 非 2xx 且非 JSON) |
41
+ | 8 | 用法错误(缺必填 / 参数非法 / `--args` 非 JSON 对象) |
42
+
43
+ ### 4. 功能要点
44
+
45
+ - `--show-params`:只打印该接口的入/出参说明与请求示例(**不调接口**),替代手工 `conf get` dump 用于正确组参;
46
+ - `--dry-run`:只组装请求不发送,**凭据一律脱敏**;
47
+ - `--limit N`(默认 50,`0` = 不截断)/ `--fields a,b`(每行投影)/ `--raw`(原样输出响应体);
48
+ - **权限/二次验证标志位识别**:`hasSearchRight=false` 与 `pageUid="checkSecondFail"` 在输出 `flags` 中点名
49
+ (前者同时以退出码 4 收口);
50
+ - **INVOICE 自动换取发票云 `token`**:`group` / `module_key` 为 `INVOICE` 时先 `GET /api/inc/proxy/getToken?appType=1`
51
+ 取 `data.token` 写入请求头,**Token 只在内存透传,不打印、不落盘**;
52
+ - **HRM 路径黑名单**在发请求前拒绝(退出码 6),杜绝手工直调 HRM 接口。
53
+
54
+ ### 5. 配套改动(非破坏性)
55
+
56
+ - `normalizeAuth` / weaver-work-cli 出口 / 进程间凭据缓存**补齐 `userAgent`** 字段
57
+ (来源 `WEAVER_USER_AGENT`),供 `module call` 组 `AgentType` UA;缺失或形态不符时
58
+ 由 `moduleUserAgent()` 从 `cookie` 的 `agentType=` 推导,再退回 `workbuddy`。**取数链路行为不变**;
59
+ - `main().catch` 支持错误对象自带 `exitCode`(未设置仍为 1,既有命令退出码零变化)。
60
+
61
+ > 📌 命令参数、输出与退出码是本包**对外契约**;技能侧 `references/support/module-api-query/module-api-query.md`
62
+ > 的「组参调用」步骤可切到本命令(`call_module.py` 保留为兜底)。
63
+
3
64
  ## 0.1.20(2026-09-24)—— 凭据来源切换为 `weaver-work-cli`(唯一来源,移除 e10-login 回退)
4
65
 
5
66
  ### 1. 凭据出口改为 `weaver-work-cli auth export-weaver-env`
package/README.md CHANGED
@@ -25,6 +25,7 @@ e10-data field id2name|name2id 关联字段翻译(test
25
25
  e10-data conf get|batch|batch-file|group|group-file|resolve|resolve-file|add|add-file|update|delete 环境参数配置中心(chatBi config map,替代 config-resolve 脚本 URL 直连;confType 默认按 CONN_/DS_/WF_/API_ 前缀推导,可 --type 显式;confType=api 时支持 confGroup 分组,见下)
26
26
  e10-data exp status|manifest|commits|file|pull|init|commit|lock|release|rollback 专家包版本管理
27
27
  e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-attendance 病假/事假等,v0.1.5 新增)
28
+ e10-data module call 模块配置接口调用(配置中心驱动;已确定「哪个模块的哪条具体接口」之后的统一取数入口,v0.1.21 新增)
28
29
  ```
29
30
 
30
31
  ## 通用约定
@@ -264,3 +265,52 @@ e10-data attend statis [--user <人员ID>] [--begin 2026-09-01] [--end 2026-09-3
264
265
  - `--json`:原样输出接口 JSON(含 `data.attendStatis` 出勤统计 / `data.attendSummary` 汇总 / `data.leaveInfo` 请假 / `data.outAttend` 外出 / `data.vacationBalanceList` 假期余额);
265
266
  - 默认人读输出:仅列出 `data.vacationBalanceList`(按 title 区分假期类型)——带薪病假 `100584850000000003`、带薪事假 `100584850000000010` 含 `lastRemaining`(历年可请)/`remaining`(今年可请)/`total`(总计可请) 三项。
266
267
 
268
+ ## module call 模块配置接口调用(v0.1.21 新增)
269
+
270
+ **用途**:**已确定「哪个模块的哪条具体接口」之后**的统一取数入口。定位模块 → 拉接口清单 → 选接口仍由技能侧完成,
271
+ **第四步「组参调用」收敛到本命令**:精取该条 `confAttrs` → 按 `http_method` / `input_params` 组参
272
+ → 发请求(自动禁代理 + `Cookie` / `eteamsid` / `User-Agent: AgentType=…,IsAgent=true` 三件套)
273
+ → 按外层结构(`datajson` / `data` / `records` / 裸数组)提取精简结果。
274
+
275
+ ```bash
276
+ e10-data module call --conf-key <confKey> --group <模块> \
277
+ [--args '<JSON>'|--args @params.json] [--query "k=v&k2=v2"] \
278
+ [--method GET|POST|FORM] [--header 'K: V'|--headers '<JSON>'|@file] \
279
+ [--limit 50] [--fields a,b] [--raw] [--show-params] [--dry-run] [--timeout 60] [--json]
280
+ ```
281
+
282
+ | 参数 | 说明 |
283
+ |---|---|
284
+ | `--conf-key` / `--group` | **必填**;`confKey` 取自模块接口清单,`--group` = 模块唯一标识(confGroup,如 `WORKFLOW` / `EHR` / `DOCUMENT`) |
285
+ | `--args` | 业务参数 JSON 对象;**含中文一律用 `--args @文件`**(规避 shell 编码损坏) |
286
+ | `--query` | 追加 query(`k=v&k2=v2`,多对用 `&` 连接;与 `--args` 合并) |
287
+ | `--method` | 覆盖 `confAttrs.http_method`(`FORM` = `x-www-form-urlencoded`) |
288
+ | `--header` / `--headers` | 追加请求头:单条 `'K: V'`,或 JSON 对象(可 `@文件`) |
289
+ | `--show-params` | **只打印该接口的入/出参说明与请求示例、不调接口** —— 替代手工 `conf get` dump,用于正确组参 |
290
+ | `--dry-run` | 只组装请求不发送(**凭据一律脱敏**) |
291
+ | `--limit` / `--fields` | 列表行数上限(默认 50,`0` = 不截断)/ 每行字段投影 |
292
+ | `--raw` | 不提取,原样输出响应体 |
293
+
294
+ - **与技能自带 `script/call_module.py` 等价**(参数名 / 退出码 / 提取口径全对齐,可互为替换),差异只有两点:
295
+ ① **复用本 CLI 的凭据缓存与 `conf` 通道**(不额外起 python、不重复取凭据);② **零临时文件**(body 内存直发,不落盘)。
296
+ - **退出码**(`main().catch` 按 `e.exitCode` 退出):`0` 正常(**空结果也算正常**)|`2` 未登录 / 凭据失效 → 重登
297
+ |`3` 非 JSON / `confAttrs` 异常 / 配置不存在 |`4` 权限受限(`hasSearchRight=false`,终端态:立即终止)
298
+ |`6` 命中 HRM 路径黑名单(拒绝调用)|`7` 请求失败 |`8` 用法错误。
299
+ - **标志位点名**:`hasSearchRight=false`(权限受限 ≠ 无数据)与 `pageUid="checkSecondFail"`(二次身份验证:
300
+ `total` 可信、明细恒 0 行)会在输出 `flags` 中列出,前者同时以退出码 `4` 收口。
301
+ - **INVOICE 自动换取发票云 `token`**:`--group` / `module_key` 为 `INVOICE` 时先 `GET /api/inc/proxy/getToken?appType=1`
302
+ 取 `data.token` 写入请求头;**Token 只在内存透传,不打印、不落盘**。
303
+ - **HRM 路径黑名单**:`/api/hrm/`、`/api/bs/hrm/`、`/api/app/hrm/` 的接口在发请求前直接拒绝(退出码 `6`)——
304
+ 人员 / 部门 / 分部 / 岗位一律走 `weaver-work-cli` 的 `hrm` 子命令。
305
+
306
+ ```bash
307
+ # 看某个接口要什么参数(不调接口)
308
+ e10-data module call --conf-key POST_EHR_GET_REPORT_LIST --group EHR --show-params
309
+
310
+ # 真实取数(含中文参数走 @文件)
311
+ e10-data module call --conf-key POST_EHR_GET_REPORT_LIST --group EHR --args @params.json --json
312
+
313
+ # GET 接口 + 只看两行、只留三个字段
314
+ e10-data module call --conf-key GET_XXX --group XXX --query "pageNo=1&pageSize=20" --limit 2 --fields id,name --json
315
+ ```
316
+
package/bin/e10-data.mjs CHANGED
@@ -23,6 +23,11 @@
23
23
  * 环境参数配置中心(chatBi config map,替代 config-resolve 脚本 URL 直连;
24
24
  * confType=api 时支持 confGroup 分组二级定位,见 --group)
25
25
  * e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-attendance 病假/事假,2026-09-07)
26
+ * e10-data module call 模块配置接口调用(配置中心驱动):**已确定「哪个模块的哪条具体接口」之后**的
27
+ * 统一取数入口 —— 精取该条 confAttrs → 按 http_method/input_params 组参 →
28
+ * 发请求(禁代理 + Cookie/eteamsid/UA 三件套)→ 提取精简结果(2026-09-28)
29
+ * 与技能自带 script/call_module.py 等价(参数/退出码/提取全对齐),
30
+ * 复用本 CLI 凭据缓存与 conf 通道、零临时文件
26
31
  *
27
32
  * 通用:--json 原样输出;复杂 JSON 参数支持 @文件 或 内联(中文走 @文件 更稳)
28
33
  * 认证:凭据**唯一来源 = weaver-work-cli**(本 CLI 不实现任何加解密,**不设 e10-login 回退**):
@@ -200,7 +205,9 @@ function normalizeAuth(o) {
200
205
  const cookie = String(o.cookie || "").trim();
201
206
  if (!baseUrl || !eteamsid || !cookie) return null;
202
207
  if (/^\((未知|空)\)$/.test(eteamsid) || /^\((未知|空)\)$/.test(cookie)) return null;
203
- return { baseUrl, eteamsid, cookie, userId: String(o.userId || "").trim(), tenantKey: String(o.tenantKey || "").trim(), source: o.source };
208
+ // userAgent:模块配置通道的请求头需要 `AgentType=<agentType>,IsAgent=true`(见 module 命令族)。
209
+ // 属"非凭据"的请求头取值,缺失时由 moduleUserAgent() 从 cookie 的 agentType= 推导,不影响取数。
210
+ return { baseUrl, eteamsid, cookie, userId: String(o.userId || "").trim(), tenantKey: String(o.tenantKey || "").trim(), userAgent: String(o.userAgent || "").trim(), source: o.source };
204
211
  }
205
212
 
206
213
  /** ① e10-login auth --json(正式契约:{v,baseUrl,eteamsId,cookie,userAgent,userId,tenantKey,agentType,profile}) */
@@ -392,6 +399,7 @@ function providerWeaverWorkCli() {
392
399
  baseUrl: env.WEAVER_BASE_URL,
393
400
  eteamsid: env.WEAVER_ETEAMSID,
394
401
  cookie: env.WEAVER_COOKIE,
402
+ userAgent: env.WEAVER_USER_AGENT,
395
403
  source: "weaver-work-cli auth export-weaver-env",
396
404
  });
397
405
  if (!auth) return null;
@@ -547,7 +555,7 @@ function credCachePublish(auth) {
547
555
  profile: auth.profile || "default",
548
556
  auth: {
549
557
  baseUrl: auth.baseUrl, eteamsid: auth.eteamsid, cookie: auth.cookie,
550
- userId: auth.userId, tenantKey: auth.tenantKey, source: auth.source,
558
+ userId: auth.userId, tenantKey: auth.tenantKey, userAgent: auth.userAgent, source: auth.source,
551
559
  },
552
560
  });
553
561
  let settled = false;
@@ -699,6 +707,7 @@ const CMD_SUBS = {
699
707
  exp: ["status", "manifest", "commits", "file", "pull", "init", "commit", "lock", "release", "rollback"],
700
708
  attend: ["statis"],
701
709
  eb: ["perm-page"],
710
+ module: ["call"],
702
711
  };
703
712
  function commandIsKnown(pos) {
704
713
  const family = pos[0], sub = pos[1];
@@ -1993,6 +2002,338 @@ async function cmdConf(sub, args, json, pos) {
1993
2002
  }
1994
2003
  }
1995
2004
 
2005
+ // ---------------- module:模块配置接口调用(配置中心驱动,2026-09-28 新增)----------------
2006
+ // 用途:**已确定「哪个模块的哪条具体接口」之后**,用本命令一条命令完成取数:
2007
+ // 复用本 CLI 已加载的凭据 → 精取该条 confAttrs(confType=api,按 confGroup 二级定位)
2008
+ // → 按 http_method / input_params 组参 → 发请求(自动禁代理 + Cookie/eteamsid/UA 三件套)
2009
+ // → 按外层结构提取精简结果(datajson / data / records / 裸数组皆可)。
2010
+ // 与技能自带 python 调用器 call_module.py **等价**(参数名、退出码、提取口径全对齐),区别:
2011
+ // · 复用本 CLI 的凭据缓存与 conf 通道(不额外起 python,也不重复取凭据);
2012
+ // · 全程零临时文件(body 由内存直发,不落盘)。
2013
+ // 退出码(照 call_module.py 语义,见 main().catch 的 exitCode 处理):
2014
+ // 0 正常(**空结果也算正常**)/ 2 未登录·凭据失效 / 3 非 JSON·confAttrs 异常
2015
+ // / 4 权限受限(终端态:立即终止)/ 6 命中 HRM 路径黑名单 / 7 请求失败 / 8 用法错误
2016
+ const MODULE_HRM_PATTERNS = ["/api/hrm/", "/api/bs/hrm/", "/api/app/hrm/"];
2017
+ const MODULE_PERM_WORDS = ["没有权限", "无权限", "权限不足", "未授权", "无权访问", "无查询权限"];
2018
+ const MODULE_INVOICE_TOKEN_PATH = "/api/inc/proxy/getToken?appType=1";
2019
+ const MODULE_EXIT = { OK: 0, AUTH: 2, JSON: 3, PERM: 4, HRM: 6, HTTP: 7, USAGE: 8 };
2020
+
2021
+ /** 带退出码的错误:main().catch 按 e.exitCode 退出(未设置仍为 1) */
2022
+ class ModuleExit extends Error {
2023
+ constructor(code, message) { super(message); this.exitCode = code; }
2024
+ }
2025
+
2026
+ /** json 模式下先向 stdout 打一份结构化错误信封,再带退出码抛出(对齐 call_module.py 的输出约定) */
2027
+ function moduleDie(json, code, msg) {
2028
+ if (json) console.log(JSON.stringify({ ok: false, exit: code, error: msg }, null, 1));
2029
+ throw new ModuleExit(code, msg);
2030
+ }
2031
+
2032
+ /** 模块接口调用的 User-Agent:`AgentType=<agentType>,IsAgent=true`
2033
+ * 优先取登录态自带的 UA(weaver-work-cli export-weaver-env 的 WEAVER_USER_AGENT),但**必须是 AgentType 形态**;
2034
+ * 缺失 / 形态不符 / 混入非 ASCII(会致请求头编码失败)时,从 cookie 的 agentType= 推导,再退回 workbuddy。 */
2035
+ function moduleUserAgent() {
2036
+ const raw = String((auth && auth.userAgent) || "").trim();
2037
+ if (raw && !/[^\x00-\x7F]/.test(raw) && /agenttype=/i.test(raw)) return raw;
2038
+ const m = String((auth && auth.cookie) || "").match(/agentType=([^;]+)/i);
2039
+ return "AgentType=" + (m ? m[1].trim() : "workbuddy") + ",IsAgent=true";
2040
+ }
2041
+
2042
+ /** 业务参数 → query 串(数组按多值展开、对象 JSON 化;对齐 urlencode(..., doseq=True)) */
2043
+ function moduleQueryString(obj) {
2044
+ const sp = new URLSearchParams();
2045
+ for (const [k, v] of Object.entries(obj || {})) {
2046
+ if (v === undefined || v === null) continue;
2047
+ if (Array.isArray(v)) for (const x of v) sp.append(k, x !== null && typeof x === "object" ? JSON.stringify(x) : String(x));
2048
+ else sp.append(k, typeof v === "object" ? JSON.stringify(v) : String(v));
2049
+ }
2050
+ return sp.toString();
2051
+ }
2052
+
2053
+ /** 发请求(带超时);网络层失败 → 退出码 7 */
2054
+ async function moduleFetch(method, pathAndQuery, headers, body, timeoutMs) {
2055
+ const ctrl = new AbortController();
2056
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
2057
+ try {
2058
+ const resp = await fetch(BASE + pathAndQuery, {
2059
+ method,
2060
+ headers,
2061
+ ...(body !== undefined ? { body } : {}),
2062
+ signal: ctrl.signal,
2063
+ });
2064
+ return { status: resp.status, text: await resp.text() };
2065
+ } catch (e) {
2066
+ const aborted = String((e && e.name) || "") === "AbortError";
2067
+ throw new ModuleExit(MODULE_EXIT.HTTP, "请求失败:" + (aborted ? "超时 " + timeoutMs + "ms" : String((e && e.message) || e)));
2068
+ } finally { clearTimeout(timer); }
2069
+ }
2070
+
2071
+ /** 按已知外层形态找列表:datajson.data / datajson.datas / data.data / data.records / data / datas / list */
2072
+ function moduleRowsOf(obj) {
2073
+ const numish = (v) => (typeof v === "number" || typeof v === "string") ? v : null;
2074
+ if (Array.isArray(obj)) return { rows: obj, total: null };
2075
+ if (!obj || typeof obj !== "object") return { rows: null, total: null };
2076
+ for (const k of ["datajson", "data", "result"]) {
2077
+ const v = obj[k];
2078
+ if (Array.isArray(v)) return { rows: v, total: null };
2079
+ if (v && typeof v === "object") {
2080
+ for (const kk of ["data", "datas", "records", "list", "items", "rows"]) {
2081
+ if (Array.isArray(v[kk])) return { rows: v[kk], total: numish(v.total) };
2082
+ }
2083
+ return { rows: null, total: null };
2084
+ }
2085
+ }
2086
+ for (const kk of ["datas", "list", "records", "items", "rows"]) {
2087
+ if (Array.isArray(obj[kk])) return { rows: obj[kk], total: numish(obj.total) };
2088
+ }
2089
+ return { rows: null, total: null };
2090
+ }
2091
+
2092
+ /** 取载荷(datajson 优先,其次 data,否则根)与来源名 */
2093
+ function modulePayloadOf(obj) {
2094
+ if (obj && typeof obj === "object" && !Array.isArray(obj)) {
2095
+ for (const k of ["datajson", "data"]) {
2096
+ if (obj[k] !== undefined && obj[k] !== null) return { value: obj[k], via: k };
2097
+ }
2098
+ }
2099
+ return { value: obj, via: "(root)" };
2100
+ }
2101
+
2102
+ /** 权限/二次验证标志位识别(权限受限 ≠ 无数据;二次验证 ≠ 无数据) */
2103
+ function moduleFlagsOf(obj, payload) {
2104
+ const flags = [];
2105
+ if (payload && typeof payload === "object" && !Array.isArray(payload) && payload.hasSearchRight === false) {
2106
+ flags.push("hasSearchRight=false(权限受限,其余字段必为空 → 立即终止,不得交付为「无数据」)");
2107
+ }
2108
+ if (payload && typeof payload === "object" && !Array.isArray(payload) && payload.pageUid === "checkSecondFail") {
2109
+ flags.push("pageUid=checkSecondFail(二次身份验证:total 可信、明细恒 0 行 → 如实说明,不得当无数据)");
2110
+ }
2111
+ let txt = "";
2112
+ try { txt = JSON.stringify(obj).slice(0, 4000); } catch { txt = ""; }
2113
+ if (MODULE_PERM_WORDS.some((w) => txt.includes(w))) flags.push("响应含权限关键词 → 按「无权限即终止」处理");
2114
+ return flags;
2115
+ }
2116
+
2117
+ /** INVOICE 业务接口的发票云 Token(内存透传;🚫 不打印、不落盘) */
2118
+ async function moduleInvoiceToken(json, timeoutMs) {
2119
+ const headers = {
2120
+ "accept": "application/json, text/plain, */*",
2121
+ "langType": "zh_CN",
2122
+ "cookie": auth.cookie,
2123
+ "eteamsid": auth.eteamsid,
2124
+ "user-agent": moduleUserAgent(),
2125
+ };
2126
+ const { status, text } = await moduleFetch("GET", MODULE_INVOICE_TOKEN_PATH, headers, undefined, timeoutMs);
2127
+ let tk = null;
2128
+ try { const j = parseJsonSafe(text); tk = j && j.data && j.data.token; } catch {}
2129
+ if (!tk) moduleDie(json, MODULE_EXIT.HTTP, "未取到发票云 Token(INVOICE 业务接口必需;HTTP " + status + ")");
2130
+ return tk;
2131
+ }
2132
+
2133
+ async function cmdModuleCall(args, json) {
2134
+ // ---- 必填与选项(缺参一律退出码 8,对齐 call_module.py)----
2135
+ if (!args["conf-key"] || args["conf-key"] === true) {
2136
+ moduleDie(json, MODULE_EXIT.USAGE, "缺必填参数 --conf-key(用法: module call --conf-key <confKey> --group <模块>)");
2137
+ }
2138
+ if (!args.group || args.group === true) {
2139
+ moduleDie(json, MODULE_EXIT.USAGE, "缺必填参数 --group(模块唯一标识,如 WORKFLOW / EHR / DOCUMENT)");
2140
+ }
2141
+ const confKey = String(args["conf-key"]);
2142
+ const group = String(args.group);
2143
+ const timeoutMs = Math.max(1, parseInt(args.timeout || "60", 10) || 60) * 1000;
2144
+ const limit = (args.limit === undefined || args.limit === true) ? 50 : (parseInt(String(args.limit), 10) || 0);
2145
+ const fields = (typeof args.fields === "string" && args.fields) ? args.fields.split(",").map((s) => s.trim()).filter(Boolean) : null;
2146
+
2147
+ // ---- ① 精取该条接口配置(confType 固定 api;confGroup 二级定位必带)----
2148
+ let rec;
2149
+ try { rec = await confGetData("api", confKey, false, group); }
2150
+ catch (e) {
2151
+ const m = String((e && e.message) || e);
2152
+ moduleDie(json, /登录态失效|未取得登录凭据/.test(m) ? MODULE_EXIT.AUTH : MODULE_EXIT.JSON,
2153
+ "conf get 失败:" + m + "(先核对 confKey / confGroup;登录态问题请重新登录)");
2154
+ }
2155
+ let attrs = rec.confAttrs;
2156
+ if (typeof attrs === "string") {
2157
+ try { attrs = parseJsonSafe(attrs, "confAttrs"); }
2158
+ catch { moduleDie(json, MODULE_EXIT.JSON, "confAttrs 二次解析失败(非 JSON 字符串): " + confKey); }
2159
+ }
2160
+ if (!attrs || typeof attrs !== "object" || Array.isArray(attrs)) {
2161
+ moduleDie(json, MODULE_EXIT.JSON, "confAttrs 缺失或形态异常: " + confKey + "(--group " + group + ")");
2162
+ }
2163
+ const apiPath = String(attrs.api_path || rec.confValue || "");
2164
+ if (!apiPath) moduleDie(json, MODULE_EXIT.JSON, "confAttrs 未提供 api_path / confValue: " + confKey);
2165
+
2166
+ // ---- ② HRM 路径黑名单(人员/部门/岗位只走 weaver-work-cli 的 hrm 子命令)----
2167
+ if (MODULE_HRM_PATTERNS.some((p) => apiPath.includes(p))) {
2168
+ moduleDie(json, MODULE_EXIT.HRM,
2169
+ "该接口命中 HRM 路径黑名单(" + apiPath + ")→ 人员/部门/岗位一律走 weaver-work-cli 的 hrm 子命令,禁止直调");
2170
+ }
2171
+
2172
+ const apiName = attrs.api_name || rec.confName || confKey;
2173
+
2174
+ // ---- ③ --show-params:只给入/出参说明与请求示例,不调接口(用于正确组参)----
2175
+ if (args["show-params"]) {
2176
+ const out = {
2177
+ confKey, module: group, api_name: apiName,
2178
+ http_method: attrs.http_method ?? null, api_path: apiPath,
2179
+ business_desc: attrs.business_desc ?? null,
2180
+ api_params: attrs.api_params ?? null,
2181
+ input_params: attrs.input_params ?? null,
2182
+ output_params: attrs.output_params ?? null,
2183
+ request_example: attrs.request_example ?? null,
2184
+ };
2185
+ console.log(JSON.stringify(out, null, 1));
2186
+ return;
2187
+ }
2188
+
2189
+ // ---- ④ 组参:--args(@文件 / 内联)+ --query(k=v&k2=v2)+ --header / --headers ----
2190
+ let bizArgs = {};
2191
+ if (args.args !== undefined && args.args !== true) {
2192
+ try { bizArgs = readJsonArg(String(args.args), "--args"); }
2193
+ catch (e) { moduleDie(json, MODULE_EXIT.USAGE, "--args 读取/解析失败:" + ((e && e.message) || e)); }
2194
+ }
2195
+ if (!bizArgs || typeof bizArgs !== "object" || Array.isArray(bizArgs)) {
2196
+ moduleDie(json, MODULE_EXIT.USAGE, "--args 必须是 JSON 对象");
2197
+ }
2198
+ if (typeof args.query === "string" && args.query) {
2199
+ for (const pair of args.query.split("&")) {
2200
+ if (!pair) continue;
2201
+ const i = pair.indexOf("=");
2202
+ if (i > 0) bizArgs[pair.slice(0, i)] = pair.slice(i + 1);
2203
+ }
2204
+ }
2205
+ const extraHeaders = {};
2206
+ if (args.headers !== undefined && args.headers !== true) {
2207
+ let h;
2208
+ try { h = readJsonArg(String(args.headers), "--headers"); }
2209
+ catch (e) { moduleDie(json, MODULE_EXIT.USAGE, "--headers 读取/解析失败:" + ((e && e.message) || e)); }
2210
+ if (h && typeof h === "object" && !Array.isArray(h)) Object.assign(extraHeaders, h);
2211
+ }
2212
+ if (typeof args.header === "string" && args.header.includes(":")) {
2213
+ const i = args.header.indexOf(":");
2214
+ extraHeaders[args.header.slice(0, i).trim()] = args.header.slice(i + 1).trim();
2215
+ }
2216
+
2217
+ const declared = String(args.method || attrs.http_method || "POST").toUpperCase();
2218
+ if (declared !== "GET" && declared !== "POST" && declared !== "FORM") {
2219
+ moduleDie(json, MODULE_EXIT.USAGE, "--method 只支持 GET / POST / FORM(当前: " + args.method + ")");
2220
+ }
2221
+ const effMethod = declared === "FORM" ? "POST" : declared;
2222
+ const isForm = declared === "FORM" || /x-www-form-urlencoded/i.test(String(attrs.api_definition || ""));
2223
+
2224
+ let qs = "", body;
2225
+ if (effMethod === "GET") {
2226
+ const q = moduleQueryString(bizArgs);
2227
+ qs = q ? "?" + q : "";
2228
+ } else {
2229
+ body = isForm ? moduleQueryString(bizArgs) : JSON.stringify(bizArgs);
2230
+ }
2231
+
2232
+ // ---- ⑤ --dry-run:只组装不发送(凭据一律脱敏)----
2233
+ if (args["dry-run"]) {
2234
+ const masked = {};
2235
+ for (const [k, v] of Object.entries(extraHeaders)) masked[k] = /^token$/i.test(k) ? "<脱敏>" : v;
2236
+ const out = {
2237
+ ok: true, confKey, api_name: apiName, method: effMethod,
2238
+ url: "<baseUrl>" + apiPath + qs,
2239
+ body: bizArgs,
2240
+ headers: {
2241
+ Cookie: "<脱敏>", eteamsid: "<脱敏>", "User-Agent": "<AgentType=…,IsAgent=true>",
2242
+ "Content-Type": effMethod === "POST" ? (isForm ? "application/x-www-form-urlencoded" : "application/json;charset=UTF-8") : null,
2243
+ ...masked,
2244
+ },
2245
+ note: "--dry-run:未发送请求",
2246
+ };
2247
+ console.log(JSON.stringify(out, null, 1));
2248
+ return;
2249
+ }
2250
+
2251
+ // ---- ⑥ 发请求 ----
2252
+ const headers = {
2253
+ "accept": "application/json, text/plain, */*",
2254
+ "langType": "zh_CN",
2255
+ "cookie": auth.cookie,
2256
+ "eteamsid": auth.eteamsid,
2257
+ "ebbusinessid": EB_BIZ_ID,
2258
+ "user-agent": moduleUserAgent(),
2259
+ "timezoneoffset": "-480",
2260
+ };
2261
+ if (effMethod === "POST") headers["Content-Type"] = isForm ? "application/x-www-form-urlencoded" : "application/json;charset=UTF-8";
2262
+ // INVOICE:业务接口除通用三件套外必须带发票云 token(先取换、再允许 --header 覆盖)
2263
+ if (group.toUpperCase() === "INVOICE" || String(attrs.module_key || "").toUpperCase() === "INVOICE") {
2264
+ headers["token"] = await moduleInvoiceToken(json, timeoutMs);
2265
+ }
2266
+ Object.assign(headers, extraHeaders);
2267
+
2268
+ const { status, text } = await moduleFetch(effMethod, apiPath + qs, headers, body, timeoutMs);
2269
+
2270
+ if (status === 401) {
2271
+ moduleDie(json, MODULE_EXIT.AUTH,
2272
+ "登录态失效(HTTP 401):凭据来源=" + ((auth && auth.source) || "未知") + ";请执行 `weaver-work-cli auth login --base-url <域名>` 重新登录");
2273
+ }
2274
+
2275
+ let obj = null;
2276
+ try { obj = parseJsonSafe(text); } catch {}
2277
+ if (obj === null) {
2278
+ // 非 JSON:原样输出前 2000 字符(2xx 视为正常,其余按请求失败)
2279
+ if (json) console.log(JSON.stringify({ ok: status >= 200 && status < 300, api_name: apiName, http: status, nonJson: text.slice(0, 2000) }, null, 1));
2280
+ else console.log(text.slice(0, 2000));
2281
+ if (status < 200 || status >= 300) throw new ModuleExit(MODULE_EXIT.HTTP, "HTTP " + status + " 且响应非 JSON(已原样输出前 2000 字符)");
2282
+ return;
2283
+ }
2284
+
2285
+ // ---- ⑦ --raw:原样输出响应体 ----
2286
+ if (args.raw) {
2287
+ console.log(JSON.stringify(obj, null, 1));
2288
+ return;
2289
+ }
2290
+
2291
+ // ---- ⑧ 提取精简结果 ----
2292
+ const payload = modulePayloadOf(obj);
2293
+ const flags = moduleFlagsOf(obj, payload.value);
2294
+ const { rows, total } = moduleRowsOf(obj);
2295
+ const out = { ok: true, api_name: apiName, http: status, via: payload.via };
2296
+ let projected = null;
2297
+ if (rows !== null) {
2298
+ const shown = limit > 0 ? rows.slice(0, limit) : rows;
2299
+ projected = fields
2300
+ ? shown.map((r) => {
2301
+ const o = {};
2302
+ if (r && typeof r === "object") for (const k of fields) if (k in r) o[k] = r[k];
2303
+ return o;
2304
+ })
2305
+ : shown;
2306
+ out.total = total;
2307
+ out.rows = rows.length;
2308
+ out.shown = projected.length;
2309
+ out.data = projected;
2310
+ } else {
2311
+ out.data = payload.value;
2312
+ }
2313
+ if (flags.length) out.flags = flags;
2314
+
2315
+ if (json) {
2316
+ console.log(JSON.stringify(out, null, 1));
2317
+ } else {
2318
+ console.log("== " + apiName + " ==");
2319
+ console.log(effMethod + " " + apiPath + " → HTTP " + status + "(via=" + payload.via + ")");
2320
+ if (rows !== null) {
2321
+ console.log("rows=" + rows.length
2322
+ + (total !== null && total !== undefined ? " / total=" + total : "")
2323
+ + (limit > 0 && rows.length > limit ? "(已截断到 " + limit + " 行)" : ""));
2324
+ printHuman("", projected);
2325
+ } else {
2326
+ console.log(JSON.stringify(payload.value, null, 1));
2327
+ }
2328
+ for (const f of flags) console.log("[!] " + f);
2329
+ }
2330
+
2331
+ // 权限受限是终端态:结果照常给出(供调用方留证),但退出码必须是非 0(对齐 call_module.py)
2332
+ if (flags.some((f) => f.startsWith("hasSearchRight=false"))) {
2333
+ throw new ModuleExit(MODULE_EXIT.PERM, "权限受限:hasSearchRight=false(立即终止,不重试、不换参、不交付为「无数据」)");
2334
+ }
2335
+ }
2336
+
1996
2337
  // ---------------- attend:考勤 REST 接口 ----------------
1997
2338
  // 2026-09-07:封装 /api/attend/web/attendInfoV2/getAttendInfoStatis(假期余额统计,hr-attendance 场景六病假/事假)。
1998
2339
  // 请求参数 {beginDate, endDate, userId};返回 data.vacationBalanceList[]:{id,title,data:[{name,value,key}]},
@@ -2449,6 +2790,20 @@ eb EB 建模表单权限校验(chatBi EB 配置映射分页查询,替
2449
2790
  # obj-id / form-table 必填(本地强校验;缺失后端只回空、不报错)
2450
2791
  # obj-id / app-id 必须字符串传参(19 位大整数,Number 舍入 → 静默 total=0)
2451
2792
  # total>=1 = 有权限,并回带该行 appId;--require 时 total=0 直接报错退出
2793
+ module 模块配置接口调用(配置中心驱动;**已确定「哪个模块的哪条具体接口」之后**用它取数,2026-09-28 新增)
2794
+ module call --conf-key <confKey> --group <模块> [--args '<JSON>'|--args @params.json]
2795
+ [--query "k=v&k2=v2"] [--method GET|POST|FORM] [--header 'K: V'|--headers '<JSON>'|@file]
2796
+ [--limit 50] [--fields a,b] [--raw] [--show-params] [--dry-run] [--timeout 60] [--json]
2797
+ # 一条命令完成:精取该条 confAttrs → 按 http_method/input_params 组参 → 发请求
2798
+ # (自动禁代理 + Cookie/eteamsid/UA 三件套)→ 按外层结构(datajson/data/records/裸数组)提取精简结果
2799
+ # 与技能自带 python 调用器 call_module.py 等价(参数名/退出码/提取口径全对齐),但复用本 CLI 凭据缓存与
2800
+ # conf 通道、零临时文件 —— 调用器首选本命令,call_module.py 仅作兜底
2801
+ # --show-params:只打印入/出参说明与请求示例(不调接口),用于正确组参
2802
+ # --dry-run:只组装请求不发送(凭据脱敏);--limit 0 = 不截断
2803
+ # --query 多对用 & 连接;INVOICE 自动换取发票云 token(内存透传,不打印不落盘)
2804
+ # 退出码:0 正常(**空结果也算正常**)|2 未登录·凭据失效|3 非 JSON·confAttrs 异常
2805
+ # |4 权限受限(hasSearchRight=false,终端态)|6 命中 HRM 路径黑名单|7 请求失败|8 用法错误
2806
+ # 示例: module call --conf-key POST_EHR_GET_ATTEND_INFO_STATIS --group EHR --args @params.json --json
2452
2807
  认证: 加解密由 e10-login 负责(本 CLI 不参与);凭据出口 = e10-login auth/whoami(自动同步镜像);皆无请先 e10-login oidc
2453
2808
  (无浏览器/无 python 环境:login xiaoe-env 用环境变量 token 直接登录)`);
2454
2809
  }
@@ -2519,7 +2874,17 @@ async function main() {
2519
2874
  case "perm-page": result = await cmdEbPermPage(args, json); break;
2520
2875
  default: usage(); process.exit(1);
2521
2876
  }
2877
+ } else if (family === "module") {
2878
+ if (!sub) { usage(); process.exit(1); }
2879
+ switch (sub) {
2880
+ case "call": result = await cmdModuleCall(args, json); break;
2881
+ default: usage(); process.exit(1);
2882
+ }
2522
2883
  } else { usage(); process.exit(1); }
2523
2884
  if (json && result !== undefined) console.log(JSON.stringify(result, null, 1));
2524
2885
  }
2525
- main().catch((e) => { console.error("ERROR: " + e.message); process.exit(1); });
2886
+ // 退出码:默认 1(用法/未知错误);错误对象带 exitCode 时按其退出(module call 用到 2/3/4/6/7/8)
2887
+ main().catch((e) => {
2888
+ console.error("ERROR: " + ((e && e.message) || e));
2889
+ process.exit(Number.isInteger(e && e.exitCode) ? e.exitCode : 1);
2890
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "e10-data",
3
- "version": "0.1.20",
4
- "description": "泛微 E10 数据接口统一 CLI(data-assistant 专家配套):登录(login xiaoe-env,环境变量 XiaoE 全局 token 换 ETEAMSID,免浏览器/免 python)/DSL 查询/数据源/文档/词库/关联字段翻译/环境参数配置中心(conf,支持 confGroup 分组)/EB 建模表单权限校验(eb perm-page)/专家包版本管理;ds 取数(list/chart/stat/count)的 appId 由调用方显式传入 --app-id(v0.1.14 起 CLI 不再自动反查;EB 建模表单 weaver-ebuilder-form-service 必须带,appId 取自 eb perm-page 权限校验返回行);认证凭据**唯一来源 = weaver-work-cli**(通过 `auth export-weaver-env` 命令出口取明文凭据,**不设 e10-login 回退**;加解密由该 CLI 自行负责,本 CLI 不实现任何加解密;仅 login xiaoe-env 在无凭据时自行换 token,写 profile 仍交 e10-login set)。v0.1.17 性能改造:免凭据命令(--version/--help/未知命令)提前返回、直连 e10-login JS 入口(去掉 shell:true 的 cmd.exe 开销)、新增凭据进程间 TTL 缓存(不落明文);v0.1.19(2026-09-24)新增 ds 发现层聚合 groups/find/describe(进程内并发,全 58 分组扫表实测 ~5.5s),并修复 ds tables --name 因 listByPage 端点切换导致的关键词过滤静默失效;v0.1.18(2026-09-24)新增 ds fields-dsl(接口 /api/dw/newetl/chatBi/ds/fieldsForDSL),并移除凭据明文镜像同步(不再写 ~/.workbuddy/e10-login-cache.md);v0.1.20(2026-09-24)凭据来源切换为 weaver-work-cli 唯一来源、移除 e10-login 回退,并补齐 userId/tenantKey 传递(修复 attend statis 等命令报「缺少查询人员」的问题)",
3
+ "version": "0.1.21",
4
+ "description": "泛微 E10 数据接口统一 CLI(data-assistant 专家配套):登录(login xiaoe-env,环境变量 XiaoE 全局 token 换 ETEAMSID,免浏览器/免 python)/DSL 查询/数据源/文档/词库/关联字段翻译/环境参数配置中心(conf,支持 confGroup 分组)/EB 建模表单权限校验(eb perm-page)/专家包版本管理;ds 取数(list/chart/stat/count)的 appId 由调用方显式传入 --app-id(v0.1.14 起 CLI 不再自动反查;EB 建模表单 weaver-ebuilder-form-service 必须带,appId 取自 eb perm-page 权限校验返回行);认证凭据**唯一来源 = weaver-work-cli**(通过 `auth export-weaver-env` 命令出口取明文凭据,**不设 e10-login 回退**;加解密由该 CLI 自行负责,本 CLI 不实现任何加解密;仅 login xiaoe-env 在无凭据时自行换 token,写 profile 仍交 e10-login set)。v0.1.17 性能改造:免凭据命令(--version/--help/未知命令)提前返回、直连 e10-login JS 入口(去掉 shell:true 的 cmd.exe 开销)、新增凭据进程间 TTL 缓存(不落明文);v0.1.19(2026-09-24)新增 ds 发现层聚合 groups/find/describe(进程内并发,全 58 分组扫表实测 ~5.5s),并修复 ds tables --name 因 listByPage 端点切换导致的关键词过滤静默失效;v0.1.18(2026-09-24)新增 ds fields-dsl(接口 /api/dw/newetl/chatBi/ds/fieldsForDSL),并移除凭据明文镜像同步(不再写 ~/.workbuddy/e10-login-cache.md);v0.1.20(2026-09-24)凭据来源切换为 weaver-work-cli 唯一来源、移除 e10-login 回退,并补齐 userId/tenantKey 传递(修复 attend statis 等命令报「缺少查询人员」的问题)。v0.1.21(2026-09-28)新增 module call —— 模块配置接口调用(配置中心驱动):已确定「哪个模块的哪条具体接口」之后的一条命令取数入口(精取该条 confAttrs → 按 http_method/input_params 组参 → 发请求(自动禁代理 + Cookie/eteamsid/UA 三件套)→ 提取精简结果),与技能自带 script/call_module.py 等价(参数名/退出码/提取口径全对齐),复用本 CLI 凭据缓存与 conf 通道、零临时文件;同时 auth 归一化与凭据缓存补齐 userAgent(供 AgentType UA 使用,缺失时从 cookie 的 agentType= 推导)。",
5
5
  "author": "e10-dw",
6
6
  "license": "MIT",
7
7
  "type": "module",