@karoc/dsh-proxy 0.1.4 → 0.1.18

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
@@ -7,6 +7,28 @@ window.__ModuleLoader__.load({
7
7
  let react = require("react");
8
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/host-match.ts
11
+ /**
12
+ * Normalize a host so ONE comparison covers every spelling it arrives in.
13
+ * @param host - hostname, possibly bracketed (`[::1]`) or IPv4-mapped.
14
+ * @returns the comparable form (brackets stripped, IPv4-mapped collapsed).
15
+ */
16
+ function normalizeHostForMatch(host) {
17
+ const bare = String(host ?? "").trim().toLowerCase().replace(/^\[|\]$/g, "");
18
+ const dotted = bare.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
19
+ if (dotted !== null) return dotted[1];
20
+ const mapped = bare.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
21
+ if (mapped === null) return bare;
22
+ const hi = Number.parseInt(mapped[1], 16);
23
+ const lo = Number.parseInt(mapped[2], 16);
24
+ return [
25
+ hi >> 8,
26
+ hi & 255,
27
+ lo >> 8,
28
+ lo & 255
29
+ ].join(".");
30
+ }
31
+ //#endregion
10
32
  //#region src/client/ProxySection.tsx
11
33
  /**
12
34
  * The "Smoothly Proxy" settings section (external plugin).
@@ -16,11 +38,11 @@ window.__ModuleLoader__.load({
16
38
  * host) through an optional upstream proxy, per host. The host half runs the
17
39
  * proxy and serves /proxy/api; this page reads and writes it with plain
18
40
  * same-origin fetch (no settingsScope — the proxy config lives in
19
- * <DSH_HOME>/proxy.json, not in dsh settings.yaml).
41
+ * <DSH_HOME>/proxy.json, not in the dsh settings document).
20
42
  *
21
43
  * UI mirrors the dsh-desktop settings window: upstream card (enabled /
22
44
  * protocol selector / host / port / username / password / test connection),
23
- * a "model providers" list derived from dsh settings.yaml, and an "other
45
+ * a "model providers" list derived from the LIVE settings document, and an "other
24
46
  * observed hosts" list, each with a per-host checkbox. A search box filters
25
47
  * both lists. Saving writes the config; the running proxy re-reads proxy.json
26
48
  * on every request, so changes take effect immediately.
@@ -28,15 +50,15 @@ window.__ModuleLoader__.load({
28
50
  const PROTOCOLS = [
29
51
  {
30
52
  id: "http",
31
- label: "HTTP"
53
+ labelKey: "protocolHttp"
32
54
  },
33
55
  {
34
56
  id: "https",
35
- label: "HTTPS"
57
+ labelKey: "protocolHttps"
36
58
  },
37
59
  {
38
60
  id: "socks5",
39
- label: "SOCKS5"
61
+ labelKey: "protocolSocks5"
40
62
  }
41
63
  ];
42
64
  /** Fetch helper bound to the same-origin /proxy/api endpoint. */
@@ -58,7 +80,7 @@ window.__ModuleLoader__.load({
58
80
  },
59
81
  items: PROTOCOLS.map((o) => ({
60
82
  id: o.id,
61
- label: o.label
83
+ label: t(o.labelKey)
62
84
  })),
63
85
  selectedId: value,
64
86
  onSelect: (id) => {
@@ -76,13 +98,36 @@ window.__ModuleLoader__.load({
76
98
  onClick: () => {
77
99
  setOpen((v) => !v);
78
100
  },
79
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: matched?.label ?? t("protocolHttp") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {})]
101
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(matched?.labelKey ?? "protocolHttp") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {})]
80
102
  })
81
103
  });
82
104
  }
105
+ /** The routing rule: which way the checkboxes read. */
106
+ function RouteModeSelector(props) {
107
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
108
+ className: "dsh-proxy-segmented",
109
+ role: "group",
110
+ "aria-label": props.t("modeLabel"),
111
+ children: [{
112
+ id: "only-listed",
113
+ labelKey: "modeOnlyListed"
114
+ }, {
115
+ id: "except-listed",
116
+ labelKey: "modeExceptListed"
117
+ }].map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
118
+ type: "button",
119
+ className: o.id === props.value ? "is-active" : "",
120
+ "aria-pressed": o.id === props.value,
121
+ onClick: () => {
122
+ props.onChange(o.id);
123
+ },
124
+ children: props.t(o.labelKey)
125
+ }, o.id))
126
+ });
127
+ }
83
128
  /** One host checkbox row (label + route status). */
84
129
  function HostRow(props) {
85
- const { host, label, checked, proxied, onToggle } = props;
130
+ const { host, label, checked, routed, onToggle, t } = props;
86
131
  const labelRef = (0, react.useRef)(null);
87
132
  const [overflowing, setOverflowing] = (0, react.useState)(false);
88
133
  (0, react.useLayoutEffect)(() => {
@@ -126,7 +171,7 @@ window.__ModuleLoader__.load({
126
171
  }) : labelNode,
127
172
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
128
173
  className: "dsh-proxy-host-route",
129
- children: proxied ? "via upstream" : "direct"
174
+ children: routed ? t("viaUpstream") : t("direct")
130
175
  })
131
176
  ]
132
177
  });
@@ -146,6 +191,7 @@ window.__ModuleLoader__.load({
146
191
  const { t } = props;
147
192
  const [view, setView] = (0, react.useState)(void 0);
148
193
  const [error, setError] = (0, react.useState)(void 0);
194
+ const [loadError, setLoadError] = (0, react.useState)(void 0);
149
195
  const [saved, setSaved] = (0, react.useState)(false);
150
196
  const [saving, setSaving] = (0, react.useState)(false);
151
197
  const [testing, setTesting] = (0, react.useState)(false);
@@ -157,6 +203,10 @@ window.__ModuleLoader__.load({
157
203
  const [port, setPort] = (0, react.useState)("");
158
204
  const [username, setUsername] = (0, react.useState)("");
159
205
  const [password, setPassword] = (0, react.useState)("");
206
+ const [listenPort, setListenPort] = (0, react.useState)("");
207
+ const [routeMode, setRouteMode] = (0, react.useState)("only-listed");
208
+ const [advancedOpen, setAdvancedOpen] = (0, react.useState)(void 0);
209
+ const [adoptedPort, setAdoptedPort] = (0, react.useState)(false);
160
210
  const [proxied, setProxied] = (0, react.useState)(/* @__PURE__ */ new Set());
161
211
  /** Load the config view from /proxy/api. */
162
212
  const load = (0, react.useCallback)(async () => {
@@ -164,15 +214,19 @@ window.__ModuleLoader__.load({
164
214
  const data = await proxyApi();
165
215
  setView(data);
166
216
  setError(void 0);
217
+ setLoadError(void 0);
167
218
  setEnabled(data.upstream.enabled);
168
219
  setProtocol(data.upstream.protocol);
169
220
  setHost(data.upstream.host);
170
221
  setPort(data.upstream.port > 0 ? String(data.upstream.port) : "");
171
222
  setUsername(data.upstream.username);
172
223
  setPassword(data.upstream.password);
224
+ setListenPort(data.listenPort > 0 ? String(data.listenPort) : "");
225
+ setAdoptedPort(data.listenPort === 0 && data.portRequested > 0);
226
+ setRouteMode(data.routeMode ?? "only-listed");
173
227
  setProxied(new Set(data.proxiedHosts));
174
228
  } catch (err) {
175
- setError(err instanceof Error ? err.message : String(err));
229
+ setLoadError(err instanceof Error ? err.message : String(err));
176
230
  }
177
231
  }, []);
178
232
  (0, react.useEffect)(() => {
@@ -231,7 +285,9 @@ window.__ModuleLoader__.load({
231
285
  username: username.trim(),
232
286
  password
233
287
  },
234
- proxiedHosts: [...proxied]
288
+ proxiedHosts: [...proxied],
289
+ routeMode,
290
+ listenPort: Number(listenPort) || 0
235
291
  })
236
292
  })).ok !== true) throw new Error("save rejected");
237
293
  setSaved(true);
@@ -242,6 +298,19 @@ window.__ModuleLoader__.load({
242
298
  setSaving(false);
243
299
  }
244
300
  };
301
+ /** Drop the observed-host list (live set + proxy.json) and refresh the view. */
302
+ const clearObserved = async () => {
303
+ try {
304
+ await proxyApi({
305
+ method: "POST",
306
+ headers: { "content-type": "application/json" },
307
+ body: JSON.stringify({ op: "forget" })
308
+ });
309
+ await load();
310
+ } catch (err) {
311
+ setError(err instanceof Error ? err.message : String(err));
312
+ }
313
+ };
245
314
  const test = async () => {
246
315
  setTesting(true);
247
316
  setTestResult(void 0);
@@ -274,8 +343,17 @@ window.__ModuleLoader__.load({
274
343
  setTesting(false);
275
344
  }
276
345
  };
346
+ const selfLoop = view !== void 0 && view.upstream.enabled && /^(127\.\d{1,3}\.\d{1,3}\.\d{1,3}|localhost|::1)$/i.test(view.upstream.host.trim()) && view.upstream.port === view.port;
347
+ const upstreamUsable = view?.upstream.enabled === true && !selfLoop;
348
+ const routedHosts = new Set([...proxied].map((h) => normalizeHostForMatch(h)));
349
+ const isRouted = (host) => upstreamUsable && (routeMode === "except-listed" ? !routedHosts.has(normalizeHostForMatch(host)) : routedHosts.has(normalizeHostForMatch(host)));
277
350
  const providerFiltered = providerHosts.filter((e) => match(e.label));
278
351
  const otherFiltered = otherHosts.filter((e) => match(e.label));
352
+ const launchPort = (view?.listenPort ?? 0) > 0 ? view.listenPort : view?.port ?? 0;
353
+ const upstreamIsLoopback = /^(127\.\d{1,3}\.\d{1,3}\.\d{1,3}|localhost|::1)$/i.test((view?.upstream.host ?? "").trim());
354
+ const portConflictsUpstream = view !== void 0 && view.listenPort > 0 && upstreamIsLoopback && view.upstream.port === view.listenPort;
355
+ const diagnosticsNeedAttention = view !== void 0 && (portConflictsUpstream || view.egressStatus === void 0 || view.egressStatus !== "self-installed" || !view.portHonored && view.portRequested > 0 || view.listenPort > 0 && view.listenPort !== view.portRequested);
356
+ const diagnosticsOpen = advancedOpen ?? diagnosticsNeedAttention;
279
357
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
280
358
  className: "dsh-proxy-section",
281
359
  children: [
@@ -366,8 +444,95 @@ window.__ModuleLoader__.load({
366
444
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
367
445
  className: "dsh-proxy-group",
368
446
  children: [
369
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("proxiedHostsTitle") }),
370
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("proxiedHostsHint") }),
447
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("egressTitle") }),
448
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("egressHint") }),
449
+ view === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
450
+ className: "dsh-proxy-egress",
451
+ children: [
452
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
453
+ className: `dsh-proxy-note ${view.egressStatus === void 0 ? "" : view.inProcessRouted ? "dsh-proxy-note-ok" : "dsh-proxy-note-warn"}`,
454
+ children: view.egressStatus === void 0 ? t("egressUnknown") : (view.inProcessRouted ? view.egressStatus === "launcher" ? t("egressRoutedLauncher") : t("egressRoutedSelf") : view.egressStatus === "launcher" ? t("egressLauncherOther") : t("egressUnavailable")).replace("{port}", String(view.port ?? "")).replace("{proxy}", String(view.launchProxy ?? "")).replace("{detail}", String(view.egressDetail ?? ""))
455
+ }),
456
+ !view.portHonored && view.portRequested > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
457
+ className: "dsh-proxy-note dsh-proxy-note-warn",
458
+ children: t("portFallback").replace("{requested}", String(view.portRequested)).replace("{bound}", String(view.port ?? "?"))
459
+ }),
460
+ portConflictsUpstream && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
461
+ className: "dsh-proxy-note dsh-proxy-note-warn",
462
+ children: t("portConflictsUpstream").replace(/\{port\}/g, String(view?.listenPort ?? ""))
463
+ }),
464
+ view.listenPort > 0 && view.listenPort !== view.portRequested && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
465
+ className: "dsh-proxy-note dsh-proxy-note-warn",
466
+ children: t("portRestartPending").replace("{listenPort}", String(view.listenPort)).replace("{port}", String(view.port ?? "?"))
467
+ }),
468
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
469
+ type: "button",
470
+ className: "dsh-proxy-disclosure",
471
+ "aria-expanded": diagnosticsOpen,
472
+ onClick: () => {
473
+ setAdvancedOpen(!diagnosticsOpen);
474
+ },
475
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, {}), t("advancedToggle")]
476
+ }),
477
+ diagnosticsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
478
+ className: "dsh-proxy-diag",
479
+ children: [
480
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
481
+ className: "dsh-proxy-note",
482
+ children: (adoptedPort ? t("listenAddressAdopted") : t("listenAddress")).replace("{port}", String(view.port ?? "?"))
483
+ }),
484
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("diagHint") }),
485
+ view.egressStatus === "unavailable" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("egressSnippetLabel") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
486
+ className: "dsh-proxy-snippet",
487
+ children: `Environment="HTTP_PROXY=http://127.0.0.1:${launchPort}" "HTTPS_PROXY=http://127.0.0.1:${launchPort}" "ALL_PROXY=http://127.0.0.1:${launchPort}" "NO_PROXY=127.0.0.1,localhost,::1" "NODE_USE_ENV_PROXY=1"`
488
+ })] }),
489
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
490
+ className: "dsh-proxy-row",
491
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
492
+ className: "dsh-proxy-proto",
493
+ children: t("listenPortLabel")
494
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
495
+ className: "dsh-proxy-port",
496
+ placeholder: t("listenPortPlaceholder"),
497
+ value: listenPort,
498
+ onChange: (e) => {
499
+ setListenPort(e.target.value);
500
+ setSaved(false);
501
+ },
502
+ autoComplete: "off",
503
+ spellCheck: false
504
+ })]
505
+ }),
506
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("listenPortHint") })
507
+ ]
508
+ })
509
+ ]
510
+ })
511
+ ]
512
+ }),
513
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
514
+ className: "dsh-proxy-group",
515
+ children: [
516
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
517
+ className: "dsh-proxy-row",
518
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
519
+ className: "dsh-proxy-proto",
520
+ children: t("modeLabel")
521
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouteModeSelector, {
522
+ value: routeMode,
523
+ onChange: (v) => {
524
+ setRouteMode(v);
525
+ setSaved(false);
526
+ },
527
+ t
528
+ })]
529
+ }),
530
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: routeMode === "except-listed" ? t("directHostsTitle") : t("proxiedHostsTitle") }),
531
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: routeMode === "except-listed" ? t("directHostsHint") : t("proxiedHostsHint") }),
532
+ selfLoop && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
533
+ className: "dsh-proxy-note dsh-proxy-note-warn",
534
+ children: t("selfLoopWarning")
535
+ }),
371
536
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
372
537
  className: "dsh-proxy-search",
373
538
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGlobeOutlineRegular, {}),
@@ -394,17 +559,29 @@ window.__ModuleLoader__.load({
394
559
  }) : providerFiltered.map((e) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostRow, {
395
560
  host: e.host,
396
561
  label: e.label,
397
- checked: proxied.has(e.host),
398
- proxied: proxied.has(e.host),
399
- onToggle: toggleHost
562
+ checked: routedHosts.has(normalizeHostForMatch(e.host)),
563
+ routed: isRouted(e.host),
564
+ onToggle: toggleHost,
565
+ t
400
566
  }, e.host))
401
567
  }),
402
568
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
403
569
  className: "dsh-proxy-heading",
404
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("othersHeading") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
405
- className: "dsh-proxy-count",
406
- children: otherHosts.length
407
- })]
570
+ children: [
571
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("othersHeading") }),
572
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
573
+ className: "dsh-proxy-count",
574
+ children: otherHosts.length
575
+ }),
576
+ otherHosts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
577
+ type: "button",
578
+ className: "dsh-proxy-disclosure",
579
+ onClick: () => {
580
+ clearObserved();
581
+ },
582
+ children: t("othersClear")
583
+ })
584
+ ]
408
585
  }),
409
586
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
410
587
  className: "dsh-proxy-hostlist",
@@ -414,9 +591,10 @@ window.__ModuleLoader__.load({
414
591
  }) : otherFiltered.map((e) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostRow, {
415
592
  host: e.host,
416
593
  label: e.label,
417
- checked: proxied.has(e.host),
418
- proxied: proxied.has(e.host),
419
- onToggle: toggleHost
594
+ checked: routedHosts.has(normalizeHostForMatch(e.host)),
595
+ routed: isRouted(e.host),
596
+ onToggle: toggleHost,
597
+ t
420
598
  }, e.host))
421
599
  })
422
600
  ]
@@ -461,6 +639,10 @@ window.__ModuleLoader__.load({
461
639
  " ",
462
640
  error
463
641
  ]
642
+ }),
643
+ loadError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
644
+ className: "dsh-proxy-status dsh-proxy-status-err",
645
+ children: /unauthorized|forbidden|40[13]/.test(loadError) ? t("sessionExpired") : `${t("loadFailed")} ${loadError}`
464
646
  })
465
647
  ]
466
648
  })
@@ -506,6 +688,20 @@ window.__ModuleLoader__.load({
506
688
  .dsh-proxy-status { font-size: 12px; color: var(--dsw-alias-label-tertiary); }
507
689
  .dsh-proxy-status-ok { color: var(--dsw-alias-state-success-primary, var(--dsw-alias-label-primary)); }
508
690
  .dsh-proxy-status-err { color: var(--dsw-alias-state-error-primary); }
691
+ .dsh-proxy-egress { display: flex; flex-direction: column; gap: 8px; }
692
+ .dsh-proxy-note { padding: 8px 10px; border-radius: 10px; font-size: 12px; line-height: 1.5; border: 1px solid var(--dsw-alias-border-l3); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); }
693
+ .dsh-proxy-note-ok { border-color: var(--dsw-alias-state-success-primary, var(--dsw-alias-border-l3)); color: var(--dsw-alias-state-success-primary, var(--dsw-alias-label-primary)); }
694
+ .dsh-proxy-note-warn { border-color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-border-l3)); color: var(--dsw-alias-label-primary); }
695
+ .dsh-proxy-snippet { padding: 8px 10px; border-radius: 10px; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 1.6; word-break: break-all; user-select: all; }
696
+ .dsh-proxy-port { width: 180px; }
697
+ .dsh-proxy-segmented { display: inline-flex; gap: 2px; padding: 2px; border-radius: 999px; background: var(--dsw-alias-bg-module-platform); }
698
+ .dsh-proxy-segmented button { padding: 4px 12px; border: none; border-radius: 999px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 12px; cursor: pointer; }
699
+ .dsh-proxy-segmented button:hover { color: var(--dsw-alias-label-primary); }
700
+ .dsh-proxy-segmented button.is-active { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-on-primary, #fff); }
701
+ .dsh-proxy-disclosure { display: inline-flex; align-items: center; gap: 6px; align-self: flex-start; padding: 4px 8px; border: none; border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 12px; cursor: pointer; }
702
+ .dsh-proxy-disclosure:hover { background: var(--dsw-alias-interactive-bg-hover); }
703
+ .dsh-proxy-disclosure[aria-expanded="true"] svg { transform: rotate(180deg); }
704
+ .dsh-proxy-diag { display: flex; flex-direction: column; gap: 8px; padding: 10px; border-radius: 12px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l3); }
509
705
  .dsh-proxy-heading { display: flex; align-items: center; gap: 6px; margin: 0 0 8px; }
510
706
  .dsh-proxy-heading h4 { margin: 0; font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); }
511
707
  .dsh-proxy-heading .dsh-proxy-count { font-size: 11px; color: var(--dsw-alias-label-tertiary); }
@@ -529,84 +725,127 @@ window.__ModuleLoader__.load({
529
725
  //#region src/client/locales.ts
530
726
  /**
531
727
  * Copy dictionaries for the Smoothly Proxy settings section
532
- * (Smoothly Proxy / 思磨力代理插件). English is the key source (en object
533
- * keyed by the same strings), zh must carry every key.
728
+ * (Smoothly Proxy / 思磨力代理插件). English is the key source (zh must carry
729
+ * every key), and the copy rule for both is: plain words, short sentences, no
730
+ * restating what the screen already shows.
534
731
  */
535
732
  const en = {
536
733
  nav: "Smoothly Proxy",
537
- title: "Smoothly Proxy",
538
- subtitle: "Model-provider traffic through an optional upstream proxy, per host.",
539
734
  upstreamTitle: "Upstream proxy (optional)",
540
- upstreamEnabled: "Enable upstream proxy (when off, every host connects directly)",
735
+ upstreamEnabled: "Send traffic through the upstream proxy",
541
736
  protocolLabel: "Protocol",
542
737
  protocolHttp: "HTTP",
543
- protocolHttps: "HTTPS (TLS to the proxy itself)",
738
+ protocolHttps: "HTTPS",
544
739
  protocolSocks5: "SOCKS5",
545
- protoHint: "Clash / V2Ray style mixed ports (e.g. 7890) usually speak HTTP. SOCKS5 upstreams only forward HTTPS targets (http targets fall back to direct).",
740
+ protoHint: "Mixed ports (e.g. 7890) are usually HTTP. SOCKS5 carries HTTPS targets only.",
546
741
  hostPlaceholder: "Proxy host, e.g. 127.0.0.1",
547
742
  portPlaceholder: "Port, e.g. 7890",
548
743
  usernamePlaceholder: "Username (optional)",
549
744
  passwordPlaceholder: "Password (optional)",
745
+ modeLabel: "Routing rule",
746
+ modeOnlyListed: "Only checked hosts",
747
+ modeExceptListed: "Everything except checked",
550
748
  proxiedHostsTitle: "Hosts going through the proxy",
551
- proxiedHostsHint: "All hosts connect directly unless checked here. Model providers are read from dsh settings (settings.yaml); the rest are hosts observed in traffic (install/update included).",
749
+ directHostsTitle: "Hosts staying direct",
750
+ proxiedHostsHint: "Checked hosts go upstream; the rest connect directly.",
751
+ directHostsHint: "Checked hosts connect directly; everything else goes upstream.",
552
752
  searchPlaceholder: "Search host / provider…",
553
753
  providersHeading: "Model providers",
554
- providersEmpty: "No model providers found yet — they appear here once you configure models in dsh settings.",
754
+ providersEmpty: "No provider endpoints yet (loopback endpoints are never proxyable).",
555
755
  othersHeading: "Other observed hosts",
556
- othersEmpty: "None yet — external hosts dsh has contacted will appear here.",
756
+ othersEmpty: "Hosts dsh contacts show up here.",
757
+ othersClear: "Clear",
758
+ loadFailed: "Could not load the proxy settings.",
759
+ sessionExpired: "This page’s session has expired — refresh the page and try again.",
557
760
  save: "Save",
558
- saveStatus: "Saved — takes effect immediately (no restart needed)",
761
+ saveStatus: "Saved.",
559
762
  saving: "Saving…",
560
763
  saveFailed: "Save failed:",
561
764
  test: "Test connection",
562
765
  testing: "Testing…",
563
- testFailed: "Test failed:",
564
766
  testStatusOk: "Connected",
565
767
  testStatusFail: "Connection failed",
566
- immediateHint: "Saved changes apply immediately; hosts related to install/update take effect on the next install/update.",
768
+ immediateHint: "Applies immediately; install/update hosts take effect on the next install or update.",
567
769
  direct: "direct",
568
770
  viaUpstream: "via upstream",
569
- selfLoopWarning: "The upstream points back at this proxy — treated as disabled (self-loop guard).",
570
- noHost: "No proxy host set",
571
- teardown: "Proxy engine stopped"
771
+ selfLoopWarning: "The upstream points at this plugin itself — ignored.",
772
+ egressTitle: "dsh's own requests",
773
+ egressHint: "Model calls and web fetch are routed here automatically. Nothing to configure.",
774
+ egressRoutedSelf: "Routing through this plugin.",
775
+ egressRoutedLauncher: "Routing through this plugin (named by the launch environment).",
776
+ egressLauncherOther: "The launch environment declares a different proxy ({proxy}), so it keeps handling these requests. Remove it, or point it here, to use this plugin instead.",
777
+ egressUnavailable: "Could not take over automatically ({detail}). Set the line below in the environment dsh starts with, then restart.",
778
+ egressUnknown: "This host half does not report egress state (page newer than plugin). Restart dsh to match.",
779
+ egressSnippetLabel: "Launch environment (systemd: the [Service] section)",
780
+ advancedToggle: "Diagnostics & advanced",
781
+ listenAddress: "Listening on 127.0.0.1:{port} — port picked automatically.",
782
+ listenAddressAdopted: "Listening on 127.0.0.1:{port} — taken from the launch environment.",
783
+ diagHint: "Point other tools here if you want to verify traffic; dsh itself needs no configuration.",
784
+ listenPortLabel: "Fixed listen port (optional — NOT the upstream address)",
785
+ listenPortPlaceholder: "e.g. 45999 (0 = auto)",
786
+ listenPortHint: "Only when a launcher must know this address in advance. The upstream address belongs in the card above.",
787
+ portFallback: "Port {requested} was taken — using {bound} instead. A launcher pointing at {requested} will not reach this plugin.",
788
+ portRestartPending: "Port changed to {listenPort}; restart dsh to apply (currently on {port}).",
789
+ portConflictsUpstream: "{port} is your upstream proxy’s port. That address belongs in the card above: here the plugin cannot bind it, and a launcher pointing at it cannot reach this plugin either."
572
790
  };
573
791
  const zh = {
574
792
  nav: "思磨力代理",
575
- title: "思磨力代理插件",
576
- subtitle: "按主机把模型提供方流量转发到可选的上游代理。",
577
793
  upstreamTitle: "上游代理(可选)",
578
- upstreamEnabled: "启用上游代理(未启用时所有主机直连)",
794
+ upstreamEnabled: "把流量交给上游代理",
579
795
  protocolLabel: "协议",
580
796
  protocolHttp: "HTTP",
581
- protocolHttps: "HTTPS(代理服务器自身加密)",
797
+ protocolHttps: "HTTPS",
582
798
  protocolSocks5: "SOCKS5",
583
- protoHint: "Clash / V2Ray 等常用混合端口(如 7890)一般为 HTTP;SOCKS5 上游仅对 HTTPS 目标生效(http 目标会直连)。",
799
+ protoHint: "混合端口(如 7890)一般选 HTTP;SOCKS5 只承载 HTTPS 目标。",
584
800
  hostPlaceholder: "代理主机,如 127.0.0.1",
585
801
  portPlaceholder: "端口,如 7890",
586
802
  usernamePlaceholder: "用户名(可选)",
587
803
  passwordPlaceholder: "密码(可选)",
804
+ modeLabel: "代理规则",
805
+ modeOnlyListed: "只代理勾选的主机",
806
+ modeExceptListed: "除勾选外全部走代理",
588
807
  proxiedHostsTitle: "走代理的主机",
589
- proxiedHostsHint: "默认全部直连,勾选才走上游代理。模型提供方来自 dsh 设置(settings.yaml);其它为实际访问过的主机(含安装/更新流量)。",
808
+ directHostsTitle: "直连的主机",
809
+ proxiedHostsHint: "勾选的主机走上游,其余直连。",
810
+ directHostsHint: "勾选的主机直连,其余全部走上游。",
590
811
  searchPlaceholder: "搜索主机 / 提供方名称…",
591
812
  providersHeading: "模型提供方",
592
- providersEmpty: "尚未读取到模型提供方(在 dsh 设置里配置模型后会自动出现)",
813
+ providersEmpty: "还没有可代理的模型端点(loopback 端点永远不可代理)。",
593
814
  othersHeading: "其它已观测主机",
594
- othersEmpty: "暂无(dsh 访问过外部主机后会自动出现,含 registry.npmjs.org 等安装流量)",
815
+ othersEmpty: "dsh 访问过的其它主机会出现在这里。",
816
+ othersClear: "清空",
817
+ loadFailed: "读取代理设置失败。",
818
+ sessionExpired: "本页面的登录会话已过期——刷新页面后重试。",
595
819
  save: "保存",
596
- saveStatus: "✓ 已保存,立即生效(无需重启)",
820
+ saveStatus: "已保存。",
597
821
  saving: "保存中…",
598
822
  saveFailed: "保存失败:",
599
823
  test: "测试连接",
600
824
  testing: "测试中…",
601
- testFailed: "测试失败:",
602
- testStatusOk: "✓ 连接成功",
603
- testStatusFail: "✗ 连接失败",
604
- immediateHint: "保存后立即生效,无需重启;改动「安装/更新」相关主机的代理会在下次安装/更新时生效。",
825
+ testStatusOk: "连接成功",
826
+ testStatusFail: "连接失败",
827
+ immediateHint: "立即生效;安装/更新类主机在下次安装或更新时生效。",
605
828
  direct: "直连",
606
- viaUpstream: "走上游代理",
607
- selfLoopWarning: "上游指向本代理自身,视为禁用(防自环保护)。",
608
- noHost: "未填写代理主机",
609
- teardown: "代理引擎已停止"
829
+ viaUpstream: "走上游",
830
+ selfLoopWarning: "上游指向本插件自己,已忽略。",
831
+ egressTitle: "dsh 自己的请求",
832
+ egressHint: "模型调用与 web fetch 已自动走本插件,无需配置。",
833
+ egressRoutedSelf: "已由本插件接管。",
834
+ egressRoutedLauncher: "已由本插件接管(启动环境指名)。",
835
+ egressLauncherOther: "启动环境声明了别的代理({proxy}),这些请求继续归它管。想让它们走本插件,删掉那个声明或把它指向本插件。",
836
+ egressUnavailable: "未能自动接管({detail})。把下面这行加进 dsh 的启动环境后重启。",
837
+ egressUnknown: "宿主半区还没上报出网状态(页面比插件新)。重启 dsh 让版本一致。",
838
+ egressSnippetLabel: "启动环境(systemd:[Service] 段)",
839
+ advancedToggle: "诊断与高级设置",
840
+ listenAddress: "正在监听 127.0.0.1:{port},端口自动选择。",
841
+ listenAddressAdopted: "正在监听 127.0.0.1:{port},取自启动环境。",
842
+ diagHint: "可以把其它工具指到这个地址验证流量;dsh 本身无需配置。",
843
+ listenPortLabel: "本插件监听端口(可选,不是上游地址)",
844
+ listenPortPlaceholder: "如 45999(0 = 自动)",
845
+ listenPortHint: "只有启动器必须提前知道这个地址时才填;上游地址在上面的卡片里。",
846
+ portFallback: "端口 {requested} 被占用,已改用 {bound}。启动环境若指向 {requested},到不了本插件。",
847
+ portRestartPending: "端口已改为 {listenPort},重启 dsh 后生效(当前仍在 {port})。",
848
+ portConflictsUpstream: "{port} 是你上游代理的端口,那个地址属于上面的卡片:填在这里插件绑不上,启动环境指向它也到不了本插件。"
610
849
  };
611
850
  //#endregion
612
851
  //#region src/client/index.ts