dsh-cliproxyapi-pro 0.0.0-stage → 1.4.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 +40 -0
- package/LICENSE +21 -0
- package/README.md +236 -2
- package/README.zh.md +237 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +9 -0
- package/lib/client.js +1867 -0
- package/lib/index.js +2249 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +68 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this plugin. Versions follow [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## 1.4.0 — 2026-10-19
|
|
6
|
+
|
|
7
|
+
First public release.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Multiple CLIProxyAPI accounts.** Each account has its own base URL, API key (own credential ref), provider route ID, protocol mode, model selection and per-model overrides, and synchronizes into its own `llm-pi-ai` routes (`cli-proxy-2`, `cli-proxy-2-responses`, `cli-proxy-2-messages`, …).
|
|
11
|
+
- **Per-account enable switch.** A disabled account keeps its settings but its routes are removed, so its models leave the picker. New accounts start disabled; a manual refresh still probes them so models can be picked before enabling.
|
|
12
|
+
- Settings panel account bar: switch accounts, **+ Add Account**, **Delete Account**, credential-ref field.
|
|
13
|
+
- `cliproxyapi` tool: `add_account` and `remove_account` actions, `account`, `enabled` and `apiKeyEnv` parameters; output lists every account.
|
|
14
|
+
- HTTP routes `POST /api/dsh-cliproxyapi/accounts/add` and `/accounts/remove`; `refresh` and `save` accept `accountId`.
|
|
15
|
+
- English `README.md` and Chinese `README.zh.md` with captioned screenshots; `screenshots.json` for plugin storefronts; `LICENSE`, `AGENTS.md`, `CHANGELOG.md`.
|
|
16
|
+
- GitHub Actions: CI (Ubuntu + Windows, Node 22 / 24) and a tag-triggered release that attaches the packed bundle and optionally publishes to npm.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `state.json` is now `{ version: 2, refreshIntervalSeconds, probeTimeoutMs, accounts: [...] }`. A v1 single-account file migrates automatically into account `default` with the same routes, key and selection; the original is kept as `state.v1.json`.
|
|
20
|
+
- An account with an explicit base URL only probes that URL and never falls back to local `:8317`, the existing route or `CLIPROXYAPI_BASE_URL`.
|
|
21
|
+
- Overlapping provider route IDs between accounts are rejected on save; an overlapping account in a hand-edited file is reported as an error and never writes or removes another account's routes.
|
|
22
|
+
- Settings writes from concurrent account syncs are serialized.
|
|
23
|
+
- `/cpa-status` and `/cpa-refresh` cover every account.
|
|
24
|
+
- The DSH version requirement moved to top-level `engines.dsh`, where DSH reads it; `./icon` is exported.
|
|
25
|
+
- `engines.node` is now `>=22` (the versions CI tests).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **First-run selection no longer uses hard-coded model ids.** An account without a saved selection is seeded once, on its first successful probe, with the newest model of each family from its own server's catalog (at most 12). Previously a fresh install pre-selected 7 fixed ids that most servers do not offer, so nothing reached the picker.
|
|
29
|
+
- **Reset to Recommended** uses the same catalog-derived list (with its count) instead of the fixed 7.
|
|
30
|
+
- Saving a new account before its catalog was ever probed no longer stores an empty selection.
|
|
31
|
+
- When a saved selection matches nothing on the server, the error names the server and the discovered models stay visible in the panel.
|
|
32
|
+
|
|
33
|
+
## 1.3.0
|
|
34
|
+
|
|
35
|
+
- Per-model native protocol routing (`protocolMode: "auto"`): Claude via Anthropic Messages, GPT/Codex/Grok via OpenAI Responses, others via OpenAI Chat, one `llm-pi-ai` route per protocol; the default model follows its route.
|
|
36
|
+
- Per-model protocol override and fixed protocol mode.
|
|
37
|
+
|
|
38
|
+
## 1.2.0 and earlier
|
|
39
|
+
|
|
40
|
+
- Auto-discovery of CLIProxyAPI, model selection filter, per-model parameter editor, credentials-service key storage, `/cpa-status` and `/cpa-refresh`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bowen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,237 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-cliproxyapi-pro (v1.4.0)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
[](https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
A [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) provider for **DeepSeek Harness (DSH `>=0.2.0-rc.1`)**. It discovers CPA servers, synchronizes their model catalogs and capabilities (context window, image input, reasoning levels) into the DSH model picker, and supports **multiple accounts** that can each be enabled or disabled, with a visual model filter and per-model parameter overrides.
|
|
9
|
+
|
|
10
|
+
- No runtime dependencies and no build step; only Node.js built-ins and DSH (Cordis) context services.
|
|
11
|
+
- API keys are stored only in the DSH credentials service, never in configuration files.
|
|
12
|
+
|
|
13
|
+
## Contents
|
|
14
|
+
|
|
15
|
+
- [Features](#features)
|
|
16
|
+
- [Screenshots](#screenshots)
|
|
17
|
+
- [Installation](#installation)
|
|
18
|
+
- [Usage](#usage)
|
|
19
|
+
- [Settings panel](#settings-panel)
|
|
20
|
+
- [Multiple accounts](#multiple-accounts)
|
|
21
|
+
- [Agent tool `cliproxyapi`](#agent-tool-cliproxyapi)
|
|
22
|
+
- [Slash commands](#slash-commands)
|
|
23
|
+
- [HTTP API](#http-api)
|
|
24
|
+
- [Protocol routing](#protocol-routing)
|
|
25
|
+
- [Storage and configuration](#storage-and-configuration)
|
|
26
|
+
- [AI installation protocol](#ai-installation-protocol)
|
|
27
|
+
- [Development](#development)
|
|
28
|
+
- [License](#license)
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
1. **Auto-discovery.** Enter a remote or local URL (suffixes such as `/v1`, `/v1/chat/completions` and `/v1/messages` are stripped), or leave it empty to try the existing route URL, the `CLIPROXYAPI_BASE_URL` environment variable, and local `127.0.0.1:8317` in turn. The enriched catalog comes from `/v1/models?client_version=pi` (OpenAI `data[]` catalogs work too); `owned_by` from `/v1/models` identifies each model's family.
|
|
33
|
+
2. **Model filter.** Search, family chips, All / Checked / Unchecked tabs and bulk selection. **Only checked models appear in the conversation model picker.** An empty selection means no filter; include / exclude keyword rules apply on top. An account that has never saved a selection starts with the newest model of each family in its own server's catalog (at most 12); **Reset to Recommended** restores that set.
|
|
34
|
+
3. **Per-model overrides.** Context window, max output, image input, thinking levels (`off` … `max`) and request protocol, each resettable to the discovered value.
|
|
35
|
+
4. **Native protocol per model family** (default). See [Protocol routing](#protocol-routing).
|
|
36
|
+
5. **Multiple accounts.** Each account has its own URL, key, routes, protocol, selection and overrides, and can be enabled or disabled on its own. See [Multiple accounts](#multiple-accounts).
|
|
37
|
+
6. **Automatic refresh.** All accounts refresh on a shared interval (300 s by default); an unchanged catalog writes no settings.
|
|
38
|
+
|
|
39
|
+
## Screenshots
|
|
40
|
+
|
|
41
|
+
The panel lives in the **Settings → Built-in plugins → CLIProxyAPI** tab and follows the DSH display language; the screenshots show the Chinese UI.
|
|
42
|
+
|
|
43
|
+
**1. Account bar and account status.** The bar at the top lists every account: a green dot means connected, a grey dot plus the **Off** badge means disabled. Here a second account, *CLI Proxy API 2*, was just added with **+ Add Account**: it starts disabled, gets its own provider route `cli-proxy-2`, and — since no base URL is set yet — the probe reports that no local CPA was found.
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
**2. Model picker filter.** All 168 models discovered from the CPA server, with All / Checked / Unchecked tabs, family chips (GPT, CLAUDE, GEMINI, GROK, …), search and bulk actions. Checked models are the only ones synchronized into the DSH model picker; each shows the protocol it will use (here OpenAI Responses), image input, reasoning levels and context window, and **Edit Parameters** opens its per-model overrides.
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
**3. Connection & Authentication.** Per-account settings: the **Enable This Account** switch, base URL, API key stored under the account's own credential ref (`CLI_PROXY_API_KEY_2`), wire protocol, account name, the collapsible advanced section, **Save & Apply** and **Delete Account**.
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
Ask the agent in a DSH conversation to install it with `plugin_manager`, using one of these targets:
|
|
58
|
+
|
|
59
|
+
| Source | `target` |
|
|
60
|
+
|---|---|
|
|
61
|
+
| GitHub, latest `main` | `git+https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git#main` |
|
|
62
|
+
| GitHub, a release tag | `git+https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git#v1.4.0` (see [Releases](https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro/releases)) |
|
|
63
|
+
| GitHub Release `.tgz` | Download [`dsh-cliproxyapi-pro.tgz`](https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro/releases/latest/download/dsh-cliproxyapi-pro.tgz) (always the latest release) and pass its absolute path |
|
|
64
|
+
| Local checkout (development, linked) | Absolute path of the cloned repository |
|
|
65
|
+
| npm (once published) | `dsh-cliproxyapi-pro` |
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{ "action": "install_bundle", "target": "<target from the table>", "enabled": true }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- Installing a directory links it (`source: link:<dir>`); code changes go live when the plugin reloads.
|
|
72
|
+
- Reinstalling over an older version is fine; replacing an installed package needs a DSH restart to load the new code.
|
|
73
|
+
- If the result's `application` is `restart-required`, restart DSH.
|
|
74
|
+
|
|
75
|
+
After installation, open:
|
|
76
|
+
|
|
77
|
+
- Sidebar → **Plugins** → **CLIProxyAPI**
|
|
78
|
+
- **Settings** → **Built-in plugins** → **CLIProxyAPI** tab
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
### Settings panel
|
|
83
|
+
|
|
84
|
+
The **account bar** at the top switches between accounts (the dot shows each account's state; disabled accounts carry an **Off** badge), and **+ Add Account** creates one. Below it, for the selected account:
|
|
85
|
+
|
|
86
|
+
| Section | Contents |
|
|
87
|
+
|---|---|
|
|
88
|
+
| Status card | Connection state, active endpoint, models in the picker / discovered; **Probe & Refresh**, **Save & Apply** |
|
|
89
|
+
| Model Picker Filter | Check models and edit per-model parameters; click **Apply Selected Models** to save |
|
|
90
|
+
| Connection & Authentication | **Enable This Account** switch, base URL, API key (clearable), wire protocol, account name |
|
|
91
|
+
| Advanced Routing & Keyword Rules | Include / exclude keywords, provider route ID, credential ref, refresh interval (shared by all accounts); **Delete Account** |
|
|
92
|
+
|
|
93
|
+
### Multiple accounts
|
|
94
|
+
|
|
95
|
+
- Each account writes its own set of `llm-pi-ai` routes: the first account uses `cli-proxy` / `cli-proxy-responses` / `cli-proxy-messages`, later ones `cli-proxy-2*`, `cli-proxy-3*`, …; credential refs are `CLI_PROXY_API_KEY`, `CLI_PROXY_API_KEY_2`, …. The model picker groups models by account name.
|
|
96
|
+
- **Provider route IDs must not overlap** (for example, another account cannot use `cli-proxy-messages`); saving such a change is rejected.
|
|
97
|
+
- **Disabling** an account keeps its settings but removes its routes, so its models leave the picker; other accounts are unaffected.
|
|
98
|
+
- **New** accounts start disabled. Fill in the URL and key, click **Probe & Refresh** to preview and check models, then turn on **Enable This Account** and save.
|
|
99
|
+
- An account with a base URL **only probes that URL** and reports an error when it is unreachable; it never falls back to local `:8317`, so two accounts cannot silently point at the same server.
|
|
100
|
+
- **Deleting** an account removes its routes but keeps its key in the credentials service.
|
|
101
|
+
|
|
102
|
+
### Agent tool `cliproxyapi`
|
|
103
|
+
|
|
104
|
+
| `action` | Effect |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `status` | Show every account (or only `account`) with its state and models |
|
|
107
|
+
| `refresh` | Probe and sync now; without `account`, all accounts |
|
|
108
|
+
| `configure` | Update one account (the first by default), then refresh it |
|
|
109
|
+
| `add_account` | Create an account; enabled when `baseURL` is given, otherwise disabled |
|
|
110
|
+
| `remove_account` | Delete `account` and its routes |
|
|
111
|
+
|
|
112
|
+
Parameters (all optional except `action`):
|
|
113
|
+
|
|
114
|
+
| Parameter | Meaning |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `account` | Account id (e.g. `default`, `account-2`), or its provider route ID or display name |
|
|
117
|
+
| `enabled` | Enable or disable the account |
|
|
118
|
+
| `baseURL` | CPA URL such as `http://127.0.0.1:8317`; an empty string means auto-discover |
|
|
119
|
+
| `apiKey` | Bearer key, stored under the account's `apiKeyEnv` ref |
|
|
120
|
+
| `apiKeyEnv` | Credential ref name (`[A-Za-z_][A-Za-z0-9_]*`) |
|
|
121
|
+
| `provider` | `llm-pi-ai` provider route ID, unique per account |
|
|
122
|
+
| `displayName` | Account name / model picker group title |
|
|
123
|
+
| `protocolMode` | `auto` (native protocol per family, default) or `fixed` |
|
|
124
|
+
| `protocol` | Protocol used in `fixed` mode: `openai-completions` / `openai-responses` / `anthropic-messages` |
|
|
125
|
+
| `selectedModels` | Model ids shown in the picker; `[]` shows all |
|
|
126
|
+
| `includePrefix` / `excludePrefix` | Comma-separated model id keywords |
|
|
127
|
+
| `modelOverrides` | `{ [modelId]: { contextWindow?, maxTokens?, image?, reasoningLevels?, protocol? } }`, replacing the whole map |
|
|
128
|
+
|
|
129
|
+
Examples:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{ "action": "configure", "baseURL": "http://127.0.0.1:8317", "apiKey": "sk-...", "selectedModels": ["gpt-6.1-sol", "claude-opus-5-5"] }
|
|
133
|
+
{ "action": "add_account", "displayName": "Backup CPA", "baseURL": "https://other-cpa:8317", "apiKey": "sk-...", "selectedModels": ["gpt-6-astra"] }
|
|
134
|
+
{ "action": "configure", "account": "account-2", "enabled": false }
|
|
135
|
+
{ "action": "remove_account", "account": "account-2" }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The output has `accounts[]` (each with `id`, `displayName`, `provider`, `enabled`, `state`, `endpoint`, `modelCount`, `totalDiscovered`, `error`) and a text `summary`. `state` is one of `idle`, `probing`, `connected`, `disabled`, `error`.
|
|
139
|
+
|
|
140
|
+
### Slash commands
|
|
141
|
+
|
|
142
|
+
- `/cpa-status` — show every account's status.
|
|
143
|
+
- `/cpa-refresh` — refresh every account now, including a catalog probe of disabled ones.
|
|
144
|
+
|
|
145
|
+
### HTTP API
|
|
146
|
+
|
|
147
|
+
Used by the settings panel. Requires the DSH Web session's authentication; POST accepts only same-origin requests from loopback or trusted hosts.
|
|
148
|
+
|
|
149
|
+
| Method | Path | Body | Meaning |
|
|
150
|
+
|---|---|---|---|
|
|
151
|
+
| GET | `/api/dsh-cliproxyapi/status` | — | `{ ok, global, accounts[] }` |
|
|
152
|
+
| POST | `/api/dsh-cliproxyapi/refresh` | `{ accountId? }` | Refresh one or all accounts |
|
|
153
|
+
| POST | `/api/dsh-cliproxyapi/save` | `{ accountId?, …account fields, apiKey?, clearApiKey?, refreshIntervalSeconds?, probeTimeoutMs? }` | Save an account (the first by default); a body with only global fields saves just the globals |
|
|
154
|
+
| POST | `/api/dsh-cliproxyapi/accounts/add` | Optional account fields | Create a disabled account; returns `createdId` |
|
|
155
|
+
| POST | `/api/dsh-cliproxyapi/accounts/remove` | `{ accountId }` | Delete an account and its routes |
|
|
156
|
+
|
|
157
|
+
## Protocol routing
|
|
158
|
+
|
|
159
|
+
CLIProxyAPI serves every model on OpenAI Chat, OpenAI Responses and Anthropic Messages, translating when the entry protocol differs from the upstream one. Auto mode (`protocolMode: "auto"`) sends each model over its upstream's own protocol:
|
|
160
|
+
|
|
161
|
+
| Model family (from `owned_by` and the model id) | Protocol | Route (for `cli-proxy`) |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| Claude (including `devin/claude-*`, Antigravity Claude) | Anthropic Messages `/v1/messages` | `cli-proxy-messages` |
|
|
164
|
+
| OpenAI / Codex / GPT, xAI Grok | OpenAI Responses `/v1/responses` | `cli-proxy-responses` |
|
|
165
|
+
| Gemini, DeepSeek, Kimi, GLM, SWE, gpt-oss, … | OpenAI Chat `/v1/chat/completions` | `cli-proxy` |
|
|
166
|
+
|
|
167
|
+
- DSH custom routes cannot speak Gemini's native `generateContent`, so Gemini uses OpenAI Chat.
|
|
168
|
+
- A DSH route has a single protocol, so models are split across the three routes; a route left without models is removed. When the default model moves to another route, `agent-default-model` follows it.
|
|
169
|
+
- Claude 4.6+ / 5.x / Fable use adaptive thinking (`effort`); older Claude models use a token budget.
|
|
170
|
+
- Switch to one protocol for every model (`fixed`), or set a protocol per model under **Edit Parameters**.
|
|
171
|
+
- Other fields you add to a managed route by hand (such as `retryPolicy`) are kept.
|
|
172
|
+
|
|
173
|
+
## Storage and configuration
|
|
174
|
+
|
|
175
|
+
- **State file**: `<profile dir>/.dsh-cliproxyapi/state.json`
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"version": 2,
|
|
180
|
+
"refreshIntervalSeconds": 300,
|
|
181
|
+
"probeTimeoutMs": 5000,
|
|
182
|
+
"accounts": [
|
|
183
|
+
{
|
|
184
|
+
"id": "default", "enabled": true, "provider": "cli-proxy", "displayName": "CLI Proxy API",
|
|
185
|
+
"baseURL": "https://cpa.example.com", "apiKeyEnv": "CLI_PROXY_API_KEY",
|
|
186
|
+
"protocol": "openai-completions", "protocolMode": "auto",
|
|
187
|
+
"includePrefix": "", "excludePrefix": "", "selectedModels": ["gpt-6.1-sol"], "modelOverrides": {}
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The file is re-read on every access; after a manual edit, click **Probe & Refresh**. A v1 single-account file migrates automatically into account `default` (same routes, key and selection), and the original is kept as `state.v1.json`.
|
|
194
|
+
|
|
195
|
+
- **Keys**: stored and resolved by ref through the DSH `credentials` service, falling back to an environment variable of the same name. A local CPA without authentication needs no key.
|
|
196
|
+
- **Environment**: `CLIPROXYAPI_BASE_URL` is used only for auto-discovery of accounts without a base URL.
|
|
197
|
+
- **Bundle row config**: the plugin row's `config` in `cordis.patch.yml` is overlaid on the state file at startup: `accounts` replaces the account list, flat fields apply to the first account. Normally keep it `{}` and configure through the panel or the tool.
|
|
198
|
+
|
|
199
|
+
## AI installation protocol
|
|
200
|
+
|
|
201
|
+
> **For the AI agent in DeepSeek Harness**: when the user asks you to install or configure this plugin, follow these steps. The package has no npm dependencies and no build scripts.
|
|
202
|
+
|
|
203
|
+
1. **Install**: call `plugin_manager` with `{"action": "install_bundle", "target": "<see Installation>", "enabled": true}`. Read `application` and `warnings` in the result; `restart-required` means DSH must restart before the new code runs.
|
|
204
|
+
2. **Configure** with the `cliproxyapi` tool:
|
|
205
|
+
- Local CPA on `http://127.0.0.1:8317` without auth: nothing to configure; call `{"action": "refresh"}`.
|
|
206
|
+
- 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.
|
|
207
|
+
- Narrow the picker with `selectedModels` (exact ids from the `status` output).
|
|
208
|
+
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**.
|
|
209
|
+
|
|
210
|
+
Never echo API keys back to the user or write them into files.
|
|
211
|
+
|
|
212
|
+
## Development
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
lib/index.js Host plugin (discovery, sync, multi-account controller, tool, commands, HTTP API)
|
|
216
|
+
lib/client.js Web settings panel (hand-written ModuleLoader module, no build)
|
|
217
|
+
locale/*.json Title and description shown by the plugin manager
|
|
218
|
+
cordis.patch.yml Bundle patch inserting the dsh-cliproxyapi-pro row
|
|
219
|
+
tests/ node:test unit and integration tests
|
|
220
|
+
.github/workflows/ CI (tests) and release (.tgz asset, optional npm publish)
|
|
221
|
+
assets/screenshots/ README screenshots (listed in screenshots.json for plugin storefronts)
|
|
222
|
+
AGENTS.md Conventions, architecture and invariants for coding agents
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
git clone https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git
|
|
227
|
+
cd dsh-cliproxyapi-pro
|
|
228
|
+
npm test # run the tests
|
|
229
|
+
npm run check # syntax check + tests
|
|
230
|
+
npm run pack:check # list the files a packed bundle contains
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Read [AGENTS.md](AGENTS.md) before changing code; see [CHANGELOG.md](CHANGELOG.md) for history. Issues and pull requests are welcome — when you change the documentation, update both the English and the Chinese README.
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
[MIT](LICENSE)
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# dsh-cliproxyapi-pro (v1.4.0)
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
[](https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) 接入插件,适用于 **DeepSeek Harness(DSH `>=0.2.0-rc.1`)**:自动发现 CPA 服务、同步模型目录与能力(上下文、视觉、推理档位)到 DSH 模型选择器,支持**多个账号**分别启用/停用、可视化勾选模型与单模型参数覆盖。
|
|
9
|
+
|
|
10
|
+
- 零运行时依赖,无需构建;仅使用 Node.js 内置模块与 DSH(Cordis)上下文服务。
|
|
11
|
+
- 密钥只存放在 DSH 凭证服务中,不会写入配置文件。
|
|
12
|
+
|
|
13
|
+
## 目录
|
|
14
|
+
|
|
15
|
+
- [功能](#功能)
|
|
16
|
+
- [截图](#截图)
|
|
17
|
+
- [安装](#安装)
|
|
18
|
+
- [使用](#使用)
|
|
19
|
+
- [设置面板](#设置面板)
|
|
20
|
+
- [多账号](#多账号)
|
|
21
|
+
- [Agent 工具 `cliproxyapi`](#agent-工具-cliproxyapi)
|
|
22
|
+
- [斜杠命令](#斜杠命令)
|
|
23
|
+
- [HTTP 接口](#http-接口)
|
|
24
|
+
- [协议路由](#协议路由)
|
|
25
|
+
- [存储与配置](#存储与配置)
|
|
26
|
+
- [AI Agent 安装与配置指南](#ai-agent-安装与配置指南)
|
|
27
|
+
- [开发](#开发)
|
|
28
|
+
- [许可证](#许可证)
|
|
29
|
+
|
|
30
|
+
## 功能
|
|
31
|
+
|
|
32
|
+
1. **自动发现**:可填写远程/本地地址(自动剥离 `/v1`、`/v1/chat/completions`、`/v1/messages` 等后缀),留空则依次尝试已有路由地址、环境变量 `CLIPROXYAPI_BASE_URL` 与本地 `127.0.0.1:8317`。读取 `/v1/models?client_version=pi` 富元数据目录(兼容 OpenAI `data[]` 格式),并用 `/v1/models` 的 `owned_by` 判断模型家族。
|
|
33
|
+
2. **模型勾选筛选**:搜索、家族标签、全部/已勾选/未勾选切换、批量勾选;**只有勾选的模型会出现在对话框模型选择器中**。勾选为空表示不过滤;另可用包含/排除关键词规则。从未保存过勾选的账号,首次探测成功后会按该服务自己的模型目录,为每个模型家族勾选最新的一个(最多 12 个);「恢复推荐精选」可恢复这组勾选。
|
|
34
|
+
3. **单模型参数覆盖**:上下文窗口、最大输出、图片输入、思考等级(`off`…`max`)、请求协议,可一键恢复自动探测值。
|
|
35
|
+
4. **按模型家族走原生协议**(默认):见[协议路由](#协议路由)。
|
|
36
|
+
5. **多账号**:每个账号独立的服务地址、密钥、路由、协议、勾选与覆盖,可单独启用/停用。见[多账号](#多账号)。
|
|
37
|
+
6. **自动刷新**:所有账号按统一间隔(默认 300 秒)刷新;数据无变化时不写入任何设置。
|
|
38
|
+
|
|
39
|
+
## 截图
|
|
40
|
+
|
|
41
|
+
面板位于 **设置 → 内置插件 → CLIProxyAPI** 标签页,界面语言跟随 DSH 设置。
|
|
42
|
+
|
|
43
|
+
**1. 账号栏与账号状态。** 顶部账号栏列出所有账号:绿点表示已连接,灰点加「未启用」标记表示已停用。图中刚用「+ 添加账号」新增了第二个账号「CLI Proxy API 2」:它默认停用,拥有独立的路由 `cli-proxy-2`;由于还没填写服务地址,探测提示本地没有找到 CPA 服务。
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
**2. 模型显示筛选。** 从 CPA 服务发现的全部 168 个模型,支持全部发现 / 已勾选 / 未勾选切换、家族标签(GPT、CLAUDE、GEMINI、GROK……)、搜索与批量操作。只有勾选的模型会同步到 DSH 模型选择器;每个模型显示将使用的协议(图中为 OpenAI Responses)、图片输入、推理档位与上下文长度,「编辑参数」可打开单模型参数覆盖。
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
**3. 连接与认证配置。** 每个账号独立的设置:「启用此账号」开关、服务地址、保存在该账号独立引用名(`CLI_PROXY_API_KEY_2`)下的 API Key、接口协议、账号名称、可折叠的高级设置,以及「保存设置并应用」与「删除此账号」。
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
## 安装
|
|
56
|
+
|
|
57
|
+
在 DSH 对话中让 Agent 调用 `plugin_manager` 安装,`target` 任选其一:
|
|
58
|
+
|
|
59
|
+
| 来源 | `target` |
|
|
60
|
+
|---|---|
|
|
61
|
+
| GitHub 最新 `main` | `git+https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git#main` |
|
|
62
|
+
| GitHub 某个版本标签 | `git+https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git#v1.4.0`(见 [Releases](https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro/releases)) |
|
|
63
|
+
| GitHub Release 的 `.tgz` | 下载 [`dsh-cliproxyapi-pro.tgz`](https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro/releases/latest/download/dsh-cliproxyapi-pro.tgz)(始终是最新版本),填写它的绝对路径 |
|
|
64
|
+
| 本地目录(开发用,链接安装) | 克隆后的仓库目录绝对路径 |
|
|
65
|
+
| npm(发布后) | `dsh-cliproxyapi-pro` |
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{ "action": "install_bundle", "target": "<上表中的 target>", "enabled": true }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- 安装目录时为链接安装(`source: link:<目录>`),修改代码后重载插件即生效。
|
|
72
|
+
- 已安装旧版本时可直接重新安装;替换已安装的包后需要重启 DSH 才能加载新代码。
|
|
73
|
+
- 安装结果中的 `application` 为 `restart-required` 时需重启 DSH。
|
|
74
|
+
|
|
75
|
+
安装后入口:
|
|
76
|
+
|
|
77
|
+
- 左侧栏 → **插件** → **CLIProxyAPI**
|
|
78
|
+
- **设置** → **内置插件** → **CLIProxyAPI** 标签页
|
|
79
|
+
|
|
80
|
+
## 使用
|
|
81
|
+
|
|
82
|
+
### 设置面板
|
|
83
|
+
|
|
84
|
+
面板顶部是**账号栏**:点击切换账号(圆点表示状态,未启用的账号带「未启用」标记),「+ 添加账号」新增账号。下方针对当前账号:
|
|
85
|
+
|
|
86
|
+
| 区块 | 内容 |
|
|
87
|
+
|---|---|
|
|
88
|
+
| 状态卡片 | 连接状态、当前节点、选择器中的模型数 / 发现总数;「立即探测刷新」「保存设置并应用」 |
|
|
89
|
+
| 模型显示筛选 | 勾选模型、编辑单模型参数;修改后点「保存模型勾选」 |
|
|
90
|
+
| 连接与认证 | 「启用此账号」开关、服务地址、API Key(可清除)、接口协议、账号名称 |
|
|
91
|
+
| 高级路由与规则过滤 | 包含/排除关键词、路由 ID、密钥引用名、自动刷新间隔(所有账号共用);「删除此账号」 |
|
|
92
|
+
|
|
93
|
+
### 多账号
|
|
94
|
+
|
|
95
|
+
- 每个账号写入自己的一组 `llm-pi-ai` 路由:首个账号为 `cli-proxy` / `cli-proxy-responses` / `cli-proxy-messages`,新增账号依次为 `cli-proxy-2*`、`cli-proxy-3*`……;密钥引用名依次为 `CLI_PROXY_API_KEY`、`CLI_PROXY_API_KEY_2`……。模型选择器中按账号名称分组。
|
|
96
|
+
- **路由 ID 不能重叠**(例如另一个账号不能使用 `cli-proxy-messages`),保存时会拒绝。
|
|
97
|
+
- **停用**账号:保留全部配置,但删除它的路由,模型从选择器中消失;其他账号不受影响。
|
|
98
|
+
- **新增**账号默认停用。填写地址与密钥后可先「立即探测刷新」预览模型并勾选,再打开「启用此账号」保存。
|
|
99
|
+
- 填写了服务地址的账号**只探测该地址**,连不上就报错,不会回退到本地 `:8317`,避免多个账号指向同一服务。
|
|
100
|
+
- **删除**账号会移除它的路由,但保留凭证服务中的密钥。
|
|
101
|
+
|
|
102
|
+
### Agent 工具 `cliproxyapi`
|
|
103
|
+
|
|
104
|
+
| `action` | 作用 |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `status` | 查看所有账号(或 `account` 指定的账号)状态与模型 |
|
|
107
|
+
| `refresh` | 立即探测并同步;不带 `account` 时刷新全部账号 |
|
|
108
|
+
| `configure` | 修改一个账号(默认第一个),保存后立即刷新 |
|
|
109
|
+
| `add_account` | 新增账号;给了 `baseURL` 时默认启用,否则停用 |
|
|
110
|
+
| `remove_account` | 删除 `account` 及其路由 |
|
|
111
|
+
|
|
112
|
+
参数(除 `action` 外均可选):
|
|
113
|
+
|
|
114
|
+
| 参数 | 说明 |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `account` | 账号 id(如 `default`、`account-2`),也可用路由 ID 或账号名称 |
|
|
117
|
+
| `enabled` | 启用 / 停用该账号 |
|
|
118
|
+
| `baseURL` | CPA 地址,如 `http://127.0.0.1:8317`;空字符串表示自动发现 |
|
|
119
|
+
| `apiKey` | Bearer 密钥,保存到该账号的 `apiKeyEnv` 引用名下 |
|
|
120
|
+
| `apiKeyEnv` | 密钥引用名(`[A-Za-z_][A-Za-z0-9_]*`) |
|
|
121
|
+
| `provider` | `llm-pi-ai` 路由 ID,每个账号唯一 |
|
|
122
|
+
| `displayName` | 账号名称 / 模型选择器分组名 |
|
|
123
|
+
| `protocolMode` | `auto`(按家族原生协议,默认)或 `fixed` |
|
|
124
|
+
| `protocol` | `fixed` 时使用的协议:`openai-completions` / `openai-responses` / `anthropic-messages` |
|
|
125
|
+
| `selectedModels` | 显示在选择器中的模型 id 列表;`[]` 表示全部 |
|
|
126
|
+
| `includePrefix` / `excludePrefix` | 逗号分隔的模型 id 关键词 |
|
|
127
|
+
| `modelOverrides` | `{ [modelId]: { contextWindow?, maxTokens?, image?, reasoningLevels?, protocol? } }`,整体替换 |
|
|
128
|
+
|
|
129
|
+
示例:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{ "action": "configure", "baseURL": "http://127.0.0.1:8317", "apiKey": "sk-...", "selectedModels": ["gpt-6.1-sol", "claude-opus-5-5"] }
|
|
133
|
+
{ "action": "add_account", "displayName": "Backup CPA", "baseURL": "https://other-cpa:8317", "apiKey": "sk-...", "selectedModels": ["gpt-6-astra"] }
|
|
134
|
+
{ "action": "configure", "account": "account-2", "enabled": false }
|
|
135
|
+
{ "action": "remove_account", "account": "account-2" }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
输出包含 `accounts[]`(每个账号的 `id`、`displayName`、`provider`、`enabled`、`state`、`endpoint`、`modelCount`、`totalDiscovered`、`error`)与文字摘要 `summary`。`state` 取值:`idle`、`probing`、`connected`、`disabled`、`error`。
|
|
139
|
+
|
|
140
|
+
### 斜杠命令
|
|
141
|
+
|
|
142
|
+
- `/cpa-status`:显示所有账号的状态。
|
|
143
|
+
- `/cpa-refresh`:立即刷新所有账号(包括探测停用账号的模型列表)。
|
|
144
|
+
|
|
145
|
+
### HTTP 接口
|
|
146
|
+
|
|
147
|
+
供设置面板使用,需要 DSH Web 会话认证;POST 仅接受本机/受信任主机的同源请求。
|
|
148
|
+
|
|
149
|
+
| 方法 | 路径 | 请求体 | 说明 |
|
|
150
|
+
|---|---|---|---|
|
|
151
|
+
| GET | `/api/dsh-cliproxyapi/status` | — | `{ ok, global, accounts[] }` |
|
|
152
|
+
| POST | `/api/dsh-cliproxyapi/refresh` | `{ accountId? }` | 刷新一个或全部账号 |
|
|
153
|
+
| POST | `/api/dsh-cliproxyapi/save` | `{ accountId?, …账号字段, apiKey?, clearApiKey?, refreshIntervalSeconds?, probeTimeoutMs? }` | 保存账号(缺省为第一个);只有全局字段时只保存全局设置 |
|
|
154
|
+
| POST | `/api/dsh-cliproxyapi/accounts/add` | 账号字段(可选) | 新增账号,默认停用;返回 `createdId` |
|
|
155
|
+
| POST | `/api/dsh-cliproxyapi/accounts/remove` | `{ accountId }` | 删除账号及其路由 |
|
|
156
|
+
|
|
157
|
+
## 协议路由
|
|
158
|
+
|
|
159
|
+
CLIProxyAPI 对每个模型同时提供 OpenAI Chat、OpenAI Responses、Anthropic Messages 三种入口,入口协议与上游不一致时会做转换。自动模式(`protocolMode: "auto"`)让每个模型直接使用上游协议:
|
|
160
|
+
|
|
161
|
+
| 模型家族(依据 `owned_by` 与模型 id) | 协议 | 路由(以 `cli-proxy` 为例) |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| Claude(含 `devin/claude-*`、Antigravity Claude) | Anthropic Messages `/v1/messages` | `cli-proxy-messages` |
|
|
164
|
+
| OpenAI / Codex / GPT、xAI Grok | OpenAI Responses `/v1/responses` | `cli-proxy-responses` |
|
|
165
|
+
| Gemini、DeepSeek、Kimi、GLM、SWE、gpt-oss 等 | OpenAI Chat `/v1/chat/completions` | `cli-proxy` |
|
|
166
|
+
|
|
167
|
+
- DSH 自定义路由不支持 Gemini 原生 `generateContent`,Gemini 走 OpenAI Chat。
|
|
168
|
+
- DSH 每条路由只有一种协议,模型按协议拆入三条路由;没有模型的路由会被移除。默认模型因此换路由时,`agent-default-model` 自动跟随。
|
|
169
|
+
- Claude 4.6+ / 5.x / Fable 使用自适应思考(`effort`),更早的 Claude 使用 token 预算。
|
|
170
|
+
- 可改为「统一使用」某一协议(`fixed`),或在单个模型的「编辑参数」中指定协议。
|
|
171
|
+
- 路由上你手动添加的其他字段(如 `retryPolicy`)会被保留。
|
|
172
|
+
|
|
173
|
+
## 存储与配置
|
|
174
|
+
|
|
175
|
+
- **状态文件**:`<Profile 目录>/.dsh-cliproxyapi/state.json`
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"version": 2,
|
|
180
|
+
"refreshIntervalSeconds": 300,
|
|
181
|
+
"probeTimeoutMs": 5000,
|
|
182
|
+
"accounts": [
|
|
183
|
+
{
|
|
184
|
+
"id": "default", "enabled": true, "provider": "cli-proxy", "displayName": "CLI Proxy API",
|
|
185
|
+
"baseURL": "https://cpa.example.com", "apiKeyEnv": "CLI_PROXY_API_KEY",
|
|
186
|
+
"protocol": "openai-completions", "protocolMode": "auto",
|
|
187
|
+
"includePrefix": "", "excludePrefix": "", "selectedModels": ["gpt-6.1-sol"], "modelOverrides": {}
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
文件每次读取时都会重新加载,手动修改后点「立即探测刷新」即可生效。v1 单账号文件会自动迁移为 id `default` 的账号(路由、密钥、勾选不变),原文件备份为 `state.v1.json`。
|
|
194
|
+
|
|
195
|
+
- **密钥**:通过 DSH `credentials` 服务按引用名存取;未存储时回退读取同名环境变量。本地免密的 CPA 可以不填密钥。
|
|
196
|
+
- **环境变量**:`CLIPROXYAPI_BASE_URL` 仅用于服务地址留空的账号的自动发现。
|
|
197
|
+
- **Bundle 行配置**:`cordis.patch.yml` 中插件行的 `config` 会在启动时叠加到状态文件上:含 `accounts` 时替换账号列表,否则平铺字段作用于第一个账号。通常保持 `{}`,使用面板或工具配置即可。
|
|
198
|
+
|
|
199
|
+
## AI Agent 安装与配置指南
|
|
200
|
+
|
|
201
|
+
> 给 DeepSeek Harness 中的 AI Agent:用户要求安装或配置本插件时按以下步骤执行。本包没有 npm 依赖,也没有构建脚本。
|
|
202
|
+
|
|
203
|
+
1. **安装**:调用 `plugin_manager`,`{"action": "install_bundle", "target": "<见安装一节>", "enabled": true}`。查看结果中的 `application` 与 `warnings`;`restart-required` 表示需重启 DSH 后新代码才会运行。
|
|
204
|
+
2. **配置**:使用 `cliproxyapi` 工具。
|
|
205
|
+
- 本地 `http://127.0.0.1:8317` 且无鉴权:无需配置,调用 `{"action": "refresh"}`。
|
|
206
|
+
- 远程地址或密钥:`{"action": "configure", "baseURL": "...", "apiKey": "..."}`(第一个账号),额外账号用 `{"action": "add_account", ...}`;指定账号时传 `"account": "<id>"`。
|
|
207
|
+
- 用 `selectedModels`(取自 `status` 输出的精确 id)精简模型选择器。
|
|
208
|
+
3. **验证**:`{"action": "status"}` 中每个启用的账号 `state` 为 `connected` 且 `modelCount > 0`。告诉用户模型已出现在 DSH 模型选择器中(以账号名称分组),可在 **左侧栏 → 插件 → CLIProxyAPI** 或 **设置 → 内置插件 → CLIProxyAPI** 管理账号。
|
|
209
|
+
|
|
210
|
+
不要把 API Key 回显给用户或写入任何文件。
|
|
211
|
+
|
|
212
|
+
## 开发
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
lib/index.js Host 插件(发现、同步、多账号控制器、工具、命令、HTTP 接口)
|
|
216
|
+
lib/client.js Web 设置面板(ModuleLoader 格式,手写,无需构建)
|
|
217
|
+
locale/*.json 插件管理器中显示的标题与描述
|
|
218
|
+
cordis.patch.yml Bundle patch,插入 dsh-cliproxyapi-pro 行
|
|
219
|
+
tests/ node:test 单元与集成测试
|
|
220
|
+
.github/workflows/ CI(测试)与发版(Release .tgz,可选 npm 发布)
|
|
221
|
+
assets/screenshots/ README 截图(同时登记在 screenshots.json 中供插件市场展示)
|
|
222
|
+
AGENTS.md 给编码 Agent 的开发约定、架构与不变量
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
```powershell
|
|
226
|
+
git clone https://github.com/TinyBOOOOM/dsh-cliproxyapi-pro.git
|
|
227
|
+
cd dsh-cliproxyapi-pro
|
|
228
|
+
npm test # 运行测试
|
|
229
|
+
npm run check # 语法检查 + 测试
|
|
230
|
+
npm run pack:check # 查看打包文件列表
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
修改代码前请阅读 [AGENTS.md](AGENTS.md);变更记录见 [CHANGELOG.md](CHANGELOG.md)。欢迎提交 Issue 与 Pull Request,修改说明文档时请同时更新中英文两份。
|
|
234
|
+
|
|
235
|
+
## 许可证
|
|
236
|
+
|
|
237
|
+
[MIT](LICENSE)
|
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
|
2
|
+
<rect x="4" y="4" width="56" height="56" rx="14" fill="#4D6BFE"/>
|
|
3
|
+
<path d="M20 22h10a6 6 0 0 1 6 6v8a6 6 0 0 1-6 6H20" stroke="#fff" stroke-width="4" stroke-linecap="round"/>
|
|
4
|
+
<circle cx="20" cy="22" r="3.5" fill="#fff"/>
|
|
5
|
+
<circle cx="20" cy="42" r="3.5" fill="#fff"/>
|
|
6
|
+
<circle cx="44" cy="32" r="4" fill="#fff"/>
|
|
7
|
+
<path d="M36 32h4" stroke="#fff" stroke-width="4" stroke-linecap="round"/>
|
|
8
|
+
<path d="M44 20v6M44 38v6" stroke="#fff" stroke-width="3.5" stroke-linecap="round" opacity="0.85"/>
|
|
9
|
+
</svg>
|