dsh-cliproxyapi-pro 1.8.0 → 1.9.0
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 +8 -0
- package/README.en.md +26 -13
- package/README.md +26 -13
- package/lib/client.js +85 -8
- package/lib/index.js +95 -8
- package/lib/media.js +15 -1
- package/lib/quota.js +114 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
本插件的所有重要变更。版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)。
|
|
6
6
|
|
|
7
|
+
## 1.9.0 — 2026-10-25
|
|
8
|
+
|
|
9
|
+
### 新增
|
|
10
|
+
- **图片和视频生成的总开关。** 「生成」标签顶部新增「启用图片和视频生成」,对所有账号生效。关闭时输入框旁不显示生成按钮,`/cpa-image`、`/cpa-video`、`/cpa-video-edit`、`/cpa-video-extend`、`/cpa-video-status` 不注册,agent 生成工具 `generate_media` 也不注册,「生成」标签只显示这个开关;已提交的视频照常完成,聊天记录里已生成的结果仍可查看(不再提供编辑 / 延长按钮)。新安装默认关闭;已有 `media-settings.json`(字段缺失视为开启)或用过生成(存在 `media-sessions.json` / `media-jobs.json`)的配置保持开启。保存在 `media-settings.json` 的新字段 `enabled` 中。
|
|
11
|
+
|
|
12
|
+
### 变更
|
|
13
|
+
- **管理密钥被拒绝后最多自动重试 3 次。** 额度查询遇到 CPA 管理接口 401/403 时,按 5 分钟缓存周期在后台继续尝试,连续 3 次被拒绝后停止自动查询(CPA 对同一地址累计 5 次错误密钥会封禁 30 分钟)。失败次数保存在 `.dsh-cliproxyapi/quota-state.json`(不含密钥),重启 DSH 或重载插件后继续计数。保存新的管理密钥(包括在 DSH 凭证页修改)、清除密钥或一次成功的查询会清零;停止后手动刷新只再试一次,再被拒绝立即停止。额度页和输入框徽标显示「已连续被拒绝 n/3 次」或已停止的说明,`/cpa-status` 也会注明。之前的版本在第一次被拒绝后就暂停(仅在本次运行内有效)。
|
|
14
|
+
|
|
7
15
|
## 1.8.0 — 2026-10-22
|
|
8
16
|
|
|
9
17
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -49,7 +49,7 @@ The plugin is published on npm as [`dsh-cliproxyapi-pro`](https://www.npmjs.com/
|
|
|
49
49
|
dsh-cliproxyapi-pro
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
To pin a version, write `dsh-cliproxyapi-pro@1.
|
|
52
|
+
To pin a version, write `dsh-cliproxyapi-pro@1.9.0`.
|
|
53
53
|
4. Click **Install**. When it finishes, the dialog offers to enable the plugin right away — accept it.
|
|
54
54
|
|
|
55
55
|
> If downloads are slow in mainland China, open **Registry** next to the input and choose **the mainland China mirror**.
|
|
@@ -78,7 +78,7 @@ In **Add plugin**, or as the `plugin_manager` `target`, you can also enter:
|
|
|
78
78
|
|
|
79
79
|
| Source | What to enter |
|
|
80
80
|
|---|---|
|
|
81
|
-
| A released version | `git+https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git#v1.
|
|
81
|
+
| A released version | `git+https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git#v1.9.0` |
|
|
82
82
|
| Latest development version | `git+https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git#main` |
|
|
83
83
|
| Offline package | Download [`dsh-cliproxyapi-pro.tgz`](https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro/releases/latest/download/dsh-cliproxyapi-pro.tgz) (always the latest release) and enter its absolute local path |
|
|
84
84
|
| Local source (development) | Absolute path of a cloned repository; installed as a link, so code changes go live when the plugin reloads |
|
|
@@ -96,29 +96,37 @@ If CPA runs on this machine on the default port `8317` without a key, there is n
|
|
|
96
96
|
|
|
97
97
|
## Screenshots
|
|
98
98
|
|
|
99
|
-
The panel follows the DSH display language; the screenshots show the Chinese UI. Images 1–
|
|
99
|
+
The panel follows the DSH display language; the screenshots show the Chinese UI. Images 1–7 are offline component renders: this plugin's real UI code with sample data (`example.com` addresses, fictional accounts) rendered in a browser, with colors approximating the DSH dark theme; the real colors follow your DSH theme. Image 8 is an actual capture of 1.6.0 in the DSH desktop app.
|
|
100
100
|
|
|
101
|
-
**1. Accounts and connection.** The **CLIProxyAPI** page in the Settings dialog's left navigation: one account bar for all tabs; under the account title are the connection state, address and model count, with the **Enabled** switch (applies at once) and the **⋯** menu (**Probe & Refresh**, **Delete Account**) on the right. The **Connection** tab has one row per setting, **Test Connection** next to the address (uses the values being edited, saves nothing), and secondary explanations in ⓘ tips.
|
|
101
|
+
**1. Accounts and connection.** The **CLIProxyAPI** page in the Settings dialog's left navigation: one account bar for all four tabs (Connection / Models / Generation / Quota); under the account title are the connection state, address and model count, with the **Enabled** switch (applies at once) and the **⋯** menu (**Probe & Refresh**, **Delete Account**) on the right. The **Connection** tab has one row per setting, **Test Connection** next to the address (uses the values being edited, saves nothing), and secondary explanations in ⓘ tips.
|
|
102
102
|
|
|
103
103
|

|
|
104
104
|
|
|
105
|
-
**2. Models and one save bar.** The **Models** tab lists every model on that CPA: All / Checked / Unchecked, family filter, search, bulk actions and **Reset to Recommended**; checked models show the protocol they will use and can be edited with **Edit Parameters**. Connection and model edits are tracked together, and a bar at the bottom shows **Unsaved changes · Discard / Save & Apply**.
|
|
105
|
+
**2. Models and one save bar.** The **Models** tab lists every chat model on that CPA: All / Checked / Unchecked, family filter, search, bulk actions and **Reset to Recommended**; checked models show the protocol they will use and can be edited with **Edit Parameters**. Connection and model edits are tracked together, and a bar at the bottom shows **Unsaved changes · Discard / Save & Apply**.
|
|
106
106
|
|
|
107
107
|

|
|
108
108
|
|
|
109
|
-
**3.
|
|
109
|
+
**3. Image and video generation.** The top of the **Generation** tab holds the master switch **Enable image and video generation** (for every account, off on a new installation; while off this page shows only the switch, and neither the composer's generation buttons nor the generation commands exist). Once on, it lists image and video models in groups, each with its own switch and kind; below are marking missed models, the save location, video defaults and the agent generation settings.
|
|
110
|
+
|
|
111
|
+

|
|
112
|
+
|
|
113
|
+
**4. Quota.** The **Quota** tab: management key, quota data and refresh, a distribution bar, credentials grouped by channel (plan, prefix, status, 5-hour / daily / weekly meters with reset times; Devin shows its daily / weekly quota, shared by every Devin model; channels without a quota endpoint show status and recent requests only), and an expandable "Model → Channel" table. The tab label shows the lowest remaining quota among the account's usable credentials.
|
|
110
114
|
|
|
111
115
|

|
|
112
116
|
|
|
113
|
-
**
|
|
117
|
+
**5. A rejected management key.** After CPA rejects the key 3 times in a row (401/403), automatic checks stop so CPA does not ban this address; the page explains why and keeps the last data it read. Saving a new management key resumes them; a manual **Refresh Quota** tries only once.
|
|
118
|
+
|
|
119
|
+

|
|
120
|
+
|
|
121
|
+
**6. Quota in the composer.** The badge left of the model selector shows the best remaining quota among the usable credentials serving the current model; clicking it lists every such credential with its quota windows. It appears only for models this plugin synchronizes whose account has a management key.
|
|
114
122
|
|
|
115
123
|

|
|
116
124
|
|
|
117
|
-
**
|
|
125
|
+
**7. Adding an account.** Enter a name, address and API key and click **Connect**: it tests first and creates an enabled account only when that works; on failure the reason is shown and **Add Anyway (Disabled)** is offered.
|
|
118
126
|
|
|
119
127
|

|
|
120
128
|
|
|
121
|
-
**
|
|
129
|
+
**8. The plugin update page under Built-in plugins** (actual 1.6.0 capture). **Settings** → **Built-in plugins** → the **CLIProxyAPI** tab holds only plugin updates: checking and offline download, **Update Now** once the DSH plugin market is detected, the update channel, the download region, and the automatic check / download / install switches. Here the plugin is already the latest Stable version, so **Update Now** is disabled.
|
|
122
130
|
|
|
123
131
|

|
|
124
132
|
|
|
@@ -180,12 +188,13 @@ Each account's **Quota** tab (next to **Connection**, **Models** and **Generatio
|
|
|
180
188
|
- **Composer badge**: a small badge left of the model selector (signal bars + percentage) shows the best remaining quota among the usable credentials serving the selected model; click it for every credential's windows. It appears only for models this plugin synchronizes whose account has a management key; without a key there is no badge. On the New Session page (before the composer takes over the session) the badge sits at the right end of the workspace row above the input box.
|
|
181
189
|
- **Exact matching**: credentials are looked up only in the account the selected route belongs to (`cli-proxy*`, `cli-proxy-2*`, …); a prefixed model (such as `devin/claude-opus-5-5`) matches only credentials with that prefix; a bare id (such as `claude-opus-5-5`) prefers un-prefixed credentials, so the Claude and Devin channels never mix. Per-model-group windows (for example Antigravity) count only the group that fits the current model.
|
|
182
190
|
- **Troubleshooting**: `/cpa-status` includes a quota summary: each account's key state, quota state, credential count, and which credentials each picker model maps to with its remaining quota (never the key).
|
|
183
|
-
- **Refreshing**: results are cached on the Host for 5 minutes; stale accounts refresh in the background only while a page is asking, or on **Refresh Quota**.
|
|
191
|
+
- **Refreshing**: results are cached on the Host for 5 minutes; stale accounts refresh in the background only while a page is asking, or on **Refresh Quota**. When the management key is rejected (401/403), background checks continue at the 5-minute interval and stop for that account after 3 rejections in a row, so CPA's bad-key protection (a 30-minute ban after 5 bad keys from one address) is not triggered. The count is stored in `.dsh-cliproxyapi/quota-state.json` (never the key) and continues after a DSH restart; saving a new management key (also on the DSH credentials page), clearing the key or a successful check resets it. Once stopped, a manual refresh tries only once and stops again on another rejection. The Quota tab and the badge show how many times the key was rejected, or that checks have stopped.
|
|
184
192
|
|
|
185
193
|
### Image and video models (the "Generation" tab)
|
|
186
194
|
|
|
187
195
|
CPA serves image and video generators (such as Grok Imagine) through `/v1/images/*` and `/v1/videos/*`. They cannot chat, so the plugin recognizes them and keeps them out of the chat model picker.
|
|
188
196
|
|
|
197
|
+
- **Master switch**: **Enable image and video generation** at the top of the Generation tab applies to every account. While off, the composer shows no generation buttons, `/cpa-image`, `/cpa-video`, `/cpa-video-edit`, `/cpa-video-extend` and `/cpa-video-status` are not registered, the agent generation tool is not registered either, and the Generation tab shows only this switch; videos already submitted still finish, and past results in the chat stay viewable (without the edit / extend buttons). New installations start with it off; a profile that already has `media-settings.json` or has used generation keeps it on after the upgrade. Either way, media models never enter the chat model picker. Everything below needs it switched on.
|
|
189
198
|
- **Recognition**, in order: a kind you set on the Generation tab; the catalog's `type` (`openai-image` is an image model) or output modalities (image-only output is an image model, video output a video model); the model families CPA routes to its image / video handlers, matched by prefix: ids starting with `grok-imagine-image` or `gpt-image-` are image models, ids starting with `grok-imagine-video` and `sora-2*` are video models. Everything else is a chat model (for example `gemini-3.1-flash-image`: CPA serves it only through chat, and DSH cannot show images returned in a chat reply). A media model that only the plain `/v1/models` listing carries is included too. A catalog may list variants CPA's generation endpoints do not accept yet (for example `grok-imagine-video-1.5-lite`); they cannot chat either, so they count as media models, and generating with one shows CPA's error.
|
|
190
199
|
- **Not in the model picker**: media models are written to no chat route and take no part in the first-run recommendation; an older selection that names them does not synchronize them (the selection itself is kept). The Models tab lists chat models only.
|
|
191
200
|
- **The Generation tab** (next to Connection / Models / Quota) lists "Image models" and "Video models"; changes are saved at once. A switch decides whether each model is offered for generation (models that appear later start switched on), and a kind menu changes it to image / video / chat; a kind you set can be reset to the detected one. A model detection missed can be marked under "Mark another model as a media model". Changing a kind re-synchronizes that account's routes immediately. `sora-2*` is labelled "→ grok-imagine-video": CPA rewrites it to the Grok video model.
|
|
@@ -259,7 +268,7 @@ Each time `/cpa-image`, `/cpa-video`, `/cpa-video-edit` or `/cpa-video-extend` r
|
|
|
259
268
|
|
|
260
269
|
#### Letting the agent generate (`generate_media` tool, off by default)
|
|
261
270
|
|
|
262
|
-
With **Let the agent generate images / videos** switched on in the Generation tab, the chat agent can call the `generate_media` tool to generate images and videos, or to edit and extend videos. While off (the default) the tool is not registered and takes no model context; when on, it is registered only while at least one generation model is enabled. Its definition is compact and lists no models.
|
|
271
|
+
With **Let the agent generate images / videos** switched on in the Generation tab, the chat agent can call the `generate_media` tool to generate images and videos, or to edit and extend videos. While off (the default) the tool is not registered and takes no model context; when on, it is registered only while the **Enable image and video generation** master switch is on and at least one generation model is enabled. Its definition is compact and lists no models.
|
|
263
272
|
|
|
264
273
|
- **What it does**: parameters `kind` (image / video) and `prompt`, optionally `mode` (generate / edit / extend), `model`, `images` (local image paths: for images, to edit or combine; for videos, 1 is the first frame and 2 or more are references), `frames` (first frame / references / first & last frame / last frame), `video` (the source of an edit / extension: a local MP4 path or a public link; without it, this session's newest generated video), `aspect`, `seconds`, `resolution`, `count`. Optional fields may be omitted or set to `null` (including on Responses strict-schema routes); video `count` should be `null`, as each call generates one video. An omitted, `null` or empty `model` uses this session's pick in the composer button (a sub-agent uses the pick of the session it belongs to). It works like the matching slash command, except that the prompt is **sent as written**: words like "10s" or "portrait" in it are not read as options and the chat model is not asked to interpret parameters. The tool description also tells the agent to use the user's own wording and level of detail, never expanding it into a longer scene description or adding camera, lighting, style, character or environment details the user did not ask for.
|
|
265
274
|
- **Result**: files go to the save location; the tool returns their paths, the model and the actual parameters. For images, when the current chat model can see images, the generated images (downscaled by DSH) are handed to it as well. The chat renders one **independent generation row** from the recorded tool lifecycle, outside the thinking process, using the slash command's exchange layout (progress, images / a playable video, open / show in folder / copy, and **Edit video… / Extend video…** under a video). There is no need to expand thinking, no extra user command is fabricated, and tool cards or legacy mirrored command cards with a matching generation row do not repeat the result.
|
|
@@ -336,6 +345,8 @@ Apart from the generation tool `generate_media` (off by default, see above), thi
|
|
|
336
345
|
- `/cpa-video-status` — list this session's video jobs.
|
|
337
346
|
- `/cpa-video-edit [--video path | --url link | --from command-id] [-m model] change` / `/cpa-video-extend [same] [-s 2-10] what happens next` — edit / extend any MP4 (an attached video file works too). See [Editing and extending videos](#editing-and-extending-videos-cpa-video-edit-cpa-video-extend).
|
|
338
347
|
|
|
348
|
+
`/cpa-image` and `/cpa-video*` exist only while **Enable image and video generation** is on in the Generation tab.
|
|
349
|
+
|
|
339
350
|
### HTTP API
|
|
340
351
|
|
|
341
352
|
Used by the settings panel. Requires the DSH Web session's authentication; POST accepts only same-origin requests from loopback or trusted hosts.
|
|
@@ -356,7 +367,7 @@ Used by the settings panel. Requires the DSH Web session's authentication; POST
|
|
|
356
367
|
| POST | `/api/dsh-cliproxyapi/quota/refresh` | `{ accountId? }` | Start refreshing the quota of one or every account and return at once (`refreshing` is set in the snapshot) |
|
|
357
368
|
| POST | `/api/dsh-cliproxyapi/quota/save` | `{ accountId, managementKey?, clear? }` | Store or clear the account's management key and start refreshing its quota; the response never contains the key |
|
|
358
369
|
| GET | `/api/dsh-cliproxyapi/media/status` | — | `{ ok, media }`: generation settings and each account's recognized image / video models; no network access |
|
|
359
|
-
| POST | `/api/dsh-cliproxyapi/media/save` | `{ account: { accountId, disabledModels?, kindOverrides? } }` |
|
|
370
|
+
| POST | `/api/dsh-cliproxyapi/media/save` | `{ enabled? }`, global fields (such as `agentTool`, `outputDir`), or `{ account: { accountId, disabledModels?, kindOverrides? } }` | `enabled` switches generation on / off (registering or removing the generation commands and the agent tool at once); save an account's switched-off media models (replaces the list) or kind overrides (merged per model; `null` restores detection); a kind change re-synchronizes that account and adds `status` to the response. Invalid values return 400 with a code such as `invalid-enabled`, `invalid-account` or `invalid-kind` |
|
|
360
371
|
| POST | `/api/dsh-cliproxyapi/media/diagnose` | `{ accountId }` | Read-only diagnostic (see above); returns `{ ok, diagnostic }` without the key |
|
|
361
372
|
| GET | `/api/dsh-cliproxyapi/media/session?sessionId=` | — | `{ ok, session }`: the image / video models to choose from, the current pick and where it comes from (`session` / `last` / `only`), and this session's recent files (names and ids only, no paths); `pendingConfirmations` lists only compatibility requests owned by this exact session when the native question service is absent, as `{ commandId, kind, en, zh, expiresAt }`; native questions never appear in that list; no network access |
|
|
362
373
|
| POST | `/api/dsh-cliproxyapi/media/session/save` | `{ sessionId?, kind: "image" \| "video", ref }` | Pick a session's model (`ref` is `accountId/modelId`; `null` clears the session's pick) and remember it as the last pick; a model that is not offered returns 400 `invalid-model`. Alternatively `{ sessionId, videoConfirm?, imageConfirm?, videoBudget? }` (`ask` / `auto` / `inherit`; the limit is `{ mode: "limited", seconds }` / `{ mode: "unlimited" }` / `inherit`) sets this session's agent-generation rules |
|
|
@@ -404,11 +415,12 @@ Failures use a top-level `errorCode` or `update.errorCode`; HTTP status or `ok`
|
|
|
404
415
|
```
|
|
405
416
|
|
|
406
417
|
Panel saves take effect immediately. After editing this file manually, reload the plugin or restart DSH to re-read it. Corrupt, structurally invalid, invalid-field or unreadable files use safe defaults with automatic network checks disabled. Check results, the candidate version, download notices and commands are in-memory only and reset on plugin reload; downloaded packages remain in the separate `updates` subdirectory for you to clean up when needed.
|
|
407
|
-
- **Generation settings**: stored separately at `<profile dir>/.dsh-cliproxyapi/media-settings.json`; `state.json` is not changed. Without this file the defaults apply; a damaged file also falls back to the defaults with a log warning, and the next save first keeps the damaged file as `media-settings.corrupt-<time>.json`.
|
|
418
|
+
- **Generation settings**: stored separately at `<profile dir>/.dsh-cliproxyapi/media-settings.json`; `state.json` is not changed. Without this file the defaults apply (`enabled: false`, generation off; but on when `media-sessions.json` or `media-jobs.json` exists); a file without the `enabled` field (written before the master switch existed) reads as on. A damaged file also falls back to the defaults with a log warning, and the next save first keeps the damaged file as `media-settings.corrupt-<time>.json`. The fields: `enabled` (master switch), `agentTool` (agent generation tool), `outputDir` (save location), `defaultVideoSeconds` / `defaultVideoResolution` (video defaults), `imageConfirm` / `videoConfirm` / `videoBudget` (agent confirmation and seconds limit), `lastSelection` (the last picked models), and each account's `disabledModels` (switched-off media models) and `kindOverrides` (kinds you set):
|
|
408
419
|
|
|
409
420
|
```json
|
|
410
421
|
{
|
|
411
422
|
"version": 1,
|
|
423
|
+
"enabled": true,
|
|
412
424
|
"agentTool": false,
|
|
413
425
|
"outputDir": "generated",
|
|
414
426
|
"defaultVideoSeconds": 6,
|
|
@@ -425,6 +437,7 @@ Failures use a top-level `errorCode` or `update.errorCode`; HTTP status or `ok`
|
|
|
425
437
|
|
|
426
438
|
- **Per-session generation records**: `<profile dir>/.dsh-cliproxyapi/media-sessions.json` keeps each session's picked generation models, records of its last 48 runs (status, model, prompt, options, start and end times, a display name for the save folder, error summary), and the paths of its last 48 generated files with the command id that produced them (for the command rows in the conversation), for the 500 most recently updated sessions. Deleting it only clears those picks and records, never generated files. A damaged file is treated as empty and kept as `media-sessions.corrupt-<time>.json` before the next write.
|
|
427
439
|
- **Video jobs**: `<profile dir>/.dsh-cliproxyapi/media-jobs.json` keeps submitted video jobs (CPA request id, account, model, session and command id, working directory and save location, prompt, length, submission time, state) so a reload or restart can keep waiting; at most 200, finished ones kept 7 days. No keys and no video URLs. A damaged file is treated as empty and kept as `media-jobs.corrupt-<time>.json` before the next write.
|
|
440
|
+
- **Management key failures**: `<profile dir>/.dsh-cliproxyapi/quota-state.json` records, per account, how many times in a row the CPA management API rejected the key (401/403), the last error code and time, so automatic checks stop after 3; it never holds the key. Deleting it only resets the counts.
|
|
428
441
|
- **Keys**: stored and resolved by ref through the DSH `credentials` service, falling back to an environment variable of the same name.
|
|
429
442
|
- **Environment**: `CLIPROXYAPI_BASE_URL` is used only for auto-discovery of accounts without a base URL.
|
|
430
443
|
- **Plugin row config**: the plugin row's `config` in `cordis.patch.yml` is overlaid on the state file at startup: `accounts` replaces the account list, other fields apply to the first account. Normally keep it `{}` and configure through the Settings panel.
|
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
dsh-cliproxyapi-pro
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
需要固定版本时可以写成 `dsh-cliproxyapi-pro@1.
|
|
52
|
+
需要固定版本时可以写成 `dsh-cliproxyapi-pro@1.9.0`。
|
|
53
53
|
4. 点击 **安装**,完成后点击 **立即启用**。
|
|
54
54
|
|
|
55
55
|
> 在中国大陆下载慢,可以点输入框旁的 **安装源**,切换到 **中国大陆镜像源**。
|
|
@@ -78,7 +78,7 @@ dsh plugin --profile <profile 名称> add dsh-cliproxyapi-pro
|
|
|
78
78
|
|
|
79
79
|
| 来源 | 填写内容 |
|
|
80
80
|
|---|---|
|
|
81
|
-
| 某个发布版本 | `git+https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git#v1.
|
|
81
|
+
| 某个发布版本 | `git+https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git#v1.9.0` |
|
|
82
82
|
| 最新开发版 | `git+https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git#main` |
|
|
83
83
|
| 离线安装包 | 下载 [`dsh-cliproxyapi-pro.tgz`](https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro/releases/latest/download/dsh-cliproxyapi-pro.tgz)(始终是最新版),填写它的本地绝对路径 |
|
|
84
84
|
| 本地源码(开发用) | 克隆后的仓库目录绝对路径;以链接方式安装,改代码后重载插件即生效 |
|
|
@@ -96,29 +96,37 @@ CPA 就在本机默认端口 `8317` 且没有设置密钥时,什么都不用
|
|
|
96
96
|
|
|
97
97
|
## 截图
|
|
98
98
|
|
|
99
|
-
图 1–
|
|
99
|
+
图 1–7 是离线组件渲染:用本插件的真实界面代码加示例数据(`example.com` 地址、虚构账号)在浏览器中渲染,配色按 DSH 深色主题近似,实际颜色跟随你的 DSH 主题。图 8 是 1.6.0 在 DSH 桌面版中的实际截图。
|
|
100
100
|
|
|
101
|
-
**1. 账号与连接。** 设置左侧导航的 **CLIProxyAPI**
|
|
101
|
+
**1. 账号与连接。** 设置左侧导航的 **CLIProxyAPI** 页:顶部账号栏四个标签(连接 / 模型 / 生成 / 额度)共用;账号标题下是连接状态、地址和模型数,右侧「启用」开关立即生效,「⋯」菜单里是「立即探测刷新」和「删除此账号」。「连接」标签里每项一行,地址旁可「测试连接」(用正在编辑的值,不保存),次要说明在 ⓘ 里。
|
|
102
102
|
|
|
103
103
|

|
|
104
104
|
|
|
105
|
-
**2. 模型与统一保存。** 「模型」标签列出该 CPA
|
|
105
|
+
**2. 模型与统一保存。** 「模型」标签列出该 CPA 上的全部对话模型:全部 / 已勾选 / 未勾选、家族筛选、搜索、批量操作与「恢复推荐精选」;勾选的模型显示将使用的协议,可「编辑参数」。连接和模型的改动统一记录,底部出现「有未保存的更改 · 放弃更改 / 保存并应用」。
|
|
106
106
|
|
|
107
107
|

|
|
108
108
|
|
|
109
|
-
**3.
|
|
109
|
+
**3. 生图 / 生视频。** 「生成」标签顶部是总开关「启用图片和视频生成」(对所有账号生效,新安装默认关闭;关闭时这一页只剩这个开关,输入框旁的生成按钮和生成命令都不出现)。打开后分组列出生图 / 生视频模型,每个模型可单独开关、改类型;下面是标记漏识别的模型、保存位置、视频默认值和 agent 生成设置。
|
|
110
|
+
|
|
111
|
+

|
|
112
|
+
|
|
113
|
+
**4. 额度。** 「额度」标签:管理密钥、额度数据与刷新、凭证分布条,按渠道分组的凭证(套餐、前缀、状态、5 小时 / 每日 / 每周等额度条与重置时间;Devin 显示每日 / 每周额度,所有 Devin 模型共用;没有额度接口的渠道只显示状态与近期请求),以及可展开的「模型 → 渠道」表。标签上显示该账号可用凭证中最低的剩余额度。
|
|
110
114
|
|
|
111
115
|

|
|
112
116
|
|
|
113
|
-
**
|
|
117
|
+
**5. 管理密钥被拒绝。** 密钥连续 3 次被 CPA 拒绝(401/403)后停止自动查询,以免 CPA 封禁本机地址;页面说明原因,保留上次成功读取的数据。保存新的管理密钥后恢复,手动「刷新额度」只再试一次。
|
|
118
|
+
|
|
119
|
+

|
|
120
|
+
|
|
121
|
+
**6. 对话框里的额度。** 模型选择器左侧的徽标显示当前模型可用凭证中最充足的剩余额度;点击后列出服务该模型的每个凭证及其额度窗口。只对本插件同步、且所属账号已填写管理密钥的模型显示。
|
|
114
122
|
|
|
115
123
|

|
|
116
124
|
|
|
117
|
-
**
|
|
125
|
+
**7. 添加账号。** 填名称、地址和 API Key 后点「连接」:先测试,成功才创建并启用账号;失败时显示原因,可选「仍然添加(不启用)」。
|
|
118
126
|
|
|
119
127
|

|
|
120
128
|
|
|
121
|
-
**
|
|
129
|
+
**8. 内置插件里的插件更新页**(1.6.0 实际截图)。**设置** → **内置插件** → **CLIProxyAPI** 标签页只有插件更新:检查更新与离线下载、检测到 DSH 插件市场后的「立即更新」、更新通道、下载区域,以及自动检查 / 自动下载 / 自动安装开关。图中已是稳定通道最新版本,所以「立即更新」不可用。
|
|
122
130
|
|
|
123
131
|

|
|
124
132
|
|
|
@@ -180,12 +188,13 @@ CPA 对每个模型同时提供 OpenAI Chat、OpenAI Responses、Anthropic Messa
|
|
|
180
188
|
- **对话框徽标**:模型选择器左侧的小徽标(信号格 + 百分比)显示当前所选模型可用凭证中最充足的剩余额度,点击可查看每个凭证的额度。只对本插件同步、且所属账号已填写管理密钥的模型显示;没有管理密钥时不显示徽标。在新建对话页(输入栏尚未接管会话时),徽标显示在输入框上方工作区一行的右侧。
|
|
181
189
|
- **精确匹配**:只在所选模型所属的账号(按路由 `cli-proxy*`、`cli-proxy-2*`…)里找凭证;带前缀的模型(如 `devin/claude-opus-5-5`)只匹配该前缀的凭证;不带前缀的同名模型(如 `claude-opus-5-5`)优先匹配不带前缀的凭证,Claude 渠道与 Devin 渠道不会混用。Antigravity 等按模型分组的额度窗口只取与当前模型相符的那一组。
|
|
182
190
|
- **排查**:`/cpa-status` 会附带额度摘要:每个账号的管理密钥状态、额度状态、凭证数,以及模型选择器中每个模型匹配到的凭证和剩余额度(不输出密钥)。
|
|
183
|
-
- **刷新**:结果在宿主端缓存 5 分钟;有页面在看时才在后台刷新过期账号,也可手动「刷新额度」。管理密钥被拒(401/403
|
|
191
|
+
- **刷新**:结果在宿主端缓存 5 分钟;有页面在看时才在后台刷新过期账号,也可手动「刷新额度」。管理密钥被拒(401/403)时,后台按 5 分钟周期继续尝试,连续 3 次被拒绝后停止该账号的自动查询,避免触发 CPA 的错误密钥封禁(同一地址累计 5 次错误会被封 30 分钟)。失败次数保存在 `.dsh-cliproxyapi/quota-state.json`(不含密钥),重启 DSH 后继续计数;保存新的管理密钥(包括在 DSH 凭证页修改)、清除密钥或一次成功的查询会清零。停止后手动刷新只再试一次,再被拒绝立即停止。额度页和徽标会显示已被拒绝的次数或已停止的说明。
|
|
184
192
|
|
|
185
193
|
### 生图 / 生视频模型(「生成」标签页)
|
|
186
194
|
|
|
187
195
|
CPA 通过 `/v1/images/*` 和 `/v1/videos/*` 提供生图、生视频模型(如 Grok Imagine)。这些模型不能用于对话,所以插件会把它们识别出来,不放进对话模型选择器。
|
|
188
196
|
|
|
197
|
+
- **总开关**:「生成」标签顶部的「启用图片和视频生成」对所有账号生效。关闭时输入框旁不显示生成按钮,`/cpa-image`、`/cpa-video`、`/cpa-video-edit`、`/cpa-video-extend`、`/cpa-video-status` 不注册,agent 生成工具也不注册,「生成」标签只显示这个开关;已提交的视频照常完成,聊天记录里已生成的结果仍可查看(不提供编辑 / 延长按钮)。新安装默认关闭;已有 `media-settings.json` 或用过生成的配置升级后保持开启。不论开关如何,生成模型都不会进入对话模型选择器。下面介绍的功能都需要先打开它。
|
|
189
198
|
- **识别规则**(按顺序):你在「生成」标签里手动指定的类型;模型目录里的 `type`(`openai-image` 为生图)或输出模态(只输出图片为生图,输出视频为生视频);CPA 分流到图片 / 视频接口的模型族,按前缀匹配:以 `grok-imagine-image` 或 `gpt-image-` 开头为生图,以 `grok-imagine-video` 开头或 `sora-2*` 为生视频。其余都是对话模型(如 `gemini-3.1-flash-image`:CPA 只通过对话接口提供它,DSH 也不能显示对话中返回的图片)。只出现在 `/v1/models` 普通列表里的生成模型也会被收录。模型目录可能列出 CPA 生成接口暂不接受的变体(例如 `grok-imagine-video-1.5-lite`),它们同样不能对话,因此归为生成模型;用它们生成时会显示 CPA 返回的错误。
|
|
190
199
|
- **不进入模型选择器**:生成模型不会写入任何对话路由,也不参与首次推荐勾选;即使旧的勾选里含有它们,也不会被同步(勾选本身保持不变)。「模型」标签只列对话模型。
|
|
191
200
|
- **「生成」标签**(与 连接 / 模型 / 额度 并列):分「生图模型」「生视频模型」两组列出,修改立即保存。每个模型可以用开关决定是否提供给生成功能(新出现的模型默认开启),用类型下拉框改为生图 / 生视频 / 对话模型;手动指定的类型可以恢复为自动识别。识别漏掉的模型可以在「把其他模型标记为生成模型」里手动标记。改动类型会立即重新同步该账号的路由。`sora-2*` 标注为「→ grok-imagine-video」:CPA 会把它改写为 Grok 视频模型调用。
|
|
@@ -259,7 +268,7 @@ CPA 通过 `/v1/images/*` 和 `/v1/videos/*` 提供生图、生视频模型(
|
|
|
259
268
|
|
|
260
269
|
#### 让 agent 生成(`generate_media` 工具,默认关闭)
|
|
261
270
|
|
|
262
|
-
在「生成」标签打开 **允许 agent 生成图片 / 视频** 后,对话里的 agent 可以调用 `generate_media`
|
|
271
|
+
在「生成」标签打开 **允许 agent 生成图片 / 视频** 后,对话里的 agent 可以调用 `generate_media` 工具生图、生视频、编辑或延长视频。关闭时(默认)不注册这个工具,不占用模型上下文;打开后,且总开关「启用图片和视频生成」打开、至少有一个启用的生成模型时才注册,工具说明保持精简,不列模型清单。
|
|
263
272
|
|
|
264
273
|
- **能做什么**:参数为 `kind`(image / video)、`prompt`,可选 `mode`(generate / edit / extend)、`model`、`images`(本地图片路径:生图时改图 / 合成,生视频时 1 张为首帧、2 张以上为参考图)、`frames`(首帧 / 参考图 / 首尾帧 / 尾帧)、`video`(编辑 / 延长的原视频:本地 MP4 路径或公开链接,不写时用本会话最新生成的视频)、`aspect`、`seconds`、`resolution`、`count`。可选字段可以省略或填 `null`(兼容 Responses 严格参数模式);视频的 `count` 应填 `null`,一次只生成一个视频。没写 `model` 或填 `null` / 空字符串时,用输入框旁按钮里本会话选中的模型(子 agent 用其所属会话的选择)。处理方式与对应的斜杠命令相同,但提示词**原样发送**:不识别提示词里的「10秒」「竖屏」等词,也不请对话模型解读参数。工具说明同时要求 agent 使用用户自己的措辞和细节,不要扩写成更长的场景描写,也不要添加用户没有要求的镜头、光线、风格、人物或环境细节。
|
|
265
274
|
- **结果**:文件保存到保存位置,工具返回文件路径、模型和实际参数。生图时,如果当前对话模型能看图,还会把生成的图片(DSH 自动缩小)一起交给它。对话根据工具调用的原始记录显示一条**独立的生成行**,放在思考过程之外,和斜杠命令一样显示一问一答(进度、图片 / 可播放的视频、打开 / 显示文件夹 / 复制,视频下方同样有「编辑视频…」「延长视频…」);不需要展开思考过程,不再额外伪造用户命令,也不会重复显示已有对应生成行的工具卡片或旧版镜像命令卡片。
|
|
@@ -336,6 +345,8 @@ CPA 通过 `/v1/images/*` 和 `/v1/videos/*` 提供生图、生视频模型(
|
|
|
336
345
|
- `/cpa-video-status`:列出本会话的视频任务。
|
|
337
346
|
- `/cpa-video-edit [--video 路径 | --url 链接 | --from 命令id] [-m 模型] 修改说明`、`/cpa-video-extend [同上] [-s 2-10] 接下来的内容`:编辑 / 延长任意 MP4(可附带视频文件)。见 [编辑 / 延长视频](#编辑--延长视频cpa-video-editcpa-video-extend)。
|
|
338
347
|
|
|
348
|
+
`/cpa-image` 和 `/cpa-video*` 只在「生成」标签的「启用图片和视频生成」打开时存在。
|
|
349
|
+
|
|
339
350
|
### HTTP 接口
|
|
340
351
|
|
|
341
352
|
供设置面板使用,需要 DSH Web 会话的认证;POST 只接受来自本机或受信任主机的同源请求。
|
|
@@ -356,7 +367,7 @@ CPA 通过 `/v1/images/*` 和 `/v1/videos/*` 提供生图、生视频模型(
|
|
|
356
367
|
| POST | `/api/dsh-cliproxyapi/quota/refresh` | `{ accountId? }` | 开始刷新一个或全部账号的额度并立即返回(快照中 `refreshing` 为真) |
|
|
357
368
|
| POST | `/api/dsh-cliproxyapi/quota/save` | `{ accountId, managementKey?, clear? }` | 保存或清除该账号的管理密钥,并开始刷新额度;响应不含密钥 |
|
|
358
369
|
| GET | `/api/dsh-cliproxyapi/media/status` | — | `{ ok, media }`:生成设置与每个账号识别出的生图 / 生视频模型;不联网 |
|
|
359
|
-
| POST | `/api/dsh-cliproxyapi/media/save` | `{ account: { accountId, disabledModels?, kindOverrides? } }` |
|
|
370
|
+
| POST | `/api/dsh-cliproxyapi/media/save` | `{ enabled? }`、全局字段(如 `agentTool`、`outputDir`),或 `{ account: { accountId, disabledModels?, kindOverrides? } }` | `enabled` 打开 / 关闭生成总开关(随即注册或移除生成命令和 agent 工具);保存某账号关闭的生成模型(整表替换)或类型覆盖(按模型合并,`null` 表示恢复自动识别);类型变化时重新同步该账号并在响应中附带 `status`。无效值返回 400 和错误码,如 `invalid-enabled`、`invalid-account`、`invalid-kind` |
|
|
360
371
|
| POST | `/api/dsh-cliproxyapi/media/diagnose` | `{ accountId }` | 只读诊断(见上文),返回 `{ ok, diagnostic }`,不含 Key |
|
|
361
372
|
| GET | `/api/dsh-cliproxyapi/media/session?sessionId=` | — | `{ ok, session }`:可选的生图 / 生视频模型、当前选择及其来源(`session` / `last` / `only`)、本会话最近生成的文件(只有文件名和 id,不含路径);`pendingConfirmations` 仅列出缺少原生提问服务时、属于这个确切会话的待确认项 `{ commandId, kind, en, zh, expiresAt }`,原生提问不在其中;不联网 |
|
|
362
373
|
| POST | `/api/dsh-cliproxyapi/media/session/save` | `{ sessionId?, kind: "image" \| "video", ref }` | 为会话选择模型(`ref` 为 `账号id/模型id`,`null` 清除本会话的选择),并记为上一次选择;不在可选列表里的模型返回 400 `invalid-model`。也可以只传 `{ sessionId, videoConfirm?, imageConfirm?, videoBudget? }`(`ask` / `auto` / `inherit`,上限为 `{ mode: "limited", seconds }` / `{ mode: "unlimited" }` / `inherit`)修改本会话的 agent 生成设置 |
|
|
@@ -404,11 +415,12 @@ CPA 通过 `/v1/images/*` 和 `/v1/videos/*` 提供生图、生视频模型(
|
|
|
404
415
|
```
|
|
405
416
|
|
|
406
417
|
设置通过面板保存后立即应用。手动编辑此文件后需重载插件或重启 DSH 才会重新读取;文件损坏、结构 / 字段无效或无法读取时采用关闭自动联网检查的安全默认值。版本检查结果、候选版本、下载完成提示与命令只在内存中,插件重载后重新检查;下载包仍位于独立的 `updates` 子目录,由用户按需清理。
|
|
407
|
-
- **生成设置**:独立保存在 `<Profile 目录>/.dsh-cliproxyapi/media-settings.json`,不改动 `state.json
|
|
418
|
+
- **生成设置**:独立保存在 `<Profile 目录>/.dsh-cliproxyapi/media-settings.json`,不改动 `state.json`。没有此文件时使用默认值(`enabled: false`,生成关闭;但已存在 `media-sessions.json` 或 `media-jobs.json` 时视为开启);文件里没有 `enabled` 字段(总开关出现之前写入的文件)时视为开启。文件损坏时也使用默认值并在日志中提示,下次保存前会把损坏的文件另存为 `media-settings.corrupt-<时间>.json`。各字段:`enabled`(总开关)、`agentTool`(agent 生成工具)、`outputDir`(保存位置)、`defaultVideoSeconds` / `defaultVideoResolution`(视频默认值)、`imageConfirm` / `videoConfirm` / `videoBudget`(agent 生成的确认方式和秒数上限)、`lastSelection`(上一次选择的模型),以及每个账号的 `disabledModels`(关闭的生成模型)和 `kindOverrides`(手动指定的类型):
|
|
408
419
|
|
|
409
420
|
```json
|
|
410
421
|
{
|
|
411
422
|
"version": 1,
|
|
423
|
+
"enabled": true,
|
|
412
424
|
"agentTool": false,
|
|
413
425
|
"outputDir": "generated",
|
|
414
426
|
"defaultVideoSeconds": 6,
|
|
@@ -425,6 +437,7 @@ CPA 通过 `/v1/images/*` 和 `/v1/videos/*` 提供生图、生视频模型(
|
|
|
425
437
|
|
|
426
438
|
- **会话生成记录**:`<Profile 目录>/.dsh-cliproxyapi/media-sessions.json` 保存每个会话选择的生成模型、最近 48 次生成的记录(状态、模型、提示词、选项、起止时间、保存目录显示名、错误摘要),以及最近生成的最多 48 个文件路径和生成它们的命令 id(供对话里的命令行显示图片),只保留最近更新的 500 个会话。删除它只会清空这些选择和记录,不会删除已生成的文件。文件损坏时按空记录处理,下次写入前另存为 `media-sessions.corrupt-<时间>.json`。
|
|
427
439
|
- **视频任务**:`<Profile 目录>/.dsh-cliproxyapi/media-jobs.json` 保存已提交的视频任务(CPA 请求 id、账号、模型、会话和命令 id、工作目录与保存位置、提示词、时长、提交时间、状态),用于重载或重启后继续等待;最多 200 个,已结束的保留 7 天。不含 Key 和视频地址。文件损坏时按空处理,下次写入前另存为 `media-jobs.corrupt-<时间>.json`。
|
|
440
|
+
- **管理密钥失败次数**:`<Profile 目录>/.dsh-cliproxyapi/quota-state.json` 记录每个账号连续被 CPA 管理接口拒绝(401/403)的次数、最后的错误码和时间,用来在 3 次后停止自动查询;不含密钥。删除它只会清零计数。
|
|
428
441
|
- **密钥**:通过 DSH 的 `credentials` 服务按引用名存取;凭证服务里没有时,回退读取同名环境变量。
|
|
429
442
|
- **环境变量**:`CLIPROXYAPI_BASE_URL` 只用于服务地址留空的账号的自动发现。
|
|
430
443
|
- **插件行配置**:`cordis.patch.yml` 中本插件行的 `config` 会在启动时叠加到状态文件上:包含 `accounts` 时替换账号列表,否则其中的字段作用于第一个账号。一般保持 `{}`,用设置面板配置即可。
|
package/lib/client.js
CHANGED
|
@@ -309,8 +309,10 @@ window.__ModuleLoader__.load({
|
|
|
309
309
|
qDurDH: "{d}天{h}小时",
|
|
310
310
|
qDurHM: "{h}小时{m}分",
|
|
311
311
|
qDurM: "{m}分钟",
|
|
312
|
-
qErrUnauthorized: "管理密钥无效(401
|
|
312
|
+
qErrUnauthorized: "管理密钥无效(401),请检查后重新保存。",
|
|
313
313
|
qErrForbidden: "CPA 拒绝了管理请求(403):请确认 CPA 已开启 remote-management.allow-remote,且密钥正确。",
|
|
314
|
+
qAuthRetrying: "已连续被拒绝 {n}/{max} 次;连续 {max} 次后停止自动查询。",
|
|
315
|
+
qAuthPaused: "已连续被拒绝 {n} 次,自动查询已停止,以免 CPA 因错误密钥封禁本机地址。保存新的管理密钥后恢复;手动刷新只会再试一次。",
|
|
314
316
|
qErrNetwork: "无法连接 CPA 管理接口,请检查服务地址与网络。",
|
|
315
317
|
qErrTimeout: "CPA 管理接口响应超时,请稍后重试。",
|
|
316
318
|
qErrHttp: "CPA 管理接口返回错误(HTTP {status})。",
|
|
@@ -484,6 +486,10 @@ window.__ModuleLoader__.load({
|
|
|
484
486
|
mediaConfirmAllowed: "已允许,开始生成",
|
|
485
487
|
mediaConfirmDeclined: "已拒绝",
|
|
486
488
|
mediaConfirmFailed: "没有送达:{error}",
|
|
489
|
+
mediaMasterTitle: "启用图片和视频生成",
|
|
490
|
+
mediaMasterHint: "对所有账号生效。关闭时输入框旁不显示生成按钮,没有 /cpa-image、/cpa-video 等生成命令,也不注册 agent 生成工具;已提交的视频会继续完成,聊天记录里已生成的结果仍可查看。",
|
|
491
|
+
mediaMasterOnNote: "已开启:可以用输入框旁的按钮、斜杠命令生成图片和视频。",
|
|
492
|
+
mediaMasterOffNote: "已关闭:界面上不显示生成功能,也没有生成命令。打开后才会出现下面的模型和设置。",
|
|
487
493
|
mediaAgentTitle: "允许 agent 生成图片 / 视频",
|
|
488
494
|
mediaAgentHint: "打开后,对话里的 agent 可以调用 generate_media 工具,用上面启用的生成模型生图、生视频、编辑或延长视频,文件保存到会话工作目录下的保存位置。关闭时不注册这个工具,不占用上下文。斜杠命令和输入框旁的按钮不受影响。",
|
|
489
495
|
mediaAgentOnNote: "已开启:agent 可以调用 generate_media。确认方式和秒数上限可在输入框旁的生成按钮里按会话单独修改。",
|
|
@@ -805,8 +811,10 @@ window.__ModuleLoader__.load({
|
|
|
805
811
|
qDurDH: "{d}d {h}h",
|
|
806
812
|
qDurHM: "{h}h {m}m",
|
|
807
813
|
qDurM: "{m}m",
|
|
808
|
-
qErrUnauthorized: "The management key was rejected (401).
|
|
814
|
+
qErrUnauthorized: "The management key was rejected (401). Check it and save it again.",
|
|
809
815
|
qErrForbidden: "CPA refused the management request (403). Make sure remote-management.allow-remote is on and the key is correct.",
|
|
816
|
+
qAuthRetrying: "Rejected {n}/{max} times in a row; automatic checks stop after {max}.",
|
|
817
|
+
qAuthPaused: "Rejected {n} times in a row, so automatic checks have stopped to keep CPA from banning this address for bad keys. Saving a new management key resumes them; a manual refresh tries only once.",
|
|
810
818
|
qErrNetwork: "Could not reach the CPA management API. Check the server address and network.",
|
|
811
819
|
qErrTimeout: "The CPA management API timed out. Retry shortly.",
|
|
812
820
|
qErrHttp: "The CPA management API returned an error (HTTP {status}).",
|
|
@@ -980,6 +988,10 @@ window.__ModuleLoader__.load({
|
|
|
980
988
|
mediaConfirmAllowed: "Allowed; generating",
|
|
981
989
|
mediaConfirmDeclined: "Declined",
|
|
982
990
|
mediaConfirmFailed: "Not delivered: {error}",
|
|
991
|
+
mediaMasterTitle: "Enable image and video generation",
|
|
992
|
+
mediaMasterHint: "Applies to every account. While off, the composer shows no generation buttons, /cpa-image, /cpa-video and the other generation commands do not exist, and no agent generation tool is registered. Videos already submitted still finish, and past results in the chat stay viewable.",
|
|
993
|
+
mediaMasterOnNote: "On: generate images and videos with the composer buttons and slash commands.",
|
|
994
|
+
mediaMasterOffNote: "Off: no generation features appear and no generation commands exist. Turn it on to see the models and settings below.",
|
|
983
995
|
mediaAgentTitle: "Let the agent generate images / videos",
|
|
984
996
|
mediaAgentHint: "When on, the chat agent can call the generate_media tool to generate images and videos, or edit / extend videos, with the generation models enabled above; files go to the save location under the session's working directory. When off the tool is not registered and takes no context. Slash commands and the composer button work either way.",
|
|
985
997
|
mediaAgentOnNote: "On: the agent can call generate_media. Confirmation and the seconds limit can be changed per session in the generation button next to the composer.",
|
|
@@ -4745,6 +4757,7 @@ select.dcpa-q-input.dcpa-media-kind { flex: none; width: auto; height: 28px; fon
|
|
|
4745
4757
|
if (!mounted.current) return;
|
|
4746
4758
|
setData(body.media ?? null);
|
|
4747
4759
|
after?.();
|
|
4760
|
+
if ("enabled" in patch) announceMediaGeneration();
|
|
4748
4761
|
setNotice({ tone: "ok", text: t("mediaSaved") });
|
|
4749
4762
|
} catch (err) {
|
|
4750
4763
|
if (mounted.current) setNotice({ tone: "error", text: mediaErrorText(t, err) });
|
|
@@ -5005,13 +5018,47 @@ select.dcpa-q-input.dcpa-media-kind { flex: none; width: auto; height: 28px; fon
|
|
|
5005
5018
|
return h("div", { className: "dcpa-tabbody dcpa-models dcpa-media" }, h("p", { className: "dcpa-q-sub" }, t("mediaLoading")));
|
|
5006
5019
|
}
|
|
5007
5020
|
|
|
5021
|
+
// The master switch (all accounts): off hides every generation feature.
|
|
5022
|
+
const generationOn = data?.settings?.enabled === true;
|
|
5023
|
+
const masterCard = h(
|
|
5024
|
+
"div",
|
|
5025
|
+
{ className: "dcpa-media-card", "data-generation": generationOn ? "on" : "off" },
|
|
5026
|
+
h(
|
|
5027
|
+
"div",
|
|
5028
|
+
{ className: "dcpa-media-agent-head" },
|
|
5029
|
+
h("span", { className: "dcpa-frow-label" }, t("mediaMasterTitle"), h(InfoTip, { t, text: t("mediaMasterHint") })),
|
|
5030
|
+
h(
|
|
5031
|
+
"button",
|
|
5032
|
+
{
|
|
5033
|
+
type: "button",
|
|
5034
|
+
role: "switch",
|
|
5035
|
+
className: "dcpa-switch",
|
|
5036
|
+
"aria-checked": generationOn,
|
|
5037
|
+
"aria-label": t("mediaMasterTitle"),
|
|
5038
|
+
disabled: busy !== null || data === null,
|
|
5039
|
+
onClick: () => void saveGlobal({ enabled: !generationOn }),
|
|
5040
|
+
},
|
|
5041
|
+
h("span", { className: "dcpa-switch-thumb" })
|
|
5042
|
+
)
|
|
5043
|
+
),
|
|
5044
|
+
h("p", { className: "dcpa-q-sub" }, t(generationOn ? "mediaMasterOnNote" : "mediaMasterOffNote"))
|
|
5045
|
+
);
|
|
5046
|
+
const messages = [
|
|
5047
|
+
error ? h("p", { key: "err", className: "dcpa-q-msg", "data-tone": "error", role: "alert" }, fmt(t, "mediaLoadError", { error })) : null,
|
|
5048
|
+
data?.settingsError ? h("p", { key: "invalid", className: "dcpa-q-msg", "data-tone": "error", role: "alert" }, t("mediaSettingsInvalid")) : null,
|
|
5049
|
+
notice ? h("p", { key: "notice", className: "dcpa-q-msg", "data-tone": notice.tone, role: notice.tone === "error" ? "alert" : "status" }, notice.text) : null,
|
|
5050
|
+
];
|
|
5051
|
+
|
|
5052
|
+
if (!generationOn) {
|
|
5053
|
+
return h("div", { className: "dcpa-tabbody dcpa-models dcpa-media" }, masterCard, ...messages);
|
|
5054
|
+
}
|
|
5055
|
+
|
|
5008
5056
|
return h(
|
|
5009
5057
|
"div",
|
|
5010
5058
|
{ className: "dcpa-tabbody dcpa-models dcpa-media" },
|
|
5059
|
+
masterCard,
|
|
5011
5060
|
h("p", { className: "dcpa-q-sub" }, t("mediaIntro")),
|
|
5012
|
-
|
|
5013
|
-
data?.settingsError ? h("p", { className: "dcpa-q-msg", "data-tone": "error", role: "alert" }, t("mediaSettingsInvalid")) : null,
|
|
5014
|
-
notice ? h("p", { className: "dcpa-q-msg", "data-tone": notice.tone, role: notice.tone === "error" ? "alert" : "status" }, notice.text) : null,
|
|
5061
|
+
...messages,
|
|
5015
5062
|
mine && models.length === 0
|
|
5016
5063
|
? h("p", { className: "dcpa-hint" }, t(catalogRead ? "mediaNone" : "mediaNoCatalog"))
|
|
5017
5064
|
: null,
|
|
@@ -5215,7 +5262,12 @@ select.dcpa-q-input.dcpa-media-kind { flex: none; width: auto; height: 28px; fon
|
|
|
5215
5262
|
function quotaErrorText(t, account) {
|
|
5216
5263
|
const key = QUOTA_ERROR_KEYS[account?.errorCode];
|
|
5217
5264
|
if (key === undefined) return null;
|
|
5218
|
-
|
|
5265
|
+
const text = fmt(t, key, { status: account.errorStatus ?? "?" });
|
|
5266
|
+
// Consecutive management-key rejections: the count, or that background checks stopped.
|
|
5267
|
+
const n = account.authFailures;
|
|
5268
|
+
if ((account.errorCode !== "unauthorized" && account.errorCode !== "forbidden") || !Number.isInteger(n) || n <= 0) return text;
|
|
5269
|
+
const max = account.authFailureLimit ?? 3;
|
|
5270
|
+
return `${text} ${fmt(t, account.authPaused ? "qAuthPaused" : "qAuthRetrying", { n, max })}`;
|
|
5219
5271
|
}
|
|
5220
5272
|
|
|
5221
5273
|
function windowLabel(t, w) {
|
|
@@ -5527,6 +5579,16 @@ select.dcpa-q-input.dcpa-media-kind { flex: none; width: auto; height: 28px; fon
|
|
|
5527
5579
|
const composerBadges = createBadgePresence();
|
|
5528
5580
|
// The generation-model button has its own composer/hero presence.
|
|
5529
5581
|
const mediaChips = createBadgePresence();
|
|
5582
|
+
// The Generation tab tells the composer buttons when the master switch
|
|
5583
|
+
// changed, so they appear / disappear without waiting for a retry.
|
|
5584
|
+
const mediaGenerationListeners = new Set();
|
|
5585
|
+
const announceMediaGeneration = () => {
|
|
5586
|
+
for (const fn of [...mediaGenerationListeners]) {
|
|
5587
|
+
try {
|
|
5588
|
+
fn();
|
|
5589
|
+
} catch {}
|
|
5590
|
+
}
|
|
5591
|
+
};
|
|
5530
5592
|
|
|
5531
5593
|
/**
|
|
5532
5594
|
* Composer placement announces itself for its session; hero placement
|
|
@@ -6293,8 +6355,10 @@ select.dcpa-q-input.dcpa-media-kind { flex: none; width: auto; height: 28px; fon
|
|
|
6293
6355
|
if (controller.signal.aborted) return;
|
|
6294
6356
|
if (!res.ok || body.ok === false) throw new Error(body.error || `HTTP ${res.status}`);
|
|
6295
6357
|
setData(body.session ?? null);
|
|
6296
|
-
|
|
6297
|
-
|
|
6358
|
+
// Generation switched off: nothing to show and nothing to wait for
|
|
6359
|
+
// (the Generation tab announces when it is switched on again).
|
|
6360
|
+
needsRetry = body.session?.generation !== false && (body.session?.catalogIncomplete === true ||
|
|
6361
|
+
!["image", "video"].some((kind) => Array.isArray(body.session?.options?.[kind]) && body.session.options[kind].length > 0));
|
|
6298
6362
|
} catch (err) {
|
|
6299
6363
|
if (!controller.signal.aborted) setError(err instanceof Error ? err.message : String(err));
|
|
6300
6364
|
} finally {
|
|
@@ -6309,6 +6373,19 @@ select.dcpa-q-input.dcpa-media-kind { flex: none; width: auto; height: 28px; fon
|
|
|
6309
6373
|
};
|
|
6310
6374
|
}, [sessionId, shadowed, openKind, retry]);
|
|
6311
6375
|
|
|
6376
|
+
// The master switch changed on the Generation tab: load again now.
|
|
6377
|
+
useEffect(() => {
|
|
6378
|
+
const reload = () => {
|
|
6379
|
+
if (!mounted.current) return;
|
|
6380
|
+
closeMenu();
|
|
6381
|
+
setRetry((n) => n + 1);
|
|
6382
|
+
};
|
|
6383
|
+
mediaGenerationListeners.add(reload);
|
|
6384
|
+
return () => {
|
|
6385
|
+
mediaGenerationListeners.delete(reload);
|
|
6386
|
+
};
|
|
6387
|
+
}, []);
|
|
6388
|
+
|
|
6312
6389
|
useEffect(() => {
|
|
6313
6390
|
if (openKind === null) return undefined;
|
|
6314
6391
|
const place = () => {
|
package/lib/index.js
CHANGED
|
@@ -2156,7 +2156,11 @@ export function renderQuotaText(snapshot, onlyAccountId) {
|
|
|
2156
2156
|
continue;
|
|
2157
2157
|
}
|
|
2158
2158
|
const creds = Array.isArray(a.credentials) ? a.credentials : [];
|
|
2159
|
-
const state = a.state + (a.errorCode ? ` (${a.errorCode}${a.errorStatus ? ` ${a.errorStatus}` : ""})` : "") +
|
|
2159
|
+
const state = a.state + (a.errorCode ? ` (${a.errorCode}${a.errorStatus ? ` ${a.errorStatus}` : ""})` : "") +
|
|
2160
|
+
(Number.isInteger(a.authFailures) && a.authFailures > 0
|
|
2161
|
+
? `, key rejected ${a.authFailures}/${a.authFailureLimit}${a.authPaused ? ", automatic checks stopped until a new management key is saved" : ""}`
|
|
2162
|
+
: "") +
|
|
2163
|
+
(a.refreshing ? ", refreshing" : "");
|
|
2160
2164
|
lines.push(`- ${a.displayName} [${a.accountId}]: ${state}; ${creds.length} credential(s)${a.fetchedAt ? `; fetched ${a.fetchedAt}` : ""}; routes ${(a.routes ?? []).join(", ") || "none"}`);
|
|
2161
2165
|
for (const m of a.pickerModels ?? []) {
|
|
2162
2166
|
const ids = a.modelIndex?.[m.id] ?? [];
|
|
@@ -2356,8 +2360,16 @@ function applyImpl(ctx, config) {
|
|
|
2356
2360
|
// account state, never inside it, so no state migration is involved.
|
|
2357
2361
|
const mediaSettingsPath = join(updateDirectory, "media-settings.json");
|
|
2358
2362
|
let mediaWarned = false;
|
|
2363
|
+
// Generation is off for a new profile; one that already picked a model or
|
|
2364
|
+
// generated something before the switch existed (session or job file, no
|
|
2365
|
+
// readable settings file) keeps it on.
|
|
2366
|
+
const mediaUsedBefore = () =>
|
|
2367
|
+
existsSync(join(updateDirectory, "media-sessions.json")) || existsSync(join(updateDirectory, "media-jobs.json"));
|
|
2359
2368
|
const readMedia = () => {
|
|
2360
2369
|
const loaded = loadMediaSettings(mediaSettingsPath);
|
|
2370
|
+
if ((loaded.error !== undefined || !existsSync(mediaSettingsPath)) && mediaUsedBefore()) {
|
|
2371
|
+
loaded.settings = { ...loaded.settings, enabled: true };
|
|
2372
|
+
}
|
|
2361
2373
|
if (loaded.error === undefined) {
|
|
2362
2374
|
mediaWarned = false;
|
|
2363
2375
|
} else if (!mediaWarned) {
|
|
@@ -2396,7 +2408,19 @@ function applyImpl(ctx, config) {
|
|
|
2396
2408
|
|
|
2397
2409
|
// Quota ("额度" tab + composer badge): management keys live in the
|
|
2398
2410
|
// credentials service under their own refs, never in account state.
|
|
2411
|
+
// Consecutive management-key rejections per account (never the key itself),
|
|
2412
|
+
// so a wrong key stays stopped across reloads and restarts.
|
|
2413
|
+
const quotaStatePath = join(updateDirectory, "quota-state.json");
|
|
2399
2414
|
const quota = createQuotaController({
|
|
2415
|
+
authStore: {
|
|
2416
|
+
read: () => (existsSync(quotaStatePath) ? JSON.parse(readFileSync(quotaStatePath, "utf8")) : undefined),
|
|
2417
|
+
write: (value) => {
|
|
2418
|
+
mkdirSync(dirname(quotaStatePath), { recursive: true });
|
|
2419
|
+
const tmp = `${quotaStatePath}.${Date.now().toString(36)}.tmp`;
|
|
2420
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
2421
|
+
renameSync(tmp, quotaStatePath);
|
|
2422
|
+
},
|
|
2423
|
+
},
|
|
2400
2424
|
listAccounts: () => controller.quotaAccounts(),
|
|
2401
2425
|
resolveKey: (ref) => resolveApiKey(ref),
|
|
2402
2426
|
describeKey: (ref) => describeApiKey(ref),
|
|
@@ -2453,13 +2477,18 @@ function applyImpl(ctx, config) {
|
|
|
2453
2477
|
// Per-session model picks and recent outputs, in their own file.
|
|
2454
2478
|
const mediaSessions = createMediaSessionStore(join(updateDirectory, "media-sessions.json"));
|
|
2455
2479
|
|
|
2480
|
+
// With generation switched off nothing is offered: no composer buttons, no
|
|
2481
|
+
// agent tool, and no command can pick a model.
|
|
2456
2482
|
const mediaChoices = () => {
|
|
2457
2483
|
const { settings } = readMedia();
|
|
2458
2484
|
const accounts = controller.mediaAccounts();
|
|
2485
|
+
const on = settings.enabled === true;
|
|
2459
2486
|
return {
|
|
2460
2487
|
settings,
|
|
2461
2488
|
accounts,
|
|
2462
|
-
options:
|
|
2489
|
+
options: on
|
|
2490
|
+
? { image: mediaOptions(accounts, settings, "image"), video: mediaOptions(accounts, settings, "video") }
|
|
2491
|
+
: { image: [], video: [] },
|
|
2463
2492
|
};
|
|
2464
2493
|
};
|
|
2465
2494
|
|
|
@@ -2474,6 +2503,7 @@ function applyImpl(ctx, config) {
|
|
|
2474
2503
|
let catalogRetryAt = 0;
|
|
2475
2504
|
const mediaChoicesFor = async (kind) => {
|
|
2476
2505
|
let choices = mediaChoices();
|
|
2506
|
+
if (choices.settings.enabled !== true) return choices;
|
|
2477
2507
|
const missing = (k) => choices.options[k].length === 0;
|
|
2478
2508
|
const someMissing = kind === "any" ? missing("image") || missing("video") : missing(kind);
|
|
2479
2509
|
const cold = choices.accounts.some((a) => a.enabled && a.discovered === 0);
|
|
@@ -2513,6 +2543,8 @@ function applyImpl(ctx, config) {
|
|
|
2513
2543
|
const policy = agentPolicy(settings, entry);
|
|
2514
2544
|
return {
|
|
2515
2545
|
sessionId: sessionId ?? null,
|
|
2546
|
+
// The Generation tab's master switch; off = the composer shows nothing.
|
|
2547
|
+
generation: settings.enabled === true,
|
|
2516
2548
|
options,
|
|
2517
2549
|
current,
|
|
2518
2550
|
// Compatibility composer fallback; native questions never enter this map.
|
|
@@ -2522,7 +2554,7 @@ function applyImpl(ctx, config) {
|
|
|
2522
2554
|
// True while an enabled account's last catalog read missed the plain
|
|
2523
2555
|
// listing; the composer button retries instead of keeping an old,
|
|
2524
2556
|
// image-less list.
|
|
2525
|
-
catalogIncomplete: accounts.some((a) => a.enabled === true && a.catalogIncomplete === true),
|
|
2557
|
+
catalogIncomplete: settings.enabled === true && accounts.some((a) => a.enabled === true && a.catalogIncomplete === true),
|
|
2526
2558
|
// The agent tool's rules for this session (shown while the tool is switched on).
|
|
2527
2559
|
agent: {
|
|
2528
2560
|
enabled: settings.agentTool === true,
|
|
@@ -3608,6 +3640,9 @@ function applyImpl(ctx, config) {
|
|
|
3608
3640
|
const TOOL_PREVIEWS = 4;
|
|
3609
3641
|
let toolsCtx;
|
|
3610
3642
|
let toolOff;
|
|
3643
|
+
// The generation slash commands (registered only while generation is on).
|
|
3644
|
+
let commandsCtx;
|
|
3645
|
+
let mediaCommandsOff;
|
|
3611
3646
|
|
|
3612
3647
|
const gateError = (message) => Object.assign(new Error(message), { gateError: true });
|
|
3613
3648
|
|
|
@@ -3964,6 +3999,37 @@ function applyImpl(ctx, config) {
|
|
|
3964
3999
|
}
|
|
3965
4000
|
ctx.logger?.info?.("cpa: generate_media tool unregistered");
|
|
3966
4001
|
}
|
|
4002
|
+
syncMediaCommands();
|
|
4003
|
+
};
|
|
4004
|
+
|
|
4005
|
+
/**
|
|
4006
|
+
* Register or unregister /cpa-image and the /cpa-video* commands to match
|
|
4007
|
+
* the Generation tab's master switch. Jobs already submitted keep running.
|
|
4008
|
+
*/
|
|
4009
|
+
const syncMediaCommands = () => {
|
|
4010
|
+
let wanted = false;
|
|
4011
|
+
try {
|
|
4012
|
+
wanted = readMedia().settings.enabled === true;
|
|
4013
|
+
} catch {
|
|
4014
|
+
wanted = false;
|
|
4015
|
+
}
|
|
4016
|
+
if (wanted && mediaCommandsOff === undefined && commandsCtx !== undefined) {
|
|
4017
|
+
try {
|
|
4018
|
+
mediaCommandsOff = registerMediaCommands(commandsCtx);
|
|
4019
|
+
ctx.logger?.info?.("cpa: generation commands registered");
|
|
4020
|
+
} catch (error) {
|
|
4021
|
+
ctx.logger?.warn?.(`cpa: could not register the generation commands: ${error instanceof Error ? error.message : String(error)}`);
|
|
4022
|
+
}
|
|
4023
|
+
} else if (!wanted && mediaCommandsOff !== undefined) {
|
|
4024
|
+
const off = mediaCommandsOff;
|
|
4025
|
+
mediaCommandsOff = undefined;
|
|
4026
|
+
try {
|
|
4027
|
+
off();
|
|
4028
|
+
} catch {
|
|
4029
|
+
// already gone with its scope
|
|
4030
|
+
}
|
|
4031
|
+
ctx.logger?.info?.("cpa: generation commands unregistered");
|
|
4032
|
+
}
|
|
3967
4033
|
};
|
|
3968
4034
|
// #endregion
|
|
3969
4035
|
|
|
@@ -3977,6 +4043,8 @@ function applyImpl(ctx, config) {
|
|
|
3977
4043
|
);
|
|
3978
4044
|
|
|
3979
4045
|
ctx.on("credentials/reference-updated", (ref) => {
|
|
4046
|
+
// A management key changed (also from the DSH credentials page): start a new failure count.
|
|
4047
|
+
quota.keyChanged(ref);
|
|
3980
4048
|
for (const id of controller.accountsUsingKey(ref)) {
|
|
3981
4049
|
void controller.refreshAccount(id);
|
|
3982
4050
|
}
|
|
@@ -4013,6 +4081,26 @@ function applyImpl(ctx, config) {
|
|
|
4013
4081
|
return { kind: failed ? "error" : "success", text: renderStatusText(payload) };
|
|
4014
4082
|
},
|
|
4015
4083
|
});
|
|
4084
|
+
commandsCtx = cmdCtx;
|
|
4085
|
+
syncMediaCommands();
|
|
4086
|
+
return () => {
|
|
4087
|
+
offStatus?.();
|
|
4088
|
+
offRefresh?.();
|
|
4089
|
+
commandsCtx = undefined;
|
|
4090
|
+
const off = mediaCommandsOff;
|
|
4091
|
+
mediaCommandsOff = undefined;
|
|
4092
|
+
try {
|
|
4093
|
+
off?.();
|
|
4094
|
+
} catch {
|
|
4095
|
+
// disposed with its scope
|
|
4096
|
+
}
|
|
4097
|
+
};
|
|
4098
|
+
}, "cpa: commands");
|
|
4099
|
+
});
|
|
4100
|
+
|
|
4101
|
+
/** The generation commands, registered only while generation is switched on (see syncMediaCommands). */
|
|
4102
|
+
function registerMediaCommands(cmdCtx) {
|
|
4103
|
+
{
|
|
4016
4104
|
const offVideo = cmdCtx.commands.register({
|
|
4017
4105
|
name: "cpa-video",
|
|
4018
4106
|
description: "Generate a video with a CLIProxyAPI video model: text only, one image as the first frame, reference images (--ref, or 2+ images), or pinned first / last frames (--first-last, --last). A submitted video cannot be cancelled.",
|
|
@@ -4043,16 +4131,14 @@ function applyImpl(ctx, config) {
|
|
|
4043
4131
|
handler: runImageCommand,
|
|
4044
4132
|
});
|
|
4045
4133
|
return () => {
|
|
4046
|
-
offStatus?.();
|
|
4047
|
-
offRefresh?.();
|
|
4048
4134
|
offImage?.();
|
|
4049
4135
|
offVideo?.();
|
|
4050
4136
|
offVideoStatus?.();
|
|
4051
4137
|
offVideoEdit?.();
|
|
4052
4138
|
offVideoExtend?.();
|
|
4053
4139
|
};
|
|
4054
|
-
}
|
|
4055
|
-
}
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4056
4142
|
|
|
4057
4143
|
// The optional agent tool (registered only while switched on; see syncAgentTool).
|
|
4058
4144
|
ctx.inject(["tools"], (scope) => {
|
|
@@ -4230,8 +4316,9 @@ function applyImpl(ctx, config) {
|
|
|
4230
4316
|
const desktop = process.platform === "win32" ? "explorer" : process.platform === "darwin" ? "finder" : "filemanager";
|
|
4231
4317
|
// Edit / extend this video: by xAI's short-lived link while it is
|
|
4232
4318
|
// remembered, else by sending its file inline when small enough.
|
|
4319
|
+
// Past results stay visible with generation off, without the edit / extend actions.
|
|
4233
4320
|
let actions;
|
|
4234
|
-
if (run?.kind === "video" && run.status === "done") {
|
|
4321
|
+
if (run?.kind === "video" && run.status === "done" && readMedia().settings.enabled === true) {
|
|
4235
4322
|
const source = usableSource(commandId);
|
|
4236
4323
|
const video = files.find((f) => f.kind === "video");
|
|
4237
4324
|
let size;
|
package/lib/media.js
CHANGED
|
@@ -97,9 +97,15 @@ export class MediaError extends Error {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Defaults for a profile without `media-settings.json`: generation is off
|
|
102
|
+
* until the user switches it on (an older file without `enabled` reads as on,
|
|
103
|
+
* see {@link normalizeMediaSettings}).
|
|
104
|
+
*/
|
|
100
105
|
export function defaultMediaSettings() {
|
|
101
106
|
return {
|
|
102
107
|
version: MEDIA_SETTINGS_VERSION,
|
|
108
|
+
enabled: false,
|
|
103
109
|
agentTool: false,
|
|
104
110
|
outputDir: "generated",
|
|
105
111
|
defaultVideoSeconds: 6,
|
|
@@ -182,7 +188,9 @@ function normalizeAccountMedia(value) {
|
|
|
182
188
|
|
|
183
189
|
/**
|
|
184
190
|
* Tolerant normalization: every invalid or missing field takes its default,
|
|
185
|
-
* unknown fields are dropped. The result always has `version: 1`.
|
|
191
|
+
* unknown fields are dropped. The result always has `version: 1`. A missing
|
|
192
|
+
* `enabled` means a file written before the switch existed (by someone who
|
|
193
|
+
* already used generation), so it reads as on.
|
|
186
194
|
*/
|
|
187
195
|
export function normalizeMediaSettings(raw) {
|
|
188
196
|
const input = plain(raw) ?? {};
|
|
@@ -199,6 +207,7 @@ export function normalizeMediaSettings(raw) {
|
|
|
199
207
|
}
|
|
200
208
|
return {
|
|
201
209
|
version: MEDIA_SETTINGS_VERSION,
|
|
210
|
+
enabled: typeof input.enabled === "boolean" ? input.enabled : true,
|
|
202
211
|
agentTool: input.agentTool === true,
|
|
203
212
|
outputDir: validOutputDir(input.outputDir) ?? defaults.outputDir,
|
|
204
213
|
defaultVideoSeconds: intIn(input.defaultVideoSeconds, VIDEO_SECONDS) ?? defaults.defaultVideoSeconds,
|
|
@@ -241,6 +250,7 @@ export function saveMediaSettings(filePath, settings) {
|
|
|
241
250
|
}
|
|
242
251
|
|
|
243
252
|
const PATCH_FIELDS = [
|
|
253
|
+
"enabled",
|
|
244
254
|
"agentTool",
|
|
245
255
|
"outputDir",
|
|
246
256
|
"defaultVideoSeconds",
|
|
@@ -265,6 +275,10 @@ export function applyMediaPatch(current, patch) {
|
|
|
265
275
|
const unknown = Object.keys(input).filter((key) => !PATCH_FIELDS.includes(key) && key !== "accountId");
|
|
266
276
|
if (unknown.length > 0) throw new MediaError("invalid-request", `unknown field(s): ${unknown.join(", ")}`);
|
|
267
277
|
const next = normalizeMediaSettings(current);
|
|
278
|
+
if ("enabled" in input) {
|
|
279
|
+
if (typeof input.enabled !== "boolean") throw new MediaError("invalid-enabled");
|
|
280
|
+
next.enabled = input.enabled;
|
|
281
|
+
}
|
|
268
282
|
if ("agentTool" in input) {
|
|
269
283
|
if (typeof input.agentTool !== "boolean") throw new MediaError("invalid-agent-tool");
|
|
270
284
|
next.agentTool = input.agentTool;
|
package/lib/quota.js
CHANGED
|
@@ -16,8 +16,39 @@ export const DEFAULT_QUOTA_TIMEOUT_MS = 15_000;
|
|
|
16
16
|
export const DEFAULT_QUOTA_CONCURRENCY = 4;
|
|
17
17
|
export const MAX_QUOTA_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
18
18
|
|
|
19
|
+
// Consecutive management 401/403 answers after which automatic refresh stops
|
|
20
|
+
// (CPA bans the client address for 30 minutes after 5 failed attempts).
|
|
21
|
+
export const AUTH_FAILURE_LIMIT = 3;
|
|
22
|
+
export const QUOTA_AUTH_STATE_VERSION = 1;
|
|
23
|
+
|
|
19
24
|
const MANAGEMENT_KEY_REF = "CLI_PROXY_MANAGEMENT_KEY";
|
|
20
25
|
const STATUS_MESSAGE_MAX = 160;
|
|
26
|
+
const AUTH_ERROR_CODES = ["unauthorized", "forbidden"];
|
|
27
|
+
const AUTH_ACCOUNT_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The persisted management-key failure counts (`quota-state.json`):
|
|
31
|
+
* `{ version: 1, accounts: { [id]: { authFailures, errorCode, errorStatus?, failedAt } } }`.
|
|
32
|
+
* Never holds a key. Invalid entries are dropped; accounts without failures are not kept.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeQuotaAuthState(raw) {
|
|
35
|
+
const accounts = {};
|
|
36
|
+
const input = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw.accounts : undefined;
|
|
37
|
+
if (input !== null && typeof input === "object" && !Array.isArray(input)) {
|
|
38
|
+
for (const [id, entry] of Object.entries(input)) {
|
|
39
|
+
if (!AUTH_ACCOUNT_ID.test(id) || entry === null || typeof entry !== "object") continue;
|
|
40
|
+
const failures = Number.isInteger(entry.authFailures) && entry.authFailures > 0 ? Math.min(entry.authFailures, 1000) : 0;
|
|
41
|
+
if (failures === 0) continue;
|
|
42
|
+
accounts[id] = {
|
|
43
|
+
authFailures: failures,
|
|
44
|
+
errorCode: AUTH_ERROR_CODES.includes(entry.errorCode) ? entry.errorCode : "unauthorized",
|
|
45
|
+
...(Number.isInteger(entry.errorStatus) ? { errorStatus: entry.errorStatus } : {}),
|
|
46
|
+
...(typeof entry.failedAt === "string" && entry.failedAt.length <= 40 ? { failedAt: entry.failedAt } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { version: QUOTA_AUTH_STATE_VERSION, accounts };
|
|
51
|
+
}
|
|
21
52
|
|
|
22
53
|
const UPSTREAM = {
|
|
23
54
|
codexUsage: "https://chatgpt.com/backend-api/wham/usage",
|
|
@@ -1053,14 +1084,53 @@ export function createQuotaController(deps) {
|
|
|
1053
1084
|
const agPreferred = new Map();
|
|
1054
1085
|
let disposed = false;
|
|
1055
1086
|
|
|
1087
|
+
// Consecutive management 401/403 counts survive reloads through
|
|
1088
|
+
// `deps.authStore` ({ read(), write(state) }); without one they live in memory.
|
|
1089
|
+
const readAuth = () => {
|
|
1090
|
+
try {
|
|
1091
|
+
return normalizeQuotaAuthState(deps.authStore?.read?.());
|
|
1092
|
+
} catch {
|
|
1093
|
+
return normalizeQuotaAuthState(undefined);
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
const writeAuthEntry = (id, entry) => {
|
|
1097
|
+
if (deps.authStore?.write === undefined) return;
|
|
1098
|
+
try {
|
|
1099
|
+
const next = readAuth();
|
|
1100
|
+
if (entry === undefined) {
|
|
1101
|
+
if (next.accounts[id] === undefined) return;
|
|
1102
|
+
delete next.accounts[id];
|
|
1103
|
+
} else {
|
|
1104
|
+
next.accounts[id] = entry;
|
|
1105
|
+
}
|
|
1106
|
+
deps.authStore.write(normalizeQuotaAuthState(next));
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
log("warn", `dsh-cliproxyapi: could not save the quota key failure count: ${error instanceof Error ? error.message : String(error)}`);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1056
1112
|
const stateFor = (id) => {
|
|
1057
1113
|
let st = states.get(id);
|
|
1058
1114
|
if (st === undefined) {
|
|
1059
|
-
|
|
1115
|
+
const saved = readAuth().accounts[id];
|
|
1116
|
+
st = {
|
|
1117
|
+
gen: 0,
|
|
1118
|
+
inflight: null,
|
|
1119
|
+
result: saved === undefined ? null : { state: "error", errorCode: saved.errorCode, ...(saved.errorStatus === undefined ? {} : { errorStatus: saved.errorStatus }) },
|
|
1120
|
+
data: null,
|
|
1121
|
+
authFailures: saved?.authFailures ?? 0,
|
|
1122
|
+
lastAttemptAt: undefined,
|
|
1123
|
+
};
|
|
1060
1124
|
states.set(id, st);
|
|
1061
1125
|
}
|
|
1062
1126
|
return st;
|
|
1063
1127
|
};
|
|
1128
|
+
const authPaused = (st) => st.authFailures >= AUTH_FAILURE_LIMIT;
|
|
1129
|
+
/** Forget an account's failure count (a new key, a cleared key, a removed account). */
|
|
1130
|
+
const resetAuth = (id, st = states.get(id)) => {
|
|
1131
|
+
if (st !== undefined) st.authFailures = 0;
|
|
1132
|
+
writeAuthEntry(id, undefined);
|
|
1133
|
+
};
|
|
1064
1134
|
|
|
1065
1135
|
const log = (level, message) => {
|
|
1066
1136
|
try {
|
|
@@ -1479,15 +1549,30 @@ export function createQuotaController(deps) {
|
|
|
1479
1549
|
const fetchedAt = new Date(now()).toISOString();
|
|
1480
1550
|
st.data = { credentials: result.credentials, fetchedAt };
|
|
1481
1551
|
st.result = { state: "ok" };
|
|
1482
|
-
st.
|
|
1552
|
+
if (st.authFailures > 0) resetAuth(account.id, st);
|
|
1483
1553
|
}
|
|
1484
1554
|
} catch (error) {
|
|
1485
1555
|
if (!current()) return;
|
|
1486
1556
|
const code = error instanceof QuotaError && error.code !== "aborted" ? error.code : error?.code === "aborted" ? null : "network";
|
|
1487
1557
|
if (code === null) return;
|
|
1488
1558
|
st.result = { state: "error", errorCode: code, ...(error?.status === undefined ? {} : { errorStatus: error.status }) };
|
|
1489
|
-
|
|
1490
|
-
|
|
1559
|
+
// Only a rejected key counts; network errors neither count nor reset.
|
|
1560
|
+
if (AUTH_ERROR_CODES.includes(code)) {
|
|
1561
|
+
st.authFailures += 1;
|
|
1562
|
+
writeAuthEntry(account.id, {
|
|
1563
|
+
authFailures: st.authFailures,
|
|
1564
|
+
errorCode: code,
|
|
1565
|
+
...(error?.status === undefined ? {} : { errorStatus: error.status }),
|
|
1566
|
+
failedAt: new Date(now()).toISOString(),
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
log(
|
|
1570
|
+
"warn",
|
|
1571
|
+
`dsh-cliproxyapi: quota refresh failed for account ${account.id}: ${code}${error?.status === undefined ? "" : ` (${error.status})`}` +
|
|
1572
|
+
(AUTH_ERROR_CODES.includes(code)
|
|
1573
|
+
? ` — management key rejected ${st.authFailures}/${AUTH_FAILURE_LIMIT}${authPaused(st) ? "; automatic refresh stopped" : ""}`
|
|
1574
|
+
: "")
|
|
1575
|
+
);
|
|
1491
1576
|
} finally {
|
|
1492
1577
|
root.signal.removeEventListener("abort", onRootAbort);
|
|
1493
1578
|
if (st.gen === gen) {
|
|
@@ -1532,6 +1617,9 @@ export function createQuotaController(deps) {
|
|
|
1532
1617
|
...(state === "error" ? { errorCode: st.result.errorCode, ...(st.result.errorStatus === undefined ? {} : { errorStatus: st.result.errorStatus }) } : {}),
|
|
1533
1618
|
...(configured && st.data !== null ? { fetchedAt: st.data.fetchedAt } : {}),
|
|
1534
1619
|
refreshing,
|
|
1620
|
+
...(configured && st.authFailures > 0
|
|
1621
|
+
? { authFailures: st.authFailures, authFailureLimit: AUTH_FAILURE_LIMIT, authPaused: authPaused(st) }
|
|
1622
|
+
: {}),
|
|
1535
1623
|
credentials: configured && st.data !== null ? structuredClone(st.data.credentials) : [],
|
|
1536
1624
|
pickerModels,
|
|
1537
1625
|
};
|
|
@@ -1552,6 +1640,8 @@ export function createQuotaController(deps) {
|
|
|
1552
1640
|
if (!known.has(id)) {
|
|
1553
1641
|
cancelInflight(st);
|
|
1554
1642
|
states.delete(id);
|
|
1643
|
+
// An empty list may be a failed read; keep the stored count then.
|
|
1644
|
+
if (accounts.length > 0) resetAuth(id);
|
|
1555
1645
|
}
|
|
1556
1646
|
}
|
|
1557
1647
|
return Promise.all(accounts.map(async (account) => {
|
|
@@ -1568,7 +1658,8 @@ export function createQuotaController(deps) {
|
|
|
1568
1658
|
for (const { account, keyConfigured: configured } of prepared) {
|
|
1569
1659
|
if (account.enabled !== true || !configured) continue;
|
|
1570
1660
|
const st = stateFor(account.id);
|
|
1571
|
-
|
|
1661
|
+
// A key rejected AUTH_FAILURE_LIMIT times in a row is not tried in the background again.
|
|
1662
|
+
if (st.inflight !== null || authPaused(st)) continue;
|
|
1572
1663
|
if (st.lastAttemptAt !== undefined && at - st.lastAttemptAt < ttlMs) continue;
|
|
1573
1664
|
startRefresh(account);
|
|
1574
1665
|
}
|
|
@@ -1582,8 +1673,8 @@ export function createQuotaController(deps) {
|
|
|
1582
1673
|
if (accountId !== undefined && accountId !== null && accountId !== "") {
|
|
1583
1674
|
const account = findAccount(accountId);
|
|
1584
1675
|
if (!disposed) {
|
|
1585
|
-
|
|
1586
|
-
|
|
1676
|
+
// A manual refresh tries once even when paused; the count is kept,
|
|
1677
|
+
// so another rejection pauses again at once (a success resets it).
|
|
1587
1678
|
if (await keyConfigured(managementKeyRef(account.id))) {
|
|
1588
1679
|
const running = startRefresh(account);
|
|
1589
1680
|
if (wait) await running;
|
|
@@ -1593,9 +1684,9 @@ export function createQuotaController(deps) {
|
|
|
1593
1684
|
}
|
|
1594
1685
|
const prepared = await prepare();
|
|
1595
1686
|
if (!disposed) {
|
|
1596
|
-
// Refresh-all never retries a
|
|
1687
|
+
// Refresh-all never retries a paused account; only refresh(id) tries it once, and a new key resets it.
|
|
1597
1688
|
await Promise.all(prepared
|
|
1598
|
-
.filter(({ account, keyConfigured: configured }) => account.enabled === true && configured && !stateFor(account.id)
|
|
1689
|
+
.filter(({ account, keyConfigured: configured }) => account.enabled === true && configured && !authPaused(stateFor(account.id)))
|
|
1599
1690
|
.map(({ account }) => (wait ? startRefresh(account) : (startRefresh(account), undefined))));
|
|
1600
1691
|
}
|
|
1601
1692
|
return buildSnapshot(prepared);
|
|
@@ -1611,7 +1702,7 @@ export function createQuotaController(deps) {
|
|
|
1611
1702
|
await deps.unsetKey(ref);
|
|
1612
1703
|
st.data = null;
|
|
1613
1704
|
st.result = null;
|
|
1614
|
-
|
|
1705
|
+
resetAuth(account.id, st);
|
|
1615
1706
|
st.lastAttemptAt = undefined;
|
|
1616
1707
|
return buildSnapshot();
|
|
1617
1708
|
}
|
|
@@ -1621,16 +1712,27 @@ export function createQuotaController(deps) {
|
|
|
1621
1712
|
cancelInflight(st);
|
|
1622
1713
|
await deps.setKey(ref, value);
|
|
1623
1714
|
st.result = null;
|
|
1624
|
-
|
|
1715
|
+
resetAuth(account.id, st);
|
|
1625
1716
|
}
|
|
1626
1717
|
if (!disposed && (await keyConfigured(ref))) {
|
|
1627
|
-
st.lockedOut = false;
|
|
1628
1718
|
const running = startRefresh(account);
|
|
1629
1719
|
if (wait) await running;
|
|
1630
1720
|
}
|
|
1631
1721
|
return buildSnapshot();
|
|
1632
1722
|
},
|
|
1633
1723
|
|
|
1724
|
+
/**
|
|
1725
|
+
* A credential reference changed outside saveKey (e.g. the DSH credentials
|
|
1726
|
+
* page): when it is an account's management key, forget its failure count.
|
|
1727
|
+
* @returns the account id, or undefined for any other reference.
|
|
1728
|
+
*/
|
|
1729
|
+
keyChanged(ref) {
|
|
1730
|
+
const account = listAccounts().find((a) => managementKeyRef(a.id) === ref);
|
|
1731
|
+
if (account === undefined) return undefined;
|
|
1732
|
+
resetAuth(account.id);
|
|
1733
|
+
return account.id;
|
|
1734
|
+
},
|
|
1735
|
+
|
|
1634
1736
|
dispose() {
|
|
1635
1737
|
if (disposed) return;
|
|
1636
1738
|
disposed = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cliproxyapi-pro",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Auto-discovery CLIProxyAPI (CPA) provider and management UI for DeepSeek Harness — multiple accounts, dynamic model sync, capability detection, and visual settings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|