@mars-sea/dsh-commandcode-provider 0.10.2 → 0.10.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/CHANGELOG.md +41 -1
- package/README.md +44 -2
- package/README.zh-CN.md +44 -2
- package/lib/client.js +132 -35
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +217 -24
- package/lib/index.js +502 -48
- package/lib/index.js.map +1 -1
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,47 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [0.10.4] - 2026-09-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **A Command Code page in the dsh-TUI settings screen** ([#28](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/28)). The terminal front door now lists Command Code under `/settings`, with the API key, the API base, the out-of-plan model filter, the model allowlist, the active account, and the command language. The key is a write-only credential field: the literal goes to the credential store and never into a settings document, and the field shows only whether one is configured. Until now a TUI-only user had no way to enter a key at all — dsh-TUI's own `/provider` wizard manages its `llm-pi-ai` routes exclusively, and the web Models page was the only surface that could write it. The page appears only on a profile that mounts dsh-TUI; everywhere else the plugin stays a plain LLM-provider bundle. Every model list and account choice reads live settings, so a change lands on the next request with no restart.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Per-release compatibility now covers dsh `0.1.5-rc.1`.** `dsh.compatibility.dshReleases` declares it `compatible` alongside `0.1.2-rc.1`, `0.1.3-alpha.1`, `0.1.3-alpha.2` and `0.1.5-alpha.1`, so the DSH STORE can list the plugin for the current Harness release. Verified on both surfaces: the web profile runs the plugin on this engine (settings page, Models-page card, chat and the `/commandcode` dashboard), and a dsh-TUI 0.10.0 profile selects and serves Command Code there. An older release keeps its own record rather than being replaced, so a user who has not moved to the newest engine still sees the listing.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
- **A literal API key in the composition config no longer rides a settings response.** The `apiKey` fields (top-level and per extra account) are now declared `role('secret')`, so the harness strips them from every settings descriptor it serves. The settings page was already writing keys through the credentials seam — which is what keeps them out of a settings document — but a key written by hand into `cordis.patch.yml` or `settings.yaml` was returned to the browser verbatim, including on a remote-Host setup where that means another machine.
|
|
17
|
+
- **A web page can no longer cancel a login in progress.** The loopback callback's denial branch now checks the login's state token before it ends the attempt, matching the official CLI. A denial is terminal, and the browser sends a `text/plain` POST as a CORS simple request regardless of the origin allowlist, so any open page could previously kill the attempt (and show "authorization denied") by blindly posting `{"error":"access_denied"}` to the loopback port. A denial that carries the real token still ends the attempt, as the Studio's own flow does.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- **Cancelling a login now really cancels it.** The key delivered by the callback is validated against `/alpha/whoami` before it is stored, and cancelling during that check was ignored: the credential was written anyway and the state flipped back to success while the page (which had already stopped polling) kept showing "cancelled". The completion now re-checks that it still owns the attempt — before writing the key and before publishing any status — so a cancel or a newly started attempt wins.
|
|
21
|
+
- **"Sign in" after a completed callback starts a fresh attempt.** Once the callback is consumed the loopback server is closed, but the status still read `waiting`, so the next sign-in handed back the previous attempt's URL — a link to a dead port. A `waiting` status whose server is gone is now retired instead of reused.
|
|
22
|
+
- **Saving the settings page no longer deletes account entries it cannot show.** The accounts list is written whole, and the page rebuilt it from the rows it displays — so a composition-config entry carrying a literal `apiKey` (which has no row) was silently deleted by any save, and the literal key of the entries it could show was stripped. The write now starts from the stored list and only rewrites the rows the page manages, so unknown entries and literal keys survive.
|
|
23
|
+
- **A failed save no longer throws away what you typed.** Two staged edits were cleared even when the failing write never ran: an account's label reverted to the generated name (and was then persisted that way on the retry), and a routing-rule edit was silently reverted with the page left clean — no dirty marker and no way to retry. Both are now kept until the stored section proves the write landed. Rule drafts are still dropped once the rules write itself lands, because the positional row ids shift and a kept draft would be applied to the wrong row.
|
|
24
|
+
- **The "clean up stale models" button no longer empties the allowlist while the catalog is missing.** Before the first catalog fetch lands — and after a failure — the catalog is empty, so every selected model looked retired and one click cleared the whole list. The cleanup, and its hint, are now shown only when a catalog is actually in hand; the explicit "show all" action remains available.
|
|
25
|
+
- **Whitespace around a stored API key no longer breaks multi-account rotation.** The account pool marked a rejected key exactly as it was stored, while the request path trims it (a key from the credentials seam, a `.env` line, or a shell export picks up surrounding whitespace), so a 429/401 mark landed on a string nothing looked up again: the same account was offered again, the account card showed no mark, and the usage endpoints failed while chat kept working. Keys are now normalized once, where a slot's key enters the pool.
|
|
26
|
+
- **Images returned by a tool are no longer invisible to Vision models** ([#30](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/30)). When the agent read a local image with `read_image` (an absolute path in the prompt) instead of receiving it as an attachment or pasted image, the request replayed only the tool's metadata text — the nested `image` block inside the tool result was flattened away, so the model answered that it could see no pixels. Both transports now carry the image's bytes in a user message right after the tool message, introduced by an `Attached image(s) from tool result:` note: the image stays outside the tool result, where neither `/alpha/generate`'s text-only tool output nor Chat Completions' `role: 'tool'` content could hold it. A tool result that repeats one attachment sends it once, an image-only result no longer produces an empty tool message, and text-only tool results are byte-for-byte unchanged.
|
|
27
|
+
- **Test coverage for the above**, plus the previously unpinned paths they exposed: account writes preserving composition entries, label/rule drafts across every failure order, the login callback's authentication and attempt ownership, credential normalization in the pool, and the catalog-readiness gate (each verified by reverting its fix and watching the new test fail).
|
|
28
|
+
|
|
29
|
+
## [0.10.3] - 2026-09-10
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **Synced with the official command-code@1.53.0 CLI** (2026-09-10: "Add new deepseek/deepseek-v4.1-flash model"). `COMMAND_CODE_CLI_VERSION` is now `1.53.0`. The only catalog change is the new DeepSeek V4.1 Flash (Go-tier, Vision, 1M context, selectable `low` / `high` / `max` reasoning efforts, same hourly schedule as the other DeepSeek models: $0.15/$0.60 off-peak, $0.30/$1.20 peak, 01–04 & 06–10 UTC Mon–Fri). Wire protocol, endpoints, subscription plan maps, every other plan tier, and every deal are unchanged; `gpt-6-astra` remains a CLI/pricing/docs-only model (absent from the public Provider catalog, as before) and was deliberately kept.
|
|
34
|
+
- **DeepSeek V4.1 Flash is now selectable** — a Go-tier Vision model (1M context) with the `Peak`/`Half` hourly marker and the `Image` badge in the picker.
|
|
35
|
+
- **Synced with the official command-code@1.52.0 CLI** (2026-09-10). `COMMAND_CODE_CLI_VERSION` is now `1.52.0`. Upstream moved 1.51.3 → 1.52.0 the same day; the CLI changelog page still stops at 1.51.0, so the snapshot was read from the bundled model table. The only catalog change is the new free `inclusionai/ling-3.0-flash-sante:free` (Ling 3.0 Flash Sante, 262K context, text-only, automatic reasoning, 100 requests/day). Wire protocol, endpoints, effort map, subscription plan maps, peak/off-peak membership and schedule, and every other deal are unchanged; `gpt-6-astra` remains a CLI/pricing/docs-only model (absent from the public Provider catalog, as before) and was deliberately kept.
|
|
36
|
+
- **Ling 3.0 Flash Sante is now selectable** — a free Go-tier model on every plan (262K context, text-only, reasons automatically), shown with the `FREE` badge at the top of the picker. It succeeds the retired `inclusionai/ling-3.0-flash-free` promo.
|
|
37
|
+
- **Synced with the official command-code@1.51.3 CLI** (2026-09-09). `COMMAND_CODE_CLI_VERSION` is now `1.51.3`. Upstream moved 1.50.0 → 1.50.1 (CLI-local changes only: stable process title, UNIX-socket idle/working/blocked status) → 1.51.0 (briefly added DeepSeek V4.1 Flash Beta behind a 2026-09-10 expiry gate) → 1.51.2 (removed the beta model from the bundle entirely, so no snapshot entry is needed) → 1.51.3 (see below). Re-verified against the official sources: wire protocol (both the `/alpha/generate` CLI transport and the `/provider/v1/chat/completions` Provider transport), endpoints, auth flow, model catalog, plan tiers, subscription plan maps, and deals are all identical to 1.50.0; the peak/off-peak *hours* are unchanged, but the same page now also restricts them to weekdays — see Fixed. There is no CLI changelog entry for 1.51.1–1.51.3 yet, so the 1.51.3 snapshot was read from the bundled model table.
|
|
38
|
+
- **MiniMax M3 now offers selectable reasoning effort** (`low` / `medium` / `high`). The official 1.51.3 bundle gave the model effort levels where it previously let Command Code choose the depth automatically, so the picker's effort selector now lists them (its hidden free sibling gained the same set in the bundle).
|
|
39
|
+
- **The "Visible models" card is now "Model allowlist".** The label and hint on the settings page (zh/en) and in both READMEs say what the card does — checked models are the only ones the picker lists, and checking nothing shows everything — instead of relying on the word "visible". The stored `visibleModels` config key is unchanged, so existing allowlists keep working with no migration.
|
|
40
|
+
- **Per-release compatibility now covers dsh `0.1.5-alpha.1`.** `dsh.compatibility.dshReleases` declares it `compatible` alongside `0.1.2-rc.1`, `0.1.3-alpha.1` and `0.1.3-alpha.2`, so the DSH STORE can list the plugin for the current Harness release.
|
|
41
|
+
- **The build config uses the current tsdown option shape** (`outExtensions` and `deps.neverBundle` instead of the removed `outExtension` / `external`). The external set is byte-for-byte the same list as before, so the published `lib/` output is unchanged in behavior; the change only silences the deprecated-option path on current tsdown.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **The usage report no longer prints a raw success-rate float.** `/commandcode` and the settings page's Account usage card rendered the upstream value verbatim, so a rate of `99.965552876334` showed up as a wall of digits. Both the Host command and the browser card now round to at most two decimals and trim trailing zeros — `99.97`, `100`, `97.6` — through one shared formatting rule on each side.
|
|
46
|
+
- **DeepSeek peak/off-peak pricing now follows the official weekday rule.** Peak rates (01:00–04:00 and 06:00–10:00 UTC, full price) apply **Monday to Friday only** — Saturday and Sunday are charged completely off-peak for all 24 hours. The picker previously derived `Peak`/`Half` from the UTC hour alone, so it showed `Peak` on weekend mornings when the model actually billed at half price. Peak hours are now 35 per week (7 per weekday, 0 on weekends).
|
|
47
|
+
- **DeepSeek V4 Flash Fast no longer shows a bogus `Peak`/`Half` marker.** Its official pricing row is flat ($0.28/$0.56/$0.07) with no `timeOfDay` block, so it is not billed by the hour; the picker now shows just `Go · 1M`. The hourly-priced set is DeepSeek V4 Pro, V4 Flash, V4 Flash Vision (exp), and — from command-code@1.53.0 — V4.1 Flash. The upstream snapshot procedure was tightened to read the pricing page's embedded model JSON instead of the rendered rows, whose annotations sit one row off when flattened to text.
|
|
8
48
|
|
|
9
49
|
## [0.10.2] - 2026-09-08
|
|
10
50
|
|
package/README.md
CHANGED
|
@@ -18,12 +18,13 @@ Unofficial [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harnes
|
|
|
18
18
|
|
|
19
19
|
- **Plugin bundle** — install into any dsh profile with `dsh plugin add`; registers a `commandcode` provider route with a live model catalog.
|
|
20
20
|
- **Dedicated settings page** — API key, connection options, a live account-usage card, and a "Hide out-of-plan models" toggle.
|
|
21
|
+
- **Works in the terminal too** — the same install serves a [dsh-TUI](#terminal-ui-dsh-tui) profile, with its own **`/settings` → Command Code** page for the API key and the model controls.
|
|
21
22
|
- **Models-page key card** — the **Settings → Models → Command Code** card carries the key status, a paste field, and the sign-in button inline.
|
|
22
23
|
- **In-browser sign-in for keys** — start the official authorization flow (the same one `cmd login` runs) from the settings page; the approved key lands in the local credential service automatically. Manual paste remains the fallback.
|
|
23
24
|
- **Multi-account rotation** — when one account hits its usage limit, requests switch to the next account automatically. See [Account rotation](#account-rotation).
|
|
24
25
|
- **Flexible API key setup** — via the settings page, an environment variable, or the official CLI login file.
|
|
25
26
|
- **Model-picker annotations** — minimum plan, active deal or `FREE` badge, peak/off-peak state, image support, and context window; free models listed first.
|
|
26
|
-
- **Plan-aware picker** — models above your subscription tier are hidden by default (toggleable); an optional **
|
|
27
|
+
- **Plan-aware picker** — models above your subscription tier are hidden by default (toggleable); an optional **Model allowlist** keeps only your favorites in the picker.
|
|
27
28
|
- **Reasoning-effort support** — models with selectable reasoning effort levels expose them in the picker.
|
|
28
29
|
- **Image input** — Vision-capable models accept images.
|
|
29
30
|
- **Web search** — the dsh `web_search` tool is backed by the Command Code Provider API (`/alpha/web-search`) with the same key/endpoint as chat, so no separate search key or base URL is needed. See [Web search](#web-search).
|
|
@@ -78,6 +79,47 @@ Or skip the CLI: click **Sign in to Command Code** under **Settings → Command
|
|
|
78
79
|
|
|
79
80
|
After restart, enter your API key in **Settings → Command Code** and save; **Settings → Models** shows a **Command Code** card, and the model picker lists the live catalog under **commandcode**. Send a message with a model your plan includes.
|
|
80
81
|
|
|
82
|
+
## Terminal UI (dsh-TUI)
|
|
83
|
+
|
|
84
|
+
The plugin also works under a terminal front door — no separate install and no extra configuration:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Then pick the provider in the model selector, or name it directly:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
/model commandcode/deepseek/deepseek-v4.1-flash
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`/model` lists every registered provider, with Command Code's live catalog and its plan/deal/context annotations. The `/commandcode` usage dashboard works there too.
|
|
97
|
+
|
|
98
|
+
**Entering the key.** The TUI has no web Models page, so the plugin declares its own page in the TUI settings screen — **`/settings` → Command Code** — with the API key, the API base, the out-of-plan model filter, the model allowlist, the active account, and the command language. The key field is write-only: it shows whether a key is configured and writes what you type to the credential store, never to a settings document. The same page is the only place a TUI-only user needs to visit.
|
|
99
|
+
|
|
100
|
+
Alternatively, set the key outside the TUI — any of these work, in this order of precedence:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
export COMMANDCODE_API_KEY="user_..." # launching environment
|
|
104
|
+
cmd login # writes ~/.commandcode/auth.json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Making Command Code the default.** dsh-TUI pins its own agent route, and its `agent-default-model` setting does not override it. To start every session on Command Code, override the `agent-loop` row in your profile patch (`$DSH_HOME/profiles/dsh-tui/cordis.patch.yml`):
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
- id: agent-loop
|
|
111
|
+
inject: [tuiStartup]
|
|
112
|
+
config:
|
|
113
|
+
agents:
|
|
114
|
+
- id: main
|
|
115
|
+
provider: commandcode
|
|
116
|
+
model: deepseek/deepseek-v4.1-flash
|
|
117
|
+
reasoningEffort: max
|
|
118
|
+
cwd: !!js process.cwd()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Engine version.** The plugin needs a dsh engine that exports `ToolCallId` from `@deepseek-ai/dsh-llm` — **dsh 0.1.2-alpha.3 or later**. That includes the engine dsh-TUI recommends (0.1.2-rc.1) and every later release, but *not* the oldest engines its peer range nominally allows: on dsh 0.1.0-rc.6 or 0.1.1-rc.2 the plugin's module import fails and the TUI will not start. Upgrade the engine, or use the web profile.
|
|
122
|
+
|
|
81
123
|
## Usage dashboard
|
|
82
124
|
|
|
83
125
|
The plugin registers a `/commandcode` slash command showing per-account usage:
|
|
@@ -95,7 +137,7 @@ With several Command Code subscriptions, the plugin **switches to the next accou
|
|
|
95
137
|
- **Setup** — use the **Account rotation** card at Settings → **Command Code** to add accounts with a label and API key; the top-level key always serves first as the `default` account.
|
|
96
138
|
- **Manual switching** — the **Active account** dropdown pins a preferred account; if it is exhausted, requests fall back to other accounts and return once its window resets.
|
|
97
139
|
- **Route models to accounts** — the **Route models to accounts** card picks catalog models (multi-select, fetched from the live catalog) and routes them to an account. A request whose model is in a rule serves from that account while it is usable; an exhausted or invalid routed account falls back to the normal rotation. Rules match in list order — the first hit wins.
|
|
98
|
-
- **Show only favorite models** — the **
|
|
140
|
+
- **Show only favorite models** — the **Model allowlist** card keeps only the checked models in the model picker; unchecked shows all (the default).
|
|
99
141
|
- **Status** — the **Account usage** card and `/commandcode` report per-account state.
|
|
100
142
|
|
|
101
143
|
The equivalent YAML (`$DSH_HOME/settings.yaml` or composition config):
|
package/README.zh-CN.md
CHANGED
|
@@ -18,12 +18,13 @@
|
|
|
18
18
|
|
|
19
19
|
- **插件包**:一条 `dsh plugin add` 命令安装到任意 dsh 配置,注册 `commandcode` provider 路由,带实时模型目录。
|
|
20
20
|
- **专属设置页**:API key 输入、连接参数、实时「账户用量」卡片和「隐藏套餐外模型」开关。
|
|
21
|
+
- **终端界面同样可用**:同一次安装即可服务 [dsh-TUI](#终端界面dsh-tui) 配置,终端里有自己的 **`/settings` → Command Code** 页面来填 key 和调模型。
|
|
21
22
|
- **Models 页快捷卡片**:**设置 → Models → Command Code** 卡片内直接显示 key 状态、粘贴输入框和登录按钮。
|
|
22
23
|
- **浏览器内登录获取 key**:设置页一键发起官方授权(与 `cmd login` 同一流程),完成后密钥自动写入本机凭据服务,无需手动创建或粘贴;不可用时随时退回手动粘贴。
|
|
23
24
|
- **多账户轮换**:一个账户用量打满后,请求自动切换到下一个账户。详见[多账户轮换](#多账户轮换)。
|
|
24
25
|
- **key 配置灵活**:设置页填写、环境变量或官方 CLI 登录文件均可。
|
|
25
26
|
- **模型选择器标注**:每个模型标注最低套餐、折扣/FREE 徽章、峰谷时段、图片支持与上下文长度,免费模型置顶。
|
|
26
|
-
-
|
|
27
|
+
- **按套餐过滤**:默认隐藏超出订阅套餐的模型,可一键关闭;「模型白名单」可进一步只保留常用模型。
|
|
27
28
|
- **推理强度支持**:支持推理强度的模型可在选择器中选择档位。
|
|
28
29
|
- **图片输入**:Vision 模型支持发送图片。
|
|
29
30
|
- **联网搜索**:dsh 的 `web_search` 工具由 Command Code Provider API(`/alpha/web-search`)承载,复用聊天同一个 key 与端点,无需单独配置搜索 key 或地址。详见[联网搜索](#联网搜索)。
|
|
@@ -76,6 +77,47 @@ cmd login # macOS/Linux;Windows 原生版:cmdc login
|
|
|
76
77
|
|
|
77
78
|
重启后,在 **设置 → Command Code** 填入 API key 并保存;**设置 → Models** 出现 **Command Code** 卡片,模型选择器在 **commandcode** 下列出实时目录。选择套餐内包含的模型发送消息即可。
|
|
78
79
|
|
|
80
|
+
## 终端界面(dsh-TUI)
|
|
81
|
+
|
|
82
|
+
插件同样支持终端前端,无需单独安装、无需额外配置:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
dsh plugin --profile dsh-tui add @mars-sea/dsh-commandcode-provider
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
之后在模型选择器里选,或者直接指定:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
/model commandcode/deepseek/deepseek-v4.1-flash
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`/model` 会列出所有已注册的 provider,Command Code 的实时目录连同套餐/优惠/上下文标注一起出现,`/commandcode` 用量面板在终端里同样可用。
|
|
95
|
+
|
|
96
|
+
**填写 API key。** 终端没有网页版 Models 页面,所以插件会在终端设置页里声明自己的页面 —— **`/settings` → Command Code** —— 包含 API key、API 地址、隐藏套餐外模型、模型白名单、当前账号和命令语言。key 字段是只写的:它只显示"是否已配置",输入的内容写进凭据库,不会写进任何 settings 文档。只用终端的用户只需要访问这一个页面。
|
|
97
|
+
|
|
98
|
+
也可以在终端外配置 key,以下三种方式按优先级生效:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
export COMMANDCODE_API_KEY="user_..." # 启动环境变量
|
|
102
|
+
cmd login # 写入 ~/.commandcode/auth.json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**把 Command Code 设为默认模型。** dsh-TUI 写死了自己的 agent 路由,它的 `agent-default-model` 设置不会覆盖它。要让每个会话默认走 Command Code,请在自己的 profile patch(`$DSH_HOME/profiles/dsh-tui/cordis.patch.yml`)里覆盖 `agent-loop` 行:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
- id: agent-loop
|
|
109
|
+
inject: [tuiStartup]
|
|
110
|
+
config:
|
|
111
|
+
agents:
|
|
112
|
+
- id: main
|
|
113
|
+
provider: commandcode
|
|
114
|
+
model: deepseek/deepseek-v4.1-flash
|
|
115
|
+
reasoningEffort: max
|
|
116
|
+
cwd: !!js process.cwd()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**引擎版本要求。** 插件需要导出了 `ToolCallId` 的 dsh 引擎,即 **dsh 0.1.2-alpha.3 或更高**。这覆盖了 dsh-TUI 推荐的引擎(0.1.2-rc.1)以及之后的所有版本,但不包含它 peer 范围里名义上允许的最老引擎:在 dsh 0.1.0-rc.6 / 0.1.1-rc.2 上,插件的模块导入会失败,TUI 无法启动。请升级引擎,或改用 web profile。
|
|
120
|
+
|
|
79
121
|
## 用量面板
|
|
80
122
|
|
|
81
123
|
插件注册了 `/commandcode` 斜杠命令,显示各账户的用量状态:
|
|
@@ -93,7 +135,7 @@ cmd login # macOS/Linux;Windows 原生版:cmdc login
|
|
|
93
135
|
- **配置**:在 **设置 → Command Code** 的「多账户轮换」卡片添加账户并填写备注名与 API key;顶层 key 始终是第一顺位的 `default` 账户。
|
|
94
136
|
- **手动切换**:卡片上的「当前使用账户」下拉框可指定优先账户;所选账户耗尽时自动回落到其他账户,窗口重置后自动恢复。
|
|
95
137
|
- **按模型切换账户**:在「按模型切换账户」卡片从实时模型目录**多选**模型并固定到某个账户。请求的模型在规则列表中且该账户可用时使用该账户;账户耗尽或密钥失效时自动回落到常规轮换。规则按列表顺序匹配,第一条命中生效。
|
|
96
|
-
-
|
|
138
|
+
- **只显示常用模型**:在「模型白名单」卡片勾选要保留的模型,模型选择器只列出这些;不勾选则显示全部(默认行为不变)。
|
|
97
139
|
- **状态展示**:「账户用量」卡片与 `/commandcode` 均按账户分别显示状态。
|
|
98
140
|
|
|
99
141
|
等价的 YAML(`$DSH_HOME/settings.yaml` 或组合配置):
|
package/lib/client.js
CHANGED
|
@@ -312,6 +312,14 @@ window.__ModuleLoader__.load({
|
|
|
312
312
|
return b.every((id) => set.has(id));
|
|
313
313
|
}
|
|
314
314
|
/**
|
|
315
|
+
* Order-sensitive content fingerprint of the stored routing rules. Stored rule
|
|
316
|
+
* ids are positional (`rule-<index>`), so equality of this fingerprint across a
|
|
317
|
+
* save is exactly the statement "no rules write landed and no row shifted".
|
|
318
|
+
*/
|
|
319
|
+
function ruleFingerprint(rules) {
|
|
320
|
+
return JSON.stringify(rules.map((rule) => [rule.models, rule.account]));
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
315
323
|
* Controller bridging the `llm-commandcode` scope and the credentials domain
|
|
316
324
|
* onto the page. Public API mirrors the harness's CardForm actions, so the
|
|
317
325
|
* component stays thin.
|
|
@@ -351,6 +359,12 @@ window.__ModuleLoader__.load({
|
|
|
351
359
|
* would delete the wrong row).
|
|
352
360
|
*/
|
|
353
361
|
removedRuleIds = /* @__PURE__ */ new Map();
|
|
362
|
+
/**
|
|
363
|
+
* Fingerprint of the stored routing rules when the last `save()` started;
|
|
364
|
+
* `undefined` outside a save. Lets reconcile distinguish a landed rules
|
|
365
|
+
* write from a save that failed before reaching it.
|
|
366
|
+
*/
|
|
367
|
+
rulesBeforeSave = void 0;
|
|
354
368
|
/** Staged visible-model allowlist (undefined = no draft). */
|
|
355
369
|
visibleModelsDraft = void 0;
|
|
356
370
|
/** The catalog the model editors offer (Host-side). */
|
|
@@ -646,6 +660,7 @@ window.__ModuleLoader__.load({
|
|
|
646
660
|
}
|
|
647
661
|
this.saving = true;
|
|
648
662
|
this.failed = false;
|
|
663
|
+
this.rulesBeforeSave = ruleFingerprint(this.storedRules());
|
|
649
664
|
this.publish();
|
|
650
665
|
let landed = true;
|
|
651
666
|
for (const run of [
|
|
@@ -682,10 +697,10 @@ window.__ModuleLoader__.load({
|
|
|
682
697
|
}
|
|
683
698
|
/**
|
|
684
699
|
* Drop account staging the stored section already reflects: additions whose
|
|
685
|
-
* ref is now stored, removals whose ref is gone, and label drafts
|
|
686
|
-
*
|
|
687
|
-
* on retry, and the draft carries the user's intent when it was
|
|
688
|
-
* accounts write that failed.
|
|
700
|
+
* ref is now stored, removals whose ref is gone, and label drafts that the
|
|
701
|
+
* stored label proves landed. Key drafts are kept — a landed key write is
|
|
702
|
+
* idempotent on retry, and the draft carries the user's intent when it was
|
|
703
|
+
* the accounts write that failed.
|
|
689
704
|
*/
|
|
690
705
|
reconcileAccountStaging() {
|
|
691
706
|
const stored = new Set(this.storedExtras().map((extra) => extra.ref));
|
|
@@ -693,7 +708,7 @@ window.__ModuleLoader__.load({
|
|
|
693
708
|
for (const ref of [...this.removedRefs]) if (!stored.has(ref)) this.removedRefs.delete(ref);
|
|
694
709
|
for (const [ref, text] of [...this.labelDrafts]) {
|
|
695
710
|
const storedLabel = this.storedExtras().find((extra) => extra.ref === ref)?.label;
|
|
696
|
-
if (storedLabel
|
|
711
|
+
if (storedLabel !== void 0 && storedLabel === text.trim()) this.labelDrafts.delete(ref);
|
|
697
712
|
}
|
|
698
713
|
for (const ref of [...this.keyClears]) if (this.credentialStates.get(ref)?.configured !== true) this.keyClears.delete(ref);
|
|
699
714
|
}
|
|
@@ -861,14 +876,25 @@ window.__ModuleLoader__.load({
|
|
|
861
876
|
this.catalogFailed = true;
|
|
862
877
|
}).then(() => this.publish());
|
|
863
878
|
}
|
|
864
|
-
/** The
|
|
865
|
-
|
|
879
|
+
/** The raw `accounts` array of the stored section, verbatim. */
|
|
880
|
+
rawStoredAccounts() {
|
|
866
881
|
const raw = this.scope.getSnapshot().value?.accounts;
|
|
867
882
|
if (!Array.isArray(raw)) return [];
|
|
883
|
+
return raw.filter((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry));
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* The stored extra accounts from the settings section (`accounts`): the rows
|
|
887
|
+
* this page can address, i.e. the ones carrying a credential reference.
|
|
888
|
+
*
|
|
889
|
+
* Entries the page cannot name are deliberately NOT listed here but are also
|
|
890
|
+
* never dropped — `writeAccounts()` rebuilds the stored list from
|
|
891
|
+
* {@link rawStoredAccounts} and only rewrites the reference-carrying entries
|
|
892
|
+
* it manages (see the note there), so a literal-key entry stays in the
|
|
893
|
+
* document and simply has no row.
|
|
894
|
+
*/
|
|
895
|
+
storedExtras() {
|
|
868
896
|
const out = [];
|
|
869
|
-
for (const
|
|
870
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
871
|
-
const record = entry;
|
|
897
|
+
for (const record of this.rawStoredAccounts()) {
|
|
872
898
|
const ref = record.apiKeyEnv;
|
|
873
899
|
if (typeof ref !== "string" || ref === "") continue;
|
|
874
900
|
const label = record.label;
|
|
@@ -945,20 +971,54 @@ window.__ModuleLoader__.load({
|
|
|
945
971
|
runs.push(() => this.writeAccounts());
|
|
946
972
|
return runs;
|
|
947
973
|
}
|
|
948
|
-
/**
|
|
974
|
+
/**
|
|
975
|
+
* Persist the staged accounts list into the settings section.
|
|
976
|
+
*
|
|
977
|
+
* The stored list is the base — NOT a list rebuilt from this page's rows.
|
|
978
|
+
* A composition-config entry may carry a literal `apiKey` (or a shape this
|
|
979
|
+
* page does not know), and the settings layer replaces the whole array, so a
|
|
980
|
+
* rebuilt list would silently delete every entry the page cannot name along
|
|
981
|
+
* with the literal keys of the entries it can. Entries are therefore carried
|
|
982
|
+
* over verbatim and only the reference-carrying rows are rewritten (label
|
|
983
|
+
* draft applied, staged removals dropped, staged additions appended).
|
|
984
|
+
*/
|
|
949
985
|
async writeAccounts() {
|
|
950
|
-
const
|
|
951
|
-
const
|
|
952
|
-
const list =
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
986
|
+
const removed = this.removedRefs;
|
|
987
|
+
const written = /* @__PURE__ */ new Set();
|
|
988
|
+
const list = [];
|
|
989
|
+
for (const entry of this.rawStoredAccounts()) {
|
|
990
|
+
const ref = entry.apiKeyEnv;
|
|
991
|
+
if (typeof ref !== "string" || ref === "") {
|
|
992
|
+
list.push({ ...entry });
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
if (removed.has(ref) || written.has(ref)) continue;
|
|
996
|
+
written.add(ref);
|
|
997
|
+
list.push(this.accountEntry(ref, entry));
|
|
998
|
+
}
|
|
999
|
+
for (const extra of this.addedAccounts) {
|
|
1000
|
+
if (written.has(extra.ref)) continue;
|
|
1001
|
+
written.add(extra.ref);
|
|
1002
|
+
list.push(this.accountEntry(extra.ref, extra));
|
|
1003
|
+
}
|
|
959
1004
|
await this.scope.set("accounts", list);
|
|
960
|
-
const after = this.
|
|
961
|
-
return after.length === list.length && list.every((item, index) => after[index]?.
|
|
1005
|
+
const after = this.rawStoredAccounts();
|
|
1006
|
+
return after.length === list.length && list.every((item, index) => after[index]?.apiKeyEnv === item.apiKeyEnv);
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* One written account entry: the stored/added facts plus the label draft.
|
|
1010
|
+
* `fallback` contributes the non-managed fields (a stored entry's literal
|
|
1011
|
+
* `apiKey`, or any future field) so a rewrite never strips them.
|
|
1012
|
+
*/
|
|
1013
|
+
accountEntry(ref, fallback) {
|
|
1014
|
+
const draft = this.labelDrafts.get(ref)?.trim();
|
|
1015
|
+
const base = "ref" in fallback ? { label: fallback.label } : { ...fallback };
|
|
1016
|
+
const storedLabel = base.label;
|
|
1017
|
+
return {
|
|
1018
|
+
...base,
|
|
1019
|
+
label: draft !== void 0 && draft !== "" ? draft : typeof storedLabel === "string" && storedLabel !== "" ? storedLabel : ref,
|
|
1020
|
+
apiKeyEnv: ref
|
|
1021
|
+
};
|
|
962
1022
|
}
|
|
963
1023
|
/** The stored routing rules from the settings section (`modelAccountRules`). */
|
|
964
1024
|
storedRules() {
|
|
@@ -1034,6 +1094,7 @@ window.__ModuleLoader__.load({
|
|
|
1034
1094
|
const stored = this.storedRules();
|
|
1035
1095
|
this.addedRules = this.addedRules.filter((added) => !stored.some((rule) => sameModels(added.models, rule.models) && added.account === rule.account));
|
|
1036
1096
|
for (const [id, snapshot] of [...this.removedRuleIds]) if (!stored.some((rule) => sameModels(snapshot.models, rule.models) && snapshot.account === rule.account)) this.removedRuleIds.delete(id);
|
|
1097
|
+
if (this.rulesBeforeSave !== void 0 && ruleFingerprint(stored) === this.rulesBeforeSave) return;
|
|
1037
1098
|
this.ruleDrafts.clear();
|
|
1038
1099
|
}
|
|
1039
1100
|
/** The stored visible-model allowlist (`visibleModels`); empty = show all. */
|
|
@@ -1260,6 +1321,16 @@ window.__ModuleLoader__.load({
|
|
|
1260
1321
|
if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
1261
1322
|
return String(value);
|
|
1262
1323
|
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Format a success-rate percentage (already in percent units, e.g. 99.96):
|
|
1326
|
+
* at most two decimals, trailing zeros trimmed — `100` stays `100`, not
|
|
1327
|
+
* `100.00`, and the raw upstream float `99.965552876334` becomes `99.97`.
|
|
1328
|
+
* The `%` suffix is appended by the caller (the card and the dashboard line
|
|
1329
|
+
* both compose it).
|
|
1330
|
+
*/
|
|
1331
|
+
function formatSuccessRate(value) {
|
|
1332
|
+
return String(Number(value.toFixed(2)));
|
|
1333
|
+
}
|
|
1263
1334
|
/** One window's fill ratio in [0, 1]; 0 when uncapped. */
|
|
1264
1335
|
function windowRatio(used, cap) {
|
|
1265
1336
|
if (cap <= 0) return 0;
|
|
@@ -1956,9 +2027,31 @@ window.__ModuleLoader__.load({
|
|
|
1956
2027
|
function toggleModelSelection(selected, modelId) {
|
|
1957
2028
|
return selected.includes(modelId) ? selected.filter((value) => value !== modelId) : [...selected, modelId];
|
|
1958
2029
|
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Whether the catalog is trustworthy enough to call an unlisted selection
|
|
2032
|
+
* "retired". FALSE while the first fetch is still in flight and after a
|
|
2033
|
+
* failure, because the catalog is empty then and every selected id would look
|
|
2034
|
+
* stale — which turns the one-click stale cleanup into a button that silently
|
|
2035
|
+
* empties the allowlist. A successfully loaded but empty catalog is treated as
|
|
2036
|
+
* untrustworthy too: an empty list is far more likely a Host problem than every
|
|
2037
|
+
* model being retired at once, and the explicit "show all" entry covers the
|
|
2038
|
+
* user who really wants to clear the list.
|
|
2039
|
+
*/
|
|
2040
|
+
function catalogIsReady(readiness) {
|
|
2041
|
+
return readiness.catalogIds.length > 0 && !readiness.catalogFailed;
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Selected ids the loaded catalog no longer carries, in selection order.
|
|
2045
|
+
* Callers gate user-visible "stale" affordances on {@link catalogIsReady} —
|
|
2046
|
+
* the list itself is informational.
|
|
2047
|
+
*/
|
|
2048
|
+
function staleModelIds(selected, readiness) {
|
|
2049
|
+
const catalogIds = new Set(readiness.catalogIds);
|
|
2050
|
+
return selected.filter((id) => !catalogIds.has(id));
|
|
2051
|
+
}
|
|
1959
2052
|
//#endregion
|
|
1960
2053
|
//#region package.json
|
|
1961
|
-
var version = "0.10.
|
|
2054
|
+
var version = "0.10.4";
|
|
1962
2055
|
var repository = {
|
|
1963
2056
|
"type": "git",
|
|
1964
2057
|
"url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
|
|
@@ -2605,7 +2698,7 @@ window.__ModuleLoader__.load({
|
|
|
2605
2698
|
}),
|
|
2606
2699
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
|
|
2607
2700
|
label: t("usageSuccessRate"),
|
|
2608
|
-
value: `${report.usage.successRate}%`
|
|
2701
|
+
value: `${formatSuccessRate(report.usage.successRate)}%`
|
|
2609
2702
|
}),
|
|
2610
2703
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
|
|
2611
2704
|
label: t("usageCost"),
|
|
@@ -3180,8 +3273,12 @@ window.__ModuleLoader__.load({
|
|
|
3180
3273
|
if (key === "ruleModelCount") return t("visibleModelsCount", params);
|
|
3181
3274
|
return t(key, params);
|
|
3182
3275
|
};
|
|
3183
|
-
const
|
|
3184
|
-
|
|
3276
|
+
const readiness = {
|
|
3277
|
+
catalogIds: state.catalogModels.map((model) => model.id),
|
|
3278
|
+
catalogFailed: state.catalogFailed
|
|
3279
|
+
};
|
|
3280
|
+
const staleIds = staleModelIds(state.visibleModels, readiness);
|
|
3281
|
+
const catalogReady = catalogIsReady(readiness);
|
|
3185
3282
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3186
3283
|
className: "cc-card",
|
|
3187
3284
|
"aria-label": t("visibleModelsTitle"),
|
|
@@ -3195,11 +3292,11 @@ window.__ModuleLoader__.load({
|
|
|
3195
3292
|
children: t("visibleModelsTitle")
|
|
3196
3293
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3197
3294
|
className: "cc-badges",
|
|
3198
|
-
children: [staleIds.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3295
|
+
children: [catalogReady && staleIds.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3199
3296
|
type: "button",
|
|
3200
3297
|
className: "cc-reset",
|
|
3201
3298
|
disabled,
|
|
3202
|
-
onClick: () => onSelect(state.visibleModels.filter((id) =>
|
|
3299
|
+
onClick: () => onSelect(state.visibleModels.filter((id) => !staleIds.includes(id))),
|
|
3203
3300
|
children: t("visibleModelsCleanStale", { count: staleIds.length })
|
|
3204
3301
|
}) : null, count > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3205
3302
|
type: "button",
|
|
@@ -3218,7 +3315,7 @@ window.__ModuleLoader__.load({
|
|
|
3218
3315
|
className: "cc-invalid",
|
|
3219
3316
|
children: t("rulesCatalogFailed")
|
|
3220
3317
|
}) : null,
|
|
3221
|
-
staleIds.length > 0 &&
|
|
3318
|
+
staleIds.length > 0 && catalogReady ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3222
3319
|
className: "cc-hint",
|
|
3223
3320
|
children: t("visibleModelsStaleHint", { count: staleIds.length })
|
|
3224
3321
|
}) : null,
|
|
@@ -3683,9 +3780,9 @@ window.__ModuleLoader__.load({
|
|
|
3683
3780
|
modelSearchPlaceholder: "搜索模型…",
|
|
3684
3781
|
modelSearchEmpty: "没有匹配的模型。",
|
|
3685
3782
|
modelStale: "已下架",
|
|
3686
|
-
visibleModelsTitle: "
|
|
3687
|
-
visibleModelsHint: "
|
|
3688
|
-
visibleModelsPick: "
|
|
3783
|
+
visibleModelsTitle: "模型白名单",
|
|
3784
|
+
visibleModelsHint: "勾选要保留的模型,模型选择器就只列出这些;一个都不勾选时则显示全部模型。保存后,下次打开模型选择器生效。",
|
|
3785
|
+
visibleModelsPick: "选择要保留的模型…",
|
|
3689
3786
|
visibleModelsCount: "已选 {count} 个模型",
|
|
3690
3787
|
visibleModelsShowAll: "显示全部",
|
|
3691
3788
|
visibleModelsStaleHint: "有 {count} 个已选模型在目录中找不到了(可能已下架),不影响其他模型;可清理或保留。",
|
|
@@ -3815,9 +3912,9 @@ window.__ModuleLoader__.load({
|
|
|
3815
3912
|
modelSearchPlaceholder: "Search models…",
|
|
3816
3913
|
modelSearchEmpty: "No matching models.",
|
|
3817
3914
|
modelStale: "Retired",
|
|
3818
|
-
visibleModelsTitle: "
|
|
3819
|
-
visibleModelsHint: "
|
|
3820
|
-
visibleModelsPick: "Select models to
|
|
3915
|
+
visibleModelsTitle: "Model allowlist",
|
|
3916
|
+
visibleModelsHint: "Check the models you want to keep, and model pickers will list only those. If nothing is checked, every model is shown. After saving, the change applies the next time you open a model picker.",
|
|
3917
|
+
visibleModelsPick: "Select models to keep…",
|
|
3821
3918
|
visibleModelsCount: "{count} model(s) selected",
|
|
3822
3919
|
visibleModelsShowAll: "Show all",
|
|
3823
3920
|
visibleModelsStaleHint: "{count} selected model(s) are no longer in the catalog (possibly retired); other models are unaffected. Clean them up or keep them.",
|