dsh-cliproxyapi-pro 1.5.1 → 1.6.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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this plugin. Versions follow [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## 1.6.0 — 2026-10-20
6
+
7
+ ### Changed
8
+ - **Accounts and models moved to the Settings dialog's left navigation.** The account/model page is now its own `settings.section` entry ("CLIProxyAPI"), like the dshmarket "Market" section. The plugin's bundle page (Sidebar → Plugins) and its Built-in plugins tab now contain only plugin updates: a pointer to the new section plus the update card, expanded by default there.
9
+
10
+ ### Fixed
11
+ - **"Check for Updates" always failed on the DSH desktop app.** The desktop shell strips the `Origin` header when it forwards page requests to the Host, and update POSTs (`check`, `save`, `download`) required an explicit exact `Origin`, so every one was refused with `forbidden-origin`. Update POSTs now follow the host connection fence: a missing `Origin` relies on the host session authentication, while a present `Origin` must still be this listener's own authority (compared under its own `http(s)` scheme). A refused origin now has its own localized message.
12
+
13
+ ### Added
14
+ - **One-click updates through the DSH plugin market.** When dshmarket's public Update API v1 is detected (`GET /dsh-market/api/v1/capabilities`), the update card offers **Update Now**: after a version confirmation it submits the update to the market, follows the operation's progress, reports the version actually installed (and warns when it is below the checked target), and shows restart/refresh guidance from the market's outcome, with a restart button only when the market owns restarts. Market failure codes are localized; **Install Anyway** is offered only after a release-age wait or unresolved version. Linked installs are never installed through the market. This plugin still never runs a package manager itself.
15
+ - **Automatic installation (opt-in, `autoInstall`, default off).** With automatic checks on and dshmarket present, a version found by the Host's automatic check is installed through the market while a DSH page is open; each version is tried at most once every 6 hours and never again once installed, and automatic mode never forces. Stored as an optional field of the v1 `update-settings.json`; older files load as off.
16
+
5
17
  ## 1.5.1 — 2026-10-19
6
18
 
7
19
  ### Fixed
package/README.en.md CHANGED
@@ -14,7 +14,7 @@ Enter a server URL and an API key, and the plugin reads the CPA model catalog an
14
14
  - **Works out of the box**: on the first connection, the newest model of each family in that server's own catalog is checked (at most 12), so the picker isn't flooded with hundreds of models.
15
15
  - **Native protocols**: Claude over Anthropic Messages, GPT / Codex / Grok over OpenAI Responses, everything else over OpenAI Chat — fewer protocol translations inside CPA.
16
16
  - **Visual management**: check models and edit per-model parameters in the settings panel, or let the DSH agent configure things for you through the `cliproxyapi` tool.
17
- - **Plugin updates**: automatically check Stable / Beta / Dev channels, choose global / Mainland China sources, and optionally download verified offline packages. Upgrade manually with DSH closed; nothing installs or restarts automatically.
17
+ - **Plugin updates**: automatically check Stable / Beta / Dev channels and choose global / Mainland China sources. With the [DSH plugin market (dshmarket)](https://github.com/dsh-market/dsh-market) installed, new versions install in one click or automatically; otherwise download a verified offline package and upgrade with DSH closed.
18
18
  - **Secure**: API keys live only in the DSH credentials service, never in configuration files.
19
19
  - **Zero dependencies**: no npm dependencies and no build step; installs in seconds.
20
20
 
@@ -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.5.1`.
52
+ To pin a version, write `dsh-cliproxyapi-pro@1.6.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.5.1` |
81
+ | A released version | `git+https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git#v1.6.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 |
@@ -87,7 +87,7 @@ All versions are listed under [Releases](https://github.com/TinyBOOOOM/dsh-clipr
87
87
 
88
88
  ## Getting started
89
89
 
90
- 1. **Open the settings panel**: sidebar **Plugins** → **CLIProxyAPI**, or **Settings** → **Built-in plugins** → the **CLIProxyAPI** tab.
90
+ 1. **Open the settings panel**: **Settings** → the **CLIProxyAPI** page in the left navigation (accounts, models and protocols live there). The plugin's own updates are on sidebar **Plugins** → **CLIProxyAPI** and on **Settings** → **Built-in plugins** → the **CLIProxyAPI** tab; those two places contain only update features.
91
91
  2. **Enter the connection**: under **Connection & Authentication**, fill in the **CLIProxyAPI Base URL** (e.g. `http://127.0.0.1:8317`, without `/v1`) and the **API Key** (leave it empty for a local CPA without authentication), then click **Save & Apply**.
92
92
  3. **Choose models**: once connected, **Model Picker Filter** lists every model on the server, with the newest model of each family already checked. Adjust the selection and click **Apply Selected Models**.
93
93
  4. **Use them**: open the model picker in a conversation; the models appear in a group named after the account.
@@ -110,7 +110,7 @@ The panel follows the DSH display language; the screenshots show the Chinese UI.
110
110
 
111
111
  ![Connection settings of a disabled account with its own credential ref](https://raw.githubusercontent.com/TinyBOOOOM/dsh-cliproxyapi-pro/main/assets/screenshots/connection.png)
112
112
 
113
- **4. Plugin update card (collapsed by default).** The real component is rendered offline with fictional update state: it takes a single row showing the current version and update status, and expands to reveal channels, download regions and automatic-update switches. This is not a live GUI screenshot and does not indicate an actual published version.
113
+ **4. Plugin update card.** The real component is rendered offline with fictional update state (captured when the card still sat collapsed at the top of the account page): a single row shows the current version and update status, and expanding it reveals channels, download regions and automatic-update switches. It now lives alone on the plugin page and starts expanded there. This is not a live GUI screenshot and does not indicate an actual published version.
114
114
 
115
115
  ![Collapsed plugin update row showing the current version and an available update](https://raw.githubusercontent.com/TinyBOOOOM/dsh-cliproxyapi-pro/main/assets/screenshots/updates.png)
116
116
 
@@ -162,9 +162,9 @@ CPA serves every model on OpenAI Chat, OpenAI Responses and Anthropic Messages,
162
162
 
163
163
  ## Updating this plugin
164
164
 
165
- The **Plugin Updates** card near the top of the settings panel is collapsed to one row by default, showing only the current version and update status; click it to expand channels, download regions and automatic-update settings. Background checks still follow the saved switches, and polling never resets the expanded state. It manages only `dsh-cliproxyapi-pro`. It does not update the CPA server or DSH, or change CPA routes, accounts or model selections. Update settings save immediately; the card polls independently without reloading unsaved account forms.
165
+ **Plugin Updates** live on the plugin page (sidebar **Plugins** → **CLIProxyAPI**) and on **Settings** → **Built-in plugins** → the **CLIProxyAPI** tab; those places contain only update features, while accounts and models are on the **CLIProxyAPI** page in the Settings dialog's left navigation. On the plugin page the update card starts expanded, with a summary row showing the current version and update status, and can be collapsed at any time. Background checks still follow the saved switches, and polling never resets the expanded state. It manages only `dsh-cliproxyapi-pro`. It does not update the CPA server or DSH, or change CPA routes, accounts or model selections. Update settings save immediately; the card polls independently without reloading unsaved account forms.
166
166
 
167
- > **Downloading is not installing.** Current DSH versions cannot replace an installed plugin while running. This plugin only checks versions and downloads verified offline packages: it never automatically installs, uninstalls, overwrites source code, or restarts DSH. Close DSH before manually upgrading, then start it again.
167
+ > **Installation is handed to the plugin market.** This plugin itself never runs a package manager, uninstalls, overwrites source code, or restarts DSH. With the [DSH plugin market (dshmarket)](https://github.com/dsh-market/dsh-market) installed, the card installs new versions in one click or automatically through the market's public Update API v1. Without the market it can only download and verify an offline package for a manual upgrade with DSH closed.
168
168
 
169
169
  ### Channels, regions and automatic controls
170
170
 
@@ -173,15 +173,26 @@ The **Plugin Updates** card near the top of the settings panel is collapsed to o
173
173
  | Update channel | Stable (`stable`) | Stable / Beta / Dev read the npm `latest` / `beta` / `dev` dist-tags respectively. An unpublished Beta / Dev channel reports an error rather than silently falling back to Stable. |
174
174
  | Download region | Auto (`auto`) | Auto races global npmjs and Mainland China npmmirror, selecting the first source with **valid version metadata**. Global (`global`) uses only `https://registry.npmjs.org`; Mainland China (`cn`) uses `https://registry.npmmirror.com` and its official `cdn.npmmirror.com` package host. The offline package uses the selected source, with at most 3 HTTPS redirects validated against the host allowlist and exact package/version path; it never falls back across regions. |
175
175
  | Automatically check for updates | On | Check about 3 seconds after plugin startup or rescheduling update settings, then every 6 hours. **Check for Updates** remains available when disabled. |
176
- | Automatically download updates | Off | Download and verify an offline package only when an **automatic check** finds a newer version. Never installs or restarts automatically. Enabling this while automatic checks are off does not trigger background downloads. |
176
+ | Automatically download updates | Off | Download and verify an offline package only when an **automatic check** finds a newer version. Downloading itself never installs or restarts. Enabling this while automatic checks are off does not trigger background downloads. |
177
+ | Automatically install updates | Off | Requires automatic checks and dshmarket (cannot be enabled while the market is not detected). When an **automatic check** finds a newer version, an open DSH page asks the plugin market to install it; each version is tried at most once every 6 hours and never again once installed. Market rules such as its release-age wait still apply, and automatic mode never forces an install. A DSH restart or page refresh may be needed for it to take effect. |
177
178
 
178
179
  - Strict SemVer comparison offers downloads only when the selected channel is newer than the running version; there is no automatic downgrade. Automatic region selection never falls back across channels.
179
180
  - The region affects only this plugin's update metadata and package downloads, **not CPA connections, model requests, or other plugins' installation sources**.
180
181
  - Check again after switching channel or region. Opening the card, polling, or reloading status only reads in-memory state and does not itself contact an update source; background checks follow the controls above.
181
182
  - Errors are localized, including unpublished channels, network failures, package integrity failures and storage failures, without exposing backend stack traces.
182
183
 
184
+ ### One-click and automatic updates through the plugin market
185
+
186
+ 1. The card only reads the market's local `GET /dsh-market/api/v1/capabilities` when it opens and never contacts a registry for it; once detected it shows **Update Now**.
187
+ 2. After a check finds a newer version, click **Update Now** and confirm the version. The card submits `POST /dsh-market/api/v1/updates { "packageName": "dsh-cliproxyapi-pro" }` to the market, then polls that operation's progress and result. Version resolution, download, installation, version verification and recovery are all done by the market.
188
+ 3. When it finishes, the card shows the version actually installed and, following the market's result, asks you to **Restart DSH** (a button appears only when the market says it owns restarts; quit and reopen the desktop app completely) or **Refresh Page**. If the market, using its own channel, installed an older version than this card found, the card says so.
189
+ 4. During the market's release-age wait (`RELEASE_TOO_FRESH`) or before its mirror catches up (`VERSION_UNCHANGED`) you can choose **Install Anyway**; other failures show only a localized reason, never backend text.
190
+ 5. Linked local installations (`link:`) are not installed through the market, so their source link is never replaced.
191
+
183
192
  ### Download, verification and offline upgrade
184
193
 
194
+ Use this without the plugin market or for offline upgrades:
195
+
185
196
  1. Click **Check for Updates**. If a newer version is available, click **Download Update** and confirm its version, or wait for enabled automatic checks and downloads.
186
197
  2. The download must match the npm metadata's **SHA-512** integrity, then pass gzip / tar, expected file and directory, package name and version checks. Path traversal, links and other special entries, dependencies and installation lifecycle scripts are rejected. The package is saved without extracting into the installation directory or executing its code.
187
198
  3. After verification, the card displays the absolute package path and an upgrade command when applicable. Packages are stored at `<profile dir>/.dsh-cliproxyapi/updates/dsh-cliproxyapi-pro-<version>.tgz`.
@@ -255,12 +266,12 @@ Used by the settings panel. Requires the DSH Web session's authentication; POST
255
266
  | POST | `/api/dsh-cliproxyapi/accounts/remove` | `{ accountId }` | Delete an account and its routes |
256
267
  | GET | `/api/dsh-cliproxyapi/updates/status` | — | Read update state without contacting a registry |
257
268
  | POST | `/api/dsh-cliproxyapi/updates/check` | `{}` | Check the selected channel and region now; a manual check does not trigger automatic downloading |
258
- | POST | `/api/dsh-cliproxyapi/updates/save` | `{ autoCheck?, autoDownload?, channel?, region? }` | Save only these four update preferences, independently of accounts |
269
+ | POST | `/api/dsh-cliproxyapi/updates/save` | `{ autoCheck?, autoDownload?, autoInstall?, channel?, region? }` | Save only these five update preferences, independently of accounts |
259
270
  | POST | `/api/dsh-cliproxyapi/updates/download` | `{ version }` | Download the newer version confirmed by the latest check, verify it, and save an offline package without installing |
260
271
 
261
- Update operation responses use `{ ok, update }`. State includes `currentVersion`, `latestVersion`, `available`, `channel`, `region`, `effectiveRegion`, `autoCheck`, `autoDownload`, `checking`, `downloading`, `lastCheckedAt`, `error` / `errorCode`, `blockedReason`, `downloadedVersion`, `downloadPath` and `upgradeCommand`. Fields without a check or download result are `null`. `channel` is `stable` / `beta` / `dev`; `region` is `auto` / `global` / `cn`; `effectiveRegion` is the `global` / `cn` source used by the last successful check (`null` before one).
272
+ Update operation responses use `{ ok, update }`. State includes `currentVersion`, `latestVersion`, `available`, `channel`, `region`, `effectiveRegion`, `autoCheck`, `autoDownload`, `autoInstall`, `checking`, `downloading`, `lastCheckedAt`, `error` / `errorCode`, `blockedReason`, `downloadedVersion`, `downloadPath` and `upgradeCommand`. Fields without a check or download result are `null`. `channel` is `stable` / `beta` / `dev`; `region` is `auto` / `global` / `cn`; `effectiveRegion` is the `global` / `cn` source used by the last successful check (`null` before one).
262
273
 
263
- Failures use a top-level `errorCode` or `update.errorCode`; HTTP status or `ok` alone does not establish success. Common codes include `network-error`, `invalid-metadata`, `channel-unavailable`, `integrity-error`, `invalid-package`, `storage-error`, `busy`, `stale-version` and `disposed`. In addition to host authentication and trusted-host policy, update POSTs **require an explicit `Origin` matching the actual connection protocol and Host exactly**. Missing or mismatched origins are rejected; the account endpoints' compatibility with absent origins does not apply. Plain unauthenticated `curl` cannot call these endpoints. Scheme validation uses the actual connection, not arbitrary `Forwarded` / `X-Forwarded-*` claims; the `Origin` must match end to end. A proxy that terminates HTTPS and connects to DSH over HTTP will have update POSTs rejected. In that case, use the local DSH page on the machine running DSH first; compatibility with every reverse-proxy deployment is not implied.
274
+ Failures use a top-level `errorCode` or `update.errorCode`; HTTP status or `ok` alone does not establish success. Common codes include `network-error`, `invalid-metadata`, `channel-unavailable`, `integrity-error`, `invalid-package`, `storage-error`, `busy`, `stale-version`, `service-unavailable`, `forbidden-origin` and `disposed`. Update POSTs still rely on the host's session authentication and trusted-host policy: when a request carries `Origin`, it must belong to the same authority as this listener (host and port compared under the Origin's own scheme, matching the host's own rule), otherwise the response is `forbidden-origin`. The desktop carrier may proxy API calls without an `Origin`; such a request is allowed once the host session authenticates it, while unauthenticated callers are still rejected before reaching the route. Plain unauthenticated `curl` cannot call these endpoints.
264
275
 
265
276
  ## Storage and configuration
266
277
 
@@ -291,6 +302,7 @@ Failures use a top-level `errorCode` or `update.errorCode`; HTTP status or `ok`
291
302
  "version": 1,
292
303
  "autoCheck": true,
293
304
  "autoDownload": false,
305
+ "autoInstall": false,
294
306
  "channel": "stable",
295
307
  "region": "auto"
296
308
  }
@@ -303,7 +315,7 @@ Failures use a top-level `errorCode` or `update.errorCode`; HTTP status or `ok`
303
315
 
304
316
  ## Upgrading and uninstalling
305
317
 
306
- - **Upgrade**: preferably download a verified offline package through the card above, save your work and close DSH, run the displayed `dsh plugin --profile 'name' add 'absolute-package-path'` command, then restart. There is no need to uninstall first. Alternatively, with DSH closed, run `dsh plugin --profile <profile name> add dsh-cliproxyapi-pro@latest` to upgrade from npm. Accounts, selections and parameters remain in the profile directory, and keys in the credentials service; the updater never rewrites them. To preserve a linked source installation, update its source yourself rather than reinstalling from an offline package.
318
+ - **Upgrade**: with dshmarket installed, click **Update Now** on the plugin update card (or enable automatic installation), then restart DSH or refresh the page as prompted. Without the market, download a verified offline package through the card, save your work and close DSH, run the displayed `dsh plugin --profile 'name' add 'absolute-package-path'` command, then restart. There is no need to uninstall first. Alternatively, with DSH closed, run `dsh plugin --profile <profile name> add dsh-cliproxyapi-pro@latest` to upgrade from npm. Accounts, selections and parameters remain in the profile directory, and keys in the credentials service; the updater never rewrites them. To preserve a linked source installation, update its source yourself rather than reinstalling from an offline package.
307
319
  - **Uninstall**: uninstall on the **Plugins** page. The synchronized model routes are stored in DSH settings and are not removed with the plugin; to remove those models from the picker too, disable or delete every account before uninstalling.
308
320
 
309
321
  ## FAQ
@@ -320,8 +332,11 @@ Make sure the base URL is reachable from the machine running DSH and the API key
320
332
  **A just-published version won't install?**
321
333
  The pnpm used by DSH may enforce a waiting period for new releases (`minimumReleaseAge`); the install screen shows which version it actually installed. Try again later or name the version explicitly.
322
334
 
323
- **Why automatic downloads, but no automatic installation?**
324
- Current DSH versions reject replacing an installed plugin while running. To avoid interrupting sessions using this plugin's routes, the updater never tries to uninstall, overwrite files or restart DSH; it prepares an offline package for an upgrade after DSH is closed.
335
+ **How do I install updates automatically?**
336
+ Install and enable the [DSH plugin market (dshmarket)](https://github.com/dsh-market/dsh-market), then turn on **Automatically Check for Updates** and **Automatically Install Updates** on the plugin update card. The market performs the installation (package manager, version verification and recovery); this plugin itself never runs a package manager, overwrites files or restarts DSH. Without the market it can only prepare an offline package for an upgrade with DSH closed.
337
+
338
+ **"Check for Updates" fails immediately (especially on the desktop app)?**
339
+ Versions 1.5.1 and earlier required an `Origin` header on update POSTs, but the DSH desktop app strips that header when it forwards page requests, so checks, update-setting saves and downloads were all refused. Since 1.6.0 the plugin follows the host's own rule: without `Origin` the host session authentication decides; with `Origin` it must still be same-origin. Upgrade older versions through the plugin market, the plugin manager or the offline path.
325
340
 
326
341
  **No release available on Beta / Dev?**
327
342
  A channel is usable only after the maintainer publishes its npm dist-tag. Stable is never presented as an unpublished Beta / Dev build. A channel version no newer than the running version does not offer a downgrade download either.
@@ -341,7 +356,7 @@ Downloading and verification do not install anything. Exit DSH, run the card's u
341
356
  - Local CPA on `http://127.0.0.1:8317` without auth: nothing to configure; call `{"action": "refresh"}`.
342
357
  - Remote URL or key: `{"action": "configure", "baseURL": "...", "apiKey": "..."}` (first account), or `{"action": "add_account", ...}` for another account. Pass `"account": "<id>"` to target a specific account.
343
358
  - Narrow the picker with `selectedModels` (exact ids from the `status` output).
344
- 3. **Verify**: `{"action": "status"}` shows `state: "connected"` and `modelCount > 0` for each enabled account. Tell the user the models are in the DSH model picker under the account's name, and that accounts are managed in **Sidebar → Plugins → CLIProxyAPI** or **Settings → Built-in plugins → CLIProxyAPI**.
359
+ 3. **Verify**: `{"action": "status"}` shows `state: "connected"` and `modelCount > 0` for each enabled account. Tell the user the models are in the DSH model picker under the account's name, and that accounts are managed on the **CLIProxyAPI** page in the Settings dialog's left navigation.
345
360
 
346
361
  Never echo API keys back to the user or write them into files.
347
362
 
@@ -349,7 +364,7 @@ Never echo API keys back to the user or write them into files.
349
364
 
350
365
  ```text
351
366
  lib/index.js Host plugin (discovery, sync, multi-account controller, tool, commands, HTTP API)
352
- lib/client.js Web settings panel (ModuleLoader; isolated update card and account forms, no build)
367
+ lib/client.js Web UI (ModuleLoader: account page in Settings, update card on the plugin pages, no build)
353
368
  lib/updater.js Update settings, SemVer/channels, region race, SHA-512/package checks, offline storage
354
369
  locale/*.json Title and description shown by the plugin manager
355
370
  cordis.patch.yml Bundle patch inserting the dsh-cliproxyapi-pro row
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  - **开箱即用**:首次连接时按该服务自己的模型目录,为每个模型家族勾选最新的一个(最多 12 个),模型选择器不会被上百个模型淹没。
15
15
  - **原生协议**:Claude 走 Anthropic Messages,GPT / Codex / Grok 走 OpenAI Responses,其余走 OpenAI Chat,减少 CPA 的协议转换。
16
16
  - **可视化管理**:设置面板里勾选模型、编辑单个模型的参数;也可以让 DSH 的 Agent 通过 `cliproxyapi` 工具帮你配置。
17
- - **插件更新**:自动检查稳定 / Beta / 开发通道,可选择全球 / 中国大陆下载源并自动下载校验离线包;关闭 DSH 后手动升级,不会自动安装或重启。
17
+ - **插件更新**:自动检查稳定 / Beta / 开发通道,可选择全球 / 中国大陆下载源;装有 [DSH 插件市场(dshmarket)](https://github.com/dsh-market/dsh-market) 时可一键或自动安装新版本,否则下载校验离线包,在关闭 DSH 后手动升级。
18
18
  - **安全**:API Key 只保存在 DSH 凭证服务里,不写入任何配置文件。
19
19
  - **零依赖**:不依赖任何 npm 包,无需构建,安装几秒完成。
20
20
 
@@ -49,7 +49,7 @@
49
49
  dsh-cliproxyapi-pro
50
50
  ```
51
51
 
52
- 需要固定版本时可以写成 `dsh-cliproxyapi-pro@1.5.1`。
52
+ 需要固定版本时可以写成 `dsh-cliproxyapi-pro@1.6.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.5.1` |
81
+ | 某个发布版本 | `git+https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git#v1.6.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
  | 本地源码(开发用) | 克隆后的仓库目录绝对路径;以链接方式安装,改代码后重载插件即生效 |
@@ -87,7 +87,7 @@ dsh plugin --profile <profile 名称> add dsh-cliproxyapi-pro
87
87
 
88
88
  ## 快速上手
89
89
 
90
- 1. **打开设置面板**:左侧栏 **插件** → **CLIProxyAPI**,或 **设置** → **内置插件** → **CLIProxyAPI** 标签页。
90
+ 1. **打开设置面板**:**设置** → 左侧导航的 **CLIProxyAPI** 页(账号、模型与协议都在这里)。插件自身的更新在左侧栏 **插件** → **CLIProxyAPI**,或 **设置** → **内置插件** → **CLIProxyAPI** 标签页,这两处只有更新功能。
91
91
  2. **填写连接信息**:在「连接与认证配置」里填 **CLIProxyAPI 服务地址**(如 `http://127.0.0.1:8317`,不用加 `/v1`)和 **API Key**(本地免密的 CPA 可留空),点 **保存设置并应用**。
92
92
  3. **选择模型**:连接成功后,「模型显示筛选」会列出 CPA 上的全部模型,并已为每个家族勾好最新的一个。按需增减后点 **保存模型勾选**。
93
93
  4. **开始使用**:回到对话框,打开模型选择器,就能在以账号名称命名的分组里看到这些模型。
@@ -108,7 +108,7 @@ CPA 就在本机默认端口 `8317` 且没有设置密钥时,什么都不用
108
108
 
109
109
  ![未启用账号的连接设置,使用独立的密钥引用名](https://raw.githubusercontent.com/TinyBOOOOM/dsh-cliproxyapi-pro/main/assets/screenshots/connection.png)
110
110
 
111
- **4. 插件更新卡片(默认收起)。** 使用真实组件与虚构的更新状态离线渲染:默认只占一行,显示当前版本与更新状态;点击后展开通道、下载区域和自动更新开关。不是正在运行的 GUI 截图,也不代表实际发布版本。
111
+ **4. 插件更新卡片。** 使用真实组件与虚构的更新状态离线渲染(拍摄于更新卡片仍位于账号页顶部、默认收起的版本):一行显示当前版本与更新状态,展开后是通道、下载区域和自动更新开关。现在它单独位于插件页,并默认展开。不是正在运行的 GUI 截图,也不代表实际发布版本。
112
112
 
113
113
  ![插件更新卡片默认收起:一行显示当前版本与新版本提示](https://raw.githubusercontent.com/TinyBOOOOM/dsh-cliproxyapi-pro/main/assets/screenshots/updates.png)
114
114
 
@@ -160,9 +160,9 @@ CPA 对每个模型同时提供 OpenAI Chat、OpenAI Responses、Anthropic Messa
160
160
 
161
161
  ## 插件自身更新
162
162
 
163
- 设置面板顶部的 **插件更新** 卡片默认收起为一行,只显示当前版本和更新状态,点击才会展开通道、下载区域与自动更新设置;后台检查仍按保存的开关运行,展开状态不会被轮询重置。它只管理 `dsh-cliproxyapi-pro` 自身,不会更新 CPA 服务或 DSH 宿主,也不改变 CPA 请求的路由、账号或模型勾选。更新设置修改后立即保存;卡片独立轮询状态,不会重载未保存的账号表单。
163
+ **插件更新** 位于左侧栏 **插件** → **CLIProxyAPI** 的插件页,以及 **设置** → **内置插件** → **CLIProxyAPI** 标签页;这两处只有更新功能,账号与模型设置在 **设置** 左侧导航的 **CLIProxyAPI** 页。更新卡片在插件页默认展开,摘要行显示当前版本和更新状态,可随时收起;后台检查仍按保存的开关运行,展开状态不会被轮询重置。它只管理 `dsh-cliproxyapi-pro` 自身,不会更新 CPA 服务或 DSH 宿主,也不改变 CPA 请求的路由、账号或模型勾选。更新设置修改后立即保存;卡片独立轮询状态,不会重载未保存的账号表单。
164
164
 
165
- > **下载不等于安装。** 当前 DSH 不允许在运行中替换已安装的插件。本插件只检查版本、下载并校验离线包:不会自动安装、卸载插件、覆盖源码或重启 DSH。升级须在关闭 DSH 后手动执行,再启动生效。
165
+ > **安装交给插件市场。** 本插件自身从不运行包管理器、卸载插件、覆盖源码或重启 DSH。装有 [DSH 插件市场(dshmarket)](https://github.com/dsh-market/dsh-market) 时,卡片通过它公开的 Update API v1 一键或自动安装新版本;没有插件市场时,只能下载并校验离线包,在关闭 DSH 后手动升级。
166
166
 
167
167
  ### 通道、区域与自动开关
168
168
 
@@ -171,15 +171,26 @@ CPA 对每个模型同时提供 OpenAI Chat、OpenAI Responses、Anthropic Messa
171
171
  | 更新通道 | 稳定(`stable`) | 稳定 / Beta / 开发分别读取 npm 的 `latest` / `beta` / `dev` dist-tag。Beta / 开发通道尚未发布时明确报错,不悄悄回退稳定版。 |
172
172
  | 下载区域 | 自动(`auto`) | 自动模式同时请求全球 npmjs 与中国大陆 npmmirror,选择最先返回**有效版本元数据**的源;全球(`global`)只使用 `https://registry.npmjs.org`,中国大陆(`cn`)使用 `https://registry.npmmirror.com` 及其官方 `cdn.npmmirror.com` 包下载域名。离线包从本次检查选中的源下载,至多跟随 3 次经过白名单与精确包名/版本路径校验的 HTTPS 跳转,不跨区域回退。 |
173
173
  | 自动检查更新 | 开启 | 插件启动或更新设置重新调度后约 3 秒检查,之后每 6 小时检查所选通道;关闭后仍可手动点 **检查更新**。 |
174
- | 自动下载更新 | 关闭 | 仅在**自动检查**发现新版本时,自动下载并校验离线包;不会自动安装或重启。仅开启此项而关闭自动检查,不会触发后台下载。 |
174
+ | 自动下载更新 | 关闭 | 仅在**自动检查**发现新版本时,自动下载并校验离线包;下载本身不安装、不重启。仅开启此项而关闭自动检查,不会触发后台下载。 |
175
+ | 自动安装更新 | 关闭 | 需要开启自动检查并装有 dshmarket(未检测到插件市场时无法开启)。**自动检查**发现新版本后,由打开着的 DSH 页面调用插件市场安装;同一版本 6 小时内最多尝试一次,安装成功的版本不再重试。插件市场的发布冷却期等限制照常生效,自动模式从不强制安装。生效可能需要重启 DSH 或刷新页面。 |
175
176
 
176
177
  - 检查使用严格 SemVer 比较:只有所选通道版本高于当前运行版本时才提供下载,不自动降级。自动区域选择也不会跨通道回退。
177
178
  - 区域只影响本插件的更新元数据与安装包下载,**不影响 CPA 的连接、模型请求或其他插件的安装源**。
178
179
  - 切换通道或区域后,请重新检查更新。打开卡片、轮询或重新读取状态本身只读取内存状态,不主动访问更新源;后台自动检查由上述开关控制。
179
180
  - 失败时卡片显示本地化提示,例如通道未发布、网络异常、包完整性校验失败或磁盘写入失败,不显示后端堆栈。
180
181
 
182
+ ### 通过插件市场一键更新 / 自动更新
183
+
184
+ 1. 卡片打开时只读取插件市场本地的 `GET /dsh-market/api/v1/capabilities`,不访问任何更新源;检测到后显示 **立即更新**。
185
+ 2. 检查到新版本后点 **立即更新** 并确认版本号;卡片向插件市场提交 `POST /dsh-market/api/v1/updates { "packageName": "dsh-cliproxyapi-pro" }`,再轮询该任务的进度与结果。版本解析、下载、安装、版本核对与恢复都由插件市场完成。
186
+ 3. 完成后卡片显示实际装上的版本,并按插件市场的结果提示 **重启 DSH**(仅插件市场声明可以重启时显示按钮;桌面版需完全退出后重新打开)或 **刷新页面**。若插件市场按其自身通道装上的版本低于本卡片检查到的版本,会明确提示。
187
+ 4. 遇到发布冷却期(`RELEASE_TOO_FRESH`)或镜像未同步(`VERSION_UNCHANGED`)时可手动选择 **仍然安装**;其他失败只显示本地化原因,不显示后端信息。
188
+ 5. 本地链接安装(`link:`)不提供插件市场安装,以免替换源码链接。
189
+
181
190
  ### 下载、校验与离线升级
182
191
 
192
+ 没有插件市场或需要离线升级时使用:
193
+
183
194
  1. 点击 **检查更新**;有更高版本时,点 **下载更新** 并确认版本号,或等待已启用的自动检查 / 自动下载。
184
195
  2. 下载先通过 npm 元数据中的 **SHA-512** 完整性校验,再验证 gzip / tar、预期文件与目录、包名和版本;拒绝路径穿越、链接等特殊条目、依赖和安装生命周期脚本。只保存离线包,不解压到安装目录,也不执行包内代码。
185
196
  3. 校验成功后,卡片显示安装包的绝对路径与适用的升级命令。包保存在 `<Profile 目录>/.dsh-cliproxyapi/updates/dsh-cliproxyapi-pro-<version>.tgz`。
@@ -253,12 +264,12 @@ DSH 的 Agent 可以用这个工具查看和配置插件,你只需用自然语
253
264
  | POST | `/api/dsh-cliproxyapi/accounts/remove` | `{ accountId }` | 删除账号及其路由 |
254
265
  | GET | `/api/dsh-cliproxyapi/updates/status` | — | 只读更新状态,不主动联网检查 |
255
266
  | POST | `/api/dsh-cliproxyapi/updates/check` | `{}` | 主动检查所选通道与区域;手动检查不触发自动下载 |
256
- | POST | `/api/dsh-cliproxyapi/updates/save` | `{ autoCheck?, autoDownload?, channel?, region? }` | 只保存四项更新设置,与账号配置独立 |
267
+ | POST | `/api/dsh-cliproxyapi/updates/save` | `{ autoCheck?, autoDownload?, autoInstall?, channel?, region? }` | 只保存这五项更新设置,与账号配置独立 |
257
268
  | POST | `/api/dsh-cliproxyapi/updates/download` | `{ version }` | 下载最近一次检查确认的新版本,完成校验后保存离线包;不安装 |
258
269
 
259
- 更新接口的操作响应采用 `{ ok, update }`,状态包含 `currentVersion`、`latestVersion`、`available`、`channel`、`region`、`effectiveRegion`、`autoCheck`、`autoDownload`、`checking`、`downloading`、`lastCheckedAt`、`error` / `errorCode`、`blockedReason`、`downloadedVersion`、`downloadPath` 与 `upgradeCommand`。未检查或未下载的相关字段为 `null`。`channel` 为 `stable` / `beta` / `dev`,`region` 为 `auto` / `global` / `cn`;`effectiveRegion` 为本次成功检查使用的 `global` / `cn`(此前为 `null`)。
270
+ 更新接口的操作响应采用 `{ ok, update }`,状态包含 `currentVersion`、`latestVersion`、`available`、`channel`、`region`、`effectiveRegion`、`autoCheck`、`autoDownload`、`autoInstall`、`checking`、`downloading`、`lastCheckedAt`、`error` / `errorCode`、`blockedReason`、`downloadedVersion`、`downloadPath` 与 `upgradeCommand`。未检查或未下载的相关字段为 `null`。`channel` 为 `stable` / `beta` / `dev`,`region` 为 `auto` / `global` / `cn`;`effectiveRegion` 为本次成功检查使用的 `global` / `cn`(此前为 `null`)。
260
271
 
261
- 失败用顶层 `errorCode` 或 `update.errorCode` 表达,不能只凭 HTTP 状态或 `ok` 判断操作成功;常见值包括 `network-error`、`invalid-metadata`、`channel-unavailable`、`integrity-error`、`invalid-package`、`storage-error`、`busy`、`stale-version` 与 `disposed`。更新 POST 在宿主鉴权和可信主机策略之外,**必须带与当前连接协议和 Host 完全匹配的显式 `Origin`**;缺失或不匹配会拒绝,不沿用账号接口对缺省 `Origin` 的兼容行为。普通未认证 `curl` 不能调用这些接口。协议判断使用实际连接,不信任任意 `Forwarded` / `X-Forwarded-*` 声明;需要端到端一致的 `Origin`。若反向代理在 HTTPS 终止后以 HTTP 连接 DSH,更新 POST 会被拒绝,请先从运行 DSH 机器上的本机 DSH 页面操作,不要假定所有反代部署都兼容。
272
+ 失败用顶层 `errorCode` 或 `update.errorCode` 表达,不能只凭 HTTP 状态或 `ok` 判断操作成功;常见值包括 `network-error`、`invalid-metadata`、`channel-unavailable`、`integrity-error`、`invalid-package`、`storage-error`、`busy`、`stale-version`、`service-unavailable`、`forbidden-origin` 与 `disposed`。更新 POST 仍然依赖宿主的会话鉴权与可信主机策略:请求带 `Origin` 时必须属于当前监听的同一 authority(按 `Origin` 自身协议比较主机与端口,与宿主规则一致),否则返回 `forbidden-origin`;桌面端桥接请求可能不带 `Origin`,此时只要通过宿主会话鉴权即可执行,未认证请求仍会在到达路由前被拒绝。普通未认证 `curl` 不能调用这些接口。
262
273
 
263
274
  ## 存储与配置
264
275
 
@@ -289,6 +300,7 @@ DSH 的 Agent 可以用这个工具查看和配置插件,你只需用自然语
289
300
  "version": 1,
290
301
  "autoCheck": true,
291
302
  "autoDownload": false,
303
+ "autoInstall": false,
292
304
  "channel": "stable",
293
305
  "region": "auto"
294
306
  }
@@ -301,7 +313,7 @@ DSH 的 Agent 可以用这个工具查看和配置插件,你只需用自然语
301
313
 
302
314
  ## 升级与卸载
303
315
 
304
- - **升级**:优先使用上方插件更新卡片下载校验后的离线包,保存工作并关闭 DSH,再执行卡片的 `dsh plugin --profile '名称' add '离线包绝对路径'` 命令,最后重新启动;无需先卸载。也可在关闭 DSH 后运行 `dsh plugin --profile <profile 名称> add dsh-cliproxyapi-pro@latest` 从 npm 升级。账号、勾选和参数仍保存在原 profile 目录里,密钥仍在凭证服务里;更新下载功能不会改写这些数据。本地链接安装要保留源码方式时应自行更新源码,而不是从离线包重装。
316
+ - **升级**:装有 dshmarket 时,直接在插件更新卡片点 **立即更新**(或开启自动安装),完成后按提示重启 DSH 或刷新页面。没有插件市场时,使用卡片下载校验后的离线包,保存工作并关闭 DSH,再执行卡片的 `dsh plugin --profile '名称' add '离线包绝对路径'` 命令,最后重新启动;无需先卸载。也可在关闭 DSH 后运行 `dsh plugin --profile <profile 名称> add dsh-cliproxyapi-pro@latest` 从 npm 升级。账号、勾选和参数仍保存在原 profile 目录里,密钥仍在凭证服务里;更新下载功能不会改写这些数据。本地链接安装要保留源码方式时应自行更新源码,而不是从离线包重装。
305
317
  - **卸载**:在 **插件** 页卸载。已同步的模型路由保存在 DSH 设置里,卸载插件不会自动删除它们;想让这些模型一并从选择器消失,请在卸载前把各账号停用或删除。
306
318
 
307
319
  ## 常见问题
@@ -318,8 +330,11 @@ DSH 的 Agent 可以用这个工具查看和配置插件,你只需用自然语
318
330
  **刚发布的新版本装不上?**
319
331
  DSH 使用的 pnpm 可能设置了新版本发布后的等待期(`minimumReleaseAge`),安装界面会说明实际装到的版本。稍后再装,或明确写出版本号。
320
332
 
321
- **为什么只有自动下载,没有自动安装?**
322
- 当前 DSH 不允许覆盖运行中的已装插件。为避免中断正在使用本插件路由的会话,更新器不会尝试卸载、覆盖文件或重启,只准备离线包供关闭 DSH 后升级。
333
+ **怎样自动安装更新?**
334
+ 安装并启用 [DSH 插件市场(dshmarket)](https://github.com/dsh-market/dsh-market),在插件更新卡片打开 **自动检查更新** 和 **自动安装更新**。安装由插件市场完成(它负责包管理器、版本核对与恢复),本插件自身不会运行包管理器、覆盖文件或重启。没有插件市场时只能准备离线包,在关闭 DSH 后升级。
335
+
336
+ **点「检查更新」就报错(尤其是桌面版)?**
337
+ 1.5.1 及以前的版本要求更新 POST 必须带 `Origin`,而 DSH 桌面版转发页面请求时会去掉这个请求头,所以检查、保存更新设置和下载都会被拒绝。1.6.0 起已与宿主规则一致:缺少 `Origin` 时依靠宿主会话鉴权,带 `Origin` 时仍须同源。旧版本请通过插件市场、插件管理器或离线方式升级到新版本。
323
338
 
324
339
  **Beta / 开发通道提示暂无版本?**
325
340
  只有维护者发布对应 npm dist-tag 后才可使用;插件不会把稳定版伪装成 Beta / 开发版。若通道版本不高于当前版本,也不会提供降级下载。
@@ -339,7 +354,7 @@ DSH 使用的 pnpm 可能设置了新版本发布后的等待期(`minimumRelea
339
354
  - CPA 在本机 `http://127.0.0.1:8317` 且无鉴权:无需配置,调用 `{"action": "refresh"}`。
340
355
  - 远程地址或有密钥:`{"action": "configure", "baseURL": "...", "apiKey": "..."}`(作用于第一个账号);额外的账号用 `{"action": "add_account", ...}`;指定账号时传 `"account": "<id>"`。
341
356
  - 用 `selectedModels`(取自 `status` 输出中的精确 id)精简模型选择器。
342
- 3. **验证**:`{"action": "status"}` 中每个启用的账号 `state` 为 `connected` 且 `modelCount > 0`。告诉用户模型已出现在 DSH 模型选择器中(按账号名称分组),可在 **左侧栏 → 插件 → CLIProxyAPI** 或 **设置 → 内置插件 → CLIProxyAPI** 管理账号。
357
+ 3. **验证**:`{"action": "status"}` 中每个启用的账号 `state` 为 `connected` 且 `modelCount > 0`。告诉用户模型已出现在 DSH 模型选择器中(按账号名称分组),可在 **设置** 左侧导航的 **CLIProxyAPI** 页管理账号。
343
358
 
344
359
  不要把 API Key 回显给用户,也不要写入任何文件。
345
360
 
@@ -347,7 +362,7 @@ DSH 使用的 pnpm 可能设置了新版本发布后的等待期(`minimumRelea
347
362
 
348
363
  ```text
349
364
  lib/index.js Host 插件(发现、同步、多账号控制器、工具、命令、HTTP 接口)
350
- lib/client.js Web 设置面板(ModuleLoader 格式,独立更新卡片与账号表单,无需构建)
365
+ lib/client.js Web 界面(ModuleLoader 格式:设置左侧的账号页、插件页的更新卡片,无需构建)
351
366
  lib/updater.js 更新设置、SemVer/通道、区域竞速、SHA-512/包结构检查与离线包存储
352
367
  locale/*.json 插件管理器中显示的标题与描述
353
368
  cordis.patch.yml Bundle patch,插入 dsh-cliproxyapi-pro 行
package/lib/client.js CHANGED
@@ -12,6 +12,8 @@ window.__ModuleLoader__.load({
12
12
  const PACKAGE_NAME = "dsh-cliproxyapi-pro";
13
13
  const MODULE_ID = "dsh-cliproxyapi-pro";
14
14
  const NS = "dsh-cliproxyapi-pro";
15
+ // Stable id of the account/model page in the Settings left navigation.
16
+ const SETTINGS_SECTION_ID = "cliproxyapi";
15
17
 
16
18
  // Document-relative API paths (compatible with <base href="./"> in DSH Web/Desktop)
17
19
  const API = {
@@ -26,6 +28,17 @@ window.__ModuleLoader__.load({
26
28
  updateDownload: "api/dsh-cliproxyapi/updates/download",
27
29
  };
28
30
 
31
+ // Public, versioned dshmarket Update API (UPDATE-API-V1.md in dshmarket).
32
+ // Installation is delegated to the market; this plugin never runs a
33
+ // package manager. Paths are document-relative like API above.
34
+ const MARKET_API = {
35
+ capabilities: "dsh-market/api/v1/capabilities",
36
+ updates: "dsh-market/api/v1/updates",
37
+ operations: "dsh-market/api/v1/operations",
38
+ restart: "dsh-market/api/v1/restart",
39
+ };
40
+ const MARKET_SCHEMA = "dsh-market/update-api/v1";
41
+
29
42
  const zh = {
30
43
  nav: "CLIProxyAPI",
31
44
  title: "CLIProxyAPI 自动发现与模型筛选",
@@ -123,6 +136,7 @@ window.__ModuleLoader__.load({
123
136
  paramsHint: "留空并保存即恢复 CLIProxyAPI 自动探测值。修改后请点击「保存模型勾选」才会写入模型选择器。",
124
137
  discoveredValue: "探测值",
125
138
  updateTitle: "插件更新",
139
+ updatePageHint: "这里只管理插件自身的更新。CLIProxyAPI 的账号、模型和协议设置位于「设置」左侧导航的「CLIProxyAPI」页。",
126
140
  updateToggleDetails: "展开或收起插件更新设置",
127
141
  updateCompactCurrent: "当前 {version}",
128
142
  updateCompactAvailable: "有新版本 {version}",
@@ -133,6 +147,7 @@ window.__ModuleLoader__.load({
133
147
  updateCompactErrorChannel: "通道未发布",
134
148
  updateStatusUnavailableHint: "无法读取更新服务,可能是插件宿主代码尚未重载:重启 DSH(仅刷新页面不够),或检查更新接口是否可用。",
135
149
  updateServiceUnavailableError: "更新服务未就绪:当前运行的插件宿主代码仍是旧版,可能尚未重启 DSH。重启 DSH 后重试。",
150
+ updateOriginError: "请求来源校验未通过,更新操作已拒绝。请从 DSH 打开的页面重试;使用反向代理时请保持与页面一致的地址访问。",
136
151
  updateSubtitle: "仅更新 CLIProxyAPI 插件自身,不更新 CPA 服务或 DSH 宿主。设置修改后立即保存。",
137
152
  updateNewVersion: "新版本",
138
153
  updateVersionSummary: "当前版本 {current} · 最新版本 {latest}",
@@ -195,7 +210,51 @@ window.__ModuleLoader__.load({
195
210
  updateDownloadPathLabel: "已校验的离线包",
196
211
  updateUpgradeCommandLabel: "关闭 DSH 后执行升级命令",
197
212
  updateCommandUnavailable: "未生成升级命令。请先确认 DSH 支持的离线包升级方式;升级前关闭 DSH,完成后重新启动。",
198
- updateOfflineHint: "自动下载不等于自动安装。插件不会关闭、重启 DSH,也不会覆盖正在使用的插件文件。",
213
+ updateOfflineHint: "离线包下载不会自动安装。本插件自身不会运行包管理器、关闭或重启 DSH;一键更新和自动安装都交给 DSH 插件市场(dshmarket)完成。",
214
+ updateCompactInstalled: "{version} 已安装",
215
+ updateMarketLabel: "通过插件市场更新",
216
+ updateMarketDetecting: "正在检测 DSH 插件市场…",
217
+ updateMarketReadyHint: "已检测到 DSH 插件市场(dshmarket),可一键安装新版本。安装和版本解析都由插件市场完成,可能需要重启 DSH 或刷新页面才能生效。",
218
+ updateMarketUnavailableHint: "未检测到 DSH 插件市场(dshmarket)的更新接口。安装插件市场后即可在此一键更新或自动更新;目前可使用下方的离线包下载。",
219
+ updateMarketLinkedHint: "当前为本地链接安装,不通过插件市场安装,以免替换源码链接。",
220
+ updateMarketInstallBtn: "立即更新",
221
+ updateMarketInstalledBtn: "已安装",
222
+ updateMarketInstalling: "正在通过插件市场安装…",
223
+ updateMarketProgress: "正在通过插件市场安装… {percent}%",
224
+ updateMarketRedetectBtn: "重新检测",
225
+ updateMarketConfirmTitle: "通过插件市场安装版本 {version}?",
226
+ updateMarketConfirmHint: "DSH 插件市场会安装本插件的新版本,并在安装后核对实际装上的版本,不一致时恢复原版本。安装期间请勿关闭 DSH;完成后可能需要重启 DSH 或刷新页面。",
227
+ updateMarketConfirmBtn: "确认安装",
228
+ updateMarketAutomaticPrefix: "自动更新:",
229
+ updateMarketInstalled: "已安装版本 {version}。",
230
+ updateMarketVersionLower: "插件市场安装的是 {installed},低于检查到的 {target}:插件市场按其自身的通道与镜像解析版本。",
231
+ updateMarketRestartHint: "重启 DSH 后新版本生效。",
232
+ updateMarketRestartDesktopHint: "请完全退出 DSH 桌面版并重新打开,新版本才会生效。",
233
+ updateMarketRestartManualHint: "请重启 DSH 服务,新版本才会生效。",
234
+ updateMarketRefreshHint: "刷新页面后新版本界面生效。",
235
+ updateMarketAppliedHint: "新版本已生效;若界面仍是旧版,请刷新页面。",
236
+ updateMarketRestartBtn: "重启 DSH",
237
+ updateMarketRestarting: "正在请求重启…",
238
+ updateMarketRestartRequested: "已请求重启 DSH,页面将在 DSH 重新启动后恢复。",
239
+ updateMarketRefreshBtn: "刷新页面",
240
+ updateMarketUnchanged: "插件市场报告完成,但插件版本没有变化。",
241
+ updateMarketCancelled: "插件市场的安装任务已取消。",
242
+ updateMarketRolledBack: "插件市场已回滚此次更新,插件保持原版本。",
243
+ updateMarketForceBtn: "仍然安装",
244
+ updateMarketBusy: "插件市场正在执行另一项更新,请稍后重试。",
245
+ updateMarketAgentsRunning: "有智能体正在运行,插件市场暂不安装。请等待任务结束后重试。",
246
+ updateMarketTooFresh: "新版本刚发布,仍在插件市场的发布冷却期内。可稍后重试,或选择「仍然安装」。",
247
+ updateMarketVersionUnchanged: "插件市场没有解析到更新的版本(镜像可能尚未同步)。可稍后重试,或选择「仍然安装」。",
248
+ updateMarketDowngrade: "插件市场解析到的版本低于当前版本,已自动恢复,未做更改。",
249
+ updateMarketMismatch: "插件市场安装的版本与目标版本不一致,已自动恢复。请等待镜像同步后重试。",
250
+ updateMarketTimeout: "插件市场安装超时,请稍后重试。",
251
+ updateMarketForbidden: "插件市场拒绝了此次更新请求,请从 DSH 打开的页面重试。",
252
+ updateMarketLost: "无法继续跟踪插件市场的安装任务(DSH 可能已重启)。请重新检查更新以确认当前版本。",
253
+ updateMarketUnreachable: "无法连接 DSH 插件市场的更新接口,请确认 dshmarket 已启用后重试。",
254
+ updateMarketRestartFailed: "重启请求失败,请手动重启 DSH。",
255
+ updateMarketFailed: "插件市场安装失败。可在插件市场中查看详情,稍后重试,或使用离线包下载。",
256
+ updateAutoInstallLabel: "自动安装更新",
257
+ updateAutoInstallHint: "自动检查发现新版本后,通过 DSH 插件市场自动安装(需开启自动检查并安装 dshmarket,仅在 DSH 页面打开时进行;同一版本 6 小时内最多尝试一次)。生效可能需要重启 DSH 或刷新页面。",
199
258
  };
200
259
 
201
260
  const en = {
@@ -295,6 +354,7 @@ window.__ModuleLoader__.load({
295
354
  paramsHint: "Clear a value and save to restore the value discovered from CLIProxyAPI. Click Apply Selected Models to write changes to the picker.",
296
355
  discoveredValue: "Discovered",
297
356
  updateTitle: "Plugin Updates",
357
+ updatePageHint: "This page only manages updates of the plugin itself. CLIProxyAPI accounts, models and protocol settings are on the \"CLIProxyAPI\" page in the Settings dialog's left navigation.",
298
358
  updateToggleDetails: "Expand or collapse plugin update settings",
299
359
  updateCompactCurrent: "Current {version}",
300
360
  updateCompactAvailable: "Update {version}",
@@ -305,6 +365,7 @@ window.__ModuleLoader__.load({
305
365
  updateCompactErrorChannel: "Channel unpublished",
306
366
  updateStatusUnavailableHint: "Could not reach the update service; the running host plugin code may still be the previous version. Restart DSH (refreshing the page is not enough), or check that the update endpoint is registered.",
307
367
  updateServiceUnavailableError: "The update service is not ready: the running host plugin code is still the previous version, so DSH may not have been restarted. Restart DSH and retry.",
368
+ updateOriginError: "The request origin was rejected, so the update action was refused. Retry from the page DSH opened, and reach it through the same address you see (relevant behind a reverse proxy).",
308
369
  updateSubtitle: "Updates this CLIProxyAPI plugin only, not the CPA server or DSH. Changes to update settings are saved immediately.",
309
370
  updateNewVersion: "New Version",
310
371
  updateVersionSummary: "Current {current} · Latest {latest}",
@@ -367,7 +428,51 @@ window.__ModuleLoader__.load({
367
428
  updateDownloadPathLabel: "Verified Offline Package",
368
429
  updateUpgradeCommandLabel: "Upgrade Command — Run After Closing DSH",
369
430
  updateCommandUnavailable: "No upgrade command was generated. First verify the offline-package upgrade method supported by your DSH version. Close DSH before upgrading, then start it again.",
370
- updateOfflineHint: "Automatic downloading is not automatic installation. This plugin never closes or restarts DSH, or overwrites plugin files that are in use.",
431
+ updateOfflineHint: "Downloading an offline package never installs it. This plugin itself never runs a package manager, closes or restarts DSH; one-click and automatic installation are handed to the DSH plugin market (dshmarket).",
432
+ updateCompactInstalled: "{version} installed",
433
+ updateMarketLabel: "Update Through Plugin Market",
434
+ updateMarketDetecting: "Detecting the DSH plugin market…",
435
+ updateMarketReadyHint: "The DSH plugin market (dshmarket) was detected, so new versions can be installed in one click. The market installs and resolves the version; a DSH restart or page refresh may be needed for it to take effect.",
436
+ updateMarketUnavailableHint: "The DSH plugin market (dshmarket) update API was not detected. Install the plugin market to enable one-click and automatic updates here; until then, use the offline package download below.",
437
+ updateMarketLinkedHint: "This is a linked local installation, so it is not installed through the plugin market, which would replace the source link.",
438
+ updateMarketInstallBtn: "Update Now",
439
+ updateMarketInstalledBtn: "Installed",
440
+ updateMarketInstalling: "Installing through the plugin market…",
441
+ updateMarketProgress: "Installing through the plugin market… {percent}%",
442
+ updateMarketRedetectBtn: "Detect Again",
443
+ updateMarketConfirmTitle: "Install version {version} through the plugin market?",
444
+ updateMarketConfirmHint: "The DSH plugin market installs the new version of this plugin, then verifies the version it actually placed and restores the previous one on a mismatch. Keep DSH open while it runs; a DSH restart or page refresh may be needed afterwards.",
445
+ updateMarketConfirmBtn: "Confirm Install",
446
+ updateMarketAutomaticPrefix: "Automatic update:",
447
+ updateMarketInstalled: "Version {version} is installed.",
448
+ updateMarketVersionLower: "The plugin market installed {installed}, older than the checked {target}: the market resolves versions with its own channel and mirror.",
449
+ updateMarketRestartHint: "Restart DSH for the new version to take effect.",
450
+ updateMarketRestartDesktopHint: "Quit the DSH desktop app completely and reopen it for the new version to take effect.",
451
+ updateMarketRestartManualHint: "Restart the DSH service for the new version to take effect.",
452
+ updateMarketRefreshHint: "Refresh the page to load the new interface.",
453
+ updateMarketAppliedHint: "The new version is active; refresh the page if the interface still looks old.",
454
+ updateMarketRestartBtn: "Restart DSH",
455
+ updateMarketRestarting: "Requesting restart…",
456
+ updateMarketRestartRequested: "A DSH restart was requested. The page recovers once DSH is back.",
457
+ updateMarketRefreshBtn: "Refresh Page",
458
+ updateMarketUnchanged: "The plugin market reported success, but the plugin version did not change.",
459
+ updateMarketCancelled: "The plugin market cancelled the installation.",
460
+ updateMarketRolledBack: "The plugin market rolled this update back; the previous version is kept.",
461
+ updateMarketForceBtn: "Install Anyway",
462
+ updateMarketBusy: "The plugin market is running another update. Retry shortly.",
463
+ updateMarketAgentsRunning: "Agents are running, so the plugin market will not install now. Retry when they finish.",
464
+ updateMarketTooFresh: "This release is still inside the plugin market's release-age wait. Retry later, or choose Install Anyway.",
465
+ updateMarketVersionUnchanged: "The plugin market did not resolve a newer version (its mirror may still be syncing). Retry later, or choose Install Anyway.",
466
+ updateMarketDowngrade: "The plugin market resolved a version older than the current one and restored it; nothing changed.",
467
+ updateMarketMismatch: "The plugin market installed a version other than the target and restored the previous one. Retry after the mirror catches up.",
468
+ updateMarketTimeout: "The plugin market installation timed out. Retry later.",
469
+ updateMarketForbidden: "The plugin market refused this update request. Retry from the page DSH opened.",
470
+ updateMarketLost: "The plugin market operation can no longer be followed (DSH may have restarted). Check for updates again to confirm the current version.",
471
+ updateMarketUnreachable: "Could not reach the DSH plugin market update API. Make sure dshmarket is enabled and retry.",
472
+ updateMarketRestartFailed: "The restart request failed. Restart DSH manually.",
473
+ updateMarketFailed: "The plugin market installation failed. See the plugin market for details, retry later, or use the offline package download.",
474
+ updateAutoInstallLabel: "Automatically Install Updates",
475
+ updateAutoInstallHint: "Install versions found by automatic checks through the DSH plugin market (requires Auto Check and dshmarket; runs only while a DSH page is open, at most once per version every 6 hours). A DSH restart or page refresh may be needed for it to take effect.",
371
476
  };
372
477
 
373
478
  const CSS = `
@@ -858,7 +963,9 @@ window.__ModuleLoader__.load({
858
963
  .dcpa-update-summary-current { flex: 0 1 auto; }
859
964
  .dcpa-update-summary-status { flex: 1; text-align: right; }
860
965
  .dcpa-update-summary-status[data-state="available"],
861
- .dcpa-update-summary-status[data-state="downloaded"] { color: var(--dsw-alias-brand-primary); }
966
+ .dcpa-update-summary-status[data-state="downloaded"],
967
+ .dcpa-update-summary-status[data-state="installing"],
968
+ .dcpa-update-summary-status[data-state="installed"] { color: var(--dsw-alias-brand-primary); }
862
969
  .dcpa-update-summary-status[data-state="error"] { color: var(--dsw-alias-state-error-primary); }
863
970
  .dcpa-update-summary::after {
864
971
  content: "";
@@ -993,6 +1100,7 @@ window.__ModuleLoader__.load({
993
1100
  "invalid-metadata": "updateMetadataError",
994
1101
  "invalid-response": "updateMetadataError",
995
1102
  "service-unavailable": "updateServiceUnavailableError",
1103
+ "forbidden-origin": "updateOriginError",
996
1104
  "channel-unavailable": "updateChannelUnavailable",
997
1105
  "unavailable-channel": "updateChannelUnavailable",
998
1106
  "integrity-error": "updateIntegrityError",
@@ -1062,8 +1170,303 @@ window.__ModuleLoader__.load({
1062
1170
  return body.update;
1063
1171
  }
1064
1172
 
1173
+ // #region dshmarket installation (Update API v1)
1174
+
1175
+ const MARKET_TERMINAL_STATES = new Set(["succeeded", "failed", "cancelled", "rolled-back"]);
1176
+ const MARKET_POLL_MS = 1500;
1177
+ const MARKET_MAX_POLL_FAILURES = 20;
1178
+ const MARKET_FORCEABLE = new Set(["RELEASE_TOO_FRESH", "VERSION_UNCHANGED"]);
1179
+ const AUTO_INSTALL_FIRST_MS = 15000;
1180
+ const AUTO_INSTALL_INTERVAL_MS = 5 * 60 * 1000;
1181
+ const AUTO_INSTALL_RETRY_MS = 6 * 60 * 60 * 1000;
1182
+ const AUTO_INSTALL_MEMORY_KEY = `${PACKAGE_NAME}:market-auto-install`;
1183
+ const MARKET_ERROR_KEYS = {
1184
+ OPERATION_BUSY: "updateMarketBusy",
1185
+ AGENTS_RUNNING: "updateMarketAgentsRunning",
1186
+ RELEASE_TOO_FRESH: "updateMarketTooFresh",
1187
+ VERSION_UNCHANGED: "updateMarketVersionUnchanged",
1188
+ DOWNGRADE_DETECTED: "updateMarketDowngrade",
1189
+ RESOLVED_VERSION_MISMATCH: "updateMarketMismatch",
1190
+ UPDATE_TIMEOUT: "updateMarketTimeout",
1191
+ UPDATE_FORBIDDEN: "updateMarketForbidden",
1192
+ PLUGIN_NOT_INSTALLED: "updateNotInstalledHint",
1193
+ OPERATION_LOST: "updateMarketLost",
1194
+ MARKET_UNREACHABLE: "updateMarketUnreachable",
1195
+ RESTART_FAILED: "updateMarketRestartFailed",
1196
+ };
1197
+
1198
+ function parseVersion(value) {
1199
+ const match = typeof value === "string"
1200
+ ? /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value)
1201
+ : null;
1202
+ return match ? { parts: match.slice(1, 4).map(Number), pre: match[4] ? match[4].split(".") : [] } : null;
1203
+ }
1204
+
1205
+ /** SemVer precedence, or null when either side is not a version. */
1206
+ function compareVersions(a, b) {
1207
+ const x = parseVersion(a), y = parseVersion(b);
1208
+ if (!x || !y) return null;
1209
+ for (let i = 0; i < 3; i++) if (x.parts[i] !== y.parts[i]) return x.parts[i] > y.parts[i] ? 1 : -1;
1210
+ if (!x.pre.length || !y.pre.length) return x.pre.length ? -1 : y.pre.length ? 1 : 0;
1211
+ for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
1212
+ const p = x.pre[i], q = y.pre[i];
1213
+ if (p === undefined || q === undefined) return p === undefined ? -1 : 1;
1214
+ if (p === q) continue;
1215
+ const pn = /^\d+$/.test(p), qn = /^\d+$/.test(q);
1216
+ if (pn && qn) return Number(p) > Number(q) ? 1 : -1;
1217
+ if (pn !== qn) return pn ? -1 : 1;
1218
+ return p > q ? 1 : -1;
1219
+ }
1220
+ return 0;
1221
+ }
1222
+
1223
+ function validMarketOperation(value) {
1224
+ return !!value && typeof value === "object" && value.schema === MARKET_SCHEMA &&
1225
+ typeof value.operationId === "string" && value.operationId !== "" &&
1226
+ value.packageName === PACKAGE_NAME && typeof value.state === "string";
1227
+ }
1228
+
1229
+ const marketFailureCode = (operation) =>
1230
+ typeof operation?.failure?.code === "string" && operation.failure.code ? operation.failure.code : "UPDATE_FAILED";
1231
+
1232
+ function marketErrorText(t, code) {
1233
+ return t(Object.prototype.hasOwnProperty.call(MARKET_ERROR_KEYS, code) ? MARKET_ERROR_KEYS[code] : "updateMarketFailed");
1234
+ }
1235
+
1236
+ /**
1237
+ * Shared, page-wide client of the dshmarket Update API. One instance lives
1238
+ * per client activation so the settings card and the background
1239
+ * auto-install runner observe the same operation.
1240
+ */
1241
+ function createMarketUpdater(io = {}) {
1242
+ const doFetch = io.fetch ?? ((url, init) => fetch(url, init));
1243
+ const later = io.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
1244
+ const cancelLater = io.clearTimeout ?? ((id) => clearTimeout(id));
1245
+ let state = {
1246
+ phase: "unknown", // unknown | discovering | ready | unavailable
1247
+ runtime: null,
1248
+ restartSupported: false,
1249
+ installing: false,
1250
+ automatic: false,
1251
+ targetVersion: null,
1252
+ operation: null,
1253
+ errorCode: null,
1254
+ restarting: false,
1255
+ restartRequested: false,
1256
+ };
1257
+ const listeners = new Set();
1258
+ const controllers = new Set();
1259
+ let disposed = false;
1260
+ let discovering = null;
1261
+ let pollTimer = null;
1262
+ let pollFailures = 0;
1263
+
1264
+ const set = (patch) => {
1265
+ if (disposed) return;
1266
+ state = { ...state, ...patch };
1267
+ for (const listener of [...listeners]) listener(state);
1268
+ };
1269
+ const request = async (url, init = {}) => {
1270
+ const controller = new AbortController();
1271
+ controllers.add(controller);
1272
+ try {
1273
+ const res = await doFetch(url, { cache: "no-store", ...init, signal: controller.signal });
1274
+ let body = null;
1275
+ try { body = await res.json(); } catch {}
1276
+ return { status: res.status, ok: res.ok, body };
1277
+ } finally {
1278
+ controllers.delete(controller);
1279
+ }
1280
+ };
1281
+ const post = (url, payload) => request(url, {
1282
+ method: "POST",
1283
+ headers: { "content-type": "application/json" },
1284
+ body: JSON.stringify(payload),
1285
+ });
1286
+ const applyOperation = (operation) => {
1287
+ const done = MARKET_TERMINAL_STATES.has(operation.state);
1288
+ set({
1289
+ operation,
1290
+ installing: !done,
1291
+ errorCode: done && operation.state === "failed" ? marketFailureCode(operation) : null,
1292
+ });
1293
+ if (!done) schedulePoll();
1294
+ };
1295
+ const schedulePoll = () => {
1296
+ if (pollTimer !== null) cancelLater(pollTimer);
1297
+ pollTimer = later(poll, MARKET_POLL_MS);
1298
+ };
1299
+ const poll = async () => {
1300
+ pollTimer = null;
1301
+ const id = state.operation?.operationId;
1302
+ if (disposed || !id) return;
1303
+ try {
1304
+ const { status, body } = await request(`${MARKET_API.operations}?operationId=${encodeURIComponent(id)}`);
1305
+ if (disposed || state.operation?.operationId !== id) return;
1306
+ if (status === 404) {
1307
+ // Operations live in the current Host process only.
1308
+ set({ installing: false, errorCode: "OPERATION_LOST" });
1309
+ return;
1310
+ }
1311
+ if (status !== 200 || !validMarketOperation(body?.operation) || body.operation.operationId !== id) {
1312
+ throw new Error("Invalid market operation");
1313
+ }
1314
+ pollFailures = 0;
1315
+ applyOperation(body.operation);
1316
+ } catch (error) {
1317
+ if (disposed || error?.name === "AbortError") return;
1318
+ if (++pollFailures >= MARKET_MAX_POLL_FAILURES) set({ installing: false, errorCode: "OPERATION_LOST" });
1319
+ else schedulePoll();
1320
+ }
1321
+ };
1322
+
1323
+ return {
1324
+ getState: () => state,
1325
+ subscribe(listener) {
1326
+ listeners.add(listener);
1327
+ return () => listeners.delete(listener);
1328
+ },
1329
+ /** Reads only the local capability document; never contacts a registry. */
1330
+ discover() {
1331
+ if (disposed) return Promise.resolve(state);
1332
+ if (discovering) return discovering;
1333
+ if (state.phase !== "ready") set({ phase: "discovering" });
1334
+ discovering = request(MARKET_API.capabilities)
1335
+ .then(({ ok, body }) => {
1336
+ const usable = ok && body?.schema === MARKET_SCHEMA && body.apiVersion === 1 && body.features?.update === true;
1337
+ set({
1338
+ phase: usable ? "ready" : "unavailable",
1339
+ runtime: usable && typeof body.runtime === "string" ? body.runtime : null,
1340
+ restartSupported: usable && body.features?.restart === true && body.restart?.supported === true,
1341
+ });
1342
+ }, () => {
1343
+ set({ phase: "unavailable", runtime: null, restartSupported: false });
1344
+ })
1345
+ .finally(() => { discovering = null; })
1346
+ .then(() => state);
1347
+ return discovering;
1348
+ },
1349
+ /** Starts one market update operation, then follows it until it settles. */
1350
+ async install({ targetVersion = null, force = false, automatic = false } = {}) {
1351
+ if (disposed || state.phase !== "ready" || state.installing) return state;
1352
+ set({ installing: true, automatic, targetVersion, operation: null, errorCode: null, restartRequested: false });
1353
+ try {
1354
+ const { status, body } = await post(
1355
+ MARKET_API.updates,
1356
+ force ? { packageName: PACKAGE_NAME, force: true } : { packageName: PACKAGE_NAME }
1357
+ );
1358
+ if (disposed) return state;
1359
+ if (status === 202 && validMarketOperation(body?.operation)) {
1360
+ pollFailures = 0;
1361
+ applyOperation(body.operation);
1362
+ return state;
1363
+ }
1364
+ const code = typeof body?.failure?.code === "string" && body.failure.code ? body.failure.code
1365
+ : status === 409 ? "OPERATION_BUSY"
1366
+ : status === 403 ? "UPDATE_FORBIDDEN"
1367
+ : status === 400 ? "UPDATE_REJECTED"
1368
+ : status === 404 ? "MARKET_UNREACHABLE"
1369
+ : "UPDATE_FAILED";
1370
+ set({ installing: false, errorCode: code });
1371
+ } catch (error) {
1372
+ if (!disposed) set({ installing: false, errorCode: error?.name === "AbortError" ? null : "MARKET_UNREACHABLE" });
1373
+ }
1374
+ return state;
1375
+ },
1376
+ /** Only offered when the market reports that it owns process restarts. */
1377
+ async restart() {
1378
+ if (disposed || !state.restartSupported || state.restarting) return state;
1379
+ set({ restarting: true, errorCode: null });
1380
+ try {
1381
+ const { ok } = await post(MARKET_API.restart, {});
1382
+ set({ restarting: false, restartRequested: ok, errorCode: ok ? null : "RESTART_FAILED" });
1383
+ } catch (error) {
1384
+ set({ restarting: false, errorCode: error?.name === "AbortError" ? null : "RESTART_FAILED" });
1385
+ }
1386
+ return state;
1387
+ },
1388
+ dispose() {
1389
+ disposed = true;
1390
+ if (pollTimer !== null) cancelLater(pollTimer);
1391
+ pollTimer = null;
1392
+ for (const controller of controllers) controller.abort();
1393
+ controllers.clear();
1394
+ listeners.clear();
1395
+ },
1396
+ };
1397
+ }
1398
+
1399
+ function readAutoInstallMemory() {
1400
+ try {
1401
+ const raw = JSON.parse(globalThis.localStorage?.getItem(AUTO_INSTALL_MEMORY_KEY) ?? "null");
1402
+ return raw && typeof raw.version === "string" && Number.isFinite(raw.at) ? raw : null;
1403
+ } catch {
1404
+ return null;
1405
+ }
1406
+ }
1407
+
1408
+ function writeAutoInstallMemory(record) {
1409
+ try { globalThis.localStorage?.setItem(AUTO_INSTALL_MEMORY_KEY, JSON.stringify(record)); } catch {}
1410
+ }
1411
+
1412
+ /**
1413
+ * Opt-in automatic installation. The Host's own automatic check finds the
1414
+ * version (channel/region aware); dshmarket installs it. Runs only while a
1415
+ * DSH page is open, reads the local status endpoint only, and tries each
1416
+ * version at most once per {@link AUTO_INSTALL_RETRY_MS} across reloads.
1417
+ */
1418
+ function startAutoInstall(market, io = {}) {
1419
+ const later = io.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
1420
+ const cancelLater = io.clearTimeout ?? ((id) => clearTimeout(id));
1421
+ const now = io.now ?? (() => Date.now());
1422
+ const memory = io.memory ?? { read: readAutoInstallMemory, write: writeAutoInstallMemory };
1423
+ const controller = new AbortController();
1424
+ const readStatus = io.readStatus ?? ((signal) => requestPluginUpdate(API.updateStatus, undefined, signal));
1425
+ let stopped = false;
1426
+ let timer = null;
1427
+ const unsubscribe = market.subscribe((state) => {
1428
+ const operation = state.operation;
1429
+ if (state.automatic && operation?.state === "succeeded" && typeof state.targetVersion === "string" &&
1430
+ compareVersions(operation.installedVersion, operation.beforeVersion) === 1) {
1431
+ memory.write({ version: state.targetVersion, at: now(), done: true });
1432
+ }
1433
+ });
1434
+ const tick = async () => {
1435
+ timer = null;
1436
+ try {
1437
+ const status = await readStatus(controller.signal);
1438
+ if (stopped) return;
1439
+ const version = status?.latestVersion;
1440
+ if (status?.autoInstall !== true || status.autoCheck !== true || status.available !== true ||
1441
+ status.blockedReason || typeof version !== "string" || compareVersions(version, status.currentVersion) !== 1) return;
1442
+ const last = memory.read();
1443
+ if (last && last.version === version && (last.done === true || now() - last.at < AUTO_INSTALL_RETRY_MS)) return;
1444
+ if (market.getState().phase !== "ready") await market.discover();
1445
+ const snapshot = market.getState();
1446
+ if (stopped || snapshot.phase !== "ready" || snapshot.installing) return;
1447
+ memory.write({ version, at: now() });
1448
+ await market.install({ targetVersion: version, automatic: true });
1449
+ } catch {
1450
+ // Background work never surfaces errors; the card shows the market state.
1451
+ } finally {
1452
+ if (!stopped) timer = later(tick, AUTO_INSTALL_INTERVAL_MS);
1453
+ }
1454
+ };
1455
+ timer = later(tick, AUTO_INSTALL_FIRST_MS);
1456
+ return () => {
1457
+ stopped = true;
1458
+ if (timer !== null) cancelLater(timer);
1459
+ controller.abort();
1460
+ unsubscribe();
1461
+ };
1462
+ }
1463
+
1464
+ let sharedMarket = null;
1465
+
1466
+ // #endregion
1467
+
1065
1468
  /** Update state is isolated: polling never reloads account data or drafts. */
1066
- function PluginUpdatePanel({ t }) {
1469
+ function PluginUpdatePanel({ t, market = null, initiallyOpen = false }) {
1067
1470
  const [update, setUpdate] = useState(null);
1068
1471
  const [loading, setLoading] = useState(true);
1069
1472
  const [busy, setBusy] = useState(null);
@@ -1169,6 +1572,21 @@ window.__ModuleLoader__.load({
1169
1572
  }
1170
1573
  }, []);
1171
1574
 
1575
+ // dshmarket state is shared with the background auto-install runner.
1576
+ const [marketState, setMarketState] = useState(() => market?.getState() ?? null);
1577
+ const [confirmInstall, setConfirmInstall] = useState(null);
1578
+ useEffect(() => {
1579
+ if (!market) return undefined;
1580
+ let live = true;
1581
+ const unsubscribe = market.subscribe((next) => { if (live) setMarketState(next); });
1582
+ setMarketState(market.getState());
1583
+ if (market.getState().phase === "unknown") void market.discover();
1584
+ return () => {
1585
+ live = false;
1586
+ unsubscribe();
1587
+ };
1588
+ }, [market]);
1589
+
1172
1590
  const working = loading || busy !== null || update?.checking === true || update?.downloading === true;
1173
1591
  const blockedReason = update?.blockedReason;
1174
1592
  const downloadBlocked = !!blockedReason && blockedReason !== "linked-install";
@@ -1189,7 +1607,17 @@ window.__ModuleLoader__.load({
1189
1607
  const statusError = update?.error || update?.errorCode
1190
1608
  ? { updateCode: update.errorCode }
1191
1609
  : null;
1192
- const compactState = loading ? "loading" : downloading ? "downloading" : checking ? "checking" :
1610
+ const linkedInstall = blockedReason === "linked-install";
1611
+ const marketReady = marketState?.phase === "ready";
1612
+ const marketOperation = marketState?.operation ?? null;
1613
+ const marketInstalling = marketState?.installing === true;
1614
+ const marketInstalledVersion = marketOperation?.state === "succeeded" &&
1615
+ compareVersions(marketOperation.installedVersion, marketOperation.beforeVersion) === 1
1616
+ ? marketOperation.installedVersion : null;
1617
+ const canMarketInstall = marketReady && !linkedInstall && !downloadBlocked && !marketInstalling &&
1618
+ !marketInstalledVersion && !!update?.available && typeof update.latestVersion === "string" && !!update.latestVersion;
1619
+ const compactState = loading ? "loading" : marketInstalling ? "installing" : downloading ? "downloading" : checking ? "checking" :
1620
+ marketInstalledVersion ? "installed" :
1193
1621
  error || statusError ? "error" : update?.downloadedVersion && update.downloadedVersion === update.latestVersion ? "downloaded" :
1194
1622
  update?.available ? "available" : "idle";
1195
1623
  // Name the actual failure instead of a generic "update error": the
@@ -1202,7 +1630,9 @@ window.__ModuleLoader__.load({
1202
1630
  : failureCode === "service-unavailable" ? "updateCompactStatusUnavailable"
1203
1631
  : failureCode === "channel-unavailable" ? "updateCompactErrorChannel"
1204
1632
  : "updateCompactError";
1205
- const compactText = loading ? t("updateLoading") : downloading ? t("updateDownloading") : checking ? t("updateChecking") :
1633
+ const compactText = loading ? t("updateLoading") : marketInstalling ? t("updateMarketInstalling") :
1634
+ downloading ? t("updateDownloading") : checking ? t("updateChecking") :
1635
+ marketInstalledVersion ? t("updateCompactInstalled").replace("{version}", marketInstalledVersion) :
1206
1636
  error || statusError ? t(compactErrorKey) : compactState === "downloaded"
1207
1637
  ? t("updateCompactDownloaded").replace("{version}", update.downloadedVersion)
1208
1638
  : update?.available ? t("updateCompactAvailable").replace("{version}", availableVersion)
@@ -1229,7 +1659,7 @@ window.__ModuleLoader__.load({
1229
1659
  }, t(key))
1230
1660
  )
1231
1661
  );
1232
- const toggle = (field, label, hint, checked) =>
1662
+ const toggle = (field, label, hint, checked, unavailable = false) =>
1233
1663
  h(
1234
1664
  "div",
1235
1665
  { className: "dcpa-update-row dcpa-switch-row" },
@@ -1243,16 +1673,143 @@ window.__ModuleLoader__.load({
1243
1673
  className: "dcpa-switch",
1244
1674
  "aria-label": t(label),
1245
1675
  "aria-checked": checked,
1246
- disabled: working || !update,
1676
+ // An unavailable feature can still be switched off, never on.
1677
+ disabled: working || !update || unavailable && !checked,
1247
1678
  onClick: () => { void runAction("save", { [field]: !checked }); },
1248
1679
  }, h("span", { className: "dcpa-switch-thumb" }))
1249
1680
  );
1250
1681
 
1682
+ // One-click/automatic installation through dshmarket. Hidden entirely
1683
+ // when the card is rendered without a market client (tests, previews).
1684
+ let marketSection = null;
1685
+ if (market) {
1686
+ const phase = marketState?.phase ?? "unknown";
1687
+ const hintKey = phase === "ready"
1688
+ ? linkedInstall ? "updateMarketLinkedHint" : "updateMarketReadyHint"
1689
+ : phase === "unavailable" ? "updateMarketUnavailableHint" : "updateMarketDetecting";
1690
+ const operation = marketOperation;
1691
+ const progress = operation?.progress;
1692
+ const percent = typeof progress?.percent === "number" && Number.isFinite(progress.percent) ? Math.round(progress.percent) : null;
1693
+ const errorCode = marketState?.errorCode ?? null;
1694
+ const outcome = operation?.outcome ?? {};
1695
+ const target = marketState?.targetVersion;
1696
+ const prefix = marketState?.automatic ? `${t("updateMarketAutomaticPrefix")} ` : "";
1697
+ let result = null;
1698
+ if (marketInstalling) {
1699
+ result = h("div", { className: "dcpa-update-notice", role: "status" },
1700
+ h("p", { className: "dcpa-label" }, prefix + (percent === null
1701
+ ? t("updateMarketInstalling")
1702
+ : t("updateMarketProgress").replace("{percent}", String(percent))))
1703
+ );
1704
+ } else if (marketInstalledVersion) {
1705
+ const restartHint = outcome.restartRequired
1706
+ ? marketState?.restartSupported ? "updateMarketRestartHint"
1707
+ : marketState?.runtime === "desktop" ? "updateMarketRestartDesktopHint" : "updateMarketRestartManualHint"
1708
+ : outcome.refreshRequired ? "updateMarketRefreshHint" : "updateMarketAppliedHint";
1709
+ result = h("div", { className: "dcpa-update-notice", role: "status" },
1710
+ h("p", { className: "dcpa-success" }, prefix + t("updateMarketInstalled").replace("{version}", marketInstalledVersion)),
1711
+ typeof target === "string" && compareVersions(marketInstalledVersion, target) === -1
1712
+ ? h("p", { className: "dcpa-hint" }, t("updateMarketVersionLower").replace("{installed}", marketInstalledVersion).replace("{target}", target))
1713
+ : null,
1714
+ h("p", { className: "dcpa-hint" }, t(marketState?.restartRequested ? "updateMarketRestartRequested" : restartHint)),
1715
+ outcome.restartRequired && marketState?.restartSupported && !marketState?.restartRequested
1716
+ ? h("div", { className: "dcpa-actions" },
1717
+ h("button", {
1718
+ type: "button",
1719
+ className: "dcpa-btn dcpa-btn-primary",
1720
+ disabled: marketState?.restarting === true,
1721
+ onClick: () => { void market.restart(); },
1722
+ }, marketState?.restarting ? t("updateMarketRestarting") : t("updateMarketRestartBtn")))
1723
+ : !outcome.restartRequired && outcome.refreshRequired
1724
+ ? h("div", { className: "dcpa-actions" },
1725
+ h("button", {
1726
+ type: "button",
1727
+ className: "dcpa-btn dcpa-btn-primary",
1728
+ onClick: () => { globalThis.location?.reload(); },
1729
+ }, t("updateMarketRefreshBtn")))
1730
+ : null
1731
+ );
1732
+ } else if (operation?.state === "succeeded") {
1733
+ result = h("div", { className: "dcpa-update-notice", role: "status" },
1734
+ h("p", { className: "dcpa-hint" }, prefix + t("updateMarketUnchanged")));
1735
+ } else if (operation?.state === "cancelled" || operation?.state === "rolled-back") {
1736
+ result = h("div", { className: "dcpa-update-notice", role: "status" },
1737
+ h("p", { className: "dcpa-hint" }, prefix + t(operation.state === "cancelled" ? "updateMarketCancelled" : "updateMarketRolledBack")));
1738
+ }
1739
+ const errorBlock = errorCode && !marketInstalling
1740
+ ? h("div", { className: "dcpa-update-notice" },
1741
+ h("p", { className: "dcpa-error", role: "alert" }, prefix + marketErrorText(t, errorCode)),
1742
+ MARKET_FORCEABLE.has(errorCode) && canMarketInstall
1743
+ ? h("div", { className: "dcpa-actions" },
1744
+ h("button", {
1745
+ type: "button",
1746
+ className: "dcpa-btn",
1747
+ disabled: working,
1748
+ onClick: () => { void market.install({ targetVersion: update.latestVersion, force: true }); },
1749
+ }, t("updateMarketForceBtn")))
1750
+ : null)
1751
+ : null;
1752
+ marketSection = h(React.Fragment, null,
1753
+ h("div", { className: "dcpa-update-row" },
1754
+ h("div", { className: "dcpa-field" },
1755
+ h("span", { className: "dcpa-label" }, t("updateMarketLabel")),
1756
+ h("p", { className: "dcpa-hint" }, t(hintKey))
1757
+ ),
1758
+ phase === "ready" && !linkedInstall
1759
+ ? h("div", { className: "dcpa-actions" },
1760
+ h("button", {
1761
+ type: "button",
1762
+ className: "dcpa-btn dcpa-btn-primary",
1763
+ disabled: working || !canMarketInstall,
1764
+ onClick: () => { setConfirmVersion(null); setConfirmInstall(update.latestVersion); },
1765
+ }, marketInstalling ? t("updateMarketInstalling")
1766
+ : marketInstalledVersion ? t("updateMarketInstalledBtn") : t("updateMarketInstallBtn")))
1767
+ : phase === "unavailable"
1768
+ ? h("div", { className: "dcpa-actions" },
1769
+ h("button", { type: "button", className: "dcpa-btn", onClick: () => { void market.discover(); } }, t("updateMarketRedetectBtn")))
1770
+ : null
1771
+ ),
1772
+ confirmInstall ? h("div", {
1773
+ className: "dcpa-update-notice",
1774
+ role: "alertdialog",
1775
+ "aria-modal": false,
1776
+ "aria-label": t("updateMarketConfirmTitle").replace("{version}", confirmInstall),
1777
+ },
1778
+ h("p", { className: "dcpa-label" }, t("updateMarketConfirmTitle").replace("{version}", confirmInstall)),
1779
+ h("p", { className: "dcpa-hint" }, t("updateMarketConfirmHint")),
1780
+ h("div", { className: "dcpa-actions" },
1781
+ h("button", {
1782
+ type: "button",
1783
+ className: "dcpa-btn dcpa-btn-primary",
1784
+ disabled: working || !canMarketInstall || confirmInstall !== update?.latestVersion,
1785
+ onClick: () => {
1786
+ const version = confirmInstall;
1787
+ setConfirmInstall(null);
1788
+ void market.install({ targetVersion: version });
1789
+ },
1790
+ }, t("updateMarketConfirmBtn")),
1791
+ h("button", { type: "button", className: "dcpa-btn", onClick: () => setConfirmInstall(null) }, t("updateCancelBtn"))
1792
+ ),
1793
+ confirmInstall !== update?.latestVersion ? h("p", { className: "dcpa-hint" }, t("updateStaleVersion")) : null
1794
+ ) : null,
1795
+ result,
1796
+ errorBlock
1797
+ );
1798
+ }
1799
+
1251
1800
  // Native details starts closed. Do not control `open`: polling and saves
1252
1801
  // must preserve the user's expanded/collapsed choice without remounting.
1802
+ // `initiallyOpen` (the update-only plugin page) passes a constant `true`
1803
+ // that never changes, so React never rewrites it after mount and the
1804
+ // user can still collapse the card.
1253
1805
  return h(
1254
1806
  "details",
1255
- { className: "dcpa-card dcpa-update-card", "aria-label": t("updateTitle"), "aria-busy": working },
1807
+ {
1808
+ className: "dcpa-card dcpa-update-card",
1809
+ "aria-label": t("updateTitle"),
1810
+ "aria-busy": working,
1811
+ ...(initiallyOpen ? { open: true } : {}),
1812
+ },
1256
1813
  h("summary", { className: "dcpa-update-summary", title: t("updateToggleDetails") },
1257
1814
  h("span", { className: "dcpa-update-summary-title" }, t("updateTitle")),
1258
1815
  h("span", { className: "dcpa-update-summary-current" },
@@ -1280,9 +1837,9 @@ window.__ModuleLoader__.load({
1280
1837
  }, checking ? t("updateChecking") : !update && !loading ? t("updateRetryBtn") : t("updateCheckBtn")),
1281
1838
  h("button", {
1282
1839
  type: "button",
1283
- className: "dcpa-btn dcpa-btn-primary",
1840
+ className: marketReady && !linkedInstall ? "dcpa-btn" : "dcpa-btn dcpa-btn-primary",
1284
1841
  disabled: working || !canDownload,
1285
- onClick: () => setConfirmVersion(update.latestVersion),
1842
+ onClick: () => { setConfirmInstall(null); setConfirmVersion(update.latestVersion); },
1286
1843
  }, downloading ? t("updateDownloading") : update?.downloadedVersion === update?.latestVersion && update?.downloadedVersion
1287
1844
  ? t("updateDownloadedBtn") : t("updateDownloadBtn"))
1288
1845
  )
@@ -1306,6 +1863,7 @@ window.__ModuleLoader__.load({
1306
1863
  ),
1307
1864
  confirmVersion !== update?.latestVersion ? h("p", { className: "dcpa-hint" }, t("updateStaleVersion")) : null
1308
1865
  ) : null,
1866
+ marketSection,
1309
1867
  h("div", { className: "dcpa-update-row" },
1310
1868
  h("div", { className: "dcpa-field" },
1311
1869
  h("span", { className: "dcpa-label" }, t("updateChannelLabel")),
@@ -1327,6 +1885,8 @@ window.__ModuleLoader__.load({
1327
1885
  ),
1328
1886
  toggle("autoCheck", "updateAutoCheckLabel", "updateAutoCheckHint", update?.autoCheck !== false),
1329
1887
  toggle("autoDownload", "updateAutoDownloadLabel", "updateAutoDownloadHint", update?.autoDownload === true),
1888
+ toggle("autoInstall", "updateAutoInstallLabel", "updateAutoInstallHint", update?.autoInstall === true,
1889
+ !marketReady || linkedInstall || downloadBlocked),
1330
1890
  blockedReason ? h("div", { className: "dcpa-update-notice" }, h("p", { className: "dcpa-hint" }, t(blockedKey))) : null,
1331
1891
  update?.downloadedVersion && update?.downloadPath ? h("div", { className: "dcpa-update-notice", role: "status" },
1332
1892
  h("p", { className: "dcpa-label" }, t(blockedReason === "linked-install" ? "updateLinkedDownloadedHint" : "updateDownloadedHint").replace("{version}", update.downloadedVersion)),
@@ -1351,8 +1911,23 @@ window.__ModuleLoader__.load({
1351
1911
  }
1352
1912
 
1353
1913
  /**
1354
- * Account list + switcher. Each account is edited in its own
1355
- * {@link AccountPanel}, remounted when the selection changes.
1914
+ * The plugin's own page under Plugins (bundle page and Plugins tab): only
1915
+ * plugin updates. Account and model settings live in their own settings
1916
+ * section ({@link CpaSettingsPanel}).
1917
+ */
1918
+ function PluginUpdatePage({ t }) {
1919
+ return h(
1920
+ "div",
1921
+ { className: "dcpa-wrap", "data-dsh-plugin": MODULE_ID },
1922
+ h("p", { className: "dcpa-hint" }, t("updatePageHint")),
1923
+ h(PluginUpdatePanel, { t, market: sharedMarket, initiallyOpen: true })
1924
+ );
1925
+ }
1926
+
1927
+ /**
1928
+ * Account list + switcher (the "CLIProxyAPI" settings section). Each
1929
+ * account is edited in its own {@link AccountPanel}, remounted when the
1930
+ * selection changes.
1356
1931
  */
1357
1932
  function CpaSettingsPanel({ t }) {
1358
1933
  const [loading, setLoading] = useState(true);
@@ -1504,7 +2079,6 @@ window.__ModuleLoader__.load({
1504
2079
  error ? h("p", { className: "dcpa-error", role: "alert" }, error) : null,
1505
2080
  toast ? h("p", { className: "dcpa-success", role: "status" }, toast) : null
1506
2081
  ),
1507
- h(PluginUpdatePanel, { t }),
1508
2082
  active
1509
2083
  ? h(AccountPanel, {
1510
2084
  key: active.id,
@@ -2478,7 +3052,37 @@ window.__ModuleLoader__.load({
2478
3052
  );
2479
3053
  }
2480
3054
 
2481
- // 1. Register on the Bundle detail page in Sidebar -> Plugins -> CLIProxyAPI
3055
+ // dshmarket installation client, shared by the card and the opt-in
3056
+ // background auto-install runner. No DOM writes; timers and requests
3057
+ // are disposed with this activation (HMR/unmount).
3058
+ ctx.effect(() => {
3059
+ const market = createMarketUpdater();
3060
+ sharedMarket = market;
3061
+ const stopAutoInstall = startAutoInstall(market);
3062
+ return () => {
3063
+ stopAutoInstall();
3064
+ market.dispose();
3065
+ if (sharedMarket === market) sharedMarket = null;
3066
+ };
3067
+ }, "dsh-cliproxyapi: market updates");
3068
+
3069
+ // 1. Accounts and models: a page of its own in the Settings dialog's left
3070
+ // navigation (like dshmarket's "Market" section). The slot owner gives
3071
+ // unregistered ids the default nav glyph; no DOM is touched here.
3072
+ ctx.slots.inject("settings.section", () =>
3073
+ ctx.slots.register(
3074
+ {
3075
+ name: "settings.section",
3076
+ id: SETTINGS_SECTION_ID,
3077
+ order: 45,
3078
+ label: () => t("nav"),
3079
+ locale: NS,
3080
+ },
3081
+ () => h(CpaSettingsPanel, { t })
3082
+ )
3083
+ );
3084
+
3085
+ // 2. Plugin updates only, on the bundle page in Sidebar -> Plugins.
2482
3086
  ctx.slots.inject("plugins.bundle.config", () =>
2483
3087
  ctx.slots.register(
2484
3088
  {
@@ -2486,11 +3090,11 @@ window.__ModuleLoader__.load({
2486
3090
  key: MODULE_ID,
2487
3091
  locale: NS,
2488
3092
  },
2489
- (ownerProps = {}) => (ownerProps.view === "summary" ? null : h(CpaSettingsPanel, { t }))
3093
+ (ownerProps = {}) => (ownerProps.view === "summary" ? null : h(PluginUpdatePage, { t }))
2490
3094
  )
2491
3095
  );
2492
3096
 
2493
- // 2. Register as a dedicated tab in Settings -> Built-in plugins -> CLIProxyAPI
3097
+ // 3. Plugin updates only, as a tab in Settings -> Plugins.
2494
3098
  ctx.slots.inject("settings.plugins.tab", () =>
2495
3099
  ctx.slots.register(
2496
3100
  {
@@ -2500,7 +3104,7 @@ window.__ModuleLoader__.load({
2500
3104
  label: () => t("nav"),
2501
3105
  locale: NS,
2502
3106
  },
2503
- () => h(CpaSettingsPanel, { t })
3107
+ () => h(PluginUpdatePage, { t })
2504
3108
  )
2505
3109
  );
2506
3110
  }
package/lib/index.js CHANGED
@@ -1695,18 +1695,33 @@ function admitRequest(ctx, req, res, expectedMethod) {
1695
1695
  return true;
1696
1696
  }
1697
1697
 
1698
- /** Update writes are browser-only: require an explicit, exact same Origin. */
1698
+ /** Same authority, using the scheme the Origin itself declares (host comparison only). */
1699
+ function sameAuthorityOrigin(origin, host) {
1700
+ let originUrl;
1701
+ try { originUrl = new URL(origin); } catch { return false; }
1702
+ if (originUrl.protocol !== "http:" && originUrl.protocol !== "https:") return false;
1703
+ if (originUrl.username || originUrl.password) return false;
1704
+ try {
1705
+ return originUrl.host === new URL(`${originUrl.protocol}//${host}`).host;
1706
+ } catch {
1707
+ return false;
1708
+ }
1709
+ }
1710
+
1711
+ /**
1712
+ * Update writes need an authenticated browser request. The host connection
1713
+ * service already enforces the Host/Origin fence and its session cookie, so
1714
+ * this only narrows it: when a browser sends an Origin it must belong to this
1715
+ * listener. A carrier that omits Origin (the desktop shell proxies API calls
1716
+ * without it) is allowed, because the session cookie is still required and an
1717
+ * unauthenticated caller is rejected before reaching the route.
1718
+ */
1699
1719
  function admitUpdateRequest(ctx, req, res, method) {
1700
1720
  if (!admitRequest(ctx, req, res, method)) return false;
1701
1721
  if (method === "GET") return true;
1702
- const protocol = req.socket?.encrypted ? "https:" : "http:";
1703
- try {
1704
- const origin = new URL(req.headers.origin);
1705
- const target = new URL(`${protocol}//${req.headers.host}`);
1706
- if (!req.headers.host || origin.origin !== target.origin || req.headers.origin !== origin.origin || origin.username || origin.password) {
1707
- throw new Error("Origin mismatch");
1708
- }
1709
- } catch {
1722
+ const host = req.headers.host;
1723
+ const origin = req.headers.origin;
1724
+ if (host === undefined || origin !== undefined && !sameAuthorityOrigin(origin, host)) {
1710
1725
  sendJson(res, 403, { ok: false, errorCode: "forbidden-origin", error: "Same-origin browser request required" });
1711
1726
  return false;
1712
1727
  }
package/lib/updater.js CHANGED
@@ -13,8 +13,9 @@ export const UPDATE_REGISTRIES = Object.freeze({
13
13
  export const UPDATE_TAGS = Object.freeze({ stable: "latest", beta: "beta", dev: "dev" });
14
14
  export const UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000;
15
15
  export const DEFAULT_UPDATE_SETTINGS = Object.freeze({
16
- autoCheck: true, autoDownload: false, channel: "stable", region: "auto",
16
+ autoCheck: true, autoDownload: false, autoInstall: false, channel: "stable", region: "auto",
17
17
  });
18
+ const BOOLEAN_SETTINGS = ["autoCheck", "autoDownload", "autoInstall"];
18
19
  const MAX_METADATA_BYTES = 512 * 1024;
19
20
  const MAX_PACKAGE_BYTES = 8 * 1024 * 1024;
20
21
  const MAX_UNPACKED_BYTES = 16 * 1024 * 1024;
@@ -29,6 +30,8 @@ export function normalizeUpdateSettings(raw = {}) {
29
30
  return {
30
31
  autoCheck: typeof raw?.autoCheck === "boolean" ? raw.autoCheck : true,
31
32
  autoDownload: raw?.autoDownload === true,
33
+ // Consumed by the Web client, which delegates installation to dshmarket.
34
+ autoInstall: raw?.autoInstall === true,
32
35
  channel: Object.hasOwn(UPDATE_TAGS, raw?.channel ?? "") ? raw.channel : "stable",
33
36
  region: ["auto", "global", "cn"].includes(raw?.region) ? raw.region : "auto",
34
37
  };
@@ -38,7 +41,7 @@ export function loadUpdateSettings(file) {
38
41
  try {
39
42
  const raw = JSON.parse(readFileSync(file, "utf8"));
40
43
  if (!raw || typeof raw !== "object" || Array.isArray(raw) || raw.version !== 1 ||
41
- ["autoCheck", "autoDownload"].some((key) => raw[key] !== undefined && typeof raw[key] !== "boolean") ||
44
+ BOOLEAN_SETTINGS.some((key) => raw[key] !== undefined && typeof raw[key] !== "boolean") ||
42
45
  raw.channel !== undefined && !Object.hasOwn(UPDATE_TAGS, raw.channel) ||
43
46
  raw.region !== undefined && !["auto", "global", "cn"].includes(raw.region)) fail("invalid-settings");
44
47
  return normalizeUpdateSettings(raw);
@@ -340,7 +343,7 @@ export function createPluginUpdater(deps) {
340
343
  if (!patch || typeof patch !== "object" || Array.isArray(patch)) fail("invalid-settings");
341
344
  if (Object.keys(patch).some((key) => !Object.hasOwn(DEFAULT_UPDATE_SETTINGS, key))) fail("invalid-settings");
342
345
  if (Object.hasOwn(patch, "channel") && !Object.hasOwn(UPDATE_TAGS, patch.channel) || Object.hasOwn(patch, "region") && !["auto", "global", "cn"].includes(patch.region)) fail("invalid-settings");
343
- for (const key of ["autoCheck", "autoDownload"]) if (Object.hasOwn(patch, key) && typeof patch[key] !== "boolean") fail("invalid-settings");
346
+ for (const key of BOOLEAN_SETTINGS) if (Object.hasOwn(patch, key) && typeof patch[key] !== "boolean") fail("invalid-settings");
344
347
  const next = normalizeUpdateSettings({ ...settings, ...patch });
345
348
  saving = true;
346
349
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cliproxyapi-pro",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "Auto-discovery CLIProxyAPI provider and management UI for DeepSeek Harness — multiple accounts, dynamic model sync, capability detection, and visual settings.",
5
5
  "keywords": [
6
6
  "dsh",