@xbongbong/xbbcli 2.4.2 → 2.4.3

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
@@ -162,7 +162,7 @@ xbbcli auth-logout # 登出:删除激活的那一家配置 +
162
162
  xbbcli auth-logout --env 0 # 只删 config.env,保留环境变量
163
163
  ```
164
164
 
165
- - `auth-status` 以 `key` / `value` 两列回显当前生效的配置值,固定 6 行:`corpid`、`corpName`、`userName`、`userId`、`baseurl`、`token`;不再输出 `status`、`source`、`message`、`enable`、`companyCount`、`envActive`、`configFile`、`code`、`msg` 这些元信息列。
165
+ - `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
166
 
167
167
  ```text
168
168
  key value
@@ -172,15 +172,17 @@ userName 陈会兵
172
172
  userId 02415643151585
173
173
  baseurl https://proapi.xbongbong.com
174
174
  token user_6***a7da
175
+ 验证 有效
175
176
  note 以上取值来自环境变量 XBB_*(config.env 中没有启用公司,回落到环境变量);环境变量模式不支持多公司切换
176
177
  ```
177
178
 
178
- - 来源(`config.env` 启用公司 还是 `XBB_*`)与冲突提醒只在需要时以 `note` 行追加:环境变量与 `config.env` 同时存在时提示「config.env 启用项优先」;`config.env` 里出现多个 `enable=true` 时提示用 `token-use` 修正。
179
- - 没有任何生效配置时,6 个值一律留空并给出 `note` 提示下一步命令(即使 `XBB_BASEURL` / `XBB_USERID` 单独残留也不会当成生效值回显)。配置文件路径、公司数量、生效的环境变量名改用 `--debug` 输出到 stderr。
179
+ - 来源(`config.env` 启用公司 还是 `XBB_*`)与冲突提醒只在需要时以 `note` 行追加:环境变量与 `config.env` 同时存在时提示「环境变量优先,`config.env` 中启用(`enable=true`)的公司被覆盖」并提示清除环境变量可切回配置文件;`config.env` 里出现多个 `enable=true` 时提示用 `token-use` 修正。
180
+ - `auth-status` 的 `验证` 行会实打实发起一次 `user-list --pageSize 1` 请求(等价于手工执行 `xbbcli user-list --pageSize 1`):接口成功(含 `NO_DATA` 空清单)→ `有效`;接口返回业务错误码或请求异常 → `无效-请重新配置token`。没有生效配置或缺少 token 时不再发起请求,直接给出 `无效-请重新配置token`;`--debug` 会输出判定结果与原因。
181
+ - 没有任何生效配置时,6 个配置值一律留空(`验证` 行为 `无效-请重新配置token`)并给出 `note` 提示下一步命令(即使 `XBB_BASEURL` / `XBB_USERID` 单独残留也不会当成生效值回显)。配置文件路径、公司数量、生效的环境变量名改用 `--debug` 输出到 stderr。
180
182
  - `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>`(它会保证剩余配置仍有且仅有一个启用)。
183
+ - `auth-logout` 只删除 `enable=true` 的那一条,**不会**把其他公司自动提升为启用:登出后本地可能一个启用公司都没有,需要 `xbbcli token-use --corpid <CORPID>` 或重新 `auth-login` / `token-set`。想保留其他公司并可切换时,用 `xbbcli token-del --corpid <CORPID>`(它会保证剩余配置仍有且仅有一个启用;默认还会删除 `XBB_*` 环境变量,传 `--env 0` 可保留)。
182
184
  - 删除前 `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` 时提示为「已强制只用环境变量」)。
185
+ - 默认**同时**清除 `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` 会提示「其优先级高于 config.env,业务命令仍会使用它,本次不算彻底登出」(`XBB_ENV_ONLY=1` 时提示为「已强制只用环境变量」)。
184
186
  - `config.env` 没有启用公司时:只有在环境变量确实存在时才只清环境变量(`config.env` 保持不动);两者都不存在则返回 `NO_ACTIVE_CONFIG`,不写任何文件。
185
187
  - 任何情况都不改动 `<corpid>.formlist.json` / `<corpid>.command-map.md` / `<corpid>.department-user.json` 三份缓存,重新启用该公司时无需再次拉取。
186
188
 
@@ -199,31 +201,33 @@ note 以上取值来自环境变量 XBB_*(config.env 中没有启用公
199
201
 
200
202
  读取优先级:
201
203
 
202
- 1. `config.env` 中存在 `enable=true` 的公司时,使用该配置(多公司切换行为不变)
203
- 2. `config.env` 缺失、解析失败或没有启用公司时,回落到上述环境变量
204
+ 1. 环境变量 `XBB_*` 中 `XBB_CORPID` / `XBB_TOKEN` 有值时,优先使用环境变量(`config.env` 中 `enable=true` 的公司被覆盖,命令会以环境变量为准)
205
+ 2. 环境变量不存在时,使用 `config.env` 中存在 `enable=true` 的公司(多公司切换行为不变)
204
206
  3. 设置 `XBB_ENV_ONLY=1` 可强制只使用环境变量(完全忽略 `config.env`)
205
207
 
208
+ > 注意:由于环境变量优先,早先 `--env 1` 写入的 `XBB_*` 会覆盖之后 `token-set --env 0` 写入的 `config.env`(`token-set` 的 `message` 会给出提示)。要切回 `config.env` 请先清除 `XBB_*` 环境变量,或执行 `xbbcli auth-logout`(默认会一并清除)。
209
+
206
210
  `token-set` / `auth-login` 通过 `--env` 控制存储方式:`--env 0`(默认)仅写入本地文件(`config.env` 与各类缓存),`--env 1` 仅写入环境变量(不写任何本地文件)。选 `--env 1` 时会把 `corpid`/`token`/`baseurl`/`userId` 写入环境变量:
207
211
 
208
212
  - Windows:用 `setx` 写入用户级环境变量,**只对新开的终端生效**
209
213
  - macOS/Linux:写入 `~/.xbbcli/env.sh`(`export` 形式,权限 600),需要 `source ~/.xbbcli/env.sh` 才在当前 shell 生效
210
214
 
211
- 环境变量模式只支持一家公司:此时 `token-list` 以 `source=env` 单行显示,`token-use` / `token-del` 会返回不支持多公司的错误行,并提示改用 `config.env`。
215
+ 环境变量模式只支持一家公司:此时 `token-list` 会把环境变量行(`source=env`)与 `config.env` 中的公司行(`source=config`)一起列出,并在 `msg` 列标注各自当前是否生效;`token-use` 会在切换 `config.env` 启用公司的同时,把该公司凭证同步写回 `XBB_*` 环境变量(目标公司缺 `token` 时拒绝切换,避免残留旧 `XBB_TOKEN`);`token-del` 默认在删除公司的同时一并删除 `XBB_*` 环境变量(`--env 0` 可保留),并在 `message` 说明删除后的实际生效来源。
212
216
 
213
- > 注意:`token-del` 不会清理已经写入的环境变量。要连环境变量一起登出,执行 `xbbcli auth-logout`(默认就会清除 `XBB_*`);也可以手工执行 `setx XBB_TOKEN ""`(其余 `XBB_*` 同理,Windows 需重开终端)或删除 `~/.xbbcli/env.sh` 并 `unset` 当前 shell 中已导出的变量。
217
+ > 注意:`token-del --env 0` 保留环境变量时,若 `XBB_*` 仍有效,其优先级高于 `config.env`,业务命令仍会使用它(`message` 会给出提示);要连环境变量一起清理,用 `token-del` 默认行为(不传 `--env`)或执行 `xbbcli auth-logout`(默认就会清除 `XBB_*`);也可以手工执行 `setx XBB_TOKEN ""`(其余 `XBB_*` 同理,Windows 需重开终端)或删除 `~/.xbbcli/env.sh` 并 `unset` 当前 shell 中已导出的变量。
214
218
 
215
219
  ## 当前支持的命令
216
220
 
217
221
  ### 配置
218
222
 
219
223
  - `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 输出配置文件路径、公司数量与生效的环境变量名
224
+ - `auth-status`:以 `key` / `value` 两列回显当前激活配置的 `corpid`、`corpName`、`userName`、`userId`、`baseurl`、`token`(token 默认中间掩码,`--showToken` 出明文),并追加 `验证` 行(调用一次 `user-list --pageSize 1` 实测 token,`有效` / `无效-请重新配置token`);来源与冲突提醒以 `note` 行追加,无生效配置时六个值留空并给出 `note`;`--debug` 在 stderr 输出配置文件路径、公司数量、生效的环境变量名与验证结果
221
225
  - `auth-logout`:删除 `config.env` 中 `enable=true` 的那一条配置,删除前整份备份为 `~/.xbbcli/config.env.bak`;不自动把其他公司提升为启用;默认同时清除 `XBB_*` 环境变量(`--env 0` 跳过,返回 `envStored` / `envFile`);既无启用公司又无环境变量时返回 `NO_ACTIVE_CONFIG` 且不改动文件
222
226
  - `auth-refresh-cache`:对当前启用公司重新拉取表单缓存、命令映射与部门/员工缓存,并回填 `corpName` / `userName`;失败时返回 `code=CACHE_REFRESH_FAILED`,`message` 注明中断步骤(`formlist` / `command-map` / `department-user` / `profile`),已完成的部分照常输出;支持 `--userId`、`--debug`
223
227
  - `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>`),并保证剩余配置中仅且只有一个公司被启用
228
+ - `token-list`:同时列出环境变量行(`source=env`,仅 `XBB_CORPID` / `XBB_TOKEN` 有值时出现)与 `config.env` 中保存的全部公司(`source=config`,含 `corpName` 公司名称与 `userName` 操作人姓名),`msg` 列标注各行当前状态(当前生效 / 被环境变量覆盖 / 被 `XBB_ENV_ONLY=1` 忽略);`--showToken` 显示完整 token(默认脱敏)
229
+ - `token-use`:切换当前启用的公司(`xbbcli token-use --corpid <CORPID>`),保证仅且只有一个公司被启用;若环境变量 `XBB_*` 有值,同时把该公司凭证(`corpid`/`token`/`baseurl`/`userId`/`corpName`/`userName`,缺 `baseurl` 时按 `corpid` 推导)同步写回环境变量——Windows 用 `setx`(需重开终端)、macOS/Linux 写 `~/.xbbcli/env.sh`(需重新 `source`);同步失败时返回 `status=partial`,目标公司缺 `token` 时返回错误且不改动任何配置
230
+ - `token-del`:删除指定公司的本地配置(`xbbcli token-del --corpid <CORPID>`),并保证剩余配置中仅且只有一个公司被启用;默认同时删除 `XBB_*` 环境变量(`--env 0` 保留,返回 `envStored` / `envFile` 两列)
227
231
  - `token-generate`:生成/获取个人 token,`--resetToken 0` 获取(默认)、`1` 刷新;`--checkUserId` 未传则用配置中的 `userId`
228
232
 
229
233
  ### 组织与人员
@@ -470,7 +474,7 @@ note 以上取值来自环境变量 XBB_*(config.env 中没有启用公
470
474
  - 用 `xbbcli auth-status` 查看当前生效的配置来自 `config.env` 还是环境变量;`xbbcli auth-logout` 删除激活配置并默认清除 `XBB_*` 环境变量(`--env 0` 保留)
471
475
  - 除 `token-set` 外,其余命令都会从 `~/.xbbcli/config.env` 读取 `corpid`
472
476
  - 大部分命令会从 `~/.xbbcli/config.env` 读取 `token`
473
- - 未配置 `config.env`(或其中没有启用公司)时,命令会回落到 `XBB_CORPID` / `XBB_TOKEN` / `XBB_BASEURL` / `XBB_USERID` / `XBB_CORPNAME` / `XBB_USERNAME` 环境变量;`XBB_ENV_ONLY=1` 可强制只用环境变量
477
+ - 环境变量 `XBB_*` 的优先级高于 `config.env`:`XBB_CORPID` / `XBB_TOKEN` 有值时,所有命令都以环境变量为准(`config.env` 中的启用公司被覆盖);两者都不存在时才没有任何生效配置。`XBB_ENV_ONLY=1` 可强制只用环境变量
474
478
  - 所有命令会从配置中读取 `userId` 并附加到请求 header 中
475
479
  - 大部分命令需要 formId 参数,可以根据业务名称或 businessType 从 `~/.xbbcli/<corpid>.formlist.json` 中获取 formId
476
480
  - 需要查询部门 id/名称,或员工 userId/所属部门时,可以先查 `~/.xbbcli/<corpid>.department-user.json`(由 `token-set` 分页抓取并缓存全部部门与员工)
package/auth-logout.js CHANGED
@@ -138,7 +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 已强制只用环境变量' : '业务命令会回落到它';
141
+ const forced = isEnvOnly() ? '且 XBB_ENV_ONLY=1 已强制只用环境变量' : '且其优先级高于 config.env,业务命令仍会使用它';
142
142
  parts.push(`注意:环境变量 XBB_*(corpid=${envBefore})仍然有效,${forced},本次不算彻底登出;如需一并清除请改用 --env 1(或不指定该参数,auth-logout 默认会清除环境变量)`);
143
143
  }
144
144
 
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, isEnvActive, isEnvOnly, 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,9 +50,35 @@ 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
82
  const envUsed = isEnvActive();
57
83
  const active = readActiveConfig();
58
84
  const companies = readCompanies();
@@ -67,31 +93,49 @@ async function authStatus(kwargs) {
67
93
  const token = trim(active.token);
68
94
  const rows = toRows({ ...active, token: showToken ? token : maskToken(token) });
69
95
 
96
+ // 没有生效配置或缺少 token 时无需发起调用,直接判定为无效
97
+ let verify;
98
+ if (source === 'none' || !token) {
99
+ verify = { valid: false, detail: source === 'none' ? '没有生效配置,无法验证' : '生效配置缺少 token,无法验证' };
100
+ } else {
101
+ verify = await verifyToken();
102
+ }
103
+ if (kwargs.debug) {
104
+ process.stderr.write(`[debug] Verify: ${verify.valid ? 'valid' : 'invalid'}(${verify.detail})\n`);
105
+ }
106
+ const verifiedRows = withVerify(rows, verify);
107
+
70
108
  if (source === 'none') {
71
109
  // 没有生效配置时不把残留的环境变量值当真值回显(XBB_BASEURL 等可能单独存在)
72
- return withNotes(toRows({}), [
110
+ return withNotes(verifiedRows, [
73
111
  '没有处于激活状态的配置:config.env 中不存在 enable=true 的公司,环境变量 XBB_* 也为空;请先执行 xbbcli auth-login 或 xbbcli token-set',
74
112
  ]);
75
113
  }
76
114
 
77
115
  if (source === 'env') {
78
- const reason = isEnvOnly() ? 'XBB_ENV_ONLY=1 已强制使用环境变量' : 'config.env 中没有启用公司,回落到环境变量';
79
- return withNotes(rows, [
80
- `以上取值来自环境变量 XBB_*(${reason});环境变量模式不支持多公司切换,配置文件路径与公司数量见 --debug`,
81
- ]);
116
+ const enabledCount = companies.filter((item) => item.enable === true).length;
117
+ const reason = isEnvOnly()
118
+ ? 'XBB_ENV_ONLY=1 已强制使用环境变量'
119
+ : enabledCount > 0
120
+ ? '环境变量优先于 config.env,其中 enable=true 的公司被覆盖'
121
+ : 'config.env 中没有启用公司,回落到环境变量';
122
+ const notes = [`以上取值来自环境变量 XBB_*(${reason});环境变量模式不支持多公司切换,配置文件路径与公司数量见 --debug`];
123
+ if (!isEnvOnly() && enabledCount > 0) {
124
+ notes.push('如需改用 config.env 中的启用公司,请先清除 XBB_* 环境变量(或执行 xbbcli auth-logout)');
125
+ }
126
+ return withNotes(verifiedRows, notes);
82
127
  }
83
128
 
84
129
  const notes = [];
85
- if (envActive) notes.push('环境变量 XBB_* 同时存在,但 config.env 启用项优先;如需彻底登出请执行 xbbcli auth-logout');
86
130
  const enabledCount = companies.filter((item) => item.enable === true).length;
87
131
  if (enabledCount > 1) notes.push(`config.env 中存在 ${enabledCount} 个 enable=true,仅第一条生效,请用 xbbcli token-use 修正`);
88
- return withNotes(rows, notes);
132
+ return withNotes(verifiedRows, notes);
89
133
  }
90
134
 
91
135
  cli({
92
136
  site: 'xbb',
93
137
  name: 'auth-status',
94
- description: '回显当前激活的配置',
138
+ description: '回显当前激活的配置,并验证 token 是否有效',
95
139
  strategy: Strategy.PUBLIC,
96
140
  access: 'read',
97
141
  browser: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xbongbong/xbbcli",
3
- "version": "2.4.2",
3
+ "version": "2.4.3",
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, isEnvOnly, 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,61 @@ 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 (isEnvOnly()) {
110
+ parts.push('注意:XBB_ENV_ONLY=1 仍然开启,环境变量已清空,业务命令将报 NO_ACTIVE_CONFIG;如需恢复请先写入凭证或去掉该变量');
111
+ } else if (next.length) {
112
+ parts.push(`此后业务命令使用 config.env 中启用的 corpid=${enabledCorpid}`);
113
+ } else {
114
+ parts.push('此后没有可用凭证,请重新执行 xbbcli token-set 或 xbbcli auth-login');
115
+ }
116
+ }
117
+ } catch (error) {
118
+ envStored = 'failed';
119
+ envFile = String(error.message || error);
120
+ return createResult('partial', `${parts.join(';')};删除环境变量失败:${envFile}`, corpid, next.length ? 'true' : '', next.length, envStored, envFile);
121
+ }
122
+ } else if (envPresent) {
123
+ const forced = isEnvOnly()
124
+ ? '且 XBB_ENV_ONLY=1 已强制只用环境变量(config.env 被忽略)'
125
+ : '且其优先级高于 config.env';
126
+ parts.push(`注意:环境变量 XBB_*(corpid=${envBefore || '未知'})仍然有效,${forced},业务命令仍会使用它;如需一并删除请改用 --env 1(或不指定该参数)`);
127
+ }
128
+
129
+ return createResult('ok', parts.join(';'), corpid, next.length ? 'true' : '', next.length, envStored, envFile);
91
130
  }
92
131
 
93
132
  cli({
94
133
  site: 'xbb',
95
134
  name: 'token-del',
96
- description: '删除指定的已保存公司配置,并保证剩余配置中仅且只有一个被启用',
135
+ description: '删除指定的已保存公司配置,并保证剩余配置中仅且只有一个被启用;默认同时删除 XBB_* 环境变量',
97
136
  strategy: Strategy.PUBLIC,
98
137
  access: 'write',
99
138
  browser: false,
100
139
  domain: 'proapi.xbongbong.com',
101
140
  args: [
102
141
  { name: 'corpid', type: 'str', help: '要删除的公司 id,必须是已保存的 corpid(必填)' },
142
+ { name: 'env', type: 'str', default: '1', help: '是否同时删除 XBB_* 环境变量:1 删除(默认),0 仅删除 config.env 保留环境变量。Windows 删除用户级变量,其他平台清理 ~/.xbbcli/env.sh' },
103
143
  ],
104
- columns: ['status', 'message', 'configFile', 'corpid', 'enable', 'companyCount'],
144
+ columns: ['status', 'message', 'configFile', 'corpid', 'enable', 'companyCount', 'envStored', 'envFile'],
105
145
  func: delCompany,
106
146
  });
package/token-list.js CHANGED
@@ -67,40 +67,59 @@ 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 [{
73
+ const envPresent = hasEnvConfig(envConfig);
74
+ const envOnly = isEnvOnly();
75
+ const rows = [];
76
+
77
+ // 环境变量行:XBB_CORPID / XBB_TOKEN 有值就回显,source=env(优先级高于 config.env)
78
+ if (envPresent) {
79
+ rows.push({
74
80
  enable: 'true',
75
81
  corpid: String(envConfig.corpid || ''),
76
82
  userId: String(envConfig.userId || ''),
77
83
  baseurl: String(envConfig.baseurl || ''),
78
- token: Boolean(kwargs.showToken) ? String(envConfig.token || '') : maskToken(envConfig.token),
84
+ token: showToken ? String(envConfig.token || '') : maskToken(envConfig.token),
79
85
  corpName: String(envConfig.corpName || ''),
80
86
  userName: String(envConfig.userName || ''),
81
87
  code: '',
82
- msg: '来源为环境变量 XBB_*(环境变量模式不支持多公司切换)',
88
+ msg: envOnly
89
+ ? '来源为环境变量 XBB_*(XBB_ENV_ONLY=1 已强制只用环境变量,config.env 被忽略)'
90
+ : '来源为环境变量 XBB_*(优先级高于 config.env,当前生效)',
83
91
  source: 'env',
84
- }];
92
+ });
85
93
  }
86
- if (!companies.length) {
94
+
95
+ const enabledCount = companies.filter((item) => item.enable === true).length;
96
+ for (const item of companies) {
97
+ const isEnabled = item.enable === true;
98
+ rows.push({
99
+ enable: isEnabled ? 'true' : 'false',
100
+ corpid: String(item.corpid || ''),
101
+ userId: String(item.userId || ''),
102
+ baseurl: String(item.baseurl || ''),
103
+ token: showToken ? String(item.token || '') : maskToken(item.token),
104
+ corpName: String(item.corpName || ''),
105
+ userName: String(item.userName || ''),
106
+ code: '',
107
+ msg: isEnabled
108
+ ? envOnly
109
+ ? '来源为 config.env,但 XBB_ENV_ONLY=1 已将其忽略,当前不生效'
110
+ : envPresent
111
+ ? '来源为 config.env,但已被环境变量 XBB_* 覆盖,当前不生效'
112
+ : '来源为 config.env,当前生效'
113
+ : '',
114
+ source: 'config',
115
+ });
116
+ }
117
+
118
+ if (!rows.length) {
87
119
  return makeErrorRow('NO_CONFIG', '缺少本地配置;请先执行 xbbcli token-set --corpid <CORPID> --token <TOKEN> --userId <USERID>');
88
120
  }
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) {
121
+
122
+ if (companies.length && enabledCount !== 1) {
104
123
  rows.push({
105
124
  enable: '',
106
125
  corpid: '',
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, isEnvOnly, getEnvVarNames, 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,8 +68,10 @@ 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, '', '');
71
+ const envPresent = hasEnvConfig();
72
+ // XBB_ENV_ONLY=1 且 XBB_* 全空:config.env 被强制忽略,写入也无法生效
73
+ if (isEnvOnly() && !envPresent) {
74
+ return createResult('error', `当前 XBB_ENV_ONLY=1 已强制只用环境变量,但 ${getEnvVarNames().join(', ')} 均为空;请先执行 xbbcli token-set --env 1 写入环境变量`, corpid, '', '');
72
75
  }
73
76
 
74
77
  const companies = readCompanies();
@@ -81,15 +84,45 @@ async function useCompany(kwargs) {
81
84
  return createResult('error', `未找到 corpid=${corpid} 的本地配置`, corpid, '', companies.length);
82
85
  }
83
86
 
87
+ const target = companies[index];
88
+ // 同步 XBB_* 需要完整凭证:缺 token 时直接拒绝,避免残留上一家公司的 XBB_TOKEN 被继续使用
89
+ if (envPresent && !String(target.token || '').trim()) {
90
+ return createResult('error', `corpid=${corpid} 的本地配置缺少 token,无法同步到 XBB_* 环境变量;请先执行 xbbcli token-set --corpid ${corpid} --token <TOKEN> --userId <USERID>`, corpid, '', companies.length);
91
+ }
92
+
84
93
  const next = enableOnly(companies, index);
85
94
  writeCompanies(next);
86
- return createResult('ok', `已启用 corpid=${corpid},其余公司已标记为 enable=false`, corpid, 'true', next.length);
95
+
96
+ const parts = [`已启用 corpid=${corpid},其余公司已标记为 enable=false`];
97
+
98
+ // 环境变量中有值时把生效公司同步写回 XBB_*,否则切换会被优先级更高的环境变量覆盖
99
+ if (envPresent) {
100
+ try {
101
+ persistEnvVars({
102
+ corpid: target.corpid,
103
+ token: target.token,
104
+ // 条目缺少 baseurl 时按 corpid 规则推导,避免残留上一家公司的 XBB_BASEURL
105
+ baseurl: target.baseurl || resolveBaseUrl(target.corpid),
106
+ userId: target.userId,
107
+ corpName: target.corpName,
108
+ userName: target.userName,
109
+ });
110
+ const suffix = process.platform === 'win32'
111
+ ? '(Windows 需重开终端后才生效)'
112
+ : '(重新 source ~/.xbbcli/env.sh 后才生效)';
113
+ parts.push(`检测到环境变量 XBB_* 有值,已同步更新为该公司凭证${suffix}`);
114
+ } catch (error) {
115
+ return createResult('partial', `已启用 corpid=${corpid},但同步更新环境变量失败:${String(error.message || error)}`, corpid, 'true', next.length);
116
+ }
117
+ }
118
+
119
+ return createResult('ok', parts.join(';'), corpid, 'true', next.length);
87
120
  }
88
121
 
89
122
  cli({
90
123
  site: 'xbb',
91
124
  name: 'token-use',
92
- description: '切换当前启用的公司(enable),保证仅且只有一个公司被启用',
125
+ description: '切换当前启用的公司(enable),保证仅且只有一个公司被启用;XBB_* 环境变量有值时同步更新为该公司凭证',
93
126
  strategy: Strategy.PUBLIC,
94
127
  access: 'write',
95
128
  browser: false,
package/xbb-config.js CHANGED
@@ -62,25 +62,21 @@ function readFileConfig() {
62
62
  }
63
63
  }
64
64
 
65
- // 配置文件优先:有启用公司就用配置,否则回落到环境变量
65
+ // 环境变量优先:XBB_* 中 corpid/token 非空时只用环境变量,否则用 config.env 里 enable=true 的公司
66
66
  export function readActiveConfig() {
67
67
  const envConfig = readEnvConfig();
68
68
  if (isEnvOnly()) {
69
69
  return envConfig;
70
70
  }
71
- const fileConfig = readFileConfig();
72
- if (Object.keys(fileConfig).length > 0) {
73
- return fileConfig;
71
+ if (hasEnvConfig(envConfig)) {
72
+ return envConfig;
74
73
  }
75
- return envConfig;
74
+ return readFileConfig();
76
75
  }
77
76
 
78
77
  // 当前生效来源是否为环境变量
79
78
  export function isEnvActive() {
80
- if (isEnvOnly()) {
81
- return hasEnvConfig();
82
- }
83
- return Object.keys(readFileConfig()).length === 0 && hasEnvConfig();
79
+ return hasEnvConfig();
84
80
  }
85
81
 
86
82
  export function getEnvVarNames() {
@@ -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 { getRegistry } from './xbb-registry.js';
5
- import { persistEnvVars } from './xbb-config.js';
5
+ import { persistEnvVars, hasEnvConfig } from './xbb-config.js';
6
6
 
7
7
  // token-set 与 auth-login 共用的凭证落盘逻辑:写 config.env、表单缓存、命令映射、部门/员工缓存、环境变量
8
8
  export const CONFIG_DIR = path.join(os.homedir(), '.xbbcli');
@@ -445,14 +445,19 @@ export async function saveCompanyCredentials(kwargs) {
445
445
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
446
446
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(companies, null, 2) + '\n', 'utf8');
447
447
 
448
+ // 环境变量优先级高于 config.env:此时只写文件不会改变业务命令实际使用的凭证,需在结果里提示
449
+ const envShadow = hasEnvConfig()
450
+ ? ';注意:检测到 XBB_* 环境变量,其优先级高于 config.env,业务命令仍会使用环境变量;如需改用本次保存的配置,请清除 XBB_* 环境变量或执行 xbbcli auth-logout'
451
+ : '';
452
+
448
453
  try {
449
454
  const cache = await refreshLocalCaches(corpid, userId);
450
455
  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', '');
456
+ return createResult('ok', `已保存 corpid、token、baseurl、userId,并同步表单模板缓存、命令映射文件与部门/员工缓存${envShadow}`, corpid, baseurl, userId, formlistFile, commandMapFile, true, companies.length, departmentUserFile, corpName, userName, 'skipped', '');
452
457
  } catch (error) {
453
458
  return createResult(
454
459
  'partial',
455
- `已保存 corpid、token、baseurl、userId,但同步缓存失败${error.step ? `(步骤 ${error.step})` : ''}:${error.message}`,
460
+ `已保存 corpid、token、baseurl、userId,但同步缓存失败${error.step ? `(步骤 ${error.step})` : ''}:${error.message}${envShadow}`,
456
461
  corpid,
457
462
  baseurl,
458
463
  userId,