@yanglaofish/dsh-proxy-pro 1.0.7 → 1.0.8
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 +35 -1
- package/lib/client.js +37 -26
- package/lib/index.js +51 -20
- package/lib/proxy-core.js +79 -0
- package/lib/tool-schemas.js +29 -20
- package/package.json +1 -1
package/docs/LESSONS.md
CHANGED
|
@@ -780,4 +780,38 @@ API 端点就这样被误判。**404 从来不是「网络不可达」的证据*
|
|
|
780
780
|
|
|
781
781
|
**教训**:① 探测方法本身就是误判来源——HEAD「轻量」不等于「等价」;② 把 404 当「不存在」
|
|
782
782
|
之前先问「服务器认不认这个方法」;③ 状态码是二手的,链路层结论只能由「是否拿到 HTTP
|
|
783
|
-
应答」给出。
|
|
783
|
+
应答」给出。
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## 28. 发布铁律:每次 npm publish 后必须主动强制触发淘宝源同步(用户定案,2026-09-18)
|
|
788
|
+
|
|
789
|
+
**用户的规矩(说了两次,不可违背)**:每次我们更新 npm 包(publish 成功)后,**必须手动
|
|
790
|
+
主动触发淘宝源(npmmirror)更新,不许等待**——用户原话「每次更新了 npm,都请你手动强制
|
|
791
|
+
触发淘宝源更新」「不要等待,就是走淘宝的主动同步」。
|
|
792
|
+
|
|
793
|
+
**为什么**:npmmirror 自己拉取官方源有队列延迟,干等(只 PUT 一次 syncs 然后 poll)会 stuck
|
|
794
|
+
几分钟甚至更久,导致 `dsh plugin add <新版本>` 在淘宝源上解析不到。**主动循环触发**(每轮
|
|
795
|
+
先 PUT syncs、再 poll)实测 1.0.5=11 轮、1.0.6=13 轮、1.0.7=33 轮(约 2-4 分钟)必定拉到。
|
|
796
|
+
|
|
797
|
+
**标准流程(每一步都要做)**:
|
|
798
|
+
1. `npm publish --access=public --registry=https://registry.npmjs.org --replace-registry-host=never` 成功;
|
|
799
|
+
2. 循环最多 60 轮:`PUT https://registry.npmmirror.com/-/package/@yanglaofish%2Fdsh-proxy-pro/syncs`
|
|
800
|
+
带 body `{"version":"<新版本>"}`(ContentType application/json),随后
|
|
801
|
+
`GET https://registry.npmmirror.com/@yanglaofish%2Fdsh-proxy-pro` 看 `dist-tags.latest`;
|
|
802
|
+
每轮间隔 8-10s,**一直循环到 latest == 新版本**(不许提前放弃);
|
|
803
|
+
3. latest 到位后立即:`dsh plugin --profile test add '@yanglaofish/dsh-proxy-pro@^<新版本>'`
|
|
804
|
+
与 `dsh plugin --profile web add ...`,并验证两个 profile 的 node_modules 版本。
|
|
805
|
+
|
|
806
|
+
**教训**:① 「PUT syncs 返回 201」只是入队确认,不是同步完成——以 `dist-tags.latest` 为准;
|
|
807
|
+
② 官方 registry 先可见不代表淘宝可见,淘宝拉取是独立队列;③ 用户规则优先于"省事"——主动
|
|
808
|
+
触发是默认动作,不是可选优化。
|
|
809
|
+
|
|
810
|
+
**补充:git push 的代理必须与「被暖窗的代理」一致(2026-09-18 实测)**:git 全局配置里写死的
|
|
811
|
+
`http.proxy=http://proxyhk.huawei.com:8080` 是历史遗留——当前网络环境该代理已 407 失效,
|
|
812
|
+
而系统代理是 `proxyza`(南非)。正确姿势:push 前用 `Invoke-WebRequest https://github.com`
|
|
813
|
+
暖**当前系统代理**(.NET 自动 NTLM),然后 `git -c http.proxy=http://<当前系统代理> push`
|
|
814
|
+
(用 `-c` 临时覆盖写死的 config)。若暖窗对象与 git 实际走的代理不一致,必然 407 CONNECT
|
|
815
|
+
tunnel failed(proxyhk 暖窗 407、proxyza 暖窗 200 → 只有后者能推成功)。长期可选:
|
|
816
|
+
`git config --global --unset http.proxy` 让 git 走插件写入的 `http_proxy` 环境变量(动态跟随
|
|
817
|
+
当前系统代理),注意 CLI 环境无插件时需手动 `-c` 指定。
|
package/lib/client.js
CHANGED
|
@@ -74,9 +74,15 @@ window.__ModuleLoader__.load({
|
|
|
74
74
|
".dsxprx-diagwhy{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary)}",
|
|
75
75
|
".dsxprx-diagfix{font-size:12px;line-height:17px;color:var(--dsw-alias-label-primary)}",
|
|
76
76
|
".dsxprx-diagnote{font-size:11px;line-height:16px;color:var(--dsw-alias-label-caption)}",
|
|
77
|
+
".dsxprx-verdict{display:inline-flex;align-items:center;gap:6px;padding:2px 10px;border-radius:999px;font-weight:600;font-size:12px;line-height:18px}",
|
|
78
|
+
".dsxprx-verdict[data-tone=ok]{background:rgba(16,185,129,.12);color:var(--dsw-alias-state-success-primary,#10b981)}",
|
|
79
|
+
".dsxprx-verdict[data-tone=warn]{background:rgba(245,158,11,.14);color:#f59e0b}",
|
|
80
|
+
".dsxprx-verdict[data-tone=bad]{background:rgba(239,68,68,.12);color:var(--dsw-alias-state-error-primary,#ef4444)}",
|
|
81
|
+
".dsxprx-diagprobe{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
82
|
+
".dsxprx-diotag{flex:none;min-width:44px;font-size:12px;line-height:17px;color:var(--dsw-alias-label-caption)}",
|
|
83
|
+
".dsxprx-verdictlarge{font-size:13px;font-weight:600;line-height:19px;color:var(--dsw-alias-label-primary)}",
|
|
84
|
+
".dsxprx-diagverdict{display:flex;flex-direction:column;gap:3px;padding:6px 0 2px;border-top:1px dashed var(--dsw-alias-border-l1)}",
|
|
77
85
|
".dsxprx-title{font-size:16px;font-weight:700;line-height:22px;color:var(--dsw-alias-label-primary)}",
|
|
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}",
|
|
79
|
-
".dsxprx-diagcheck input{width:14px;height:14px;accent-color:#4d6bfe;margin:0}",
|
|
80
86
|
".dsxprx-hdr{display:flex;align-items:center;gap:6px;max-width:180px;height:26px;padding:0 10px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:16px;cursor:pointer;white-space:nowrap;overflow:hidden}",
|
|
81
87
|
".dsxprx-hdr:hover{background:var(--dsw-alias-state-hover)}",
|
|
82
88
|
".dsxprx-hdr .dsxprx-dot{width:8px;height:8px}",
|
|
@@ -296,26 +302,29 @@ window.__ModuleLoader__.load({
|
|
|
296
302
|
});
|
|
297
303
|
};
|
|
298
304
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
/** Three-state headline for one probe verdict (usable / degraded / unusable). */
|
|
302
|
-
function probeHeadline(probe) {
|
|
303
|
-
if (!probe) return "未知";
|
|
304
|
-
if (probe.verdict === "usable") return "可用" + (probe.status === undefined ? "" : "(HTTP " + probe.status + ")");
|
|
305
|
-
if (probe.verdict === "degraded") return "降级:" + (probe.short || "未知");
|
|
306
|
-
if (probe.verdict === "unusable") return "不可用:" + (probe.short || "未知");
|
|
307
|
-
return probe.ok ? "可用" : (probe.short || "未知");
|
|
308
|
-
}
|
|
305
|
+
// 诊断双通道并行(走代理 + 直连),由 host 一次给出两侧徽章和推荐,
|
|
306
|
+
// 不再让用户手动勾选通道(2026-09-18:单侧探针分不清被拒者是谁)
|
|
309
307
|
function runDiag() {
|
|
310
308
|
var target = diagUrl.trim();
|
|
311
309
|
if (!target) return;
|
|
312
310
|
setDiagBusy(true);
|
|
313
311
|
setDiagResult(null);
|
|
314
|
-
fetchJson("/route?url=" + encodeURIComponent(target)
|
|
312
|
+
fetchJson("/route?url=" + encodeURIComponent(target)).then(function (data) {
|
|
315
313
|
setDiagResult(data === null ? { error: "诊断接口不可用" } : data);
|
|
316
314
|
}).finally(function () { setDiagBusy(false); });
|
|
317
315
|
}
|
|
318
316
|
var diag = diagResult[0];
|
|
317
|
+
/** 通道徽章:优先用 host 的 proxyBadge/directBadge(人话+色),缺失时回退旧判定。 */
|
|
318
|
+
function channelBadge(name) {
|
|
319
|
+
var b = diag && typeof diag[name] === "object" && diag[name] !== null ? diag[name] : null;
|
|
320
|
+
if (b && b.text && b.tone) return b;
|
|
321
|
+
var fallback = name === "proxyBadge" ? diag?.proxy : diag?.direct;
|
|
322
|
+
if (!fallback) return { text: "未知", tone: "warn" };
|
|
323
|
+
if (fallback.verdict === "usable") return { text: "链路通 · 目标正常响应", tone: "ok" };
|
|
324
|
+
if (fallback.verdict === "degraded") return { text: "链路通 · 这次请求被目标拒绝", tone: "warn" };
|
|
325
|
+
if (fallback.verdict === "unusable") return { text: "链路不通 · 网络/代理问题", tone: "bad" };
|
|
326
|
+
return fallback.ok ? { text: "链路通", tone: "ok" } : { text: "链路不通", tone: "bad" };
|
|
327
|
+
}
|
|
319
328
|
|
|
320
329
|
return react.createElement("div", { className: "dsxprx", "data-proxy-settings": true },
|
|
321
330
|
|
|
@@ -402,27 +411,29 @@ window.__ModuleLoader__.load({
|
|
|
402
411
|
onChange: function (event) { setDiagUrl(event.target.value); },
|
|
403
412
|
onKeyDown: function (event) { if (event.key === "Enter") runDiag(); },
|
|
404
413
|
}),
|
|
405
|
-
react.createElement("label", { className: "dsxprx-diagcheck" },
|
|
406
|
-
react.createElement("input", {
|
|
407
|
-
type: "checkbox",
|
|
408
|
-
checked: diagUseProxy[0],
|
|
409
|
-
onChange: function (event) { setDiagUseProxy(event.target.checked); },
|
|
410
|
-
}),
|
|
411
|
-
"使用代理"),
|
|
412
414
|
react.createElement("button", { type: "button", className: "dsxprx-diagbtn", onClick: runDiag, disabled: diagBusy[0] || !diagUrl.trim() }, "测试路由")
|
|
413
415
|
),
|
|
414
|
-
react.createElement("span", { className: "dsxprx-hint" }, "
|
|
416
|
+
react.createElement("span", { className: "dsxprx-hint" }, "同时测「走代理」与「直连」两条通道并给出推荐,可判断该域名该不该加进 NO_PROXY。"),
|
|
415
417
|
diag !== null
|
|
416
418
|
? react.createElement("div", { className: "dsxprx-diagout" },
|
|
417
419
|
diag.error
|
|
418
420
|
? react.createElement("span", null, "诊断失败:" + diag.error)
|
|
419
421
|
: [
|
|
420
|
-
react.createElement("span", { key: "c" }, "测试通道: " + (diag.channel === "proxy" ? "经代理(强制)" : diag.channel === "direct" ? "直连(强制绕过)" : "按当前策略")),
|
|
421
422
|
react.createElement("span", { key: "r" }, "策略判定: " + diag.route + (diag.bypassed ? "(NO_PROXY 命中)" : "")),
|
|
422
|
-
react.createElement("span", { key: "
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
423
|
+
react.createElement("span", { key: "px", className: "dsxprx-diagprobe" },
|
|
424
|
+
react.createElement("span", { className: "dsxprx-diotag" }, "走代理"),
|
|
425
|
+
react.createElement("span", { className: "dsxprx-verdict", "data-tone": channelBadge("proxyBadge").tone }, channelBadge("proxyBadge").text)
|
|
426
|
+
),
|
|
427
|
+
react.createElement("span", { key: "dx", className: "dsxprx-diagprobe" },
|
|
428
|
+
react.createElement("span", { className: "dsxprx-diotag" }, "直连"),
|
|
429
|
+
react.createElement("span", { className: "dsxprx-verdict", "data-tone": channelBadge("directBadge").tone }, channelBadge("directBadge").text)
|
|
430
|
+
),
|
|
431
|
+
diag.verdict ? react.createElement("span", { key: "v", className: "dsxprx-diagverdict" },
|
|
432
|
+
react.createElement("span", { className: "dsxprx-verdictlarge" }, "推荐: " + (diag.short || "未知")),
|
|
433
|
+
diag.why ? react.createElement("span", { className: "dsxprx-diagwhy" }, "可能原因: " + diag.why) : null,
|
|
434
|
+
diag.fix ? react.createElement("span", { className: "dsxprx-diagfix" }, "挽救措施: " + diag.fix) : null
|
|
435
|
+
) : null,
|
|
436
|
+
diag.proxy && diag.proxy.note ? react.createElement("span", { key: "n", className: "dsxprx-diagnote" }, "备注: " + diag.proxy.note) : null,
|
|
426
437
|
diag.hint ? react.createElement("span", { key: "h" }, "提示: " + diag.hint) : null,
|
|
427
438
|
]
|
|
428
439
|
)
|
package/lib/index.js
CHANGED
|
@@ -32,6 +32,7 @@ import { promisify } from 'node:util'
|
|
|
32
32
|
import {
|
|
33
33
|
classifyProbeStatus,
|
|
34
34
|
classifyTargetFailure,
|
|
35
|
+
combineDualProbe,
|
|
35
36
|
headNeedsGetRetry,
|
|
36
37
|
keepaliveHint,
|
|
37
38
|
makeSystemProxyReader,
|
|
@@ -267,18 +268,25 @@ async function policyProbe(target) {
|
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
/**
|
|
270
|
-
* 按 web_fetch
|
|
271
|
-
*
|
|
272
|
-
*
|
|
271
|
+
* 按 web_fetch 的方式探测一个目标,**同时**强制走代理 + 强制直连两个通道,
|
|
272
|
+
* 再把两侧结果综合成一条人话推荐(combineDualProbe)。
|
|
273
|
+
*
|
|
274
|
+
* 单侧探测无法判断被拒是谁干的(目标 or 代理?)——只有两侧对比才能得出
|
|
275
|
+
* 方向性结论(直连通+代理失败 → 问题在代理一侧;代理通+直连失败 → 代理是
|
|
276
|
+
* 唯一通路)。`channel` 保留兼容:'policy'/'proxy'/'direct' 仍是旧的单通道
|
|
277
|
+
* 探测(HTTP API 的历史 useProxy 参数使用),默认 'dual' 走双通道并行。
|
|
273
278
|
*/
|
|
274
|
-
async function probeTarget(url, snapshot, channel = '
|
|
279
|
+
async function probeTarget(url, snapshot, channel = 'dual') {
|
|
275
280
|
let parsed
|
|
276
281
|
try {
|
|
277
282
|
parsed = new URL(url)
|
|
278
283
|
} catch {
|
|
279
|
-
return { url, route: 'invalid', bypassed: false,
|
|
280
|
-
|
|
281
|
-
why: '输入不是可解析的绝对 URL。', fix: '用完整形式,如 https://github.com/foo/bar。' }
|
|
284
|
+
return { url, route: 'invalid', bypassed: false, channel,
|
|
285
|
+
proxy: { ok: false, verdict: 'unusable', kind: 'invalid', short: 'URL 无效 — 需要 http(s)://主机/路径',
|
|
286
|
+
why: '输入不是可解析的绝对 URL。', fix: '用完整形式,如 https://github.com/foo/bar。' },
|
|
287
|
+
direct: { ok: false, verdict: 'unusable', kind: 'invalid', short: 'URL 无效 — 需要 http(s)://主机/路径',
|
|
288
|
+
why: '输入不是可解析的绝对 URL。', fix: '用完整形式,如 https://github.com/foo/bar。' },
|
|
289
|
+
verdict: 'both-bad', short: 'URL 无效', why: '输入不是可解析的绝对 URL。', fix: '用完整形式,如 https://github.com/foo/bar。' }
|
|
282
290
|
}
|
|
283
291
|
let routeName = 'DIRECT'
|
|
284
292
|
try {
|
|
@@ -292,6 +300,8 @@ async function probeTarget(url, snapshot, channel = 'policy') {
|
|
|
292
300
|
const host = entry.trim().replace(/^\*?\./, '').toLowerCase()
|
|
293
301
|
return host.length > 0 && (host === parsed.hostname.toLowerCase() || parsed.hostname.toLowerCase().endsWith(`.${host}`))
|
|
294
302
|
})
|
|
303
|
+
|
|
304
|
+
// 兼容旧单通道调用(HTTP API 历史 useProxy 参数)
|
|
295
305
|
if (channel === 'proxy' || channel === 'direct') {
|
|
296
306
|
const proxyUrl = channel === 'proxy' ? (snapshot?.effective?.url ?? '') : ''
|
|
297
307
|
const probe = proxyUrl === '' && channel === 'proxy'
|
|
@@ -301,8 +311,27 @@ async function probeTarget(url, snapshot, channel = 'policy') {
|
|
|
301
311
|
: await forceChannelProbe(parsed, channel, proxyUrl)
|
|
302
312
|
return { url, route: routeName, bypassed, channel, probe }
|
|
303
313
|
}
|
|
304
|
-
|
|
305
|
-
|
|
314
|
+
if (channel === 'policy') {
|
|
315
|
+
const probe = await policyProbe(parsed)
|
|
316
|
+
return { url, route: routeName, bypassed, channel, probe }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 双通道并行:强制走代理 + 强制直连,各自独立探测(互不干扰)
|
|
320
|
+
const proxyUrl = snapshot?.effective?.url ?? ''
|
|
321
|
+
const proxyProbe = proxyUrl === ''
|
|
322
|
+
? { ok: false, verdict: 'unusable', kind: 'bad-proxy', short: '当前快照里没有可用的代理地址',
|
|
323
|
+
why: '代理开关处于关闭(或 Windows 系统代理关闭),没有可强制走代理的对象。',
|
|
324
|
+
fix: '打开代理,或在「代理配置」里修复地址/模式。' }
|
|
325
|
+
: await forceChannelProbe(parsed, 'proxy', proxyUrl)
|
|
326
|
+
const directProbe = await forceChannelProbe(parsed, 'direct', '')
|
|
327
|
+
const combined = combineDualProbe(proxyProbe, directProbe)
|
|
328
|
+
return {
|
|
329
|
+
url, route: routeName, bypassed, channel: 'dual',
|
|
330
|
+
proxy: proxyProbe, direct: directProbe,
|
|
331
|
+
verdict: combined.verdict, short: combined.short,
|
|
332
|
+
why: combined.why, fix: combined.fix,
|
|
333
|
+
proxyBadge: combined.proxyBadge, directBadge: combined.directBadge,
|
|
334
|
+
}
|
|
306
335
|
}
|
|
307
336
|
|
|
308
337
|
// ---- browser-trust fence (mirrors dsh's /api fence) ------------------------
|
|
@@ -532,18 +561,16 @@ function apply(ctx, config) {
|
|
|
532
561
|
output: {
|
|
533
562
|
schema: PROXY_TEST_SCHEMA,
|
|
534
563
|
render: (_args, value) => {
|
|
535
|
-
const
|
|
536
|
-
const head = p.verdict === 'usable'
|
|
537
|
-
? `可用${p.status === undefined ? '' : `(HTTP ${p.status})`}`
|
|
538
|
-
: `${p.verdict === 'degraded' ? '降级' : '不可用'} — ${p.short ?? '未知'}`
|
|
564
|
+
const badge = (b) => (b?.tone === 'ok' ? '🟢' : b?.tone === 'warn' ? '🟡' : '🔴') + ' ' + (b?.text ?? '未知')
|
|
539
565
|
const lines = [
|
|
540
566
|
`路由: ${value.route}`,
|
|
541
567
|
`NO_PROXY 命中: ${value.bypassed}`,
|
|
542
|
-
|
|
568
|
+
`走代理: ${badge(value.proxyBadge)}`,
|
|
569
|
+
`直连: ${badge(value.directBadge)}`,
|
|
570
|
+
`结论: ${value.short ?? '未知'}`,
|
|
543
571
|
]
|
|
544
|
-
if (
|
|
545
|
-
if (
|
|
546
|
-
if (p.note) lines.push(`备注: ${p.note}`)
|
|
572
|
+
if (value.why) lines.push(`可能原因: ${value.why}`)
|
|
573
|
+
if (value.fix) lines.push(`挽救措施: ${value.fix}`)
|
|
547
574
|
if (value.hint) lines.push(`提示: ${value.hint}`)
|
|
548
575
|
return [{ type: 'text', text: lines.join('\n') }]
|
|
549
576
|
},
|
|
@@ -551,7 +578,7 @@ function apply(ctx, config) {
|
|
|
551
578
|
execute: async (args) => {
|
|
552
579
|
await requestSync()
|
|
553
580
|
const result = await probeTarget(String(args.url ?? ''), snapshot)
|
|
554
|
-
result.hint = keepaliveHint(result.
|
|
581
|
+
result.hint = keepaliveHint(`${result.proxy?.short ?? ''} ${result.direct?.short ?? ''}`, snapshot?.effective?.url ?? '')
|
|
555
582
|
return result
|
|
556
583
|
},
|
|
557
584
|
presentCall: (args) => ({
|
|
@@ -668,10 +695,14 @@ function apply(ctx, config) {
|
|
|
668
695
|
}
|
|
669
696
|
if (is('/route', 'GET')) {
|
|
670
697
|
const target = url.searchParams.get('url') ?? ''
|
|
698
|
+
// useProxy 保留兼容:'1'/'0' 走旧的单通道强制;无参数(新版面板)走双通道
|
|
671
699
|
const useProxy = url.searchParams.get('useProxy')
|
|
672
|
-
const channel = useProxy === '1' ? 'proxy' : useProxy === '0' ? 'direct' : '
|
|
700
|
+
const channel = useProxy === '1' ? 'proxy' : useProxy === '0' ? 'direct' : 'dual'
|
|
673
701
|
const result = await probeTarget(target, snapshot, channel)
|
|
674
|
-
result.hint = keepaliveHint(
|
|
702
|
+
result.hint = keepaliveHint(
|
|
703
|
+
`${result.proxy?.short ?? ''} ${result.direct?.short ?? ''}`.trim(),
|
|
704
|
+
snapshot?.effective?.url ?? ''
|
|
705
|
+
)
|
|
675
706
|
sendJson(res, 200, { ok: true, ...result })
|
|
676
707
|
return
|
|
677
708
|
}
|
package/lib/proxy-core.js
CHANGED
|
@@ -416,3 +416,82 @@ export function classifyTargetFailure(error) {
|
|
|
416
416
|
why: '请求失败,探测器无法归类原因。',
|
|
417
417
|
fix: '对比直连/代理两侧并重试;用 proxy_status 查看生效路由。' }
|
|
418
418
|
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* 双通道综合判定:把「强制走代理」与「强制直连」两次探测合成为一份可给人
|
|
422
|
+
* 看的结果:两条徽章(各自带 tone 色)+ 一条推荐。
|
|
423
|
+
*
|
|
424
|
+
* 单侧探针拿不到应答者身份,因此「代理策略拦截」只能靠两侧对比得到方向性
|
|
425
|
+
* 结论(LESSONS §30:直连通+代理失败 → 问题在代理一侧;代理通+直连失败 →
|
|
426
|
+
* 代理是唯一通路;都失败 → 环境/服务端问题;都通 → 保持现状)。
|
|
427
|
+
*
|
|
428
|
+
* @param proxyProbe - 强制走代理通道的探测结果(classify 输出形态)。
|
|
429
|
+
* @param directProbe - 强制直连通道的探测结果。
|
|
430
|
+
* @returns `{ verdict, short, why, fix, proxyBadge, directBadge }`,
|
|
431
|
+
* badge = { text, tone: 'ok'|'warn'|'bad' }。
|
|
432
|
+
*/
|
|
433
|
+
export function combineDualProbe(proxyProbe, directProbe) {
|
|
434
|
+
const proxyOk = proxyProbe?.ok === true
|
|
435
|
+
const directOk = directProbe?.ok === true
|
|
436
|
+
const proxyAuth = proxyProbe?.kind === 'auth'
|
|
437
|
+
|| (proxyProbe?.verdict === 'unusable' && /NTLM|407/.test(String(proxyProbe?.short ?? '')))
|
|
438
|
+
|
|
439
|
+
const notReachable = (probe, label) => {
|
|
440
|
+
const detail = (probe?.short ?? '未知原因').replace(/^HTTP \d+ — /, '')
|
|
441
|
+
return `${label}:${detail}`
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 都通 → 都可以
|
|
445
|
+
if (proxyOk && directOk) {
|
|
446
|
+
return {
|
|
447
|
+
verdict: 'both-ok',
|
|
448
|
+
short: '代理与直连均通',
|
|
449
|
+
why: '两条通道都拿到了正常应答,目标本身可用。',
|
|
450
|
+
fix: '保持当前策略即可;无需改 NO_PROXY。',
|
|
451
|
+
proxyBadge: { text: '链路通 · 目标正常响应', tone: 'ok' },
|
|
452
|
+
directBadge: { text: '链路通 · 目标正常响应', tone: 'ok' },
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 只有代理通 → 走代理
|
|
457
|
+
if (proxyOk && !directOk) {
|
|
458
|
+
return {
|
|
459
|
+
verdict: 'proxy-only',
|
|
460
|
+
short: '只有走代理能通',
|
|
461
|
+
why: `直连 ${notReachable(directProbe, '失败')};走代理正常——该目标在当前网络需经代理访问。`,
|
|
462
|
+
fix: '确认该域名不在 NO_PROXY 直连名单里;保持代理开启。',
|
|
463
|
+
proxyBadge: { text: '链路通 · 目标正常响应', tone: 'ok' },
|
|
464
|
+
directBadge: { text: '链路不通 · 直连失败', tone: 'bad' },
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 只有直连通 → 问题在代理一侧(可能是策略拦截/认证/代理故障)
|
|
469
|
+
if (!proxyOk && directOk) {
|
|
470
|
+
const base = proxyAuth
|
|
471
|
+
? {
|
|
472
|
+
why: '代理要求 NTLM 认证(407),而直连正常——该目标不值得绕代理。',
|
|
473
|
+
fix: '该域名走代理会被认证卡住:把它加入 NO_PROXY 直连名单即可;无需打开认证窗口。',
|
|
474
|
+
}
|
|
475
|
+
: {
|
|
476
|
+
why: '经代理失败而直连正常——问题最可能在代理一侧:访问策略拦截、代理故障,或代理到目标之间的链路异常。',
|
|
477
|
+
fix: '先重试排除临时故障;仍失败则检查代理可用性或联系管理员。若确认是策略拦截,把该域名加入 NO_PROXY 直连名单。',
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
verdict: proxyAuth ? 'direct-only-auth' : 'direct-only',
|
|
481
|
+
short: proxyAuth ? '直连可通 · 代理需认证' : '直连可通 · 代理被拦',
|
|
482
|
+
...base,
|
|
483
|
+
proxyBadge: { text: proxyAuth ? '链路不通 · 代理要求认证' : '链路不通 · 经代理访问失败', tone: 'bad' },
|
|
484
|
+
directBadge: { text: '链路通 · 目标正常响应', tone: 'ok' },
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 都失败 → 环境/服务端问题
|
|
489
|
+
return {
|
|
490
|
+
verdict: 'both-bad',
|
|
491
|
+
short: '代理与直连均失败',
|
|
492
|
+
why: `两条通道都没拿到正常应答:代理 ${notReachable(proxyProbe, '失败')};直连 ${notReachable(directProbe, '失败')}。`,
|
|
493
|
+
fix: '是环境或服务端问题,与代理开关无关:检查网络/目标服务,或稍后重试。',
|
|
494
|
+
proxyBadge: { text: `链路不通 · ${notReachable(proxyProbe, '代理失败')}`, tone: 'bad' },
|
|
495
|
+
directBadge: { text: `链路不通 · ${notReachable(directProbe, '直连失败')}`, tone: 'bad' },
|
|
496
|
+
}
|
|
497
|
+
}
|
package/lib/tool-schemas.js
CHANGED
|
@@ -54,7 +54,28 @@ export const PROXY_TEST_PARAMETERS = {
|
|
|
54
54
|
},
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
57
|
+
/** Reusable per-channel probe result (a classifyProbeStatus / classifyTargetFailure
|
|
58
|
+
* output). Schema DSL has no $ref, so this inlines into PROXY_TEST_SCHEMA. */
|
|
59
|
+
const PROBE_RESULT_SCHEMA = {
|
|
60
|
+
type: 'object',
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
required: true,
|
|
63
|
+
properties: {
|
|
64
|
+
ok: { type: 'boolean', required: true },
|
|
65
|
+
verdict: { type: 'string' },
|
|
66
|
+
status: { type: 'number' },
|
|
67
|
+
kind: { type: 'string' },
|
|
68
|
+
short: { type: 'string' },
|
|
69
|
+
why: { type: 'string' },
|
|
70
|
+
fix: { type: 'string' },
|
|
71
|
+
note: { type: 'string' },
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Output schema of `proxy_test`. Dual-channel: forces both legs in parallel
|
|
76
|
+
* (走代理 / 直连), then combines them into one human verdict + recommendation
|
|
77
|
+
* (2026-09-18: single-side probes cannot tell WHO answered, so a strategy
|
|
78
|
+
* block can only be inferred by comparing both sides — LESSONS §30). */
|
|
58
79
|
export const PROXY_TEST_SCHEMA = {
|
|
59
80
|
type: 'object',
|
|
60
81
|
additionalProperties: false,
|
|
@@ -62,25 +83,13 @@ export const PROXY_TEST_SCHEMA = {
|
|
|
62
83
|
url: { type: 'string', required: true },
|
|
63
84
|
route: { type: 'string', required: true },
|
|
64
85
|
bypassed: { type: 'boolean', required: true },
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// render (2026-09-17: a 504 used to print as "reachable"). Every field
|
|
73
|
-
// must be listed here — the object is additionalProperties:false, so an
|
|
74
|
-
// undeclared field fails schema validation.
|
|
75
|
-
verdict: { type: 'string' },
|
|
76
|
-
status: { type: 'number' },
|
|
77
|
-
kind: { type: 'string' },
|
|
78
|
-
short: { type: 'string' },
|
|
79
|
-
why: { type: 'string' },
|
|
80
|
-
fix: { type: 'string' },
|
|
81
|
-
note: { type: 'string' },
|
|
82
|
-
},
|
|
83
|
-
},
|
|
86
|
+
channel: { type: 'string', required: true },
|
|
87
|
+
proxy: PROBE_RESULT_SCHEMA,
|
|
88
|
+
direct: PROBE_RESULT_SCHEMA,
|
|
89
|
+
verdict: { type: 'string', required: true },
|
|
90
|
+
short: { type: 'string', required: true },
|
|
91
|
+
why: { type: 'string', required: true },
|
|
92
|
+
fix: { type: 'string', required: true },
|
|
84
93
|
hint: { type: 'string' },
|
|
85
94
|
},
|
|
86
95
|
}
|
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.8",
|
|
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",
|