e10-data 0.1.18-xiaoe → 0.1.20

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,94 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.1.20(2026-09-24)—— 凭据来源切换为 `weaver-work-cli`(唯一来源,移除 e10-login 回退)
4
+
5
+ ### 1. 凭据出口改为 `weaver-work-cli auth export-weaver-env`
6
+
7
+ 执行 `auth export-weaver-env --shell posix`,解析 `WEAVER_BASE_URL` / `WEAVER_ETEAMSID` /
8
+ `WEAVER_COOKIE` 三项(正好对应本 CLI 需要的 `baseUrl` / `eteamsid` / `cookie`)。
9
+ 凭据**只在进程内存中解析**——不经 shell、不落盘、不打印。
10
+
11
+ - 入口定位沿用与 e10-login 相同的四段式:`WEAVER_WORK_CLI_ENTRY` → Node 模块解析 →
12
+ PATH shim → 常见全局前缀;
13
+ - `weaver-work-cli` 的登录态存于 `~/.weaver-work-cli/`(`@napi-rs/keyring` 加密),
14
+ 与 e10-login **完全独立、互不通用**。
15
+
16
+ ### 2. ⚠️ 破坏性变更:移除 e10-login 回退
17
+
18
+ `AUTH_PROVIDERS` **收敛为只保留 `weaver-work-cli`**。原四个 e10-login 系出口
19
+ (`auth --json` / `whoami --json` / `whoami` 文本 / 明文镜像)**保留函数定义但不再挂载**;
20
+ 如需恢复回退,把对应条目加回 `AUTH_PROVIDERS` 数组即可。
21
+
22
+ - 未取得凭据的报错与 401 提示统一改为「唯一来源 = weaver-work-cli,**不设回退**」,
23
+ 并直接引导 `weaver-work-cli auth login --base-url <域名>`;
24
+ - ⇒ **使用前必须已登录 `weaver-work-cli`**,否则所有需要凭据的命令都会报错
25
+ (用 `weaver-work-cli auth status --no-check` 确认)。`login xiaoe-env` 不受影响。
26
+
27
+ ### 3. 修复:补齐 `userId` / `tenantKey` 传递
28
+
29
+ `auth export-weaver-env` **只导出 5 个变量**(`WEAVER_BASE_URL` / `WEAVER_ETEAMSID` /
30
+ `WEAVER_COOKIE` / `WEAVER_USER_AGENT` / `WEAVER_AGENT_TYPE`),**不含 `userId`**。
31
+ 而 `auth.userId` 会被部分命令用作「当前登录用户」的默认值——实测 `e10-data attend statis`
32
+ 缺它直接报 `ERROR: 缺少查询人员:--user <人员ID>`。
33
+
34
+ ⇒ 在凭据出口内**追加一次 `auth status --json --no-check`** 取回 `userId` / `tenantKey`
35
+ (二者为非凭据身份字段,取不到不影响凭据可用性;凭据进程间 TTL 缓存仍兜住额外开销)。
36
+
37
+ ## 0.1.19(2026-09-24)—— 发现层聚合:新增 `ds find` / `ds describe` / `ds groups`,修复 `--name` 静默失效
38
+
39
+ ### 1. 新增 `ds find`(跨分组找表,进程内并发)
40
+
41
+ 把原来「定分组 → 找表」的多跳串行收敛成一次调用。**关键收益来自进程内并发**:单次 CLI 调用约 5.4s,
42
+ 其中「凭据现取」固定约 4.8s 且**与请求条数无关**(实测取 48 条 vs 取 1 条:5.5s vs 5.8s),
43
+ 所以 N 个分组分散在 N 个进程里是纯浪费。
44
+
45
+ - `ds find --name <关键词>`:默认扫**核心 3 组**(业务表单 / e-builder表单 / 流程)+ 语义命中组(≤ `--max-groups`,默认 6)
46
+ - `--all-groups`:扫全部 58 个内置分组 —— 实测 **5.3~5.9s**(对比:逐进程并发 47.5s、逐进程串行 ~313s)
47
+ - 默认集合全 miss 时**自动升级为全分组扫一次**(`--no-escalate` 可关)
48
+ - 输出按**实体归并去重**:同一流程会同时出现在「流程」与 3 个「工作流*效率」分组,去重后落选分组记入 `alsoInGroups`
49
+ - 附带 `routeHint`:**命中分组分布本身就是通道信号**(命中 workflow 类分组 → 该走 WORKFLOW 模块通道,不走数据源)
50
+ - ⚠️ 该端点当前忽略 `name` ⇒ 关键词过滤暂由**本地**完成(抓 `--pages` × `--size` 行后按表名过滤),
51
+ 计数与样例放在 `nameFilter`(**不静默丢弃**),`--loose` 关闭该过滤;**未命中 ≠ 表不存在**(详见第 3 节)
52
+
53
+ ### 2. 新增 `ds describe`(一次并发取回「字段 + 词库」)
54
+
55
+ 两者入参同为 `(groupId, tableId)`、彼此无依赖,故并行合成一次调用(实测 ~0.5s)。
56
+
57
+ - 字段:归一化为 `main[] / detail[]`(明细子表单独成组),保留 `name / text / type / dbFieldType / isPK / dataKey`
58
+ - 词库:自动带 `data-source-query` 四参数(skillKey + sourceType + sourceGroupId + sourceTableId);
59
+ 返回 `noLexicon: true` 时是**确定结论**(四参数均取自本次调用),不再是难以区分的 500
60
+ - `relatedFields`:按 groupId/serviceMark **预判**关联字段提取是否适用 ——
61
+ 业务表单(formreport)表名是中文显示名、非物理表名,六类判定全部不适用,该跳必然空转,直接标注不必发起
62
+ - ⚠️ **不做权限校验**:`ds/listByPage` 虽然也回带 appId,但**数据源通道不拦权限**(实测未授权表照样能取到数据),
63
+ 拿它跳过校验 = 权限闸门静默失效。⇒ EB 表取数前的权限闸门仍必须单独执行 `eb perm-page`(appId 只从那次调用取)。
64
+
65
+ ### 3. `ds listByPage` 端点收敛为唯一入口 + 关键词过滤的过渡方案
66
+
67
+ **端点决策(用户 2026-09-24 确认):只保留 `/api/dw/newetl/chatBi/ds/listByPage`** ——
68
+ 另一条 `/api/ebuilder/common/ds/listByPage` **已从代码中移除**(对应的 `DS_LB_*` 双端点常量、
69
+ `E10_DS_LISTBY_PAGE_ORDER` 环境变量开关一并删除)。现由 `dsListByPage()` 单一封装发起(`DS_LB_PATH`)。
70
+
71
+ **为什么需要过渡方案**:该端点**当前忽略 `name`**(传 `name`/`tableName`/`keyword`/`searchKey`/`tableText`/`title` 任意字段名,
72
+ 都返回该分组全量,实测 8531 条)⇒ 服务端无法按关键词过滤。**后端后续会升级支持 `name`(已确认)**。
73
+
74
+ ⇒ 过渡做法:`ds find` 改为「**分页抓取 + 本地按表名过滤**」,抓取窗口 = `--size`(≤2000)× `--pages`(≤50)。
75
+
76
+ - 实测后端**接受大 pageSize**(100/500/1000/2000 均按量返回,非只认 100)→ 单次即可覆盖多数分组
77
+ - 输出新增 `nameFilter`:写明 `serverSide:false`、`pages/pageSize/rowsScanned/dropped` 与说明
78
+ - ⚠️ **未命中不再等于「表不存在」**:交付话术与输出都改为「未落在已扫描的 N 行窗口内」,
79
+ 并提示调大 `--size/--pages`;避免把「窗口不够大」误判成「没有这张表」
80
+ - 自动升级全扫增加护栏:窗口 >2000 行时不再自动全扫(否则会变成 58 组 × N 页的请求风暴)
81
+ - **后端支持 `name` 后无需改任何调用方式**:分页与 `total` 语义已按「服务端过滤」写好,届时自动恢复精确过滤
82
+
83
+ > 参照:两端点返回结构**逐字段相同**(26 个键);差异在 `name` 是否生效与数据范围
84
+ > (实测 EB 分组:ebuilder 不带 appId=633、chatBi=11535、ebuilder+appId=55,且 `name` 只在前者域内生效)——
85
+ > 这是当初考虑双端点的原因,现按决策统一走 chatBi 端点,并保留 `--app-id` 透传(EB 分组按应用分域时需要)。
86
+
87
+ ### 4. 新增 `ds groups`
88
+
89
+ 打印内置分组清单(含 `groupId` / 中文名 / `serviceMark`)、核心分组与语义规则,便于挑选 `--groups`。
90
+ ⚠️ 该清单仅用于 `ds find` 的默认扫描候选集,**不参与任何取数参数推导**;分组随环境变动时用 `--groups` 显式覆盖。
91
+
3
92
  ## 0.1.18(2026-09-24)—— 新增 ds fields-dsl / 收敛凭据明文落盘
4
93
 
5
94
  ### 1. 新增 `ds fields-dsl` 子命令
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-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`)。
12
+ > 依赖:认证**唯一来源 = `weaver-work-cli`(不设回退)**。**加解密全部由 `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
  > 唯一例外:**`e10-data login xiaoe-env`** 在「还没有任何凭据」时也能独立完成登录(用环境变量里的 XiaoE 全局 token 换 ETEAMSID;换 token 本 CLI 自己算,写 profile 仍交 `e10-login set`)。
15
15
 
@@ -32,7 +32,7 @@ e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-atte
32
32
  - `--json`:原样输出接口 JSON(供脚本/Agent 消费);默认人读输出。
33
33
  - 复杂 JSON/中文参数:推荐 `@文件` 承载(如 `--dsl @q.json`、`--where @cond.json`)。
34
34
  - ⚠️ 大整数精度(自动处理):E10 的 ID 多为 19 位,超过 JS 安全整数(2^53-1≈9e15)。CLI 解析 JSON 时**自动把超范围裸整数转成字符串**再发请求/输出(实测后端接受字符串做 =/IN/BETWEEN 数值比较),无需手动加引号,也不误伤字符串正文。
35
- - 认证:**加解密由 `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)。**没有任何可用凭据时**用 `e10-data login xiaoe-env` 登录(见下节)。
35
+ - 认证:**加解密由 `e10-login` 负责(本 CLI 不参与)**。凭据出口:`weaver-work-cli auth export-weaver-env`(解析 `WEAVER_BASE_URL` / `WEAVER_ETEAMSID` / `WEAVER_COOKIE`,另补一次 `auth status` 取 `userId` / `tenantKey`),**已移除 e10-login 回退**。命中的出口会记入 `~/.e10-cli/.e10-data-auth-mode`(只存模式名、不存凭据),下次优先复用。环境变量:`E10_DATA_AUTH_PROBE=1` 强制重新探测、`E10_DATA_AUTH_MODE=<模式名>` 手动指定、`E10_DATA_QUIET=1` 抑制告警(告警只走 stderr)。**没有任何可用凭据时**用 `e10-data login xiaoe-env` 登录(见下节)。
36
36
  - 版本更新:`npm i -g e10-data@latest`;查看源上最新 `npm view e10-data version`。
37
37
 
38
38
  ## 性能:免凭据命令与凭据缓存(v0.1.17 新增)
@@ -54,9 +54,10 @@ e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-atte
54
54
  | 变量 | 作用 |
55
55
  |---|---|
56
56
  | `E10_DATA_CRED_TTL` | 缓存 TTL 秒数,默认 `300` |
57
- | `E10_DATA_NO_CRED_CACHE=1` | 整体关闭凭据缓存(每次仍直连 e10-login) |
57
+ | `E10_DATA_NO_CRED_CACHE=1` | 整体关闭凭据缓存(每次仍直连 weaver-work-cli) |
58
58
  | `E10_DATA_AUTH_PROBE=1` / `E10_DATA_AUTH_MODE=…` | 存在时**绕过缓存**,强制重新解析凭据 |
59
- | `E10_LOGIN_ENTRY` | 显式指定 `e10-login` 的 JS 入口路径(排障用) |
59
+ | `WEAVER_WORK_CLI_ENTRY` | 显式指定 `weaver-work-cli` 的 JS 入口路径(凭据出口排障用) |
60
+ | `E10_LOGIN_ENTRY` | 显式指定 `e10-login` 的 JS 入口路径(仅 `login xiaoe-env` 落 profile 时用) |
60
61
 
61
62
  > ⚠️ TTL 采用「先到先得」:若已有一个 TTL=300 的持有者在服务,此时以更短的 `E10_DATA_CRED_TTL` 再调用不会立刻缩短它的寿命(新持有者因通道被占而静默退出)。日常使用无需关注。
62
63
 
package/bin/e10-data.mjs CHANGED
@@ -7,7 +7,11 @@
7
7
  * e10-data login xiaoe-env [--domain <url>] **无凭据时**用环境变量 XIAOE_USER_TOKEN + XIAOE_DOMAIN
8
8
  * (XiaoE 全局 token)换 ETEAMSID 并落 profile(免浏览器/免 python;2026-09-17 移植)
9
9
  * e10-data query DSL 查询 executeQueryDsl(conn/model/schema/tableId/dslQuery/debug)
10
- * e10-data ds tables|fields|list|chart|stat|count 数据源(业务模块)取数与元数据
10
+ * e10-data ds groups|find|describe|tables|fields|fields-dsl|list|chart|stat|count
11
+ * 数据源(业务模块)取数与元数据
12
+ * groups 内置分组清单(仅作 find 的默认扫描集)
13
+ * find 跨分组找表(进程内并发扇出;产物=候选表清单 + 路由建议,不取数)
14
+ * describe 一次并发取回「字段 + 词库」(不做权限校验,EB 权限仍走 eb perm-page)
11
15
  * appId 一律由调用方显式传入(v0.1.14 起 CLI 不再自动反查):
12
16
  * EB 建模表单(groupId=weaver-ebuilder-form-service)取数必须带 --app-id <id>,
13
17
  * 该值只能来自 chat_bi_eb_config_map 的权限校验查询(同一次查询给出权限与 appId);
@@ -21,9 +25,9 @@
21
25
  * e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-attendance 病假/事假,2026-09-07)
22
26
  *
23
27
  * 通用:--json 原样输出;复杂 JSON 参数支持 @文件 或 内联(中文走 @文件 更稳)
24
- * 认证:加解密全部由 e10-login 负责(本 CLI 不实现任何加解密)。凭据出口优先级:
25
- * ① e10-login auth --json → ② e10-login whoami --json → ③ e10-login whoami(文本解析)
26
- * → ④ 明文镜像 ~/.workbuddy/e10-login-cache.md(兜底,命中时告警);均不可用时提示先 e10-login oidc 登录
28
+ * 认证:凭据**唯一来源 = weaver-work-cli**(本 CLI 不实现任何加解密,**不设 e10-login 回退**):
29
+ * weaver-work-cli auth export-weaver-env —— 解析 WEAVER_BASE_URL / WEAVER_ETEAMSID / WEAVER_COOKIE
30
+ * e10-login 系出口 / 明文镜像已于 2026-09-24 停用;未登录时提示 `weaver-work-cli auth login`
27
31
  * 例外:`login xiaoe-env` 在"还没有任何凭据"时可独立完成登录(换 token 自己算,落 profile 仍交 e10-login)
28
32
  *
29
33
  * 用法示例:
@@ -270,14 +274,151 @@ function providerCacheMd() {
270
274
  });
271
275
  }
272
276
 
277
+ // ---------------- weaver-work-cli 凭据出口(首选来源)----------------
278
+ // 背景:本包要求 e10-data 的凭据**优先取自 weaver-work-cli**(与连接器无关的用户级登录体系)。
279
+ // weaver-work-cli 的登录态存于 ~/.weaver-work-cli/(@napi-rs/keyring 加密),与 e10-login 完全独立、
280
+ // 互不通用;它提供 `auth export-weaver-env` 输出 WEAVER_* 变量,其中
281
+ // WEAVER_BASE_URL / WEAVER_ETEAMSID / WEAVER_COOKIE 正好对应本 CLI 需要的三要素。
282
+ // 安全边界:**只在进程内存中解析** —— 不经 shell、不落盘、不打印、不写入任何日志/报告/错误信息;
283
+ // 凭据随进程结束即释放(后续调用仍走 v0.1.17 的内存 TTL 缓存,不落明文)。
284
+ // 定位策略:与 e10-login 相同的四段式(WEAVER_WORK_CLI_ENTRY → 模块解析 → PATH shim → 常见全局前缀)。
285
+ const WEAVER_PROVIDER_NAME = "weaver-work-cli";
286
+ const WEAVER_CLI_ENTRY_ENV = "WEAVER_WORK_CLI_ENTRY";
287
+
288
+ let _weaverEntry;
289
+ let _weaverEntryDone = false;
290
+
291
+ function resolveWeaverEntry() {
292
+ if (_weaverEntryDone) return _weaverEntry;
293
+ _weaverEntryDone = true;
294
+ const isFile = (p) => { try { return !!p && fs.statSync(p).isFile(); } catch { return false; } };
295
+
296
+ const entryFromPkgDir = (dir) => {
297
+ try {
298
+ const pj = path.join(dir, "package.json");
299
+ if (!isFile(pj)) return null;
300
+ const j = JSON.parse(fs.readFileSync(pj, "utf-8"));
301
+ if (j.name !== "weaver-work-cli") return null;
302
+ let b = j.bin;
303
+ if (typeof b === "string") b = { "weaver-work-cli": b };
304
+ const rel = b && b["weaver-work-cli"];
305
+ if (!rel) return null;
306
+ const entry = path.resolve(dir, rel);
307
+ return isFile(entry) ? entry : null;
308
+ } catch { return null; }
309
+ };
310
+
311
+ const envEntry = (process.env[WEAVER_CLI_ENTRY_ENV] || "").trim();
312
+ if (envEntry && isFile(envEntry)) { _weaverEntry = envEntry; return _weaverEntry; }
313
+
314
+ try {
315
+ const pj = createRequire(import.meta.url).resolve("weaver-work-cli/package.json");
316
+ const e = entryFromPkgDir(path.dirname(pj));
317
+ if (e) { _weaverEntry = e; return _weaverEntry; }
318
+ } catch {}
319
+
320
+ const pathDirs = String(process.env.PATH || "").split(path.delimiter).filter(Boolean);
321
+ const shimNames = process.platform === "win32"
322
+ ? ["weaver-work-cli.cmd", "weaver-work-cli.exe", "weaver-work-cli"]
323
+ : ["weaver-work-cli"];
324
+ for (const d of pathDirs) {
325
+ for (const n of shimNames) {
326
+ if (!isFile(path.join(d, n))) continue;
327
+ const e = entryFromPkgDir(path.join(d, "node_modules", "weaver-work-cli"));
328
+ if (e) { _weaverEntry = e; return _weaverEntry; }
329
+ }
330
+ }
331
+
332
+ const home = os.homedir();
333
+ const prefixes = [
334
+ path.join(path.dirname(process.execPath), "node_modules"),
335
+ path.join(path.dirname(process.execPath), "..", "lib", "node_modules"),
336
+ path.join(home, ".npm-global", "lib", "node_modules"),
337
+ process.env.APPDATA ? path.join(process.env.APPDATA, "npm", "node_modules") : null,
338
+ process.env.ProgramFiles ? path.join(process.env.ProgramFiles, "nodejs", "node_modules") : null,
339
+ ].filter(Boolean);
340
+ for (const p of prefixes) {
341
+ const e = entryFromPkgDir(path.join(p, "weaver-work-cli"));
342
+ if (e) { _weaverEntry = e; return _weaverEntry; }
343
+ }
344
+
345
+ _weaverEntry = null;
346
+ return null;
347
+ }
348
+
349
+ /** 调用 weaver-work-cli(失败返回 null,诊断留在 lastLoginDiag 供 loadAuth 汇总) */
350
+ function runWeaverCli(args, timeout = 20000) {
351
+ try {
352
+ const entry = resolveWeaverEntry();
353
+ const r = entry
354
+ ? spawnSync(process.execPath, [entry, ...args], { encoding: "utf-8", timeout, windowsHide: true })
355
+ : spawnSync("weaver-work-cli", args, { shell: true, encoding: "utf-8", timeout, windowsHide: true });
356
+ const out = (r.stdout || "").trim();
357
+ if (r.error || r.status !== 0) {
358
+ lastLoginDiag = ((r.stderr || "") + out).trim().split(/\r?\n/).slice(0, 2).join(" / ") || (r.error ? String(r.error.message) : "exit=" + r.status);
359
+ return null;
360
+ }
361
+ return out || null;
362
+ } catch (e) { lastLoginDiag = String((e && e.message) || e); return null; }
363
+ }
364
+
365
+ /** 去掉 shell 引号包装(兼容 POSIX 单/双引号与 PowerShell 双引号) */
366
+ function unquoteShellValue(raw) {
367
+ const s = String(raw || "").trim();
368
+ if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1).replace(/'\\''/g, "'");
369
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1).replace(/\\(["\\$`])/g, "$1");
370
+ return s;
371
+ }
372
+
373
+ /** 解析 KEY=VALUE 行(POSIX `export K='V'` / PowerShell `$env:K = "V"`) */
374
+ function parseShellExports(text) {
375
+ const env = {};
376
+ for (const line of String(text || "").split(/\r?\n/)) {
377
+ const s = line.trim();
378
+ if (!s) continue;
379
+ let m = s.match(/^export\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
380
+ if (!m) m = s.match(/^\$env:([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
381
+ if (m) env[m[1]] = unquoteShellValue(m[2]);
382
+ }
383
+ return env;
384
+ }
385
+
386
+ /** ⓪ weaver-work-cli auth export-weaver-env(唯一凭据来源) */
387
+ function providerWeaverWorkCli() {
388
+ const out = runWeaverCli(["auth", "export-weaver-env", "--shell", "posix"]);
389
+ if (!out) return null;
390
+ const env = parseShellExports(out);
391
+ const auth = normalizeAuth({
392
+ baseUrl: env.WEAVER_BASE_URL,
393
+ eteamsid: env.WEAVER_ETEAMSID,
394
+ cookie: env.WEAVER_COOKIE,
395
+ source: "weaver-work-cli auth export-weaver-env",
396
+ });
397
+ if (!auth) return null;
398
+ // export-weaver-env 不导出 userId / tenantKey,需另向 auth status 取。
399
+ // 二者均为「非凭据」的身份字段:userId 会被部分命令用作"当前登录用户"默认值(如 attend statis 的 --user),
400
+ // 缺失会导致这些命令报「缺少查询人员」。取不到不影响凭据本身的可用性。
401
+ try {
402
+ const st = runWeaverCli(["--json", "auth", "status", "--no-check"]);
403
+ if (st) {
404
+ const j = JSON.parse(st);
405
+ if (j && j.loggedIn) {
406
+ auth.userId = String(j.userId || "").trim();
407
+ auth.tenantKey = String(j.tenantKey || "").trim();
408
+ }
409
+ }
410
+ } catch {}
411
+ return auth;
412
+ }
413
+
414
+ // ⚠️ 2026-09-24:凭据**唯一来源 = weaver-work-cli**,不再回退 e10-login。
415
+ // providerAuthJson / providerWhoamiJson / providerWhoamiText / providerCacheMd 保留函数定义但不挂载
416
+ // (如需恢复回退,把对应条目加回本数组即可)。
273
417
  const AUTH_PROVIDERS = [
274
- ["auth-json", providerAuthJson],
275
- ["whoami-json", providerWhoamiJson],
276
- ["whoami-text", providerWhoamiText],
277
- ["cache-md", providerCacheMd],
418
+ ["weaver-work-cli", providerWeaverWorkCli],
278
419
  ];
279
- /** 兜底出口不进记忆:否则 e10-login 偶发不可用一次,就会把"用镜像"永久设为首选、次次告警 */
280
- const AUTH_FALLBACK_PROVIDERS = new Set(["cache-md"]);
420
+ /** 预留:登记"命中后不进记忆"的出口(当前为空——唯一出口需要被记忆) */
421
+ const AUTH_FALLBACK_PROVIDERS = new Set();
281
422
 
282
423
  function readAuthModeMemo() {
283
424
  try {
@@ -296,7 +437,13 @@ function loadAuth() {
296
437
  const memo = override || (process.env.E10_DATA_AUTH_PROBE ? null : readAuthModeMemo());
297
438
  const order = memo
298
439
  ? AUTH_PROVIDERS.filter(([n]) => n === memo).concat(AUTH_PROVIDERS.filter(([n]) => n !== memo))
299
- : AUTH_PROVIDERS;
440
+ : AUTH_PROVIDERS.slice();
441
+ // 首选来源固定为 weaver-work-cli:无论 memo 指向谁,都把它提到最前再依次尝试
442
+ // (仅当未用 E10_DATA_AUTH_MODE 显式指定时;它取不到凭据会自动回落后续出口)
443
+ if (!override) {
444
+ const wi = order.findIndex(([n]) => n === WEAVER_PROVIDER_NAME);
445
+ if (wi > 0) order.unshift(order.splice(wi, 1)[0]);
446
+ }
300
447
 
301
448
  const diag = [];
302
449
  for (const [name, fn] of order) {
@@ -317,10 +464,10 @@ function loadAuth() {
317
464
  }
318
465
 
319
466
  throw new Error(
320
- "未取得登录凭据(解密由 e10-login 负责,本 CLI 不参与)。请依次检查:\n" +
321
- " ① e10-login 是否可用:`e10-login --version`(缺失 → `npm i -g e10-login`);`e10-login whoami` 应能输出 cookies\n" +
322
- " ② 是否已登录 / 登录态是否失效:`e10-login oidc` 重新登录(当前 profile: " + profile + ")\n" +
323
- " ③ auth 文件:" + (fs.existsSync(file) ? "存在" : "不存在") + "(" + file + ")—— 其加解密由 e10-login 负责\n" +
467
+ "未取得登录凭据(唯一来源 = weaver-work-cli,**不设回退**)。请按顺序检查:\n" +
468
+ " ① 命令是否可用:`weaver-work-cli --version`(缺失 → 安装 weaver-work-cli)\n" +
469
+ " ② 是否已登录:`weaver-work-cli auth status --no-check`\n" +
470
+ " ③ 未登录 / 登录态失效 → 执行 `weaver-work-cli auth login --base-url <域名>` 重新登录(当前 profile: " + profile + ")\n" +
324
471
  (diag.length ? " 探测诊断:" + diag.join(" | ") + "\n" : "")
325
472
  );
326
473
  }
@@ -544,7 +691,7 @@ async function cmdLoginXiaoeEnv(args, json) {
544
691
  // 为什么需要:loadAuth() 写在模块顶层,过去"未知命令"也要先付一整笔取凭据的开销
545
692
  // (Windows 上 ~1.0s,去 shell 后 ~0.22s)才打印用法。v0.1.17 起提前判定。
546
693
  const CMD_SUBS = {
547
- ds: ["tables", "fields", "fields-dsl", "list", "chart", "stat", "count"],
694
+ ds: ["groups", "find", "describe", "tables", "fields", "fields-dsl", "list", "chart", "stat", "count"],
548
695
  doc: ["count", "list"],
549
696
  noun: ["list"],
550
697
  field: ["id2name", "name2id"],
@@ -608,9 +755,7 @@ const USER = { id: auth.userId || "", tenantKey: auth.tenantKey || "" };
608
755
  function checkAuthFail(resp, text, j) {
609
756
  if (resp.status === 401 || (j && String(j.code) === "-1" && /token|登录|失效/i.test(String(j.msg || "")))) {
610
757
  const from = (auth && auth.source) || "未知";
611
- const hint = /镜像/.test(from)
612
- ? "当前用的是明文镜像兜底凭据(可能已过期),请先确认 `e10-login whoami` 能正常导出 cookies,或执行 `e10-login oidc` 重新登录"
613
- : "凭据由 e10-login 提供,请执行 `e10-login oidc` 重新登录";
758
+ const hint = "凭据由 weaver-work-cli 提供,请执行 `weaver-work-cli auth login --base-url <域名>` 重新登录";
614
759
  throw new Error("登录态失效(" + (j.msg || resp.status) + "):凭据来源=" + from + ";" + hint);
615
760
  }
616
761
  }
@@ -865,6 +1010,15 @@ function parseOrder(val) {
865
1010
  function dsFieldsForm(gid, tid, st) {
866
1011
  return { groupId: gid, sourceId: tid, sourceType: st, detailFieldsGroup: "true", enableMerge: "true", needTableName: "true", params: "[]" };
867
1012
  }
1013
+ // ---- ds listByPage 端点(唯一):/api/dw/newetl/chatBi/ds/listByPage ----
1014
+ // ⚠️ 该端点**当前会忽略 name**(传 name/tableName/keyword/title 等任意字段名,都返回该分组全量,实测 8531 条)。
1015
+ // 后端后续会升级支持 name 过滤(用户 2026-09-24 确认)——升级后 `ds find` / `ds tables --name` **无需改代码**
1016
+ // 即自动恢复服务端过滤能力(本封装与分页逻辑已按「total 为过滤后总数」的语义写好)。
1017
+ // 在此之前,`ds find` 走「分页抓取 + 本地过滤」,未命中只代表「未在已扫描窗口内命中」。
1018
+ const DS_LB_PATH = "/api/dw/newetl/chatBi/ds/listByPage";
1019
+ async function dsListByPage(form) {
1020
+ return postMultipart(DS_LB_PATH, form);
1021
+ }
868
1022
  // ds 取数统一入口:appId 只认显式 --app-id <id>
869
1023
  // (--app-id auto / --no-app-id 一律视为未传:v0.1.14 起不再自动解析,也不做缺失拦截)
870
1024
  async function dsDto(args, tid, gid, pageNo, pageSize, queryType) {
@@ -877,7 +1031,7 @@ async function cmdTables(args, json) {
877
1031
  const gid = need("group", args);
878
1032
  const form = { name: args.name || "", pageNo: args.page || "1", pageSize: args.size || "100", groupId: gid, sourceType: args["source-type"] || "LOGIC", dataUsage: "" };
879
1033
  if (typeof args["app-id"] === "string" && args["app-id"] !== "" && args["app-id"] !== "auto") form.appId = args["app-id"];
880
- const r = await postMultipart("/api/dw/newetl/chatBi/ds/listByPage", form);
1034
+ const r = await dsListByPage(form);
881
1035
  if (json) return r;
882
1036
  const recs = ((r.data || {}).records) || [];
883
1037
  printHuman("分组表清单 (total=" + ((r.data || {}).total) + ")", recs, ["id", "name", "appId", "tableText", "detailTable", "usage"]);
@@ -950,6 +1104,433 @@ async function cmdCount(args, json) {
950
1104
  console.log("totalCount = " + (data.totalCount ?? "-"));
951
1105
  }
952
1106
 
1107
+ // ================= ds 发现层聚合:groups / find / describe(2026-09-24 新增)=================
1108
+ // 背景:一次「表单业务词」问数原本要串行跑 5 跳(定分组 → 找表 → 取字段 → 查词库 → 查权限),
1109
+ // 每跳都是一次独立 CLI 调用。实测单次调用固定开销 ~5.4s,其中「凭据现取」~4.8s 且
1110
+ // **与请求条数/返回体量无关**(取 48 条 vs 取 1 条:5.5s vs 5.8s)⇒ 5 跳 ≈ 27s;
1111
+ // 若逐进程扫 58 个分组,串行累计约 313s。
1112
+ // 故把「找表」「字段+词库」各收敛成 1 次 CLI 调用,**在进程内并发发 HTTP**(一次凭据搞定)。
1113
+ // 实测同样的全分组扇出:进程内并发 ≈5.3~5.9s(逐进程 47.5s / 逐进程串行 ~313s)。
1114
+ //
1115
+ // ⚠️ 权限闸门不在本命令范围:`ds/listByPage` 虽然也回带 appId,但**数据源通道不拦权限**
1116
+ // (实测:未授权表在数据源上照样能取到数据)。拿 listByPage 的 appId 跳过校验 = 闸门静默失效。
1117
+ // ⇒ EB 建模表单取数前**必须单独**执行 `e10-data eb perm-page`,appId 只从那次调用取。
1118
+
1119
+ // 分组清单(技能文档 data-source-query.md 1.1 的 58 个)。**仅作 ds find 的「默认扫描候选集」**,
1120
+ // 不参与任何取数参数推导。分组随环境模块安装情况增减,故支持 --groups 显式覆盖。
1121
+ const DS_GROUPS = [
1122
+ [1, "业务表单", "weaver-formreport-service", "form"],
1123
+ [2, "流程", "weaver-workflow-report-serviceworkflowreport", "workflow"],
1124
+ [3, "任务", "weaver-project-servicetask", "task"],
1125
+ [4, "日志", "weaver-elog-service", "elog"],
1126
+ [5, "会议", "weaver-meeting-service", "meeting"],
1127
+ [6, "日报", "weaver-blog-service", "blog"],
1128
+ [7, "OKR", "weaver-wr-goal-service", "goal"],
1129
+ [8, "流程列表", "weaver-workflow-list-service", "workflow"],
1130
+ [9, "考勤", "weaver-attend-service", "attend"],
1131
+ [10, "CRM", "weaver-crm-service", "customer"],
1132
+ [11, "组织画像", "weaver-portrait-service", "portrait"],
1133
+ [12, "工作流报告", "weaver-workflow-report-serviceworkflow_report", "workflow"],
1134
+ [13, "绩效", "weaver-wr-performance-service", "kpi"],
1135
+ [14, "项目", "weaver-project-service", "mainline"],
1136
+ [15, "基础定时模块", "weaver-basic-schedule-service", "escheduler"],
1137
+ [16, "知识画像", "weaver-doc-service", "document"],
1138
+ [17, "HRM", "weaver-hrm-service", "hrm"],
1139
+ [18, "e-builder报表", "weaver-edcreportd-service", null],
1140
+ [19, "e-builder应用", "weaver-ebuilder-app-service", "ebuilder"],
1141
+ [20, "e-builder表单", "weaver-ebuilder-form-service", "ebuilder"],
1142
+ [21, "日程", "weaver-calendar-service", "calendar"],
1143
+ [22, "文档", "weaver-doc-servicedocument", "document"],
1144
+ [23, "文件", "weaver-file-service", "file"],
1145
+ [24, "公文管理", "weaver-odoc-service", "odoc"],
1146
+ [25, "SCRM", "weaver-crm-market-service", "scrm"],
1147
+ [26, "协作区", "weaver-cowork-service", "cowork"],
1148
+ [27, "数据源", "weaver-datasource-service", "datasource"],
1149
+ [28, "计划报告", "weaver-wr-plan-service", "workreport"],
1150
+ [29, "电子签", "weaver-signcenter-service", "signcontract"],
1151
+ [30, "表单加密", "weaver-datasecurity", "datasecurity"],
1152
+ [31, "人事", "weaver-hr-service", null],
1153
+ [32, "动作流", "weaver-esb-setting-serviceesbCustom", "esb"],
1154
+ [33, "发票云", "weaver-inc-biz-service", "inc"],
1155
+ [34, "绩效核算", "weaver-ebuilder-contract-servicecmdatauFJXHS", "ebuilder"],
1156
+ [35, "EB合同管理", "weaver-ebuilder-contract-service", null],
1157
+ [36, "待办事项", "weaver-my-service", null],
1158
+ [37, "签名", "weaver-signature-service", null],
1159
+ [38, "邮件", "weaver-mail-base-service", "email"],
1160
+ [39, "薪酬", "weaver-hrm-salary", "hrmsalary"],
1161
+ [40, "系统安全", "weaver-security-framework-service", "security"],
1162
+ [41, "公文交换中心", "weaver-odocexchange-service", "odocexchange"],
1163
+ [42, "招聘管理", "weaver-recruit-service", "recruit"],
1164
+ [43, "微搜", "weaver-esearch-search-service", null],
1165
+ [44, "统一审批中心", "weaver-intunifytodo-server-config-service", "intunifytodos"],
1166
+ [45, "公共数据源", "weaver-component-web-service", "common"],
1167
+ [46, "数据分析", "weaver-analyze-service", "analyze"],
1168
+ [47, "财务模块", "weaver-fna-expense-servicefexs", "fna"],
1169
+ [48, "ESB连接器", "weaver-esb-setting-serviceesbConnect", "esb"],
1170
+ [49, "AI连接器", "weaver-ai-application-service", "ai"],
1171
+ [50, "外部流程", "weaver-workflow-core-serviceworkflowCoreOuterwf", "workflow"],
1172
+ [51, "绩效考核", "weaver-wr-performance-eb-service", "ebuilder"],
1173
+ [52, "问题定位平台", "weaver-tissot-service", "tissot"],
1174
+ [53, "工作流基础效率", "weaver-workflow-report-serviceworkflowEffReport", "workflow"],
1175
+ [54, "工作流操作者效率", "weaver-workflow-report-serviceworkflowEffOperateReport", "workflow"],
1176
+ [55, "工作流节点效率", "weaver-workflow-report-serviceworkflowEffNodeReport", "workflow"],
1177
+ [56, "规则引擎", "weaver-logicflow-service", "logicflow"],
1178
+ [57, "工作流表单", "weaver-workflow-report-serviceworkflowFormReport", "workflow"],
1179
+ [58, "统一认证中心", "weaver-intunifyauth-server-base-service", null],
1180
+ ];
1181
+
1182
+ // 必探的核心分组(技能文档「分组探查强制范围」:e-builder 表单 + 业务表单 必探;流程类业务词也常落在流程分组)
1183
+ const DS_CORE_GROUPS = [
1184
+ "weaver-formreport-service",
1185
+ "weaver-ebuilder-form-service",
1186
+ "weaver-workflow-report-serviceworkflowreport",
1187
+ ];
1188
+ // 语义命中规则:只是「关键词 → 分组名片段」的启发式,命中即并入扫描集(不做任何取数参数推导)
1189
+ const DS_FIND_SEMANTIC_RULES = [
1190
+ { tag: "考勤/假期", re: /考勤|打卡|出勤|请假|假期|年假|事假|病假|加班|外勤|工时|调休|补卡|申诉/, groups: ["weaver-attend-service", "weaver-hr-service"] },
1191
+ { tag: "流程/审批", re: /流程|审批|待办|已办|发起|抄送|传阅|征询|签字|节点|报销|申请单/, groups: ["weaver-workflow-report-serviceworkflowreport", "weaver-workflow-list-service"] },
1192
+ { tag: "日报/周报", re: /日报|周报|月报|年报|工作总结/, groups: ["weaver-blog-service", "weaver-wr-plan-service"] },
1193
+ { tag: "文档/附件", re: /文档|附件|文件夹|共享|资料/, groups: ["weaver-doc-servicedocument", "weaver-doc-service", "weaver-file-service"] },
1194
+ { tag: "项目/任务", re: /项目|任务|工时|里程碑|延期|验收/, groups: ["weaver-project-service", "weaver-project-servicetask"] },
1195
+ { tag: "客户/商机", re: /客户|商机|线索|联系人|公海|销售|CRM|营销/i, groups: ["weaver-crm-service", "weaver-crm-market-service"] },
1196
+ { tag: "会议/日程", re: /会议|日程|日历|预约|会议室|预定/, groups: ["weaver-meeting-service", "weaver-calendar-service"] },
1197
+ { tag: "人事/招聘", re: /招聘|入职|离职|offer|人才|员工|人员|通讯录|调岗|组织/i, groups: ["weaver-recruit-service", "weaver-hr-service", "weaver-hrm-service"] },
1198
+ { tag: "薪酬/绩效", re: /薪酬|薪资|工资|社保|公积金|绩效|考核|KPI|OKR/i, groups: ["weaver-hrm-salary", "weaver-wr-performance-service", "weaver-wr-goal-service"] },
1199
+ { tag: "合同/收款", re: /合同|签约|收款|回款|开票|发票|应收/, groups: ["weaver-ebuilder-contract-service", "weaver-ebuilder-contract-servicecmdatauFJXHS", "weaver-inc-biz-service"] },
1200
+ { tag: "邮件/公文", re: /邮件|收件箱|公文|发文|档案|借阅/, groups: ["weaver-mail-base-service", "weaver-odoc-service"] },
1201
+ { tag: "资产/日志", re: /资产|台账|日志|动作流|自动化/, groups: ["weaver-elog-service", "weaver-esb-setting-serviceesbCustom"] },
1202
+ ];
1203
+
1204
+ function dsGroupLabel(gid) { const g = DS_GROUPS.find((x) => x[2] === gid); return g ? g[1] : null; }
1205
+ function dsGroupMark(gid) { const g = DS_GROUPS.find((x) => x[2] === gid); return g ? g[3] : null; }
1206
+
1207
+ /** 进程内并发闸门:一次凭据、N 个 HTTP 并发(把「N 次 CLI 调用」压成 1 次调用的核心) */
1208
+ async function mapLimit(items, limit, fn) {
1209
+ const out = new Array(items.length);
1210
+ let next = 0;
1211
+ const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
1212
+ for (;;) {
1213
+ const i = next++;
1214
+ if (i >= items.length) return;
1215
+ out[i] = await fn(items[i], i);
1216
+ }
1217
+ });
1218
+ await Promise.all(workers);
1219
+ return out;
1220
+ }
1221
+
1222
+ async function cmdDsGroups(args, json) {
1223
+ const pat = args.pattern ? String(args.pattern) : null;
1224
+ const groups = DS_GROUPS
1225
+ .filter((g) => !pat || String(g[2]).includes(pat) || String(g[1]).includes(pat))
1226
+ .map((g) => ({ groupId: g[2], label: g[1], serviceMark: g[3] }));
1227
+ if (json) {
1228
+ return {
1229
+ count: groups.length,
1230
+ groups,
1231
+ coreGroups: DS_CORE_GROUPS,
1232
+ semanticRules: DS_FIND_SEMANTIC_RULES.map((r) => ({ tag: r.tag, groups: r.groups })),
1233
+ note: "内置分组清单,仅用于 ds find 的默认扫描候选集;分组随环境变动时用 --groups 显式覆盖",
1234
+ };
1235
+ }
1236
+ printHuman("内置分组清单 " + groups.length + " 个", groups, ["groupId", "label", "serviceMark"]);
1237
+ }
1238
+
1239
+ /** 选扫描集:--groups 显式 > --all-groups 全量 > 核心分组 + 语义命中 */
1240
+ function pickFindGroups(args, keyword) {
1241
+ if (args.groups) {
1242
+ return { mode: "explicit", groups: String(args.groups).split(",").map((s) => s.trim()).filter(Boolean) };
1243
+ }
1244
+ if (args["all-groups"]) return { mode: "all", groups: DS_GROUPS.map((g) => g[2]) };
1245
+ const max = Math.max(parseInt(args["max-groups"] || "6", 10) || 6, DS_CORE_GROUPS.length);
1246
+ const picked = [];
1247
+ const push = (gid) => { if (gid && !picked.includes(gid)) picked.push(gid); };
1248
+ for (const gid of DS_CORE_GROUPS) push(gid);
1249
+ for (const r of DS_FIND_SEMANTIC_RULES) if (r.re.test(keyword)) for (const gid of r.groups) push(gid);
1250
+ return { mode: "core", groups: picked.slice(0, max) };
1251
+ }
1252
+
1253
+ /** 进程内并发扫分组(每分组 1~N 页 listByPage)。
1254
+ * ⚠️ 端点当前忽略 name ⇒ 抓回来的是「全量窗口」,关键词过滤由调用方在本地做;
1255
+ * 窗口大小 = pageSize × pages,**未命中只代表未落在窗口内**(后端支持 name 后即为精确结果)。
1256
+ * ⚠️ appId:EB 分组(weaver-ebuilder-form-service)**按应用分域**,需要时透传 */
1257
+ async function dsScanGroups(gidList, keyword, sourceType, pageSize, concurrency, appId, pages) {
1258
+ return mapLimit(gidList, concurrency, async (gid) => {
1259
+ const out = { groupId: gid, label: dsGroupLabel(gid), serviceMark: dsGroupMark(gid), records: [], error: null, total: null, pagesFetched: 0 };
1260
+ try {
1261
+ for (let p = 1; p <= pages; p++) {
1262
+ const form = { name: keyword, pageNo: String(p), pageSize: String(pageSize), groupId: gid, sourceType, dataUsage: "" };
1263
+ if (appId) form.appId = String(appId);
1264
+ const r = await dsListByPage(form);
1265
+ const recs = ((r.data || {}).records) || [];
1266
+ if (out.total === null) out.total = (r.data || {}).total ?? null; // 后端过滤生效时 = 过滤后总数
1267
+ out.records = out.records.concat(recs);
1268
+ out.pagesFetched++;
1269
+ if (recs.length < pageSize) break; // 已到末页
1270
+ }
1271
+ } catch (e) { out.error = String((e && e.message) || e); }
1272
+ return out;
1273
+ });
1274
+ }
1275
+
1276
+ // 同一实体(如一个流程)会在多个「报表/效率」分组里重复出现 → 归并去重,并保留落选分组做溯源
1277
+ function dsGroupPriority(gid) {
1278
+ if (gid === "weaver-ebuilder-form-service") return 100;
1279
+ if (gid === "weaver-formreport-service") return 95;
1280
+ if (gid === "weaver-workflow-report-serviceworkflowreport") return 90;
1281
+ if (/workflowEff|workflowFormReport|workflow_report$/i.test(gid)) return 20;
1282
+ return 50;
1283
+ }
1284
+ function mergeDsCandidates(scanResults, limit) {
1285
+ const map = new Map();
1286
+ for (const r of scanResults) {
1287
+ for (const rec of r.records) {
1288
+ const gid = rec.groupId || r.groupId;
1289
+ const name = String(rec.originalName || rec.tableText || rec.name || "").trim();
1290
+ const key = name.replace(/\s+/g, "").toLowerCase() || gid + "|" + rec.id;
1291
+ const item = {
1292
+ groupId: gid, groupLabel: r.label, serviceMark: rec.serviceMark ?? r.serviceMark ?? null,
1293
+ tableId: String(rec.id ?? ""), name: String(rec.tableText || rec.name || name), originalName: name,
1294
+ objType: rec.objType ?? null, physicalTable: rec.physicalTable ?? null, appId: rec.appId ?? null,
1295
+ isPhysical: rec.isPhysical ?? null, detailTable: !!rec.detailTable, usage: rec.usage ?? null,
1296
+ sourceType: rec.sourceType ?? null, alsoInGroups: [], _pri: dsGroupPriority(gid),
1297
+ };
1298
+ const prev = map.get(key);
1299
+ if (!prev) { map.set(key, item); continue; }
1300
+ const win = prev._pri >= item._pri ? prev : item;
1301
+ const lose = prev._pri >= item._pri ? item : prev;
1302
+ win.alsoInGroups = win.alsoInGroups.concat(
1303
+ [{ groupId: lose.groupId, groupLabel: lose.groupLabel, tableId: lose.tableId, serviceMark: lose.serviceMark }],
1304
+ lose.alsoInGroups || [],
1305
+ );
1306
+ map.set(key, win);
1307
+ }
1308
+ }
1309
+ const list = [...map.values()].sort((a, b) => b._pri - a._pri || String(a.originalName).localeCompare(String(b.originalName)));
1310
+ const n = parseInt(limit || "0", 10) || 0;
1311
+ return (n > 0 ? list.slice(0, n) : list).map((x) => { const { _pri, ...rest } = x; return rest; });
1312
+ }
1313
+ // 路由建议:命中所在的分组分布本身就是通道信号(比“先定通道再找表”更先验)
1314
+ function dsRouteHint(cands) {
1315
+ const has = (fn) => cands.some(fn);
1316
+ if (has((c) => /workflow/i.test(c.groupId) || c.serviceMark === "workflow")) {
1317
+ return { channel: "WORKFLOW", desc: "WORKFLOW 模块配置通道 —— 流程类问题一律走模块接口,不用 DSL,也不走数据源", reason: "命中流程类分组" };
1318
+ }
1319
+ if (has((c) => c.groupId === "weaver-ebuilder-form-service" || c.serviceMark === "ebuilder")) {
1320
+ return { channel: "DS_EB", desc: "数据源通道(EB 建模表单)—— 取数前必须先过 eb perm-page 权限校验,appId 取自那次校验", reason: "命中 EB 分组" };
1321
+ }
1322
+ if (has((c) => c.serviceMark === "form")) {
1323
+ return { channel: "DS_FORM", desc: "数据源通道(业务表单 / 流程业务表单)", reason: "命中业务表单分组" };
1324
+ }
1325
+ if (has((c) => c.serviceMark === "task" || c.serviceMark === "mainline")) return { channel: "PROJECT/PMO", desc: "项目类:PMO 指标走 business/project-mgmt + DSL;明细/下拉选项走 PROJECT 模块", reason: "命中项目分组" };
1326
+ if (has((c) => c.serviceMark === "attend")) return { channel: "EHR", desc: "EHR 模块配置通道(考勤/人事)", reason: "命中考勤分组" };
1327
+ if (has((c) => c.serviceMark === "blog")) return { channel: "BLOG_DAILY", desc: "BLOG_DAILY 模块配置通道(日报)", reason: "命中日报分组" };
1328
+ if (has((c) => c.serviceMark === "document")) return { channel: "DOCUMENT", desc: "DOCUMENT 模块配置通道(文档)", reason: "命中文档分组" };
1329
+ if (has((c) => c.serviceMark === "customer" || c.serviceMark === "scrm")) return { channel: "CRM", desc: "CRM 模块配置通道(客户/商机/线索)", reason: "命中 CRM 分组" };
1330
+ if (has((c) => c.serviceMark === "esb")) return { channel: "ESB", desc: "动作流(ESB)—— 无标准问数通道,需人工确认", reason: "仅命中动作流分组" };
1331
+ return { channel: "DS", desc: "数据源通道(ds list / chart / stat / count)", reason: "通用" };
1332
+ }
1333
+
1334
+ async function cmdDsFind(args, json) {
1335
+ const keyword = String(args.name || args.keyword || "").trim();
1336
+ if (!keyword) throw new Error("缺少必填参数 --name <关键词>");
1337
+ const sourceType = args["source-type"] || "LOGIC";
1338
+ // 端点忽略 name ⇒ 抓取窗口 = --size × --pages(后端支持 name 后,total 即为过滤后总数、pages 语义不变)
1339
+ const pageSize = Math.min(Math.max(parseInt(args.size || "100", 10) || 100, 1), 2000);
1340
+ const pages = Math.min(Math.max(parseInt(args.pages || "1", 10) || 1, 1), 50);
1341
+ const concurrency = Math.max(1, parseInt(args.concurrency || "10", 10) || 10);
1342
+ const appId = (typeof args["app-id"] === "string" && args["app-id"] !== "" && args["app-id"] !== "auto") ? args["app-id"] : null;
1343
+ const picked = pickFindGroups(args, keyword);
1344
+
1345
+ const t0 = Date.now();
1346
+ let scan = await dsScanGroups(picked.groups, keyword, sourceType, pageSize, concurrency, appId, pages);
1347
+ // 后端 name 搜索是宽松的:实测部分分组会忽略 name 直接返回全量、或按描述命中 ⇒ 本地再按「表名包含关键词」过一遍。
1348
+ // ⚠️ 端点忽略 name ⇒ 关键词过滤当前**只能在本地做**(见文件头 DS_LB_PATH 注释)。
1349
+ // 落选项**不静默丢弃**,计数与样例放在 nameFilter 里;--loose 可完全关闭该过滤(保守用法)。
1350
+ const kwNorm = keyword.replace(/\s+/g, "").toLowerCase();
1351
+ const looseSample = [];
1352
+ let looseDropped = 0;
1353
+ let rowsScanned = 0;
1354
+ const applyStrict = (rows) => rows.map((r) => {
1355
+ rowsScanned += r.records.length;
1356
+ if (args.loose) return r;
1357
+ const keep = [], drop = [];
1358
+ for (const rec of r.records) {
1359
+ const nm = String(rec.originalName || rec.tableText || rec.name || "").replace(/\s+/g, "").toLowerCase();
1360
+ (nm.includes(kwNorm) ? keep : drop).push(rec);
1361
+ }
1362
+ looseDropped += drop.length;
1363
+ for (const d of drop.slice(0, 3)) {
1364
+ if (looseSample.length < 6) looseSample.push({ groupId: r.groupId, name: String(d.originalName || d.tableText || d.name || "") });
1365
+ }
1366
+ return { ...r, records: keep };
1367
+ });
1368
+ scan = applyStrict(scan);
1369
+ let escalated = false;
1370
+ // 默认小集合全 miss → 自动升级为全分组扫一次(语义规则没覆盖到时兜底;全扫也是进程内并发)
1371
+ // ⚠️ 端点忽略 name 时「miss」不代表表不存在(只是没落进窗口)⇒ 窗口较大时不再自动全扫,避免 58 组 × N 页的请求风暴
1372
+ const escalateOk = pages * pageSize <= 2000;
1373
+ if (!scan.some((r) => r.records.length) && picked.mode === "core" && !args["no-escalate"] && escalateOk) {
1374
+ escalated = true;
1375
+ const seen = new Set(picked.groups);
1376
+ const extra = DS_GROUPS.map((g) => g[2]).filter((gid) => !seen.has(gid));
1377
+ scan = scan.concat(applyStrict(await dsScanGroups(extra, keyword, sourceType, pageSize, concurrency, appId, pages)));
1378
+ }
1379
+ const elapsedMs = Date.now() - t0;
1380
+
1381
+ const okScan = scan.filter((r) => !r.error);
1382
+ const errs = scan.filter((r) => r.error);
1383
+ // 全部失败且像登录态问题 → 直接抛(不要把 401 伪装成“没找到表”)
1384
+ if (!okScan.length && errs.length && errs.some((e) => /登录态失效|401/.test(e.error))) {
1385
+ throw new Error("登录态失效:请重新登录 weaver-work-cli(`weaver-work-cli auth login --base-url <域名>`)后重试(本次未取到任何分组)");
1386
+ }
1387
+ const candidates = mergeDsCandidates(scan, args.limit);
1388
+ const out = {
1389
+ keyword, sourceType, scanMode: picked.mode + (escalated ? "+escalated" : ""),
1390
+ scannedGroups: scan.length, hitGroups: scan.filter((r) => r.records.length).length,
1391
+ escalated, elapsedMs, concurrency,
1392
+ candidates, routeHint: dsRouteHint(candidates),
1393
+ // 端点忽略 name:必须让调用方知道「未命中 ≠ 表不存在」
1394
+ nameFilter: args.loose
1395
+ ? { serverSide: false, filtered: false, pages, pageSize, rowsScanned, note: "--loose:未做本地过滤,直接采用后端返回" }
1396
+ : {
1397
+ serverSide: false, filtered: true, pages, pageSize, rowsScanned, dropped: looseDropped, sample: looseSample,
1398
+ note: "端点 /api/dw/newetl/chatBi/ds/listByPage **当前忽略 name**(后端待升级)⇒ 本次为「抓 " + pages + " 页 × " + pageSize + " 行 + 本地按表名过滤」,共比对 " + rowsScanned + " 行;"
1399
+ + "**未命中只代表未落在该窗口内,不等于该表不存在**(若该分组行数 > " + (pages * pageSize) + " 请调大 --size/--pages)。后端支持 name 后自动恢复精确过滤,无需改调用方式。",
1400
+ },
1401
+ scopeNote: scan.some((r) => r.groupId === "weaver-ebuilder-form-service")
1402
+ ? (appId
1403
+ ? "本次已带 --app-id=" + appId + "(EB 分组按应用分域,用于限定/对齐应用范围)"
1404
+ : "⚠️ 本次未带 --app-id:EB 分组(weaver-ebuilder-form-service)按应用分域,如需限定某个应用请带 --app-id <该应用ID>")
1405
+ : null,
1406
+ errors: errs.map((e) => ({ groupId: e.groupId, error: e.error })),
1407
+ };
1408
+ if (json) return out;
1409
+
1410
+ console.log("== 找表「" + keyword + "」扫描 " + out.scannedGroups + " 组 / 抓取 " + rowsScanned + " 行 / 命中 " + out.hitGroups + " 组 (" + elapsedMs + "ms" + (escalated ? ",已升级全扫" : "") + ") ==");
1411
+ if (!candidates.length) {
1412
+ console.log("(未命中) ⚠️ 端点当前忽略 name,本次是「抓 " + pages + " 页 × " + pageSize + " 行 + 本地过滤」比对 " + rowsScanned + " 行;");
1413
+ console.log(" **未命中 ≠ 该表不存在** —— 该分组行数若超过 " + (pages * pageSize) + ",请调大 --size(≤2000)/--pages(≤50)再试;");
1414
+ console.log(" 也可显式指定 --groups a,b / --all-groups;分组清单见 ds groups。");
1415
+ } else {
1416
+ for (const c of candidates) {
1417
+ const extra = c.alsoInGroups && c.alsoInGroups.length ? " [同实体亦见于: " + c.alsoInGroups.map((x) => x.groupId).join(", ") + "]" : "";
1418
+ console.log([c.groupId, c.tableId, c.originalName || c.name, c.physicalTable ? "物理表=" + c.physicalTable : "", c.appId ? "appId=" + c.appId : ""].filter(Boolean).join("\t") + extra);
1419
+ }
1420
+ }
1421
+ console.log("路由建议: " + out.routeHint.channel + " —— " + out.routeHint.desc);
1422
+ if (out.scopeNote) console.log("范围提示: " + out.scopeNote);
1423
+ if (errs.length) console.log("⚠ 有 " + errs.length + " 个分组请求失败(--json 看详情)");
1424
+ }
1425
+
1426
+ // describe:一次并发取回「字段 + 词库」(两者入参同为 (groupId, tableId),彼此无依赖)
1427
+ // ⚠️ 本命令**不做权限校验** —— EB 建模表单取数前的权限闸门仍必须**单独**执行 `e10-data eb perm-page`
1428
+ // (appId 只从那次调用取)。原因:数据源通道本身不拦权限,权限判定与取数必须分开。
1429
+ function collectFieldNodes(list, out, subName) {
1430
+ for (const f of list || []) {
1431
+ if (!f) continue;
1432
+ if (f.isDetailtable) {
1433
+ const node = { subTableName: f.name, fields: [] };
1434
+ collectFieldNodes(f.fields, node.fields, f.name);
1435
+ out.push({ __detail: true, ...node });
1436
+ } else {
1437
+ out.push({
1438
+ name: f.name, text: f.text ?? null, type: f.type ?? null, dbFieldType: f.dbFieldType ?? null,
1439
+ isPK: !!f.mainPrimaryKey, subTable: subName || null,
1440
+ dataKey: (f.config && f.config.dataKey) || null,
1441
+ formTableId: (f.config && f.config.formTableId) || null,
1442
+ });
1443
+ }
1444
+ }
1445
+ }
1446
+ function normalizeFieldsResp(r) {
1447
+ const node = (r.data || [])[0] || {};
1448
+ const flat = [];
1449
+ collectFieldNodes(node.fields, flat, null);
1450
+ const main = flat.filter((x) => !x.__detail);
1451
+ const detail = flat.filter((x) => x.__detail).map((x) => ({ subTableName: x.subTableName, fieldCount: x.fields.length, fields: x.fields }));
1452
+ return {
1453
+ tableName: node.name ?? null, tableId: node.id ?? null,
1454
+ mainCount: main.length, detailCount: detail.reduce((a, b) => a + b.fieldCount, 0), main, detail,
1455
+ };
1456
+ }
1457
+ // 关联字段提取「表来源判定」的预判:groupId/serviceMark 已隐含表来源大类,不必再花 1~6 次 DSL 去反推
1458
+ function dsRelatedHint(gid, detailCount) {
1459
+ if (gid === "weaver-ebuilder-form-service") {
1460
+ return { likelyApplicable: true, tableOrigin: "eb", note: "EB 表:按 ebdf_obj ⋈ form_table ⋈ form 三表 JOIN 定位,再用 form_field / form_table_field 提关联字段;明细子表走 sub_form(本表明细字段 " + detailCount + " 个)" };
1461
+ }
1462
+ if (/^weaver-workflow/.test(gid)) return { likelyApplicable: true, tableOrigin: "flow", note: "流程相关表:按 ec_weaver_wfp_fusion 的 form_field / form_table_field 提关联字段" };
1463
+ if (gid === "weaver-formreport-service") {
1464
+ return { likelyApplicable: false, tableOrigin: "form-report", note: "业务表单(formreport):表名是中文显示名、非物理表名 ⇒ relate-fields-extract 的 ①ETL ②SQL视图 ③④EB ⑤⑥流程 六类判定全部不适用,该跳必然空转,**不要发起**" };
1465
+ }
1466
+ return { likelyApplicable: false, tableOrigin: "unknown", note: "非 EB / 非流程表,关联字段提取通常不适用;如确需先按表名形态(sv_ / uf_ / 系统表名)人工确认" };
1467
+ }
1468
+
1469
+ async function cmdDsDescribe(args, json) {
1470
+ const gid = need("group", args);
1471
+ const tid = String(need("table", args));
1472
+ const sourceType = args["source-type"] || "LOGIC";
1473
+ const t0 = Date.now();
1474
+
1475
+ const tasks = [
1476
+ (async () => {
1477
+ try {
1478
+ const form = dsFieldsForm(gid, tid, sourceType);
1479
+ if (typeof args["app-id"] === "string" && args["app-id"] !== "" && args["app-id"] !== "auto") form.appId = args["app-id"];
1480
+ return { key: "fields", value: normalizeFieldsResp(await postMultipart("/api/ebuilder/common/ds/fields", form)) };
1481
+ } catch (e) { return { key: "fields", error: String((e && e.message) || e) }; }
1482
+ })(),
1483
+ (async () => {
1484
+ const skillKey = args.skill || "data-source-query";
1485
+ try {
1486
+ const q = new URLSearchParams({ skillKey, sourceType, sourceGroupId: gid, sourceTableId: tid });
1487
+ const r = await getUrl("/api/dw/newetl/chatBi/skill/getChatDocsBySkillKey?" + q.toString());
1488
+ const data = r.data;
1489
+ const missing = !data || !Array.isArray(data.nouns);
1490
+ return {
1491
+ key: "lexicon",
1492
+ value: {
1493
+ skillKey, sourceType, sourceGroupId: gid, sourceTableId: tid,
1494
+ noLexicon: missing, count: missing ? 0 : data.nouns.length,
1495
+ nouns: missing ? [] : data.nouns.map((n) => ({ keyword: n.keyword ?? null, synonyms: n.synonyms ?? null, wordExplain: n.wordExplain ?? null })),
1496
+ note: missing
1497
+ ? "该数据集无词库(后端:" + (r.msg || "未返回 nouns") + ")—— 四参数均取自本次调用,故为**确定结论**;按铁律如实说明『该数据集暂无名词解释』,不自动建库、不新增名词"
1498
+ : "词库命中:须先把每条规则逐条映射到 --where / --fields / --group-by / --stat 并自检无遗漏,再生成查询",
1499
+ },
1500
+ };
1501
+ } catch (e) { return { key: "lexicon", error: String((e && e.message) || e) }; }
1502
+ })(),
1503
+ ];
1504
+
1505
+ const settled = await Promise.all(tasks);
1506
+ const out = { groupId: gid, groupLabel: dsGroupLabel(gid), tableId: tid, sourceType, branches: settled.length };
1507
+ const errors = [];
1508
+ for (const s of settled) {
1509
+ if (s.error) { out[s.key] = { error: s.error }; errors.push({ branch: s.key, error: s.error }); }
1510
+ else out[s.key] = s.value;
1511
+ }
1512
+ out.relatedFields = dsRelatedHint(gid, (out.fields && out.fields.detailCount) || 0);
1513
+ out.channelHint = dsRouteHint([{ groupId: gid, serviceMark: dsGroupMark(gid) }]);
1514
+ // ⚠️ 权限闸门**不在本命令范围**:EB 建模表单取数前必须另行执行 `e10-data eb perm-page`(appId 只从那次校验取)
1515
+ out.permissionNote = gid === "weaver-ebuilder-form-service"
1516
+ ? "该分组是 EB 建模表单:取数前必须另行执行 `e10-data eb perm-page --obj-id <tableId> --form-table <物理表名>` 判权限并取 appId(--form-table 用 ds find / ds tables 返回的 physicalTable)"
1517
+ : "非 EB 分组,数据源通道无需 eb perm-page 前置校验";
1518
+ out.elapsedMs = Date.now() - t0;
1519
+ if (errors.length && args.require) throw new Error("describe 存在失败分支,--require 下不继续:" + JSON.stringify(errors));
1520
+ if (json) return out;
1521
+
1522
+ const f = out.fields || {};
1523
+ console.log("== describe " + gid + " / " + tid + " (" + out.elapsedMs + "ms,并发 " + settled.length + " 路) ==");
1524
+ if (f.error) console.log("字段: 失败 " + f.error);
1525
+ else console.log("字段: 主表 " + f.mainCount + " 个,明细 " + f.detailCount + " 个(表名 " + (f.tableName ?? "-") + ")");
1526
+ const lx = out.lexicon || {};
1527
+ if (lx.error) console.log("词库: 失败 " + lx.error);
1528
+ else console.log("词库: " + (lx.noLexicon ? "该数据集无词库" : "命中 " + lx.count + " 条(skill=" + lx.skillKey + ")"));
1529
+ console.log("关联字段: " + (out.relatedFields.likelyApplicable ? "适用(" + out.relatedFields.tableOrigin + ")" : "不适用(" + out.relatedFields.tableOrigin + ")") + " —— " + out.relatedFields.note);
1530
+ console.log("通道提示: " + out.channelHint.channel + " —— " + out.channelHint.desc);
1531
+ console.log("权限: 不在本命令范围 —— " + out.permissionNote);
1532
+ }
1533
+
953
1534
  // ---------------- eb:EB 建模表单权限校验 ----------------
954
1535
  // chatBi EB 配置映射分页查询:EB 表权限前置校验的 CLI 通道
955
1536
  // (替代原「DSL 查 dw.chat_bi_eb_config_map + 客户端拼 (all_scop=1 OR target_id=本人)」写法)
@@ -1459,7 +2040,7 @@ async function expGet(url) {
1459
2040
  });
1460
2041
  const raw = await resp.text();
1461
2042
  let j; try { j = parseJsonSafe(raw); } catch { throw new Error("HTTP " + resp.status + " 非JSON响应: " + raw.slice(0, 300)); }
1462
- if (resp.status === 401 || (j && String(j.code) === "-1" && /token|登录|失效/i.test(String(j.msg || "")))) throw new Error("登录态失效,请重新 e10-login oidc 登录");
2043
+ if (resp.status === 401 || (j && String(j.code) === "-1" && /token|登录|失效/i.test(String(j.msg || "")))) throw new Error("登录态失效,请重新登录 weaver-work-cli(`weaver-work-cli auth login --base-url <域名>`)");
1463
2044
  return { raw, j };
1464
2045
  }
1465
2046
  function rawNum(raw, key) {
@@ -1531,7 +2112,7 @@ async function rawGet(url) {
1531
2112
  method: "GET",
1532
2113
  headers: { "accept": "*/*", "langType": "zh_CN", "cookie": auth.cookie, "eteamsid": auth.eteamsid, "ebbusinessid": EB_BIZ_ID, "user-agent": UA },
1533
2114
  });
1534
- if (resp.status === 401) throw new Error("登录态失效,请重新 e10-login oidc 登录");
2115
+ if (resp.status === 401) throw new Error("登录态失效,请重新登录 weaver-work-cli(`weaver-work-cli auth login --base-url <域名>`)");
1535
2116
  return Buffer.from(await resp.arrayBuffer());
1536
2117
  }
1537
2118
  async function expStatus(args, json) {
@@ -1793,6 +2374,23 @@ login 登录(**唯一不需要已有凭据**的命令族;其余命令都
1793
2374
  query DSL 数据查询 executeQueryDsl
1794
2375
  query --conn <connId> --model <ebuilder|flow|eteams|attend|hr|document|dw|blog> --schema <数据库> [--table-id <流程ID>] [--debug true|false] --dsl @file.json|'{...}'
1795
2376
  ds 数据源(业务模块)取数与元数据
2377
+ ds groups [--pattern <关键词>] [--json] # 内置分组清单(仅用于 find 的默认扫描集)
2378
+ ds find --name <关键词> [--groups g1,g2] [--all-groups] [--max-groups N] [--limit N]
2379
+ [--app-id <id>] [--size 100] [--pages N] [--concurrency 10] [--no-escalate] [--loose] [--source-type LOGIC]
2380
+ # 跨分组找表:**进程内并发**扇出(一次凭据),产物=候选表清单 + 路由建议,**不取数**
2381
+ # 默认扫描集 = 核心 3 组(业务表单/e-builder表单/流程)+ 语义命中组,≤ --max-groups
2382
+ # 默认集合全 miss 时自动升级为全分组扫一次(--no-escalate 可关;窗口 >2000 行时不自动升级)
2383
+ # --all-groups = 直接扫全部内置分组(进程内并发,实测 ~5.5s)
2384
+ # --app-id:EB 分组(weaver-ebuilder-form-service)按应用分域,需限定应用时带上
2385
+ # ⚠️ 端点 /api/dw/newetl/chatBi/ds/listByPage **当前忽略 name**(后端待升级)⇒ 本次是
2386
+ # 「抓 --pages 页 × --size 行 + 本地按表名过滤」:**未命中 ≠ 该表不存在**,
2387
+ # 分组行数大时请调大 --size(≤2000)/--pages(≤50)。后端支持 name 后自动精确、无需改调用
2388
+ # 输出按「实体」归并去重(同一流程会在多个报表/效率分组重复出现)
2389
+ ds describe --group <gid> --table <tid> [--app-id <id>] [--skill <skillKey,默认 data-source-query>]
2390
+ [--require] [--source-type LOGIC]
2391
+ # 一次**并发**取回两件套:字段 + 词库(两者入参同为 (groupId, tableId),互不依赖)
2392
+ # ⚠️ **不做权限校验**:EB 分组取数前仍必须单独执行 eb perm-page(appId 只从那次调用取)
2393
+ # --require:任一路失败即报错退出
1796
2394
  ds tables --group <gid> [--name 关键词] [--app-id <id>]
1797
2395
  ds fields --group <gid> --table <tid> [--app-id <id>]
1798
2396
  ds fields-dsl --group <gid> --table <tid> [--app-id <id>] # 同 fields,接口 /api/dw/newetl/chatBi/ds/fieldsForDSL
@@ -1869,6 +2467,9 @@ async function main() {
1869
2467
  if (family === "ds") {
1870
2468
  if (!sub) { usage(); process.exit(1); }
1871
2469
  switch (sub) {
2470
+ case "groups": result = await cmdDsGroups(args, json); break;
2471
+ case "find": result = await cmdDsFind(args, json); break;
2472
+ case "describe": result = await cmdDsDescribe(args, json); break;
1872
2473
  case "tables": result = await cmdTables(args, json); break;
1873
2474
  case "fields": result = await cmdFields(args, json); break;
1874
2475
  case "fields-dsl": result = await cmdFieldsForDSL(args, json); break;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "e10-data",
3
- "version": "0.1.18-xiaoe",
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-e10-login(本 CLI 不再自行解密,通过 e10-login 命令出口获取凭据;仅 login xiaoe-env 在无凭据时自行换 token,写 profile 仍交 e10-login set)。v0.1.17 性能改造:免凭据命令(--version/--help/未知命令)提前返回、直连 e10-login JS 入口(去掉 shell:true 的 cmd.exe 开销)、新增凭据进程间 TTL 缓存(不落明文);v0.1.18(2026-09-24)新增 ds fields-dsl(接口 /api/dw/newetl/chatBi/ds/fieldsForDSL),并移除凭据明文镜像同步(不再写 ~/.workbuddy/e10-login-cache.md)",
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 等命令报「缺少查询人员」的问题)",
5
5
  "author": "e10-dw",
6
6
  "license": "MIT",
7
7
  "type": "module",