@yanglaofish/dsh-proxy-pro 1.0.6 → 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 +31 -1
- package/docs/SPEC.md +3 -1
- package/lib/index.js +14 -8
- package/lib/proxy-core.js +19 -2
- package/package.json +1 -1
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
|
|
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/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
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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.
|
|
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",
|