@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/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
- var busy = useState(false);
183
- var setBusy = busy[1];
184
- // Configuration-driven: the capsule mirrors the switch, resolved locally
185
- // with zero requests. The host applies the change instantly when the
186
- // settings revision lands (and follows external system-proxy changes on
187
- // its own 30s poll), so polling here buys nothing (§17).
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 active = config.enabled === true && config.mode !== "none";
190
- var enabled = config.enabled === true;
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
- setBusy(true);
196
- var next = !enabled;
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
- }).finally(function () { setBusy(false); });
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 || busy[0],
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
- if (fallback.verdict === "unusable") return { text: "链路不通 · 网络/代理问题", tone: "bad" };
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 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 ?? '')))
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
- const detail = (probe?.short ?? '未知原因').replace(/^HTTP \d+ — /, '')
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 (proxyOk && directOk) {
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 (proxyOk && !directOk) {
499
+ // ② 只有代理健康 → 走代理
500
+ if (pOk && !dOk) {
458
501
  return {
459
502
  verdict: 'proxy-only',
460
503
  short: '只有走代理能通',
461
- why: `直连 ${notReachable(directProbe, '失败')};走代理正常——该目标在当前网络需经代理访问。`,
504
+ why: `直连 ${notReachable(d, '失败')};走代理正常——该目标在当前网络需经代理访问。`,
462
505
  fix: '确认该域名不在 NO_PROXY 直连名单里;保持代理开启。',
463
506
  proxyBadge: { text: '链路通 · 目标正常响应', tone: 'ok' },
464
- directBadge: { text: '链路不通 · 直连失败', tone: 'bad' },
507
+ directBadge: dualBadge(d),
465
508
  }
466
509
  }
467
510
 
468
- // 只有直连通 → 问题在代理一侧(可能是策略拦截/认证/代理故障)
469
- if (!proxyOk && directOk) {
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: '先重试排除临时故障;仍失败则检查代理可用性或联系管理员。若确认是策略拦截,把该域名加入 NO_PROXY 直连名单。',
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: { text: proxyAuth ? '链路不通 · 代理要求认证' : '链路不通 · 经代理访问失败', tone: 'bad' },
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: `两条通道都没拿到正常应答:代理 ${notReachable(proxyProbe, '失败')};直连 ${notReachable(directProbe, '失败')}。`,
588
+ why: `两条通道都没能建立到目标的连接:代理 ${notReachable(p, '失败')};直连 ${notReachable(d, '失败')}。`,
493
589
  fix: '是环境或服务端问题,与代理开关无关:检查网络/目标服务,或稍后重试。',
494
- proxyBadge: { text: `链路不通 · ${notReachable(proxyProbe, '代理失败')}`, tone: 'bad' },
495
- directBadge: { text: `链路不通 · ${notReachable(directProbe, '直连失败')}`, tone: 'bad' },
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.8",
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",