e10-data 0.1.16 → 0.1.17-xiaoe
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 +71 -0
- package/README.md +26 -0
- package/bin/e10-cred-helper.mjs +90 -0
- package/bin/e10-data.mjs +253 -7
- package/package.json +4 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# 变更记录
|
|
2
|
+
|
|
3
|
+
## 0.1.17(2026-09-20)—— 启动性能改造
|
|
4
|
+
|
|
5
|
+
本次只做性能与健壮性改造,**不改变任何命令的参数、输出与退出码**(已用 10 条命令做新旧输出逐字节对比,全部一致)。
|
|
6
|
+
|
|
7
|
+
### 1. 免凭据命令提前返回(不再取凭据)
|
|
8
|
+
|
|
9
|
+
`--version` / `-v`、`--help` / `-h`、以及**未知命令/未知子命令的用法提示**都不需要登录态。
|
|
10
|
+
过去 `loadAuth()` 写在模块顶层,任何调用(含打印版本)都会先付一整笔取凭据开销;
|
|
11
|
+
现在这三类在取凭据**之前**就返回。
|
|
12
|
+
|
|
13
|
+
- 实测:`--version`、`--help`、未知命令 从 ~2.4s → **~0.2s**
|
|
14
|
+
- 退出码约定:显式求助(`--help` / `-h`)为 `0`;用错命令(未知命令 / 缺子命令)为 `1`
|
|
15
|
+
- 新增命令白名单 `commandIsKnown()`(与 `main()` 分发表一一对应);`exp release --version v1.0.0`
|
|
16
|
+
这类"`--version` 作为版本号参数"的用法不受影响(仅拦截顶层单参数)
|
|
17
|
+
|
|
18
|
+
### 2. 直连 `e10-login` 入口,去掉 `shell: true`
|
|
19
|
+
|
|
20
|
+
`spawnSync("e10-login", …, { shell: true })` 在 Windows 上会先起一层 `cmd.exe`;
|
|
21
|
+
实测**该层单独就耗时 ~0.98~1.04s**(杀软对每个新进程的实时扫描所致),
|
|
22
|
+
而 `e10-login` 自身只花 ~0.1s。
|
|
23
|
+
|
|
24
|
+
- 新增 `resolveLoginEntry()`:① 环境变量 `E10_LOGIN_ENTRY` → ② Node 模块解析
|
|
25
|
+
→ ③ 沿 `PATH` 找 shim 再取 `<shimDir>/node_modules/e10-login` → ④ 常见全局前缀兜底
|
|
26
|
+
- 改为 `spawnSync(process.execPath, [entry, ...args], { shell: false })`
|
|
27
|
+
- 不能直接 spawn `.cmd`:Node ≥18.20/20.12 起禁止在 `shell:false` 下执行 `.cmd`/`.bat`(CVE-2024-27980),
|
|
28
|
+
因此必须解析到 shim 背后真正的 `.js` 入口
|
|
29
|
+
- 解析失败会打印一行 warn 并**退化回 `shell: true`**(只慢不错)
|
|
30
|
+
- 实测:单次取凭据 **~1.0s → ~0.22s**(每调用省 ~0.78s)
|
|
31
|
+
- 附带收益:`login xiaoe-env` 里写 profile 的那次调用也从「拼 shell 字符串 + `shq` 转义」改为
|
|
32
|
+
**数组直传**,彻底消除 shell 注入面(`ETEAMSID_SAFE_RE` 仍作为二道防线保留)
|
|
33
|
+
|
|
34
|
+
### 3. 凭据进程间 TTL 缓存(默认 300 秒,**不落明文**)
|
|
35
|
+
|
|
36
|
+
新增 `bin/e10-cred-helper.mjs`:一个 detached 的「持有者」进程把凭据**只保存在内存**里,
|
|
37
|
+
监听本机命名管道(Windows)/ Unix 域套接字(`0600`),供后续同用户·同 profile 的调用直接取用,
|
|
38
|
+
TTL 到期自动退出。
|
|
39
|
+
|
|
40
|
+
- **不写任何文件、不经过网络**;凭据经 `stdin` 从父进程传入(不走 `argv`,避免出现在进程列表里)
|
|
41
|
+
- 通道名 = `sha1(用户主目录 + 用户名 + profile)` 前 16 位 → 不同用户 / 不同 profile 互不可见
|
|
42
|
+
- **兜底出口(明文镜像)的凭据不写入缓存**,避免把可能已过期的凭据固化整个 TTL
|
|
43
|
+
- 任何异常(连不上 / 超时 / JSON 损坏 / 已过期 / profile 不符)**静默回落**正常取凭据路径
|
|
44
|
+
- 并发:后启动的持有者遇 `EADDRINUSE` 静默退出(先到先得)
|
|
45
|
+
- 安全边界未扩大:同用户下的其它进程本来就能执行 `e10-login whoami` 拿到同样的凭据
|
|
46
|
+
|
|
47
|
+
新增环境变量:`E10_DATA_CRED_TTL`(默认 300)、`E10_DATA_NO_CRED_CACHE=1`(关闭缓存)、
|
|
48
|
+
`E10_LOGIN_ENTRY`(指定入口,排障用)。`E10_DATA_AUTH_MODE` / `E10_DATA_AUTH_PROBE` 存在时绕过缓存。
|
|
49
|
+
|
|
50
|
+
> **一处附带的行为变化**:`syncCacheMd()`(把凭据同步到已废弃的明文镜像
|
|
51
|
+
> `~/.workbuddy/e10-login-cache.md`)只在**缓存未命中**那条路径上执行。
|
|
52
|
+
> 缓存命中的 5 分钟内不再刷新该镜像。因该镜像机制已被判定废弃、且不参与正常取数,
|
|
53
|
+
> 此变化会**减少**明文落盘频率,符合既有的「不落明文」方向。
|
|
54
|
+
|
|
55
|
+
### 综合效果
|
|
56
|
+
|
|
57
|
+
| 命令 | 0.1.16 | 0.1.17 | 提升 |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| `--version` | ~2.4s | **~0.2s** | 12× |
|
|
60
|
+
| `conf get CONN_EC_EDCAPP --json` | 2.35s | **0.62s** | 3.8× |
|
|
61
|
+
|
|
62
|
+
按一次问数会话调用 10 次 `e10-data` 估算,**省约 17 秒**。
|
|
63
|
+
|
|
64
|
+
### 回归验证
|
|
65
|
+
|
|
66
|
+
- 9 个命令族 smoke(conf / noun / field / ds / doc / attend / eb / exp / query)全部通过
|
|
67
|
+
- 10 条命令与 0.1.16 输出 + 退出码**逐字节一致**
|
|
68
|
+
- helper 单元测试:TTL 到期自退(`ENOENT` + 进程消失)、`EADDRINUSE` 静默退出(退出码 0)
|
|
69
|
+
- 缓存开关对照:关闭缓存 1.61s / 开启缓存 0.62s,确认提速来自缓存本身
|
|
70
|
+
- 认证未被绕过:把 `USERPROFILE`/`HOME` 指向空目录后,真实命令仍以
|
|
71
|
+
「未取得登录凭据」退出,而免凭据命令正常工作
|
package/README.md
CHANGED
|
@@ -35,6 +35,32 @@ e10-data attend statis 考勤假期余额统计 getAttendInfoStatis(hr-atte
|
|
|
35
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` 登录(见下节)。
|
|
36
36
|
- 版本更新:`npm i -g e10-data@latest`;查看源上最新 `npm view e10-data version`。
|
|
37
37
|
|
|
38
|
+
## 性能:免凭据命令与凭据缓存(v0.1.17 新增)
|
|
39
|
+
|
|
40
|
+
**① 免凭据命令不再取凭据**:`--version` / `-v`、`--help` / `-h`、以及**未知命令的用法提示**都不需要登录态,现在在取凭据之前直接返回。实测这些路径从 ~2.4s 降到 **~0.2s**。
|
|
41
|
+
|
|
42
|
+
**② 直连 `e10-login`,去掉 shell 中转**:过去用 `spawnSync(..., { shell: true })` 拉起 `e10-login`,在 Windows 上会先起一层 `cmd.exe`——实测该层**单独就要约 1.0s**(杀软逐进程扫描)。现在改为定位 `e10-login` 的 JS 入口后用 `process.execPath` 直接拉起(`shell: false`)。单次取凭据 **~1.0s → ~0.22s**。
|
|
43
|
+
> 若自动定位失败(会打印一行 warn),可用 `E10_LOGIN_ENTRY=<e10-login/bin/e10-login.js 绝对路径>` 显式指定;仍失败则自动退化为 shell 调用(只慢不错)。
|
|
44
|
+
|
|
45
|
+
**③ 凭据进程间 TTL 缓存(默认 300 秒)**:一个 detached 的持有者进程把凭据**只保存在内存**里,监听本机命名管道(Windows)/ Unix 域套接字(0600),供后续同用户·同 profile 的调用直接取用,TTL 到期自动退出。**不写任何文件、不经过网络。**
|
|
46
|
+
> - 通道名含「用户主目录 + 用户名 + profile」哈希 → 不同用户 / 不同 profile 互不可见;
|
|
47
|
+
> - 兜底出口(明文镜像)的凭据**不写入缓存**,避免把可能过期的凭据固化;
|
|
48
|
+
> - 任何异常(连不上 / 超时 / JSON 损坏 / 已过期 / profile 不符)**静默回落**到正常取凭据路径。
|
|
49
|
+
|
|
50
|
+
**三项合计**:`e10-data conf get …` 单次调用实测 **2.35s → 0.62s(约 3.8 倍)**。
|
|
51
|
+
|
|
52
|
+
相关环境变量:
|
|
53
|
+
|
|
54
|
+
| 变量 | 作用 |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `E10_DATA_CRED_TTL` | 缓存 TTL 秒数,默认 `300` |
|
|
57
|
+
| `E10_DATA_NO_CRED_CACHE=1` | 整体关闭凭据缓存(每次仍直连 e10-login) |
|
|
58
|
+
| `E10_DATA_AUTH_PROBE=1` / `E10_DATA_AUTH_MODE=…` | 存在时**绕过缓存**,强制重新解析凭据 |
|
|
59
|
+
| `E10_LOGIN_ENTRY` | 显式指定 `e10-login` 的 JS 入口路径(排障用) |
|
|
60
|
+
|
|
61
|
+
> ⚠️ TTL 采用「先到先得」:若已有一个 TTL=300 的持有者在服务,此时以更短的 `E10_DATA_CRED_TTL` 再调用不会立刻缩短它的寿命(新持有者因通道被占而静默退出)。日常使用无需关注。
|
|
62
|
+
|
|
63
|
+
|
|
38
64
|
## 用法示例
|
|
39
65
|
|
|
40
66
|
```bash
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// -*- coding: utf-8 -*-
|
|
3
|
+
/**
|
|
4
|
+
* e10-cred-helper —— 凭据进程间 TTL 缓存的「持有者」进程(v0.1.17 新增)
|
|
5
|
+
*
|
|
6
|
+
* 为什么存在:e10-data 每次调用都要向 e10-login 取一次凭据,那是一整笔进程开销
|
|
7
|
+
* (去 shell 后 ~0.22s)。把凭据在一个**只存在于内存**的短命进程里放 TTL 秒,
|
|
8
|
+
* 后续同用户、同 profile 的 e10-data 调用直接从这里取,省掉反复拉起 e10-login。
|
|
9
|
+
*
|
|
10
|
+
* 安全边界(与既有约定一致,**不落明文**):
|
|
11
|
+
* · 凭据只保存在本进程内存里;**不写任何文件、不打印到 stdout/stderr**;
|
|
12
|
+
* · 传输走本机命名管道(Windows)/ Unix 域套接字(0600),不经过网络;
|
|
13
|
+
* · 通道名含「用户主目录 + 用户名 + profile」的哈希 → 不同用户/不同 profile 互不可见;
|
|
14
|
+
* · TTL 到期自动退出(默认 300s),不长期驻留;
|
|
15
|
+
* · 同用户下的其它进程本来就能执行 `e10-login whoami` 拿到同样的凭据,
|
|
16
|
+
* 因此本缓存**不扩大**攻击面。
|
|
17
|
+
*
|
|
18
|
+
* 用法(只应由 e10-data 拉起,不面向用户):
|
|
19
|
+
* node e10-cred-helper.mjs <pipePath> <ttlSeconds>
|
|
20
|
+
* 凭据 JSON 由父进程经 stdin 传入;父进程随即 unref 退出,本进程独立存活到 TTL。
|
|
21
|
+
* 生命周期:
|
|
22
|
+
* ① 读 stdin 拿到凭据(拿不到 → 立刻退出,不影响任何命令)
|
|
23
|
+
* ② 监听 pipePath(EADDRINUSE → 说明已有持有者,静默退出)
|
|
24
|
+
* ③ 每个连接回一行 JSON:{"v":1,"exp":<ms>,"profile":"..","auth":{..}} 后关闭
|
|
25
|
+
* ④ TTL 到期 / 收到 SIGINT·SIGTERM → 清理并退出
|
|
26
|
+
*/
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import net from "node:net";
|
|
29
|
+
|
|
30
|
+
const pipePath = process.argv[2];
|
|
31
|
+
const ttlSec = Number(process.argv[3]) || 300;
|
|
32
|
+
|
|
33
|
+
if (!pipePath) process.exit(0);
|
|
34
|
+
|
|
35
|
+
let payload = null;
|
|
36
|
+
try {
|
|
37
|
+
let raw = "";
|
|
38
|
+
process.stdin.setEncoding("utf-8");
|
|
39
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
40
|
+
payload = JSON.parse(raw);
|
|
41
|
+
} catch { payload = null; }
|
|
42
|
+
|
|
43
|
+
// 凭据不完整就不起服务(宁可让调用方回落到正常取凭据路径)
|
|
44
|
+
if (!payload || !payload.auth || !payload.auth.baseUrl || !payload.auth.eteamsid || !payload.auth.cookie) {
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const response = JSON.stringify({
|
|
49
|
+
v: 1,
|
|
50
|
+
exp: Number(payload.exp) || (Date.now() + ttlSec * 1000),
|
|
51
|
+
profile: payload.profile || "default",
|
|
52
|
+
auth: payload.auth,
|
|
53
|
+
}) + "\n";
|
|
54
|
+
|
|
55
|
+
function cleanupAndExit(code) {
|
|
56
|
+
try { server.close(); } catch {}
|
|
57
|
+
// Unix:套接字文件需要自行清理;Windows 命名管道无文件
|
|
58
|
+
if (process.platform !== "win32") { try { fs.unlinkSync(pipePath); } catch {} }
|
|
59
|
+
process.exit(code);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const server = net.createServer((sock) => {
|
|
63
|
+
// 只认 GET:其余一律拒绝,避免被当作通用通道
|
|
64
|
+
let buf = "";
|
|
65
|
+
let answered = false;
|
|
66
|
+
const reply = (text) => { if (answered) return; answered = true; try { sock.end(text); } catch {} };
|
|
67
|
+
sock.setEncoding("utf-8");
|
|
68
|
+
sock.setTimeout(3000, () => { try { sock.destroy(); } catch {} });
|
|
69
|
+
sock.on("data", (d) => {
|
|
70
|
+
buf += d;
|
|
71
|
+
if (buf.includes("\n")) reply(/"GET"/.test(buf) || /^GET/.test(buf.trim()) ? response : "MISS\n");
|
|
72
|
+
});
|
|
73
|
+
sock.on("error", () => { try { sock.destroy(); } catch {} });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
server.on("error", (e) => {
|
|
77
|
+
// EADDRINUSE:已有持有者在服务 → 本次静默退出,交给它
|
|
78
|
+
if (e && (e.code === "EADDRINUSE" || e.code === "EACCES")) process.exit(0);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
server.listen(pipePath, () => {
|
|
83
|
+
if (process.platform !== "win32") { try { fs.chmodSync(pipePath, 0o600); } catch {} }
|
|
84
|
+
// TTL 到期自退。刻意不 unref:保证 TTL 一定生效,进程不会提前或滞后退出。
|
|
85
|
+
setTimeout(() => cleanupAndExit(0), ttlSec * 1000);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
process.on("SIGINT", () => cleanupAndExit(0));
|
|
89
|
+
process.on("SIGTERM", () => cleanupAndExit(0));
|
|
90
|
+
process.on("uncaughtException", () => cleanupAndExit(0));
|
package/bin/e10-data.mjs
CHANGED
|
@@ -38,8 +38,10 @@ import fs from "node:fs";
|
|
|
38
38
|
import os from "node:os";
|
|
39
39
|
import path from "node:path";
|
|
40
40
|
import crypto from "node:crypto";
|
|
41
|
-
import
|
|
41
|
+
import net from "node:net";
|
|
42
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
42
43
|
import { fileURLToPath } from "node:url";
|
|
44
|
+
import { createRequire } from "node:module";
|
|
43
45
|
|
|
44
46
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
45
47
|
function cliVersion() {
|
|
@@ -75,11 +77,108 @@ function authFilePath() {
|
|
|
75
77
|
return { profile, file: path.join(home, ".e10-cli", "profiles", profile, "auth") };
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
// ---------------- e10-login 入口解析(v0.1.17:去掉 shell:true,这是最大的性能改动)----------------
|
|
81
|
+
// 背景(2026-09-20 实测):`spawnSync("e10-login", …, { shell: true })` 在 Windows 上会先起一层
|
|
82
|
+
// cmd.exe;该层**单独**耗时 ~0.98~1.04s(杀软对每个新进程的实时扫描所致),而 e10-login 自身
|
|
83
|
+
// 的活(读 profile + 解密 auth + 打印)只花 ~0.1s。改为「定位 e10-login 的 JS 入口,用
|
|
84
|
+
// process.execPath 直接拉起」后,实测 **1.0s → 0.22s**(每调用省 ~0.78s)。
|
|
85
|
+
// ⚠️ 不能直接 spawn `e10-login.cmd`:Node ≥18.20/20.12 起为修 CVE-2024-27980,禁止在 shell:false
|
|
86
|
+
// 下执行 .cmd/.bat(抛 EINVAL)。因此必须解析到 .cmd shim 背后**真正的 .js 入口**。
|
|
87
|
+
// 解析顺序:① 环境变量 E10_LOGIN_ENTRY → ② Node 模块解析(与 e10-data 同 prefix 时命中)
|
|
88
|
+
// → ③ 沿 PATH 找 shim,再取 <shimDir>/node_modules/e10-login → ④ 常见全局前缀兜底。
|
|
89
|
+
// 全部失败 → 返回 null,调用方退化回 shell:true(保持旧行为可用,只慢不错)。
|
|
90
|
+
let _loginEntry; // 进程内缓存解析结果;null 表示"已尝试但未找到"
|
|
91
|
+
let _loginEntryDone = false;
|
|
92
|
+
|
|
93
|
+
function resolveLoginEntry() {
|
|
94
|
+
if (_loginEntryDone) return _loginEntry;
|
|
95
|
+
_loginEntryDone = true;
|
|
96
|
+
const tried = [];
|
|
97
|
+
const isFile = (p) => { try { return !!p && fs.statSync(p).isFile(); } catch { return false; } };
|
|
98
|
+
|
|
99
|
+
/** 读 <dir>/package.json 的 bin 字段定位入口文件;非 e10-login 包返回 null */
|
|
100
|
+
const entryFromPkgDir = (dir) => {
|
|
101
|
+
try {
|
|
102
|
+
const pj = path.join(dir, "package.json");
|
|
103
|
+
if (!isFile(pj)) return null;
|
|
104
|
+
const j = JSON.parse(fs.readFileSync(pj, "utf-8"));
|
|
105
|
+
if (j.name !== "e10-login") return null;
|
|
106
|
+
let b = j.bin;
|
|
107
|
+
if (typeof b === "string") b = { "e10-login": b };
|
|
108
|
+
const rel = b && b["e10-login"];
|
|
109
|
+
if (!rel) return null;
|
|
110
|
+
const entry = path.resolve(dir, rel);
|
|
111
|
+
return isFile(entry) ? entry : null;
|
|
112
|
+
} catch { return null; }
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ① 环境变量显式指定(排障与特殊安装方式用)
|
|
116
|
+
const envEntry = (process.env.E10_LOGIN_ENTRY || "").trim();
|
|
117
|
+
if (envEntry) {
|
|
118
|
+
if (isFile(envEntry)) { _loginEntry = envEntry; return _loginEntry; }
|
|
119
|
+
tried.push("E10_LOGIN_ENTRY 指向的文件不存在: " + envEntry);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ② Node 模块解析(e10-data 与 e10-login 装在同一个 prefix 时命中)
|
|
123
|
+
try {
|
|
124
|
+
const pj = createRequire(import.meta.url).resolve("e10-login/package.json");
|
|
125
|
+
const e = entryFromPkgDir(path.dirname(pj));
|
|
126
|
+
if (e) { _loginEntry = e; return _loginEntry; }
|
|
127
|
+
tried.push("模块解析命中 " + pj + " 但 bin 入口不可用");
|
|
128
|
+
} catch (e) { tried.push("模块解析: " + ((e && e.message) || e)); }
|
|
129
|
+
|
|
130
|
+
// ③ 沿 PATH 找 shim,再取 shim 同级的 node_modules/e10-login
|
|
131
|
+
// (npm 全局安装的常规布局:<prefix>/e10-login.cmd + <prefix>/node_modules/e10-login)
|
|
132
|
+
const pathDirs = String(process.env.PATH || "").split(path.delimiter).filter(Boolean);
|
|
133
|
+
const shimNames = process.platform === "win32"
|
|
134
|
+
? ["e10-login.cmd", "e10-login.exe", "e10-login"]
|
|
135
|
+
: ["e10-login"];
|
|
136
|
+
for (const d of pathDirs) {
|
|
137
|
+
for (const n of shimNames) {
|
|
138
|
+
const shim = path.join(d, n);
|
|
139
|
+
if (!isFile(shim)) continue;
|
|
140
|
+
const e = entryFromPkgDir(path.join(d, "node_modules", "e10-login"));
|
|
141
|
+
if (e) { _loginEntry = e; return _loginEntry; }
|
|
142
|
+
tried.push("找到 shim " + shim + ",但其同级 node_modules/e10-login 不可用");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ④ 常见全局前缀兜底
|
|
147
|
+
const home = os.homedir();
|
|
148
|
+
const prefixes = [
|
|
149
|
+
path.join(path.dirname(process.execPath), "node_modules"),
|
|
150
|
+
path.join(path.dirname(process.execPath), "..", "lib", "node_modules"),
|
|
151
|
+
path.join(home, ".npm-global", "lib", "node_modules"),
|
|
152
|
+
process.env.APPDATA ? path.join(process.env.APPDATA, "npm", "node_modules") : null,
|
|
153
|
+
process.env.ProgramFiles ? path.join(process.env.ProgramFiles, "nodejs", "node_modules") : null,
|
|
154
|
+
].filter(Boolean);
|
|
155
|
+
for (const p of prefixes) {
|
|
156
|
+
const e = entryFromPkgDir(path.join(p, "e10-login"));
|
|
157
|
+
if (e) { _loginEntry = e; return _loginEntry; }
|
|
158
|
+
}
|
|
159
|
+
tried.push("常见全局前缀均未命中: " + prefixes.join(" | "));
|
|
160
|
+
|
|
161
|
+
if (!process.env.E10_DATA_QUIET) {
|
|
162
|
+
console.error("[e10-data][warn] 未定位到 e10-login 的 JS 入口,本次退化为 shell 调用(较慢,且 Windows 上多花约 1 秒)。"
|
|
163
|
+
+ "可用 E10_LOGIN_ENTRY=<e10-login/bin/e10-login.js 绝对路径> 显式指定。诊断:" + tried.join(" | "));
|
|
164
|
+
}
|
|
165
|
+
_loginEntry = null;
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
78
169
|
let lastLoginDiag = "";
|
|
170
|
+
/** 底层调用 e10-login,返回 spawnSync 结果(供需要看 stderr/status 的调用方使用)。
|
|
171
|
+
* 优先 shell:false 直连 JS 入口;解析失败才退化为 shell:true。 */
|
|
172
|
+
function spawnLoginCli(args, timeout = 20000) {
|
|
173
|
+
const entry = resolveLoginEntry();
|
|
174
|
+
if (entry) return spawnSync(process.execPath, [entry, ...args], { encoding: "utf-8", timeout, windowsHide: true });
|
|
175
|
+
return spawnSync("e10-login", args, { shell: true, encoding: "utf-8", timeout, windowsHide: true });
|
|
176
|
+
}
|
|
177
|
+
|
|
79
178
|
/** 调用 e10-login(凭据的唯一来源);失败返回 null,并把诊断信息留在 lastLoginDiag */
|
|
80
179
|
function runLoginCli(args) {
|
|
81
180
|
try {
|
|
82
|
-
const r =
|
|
181
|
+
const r = spawnLoginCli(args);
|
|
83
182
|
const out = (r.stdout || "").trim();
|
|
84
183
|
if (r.error || r.status !== 0) {
|
|
85
184
|
lastLoginDiag = ((r.stderr || "") + out).trim().split(/\r?\n/).slice(0, 2).join(" / ") || (r.error ? String(r.error.message) : "exit=" + r.status);
|
|
@@ -247,6 +346,93 @@ function loadAuth() {
|
|
|
247
346
|
(diag.length ? " 探测诊断:" + diag.join(" | ") + "\n" : "")
|
|
248
347
|
);
|
|
249
348
|
}
|
|
349
|
+
|
|
350
|
+
// ---------------- 凭据进程间 TTL 缓存(v0.1.17 新增,不落明文)----------------
|
|
351
|
+
// 目的:避免每次调用都重新拉起 e10-login 取凭据(去掉 shell 后仍约 0.22s/次)。
|
|
352
|
+
// 形态:一个 detached 的「持有者」进程把凭据**只保存在内存**里,监听本机命名管道
|
|
353
|
+
// (Windows)/ Unix 域套接字(0600),供后续同用户·同 profile 的 e10-data 调用取用,
|
|
354
|
+
// TTL(默认 300s)到期自动退出。**不写任何文件、不涉及网络。**
|
|
355
|
+
// 边界:
|
|
356
|
+
// · 任何异常(连不上/超时/JSON 坏/过期/profile 不符)一律静默回落正常取凭据路径;
|
|
357
|
+
// · 兜底出口(明文镜像)的凭据不会写入缓存,避免把过期凭据固化;
|
|
358
|
+
// · E10_DATA_NO_CRED_CACHE=1 整体关闭;E10_DATA_AUTH_MODE / E10_DATA_AUTH_PROBE 存在时绕过缓存;
|
|
359
|
+
// · 缓存通道名含「主目录+用户名+profile」哈希 → 不同用户/不同 profile 互不可见。
|
|
360
|
+
const CRED_CACHE_TTL = (() => {
|
|
361
|
+
const n = Number(process.env.E10_DATA_CRED_TTL);
|
|
362
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 300;
|
|
363
|
+
})();
|
|
364
|
+
|
|
365
|
+
/** 缓存通道地址(只有 e10-data 与 e10-cred-helper 使用;由父进程算好后以 argv 传给持有者) */
|
|
366
|
+
function credCachePath() {
|
|
367
|
+
const { profile } = authFilePath();
|
|
368
|
+
const tag = crypto.createHash("sha1")
|
|
369
|
+
.update([os.homedir(), process.env.USERNAME || process.env.USER || "", profile].join("|"))
|
|
370
|
+
.digest("hex").slice(0, 16);
|
|
371
|
+
if (process.platform === "win32") return "\\\\.\\pipe\\e10-data-cred-" + tag;
|
|
372
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : "n";
|
|
373
|
+
const dir = process.env.XDG_RUNTIME_DIR || path.join(os.homedir(), ".e10-cli");
|
|
374
|
+
return path.join(dir, "e10-data-cred-" + tag + "-" + uid + ".sock");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** 读缓存:命中返回 auth(结构同 loadAuth 的返回值);未命中/超时/任何异常 → null */
|
|
378
|
+
function credCacheGet() {
|
|
379
|
+
const { profile } = authFilePath();
|
|
380
|
+
return new Promise((resolve) => {
|
|
381
|
+
let done = false, buf = "", sock;
|
|
382
|
+
const finish = (v) => {
|
|
383
|
+
if (done) return;
|
|
384
|
+
done = true;
|
|
385
|
+
clearTimeout(timer);
|
|
386
|
+
try { sock.destroy(); } catch {}
|
|
387
|
+
resolve(v);
|
|
388
|
+
};
|
|
389
|
+
const timer = setTimeout(() => finish(null), 1500); // 取缓存必须快;超时即当未命中
|
|
390
|
+
try { sock = net.connect(credCachePath()); } catch { return finish(null); }
|
|
391
|
+
sock.setEncoding("utf-8");
|
|
392
|
+
sock.on("connect", () => { try { sock.write("GET\n"); } catch { finish(null); } });
|
|
393
|
+
sock.on("data", (d) => {
|
|
394
|
+
buf += d;
|
|
395
|
+
if (!buf.includes("\n")) return;
|
|
396
|
+
try {
|
|
397
|
+
const j = JSON.parse(buf.split("\n")[0]);
|
|
398
|
+
if (!j || j.v !== 1 || Number(j.exp) < Date.now()) return finish(null);
|
|
399
|
+
if ((j.profile || "default") !== profile) return finish(null);
|
|
400
|
+
const a = normalizeAuth(Object.assign({}, j.auth, { source: "进程间凭据缓存(e10-data cred-cache)" }));
|
|
401
|
+
return finish(a || null);
|
|
402
|
+
} catch { return finish(null); }
|
|
403
|
+
});
|
|
404
|
+
sock.on("error", () => finish(null));
|
|
405
|
+
sock.on("close", () => finish(null));
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** 写缓存:拉起 detached 持有者,凭据经 stdin 传入(不经 argv,避免出现在进程列表里) */
|
|
410
|
+
function credCachePublish(auth) {
|
|
411
|
+
return new Promise((resolve) => {
|
|
412
|
+
let child;
|
|
413
|
+
try {
|
|
414
|
+
const helper = path.join(__dirname, "e10-cred-helper.mjs");
|
|
415
|
+
if (!fs.existsSync(helper)) return resolve();
|
|
416
|
+
child = spawn(process.execPath, [helper, credCachePath(), String(CRED_CACHE_TTL)], {
|
|
417
|
+
detached: true, stdio: ["pipe", "ignore", "ignore"], windowsHide: true,
|
|
418
|
+
});
|
|
419
|
+
} catch { return resolve(); }
|
|
420
|
+
const payload = JSON.stringify({
|
|
421
|
+
exp: Date.now() + CRED_CACHE_TTL * 1000,
|
|
422
|
+
profile: auth.profile || "default",
|
|
423
|
+
auth: {
|
|
424
|
+
baseUrl: auth.baseUrl, eteamsid: auth.eteamsid, cookie: auth.cookie,
|
|
425
|
+
userId: auth.userId, tenantKey: auth.tenantKey, source: auth.source,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
let settled = false;
|
|
429
|
+
const done = () => { if (settled) return; settled = true; try { child.unref(); } catch {} resolve(); };
|
|
430
|
+
child.on("error", done);
|
|
431
|
+
child.stdin.on("error", done);
|
|
432
|
+
child.stdin.end(payload, done);
|
|
433
|
+
setTimeout(done, 1000); // 兜底:不让发布环节拖住主命令
|
|
434
|
+
});
|
|
435
|
+
}
|
|
250
436
|
// ---------------- login(唯一"不需要已有凭据"的命令族:xiaoe-env,2026-09-17 移植)----------------
|
|
251
437
|
// 来源:e10-login 1.6.4-xiaoe 的 `e10-login xiaoe-env [--domain <url>]`
|
|
252
438
|
// (lib/xiaoe.js 命令定义 + lib/login-flow.js 收尾 + scripts/xiaoe_env.py 换 token)。
|
|
@@ -317,10 +503,9 @@ async function cmdLoginXiaoeEnv(args, json) {
|
|
|
317
503
|
say("✅ 已取得 ETEAMSID(来源: env,不回显)");
|
|
318
504
|
|
|
319
505
|
if (!ETEAMSID_SAFE_RE.test(eteamsId)) throw new Error("换回的 ETEAMSID 含异常字符,已拒绝透传 e10-login(安全防护)");
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
});
|
|
506
|
+
// v0.1.17:改为数组直传 + shell:false(不再手工拼 shell 字符串、不再需要 shq 转义),
|
|
507
|
+
// 既省掉 cmd.exe 那一层开销,也彻底消除 shell 注入面。ETEAMSID_SAFE_RE 仍作为二道防线保留。
|
|
508
|
+
const r = spawnLoginCli(["set", "--eteamsid", eteamsId, "--base-url", baseUrl], 60000);
|
|
324
509
|
const out = (r.stdout || "").trim(), err = (r.stderr || "").trim();
|
|
325
510
|
if (r.error || r.status !== 0) {
|
|
326
511
|
throw new Error("换 token 成功,但 e10-login 写 profile 失败(auth 文件的加密写入只有 e10-login 能做):"
|
|
@@ -376,7 +561,68 @@ async function cmdLoginXiaoeEnv(args, json) {
|
|
|
376
561
|
}
|
|
377
562
|
}
|
|
378
563
|
|
|
379
|
-
|
|
564
|
+
// ---- 命令白名单(早期校验用;必须与下方 main() 的分发表保持一致)----
|
|
565
|
+
// 返回 true = 该命令存在、可以继续(可能还需要凭据);false = 未知命令,直接给用法提示。
|
|
566
|
+
// 为什么需要:loadAuth() 写在模块顶层,过去"未知命令"也要先付一整笔取凭据的开销
|
|
567
|
+
// (Windows 上 ~1.0s,去 shell 后 ~0.22s)才打印用法。v0.1.17 起提前判定。
|
|
568
|
+
const CMD_SUBS = {
|
|
569
|
+
ds: ["tables", "fields", "list", "chart", "stat", "count"],
|
|
570
|
+
doc: ["count", "list"],
|
|
571
|
+
noun: ["list"],
|
|
572
|
+
field: ["id2name", "name2id"],
|
|
573
|
+
conf: ["get", "batch", "batch-file", "group", "group-file", "resolve", "resolve-file", "add", "add-file", "update", "delete"],
|
|
574
|
+
exp: ["status", "manifest", "commits", "file", "pull", "init", "commit", "lock", "release", "rollback"],
|
|
575
|
+
attend: ["statis"],
|
|
576
|
+
eb: ["perm-page"],
|
|
577
|
+
};
|
|
578
|
+
function commandIsKnown(pos) {
|
|
579
|
+
const family = pos[0], sub = pos[1];
|
|
580
|
+
if (!family) return false; // 无命令 → 用法
|
|
581
|
+
if (family === "query") return true; // query 无子命令(cmdQuery 自行处理入参)
|
|
582
|
+
if (family === "login" || family === "xiaoe-env") return true; // 已由上方拦截块接管
|
|
583
|
+
const subs = CMD_SUBS[family];
|
|
584
|
+
if (!subs) return false;
|
|
585
|
+
return subs.includes(sub);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ---- 拦截(必须早于 loadAuth()):免凭据命令族 ----
|
|
589
|
+
// ① 版本 / ② 帮助 / ③ 未知命令 —— 这三类都不需要凭据,故在取凭据之前 return。
|
|
590
|
+
// (login 族由上方独立拦截块处理;exp release/rollback 的 --version 是版本号参数、长度 >1 不受影响)
|
|
591
|
+
{
|
|
592
|
+
const rawArgv = process.argv.slice(2);
|
|
593
|
+
const { args: earlyArgs, pos: earlyPos } = parseArgs(rawArgv);
|
|
594
|
+
|
|
595
|
+
// ① --version / -v:仅顶层单参数
|
|
596
|
+
if (rawArgv.length === 1 && (rawArgv[0] === "--version" || rawArgv[0] === "-v")) {
|
|
597
|
+
console.log("e10-data " + cliVersion());
|
|
598
|
+
process.exit(0);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ② --help / -h:显式求帮助 → 用法 + 退出码 0(区别于"用错了"的退出码 1)
|
|
602
|
+
// 只认「裸开关」(值为 true),避免把 --value --help 这类取值误判为求助;
|
|
603
|
+
// -h 是单短横线、parseArgs 不识别,单独按顶层单参数处理
|
|
604
|
+
if (earlyArgs.help === true || (rawArgv.length === 1 && rawArgv[0] === "-h")) {
|
|
605
|
+
usage();
|
|
606
|
+
process.exit(0);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ③ 未知命令族 / 未知子命令 / 缺子命令 → 用法提示,退出码 1
|
|
610
|
+
if (!commandIsKnown(earlyPos)) {
|
|
611
|
+
usage();
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ---- 取凭据(v0.1.17:先试进程间缓存,未命中再走 e10-login,并把结果发布给后续调用)----
|
|
617
|
+
let auth = null;
|
|
618
|
+
const CRED_CACHE_ON = !process.env.E10_DATA_NO_CRED_CACHE
|
|
619
|
+
&& !process.env.E10_DATA_AUTH_MODE && !process.env.E10_DATA_AUTH_PROBE;
|
|
620
|
+
if (CRED_CACHE_ON) auth = await credCacheGet();
|
|
621
|
+
if (!auth) {
|
|
622
|
+
auth = loadAuth();
|
|
623
|
+
// 兜底出口(明文镜像)的凭据不写入缓存 —— 避免把可能已过期的凭据固化整个 TTL
|
|
624
|
+
if (CRED_CACHE_ON && !/镜像/.test(String(auth.source || ""))) await credCachePublish(auth);
|
|
625
|
+
}
|
|
380
626
|
const BASE = auth.baseUrl.replace(/\/+$/, "");
|
|
381
627
|
const USER = { id: auth.userId || "", tenantKey: auth.tenantKey || "" };
|
|
382
628
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "e10-data",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "泛微 E10 数据接口统一 CLI(data-assistant 专家配套):登录(login xiaoe-env,环境变量 XiaoE 全局 token 换 ETEAMSID
|
|
3
|
+
"version": "0.1.17-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 缓存(不落明文)",
|
|
5
5
|
"author": "e10-dw",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"main": "bin/e10-data.mjs",
|
|
12
12
|
"files": [
|
|
13
13
|
"bin/",
|
|
14
|
-
"README.md"
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md"
|
|
15
16
|
],
|
|
16
17
|
"engines": {
|
|
17
18
|
"node": ">=18.17.0"
|