@karoc/dsh-proxy 0.1.3 → 0.1.18
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 +495 -0
- package/CONTRIBUTING.md +40 -8
- package/README.md +142 -26
- package/README.zh.md +77 -16
- package/cordis.patch.yml +2 -1
- package/lib/client.js +297 -58
- package/lib/index.js +696 -77
- package/package.json +50 -13
package/README.md
CHANGED
|
@@ -12,10 +12,12 @@ plus a **Settings page** dedicated to model-provider proxying.
|
|
|
12
12
|
|
|
13
13
|
It runs a loopback forward proxy inside the dsh host process, points the
|
|
14
14
|
process's egress traffic at it (`HTTP(S)_PROXY` + `NODE_USE_ENV_PROXY`), and
|
|
15
|
-
routes
|
|
15
|
+
routes the hosts you select through an optional upstream proxy (or, with one
|
|
16
|
+
click, every host **except** the ones you select)
|
|
16
17
|
(HTTP / HTTPS / SOCKS5, with optional Basic auth). Everything else stays
|
|
17
|
-
direct. Model-provider hosts are
|
|
18
|
-
hosts observed in traffic are collected for you — both
|
|
18
|
+
direct. Model-provider hosts are read LIVE from your running dsh
|
|
19
|
+
configuration, and hosts observed in traffic are collected for you — both
|
|
20
|
+
appear as checkboxes.
|
|
19
21
|
|
|
20
22
|
Why an external plugin: the built-in settings pages don't cover outbound
|
|
21
23
|
proxying, and adding one to a built-in package would be overwritten by the next
|
|
@@ -29,15 +31,25 @@ desktop shell had.
|
|
|
29
31
|
|
|
30
32
|
## What it adds
|
|
31
33
|
|
|
32
|
-
A new Settings section, **「思磨力代理 / Smoothly Proxy
|
|
33
|
-
built-in **Models** and
|
|
34
|
+
A new Settings section, **「思磨力代理 / Smoothly Proxy」** (nav order 25), placed
|
|
35
|
+
after the built-in **Models** page and, when that plugin is installed, the
|
|
36
|
+
external **Model reasoning** page. It shows:
|
|
34
37
|
|
|
35
38
|
- an **upstream proxy card**: enable toggle, protocol selector (HTTP / HTTPS /
|
|
36
39
|
SOCKS5), host, port, optional username/password, and a **Test connection**
|
|
37
|
-
button
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
button (a SOCKS5 handshake for SOCKS5, a `CONNECT` probe for HTTP; an HTTPS
|
|
41
|
+
upstream is only checked for TCP reachability — its TLS handshake is not
|
|
42
|
+
verified);
|
|
43
|
+
- a **routing rule** with two readings of the same checkbox list — *only checked
|
|
44
|
+
hosts* (default) or *everything except checked*, for setups whose local proxy
|
|
45
|
+
already does its own routing;
|
|
46
|
+
- a **model providers** list — hosts read LIVE from your running dsh
|
|
47
|
+
configuration (`llm-deepseek.baseURL`, `llm-pi-ai.providers.<n>.baseURL`, any
|
|
48
|
+
`llm-*` namespace), labeled with the friendly display name where available.
|
|
49
|
+
The values come from the harness's own settings document, so the list follows
|
|
50
|
+
the running configuration wherever dsh keeps it; a harness that still has the
|
|
51
|
+
pre-0.2 `<DSH_HOME>/settings.yaml` is read as the fallback. Loopback
|
|
52
|
+
endpoints are omitted — they can never be proxied;
|
|
41
53
|
- an **other observed hosts** list — hosts the proxy has seen in traffic
|
|
42
54
|
(including `registry.npmjs.org` install/update traffic), persisted into
|
|
43
55
|
`proxy.json` so they survive restarts;
|
|
@@ -66,13 +78,35 @@ dsh host process
|
|
|
66
78
|
upstream proxy (HTTP/HTTPS/SOCKS5, optional Basic auth)
|
|
67
79
|
```
|
|
68
80
|
|
|
81
|
+
The checkboxes have two readings (**Routing rule** at the top of the list):
|
|
82
|
+
|
|
83
|
+
| Rule | Checked hosts | Everything else | Pick it when |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| *Only checked hosts* (default) | go upstream | connect directly | a few providers should use the upstream |
|
|
86
|
+
| *Everything except checked* | connect directly | go upstream | your local proxy already does the filtering |
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
# a few providers through the upstream
|
|
90
|
+
rule: Only checked hosts checked: new-api.example, opencode.ai
|
|
91
|
+
|
|
92
|
+
# everything through the local proxy, except what must stay direct
|
|
93
|
+
rule: Everything except checked checked: api.internal.example (i.e. the direct list)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Either way the list title follows the rule ("hosts going through the proxy" ↔
|
|
97
|
+
"hosts staying direct"), and the per-row status shows the RESULT — a host reads
|
|
98
|
+
*direct* while the upstream is off, whatever the checkboxes say.
|
|
99
|
+
|
|
69
100
|
Safety rules (hard-coded, not configurable):
|
|
70
101
|
|
|
71
102
|
- **loopback targets are always direct** — never sent to an upstream proxy;
|
|
72
103
|
- an upstream pointing back at this very proxy is treated as **disabled**
|
|
73
104
|
(self-loop guard);
|
|
74
|
-
- the proxy itself uses raw `net`/`http
|
|
75
|
-
|
|
105
|
+
- the proxy itself uses raw `net`/`http` through an explicit `Agent` that
|
|
106
|
+
ignores the environment proxy — even when the launch environment sets
|
|
107
|
+
`NODE_USE_ENV_PROXY=1` for dsh's own fetch, its outbound connections can never
|
|
108
|
+
be routed back through itself; a mismatched `Host` is replaced with the
|
|
109
|
+
request-target's authority (RFC 9112 §3.2.2).
|
|
76
110
|
|
|
77
111
|
### Config file
|
|
78
112
|
|
|
@@ -84,12 +118,17 @@ Safety rules (hard-coded, not configurable):
|
|
|
84
118
|
{
|
|
85
119
|
"upstream": { "enabled": false, "protocol": "http", "host": "", "port": 0, "username": "", "password": "" },
|
|
86
120
|
"proxiedHosts": ["api.deepseek.com"],
|
|
87
|
-
"knownHosts": ["api.deepseek.com", "registry.npmjs.org"]
|
|
121
|
+
"knownHosts": ["api.deepseek.com", "registry.npmjs.org"],
|
|
122
|
+
"listenPort": 45999
|
|
88
123
|
}
|
|
89
124
|
```
|
|
90
125
|
|
|
91
|
-
`knownHosts` is written by the plugin (observed traffic); `upstream
|
|
92
|
-
`proxiedHosts` are written from the settings page
|
|
126
|
+
`knownHosts` is written by the plugin (observed traffic); `upstream`,
|
|
127
|
+
`proxiedHosts` and `listenPort` are written from the settings page
|
|
128
|
+
(`listenPort` takes effect after a restart; `0` means an ephemeral port).
|
|
129
|
+
`listenPort` is the address this plugin LISTENS on — it is not the upstream
|
|
130
|
+
address, which belongs in `upstream`; setting it to the upstream's own port
|
|
131
|
+
fails to bind and falls back to an ephemeral port. You can also hand-edit the
|
|
93
132
|
file while dsh runs — it is re-read live.
|
|
94
133
|
|
|
95
134
|
## Install
|
|
@@ -119,16 +158,57 @@ A git install runs the package's `prepare` script to build the bundle. pnpm ≥
|
|
|
119
158
|
Bump to the newest release with pnpm update (or re-add to pick up a newer git ref):
|
|
120
159
|
|
|
121
160
|
```sh
|
|
122
|
-
dsh plugin --profile web update dsh-proxy
|
|
123
|
-
# or, if the dependency spec is pinned: dsh plugin --profile web add dsh-proxy
|
|
161
|
+
dsh plugin --profile web update @karoc/dsh-proxy
|
|
162
|
+
# or, if the dependency spec is pinned: dsh plugin --profile web add @karoc/dsh-proxy
|
|
124
163
|
```
|
|
125
164
|
|
|
126
165
|
Then **restart `dsh web`** so the new client bundle loads.
|
|
127
166
|
|
|
167
|
+
### dsh's own requests (model calls) ride this proxy automatically
|
|
168
|
+
|
|
169
|
+
The proxy has two kinds of clients, and they are wired differently:
|
|
170
|
+
|
|
171
|
+
- **Child processes** (bash, npm, pnpm, git, curl…) inherit the `*PROXY`
|
|
172
|
+
variables this plugin publishes — no setup.
|
|
173
|
+
- **dsh's own requests** (model calls, web fetch/search) are routed by the
|
|
174
|
+
harness's egress policy, which dsh resolves from the environment it was
|
|
175
|
+
LAUNCHED with, before any plugin loads. When that environment declared no
|
|
176
|
+
proxy, this plugin installs the same policy itself, pointing at its own
|
|
177
|
+
listener, once the listener is up (`src/egress.ts`). Nothing to configure.
|
|
178
|
+
|
|
179
|
+
Settings → Smoothly Proxy states which of the following is true:
|
|
180
|
+
|
|
181
|
+
- **routed, self-installed** — the normal case: model traffic follows the host
|
|
182
|
+
checkboxes above, per host. The listen port is an ephemeral one the plugin
|
|
183
|
+
picks itself: there is nothing to configure, and the diagnostics panel (collapsed
|
|
184
|
+
unless something needs attention) only reports it for debugging.
|
|
185
|
+
- **routed, launch environment** — your launcher declared
|
|
186
|
+
`HTTP_PROXY=http://127.0.0.1:<listenPort>`, so dsh installed the policy before
|
|
187
|
+
the plugin did; set `listenPort` (`proxy.json`) to a fixed port to keep that
|
|
188
|
+
address stable across restarts.
|
|
189
|
+
- **a launcher proxy owns egress** — the launch environment declared a
|
|
190
|
+
*different* proxy. This plugin deliberately does **not** layer over it (its own
|
|
191
|
+
proxy sends un-ticked hosts direct, which would silently bypass the launcher's
|
|
192
|
+
policy). Point the launcher at this proxy, or drop the variable, to use
|
|
193
|
+
per-host routing for dsh's own requests.
|
|
194
|
+
- **unavailable** — the harness policy package could not be resolved in this
|
|
195
|
+
composition (unusual layouts). The page then prints the exact `Environment=`
|
|
196
|
+
line to declare instead, and everything else keeps working.
|
|
197
|
+
|
|
198
|
+
### Which deployment does what
|
|
199
|
+
|
|
200
|
+
| How dsh was started | What routes dsh's own requests | Page shows |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `dsh web` from a terminal or a unit you own | this plugin (it installs the policy itself) | *routing through this plugin* |
|
|
203
|
+
| dsh-desktop shell | the shell's proxy — it sets `*PROXY` before spawning dsh | *the launch environment declares a different proxy* (by design: layering ours over it would bypass the shell's policy) |
|
|
204
|
+
| a launcher that sets `HTTP_PROXY=http://127.0.0.1:<port>` itself | dsh, before any plugin loads | *routing through this plugin (named by the launch environment)* |
|
|
205
|
+
| a headless profile | nothing — this plugin is not loaded without the web server | — |
|
|
206
|
+
| a build that does not expose `@deepseek-ai/dsh-http-proxy` | nothing; the plugin cannot join | *could not take over automatically* + the line to paste |
|
|
207
|
+
|
|
128
208
|
### Removing
|
|
129
209
|
|
|
130
210
|
```sh
|
|
131
|
-
dsh plugin --profile web remove dsh-proxy
|
|
211
|
+
dsh plugin --profile web remove @karoc/dsh-proxy
|
|
132
212
|
```
|
|
133
213
|
|
|
134
214
|
This removes both the dependency and its bundle layer from the `web` profile. Restart `dsh web` for the section to disappear.
|
|
@@ -149,7 +229,11 @@ src/client/index.ts # client apply: register settings.section (id dsh-proxy)
|
|
|
149
229
|
src/client/ProxySection.tsx # the settings page (upstream card + host lists)
|
|
150
230
|
src/client/styles.ts # design-token styles (--dsw-alias-*) + injection
|
|
151
231
|
src/client/locales.ts # en/zh copy
|
|
152
|
-
scripts
|
|
232
|
+
scripts/*.spec.mjs # behavioral tests: proxy-core.ts plus
|
|
233
|
+
# host route / client boot / cordis mount — no network
|
|
234
|
+
scripts/packed-artifact.spec.mjs
|
|
235
|
+
# what npm PUBLISHES: member set, in-tarball bundles,
|
|
236
|
+
# installed-layout load + egress self-install
|
|
153
237
|
```
|
|
154
238
|
|
|
155
239
|
### The /proxy/api route
|
|
@@ -157,17 +241,36 @@ scripts/proxy-core.spec.mjs # behavioral tests (13 scenarios, no network)
|
|
|
157
241
|
The host half serves the settings page over a same-origin HTTP route (the
|
|
158
242
|
built-in `/api` prefix is reserved for the gateway, so this uses `/proxy/api`):
|
|
159
243
|
|
|
160
|
-
- `GET /proxy/api` → `{ upstream, proxiedHosts, knownHosts, hosts, providers,
|
|
161
|
-
|
|
244
|
+
- `GET /proxy/api` → `{ ok, upstream, proxiedHosts, knownHosts, hosts, providers,
|
|
245
|
+
port, routeMode, … }` plus the egress diagnostics the panel reports
|
|
246
|
+
- `POST /proxy/api` `{ op: 'save', upstream, proxiedHosts, listenPort?, routeMode? }`
|
|
247
|
+
→ sanitized + persisted config
|
|
162
248
|
- `POST /proxy/api` `{ op: 'test', upstream }` → `{ ok, detail }`
|
|
163
249
|
- `POST /proxy/api` `{ op: 'persist' }` → merge observed hosts into `knownHosts`
|
|
250
|
+
- `POST /proxy/api` `{ op: 'forget' }` → clear the observed-host list (live set +
|
|
251
|
+
`knownHosts`)
|
|
252
|
+
|
|
253
|
+
**It sits behind the same operator-session gate as the harness's own `/api`**
|
|
254
|
+
(401/403 without the browser cookie) — the route exposes the upstream address and
|
|
255
|
+
credentials and can rewrite the routing, and on a shared machine any local process
|
|
256
|
+
can reach 127.0.0.1. A CLI check therefore has to carry that cookie:
|
|
257
|
+
|
|
258
|
+
```sh
|
|
259
|
+
COOKIE=$(node -e "import('./scripts/lib/auth-cookie.mjs').then(m=>{const c=m.mintBrowserCookie();console.log(c.name+'='+c.value)})")
|
|
260
|
+
curl -s -H "cookie: $COOKIE" http://127.0.0.1:3080/proxy/api | head -c 200
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
In a composition without the harness connection service there is no credential
|
|
264
|
+
model to consult, so the route stays open as before.
|
|
164
265
|
|
|
165
266
|
## Build
|
|
166
267
|
|
|
167
268
|
```sh
|
|
168
269
|
pnpm install
|
|
169
270
|
pnpm bundle # emits lib/index.js + lib/client.js
|
|
170
|
-
pnpm
|
|
271
|
+
pnpm typecheck # tsc --noEmit
|
|
272
|
+
pnpm test # node --test: proxy-core + host-route /
|
|
273
|
+
# client-boot / cordis-mount specs (no network)
|
|
171
274
|
pnpm release:check # release gate: docs/changelog/tag/tree/build/registry must all pass
|
|
172
275
|
pnpm publish # runs the gate (prepack/prepublishOnly), then postpublish verifies the live release
|
|
173
276
|
```
|
|
@@ -189,18 +292,31 @@ module table; everything else is inlined.
|
|
|
189
292
|
- `npm`/`pnpm` install/update traffic goes through the proxy like everything
|
|
190
293
|
else; toggling a host **related to install/update** takes effect on the next
|
|
191
294
|
install/update (already-in-flight operations keep their environment).
|
|
295
|
+
- **A SOCKS5 upstream only carries HTTPS targets.** SOCKS5 has no absolute-URI
|
|
296
|
+
HTTP mode, so a selected plain `http://` host falls back to direct through a
|
|
297
|
+
SOCKS5 upstream (the settings page states the same thing).
|
|
192
298
|
- **Section nav icon is shell-assigned, not plugin-assigned.** The built-in
|
|
193
299
|
`ui-settings-general` `SettingsRoot.tsx` `navIcon(id)` maps known ids and
|
|
194
300
|
falls back to a gear for every other id — including this section's
|
|
195
301
|
`dsh-proxy`. The `settings.section` registration has no icon field, so an
|
|
196
302
|
external plugin cannot set it without patching the shell. When DSH exposes a
|
|
197
|
-
per-section icon, use `
|
|
303
|
+
per-section icon, use `IconGlobeOutlineRegular` from `dsh-client-ui-primitives`
|
|
198
304
|
for this section.
|
|
199
|
-
- **
|
|
305
|
+
- **Requires dsh ≥ 0.1.7 since v0.1.4 — v0.1.3 remains the release for
|
|
306
|
+
0.1.2–0.1.6.** DSH 0.1.7 renamed the icon exports (`IconX14` → `IconXRegular`,
|
|
307
|
+
the stroke-weight naming; upstream commit `4937343a5e`), so this section
|
|
308
|
+
imports the `*Regular` variants. The floor is declared as an **optional peer
|
|
309
|
+
dependency** on `@deepseek-ai/dsh-client-ui-settings`, which the dsh ≥ 0.1.7
|
|
310
|
+
compatibility gate compares against the running dsh version — a mismatch
|
|
311
|
+
refuses the bundle and names the exact `dsh plugin allow-version` remedy.
|
|
312
|
+
dsh < 0.1.7 predates that gate, so those runtimes still load this plugin and
|
|
313
|
+
render a broken section — install v0.1.3 there.
|
|
314
|
+
- **Install it the official way.** The loader
|
|
200
315
|
entry name, bundle registration id, and host plugin name are all
|
|
201
|
-
`@karoc/dsh-proxy` (matching the npm package name).
|
|
202
|
-
|
|
203
|
-
`
|
|
316
|
+
`@karoc/dsh-proxy` (matching the npm package name). A hand-written `link:`
|
|
317
|
+
dependency only counts once the package manager has materialized it under the
|
|
318
|
+
profile's `node_modules` and the profile lists it in `dsh.profile.bundles` —
|
|
319
|
+
install via `dsh plugin --profile web add @karoc/dsh-proxy` (npm) or
|
|
204
320
|
`dsh plugin --profile web add link:/path/to/dsh-proxy` (source), then restart
|
|
205
321
|
`dsh web`.
|
|
206
322
|
- The desktop shell (`dsh-desktop`) keeps its own proxy and tray settings
|
package/README.zh.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**思磨力代理插件**(**Smoothly Proxy** / 思磨力)是一个**外部** DeepSeek Harness 插件:为**模型提供方**提供带**按主机路由**的**正向代理**,并附带一个专职设置页。
|
|
10
10
|
|
|
11
|
-
它在 dsh host 进程内启动一个 loopback 正向代理,把进程的出站流量指向它(`HTTP(S)_PROXY` + `NODE_USE_ENV_PROXY
|
|
11
|
+
它在 dsh host 进程内启动一个 loopback 正向代理,把进程的出站流量指向它(`HTTP(S)_PROXY` + `NODE_USE_ENV_PROXY`),把你在设置页勾选的主机转发到可选的上游代理(也可以一键切换成"**除勾选外全部走代理**")(HTTP / HTTPS / SOCKS5,支持可选 Basic 认证),其余全部直连。模型提供方主机**实时**读自正在运行的 dsh 配置,流量中观测到的主机也会被收集——两者都以复选框形式出现。
|
|
12
12
|
|
|
13
13
|
为什么要做成外部插件:内置设置页不覆盖出站代理,而给内置包加一页会在官方下次发布时被覆盖。本包作为可安装的 **bundle** 交付,从不触碰仓库源码,官方更新无法覆盖它。
|
|
14
14
|
|
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
|
|
17
17
|
## 它新增了什么
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
一个新的设置项 **「思磨力代理 / Smoothly Proxy」**(导航顺序 25),排在内置 **Models** 页以及(若装了该插件)外部 **Model reasoning** 页之后,包含:
|
|
20
20
|
|
|
21
|
-
- **上游代理卡片**:启用开关、协议选择(HTTP / HTTPS / SOCKS5
|
|
22
|
-
-
|
|
21
|
+
- **上游代理卡片**:启用开关、协议选择(HTTP / HTTPS / SOCKS5)、主机、端口、可选用户名/密码,以及**测试连接**按钮(SOCKS5 上游走 SOCKS5 握手、HTTP 上游走 `CONNECT` 探测;HTTPS 上游只验证 TCP 可达——不校验其 TLS 握手);
|
|
22
|
+
- **代理规则**开关——同一份勾选列表的两种读法:*只代理勾选的主机*(默认)或*除勾选外全部走代理*,适合本机代理自己已经做了分流的场景;
|
|
23
|
+
- **模型提供方**列表——**实时**读自正在运行的 dsh 配置的主机(`llm-deepseek.baseURL`、`llm-pi-ai.providers.<n>.baseURL`、任意 `llm-*` 命名空间),有友好显示名时以显示名标注。取值来自 harness 自己的 settings 文档,因此列表跟着运行中的配置走,与 dsh 把文档放在哪里无关;仍在用 0.2 之前 `<DSH_HOME>/settings.yaml` 的 harness 会回退读那个文件。loopback 端点不会出现——它们永远不可能被代理;
|
|
23
24
|
- **其它已观测主机**列表——代理在流量中见过的主机(含 `registry.npmjs.org` 等安装/更新流量),持久化进 `proxy.json` 以跨重启保留;
|
|
24
25
|
- **搜索框**——输入时按主机 / 名称过滤上面两个列表。
|
|
25
26
|
|
|
@@ -44,11 +45,28 @@ dsh host 进程
|
|
|
44
45
|
上游代理(HTTP/HTTPS/SOCKS5,可选 Basic 认证)
|
|
45
46
|
```
|
|
46
47
|
|
|
48
|
+
同一份勾选列表有两种读法(列表上方的**代理规则**):
|
|
49
|
+
|
|
50
|
+
| 规则 | 勾选的主机 | 其余主机 | 什么时候用 |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| *只代理勾选的主机*(默认) | 走上游 | 直连 | 只有少数提供方需要走上游 |
|
|
53
|
+
| *除勾选外全部走代理* | 直连 | 走上游 | 你的本机代理已经做了分流 |
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
# 少数提供方走上游
|
|
57
|
+
规则:只代理勾选的主机 勾选:new-api.example、opencode.ai
|
|
58
|
+
|
|
59
|
+
# 全部走本机代理,只留必须直连的
|
|
60
|
+
规则:除勾选外全部走代理 勾选:api.internal.example(即"直连白名单")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
两种读法下列表标题都会跟着变(「走代理的主机」↔「直连的主机」),每行的状态显示的是**实际结果**——上游没启用时,勾了也显示"直连"。
|
|
64
|
+
|
|
47
65
|
安全规则(硬编码,不可配置):
|
|
48
66
|
|
|
49
67
|
- **loopback 目标恒直连**——绝不发给上游代理;
|
|
50
68
|
- 指向本代理自身的上游被当作**禁用**(防自环保护);
|
|
51
|
-
- 代理自身只用裸 `net`/`http
|
|
69
|
+
- 代理自身只用裸 `net`/`http`,且走一个**显式 `Agent`**、无视环境代理——即使启动环境为 dsh 自己的 fetch 设了 `NODE_USE_ENV_PROXY=1`,它的出站连接也永远不会被路由回自己;错配的 `Host` 会被替换为请求目标的 authority(RFC 9112 §3.2.2)。
|
|
52
70
|
|
|
53
71
|
### 配置文件
|
|
54
72
|
|
|
@@ -60,11 +78,12 @@ dsh host 进程
|
|
|
60
78
|
{
|
|
61
79
|
"upstream": { "enabled": false, "protocol": "http", "host": "", "port": 0, "username": "", "password": "" },
|
|
62
80
|
"proxiedHosts": ["api.deepseek.com"],
|
|
63
|
-
"knownHosts": ["api.deepseek.com", "registry.npmjs.org"]
|
|
81
|
+
"knownHosts": ["api.deepseek.com", "registry.npmjs.org"],
|
|
82
|
+
"listenPort": 45999
|
|
64
83
|
}
|
|
65
84
|
```
|
|
66
85
|
|
|
67
|
-
`knownHosts` 由插件写入(观测到的流量);`upstream` 与 `
|
|
86
|
+
`knownHosts` 由插件写入(观测到的流量);`upstream`、`proxiedHosts` 与 `listenPort` 由设置页写入(`listenPort` 重启后生效;`0` 表示随机端口)。`listenPort` 是本插件**监听**的地址,**不是上游地址**——上游地址属于 `upstream`;把它填成上游自己的端口会导致绑定失败并回退到临时端口。你也可以在 dsh 运行中手改该文件——它是实时重读的。
|
|
68
87
|
|
|
69
88
|
## 安装
|
|
70
89
|
|
|
@@ -93,16 +112,40 @@ git 安装会运行包的 `prepare` 脚本构建 bundle。pnpm ≥ 10 需要先
|
|
|
93
112
|
用 pnpm update 升到最新版(或重 `add` 以拉取更新的 git ref):
|
|
94
113
|
|
|
95
114
|
```sh
|
|
96
|
-
dsh plugin --profile web update dsh-proxy
|
|
97
|
-
# 或,若依赖规格被 pin 住:dsh plugin --profile web add dsh-proxy
|
|
115
|
+
dsh plugin --profile web update @karoc/dsh-proxy
|
|
116
|
+
# 或,若依赖规格被 pin 住:dsh plugin --profile web add @karoc/dsh-proxy
|
|
98
117
|
```
|
|
99
118
|
|
|
100
119
|
然后**重启 `dsh web`** 以加载新客户端 bundle。
|
|
101
120
|
|
|
121
|
+
### dsh 自己的请求(模型调用)会自动走本代理
|
|
122
|
+
|
|
123
|
+
本代理有两类客户端,接线方式不同:
|
|
124
|
+
|
|
125
|
+
- **子进程**(bash、npm、pnpm、git、curl…)继承本插件写进环境的 `*PROXY` 变量——无需配置;
|
|
126
|
+
- **dsh 自己的请求**(模型调用、web fetch/search)由 harness 的出网策略决定,而该策略是 dsh **在启动时、早于任何插件**从启动环境解析的。当启动环境没有声明代理时,本插件会在自己的监听就绪后**自己装上同一条策略**并指向本端口(见 `src/egress.ts`)——无需任何配置。
|
|
127
|
+
|
|
128
|
+
设置 → 思磨力代理会明说当前属于哪一种:
|
|
129
|
+
|
|
130
|
+
- **已路由(自装)**——常态:模型流量按上面的主机勾选逐台分流。监听端口是插件自己挑的临时端口:**没有任何需要配置的东西**,诊断面板(默认折叠,只在需要你注意时才展开)只是把端口报出来便于排查;
|
|
131
|
+
- **已路由(启动环境)**——你的启动环境声明了 `HTTP_PROXY=http://127.0.0.1:<listenPort>`,dsh 在插件之前就装好了策略;把 `listenPort`(`proxy.json`)固定成某个端口,这个地址才能跨重启保持稳定;
|
|
132
|
+
- **出网归启动环境的另一个代理管**——启动环境声明了**别的**代理。本插件**故意不覆盖**它(本插件的代理会把未勾选主机直连,覆盖上去等于静默绕过启动方的策略)。想让 dsh 自己的请求也按本插件分流,就把启动环境指向本代理,或删掉那个变量;
|
|
133
|
+
- **不可用**——当前组合里解析不到 harness 的策略包(少见的布局)。页面会打印可直接粘贴的 `Environment=` 行作为兜底,其它功能不受影响。
|
|
134
|
+
|
|
135
|
+
### 不同部署下会发生什么
|
|
136
|
+
|
|
137
|
+
| dsh 是怎么启动的 | dsh 自己的请求归谁 | 页面显示 |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| 终端或你自己的 unit 里跑 `dsh web` | 本插件(自己装策略) | *已由本插件接管* |
|
|
140
|
+
| dsh-desktop 桌面壳 | 壳的代理——它在 spawn dsh 之前就设好 `*PROXY` | *启动环境声明了别的代理*(有意为之:覆盖上去会绕过壳的策略) |
|
|
141
|
+
| 启动器自己设了 `HTTP_PROXY=http://127.0.0.1:<port>` | dsh(早于任何插件装载) | *已由本插件接管(启动环境指名)* |
|
|
142
|
+
| headless profile | 没有——没有 web server 就不加载本插件 | — |
|
|
143
|
+
| 不暴露 `@deepseek-ai/dsh-http-proxy` 的构建 | 没有,插件接不进去 | *未能自动接管* + 可直接粘贴的那行 |
|
|
144
|
+
|
|
102
145
|
### 卸载
|
|
103
146
|
|
|
104
147
|
```sh
|
|
105
|
-
dsh plugin --profile web remove dsh-proxy
|
|
148
|
+
dsh plugin --profile web remove @karoc/dsh-proxy
|
|
106
149
|
```
|
|
107
150
|
|
|
108
151
|
同时从 `web` profile 移除依赖与 bundle 层。重启 `dsh web` 后该设置项消失。
|
|
@@ -120,24 +163,40 @@ src/client/index.ts # client apply:注册 settings.section(id dsh-proxy)
|
|
|
120
163
|
src/client/ProxySection.tsx # 设置页(上游卡片 + 主机列表)
|
|
121
164
|
src/client/styles.ts # design-token 样式(--dsw-alias-*)+ 注入
|
|
122
165
|
src/client/locales.ts # en/zh 文案
|
|
123
|
-
scripts
|
|
166
|
+
scripts/*.spec.mjs # 行为测试:proxy-core.ts + host route / client
|
|
167
|
+
# boot / cordis mount,全程无外部网络
|
|
168
|
+
scripts/packed-artifact.spec.mjs
|
|
169
|
+
# 真正被 npm 发出去的那份:成员清单、包内产物、
|
|
170
|
+
# 以"已安装布局"加载 + 自装出网策略
|
|
124
171
|
```
|
|
125
172
|
|
|
126
173
|
### /proxy/api 路由
|
|
127
174
|
|
|
128
175
|
host 半区用同源 HTTP 路由为设置页供数(内置 `/api` 前缀被 gateway 占用,故用 `/proxy/api`):
|
|
129
176
|
|
|
130
|
-
- `GET /proxy/api` → `{ upstream, proxiedHosts, knownHosts, hosts, providers, port }`
|
|
131
|
-
- `POST /proxy/api` `{ op: 'save', upstream, proxiedHosts }` → 清洗后持久化的配置
|
|
177
|
+
- `GET /proxy/api` → `{ ok, upstream, proxiedHosts, knownHosts, hosts, providers, port, routeMode, … }` 以及面板用到的出网诊断字段
|
|
178
|
+
- `POST /proxy/api` `{ op: 'save', upstream, proxiedHosts, listenPort?, routeMode? }` → 清洗后持久化的配置
|
|
132
179
|
- `POST /proxy/api` `{ op: 'test', upstream }` → `{ ok, detail }`
|
|
133
180
|
- `POST /proxy/api` `{ op: 'persist' }` → 把观测主机并入 `knownHosts`
|
|
181
|
+
- `POST /proxy/api` `{ op: 'forget' }` → 清空观测列表(会话内集合 + `knownHosts`)
|
|
182
|
+
|
|
183
|
+
**它与 harness 自带的 `/api` 走同一道门**(没有浏览器会话 cookie 时返回 401/403)——这条路由会暴露上游地址与凭据、还能改写路由规则,而在多用户机器上任何本地进程都能访问 127.0.0.1。因此命令行自查必须带上该 cookie:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
COOKIE=$(node -e "import('./scripts/lib/auth-cookie.mjs').then(m=>{const c=m.mintBrowserCookie();console.log(c.name+'='+c.value)})")
|
|
187
|
+
curl -s -H "cookie: $COOKIE" http://127.0.0.1:3080/proxy/api | head -c 200
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
组合里没有 harness 的 connection 服务时,没有凭据模型可依,该路由保持原样。
|
|
134
191
|
|
|
135
192
|
## 构建
|
|
136
193
|
|
|
137
194
|
```sh
|
|
138
195
|
pnpm install
|
|
139
196
|
pnpm bundle # 产出 lib/index.js + lib/client.js
|
|
140
|
-
pnpm
|
|
197
|
+
pnpm typecheck # tsc --noEmit
|
|
198
|
+
pnpm test # node --test:proxy-core(16 场景)+ host-route /
|
|
199
|
+
# client-boot / cordis-mount,无外部网络
|
|
141
200
|
pnpm release:check # 发布门禁:文档/changelog/tag/工作区/构建/registry 全过
|
|
142
201
|
pnpm publish # 跑门禁(prepack/prepublishOnly),随后 postpublish 验证线上发布
|
|
143
202
|
```
|
|
@@ -148,8 +207,10 @@ bundle 把平台包(`react`、`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client
|
|
|
148
207
|
|
|
149
208
|
- **代理是进程级出站点,不是模型逐请求读的开关。** 它通过把进程的 `HTTP(S)_PROXY` 指向自身来实现。本插件 host `apply()` 之后 dsh 发起的请求(任何子进程、以及 global dispatcher 仍是惰性的 undici fetch)会走代理。若某个 dsh 内部 fetch 在插件加载前就已实体化了 dispatcher,可能不会走它——文档化的解法是重启 dsh(桌面壳通过 spawn dsh 前设环境变量来施加同样的边界)。
|
|
150
209
|
- `npm`/`pnpm` 安装/更新流量与其它一样走代理;改动**与安装/更新相关的主机**会在下次安装/更新时生效(已在进行的操作保留其环境)。
|
|
151
|
-
-
|
|
152
|
-
-
|
|
210
|
+
- **SOCKS5 上游只承载 HTTPS 目标。** SOCKS5 没有 absolute-URI 的 HTTP 模式,因此选中的纯 `http://` 主机会退回直连(设置页也写明了这一点)。
|
|
211
|
+
- **设置页导航图标由壳分配,插件无法自定义。** 内置 `ui-settings-general` 的 `SettingsRoot.tsx` `navIcon(id)` 只映射已知 id,其它 id(包括本节的 `dsh-proxy`)一律齿轮。`settings.section` 注册没有 icon 字段,外部插件不 patch 壳就改不了。等 DSH 开放每节图标后,本节建议用 `dsh-client-ui-primitives` 的 `IconGlobeOutlineRegular`。
|
|
212
|
+
- **本节自 v0.1.4 起要求 dsh ≥ 0.1.7(0.1.2–0.1.6 请用 v0.1.3)。** DSH 0.1.7 改了图标导出名(`IconX14` → `IconXRegular`,尺寸后缀并入 artwork 默认值、名字改为承载笔画权重;上游 commit `4937343a5e`),本节现在导入 `*Regular` 变体。该下限已声明为对 `@deepseek-ai/dsh-client-ui-settings` 的**可选 peer 依赖**,由 dsh ≥ 0.1.7 的兼容门禁按运行时版本判定(不匹配则拒绝加载该 bundle,并给出 `dsh plugin allow-version` 的具体解法)。0.1.7 之前的 dsh 没有这道门禁,仍会加载本插件并渲染出坏掉的分区——那些运行时请用 v0.1.3。
|
|
213
|
+
- **需以官方方式安装才生效。** loader entry 名、bundle 注册 id、host 插件名三者均为 `@karoc/dsh-proxy`(与 npm 包名一致)。手写 `link:` 依赖只有在包管理器把它落到 profile 的 `node_modules` 下、且 profile 的 `dsh.profile.bundles` 列出它之后才算数——请用 `dsh plugin --profile web add @karoc/dsh-proxy`(npm)或 `dsh plugin --profile web add link:/path/to/dsh-proxy`(源码)安装,然后重启 `dsh web`。
|
|
153
214
|
- 桌面壳(`dsh-desktop`)保留自己的代理与托盘设置窗口——本插件是独立抽取,不是替代品。两者可共存(例如把 `dsh-proxy` 装进壳的 profile,同时获得 dsh 内设置页)。
|
|
154
215
|
|
|
155
216
|
## License
|
package/cordis.patch.yml
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Bundle layer applied when a profile lists this package. The row names the
|
|
2
2
|
# package so the host loader resolves the node half and client-modules serves
|
|
3
|
-
# exports["./client"] at /plugins/dsh-proxy/client.js
|
|
3
|
+
# exports["./client"] at /plugins/@karoc/dsh-proxy/client.js (the browser row
|
|
4
|
+
# id is the package name, not the runtime plugin id). The host half starts the
|
|
4
5
|
# loopback forward proxy and registers /proxy/api; the client half registers
|
|
5
6
|
# the settings.section settings page. The loader entry `name` must equal the
|
|
6
7
|
# npm package name (@karoc/dsh-proxy) — client-modules resolves it as a
|