@yanglaofish/dsh-proxy-pro 1.0.8 → 1.0.10
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 +905 -817
- package/lib/client.js +25 -13
- package/lib/index.js +1 -0
- package/lib/proxy-core.js +117 -21
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -179,24 +179,33 @@ window.__ModuleLoader__.load({
|
|
|
179
179
|
function ProxyHeaderButton(props) {
|
|
180
180
|
var scope = props.scope;
|
|
181
181
|
var settings = useProxySnapshot(scope);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
182
|
+
// Local optimistic value (2026-09-18): the capsule flips its visual
|
|
183
|
+
// immediately on click instead of waiting for scope.set's round-trip
|
|
184
|
+
// (the settings-page switch already behaves this way). null means no
|
|
185
|
+
// unconfirmed value — the capsule mirrors settings.value.
|
|
186
|
+
var optimistic = useState(null);
|
|
187
|
+
var setOptimistic = optimistic[1];
|
|
188
188
|
var config = settings.value || {};
|
|
189
|
-
var
|
|
190
|
-
var
|
|
189
|
+
var valueEnabled = config.enabled === true;
|
|
190
|
+
var shownEnabled = optimistic[0] !== null ? optimistic[0] : valueEnabled;
|
|
191
|
+
var active = shownEnabled === true && (config.mode || "system") !== "none";
|
|
191
192
|
var mode = config.mode || "system";
|
|
192
193
|
var modeLabel = mode === "system" ? "system" : mode === "custom" ? "custom" : "none";
|
|
194
|
+
// Once the host has committed the new value, drop the optimistic echo.
|
|
195
|
+
useEffect(function () {
|
|
196
|
+
if (optimistic[0] !== null && settings.ready && settings.value !== undefined && settings.value.enabled === optimistic[0]) {
|
|
197
|
+
setOptimistic(null);
|
|
198
|
+
}
|
|
199
|
+
}, [optimistic[0], settings.value, settings.ready]);
|
|
193
200
|
function toggle() {
|
|
194
201
|
if (!settings.writable) return;
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
var next = !shownEnabled;
|
|
203
|
+
setOptimistic(next);
|
|
197
204
|
scope.set("enabled", next).catch(function (error) {
|
|
198
205
|
console.error("[dsh-proxy-pro] toggle failed:", error);
|
|
199
|
-
|
|
206
|
+
// Roll back to the committed value on failure.
|
|
207
|
+
setOptimistic(null);
|
|
208
|
+
});
|
|
200
209
|
// Toggle doubles as a manual "recheck now" even when the settings
|
|
201
210
|
// panel is not mounted: pulling /status makes the host re-read the
|
|
202
211
|
// system facts (idempotent — snapshot refresh unless the proxy really
|
|
@@ -216,7 +225,7 @@ window.__ModuleLoader__.load({
|
|
|
216
225
|
"data-active": active ? "true" : "false",
|
|
217
226
|
"data-proxy-toggle": true,
|
|
218
227
|
onClick: toggle,
|
|
219
|
-
disabled: !settings.writable
|
|
228
|
+
disabled: !settings.writable,
|
|
220
229
|
title: title,
|
|
221
230
|
"aria-pressed": active,
|
|
222
231
|
},
|
|
@@ -322,7 +331,9 @@ window.__ModuleLoader__.load({
|
|
|
322
331
|
if (!fallback) return { text: "未知", tone: "warn" };
|
|
323
332
|
if (fallback.verdict === "usable") return { text: "链路通 · 目标正常响应", tone: "ok" };
|
|
324
333
|
if (fallback.verdict === "degraded") return { text: "链路通 · 这次请求被目标拒绝", tone: "warn" };
|
|
325
|
-
|
|
334
|
+
// 5xx:拿到了 HTTP 应答,链路通但服务端/网关报错(严重性误导修复:不算"链路不通")
|
|
335
|
+
if (fallback.kind === "gateway" || fallback.kind === "server-error") return { text: "链路通 · 目标/网关报错", tone: "warn" };
|
|
336
|
+
if (fallback.unusable === true || fallback.verdict === "unusable") return { text: "链路不通 · 网络/代理问题", tone: "bad" };
|
|
326
337
|
return fallback.ok ? { text: "链路通", tone: "ok" } : { text: "链路不通", tone: "bad" };
|
|
327
338
|
}
|
|
328
339
|
|
|
@@ -420,6 +431,7 @@ window.__ModuleLoader__.load({
|
|
|
420
431
|
? react.createElement("span", null, "诊断失败:" + diag.error)
|
|
421
432
|
: [
|
|
422
433
|
react.createElement("span", { key: "r" }, "策略判定: " + diag.route + (diag.bypassed ? "(NO_PROXY 命中)" : "")),
|
|
434
|
+
react.createElement("span", { key: "codes" }, "状态码: 走代理 " + ((diag.proxy && diag.proxy.status) || "—") + " · 直连 " + ((diag.direct && diag.direct.status) || "—")),
|
|
423
435
|
react.createElement("span", { key: "px", className: "dsxprx-diagprobe" },
|
|
424
436
|
react.createElement("span", { className: "dsxprx-diotag" }, "走代理"),
|
|
425
437
|
react.createElement("span", { className: "dsxprx-verdict", "data-tone": channelBadge("proxyBadge").tone }, channelBadge("proxyBadge").text)
|
package/lib/index.js
CHANGED
|
@@ -565,6 +565,7 @@ function apply(ctx, config) {
|
|
|
565
565
|
const lines = [
|
|
566
566
|
`路由: ${value.route}`,
|
|
567
567
|
`NO_PROXY 命中: ${value.bypassed}`,
|
|
568
|
+
`状态码: 走代理 ${value.proxy?.status ?? '—'} · 直连 ${value.direct?.status ?? '—'}`,
|
|
568
569
|
`走代理: ${badge(value.proxyBadge)}`,
|
|
569
570
|
`直连: ${badge(value.directBadge)}`,
|
|
570
571
|
`结论: ${value.short ?? '未知'}`,
|
package/lib/proxy-core.js
CHANGED
|
@@ -417,10 +417,47 @@ export function classifyTargetFailure(error) {
|
|
|
417
417
|
fix: '对比直连/代理两侧并重试;用 proxy_status 查看生效路由。' }
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
/**
|
|
421
|
+
* 单侧通道的徽章:按真实语义给色,杜绝"严重性误导"(LESSONS §32)。
|
|
422
|
+
* - usable(2xx/3xx/401)→ 🟢 链路通 · 目标正常响应
|
|
423
|
+
* - degraded(403/404/405/429 等被目标拒绝)→ 🟡 链路通 · 目标拒绝(…)
|
|
424
|
+
* - 5xx(500/502/503/504 拿到应答但目标/网关报错)→ 🟡 链路通 · 目标/网关报错(…)
|
|
425
|
+
* - 407 认证拦截 → 🔴 链路不通 · 代理要求认证
|
|
426
|
+
* - 其他不可达(DNS/拒绝/超时/本地错误)→ 🔴 链路不通 · …
|
|
427
|
+
* 关键区分:**只要拿到了 HTTP 应答(含 4xx/5xx)链路就是通的**,只有
|
|
428
|
+
* 连接层失败(未拿到应答)才是"链路不通"。404 曾因 ok=false 被误标红色
|
|
429
|
+
* 链路不通 → 截图"代理与直连均失败"误导(2026-09-18 用户实况)。
|
|
430
|
+
*/
|
|
431
|
+
function dualBadge(probe) {
|
|
432
|
+
// Keep the HTTP code in the copy (2026-09-18 用户要求:展示原因时直接看到
|
|
433
|
+
// 状态码,而不是只有人话)。连接层失败没有 status,保持原文案。
|
|
434
|
+
const detail = (s) => String(s ?? '')
|
|
435
|
+
if (!probe) return { text: '结果未知', tone: 'warn' }
|
|
436
|
+
if (probe.verdict === 'usable') {
|
|
437
|
+
return { text: Number.isFinite(probe.status) ? `链路通 · 目标正常响应(HTTP ${probe.status})` : '链路通 · 目标正常响应', tone: 'ok' }
|
|
438
|
+
}
|
|
439
|
+
if (probe.verdict === 'degraded') {
|
|
440
|
+
return { text: `链路通 · 目标拒绝(${detail(probe.short) || '请求被拒'})`, tone: 'warn' }
|
|
441
|
+
}
|
|
442
|
+
if (probe.kind === 'gateway' || probe.kind === 'server-error') {
|
|
443
|
+
return { text: `链路通 · 目标/网关报错(${detail(probe.short) || `HTTP ${probe.status ?? '5xx'}`})`, tone: 'warn' }
|
|
444
|
+
}
|
|
445
|
+
if (probe.kind === 'auth') return { text: '链路不通 · 代理要求认证', tone: 'bad' }
|
|
446
|
+
return { text: `链路不通 · ${detail(probe.short) || '连接失败'}`, tone: 'bad' }
|
|
447
|
+
}
|
|
448
|
+
|
|
420
449
|
/**
|
|
421
450
|
* 双通道综合判定:把「强制走代理」与「强制直连」两次探测合成为一份可给人
|
|
422
451
|
* 看的结果:两条徽章(各自带 tone 色)+ 一条推荐。
|
|
423
452
|
*
|
|
453
|
+
* 三个概念分离(LESSONS §32 严重性误导修复):
|
|
454
|
+
* - 可达(reachable):通道真正拿到了目标应答——usable 或 degraded(4xx)。
|
|
455
|
+
* 5xx 属"链路通但目标/网关报错"(kind=gateway/server-error),不算可达
|
|
456
|
+
* 也不算不通,徽章黄色;407/连接层失败才是"不通"。
|
|
457
|
+
* - 健康(healthy):usbale,目标给了正常应答。
|
|
458
|
+
* - 徽章:由 dualBadge 按实际 verdict/kind 生成,绝不把 404/403 标红。
|
|
459
|
+
* 只有连接层失败才标红"链路不通"。
|
|
460
|
+
*
|
|
424
461
|
* 单侧探针拿不到应答者身份,因此「代理策略拦截」只能靠两侧对比得到方向性
|
|
425
462
|
* 结论(LESSONS §30:直连通+代理失败 → 问题在代理一侧;代理通+直连失败 →
|
|
426
463
|
* 代理是唯一通路;都失败 → 环境/服务端问题;都通 → 保持现状)。
|
|
@@ -431,18 +468,24 @@ export function classifyTargetFailure(error) {
|
|
|
431
468
|
* badge = { text, tone: 'ok'|'warn'|'bad' }。
|
|
432
469
|
*/
|
|
433
470
|
export function combineDualProbe(proxyProbe, directProbe) {
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
|
|
471
|
+
const p = proxyProbe ?? {}
|
|
472
|
+
const d = directProbe ?? {}
|
|
473
|
+
const pReach = p.verdict === 'usable' || p.verdict === 'degraded'
|
|
474
|
+
const dReach = d.verdict === 'usable' || d.verdict === 'degraded'
|
|
475
|
+
const pOk = p.verdict === 'usable'
|
|
476
|
+
const dOk = d.verdict === 'usable'
|
|
477
|
+
const proxyAuth = p.kind === 'auth'
|
|
478
|
+
|| (p.verdict === 'unusable' && /NTLM|407/.test(String(p.short ?? '')))
|
|
438
479
|
|
|
439
480
|
const notReachable = (probe, label) => {
|
|
440
|
-
|
|
481
|
+
// Keep the HTTP code — the reason line should name the status
|
|
482
|
+
// (2026-09-18: user asked for the code to be visible where the cause is shown).
|
|
483
|
+
const detail = probe?.short ?? '未知原因'
|
|
441
484
|
return `${label}:${detail}`
|
|
442
485
|
}
|
|
443
486
|
|
|
444
|
-
//
|
|
445
|
-
if (
|
|
487
|
+
// ① 都健康 → 都可以(保持现状)
|
|
488
|
+
if (pOk && dOk) {
|
|
446
489
|
return {
|
|
447
490
|
verdict: 'both-ok',
|
|
448
491
|
short: '代理与直连均通',
|
|
@@ -453,45 +496,98 @@ export function combineDualProbe(proxyProbe, directProbe) {
|
|
|
453
496
|
}
|
|
454
497
|
}
|
|
455
498
|
|
|
456
|
-
//
|
|
457
|
-
if (
|
|
499
|
+
// ② 只有代理健康 → 走代理
|
|
500
|
+
if (pOk && !dOk) {
|
|
458
501
|
return {
|
|
459
502
|
verdict: 'proxy-only',
|
|
460
503
|
short: '只有走代理能通',
|
|
461
|
-
why: `直连 ${notReachable(
|
|
504
|
+
why: `直连 ${notReachable(d, '失败')};走代理正常——该目标在当前网络需经代理访问。`,
|
|
462
505
|
fix: '确认该域名不在 NO_PROXY 直连名单里;保持代理开启。',
|
|
463
506
|
proxyBadge: { text: '链路通 · 目标正常响应', tone: 'ok' },
|
|
464
|
-
directBadge:
|
|
507
|
+
directBadge: dualBadge(d),
|
|
465
508
|
}
|
|
466
509
|
}
|
|
467
510
|
|
|
468
|
-
//
|
|
469
|
-
if (!
|
|
511
|
+
// ③ 只有直连健康 → 问题在代理一侧(认证拦截 / 不可达 / 被目标拒绝)
|
|
512
|
+
if (!pOk && dOk) {
|
|
513
|
+
const pSide = proxyAuth
|
|
514
|
+
? '代理要求 NTLM 认证(407),请求没到目标'
|
|
515
|
+
: p.verdict === 'degraded'
|
|
516
|
+
? '经代理已到达目标,但目标拒绝了这次请求(403/404/429 等)'
|
|
517
|
+
: '经代理未能连接目标(可能是代理策略拦截、代理故障或链路异常)'
|
|
470
518
|
const base = proxyAuth
|
|
471
519
|
? {
|
|
472
520
|
why: '代理要求 NTLM 认证(407),而直连正常——该目标不值得绕代理。',
|
|
473
521
|
fix: '该域名走代理会被认证卡住:把它加入 NO_PROXY 直连名单即可;无需打开认证窗口。',
|
|
474
522
|
}
|
|
475
523
|
: {
|
|
476
|
-
why:
|
|
477
|
-
fix:
|
|
524
|
+
why: `${pSide};而直连正常——结论:该域名走直连更可靠。`,
|
|
525
|
+
fix: p.verdict === 'degraded'
|
|
526
|
+
? '把该域名加入 NO_PROXY 直连名单即可绕开代理侧的目标拒绝;若确需走代理,检查 key/UA/权限。'
|
|
527
|
+
: '先重试排除临时故障;仍失败则检查代理可用性或联系管理员。若确认是策略拦截,把该域名加入 NO_PROXY 直连名单。',
|
|
478
528
|
}
|
|
479
529
|
return {
|
|
480
530
|
verdict: proxyAuth ? 'direct-only-auth' : 'direct-only',
|
|
481
|
-
short: proxyAuth ? '直连可通 · 代理需认证' : '直连可通 · 代理被拦',
|
|
531
|
+
short: proxyAuth ? '直连可通 · 代理需认证' : (p.verdict === 'degraded' ? '直连可通 · 代理侧被目标拒绝' : '直连可通 · 代理被拦'),
|
|
482
532
|
...base,
|
|
483
|
-
proxyBadge:
|
|
533
|
+
proxyBadge: dualBadge(p),
|
|
484
534
|
directBadge: { text: '链路通 · 目标正常响应', tone: 'ok' },
|
|
485
535
|
}
|
|
486
536
|
}
|
|
487
537
|
|
|
488
|
-
//
|
|
538
|
+
// ④ 都不健康但都可达(双 4xx:链路通,目标对探测路径有异常应答)
|
|
539
|
+
if (pReach && dReach) {
|
|
540
|
+
const pNote = notReachable(p, '代理').replace(/^失败:/, '代理:')
|
|
541
|
+
const dNote = notReachable(d, '直连').replace(/^失败:/, '直连:')
|
|
542
|
+
return {
|
|
543
|
+
verdict: 'both-reached',
|
|
544
|
+
short: '双链路通 · 目标应答异常',
|
|
545
|
+
why: `两条通道都到达了目标,但目标返回异常状态:${pNote};${dNote}。`,
|
|
546
|
+
fix: '探测的是根路径/裸前缀,目标可能对 HEAD 或未授权请求如此应答——改用真实调用端点(如 /models、/chat/completions)复测,或检查该主机的 API Key。',
|
|
547
|
+
proxyBadge: dualBadge(p),
|
|
548
|
+
directBadge: dualBadge(d),
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ⑤ 只有代理可达(哪怕 404/403,也是唯一通路)——修复重点:
|
|
553
|
+
// 404 不再落入"双失败",恢复为代理是唯一通路(腾讯 Copilot 实况)
|
|
554
|
+
if (pReach && !dReach) {
|
|
555
|
+
const pNote = p.verdict === 'usable' ? '正常响应' : notReachable(p, '请求被拒').replace(/^请求被拒:/, '')
|
|
556
|
+
return {
|
|
557
|
+
verdict: 'proxy-only',
|
|
558
|
+
short: p.verdict === 'usable' ? '只有走代理能通' : '只有走代理可达 · 目标有异常应答',
|
|
559
|
+
why: `直连 ${notReachable(d, '失败')};经代理 ${pNote}——该域名在当前网络只有代理通道能到达目标。`,
|
|
560
|
+
fix: p.verdict === 'usable'
|
|
561
|
+
? '确认该域名不在 NO_PROXY 直连名单里;保持代理开启。'
|
|
562
|
+
: '走代理链路是通的,但目标返回异常(尝试换真实端点复测);同时确认该域名不在 NO_PROXY 直连名单里。',
|
|
563
|
+
proxyBadge: dualBadge(p),
|
|
564
|
+
directBadge: dualBadge(d),
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ⑥ 只有直连可达(代理侧 4xx/5xx 或不可达)
|
|
569
|
+
if (dReach) {
|
|
570
|
+
return {
|
|
571
|
+
verdict: proxyAuth ? 'direct-only-auth' : 'direct-only',
|
|
572
|
+
short: proxyAuth ? '直连可达 · 代理需认证' : (d.verdict === 'usable' ? '直连可通 · 代理被拦' : '直连可达 · 代理侧异常'),
|
|
573
|
+
why: proxyAuth
|
|
574
|
+
? '代理要求 NTLM 认证(407),而直连正常——该目标不值得绕代理。'
|
|
575
|
+
: `经代理 ${p.kind === 'gateway' || p.kind === 'server-error' ? '返回网关/服务端错误(链路本身通,问题在目标或中间网关)' : `失败(${notReachable(p, '').replace(/^:/, '')})`};直连 ${dOk ? '正常' : notReachable(d, '可达但目标异常')}。`,
|
|
576
|
+
fix: proxyAuth
|
|
577
|
+
? '把该域名加入 NO_PROXY 直连名单即可。'
|
|
578
|
+
: '建议该域名走直连(加入 NO_PROXY);若代理侧是 4xx/5xx,也检查一下真实调用端点。',
|
|
579
|
+
proxyBadge: dualBadge(p),
|
|
580
|
+
directBadge: dualBadge(d),
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ⑦ 都不可达(连接层失败)→ 环境/服务端问题
|
|
489
585
|
return {
|
|
490
586
|
verdict: 'both-bad',
|
|
491
587
|
short: '代理与直连均失败',
|
|
492
|
-
why:
|
|
588
|
+
why: `两条通道都没能建立到目标的连接:代理 ${notReachable(p, '失败')};直连 ${notReachable(d, '失败')}。`,
|
|
493
589
|
fix: '是环境或服务端问题,与代理开关无关:检查网络/目标服务,或稍后重试。',
|
|
494
|
-
proxyBadge:
|
|
495
|
-
directBadge:
|
|
590
|
+
proxyBadge: dualBadge(p),
|
|
591
|
+
directBadge: dualBadge(d),
|
|
496
592
|
}
|
|
497
593
|
}
|
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.10",
|
|
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",
|