@yanglaofish/dsh-proxy-pro 1.0.8 → 1.0.9

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/lib/client.js CHANGED
@@ -322,7 +322,9 @@ window.__ModuleLoader__.load({
322
322
  if (!fallback) return { text: "未知", tone: "warn" };
323
323
  if (fallback.verdict === "usable") return { text: "链路通 · 目标正常响应", tone: "ok" };
324
324
  if (fallback.verdict === "degraded") return { text: "链路通 · 这次请求被目标拒绝", tone: "warn" };
325
- if (fallback.verdict === "unusable") return { text: "链路不通 · 网络/代理问题", tone: "bad" };
325
+ // 5xx:拿到了 HTTP 应答,链路通但服务端/网关报错(严重性误导修复:不算"链路不通")
326
+ if (fallback.kind === "gateway" || fallback.kind === "server-error") return { text: "链路通 · 目标/网关报错", tone: "warn" };
327
+ if (fallback.unusable === true || fallback.verdict === "unusable") return { text: "链路不通 · 网络/代理问题", tone: "bad" };
326
328
  return fallback.ok ? { text: "链路通", tone: "ok" } : { text: "链路不通", tone: "bad" };
327
329
  }
328
330
 
package/lib/proxy-core.js CHANGED
@@ -417,10 +417,43 @@ 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
+ const detail = (s) => String(s ?? '').replace(/^HTTP \d+ — /, '')
433
+ if (!probe) return { text: '结果未知', tone: 'warn' }
434
+ if (probe.verdict === 'usable') return { text: '链路通 · 目标正常响应', tone: 'ok' }
435
+ if (probe.verdict === 'degraded') {
436
+ return { text: `链路通 · 目标拒绝(${detail(probe.short) || '请求被拒'})`, tone: 'warn' }
437
+ }
438
+ if (probe.kind === 'gateway' || probe.kind === 'server-error') {
439
+ return { text: `链路通 · 目标/网关报错(${detail(probe.short) || String(probe.status ?? '5xx')})`, tone: 'warn' }
440
+ }
441
+ if (probe.kind === 'auth') return { text: '链路不通 · 代理要求认证', tone: 'bad' }
442
+ return { text: `链路不通 · ${detail(probe.short) || '连接失败'}`, tone: 'bad' }
443
+ }
444
+
420
445
  /**
421
446
  * 双通道综合判定:把「强制走代理」与「强制直连」两次探测合成为一份可给人
422
447
  * 看的结果:两条徽章(各自带 tone 色)+ 一条推荐。
423
448
  *
449
+ * 三个概念分离(LESSONS §32 严重性误导修复):
450
+ * - 可达(reachable):通道真正拿到了目标应答——usable 或 degraded(4xx)。
451
+ * 5xx 属"链路通但目标/网关报错"(kind=gateway/server-error),不算可达
452
+ * 也不算不通,徽章黄色;407/连接层失败才是"不通"。
453
+ * - 健康(healthy):usbale,目标给了正常应答。
454
+ * - 徽章:由 dualBadge 按实际 verdict/kind 生成,绝不把 404/403 标红。
455
+ * 只有连接层失败才标红"链路不通"。
456
+ *
424
457
  * 单侧探针拿不到应答者身份,因此「代理策略拦截」只能靠两侧对比得到方向性
425
458
  * 结论(LESSONS §30:直连通+代理失败 → 问题在代理一侧;代理通+直连失败 →
426
459
  * 代理是唯一通路;都失败 → 环境/服务端问题;都通 → 保持现状)。
@@ -431,18 +464,22 @@ export function classifyTargetFailure(error) {
431
464
  * badge = { text, tone: 'ok'|'warn'|'bad' }。
432
465
  */
433
466
  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 ?? '')))
467
+ const p = proxyProbe ?? {}
468
+ const d = directProbe ?? {}
469
+ const pReach = p.verdict === 'usable' || p.verdict === 'degraded'
470
+ const dReach = d.verdict === 'usable' || d.verdict === 'degraded'
471
+ const pOk = p.verdict === 'usable'
472
+ const dOk = d.verdict === 'usable'
473
+ const proxyAuth = p.kind === 'auth'
474
+ || (p.verdict === 'unusable' && /NTLM|407/.test(String(p.short ?? '')))
438
475
 
439
476
  const notReachable = (probe, label) => {
440
477
  const detail = (probe?.short ?? '未知原因').replace(/^HTTP \d+ — /, '')
441
478
  return `${label}:${detail}`
442
479
  }
443
480
 
444
- // 都通 → 都可以
445
- if (proxyOk && directOk) {
481
+ // ① 都健康 → 都可以(保持现状)
482
+ if (pOk && dOk) {
446
483
  return {
447
484
  verdict: 'both-ok',
448
485
  short: '代理与直连均通',
@@ -453,45 +490,98 @@ export function combineDualProbe(proxyProbe, directProbe) {
453
490
  }
454
491
  }
455
492
 
456
- // 只有代理通 → 走代理
457
- if (proxyOk && !directOk) {
493
+ // ② 只有代理健康 → 走代理
494
+ if (pOk && !dOk) {
458
495
  return {
459
496
  verdict: 'proxy-only',
460
497
  short: '只有走代理能通',
461
- why: `直连 ${notReachable(directProbe, '失败')};走代理正常——该目标在当前网络需经代理访问。`,
498
+ why: `直连 ${notReachable(d, '失败')};走代理正常——该目标在当前网络需经代理访问。`,
462
499
  fix: '确认该域名不在 NO_PROXY 直连名单里;保持代理开启。',
463
500
  proxyBadge: { text: '链路通 · 目标正常响应', tone: 'ok' },
464
- directBadge: { text: '链路不通 · 直连失败', tone: 'bad' },
501
+ directBadge: dualBadge(d),
465
502
  }
466
503
  }
467
504
 
468
- // 只有直连通 → 问题在代理一侧(可能是策略拦截/认证/代理故障)
469
- if (!proxyOk && directOk) {
505
+ // ③ 只有直连健康 → 问题在代理一侧(认证拦截 / 不可达 / 被目标拒绝)
506
+ if (!pOk && dOk) {
507
+ const pSide = proxyAuth
508
+ ? '代理要求 NTLM 认证(407),请求没到目标'
509
+ : p.verdict === 'degraded'
510
+ ? '经代理已到达目标,但目标拒绝了这次请求(403/404/429 等)'
511
+ : '经代理未能连接目标(可能是代理策略拦截、代理故障或链路异常)'
470
512
  const base = proxyAuth
471
513
  ? {
472
514
  why: '代理要求 NTLM 认证(407),而直连正常——该目标不值得绕代理。',
473
515
  fix: '该域名走代理会被认证卡住:把它加入 NO_PROXY 直连名单即可;无需打开认证窗口。',
474
516
  }
475
517
  : {
476
- why: '经代理失败而直连正常——问题最可能在代理一侧:访问策略拦截、代理故障,或代理到目标之间的链路异常。',
477
- fix: '先重试排除临时故障;仍失败则检查代理可用性或联系管理员。若确认是策略拦截,把该域名加入 NO_PROXY 直连名单。',
518
+ why: `${pSide};而直连正常——结论:该域名走直连更可靠。`,
519
+ fix: p.verdict === 'degraded'
520
+ ? '把该域名加入 NO_PROXY 直连名单即可绕开代理侧的目标拒绝;若确需走代理,检查 key/UA/权限。'
521
+ : '先重试排除临时故障;仍失败则检查代理可用性或联系管理员。若确认是策略拦截,把该域名加入 NO_PROXY 直连名单。',
478
522
  }
479
523
  return {
480
524
  verdict: proxyAuth ? 'direct-only-auth' : 'direct-only',
481
- short: proxyAuth ? '直连可通 · 代理需认证' : '直连可通 · 代理被拦',
525
+ short: proxyAuth ? '直连可通 · 代理需认证' : (p.verdict === 'degraded' ? '直连可通 · 代理侧被目标拒绝' : '直连可通 · 代理被拦'),
482
526
  ...base,
483
- proxyBadge: { text: proxyAuth ? '链路不通 · 代理要求认证' : '链路不通 · 经代理访问失败', tone: 'bad' },
527
+ proxyBadge: dualBadge(p),
484
528
  directBadge: { text: '链路通 · 目标正常响应', tone: 'ok' },
485
529
  }
486
530
  }
487
531
 
488
- // 都失败 → 环境/服务端问题
532
+ // ④ 都不健康但都可达(双 4xx:链路通,目标对探测路径有异常应答)
533
+ if (pReach && dReach) {
534
+ const pNote = notReachable(p, '代理').replace(/^失败:/, '代理:')
535
+ const dNote = notReachable(d, '直连').replace(/^失败:/, '直连:')
536
+ return {
537
+ verdict: 'both-reached',
538
+ short: '双链路通 · 目标应答异常',
539
+ why: `两条通道都到达了目标,但目标返回异常状态:${pNote};${dNote}。`,
540
+ fix: '探测的是根路径/裸前缀,目标可能对 HEAD 或未授权请求如此应答——改用真实调用端点(如 /models、/chat/completions)复测,或检查该主机的 API Key。',
541
+ proxyBadge: dualBadge(p),
542
+ directBadge: dualBadge(d),
543
+ }
544
+ }
545
+
546
+ // ⑤ 只有代理可达(哪怕 404/403,也是唯一通路)——修复重点:
547
+ // 404 不再落入"双失败",恢复为代理是唯一通路(腾讯 Copilot 实况)
548
+ if (pReach && !dReach) {
549
+ const pNote = p.verdict === 'usable' ? '正常响应' : notReachable(p, '请求被拒').replace(/^请求被拒:/, '')
550
+ return {
551
+ verdict: 'proxy-only',
552
+ short: p.verdict === 'usable' ? '只有走代理能通' : '只有走代理可达 · 目标有异常应答',
553
+ why: `直连 ${notReachable(d, '失败')};经代理 ${pNote}——该域名在当前网络只有代理通道能到达目标。`,
554
+ fix: p.verdict === 'usable'
555
+ ? '确认该域名不在 NO_PROXY 直连名单里;保持代理开启。'
556
+ : '走代理链路是通的,但目标返回异常(尝试换真实端点复测);同时确认该域名不在 NO_PROXY 直连名单里。',
557
+ proxyBadge: dualBadge(p),
558
+ directBadge: dualBadge(d),
559
+ }
560
+ }
561
+
562
+ // ⑥ 只有直连可达(代理侧 4xx/5xx 或不可达)
563
+ if (dReach) {
564
+ return {
565
+ verdict: proxyAuth ? 'direct-only-auth' : 'direct-only',
566
+ short: proxyAuth ? '直连可达 · 代理需认证' : (d.verdict === 'usable' ? '直连可通 · 代理被拦' : '直连可达 · 代理侧异常'),
567
+ why: proxyAuth
568
+ ? '代理要求 NTLM 认证(407),而直连正常——该目标不值得绕代理。'
569
+ : `经代理 ${p.kind === 'gateway' || p.kind === 'server-error' ? '返回网关/服务端错误(链路本身通,问题在目标或中间网关)' : `失败(${notReachable(p, '').replace(/^:/, '')})`};直连 ${dOk ? '正常' : notReachable(d, '可达但目标异常')}。`,
570
+ fix: proxyAuth
571
+ ? '把该域名加入 NO_PROXY 直连名单即可。'
572
+ : '建议该域名走直连(加入 NO_PROXY);若代理侧是 4xx/5xx,也检查一下真实调用端点。',
573
+ proxyBadge: dualBadge(p),
574
+ directBadge: dualBadge(d),
575
+ }
576
+ }
577
+
578
+ // ⑦ 都不可达(连接层失败)→ 环境/服务端问题
489
579
  return {
490
580
  verdict: 'both-bad',
491
581
  short: '代理与直连均失败',
492
- why: `两条通道都没拿到正常应答:代理 ${notReachable(proxyProbe, '失败')};直连 ${notReachable(directProbe, '失败')}。`,
582
+ why: `两条通道都没能建立到目标的连接:代理 ${notReachable(p, '失败')};直连 ${notReachable(d, '失败')}。`,
493
583
  fix: '是环境或服务端问题,与代理开关无关:检查网络/目标服务,或稍后重试。',
494
- proxyBadge: { text: `链路不通 · ${notReachable(proxyProbe, '代理失败')}`, tone: 'bad' },
495
- directBadge: { text: `链路不通 · ${notReachable(directProbe, '直连失败')}`, tone: 'bad' },
584
+ proxyBadge: dualBadge(p),
585
+ directBadge: dualBadge(d),
496
586
  }
497
587
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yanglaofish/dsh-proxy-pro",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
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",