dsh-cost-meter 1.8.11 → 1.8.12

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
@@ -8,9 +8,9 @@
8
8
 
9
9
  Per-conversation cost · daily totals · OpenCode Go subscription quota display · budget with usage percentage · official account balance · custom provider balance · balance progress bar · history · peak/off-peak pricing hours display (peak hours UTC 01:00–04:00, 06:00–10:00; weekends and Chinese public holidays are off-peak all day, with separate labels) · pre-switch popup & system-notification alerts for peak/off-peak changes (position / lead time / alert type configurable) · one-click price sync from the official docs · Codex-style token usage heat grid · multi-vendor model pricing (built-in 170+ model-ID catalog with auto-matching) · mainstream Coding Plan quota queries & display (Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow / CommandCode / SCNet / Volcano Ark / Qwen / Xiaomi MiMo) plan/API dual-track billing (subscription quota vs pay-as-you-go money separated, per-1% & full-window token/equivalent-cost estimates with daily/weekly/monthly curves) · · quota strip above the input box (budget / Go / coding-plan usage in one row, toggleable)
10
10
 
11
- [![version](https://img.shields.io/badge/version-1.8.11-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
11
+ [![version](https://img.shields.io/badge/version-1.8.12-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
12
12
 
13
- **v1.8.11** opens credential editors only on request and isolates them in their own forms. Includes sidebar-search recovery instructions for #232; the reporter's normal-browser trigger is awaiting confirmation. See the [release notes](docs/release-notes/v1.8.11.md).
13
+ **v1.8.12** adds a Bailian CLI console-login button with verified credentials, automatic quota refresh and concurrent-login cancellation. See the [release notes](docs/release-notes/v1.8.12.md).
14
14
 
15
15
  Desktop users: follow the [Desktop installation instructions](docs/install-troubleshooting.md#desktop-安装与更新) for the application's own CLI and `desktop` Profile.
16
16
 
@@ -320,22 +320,22 @@ On Node.js 20, use `npm install -g pnpm@10` instead. See [pnpm installation and
320
320
  dsh plugin --profile web add dsh-cost-meter
321
321
  ```
322
322
 
323
- **PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.8.11`** — review the script before running):
323
+ **PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.8.12`** — review the script before running):
324
324
 
325
325
  ```powershell
326
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.11/install.ps1 | iex
326
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.12/install.ps1 | iex
327
327
  ```
328
328
 
329
329
  **Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
330
330
 
331
331
  ```sh
332
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.11
332
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.12
333
333
  ```
334
334
 
335
335
  Without git, use the GitHub tag archive:
336
336
 
337
337
  ```sh
338
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.11.tar.gz
338
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.12.tar.gz
339
339
  ```
340
340
 
341
341
  After installing, **restart** `dsh web` (plugin rows, the Typert manifest and the client bundle are all scanned at startup):
package/README.zh-CN.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  本会话费用 · 当日费用 · OpenCode Go 订阅额度显示 · 预算与已用百分比 · 官方账户余额 · 自定义 Provider 余额查询(可配任意 HTTP 端点) · 余额三段进度条 · 历史记录 · 峰谷计价时段显示(UTC 01:00–04:00、06:00–10:00 为峰时段;周末与中国法定假日全天按谷价,分别标注) · 峰/谷切换前弹窗与系统通知提醒(位置/提前量/提醒类型可配) · 官方价格一键同步 · 类 Codex Token 用量热图 · 多厂商多模型价格计费(内置 170+ 模型 ID 价格目录与自动匹配) · 主流 Coding Plan 额度查询与显示(Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow / CommandCode / SCNet / 火山方舟 / 千问 / 小米 MiMo 十一家,含 Volcano Ark AK/SK 签名与 MiMo 控制台 Cookie 查询) · Plan/API 双轨计费(订阅额度与按量金额分离统计,每 1% 额度与满窗的 token/等值金额估算及日/周/月曲线) · 输入框上方额度横条(预算/Go/Coding Plan 用量一条横排显示,可开关)
10
10
 
11
- [![version](https://img.shields.io/badge/version-1.8.11-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
11
+ [![version](https://img.shields.io/badge/version-1.8.12-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
12
12
 
13
- **v1.8.11** 改为点击后才打开独立表单中的凭据编辑框,并提供 #232 的侧栏搜索恢复说明;报告者正常浏览器中的具体触发原因仍待确认。详见[更新说明](docs/release-notes/v1.8.11.md)。
13
+ **v1.8.12** 增加百炼 CLI 控制台登录按钮,登录后核验凭据并刷新额度,支持并发合并与切源取消。详见[更新说明](docs/release-notes/v1.8.12.md)。
14
14
 
15
15
  桌面端用户请按 [Desktop 安装说明](docs/install-troubleshooting.md#desktop-安装与更新),使用应用自带的 CLI 和 `desktop` Profile。
16
16
 
@@ -320,22 +320,22 @@ Node.js 20 请改用 `npm install -g pnpm@10`。版本要求见 [pnpm 官方安
320
320
  dsh plugin --profile web add dsh-cost-meter
321
321
  ```
322
322
 
323
- **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.8.11`**,建议先下载审阅再运行):
323
+ **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.8.12`**,建议先下载审阅再运行):
324
324
 
325
325
  ```powershell
326
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.11/install.ps1 | iex
326
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.12/install.ps1 | iex
327
327
  ```
328
328
 
329
329
  **或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
330
330
 
331
331
  ```sh
332
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.11
332
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.12
333
333
  ```
334
334
 
335
335
  没有 git 时可用 GitHub tag 打包直链:
336
336
 
337
337
  ```sh
338
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.11.tar.gz
338
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.12.tar.gz
339
339
  ```
340
340
 
341
341
  安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
@@ -0,0 +1,91 @@
1
+ # 千问 CLI 订阅额度(#146)
2
+
3
+ 在「设置 → 费用 → 额度 → 千问 Qwen Token Plan」展开卡片,启用后将「额度来源」切换为「官方 CLI」或「百炼 CLI」。默认仍为「本地估算」,已有配置无需迁移。
4
+
5
+ ## 准备与使用
6
+
7
+ 在**运行 DSH 的机器**上,以同一系统账号执行:
8
+
9
+ ```sh
10
+ npm install -g @qianwenai/qianwen-cli
11
+ qianwen auth login
12
+ qianwen usage summary --format json
13
+ ```
14
+
15
+ 安装后重启 DSH,使宿主读取新的 `PATH`。远程 DSH、容器或服务账号需要在对应环境安装和登录;只在本地浏览器所在机器登录不生效。插件使用 CLI 自己保存的登录态,不要求复制模型 API Key、Cookie 或管理令牌。
16
+
17
+ 选择 CLI 后可设置查询间隔(1–1440 分钟,默认 15),点击卡片刷新可立即重查。同一来源的并发刷新合并为一次查询;普通轮询复用缓存,失败也遵守查询间隔。关闭千问、关闭显示或切回本地估算后,不再启动 CLI;切换期间的旧结果不会覆盖新来源。
18
+
19
+ ## 显示口径
20
+
21
+ - 只读取 `usage summary --format json` 中的 `token_plan`:总 Credits、剩余 Credits、已用比例,以及可选加量包剩余量。已用量由总量减剩余量计算,小数 Credits 保留,标记 `(CLI)`。
22
+ - CLI 的 `resetDate` 在核验版本中来自订阅实例 `EndTime`,因此显示为「CLI 订阅到期」,不作为月度重置倒计时或采样边界。
23
+ - 官方快照包含该账号在其他工具中的用量;插件不会把这些总量写进 DSH 账本。CLI 没有可靠的当前额度周期边界,因此此模式不生成千问「每 1% / 满窗 Token」估算。
24
+ - 未登录、超时、异常 JSON 或无有效订阅额度时显示提示并清空本次额度。CLI 会把部分上游失败转换为 `subscribed: false`,插件不能据此断言账号没有订阅。可检查终端输出后刷新,或手动切回本地估算;不会自动用本地数值冒充官方数值。
25
+ - 本地估算的月度额度、起始日和抵扣率保留,切回后继续使用。`usage summary` 未提供可用主订阅额度、只有加量包时,当前显示无有效订阅额度提示。
26
+
27
+ ## 执行与验证
28
+
29
+ 插件执行固定命令,不经过 shell,不接受自定义命令参数。Windows 的 npm 安装通过包内 Node 入口运行,避免执行 `.cmd` 包装脚本。只搜索绝对 `PATH` 目录;单次执行最长 15 秒,stdout/stderr 各不超过 1 MiB,stdin 关闭,窗口隐藏。错误不会包含 CLI 原始输出,登录凭据仍由 CLI 管理。
30
+
31
+ 回归使用合成输出与真实子进程,覆盖小数和零额度、缺失字段、异常输出、登录失败、超时、并发、切源、关闭、持久化与 RPC codec;另有双语设置组件回归。未使用真实千问订阅账号验证额度。
32
+ ## 百炼 CLI(第三档额度来源)
33
+
34
+ 「百炼 CLI」档使用阿里云官方 [modelstudioai/cli](https://github.com/modelstudioai/cli)(npm 包 `bailian-cli`,命令 `bl` / `bailian`)查询百炼账号的 Token Plan 与 Coding Plan 订阅额度。在**运行 DSH 的机器**上以同一系统账号执行:
35
+
36
+ ```sh
37
+ npm install -g bailian-cli
38
+ bl auth login --console
39
+ bl usage token-plan --output json
40
+ bl usage coding-plan --output json
41
+ ```
42
+
43
+ 安装后重启 DSH 使宿主读取新的 `PATH`。这些额度子命令使用 CLI 的控制台登录;插件不复制凭据。
44
+
45
+ ### 从卡片登录(#235)
46
+
47
+ 额度查询失败后,插件会用 `bl auth status --output json --quiet` 检查控制台凭据。明确缺少控制台凭据或 CLI 返回认证失败(退出码 3)时,卡片按钮显示“登录”;仅有模型 API Key 或 AK/SK 不满足这两个额度命令的认证要求。检测本身失败时保留查询错误,不据此要求重新登录。
48
+
49
+ 点击“登录”才会执行固定的 `bl auth login --console`,并在 **DSH 主机**打开授权浏览器。完成后重新检查控制台凭据,再刷新额度。登录成功但未返回有效订阅额度时,界面分别说明这两个结果。远程、容器或无界面主机请在对应环境以同一系统账号手动登录;按钮不能在访问 Web 页面的另一台机器上配置主机凭据。
50
+
51
+ 登录最长等待 5 分钟,之后可重试。凭据仍由官方 CLI 保存;插件丢弃登录输出,错误只显示双语分类提示。并发登录合并为一次;切换来源、关闭显示或卸载插件时取消在途登录,旧结果不覆盖新配置。回归使用合成凭据与真实子进程,未执行真实账号浏览器授权。
52
+
53
+ - 一次查询并行执行 `usage token-plan` 与 `usage coding-plan` 两个只读子命令并合并:Token Plan 优先提供 5 小时 / 周窗口,Coding Plan 提供 5 小时 / 周 / 账单月三窗(`per5Hour` / `perWeek` / `perBillMonth`),另附「source」行标注实际应答的订阅与实例(如 `Coding Plan (pro) (CLI)`)。
54
+ - Token Plan 读取 `per5HourPercentage` / `per1WeekPercentage` 及对应的 `ResetTime`;Coding Plan 读取各窗口的 `percentage`(缺失时按 `usedQuota/totalQuota` 推算)。官方 CLI 返回比例值,乘以 100 后保留一位小数,例如 `0.5` 显示为 `50%`。重置时间以 epoch 毫秒转为 ISO。缺失的 Token Plan 窗口、无正额度上限的 Coding Plan 窗口不生成进度条。
55
+ - 单个子命令失败时以另一命令的结果作答;两者都失败时给出登录/失败提示,两者皆无有效额度时为软提示(无订阅不冒充错误)。与官方 CLI 档一致:不生成千问「每 1% / 满窗 Token」估算,账号总用量不写入 DSH 账本。
56
+ - 执行安全与 CLI 桥一致:固定参数、不经 shell、单命令 15 秒超时、stdout/stderr 各 1 MiB、窗口隐藏、错误不含子进程原始输出;Windows 下直接以 Node 运行包内 ESM 入口,不执行 npm 的 `.cmd` / `.ps1` 包装脚本。
57
+ - 查询间隔(1–1440 分钟)与并发合并同官方 CLI 档;回归见 `test/bailian-cli.mjs`(真实子进程、双命令合并、单边失败容错、超时/脱敏、缓存/并发/切源、账本三态与 codec),使用合成输出验证,未使用真实百炼订阅账号(`bl usage token-plan` 对未开通账号返回 `{}`)。
58
+
59
+ 参考:[官方 CLI 文档](https://platform.qianwenai.com/docs/api-reference/preparation/cli#usage-summary)、[核验源码的 Token Plan 映射](https://github.com/QianWen-AI/qianwen-cli/blob/bb7f7151494f0ffaee5ecb77d40b1f979beb7005/src/services/tokenplan-service.ts)、[输出类型](https://github.com/QianWen-AI/qianwen-cli/blob/bb7f7151494f0ffaee5ecb77d40b1f979beb7005/src/types/usage.ts)。核验日期:2026-09-15。
60
+
61
+ ## English
62
+
63
+ Expand **Settings → Cost → Quota → Qwen Token Plan**, enable it and select **Official CLI** as the quota source. Existing configurations keep **Local estimate**.
64
+
65
+ Install `@qianwenai/qianwen-cli` on the DSH host, run `qianwen auth login` as the same OS user, then verify `qianwen usage summary --format json`. Restart DSH after installation so it receives the updated PATH. Browser-side login alone does not configure a remote host or container.
66
+
67
+ The card reads the current `token_plan` credits, computes used credits from total minus remaining, preserves fractional credits, and labels the result `(CLI)`. Optional add-on credits appear separately. The verified CLI maps `resetDate` from subscription `EndTime`; it is shown as subscription expiry, never a monthly reset. No account-wide totals are imported into the ledger, and this mode does not produce Qwen per-1% / full-window token estimates without a reliable billing period.
68
+
69
+ Refreshes share one child process and use the configured 1–1440 minute cache (15 by default), including failed attempts. Manual refresh retries immediately. Disabling the source or switching to local estimates cancels pending work. Errors clear the quota and provide a setup/retry hint. Some upstream failures become `subscribed: false` inside the CLI, so that result is treated as unavailable data. Add-on-only results without a usable main subscription also show this hint. Select local estimates explicitly when needed; saved local settings remain available.
70
+
71
+ Execution uses fixed arguments without a shell, an absolute PATH entry, closed stdin, a hidden window, a 15-second timeout and 1 MiB limits for stdout/stderr. Windows npm installations run their Node entry directly. Credentials remain in the CLI's store; raw child output is never returned to the browser or saved in the ledger.
72
+
73
+ ## Bailian CLI (third quota source)
74
+
75
+ The **Bailian CLI** source reads Token Plan and Coding Plan subscription quota through the official [modelstudioai/cli](https://github.com/modelstudioai/cli) (npm package `bailian-cli`, commands `bl` / `bailian`). On the DSH host, as the same OS user: `npm install -g bailian-cli`, then `bl auth login --console`. Restart DSH afterwards to refresh PATH; credentials stay in the CLI.
76
+
77
+ ### Sign in from the card (#235)
78
+
79
+ After a failed quota query, the plugin checks `bl auth status --output json --quiet`. The button becomes **Login** only when the status confirms missing console credentials or the CLI reports an authentication failure (exit code 3). Model API keys or OpenAPI AK/SK alone cannot authenticate these quota commands. A failed status check remains a query error.
80
+
81
+ Clicking **Login** runs the fixed `bl auth login --console` command and opens the authorization browser on the **DSH host**. After completion, the plugin verifies console credentials and refreshes quota; successful login and unavailable subscription quota are reported separately. For a remote host, container or headless service, sign in manually in that environment as the same OS user. The button cannot configure host credentials on a different browser machine.
82
+
83
+ Login waits up to five minutes and can be retried. The official CLI owns credentials; the plugin discards login output and returns only classified bilingual errors. Concurrent logins share one process. Source changes, hidden quota display and plugin unload cancel pending login. Regression uses synthetic credentials and real child processes; live account browser authorization was not performed.
84
+
85
+ One query runs `usage token-plan` and `usage coding-plan` in parallel and merges them: Token Plan wins the shared 5-hour/week windows, Coding Plan contributes the 5-hour/week/billing-month windows, and a `source` row names the subscriptions that answered (e.g. `Coding Plan (pro) (CLI)`). Token Plan uses the flat `per5HourPercentage` / `per1WeekPercentage` fields and their reset times. Coding Plan uses nested `per5Hour` / `perWeek` / `perBillMonth` windows. CLI ratios are multiplied by 100 (0.5 becomes 50%); missing Coding Plan ratios are derived from used/total. Percentages retain one decimal, and reset times are epoch milliseconds. Missing Token Plan windows and Coding Plan windows without positive limits are skipped. When one subcommand fails the other answers; both failing shows the login/failure hint, and no valid quota is a soft no-subscription notice. Like the Official CLI source this mode creates no per-1%/full-window estimates and writes no account totals into the ledger. Execution uses the shared CLI bridge (fixed arguments, no shell, 15s timeout, 1 MiB caps, hidden window, no raw child output; Windows runs the package ESM entry directly via Node). Refresh interval and concurrency coalescing match the Official CLI source. Regression: `test/bailian-cli.mjs` with synthetic output and real child processes; no live Bailian subscription was used (`bl usage token-plan` returns `{}` for accounts without Token Plan).
86
+
87
+ Tests use synthetic output, real child processes and bilingual component callbacks. No live Qianwen subscription account was used.
88
+
89
+ Bailian source verification (2026-09-23): [Token Plan output](https://github.com/modelstudioai/cli/blob/72bc8fcce7f5ede6a5df81c1ce72d86de3f3ccd5/packages/commands/src/commands/usage/token-plan.ts), [Coding Plan output](https://github.com/modelstudioai/cli/blob/72bc8fcce7f5ede6a5df81c1ce72d86de3f3ccd5/packages/commands/src/commands/usage/coding-plan.ts), [ratio formatting](https://github.com/modelstudioai/cli/blob/72bc8fcce7f5ede6a5df81c1ce72d86de3f3ccd5/packages/commands/src/commands/usage/quota-box.ts). Tested with source-shaped fixtures and real local child processes; no live paid subscription was queried.
90
+
91
+ Login source verification (2026-10-05): [auth status](https://github.com/modelstudioai/cli/blob/8bbbbc722d70fb200641ef22b6f6d033aeae9f74/packages/commands/src/commands/auth/status.ts), [console login](https://github.com/modelstudioai/cli/blob/8bbbbc722d70fb200641ef22b6f6d033aeae9f74/packages/commands/src/commands/auth/login-console.ts), [exit codes](https://github.com/modelstudioai/cli/blob/8bbbbc722d70fb200641ef22b6f6d033aeae9f74/packages/core/src/errors/codes.ts).
@@ -3,10 +3,12 @@
3
3
  * Credentials remain owned by the CLI (bl auth login --console / --api-key).
4
4
  * Reads Token Plan + Coding Plan quota windows via two fixed read-only subcommands.
5
5
  */
6
- import { bridgeCode, resolveNpmCli, runCliJson } from './cli-bridge.js'
6
+ import { bridgeCode, resolveNpmCli, runCliJson, runCliCommand } from './cli-bridge.js'
7
7
 
8
8
  export const BAILIAN_TOKEN_PLAN_ARGS = Object.freeze(['usage', 'token-plan', '--output', 'json', '--quiet'])
9
9
  export const BAILIAN_CODING_PLAN_ARGS = Object.freeze(['usage', 'coding-plan', '--output', 'json', '--quiet'])
10
+ export const BAILIAN_AUTH_STATUS_ARGS = Object.freeze(['auth', 'status', '--output', 'json', '--quiet'])
11
+ export const BAILIAN_LOGIN_ARGS = Object.freeze(['auth', 'login', '--console'])
10
12
 
11
13
  const MESSAGES = {
12
14
  zh: {
@@ -15,6 +17,10 @@ const MESSAGES = {
15
17
  timeout: '百炼 CLI 查询超时,请检查网络后刷新。',
16
18
  invalid: '百炼 CLI 返回的额度格式无效,请更新 CLI 后重试。',
17
19
  unavailable: '百炼 CLI 未返回有效计划额度(可能未订阅 Token Plan / Coding Plan),请检查 CLI 或切换其他额度来源。',
20
+ auth: '百炼 CLI 未登录,请点击"登录"按钮或运行 bl auth login --console 完成授权。',
21
+ loginFailed: '百炼 CLI 登录失败,请在 DSH 主机运行 bl auth login --console 检查后重试。',
22
+ loginTimeout: '百炼 CLI 登录超时,请重新点击登录并在 DSH 主机的浏览器完成授权。',
23
+ loginInvalid: '百炼 CLI 登录状态格式无效,请更新 CLI 后重试。',
18
24
  },
19
25
  en: {
20
26
  missing: 'Bailian CLI not found. Install bailian-cli on the DSH host (npm install -g bailian-cli), run bl auth login --console, then restart DSH to refresh PATH.',
@@ -22,6 +28,10 @@ const MESSAGES = {
22
28
  timeout: 'Bailian CLI query timed out. Check the network and refresh.',
23
29
  invalid: 'Bailian CLI returned invalid quota data. Update the CLI and retry.',
24
30
  unavailable: 'Bailian CLI returned no valid plan quota (no Token Plan / Coding Plan subscription). Check the CLI or select another quota source.',
31
+ auth: 'Bailian CLI not authenticated. Click the "Login" button or run bl auth login --console to authorize.',
32
+ loginFailed: 'Bailian CLI login failed. Run bl auth login --console on the DSH host and retry.',
33
+ loginTimeout: 'Bailian CLI login timed out. Retry and complete authorization in the DSH host browser.',
34
+ loginInvalid: 'Bailian CLI returned invalid login status. Update the CLI and retry.',
25
35
  },
26
36
  }
27
37
  const message = (locale, code) => MESSAGES[locale === 'en' ? 'en' : 'zh'][code]
@@ -101,6 +111,52 @@ export function parseBailianUsage(payloads, locale = 'zh') {
101
111
  return { windows }
102
112
  }
103
113
 
114
+ /**
115
+ * 检查额度查询所需的 console 凭据,而非任意 API Key / AK/SK 登录态。
116
+ * @returns {Promise<{authenticated: boolean}>}
117
+ */
118
+ export async function checkBailianAuth(locale = 'zh', options = {}) {
119
+ const command = await resolveNpmCli({ packageName: 'bailian-cli', binName: 'bl', env: options.env, platform: options.platform })
120
+ if (!command) throw fail(locale, 'missing')
121
+ const { env = process.env, signal, timeoutMs = 5000, maxBuffer = 1024 * 1024 } = options
122
+ try {
123
+ const status = await runCliJson({
124
+ ...command,
125
+ args: [...command.args, ...BAILIAN_AUTH_STATUS_ARGS],
126
+ env, signal, timeoutMs, maxBuffer
127
+ })
128
+ signal?.throwIfAborted()
129
+ if (!record(status) || typeof status.authenticated !== 'boolean'
130
+ || (status.console != null && !record(status.console))) throw fail(locale, 'invalid')
131
+ return { authenticated: status.authenticated === true && record(status.console) }
132
+ } catch (error) {
133
+ signal?.throwIfAborted()
134
+ throw fail(locale, error.code === 'invalid' ? 'invalid' : bridgeCode(error))
135
+ }
136
+ }
137
+
138
+ /**
139
+ * 执行百炼 CLI 登录。
140
+ * @returns {Promise<{ok: boolean, message: string}>}
141
+ */
142
+ export async function loginBailianCli(locale = 'zh', options = {}) {
143
+ const command = await resolveNpmCli({ packageName: 'bailian-cli', binName: 'bl', env: options.env, platform: options.platform })
144
+ if (!command) throw fail(locale, 'missing')
145
+ const { env = process.env, signal, timeoutMs = 300000, maxBuffer = 1024 * 1024 } = options
146
+ try {
147
+ await runCliCommand({ ...command, args: [...command.args, ...BAILIAN_LOGIN_ARGS], env, signal, timeoutMs, maxBuffer })
148
+ signal?.throwIfAborted()
149
+ const status = await checkBailianAuth(locale, { ...options, timeoutMs: Math.min(timeoutMs, 5000) })
150
+ if (!status.authenticated) throw fail(locale, 'auth')
151
+ return { ok: true, message: '' }
152
+ } catch (error) {
153
+ signal?.throwIfAborted()
154
+ const kind = error.code ?? bridgeCode(error)
155
+ throw fail(locale, kind === 'auth' ? 'auth' : kind === 'timeout' ? 'loginTimeout'
156
+ : kind === 'invalid' ? 'loginInvalid' : 'loginFailed')
157
+ }
158
+ }
159
+
104
160
  export async function queryBailianCli(locale = 'zh', options = {}) {
105
161
  const command = await resolveNpmCli({ packageName: 'bailian-cli', binName: 'bl', env: options.env, platform: options.platform })
106
162
  if (!command) throw fail(locale, 'missing')
@@ -111,7 +167,7 @@ export async function queryBailianCli(locale = 'zh', options = {}) {
111
167
  if (settled.every(x => x.status === 'rejected')) {
112
168
  const errors = settled.map(x => x.reason)
113
169
  // Surface timeout first: it is the most actionable failure across both commands.
114
- throw fail(locale, bridgeCode(errors.find(e => e.kind === 'timeout') ?? errors[0]))
170
+ throw fail(locale, bridgeCode(errors.find(e => e.kind === 'timeout') ?? errors.find(e => e.kind === 'exit' && e.exitCode === 3) ?? errors[0], { authExitCode: 3 }))
115
171
  }
116
172
  try {
117
173
  return parseBailianUsage({
@@ -123,7 +179,7 @@ export async function queryBailianCli(locale = 'zh', options = {}) {
123
179
  // valid remains does the broken command's real failure become the message.
124
180
  if (error.code === 'unavailable') {
125
181
  const broken = settled.map(x => x.reason).find((_, i) => settled[i].status === 'rejected')
126
- if (broken) throw fail(locale, bridgeCode(broken))
182
+ if (broken) throw fail(locale, bridgeCode(broken, { authExitCode: 3 }))
127
183
  }
128
184
  throw error
129
185
  }
package/lib/cli-bridge.js CHANGED
@@ -38,10 +38,10 @@ export async function resolveNpmCli({ packageName, binName, env = process.env, p
38
38
  }
39
39
 
40
40
  /**
41
- * Run a fixed CLI command and parse stdout as JSON. Rejects only { kind, exitCode }:
41
+ * Run a fixed CLI command and consume stdout. Rejects only { kind, exitCode }:
42
42
  * 'maxbuffer' | 'timeout' | 'exit' | 'spawn' | 'json' — never raw output or paths.
43
43
  */
44
- export function runCliJson({ file, args, env = process.env, signal, timeoutMs = 15000, maxBuffer = 1024 * 1024 }) {
44
+ function runCli({ file, args, env = process.env, signal, timeoutMs = 15000, maxBuffer = 1024 * 1024 }, readOutput) {
45
45
  return new Promise((done, reject) => {
46
46
  const child = execFile(file, args, {
47
47
  encoding: 'utf8', windowsHide: true, shell: false, env, signal, timeout: timeoutMs, killSignal: 'SIGKILL', maxBuffer,
@@ -54,12 +54,16 @@ export function runCliJson({ file, args, env = process.env, signal, timeoutMs =
54
54
  reject(Object.assign(new Error('cli'), { kind, exitCode: typeof error.code === 'number' ? error.code : undefined }))
55
55
  return
56
56
  }
57
- try { done(JSON.parse(out)) } catch { reject(Object.assign(new Error('cli'), { kind: 'json' })) }
57
+ try { done(readOutput(out)) } catch { reject(Object.assign(new Error('cli'), { kind: 'json' })) }
58
58
  })
59
59
  child.stdin?.end()
60
60
  })
61
61
  }
62
62
 
63
+ export const runCliJson = options => runCli(options, JSON.parse)
64
+ /** Login output can contain private paths or credentials; discard it completely. */
65
+ export const runCliCommand = options => runCli(options, () => undefined)
66
+
63
67
  /** Map a bridge error kind to a per-CLI message code (exitCode may refine 'exit'). */
64
68
  export const bridgeCode = (error, { authExitCode } = {}) =>
65
69
  error.kind === 'timeout' ? 'timeout'