@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/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 **only the hosts you select** through an optional upstream proxy
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 derived from your dsh `settings.yaml`, and
18
- hosts observed in traffic are collected for you — both appear as checkboxes.
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」**, placed after the
33
- built-in **Models** and **Model reasoning** pages. It shows:
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 that verifies the upstream speaks its protocol;
38
- - a **model providers** list — hosts read from your dsh `settings.yaml`
39
- (`llm-deepseek.baseURL`, `llm-pi-ai.providers.<n>.baseURL`, any `llm-*`
40
- namespace), labeled with the friendly display name where available;
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`, so it can never route its own
75
- outbound connections back through itself.
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` and
92
- `proxiedHosts` are written from the settings page. You can also hand-edit the
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/proxy-core.spec.mjs # behavioral tests (13 scenarios, no network)
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, port }`
161
- - `POST /proxy/api` `{ op: 'save', upstream, proxiedHosts }` → sanitized + persisted config
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 test # tsc --noEmit + proxy-core.spec.mjs (13 scenarios)
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 `IconGlobeOutline14` from `dsh-client-ui-primitives`
303
+ per-section icon, use `IconGlobeOutlineRegular` from `dsh-client-ui-primitives`
198
304
  for this section.
199
- - **Works with dsh ≥ 0.1.2 when installed the official way.** The loader
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). Hand-written `link:`
202
- dependencies are not recognized as packages by newer loaders — install via
203
- `dsh plugin --profile web add @karoc/dsh-proxy` (npm) or
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`),**只把你在设置页勾选的主机**转发到可选的上游代理(HTTP / HTTPS / SOCKS5,支持可选 Basic 认证),其余全部直连。模型提供方主机从你的 dsh `settings.yaml` 读取,流量中观测到的主机也会被收集——两者都以复选框形式出现。
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
- 一个放在内置 **Models** 与 **Model reasoning** 页之后的新设置项 **「思磨力代理 / Smoothly Proxy」**,包含:
19
+ 一个新的设置项 **「思磨力代理 / Smoothly Proxy」**(导航顺序 25),排在内置 **Models** 页以及(若装了该插件)外部 **Model reasoning** 页之后,包含:
20
20
 
21
- - **上游代理卡片**:启用开关、协议选择(HTTP / HTTPS / SOCKS5)、主机、端口、可选用户名/密码,以及**测试连接**按钮(验证上游是否会说对应协议);
22
- - **模型提供方**列表——从你的 dsh `settings.yaml` 读取的主机(`llm-deepseek.baseURL`、`llm-pi-ai.providers.<n>.baseURL`、任意 `llm-*` 命名空间),有友好显示名时以显示名标注;
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` 与 `proxiedHosts` 由设置页写入。你也可以在 dsh 运行中手改该文件——它是实时重读的。
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/proxy-core.spec.mjs # 行为测试(13 场景,无外部网络)
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 test # tsc --noEmit + proxy-core.spec.mjs(13 场景)
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
- - **设置页导航图标由壳分配,插件无法自定义。** 内置 `ui-settings-general` 的 `SettingsRoot.tsx` `navIcon(id)` 只映射已知 id,其它 id(包括本节的 `dsh-proxy`)一律齿轮。`settings.section` 注册没有 icon 字段,外部插件不 patch 壳就改不了。等 DSH 开放每节图标后,本节建议用 `dsh-client-ui-primitives` 的 `IconGlobeOutline14`。
152
- - **dsh ≥ 0.1.2 需以官方方式安装才生效。** loader entry 名、bundle 注册 id、host 插件名三者均为 `@karoc/dsh-proxy`(与 npm 包名一致)。新版 loader 不再把手写 `link:` 依赖识别为包——请用 `dsh plugin --profile web add @karoc/dsh-proxy`(npm)或 `dsh plugin --profile web add link:/path/to/dsh-proxy`(源码)安装,然后重启 `dsh web`。
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. The host half starts the
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