@xbongbong/xbbcli 2.4.2 → 2.4.4

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,9 +91,9 @@ xbbcli auth-login --corpid <CORPID> # 校验登录的企业,不一致时报
91
91
  xbbcli auth-login --keepOpen # 登录成功后保留浏览器窗口(默认自动关闭)
92
92
  ```
93
93
 
94
- 执行后与 `token-set` 等价:`--env 0`(默认)写入 `~/.xbbcli/config.env`、刷新表单缓存、命令映射与部门/员工缓存(不写环境变量);`--env 1` 仅写入 `XBB_*` 环境变量,不写任何本地文件。
94
+ 执行后与 `token-set` 等价:`--env 0`(默认)写入 `~/.xbbcli/config.env`、刷新表单缓存、命令映射与部门/员工缓存(不写环境变量);`--env 1` 仅写入 `XBB_*` 环境变量(供外部系统使用,本 CLI 不读取),不写任何本地文件。
95
95
 
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)。
96
+ 技术实现:零依赖,用 `node:child_process` 以 `--remote-debugging-pipe` 启动浏览器(fd3 写 / fd4 读 CDP,**不开任何调试端口**),profile 固定为 `~/.xbbcli/browser-profile`(与日常浏览器隔离,登录态可复用);CLI 读取页面 `localStorage` 的 `{corpid, userId, xbbAccessToken}`,再在页面内 `fetch` 网关 `apiPersonalToken/generate` 换取个人 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` 等
@@ -107,9 +107,8 @@ xbbcli auth-refresh-cache -f json
107
107
  ```
108
108
 
109
109
  - 刷新范围(仅当前启用公司):`~/.xbbcli/<corpid>.formlist.json`(表单清单)、`~/.xbbcli/<corpid>.command-map.md`(命令映射)、`~/.xbbcli/<corpid>.department-user.json`(部门/员工清单),并把 `corpName` / `userName` 回填进 `config.env`
110
- - 环境变量模式(`XBB_*`)下同样可用:只刷新缓存文件,不回填 `config.env`
111
110
  - 只支持当前启用公司:底层接口一律使用启用公司的凭证,切换公司请先用 `xbbcli token-use`
112
- - `--userId` 覆盖用于匹配 `userName` 的员工 id(默认取配置里的 `userId`);`--debug` 在 stderr 输出配置来源、corpid 与三份缓存文件路径
111
+ - `--userId` 覆盖用于匹配 `userName` 的员工 id(默认取配置里的 `userId`);`--debug` 在 stderr 输出 corpid、userId 与三份缓存文件路径
113
112
  - 失败时返回 `status=error`、`code=CACHE_REFRESH_FAILED`,`message` 以「步骤 formlist / command-map / department-user / profile」指出中断位置,已完成的部分照常在对应列输出
114
113
 
115
114
  `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` 的公司配置。
@@ -162,7 +161,7 @@ xbbcli auth-logout # 登出:删除激活的那一家配置 +
162
161
  xbbcli auth-logout --env 0 # 只删 config.env,保留环境变量
163
162
  ```
164
163
 
165
- - `auth-status` 以 `key` / `value` 两列回显当前生效的配置值,固定 6 行:`corpid`、`corpName`、`userName`、`userId`、`baseurl`、`token`;不再输出 `status`、`source`、`message`、`enable`、`companyCount`、`envActive`、`configFile`、`code`、`msg` 这些元信息列。
164
+ - `auth-status` 以 `key` / `value` 两列回显当前生效的配置值,固定 6 行:`corpid`、`corpName`、`userName`、`userId`、`baseurl`、`token`;随后追加 1 行 `验证`(用一次 `user-list --pageSize 1` 调用实测 token:`有效` / `无效-请重新配置token`);不再输出 `status`、`source`、`message`、`enable`、`companyCount`、`envActive`、`configFile`、`code`、`msg` 这些元信息列。
166
165
 
167
166
  ```text
168
167
  key value
@@ -172,21 +171,23 @@ userName 陈会兵
172
171
  userId 02415643151585
173
172
  baseurl https://proapi.xbongbong.com
174
173
  token user_6***a7da
175
- note 以上取值来自环境变量 XBB_*(config.env 中没有启用公司,回落到环境变量);环境变量模式不支持多公司切换
174
+ 验证 有效
175
+ note config.env 中存在 2 个 enable=true,仅第一条生效,请用 xbbcli token-use 修正
176
176
  ```
177
177
 
178
- - 来源(`config.env` 启用公司 还是 `XBB_*`)与冲突提醒只在需要时以 `note` 行追加:环境变量与 `config.env` 同时存在时提示「config.env 启用项优先」;`config.env` 里出现多个 `enable=true` 时提示用 `token-use` 修正。
179
- - 没有任何生效配置时,6 个值一律留空并给出 `note` 提示下一步命令(即使 `XBB_BASEURL` / `XBB_USERID` 单独残留也不会当成生效值回显)。配置文件路径、公司数量、生效的环境变量名改用 `--debug` 输出到 stderr。
178
+ - 冲突提醒只在需要时以 `note` 行追加:`config.env` 里出现多个 `enable=true` 时提示用 `token-use` 修正;没有任何生效配置时提示下一步命令,若同时检测到 `XBB_*` 环境变量,会追加一条「检测到 XBB_* 环境变量,但本版本不再读取环境变量」。
179
+ - `auth-status` 的 `验证` 行会实打实发起一次 `user-list --pageSize 1` 请求(等价于手工执行 `xbbcli user-list --pageSize 1`):接口成功(含 `NO_DATA` 空清单)→ `有效`;接口返回业务错误码或请求异常 → `无效-请重新配置token`。没有生效配置或缺少 token 时不再发起请求,直接给出 `无效-请重新配置token`;`--debug` 会输出判定结果与原因。
180
+ - 没有任何生效配置时,6 个配置值一律留空(`验证` 行为 `无效-请重新配置token`)并给出 `note` 提示下一步命令(即使 `XBB_BASEURL` / `XBB_USERID` 单独残留也不会当成生效值回显);检测到 `XBB_*` 环境变量时追加提示「本版本不再读取环境变量」。配置文件路径、公司数量与检测到的 `XBB_*` 环境变量名改用 `--debug` 输出到 stderr。
180
181
  - `auth-status` 默认把 token 掩码为「前 6 + `***` + 后 4」(长度不足 11 时只保留前 2 位),只有 `--showToken` 才输出明文。
181
- - `auth-logout` 只删除 `enable=true` 的那一条,**不会**把其他公司自动提升为启用:登出后本地可能一个启用公司都没有,需要 `xbbcli token-use --corpid <CORPID>` 或重新 `auth-login` / `token-set`。想保留其他公司并可切换时,用 `xbbcli token-del --corpid <CORPID>`(它会保证剩余配置仍有且仅有一个启用)。
182
+ - `auth-logout` 只删除 `enable=true` 的那一条,**不会**把其他公司自动提升为启用:登出后本地可能一个启用公司都没有,需要 `xbbcli token-use --corpid <CORPID>` 或重新 `auth-login` / `token-set`。想保留其他公司并可切换时,用 `xbbcli token-del --corpid <CORPID>`(它会保证剩余配置仍有且仅有一个启用;默认还会删除 `XBB_*` 环境变量,传 `--env 0` 可保留)。
182
183
  - 删除前 `config.env` 会整份备份到 `~/.xbbcli/config.env.bak`(每次登出覆盖),`backupFile` 列给出路径;误删把 `.bak` 复制回 `config.env` 即可恢复。
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` 时提示为「已强制只用环境变量」)。
184
+ - 默认**同时**清除 `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` 会提示环境变量仍存在(供外部系统使用;业务命令不再读取),并给出改用 `--env 1` 一并清除的建议。
184
185
  - `config.env` 没有启用公司时:只有在环境变量确实存在时才只清环境变量(`config.env` 保持不动);两者都不存在则返回 `NO_ACTIVE_CONFIG`,不写任何文件。
185
186
  - 任何情况都不改动 `<corpid>.formlist.json` / `<corpid>.command-map.md` / `<corpid>.department-user.json` 三份缓存,重新启用该公司时无需再次拉取。
186
187
 
187
- ## 环境变量模式(单公司)
188
+ ## 环境变量输出(供外部集成)
188
189
 
189
- 除 `config.env` 外,命令也支持从环境变量读取一家公司的凭证,便于 CI、容器与其他工具集成:
190
+ 自本版本起,业务命令不再读取 `XBB_*` 环境变量,凭证来源唯一化为 `config.env` 中 `enable=true` 的公司;`token-set` / `auth-login` 的 `--env 1` 仍会把凭证写入环境变量,供 CI、容器与其他外部工具集成消费:
190
191
 
191
192
  | 环境变量 | 对应字段 |
192
193
  | --- | --- |
@@ -197,33 +198,27 @@ note 以上取值来自环境变量 XBB_*(config.env 中没有启用公
197
198
  | `XBB_CORPNAME` | `corpName` |
198
199
  | `XBB_USERNAME` | `userName` |
199
200
 
200
- 读取优先级:
201
-
202
- 1. `config.env` 中存在 `enable=true` 的公司时,使用该配置(多公司切换行为不变)
203
- 2. `config.env` 缺失、解析失败或没有启用公司时,回落到上述环境变量
204
- 3. 设置 `XBB_ENV_ONLY=1` 可强制只使用环境变量(完全忽略 `config.env`)
205
-
206
201
  `token-set` / `auth-login` 通过 `--env` 控制存储方式:`--env 0`(默认)仅写入本地文件(`config.env` 与各类缓存),`--env 1` 仅写入环境变量(不写任何本地文件)。选 `--env 1` 时会把 `corpid`/`token`/`baseurl`/`userId` 写入环境变量:
207
202
 
208
203
  - Windows:用 `setx` 写入用户级环境变量,**只对新开的终端生效**
209
204
  - macOS/Linux:写入 `~/.xbbcli/env.sh`(`export` 形式,权限 600),需要 `source ~/.xbbcli/env.sh` 才在当前 shell 生效
210
205
 
211
- 环境变量模式只支持一家公司:此时 `token-list` 以 `source=env` 单行显示,`token-use` / `token-del` 会返回不支持多公司的错误行,并提示改用 `config.env`。
206
+ `token-list` 会把环境变量行(`source=env`)与 `config.env` 中的公司行(`source=config`)一起列出:环境变量行在 `msg` 列标注「XBB_* 环境变量存在(仅供外部系统,业务命令不再读取)」,`config.env` 的启用行固定标注「来源为 config.env,当前生效」;`token-use` 在切换 `config.env` 启用公司的同时,若检测到 `XBB_*` 有值,会把该公司凭证同步写回环境变量(目标公司缺 `token` 时拒绝切换,避免残留旧 `XBB_TOKEN`),便于外部系统跟随切换;`token-del` 默认在删除公司的同时一并删除 `XBB_*` 环境变量(`--env 0` 可保留),并在 `message` 说明删除后的实际生效来源。
212
207
 
213
- > 注意:`token-del` 不会清理已经写入的环境变量。要连环境变量一起登出,执行 `xbbcli auth-logout`(默认就会清除 `XBB_*`);也可以手工执行 `setx XBB_TOKEN ""`(其余 `XBB_*` 同理,Windows 需重开终端)或删除 `~/.xbbcli/env.sh` 并 `unset` 当前 shell 中已导出的变量。
208
+ > 注意:`token-del --env 0` 保留环境变量时,`XBB_*` 仍会存在(仅供外部系统使用;业务命令不再读取),`message` 会给出提示;要连环境变量一起清理,用 `token-del` 默认行为(不传 `--env`)或执行 `xbbcli auth-logout`(默认就会清除 `XBB_*`);也可以手工执行 `setx XBB_TOKEN ""`(其余 `XBB_*` 同理,Windows 需重开终端)或删除 `~/.xbbcli/env.sh` 并 `unset` 当前 shell 中已导出的变量。
214
209
 
215
210
  ## 当前支持的命令
216
211
 
217
212
  ### 配置
218
213
 
219
214
  - `auth-login`:拉起本地浏览器完成交互式登录(账号密码 / 短信 / 扫码均可),自动读取 Web 会话、换取 API token 并复用 `token-set` 落盘;支持 `--browser`、`--timeout`、`--corpid`、`--keepOpen`、`--env`、`--debug`、`--raw`
220
- - `auth-status`:以 `key` / `value` 两列回显当前激活配置的 `corpid`、`corpName`、`userName`、`userId`、`baseurl`、`token`(token 默认中间掩码,`--showToken` 出明文),来源与冲突提醒以 `note` 行追加,无生效配置时六个值留空并给出 `note`;`--debug` 在 stderr 输出配置文件路径、公司数量与生效的环境变量名
215
+ - `auth-status`:以 `key` / `value` 两列回显当前激活配置的 `corpid`、`corpName`、`userName`、`userId`、`baseurl`、`token`(token 默认中间掩码,`--showToken` 出明文),并追加 `验证` 行(调用一次 `user-list --pageSize 1` 实测 token,`有效` / `无效-请重新配置token`);来源与冲突提醒以 `note` 行追加(多个 `enable=true`;无生效配置时提示下一步命令,检测到 `XBB_*` 时说明本版本不再读取);`--debug` 在 stderr 输出配置文件路径、公司数量、检测到的 `XBB_*` 环境变量名与验证结果
221
216
  - `auth-logout`:删除 `config.env` 中 `enable=true` 的那一条配置,删除前整份备份为 `~/.xbbcli/config.env.bak`;不自动把其他公司提升为启用;默认同时清除 `XBB_*` 环境变量(`--env 0` 跳过,返回 `envStored` / `envFile`);既无启用公司又无环境变量时返回 `NO_ACTIVE_CONFIG` 且不改动文件
222
217
  - `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` 两列
224
- - `token-list`:列出本地保存的所有公司配置和唯一启用的公司(含 `corpName` 公司名称与 `userName` 操作人姓名,`source` 列标记来源 `config`/`env`);`--showToken` 显示完整 token(默认脱敏);环境变量模式下单行显示
225
- - `token-use`:切换当前启用的公司(`xbbcli token-use --corpid <CORPID>`),保证仅且只有一个公司被启用
226
- - `token-del`:删除指定公司的本地配置(`xbbcli token-del --corpid <CORPID>`),并保证剩余配置中仅且只有一个公司被启用
218
+ - `token-set`:保存个人 token、`corpid`、`userId`、`baseurl`,并刷新本地表单缓存、命令映射文件与部门/员工缓存;传入的 token 不以 `user_` 开头时,会先为该 `userId` 刷新并保存个人 token;`--env` 控制存储方式:`0`(默认)仅写入本地文件,`1` 仅写入 `XBB_*` 环境变量(供外部系统使用,本 CLI 不读取;Windows 用 `setx`,其他平台写 `~/.xbbcli/env.sh`)且不写任何本地文件;返回 `envStored` / `envFile` 两列
219
+ - `token-list`:同时列出环境变量行(`source=env`,仅 `XBB_CORPID` / `XBB_TOKEN` 有值时出现,`msg` 标注「仅供外部系统,业务命令不再读取」)与 `config.env` 中保存的全部公司(`source=config`,含 `corpName` 公司名称与 `userName` 操作人姓名),config 启用行在 `msg` 列标注「来源为 config.env,当前生效」;`--showToken` 显示完整 token(默认脱敏)
220
+ - `token-use`:切换当前启用的公司(`xbbcli token-use --corpid <CORPID>`),保证仅且只有一个公司被启用;若环境变量 `XBB_*` 有值,同时把该公司凭证(`corpid`/`token`/`baseurl`/`userId`/`corpName`/`userName`,缺 `baseurl` 时按 `corpid` 推导)同步写回环境变量(供外部系统使用,本 CLI 不读取)——Windows 用 `setx`(需重开终端)、macOS/Linux 写 `~/.xbbcli/env.sh`(需重新 `source`);同步失败时返回 `status=partial`,目标公司缺 `token` 时返回错误且不改动任何配置
221
+ - `token-del`:删除指定公司的本地配置(`xbbcli token-del --corpid <CORPID>`),并保证剩余配置中仅且只有一个公司被启用;默认同时删除 `XBB_*` 环境变量(`--env 0` 保留,返回 `envStored` / `envFile` 两列)
227
222
  - `token-generate`:生成/获取个人 token,`--resetToken 0` 获取(默认)、`1` 刷新;`--checkUserId` 未传则用配置中的 `userId`
228
223
 
229
224
  ### 组织与人员
@@ -467,10 +462,10 @@ note 以上取值来自环境变量 XBB_*(config.env 中没有启用公
467
462
  ## 通用行为
468
463
 
469
464
  - 先执行一次 `token-set`(或 `auth-login`)保存有效的 `corpid`、`token`、`userId`
470
- - 用 `xbbcli auth-status` 查看当前生效的配置来自 `config.env` 还是环境变量;`xbbcli auth-logout` 删除激活配置并默认清除 `XBB_*` 环境变量(`--env 0` 保留)
465
+ - 用 `xbbcli auth-status` 查看当前生效的配置(`config.env` 中 `enable=true` 的公司);`xbbcli auth-logout` 删除激活配置并默认清除 `XBB_*` 环境变量(`--env 0` 保留)
471
466
  - 除 `token-set` 外,其余命令都会从 `~/.xbbcli/config.env` 读取 `corpid`
472
467
  - 大部分命令会从 `~/.xbbcli/config.env` 读取 `token`
473
- - 未配置 `config.env`(或其中没有启用公司)时,命令会回落到 `XBB_CORPID` / `XBB_TOKEN` / `XBB_BASEURL` / `XBB_USERID` / `XBB_CORPNAME` / `XBB_USERNAME` 环境变量;`XBB_ENV_ONLY=1` 可强制只用环境变量
468
+ - 业务命令不再读取 `XBB_*` 环境变量:凭证来源唯一化为 `config.env` 中 `enable=true` 的公司;`--env 1` 写入的 `XBB_*` 仅供外部系统集成使用
474
469
  - 所有命令会从配置中读取 `userId` 并附加到请求 header 中
475
470
  - 大部分命令需要 formId 参数,可以根据业务名称或 businessType 从 `~/.xbbcli/<corpid>.formlist.json` 中获取 formId
476
471
  - 需要查询部门 id/名称,或员工 userId/所属部门时,可以先查 `~/.xbbcli/<corpid>.department-user.json`(由 `token-set` 分页抓取并缓存全部部门与员工)
package/auth-login.js CHANGED
@@ -3,7 +3,7 @@ import { cli, Strategy } from './xbb-registry.js';
3
3
  import { openBrowser, findBrowserExecutable, LOGIN_PAGE_URL } from './xbb-browser.js';
4
4
  import { saveCompanyCredentials } from './xbb-token-store.js';
5
5
 
6
- const API_TOKEN_PATH = '/pro/v1/apiToken/getApiToken';
6
+ const API_TOKEN_PATH = '/pro/v1/apiPersonalToken/generate';
7
7
  const PRO_GATEWAY_ORIGIN = 'https://progateway.xbongbong.com';
8
8
  const APP_GATEWAY_ORIGIN = 'https://appgateway.xbongbong.com';
9
9
  const PLATFORM = 'web';
@@ -39,6 +39,11 @@ function maskSecret(value) {
39
39
  return `${text.slice(0, 6)}***(${text.length})`;
40
40
  }
41
41
 
42
+ // debug 回显换取响应前对 token 字段做掩码(凭证明文只允许走 --raw 出口)
43
+ function maskResponseTokens(text) {
44
+ return String(text || '').replace(/"(?:personalToken|token)"\s*:\s*"([^"]*)"/gi, (match, value) => match.replace(value, maskSecret(value)));
45
+ }
46
+
42
47
  // 统一输出:成功时与 token-set 的列保持一致,失败时回落到合成错误行(不抛异常)
43
48
  function makeRow(fields) {
44
49
  return [{
@@ -91,10 +96,11 @@ async function readSession(session) {
91
96
  };
92
97
  }
93
98
 
94
- // 在页面内调用网关换取 API token:CORS 全开,sign = SHA256(JSON body + xbbAccessToken)
95
- async function exchangeApiToken(session, loginSession, debug) {
99
+ // 在页面内调用网关换取个人 token:CORS 全开,sign = SHA256(JSON body + xbbAccessToken)
100
+ // resetToken=0 复用已有 token、1 重新生成;checkUserId 为目标人员,自助登录时与登录用户一致
101
+ async function exchangeApiToken(session, loginSession, resetToken, debug) {
96
102
  const url = resolveGatewayUrl(loginSession.corpid);
97
- const body = JSON.stringify({ corpid: loginSession.corpid, userId: loginSession.userId, platform: PLATFORM });
103
+ const body = JSON.stringify({ corpid: loginSession.corpid, userId: loginSession.userId, checkUserId: loginSession.userId, platform: PLATFORM, resetToken });
98
104
  const sign = buildSign(body, loginSession.accessToken);
99
105
  const expression = `(async () => {
100
106
  const response = await fetch(${JSON.stringify(url)}, {
@@ -111,7 +117,7 @@ async function exchangeApiToken(session, loginSession, debug) {
111
117
 
112
118
  const text = await session.evaluate(expression, { attempts: 3, delayMs: 200 });
113
119
  const responseText = typeof text === 'string' ? text : JSON.stringify(text);
114
- if (debug) process.stderr.write(`[debug] ResponseBody: ${responseText}\n`);
120
+ if (debug) process.stderr.write(`[debug] ResponseBody: ${maskResponseTokens(responseText)}\n`);
115
121
 
116
122
  let data;
117
123
  try {
@@ -127,10 +133,7 @@ async function exchangeApiToken(session, loginSession, debug) {
127
133
  }
128
134
 
129
135
  const result = data.result;
130
- const token = normalizeArg(typeof result === 'string' ? result : (result && result.token));
131
- if (!token) {
132
- return { error: '接口未返回 API token;可到销帮帮「个人中心 - API 设置」复制 token 后执行 xbbcli token-set' };
133
- }
136
+ const token = normalizeArg(typeof result === 'string' ? result : (result && (result.personalToken || result.token)));
134
137
  return {
135
138
  token,
136
139
  corpid: normalizeArg(result && result.corpid) || loginSession.corpid,
@@ -139,6 +142,18 @@ async function exchangeApiToken(session, loginSession, debug) {
139
142
  };
140
143
  }
141
144
 
145
+ // resetToken=0 复用已有 token;接口返回空 token 时改用 resetToken=1 重新生成,仍未返回则给出合成错误
146
+ async function exchangeApiTokenWithFallback(session, loginSession, debug) {
147
+ const existing = await exchangeApiToken(session, loginSession, 0, debug);
148
+ if (existing.error || existing.sessionExpired || existing.token) return existing;
149
+
150
+ const regenerated = await exchangeApiToken(session, loginSession, 1, debug);
151
+ if (!regenerated.error && !regenerated.sessionExpired && !regenerated.token) {
152
+ return { ...regenerated, error: '接口未返回 API token;可到销帮帮「个人中心 - API 设置」复制 token 后执行 xbbcli token-set' };
153
+ }
154
+ return regenerated;
155
+ }
156
+
142
157
  async function clearAccessToken(session) {
143
158
  try {
144
159
  await session.evaluate(`(() => { localStorage.removeItem('xbbAccessToken'); return 'ok'; })()`, { attempts: 1 });
@@ -224,7 +239,19 @@ async function authLogin(kwargs) {
224
239
  continue;
225
240
  }
226
241
 
227
- const exchange = await exchangeApiToken(session, current, debug);
242
+ let exchange;
243
+ try {
244
+ exchange = await exchangeApiTokenWithFallback(session, current, debug);
245
+ } catch (error) {
246
+ // 页面导航、执行上下文重建等瞬时失败:记录原因后继续轮询;浏览器已关闭则直接返回
247
+ if (session.isClosed()) {
248
+ await finish(session, keepOpen);
249
+ return makeErrorRow('BROWSER_CLOSED', '浏览器已被关闭,登录未完成');
250
+ }
251
+ lastMessage = String(error.message || error);
252
+ await delay(POLL_INTERVAL_MS);
253
+ continue;
254
+ }
228
255
  if (exchange.sessionExpired) {
229
256
  // 旧会话已过期:清掉后继续等待用户重新登录
230
257
  await clearAccessToken(session);
@@ -297,7 +324,7 @@ cli({
297
324
  { name: 'timeout', type: 'str', default: '', help: `等待登录完成的秒数(默认 ${DEFAULT_TIMEOUT_SECONDS})` },
298
325
  { name: 'corpid', type: 'str', default: '', help: '期望登录的企业 ID;与实际登录企业不一致时报错(可选)' },
299
326
  { name: 'keepOpen', type: 'bool', default: false, help: '登录成功后保留浏览器窗口(默认自动关闭)' },
300
- { name: 'env', type: 'str', default: '0', help: '存储方式:0 仅写入 config.env 文件(默认),1 仅写入环境变量(不写任何本地文件)' },
327
+ { name: 'env', type: 'str', default: '0', help: '存储方式:0 仅写入 config.env 文件(默认);1 仅写入 XBB_* 环境变量(不写任何本地文件,供外部系统使用,本 CLI 不读取)' },
301
328
  { name: 'debug', type: 'bool', default: false, help: '输出请求体、返回体与浏览器信息(密钥只显示掩码)' },
302
329
  { name: 'raw', type: 'bool', default: false, help: '输出换取 API token 接口返回的原文' },
303
330
  ],
package/auth-logout.js CHANGED
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { cli, Strategy } from './xbb-registry.js';
5
- import { clearEnvVars, getEnvVarNames, hasEnvConfig, isEnvOnly, readEnvConfig } from './xbb-config.js';
5
+ import { clearEnvVars, getEnvVarNames, hasEnvConfig, readEnvConfig } from './xbb-config.js';
6
6
 
7
7
  const CONFIG_DIR = path.join(os.homedir(), '.xbbcli');
8
8
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.env');
@@ -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_* 仍存在,本次未清除(用 --env 1 或不指定该参数即可一并清除)'
79
+ ? '环境变量 XBB_* 仍存在(供外部系统使用;业务命令不再读取),本次未清除(用 --env 1 或不指定该参数即可一并清除)'
80
80
  : '环境变量 XBB_* 也不存在,本次无可清除内容';
81
81
  const hint = `${scope};${envNote}`;
82
82
  return createRow({
@@ -86,7 +86,7 @@ async function authLogout(kwargs) {
86
86
  hasActive: 'false',
87
87
  envStored: 'skipped',
88
88
  code: 'NO_ACTIVE_CONFIG',
89
- msg: isEnvOnly() ? `${hint}(当前 XBB_ENV_ONLY=1,config.env 被忽略)` : hint,
89
+ msg: hint,
90
90
  });
91
91
  }
92
92
 
@@ -138,8 +138,7 @@ async function authLogout(kwargs) {
138
138
  parts.push(`清除环境变量失败:${envFile}`);
139
139
  }
140
140
  } else if (envBefore) {
141
- const forced = isEnvOnly() ? '且 XBB_ENV_ONLY=1 已强制只用环境变量' : '业务命令会回落到它';
142
- parts.push(`注意:环境变量 XBB_*(corpid=${envBefore})仍然有效,${forced},本次不算彻底登出;如需一并清除请改用 --env 1(或不指定该参数,auth-logout 默认会清除环境变量)`);
141
+ parts.push(`注意:环境变量 XBB_*(corpid=${envBefore})仍然存在(供外部系统使用;业务命令不再读取),本次不算彻底登出;如需一并清除请改用 --env 1(或不指定该参数,auth-logout 默认会清除环境变量)`);
143
142
  }
144
143
 
145
144
  const remainingCompanies = target ? remaining : readCompanies();
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from './xbb-registry.js';
2
- import { readActiveConfig, isEnvActive } from './xbb-config.js';
2
+ import { readActiveConfig } from './xbb-config.js';
3
3
  import {
4
4
  refreshLocalCaches,
5
5
  getFormlistFile,
@@ -43,12 +43,11 @@ async function authRefreshCache(kwargs) {
43
43
  const userId = trim(kwargs.userId) || trim(active.userId);
44
44
 
45
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`);
46
+ process.stderr.write(`[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
47
  }
49
48
 
50
49
  if (!corpid) {
51
- return makeErrorRow('NO_ACTIVE_CONFIG', '没有生效的配置:config.env 中不存在 enable=true 的公司,环境变量 XBB_* 也为空;请先执行 xbbcli auth-login 或 xbbcli token-set');
50
+ return makeErrorRow('NO_ACTIVE_CONFIG', '没有生效的配置:config.env 中不存在 enable=true 的公司;请先执行 xbbcli auth-login 或 xbbcli token-set');
52
51
  }
53
52
 
54
53
  let cache;
@@ -79,9 +78,7 @@ async function authRefreshCache(kwargs) {
79
78
 
80
79
  const message = cache.profileUpdated
81
80
  ? '已刷新表单清单缓存、命令映射与部门/员工缓存,并回填 corpName/userName 到 config.env'
82
- : isEnvActive()
83
- ? '已刷新表单清单缓存、命令映射与部门/员工缓存;当前为环境变量模式,corpName/userName 未回填 config.env'
84
- : '已刷新表单清单缓存、命令映射与部门/员工缓存;该公司不在 config.env 中,corpName/userName 未回填';
81
+ : '已刷新表单清单缓存、命令映射与部门/员工缓存;该公司不在 config.env 中,corpName/userName 未回填';
85
82
 
86
83
  return makeRow({
87
84
  status: 'ok',
package/auth-status.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { cli, Strategy } from './xbb-registry.js';
5
- import { readActiveConfig, hasEnvConfig, isEnvActive, isEnvOnly, getEnvVarNames } from './xbb-config.js';
4
+ import { cli, Strategy, getRegistry } from './xbb-registry.js';
5
+ import { readActiveConfig, getEnvVarNames } from './xbb-config.js';
6
6
 
7
7
  const CONFIG_DIR = path.join(os.homedir(), '.xbbcli');
8
8
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.env');
@@ -50,48 +50,79 @@ function withNotes(rows, notes) {
50
50
  return [...rows, ...notes.map((value) => ({ key: 'note', value }))];
51
51
  }
52
52
 
53
+ const VERIFY_VALID = '有效';
54
+ const VERIFY_INVALID = '无效-请重新配置token';
55
+
56
+ // 用一次轻量调用(等价于 user-list --pageSize 1)验证当前 token 是否有效
57
+ // 判定:业务错误码(code 非空且非 NO_DATA)或请求异常 → 无效;成功(含 NO_DATA 空清单)→ 有效
58
+ async function verifyToken() {
59
+ const command = getRegistry().get('xbb/user-list');
60
+ if (!command || typeof command.func !== 'function') {
61
+ return { valid: false, detail: '找不到 user-list 命令,无法验证' };
62
+ }
63
+ try {
64
+ const rows = await command.func({ page: '1', pageSize: '1' });
65
+ const list = Array.isArray(rows) ? rows : [];
66
+ const errorRow = list.find((item) => item && String(item.code ?? '') !== '');
67
+ if (!errorRow) return { valid: true, detail: 'user-list 调用成功' };
68
+ // NO_DATA 表示接口成功但清单为空,token 仍然有效
69
+ if (String(errorRow.code) === 'NO_DATA') return { valid: true, detail: 'user-list 调用成功(无数据)' };
70
+ return { valid: false, detail: `user-list 返回 ${errorRow.code} ${errorRow.msg || ''}`.trim() };
71
+ } catch (error) {
72
+ return { valid: false, detail: `user-list 调用异常:${String(error.message || error)}` };
73
+ }
74
+ }
75
+
76
+ function withVerify(rows, verify) {
77
+ return [...rows, { key: 'token验证', value: verify.valid ? VERIFY_VALID : VERIFY_INVALID }];
78
+ }
79
+
53
80
  async function authStatus(kwargs) {
54
81
  const showToken = Boolean(kwargs.showToken);
55
- const envActive = hasEnvConfig();
56
- const envUsed = isEnvActive();
57
82
  const active = readActiveConfig();
58
83
  const companies = readCompanies();
59
84
  const hasActive = Boolean(trim(active.corpid) || trim(active.token));
60
- const source = hasActive ? (envUsed ? 'env' : 'config') : 'none';
85
+ const source = hasActive ? 'config' : 'none';
86
+ const envNames = getEnvVarNames().filter((name) => trim(process.env[name]));
61
87
 
62
88
  if (kwargs.debug) {
63
- const envNames = getEnvVarNames().filter((name) => trim(process.env[name])).join(', ');
64
- process.stderr.write(`[debug] ConfigFile: ${CONFIG_FILE}\n[debug] Companies: ${companies.length}\n[debug] Source: ${source}\n[debug] XBB_ENV_ONLY: ${trim(process.env.XBB_ENV_ONLY) || 'unset'}\n[debug] EnvVarsSet: ${envNames || 'none'}\n`);
89
+ process.stderr.write(`[debug] ConfigFile: ${CONFIG_FILE}\n[debug] Companies: ${companies.length}\n[debug] EnvVarsSet: ${envNames.join(', ') || 'none'}\n`);
65
90
  }
66
91
 
67
92
  const token = trim(active.token);
68
93
  const rows = toRows({ ...active, token: showToken ? token : maskToken(token) });
69
94
 
70
- if (source === 'none') {
71
- // 没有生效配置时不把残留的环境变量值当真值回显(XBB_BASEURL 等可能单独存在)
72
- return withNotes(toRows({}), [
73
- '没有处于激活状态的配置:config.env 中不存在 enable=true 的公司,环境变量 XBB_* 也为空;请先执行 xbbcli auth-login 或 xbbcli token-set',
74
- ]);
95
+ // 没有生效配置或缺少 token 时无需发起调用,直接判定为无效
96
+ let verify;
97
+ if (source === 'none' || !token) {
98
+ verify = { valid: false, detail: source === 'none' ? '没有生效配置,无法验证' : '生效配置缺少 token,无法验证' };
99
+ } else {
100
+ verify = await verifyToken();
101
+ }
102
+ if (kwargs.debug) {
103
+ process.stderr.write(`[debug] Verify: ${verify.valid ? 'valid' : 'invalid'}(${verify.detail})\n`);
75
104
  }
105
+ const verifiedRows = withVerify(rows, verify);
76
106
 
77
- if (source === 'env') {
78
- const reason = isEnvOnly() ? 'XBB_ENV_ONLY=1 已强制使用环境变量' : 'config.env 中没有启用公司,回落到环境变量';
79
- return withNotes(rows, [
80
- `以上取值来自环境变量 XBB_*(${reason});环境变量模式不支持多公司切换,配置文件路径与公司数量见 --debug`,
81
- ]);
107
+ if (source === 'none') {
108
+ // 没有生效配置时不把残留的环境变量值当真值回显;检测到 XBB_* 时提示本版本不再读取
109
+ const notes = ['没有处于激活状态的配置:config.env 中不存在 enable=true 的公司;请先执行 xbbcli auth-login 或 xbbcli token-set'];
110
+ if (envNames.length) {
111
+ notes.push('检测到 XBB_* 环境变量,但本版本不再读取环境变量');
112
+ }
113
+ return withNotes(verifiedRows, notes);
82
114
  }
83
115
 
84
116
  const notes = [];
85
- if (envActive) notes.push('环境变量 XBB_* 同时存在,但 config.env 启用项优先;如需彻底登出请执行 xbbcli auth-logout');
86
117
  const enabledCount = companies.filter((item) => item.enable === true).length;
87
118
  if (enabledCount > 1) notes.push(`config.env 中存在 ${enabledCount} 个 enable=true,仅第一条生效,请用 xbbcli token-use 修正`);
88
- return withNotes(rows, notes);
119
+ return withNotes(verifiedRows, notes);
89
120
  }
90
121
 
91
122
  cli({
92
123
  site: 'xbb',
93
124
  name: 'auth-status',
94
- description: '回显当前激活的配置',
125
+ description: '回显当前激活的配置,并验证 token 是否有效',
95
126
  strategy: Strategy.PUBLIC,
96
127
  access: 'read',
97
128
  browser: false,
@@ -31,6 +31,15 @@ function buildApiUrl(baseUrl, defaultUrl) {
31
31
  }
32
32
 
33
33
  function buildConditions(kwargs) {
34
+ const conditions = kwargs.conditions;
35
+ if (typeof conditions === 'string' && conditions.trim()) {
36
+ const parsed = JSON.parse(conditions);
37
+ if (!Array.isArray(parsed)) {
38
+ throw new Error('INVALID_CONDITIONS:conditions 必须是 JSON 数组');
39
+ }
40
+ return parsed;
41
+ }
42
+
34
43
  if (!(kwargs.attr && kwargs.value)) {
35
44
  return [];
36
45
  }
@@ -126,6 +135,7 @@ cli({
126
135
  args: [
127
136
  { name: 'userId', type: 'str', default: '', help: '操作人id(可选)' },
128
137
  { name: 'viewApproval', type: 'str', default: '', help: '是否查询审批中数据,1是,0否' },
138
+ { name: 'conditions', type: 'str', default: '', help: '条件集合 JSON 字符串,例如 [{"attr":"text_1","value":["025554"],"symbol":"equal"}]' },
129
139
  { name: 'attr', type: 'str', default: '', help: '筛选字段 attr,例如 text_1' },
130
140
  { name: 'value', type: 'str', default: '', help: '筛选值,和 --attr 配合使用' },
131
141
  { name: 'symbol', type: 'str', default: 'equal', help: '筛选操作符,默认 equal' },
@@ -139,7 +149,16 @@ cli({
139
149
  func: async function (kwargs) {
140
150
  const debug = Boolean(kwargs.debug);
141
151
  const { corpid, token, baseUrl, userId } = getRuntimeConfig();
142
- const payload = buildPayload(kwargs, corpid);
152
+ let payload;
153
+ try {
154
+ payload = buildPayload(kwargs, corpid);
155
+ } catch (error) {
156
+ const message = String(error?.message || error);
157
+ const separatorIndex = message.indexOf(':');
158
+ const code = separatorIndex > 0 ? message.slice(0, separatorIndex) : 'INVALID_PAYLOAD';
159
+ const detail = separatorIndex > 0 ? message.slice(separatorIndex + 1) : message;
160
+ return makeErrorRow(code, detail);
161
+ }
143
162
  const body = JSON.stringify(payload);
144
163
 
145
164
  const validationError = getValidationError(payload, token);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xbongbong/xbbcli",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
4
4
  "description": "销帮帮 CRM 的独立命令行工具",
5
5
  "type": "module",
6
6
  "bin": {
package/token-del.js CHANGED
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { cli, Strategy } from './xbb-registry.js';
5
- import { isEnvActive, isEnvOnly, getEnvVarNames } from './xbb-config.js';
5
+ import { clearEnvVars, getEnvVarNames, hasEnvConfig, readEnvConfig } from './xbb-config.js';
6
6
 
7
7
  const CONFIG_DIR = path.join(os.homedir(), '.xbbcli');
8
8
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.env');
@@ -50,7 +50,7 @@ function maskToken(token) {
50
50
  if (value.length <= 12) return `${value.slice(0, 4)}****`;
51
51
  return `${value.slice(0, 8)}****${value.slice(-4)}`;
52
52
  }
53
- function createResult(status, message, corpid, enable, companyCount) {
53
+ function createResult(status, message, corpid, enable, companyCount, envStored = 'skipped', envFile = '') {
54
54
  return [{
55
55
  status,
56
56
  message,
@@ -58,6 +58,8 @@ function createResult(status, message, corpid, enable, companyCount) {
58
58
  corpid,
59
59
  enable,
60
60
  companyCount,
61
+ envStored,
62
+ envFile,
61
63
  }];
62
64
  }
63
65
 
@@ -67,9 +69,9 @@ async function delCompany(kwargs) {
67
69
  return createResult('error', '缺少 --corpid', '', '', '');
68
70
  }
69
71
 
70
- if (isEnvOnly() || isEnvActive()) {
71
- return createResult('error', `当前为环境变量模式(${getEnvVarNames().join(', ')}),不支持删除公司配置;如需多公司请改用 config.env`, corpid, '', '');
72
- }
72
+ const alsoEnv = String(kwargs.env ?? '1').trim() !== '0';
73
+ const envPresent = hasEnvConfig();
74
+ const envBefore = envPresent ? String(readEnvConfig().corpid || '').trim() : '';
73
75
 
74
76
  const companies = readCompanies();
75
77
  if (!companies.length) {
@@ -84,23 +86,56 @@ async function delCompany(kwargs) {
84
86
  const next = remaining.length ? enableSingle(remaining) : [];
85
87
  writeCompanies(next);
86
88
  const enabledCorpid = next.find((item) => item.enable === true)?.corpid || '';
87
- const message = next.length
89
+ const parts = [next.length
88
90
  ? `已删除 corpid=${corpid},剩余 ${next.length} 家公司,当前启用 corpid=${enabledCorpid}`
89
- : `已删除 corpid=${corpid},本地已无公司配置`;
90
- return createResult('ok', message, corpid, next.length ? 'true' : '', next.length);
91
+ : `已删除 corpid=${corpid},本地已无公司配置`];
92
+
93
+ let envStored = 'skipped';
94
+ let envFile = '';
95
+
96
+ if (alsoEnv) {
97
+ try {
98
+ const result = clearEnvVars();
99
+ envStored = result.stored;
100
+ envFile = result.file;
101
+ if (envStored === 'none') {
102
+ parts.push('未检测到 XBB_* 环境变量,无需删除');
103
+ } else {
104
+ const suffix = process.platform === 'win32' ? '(已打开的终端需重开才会消失)' : '';
105
+ parts.push(`环境变量 ${getEnvVarNames().join('、')} 已删除${suffix}`);
106
+ }
107
+ // 环境变量已清空,说明删除后的实际生效来源
108
+ if (envStored !== 'none' || envBefore) {
109
+ if (next.length) {
110
+ parts.push(`此后业务命令使用 config.env 中启用的 corpid=${enabledCorpid}`);
111
+ } else {
112
+ parts.push('此后没有可用凭证,请重新执行 xbbcli token-set 或 xbbcli auth-login');
113
+ }
114
+ }
115
+ } catch (error) {
116
+ envStored = 'failed';
117
+ envFile = String(error.message || error);
118
+ return createResult('partial', `${parts.join(';')};删除环境变量失败:${envFile}`, corpid, next.length ? 'true' : '', next.length, envStored, envFile);
119
+ }
120
+ } else if (envPresent) {
121
+ parts.push(`注意:环境变量 XBB_*(corpid=${envBefore || '未知'})仍然存在(供外部系统使用;业务命令不再读取);如需一并删除请改用 --env 1(或不指定该参数)`);
122
+ }
123
+
124
+ return createResult('ok', parts.join(';'), corpid, next.length ? 'true' : '', next.length, envStored, envFile);
91
125
  }
92
126
 
93
127
  cli({
94
128
  site: 'xbb',
95
129
  name: 'token-del',
96
- description: '删除指定的已保存公司配置,并保证剩余配置中仅且只有一个被启用',
130
+ description: '删除指定的已保存公司配置,并保证剩余配置中仅且只有一个被启用;默认同时删除 XBB_* 环境变量',
97
131
  strategy: Strategy.PUBLIC,
98
132
  access: 'write',
99
133
  browser: false,
100
134
  domain: 'proapi.xbongbong.com',
101
135
  args: [
102
136
  { name: 'corpid', type: 'str', help: '要删除的公司 id,必须是已保存的 corpid(必填)' },
137
+ { name: 'env', type: 'str', default: '1', help: '是否同时删除 XBB_* 环境变量:1 删除(默认),0 仅删除 config.env 保留环境变量。Windows 删除用户级变量,其他平台清理 ~/.xbbcli/env.sh' },
103
138
  ],
104
- columns: ['status', 'message', 'configFile', 'corpid', 'enable', 'companyCount'],
139
+ columns: ['status', 'message', 'configFile', 'corpid', 'enable', 'companyCount', 'envStored', 'envFile'],
105
140
  func: delCompany,
106
141
  });
package/token-list.js CHANGED
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { cli, Strategy } from './xbb-registry.js';
5
- import { readEnvConfig, hasEnvConfig, isEnvOnly } from './xbb-config.js';
5
+ import { readEnvConfig, hasEnvConfig } from './xbb-config.js';
6
6
 
7
7
  const CONFIG_DIR = path.join(os.homedir(), '.xbbcli');
8
8
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.env');
@@ -67,40 +67,50 @@ cli({
67
67
  ],
68
68
  columns: ['enable', 'corpid', 'userId', 'baseurl', 'token', 'corpName', 'userName', 'code', 'msg', 'source'],
69
69
  func: async (kwargs) => {
70
+ const showToken = Boolean(kwargs.showToken);
70
71
  const companies = readCompanies();
71
72
  const envConfig = readEnvConfig();
72
- if (isEnvOnly() || (!companies.some((item) => item && item.enable === true) && hasEnvConfig(envConfig))) {
73
- return [{
74
- enable: 'true',
73
+ const envPresent = hasEnvConfig(envConfig);
74
+ const rows = [];
75
+
76
+ // 环境变量行:XBB_CORPID / XBB_TOKEN 有值就回显,source=env;仅供外部系统消费,业务命令不再读取
77
+ if (envPresent) {
78
+ rows.push({
79
+ enable: '',
75
80
  corpid: String(envConfig.corpid || ''),
76
81
  userId: String(envConfig.userId || ''),
77
82
  baseurl: String(envConfig.baseurl || ''),
78
- token: Boolean(kwargs.showToken) ? String(envConfig.token || '') : maskToken(envConfig.token),
83
+ token: showToken ? String(envConfig.token || '') : maskToken(envConfig.token),
79
84
  corpName: String(envConfig.corpName || ''),
80
85
  userName: String(envConfig.userName || ''),
81
86
  code: '',
82
- msg: '来源为环境变量 XBB_*(环境变量模式不支持多公司切换)',
87
+ msg: 'XBB_* 环境变量存在(仅供外部系统,业务命令不再读取)',
83
88
  source: 'env',
84
- }];
89
+ });
90
+ }
91
+
92
+ const enabledCount = companies.filter((item) => item.enable === true).length;
93
+ for (const item of companies) {
94
+ const isEnabled = item.enable === true;
95
+ rows.push({
96
+ enable: isEnabled ? 'true' : 'false',
97
+ corpid: String(item.corpid || ''),
98
+ userId: String(item.userId || ''),
99
+ baseurl: String(item.baseurl || ''),
100
+ token: showToken ? String(item.token || '') : maskToken(item.token),
101
+ corpName: String(item.corpName || ''),
102
+ userName: String(item.userName || ''),
103
+ code: '',
104
+ msg: isEnabled ? '来源为 config.env,当前生效' : '',
105
+ source: 'config',
106
+ });
85
107
  }
86
- if (!companies.length) {
108
+
109
+ if (!rows.length) {
87
110
  return makeErrorRow('NO_CONFIG', '缺少本地配置;请先执行 xbbcli token-set --corpid <CORPID> --token <TOKEN> --userId <USERID>');
88
111
  }
89
- const showToken = Boolean(kwargs.showToken);
90
- const enabledCount = companies.filter((item) => item.enable === true).length;
91
- const rows = companies.map((item) => ({
92
- enable: item.enable === true ? 'true' : 'false',
93
- corpid: String(item.corpid || ''),
94
- userId: String(item.userId || ''),
95
- baseurl: String(item.baseurl || ''),
96
- token: showToken ? String(item.token || '') : maskToken(item.token),
97
- corpName: String(item.corpName || ''),
98
- userName: String(item.userName || ''),
99
- code: '',
100
- msg: '',
101
- source: 'config',
102
- }));
103
- if (enabledCount !== 1) {
112
+
113
+ if (companies.length && enabledCount !== 1) {
104
114
  rows.push({
105
115
  enable: '',
106
116
  corpid: '',
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: 'env', type: 'str', default: '0', help: '存储方式:0 仅写入 config.env 文件(默认),1 仅写入环境变量(不写任何本地文件)' },
15
+ { name: 'env', type: 'str', default: '0', help: '存储方式:0 仅写入 config.env 文件(默认);1 仅写入 XBB_* 环境变量(不写任何本地文件,供外部系统使用,本 CLI 不读取)' },
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/token-use.js CHANGED
@@ -2,7 +2,8 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { cli, Strategy } from './xbb-registry.js';
5
- import { isEnvActive, isEnvOnly, getEnvVarNames } from './xbb-config.js';
5
+ import { hasEnvConfig, persistEnvVars } from './xbb-config.js';
6
+ import { resolveBaseUrl } from './xbb-token-store.js';
6
7
 
7
8
  const CONFIG_DIR = path.join(os.homedir(), '.xbbcli');
8
9
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.env');
@@ -67,9 +68,7 @@ async function useCompany(kwargs) {
67
68
  return createResult('error', '缺少 --corpid', '', '', '');
68
69
  }
69
70
 
70
- if (isEnvOnly() || isEnvActive()) {
71
- return createResult('error', `当前为环境变量模式(${getEnvVarNames().join(', ')}),不支持切换公司;如需多公司请改用 config.env`, corpid, '', '');
72
- }
71
+ const envPresent = hasEnvConfig();
73
72
 
74
73
  const companies = readCompanies();
75
74
  if (!companies.length) {
@@ -81,15 +80,45 @@ async function useCompany(kwargs) {
81
80
  return createResult('error', `未找到 corpid=${corpid} 的本地配置`, corpid, '', companies.length);
82
81
  }
83
82
 
83
+ const target = companies[index];
84
+ // XBB_* 有值时会同步覆盖环境变量,需要完整凭证:缺 token 时直接拒绝,避免残留上一家公司的 XBB_TOKEN
85
+ if (envPresent && !String(target.token || '').trim()) {
86
+ return createResult('error', `corpid=${corpid} 的本地配置缺少 token,无法同步到 XBB_* 环境变量;请先执行 xbbcli token-set --corpid ${corpid} --token <TOKEN> --userId <USERID>`, corpid, '', companies.length);
87
+ }
88
+
84
89
  const next = enableOnly(companies, index);
85
90
  writeCompanies(next);
86
- return createResult('ok', `已启用 corpid=${corpid},其余公司已标记为 enable=false`, corpid, 'true', next.length);
91
+
92
+ const parts = [`已启用 corpid=${corpid},其余公司已标记为 enable=false`];
93
+
94
+ // 环境变量中有值时把启用公司凭证同步写回 XBB_*(供外部系统消费,本 CLI 不再读取)
95
+ if (envPresent) {
96
+ try {
97
+ persistEnvVars({
98
+ corpid: target.corpid,
99
+ token: target.token,
100
+ // 条目缺少 baseurl 时按 corpid 规则推导,避免残留上一家公司的 XBB_BASEURL
101
+ baseurl: target.baseurl || resolveBaseUrl(target.corpid),
102
+ userId: target.userId,
103
+ corpName: target.corpName,
104
+ userName: target.userName,
105
+ });
106
+ const suffix = process.platform === 'win32'
107
+ ? '(Windows 需重开终端后才生效)'
108
+ : '(重新 source ~/.xbbcli/env.sh 后才生效)';
109
+ parts.push(`检测到环境变量 XBB_* 有值,已同步更新为该公司凭证(供外部系统使用,本 CLI 不读取)${suffix}`);
110
+ } catch (error) {
111
+ return createResult('partial', `已启用 corpid=${corpid},但同步更新环境变量失败:${String(error.message || error)}`, corpid, 'true', next.length);
112
+ }
113
+ }
114
+
115
+ return createResult('ok', parts.join(';'), corpid, 'true', next.length);
87
116
  }
88
117
 
89
118
  cli({
90
119
  site: 'xbb',
91
120
  name: 'token-use',
92
- description: '切换当前启用的公司(enable),保证仅且只有一个公司被启用',
121
+ description: '切换当前启用的公司(enable),保证仅且只有一个公司被启用;XBB_* 环境变量有值时同步更新为该公司凭证(供外部系统使用,本 CLI 不读取)',
93
122
  strategy: Strategy.PUBLIC,
94
123
  access: 'write',
95
124
  browser: false,
package/xbb-config.js CHANGED
@@ -7,7 +7,7 @@ const CONFIG_DIR = path.join(os.homedir(), '.xbbcli');
7
7
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.env');
8
8
  const ENV_FILE = path.join(CONFIG_DIR, 'env.sh');
9
9
 
10
- // config.env 字段名 -> 对应环境变量名(环境变量模式只支持单公司)
10
+ // config.env 字段名 -> 对应环境变量名;仅供写入、同步、清除与展示类命令检测使用,业务命令不再读取
11
11
  export const ENV_VAR_NAMES = Object.freeze({
12
12
  corpid: 'XBB_CORPID',
13
13
  token: 'XBB_TOKEN',
@@ -21,10 +21,6 @@ function trim(value) {
21
21
  return String(value ?? '').trim();
22
22
  }
23
23
 
24
- function isTruthy(value) {
25
- return ['1', 'true', 'yes', 'on'].includes(trim(value).toLowerCase());
26
- }
27
-
28
24
  // 读取环境中非空的 XBB_* 变量,返回与 config.env 单条配置同构的对象
29
25
  export function readEnvConfig() {
30
26
  const config = {};
@@ -39,12 +35,7 @@ export function hasEnvConfig(config = readEnvConfig()) {
39
35
  return Boolean(trim(config.corpid) || trim(config.token));
40
36
  }
41
37
 
42
- // XBB_ENV_ONLY=1 时强制只使用环境变量,忽略 config.env
43
- export function isEnvOnly() {
44
- return isTruthy(process.env.XBB_ENV_ONLY);
45
- }
46
-
47
- // 与旧版每个命令内联的 readConfig 行为保持一致:优先返回 enable=true 的公司
38
+ // 与旧版每个命令内联的 readConfig 行为保持一致:返回 enable=true 的公司
48
39
  function readFileConfig() {
49
40
  try {
50
41
  const parsed = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
@@ -62,25 +53,9 @@ function readFileConfig() {
62
53
  }
63
54
  }
64
55
 
65
- // 配置文件优先:有启用公司就用配置,否则回落到环境变量
56
+ // 业务命令统一从 config.env 读取 enable=true 的公司;XBB_* 环境变量不再参与读取
66
57
  export function readActiveConfig() {
67
- const envConfig = readEnvConfig();
68
- if (isEnvOnly()) {
69
- return envConfig;
70
- }
71
- const fileConfig = readFileConfig();
72
- if (Object.keys(fileConfig).length > 0) {
73
- return fileConfig;
74
- }
75
- return envConfig;
76
- }
77
-
78
- // 当前生效来源是否为环境变量
79
- export function isEnvActive() {
80
- if (isEnvOnly()) {
81
- return hasEnvConfig();
82
- }
83
- return Object.keys(readFileConfig()).length === 0 && hasEnvConfig();
58
+ return readFileConfig();
84
59
  }
85
60
 
86
61
  export function getEnvVarNames() {