dsh-provider-proxy 0.4.1 → 0.5.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/README.md +58 -4
- package/lib/client.js +628 -87
- package/lib/client.js.map +4 -4
- package/lib/index.js +384 -86
- package/lib/index.js.map +3 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -39,13 +39,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
39
39
|
var require_client = __commonJS({
|
|
40
40
|
"src/client.css"(exports, module2) {
|
|
41
41
|
"use strict";
|
|
42
|
-
var __dshMap = { "root": "
|
|
42
|
+
var __dshMap = { "root": "7a51762d_root", "lead": "c71659b2_lead", "empty": "c42c4abe_empty", "card": "53acf569_card", "cardActive": "bd4546f8_cardActive", "defaultCard": "1103ba6e_defaultCard", "defaultLead": "0b907dc6_defaultLead", "cardHead": "d06dccdb_cardHead", "cardTitles": "ff20149f_cardTitles", "name": "4e36a10e_name", "id": "79208064_id", "pillOn": "3e0969fd_pillOn", "pillLive": "895a1b6e_pillLive", "pillOff": "f5c4d227_pillOff", "toggle": "94e6424f_toggle", "field": "5dfb5268_field", "label": "2e13439d_label", "hint": "cdd952c6_hint", "input": "31dd2d99_input", "testButton": "028bc91c_testButton", "diag": "5d114b0b_diag", "diagSummary": "27518b39_diagSummary", "diagList": "b3005b62_diagList" };
|
|
43
43
|
var __dshStyles = globalThis.__dshPluginStyles || (globalThis.__dshPluginStyles = /* @__PURE__ */ new Set());
|
|
44
44
|
if (!__dshStyles.has("dsh-provider-proxy/client.css")) {
|
|
45
45
|
__dshStyles.add("dsh-provider-proxy/client.css");
|
|
46
46
|
__dshStyle = document.createElement("style");
|
|
47
47
|
__dshStyle.setAttribute("data-plugin-css", "dsh-provider-proxy/client.css");
|
|
48
|
-
__dshStyle.textContent =
|
|
48
|
+
__dshStyle.textContent = '/* dsh-provider-proxy \u2014 provider proxy settings page.\n Tokens only: no literal colours, matching the shared --dsw-* system. */\n\n.7a51762d_root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n max-width: 640px;\n}\n\n.c71659b2_lead {\n margin: 0 0 4px;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\n}\n\n.c42c4abe_empty {\n margin: 0;\n padding: 16px;\n border: 1px dashed var(--dsw-alias-border-l1);\n border-radius: 8px;\n font-size: 12px;\n text-align: center;\n color: var(--dsw-alias-state-idle-primary);\n}\n\n/* ---- provider card ---- */\n\n.53acf569_card,\n.bd4546f8_cardActive {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.bd4546f8_cardActive {\n border-color: var(--dsw-alias-state-success-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* ---- shared default endpoint ---- */\n\n/* A distinct outer layer, so it reads as "one setting for the page" rather than\n as one more provider card. */\n.1103ba6e_defaultCard {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 14px 16px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.0b907dc6_defaultLead {\n margin: -2px 0 0;\n font-size: 11px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n}\n\n.d06dccdb_cardHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.ff20149f_cardTitles {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.4e36a10e_name {\n overflow: hidden;\n font-size: 13px;\n font-weight: 600;\n white-space: nowrap;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.79208064_id {\n flex: none;\n padding: 1px 6px;\n border-radius: 4px;\n background: var(--dsw-alias-bg-layer-2);\n font-family: ui-monospace, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* ---- status pill ---- */\n\n.3e0969fd_pillOn,\n.895a1b6e_pillLive,\n.f5c4d227_pillOff {\n flex: none;\n padding: 2px 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n.3e0969fd_pillOn {\n background: var(--dsw-alias-state-success-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.895a1b6e_pillLive {\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-secondary);\n}\n\n.f5c4d227_pillOff {\n color: var(--dsw-alias-state-idle-primary);\n}\n\n/* ---- toggle ---- */\n\n.94e6424f_toggle {\n display: flex;\n align-items: center;\n gap: 8px;\n padding-top: 2px;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n}\n\n.94e6424f_toggle input {\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n/* ---- fields ---- */\n\n.5dfb5268_field {\n display: grid;\n grid-template-columns: 96px 1fr;\n align-items: center;\n gap: 10px;\n font-size: 12px;\n}\n\n.2e13439d_label {\n color: var(--dsw-alias-label-secondary);\n}\n\n.cdd952c6_hint {\n grid-column: 2;\n margin-top: -2px;\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.31dd2d99_input {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n padding: 5px 8px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n outline: none;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n/* The base look of every input is INLINE too, so these three state rules only\n ever take effect as author-important declarations \u2014 same reason as the\n self-check button above. `cursor` has no inline base and needs none, and the\n hover border uses `l3` because `l1`\u2192`l2` is a 4%\u219210% black step nobody can\n see. */\n\n.31dd2d99_input:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n}\n\n.31dd2d99_input:focus {\n border-color: var(--dsw-alias-brand-primary) !important;\n}\n\n.31dd2d99_input:disabled {\n color: var(--dsw-alias-state-idle-primary) !important;\n background: var(--dsw-alias-bg-layer-2) !important;\n cursor: not-allowed;\n}\n\n/* ---- self-check ---- */\n\n/* The hover state is the one thing an inline style cannot express, which is why\n this element carries a class at all. Layout stays inline.\n\n Two traps, both hit before this comment existed:\n - `!important` is load-bearing: the base border and background are INLINE, and\n the cascade sorts a style attribute after every author rule, so a plain\n declaration here would ship, match, and still lose.\n - the TOKENS must be visible ones. `bg-layer-1/2/3` all resolve to\n `neutral-bluish-00` in the light theme, so a layer swap is a no-op there, and\n `border-l1`\u2192`l2` is 4%\u219210% black on a 1px line. The app\'s own hovers use\n `interactive-bg-hover`; using it here is what makes the feedback exist. */\n.028bc91c_testButton {\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n.028bc91c_testButton:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\n/* ---- diagnostics ---- */\n\n.5d114b0b_diag {\n margin-top: 2px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.27518b39_diagSummary {\n cursor: pointer;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.b3005b62_diagList {\n margin: 6px 0 0;\n padding-left: 18px;\n font-family: ui-monospace, monospace;\n word-break: break-all;\n}\n';
|
|
49
49
|
document.head.appendChild(__dshStyle);
|
|
50
50
|
}
|
|
51
51
|
var __dshStyle;
|
|
@@ -63,6 +63,27 @@ __export(client_exports, {
|
|
|
63
63
|
});
|
|
64
64
|
module.exports = __toCommonJS(client_exports);
|
|
65
65
|
var import_client = __toESM(require_client(), 1);
|
|
66
|
+
|
|
67
|
+
// src/proxy-address.ts
|
|
68
|
+
function defaultProxyProblem(endpoint) {
|
|
69
|
+
const host = (endpoint.host ?? "").trim();
|
|
70
|
+
if (host.length === 0) return "\u672A\u586B\u5199\u4EE3\u7406\u5730\u5740";
|
|
71
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(host) || host.includes("/")) {
|
|
72
|
+
return "\u4EE3\u7406\u5730\u5740\u53EA\u586B\u4E3B\u673A\u540D\u6216 IP\uFF0C\u4E0D\u8981\u5E26\u534F\u8BAE\u524D\u7F00\u6216\u8DEF\u5F84";
|
|
73
|
+
}
|
|
74
|
+
const port = endpoint.port ?? 0;
|
|
75
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
76
|
+
return "\u4EE3\u7406\u7AEF\u53E3\u65E0\u6548\uFF08\u9700\u4E3A 1-65535 \u7684\u6574\u6570\uFF09";
|
|
77
|
+
}
|
|
78
|
+
const hasUser = (endpoint.username ?? "").length > 0;
|
|
79
|
+
const hasPassword = (endpoint.password ?? "").length > 0;
|
|
80
|
+
if (hasUser !== hasPassword) {
|
|
81
|
+
return hasUser ? "\u914D\u7F6E\u4E86\u4EE3\u7406\u7528\u6237\u540D\u4F46\u6CA1\u6709\u5BC6\u7801\uFF1B\u8BF7\u8865\u5168\u5BC6\u7801\u6216\u6E05\u7A7A\u7528\u6237\u540D" : "\u914D\u7F6E\u4E86\u4EE3\u7406\u5BC6\u7801\u4F46\u6CA1\u6709\u7528\u6237\u540D\uFF1B\u8BF7\u8865\u5168\u7528\u6237\u540D\u6216\u6E05\u7A7A\u5BC6\u7801";
|
|
82
|
+
}
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/client.ts
|
|
66
87
|
var ReactCache;
|
|
67
88
|
function react() {
|
|
68
89
|
if (ReactCache === void 0) {
|
|
@@ -88,7 +109,7 @@ function apply(ctx) {
|
|
|
88
109
|
name: "settings.section",
|
|
89
110
|
id: NAMESPACE,
|
|
90
111
|
order: 20,
|
|
91
|
-
label: () =>
|
|
112
|
+
label: () => "Provider \u4EE3\u7406"
|
|
92
113
|
},
|
|
93
114
|
() => renderPage(ctx)
|
|
94
115
|
)
|
|
@@ -175,11 +196,24 @@ function testResultFrom(body, strings) {
|
|
|
175
196
|
ok,
|
|
176
197
|
ms: typeof body?.ms === "number" && Number.isFinite(body.ms) ? body.ms : void 0,
|
|
177
198
|
ip: typeof body?.exitIp === "string" && body.exitIp.length > 0 ? body.exitIp : void 0,
|
|
199
|
+
// The Host probes a target the way routing would: a loopback/private upstream
|
|
200
|
+
// (or a bypassHosts match) is reached DIRECT, not through the proxy. The card
|
|
201
|
+
// says so, so a green check is not read as "the proxy works".
|
|
202
|
+
direct: body?.viaProxy === false,
|
|
178
203
|
error: ok ? void 0 : typeof body?.error === "string" && body.error.length > 0 ? body.error : strings.testFailed
|
|
179
204
|
};
|
|
180
205
|
}
|
|
181
206
|
var catalogueSetter = () => {
|
|
182
207
|
};
|
|
208
|
+
function relativeTime(at, t) {
|
|
209
|
+
const seconds = Math.max(0, Math.round((Date.now() - at) / 1e3));
|
|
210
|
+
if (seconds < 60) return t.justNow;
|
|
211
|
+
const minutes = Math.floor(seconds / 60);
|
|
212
|
+
if (minutes < 60) return `${minutes}${t.minutesAgo}`;
|
|
213
|
+
const hours = Math.floor(minutes / 60);
|
|
214
|
+
if (hours < 24) return `${hours}${t.hoursAgo}`;
|
|
215
|
+
return `${Math.floor(hours / 24)}${t.daysAgo}`;
|
|
216
|
+
}
|
|
183
217
|
async function fetchCatalog() {
|
|
184
218
|
const response = await globalThis.fetch(CATALOG_PATH, { headers: { accept: "application/json" } });
|
|
185
219
|
if (!response.ok) throw new Error(`Host answered ${response.status}`);
|
|
@@ -204,21 +238,92 @@ async function fetchCatalog() {
|
|
|
204
238
|
if (typeof message === "string" && message.length > 0) problems[id] = message;
|
|
205
239
|
}
|
|
206
240
|
}
|
|
207
|
-
|
|
241
|
+
const lastUse = {};
|
|
242
|
+
const publishedLastUse = body?.lastUse;
|
|
243
|
+
if (publishedLastUse !== null && typeof publishedLastUse === "object") {
|
|
244
|
+
for (const [id, raw] of Object.entries(publishedLastUse)) {
|
|
245
|
+
const entry = raw;
|
|
246
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
247
|
+
const at = typeof entry.at === "number" && Number.isFinite(entry.at) ? entry.at : void 0;
|
|
248
|
+
const startedAt = typeof entry.startedAt === "number" && Number.isFinite(entry.startedAt) ? entry.startedAt : void 0;
|
|
249
|
+
if (at === void 0 && startedAt === void 0) continue;
|
|
250
|
+
lastUse[id] = {
|
|
251
|
+
...startedAt === void 0 ? {} : { startedAt },
|
|
252
|
+
...at === void 0 ? {} : { at },
|
|
253
|
+
...typeof entry.ok === "boolean" ? { ok: entry.ok } : {},
|
|
254
|
+
...typeof entry.code === "string" && entry.code.length > 0 ? { code: entry.code } : {}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { rows, hits, problems, lastUse };
|
|
259
|
+
}
|
|
260
|
+
var CATALOG_REFRESH_MS = 1e4;
|
|
261
|
+
var catalogPoll;
|
|
262
|
+
var catalogTouchedAt = 0;
|
|
263
|
+
function touchCatalogPolling() {
|
|
264
|
+
catalogTouchedAt = Date.now();
|
|
265
|
+
if (catalogPoll !== void 0) return;
|
|
266
|
+
catalogPoll = setInterval(() => {
|
|
267
|
+
if (Date.now() - catalogTouchedAt > CATALOG_REFRESH_MS * 2 + 1e3) {
|
|
268
|
+
clearInterval(catalogPoll);
|
|
269
|
+
catalogPoll = void 0;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
void fetchCatalog().then((catalog) => catalogueSetter?.(catalog)).catch(() => void 0);
|
|
273
|
+
}, CATALOG_REFRESH_MS);
|
|
208
274
|
}
|
|
209
275
|
var byName = (a, b) => a.name.localeCompare(b.name);
|
|
276
|
+
function deadRuleEntries(raw) {
|
|
277
|
+
return raw.split(/[,\s]+/).map((entry) => entry.trim()).filter((entry) => /^[a-z][a-z0-9+.-]*:\/\//i.test(entry) || entry.includes("/"));
|
|
278
|
+
}
|
|
210
279
|
function view(route) {
|
|
211
280
|
return {
|
|
212
281
|
enabled: route?.enabled === true,
|
|
282
|
+
useDefault: route?.useDefault === true,
|
|
213
283
|
protocol: PROTOCOLS.includes(route?.protocol) ? route.protocol : "http",
|
|
214
284
|
host: route?.host ?? "",
|
|
215
285
|
port: route?.port ?? 0,
|
|
216
286
|
username: route?.username ?? "",
|
|
217
287
|
password: route?.password ?? "",
|
|
218
288
|
onlyHosts: route?.onlyHosts ?? "",
|
|
219
|
-
bypassHosts: route?.bypassHosts ?? ""
|
|
289
|
+
bypassHosts: route?.bypassHosts ?? "",
|
|
290
|
+
testTarget: route?.testTarget ?? ""
|
|
220
291
|
};
|
|
221
292
|
}
|
|
293
|
+
function unbracket(hostname) {
|
|
294
|
+
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
295
|
+
}
|
|
296
|
+
function splitProxyUrl(text) {
|
|
297
|
+
const trimmed = text.trim();
|
|
298
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return void 0;
|
|
299
|
+
let url;
|
|
300
|
+
try {
|
|
301
|
+
url = new URL(trimmed);
|
|
302
|
+
} catch {
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
const scheme = url.protocol.slice(0, -1).toLowerCase();
|
|
306
|
+
const protocol = scheme === "socks" ? "socks5" : scheme;
|
|
307
|
+
if (!PROTOCOLS.includes(protocol)) return void 0;
|
|
308
|
+
if (url.hostname.length === 0) return void 0;
|
|
309
|
+
const decode = (value) => {
|
|
310
|
+
try {
|
|
311
|
+
return decodeURIComponent(value);
|
|
312
|
+
} catch {
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
const parsedPort = url.port.length > 0 ? Number.parseInt(url.port, 10) : 0;
|
|
317
|
+
return {
|
|
318
|
+
protocol,
|
|
319
|
+
host: unbracket(url.hostname),
|
|
320
|
+
port: Number.isNaN(parsedPort) ? 0 : parsedPort,
|
|
321
|
+
username: decode(url.username),
|
|
322
|
+
password: decode(url.password)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
var WRITE_DELAY_MS = 300;
|
|
326
|
+
var pendingWrites = /* @__PURE__ */ new Map();
|
|
222
327
|
var diagnostics = [];
|
|
223
328
|
function note(message) {
|
|
224
329
|
if (!diagnostics.includes(message)) diagnostics.push(message);
|
|
@@ -334,6 +439,40 @@ var S = {
|
|
|
334
439
|
color: "var(--dsw-alias-label-secondary)"
|
|
335
440
|
},
|
|
336
441
|
pillOff: { color: "var(--dsw-alias-state-idle-primary)" },
|
|
442
|
+
/**
|
|
443
|
+
* The shared default endpoint sits on its own layer so it reads as global
|
|
444
|
+
* rather than as one more provider card — the mistake it exists to prevent is
|
|
445
|
+
* a user typing the same address into every card.
|
|
446
|
+
*/
|
|
447
|
+
defaultCard: {
|
|
448
|
+
display: "flex",
|
|
449
|
+
flexDirection: "column",
|
|
450
|
+
gap: "10px",
|
|
451
|
+
padding: "14px 16px",
|
|
452
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
453
|
+
borderRadius: "10px",
|
|
454
|
+
background: "var(--dsw-alias-bg-layer-2)"
|
|
455
|
+
},
|
|
456
|
+
defaultLead: {
|
|
457
|
+
margin: "-2px 0 0",
|
|
458
|
+
fontSize: "11px",
|
|
459
|
+
lineHeight: "1.5",
|
|
460
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
461
|
+
},
|
|
462
|
+
inheritNote: {
|
|
463
|
+
margin: "0",
|
|
464
|
+
fontSize: "11px",
|
|
465
|
+
lineHeight: "1.5",
|
|
466
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
467
|
+
},
|
|
468
|
+
/** The default is not configured: an answer, so it takes the error colour. */
|
|
469
|
+
inheritWarning: {
|
|
470
|
+
margin: "0",
|
|
471
|
+
fontSize: "11px",
|
|
472
|
+
lineHeight: "1.5",
|
|
473
|
+
fontWeight: "500",
|
|
474
|
+
color: "var(--dsw-alias-state-error-primary, #f85149)"
|
|
475
|
+
},
|
|
337
476
|
toggle: {
|
|
338
477
|
display: "flex",
|
|
339
478
|
alignItems: "center",
|
|
@@ -383,18 +522,34 @@ function renderPage(ctx) {
|
|
|
383
522
|
}]))(() => {
|
|
384
523
|
void fetchCatalog().then((catalog) => catalogueSetter?.(catalog)).catch((error) => {
|
|
385
524
|
note(`provider \u5217\u8868\u83B7\u53D6\u5931\u8D25\uFF1A${error?.message ?? error}`);
|
|
386
|
-
catalogueSetter?.({ rows: [], hits: {}, problems: {} });
|
|
525
|
+
catalogueSetter?.({ rows: [], hits: {}, problems: {}, lastUse: {} });
|
|
387
526
|
});
|
|
388
527
|
return {
|
|
389
528
|
rows: void 0,
|
|
390
529
|
hits: {},
|
|
391
530
|
problems: {},
|
|
531
|
+
lastUse: {},
|
|
392
532
|
pending: true
|
|
393
533
|
};
|
|
394
534
|
});
|
|
395
|
-
catalogueSetter = (catalog) => setState({
|
|
535
|
+
catalogueSetter = (catalog) => setState({
|
|
536
|
+
rows: catalog.rows,
|
|
537
|
+
hits: catalog.hits,
|
|
538
|
+
problems: catalog.problems,
|
|
539
|
+
lastUse: catalog.lastUse,
|
|
540
|
+
pending: false
|
|
541
|
+
});
|
|
542
|
+
touchCatalogPolling();
|
|
396
543
|
const published = state.rows ?? [];
|
|
397
544
|
const hits = state.hits ?? {};
|
|
545
|
+
const lastUse = state.lastUse ?? {};
|
|
546
|
+
const lastUseText = (id) => {
|
|
547
|
+
const use = lastUse[id];
|
|
548
|
+
if (use === void 0) return void 0;
|
|
549
|
+
if (use.ok === true) return `${t.recentOk}${relativeTime(use.at ?? use.startedAt ?? Date.now(), t)}`;
|
|
550
|
+
if (use.ok === false) return `${t.recentFail}${use.code ?? "ERROR"}`;
|
|
551
|
+
return `${t.recentStarted}${relativeTime(use.startedAt ?? use.at ?? Date.now(), t)}`;
|
|
552
|
+
};
|
|
398
553
|
const problems = state.problems ?? {};
|
|
399
554
|
const problemLines = Object.entries(problems).map(
|
|
400
555
|
([id, message]) => h("li", { key: `p:${id}`, style: S.diagProblem }, `${id}\uFF1A${message}`)
|
|
@@ -406,8 +561,13 @@ function renderPage(ctx) {
|
|
|
406
561
|
const [draft, setDraft] = (React.useState ?? ((v) => [v, () => {
|
|
407
562
|
}]))({});
|
|
408
563
|
const edit = (id, field2, value) => setDraft((current) => ({ ...current, [id]: { ...current[id], [field2]: value } }));
|
|
564
|
+
const [defaultDraft, setDefaultDraft] = (React.useState ?? ((v) => [v, () => {
|
|
565
|
+
}]))({});
|
|
566
|
+
const editDefault = (field2, value) => setDefaultDraft((current) => ({ ...current, [field2]: value }));
|
|
409
567
|
const [tests, setTests] = (React.useState ?? ((v) => [v, () => {
|
|
410
568
|
}]))({});
|
|
569
|
+
const [filter, setFilter] = (React.useState ?? ((v) => [v, () => {
|
|
570
|
+
}]))("");
|
|
411
571
|
const runTest = (id) => {
|
|
412
572
|
setTests((current) => ({ ...current, [id]: { pending: true } }));
|
|
413
573
|
void (async () => {
|
|
@@ -428,34 +588,293 @@ function renderPage(ctx) {
|
|
|
428
588
|
})();
|
|
429
589
|
};
|
|
430
590
|
const rows = knownProviders({ ...configured, ...draft }, published);
|
|
431
|
-
const save = (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
591
|
+
const save = (key, path, label, value, onRefused) => {
|
|
592
|
+
const pending = pendingWrites.get(key);
|
|
593
|
+
if (pending !== void 0) clearTimeout(pending.timer);
|
|
594
|
+
const timer = setTimeout(() => {
|
|
595
|
+
pendingWrites.delete(key);
|
|
596
|
+
void writeConfig(ctx, [{ op: "set", path, value }]).then(() => {
|
|
597
|
+
note(`\u5DF2\u4FDD\u5B58 ${label} = ${JSON.stringify(value)}`);
|
|
598
|
+
}).catch((error) => {
|
|
599
|
+
note(`\u4FDD\u5B58\u5931\u8D25 ${label}\uFF1A${error?.message ?? error}`);
|
|
600
|
+
onRefused();
|
|
440
601
|
});
|
|
602
|
+
}, WRITE_DELAY_MS);
|
|
603
|
+
pendingWrites.set(key, { timer });
|
|
604
|
+
};
|
|
605
|
+
const rollbackRouteField = (id, field2) => {
|
|
606
|
+
setDraft((current) => {
|
|
607
|
+
const entry = current[id];
|
|
608
|
+
if (entry === void 0 || entry[field2] === void 0) return current;
|
|
609
|
+
const nextEntry = { ...entry };
|
|
610
|
+
delete nextEntry[field2];
|
|
611
|
+
const next = { ...current };
|
|
612
|
+
if (Object.keys(nextEntry).length === 0) delete next[id];
|
|
613
|
+
else next[id] = nextEntry;
|
|
614
|
+
return next;
|
|
441
615
|
});
|
|
442
616
|
};
|
|
443
617
|
const bind = (id, field2) => (value) => {
|
|
444
618
|
edit(id, field2, value);
|
|
445
|
-
save(
|
|
619
|
+
save(
|
|
620
|
+
`${id}\0${field2}`,
|
|
621
|
+
["providers", id, field2],
|
|
622
|
+
`${id}.${field2}`,
|
|
623
|
+
value,
|
|
624
|
+
() => rollbackRouteField(id, field2)
|
|
625
|
+
);
|
|
446
626
|
};
|
|
447
|
-
const
|
|
627
|
+
const bindDefault = (field2) => (value) => {
|
|
628
|
+
editDefault(field2, value);
|
|
629
|
+
save(`\0default\0${field2}`, ["defaultProxy", field2], `defaultProxy.${field2}`, value, () => {
|
|
630
|
+
setDefaultDraft((current) => {
|
|
631
|
+
if (current[field2] === void 0) return current;
|
|
632
|
+
const next = { ...current };
|
|
633
|
+
delete next[field2];
|
|
634
|
+
return next;
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
const saveMany = (key, ops, label, onRefused) => {
|
|
639
|
+
const pending = pendingWrites.get(key);
|
|
640
|
+
if (pending !== void 0) clearTimeout(pending.timer);
|
|
641
|
+
const timer = setTimeout(() => {
|
|
642
|
+
pendingWrites.delete(key);
|
|
643
|
+
void writeConfig(ctx, ops).then(() => {
|
|
644
|
+
note(`\u5DF2\u4FDD\u5B58 ${label}`);
|
|
645
|
+
}).catch((error) => {
|
|
646
|
+
note(`\u4FDD\u5B58\u5931\u8D25 ${label}\uFF1A${error?.message ?? error}`);
|
|
647
|
+
onRefused();
|
|
648
|
+
});
|
|
649
|
+
}, WRITE_DELAY_MS);
|
|
650
|
+
pendingWrites.set(key, { timer });
|
|
651
|
+
};
|
|
652
|
+
const rollbackRouteFields = (id, fields) => {
|
|
653
|
+
setDraft((current) => {
|
|
654
|
+
const entry = current[id];
|
|
655
|
+
if (entry === void 0) return current;
|
|
656
|
+
const nextEntry = { ...entry };
|
|
657
|
+
for (const field2 of fields) delete nextEntry[field2];
|
|
658
|
+
const next = { ...current };
|
|
659
|
+
if (Object.keys(nextEntry).length === 0) delete next[id];
|
|
660
|
+
else next[id] = nextEntry;
|
|
661
|
+
return next;
|
|
662
|
+
});
|
|
663
|
+
};
|
|
664
|
+
const applyCardUrl = (id, parts) => {
|
|
665
|
+
setDraft((current) => ({ ...current, [id]: { ...current[id], ...parts } }));
|
|
666
|
+
const fields = Object.keys(parts);
|
|
667
|
+
saveMany(
|
|
668
|
+
`${id}\0*paste`,
|
|
669
|
+
fields.map((field2) => ({ op: "set", path: ["providers", id, field2], value: parts[field2] })),
|
|
670
|
+
`${id}.*\uFF08\u7C98\u8D34\u4EE3\u7406 URL\uFF09`,
|
|
671
|
+
() => rollbackRouteFields(id, fields)
|
|
672
|
+
);
|
|
673
|
+
};
|
|
674
|
+
const applyDefaultUrl = (parts) => {
|
|
675
|
+
setDefaultDraft((current) => ({ ...current, ...parts }));
|
|
676
|
+
const fields = Object.keys(parts);
|
|
677
|
+
saveMany(
|
|
678
|
+
"\0default\0*paste",
|
|
679
|
+
fields.map((field2) => ({ op: "set", path: ["defaultProxy", field2], value: parts[field2] })),
|
|
680
|
+
"\u9ED8\u8BA4\u4EE3\u7406\uFF08\u7C98\u8D34\u4EE3\u7406 URL\uFF09",
|
|
681
|
+
() => {
|
|
682
|
+
setDefaultDraft((current) => {
|
|
683
|
+
const next = { ...current };
|
|
684
|
+
for (const field2 of fields) delete next[field2];
|
|
685
|
+
return next;
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
);
|
|
689
|
+
};
|
|
690
|
+
const promoteToDefault = (id) => {
|
|
691
|
+
const route = view({ ...configured[id], ...draft[id] });
|
|
692
|
+
applyDefaultUrl({
|
|
693
|
+
protocol: route.protocol,
|
|
694
|
+
host: route.host.trim(),
|
|
695
|
+
port: route.port,
|
|
696
|
+
username: route.username,
|
|
697
|
+
password: route.password
|
|
698
|
+
});
|
|
699
|
+
};
|
|
700
|
+
const applyDefaultToAll = () => {
|
|
701
|
+
const ids = rows.map((row) => row.id);
|
|
702
|
+
if (ids.length === 0) return;
|
|
703
|
+
setDraft((current) => {
|
|
704
|
+
const next = { ...current };
|
|
705
|
+
for (const id of ids) next[id] = { ...next[id], useDefault: true };
|
|
706
|
+
return next;
|
|
707
|
+
});
|
|
708
|
+
saveMany(
|
|
709
|
+
"\0default\0applyAll",
|
|
710
|
+
ids.map((id) => ({ op: "set", path: ["providers", id, "useDefault"], value: true })),
|
|
711
|
+
"\u6240\u6709 provider \u7684 useDefault",
|
|
712
|
+
() => setDraft((current) => {
|
|
713
|
+
const next = { ...current };
|
|
714
|
+
for (const id of ids) {
|
|
715
|
+
const entry = next[id];
|
|
716
|
+
if (entry === void 0 || entry.useDefault === void 0) continue;
|
|
717
|
+
const nextEntry = { ...entry };
|
|
718
|
+
delete nextEntry.useDefault;
|
|
719
|
+
if (Object.keys(nextEntry).length === 0) delete next[id];
|
|
720
|
+
else next[id] = nextEntry;
|
|
721
|
+
}
|
|
722
|
+
return next;
|
|
723
|
+
})
|
|
724
|
+
);
|
|
725
|
+
};
|
|
726
|
+
const onProxyUrlPaste = (apply2) => (event) => {
|
|
727
|
+
const text = event?.clipboardData?.getData?.("text");
|
|
728
|
+
if (typeof text !== "string") return;
|
|
729
|
+
const parts = splitProxyUrl(text);
|
|
730
|
+
if (parts === void 0) return;
|
|
731
|
+
event.preventDefault?.();
|
|
732
|
+
apply2(parts);
|
|
733
|
+
};
|
|
734
|
+
const field = (label, control, hint, hintError) => h(
|
|
448
735
|
"label",
|
|
449
736
|
{ className: import_client.default.field, style: S.field, key: label },
|
|
450
737
|
h("span", { className: import_client.default.label, style: S.label }, label),
|
|
451
738
|
control,
|
|
452
739
|
hint === void 0 ? null : h(
|
|
453
740
|
"span",
|
|
454
|
-
{
|
|
741
|
+
{
|
|
742
|
+
className: import_client.default.hint,
|
|
743
|
+
style: {
|
|
744
|
+
gridColumn: "2",
|
|
745
|
+
fontSize: "11px",
|
|
746
|
+
// A warning that a value can never work is an answer, not
|
|
747
|
+
// decoration — same rule as the failed self-check text.
|
|
748
|
+
...hintError === true ? { color: "var(--dsw-alias-state-error-primary, #f85149)" } : {}
|
|
749
|
+
}
|
|
750
|
+
},
|
|
455
751
|
hint
|
|
456
752
|
)
|
|
457
753
|
);
|
|
458
|
-
const
|
|
754
|
+
const storedDefault = snapshot.value?.defaultProxy ?? {};
|
|
755
|
+
const defaultView = view({ ...storedDefault, ...defaultDraft });
|
|
756
|
+
const defaultWhy = defaultProxyProblem(defaultView);
|
|
757
|
+
const defaultUsable = defaultWhy === void 0;
|
|
758
|
+
const defaultOrigin = `${defaultView.protocol}://${defaultView.host}:${defaultView.port}`;
|
|
759
|
+
const defaultBlock = h(
|
|
760
|
+
"div",
|
|
761
|
+
{ className: import_client.default.defaultCard, style: S.defaultCard, key: "default-proxy" },
|
|
762
|
+
h(
|
|
763
|
+
"header",
|
|
764
|
+
{ className: import_client.default.cardHead, style: S.head },
|
|
765
|
+
h(
|
|
766
|
+
"div",
|
|
767
|
+
{ className: import_client.default.cardTitles, style: S.titles },
|
|
768
|
+
h("span", { className: import_client.default.name, style: S.name }, t.defaultTitle)
|
|
769
|
+
),
|
|
770
|
+
h(
|
|
771
|
+
"span",
|
|
772
|
+
{
|
|
773
|
+
className: defaultUsable ? import_client.default.pillOn : import_client.default.pillOff,
|
|
774
|
+
style: { ...S.pill, ...defaultUsable ? S.pillOn : S.pillOff }
|
|
775
|
+
},
|
|
776
|
+
defaultUsable ? t.on : t.off
|
|
777
|
+
)
|
|
778
|
+
),
|
|
779
|
+
h("p", { className: import_client.default.defaultLead, style: S.defaultLead }, t.defaultLead),
|
|
780
|
+
field(
|
|
781
|
+
t.protocol,
|
|
782
|
+
h(
|
|
783
|
+
"select",
|
|
784
|
+
{
|
|
785
|
+
className: import_client.default.input,
|
|
786
|
+
style: S.input,
|
|
787
|
+
value: defaultView.protocol,
|
|
788
|
+
onChange: (event) => bindDefault("protocol")(event.target.value)
|
|
789
|
+
},
|
|
790
|
+
...PROTOCOLS.map(
|
|
791
|
+
(protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
|
|
792
|
+
)
|
|
793
|
+
)
|
|
794
|
+
),
|
|
795
|
+
field(
|
|
796
|
+
t.host,
|
|
797
|
+
h("input", {
|
|
798
|
+
className: import_client.default.input,
|
|
799
|
+
style: S.input,
|
|
800
|
+
placeholder: "127.0.0.1",
|
|
801
|
+
value: defaultView.host,
|
|
802
|
+
onChange: (event) => bindDefault("host")(event.target.value),
|
|
803
|
+
// Same convenience as the cards: a full URL fills the whole block.
|
|
804
|
+
onPaste: onProxyUrlPaste(applyDefaultUrl)
|
|
805
|
+
}),
|
|
806
|
+
t.hostHint
|
|
807
|
+
),
|
|
808
|
+
field(
|
|
809
|
+
t.port,
|
|
810
|
+
h("input", {
|
|
811
|
+
className: import_client.default.input,
|
|
812
|
+
style: S.input,
|
|
813
|
+
type: "number",
|
|
814
|
+
min: 1,
|
|
815
|
+
max: 65535,
|
|
816
|
+
placeholder: "7890",
|
|
817
|
+
value: defaultView.port === 0 ? "" : String(defaultView.port),
|
|
818
|
+
onChange: (event) => {
|
|
819
|
+
const parsed = Number.parseInt(event.target.value, 10);
|
|
820
|
+
bindDefault("port")(Number.isNaN(parsed) ? 0 : parsed);
|
|
821
|
+
}
|
|
822
|
+
})
|
|
823
|
+
),
|
|
824
|
+
field(
|
|
825
|
+
t.username,
|
|
826
|
+
h("input", {
|
|
827
|
+
className: import_client.default.input,
|
|
828
|
+
style: S.input,
|
|
829
|
+
autocomplete: "off",
|
|
830
|
+
value: defaultView.username,
|
|
831
|
+
onChange: (event) => bindDefault("username")(event.target.value)
|
|
832
|
+
})
|
|
833
|
+
),
|
|
834
|
+
field(
|
|
835
|
+
t.password,
|
|
836
|
+
h("input", {
|
|
837
|
+
className: import_client.default.input,
|
|
838
|
+
style: S.input,
|
|
839
|
+
type: "password",
|
|
840
|
+
autocomplete: "off",
|
|
841
|
+
value: defaultView.password,
|
|
842
|
+
onChange: (event) => bindDefault("password")(event.target.value)
|
|
843
|
+
}),
|
|
844
|
+
t.passwordHintProxy
|
|
845
|
+
),
|
|
846
|
+
// Bulk opt-in, in ONE write: a per-card walk would rebuild the table N times.
|
|
847
|
+
// Disabled while the default is unusable, because pointing every card at an
|
|
848
|
+
// empty endpoint would just refuse them all.
|
|
849
|
+
h(
|
|
850
|
+
"div",
|
|
851
|
+
{ style: S.testRow, key: "default:apply" },
|
|
852
|
+
h(
|
|
853
|
+
"button",
|
|
854
|
+
{
|
|
855
|
+
className: import_client.default.testButton,
|
|
856
|
+
style: S.testButton,
|
|
857
|
+
type: "button",
|
|
858
|
+
disabled: !defaultUsable || rows.length === 0,
|
|
859
|
+
onClick: applyDefaultToAll
|
|
860
|
+
},
|
|
861
|
+
t.applyDefaultAll
|
|
862
|
+
)
|
|
863
|
+
)
|
|
864
|
+
);
|
|
865
|
+
const query = filter.trim().toLowerCase();
|
|
866
|
+
const shown = query.length === 0 ? rows : rows.filter((row) => `${row.id} ${row.name}`.toLowerCase().includes(query));
|
|
867
|
+
const filterRow = rows.length === 0 ? null : field(
|
|
868
|
+
t.filterLabel,
|
|
869
|
+
h("input", {
|
|
870
|
+
className: import_client.default.input,
|
|
871
|
+
style: S.input,
|
|
872
|
+
placeholder: t.filterPlaceholder,
|
|
873
|
+
value: filter,
|
|
874
|
+
onChange: (event) => setFilter(event.target.value)
|
|
875
|
+
})
|
|
876
|
+
);
|
|
877
|
+
const body = shown.length === 0 ? [
|
|
459
878
|
h(
|
|
460
879
|
"p",
|
|
461
880
|
{
|
|
@@ -471,57 +890,18 @@ function renderPage(ctx) {
|
|
|
471
890
|
color: "var(--dsw-alias-state-idle-primary)"
|
|
472
891
|
}
|
|
473
892
|
},
|
|
474
|
-
t.empty
|
|
893
|
+
query.length > 0 ? t.noMatch : t.empty
|
|
475
894
|
)
|
|
476
|
-
] :
|
|
895
|
+
] : shown.map((row) => {
|
|
477
896
|
const route = view({ ...configured[row.id], ...draft[row.id] });
|
|
478
|
-
const
|
|
897
|
+
const lastText = lastUseText(row.id) ?? (route.enabled ? t.noRecord : void 0);
|
|
479
898
|
const result = tests[row.id];
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
},
|
|
487
|
-
h(
|
|
488
|
-
"header",
|
|
489
|
-
{ className: import_client.default.cardHead, style: S.head },
|
|
490
|
-
h(
|
|
491
|
-
"div",
|
|
492
|
-
{ className: import_client.default.cardTitles, style: S.titles },
|
|
493
|
-
h("span", { className: import_client.default.name, style: S.name }, row.name),
|
|
494
|
-
h("span", { className: import_client.default.id, style: S.id }, row.id),
|
|
495
|
-
// A disabled route can never have hits, so "0" there would only add
|
|
496
|
-
// noise; the count shows once the route is on (or has counted).
|
|
497
|
-
route.enabled || (hits[row.id] ?? 0) > 0 ? h(
|
|
498
|
-
"span",
|
|
499
|
-
{ style: S.hits },
|
|
500
|
-
`${t.hitCountPrefix}${hits[row.id] ?? 0}${t.hitCountSuffix}`
|
|
501
|
-
) : null
|
|
502
|
-
),
|
|
503
|
-
h(
|
|
504
|
-
"span",
|
|
505
|
-
{
|
|
506
|
-
className: route.enabled ? import_client.default.pillOn : row.live ? import_client.default.pillLive : import_client.default.pillOff,
|
|
507
|
-
style: {
|
|
508
|
-
...S.pill,
|
|
509
|
-
...route.enabled ? S.pillOn : row.live ? S.pillLive : S.pillOff
|
|
510
|
-
}
|
|
511
|
-
},
|
|
512
|
-
route.enabled ? t.on : row.live ? t.live : t.off
|
|
513
|
-
)
|
|
514
|
-
),
|
|
515
|
-
h(
|
|
516
|
-
"label",
|
|
517
|
-
{ className: import_client.default.toggle, style: S.toggle },
|
|
518
|
-
h("input", {
|
|
519
|
-
type: "checkbox",
|
|
520
|
-
checked: route.enabled,
|
|
521
|
-
onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
|
|
522
|
-
}),
|
|
523
|
-
h("span", null, t.useProxy)
|
|
524
|
-
),
|
|
899
|
+
const onlyDead = deadRuleEntries(route.onlyHosts);
|
|
900
|
+
const bypassDead = deadRuleEntries(route.bypassHosts);
|
|
901
|
+
const useDefault = route.useDefault === true;
|
|
902
|
+
const endpointStored = route.useDefault ? defaultUsable : route.host.trim().length > 0 && Number.isInteger(route.port) && route.port >= 1 && route.port <= 65535;
|
|
903
|
+
const showTest = route.enabled || endpointStored;
|
|
904
|
+
const endpointFields = [
|
|
525
905
|
field(
|
|
526
906
|
t.protocol,
|
|
527
907
|
h(
|
|
@@ -529,7 +909,6 @@ function renderPage(ctx) {
|
|
|
529
909
|
{
|
|
530
910
|
className: import_client.default.input,
|
|
531
911
|
style: S.input,
|
|
532
|
-
disabled,
|
|
533
912
|
value: route.protocol,
|
|
534
913
|
onChange: (event) => bind(row.id, "protocol")(event.target.value)
|
|
535
914
|
},
|
|
@@ -544,10 +923,11 @@ function renderPage(ctx) {
|
|
|
544
923
|
h("input", {
|
|
545
924
|
className: import_client.default.input,
|
|
546
925
|
style: S.input,
|
|
547
|
-
disabled,
|
|
548
926
|
placeholder: "127.0.0.1",
|
|
549
927
|
value: route.host,
|
|
550
|
-
onChange: (event) => bind(row.id, "host")(event.target.value)
|
|
928
|
+
onChange: (event) => bind(row.id, "host")(event.target.value),
|
|
929
|
+
// A pasted full proxy URL fills every transport field at once.
|
|
930
|
+
onPaste: onProxyUrlPaste((parts) => applyCardUrl(row.id, parts))
|
|
551
931
|
}),
|
|
552
932
|
t.hostHint
|
|
553
933
|
),
|
|
@@ -556,7 +936,6 @@ function renderPage(ctx) {
|
|
|
556
936
|
h("input", {
|
|
557
937
|
className: import_client.default.input,
|
|
558
938
|
style: S.input,
|
|
559
|
-
disabled,
|
|
560
939
|
type: "number",
|
|
561
940
|
min: 1,
|
|
562
941
|
max: 65535,
|
|
@@ -573,13 +952,12 @@ function renderPage(ctx) {
|
|
|
573
952
|
h("input", {
|
|
574
953
|
className: import_client.default.input,
|
|
575
954
|
style: S.input,
|
|
576
|
-
disabled,
|
|
577
955
|
autocomplete: "off",
|
|
578
956
|
value: route.username,
|
|
579
957
|
onChange: (event) => bind(row.id, "username")(event.target.value)
|
|
580
958
|
})
|
|
581
959
|
),
|
|
582
|
-
// Every transport accepts a password
|
|
960
|
+
// Every transport accepts a password: SOCKS5 hands it to
|
|
583
961
|
// Socks5ProxyAgent as an option, HTTP/HTTPS embed it in the proxy URL
|
|
584
962
|
// userinfo (undici turns that into Proxy-Authorization). It is stored on
|
|
585
963
|
// the route, so it lands in the settings file with the rest of the
|
|
@@ -589,19 +967,106 @@ function renderPage(ctx) {
|
|
|
589
967
|
h("input", {
|
|
590
968
|
className: import_client.default.input,
|
|
591
969
|
style: S.input,
|
|
592
|
-
disabled,
|
|
593
970
|
type: "password",
|
|
594
971
|
autocomplete: "off",
|
|
595
972
|
value: route.password,
|
|
596
973
|
onChange: (event) => bind(row.id, "password")(event.target.value)
|
|
597
974
|
}),
|
|
598
975
|
route.protocol === "socks5" ? t.passwordHint : t.passwordHintProxy
|
|
976
|
+
)
|
|
977
|
+
];
|
|
978
|
+
return h(
|
|
979
|
+
"section",
|
|
980
|
+
{
|
|
981
|
+
className: route.enabled ? import_client.default.cardActive : import_client.default.card,
|
|
982
|
+
style: route.enabled ? { ...S.card, ...S.cardActive } : S.card,
|
|
983
|
+
key: row.id
|
|
984
|
+
},
|
|
985
|
+
h(
|
|
986
|
+
"header",
|
|
987
|
+
{ className: import_client.default.cardHead, style: S.head },
|
|
988
|
+
h(
|
|
989
|
+
"div",
|
|
990
|
+
{ className: import_client.default.cardTitles, style: S.titles },
|
|
991
|
+
h("span", { className: import_client.default.name, style: S.name }, row.name),
|
|
992
|
+
h("span", { className: import_client.default.id, style: S.id }, row.id),
|
|
993
|
+
// A disabled route can never have hits, so "0" there would only add
|
|
994
|
+
// noise; the count shows once the route is on (or has counted).
|
|
995
|
+
route.enabled || (hits[row.id] ?? 0) > 0 ? h(
|
|
996
|
+
"span",
|
|
997
|
+
{ style: S.hits },
|
|
998
|
+
`${t.hitCountPrefix}${hits[row.id] ?? 0}${t.hitCountSuffix}`
|
|
999
|
+
) : null,
|
|
1000
|
+
// The last-use line. A failure takes the error colour, same rule as
|
|
1001
|
+
// a failed self-check: it is an answer, not decoration.
|
|
1002
|
+
lastText === void 0 ? null : h(
|
|
1003
|
+
"span",
|
|
1004
|
+
{
|
|
1005
|
+
style: {
|
|
1006
|
+
...S.hits,
|
|
1007
|
+
...lastUse[row.id]?.ok === false ? S.inheritWarning : {}
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
lastText
|
|
1011
|
+
)
|
|
1012
|
+
),
|
|
1013
|
+
h(
|
|
1014
|
+
"span",
|
|
1015
|
+
{
|
|
1016
|
+
className: route.enabled ? import_client.default.pillOn : row.live ? import_client.default.pillLive : import_client.default.pillOff,
|
|
1017
|
+
style: {
|
|
1018
|
+
...S.pill,
|
|
1019
|
+
...route.enabled ? S.pillOn : row.live ? S.pillLive : S.pillOff
|
|
1020
|
+
}
|
|
1021
|
+
},
|
|
1022
|
+
route.enabled ? t.on : row.live ? t.live : t.off
|
|
1023
|
+
)
|
|
599
1024
|
),
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1025
|
+
h(
|
|
1026
|
+
"label",
|
|
1027
|
+
{ className: import_client.default.toggle, style: S.toggle },
|
|
1028
|
+
h("input", {
|
|
1029
|
+
type: "checkbox",
|
|
1030
|
+
checked: route.enabled,
|
|
1031
|
+
onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
|
|
1032
|
+
}),
|
|
1033
|
+
h("span", null, t.useProxy)
|
|
1034
|
+
),
|
|
1035
|
+
// Opting into the shared endpoint. The card then shows the address it
|
|
1036
|
+
// will actually use — or, when the default is not configured, why the
|
|
1037
|
+
// route is refused — instead of a set of empty inputs the user never
|
|
1038
|
+
// filled and cannot interpret.
|
|
1039
|
+
h(
|
|
1040
|
+
"label",
|
|
1041
|
+
{ className: import_client.default.toggle, style: S.toggle, key: `${row.id}:usedefault` },
|
|
1042
|
+
h("input", {
|
|
1043
|
+
type: "checkbox",
|
|
1044
|
+
checked: useDefault,
|
|
1045
|
+
onChange: (event) => bind(row.id, "useDefault")(event.target.checked === true)
|
|
1046
|
+
}),
|
|
1047
|
+
h("span", null, t.useDefault)
|
|
1048
|
+
),
|
|
1049
|
+
...useDefault ? [
|
|
1050
|
+
h(
|
|
1051
|
+
"p",
|
|
1052
|
+
{
|
|
1053
|
+
key: `${row.id}:inherit`,
|
|
1054
|
+
style: defaultUsable ? S.inheritNote : S.inheritWarning
|
|
1055
|
+
},
|
|
1056
|
+
defaultUsable ? `${t.inheritingDefault}${defaultOrigin}` : `${t.defaultMissing}${defaultWhy}`
|
|
1057
|
+
)
|
|
1058
|
+
] : endpointFields,
|
|
1059
|
+
// The self-check is offered whenever there is something to check: an
|
|
1060
|
+
// enabled route (the Host's table carries it, and a rejected one still
|
|
1061
|
+
// reports WHY), or a disabled one whose endpoint is already complete —
|
|
1062
|
+
// the Host builds that one for the check alone. A disabled route with
|
|
1063
|
+
// no endpoint has nothing to test and would only answer "not enabled".
|
|
1064
|
+
showTest ? h(
|
|
603
1065
|
"div",
|
|
604
1066
|
{ style: S.testRow, key: `${row.id}:test` },
|
|
1067
|
+
// A check on a route that is still off is a trial run, and it says
|
|
1068
|
+
// so — otherwise the result reads as though the route were live.
|
|
1069
|
+
route.enabled ? null : h("span", { style: S.testResult }, t.testDisabledNote),
|
|
605
1070
|
h(
|
|
606
1071
|
"button",
|
|
607
1072
|
{
|
|
@@ -624,8 +1089,22 @@ function renderPage(ctx) {
|
|
|
624
1089
|
{ key: "lat", style: { color: latencyColor(result.ms) } },
|
|
625
1090
|
`\u2713 ${typeof result.ms === "number" ? result.ms : "\u2014"} ms`
|
|
626
1091
|
),
|
|
627
|
-
result.ip === void 0 ? null : ` \xB7 ${t.exitIpLabel}${result.ip}
|
|
1092
|
+
result.ip === void 0 ? null : ` \xB7 ${t.exitIpLabel}${result.ip}`,
|
|
1093
|
+
result.direct === true ? ` \xB7 ${t.directLabel}` : null
|
|
628
1094
|
] : h("span", { key: "err", style: S.testFailure }, `\u2717 ${result.error}`)
|
|
1095
|
+
) : null,
|
|
1096
|
+
// Promote this card's endpoint to the shared default. Offered only
|
|
1097
|
+
// when the card carries its OWN endpoint: an inheriting card would
|
|
1098
|
+
// just be copying the default onto itself.
|
|
1099
|
+
endpointStored && !useDefault ? h(
|
|
1100
|
+
"button",
|
|
1101
|
+
{
|
|
1102
|
+
className: import_client.default.testButton,
|
|
1103
|
+
style: S.testButton,
|
|
1104
|
+
type: "button",
|
|
1105
|
+
onClick: () => promoteToDefault(row.id)
|
|
1106
|
+
},
|
|
1107
|
+
t.setAsDefault
|
|
629
1108
|
) : null
|
|
630
1109
|
) : null,
|
|
631
1110
|
// Target-host rules, collapsed so the common case stays one short card.
|
|
@@ -638,24 +1117,38 @@ function renderPage(ctx) {
|
|
|
638
1117
|
h("input", {
|
|
639
1118
|
className: import_client.default.input,
|
|
640
1119
|
style: S.input,
|
|
641
|
-
disabled,
|
|
642
1120
|
placeholder: "api.example.com",
|
|
643
1121
|
value: route.onlyHosts,
|
|
644
1122
|
onChange: (event) => bind(row.id, "onlyHosts")(event.target.value)
|
|
645
1123
|
}),
|
|
646
|
-
t.onlyHostsHint
|
|
1124
|
+
onlyDead.length === 0 ? t.onlyHostsHint : `${t.ruleDeadHint}${onlyDead.join("\u3001")}`,
|
|
1125
|
+
onlyDead.length > 0
|
|
647
1126
|
),
|
|
648
1127
|
field(
|
|
649
1128
|
t.bypassHosts,
|
|
650
1129
|
h("input", {
|
|
651
1130
|
className: import_client.default.input,
|
|
652
1131
|
style: S.input,
|
|
653
|
-
disabled,
|
|
654
1132
|
placeholder: "localhost",
|
|
655
1133
|
value: route.bypassHosts,
|
|
656
1134
|
onChange: (event) => bind(row.id, "bypassHosts")(event.target.value)
|
|
657
1135
|
}),
|
|
658
|
-
t.bypassHostsHint
|
|
1136
|
+
bypassDead.length === 0 ? t.bypassHostsHint : `${t.ruleDeadHint}${bypassDead.join("\u3001")}`,
|
|
1137
|
+
bypassDead.length > 0
|
|
1138
|
+
),
|
|
1139
|
+
// The self-check target: empty uses the top-level testUrl, set makes
|
|
1140
|
+
// this card's Test request this URL — a real upstream proves the whole
|
|
1141
|
+
// path, not just the proxy hop.
|
|
1142
|
+
field(
|
|
1143
|
+
t.testTarget,
|
|
1144
|
+
h("input", {
|
|
1145
|
+
className: import_client.default.input,
|
|
1146
|
+
style: S.input,
|
|
1147
|
+
placeholder: "https://api.example.com",
|
|
1148
|
+
value: route.testTarget,
|
|
1149
|
+
onChange: (event) => bind(row.id, "testTarget")(event.target.value)
|
|
1150
|
+
}),
|
|
1151
|
+
t.testTargetHint
|
|
659
1152
|
)
|
|
660
1153
|
)
|
|
661
1154
|
);
|
|
@@ -664,6 +1157,8 @@ function renderPage(ctx) {
|
|
|
664
1157
|
"div",
|
|
665
1158
|
{ className: import_client.default.root, style: S.root },
|
|
666
1159
|
h("p", { className: import_client.default.lead, style: { margin: "0 0 4px", fontSize: "12px", lineHeight: "1.6", color: "var(--dsw-alias-label-secondary)" } }, t.lead),
|
|
1160
|
+
defaultBlock,
|
|
1161
|
+
filterRow,
|
|
667
1162
|
...body,
|
|
668
1163
|
h(
|
|
669
1164
|
"details",
|
|
@@ -699,28 +1194,51 @@ function renderPage(ctx) {
|
|
|
699
1194
|
);
|
|
700
1195
|
}
|
|
701
1196
|
var EN = {
|
|
702
|
-
lead: "Route selected model providers through your own forward proxy. Other providers stay direct.",
|
|
1197
|
+
lead: "Route selected model providers through your own forward proxy. Other providers stay direct. Requests to loopback or private addresses (localhost, 127.x, private ranges) always go direct \u2014 a forward proxy cannot reach your own machine.",
|
|
703
1198
|
empty: "No providers found yet.",
|
|
704
1199
|
useProxy: "Use a proxy for this provider",
|
|
1200
|
+
useDefault: "Use the default proxy",
|
|
1201
|
+
defaultTitle: "Default proxy",
|
|
1202
|
+
defaultLead: 'One endpoint, reused by every provider that turns on "Use the default proxy".',
|
|
1203
|
+
inheritingDefault: "Using the default proxy: ",
|
|
1204
|
+
defaultMissing: "The default proxy cannot be used, so this provider is not proxied: ",
|
|
1205
|
+
filterLabel: "Filter",
|
|
1206
|
+
filterPlaceholder: "Filter by name or id\u2026",
|
|
1207
|
+
noMatch: "No provider matches the filter.",
|
|
1208
|
+
setAsDefault: "Set as the default proxy",
|
|
1209
|
+
applyDefaultAll: "Use the default proxy for all providers",
|
|
705
1210
|
protocol: "Type",
|
|
706
1211
|
host: "IP / host",
|
|
707
1212
|
port: "Port",
|
|
708
1213
|
username: "Username",
|
|
709
1214
|
password: "Password",
|
|
710
|
-
hostHint: "Host or IP only, without scheme or path.",
|
|
1215
|
+
hostHint: "Host or IP only, without scheme or path. A full proxy URL (socks5://user:pass@host:1080) can be pasted instead \u2014 it is split into the fields.",
|
|
711
1216
|
passwordHint: "Sent to the SOCKS5 proxy for authentication; stored with this setting.",
|
|
712
1217
|
passwordHintProxy: "Sent to the proxy as an authentication password; stored with this setting.",
|
|
713
1218
|
hitCountPrefix: "",
|
|
714
1219
|
hitCountSuffix: " proxied",
|
|
1220
|
+
recentOk: "last ok ",
|
|
1221
|
+
recentFail: "last failure ",
|
|
1222
|
+
recentStarted: "last call ",
|
|
1223
|
+
noRecord: "no proxied request yet",
|
|
1224
|
+
justNow: "just now",
|
|
1225
|
+
minutesAgo: " min ago",
|
|
1226
|
+
hoursAgo: " h ago",
|
|
1227
|
+
daysAgo: " d ago",
|
|
715
1228
|
testButton: "Test",
|
|
716
1229
|
testRunning: "Testing\u2026",
|
|
1230
|
+
testDisabledNote: "Not enabled \u2014 test only: ",
|
|
717
1231
|
testFailed: "Test failed",
|
|
718
1232
|
exitIpLabel: "exit IP ",
|
|
1233
|
+
directLabel: "direct (not through the proxy)",
|
|
719
1234
|
advanced: "Advanced: target host rules",
|
|
720
1235
|
onlyHosts: "Proxy only",
|
|
721
1236
|
onlyHostsHint: "Comma/space separated; when set, only requests to these hosts are proxied (suffix match, * for all).",
|
|
722
1237
|
bypassHosts: "Bypass",
|
|
723
1238
|
bypassHostsHint: "Comma/space separated; requests to these hosts go direct, taking priority.",
|
|
1239
|
+
ruleDeadHint: "These entries never match \u2014 rules take host names only, without scheme or path: ",
|
|
1240
|
+
testTarget: "Self-check target",
|
|
1241
|
+
testTargetHint: "Empty uses the top-level testUrl; when set, this card's Test requests this URL instead \u2014 point it at a real upstream to verify the end-to-end path.",
|
|
724
1242
|
socks5Hint: "SOCKS5 tunnels the connection; the proxy resolves the target host name.",
|
|
725
1243
|
on: "proxied",
|
|
726
1244
|
live: "active",
|
|
@@ -729,28 +1247,51 @@ var EN = {
|
|
|
729
1247
|
diagPending: "(collecting\u2026)"
|
|
730
1248
|
};
|
|
731
1249
|
var ZH = {
|
|
732
|
-
lead: "\u8BA9\u6307\u5B9A\u7684 provider \u8D70\u4F60\u81EA\u5DF1\u7684\u6B63\u5411\u4EE3\u7406\uFF0C\u5176\u5B83 provider \u4FDD\u6301\u76F4\u8FDE\u3002",
|
|
1250
|
+
lead: "\u8BA9\u6307\u5B9A\u7684 provider \u8D70\u4F60\u81EA\u5DF1\u7684\u6B63\u5411\u4EE3\u7406\uFF0C\u5176\u5B83 provider \u4FDD\u6301\u76F4\u8FDE\u3002\u6307\u5411\u672C\u673A\u6216\u5185\u7F51\u5730\u5740\uFF08localhost\u3001127.x\u3001\u79C1\u6709\u7F51\u6BB5\uFF09\u7684\u8BF7\u6C42\u59CB\u7EC8\u76F4\u8FDE\u2014\u2014\u6B63\u5411\u4EE3\u7406\u591F\u4E0D\u5230\u4F60\u81EA\u5DF1\u7684\u673A\u5668\u3002",
|
|
733
1251
|
empty: "\u6CA1\u6709\u68C0\u6D4B\u5230\u4EFB\u4F55 provider\u3002",
|
|
734
1252
|
useProxy: "\u4E3A\u8BE5 provider \u542F\u7528\u4EE3\u7406",
|
|
1253
|
+
useDefault: "\u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406",
|
|
1254
|
+
defaultTitle: "\u9ED8\u8BA4\u4EE3\u7406",
|
|
1255
|
+
defaultLead: "\u4E3A\u52FE\u9009\u300C\u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406\u300D\u7684 provider \u590D\u7528\u7684\u7AEF\u70B9\uFF0C\u53EA\u586B\u4E00\u6B21\u3002",
|
|
1256
|
+
inheritingDefault: "\u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406\uFF1A",
|
|
1257
|
+
defaultMissing: "\u9876\u5C42\u9ED8\u8BA4\u4EE3\u7406\u4E0D\u53EF\u7528\uFF0C\u8BE5 provider \u4E0D\u4F1A\u8D70\u4EE3\u7406\uFF1A",
|
|
1258
|
+
filterLabel: "\u8FC7\u6EE4",
|
|
1259
|
+
filterPlaceholder: "\u6309\u540D\u79F0\u6216 id \u8FC7\u6EE4\u2026",
|
|
1260
|
+
noMatch: "\u6CA1\u6709\u5339\u914D\u7684 provider\u3002",
|
|
1261
|
+
setAsDefault: "\u8BBE\u4E3A\u9ED8\u8BA4\u4EE3\u7406",
|
|
1262
|
+
applyDefaultAll: "\u6240\u6709 provider \u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406",
|
|
735
1263
|
protocol: "\u7C7B\u578B",
|
|
736
1264
|
host: "IP / \u4E3B\u673A",
|
|
737
1265
|
port: "\u7AEF\u53E3",
|
|
738
1266
|
username: "\u7528\u6237\u540D",
|
|
739
1267
|
password: "\u5BC6\u7801",
|
|
740
|
-
hostHint: "\u53EA\u586B\u4E3B\u673A\u540D\u6216 IP\uFF0C\u4E0D\u8981\u5E26\u534F\u8BAE\u524D\u7F00\u6216\u8DEF\u5F84\u3002",
|
|
1268
|
+
hostHint: "\u53EA\u586B\u4E3B\u673A\u540D\u6216 IP\uFF0C\u4E0D\u8981\u5E26\u534F\u8BAE\u524D\u7F00\u6216\u8DEF\u5F84\uFF1B\u4E5F\u53EF\u4EE5\u76F4\u63A5\u7C98\u8D34\u5B8C\u6574\u4EE3\u7406 URL\uFF08\u5982 socks5://user:pass@host:1080\uFF09\uFF0C\u4F1A\u81EA\u52A8\u62C6\u5206\u5230\u5404\u5B57\u6BB5\u3002",
|
|
741
1269
|
passwordHint: "\u4EC5\u4F5C\u4E3A SOCKS5 \u8BA4\u8BC1\u53E3\u4EE4\u53D1\u9001\u7ED9\u4EE3\u7406\uFF1B\u968F\u8BE5\u8BBE\u7F6E\u4E00\u8D77\u4FDD\u5B58\u3002",
|
|
742
1270
|
passwordHintProxy: "\u4F5C\u4E3A\u4EE3\u7406\u8BA4\u8BC1\u53E3\u4EE4\u53D1\u9001\u7ED9\u4EE3\u7406\uFF1B\u968F\u8BE5\u8BBE\u7F6E\u4E00\u8D77\u4FDD\u5B58\u3002",
|
|
743
1271
|
hitCountPrefix: "\u5DF2\u4EE3\u7406 ",
|
|
744
1272
|
hitCountSuffix: " \u6B21",
|
|
1273
|
+
recentOk: "\u6700\u8FD1\u6210\u529F ",
|
|
1274
|
+
recentFail: "\u6700\u8FD1\u5931\u8D25 ",
|
|
1275
|
+
recentStarted: "\u6700\u8FD1\u8C03\u7528 ",
|
|
1276
|
+
noRecord: "\u5C1A\u65E0\u4EE3\u7406\u8BB0\u5F55",
|
|
1277
|
+
justNow: "\u521A\u521A",
|
|
1278
|
+
minutesAgo: " \u5206\u949F\u524D",
|
|
1279
|
+
hoursAgo: " \u5C0F\u65F6\u524D",
|
|
1280
|
+
daysAgo: " \u5929\u524D",
|
|
745
1281
|
testButton: "\u6D4B\u8BD5",
|
|
746
1282
|
testRunning: "\u6D4B\u8BD5\u4E2D\u2026",
|
|
1283
|
+
testDisabledNote: "\u672A\u542F\u7528\uFF0C\u4EC5\u6D4B\u8BD5\uFF1A",
|
|
747
1284
|
testFailed: "\u6D4B\u8BD5\u5931\u8D25",
|
|
748
1285
|
exitIpLabel: "\u51FA\u53E3 IP ",
|
|
1286
|
+
directLabel: "\u76F4\u8FDE\uFF08\u672A\u7ECF\u8FC7\u4EE3\u7406\uFF09",
|
|
749
1287
|
advanced: "\u9AD8\u7EA7\uFF1A\u76EE\u6807\u57DF\u540D\u89C4\u5219",
|
|
750
1288
|
onlyHosts: "\u4EC5\u8D70\u4EE3\u7406",
|
|
751
1289
|
onlyHostsHint: "\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694\uFF1B\u975E\u7A7A\u65F6\u53EA\u6709\u5339\u914D\u8FD9\u4E9B\u4E3B\u673A\u7684\u8BF7\u6C42\u8D70\u4EE3\u7406\uFF08\u540E\u7F00\u5339\u914D\uFF0C* \u8868\u793A\u5168\u90E8\uFF09\u3002",
|
|
752
1290
|
bypassHosts: "\u76F4\u8FDE",
|
|
753
1291
|
bypassHostsHint: "\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694\uFF1B\u5339\u914D\u8FD9\u4E9B\u4E3B\u673A\u7684\u8BF7\u6C42\u76F4\u8FDE\uFF0C\u4F18\u5148\u4E8E\u4E0A\u4E00\u9879\u3002",
|
|
1292
|
+
ruleDeadHint: "\u4EE5\u4E0B\u6761\u76EE\u6C38\u8FDC\u5339\u914D\u4E0D\u4E0A\u2014\u2014\u89C4\u5219\u53EA\u8BA4\u4E3B\u673A\u540D\uFF0C\u4E0D\u8981\u5E26\u534F\u8BAE\u524D\u7F00\u6216\u8DEF\u5F84\uFF1A",
|
|
1293
|
+
testTarget: "\u81EA\u68C0\u76EE\u6807",
|
|
1294
|
+
testTargetHint: "\u7559\u7A7A\u7528\u9876\u5C42 testUrl\uFF1B\u586B\u5199\u540E\u8BE5\u5361\u7247\u7684\u300C\u6D4B\u8BD5\u300D\u6539\u4E3A\u8BF7\u6C42\u8FD9\u4E2A\u5730\u5740\uFF0C\u53EF\u62FF\u771F\u5B9E\u4E0A\u6E38\u9A8C\u8BC1\u7AEF\u5230\u7AEF\u8DEF\u5F84\u3002",
|
|
754
1295
|
socks5Hint: "SOCKS5 \u4F1A\u5EFA\u7ACB\u96A7\u9053\uFF0C\u76EE\u6807\u57DF\u540D\u7531\u4EE3\u7406\u7AEF\u89E3\u6790\u3002",
|
|
755
1296
|
on: "\u8D70\u4EE3\u7406",
|
|
756
1297
|
live: "\u542F\u7528\u4E2D",
|