@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 +31 -0
- package/CONTRIBUTING.md +97 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/README.zh.md +160 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +645 -0
- package/lib/index.js +960 -0
- package/package.json +62 -0
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.
|
package/CONTRIBUTING.md
ADDED
|
@@ -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
|
+
[](https://www.npmjs.com/package/@karoc/dsh-proxy)
|
|
6
|
+
[](https://www.npmjs.com/package/@karoc/dsh-proxy)
|
|
7
|
+
[](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
|
+
[](https://www.npmjs.com/package/@karoc/dsh-proxy)
|
|
6
|
+
[](https://www.npmjs.com/package/@karoc/dsh-proxy)
|
|
7
|
+
[](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)(版本历史)。
|
package/cordis.patch.yml
ADDED
|
@@ -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
|