@yanglaofish/dsh-proxy-pro 1.0.0 → 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 +3 -3
- package/docs/ACCEPTANCE.md +3 -3
- package/docs/LESSONS.md +678 -551
- package/docs/SPEC.md +3 -3
- package/lib/client.js +7 -1
- package/lib/index.js +10 -13
- package/lib/proxy-core.js +32 -0
- package/package.json +1 -1
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/client.js
CHANGED
|
@@ -18,9 +18,15 @@
|
|
|
18
18
|
* This hand-written module mirrors the loader format the in-repo client
|
|
19
19
|
* bundles emit (window.__ModuleLoader__.load with a CommonJS factory): react
|
|
20
20
|
* is required through the app's module table, no JSX, no bundler, no TS.
|
|
21
|
+
*
|
|
22
|
+
* The registration id MUST be the PACKAGE name: dsh-client-modules stores the
|
|
23
|
+
* factory under stripClientSuffix(id) and later looks it up by the graph row
|
|
24
|
+
* id (= package name). Registering "dsh-proxy-pro/client" (the pre-scope name)
|
|
25
|
+
* throws 'bundle ... loaded without registering "@yanglaofish/dsh-proxy-pro"
|
|
26
|
+
* via __ModuleLoader__.load' (see docs/LESSONS.md §18).
|
|
21
27
|
*/
|
|
22
28
|
window.__ModuleLoader__.load({
|
|
23
|
-
id: "dsh-proxy-pro
|
|
29
|
+
id: "@yanglaofish/dsh-proxy-pro",
|
|
24
30
|
factory: (require) => {
|
|
25
31
|
var module = { exports: {} };
|
|
26
32
|
var exports = module.exports;
|
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",
|