@karoc/dsh-proxy 0.1.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 ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0]
4
+
5
+ - Initial release: external DeepSeek Harness plugin providing a model-provider
6
+ forward proxy with per-host routing and a dedicated Settings page.
7
+ - Host half (`src/index.ts` + `src/proxy-core.ts`): starts a loopback forward
8
+ proxy (HTTP + CONNECT, HTTP/HTTPS/SOCKS5 upstreams, Basic auth, per-host
9
+ routing, live config re-read, self-loop guard), points the process's
10
+ `HTTP(S)_PROXY` / `NODE_USE_ENV_PROXY` / `NO_PROXY` at it, and serves
11
+ `/proxy/api` (GET view, POST save / test / persist). Engine is a faithful TS
12
+ port of dsh-desktop's `scripts/proxy.mjs`; `test` probe and `sanitizeUpstream`
13
+ mirror the desktop shell's Rust implementations.
14
+ - Client half (`src/client/*`): registers a `settings.section` (id `dsh-proxy`,
15
+ order 25) after the built-in Models and Model-reasoning pages. UI mirrors the
16
+ desktop settings window: upstream card (enable / protocol / host / port /
17
+ user / pass / test connection), model-provider hosts derived from
18
+ `settings.yaml`, other observed hosts, per-host checkboxes, and a search box.
19
+ - Config persisted at `<DSH_HOME>/proxy.json`; changes take effect immediately
20
+ (the proxy re-reads the file on every request).
21
+ - Settings page: a provider/observed-host row whose label truncates shows the
22
+ full label (all display names + host) in a custom hover bubble (DSH
23
+ `Tooltip`) — no native `title` tooltip. Overflow is measured on every commit
24
+ plus `ResizeObserver` / `document.fonts.ready` / window resize, so a late
25
+ webfont that widens the text retroactively arms the bubble; short labels stay
26
+ quiet.
27
+ - Tests: `scripts/proxy-core.spec.mjs` — 13 scenarios (ported from
28
+ dsh-desktop's `test-proxy.mjs` plus settings-helper coverage), no external
29
+ network.
30
+ - Full release tooling: bilingual README, CHANGELOG, CONTRIBUTING, release
31
+ gate + post-publish verification.
@@ -0,0 +1,97 @@
1
+ # Contributing to dsh-proxy
2
+
3
+ Thanks for helping with `dsh-proxy` — an external DeepSeek Harness plugin for
4
+ model-provider proxying.
5
+
6
+ ## Development
7
+
8
+ ```sh
9
+ pnpm install # dev deps (tsdown, typescript, react types)
10
+ pnpm typecheck # tsc --noEmit
11
+ pnpm test # typecheck + scripts/proxy-core.spec.mjs (13 scenarios)
12
+ pnpm bundle # emit lib/index.js + lib/client.js
13
+ ```
14
+
15
+ The host half (`src/index.ts` + `src/proxy-core.ts`) is pure Node — no dsh
16
+ runtime needed to test it: `pnpm test` runs the proxy against fake origins and
17
+ fake upstreams on `127.0.0.1` (no external network). The client half
18
+ (`src/client/*`) is only exercised inside a running `dsh web`.
19
+
20
+ ### Local install without publishing
21
+
22
+ To see the plugin live in the GUI, link it into a profile:
23
+
24
+ ```jsonc
25
+ // ~/.dsh/profiles/web/package.json
26
+ "dependencies": { "@karoc/dsh-proxy": "link:/path/to/dsh-proxy", ... },
27
+ "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", ..., "dsh-proxy"] } }
28
+ ```
29
+
30
+ Then `cd ~/.dsh/profiles/web && pnpm install` and **restart `dsh web`**
31
+ (changing the plugin set requires a restart; only bundle content changes hot
32
+ reload via the dev-server watcher).
33
+
34
+ ## Layout
35
+
36
+ - `src/proxy-core.ts` — the forward proxy engine (ported from dsh-desktop;
37
+ keep behavior compatible). Pure Node, no dsh imports.
38
+ - `src/index.ts` — host `apply`: starts the proxy, sets `*PROXY` env vars,
39
+ registers `/proxy/api`.
40
+ - `src/client/` — settings page + slot registration + i18n + styles.
41
+
42
+ ## Release checklist
43
+
44
+ Every release must be done in one pass (code + docs + changelog + version +
45
+ tag). The gate script (`pnpm release:check`, also run by `prepack` and
46
+ `prepublishOnly`) enforces:
47
+
48
+ 1. `README.md` and `README.zh.md` exist and have the **same number of `##` and
49
+ `###` sections** (bilingual structural sync — add/remove sections in both).
50
+ 2. `CHANGELOG.md` has a non-empty entry for the current version, as the latest
51
+ released entry.
52
+ 3. `package.json` version matches the latest CHANGELOG entry.
53
+ 4. Git tag `v<version>` exists and points at HEAD.
54
+ 5. Working tree is clean (everything committed).
55
+ 6. `lib/` is present and fresh (run `pnpm bundle` after touching `src/`).
56
+ 7. The version is not already published on npm.
57
+
58
+ Release steps:
59
+
60
+ ```sh
61
+ # 1. write code + update README.md/README.zh.md + CHANGELOG.md together
62
+ # 2. bump version in package.json
63
+ git add -A && git commit -m "dsh-proxy v<version>"
64
+ git tag v<version> && git push origin v<version>
65
+ pnpm release:check
66
+ ```
67
+
68
+ ### Publishing is a manual step (2FA)
69
+
70
+ The npm account has two-factor authentication enabled, so **the agent cannot
71
+ complete the publish** — OTP needs a human. Everything must be prepared to
72
+ "one command away" (clean tree, version + tag in place, `release:check`
73
+ passing, `npm pack --dry-run` content confirmed), then the human runs:
74
+
75
+ ```sh
76
+ npm login # 2FA
77
+ npm publish # runs the gate (prepack/prepublishOnly), then postpublish verifies
78
+ ```
79
+
80
+ `npm publish --ignore-scripts` bypasses all gates and post-publish verification
81
+ — treat it as a process violation. After a successful publish, `postpublish`
82
+ polls the registry until the version is visible and verifies `dist-tags.latest`
83
+ plus the tarball contents.
84
+
85
+ ### Known release-check "failures" that are actually fine
86
+
87
+ - After a release, the next `release:check` fails with "version already
88
+ published" / "tag not at HEAD" — that's the gate protecting an already-live
89
+ version. Bump to the next version and tag it instead of overwriting.
90
+ - The first publish of a brand-new package: the post-publish registry poll
91
+ covers the whole-package-not-yet-visible case; if it still times out, verify
92
+ manually with `npm view @karoc/dsh-proxy versions` — do not re-publish the
93
+ same version without checking.
94
+
95
+ ## License
96
+
97
+ MIT
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 karoc
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 ADDED
@@ -0,0 +1,211 @@
1
+ # dsh-proxy
2
+
3
+ English | [简体中文](README.zh.md)
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@karoc/dsh-proxy.svg)](https://www.npmjs.com/package/@karoc/dsh-proxy)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@karoc/dsh-proxy.svg)](https://www.npmjs.com/package/@karoc/dsh-proxy)
7
+ [![license MIT](https://img.shields.io/npm/l/@karoc/dsh-proxy.svg)](LICENSE)
8
+
9
+ An **external** DeepSeek Harness plugin: a model-provider **forward proxy** with
10
+ **per-host routing**, plus a **Settings page** dedicated to model-provider
11
+ proxying.
12
+
13
+ It runs a loopback forward proxy inside the dsh host process, points the
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
16
+ (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.
19
+
20
+ Why an external plugin: the built-in settings pages don't cover outbound
21
+ proxying, and adding one to a built-in package would be overwritten by the next
22
+ official release. This package ships as an installable **bundle** that never
23
+ touches repository source, so official updates cannot clobber it.
24
+
25
+ The proxy engine and settings UI are extracted from
26
+ [karoc/dsh-desktop](https://github.com/karoc/dsh-desktop) (behavior preserved)
27
+ so a browser-only `dsh web` gets the same model-provider proxy controls the
28
+ desktop shell had.
29
+
30
+ ## What it adds
31
+
32
+ A new Settings section, **「代理 / Proxy」**, placed after the built-in
33
+ **Models** and **Model reasoning** pages. It shows:
34
+
35
+ - an **upstream proxy card**: enable toggle, protocol selector (HTTP / HTTPS /
36
+ 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;
41
+ - an **other observed hosts** list — hosts the proxy has seen in traffic
42
+ (including `registry.npmjs.org` install/update traffic), persisted into
43
+ `proxy.json` so they survive restarts;
44
+ - a **search box** that filters both lists by host / name as you type.
45
+
46
+ Check a host to route it through the upstream proxy. Saving writes
47
+ `<DSH_HOME>/proxy.json`; the running proxy **re-reads the file on every
48
+ request**, so changes take effect immediately — no dsh restart needed.
49
+
50
+ ### How the proxy routes traffic
51
+
52
+ ```
53
+ dsh host process
54
+ ├─ undici fetch (model requests, web search) ──┐
55
+ ├─ npm / pnpm / git / child CLIs ──────────────┤ HTTP(S)_PROXY → loopback proxy
56
+ └─ subagents ──────────────────────────────────┘ │
57
+ ▼
58
+ 127.0.0.1:<random> forward proxy (raw net/http)
59
+ │
60
+ ┌───────────────────────────┴────────────┐
61
+ ▼ ▼
62
+ hosts in proxiedHosts everything else
63
+ + upstream enabled DIRECT (loopback ALWAYS direct)
64
+ │
65
+ ▼
66
+ upstream proxy (HTTP/HTTPS/SOCKS5, optional Basic auth)
67
+ ```
68
+
69
+ Safety rules (hard-coded, not configurable):
70
+
71
+ - **loopback targets are always direct** — never sent to an upstream proxy;
72
+ - an upstream pointing back at this very proxy is treated as **disabled**
73
+ (self-loop guard);
74
+ - the proxy itself uses raw `net`/`http`, so it can never route its own
75
+ outbound connections back through itself.
76
+
77
+ ### Config file
78
+
79
+ ```
80
+ <DSH_HOME>/proxy.json # $DSH_HOME, or ~/.dsh by default
81
+ ```
82
+
83
+ ```json
84
+ {
85
+ "upstream": { "enabled": false, "protocol": "http", "host": "", "port": 0, "username": "", "password": "" },
86
+ "proxiedHosts": ["api.deepseek.com"],
87
+ "knownHosts": ["api.deepseek.com", "registry.npmjs.org"]
88
+ }
89
+ ```
90
+
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
93
+ file while dsh runs — it is re-read live.
94
+
95
+ ## Install
96
+
97
+ **Prerequisites:** a DeepSeek Harness install with the `dsh` CLI, plus [pnpm](https://pnpm.io) (the `dsh plugin` command runs pnpm under the hood). This is an installable **bundle** — it is loaded by `dsh`, not imported as a library.
98
+
99
+ ### From npm (recommended)
100
+
101
+ The package is published to npm as `@karoc/dsh-proxy`:
102
+
103
+ ```sh
104
+ dsh plugin --profile web add @karoc/dsh-proxy
105
+ ```
106
+
107
+ This installs the prebuilt bundle and appends it to the `web` profile. Then **restart `dsh web`** and open **Settings → 代理 / Proxy**.
108
+
109
+ ### From git
110
+
111
+ ```sh
112
+ dsh plugin --profile web add github:karoc/dsh-proxy#<sha>
113
+ ```
114
+
115
+ A git install runs the package's `prepare` script to build the bundle. pnpm ≥ 10 requires allowlisting that build once — copy the exact package key pnpm prints into the profile's `pnpm-workspace.yaml` under `allowBuilds`, then re-run `add` (see `docs/user/develop/basic/publish.md` in the DSH repo).
116
+
117
+ ### Updating
118
+
119
+ Bump to the newest release with pnpm update (or re-add to pick up a newer git ref):
120
+
121
+ ```sh
122
+ dsh plugin --profile web update dsh-proxy
123
+ # or, if the dependency spec is pinned: dsh plugin --profile web add dsh-proxy
124
+ ```
125
+
126
+ Then **restart `dsh web`** so the new client bundle loads.
127
+
128
+ ### Removing
129
+
130
+ ```sh
131
+ dsh plugin --profile web remove dsh-proxy
132
+ ```
133
+
134
+ This removes both the dependency and its bundle layer from the `web` profile. Restart `dsh web` for the section to disappear.
135
+
136
+ ## Layout
137
+
138
+ ```
139
+ cordis.patch.yml # bundle layer: mounts the row that the client-modules
140
+ # service discovers (dsh.client manifest)
141
+ package.json # dsh.bundle (patch) + dsh.client (web) + exports["./client"]
142
+ tsdown.config.ts # self-contained build: node half + module-table client bundle
143
+ src/proxy-core.ts # forward proxy engine (HTTP+CONNECT, SOCKS5/HTTPS upstream,
144
+ # per-host routing, live config, test probe) — TS port of
145
+ # dsh-desktop's scripts/proxy.mjs
146
+ src/index.ts # host apply: starts the proxy, sets *PROXY env vars,
147
+ # registers /proxy/api (GET view / POST save·test·persist)
148
+ src/client/index.ts # client apply: register settings.section (id dsh-proxy)
149
+ src/client/ProxySection.tsx # the settings page (upstream card + host lists)
150
+ src/client/styles.ts # design-token styles (--dsw-alias-*) + injection
151
+ src/client/locales.ts # en/zh copy
152
+ scripts/proxy-core.spec.mjs # behavioral tests (13 scenarios, no network)
153
+ ```
154
+
155
+ ### The /proxy/api route
156
+
157
+ The host half serves the settings page over a same-origin HTTP route (the
158
+ built-in `/api` prefix is reserved for the gateway, so this uses `/proxy/api`):
159
+
160
+ - `GET /proxy/api` → `{ upstream, proxiedHosts, knownHosts, hosts, providers, port }`
161
+ - `POST /proxy/api` `{ op: 'save', upstream, proxiedHosts }` → sanitized + persisted config
162
+ - `POST /proxy/api` `{ op: 'test', upstream }` → `{ ok, detail }`
163
+ - `POST /proxy/api` `{ op: 'persist' }` → merge observed hosts into `knownHosts`
164
+
165
+ ## Build
166
+
167
+ ```sh
168
+ pnpm install
169
+ pnpm bundle # emits lib/index.js + lib/client.js
170
+ pnpm test # tsc --noEmit + proxy-core.spec.mjs (13 scenarios)
171
+ pnpm release:check # release gate: docs/changelog/tag/tree/build/registry must all pass
172
+ pnpm publish # runs the gate (prepack/prepublishOnly), then postpublish verifies the live release
173
+ ```
174
+
175
+ The bundle leaves the platform packages (`react`, `@deepseek-ai/cordis`,
176
+ `@deepseek-ai/dsh-client-*`) external — they resolve at runtime from the loader's
177
+ module table; everything else is inlined.
178
+
179
+ ## Notes / limitations
180
+
181
+ - **The proxy is an egress point for the process, not a per-request switch the
182
+ model reads.** It works by pointing the process's `HTTP(S)_PROXY` at itself.
183
+ Requests that dsh starts *after* this plugin's host `apply()` runs (any
184
+ subprocess, and undici fetches whose global dispatcher is still lazy) ride
185
+ the proxy. A dsh-internal fetch that already materialized its dispatcher
186
+ before plugin load may not route through it — a dsh restart is the documented
187
+ remedy (the desktop shell enforces the same boundary by setting env before
188
+ spawning dsh).
189
+ - `npm`/`pnpm` install/update traffic goes through the proxy like everything
190
+ else; toggling a host **related to install/update** takes effect on the next
191
+ install/update (already-in-flight operations keep their environment).
192
+ - **Section nav icon is shell-assigned, not plugin-assigned.** The built-in
193
+ `ui-settings-general` `SettingsRoot.tsx` `navIcon(id)` maps known ids and
194
+ falls back to a gear for every other id — including this section's
195
+ `dsh-proxy`. The `settings.section` registration has no icon field, so an
196
+ external plugin cannot set it without patching the shell. When DSH exposes a
197
+ per-section icon, use `IconGlobeOutline14` from `dsh-client-ui-primitives`
198
+ for this section.
199
+ - The desktop shell (`dsh-desktop`) keeps its own proxy and tray settings
200
+ window — this plugin is a standalone extraction, not a replacement. Both can
201
+ coexist (e.g. install `dsh-proxy` into the shell's profile to get the
202
+ in-dsh settings page too).
203
+
204
+ ## License
205
+
206
+ [MIT](LICENSE)
207
+
208
+ ## Contributing
209
+
210
+ See [CONTRIBUTING.md](CONTRIBUTING.md) (development + release checklist) and
211
+ [CHANGELOG.md](CHANGELOG.md) for version history.
package/README.zh.md ADDED
@@ -0,0 +1,160 @@
1
+ # dsh-proxy
2
+
3
+ [English](README.md) | 简体中文
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@karoc/dsh-proxy.svg)](https://www.npmjs.com/package/@karoc/dsh-proxy)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@karoc/dsh-proxy.svg)](https://www.npmjs.com/package/@karoc/dsh-proxy)
7
+ [![license MIT](https://img.shields.io/npm/l/@karoc/dsh-proxy.svg)](LICENSE)
8
+
9
+ 一个**外部** DeepSeek Harness 插件:为**模型提供方**提供带**按主机路由**的**正向代理**,并附带一个专职设置页。
10
+
11
+ 它在 dsh host 进程内启动一个 loopback 正向代理,把进程的出站流量指向它(`HTTP(S)_PROXY` + `NODE_USE_ENV_PROXY`),**只把你在设置页勾选的主机**转发到可选的上游代理(HTTP / HTTPS / SOCKS5,支持可选 Basic 认证),其余全部直连。模型提供方主机从你的 dsh `settings.yaml` 读取,流量中观测到的主机也会被收集——两者都以复选框形式出现。
12
+
13
+ 为什么要做成外部插件:内置设置页不覆盖出站代理,而给内置包加一页会在官方下次发布时被覆盖。本包作为可安装的 **bundle** 交付,从不触碰仓库源码,官方更新无法覆盖它。
14
+
15
+ 代理引擎与设置 UI 抽取自 [karoc/dsh-desktop](https://github.com/karoc/dsh-desktop)(行为保持一致),让纯浏览器版的 `dsh web` 也能获得桌面壳原有的模型提供方代理控制。
16
+
17
+ ## 它新增了什么
18
+
19
+ 一个放在内置 **Models** 与 **Model reasoning** 页之后的新设置项 **「代理 / Proxy」**,包含:
20
+
21
+ - **上游代理卡片**:启用开关、协议选择(HTTP / HTTPS / SOCKS5)、主机、端口、可选用户名/密码,以及**测试连接**按钮(验证上游是否会说对应协议);
22
+ - **模型提供方**列表——从你的 dsh `settings.yaml` 读取的主机(`llm-deepseek.baseURL`、`llm-pi-ai.providers.<n>.baseURL`、任意 `llm-*` 命名空间),有友好显示名时以显示名标注;
23
+ - **其它已观测主机**列表——代理在流量中见过的主机(含 `registry.npmjs.org` 等安装/更新流量),持久化进 `proxy.json` 以跨重启保留;
24
+ - **搜索框**——输入时按主机 / 名称过滤上面两个列表。
25
+
26
+ 勾选主机即让该主机走上游代理。保存会写入 `<DSH_HOME>/proxy.json`;运行中的代理**每个请求都重读该文件**,因此改动立即生效——无需重启 dsh。
27
+
28
+ ### 代理如何路由流量
29
+
30
+ ```
31
+ dsh host 进程
32
+ ├─ undici fetch(模型请求、联网搜索)──┐
33
+ ├─ npm / pnpm / git / 子 CLI ──────────┤ HTTP(S)_PROXY → loopback 代理
34
+ └─ 子代理 ─────────────────────────────┘ │
35
+ ▼
36
+ 127.0.0.1:<随机> forward proxy(裸 net/http)
37
+ │
38
+ ┌──────────────────────┴──────────────┐
39
+ ▼ ▼
40
+ 在 proxiedHosts 中的主机 其它一切
41
+ + 上游已启用 DIRECT(loopback 恒直连)
42
+ │
43
+ ▼
44
+ 上游代理(HTTP/HTTPS/SOCKS5,可选 Basic 认证)
45
+ ```
46
+
47
+ 安全规则(硬编码,不可配置):
48
+
49
+ - **loopback 目标恒直连**——绝不发给上游代理;
50
+ - 指向本代理自身的上游被当作**禁用**(防自环保护);
51
+ - 代理自身只用裸 `net`/`http`,所以永远不可能把自己出站连接再路由回自己。
52
+
53
+ ### 配置文件
54
+
55
+ ```
56
+ <DSH_HOME>/proxy.json # $DSH_HOME,默认 ~/.dsh
57
+ ```
58
+
59
+ ```json
60
+ {
61
+ "upstream": { "enabled": false, "protocol": "http", "host": "", "port": 0, "username": "", "password": "" },
62
+ "proxiedHosts": ["api.deepseek.com"],
63
+ "knownHosts": ["api.deepseek.com", "registry.npmjs.org"]
64
+ }
65
+ ```
66
+
67
+ `knownHosts` 由插件写入(观测到的流量);`upstream` 与 `proxiedHosts` 由设置页写入。你也可以在 dsh 运行中手改该文件——它是实时重读的。
68
+
69
+ ## 安装
70
+
71
+ **前置要求:** 已安装带 `dsh` CLI 的 DeepSeek Harness,以及 [pnpm](https://pnpm.io)(`dsh plugin` 命令底层调用 pnpm)。这是一个可安装的 **bundle**——由 `dsh` 加载,不是当作库 import。
72
+
73
+ ### 从 npm 安装(推荐)
74
+
75
+ 包已发布到 npm,名为 `@karoc/dsh-proxy`:
76
+
77
+ ```sh
78
+ dsh plugin --profile web add @karoc/dsh-proxy
79
+ ```
80
+
81
+ 安装预构建 bundle 并追加到 `web` profile。然后**重启 `dsh web`**,打开 **设置 → 代理 / Proxy**。
82
+
83
+ ### 从 git 安装
84
+
85
+ ```sh
86
+ dsh plugin --profile web add github:karoc/dsh-proxy#<sha>
87
+ ```
88
+
89
+ git 安装会运行包的 `prepare` 脚本构建 bundle。pnpm ≥ 10 需要先放行该构建——把 pnpm 打印的包 key 复制到 profile 的 `pnpm-workspace.yaml` 的 `allowBuilds` 下,再重跑 `add`(见 DSH 仓库 `docs/user/develop/basic/publish.md`)。
90
+
91
+ ### 更新
92
+
93
+ 用 pnpm update 升到最新版(或重 `add` 以拉取更新的 git ref):
94
+
95
+ ```sh
96
+ dsh plugin --profile web update dsh-proxy
97
+ # 或,若依赖规格被 pin 住:dsh plugin --profile web add dsh-proxy
98
+ ```
99
+
100
+ 然后**重启 `dsh web`** 以加载新客户端 bundle。
101
+
102
+ ### 卸载
103
+
104
+ ```sh
105
+ dsh plugin --profile web remove dsh-proxy
106
+ ```
107
+
108
+ 同时从 `web` profile 移除依赖与 bundle 层。重启 `dsh web` 后该设置项消失。
109
+
110
+ ## 目录结构
111
+
112
+ ```
113
+ cordis.patch.yml # bundle 层:挂一行让 client-modules 服务发现(dsh.client manifest)
114
+ package.json # dsh.bundle (patch) + dsh.client (web) + exports["./client"]
115
+ tsdown.config.ts # 自包含构建:node 半区 + 模块表客户端 bundle
116
+ src/proxy-core.ts # 前向代理引擎(HTTP+CONNECT、SOCKS5/HTTPS 上游、按主机路由、
117
+ # 实时配置、测试探针)—— dsh-desktop scripts/proxy.mjs 的 TS 移植
118
+ src/index.ts # host apply:起代理、设 *PROXY 环境变量、注册 /proxy/api(GET 视图 / POST save·test·persist)
119
+ src/client/index.ts # client apply:注册 settings.section(id dsh-proxy)
120
+ src/client/ProxySection.tsx # 设置页(上游卡片 + 主机列表)
121
+ src/client/styles.ts # design-token 样式(--dsw-alias-*)+ 注入
122
+ src/client/locales.ts # en/zh 文案
123
+ scripts/proxy-core.spec.mjs # 行为测试(13 场景,无外部网络)
124
+ ```
125
+
126
+ ### /proxy/api 路由
127
+
128
+ host 半区用同源 HTTP 路由为设置页供数(内置 `/api` 前缀被 gateway 占用,故用 `/proxy/api`):
129
+
130
+ - `GET /proxy/api` → `{ upstream, proxiedHosts, knownHosts, hosts, providers, port }`
131
+ - `POST /proxy/api` `{ op: 'save', upstream, proxiedHosts }` → 清洗后持久化的配置
132
+ - `POST /proxy/api` `{ op: 'test', upstream }` → `{ ok, detail }`
133
+ - `POST /proxy/api` `{ op: 'persist' }` → 把观测主机并入 `knownHosts`
134
+
135
+ ## 构建
136
+
137
+ ```sh
138
+ pnpm install
139
+ pnpm bundle # 产出 lib/index.js + lib/client.js
140
+ pnpm test # tsc --noEmit + proxy-core.spec.mjs(13 场景)
141
+ pnpm release:check # 发布门禁:文档/changelog/tag/工作区/构建/registry 全过
142
+ pnpm publish # 跑门禁(prepack/prepublishOnly),随后 postpublish 验证线上发布
143
+ ```
144
+
145
+ bundle 把平台包(`react`、`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client-*`)保持 external——它们在运行时从 loader 的模块表解析;其余全部内联。
146
+
147
+ ## 注意事项 / 限制
148
+
149
+ - **代理是进程级出站点,不是模型逐请求读的开关。** 它通过把进程的 `HTTP(S)_PROXY` 指向自身来实现。本插件 host `apply()` 之后 dsh 发起的请求(任何子进程、以及 global dispatcher 仍是惰性的 undici fetch)会走代理。若某个 dsh 内部 fetch 在插件加载前就已实体化了 dispatcher,可能不会走它——文档化的解法是重启 dsh(桌面壳通过 spawn dsh 前设环境变量来施加同样的边界)。
150
+ - `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-desktop`)保留自己的代理与托盘设置窗口——本插件是独立抽取,不是替代品。两者可共存(例如把 `dsh-proxy` 装进壳的 profile,同时获得 dsh 内设置页)。
153
+
154
+ ## License
155
+
156
+ [MIT](LICENSE)
157
+
158
+ ## 参与贡献
159
+
160
+ 见 [CONTRIBUTING.md](CONTRIBUTING.md)(开发 + 发布清单)与 [CHANGELOG.md](CHANGELOG.md)(版本历史)。
@@ -0,0 +1,8 @@
1
+ # Bundle layer applied when a profile lists this package. The row names the
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
4
+ # loopback forward proxy and registers /proxy/api; the client half registers
5
+ # the settings.section settings page.
6
+ - insert:
7
+ - id: dsh-proxy
8
+ name: dsh-proxy