@yanglaofish/dsh-proxy-pro 1.0.9 → 1.1.0

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
@@ -10,9 +10,10 @@
10
10
  * NOT derived from settings), the editable config fields (mode / custom
11
11
  * URL / NO_PROXY), and a per-URL route diagnostic.
12
12
  *
13
- * Both read and write the `proxy` settings namespace through the shared
14
- * `ctx.settingsScope` transport (same as dsh-plugin-proxy); runtime facts
15
- * come from the same-origin /dsh-proxy-pro/api endpoints served by the host
13
+ * Both read and write configuration through the host's own
14
+ * `/dsh-proxy-pro/api/config` endpoint (the browser settings transport
15
+ * `ctx.settingsScope` was removed in dsh 0.2 — LESSONS §33); runtime facts
16
+ * come from the same-origin `/dsh-proxy-pro/api` endpoints served by the host
16
17
  * half (browser-trust fenced, see lib/index.js).
17
18
  *
18
19
  * This hand-written module mirrors the loader format the in-repo client
@@ -107,18 +108,85 @@ window.__ModuleLoader__.load({
107
108
  .catch(function () { return null; });
108
109
  }
109
110
 
110
- // ---- reactive settings scope (the `proxy` namespace) ----
111
- /** @returns `{ value, writable, ready }` from a bound SettingsScope. */
112
- function useProxySnapshot(scope) {
111
+ // ---- host-backed configuration store (dsh >= 0.2) ------------------
112
+ // The browser settings transport (`ctx.settingsScope`) was removed in dsh
113
+ // 0.2 (LESSONS §33), so the capsule and the settings page read/write the
114
+ // host's own /config API. The snapshot shape mirrors the old settings
115
+ // scope (`{ status, value, writable }`), so the render code below is
116
+ // unchanged; an older host simply has no /config route and the panel
117
+ // reports itself unavailable instead of crashing.
118
+ function createProxyStore() {
119
+ var listeners = [];
120
+ var snapshot = { status: "loading", value: undefined, writable: false };
121
+ var inflight = null;
122
+ function emit(next) {
123
+ snapshot = next;
124
+ listeners.slice().forEach(function (fn) {
125
+ try { fn(); } catch (error) { console.error("[dsh-proxy-pro] listener failed:", error); }
126
+ });
127
+ }
128
+ function accept(data) {
129
+ if (data === null || data.ok !== true || data.config === undefined || data.config === null) {
130
+ emit({ status: "unavailable", value: undefined, writable: false });
131
+ return false;
132
+ }
133
+ emit({ status: "ready", value: data.config, writable: data.writable !== false });
134
+ return true;
135
+ }
136
+ function refresh() {
137
+ if (inflight !== null) return inflight;
138
+ inflight = fetchJson("/config").then(function (data) {
139
+ inflight = null;
140
+ accept(data);
141
+ return data;
142
+ }, function () { inflight = null; return null; });
143
+ return inflight;
144
+ }
145
+ function set(field, value) {
146
+ var body = {};
147
+ body[field] = value;
148
+ return fetchJson("/config", {
149
+ method: "POST",
150
+ headers: { "content-type": "application/json" },
151
+ body: JSON.stringify(body),
152
+ }).then(function (data) {
153
+ if (data === null || data.ok !== true) {
154
+ throw new Error((data && data.error) || "config write failed");
155
+ }
156
+ accept(data);
157
+ });
158
+ }
159
+ return {
160
+ subscribe: function (fn) {
161
+ listeners.push(fn);
162
+ return function () {
163
+ var index = listeners.indexOf(fn);
164
+ if (index >= 0) listeners.splice(index, 1);
165
+ };
166
+ },
167
+ getSnapshot: function () { return snapshot; },
168
+ refresh: refresh,
169
+ set: set,
170
+ setEnabled: function (next) { return set("enabled", next === true); },
171
+ };
172
+ }
173
+
174
+ /** Subscribe to the host-backed config store → `{ value, writable, ready }`. */
175
+ function useProxySettings(store) {
113
176
  var snapshot = useSyncExternalStore(
114
- function (onChange) { return scope.subscribe(onChange); },
115
- function () { return scope.getSnapshot(); }
177
+ function (onChange) {
178
+ var unsubscribe = store.subscribe(onChange);
179
+ store.refresh();
180
+ return unsubscribe;
181
+ },
182
+ function () { return store.getSnapshot(); }
116
183
  );
117
184
  var ready = snapshot.status === "ready" && snapshot.value !== undefined;
118
185
  return {
119
186
  value: ready ? snapshot.value : undefined,
120
187
  writable: snapshot.writable === true && ready,
121
188
  ready: snapshot.status === "unavailable" ? false : snapshot.status === "ready",
189
+ unavailable: snapshot.status === "unavailable",
122
190
  };
123
191
  }
124
192
 
@@ -177,26 +245,35 @@ window.__ModuleLoader__.load({
177
245
 
178
246
  // ---- conversation header capsule button ----
179
247
  function ProxyHeaderButton(props) {
180
- var scope = props.scope;
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).
248
+ var store = props.store;
249
+ var settings = useProxySettings(store);
250
+ // Local optimistic value (2026-09-18): the capsule flips its visual
251
+ // immediately on click instead of waiting for scope.set's round-trip
252
+ // (the settings-page switch already behaves this way). null means no
253
+ // unconfirmed value — the capsule mirrors settings.value.
254
+ var optimistic = useState(null);
255
+ var setOptimistic = optimistic[1];
188
256
  var config = settings.value || {};
189
- var active = config.enabled === true && config.mode !== "none";
190
- var enabled = config.enabled === true;
257
+ var valueEnabled = config.enabled === true;
258
+ var shownEnabled = optimistic[0] !== null ? optimistic[0] : valueEnabled;
259
+ var active = shownEnabled === true && (config.mode || "system") !== "none";
191
260
  var mode = config.mode || "system";
192
261
  var modeLabel = mode === "system" ? "system" : mode === "custom" ? "custom" : "none";
262
+ // Once the host has committed the new value, drop the optimistic echo.
263
+ useEffect(function () {
264
+ if (optimistic[0] !== null && settings.ready && settings.value !== undefined && settings.value.enabled === optimistic[0]) {
265
+ setOptimistic(null);
266
+ }
267
+ }, [optimistic[0], settings.value, settings.ready]);
193
268
  function toggle() {
194
269
  if (!settings.writable) return;
195
- setBusy(true);
196
- var next = !enabled;
197
- scope.set("enabled", next).catch(function (error) {
270
+ var next = !shownEnabled;
271
+ setOptimistic(next);
272
+ store.setEnabled(next).catch(function (error) {
198
273
  console.error("[dsh-proxy-pro] toggle failed:", error);
199
- }).finally(function () { setBusy(false); });
274
+ // Roll back to the committed value on failure.
275
+ setOptimistic(null);
276
+ });
200
277
  // Toggle doubles as a manual "recheck now" even when the settings
201
278
  // panel is not mounted: pulling /status makes the host re-read the
202
279
  // system facts (idempotent — snapshot refresh unless the proxy really
@@ -216,7 +293,7 @@ window.__ModuleLoader__.load({
216
293
  "data-active": active ? "true" : "false",
217
294
  "data-proxy-toggle": true,
218
295
  onClick: toggle,
219
- disabled: !settings.writable || busy[0],
296
+ disabled: !settings.writable,
220
297
  title: title,
221
298
  "aria-pressed": active,
222
299
  },
@@ -227,8 +304,8 @@ window.__ModuleLoader__.load({
227
304
 
228
305
  // ---- Settings page: 代理管理 ----
229
306
  function ProxyManagerPanel(props) {
230
- var scope = props.scope;
231
- var settings = useProxySnapshot(scope);
307
+ var store = props.store;
308
+ var settings = useProxySettings(store);
232
309
  // Re-fetch the host snapshot when the configuration changes (enabled /
233
310
  // mode / customUrl / noProxy are the only inputs that move the effKey);
234
311
  // no timed polling, mount + change-driven only (§17.3d).
@@ -251,7 +328,10 @@ window.__ModuleLoader__.load({
251
328
 
252
329
  if (!settings.ready) {
253
330
  return react.createElement("div", { className: "dsxprx", "data-proxy-settings": true },
254
- react.createElement("p", { className: "dsxprx-hint" }, "代理设置加载中…"));
331
+ react.createElement("p", { className: "dsxprx-hint" },
332
+ settings.unavailable
333
+ ? "宿主未提供配置接口(/dsh-proxy-pro/api/config)——请确认插件已完整安装、宿主已重启。"
334
+ : "代理设置加载中…"));
255
335
  }
256
336
  var value = settings.value || { enabled: false, mode: "system", customUrl: "", noProxy: "" };
257
337
  var writable = settings.writable;
@@ -297,7 +377,7 @@ window.__ModuleLoader__.load({
297
377
 
298
378
  function setField(field) {
299
379
  return function (next) {
300
- scope.set(field, next).catch(function (error) {
380
+ store.set(field, next).catch(function (error) {
301
381
  console.error("[dsh-proxy-pro] save failed:", error);
302
382
  });
303
383
  };
@@ -422,6 +502,7 @@ window.__ModuleLoader__.load({
422
502
  ? react.createElement("span", null, "诊断失败:" + diag.error)
423
503
  : [
424
504
  react.createElement("span", { key: "r" }, "策略判定: " + diag.route + (diag.bypassed ? "(NO_PROXY 命中)" : "")),
505
+ react.createElement("span", { key: "codes" }, "状态码: 走代理 " + ((diag.proxy && diag.proxy.status) || "—") + " · 直连 " + ((diag.direct && diag.direct.status) || "—")),
425
506
  react.createElement("span", { key: "px", className: "dsxprx-diagprobe" },
426
507
  react.createElement("span", { className: "dsxprx-diotag" }, "走代理"),
427
508
  react.createElement("span", { className: "dsxprx-verdict", "data-tone": channelBadge("proxyBadge").tone }, channelBadge("proxyBadge").text)
@@ -447,12 +528,13 @@ window.__ModuleLoader__.load({
447
528
  }
448
529
 
449
530
  /**
450
- * Browser plugin body: bind the `proxy` settings scope, then register the
451
- * conversation header button and the 代理配置 settings section.
531
+ * Browser plugin body: create the host-backed config store, then register
532
+ * the conversation header button and the 代理配置 settings section.
452
533
  * @param ctx - client cordis context.
453
534
  */
454
535
  function apply(ctx) {
455
- var scope = ctx.settingsScope.bind({ namespace: "proxy" });
536
+ var store = createProxyStore();
537
+ ctx.effect(function () { return function () { /* no timers to clear */ }; }, "dsh-proxy-pro: config store");
456
538
  ctx.slots.inject("settings.section", function () {
457
539
  return ctx.slots.register({
458
540
  name: "settings.section",
@@ -460,7 +542,7 @@ window.__ModuleLoader__.load({
460
542
  order: 20,
461
543
  label: "代理配置",
462
544
  }, function Section() {
463
- return react.createElement(ProxyManagerPanel, { scope: scope });
545
+ return react.createElement(ProxyManagerPanel, { store: store });
464
546
  });
465
547
  });
466
548
  ctx.slots.inject("conversation.session.header.utilities", function () {
@@ -470,7 +552,7 @@ window.__ModuleLoader__.load({
470
552
  order: 10,
471
553
  label: "代理",
472
554
  }, function Header() {
473
- return react.createElement(ProxyHeaderButton, { scope: scope });
555
+ return react.createElement(ProxyHeaderButton, { store: store });
474
556
  });
475
557
  });
476
558
  }
@@ -480,10 +562,11 @@ window.__ModuleLoader__.load({
480
562
  // Cordis service names the browser loader must wait for — NOT package
481
563
  // names (those belong in package.json's dsh.client.inject). Writing
482
564
  // package names here keeps the plugin permanently pending and blocks
483
- // web boot (see docs/LESSONS.md §6).
565
+ // web boot (see docs/LESSONS.md §6). `settingsScope` was one of these
566
+ // until dsh 0.2 removed that service; requiring it now would keep the
567
+ // whole client half permanently pending (LESSONS §33).
484
568
  exports.inject = [
485
569
  "slots",
486
- "settingsScope",
487
570
  ];
488
571
  return module.exports;
489
572
  },
package/lib/index.js CHANGED
@@ -33,10 +33,14 @@ import {
33
33
  classifyProbeStatus,
34
34
  classifyTargetFailure,
35
35
  combineDualProbe,
36
+ configPayload,
37
+ DEFAULT_ENTRY_ID,
38
+ discoverEntryId,
36
39
  headNeedsGetRetry,
37
40
  keepaliveHint,
38
41
  makeSystemProxyReader,
39
42
  normalizeProxyUrl,
43
+ readEntryConfig,
40
44
  resolveProxyState,
41
45
  summarize,
42
46
  systemFactsEqual,
@@ -114,8 +118,7 @@ function makeEnvLike(effective) {
114
118
  }
115
119
  }
116
120
 
117
- /** Build the agent-facing status text from the latest sync snapshot. */
118
- function renderStatusText(snapshot) {
121
+ /** Build the agent-facing status text from the latest sync snapshot. */function renderStatusText(snapshot) {
119
122
  const effective = snapshot?.effective
120
123
  const config = snapshot?.config
121
124
  if (effective === undefined || config === undefined) {
@@ -478,23 +481,39 @@ function apply(ctx, config) {
478
481
  return syncing
479
482
  }
480
483
 
481
- // Configuration source: the `proxy` settings namespace layered over the
482
- // composition entry; every committed change re-syncs live. Registration goes
483
- // through the settings SERVICE — newer dsh-settings removed the
484
- // `installSettingsSection` export entirely, so importing it throws a
485
- // SyntaxError at boot and kills the whole profile (LESSONS §26).
486
- // register() + scope.get()/watch() exist on every version we target, and
487
- // update() recomputes registration.resolved, so scope.get() stays fresh.
484
+ // Configuration source — two settings generations (LESSONS §26, §33):
485
+ // - dsh <= 0.1.x: a per-plugin namespace registered through the settings
486
+ // service (register(ns, schema, {base}) → scope.get()/watch()).
487
+ // - dsh >= 0.2: register()/watch() are gone. The plugin's OWN composition
488
+ // entry IS the settings form (namespace = entry id, schema = this Config),
489
+ // writes go through settings.update(entryId, patch) and every commit
490
+ // emits `settings/document-updated`, which is what re-syncs us live.
488
491
  const settingsService = ctx.get('settings')
489
- if (settingsService !== undefined && typeof settingsService.register === 'function') {
492
+ const configEditor = ctx.get('configEditor')
493
+ const legacyRegister = typeof settingsService?.register === 'function'
494
+ const entryId = discoverEntryId(configEditor, ctx) ?? DEFAULT_ENTRY_ID
495
+ if (legacyRegister) {
490
496
  const proxyScope = settingsService.register(PROXY_NS, Config, { base: config })
491
497
  source = () => proxyScope.get()
492
498
  proxyScope.watch(() => {
493
499
  void requestSync().catch(() => {})
494
500
  })
501
+ } else if (typeof settingsService?.update === 'function') {
502
+ source = () => readEntryConfig(configEditor, entryId, config)
503
+ ctx.on('settings/document-updated', (ns) => {
504
+ if (ns === entryId) void requestSync().catch(() => {})
505
+ })
506
+ }
507
+ // Without either generation the plugin still runs read-only on the
508
+ // composition config (source keeps returning it).
509
+
510
+ /** Persist one config patch through whichever settings generation is present. */
511
+ const writeSettings = async (patch) => {
512
+ if (settingsService === undefined) {
513
+ throw new Error('the settings service is unavailable in this composition — change the proxy configuration in the composition instead')
514
+ }
515
+ await settingsService.update(legacyRegister ? PROXY_NS : entryId, patch)
495
516
  }
496
- // Without a settings service in this composition, `source` keeps the
497
- // composition default (initialized above) and the plugin still runs, read-only.
498
517
 
499
518
  // ---------- Agent tools ----------
500
519
 
@@ -538,11 +557,11 @@ function apply(ctx, config) {
538
557
  }],
539
558
  },
540
559
  execute: async (args) => {
541
- const settings = ctx.get('settings')
542
- if (settings === undefined) {
543
- throw new Error('proxy_set: the settings service is unavailable in this composition — change the proxy config instead')
560
+ try {
561
+ await writeSettings({ enabled: args.enabled === true })
562
+ } catch (error) {
563
+ throw new Error(`proxy_set: ${error?.message ?? error}`)
544
564
  }
545
- await settings.update(PROXY_NS, { enabled: args.enabled === true })
546
565
  await requestSync()
547
566
  return summarize(snapshot)
548
567
  },
@@ -565,6 +584,7 @@ function apply(ctx, config) {
565
584
  const lines = [
566
585
  `路由: ${value.route}`,
567
586
  `NO_PROXY 命中: ${value.bypassed}`,
587
+ `状态码: 走代理 ${value.proxy?.status ?? '—'} · 直连 ${value.direct?.status ?? '—'}`,
568
588
  `走代理: ${badge(value.proxyBadge)}`,
569
589
  `直连: ${badge(value.directBadge)}`,
570
590
  `结论: ${value.short ?? '未知'}`,
@@ -603,10 +623,6 @@ function apply(ctx, config) {
603
623
  }],
604
624
  },
605
625
  execute: async (args) => {
606
- const settings = ctx.get('settings')
607
- if (settings === undefined) {
608
- throw new Error('proxy_config: the settings service is unavailable in this composition — change the proxy configuration in Settings instead')
609
- }
610
626
  const patch = {}
611
627
  if (args.enabled !== undefined) patch.enabled = args.enabled === true
612
628
  if (args.mode !== undefined) patch.mode = args.mode
@@ -623,7 +639,7 @@ function apply(ctx, config) {
623
639
  }
624
640
  }
625
641
  if (args.noProxy !== undefined) patch.noProxy = String(args.noProxy).trim()
626
- if (Object.keys(patch).length > 0) await settings.update(PROXY_NS, patch)
642
+ if (Object.keys(patch).length > 0) await writeSettings(patch)
627
643
  await requestSync()
628
644
  return summarize(snapshot)
629
645
  },
@@ -708,16 +724,58 @@ function apply(ctx, config) {
708
724
  }
709
725
  if (is('/toggle', 'POST')) {
710
726
  const body = await readJsonBody(req)
711
- const settings = ctx.get('settings')
712
- if (settings === undefined) {
713
- sendJson(res, 503, { ok: false, error: 'settings service unavailable' })
727
+ try {
728
+ await writeSettings({ enabled: body.enabled === true })
729
+ } catch (error) {
730
+ sendJson(res, 503, { ok: false, error: String(error?.message ?? error) })
714
731
  return
715
732
  }
716
- await settings.update(PROXY_NS, { enabled: body.enabled === true })
717
733
  await requestSync()
718
734
  sendJson(res, 200, { ok: true, status: summarize(snapshot) })
719
735
  return
720
736
  }
737
+ // Editable configuration for the client panel. dsh >= 0.2 removed
738
+ // the browser settings transport (`ctx.settingsScope`), so the panel
739
+ // reads and writes through this API instead (LESSONS §33).
740
+ if (is('/config', 'GET')) {
741
+ sendJson(res, 200, { ok: true, ...configPayload(source()) })
742
+ return
743
+ }
744
+ if (is('/config', 'POST')) {
745
+ const body = await readJsonBody(req)
746
+ const patch = {}
747
+ if (body.enabled !== undefined) patch.enabled = body.enabled === true
748
+ if (body.mode !== undefined) {
749
+ if (body.mode !== 'system' && body.mode !== 'custom') {
750
+ sendJson(res, 400, { ok: false, error: 'mode must be "system" or "custom"' })
751
+ return
752
+ }
753
+ patch.mode = body.mode
754
+ }
755
+ if (body.customUrl !== undefined) {
756
+ const raw = String(body.customUrl).trim()
757
+ if (raw === '') {
758
+ patch.customUrl = ''
759
+ } else {
760
+ const normalized = normalizeProxyUrl(raw)
761
+ if (normalized === null) {
762
+ sendJson(res, 400, { ok: false, error: `"${body.customUrl}" is not a usable proxy address — expected http(s)://host:port` })
763
+ return
764
+ }
765
+ patch.customUrl = normalized
766
+ }
767
+ }
768
+ if (body.noProxy !== undefined) patch.noProxy = String(body.noProxy).trim()
769
+ try {
770
+ if (Object.keys(patch).length > 0) await writeSettings(patch)
771
+ } catch (error) {
772
+ sendJson(res, 503, { ok: false, error: String(error?.message ?? error) })
773
+ return
774
+ }
775
+ await requestSync()
776
+ sendJson(res, 200, { ok: true, ...configPayload(source()) })
777
+ return
778
+ }
721
779
  sendJson(res, 404, { ok: false, error: 'not found' })
722
780
  } catch (error) {
723
781
  sendJson(res, 500, { ok: false, error: String(error?.message ?? error) })
package/lib/proxy-core.js CHANGED
@@ -15,6 +15,60 @@
15
15
  const PROXY_REG_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'
16
16
  const LOCAL_ENTRIES = ['localhost', '127.0.0.1', '::1']
17
17
 
18
+ /** Bundle row id (cordis.patch.yml) — also the settings namespace on dsh >= 0.2,
19
+ * where a plugin's OWN composition entry is its settings form (LESSONS §33). */
20
+ export const DEFAULT_ENTRY_ID = 'dsh-proxy-pro'
21
+
22
+ /**
23
+ * Resolve this plugin's composition entry id — the settings namespace on
24
+ * dsh >= 0.2 (LESSONS §33). Prefers the row whose fiber IS this context, then
25
+ * the known bundle id; undefined when the editor cannot be read.
26
+ */
27
+ export function discoverEntryId(configEditor, ctx) {
28
+ try {
29
+ const rows = configEditor?.configuration?.() ?? []
30
+ const own = rows.find((row) => row?.entry?.fiber === ctx?.fiber)
31
+ ?? rows.find((row) => row?.entry?.options?.id === DEFAULT_ENTRY_ID)
32
+ const id = own?.entry?.options?.id
33
+ return typeof id === 'string' && id.length > 0 ? id : undefined
34
+ } catch {
35
+ return undefined
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Effective config of one entry: the composed layers, then the profile patch.
41
+ * Every Config field is a scalar, so a shallow merge is exact.
42
+ */
43
+ export function readEntryConfig(configEditor, entryId, fallback) {
44
+ try {
45
+ const rows = configEditor?.configuration?.() ?? []
46
+ const own = rows.find((row) => row?.entry?.options?.id === entryId)
47
+ if (own !== undefined) return { ...fallback, ...(own.inherited ?? {}), ...(own.override ?? {}) }
48
+ } catch {
49
+ /* fall through to the apply-time config */
50
+ }
51
+ return fallback
52
+ }
53
+
54
+ /**
55
+ * Editable configuration payload for the client panel (`/config` API). The
56
+ * browser cannot reach the settings transport on dsh >= 0.2, so the host hands
57
+ * the panel exactly the fields it edits (LESSONS §33).
58
+ */
59
+ export function configPayload(resolved) {
60
+ return {
61
+ config: {
62
+ enabled: resolved?.enabled === true,
63
+ mode: resolved?.mode ?? 'none',
64
+ customUrl: resolved?.customUrl ?? '',
65
+ noProxy: resolved?.noProxy ?? '',
66
+ systemPollMs: resolved?.systemPollMs ?? 0,
67
+ },
68
+ writable: true,
69
+ }
70
+ }
71
+
18
72
  /** Normalize a bare `host:port` or already-schemed URL into an http(s) URL. */
19
73
  export function normalizeProxyUrl(value) {
20
74
  if (typeof value !== 'string') return null
@@ -429,14 +483,18 @@ export function classifyTargetFailure(error) {
429
483
  * 链路不通 → 截图"代理与直连均失败"误导(2026-09-18 用户实况)。
430
484
  */
431
485
  function dualBadge(probe) {
432
- const detail = (s) => String(s ?? '').replace(/^HTTP \d+ — /, '')
486
+ // Keep the HTTP code in the copy (2026-09-18 用户要求:展示原因时直接看到
487
+ // 状态码,而不是只有人话)。连接层失败没有 status,保持原文案。
488
+ const detail = (s) => String(s ?? '')
433
489
  if (!probe) return { text: '结果未知', tone: 'warn' }
434
- if (probe.verdict === 'usable') return { text: '链路通 · 目标正常响应', tone: 'ok' }
490
+ if (probe.verdict === 'usable') {
491
+ return { text: Number.isFinite(probe.status) ? `链路通 · 目标正常响应(HTTP ${probe.status})` : '链路通 · 目标正常响应', tone: 'ok' }
492
+ }
435
493
  if (probe.verdict === 'degraded') {
436
494
  return { text: `链路通 · 目标拒绝(${detail(probe.short) || '请求被拒'})`, tone: 'warn' }
437
495
  }
438
496
  if (probe.kind === 'gateway' || probe.kind === 'server-error') {
439
- return { text: `链路通 · 目标/网关报错(${detail(probe.short) || String(probe.status ?? '5xx')})`, tone: 'warn' }
497
+ return { text: `链路通 · 目标/网关报错(${detail(probe.short) || `HTTP ${probe.status ?? '5xx'}`})`, tone: 'warn' }
440
498
  }
441
499
  if (probe.kind === 'auth') return { text: '链路不通 · 代理要求认证', tone: 'bad' }
442
500
  return { text: `链路不通 · ${detail(probe.short) || '连接失败'}`, tone: 'bad' }
@@ -474,7 +532,9 @@ export function combineDualProbe(proxyProbe, directProbe) {
474
532
  || (p.verdict === 'unusable' && /NTLM|407/.test(String(p.short ?? '')))
475
533
 
476
534
  const notReachable = (probe, label) => {
477
- const detail = (probe?.short ?? '未知原因').replace(/^HTTP \d+ — /, '')
535
+ // Keep the HTTP code — the reason line should name the status
536
+ // (2026-09-18: user asked for the code to be visible where the cause is shown).
537
+ const detail = probe?.short ?? '未知原因'
478
538
  return `${label}:${detail}`
479
539
  }
480
540
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yanglaofish/dsh-proxy-pro",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
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",
@@ -40,10 +40,10 @@
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@deepseek-ai/cordis": "^4.0.0",
43
- "@deepseek-ai/dsh-http-proxy": "^0.1.5-rc.2",
44
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
45
- "@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2",
46
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
43
+ "@deepseek-ai/dsh-http-proxy": ">=0.1.5-rc.2 <0.3.0",
44
+ "@deepseek-ai/dsh-settings": ">=0.1.5-rc.2 <0.3.0",
45
+ "@deepseek-ai/dsh-system-prompt": ">=0.1.5-rc.2 <0.3.0",
46
+ "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2 <0.3.0",
47
47
  "@deepseek-ai/schemastery": "^3.18.0"
48
48
  },
49
49
  "dsh": {