@karoc/dsh-proxy 0.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 ADDED
@@ -0,0 +1,645 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-proxy",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/client/ProxySection.tsx
11
+ /**
12
+ * The "Proxy" settings section (external plugin).
13
+ *
14
+ * A companion page to the built-in Models page: it configures a loopback
15
+ * forward proxy that routes model-provider traffic (and any other selected
16
+ * host) through an optional upstream proxy, per host. The host half runs the
17
+ * proxy and serves /proxy/api; this page reads and writes it with plain
18
+ * same-origin fetch (no settingsScope — the proxy config lives in
19
+ * <DSH_HOME>/proxy.json, not in dsh settings.yaml).
20
+ *
21
+ * UI mirrors the dsh-desktop settings window: upstream card (enabled /
22
+ * protocol selector / host / port / username / password / test connection),
23
+ * a "model providers" list derived from dsh settings.yaml, and an "other
24
+ * observed hosts" list, each with a per-host checkbox. A search box filters
25
+ * both lists. Saving writes the config; the running proxy re-reads proxy.json
26
+ * on every request, so changes take effect immediately.
27
+ */
28
+ const PROTOCOLS = [
29
+ {
30
+ id: "http",
31
+ label: "HTTP"
32
+ },
33
+ {
34
+ id: "https",
35
+ label: "HTTPS"
36
+ },
37
+ {
38
+ id: "socks5",
39
+ label: "SOCKS5"
40
+ }
41
+ ];
42
+ /** Fetch helper bound to the same-origin /proxy/api endpoint. */
43
+ async function proxyApi(init) {
44
+ const res = await fetch("/proxy/api", init);
45
+ const body = await res.json();
46
+ if (!res.ok) throw new Error(body.error ?? `proxy/api failed with ${res.status}`);
47
+ return body;
48
+ }
49
+ /** A protocol selector: a pill button opening a Menu (never a native <select>). */
50
+ function ProtocolSelector(props) {
51
+ const { value, onChange, disabled, t } = props;
52
+ const [open, setOpen] = (0, react.useState)(false);
53
+ const matched = PROTOCOLS.find((o) => o.id === value);
54
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
55
+ open,
56
+ onClose: () => {
57
+ setOpen(false);
58
+ },
59
+ items: PROTOCOLS.map((o) => ({
60
+ id: o.id,
61
+ label: o.label
62
+ })),
63
+ selectedId: value,
64
+ onSelect: (id) => {
65
+ onChange(id);
66
+ setOpen(false);
67
+ },
68
+ align: "start",
69
+ portal: true,
70
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
71
+ type: "button",
72
+ className: "dsh-proxy-selector",
73
+ disabled,
74
+ "aria-haspopup": "menu",
75
+ "aria-expanded": open,
76
+ onClick: () => {
77
+ setOpen((v) => !v);
78
+ },
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.IconChevronDownOutline14, {})]
80
+ })
81
+ });
82
+ }
83
+ /** One host checkbox row (label + route status). */
84
+ function HostRow(props) {
85
+ const { host, label, checked, proxied, onToggle } = props;
86
+ const labelRef = (0, react.useRef)(null);
87
+ const [overflowing, setOverflowing] = (0, react.useState)(false);
88
+ (0, react.useLayoutEffect)(() => {
89
+ const el = labelRef.current;
90
+ if (el === null) return;
91
+ const check = () => {
92
+ const over = el.scrollWidth > el.clientWidth + 1;
93
+ setOverflowing((prev) => prev === over ? prev : over);
94
+ };
95
+ check();
96
+ const observer = new ResizeObserver(check);
97
+ observer.observe(el);
98
+ document.fonts?.ready?.then(check).catch(() => {});
99
+ window.addEventListener("resize", check);
100
+ return () => {
101
+ observer.disconnect();
102
+ window.removeEventListener("resize", check);
103
+ };
104
+ });
105
+ const labelNode = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
106
+ ref: labelRef,
107
+ className: "dsh-proxy-host-label",
108
+ children: label
109
+ });
110
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
111
+ className: "dsh-proxy-host",
112
+ children: [
113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
114
+ type: "checkbox",
115
+ checked,
116
+ onChange: () => {
117
+ onToggle(host);
118
+ }
119
+ }),
120
+ overflowing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
121
+ label,
122
+ side: "top",
123
+ maxWidth: 480,
124
+ delayMs: 300,
125
+ children: labelNode
126
+ }) : labelNode,
127
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
128
+ className: "dsh-proxy-host-route",
129
+ children: proxied ? "via upstream" : "direct"
130
+ })
131
+ ]
132
+ });
133
+ }
134
+ /**
135
+ * Render the Proxy settings page.
136
+ * @param props - the inject face.
137
+ * @returns the section, or null while the shell has not injected yet.
138
+ */
139
+ function ProxySection(props) {
140
+ const { t } = props;
141
+ if (t === void 0) return null;
142
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProxySectionLoaded, { t });
143
+ }
144
+ /** The mounted editor (all hooks run unconditionally here). */
145
+ function ProxySectionLoaded(props) {
146
+ const { t } = props;
147
+ const [view, setView] = (0, react.useState)(void 0);
148
+ const [error, setError] = (0, react.useState)(void 0);
149
+ const [saved, setSaved] = (0, react.useState)(false);
150
+ const [saving, setSaving] = (0, react.useState)(false);
151
+ const [testing, setTesting] = (0, react.useState)(false);
152
+ const [testResult, setTestResult] = (0, react.useState)(void 0);
153
+ const [query, setQuery] = (0, react.useState)("");
154
+ const [enabled, setEnabled] = (0, react.useState)(false);
155
+ const [protocol, setProtocol] = (0, react.useState)("http");
156
+ const [host, setHost] = (0, react.useState)("");
157
+ const [port, setPort] = (0, react.useState)("");
158
+ const [username, setUsername] = (0, react.useState)("");
159
+ const [password, setPassword] = (0, react.useState)("");
160
+ const [proxied, setProxied] = (0, react.useState)(/* @__PURE__ */ new Set());
161
+ /** Load the config view from /proxy/api. */
162
+ const load = (0, react.useCallback)(async () => {
163
+ try {
164
+ const data = await proxyApi();
165
+ setView(data);
166
+ setError(void 0);
167
+ setEnabled(data.upstream.enabled);
168
+ setProtocol(data.upstream.protocol);
169
+ setHost(data.upstream.host);
170
+ setPort(data.upstream.port > 0 ? String(data.upstream.port) : "");
171
+ setUsername(data.upstream.username);
172
+ setPassword(data.upstream.password);
173
+ setProxied(new Set(data.proxiedHosts));
174
+ } catch (err) {
175
+ setError(err instanceof Error ? err.message : String(err));
176
+ }
177
+ }, []);
178
+ (0, react.useEffect)(() => {
179
+ load();
180
+ }, [load]);
181
+ const q = query.trim().toLowerCase();
182
+ const match = (label) => q === "" || label.toLowerCase().includes(q);
183
+ const byHost = (0, react.useMemo)(() => {
184
+ const map = /* @__PURE__ */ new Map();
185
+ for (const p of view?.providers ?? []) {
186
+ if (!p.host) continue;
187
+ const label = p.displayName ?? (p.name === "llm-deepseek" ? "DeepSeek" : p.name.split("/").pop() ?? p.host);
188
+ if (!map.has(p.host)) map.set(p.host, []);
189
+ map.get(p.host).push(label);
190
+ }
191
+ return map;
192
+ }, [view]);
193
+ const providerHosts = (0, react.useMemo)(() => [...byHost.entries()].map(([host, names]) => ({
194
+ host,
195
+ label: `${[...new Set(names)].join(" / ")}(${host})`
196
+ })), [byHost]);
197
+ const otherHosts = (0, react.useMemo)(() => {
198
+ const seen = /* @__PURE__ */ new Map();
199
+ for (const h of [...view?.knownHosts ?? [], ...view?.hosts ?? []]) {
200
+ const key = h.trim().toLowerCase();
201
+ if (key === "" || byHost.has(key)) continue;
202
+ seen.set(key, h);
203
+ }
204
+ return [...seen.keys()].sort().map((h) => ({
205
+ host: h,
206
+ label: h
207
+ }));
208
+ }, [view, byHost]);
209
+ const toggleHost = (host) => {
210
+ setSaved(false);
211
+ setProxied((current) => {
212
+ const next = new Set(current);
213
+ if (!next.delete(host)) next.add(host);
214
+ return next;
215
+ });
216
+ };
217
+ const save = async () => {
218
+ setSaving(true);
219
+ setError(void 0);
220
+ try {
221
+ if ((await proxyApi({
222
+ method: "POST",
223
+ headers: { "content-type": "application/json" },
224
+ body: JSON.stringify({
225
+ op: "save",
226
+ upstream: {
227
+ enabled,
228
+ protocol,
229
+ host: host.trim(),
230
+ port: Number(port) || 0,
231
+ username: username.trim(),
232
+ password
233
+ },
234
+ proxiedHosts: [...proxied]
235
+ })
236
+ })).ok !== true) throw new Error("save rejected");
237
+ setSaved(true);
238
+ load();
239
+ } catch (err) {
240
+ setError(err instanceof Error ? err.message : String(err));
241
+ } finally {
242
+ setSaving(false);
243
+ }
244
+ };
245
+ const test = async () => {
246
+ setTesting(true);
247
+ setTestResult(void 0);
248
+ try {
249
+ const body = await proxyApi({
250
+ method: "POST",
251
+ headers: { "content-type": "application/json" },
252
+ body: JSON.stringify({
253
+ op: "test",
254
+ upstream: {
255
+ enabled,
256
+ protocol,
257
+ host: host.trim(),
258
+ port: Number(port) || 0,
259
+ username: username.trim(),
260
+ password
261
+ }
262
+ })
263
+ });
264
+ setTestResult({
265
+ ok: body.ok === true,
266
+ detail: body.detail
267
+ });
268
+ } catch (err) {
269
+ setTestResult({
270
+ ok: false,
271
+ detail: err instanceof Error ? err.message : String(err)
272
+ });
273
+ } finally {
274
+ setTesting(false);
275
+ }
276
+ };
277
+ const providerFiltered = providerHosts.filter((e) => match(e.label));
278
+ const otherFiltered = otherHosts.filter((e) => match(e.label));
279
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
280
+ className: "dsh-proxy-section",
281
+ children: [
282
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
283
+ className: "dsh-proxy-group",
284
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("upstreamTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
285
+ className: "dsh-proxy-upstream",
286
+ children: [
287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
288
+ className: "dsh-proxy-check",
289
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
290
+ type: "checkbox",
291
+ checked: enabled,
292
+ onChange: (e) => {
293
+ setEnabled(e.target.checked);
294
+ setSaved(false);
295
+ }
296
+ }), t("upstreamEnabled")]
297
+ }),
298
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
299
+ className: "dsh-proxy-row",
300
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
301
+ className: "dsh-proxy-proto",
302
+ children: t("protocolLabel")
303
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProtocolSelector, {
304
+ value: protocol,
305
+ onChange: (v) => {
306
+ setProtocol(v);
307
+ setSaved(false);
308
+ },
309
+ disabled: !enabled,
310
+ t
311
+ })]
312
+ }),
313
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
314
+ className: "dsh-proxy-inputs",
315
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
316
+ placeholder: t("hostPlaceholder"),
317
+ value: host,
318
+ onChange: (e) => {
319
+ setHost(e.target.value);
320
+ setSaved(false);
321
+ },
322
+ disabled: !enabled,
323
+ autoComplete: "off",
324
+ spellCheck: false
325
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
326
+ placeholder: t("portPlaceholder"),
327
+ value: port,
328
+ onChange: (e) => {
329
+ setPort(e.target.value);
330
+ setSaved(false);
331
+ },
332
+ disabled: !enabled,
333
+ type: "number",
334
+ min: 1,
335
+ max: 65535
336
+ })]
337
+ }),
338
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
339
+ className: "dsh-proxy-inputs",
340
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
341
+ placeholder: t("usernamePlaceholder"),
342
+ value: username,
343
+ onChange: (e) => {
344
+ setUsername(e.target.value);
345
+ setSaved(false);
346
+ },
347
+ disabled: !enabled,
348
+ autoComplete: "off",
349
+ spellCheck: false
350
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
351
+ placeholder: t("passwordPlaceholder"),
352
+ value: password,
353
+ onChange: (e) => {
354
+ setPassword(e.target.value);
355
+ setSaved(false);
356
+ },
357
+ disabled: !enabled,
358
+ type: "password",
359
+ autoComplete: "new-password"
360
+ })]
361
+ }),
362
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("protoHint") })
363
+ ]
364
+ })]
365
+ }),
366
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
367
+ className: "dsh-proxy-group",
368
+ children: [
369
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("proxiedHostsTitle") }),
370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("proxiedHostsHint") }),
371
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
372
+ className: "dsh-proxy-search",
373
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGlobeOutline14, {}),
374
+ placeholder: t("searchPlaceholder"),
375
+ value: query,
376
+ onChange: (e) => {
377
+ setQuery(e.target.value);
378
+ },
379
+ autoComplete: "off",
380
+ spellCheck: false
381
+ }),
382
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
383
+ className: "dsh-proxy-heading",
384
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("providersHeading") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
385
+ className: "dsh-proxy-count",
386
+ children: providerHosts.length
387
+ })]
388
+ }),
389
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
390
+ className: "dsh-proxy-hostlist",
391
+ children: providerFiltered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
392
+ className: "dsh-proxy-empty",
393
+ children: t("providersEmpty")
394
+ }) : providerFiltered.map((e) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostRow, {
395
+ host: e.host,
396
+ label: e.label,
397
+ checked: proxied.has(e.host),
398
+ proxied: proxied.has(e.host),
399
+ onToggle: toggleHost
400
+ }, e.host))
401
+ }),
402
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
403
+ 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
+ })]
408
+ }),
409
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
410
+ className: "dsh-proxy-hostlist",
411
+ children: otherFiltered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
412
+ className: "dsh-proxy-empty",
413
+ children: t("othersEmpty")
414
+ }) : otherFiltered.map((e) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostRow, {
415
+ host: e.host,
416
+ label: e.label,
417
+ checked: proxied.has(e.host),
418
+ proxied: proxied.has(e.host),
419
+ onToggle: toggleHost
420
+ }, e.host))
421
+ })
422
+ ]
423
+ }),
424
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("immediateHint") }),
425
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
426
+ className: "dsh-proxy-actions",
427
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
428
+ variant: "outline",
429
+ size: "md",
430
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLinkOutline14, {}),
431
+ disabled: testing || !enabled,
432
+ onClick: () => {
433
+ test();
434
+ },
435
+ children: testing ? t("testing") : t("test")
436
+ }), testResult !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
437
+ className: `dsh-proxy-status ${testResult.ok ? "dsh-proxy-status-ok" : "dsh-proxy-status-err"}`,
438
+ children: [testResult.ok ? t("testStatusOk") : t("testStatusFail"), testResult.detail ? `(${testResult.detail})` : ""]
439
+ })]
440
+ }),
441
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
442
+ className: "dsh-proxy-actions",
443
+ children: [
444
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
445
+ variant: "primary",
446
+ size: "md",
447
+ disabled: saving,
448
+ onClick: () => {
449
+ save();
450
+ },
451
+ children: saving ? t("saving") : t("save")
452
+ }),
453
+ saved && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
454
+ className: "dsh-proxy-status dsh-proxy-status-ok",
455
+ children: t("saveStatus")
456
+ }),
457
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
458
+ className: "dsh-proxy-status dsh-proxy-status-err",
459
+ children: [
460
+ t("saveFailed"),
461
+ " ",
462
+ error
463
+ ]
464
+ })
465
+ ]
466
+ })
467
+ ]
468
+ });
469
+ }
470
+ //#endregion
471
+ //#region src/client/styles.ts
472
+ /**
473
+ * Design-token styles for the Proxy settings section.
474
+ *
475
+ * The plugin is external (not part of the DSH repository), so it cannot import
476
+ * the built-in CSS modules; instead it re-declares the same rules against the
477
+ * shared `--dsw-alias-*` tokens, namespaced under `dsh-proxy-` to avoid any
478
+ * collision with host styles. Injected once at module evaluation, tagged by
479
+ * plugin id so repeated mounts stay idempotent.
480
+ */
481
+ const PROXY_STYLES = `
482
+ .dsh-proxy-section { display: flex; flex-direction: column; gap: 20px; max-width: 720px; padding: 4px 2px 20px; }
483
+ .dsh-proxy-group { display: flex; flex-direction: column; gap: 10px; }
484
+ .dsh-proxy-group > h3 { margin: 0; font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary); }
485
+ .dsh-proxy-group > p { margin: 0; font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-tertiary); }
486
+ .dsh-proxy-upstream { display: flex; flex-direction: column; gap: 10px; padding: 12px; border-radius: 12px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l3); }
487
+ .dsh-proxy-row { display: flex; align-items: center; gap: 8px; }
488
+ .dsh-proxy-check { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--dsw-alias-label-primary); cursor: pointer; user-select: none; }
489
+ .dsh-proxy-check input { accent-color: var(--dsw-alias-button-primary-fill); cursor: pointer; }
490
+ .dsh-proxy-proto { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--dsw-alias-label-secondary); }
491
+ .dsh-proxy-selector { display: inline-flex; align-items: center; gap: 4px; height: 32px; padding: 0 10px; border: none; border-radius: 16px; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary); font-size: 13px; cursor: pointer; }
492
+ .dsh-proxy-selector:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
493
+ .dsh-proxy-selector:disabled { opacity: 0.6; cursor: default; }
494
+ .dsh-proxy-inputs { display: grid; grid-template-columns: 1fr 120px; gap: 8px; }
495
+ .dsh-proxy-inputs input { width: 100%; }
496
+ .dsh-proxy-search { margin-bottom: 4px; }
497
+ .dsh-proxy-hostlist { display: flex; flex-direction: column; gap: 4px; max-height: 220px; overflow-y: auto; scrollbar-width: none; }
498
+ .dsh-proxy-hostlist::-webkit-scrollbar { display: none; }
499
+ .dsh-proxy-host { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: 8px; font-size: 13px; color: var(--dsw-alias-label-primary); }
500
+ .dsh-proxy-host:hover { background: var(--dsw-alias-bg-layer-1); }
501
+ .dsh-proxy-host input { accent-color: var(--dsw-alias-button-primary-fill); cursor: pointer; }
502
+ .dsh-proxy-host-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
503
+ .dsh-proxy-host-route { font-size: 11px; color: var(--dsw-alias-label-tertiary); white-space: nowrap; }
504
+ .dsh-proxy-empty { padding: 16px; border: 1px dashed var(--dsw-alias-border-l3); border-radius: 12px; text-align: center; font-size: 12px; color: var(--dsw-alias-label-tertiary); }
505
+ .dsh-proxy-actions { display: flex; align-items: center; gap: 12px; }
506
+ .dsh-proxy-status { font-size: 12px; color: var(--dsw-alias-label-tertiary); }
507
+ .dsh-proxy-status-ok { color: var(--dsw-alias-state-success-primary, var(--dsw-alias-label-primary)); }
508
+ .dsh-proxy-status-err { color: var(--dsw-alias-state-error-primary); }
509
+ .dsh-proxy-heading { display: flex; align-items: center; gap: 6px; margin: 0 0 8px; }
510
+ .dsh-proxy-heading h4 { margin: 0; font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); }
511
+ .dsh-proxy-heading .dsh-proxy-count { font-size: 11px; color: var(--dsw-alias-label-tertiary); }
512
+ `;
513
+ /**
514
+ * Inject {@link PROXY_STYLES} once, tagged by plugin id so re-evaluation
515
+ * and repeated mounts stay idempotent (mirrors how the loader handles plugin
516
+ * CSS). Called from the client `apply`.
517
+ */
518
+ function injectProxyStyles(pluginId) {
519
+ if (typeof document === "undefined") return;
520
+ const selector = `style[data-dsh-plugin-css="${pluginId}"]`;
521
+ if (document.querySelector(selector) !== null) return;
522
+ const tag = document.createElement("style");
523
+ tag.setAttribute("data-dsh-plugin-css", pluginId);
524
+ tag.textContent = PROXY_STYLES;
525
+ document.head.appendChild(tag);
526
+ }
527
+ injectProxyStyles("dsh-proxy");
528
+ //#endregion
529
+ //#region src/client/locales.ts
530
+ /**
531
+ * Copy dictionaries for the Proxy settings section. English is the key source
532
+ * (en object keyed by the same strings), zh must carry every key.
533
+ */
534
+ const en = {
535
+ nav: "Proxy",
536
+ title: "Proxy settings",
537
+ subtitle: "Model-provider traffic through an optional upstream proxy, per host.",
538
+ upstreamTitle: "Upstream proxy (optional)",
539
+ upstreamEnabled: "Enable upstream proxy (when off, every host connects directly)",
540
+ protocolLabel: "Protocol",
541
+ protocolHttp: "HTTP",
542
+ protocolHttps: "HTTPS (TLS to the proxy itself)",
543
+ protocolSocks5: "SOCKS5",
544
+ protoHint: "Clash / V2Ray style mixed ports (e.g. 7890) usually speak HTTP. SOCKS5 upstreams only forward HTTPS targets (http targets fall back to direct).",
545
+ hostPlaceholder: "Proxy host, e.g. 127.0.0.1",
546
+ portPlaceholder: "Port, e.g. 7890",
547
+ usernamePlaceholder: "Username (optional)",
548
+ passwordPlaceholder: "Password (optional)",
549
+ proxiedHostsTitle: "Hosts going through the proxy",
550
+ 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).",
551
+ searchPlaceholder: "Search host / provider…",
552
+ providersHeading: "Model providers",
553
+ providersEmpty: "No model providers found yet — they appear here once you configure models in dsh settings.",
554
+ othersHeading: "Other observed hosts",
555
+ othersEmpty: "None yet — external hosts dsh has contacted will appear here.",
556
+ save: "Save",
557
+ saveStatus: "Saved — takes effect immediately (no restart needed)",
558
+ saving: "Saving…",
559
+ saveFailed: "Save failed:",
560
+ test: "Test connection",
561
+ testing: "Testing…",
562
+ testFailed: "Test failed:",
563
+ testStatusOk: "Connected",
564
+ testStatusFail: "Connection failed",
565
+ immediateHint: "Saved changes apply immediately; hosts related to install/update take effect on the next install/update.",
566
+ direct: "direct",
567
+ viaUpstream: "via upstream",
568
+ selfLoopWarning: "The upstream points back at this proxy — treated as disabled (self-loop guard).",
569
+ noHost: "No proxy host set",
570
+ teardown: "Proxy engine stopped"
571
+ };
572
+ const zh = {
573
+ nav: "代理",
574
+ title: "代理设置",
575
+ subtitle: "按主机把模型提供方流量转发到可选的上游代理。",
576
+ upstreamTitle: "上游代理(可选)",
577
+ upstreamEnabled: "启用上游代理(未启用时所有主机直连)",
578
+ protocolLabel: "协议",
579
+ protocolHttp: "HTTP",
580
+ protocolHttps: "HTTPS(代理服务器自身加密)",
581
+ protocolSocks5: "SOCKS5",
582
+ protoHint: "Clash / V2Ray 等常用混合端口(如 7890)一般为 HTTP;SOCKS5 上游仅对 HTTPS 目标生效(http 目标会直连)。",
583
+ hostPlaceholder: "代理主机,如 127.0.0.1",
584
+ portPlaceholder: "端口,如 7890",
585
+ usernamePlaceholder: "用户名(可选)",
586
+ passwordPlaceholder: "密码(可选)",
587
+ proxiedHostsTitle: "走代理的主机",
588
+ proxiedHostsHint: "默认全部直连,勾选才走上游代理。模型提供方来自 dsh 设置(settings.yaml);其它为实际访问过的主机(含安装/更新流量)。",
589
+ searchPlaceholder: "搜索主机 / 提供方名称…",
590
+ providersHeading: "模型提供方",
591
+ providersEmpty: "尚未读取到模型提供方(在 dsh 设置里配置模型后会自动出现)",
592
+ othersHeading: "其它已观测主机",
593
+ othersEmpty: "暂无(dsh 访问过外部主机后会自动出现,含 registry.npmjs.org 等安装流量)",
594
+ save: "保存",
595
+ saveStatus: "✓ 已保存,立即生效(无需重启)",
596
+ saving: "保存中…",
597
+ saveFailed: "保存失败:",
598
+ test: "测试连接",
599
+ testing: "测试中…",
600
+ testFailed: "测试失败:",
601
+ testStatusOk: "✓ 连接成功",
602
+ testStatusFail: "✗ 连接失败",
603
+ immediateHint: "保存后立即生效,无需重启;改动「安装/更新」相关主机的代理会在下次安装/更新时生效。",
604
+ direct: "直连",
605
+ viaUpstream: "走上游代理",
606
+ selfLoopWarning: "上游指向本代理自身,视为禁用(防自环保护)。",
607
+ noHost: "未填写代理主机",
608
+ teardown: "代理引擎已停止"
609
+ };
610
+ //#endregion
611
+ //#region src/client/index.ts
612
+ /** Dictionary namespace owned by this plugin. */
613
+ const NS = "dsh-proxy";
614
+ /** Required services (cordis fiber inject). The target slot is declared by
615
+ * ui-settings; registration depends on it through `slots.inject()`. */
616
+ const inject = ["slots", "locale"];
617
+ /**
618
+ * Register the Proxy section once the `settings.section` declaration is on
619
+ * the ledger. No settingsScope/connection: the section talks to the host
620
+ * half's /proxy/api route with plain same-origin fetch.
621
+ * @param ctx - client root context.
622
+ */
623
+ function apply(ctx) {
624
+ ctx.effect(() => ctx.locale.register(NS, {
625
+ zh,
626
+ en
627
+ }), "dsh-proxy: copy dictionaries");
628
+ const t = ctx.locale.bind(NS);
629
+ const injected = () => ({ t });
630
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
631
+ name: "settings.section",
632
+ id: "dsh-proxy",
633
+ order: 25,
634
+ label: () => t("nav"),
635
+ inject: injected
636
+ }, ProxySection));
637
+ }
638
+ //#endregion
639
+ exports.apply = apply;
640
+ exports.inject = inject;
641
+ return module.exports;
642
+ }
643
+ });
644
+
645
+ //# sourceMappingURL=client.js.map