@xbongbong/xbbcli 2.4.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -8
- package/auth-login.js +3 -3
- package/auth-logout.js +4 -5
- package/auth-refresh-cache.js +117 -0
- package/orginal-text-list.js +131 -0
- package/package.json +1 -1
- package/token-set.js +1 -1
- package/xbb-token-store.js +103 -26
package/README.md
CHANGED
|
@@ -91,13 +91,27 @@ xbbcli auth-login --corpid <CORPID> # 校验登录的企业,不一致时报
|
|
|
91
91
|
xbbcli auth-login --keepOpen # 登录成功后保留浏览器窗口(默认自动关闭)
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
执行后与 `token-set`
|
|
94
|
+
执行后与 `token-set` 等价:`--env 0`(默认)写入 `~/.xbbcli/config.env`、刷新表单缓存、命令映射与部门/员工缓存(不写环境变量);`--env 1` 仅写入 `XBB_*` 环境变量,不写任何本地文件。
|
|
95
95
|
|
|
96
96
|
技术实现:零依赖,用 `node:child_process` 以 `--remote-debugging-pipe` 启动浏览器(fd3 写 / fd4 读 CDP,**不开任何调试端口**),profile 固定为 `~/.xbbcli/browser-profile`(与日常浏览器隔离,登录态可复用);CLI 读取页面 `localStorage` 的 `{corpid, userId, xbbAccessToken}`,再在页面内 `fetch` 网关 `apiToken/getApiToken` 换取个人 token。只支持 Chromium 内核浏览器(Chrome / Edge / Chromium),更多细节见 [`doc/auth-login-command-design.md`](doc/auth-login-command-design.md)。
|
|
97
97
|
|
|
98
98
|
- `--debug` 输出请求体、返回体与浏览器信息(密钥只显示掩码),`--raw` 输出换取 API token 接口的原始响应
|
|
99
99
|
- 失败时返回合成错误行:`NO_BROWSER`、`BROWSER_CLOSED`、`LOGIN_TIMEOUT`、`API_TOKEN_FAILED`、`CORPID_MISMATCH`、`SAVE_FAILED` 等
|
|
100
100
|
|
|
101
|
+
### 刷新本地缓存(auth-refresh-cache)
|
|
102
|
+
|
|
103
|
+
`token-set` / `auth-login` 保存凭证时会顺带刷新三份本地缓存。插件升级(新增命令导致命令映射变化)、缓存过期或被删除后,可以单独重刷,不需要重新输入 token:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
xbbcli auth-refresh-cache -f json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- 刷新范围(仅当前启用公司):`~/.xbbcli/<corpid>.formlist.json`(表单清单)、`~/.xbbcli/<corpid>.command-map.md`(命令映射)、`~/.xbbcli/<corpid>.department-user.json`(部门/员工清单),并把 `corpName` / `userName` 回填进 `config.env`
|
|
110
|
+
- 环境变量模式(`XBB_*`)下同样可用:只刷新缓存文件,不回填 `config.env`
|
|
111
|
+
- 只支持当前启用公司:底层接口一律使用启用公司的凭证,切换公司请先用 `xbbcli token-use`
|
|
112
|
+
- `--userId` 覆盖用于匹配 `userName` 的员工 id(默认取配置里的 `userId`);`--debug` 在 stderr 输出配置来源、corpid 与三份缓存文件路径
|
|
113
|
+
- 失败时返回 `status=error`、`code=CACHE_REFRESH_FAILED`,`message` 以「步骤 formlist / command-map / department-user / profile」指出中断位置,已完成的部分照常在对应列输出
|
|
114
|
+
|
|
101
115
|
`token-set` 会按 `corpid` 新增或覆盖一家公司,并把该公司置为 `enable=true`(其余公司自动置为 `false`),同时写入公司名称 `corpName`(取部门 `id` 为 `1` 的部门名称)和操作人姓名 `userName`(从员工列表中按 `userId` 匹配)。除 `token-set`、`token-list`、`token-use`、`token-del`、`auth-status`、`auth-logout` 外,其余命令都使用 `enable` 为 `true` 的公司配置。
|
|
102
116
|
|
|
103
117
|
同时会自动拉取两份表单清单并合并缓存到:
|
|
@@ -145,7 +159,7 @@ xbbcli user-list --pageSize 200 -f json
|
|
|
145
159
|
xbbcli auth-status # 回显当前激活的配置(token 默认掩码)
|
|
146
160
|
xbbcli auth-status --showToken # 需要完整 token 时
|
|
147
161
|
xbbcli auth-logout # 登出:删除激活的那一家配置 + 清除 XBB_* 环境变量
|
|
148
|
-
xbbcli auth-logout --
|
|
162
|
+
xbbcli auth-logout --env 0 # 只删 config.env,保留环境变量
|
|
149
163
|
```
|
|
150
164
|
|
|
151
165
|
- `auth-status` 以 `key` / `value` 两列回显当前生效的配置值,固定 6 行:`corpid`、`corpName`、`userName`、`userId`、`baseurl`、`token`;不再输出 `status`、`source`、`message`、`enable`、`companyCount`、`envActive`、`configFile`、`code`、`msg` 这些元信息列。
|
|
@@ -166,7 +180,7 @@ note 以上取值来自环境变量 XBB_*(config.env 中没有启用公
|
|
|
166
180
|
- `auth-status` 默认把 token 掩码为「前 6 + `***` + 后 4」(长度不足 11 时只保留前 2 位),只有 `--showToken` 才输出明文。
|
|
167
181
|
- `auth-logout` 只删除 `enable=true` 的那一条,**不会**把其他公司自动提升为启用:登出后本地可能一个启用公司都没有,需要 `xbbcli token-use --corpid <CORPID>` 或重新 `auth-login` / `token-set`。想保留其他公司并可切换时,用 `xbbcli token-del --corpid <CORPID>`(它会保证剩余配置仍有且仅有一个启用)。
|
|
168
182
|
- 删除前 `config.env` 会整份备份到 `~/.xbbcli/config.env.bak`(每次登出覆盖),`backupFile` 列给出路径;误删把 `.bak` 复制回 `config.env` 即可恢复。
|
|
169
|
-
- 默认**同时**清除 `XBB_*` 环境变量:Windows 用 `reg delete HKCU\Environment` 删除用户级变量(已打开的终端要重开才生效,`envStored` 为 `user`,原本就不存在时为 `none`);macOS/Linux 清理 `~/.xbbcli/env.sh` 里的 `export XBB_*` 行(文件清空则删除,`envStored` 为 `cleared` / `removed` / `none
|
|
183
|
+
- 默认**同时**清除 `XBB_*` 环境变量:Windows 用 `reg delete HKCU\Environment` 删除用户级变量(已打开的终端要重开才生效,`envStored` 为 `user`,原本就不存在时为 `none`);macOS/Linux 清理 `~/.xbbcli/env.sh` 里的 `export XBB_*` 行(文件清空则删除,`envStored` 为 `cleared` / `removed` / `none`)。传 `--env 0` 时只删 `config.env`,此时若环境变量仍在,`message` 会提示「业务命令会回落到它,本次不算彻底登出」(`XBB_ENV_ONLY=1` 时提示为「已强制只用环境变量」)。
|
|
170
184
|
- `config.env` 没有启用公司时:只有在环境变量确实存在时才只清环境变量(`config.env` 保持不动);两者都不存在则返回 `NO_ACTIVE_CONFIG`,不写任何文件。
|
|
171
185
|
- 任何情况都不改动 `<corpid>.formlist.json` / `<corpid>.command-map.md` / `<corpid>.department-user.json` 三份缓存,重新启用该公司时无需再次拉取。
|
|
172
186
|
|
|
@@ -189,7 +203,7 @@ note 以上取值来自环境变量 XBB_*(config.env 中没有启用公
|
|
|
189
203
|
2. `config.env` 缺失、解析失败或没有启用公司时,回落到上述环境变量
|
|
190
204
|
3. 设置 `XBB_ENV_ONLY=1` 可强制只使用环境变量(完全忽略 `config.env`)
|
|
191
205
|
|
|
192
|
-
`token-set`
|
|
206
|
+
`token-set` / `auth-login` 通过 `--env` 控制存储方式:`--env 0`(默认)仅写入本地文件(`config.env` 与各类缓存),`--env 1` 仅写入环境变量(不写任何本地文件)。选 `--env 1` 时会把 `corpid`/`token`/`baseurl`/`userId` 写入环境变量:
|
|
193
207
|
|
|
194
208
|
- Windows:用 `setx` 写入用户级环境变量,**只对新开的终端生效**
|
|
195
209
|
- macOS/Linux:写入 `~/.xbbcli/env.sh`(`export` 形式,权限 600),需要 `source ~/.xbbcli/env.sh` 才在当前 shell 生效
|
|
@@ -202,10 +216,11 @@ note 以上取值来自环境变量 XBB_*(config.env 中没有启用公
|
|
|
202
216
|
|
|
203
217
|
### 配置
|
|
204
218
|
|
|
205
|
-
- `auth-login`:拉起本地浏览器完成交互式登录(账号密码 / 短信 / 扫码均可),自动读取 Web 会话、换取 API token 并复用 `token-set` 落盘;支持 `--browser`、`--timeout`、`--corpid`、`--keepOpen`、`--
|
|
219
|
+
- `auth-login`:拉起本地浏览器完成交互式登录(账号密码 / 短信 / 扫码均可),自动读取 Web 会话、换取 API token 并复用 `token-set` 落盘;支持 `--browser`、`--timeout`、`--corpid`、`--keepOpen`、`--env`、`--debug`、`--raw`
|
|
206
220
|
- `auth-status`:以 `key` / `value` 两列回显当前激活配置的 `corpid`、`corpName`、`userName`、`userId`、`baseurl`、`token`(token 默认中间掩码,`--showToken` 出明文),来源与冲突提醒以 `note` 行追加,无生效配置时六个值留空并给出 `note`;`--debug` 在 stderr 输出配置文件路径、公司数量与生效的环境变量名
|
|
207
|
-
- `auth-logout`:删除 `config.env` 中 `enable=true` 的那一条配置,删除前整份备份为 `~/.xbbcli/config.env.bak`;不自动把其他公司提升为启用;默认同时清除 `XBB_*` 环境变量(`--
|
|
208
|
-
- `
|
|
221
|
+
- `auth-logout`:删除 `config.env` 中 `enable=true` 的那一条配置,删除前整份备份为 `~/.xbbcli/config.env.bak`;不自动把其他公司提升为启用;默认同时清除 `XBB_*` 环境变量(`--env 0` 跳过,返回 `envStored` / `envFile`);既无启用公司又无环境变量时返回 `NO_ACTIVE_CONFIG` 且不改动文件
|
|
222
|
+
- `auth-refresh-cache`:对当前启用公司重新拉取表单缓存、命令映射与部门/员工缓存,并回填 `corpName` / `userName`;失败时返回 `code=CACHE_REFRESH_FAILED`,`message` 注明中断步骤(`formlist` / `command-map` / `department-user` / `profile`),已完成的部分照常输出;支持 `--userId`、`--debug`
|
|
223
|
+
- `token-set`:保存个人 token、`corpid`、`userId`、`baseurl`,并刷新本地表单缓存、命令映射文件与部门/员工缓存;传入的 token 不以 `user_` 开头时,会先为该 `userId` 刷新并保存个人 token;`--env` 控制存储方式:`0`(默认)仅写入本地文件,`1` 仅写入 `XBB_*` 环境变量(Windows 用 `setx`,其他平台写 `~/.xbbcli/env.sh`)且不写任何本地文件;返回 `envStored` / `envFile` 两列
|
|
209
224
|
- `token-list`:列出本地保存的所有公司配置和唯一启用的公司(含 `corpName` 公司名称与 `userName` 操作人姓名,`source` 列标记来源 `config`/`env`);`--showToken` 显示完整 token(默认脱敏);环境变量模式下单行显示
|
|
210
225
|
- `token-use`:切换当前启用的公司(`xbbcli token-use --corpid <CORPID>`),保证仅且只有一个公司被启用
|
|
211
226
|
- `token-del`:删除指定公司的本地配置(`xbbcli token-del --corpid <CORPID>`),并保证剩余配置中仅且只有一个公司被启用
|
|
@@ -452,7 +467,7 @@ note 以上取值来自环境变量 XBB_*(config.env 中没有启用公
|
|
|
452
467
|
## 通用行为
|
|
453
468
|
|
|
454
469
|
- 先执行一次 `token-set`(或 `auth-login`)保存有效的 `corpid`、`token`、`userId`
|
|
455
|
-
- 用 `xbbcli auth-status` 查看当前生效的配置来自 `config.env` 还是环境变量;`xbbcli auth-logout` 删除激活配置并默认清除 `XBB_*` 环境变量(`--
|
|
470
|
+
- 用 `xbbcli auth-status` 查看当前生效的配置来自 `config.env` 还是环境变量;`xbbcli auth-logout` 删除激活配置并默认清除 `XBB_*` 环境变量(`--env 0` 保留)
|
|
456
471
|
- 除 `token-set` 外,其余命令都会从 `~/.xbbcli/config.env` 读取 `corpid`
|
|
457
472
|
- 大部分命令会从 `~/.xbbcli/config.env` 读取 `token`
|
|
458
473
|
- 未配置 `config.env`(或其中没有启用公司)时,命令会回落到 `XBB_CORPID` / `XBB_TOKEN` / `XBB_BASEURL` / `XBB_USERID` / `XBB_CORPNAME` / `XBB_USERNAME` 环境变量;`XBB_ENV_ONLY=1` 可强制只用环境变量
|
package/auth-login.js
CHANGED
|
@@ -254,7 +254,7 @@ async function authLogin(kwargs) {
|
|
|
254
254
|
corpid: exchange.corpid,
|
|
255
255
|
token: exchange.token,
|
|
256
256
|
userId: exchange.userId,
|
|
257
|
-
|
|
257
|
+
env: String(kwargs.env ?? '0'),
|
|
258
258
|
});
|
|
259
259
|
} catch (error) {
|
|
260
260
|
await finish(session, keepOpen);
|
|
@@ -287,7 +287,7 @@ async function authLogin(kwargs) {
|
|
|
287
287
|
cli({
|
|
288
288
|
site: 'xbb',
|
|
289
289
|
name: 'auth-login',
|
|
290
|
-
description: '
|
|
290
|
+
description: '打开浏览器完成销帮帮登录授权',
|
|
291
291
|
strategy: Strategy.PUBLIC,
|
|
292
292
|
access: 'write',
|
|
293
293
|
browser: false,
|
|
@@ -297,7 +297,7 @@ cli({
|
|
|
297
297
|
{ name: 'timeout', type: 'str', default: '', help: `等待登录完成的秒数(默认 ${DEFAULT_TIMEOUT_SECONDS})` },
|
|
298
298
|
{ name: 'corpid', type: 'str', default: '', help: '期望登录的企业 ID;与实际登录企业不一致时报错(可选)' },
|
|
299
299
|
{ name: 'keepOpen', type: 'bool', default: false, help: '登录成功后保留浏览器窗口(默认自动关闭)' },
|
|
300
|
-
{ name: '
|
|
300
|
+
{ name: 'env', type: 'str', default: '0', help: '存储方式:0 仅写入 config.env 文件(默认),1 仅写入环境变量(不写任何本地文件)' },
|
|
301
301
|
{ name: 'debug', type: 'bool', default: false, help: '输出请求体、返回体与浏览器信息(密钥只显示掩码)' },
|
|
302
302
|
{ name: 'raw', type: 'bool', default: false, help: '输出换取 API token 接口返回的原文' },
|
|
303
303
|
],
|
package/auth-logout.js
CHANGED
|
@@ -61,7 +61,7 @@ function previewCorpids(companies) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
async function authLogout(kwargs) {
|
|
64
|
-
const alsoEnv =
|
|
64
|
+
const alsoEnv = String(kwargs.env ?? '1').trim() !== '0';
|
|
65
65
|
const companies = readCompanies();
|
|
66
66
|
const index = companies.findIndex((item) => item && item.enable === true);
|
|
67
67
|
const envPresent = hasEnvConfig();
|
|
@@ -76,7 +76,7 @@ async function authLogout(kwargs) {
|
|
|
76
76
|
? `本地共 ${companies.length} 家公司(${previewCorpids(companies)})且均未启用;要登出某家请先用 xbbcli token-use --corpid <CORPID> 启用,或改用 xbbcli token-del --corpid <CORPID> 直接删除`
|
|
77
77
|
: '本地没有任何公司配置';
|
|
78
78
|
const envNote = envPresent
|
|
79
|
-
? '环境变量 XBB_*
|
|
79
|
+
? '环境变量 XBB_* 仍存在,本次未清除(用 --env 1 或不指定该参数即可一并清除)'
|
|
80
80
|
: '环境变量 XBB_* 也不存在,本次无可清除内容';
|
|
81
81
|
const hint = `${scope};${envNote}`;
|
|
82
82
|
return createRow({
|
|
@@ -139,7 +139,7 @@ async function authLogout(kwargs) {
|
|
|
139
139
|
}
|
|
140
140
|
} else if (envBefore) {
|
|
141
141
|
const forced = isEnvOnly() ? '且 XBB_ENV_ONLY=1 已强制只用环境变量' : '业务命令会回落到它';
|
|
142
|
-
parts.push(`注意:环境变量 XBB_*(corpid=${envBefore})仍然有效,${forced}
|
|
142
|
+
parts.push(`注意:环境变量 XBB_*(corpid=${envBefore})仍然有效,${forced},本次不算彻底登出;如需一并清除请改用 --env 1(或不指定该参数,auth-logout 默认会清除环境变量)`);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
const remainingCompanies = target ? remaining : readCompanies();
|
|
@@ -166,8 +166,7 @@ cli({
|
|
|
166
166
|
browser: false,
|
|
167
167
|
domain: 'proapi.xbongbong.com',
|
|
168
168
|
args: [
|
|
169
|
-
{ name: 'env', type: '
|
|
170
|
-
{ name: 'noEnv', type: 'bool', default: false, help: '不清除环境变量,仅删除 config.env 中的激活配置(与 token-set 的 --noEnv 一致)' },
|
|
169
|
+
{ name: 'env', type: 'str', default: '1', help: '是否同时清除 XBB_* 环境变量:1 清除(默认),0 仅删除 config.env 保留环境变量。Windows 删除用户级变量,其他平台清理 ~/.xbbcli/env.sh' },
|
|
171
170
|
{ name: 'debug', type: 'bool', default: false, help: '输出配置文件、备份路径与生效的环境变量名(不打印 token)' },
|
|
172
171
|
],
|
|
173
172
|
columns: ['status', 'message', 'configFile', 'backupFile', 'corpid', 'corpName', 'userId', 'companyCount', 'hasActive', 'envStored', 'envFile', 'code', 'msg'],
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { cli, Strategy } from './xbb-registry.js';
|
|
2
|
+
import { readActiveConfig, isEnvActive } from './xbb-config.js';
|
|
3
|
+
import {
|
|
4
|
+
refreshLocalCaches,
|
|
5
|
+
getFormlistFile,
|
|
6
|
+
getCommandMapFile,
|
|
7
|
+
getDepartmentUserFile,
|
|
8
|
+
} from './xbb-token-store.js';
|
|
9
|
+
|
|
10
|
+
function trim(value) {
|
|
11
|
+
return String(value ?? '').trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 统一输出:成功时给出三份缓存与回填结果,失败时回落到合成错误行(不抛异常)
|
|
15
|
+
function makeRow(fields) {
|
|
16
|
+
return [{
|
|
17
|
+
status: '',
|
|
18
|
+
message: '',
|
|
19
|
+
corpid: '',
|
|
20
|
+
userId: '',
|
|
21
|
+
corpName: '',
|
|
22
|
+
userName: '',
|
|
23
|
+
formlistFile: '',
|
|
24
|
+
formlistCount: '',
|
|
25
|
+
commandMapFile: '',
|
|
26
|
+
departmentUserFile: '',
|
|
27
|
+
departmentCount: '',
|
|
28
|
+
userCount: '',
|
|
29
|
+
profileUpdated: '',
|
|
30
|
+
code: '',
|
|
31
|
+
msg: '',
|
|
32
|
+
...fields,
|
|
33
|
+
}];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeErrorRow(code, msg, extra = {}) {
|
|
37
|
+
return makeRow({ status: 'error', code, msg, message: msg, ...extra });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function authRefreshCache(kwargs) {
|
|
41
|
+
const active = readActiveConfig();
|
|
42
|
+
const corpid = trim(active.corpid);
|
|
43
|
+
const userId = trim(kwargs.userId) || trim(active.userId);
|
|
44
|
+
|
|
45
|
+
if (kwargs.debug) {
|
|
46
|
+
const source = isEnvActive() ? 'env' : 'config';
|
|
47
|
+
process.stderr.write(`[debug] ConfigSource: ${source}\n[debug] Corpid: ${corpid || '(none)'}\n[debug] UserId: ${userId || '(none)'}\n[debug] FormlistFile: ${corpid ? getFormlistFile(corpid) : ''}\n[debug] CommandMapFile: ${corpid ? getCommandMapFile(corpid) : ''}\n[debug] DepartmentUserFile: ${corpid ? getDepartmentUserFile(corpid) : ''}\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!corpid) {
|
|
51
|
+
return makeErrorRow('NO_ACTIVE_CONFIG', '没有生效的配置:config.env 中不存在 enable=true 的公司,环境变量 XBB_* 也为空;请先执行 xbbcli auth-login 或 xbbcli token-set');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let cache;
|
|
55
|
+
try {
|
|
56
|
+
cache = await refreshLocalCaches(corpid, userId);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const partial = error.cache || {};
|
|
59
|
+
const step = trim(error.step) || 'unknown';
|
|
60
|
+
const message = `刷新本地缓存失败(步骤 ${step}):${error.message || error}`;
|
|
61
|
+
return makeRow({
|
|
62
|
+
status: 'error',
|
|
63
|
+
code: 'CACHE_REFRESH_FAILED',
|
|
64
|
+
msg: message,
|
|
65
|
+
message,
|
|
66
|
+
corpid,
|
|
67
|
+
userId,
|
|
68
|
+
corpName: partial.corpName ?? '',
|
|
69
|
+
userName: partial.userName ?? '',
|
|
70
|
+
formlistFile: partial.formlistFile ?? '',
|
|
71
|
+
formlistCount: partial.formlistCount ?? '',
|
|
72
|
+
commandMapFile: partial.commandMapFile ?? '',
|
|
73
|
+
departmentUserFile: partial.departmentUserFile ?? '',
|
|
74
|
+
departmentCount: partial.departmentCount ?? '',
|
|
75
|
+
userCount: partial.userCount ?? '',
|
|
76
|
+
profileUpdated: partial.profileUpdated ?? '',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const message = cache.profileUpdated
|
|
81
|
+
? '已刷新表单清单缓存、命令映射与部门/员工缓存,并回填 corpName/userName 到 config.env'
|
|
82
|
+
: isEnvActive()
|
|
83
|
+
? '已刷新表单清单缓存、命令映射与部门/员工缓存;当前为环境变量模式,corpName/userName 未回填 config.env'
|
|
84
|
+
: '已刷新表单清单缓存、命令映射与部门/员工缓存;该公司不在 config.env 中,corpName/userName 未回填';
|
|
85
|
+
|
|
86
|
+
return makeRow({
|
|
87
|
+
status: 'ok',
|
|
88
|
+
message,
|
|
89
|
+
corpid,
|
|
90
|
+
userId,
|
|
91
|
+
corpName: cache.corpName,
|
|
92
|
+
userName: cache.userName,
|
|
93
|
+
formlistFile: cache.formlistFile,
|
|
94
|
+
formlistCount: cache.formlistCount,
|
|
95
|
+
commandMapFile: cache.commandMapFile,
|
|
96
|
+
departmentUserFile: cache.departmentUserFile,
|
|
97
|
+
departmentCount: cache.departmentCount,
|
|
98
|
+
userCount: cache.userCount,
|
|
99
|
+
profileUpdated: cache.profileUpdated,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
cli({
|
|
104
|
+
site: 'xbb',
|
|
105
|
+
name: 'auth-refresh-cache',
|
|
106
|
+
description: '刷新当前启用公司的本地缓存(表单清单、命令映射等)',
|
|
107
|
+
strategy: Strategy.PUBLIC,
|
|
108
|
+
access: 'write',
|
|
109
|
+
browser: false,
|
|
110
|
+
domain: 'proapi.xbongbong.com',
|
|
111
|
+
args: [
|
|
112
|
+
{ name: 'userId', type: 'str', default: '', help: '操作人 id,用于从员工清单匹配 userName;默认取当前启用配置的 userId' },
|
|
113
|
+
{ name: 'debug', type: 'bool', default: false, help: '在 stderr 输出配置来源、corpid 与三份缓存文件路径' },
|
|
114
|
+
],
|
|
115
|
+
columns: ['status', 'message', 'corpid', 'userId', 'corpName', 'userName', 'formlistFile', 'formlistCount', 'commandMapFile', 'departmentUserFile', 'departmentCount', 'userCount', 'profileUpdated', 'code', 'msg'],
|
|
116
|
+
func: authRefreshCache,
|
|
117
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { cli, Strategy } from './xbb-registry.js';
|
|
5
|
+
import { readActiveConfig } from './xbb-config.js';
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), '.xbbcli');
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.env');
|
|
9
|
+
const DEFAULT_BASE_URL = 'https://proapi.xbongbong.com';
|
|
10
|
+
const ORIGINAL_TEXT_LIST_API_PATH = '/pro/v2/api/aiAgent/getAiOriginalText';
|
|
11
|
+
let lastPagination = null;
|
|
12
|
+
|
|
13
|
+
function readConfig() {
|
|
14
|
+
return readActiveConfig();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getRuntimeConfig() {
|
|
18
|
+
const config = readConfig();
|
|
19
|
+
return {
|
|
20
|
+
corpid: String(config.corpid || '').trim(),
|
|
21
|
+
token: String(config.token || '').trim(),
|
|
22
|
+
baseUrl: String(config.baseurl || DEFAULT_BASE_URL).trim(),
|
|
23
|
+
userId: String(config.userId || '').trim(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildApiUrl(baseUrl) {
|
|
28
|
+
return `${baseUrl.replace(/\/+$/, '')}${ORIGINAL_TEXT_LIST_API_PATH}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildPayload(kwargs, corpid) {
|
|
32
|
+
const payload = {
|
|
33
|
+
corpid,
|
|
34
|
+
...(String(kwargs.userId ?? '') !== '' ? { userId: String(kwargs.userId) } : {}),
|
|
35
|
+
...(String(kwargs.page ?? '') !== '' ? { page: Number(kwargs.page) } : {}),
|
|
36
|
+
...(String(kwargs.pageSize ?? '') !== '' ? { pageSize: Number(kwargs.pageSize) } : {}),
|
|
37
|
+
...(String(kwargs.startTime ?? '') !== '' ? { startTime: Number(kwargs.startTime) } : {}),
|
|
38
|
+
...(String(kwargs.endTime ?? '') !== '' ? { endTime: Number(kwargs.endTime) } : {}),
|
|
39
|
+
};
|
|
40
|
+
return payload;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeErrorRow(code, msg) {
|
|
44
|
+
return [{
|
|
45
|
+
rank: '',
|
|
46
|
+
originalUrl: '',
|
|
47
|
+
code,
|
|
48
|
+
msg,
|
|
49
|
+
}];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
cli({
|
|
53
|
+
site: 'xbb',
|
|
54
|
+
name: 'orginal-text-list',
|
|
55
|
+
description: '员工AI转录原文数据列表',
|
|
56
|
+
strategy: Strategy.PUBLIC,
|
|
57
|
+
access: 'read',
|
|
58
|
+
browser: false,
|
|
59
|
+
domain: 'proapi.xbongbong.com',
|
|
60
|
+
args: [
|
|
61
|
+
{ name: 'userId', type: 'str', default: '', help: '员工userId' },
|
|
62
|
+
{ name: 'page', type: 'str', default: '', help: '页码(可选)' },
|
|
63
|
+
{ name: 'pageSize', type: 'str', default: '', help: '每页数量(可选)' },
|
|
64
|
+
{ name: 'startTime', type: 'str', default: '', help: '开始时间戳,单位秒(可选)' },
|
|
65
|
+
{ name: 'endTime', type: 'str', default: '', help: '结束时间戳,单位秒(可选)' },
|
|
66
|
+
{ name: 'debug', type: 'bool', default: false, help: '输出请求体和返回体调试信息' },
|
|
67
|
+
{ name: 'raw', type: 'bool', default: false, help: '输出接口返回的原文' },
|
|
68
|
+
],
|
|
69
|
+
columns: ['rank', 'originalUrl', 'code', 'msg'],
|
|
70
|
+
footerExtra: () => (lastPagination ? `totalCount ${lastPagination.totalCount}` : undefined),
|
|
71
|
+
func: async (kwargs) => {
|
|
72
|
+
const debug = Boolean(kwargs.debug);
|
|
73
|
+
const { corpid, token, baseUrl, userId } = getRuntimeConfig();
|
|
74
|
+
const payload = buildPayload(kwargs, corpid);
|
|
75
|
+
const body = JSON.stringify(payload);
|
|
76
|
+
|
|
77
|
+
if (!payload.corpid) {
|
|
78
|
+
return makeErrorRow('NO_CORPID', '缺少本地 corpid;请先执行 xbbcli token-set --corpid <CORPID> --token <TOKEN> --userId <USERID>');
|
|
79
|
+
}
|
|
80
|
+
if (!token) {
|
|
81
|
+
return makeErrorRow('NO_TOKEN', '缺少 token;请先执行 xbbcli token-set --corpid <CORPID> --token <TOKEN> --userId <USERID>');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const sign = crypto.createHash('sha256').update(body + token).digest('hex');
|
|
85
|
+
const headers = {
|
|
86
|
+
'Content-Type': 'application/json;charset=UTF-8',
|
|
87
|
+
sign,
|
|
88
|
+
};
|
|
89
|
+
if (userId) headers.userId = userId;
|
|
90
|
+
const apiUrl = buildApiUrl(baseUrl);
|
|
91
|
+
|
|
92
|
+
if (debug) {
|
|
93
|
+
process.stderr.write(`[debug] URL: ${apiUrl}\n[debug] Headers: ${JSON.stringify(headers)}\n[debug] RequestBody: ${body}\n`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const resp = await fetch(apiUrl, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers,
|
|
99
|
+
body,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!resp.ok) {
|
|
103
|
+
const responseText = await resp.text();
|
|
104
|
+
if (debug) process.stderr.write(`[debug] ResponseBody: ${responseText}\n`);
|
|
105
|
+
return makeErrorRow(resp.status, `HTTP ${resp.status} ${resp.statusText}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const data = await resp.json();
|
|
109
|
+
lastPagination = data?.code === 1 && data?.totalCount != null
|
|
110
|
+
? { totalCount: data.totalCount }
|
|
111
|
+
: null;
|
|
112
|
+
const responseBody = JSON.stringify(data);
|
|
113
|
+
if (debug) process.stderr.write(`[debug] ResponseBody: ${responseBody}\n`);
|
|
114
|
+
if (kwargs.raw) return [{ raw: responseBody }];
|
|
115
|
+
if (data.code !== 1) {
|
|
116
|
+
return makeErrorRow(data.code ?? '', data.msg ?? '未知错误');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const list = Array.isArray(data.result?.list) ? data.result.list : [];
|
|
120
|
+
if (!list.length) {
|
|
121
|
+
return makeErrorRow('NO_DATA', '接口成功,但 list 为空');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return list.map((item, index) => ({
|
|
125
|
+
rank: index + 1,
|
|
126
|
+
originalUrl: item.originalUrl || '',
|
|
127
|
+
code: '',
|
|
128
|
+
msg: '',
|
|
129
|
+
}));
|
|
130
|
+
},
|
|
131
|
+
});
|
package/package.json
CHANGED
package/token-set.js
CHANGED
|
@@ -12,7 +12,7 @@ cli({
|
|
|
12
12
|
{ name: 'corpid', type: 'str', help: '公司id(必填)' },
|
|
13
13
|
{ name: 'token', type: 'str', help: '要保存的 API token' },
|
|
14
14
|
{ name: 'userId', type: 'str', help: '操作人id(必填)' },
|
|
15
|
-
{ name: '
|
|
15
|
+
{ name: 'env', type: 'str', default: '0', help: '存储方式:0 仅写入 config.env 文件(默认),1 仅写入环境变量(不写任何本地文件)' },
|
|
16
16
|
],
|
|
17
17
|
columns: ['status', 'message', 'configFile', 'corpid', 'baseurl', 'userId', 'enable', 'companyCount', 'formlistFile', 'commandMapFile', 'departmentUserFile', 'corpName', 'userName', 'envStored', 'envFile'],
|
|
18
18
|
func: saveCompanyCredentials,
|
package/xbb-token-store.js
CHANGED
|
@@ -53,6 +53,10 @@ async function getFormlistRows(corpid, saasMark) {
|
|
|
53
53
|
|
|
54
54
|
const errorRow = rows.find((item) => item && item.code);
|
|
55
55
|
if (errorRow) {
|
|
56
|
+
// 该公司在某个 saasMark 下没有表单时,form-list 返回 NO_DATA 属正常情况,按空清单继续
|
|
57
|
+
if (String(errorRow.code) === 'NO_DATA') {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
56
60
|
throw new Error(`form-list 获取失败(saasMark=${saasMark}):${errorRow.code} ${errorRow.msg || ''}`.trim());
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -105,7 +109,7 @@ async function writeFormlistFile(corpid) {
|
|
|
105
109
|
const mergedRows = [...customForms, ...systemForms];
|
|
106
110
|
|
|
107
111
|
fs.writeFileSync(formlistFile, JSON.stringify(mergedRows, null, 2) + '\n', 'utf8');
|
|
108
|
-
return formlistFile;
|
|
112
|
+
return { file: formlistFile, count: mergedRows.length };
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
async function fetchAllRows(name, pageSize) {
|
|
@@ -174,7 +178,7 @@ async function writeDepartmentUserFile(corpid, userId) {
|
|
|
174
178
|
const file = getDepartmentUserFile(corpid);
|
|
175
179
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
176
180
|
fs.writeFileSync(file, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
177
|
-
return { file, corpName: getCorpName(departmentRows), userName: getUserName(userRows, userId) };
|
|
181
|
+
return { file, corpName: getCorpName(departmentRows), userName: getUserName(userRows, userId), departmentCount: departmentRows.length, userCount: userRows.length };
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
function getCorpName(departmentRows) {
|
|
@@ -192,16 +196,89 @@ function getUserName(userRows, userId) {
|
|
|
192
196
|
return matched ? String(matched.name || '').trim() : '';
|
|
193
197
|
}
|
|
194
198
|
|
|
199
|
+
// 把 corpName / userName 合并进 config.env 对应公司,返回是否真的写过文件
|
|
200
|
+
// config.env 里没有这家公司的条目时(纯环境变量模式)直接跳过,避免凭空生成一份只有 corpName 的 config.env
|
|
195
201
|
export function writeCompanyProfile(corpid, profile) {
|
|
202
|
+
const target = String(corpid || '').trim();
|
|
196
203
|
const fields = Object.entries(profile).filter(([, value]) => String(value || '').trim() !== '');
|
|
197
|
-
if (!fields.length) {
|
|
198
|
-
return;
|
|
204
|
+
if (!target || !fields.length) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
const companies = readCompanies();
|
|
208
|
+
const index = companies.findIndex((item) => String(item.corpid || '').trim() === target);
|
|
209
|
+
if (index === -1) {
|
|
210
|
+
return false;
|
|
199
211
|
}
|
|
200
212
|
const patch = Object.fromEntries(fields);
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
213
|
+
const next = companies.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item));
|
|
214
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2) + '\n', 'utf8');
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// token-set / auth-login / auth-refresh-cache 共用:刷新三份本地缓存并回填 corpName、userName
|
|
219
|
+
// 失败时抛出的 error 上带 step(formlist/command-map/department-user/profile)与已完成部分的 cache,便于命令回显进度
|
|
220
|
+
export async function refreshLocalCaches(corpid, userId) {
|
|
221
|
+
const target = String(corpid || '').trim();
|
|
222
|
+
if (!target) {
|
|
223
|
+
const error = new Error('缺少 corpid,无法确定要刷新哪家公司的缓存');
|
|
224
|
+
error.step = 'formlist';
|
|
225
|
+
error.cache = { formlistFile: '', commandMapFile: '', departmentUserFile: '', formlistCount: '', departmentCount: '', userCount: '', corpName: '', userName: '', profileUpdated: false };
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
const cache = {
|
|
229
|
+
formlistFile: getFormlistFile(target),
|
|
230
|
+
commandMapFile: '',
|
|
231
|
+
departmentUserFile: '',
|
|
232
|
+
formlistCount: '',
|
|
233
|
+
departmentCount: '',
|
|
234
|
+
userCount: '',
|
|
235
|
+
corpName: '',
|
|
236
|
+
userName: '',
|
|
237
|
+
profileUpdated: false,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const formlist = await writeFormlistFile(target);
|
|
244
|
+
cache.formlistFile = formlist.file;
|
|
245
|
+
cache.formlistCount = formlist.count;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
error.step = 'formlist';
|
|
248
|
+
error.cache = cache;
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
cache.commandMapFile = writeCommandMap(target);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
error.step = 'command-map';
|
|
256
|
+
error.cache = cache;
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const departmentUser = await writeDepartmentUserFile(target, userId);
|
|
262
|
+
cache.departmentUserFile = departmentUser.file;
|
|
263
|
+
cache.corpName = departmentUser.corpName;
|
|
264
|
+
cache.userName = departmentUser.userName;
|
|
265
|
+
cache.departmentCount = departmentUser.departmentCount;
|
|
266
|
+
cache.userCount = departmentUser.userCount;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
error.step = 'department-user';
|
|
269
|
+
error.cache = cache;
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
cache.profileUpdated = writeCompanyProfile(target, { corpName: cache.corpName, userName: cache.userName });
|
|
275
|
+
} catch (error) {
|
|
276
|
+
error.step = 'profile';
|
|
277
|
+
error.cache = cache;
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return cache;
|
|
205
282
|
}
|
|
206
283
|
|
|
207
284
|
function normalizeBusinessType(value) {
|
|
@@ -349,33 +426,33 @@ export async function saveCompanyCredentials(kwargs) {
|
|
|
349
426
|
}
|
|
350
427
|
|
|
351
428
|
const baseurl = resolveBaseUrl(corpid);
|
|
429
|
+
const envOnly = String(kwargs.env ?? '0').trim() === '1';
|
|
430
|
+
|
|
431
|
+
// env=1:仅写入环境变量,跳过所有本地文件写入(config.env、表单缓存、命令映射、部门/员工缓存)
|
|
432
|
+
if (envOnly) {
|
|
433
|
+
try {
|
|
434
|
+
const envResult = persistEnvVars({ corpid, token: personalToken, baseurl, userId });
|
|
435
|
+
return createResult('ok', '已写入环境变量(未写入任何本地文件)', corpid, baseurl, userId, '', '', '', '', '', '', '', envResult.stored, envResult.file);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
const detail = String(error.message || error);
|
|
438
|
+
return createResult('error', `写入环境变量失败:${detail}`, corpid, baseurl, userId, '', '', '', '', '', '', '', 'failed', detail);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// env=0(默认):仅写入 config.env 文件与各类缓存,不写入环境变量
|
|
352
443
|
const companies = upsertCompany(readCompanies(), { corpid, token: personalToken, baseurl, userId });
|
|
353
444
|
|
|
354
445
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
355
446
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(companies, null, 2) + '\n', 'utf8');
|
|
356
447
|
|
|
357
448
|
try {
|
|
358
|
-
const
|
|
359
|
-
const commandMapFile =
|
|
360
|
-
|
|
361
|
-
writeCompanyProfile(corpid, { corpName, userName });
|
|
362
|
-
let envStored = 'skipped';
|
|
363
|
-
let envFile = '';
|
|
364
|
-
if (!kwargs.noEnv) {
|
|
365
|
-
try {
|
|
366
|
-
const envResult = persistEnvVars({ corpid, token: personalToken, baseurl, userId, corpName, userName });
|
|
367
|
-
envStored = envResult.stored;
|
|
368
|
-
envFile = envResult.file;
|
|
369
|
-
} catch (error) {
|
|
370
|
-
envStored = 'failed';
|
|
371
|
-
envFile = String(error.message || error);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return createResult('ok', '已保存 corpid、token、baseurl、userId,并同步表单模板缓存、命令映射文件与部门/员工缓存', corpid, baseurl, userId, formlistFile, commandMapFile, true, companies.length, departmentUserFile, corpName, userName, envStored, envFile);
|
|
449
|
+
const cache = await refreshLocalCaches(corpid, userId);
|
|
450
|
+
const { formlistFile, commandMapFile, departmentUserFile, corpName, userName } = cache;
|
|
451
|
+
return createResult('ok', '已保存 corpid、token、baseurl、userId,并同步表单模板缓存、命令映射文件与部门/员工缓存', corpid, baseurl, userId, formlistFile, commandMapFile, true, companies.length, departmentUserFile, corpName, userName, 'skipped', '');
|
|
375
452
|
} catch (error) {
|
|
376
453
|
return createResult(
|
|
377
454
|
'partial',
|
|
378
|
-
`已保存 corpid、token、baseurl、userId
|
|
455
|
+
`已保存 corpid、token、baseurl、userId,但同步缓存失败${error.step ? `(步骤 ${error.step})` : ''}:${error.message}`,
|
|
379
456
|
corpid,
|
|
380
457
|
baseurl,
|
|
381
458
|
userId,
|