@yanglaofish/dsh-proxy-pro 1.0.1 → 1.0.2
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 +2 -2
- package/docs/ACCEPTANCE.md +3 -3
- package/docs/LESSONS.md +50 -8
- package/docs/SPEC.md +3 -3
- package/lib/index.js +10 -13
- package/lib/proxy-core.js +32 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
```sh
|
|
23
23
|
dsh plugin --profile web add @yanglaofish/dsh-proxy-pro
|
|
24
|
-
dsh plugin --profile
|
|
24
|
+
dsh plugin --profile test add @yanglaofish/dsh-proxy-pro
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
**方式 B:GitHub 源安装**
|
|
@@ -70,7 +70,7 @@ proxy:
|
|
|
70
70
|
|
|
71
71
|
```sh
|
|
72
72
|
dsh plugin --profile web remove @yanglaofish/dsh-proxy-pro
|
|
73
|
-
dsh plugin --profile
|
|
73
|
+
dsh plugin --profile test remove @yanglaofish/dsh-proxy-pro
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
卸载即恢复原始路由行为,不留残留。
|
package/docs/ACCEPTANCE.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
## 0. 前置(已就绪,无需操作)
|
|
8
8
|
|
|
9
9
|
- [x] 单测 16/16 通过、语法 OK、git 干净(head = afb96b4)
|
|
10
|
-
- [x] web /
|
|
10
|
+
- [x] web / test 两 profile 已装入 `dsh-proxy-pro`(node_modules 为 Junction 软链)
|
|
11
11
|
- [x] `dsh --dump-config` 预检:两 profile 组合树均含 `- id: dsh-proxy-pro` 行
|
|
12
12
|
- [x] settings.yaml 的 `proxy.enabled: true` → 重启后插件**启动即接管**系统代理(预期行为)
|
|
13
13
|
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
`<profile>/cordis.patch.yml` 追加一行 `- id: proxy, disabled: true`(禁旧
|
|
53
53
|
dsh-plugin-proxy)→ 重启 → 复测 A1-A6。回滚 = 删该行,settings.yaml 全程不动。
|
|
54
54
|
|
|
55
|
-
## 6.
|
|
55
|
+
## 6. test 复用
|
|
56
56
|
|
|
57
|
-
用 `dsh --profile
|
|
57
|
+
用 `dsh --profile test` 启动 → 重复 §2-§3(A10)。
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
package/docs/LESSONS.md
CHANGED
|
@@ -106,7 +106,7 @@ const envLike = {
|
|
|
106
106
|
## 5. 为什么新插件放独立目录(避开 node_modules)
|
|
107
107
|
|
|
108
108
|
```
|
|
109
|
-
C:\Users\...\profiles\[web|
|
|
109
|
+
C:\Users\...\profiles\[web|test]\node_modules\dsh-plugin-proxy\package.json
|
|
110
110
|
LinkType = HardLink → C:\Users\...\pnpm\store\v11\files\...(pnpm 全局 store)
|
|
111
111
|
```
|
|
112
112
|
|
|
@@ -167,7 +167,7 @@ window.__ModuleLoader__.load({
|
|
|
167
167
|
});
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
-
技能库里的 `dsh-skill-manager@4.3.3`(
|
|
170
|
+
技能库里的 `dsh-skill-manager@4.3.3`(test profile 已装)是**现代写法参照模板**:
|
|
171
171
|
|
|
172
172
|
```js
|
|
173
173
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
@@ -270,7 +270,7 @@ POST /dsh-proxy-pro/api/toggle {"enabled":bool} → 更新 settings + 立即 syn
|
|
|
270
270
|
| Web GUI | http://127.0.0.1:43120(NodeService 21872 监听) |
|
|
271
271
|
| 系统代理 | proxyhk.huawei.com:8080(NTLM SWG,Proxy ON) |
|
|
272
272
|
| NO_PROXY | 大列表:`*.huawei.com` 等内网全量 + localhost/127.0.0.1/::1 |
|
|
273
|
-
| profile | web /
|
|
273
|
+
| profile | web / test(两份,插件需都装) |
|
|
274
274
|
| settings.yaml | proxy: {enabled: true, mode: system, customUrl: http://127.0.0.1:7890} |
|
|
275
275
|
| 验证基线 | github → PROXIED;deepseek.com → DIRECT;web_fetch PR#3574 → 200 |
|
|
276
276
|
| loadLayeredEnv | process.env + cwd/.env + ~/.dsh/.env(仅 undefined 时写入) |
|
|
@@ -280,8 +280,8 @@ POST /dsh-proxy-pro/api/toggle {"enabled":bool} → 更新 settings + 立即 syn
|
|
|
280
280
|
- 打包源码:`C:\Users\w00958282\AppData\Local\Programs\DSH Desktop\resources\app\node_modules\@deepseek-ai\...`
|
|
281
281
|
(dsh-web-fetch-http / dsh-http-proxy / dsh-app-boot / dsh-web / dsh-tool-web /
|
|
282
282
|
dsh-client-ui-conversation / dsh-client-ui-chat / dsh-client-modules / dsh-settings)
|
|
283
|
-
- 原插件(已打补丁 + .bak):`~/.dsh/profiles\{web,
|
|
284
|
-
- 参照模板:`~/.dsh/profiles\
|
|
283
|
+
- 原插件(已打补丁 + .bak):`~/.dsh/profiles\{web,test}\node_modules\dsh-plugin-proxy\lib\`
|
|
284
|
+
- 参照模板:`~/.dsh/profiles\test\node_modules\dsh-skill-manager\lib\`(index.js + client.js)
|
|
285
285
|
- 本插件:`~/.dsh/plugins/dsh-proxy-pro/`(git 仓库,发布形态,见 §12)
|
|
286
286
|
- 环境事实:`~/.dsh/.env`(已删,纯冗余证据);`settings.yaml`、`cordis.patch.yml`、`cordis.yml`
|
|
287
287
|
|
|
@@ -390,7 +390,7 @@ POST /dsh-proxy-pro/api/toggle {"enabled":bool} → 更新 settings + 立即 syn
|
|
|
390
390
|
——这就是 8ce5e21 修的那个 schema bug(跑在修复前)。
|
|
391
391
|
- 决定性证据:`profiles/web/package.json` LastWriteTime = **11:50:28**,
|
|
392
392
|
恰好在 desktop 的 `recovery plugin uninstall failed`(11:50:09 error.log)
|
|
393
|
-
窗口内;对读 `profiles/
|
|
393
|
+
窗口内;对读 `profiles/test/package.json` 完好(11:35 未动)。
|
|
394
394
|
- **根因:桌面版启动时对解析失败的插件执行 recovery 卸载**
|
|
395
395
|
(`dsh plugin --profile web remove @yanglaofish/dsh-skill-manager`),
|
|
396
396
|
该操作重写了 web profile 的 package.json,**把我们手工加的两处
|
|
@@ -401,7 +401,7 @@ POST /dsh-proxy-pro/api/toggle {"enabled":bool} → 更新 settings + 立即 syn
|
|
|
401
401
|
|
|
402
402
|
### 14.2 修复与预防
|
|
403
403
|
|
|
404
|
-
- 修复:对照
|
|
404
|
+
- 修复:对照 test 的形态把 `dependencies["dsh-proxy-pro"] =
|
|
405
405
|
link:...` 与 `dsh.profile.bundles` 里的 `"dsh-proxy-pro"` 加回
|
|
406
406
|
web/package.json;`dsh --profile web --dump-config` 预检两行都在、exit=0。
|
|
407
407
|
- 预防:每次用户重启前**先读 profile 的 package.json** 确认条目在(read
|
|
@@ -633,4 +633,46 @@ git log --oneline # 历史完整(refs/heads/master 里的 tip 仍在)
|
|
|
633
633
|
前提:`.git.broken/refs/heads/<branch>` 仍指向有效 commit、objects 完整(松散或 pack)。
|
|
634
634
|
若 refs 也没了,就只能等网络恢复后 `git fetch` 远端历史再 `git reset --mixed origin/<branch>`。
|
|
635
635
|
本例 `git fetch` 失败于 `Could not resolve proxy: proxyhk.huawei.com`——git 的 `http.proxy`
|
|
636
|
-
指向公司代理,离开公司网络(或代理关闭)时 GitHub 不可达;此时**不要** push/force push。
|
|
636
|
+
指向公司代理,离开公司网络(或代理关闭)时 GitHub 不可达;此时**不要** push/force push。
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## 22. 强制通道探测必须用同一个 undici 副本(2026-09-17)
|
|
641
|
+
|
|
642
|
+
**症状**:设置页「代理配置」的逐 URL 诊断,无论勾不勾「使用代理」都返回 `fail`
|
|
643
|
+
(面板只显示 `fetch failed`);而 `curl`、独立 node 进程、policy 通道都正常。
|
|
644
|
+
|
|
645
|
+
**根因(实测)**:`forceChannelProbe` 用 `import('undici')`(app 的 undici 包)造
|
|
646
|
+
`Agent`/`ProxyAgent`,却把它交给**全局 `fetch`**(Node 内置的另一份 undici)当 `dispatcher`:
|
|
647
|
+
|
|
648
|
+
```
|
|
649
|
+
CONTROL 内置 fetch(无 dispatcher) -> OK 401
|
|
650
|
+
A 内置 fetch + 外部 undici Agent -> ERR fetch failed | UND_ERR_INVALID_ARG: invalid onRequestStart method
|
|
651
|
+
B 外部 undici 的 fetch + 同一 Agent -> OK 401
|
|
652
|
+
D 内置 fetch + 外部 ProxyAgent -> ERR fetch failed | UND_ERR_INVALID_ARG
|
|
653
|
+
E 外部 undici 的 fetch + 同一 ProxyAgent -> OK 401
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
**规矩**:凡是要传 `dispatcher` 的请求,**请求与被传的 dispatcher 必须来自同一次
|
|
657
|
+
`import('undici')`**(即 `und.fetch(url, { dispatcher })`)。这正是 §3 的老教训
|
|
658
|
+
(mixed undici instances → UND_ERR_INVALID_ARG)——插件主干一直靠"委托
|
|
659
|
+
`installProxyFromEnvironment`、自己绝不碰 undici"规避它,而强制通道绕过了这条原则。
|
|
660
|
+
|
|
661
|
+
**顺带修掉的诊断缺陷**:`classifyTargetFailure` 原本只回 `error.message`("fetch failed"),
|
|
662
|
+
丢掉了 `error.cause` 里的 `code`。现在优先输出 cause code,并把 `UND_ERR_INVALID_ARG`
|
|
663
|
+
单独归类(`kind: 'dispatcher'`,提示 "mixed undici copies")。
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## 23. 与 @deepseek-ai/dsh-http-proxy 插件行共存会抢全局 dispatcher
|
|
668
|
+
|
|
669
|
+
同一天的另一条线索:把 `@deepseek-ai/dsh-http-proxy` **作为插件行同时启用**时,policy 通道
|
|
670
|
+
探测也出现过 `fetch failed`;从 profile 的 bundles/依赖里移除该行后恢复正常。
|
|
671
|
+
|
|
672
|
+
原因:两个插件都调同一个库的 `installProxyFromEnvironment()`,各自
|
|
673
|
+
`setGlobalDispatcher()`;谁先卸载,谁的 disposer 就把"它安装前"的 dispatcher
|
|
674
|
+
(`previousDispatcher`)恢复回去,全局 dispatcher 于是可能指向已关闭/陈旧实例。
|
|
675
|
+
|
|
676
|
+
**结论**:本插件与 `dsh-http-proxy` 插件行**二选一**。库本身必须保留——插件 import 它的
|
|
677
|
+
`installProxyFromEnvironment` / `proxyRouteFor`,而 DSH 的 web_fetch 通道读的正是同一模块的
|
|
678
|
+
module-level policy(§2-§3)。卸载"插件行"是安全的,删除"包"会让本插件 import 直接失败。
|
package/docs/SPEC.md
CHANGED
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
| NFR-3 | 可回滚 | 切回原插件只需恢复 cordis.patch.yml 一行 + 保留 `.bak` |
|
|
66
66
|
| NFR-4 | 单一 dispatcher 主人 | 与 dsh-plugin-proxy 二选一启用,不同时 |
|
|
67
67
|
| NFR-5 | 无新增第三方依赖 | 纯 ESM + 现有 DSH 包;逻辑层(proxy-core.js)零依赖可单测 |
|
|
68
|
-
| NFR-6 | 双 profile 部署 | web +
|
|
68
|
+
| NFR-6 | 双 profile 部署 | web + test 都要装 |
|
|
69
69
|
|
|
70
70
|
---
|
|
71
71
|
|
|
@@ -321,7 +321,7 @@ systemPollMs: 30000
|
|
|
321
321
|
3. 重启 DSH → `proxy_status` 确认 active=true、url=proxyhk → 手动 web_fetch
|
|
322
322
|
一个被墙 URL(如 github)确认 200。
|
|
323
323
|
4. 验证 proxy_test github=PROXIED / deepseek=DIRECT。
|
|
324
|
-
5. 再对
|
|
324
|
+
5. 再对 test profile 做同样操作,重复验证。
|
|
325
325
|
6. 回滚 = 恢复原插件行 enabled(settings.yaml 没动过,天然安全),或
|
|
326
326
|
`dsh plugin --profile web remove @yanglaofish/dsh-proxy-pro`。
|
|
327
327
|
|
|
@@ -378,4 +378,4 @@ _运行时快照(2026-09-17,⚠️ 尚未重启):`/dsh-proxy-pro/api/sta
|
|
|
378
378
|
5. ✅ 已装入两 profile + `--dump-config` 预检通过(两个组合树都含 dsh-proxy-pro 行);
|
|
379
379
|
⏳ 待用户重启(当前**尚未重启**,403 为应用安全门、非插件响应)
|
|
380
380
|
6. ⏳ GUI 确认 A1/A2(头部胶囊 + 设置页「代理管理」)→ 运行时验收 A3-A9
|
|
381
|
-
→
|
|
381
|
+
→ test A10 → 发布收尾
|
package/lib/index.js
CHANGED
|
@@ -31,6 +31,7 @@ import { installProxyFromEnvironment, proxyRouteFor } from '@deepseek-ai/dsh-htt
|
|
|
31
31
|
import { execFile } from 'node:child_process'
|
|
32
32
|
import { promisify } from 'node:util'
|
|
33
33
|
import {
|
|
34
|
+
classifyTargetFailure,
|
|
34
35
|
keepaliveHint,
|
|
35
36
|
makeSystemProxyReader,
|
|
36
37
|
normalizeProxyUrl,
|
|
@@ -149,18 +150,8 @@ function renderStatusText(snapshot) {
|
|
|
149
150
|
].join('\n')
|
|
150
151
|
}
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const cause = error?.cause
|
|
155
|
-
const code = cause?.code ?? error?.code ?? ''
|
|
156
|
-
if (code === 'EACCES' || code === 'ENETUNREACH' || code === 'EHOSTUNREACH' || code === 'ENOTFOUND' || code === 'ECONNREFUSED' || code === 'ETIMEDOUT') {
|
|
157
|
-
return { ok: false, kind: 'unreachable', short: `cannot reach directly (${code}) — needs a proxy route` }
|
|
158
|
-
}
|
|
159
|
-
if (/407|NTLM|authentication required/i.test(String(error?.message ?? ''))) {
|
|
160
|
-
return { ok: false, kind: 'auth', short: 'the proxy asks for NTLM authentication' }
|
|
161
|
-
}
|
|
162
|
-
return { ok: false, kind: 'other', short: (error?.message ?? String(error)).slice(0, 120) }
|
|
163
|
-
}
|
|
153
|
+
// classifyTargetFailure lives in proxy-core.js (pure logic, unit-tested) so the
|
|
154
|
+
// panel's verdict can name the undici cause instead of a bare "fetch failed".
|
|
164
155
|
|
|
165
156
|
// Forced-channel probing uses undici directly. Load lazily and never let a
|
|
166
157
|
// resolution failure block boot (LESSONS §14: boot must not depend on a new
|
|
@@ -195,7 +186,13 @@ async function forceChannelProbe(url, channel, proxyUrl) {
|
|
|
195
186
|
const controller = new AbortController()
|
|
196
187
|
const timer = setTimeout(() => controller.abort(), 6000)
|
|
197
188
|
try {
|
|
198
|
-
|
|
189
|
+
// MUST issue the request with THIS undici copy. The host runs two copies —
|
|
190
|
+
// Node's built-in `fetch` and the app's `undici` package — and a dispatcher
|
|
191
|
+
// built by one handed to the other rejects with UND_ERR_INVALID_ARG
|
|
192
|
+
// ("invalid onRequestStart method"); the panel rendered that as a bare
|
|
193
|
+
// "fetch failed" for BOTH forced channels (LESSONS §22). und.fetch and the
|
|
194
|
+
// Agent/ProxyAgent above come from the same import, so they always agree.
|
|
195
|
+
const res = await und.fetch(url, {
|
|
199
196
|
method: 'HEAD',
|
|
200
197
|
redirect: 'manual',
|
|
201
198
|
signal: controller.signal,
|
package/lib/proxy-core.js
CHANGED
|
@@ -266,3 +266,35 @@ export function keepaliveHint(message = '', proxyUrl = '') {
|
|
|
266
266
|
const example = typeof proxyUrl === 'string' && proxyUrl !== '' ? proxyUrl : 'http://localhost:7890'
|
|
267
267
|
return `This proxy asks for NTLM authentication, which undici cannot negotiate by itself. A recent curl/browser connection to the same proxy keeps an authenticated window open — run a \`curl\` through the proxy (e.g. \`curl -x ${example} -I https://github.com\`) once, then retry.`
|
|
268
268
|
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Turn a fetch rejection into a short, actionable verdict for the panel and the
|
|
272
|
+
* `proxy_test` tool.
|
|
273
|
+
*
|
|
274
|
+
* A bare `fetch failed` hides the real cause. The host runs TWO undici copies —
|
|
275
|
+
* Node's built-in `fetch` and the app's `undici` package — and a dispatcher
|
|
276
|
+
* built by one handed to the other rejects with UND_ERR_INVALID_ARG ("invalid
|
|
277
|
+
* onRequestStart method"); that is exactly how the forced-channel diagnostic
|
|
278
|
+
* failed (LESSONS §22). Surface the cause code so the next report names it.
|
|
279
|
+
*
|
|
280
|
+
* @param error - the rejection from `fetch`.
|
|
281
|
+
* @returns `{ ok: false, kind, short }` with a cause-aware message.
|
|
282
|
+
*/
|
|
283
|
+
export function classifyTargetFailure(error) {
|
|
284
|
+
const cause = error?.cause
|
|
285
|
+
const code = cause?.code ?? error?.code ?? ''
|
|
286
|
+
if (code === 'EACCES' || code === 'ENETUNREACH' || code === 'EHOSTUNREACH' || code === 'ENOTFOUND' || code === 'ECONNREFUSED' || code === 'ETIMEDOUT') {
|
|
287
|
+
return { ok: false, kind: 'unreachable', short: `cannot reach directly (${code}) — needs a proxy route` }
|
|
288
|
+
}
|
|
289
|
+
if (code === 'UND_ERR_INVALID_ARG' || /invalid onRequestStart/i.test(String(cause?.message ?? ''))) {
|
|
290
|
+
return { ok: false, kind: 'dispatcher', short: `dispatcher rejected by this undici instance (${code || 'UND_ERR_INVALID_ARG'}) — mixed undici copies` }
|
|
291
|
+
}
|
|
292
|
+
if (/407|NTLM|authentication required/i.test(String(error?.message ?? ''))) {
|
|
293
|
+
return { ok: false, kind: 'auth', short: 'the proxy asks for NTLM authentication' }
|
|
294
|
+
}
|
|
295
|
+
const detail = cause?.code
|
|
296
|
+
? `${cause.code}${cause?.message ? `: ${String(cause.message).slice(0, 90)}` : ''}`
|
|
297
|
+
: (cause?.message ?? '')
|
|
298
|
+
const base = String(error?.message ?? error)
|
|
299
|
+
return { ok: false, kind: 'other', short: (detail ? `${base} (${detail})` : base).slice(0, 180) }
|
|
300
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yanglaofish/dsh-proxy-pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "DSH proxy manager: route model requests, web fetches and spawned tools through the Windows system proxy or a custom address; per-URL route diagnosis, one-click toggle from the conversation header, a dedicated Settings page, and keepalive-aware 407 hints.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|