@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/CHANGELOG.md +31 -0
- package/CONTRIBUTING.md +97 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/README.zh.md +160 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +645 -0
- package/lib/index.js +960 -0
- package/package.json +62 -0
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
|