dsh-provider-proxy 0.4.0 → 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 +61 -7
- package/lib/client.js +676 -99
- package/lib/client.js.map +4 -4
- package/lib/index.js +408 -73
- 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}`);
|
|
@@ -197,21 +231,99 @@ async function fetchCatalog() {
|
|
|
197
231
|
if (typeof count === "number" && Number.isFinite(count)) hits[id] = count;
|
|
198
232
|
}
|
|
199
233
|
}
|
|
200
|
-
|
|
234
|
+
const problems = {};
|
|
235
|
+
const rejected = body?.problems;
|
|
236
|
+
if (rejected !== null && typeof rejected === "object") {
|
|
237
|
+
for (const [id, message] of Object.entries(rejected)) {
|
|
238
|
+
if (typeof message === "string" && message.length > 0) problems[id] = message;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
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);
|
|
201
274
|
}
|
|
202
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
|
+
}
|
|
203
279
|
function view(route) {
|
|
204
280
|
return {
|
|
205
281
|
enabled: route?.enabled === true,
|
|
282
|
+
useDefault: route?.useDefault === true,
|
|
206
283
|
protocol: PROTOCOLS.includes(route?.protocol) ? route.protocol : "http",
|
|
207
284
|
host: route?.host ?? "",
|
|
208
285
|
port: route?.port ?? 0,
|
|
209
286
|
username: route?.username ?? "",
|
|
210
287
|
password: route?.password ?? "",
|
|
211
288
|
onlyHosts: route?.onlyHosts ?? "",
|
|
212
|
-
bypassHosts: route?.bypassHosts ?? ""
|
|
289
|
+
bypassHosts: route?.bypassHosts ?? "",
|
|
290
|
+
testTarget: route?.testTarget ?? ""
|
|
291
|
+
};
|
|
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)
|
|
213
323
|
};
|
|
214
324
|
}
|
|
325
|
+
var WRITE_DELAY_MS = 300;
|
|
326
|
+
var pendingWrites = /* @__PURE__ */ new Map();
|
|
215
327
|
var diagnostics = [];
|
|
216
328
|
function note(message) {
|
|
217
329
|
if (!diagnostics.includes(message)) diagnostics.push(message);
|
|
@@ -288,6 +400,19 @@ var S = {
|
|
|
288
400
|
color: "var(--dsw-alias-label-secondary)",
|
|
289
401
|
wordBreak: "break-all"
|
|
290
402
|
},
|
|
403
|
+
/**
|
|
404
|
+
* A failed check is the answer, not decoration: it takes the error colour and
|
|
405
|
+
* a heavier weight, unlike the latency band colour which only tints a number
|
|
406
|
+
* that is printed anyway.
|
|
407
|
+
*/
|
|
408
|
+
testFailure: {
|
|
409
|
+
color: "var(--dsw-alias-state-error-primary, #f85149)",
|
|
410
|
+
fontWeight: "500"
|
|
411
|
+
},
|
|
412
|
+
/** A rejected route is a failure of the settings, not a warning about them. */
|
|
413
|
+
diagProblem: {
|
|
414
|
+
color: "var(--dsw-alias-state-error-primary, #f85149)"
|
|
415
|
+
},
|
|
291
416
|
advanced: {
|
|
292
417
|
marginTop: "4px",
|
|
293
418
|
fontSize: "12px"
|
|
@@ -314,6 +439,40 @@ var S = {
|
|
|
314
439
|
color: "var(--dsw-alias-label-secondary)"
|
|
315
440
|
},
|
|
316
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
|
+
},
|
|
317
476
|
toggle: {
|
|
318
477
|
display: "flex",
|
|
319
478
|
alignItems: "center",
|
|
@@ -363,21 +522,52 @@ function renderPage(ctx) {
|
|
|
363
522
|
}]))(() => {
|
|
364
523
|
void fetchCatalog().then((catalog) => catalogueSetter?.(catalog)).catch((error) => {
|
|
365
524
|
note(`provider \u5217\u8868\u83B7\u53D6\u5931\u8D25\uFF1A${error?.message ?? error}`);
|
|
366
|
-
catalogueSetter?.({ rows: [], hits: {} });
|
|
525
|
+
catalogueSetter?.({ rows: [], hits: {}, problems: {}, lastUse: {} });
|
|
367
526
|
});
|
|
368
|
-
return {
|
|
527
|
+
return {
|
|
528
|
+
rows: void 0,
|
|
529
|
+
hits: {},
|
|
530
|
+
problems: {},
|
|
531
|
+
lastUse: {},
|
|
532
|
+
pending: true
|
|
533
|
+
};
|
|
369
534
|
});
|
|
370
|
-
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();
|
|
371
543
|
const published = state.rows ?? [];
|
|
372
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
|
+
};
|
|
553
|
+
const problems = state.problems ?? {};
|
|
554
|
+
const problemLines = Object.entries(problems).map(
|
|
555
|
+
([id, message]) => h("li", { key: `p:${id}`, style: S.diagProblem }, `${id}\uFF1A${message}`)
|
|
556
|
+
);
|
|
557
|
+
const problemCount = problemLines.length;
|
|
373
558
|
note(
|
|
374
559
|
`provider \u5217\u8868\uFF1A\u6765\u81EA Host \u5171 ${published.length} \u4E2A\uFF08${published.map((r) => r.id).join("\u3001") || "\u65E0"}\uFF09`
|
|
375
560
|
);
|
|
376
561
|
const [draft, setDraft] = (React.useState ?? ((v) => [v, () => {
|
|
377
562
|
}]))({});
|
|
378
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 }));
|
|
379
567
|
const [tests, setTests] = (React.useState ?? ((v) => [v, () => {
|
|
380
568
|
}]))({});
|
|
569
|
+
const [filter, setFilter] = (React.useState ?? ((v) => [v, () => {
|
|
570
|
+
}]))("");
|
|
381
571
|
const runTest = (id) => {
|
|
382
572
|
setTests((current) => ({ ...current, [id]: { pending: true } }));
|
|
383
573
|
void (async () => {
|
|
@@ -389,10 +579,6 @@ function renderPage(ctx) {
|
|
|
389
579
|
});
|
|
390
580
|
const body2 = await response.json();
|
|
391
581
|
setTests((current) => ({ ...current, [id]: testResultFrom(body2, t) }));
|
|
392
|
-
if (body2?.ok === true) {
|
|
393
|
-
void fetchCatalog().then((catalog) => catalogueSetter?.(catalog)).catch(() => {
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
582
|
} catch (error) {
|
|
397
583
|
setTests((current) => ({
|
|
398
584
|
...current,
|
|
@@ -402,34 +588,293 @@ function renderPage(ctx) {
|
|
|
402
588
|
})();
|
|
403
589
|
};
|
|
404
590
|
const rows = knownProviders({ ...configured, ...draft }, published);
|
|
405
|
-
const save = (
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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();
|
|
414
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;
|
|
415
615
|
});
|
|
416
616
|
};
|
|
417
617
|
const bind = (id, field2) => (value) => {
|
|
418
618
|
edit(id, field2, value);
|
|
419
|
-
save(
|
|
619
|
+
save(
|
|
620
|
+
`${id}\0${field2}`,
|
|
621
|
+
["providers", id, field2],
|
|
622
|
+
`${id}.${field2}`,
|
|
623
|
+
value,
|
|
624
|
+
() => rollbackRouteField(id, field2)
|
|
625
|
+
);
|
|
626
|
+
};
|
|
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);
|
|
420
733
|
};
|
|
421
|
-
const field = (label, control, hint) => h(
|
|
734
|
+
const field = (label, control, hint, hintError) => h(
|
|
422
735
|
"label",
|
|
423
736
|
{ className: import_client.default.field, style: S.field, key: label },
|
|
424
737
|
h("span", { className: import_client.default.label, style: S.label }, label),
|
|
425
738
|
control,
|
|
426
739
|
hint === void 0 ? null : h(
|
|
427
740
|
"span",
|
|
428
|
-
{
|
|
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
|
+
},
|
|
429
751
|
hint
|
|
430
752
|
)
|
|
431
753
|
);
|
|
432
|
-
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 ? [
|
|
433
878
|
h(
|
|
434
879
|
"p",
|
|
435
880
|
{
|
|
@@ -445,57 +890,18 @@ function renderPage(ctx) {
|
|
|
445
890
|
color: "var(--dsw-alias-state-idle-primary)"
|
|
446
891
|
}
|
|
447
892
|
},
|
|
448
|
-
t.empty
|
|
893
|
+
query.length > 0 ? t.noMatch : t.empty
|
|
449
894
|
)
|
|
450
|
-
] :
|
|
895
|
+
] : shown.map((row) => {
|
|
451
896
|
const route = view({ ...configured[row.id], ...draft[row.id] });
|
|
452
|
-
const
|
|
897
|
+
const lastText = lastUseText(row.id) ?? (route.enabled ? t.noRecord : void 0);
|
|
453
898
|
const result = tests[row.id];
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
},
|
|
461
|
-
h(
|
|
462
|
-
"header",
|
|
463
|
-
{ className: import_client.default.cardHead, style: S.head },
|
|
464
|
-
h(
|
|
465
|
-
"div",
|
|
466
|
-
{ className: import_client.default.cardTitles, style: S.titles },
|
|
467
|
-
h("span", { className: import_client.default.name, style: S.name }, row.name),
|
|
468
|
-
h("span", { className: import_client.default.id, style: S.id }, row.id),
|
|
469
|
-
// A disabled route can never have hits, so "0" there would only add
|
|
470
|
-
// noise; the count shows once the route is on (or has counted).
|
|
471
|
-
route.enabled || (hits[row.id] ?? 0) > 0 ? h(
|
|
472
|
-
"span",
|
|
473
|
-
{ className: import_client.default.hits, style: S.hits },
|
|
474
|
-
`${t.hitCountPrefix}${hits[row.id] ?? 0}${t.hitCountSuffix}`
|
|
475
|
-
) : null
|
|
476
|
-
),
|
|
477
|
-
h(
|
|
478
|
-
"span",
|
|
479
|
-
{
|
|
480
|
-
className: route.enabled ? import_client.default.pillOn : row.live ? import_client.default.pillLive : import_client.default.pillOff,
|
|
481
|
-
style: {
|
|
482
|
-
...S.pill,
|
|
483
|
-
...route.enabled ? S.pillOn : row.live ? S.pillLive : S.pillOff
|
|
484
|
-
}
|
|
485
|
-
},
|
|
486
|
-
route.enabled ? t.on : row.live ? t.live : t.off
|
|
487
|
-
)
|
|
488
|
-
),
|
|
489
|
-
h(
|
|
490
|
-
"label",
|
|
491
|
-
{ className: import_client.default.toggle, style: S.toggle },
|
|
492
|
-
h("input", {
|
|
493
|
-
type: "checkbox",
|
|
494
|
-
checked: route.enabled,
|
|
495
|
-
onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
|
|
496
|
-
}),
|
|
497
|
-
h("span", null, t.useProxy)
|
|
498
|
-
),
|
|
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 = [
|
|
499
905
|
field(
|
|
500
906
|
t.protocol,
|
|
501
907
|
h(
|
|
@@ -503,7 +909,6 @@ function renderPage(ctx) {
|
|
|
503
909
|
{
|
|
504
910
|
className: import_client.default.input,
|
|
505
911
|
style: S.input,
|
|
506
|
-
disabled,
|
|
507
912
|
value: route.protocol,
|
|
508
913
|
onChange: (event) => bind(row.id, "protocol")(event.target.value)
|
|
509
914
|
},
|
|
@@ -518,10 +923,11 @@ function renderPage(ctx) {
|
|
|
518
923
|
h("input", {
|
|
519
924
|
className: import_client.default.input,
|
|
520
925
|
style: S.input,
|
|
521
|
-
disabled,
|
|
522
926
|
placeholder: "127.0.0.1",
|
|
523
927
|
value: route.host,
|
|
524
|
-
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))
|
|
525
931
|
}),
|
|
526
932
|
t.hostHint
|
|
527
933
|
),
|
|
@@ -530,7 +936,6 @@ function renderPage(ctx) {
|
|
|
530
936
|
h("input", {
|
|
531
937
|
className: import_client.default.input,
|
|
532
938
|
style: S.input,
|
|
533
|
-
disabled,
|
|
534
939
|
type: "number",
|
|
535
940
|
min: 1,
|
|
536
941
|
max: 65535,
|
|
@@ -547,13 +952,12 @@ function renderPage(ctx) {
|
|
|
547
952
|
h("input", {
|
|
548
953
|
className: import_client.default.input,
|
|
549
954
|
style: S.input,
|
|
550
|
-
disabled,
|
|
551
955
|
autocomplete: "off",
|
|
552
956
|
value: route.username,
|
|
553
957
|
onChange: (event) => bind(row.id, "username")(event.target.value)
|
|
554
958
|
})
|
|
555
959
|
),
|
|
556
|
-
// Every transport accepts a password
|
|
960
|
+
// Every transport accepts a password: SOCKS5 hands it to
|
|
557
961
|
// Socks5ProxyAgent as an option, HTTP/HTTPS embed it in the proxy URL
|
|
558
962
|
// userinfo (undici turns that into Proxy-Authorization). It is stored on
|
|
559
963
|
// the route, so it lands in the settings file with the rest of the
|
|
@@ -563,19 +967,106 @@ function renderPage(ctx) {
|
|
|
563
967
|
h("input", {
|
|
564
968
|
className: import_client.default.input,
|
|
565
969
|
style: S.input,
|
|
566
|
-
disabled,
|
|
567
970
|
type: "password",
|
|
568
971
|
autocomplete: "off",
|
|
569
972
|
value: route.password,
|
|
570
973
|
onChange: (event) => bind(row.id, "password")(event.target.value)
|
|
571
974
|
}),
|
|
572
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
|
+
)
|
|
1024
|
+
),
|
|
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)
|
|
573
1048
|
),
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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(
|
|
577
1065
|
"div",
|
|
578
|
-
{
|
|
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),
|
|
579
1070
|
h(
|
|
580
1071
|
"button",
|
|
581
1072
|
{
|
|
@@ -589,7 +1080,7 @@ function renderPage(ctx) {
|
|
|
589
1080
|
),
|
|
590
1081
|
result?.pending === false ? h(
|
|
591
1082
|
"span",
|
|
592
|
-
{
|
|
1083
|
+
{ style: S.testResult },
|
|
593
1084
|
result.ok === true ? [
|
|
594
1085
|
// Only the latency carries the band colour; the exit IP
|
|
595
1086
|
// stays in the secondary label colour.
|
|
@@ -598,38 +1089,66 @@ function renderPage(ctx) {
|
|
|
598
1089
|
{ key: "lat", style: { color: latencyColor(result.ms) } },
|
|
599
1090
|
`\u2713 ${typeof result.ms === "number" ? result.ms : "\u2014"} ms`
|
|
600
1091
|
),
|
|
601
|
-
result.ip === void 0 ? null : ` \xB7 ${t.exitIpLabel}${result.ip}
|
|
602
|
-
|
|
1092
|
+
result.ip === void 0 ? null : ` \xB7 ${t.exitIpLabel}${result.ip}`,
|
|
1093
|
+
result.direct === true ? ` \xB7 ${t.directLabel}` : null
|
|
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
|
|
603
1108
|
) : null
|
|
604
1109
|
) : null,
|
|
605
1110
|
// Target-host rules, collapsed so the common case stays one short card.
|
|
606
1111
|
h(
|
|
607
1112
|
"details",
|
|
608
|
-
{
|
|
609
|
-
h("summary", {
|
|
1113
|
+
{ style: S.advanced, key: `${row.id}:rules` },
|
|
1114
|
+
h("summary", { style: S.advancedSummary }, t.advanced),
|
|
610
1115
|
field(
|
|
611
1116
|
t.onlyHosts,
|
|
612
1117
|
h("input", {
|
|
613
1118
|
className: import_client.default.input,
|
|
614
1119
|
style: S.input,
|
|
615
|
-
disabled,
|
|
616
1120
|
placeholder: "api.example.com",
|
|
617
1121
|
value: route.onlyHosts,
|
|
618
1122
|
onChange: (event) => bind(row.id, "onlyHosts")(event.target.value)
|
|
619
1123
|
}),
|
|
620
|
-
t.onlyHostsHint
|
|
1124
|
+
onlyDead.length === 0 ? t.onlyHostsHint : `${t.ruleDeadHint}${onlyDead.join("\u3001")}`,
|
|
1125
|
+
onlyDead.length > 0
|
|
621
1126
|
),
|
|
622
1127
|
field(
|
|
623
1128
|
t.bypassHosts,
|
|
624
1129
|
h("input", {
|
|
625
1130
|
className: import_client.default.input,
|
|
626
1131
|
style: S.input,
|
|
627
|
-
disabled,
|
|
628
1132
|
placeholder: "localhost",
|
|
629
1133
|
value: route.bypassHosts,
|
|
630
1134
|
onChange: (event) => bind(row.id, "bypassHosts")(event.target.value)
|
|
631
1135
|
}),
|
|
632
|
-
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
|
|
633
1152
|
)
|
|
634
1153
|
)
|
|
635
1154
|
);
|
|
@@ -638,6 +1157,8 @@ function renderPage(ctx) {
|
|
|
638
1157
|
"div",
|
|
639
1158
|
{ className: import_client.default.root, style: S.root },
|
|
640
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,
|
|
641
1162
|
...body,
|
|
642
1163
|
h(
|
|
643
1164
|
"details",
|
|
@@ -653,38 +1174,71 @@ function renderPage(ctx) {
|
|
|
653
1174
|
color: "var(--dsw-alias-label-secondary)"
|
|
654
1175
|
}
|
|
655
1176
|
},
|
|
656
|
-
|
|
1177
|
+
// The count is the only visible signal while the panel is collapsed: a
|
|
1178
|
+
// rejected route is not a page that refuses to render, it is one that
|
|
1179
|
+
// quietly does nothing.
|
|
1180
|
+
h(
|
|
1181
|
+
"summary",
|
|
1182
|
+
{ className: import_client.default.diagSummary },
|
|
1183
|
+
problemCount === 0 ? t.diagnostics : `${t.diagnostics} (${problemCount})`
|
|
1184
|
+
),
|
|
657
1185
|
h(
|
|
658
1186
|
"ul",
|
|
659
1187
|
{ className: import_client.default.diagList, style: { margin: "6px 0 0", paddingLeft: "18px" } },
|
|
660
|
-
...diagnostics.length === 0 ? [h("li", { key: "none" }, t.diagPending)] :
|
|
1188
|
+
...problemCount === 0 && diagnostics.length === 0 ? [h("li", { key: "none" }, t.diagPending)] : [
|
|
1189
|
+
...problemLines,
|
|
1190
|
+
...diagnostics.map((line, index) => h("li", { key: `d${index}` }, line))
|
|
1191
|
+
]
|
|
661
1192
|
)
|
|
662
1193
|
)
|
|
663
1194
|
);
|
|
664
1195
|
}
|
|
665
1196
|
var EN = {
|
|
666
|
-
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.",
|
|
667
1198
|
empty: "No providers found yet.",
|
|
668
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",
|
|
669
1210
|
protocol: "Type",
|
|
670
1211
|
host: "IP / host",
|
|
671
1212
|
port: "Port",
|
|
672
1213
|
username: "Username",
|
|
673
1214
|
password: "Password",
|
|
674
|
-
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.",
|
|
675
1216
|
passwordHint: "Sent to the SOCKS5 proxy for authentication; stored with this setting.",
|
|
676
1217
|
passwordHintProxy: "Sent to the proxy as an authentication password; stored with this setting.",
|
|
677
1218
|
hitCountPrefix: "",
|
|
678
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",
|
|
679
1228
|
testButton: "Test",
|
|
680
1229
|
testRunning: "Testing\u2026",
|
|
1230
|
+
testDisabledNote: "Not enabled \u2014 test only: ",
|
|
681
1231
|
testFailed: "Test failed",
|
|
682
1232
|
exitIpLabel: "exit IP ",
|
|
1233
|
+
directLabel: "direct (not through the proxy)",
|
|
683
1234
|
advanced: "Advanced: target host rules",
|
|
684
1235
|
onlyHosts: "Proxy only",
|
|
685
1236
|
onlyHostsHint: "Comma/space separated; when set, only requests to these hosts are proxied (suffix match, * for all).",
|
|
686
1237
|
bypassHosts: "Bypass",
|
|
687
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.",
|
|
688
1242
|
socks5Hint: "SOCKS5 tunnels the connection; the proxy resolves the target host name.",
|
|
689
1243
|
on: "proxied",
|
|
690
1244
|
live: "active",
|
|
@@ -693,28 +1247,51 @@ var EN = {
|
|
|
693
1247
|
diagPending: "(collecting\u2026)"
|
|
694
1248
|
};
|
|
695
1249
|
var ZH = {
|
|
696
|
-
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",
|
|
697
1251
|
empty: "\u6CA1\u6709\u68C0\u6D4B\u5230\u4EFB\u4F55 provider\u3002",
|
|
698
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",
|
|
699
1263
|
protocol: "\u7C7B\u578B",
|
|
700
1264
|
host: "IP / \u4E3B\u673A",
|
|
701
1265
|
port: "\u7AEF\u53E3",
|
|
702
1266
|
username: "\u7528\u6237\u540D",
|
|
703
1267
|
password: "\u5BC6\u7801",
|
|
704
|
-
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",
|
|
705
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",
|
|
706
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",
|
|
707
1271
|
hitCountPrefix: "\u5DF2\u4EE3\u7406 ",
|
|
708
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",
|
|
709
1281
|
testButton: "\u6D4B\u8BD5",
|
|
710
1282
|
testRunning: "\u6D4B\u8BD5\u4E2D\u2026",
|
|
1283
|
+
testDisabledNote: "\u672A\u542F\u7528\uFF0C\u4EC5\u6D4B\u8BD5\uFF1A",
|
|
711
1284
|
testFailed: "\u6D4B\u8BD5\u5931\u8D25",
|
|
712
1285
|
exitIpLabel: "\u51FA\u53E3 IP ",
|
|
1286
|
+
directLabel: "\u76F4\u8FDE\uFF08\u672A\u7ECF\u8FC7\u4EE3\u7406\uFF09",
|
|
713
1287
|
advanced: "\u9AD8\u7EA7\uFF1A\u76EE\u6807\u57DF\u540D\u89C4\u5219",
|
|
714
1288
|
onlyHosts: "\u4EC5\u8D70\u4EE3\u7406",
|
|
715
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",
|
|
716
1290
|
bypassHosts: "\u76F4\u8FDE",
|
|
717
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",
|
|
718
1295
|
socks5Hint: "SOCKS5 \u4F1A\u5EFA\u7ACB\u96A7\u9053\uFF0C\u76EE\u6807\u57DF\u540D\u7531\u4EE3\u7406\u7AEF\u89E3\u6790\u3002",
|
|
719
1296
|
on: "\u8D70\u4EE3\u7406",
|
|
720
1297
|
live: "\u542F\u7528\u4E2D",
|