@xbongbong/xbbcli 2.4.0 → 2.4.1

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 CHANGED
@@ -91,7 +91,7 @@ xbbcli auth-login --corpid <CORPID> # 校验登录的企业,不一致时报
91
91
  xbbcli auth-login --keepOpen # 登录成功后保留浏览器窗口(默认自动关闭)
92
92
  ```
93
93
 
94
- 执行后与 `token-set` 等价:写入 `~/.xbbcli/config.env`、刷新表单缓存、命令映射与部门/员工缓存,并同步 `XBB_*` 环境变量(加 `--noEnv` 可跳过)。
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
 
@@ -189,7 +189,7 @@ note 以上取值来自环境变量 XBB_*(config.env 中没有启用公
189
189
  2. `config.env` 缺失、解析失败或没有启用公司时,回落到上述环境变量
190
190
  3. 设置 `XBB_ENV_ONLY=1` 可强制只使用环境变量(完全忽略 `config.env`)
191
191
 
192
- `token-set` 成功后会自动把这 6 个值写入环境变量(加 `--noEnv` 可跳过):
192
+ `token-set` / `auth-login` 通过 `--env` 控制存储方式:`--env 0`(默认)仅写入本地文件(`config.env` 与各类缓存),`--env 1` 仅写入环境变量(不写任何本地文件)。选 `--env 1` 时会把 `corpid`/`token`/`baseurl`/`userId` 写入环境变量:
193
193
 
194
194
  - Windows:用 `setx` 写入用户级环境变量,**只对新开的终端生效**
195
195
  - macOS/Linux:写入 `~/.xbbcli/env.sh`(`export` 形式,权限 600),需要 `source ~/.xbbcli/env.sh` 才在当前 shell 生效
@@ -202,10 +202,10 @@ note 以上取值来自环境变量 XBB_*(config.env 中没有启用公
202
202
 
203
203
  ### 配置
204
204
 
205
- - `auth-login`:拉起本地浏览器完成交互式登录(账号密码 / 短信 / 扫码均可),自动读取 Web 会话、换取 API token 并复用 `token-set` 落盘;支持 `--browser`、`--timeout`、`--corpid`、`--keepOpen`、`--noEnv`、`--debug`、`--raw`
205
+ - `auth-login`:拉起本地浏览器完成交互式登录(账号密码 / 短信 / 扫码均可),自动读取 Web 会话、换取 API token 并复用 `token-set` 落盘;支持 `--browser`、`--timeout`、`--corpid`、`--keepOpen`、`--env`、`--debug`、`--raw`
206
206
  - `auth-status`:以 `key` / `value` 两列回显当前激活配置的 `corpid`、`corpName`、`userName`、`userId`、`baseurl`、`token`(token 默认中间掩码,`--showToken` 出明文),来源与冲突提醒以 `note` 行追加,无生效配置时六个值留空并给出 `note`;`--debug` 在 stderr 输出配置文件路径、公司数量与生效的环境变量名
207
207
  - `auth-logout`:删除 `config.env` 中 `enable=true` 的那一条配置,删除前整份备份为 `~/.xbbcli/config.env.bak`;不自动把其他公司提升为启用;默认同时清除 `XBB_*` 环境变量(`--noEnv` / `--env=false` 跳过,返回 `envStored` / `envFile`);既无启用公司又无环境变量时返回 `NO_ACTIVE_CONFIG` 且不改动文件
208
- - `token-set`:保存个人 token、`corpid`、`userId`、`baseurl`,并刷新本地表单缓存、命令映射文件与部门/员工缓存;传入的 token 不以 `user_` 开头时,会先为该 `userId` 刷新并保存个人 token;成功后同步写入 `XBB_*` 环境变量(Windows 用 `setx`,其他平台写 `~/.xbbcli/env.sh`),`--noEnv` 可跳过;返回 `envStored` / `envFile` 两列
208
+ - `token-set`:保存个人 token、`corpid`、`userId`、`baseurl`,并刷新本地表单缓存、命令映射文件与部门/员工缓存;传入的 token 不以 `user_` 开头时,会先为该 `userId` 刷新并保存个人 token;`--env` 控制存储方式:`0`(默认)仅写入本地文件,`1` 仅写入 `XBB_*` 环境变量(Windows 用 `setx`,其他平台写 `~/.xbbcli/env.sh`)且不写任何本地文件;返回 `envStored` / `envFile` 两列
209
209
  - `token-list`:列出本地保存的所有公司配置和唯一启用的公司(含 `corpName` 公司名称与 `userName` 操作人姓名,`source` 列标记来源 `config`/`env`);`--showToken` 显示完整 token(默认脱敏);环境变量模式下单行显示
210
210
  - `token-use`:切换当前启用的公司(`xbbcli token-use --corpid <CORPID>`),保证仅且只有一个公司被启用
211
211
  - `token-del`:删除指定公司的本地配置(`xbbcli token-del --corpid <CORPID>`),并保证剩余配置中仅且只有一个公司被启用
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
- noEnv: Boolean(kwargs.noEnv),
257
+ env: String(kwargs.env ?? '0'),
258
258
  });
259
259
  } catch (error) {
260
260
  await finish(session, keepOpen);
@@ -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: 'noEnv', type: 'bool', default: false, help: '不写入环境变量,仅保存到 config.env' },
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
@@ -167,7 +167,7 @@ cli({
167
167
  domain: 'proapi.xbongbong.com',
168
168
  args: [
169
169
  { name: 'env', type: 'bool', default: true, help: '同时清除 XBB_* 环境变量(默认开启;--env=false 跳过)。Windows 删除用户级变量,其他平台清理 ~/.xbbcli/env.sh' },
170
- { name: 'noEnv', type: 'bool', default: false, help: '不清除环境变量,仅删除 config.env 中的激活配置(与 token-set 的 --noEnv 一致)' },
170
+ { name: 'noEnv', type: 'bool', default: false, help: '不清除环境变量,仅删除 config.env 中的激活配置(等价于 --env=false)' },
171
171
  { name: 'debug', type: 'bool', default: false, help: '输出配置文件、备份路径与生效的环境变量名(不打印 token)' },
172
172
  ],
173
173
  columns: ['status', 'message', 'configFile', 'backupFile', 'corpid', 'corpName', 'userId', 'companyCount', 'hasActive', 'envStored', 'envFile', 'code', 'msg'],
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xbongbong/xbbcli",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "销帮帮 CRM 的独立命令行工具",
5
5
  "type": "module",
6
6
  "bin": {
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: 'noEnv', type: 'bool', default: false, help: '不写入环境变量,仅保存到 config.env' },
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,
@@ -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
 
@@ -349,6 +353,20 @@ export async function saveCompanyCredentials(kwargs) {
349
353
  }
350
354
 
351
355
  const baseurl = resolveBaseUrl(corpid);
356
+ const envOnly = String(kwargs.env ?? '0').trim() === '1';
357
+
358
+ // env=1:仅写入环境变量,跳过所有本地文件写入(config.env、表单缓存、命令映射、部门/员工缓存)
359
+ if (envOnly) {
360
+ try {
361
+ const envResult = persistEnvVars({ corpid, token: personalToken, baseurl, userId });
362
+ return createResult('ok', '已写入环境变量(未写入任何本地文件)', corpid, baseurl, userId, '', '', '', '', '', '', '', envResult.stored, envResult.file);
363
+ } catch (error) {
364
+ const detail = String(error.message || error);
365
+ return createResult('error', `写入环境变量失败:${detail}`, corpid, baseurl, userId, '', '', '', '', '', '', '', 'failed', detail);
366
+ }
367
+ }
368
+
369
+ // env=0(默认):仅写入 config.env 文件与各类缓存,不写入环境变量
352
370
  const companies = upsertCompany(readCompanies(), { corpid, token: personalToken, baseurl, userId });
353
371
 
354
372
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
@@ -359,19 +377,7 @@ export async function saveCompanyCredentials(kwargs) {
359
377
  const commandMapFile = writeCommandMap(corpid);
360
378
  const { file: departmentUserFile, corpName, userName } = await writeDepartmentUserFile(corpid, userId);
361
379
  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);
380
+ return createResult('ok', '已保存 corpid、token、baseurl、userId,并同步表单模板缓存、命令映射文件与部门/员工缓存', corpid, baseurl, userId, formlistFile, commandMapFile, true, companies.length, departmentUserFile, corpName, userName, 'skipped', '');
375
381
  } catch (error) {
376
382
  return createResult(
377
383
  'partial',