@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/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 + obsidian-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. 再对 obsidian-web profile 做同样操作,重复验证。
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
- → obsidian-web A10 → 发布收尾
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/client",
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
- /** Isolate NetworkError-ish codes that mean "could not connect at all". */
153
- function classifyTargetFailure(error) {
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
- const res = await fetch(url, {
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.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",