@yanglaofish/dsh-proxy-pro 1.0.5 → 1.0.7

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/LESSONS.md CHANGED
@@ -750,4 +750,34 @@ scope.watch(() => { void requestSync().catch(() => {}) })
750
750
 
751
751
  **教训**:① ESM 顶层 import 命中断言是**加载期**错误——一次错误的 import 能杀死整个进程,所以
752
752
  对外部包的导出要查运行环境的实际版本,而不是凭记忆;② 能从服务拿到的(settings 是 inject 服务)
753
- 就不要 import 包;③ 「桌面版」与「全局 CLI 版」是两套依赖树,一个功能必须两端都能加载。
753
+ 就不要 import 包;③ 「桌面版」与「全局 CLI 版」是两套依赖树,一个功能必须两端都能加载。
754
+
755
+ ---
756
+
757
+ ## 27. HEAD 404 ≠ 路径不存在:探测必须 GET 复测(2026-09-18)
758
+
759
+ **症状**:面板测 `https://dashscope.aliyuncs.com/compatible-mode/v1` 和 `.../v1/models`
760
+ 一律显示「降级 — HTTP 404 — 路径不存在」;连测几个厂商的 API 都是 404,用户开始怀疑
761
+ 「是不是网络不可达」。
762
+
763
+ **真相**(实测对照):
764
+
765
+ | 请求 | DashScope 应答 |
766
+ | --- | --- |
767
+ | GET `/compatible-mode/v1/models` | **401**(端点存在,只缺 API Key) |
768
+ | GET `/compatible-mode/v1/chat/completions` | **400**(端点存在,请求不合法) |
769
+ | GET `/` | 404 |
770
+ | HEAD `/compatible-mode/v1/models` | **404** ← 同一个端点! |
771
+
772
+ 网关不给 HEAD 注册路由,对 HEAD 一律回 404;而 404 被探测当成「路径不存在」,健康的
773
+ API 端点就这样被误判。**404 从来不是「网络不可达」的证据**——它恰恰证明拿到了 HTTP 应答
774
+ (真正的不可达是 ENOTFOUND/ECONNREFUSED/ETIMEDOUT/代理 407)。
775
+
776
+ **改法**(1.0.7):回退条件从 405/501 扩到 **404/405/501**,提为纯函数
777
+ `headNeedsGetRetry(status)`(proxy-core,可单测)。GET 复测带 `Range: bytes=0-0` 只取首
778
+ 字节,代价极小;note 写明「HEAD 返回 X,GET 得到 Y — 以 GET 为准」。404 的 why/fix 改为
779
+ 「HEAD 与 GET 都 404」并引导换真实完整端点复测。
780
+
781
+ **教训**:① 探测方法本身就是误判来源——HEAD「轻量」不等于「等价」;② 把 404 当「不存在」
782
+ 之前先问「服务器认不认这个方法」;③ 状态码是二手的,链路层结论只能由「是否拿到 HTTP
783
+ 应答」给出。
package/docs/SPEC.md CHANGED
@@ -199,7 +199,9 @@ installProxyFromEnvironment 的 none-branch 完成:恢复首次安装前的 en
199
199
 
200
200
  `proxy_test` 内部:`proxyRouteFor(parsed)` 判路由名(try/catch,不可用时回退快照
201
201
  推导)→ NO_PROXY 列表命中判断 → `fetch(url, {method:'HEAD', redirect:'manual'})`
202
- 实测 6s 超时(AbortController)→ `classifyTargetFailure` 归类
202
+ 实测 6s 超时(AbortController);HEAD 状态为 404/405/501 时按 `headNeedsGetRetry`
203
+ 用 GET(`Range: bytes=0-0`)复测并以 GET 为准(LESSONS §27:网关常不给 HEAD 注册
204
+ 路由,HEAD 404 ≠ 路径不存在)→ `classifyTargetFailure` 归类
203
205
  (EACCES/ENETUNREACH/EHOSTUNREACH → "needs a proxy route";407/NTLM → auth)。
204
206
 
205
207
  ### 4.7 系统提示段落
package/lib/client.js CHANGED
@@ -71,8 +71,9 @@ window.__ModuleLoader__.load({
71
71
  ".dsxprx-diagbtn{flex:none;padding:6px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-specific-tip);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;cursor:pointer}",
72
72
  ".dsxprx-diagbtn:disabled{opacity:.5;cursor:not-allowed}",
73
73
  ".dsxprx-diagout{display:flex;flex-direction:column;gap:3px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-specific-tip);font-size:12px;line-height:17px;color:var(--dsw-alias-label-primary)}",
74
- ".dsxprx-diagwhy{font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption)}",
75
- ".dsxprx-diagfix{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}",
74
+ ".dsxprx-diagwhy{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary)}",
75
+ ".dsxprx-diagfix{font-size:12px;line-height:17px;color:var(--dsw-alias-label-primary)}",
76
+ ".dsxprx-diagnote{font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption)}",
76
77
  ".dsxprx-title{font-size:16px;font-weight:700;line-height:22px;color:var(--dsw-alias-label-primary)}",
77
78
  ".dsxprx-diagcheck{flex:none;display:flex;align-items:center;gap:5px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap}",
78
79
  ".dsxprx-diagcheck input{width:14px;height:14px;accent-color:#4d6bfe;margin:0}",
@@ -421,7 +422,7 @@ window.__ModuleLoader__.load({
421
422
  react.createElement("span", { key: "p" }, "连通探测: " + probeHeadline(diag.probe)),
422
423
  diag.probe && diag.probe.why ? react.createElement("span", { key: "w", className: "dsxprx-diagwhy" }, "可能原因: " + diag.probe.why) : null,
423
424
  diag.probe && diag.probe.fix ? react.createElement("span", { key: "f", className: "dsxprx-diagfix" }, "挽救措施: " + diag.probe.fix) : null,
424
- diag.probe && diag.probe.note ? react.createElement("span", { key: "n", className: "dsxprx-diagwhy" }, "备注: " + diag.probe.note) : null,
425
+ diag.probe && diag.probe.note ? react.createElement("span", { key: "n", className: "dsxprx-diagnote" }, "备注: " + diag.probe.note) : null,
425
426
  diag.hint ? react.createElement("span", { key: "h" }, "提示: " + diag.hint) : null,
426
427
  ]
427
428
  )
package/lib/index.js CHANGED
@@ -32,6 +32,7 @@ import { promisify } from 'node:util'
32
32
  import {
33
33
  classifyProbeStatus,
34
34
  classifyTargetFailure,
35
+ headNeedsGetRetry,
35
36
  keepaliveHint,
36
37
  makeSystemProxyReader,
37
38
  normalizeProxyUrl,
@@ -171,10 +172,12 @@ const PROBE_UA = 'dsh-proxy-pro/0.1'
171
172
  const PROBE_TIMEOUT_MS = 6000
172
173
 
173
174
  /**
174
- * 一个探测尝试:先 HEAD,端点不支持 HEAD(405/501)时再用 GET 复测——
175
- * 很多 API 只实现 GET,只用 HEAD 会冤枉一个可用主机。`request` 是调用方
176
- * 的 fetch:两条通道现在都用 app undici 的 `und.fetch` 配同副本 dispatcher
177
- * (LESSONS §22/§25 —— 绝不要把两份副本混在一起)。
175
+ * 一个探测尝试:先 HEAD,HEAD 结论不可信(404/405/501)时再用 GET 复测——
176
+ * 405/501 是「不支持该方法」,404 是网关不给 HEAD 注册路由时的伪装
177
+ * (LESSONS §27:DashScope `HEAD /models` → 404,同路径 GET → 401),只用
178
+ * HEAD 会把健康端点误判成「路径不存在」。`request` 是调用方的 fetch:两条
179
+ * 通道都用 app undici 的 `und.fetch` 配同副本 dispatcher(LESSONS §22/§25
180
+ * —— 绝不要把两份副本混在一起)。
178
181
  *
179
182
  * @returns classifyProbeStatus / classifyTargetFailure 的判定。
180
183
  */
@@ -197,13 +200,16 @@ async function probeHttp(request, target, dispatcher) {
197
200
  const head = await attempt('HEAD')
198
201
  if (head.error !== undefined) return classifyTargetFailure(head.error)
199
202
  const verdict = classifyProbeStatus(head.res.status)
200
- if (verdict.kind !== 'http-error' || (head.res.status !== 405 && head.res.status !== 501)) return verdict
203
+ if (verdict.kind !== 'http-error' || !headNeedsGetRetry(head.res.status)) return verdict
201
204
  const get = await attempt('GET')
202
205
  if (get.error !== undefined) return classifyTargetFailure(get.error)
203
206
  const retried = classifyProbeStatus(get.res.status)
204
- retried.note = retried.ok === true
205
- ? 'HEAD 被拒绝但 GET 可用 — 主机可用。'
206
- : 'HEAD 被拒绝;GET 复测也遇到同类问题。'
207
+ const sameStatus = get.res.status === head.res.status
208
+ retried.note = sameStatus
209
+ ? `HEAD 与 GET 均返回 ${get.res.status} — 该路径确实不可用。`
210
+ : retried.ok === true
211
+ ? `HEAD 返回 ${head.res.status},GET 得到 ${get.res.status} — 以 GET 为准(网关常不给 HEAD 注册路由)。`
212
+ : `HEAD 返回 ${head.res.status},GET 得到 ${get.res.status} — 以 GET 为准。`
207
213
  return retried
208
214
  }
209
215
 
package/lib/proxy-core.js CHANGED
@@ -282,6 +282,23 @@ export function keepaliveHint(message = '', proxyUrl = '') {
282
282
  * @param status - 探测收到的 HTTP 状态码。
283
283
  * @returns `{ ok, verdict, kind, status, short, why, fix }`(中文文案)。
284
284
  */
285
+ /**
286
+ * HEAD 结论不可信、必须换 GET 复测的状态码。
287
+ *
288
+ * 405/501 是服务器明说「不支持该方法」;**404 同样是陷阱**:很多网关不给
289
+ * HEAD 注册路由,对 HEAD 一律回 404,而同一路径 GET 会正常回 401/200 ——
290
+ * 实测 DashScope:`HEAD /compatible-mode/v1/models` → 404,同路径 GET → 401
291
+ * (2026-09-18 用户报「测什么都 404」时定位)。不复测就会把健康端点误判成
292
+ * 「路径不存在」。
293
+ *
294
+ * @param status - HEAD 探测收到的状态码。
295
+ * @returns 是否需要用 GET 复测。
296
+ */
297
+ export function headNeedsGetRetry(status) {
298
+ const code = Number(status)
299
+ return code === 404 || code === 405 || code === 501
300
+ }
301
+
285
302
  export function classifyProbeStatus(status) {
286
303
  const code = Number(status)
287
304
  const base = { status: code }
@@ -305,8 +322,8 @@ export function classifyProbeStatus(status) {
305
322
  }
306
323
  if (code === 404) {
307
324
  return { ...base, ok: false, verdict: 'degraded', kind: 'http-error', short: 'HTTP 404 — 路径不存在',
308
- why: '路由已到服务器,但该路径不存在(探测常打根路径)。',
309
- fix: '用真实调用的完整路径复测;能通就忽略此结果。' }
325
+ why: 'HEAD 与 GET 都拿到了 404:路由已到服务器,但该路径确实没有资源(探测常打根路径或裸前缀)。',
326
+ fix: '改用真实调用的完整端点复测(如 .../models、.../chat/completions);那里有响应就说明主机可用。' }
310
327
  }
311
328
  if (code === 405 || code === 501) {
312
329
  return { ...base, ok: false, verdict: 'degraded', kind: 'http-error', short: `HTTP ${code} — 端点不支持 HEAD`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yanglaofish/dsh-proxy-pro",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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",