dsh-provider-proxy 0.4.1 → 0.5.1
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 +74 -7
- package/lib/client.js +752 -93
- package/lib/client.js.map +4 -4
- package/lib/index.js +407 -91
- 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": "bcf3ba25_root", "lead": "86cd7a9a_lead", "empty": "4ec3d74d_empty", "card": "61dfce67_card", "cardActive": "56b6c433_cardActive", "cardRejected": "24b300e2_cardRejected", "defaultCard": "665ef72b_defaultCard", "defaultLead": "351db191_defaultLead", "cardHead": "9ee651d9_cardHead", "cardTitles": "bb118443_cardTitles", "name": "a1b940d4_name", "id": "1870f756_id", "pillOn": "e9cfdcad_pillOn", "pillLive": "3d3b7405_pillLive", "pillOff": "a517f862_pillOff", "pillRejected": "3e11f4ea_pillRejected", "toggle": "31ab2dc8_toggle", "field": "52ffb01c_field", "label": "b8d05488_label", "hint": "d8b3bd46_hint", "input": "0b74e44b_input", "testButton": "47b71ed7_testButton", "diag": "f395d2a5_diag", "diagSummary": "84895b20_diagSummary", "diagList": "9ecca3cd_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.bcf3ba25_root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n max-width: 640px;\n}\n\n.86cd7a9a_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.4ec3d74d_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.61dfce67_card,\n.56b6c433_cardActive,\n.24b300e2_cardRejected {\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.56b6c433_cardActive {\n border-color: var(--dsw-alias-state-success-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An enabled route the Host refused: never dress it as active. */\n.24b300e2_cardRejected {\n border-color: var(--dsw-alias-state-error-primary, #f85149);\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.665ef72b_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.351db191_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.9ee651d9_cardHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.bb118443_cardTitles {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.a1b940d4_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.1870f756_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.e9cfdcad_pillOn,\n.3d3b7405_pillLive,\n.a517f862_pillOff,\n.3e11f4ea_pillRejected {\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.e9cfdcad_pillOn {\n background: var(--dsw-alias-state-success-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.3d3b7405_pillLive {\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-secondary);\n}\n\n.a517f862_pillOff {\n color: var(--dsw-alias-state-idle-primary);\n}\n\n.3e11f4ea_pillRejected {\n border: 1px solid var(--dsw-alias-state-error-primary, #f85149);\n color: var(--dsw-alias-state-error-primary, #f85149);\n}\n\n/* ---- toggle ---- */\n\n.31ab2dc8_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.31ab2dc8_toggle input {\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n/* ---- fields ---- */\n\n.52ffb01c_field {\n display: grid;\n grid-template-columns: 96px 1fr;\n align-items: center;\n gap: 10px;\n font-size: 12px;\n}\n\n.b8d05488_label {\n color: var(--dsw-alias-label-secondary);\n}\n\n.d8b3bd46_hint {\n grid-column: 2;\n margin-top: -2px;\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.0b74e44b_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.0b74e44b_input:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n}\n\n.0b74e44b_input:focus {\n border-color: var(--dsw-alias-brand-primary) !important;\n}\n\n.0b74e44b_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.47b71ed7_testButton {\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n.47b71ed7_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.f395d2a5_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.84895b20_diagSummary {\n cursor: pointer;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.9ecca3cd_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,44 @@ __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
|
+
var PROXY_HOST_INVALID = "\u4EE3\u7406\u5730\u5740\u53EA\u80FD\u586B IPv4\u3001IPv6 \u6216\u57DF\u540D";
|
|
69
|
+
var IPV4 = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
70
|
+
var DOMAIN = /^(?=.*[a-z])(?=.{1,253}$)(?!.*\.\d+$)[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?)*$/i;
|
|
71
|
+
var IPV6_CHARS = /^[0-9a-f:.]+$/i;
|
|
72
|
+
function proxyHostProblem(rawHost) {
|
|
73
|
+
const host = (rawHost ?? "").trim().replace(/\.$/, "");
|
|
74
|
+
if (host.length === 0) return "\u672A\u586B\u5199\u4EE3\u7406\u5730\u5740";
|
|
75
|
+
if (IPV4.test(host)) return void 0;
|
|
76
|
+
if (host.includes(":") || host.startsWith("[")) {
|
|
77
|
+
const literal = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
78
|
+
if (!IPV6_CHARS.test(literal)) return PROXY_HOST_INVALID;
|
|
79
|
+
try {
|
|
80
|
+
new URL(`http://[${literal}]:1`);
|
|
81
|
+
return void 0;
|
|
82
|
+
} catch {
|
|
83
|
+
return PROXY_HOST_INVALID;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return DOMAIN.test(host) ? void 0 : PROXY_HOST_INVALID;
|
|
87
|
+
}
|
|
88
|
+
function defaultProxyProblem(endpoint) {
|
|
89
|
+
const hostProblem = proxyHostProblem(endpoint.host);
|
|
90
|
+
if (hostProblem !== void 0) return hostProblem;
|
|
91
|
+
const port = endpoint.port ?? 0;
|
|
92
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
93
|
+
return "\u4EE3\u7406\u7AEF\u53E3\u65E0\u6548\uFF08\u9700\u4E3A 1-65535 \u7684\u6574\u6570\uFF09";
|
|
94
|
+
}
|
|
95
|
+
const hasUser = (endpoint.username ?? "").trim().length > 0;
|
|
96
|
+
const hasPassword = (endpoint.password ?? "").length > 0;
|
|
97
|
+
if (hasUser !== hasPassword) {
|
|
98
|
+
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";
|
|
99
|
+
}
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/client.ts
|
|
66
104
|
var ReactCache;
|
|
67
105
|
function react() {
|
|
68
106
|
if (ReactCache === void 0) {
|
|
@@ -88,7 +126,7 @@ function apply(ctx) {
|
|
|
88
126
|
name: "settings.section",
|
|
89
127
|
id: NAMESPACE,
|
|
90
128
|
order: 20,
|
|
91
|
-
label: () =>
|
|
129
|
+
label: () => "Provider \u4EE3\u7406"
|
|
92
130
|
},
|
|
93
131
|
() => renderPage(ctx)
|
|
94
132
|
)
|
|
@@ -149,12 +187,15 @@ async function writeConfig(ctx, ops) {
|
|
|
149
187
|
}
|
|
150
188
|
function knownProviders(configured, published) {
|
|
151
189
|
const rows = /* @__PURE__ */ new Map();
|
|
190
|
+
const byId = /* @__PURE__ */ new Map();
|
|
152
191
|
for (const row of published) {
|
|
192
|
+
byId.set(row.id, row);
|
|
153
193
|
if (row.live) rows.set(row.id, row);
|
|
154
194
|
}
|
|
155
|
-
for (const
|
|
195
|
+
for (const id of Object.keys(configured)) {
|
|
156
196
|
if (rows.has(id)) continue;
|
|
157
|
-
|
|
197
|
+
const known = byId.get(id);
|
|
198
|
+
rows.set(id, { id, name: known?.name ?? id, live: known?.live === true });
|
|
158
199
|
}
|
|
159
200
|
return [...rows.values()].sort(byName);
|
|
160
201
|
}
|
|
@@ -175,11 +216,24 @@ function testResultFrom(body, strings) {
|
|
|
175
216
|
ok,
|
|
176
217
|
ms: typeof body?.ms === "number" && Number.isFinite(body.ms) ? body.ms : void 0,
|
|
177
218
|
ip: typeof body?.exitIp === "string" && body.exitIp.length > 0 ? body.exitIp : void 0,
|
|
219
|
+
// The Host probes a target the way routing would: a loopback/private upstream
|
|
220
|
+
// (or a bypassHosts match) is reached DIRECT, not through the proxy. The card
|
|
221
|
+
// says so, so a green check is not read as "the proxy works".
|
|
222
|
+
direct: body?.viaProxy === false,
|
|
178
223
|
error: ok ? void 0 : typeof body?.error === "string" && body.error.length > 0 ? body.error : strings.testFailed
|
|
179
224
|
};
|
|
180
225
|
}
|
|
181
226
|
var catalogueSetter = () => {
|
|
182
227
|
};
|
|
228
|
+
function relativeTime(at, t) {
|
|
229
|
+
const seconds = Math.max(0, Math.round((Date.now() - at) / 1e3));
|
|
230
|
+
if (seconds < 60) return t.justNow;
|
|
231
|
+
const minutes = Math.floor(seconds / 60);
|
|
232
|
+
if (minutes < 60) return `${minutes}${t.minutesAgo}`;
|
|
233
|
+
const hours = Math.floor(minutes / 60);
|
|
234
|
+
if (hours < 24) return `${hours}${t.hoursAgo}`;
|
|
235
|
+
return `${Math.floor(hours / 24)}${t.daysAgo}`;
|
|
236
|
+
}
|
|
183
237
|
async function fetchCatalog() {
|
|
184
238
|
const response = await globalThis.fetch(CATALOG_PATH, { headers: { accept: "application/json" } });
|
|
185
239
|
if (!response.ok) throw new Error(`Host answered ${response.status}`);
|
|
@@ -204,21 +258,133 @@ async function fetchCatalog() {
|
|
|
204
258
|
if (typeof message === "string" && message.length > 0) problems[id] = message;
|
|
205
259
|
}
|
|
206
260
|
}
|
|
207
|
-
|
|
261
|
+
const lastUse = {};
|
|
262
|
+
const publishedLastUse = body?.lastUse;
|
|
263
|
+
if (publishedLastUse !== null && typeof publishedLastUse === "object") {
|
|
264
|
+
for (const [id, raw] of Object.entries(publishedLastUse)) {
|
|
265
|
+
const entry = raw;
|
|
266
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
267
|
+
const at = typeof entry.at === "number" && Number.isFinite(entry.at) ? entry.at : void 0;
|
|
268
|
+
const startedAt = typeof entry.startedAt === "number" && Number.isFinite(entry.startedAt) ? entry.startedAt : void 0;
|
|
269
|
+
if (at === void 0 && startedAt === void 0) continue;
|
|
270
|
+
lastUse[id] = {
|
|
271
|
+
...startedAt === void 0 ? {} : { startedAt },
|
|
272
|
+
...at === void 0 ? {} : { at },
|
|
273
|
+
...typeof entry.ok === "boolean" ? { ok: entry.ok } : {},
|
|
274
|
+
...typeof entry.code === "string" && entry.code.length > 0 ? { code: entry.code } : {}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return { rows, hits, problems, lastUse };
|
|
279
|
+
}
|
|
280
|
+
var CATALOG_REFRESH_MS = 1e4;
|
|
281
|
+
var catalogPoll;
|
|
282
|
+
var catalogTouchedAt = 0;
|
|
283
|
+
function touchCatalogPolling() {
|
|
284
|
+
catalogTouchedAt = Date.now();
|
|
285
|
+
if (catalogPoll !== void 0) return;
|
|
286
|
+
catalogPoll = setInterval(() => {
|
|
287
|
+
if (Date.now() - catalogTouchedAt > CATALOG_REFRESH_MS * 2 + 1e3) {
|
|
288
|
+
clearInterval(catalogPoll);
|
|
289
|
+
catalogPoll = void 0;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
void fetchCatalog().then((catalog) => catalogueSetter?.(catalog)).catch(() => void 0);
|
|
293
|
+
}, CATALOG_REFRESH_MS);
|
|
208
294
|
}
|
|
209
295
|
var byName = (a, b) => a.name.localeCompare(b.name);
|
|
296
|
+
function deadRuleEntries(raw) {
|
|
297
|
+
return raw.split(/[,\s]+/).map((entry) => entry.trim()).filter((entry) => /^[a-z][a-z0-9+.-]*:\/\//i.test(entry) || entry.includes("/"));
|
|
298
|
+
}
|
|
210
299
|
function view(route) {
|
|
211
300
|
return {
|
|
212
301
|
enabled: route?.enabled === true,
|
|
302
|
+
useDefault: route?.useDefault === true,
|
|
213
303
|
protocol: PROTOCOLS.includes(route?.protocol) ? route.protocol : "http",
|
|
214
304
|
host: route?.host ?? "",
|
|
215
305
|
port: route?.port ?? 0,
|
|
216
306
|
username: route?.username ?? "",
|
|
217
307
|
password: route?.password ?? "",
|
|
218
308
|
onlyHosts: route?.onlyHosts ?? "",
|
|
219
|
-
bypassHosts: route?.bypassHosts ?? ""
|
|
309
|
+
bypassHosts: route?.bypassHosts ?? "",
|
|
310
|
+
testTarget: route?.testTarget ?? ""
|
|
220
311
|
};
|
|
221
312
|
}
|
|
313
|
+
function unbracket(hostname) {
|
|
314
|
+
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
315
|
+
}
|
|
316
|
+
function explicitPortOf(text) {
|
|
317
|
+
const authority = text.slice(text.indexOf("://") + 3).split(/[/?#]/, 1)[0];
|
|
318
|
+
const hostPort = authority.slice(authority.lastIndexOf("@") + 1);
|
|
319
|
+
const match = /^\[[^\]]+\]:(\d+)$/.exec(hostPort) ?? /^[^:]+:(\d+)$/.exec(hostPort);
|
|
320
|
+
if (match === null) return 0;
|
|
321
|
+
const port = Number.parseInt(match[1], 10);
|
|
322
|
+
return Number.isNaN(port) ? 0 : port;
|
|
323
|
+
}
|
|
324
|
+
function splitProxyUrl(text) {
|
|
325
|
+
const trimmed = text.trim();
|
|
326
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return void 0;
|
|
327
|
+
let url;
|
|
328
|
+
try {
|
|
329
|
+
url = new URL(trimmed);
|
|
330
|
+
} catch {
|
|
331
|
+
return void 0;
|
|
332
|
+
}
|
|
333
|
+
const scheme = url.protocol.slice(0, -1).toLowerCase();
|
|
334
|
+
const protocol = scheme === "socks" ? "socks5" : scheme;
|
|
335
|
+
if (!PROTOCOLS.includes(protocol)) return void 0;
|
|
336
|
+
if (url.hostname.length === 0) return void 0;
|
|
337
|
+
const decode = (value) => {
|
|
338
|
+
try {
|
|
339
|
+
return decodeURIComponent(value);
|
|
340
|
+
} catch {
|
|
341
|
+
return value;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const parsedPort = url.port.length > 0 ? Number.parseInt(url.port, 10) : explicitPortOf(trimmed);
|
|
345
|
+
return {
|
|
346
|
+
protocol,
|
|
347
|
+
host: unbracket(url.hostname),
|
|
348
|
+
port: Number.isNaN(parsedPort) ? 0 : parsedPort,
|
|
349
|
+
username: decode(url.username),
|
|
350
|
+
password: decode(url.password)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
var WRITE_DELAY_MS = 300;
|
|
354
|
+
var pendingWrites = /* @__PURE__ */ new Map();
|
|
355
|
+
var writeQueue = Promise.resolve();
|
|
356
|
+
function enqueueWrite(run) {
|
|
357
|
+
const next = writeQueue.then(run, run);
|
|
358
|
+
writeQueue = next.catch(() => void 0);
|
|
359
|
+
return next;
|
|
360
|
+
}
|
|
361
|
+
function scheduleWrite(key, run) {
|
|
362
|
+
const pending = pendingWrites.get(key);
|
|
363
|
+
if (pending?.timer !== void 0) clearTimeout(pending.timer);
|
|
364
|
+
const entry = {
|
|
365
|
+
flush: () => {
|
|
366
|
+
if (entry.timer !== void 0) clearTimeout(entry.timer);
|
|
367
|
+
pendingWrites.delete(key);
|
|
368
|
+
return enqueueWrite(run);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
entry.timer = setTimeout(() => {
|
|
372
|
+
void entry.flush();
|
|
373
|
+
}, WRITE_DELAY_MS);
|
|
374
|
+
pendingWrites.set(key, entry);
|
|
375
|
+
}
|
|
376
|
+
var testGeneration = /* @__PURE__ */ new Map();
|
|
377
|
+
var TEST_RELEVANT_FIELDS = /* @__PURE__ */ new Set([
|
|
378
|
+
"protocol",
|
|
379
|
+
"host",
|
|
380
|
+
"port",
|
|
381
|
+
"username",
|
|
382
|
+
"password",
|
|
383
|
+
"onlyHosts",
|
|
384
|
+
"bypassHosts",
|
|
385
|
+
"testTarget",
|
|
386
|
+
"useDefault"
|
|
387
|
+
]);
|
|
222
388
|
var diagnostics = [];
|
|
223
389
|
function note(message) {
|
|
224
390
|
if (!diagnostics.includes(message)) diagnostics.push(message);
|
|
@@ -244,6 +410,11 @@ var S = {
|
|
|
244
410
|
borderColor: "var(--dsw-alias-state-success-primary)",
|
|
245
411
|
background: "var(--dsw-alias-bg-layer-2)"
|
|
246
412
|
},
|
|
413
|
+
/** An enabled route the Host refused: active-looking styling would be a lie. */
|
|
414
|
+
cardRejected: {
|
|
415
|
+
borderColor: "var(--dsw-alias-state-error-primary, #f85149)",
|
|
416
|
+
background: "var(--dsw-alias-bg-layer-2)"
|
|
417
|
+
},
|
|
247
418
|
head: {
|
|
248
419
|
display: "flex",
|
|
249
420
|
alignItems: "center",
|
|
@@ -334,6 +505,44 @@ var S = {
|
|
|
334
505
|
color: "var(--dsw-alias-label-secondary)"
|
|
335
506
|
},
|
|
336
507
|
pillOff: { color: "var(--dsw-alias-state-idle-primary)" },
|
|
508
|
+
pillRejected: {
|
|
509
|
+
border: "1px solid var(--dsw-alias-state-error-primary, #f85149)",
|
|
510
|
+
color: "var(--dsw-alias-state-error-primary, #f85149)"
|
|
511
|
+
},
|
|
512
|
+
/**
|
|
513
|
+
* The shared default endpoint sits on its own layer so it reads as global
|
|
514
|
+
* rather than as one more provider card — the mistake it exists to prevent is
|
|
515
|
+
* a user typing the same address into every card.
|
|
516
|
+
*/
|
|
517
|
+
defaultCard: {
|
|
518
|
+
display: "flex",
|
|
519
|
+
flexDirection: "column",
|
|
520
|
+
gap: "10px",
|
|
521
|
+
padding: "14px 16px",
|
|
522
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
523
|
+
borderRadius: "10px",
|
|
524
|
+
background: "var(--dsw-alias-bg-layer-2)"
|
|
525
|
+
},
|
|
526
|
+
defaultLead: {
|
|
527
|
+
margin: "-2px 0 0",
|
|
528
|
+
fontSize: "11px",
|
|
529
|
+
lineHeight: "1.5",
|
|
530
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
531
|
+
},
|
|
532
|
+
inheritNote: {
|
|
533
|
+
margin: "0",
|
|
534
|
+
fontSize: "11px",
|
|
535
|
+
lineHeight: "1.5",
|
|
536
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
537
|
+
},
|
|
538
|
+
/** The default is not configured: an answer, so it takes the error colour. */
|
|
539
|
+
inheritWarning: {
|
|
540
|
+
margin: "0",
|
|
541
|
+
fontSize: "11px",
|
|
542
|
+
lineHeight: "1.5",
|
|
543
|
+
fontWeight: "500",
|
|
544
|
+
color: "var(--dsw-alias-state-error-primary, #f85149)"
|
|
545
|
+
},
|
|
337
546
|
toggle: {
|
|
338
547
|
display: "flex",
|
|
339
548
|
alignItems: "center",
|
|
@@ -382,19 +591,42 @@ function renderPage(ctx) {
|
|
|
382
591
|
const [state, setState] = (React.useState ?? ((fn) => [fn(), () => {
|
|
383
592
|
}]))(() => {
|
|
384
593
|
void fetchCatalog().then((catalog) => catalogueSetter?.(catalog)).catch((error) => {
|
|
385
|
-
|
|
386
|
-
|
|
594
|
+
catalogueSetter?.({
|
|
595
|
+
rows: [],
|
|
596
|
+
hits: {},
|
|
597
|
+
problems: {},
|
|
598
|
+
lastUse: {},
|
|
599
|
+
error: String(error?.message ?? error)
|
|
600
|
+
});
|
|
387
601
|
});
|
|
388
602
|
return {
|
|
389
603
|
rows: void 0,
|
|
390
604
|
hits: {},
|
|
391
605
|
problems: {},
|
|
606
|
+
lastUse: {},
|
|
607
|
+
error: void 0,
|
|
392
608
|
pending: true
|
|
393
609
|
};
|
|
394
610
|
});
|
|
395
|
-
catalogueSetter = (catalog) => setState({
|
|
611
|
+
catalogueSetter = (catalog) => setState({
|
|
612
|
+
rows: catalog.rows,
|
|
613
|
+
hits: catalog.hits,
|
|
614
|
+
problems: catalog.problems,
|
|
615
|
+
lastUse: catalog.lastUse,
|
|
616
|
+
error: catalog.error,
|
|
617
|
+
pending: false
|
|
618
|
+
});
|
|
619
|
+
touchCatalogPolling();
|
|
396
620
|
const published = state.rows ?? [];
|
|
397
621
|
const hits = state.hits ?? {};
|
|
622
|
+
const lastUse = state.lastUse ?? {};
|
|
623
|
+
const lastUseText = (id) => {
|
|
624
|
+
const use = lastUse[id];
|
|
625
|
+
if (use === void 0) return void 0;
|
|
626
|
+
if (use.ok === true) return `${t.recentOk}${relativeTime(use.at ?? use.startedAt ?? Date.now(), t)}`;
|
|
627
|
+
if (use.ok === false) return `${t.recentFail}${use.code ?? "ERROR"}`;
|
|
628
|
+
return `${t.recentStarted}${relativeTime(use.startedAt ?? use.at ?? Date.now(), t)}`;
|
|
629
|
+
};
|
|
398
630
|
const problems = state.problems ?? {};
|
|
399
631
|
const problemLines = Object.entries(problems).map(
|
|
400
632
|
([id, message]) => h("li", { key: `p:${id}`, style: S.diagProblem }, `${id}\uFF1A${message}`)
|
|
@@ -406,20 +638,52 @@ function renderPage(ctx) {
|
|
|
406
638
|
const [draft, setDraft] = (React.useState ?? ((v) => [v, () => {
|
|
407
639
|
}]))({});
|
|
408
640
|
const edit = (id, field2, value) => setDraft((current) => ({ ...current, [id]: { ...current[id], [field2]: value } }));
|
|
641
|
+
const [defaultDraft, setDefaultDraft] = (React.useState ?? ((v) => [v, () => {
|
|
642
|
+
}]))({});
|
|
643
|
+
const editDefault = (field2, value) => setDefaultDraft((current) => ({ ...current, [field2]: value }));
|
|
409
644
|
const [tests, setTests] = (React.useState ?? ((v) => [v, () => {
|
|
410
645
|
}]))({});
|
|
646
|
+
const invalidateTests = (ids) => {
|
|
647
|
+
if (ids.length === 0) return;
|
|
648
|
+
for (const id of ids) {
|
|
649
|
+
testGeneration.set(id, (testGeneration.get(id) ?? 0) + 1);
|
|
650
|
+
}
|
|
651
|
+
setTests((current) => {
|
|
652
|
+
let changed = false;
|
|
653
|
+
const next = { ...current };
|
|
654
|
+
for (const id of ids) {
|
|
655
|
+
if (next[id] === void 0) continue;
|
|
656
|
+
delete next[id];
|
|
657
|
+
changed = true;
|
|
658
|
+
}
|
|
659
|
+
return changed ? next : current;
|
|
660
|
+
});
|
|
661
|
+
};
|
|
662
|
+
const [filter, setFilter] = (React.useState ?? ((v) => [v, () => {
|
|
663
|
+
}]))("");
|
|
411
664
|
const runTest = (id) => {
|
|
665
|
+
const generation = (testGeneration.get(id) ?? 0) + 1;
|
|
666
|
+
testGeneration.set(id, generation);
|
|
412
667
|
setTests((current) => ({ ...current, [id]: { pending: true } }));
|
|
413
668
|
void (async () => {
|
|
414
669
|
try {
|
|
670
|
+
for (const [key, entry] of [...pendingWrites]) {
|
|
671
|
+
if (key.startsWith(`${id}\0`) || key.startsWith("\0default\0")) {
|
|
672
|
+
void entry.flush();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
await writeQueue;
|
|
676
|
+
if (testGeneration.get(id) !== generation) return;
|
|
415
677
|
const response = await globalThis.fetch(TEST_PATH, {
|
|
416
678
|
method: "POST",
|
|
417
679
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
418
680
|
body: JSON.stringify({ provider: id })
|
|
419
681
|
});
|
|
420
682
|
const body2 = await response.json();
|
|
683
|
+
if (testGeneration.get(id) !== generation) return;
|
|
421
684
|
setTests((current) => ({ ...current, [id]: testResultFrom(body2, t) }));
|
|
422
685
|
} catch (error) {
|
|
686
|
+
if (testGeneration.get(id) !== generation) return;
|
|
423
687
|
setTests((current) => ({
|
|
424
688
|
...current,
|
|
425
689
|
[id]: { pending: false, ok: false, error: String(error?.message ?? error) }
|
|
@@ -428,34 +692,301 @@ function renderPage(ctx) {
|
|
|
428
692
|
})();
|
|
429
693
|
};
|
|
430
694
|
const rows = knownProviders({ ...configured, ...draft }, published);
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
695
|
+
const inheritingIds = () => rows.filter((row) => view({ ...configured[row.id], ...draft[row.id] }).useDefault === true).map((row) => row.id);
|
|
696
|
+
const save = (key, path, label, value, onRefused) => {
|
|
697
|
+
scheduleWrite(
|
|
698
|
+
key,
|
|
699
|
+
() => writeConfig(ctx, [{ op: "set", path, value }]).then(() => {
|
|
700
|
+
note(`\u5DF2\u4FDD\u5B58 ${label} = ${JSON.stringify(value)}`);
|
|
701
|
+
}).catch((error) => {
|
|
702
|
+
note(`\u4FDD\u5B58\u5931\u8D25 ${label}\uFF1A${error?.message ?? error}`);
|
|
703
|
+
onRefused();
|
|
704
|
+
})
|
|
705
|
+
);
|
|
706
|
+
};
|
|
707
|
+
const rollbackRouteField = (id, field2) => {
|
|
708
|
+
setDraft((current) => {
|
|
709
|
+
const entry = current[id];
|
|
710
|
+
if (entry === void 0 || entry[field2] === void 0) return current;
|
|
711
|
+
const nextEntry = { ...entry };
|
|
712
|
+
delete nextEntry[field2];
|
|
713
|
+
const next = { ...current };
|
|
714
|
+
if (Object.keys(nextEntry).length === 0) delete next[id];
|
|
715
|
+
else next[id] = nextEntry;
|
|
716
|
+
return next;
|
|
717
|
+
});
|
|
718
|
+
};
|
|
719
|
+
const bind = (id, field2) => (value) => {
|
|
720
|
+
edit(id, field2, value);
|
|
721
|
+
if (TEST_RELEVANT_FIELDS.has(field2)) invalidateTests([id]);
|
|
722
|
+
save(
|
|
723
|
+
`${id}\0${field2}`,
|
|
724
|
+
["providers", id, field2],
|
|
725
|
+
`${id}.${field2}`,
|
|
726
|
+
value,
|
|
727
|
+
() => rollbackRouteField(id, field2)
|
|
728
|
+
);
|
|
729
|
+
};
|
|
730
|
+
const bindDefault = (field2) => (value) => {
|
|
731
|
+
editDefault(field2, value);
|
|
732
|
+
invalidateTests(inheritingIds());
|
|
733
|
+
save(`\0default\0${field2}`, ["defaultProxy", field2], `defaultProxy.${field2}`, value, () => {
|
|
734
|
+
setDefaultDraft((current) => {
|
|
735
|
+
if (current[field2] === void 0) return current;
|
|
437
736
|
const next = { ...current };
|
|
438
|
-
delete next[
|
|
737
|
+
delete next[field2];
|
|
439
738
|
return next;
|
|
440
739
|
});
|
|
441
740
|
});
|
|
442
741
|
};
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
742
|
+
const saveMany = (key, ops, label, onRefused) => {
|
|
743
|
+
scheduleWrite(
|
|
744
|
+
key,
|
|
745
|
+
() => writeConfig(ctx, ops).then(() => {
|
|
746
|
+
note(`\u5DF2\u4FDD\u5B58 ${label}`);
|
|
747
|
+
}).catch((error) => {
|
|
748
|
+
note(`\u4FDD\u5B58\u5931\u8D25 ${label}\uFF1A${error?.message ?? error}`);
|
|
749
|
+
onRefused();
|
|
750
|
+
})
|
|
751
|
+
);
|
|
752
|
+
};
|
|
753
|
+
const rollbackRouteFields = (id, fields) => {
|
|
754
|
+
setDraft((current) => {
|
|
755
|
+
const entry = current[id];
|
|
756
|
+
if (entry === void 0) return current;
|
|
757
|
+
const nextEntry = { ...entry };
|
|
758
|
+
for (const field2 of fields) delete nextEntry[field2];
|
|
759
|
+
const next = { ...current };
|
|
760
|
+
if (Object.keys(nextEntry).length === 0) delete next[id];
|
|
761
|
+
else next[id] = nextEntry;
|
|
762
|
+
return next;
|
|
763
|
+
});
|
|
764
|
+
};
|
|
765
|
+
const applyCardUrl = (id, parts) => {
|
|
766
|
+
setDraft((current) => ({ ...current, [id]: { ...current[id], ...parts } }));
|
|
767
|
+
invalidateTests([id]);
|
|
768
|
+
const fields = Object.keys(parts);
|
|
769
|
+
saveMany(
|
|
770
|
+
`${id}\0*paste`,
|
|
771
|
+
fields.map((field2) => ({ op: "set", path: ["providers", id, field2], value: parts[field2] })),
|
|
772
|
+
`${id}.*\uFF08\u7C98\u8D34\u4EE3\u7406 URL\uFF09`,
|
|
773
|
+
() => rollbackRouteFields(id, fields)
|
|
774
|
+
);
|
|
775
|
+
};
|
|
776
|
+
const applyDefaultUrl = (parts) => {
|
|
777
|
+
setDefaultDraft((current) => ({ ...current, ...parts }));
|
|
778
|
+
invalidateTests(inheritingIds());
|
|
779
|
+
const fields = Object.keys(parts);
|
|
780
|
+
saveMany(
|
|
781
|
+
"\0default\0*paste",
|
|
782
|
+
fields.map((field2) => ({ op: "set", path: ["defaultProxy", field2], value: parts[field2] })),
|
|
783
|
+
"\u9ED8\u8BA4\u4EE3\u7406\uFF08\u7C98\u8D34\u4EE3\u7406 URL\uFF09",
|
|
784
|
+
() => {
|
|
785
|
+
setDefaultDraft((current) => {
|
|
786
|
+
const next = { ...current };
|
|
787
|
+
for (const field2 of fields) delete next[field2];
|
|
788
|
+
return next;
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
);
|
|
792
|
+
};
|
|
793
|
+
const promoteToDefault = (id) => {
|
|
794
|
+
const route = view({ ...configured[id], ...draft[id] });
|
|
795
|
+
applyDefaultUrl({
|
|
796
|
+
protocol: route.protocol,
|
|
797
|
+
host: route.host.trim(),
|
|
798
|
+
port: route.port,
|
|
799
|
+
username: route.username,
|
|
800
|
+
password: route.password
|
|
801
|
+
});
|
|
802
|
+
};
|
|
803
|
+
const applyDefaultToAll = () => {
|
|
804
|
+
const ids = rows.map((row) => row.id);
|
|
805
|
+
if (ids.length === 0) return;
|
|
806
|
+
setDraft((current) => {
|
|
807
|
+
const next = { ...current };
|
|
808
|
+
for (const id of ids) next[id] = { ...next[id], useDefault: true };
|
|
809
|
+
return next;
|
|
810
|
+
});
|
|
811
|
+
invalidateTests(ids);
|
|
812
|
+
saveMany(
|
|
813
|
+
"\0default\0applyAll",
|
|
814
|
+
ids.map((id) => ({ op: "set", path: ["providers", id, "useDefault"], value: true })),
|
|
815
|
+
"\u6240\u6709 provider \u7684 useDefault",
|
|
816
|
+
() => setDraft((current) => {
|
|
817
|
+
const next = { ...current };
|
|
818
|
+
for (const id of ids) {
|
|
819
|
+
const entry = next[id];
|
|
820
|
+
if (entry === void 0 || entry.useDefault === void 0) continue;
|
|
821
|
+
const nextEntry = { ...entry };
|
|
822
|
+
delete nextEntry.useDefault;
|
|
823
|
+
if (Object.keys(nextEntry).length === 0) delete next[id];
|
|
824
|
+
else next[id] = nextEntry;
|
|
825
|
+
}
|
|
826
|
+
return next;
|
|
827
|
+
})
|
|
828
|
+
);
|
|
829
|
+
};
|
|
830
|
+
const onProxyUrlPaste = (apply2) => (event) => {
|
|
831
|
+
const text = event?.clipboardData?.getData?.("text");
|
|
832
|
+
if (typeof text !== "string") return;
|
|
833
|
+
const parts = splitProxyUrl(text);
|
|
834
|
+
if (parts === void 0) return;
|
|
835
|
+
event.preventDefault?.();
|
|
836
|
+
apply2(parts);
|
|
446
837
|
};
|
|
447
|
-
const field = (label, control, hint) => h(
|
|
838
|
+
const field = (label, control, hint, hintError) => h(
|
|
448
839
|
"label",
|
|
449
840
|
{ className: import_client.default.field, style: S.field, key: label },
|
|
450
841
|
h("span", { className: import_client.default.label, style: S.label }, label),
|
|
451
842
|
control,
|
|
452
843
|
hint === void 0 ? null : h(
|
|
453
844
|
"span",
|
|
454
|
-
{
|
|
845
|
+
{
|
|
846
|
+
className: import_client.default.hint,
|
|
847
|
+
style: {
|
|
848
|
+
gridColumn: "2",
|
|
849
|
+
fontSize: "11px",
|
|
850
|
+
// A warning that a value can never work is an answer, not
|
|
851
|
+
// decoration — same rule as the failed self-check text.
|
|
852
|
+
...hintError === true ? { color: "var(--dsw-alias-state-error-primary, #f85149)" } : {}
|
|
853
|
+
}
|
|
854
|
+
},
|
|
455
855
|
hint
|
|
456
856
|
)
|
|
457
857
|
);
|
|
458
|
-
const
|
|
858
|
+
const storedDefault = snapshot.value?.defaultProxy ?? {};
|
|
859
|
+
const defaultView = view({ ...storedDefault, ...defaultDraft });
|
|
860
|
+
const defaultWhy = defaultProxyProblem(defaultView);
|
|
861
|
+
const defaultHostWhy = defaultView.host.trim().length === 0 ? void 0 : proxyHostProblem(defaultView.host);
|
|
862
|
+
const defaultComplete = defaultWhy === void 0;
|
|
863
|
+
const defaultOrigin = `${defaultView.protocol}://${defaultView.host}:${defaultView.port}`;
|
|
864
|
+
const defaultBlock = h(
|
|
865
|
+
"div",
|
|
866
|
+
{ className: import_client.default.defaultCard, style: S.defaultCard, key: "default-proxy" },
|
|
867
|
+
h(
|
|
868
|
+
"header",
|
|
869
|
+
{ className: import_client.default.cardHead, style: S.head },
|
|
870
|
+
h(
|
|
871
|
+
"div",
|
|
872
|
+
{ className: import_client.default.cardTitles, style: S.titles },
|
|
873
|
+
h("span", { className: import_client.default.name, style: S.name }, t.defaultTitle)
|
|
874
|
+
),
|
|
875
|
+
h(
|
|
876
|
+
"span",
|
|
877
|
+
{
|
|
878
|
+
className: defaultComplete ? import_client.default.pillOn : import_client.default.pillOff,
|
|
879
|
+
style: { ...S.pill, ...defaultComplete ? S.pillOn : S.pillOff }
|
|
880
|
+
},
|
|
881
|
+
defaultComplete ? t.complete : t.notConfigured
|
|
882
|
+
)
|
|
883
|
+
),
|
|
884
|
+
h("p", { className: import_client.default.defaultLead, style: S.defaultLead }, t.defaultLead),
|
|
885
|
+
field(
|
|
886
|
+
t.protocol,
|
|
887
|
+
h(
|
|
888
|
+
"select",
|
|
889
|
+
{
|
|
890
|
+
className: import_client.default.input,
|
|
891
|
+
style: S.input,
|
|
892
|
+
value: defaultView.protocol,
|
|
893
|
+
onChange: (event) => bindDefault("protocol")(event.target.value)
|
|
894
|
+
},
|
|
895
|
+
...PROTOCOLS.map(
|
|
896
|
+
(protocol) => h("option", { key: protocol, value: protocol }, protocol.toUpperCase())
|
|
897
|
+
)
|
|
898
|
+
)
|
|
899
|
+
),
|
|
900
|
+
field(
|
|
901
|
+
t.host,
|
|
902
|
+
h("input", {
|
|
903
|
+
className: import_client.default.input,
|
|
904
|
+
style: S.input,
|
|
905
|
+
placeholder: "127.0.0.1",
|
|
906
|
+
value: defaultView.host,
|
|
907
|
+
onChange: (event) => bindDefault("host")(event.target.value),
|
|
908
|
+
// Same convenience as the cards: a full URL fills the whole block.
|
|
909
|
+
onPaste: onProxyUrlPaste(applyDefaultUrl)
|
|
910
|
+
}),
|
|
911
|
+
defaultHostWhy ?? t.hostHint,
|
|
912
|
+
defaultHostWhy !== void 0
|
|
913
|
+
),
|
|
914
|
+
field(
|
|
915
|
+
t.port,
|
|
916
|
+
h("input", {
|
|
917
|
+
className: import_client.default.input,
|
|
918
|
+
style: S.input,
|
|
919
|
+
type: "number",
|
|
920
|
+
min: 1,
|
|
921
|
+
max: 65535,
|
|
922
|
+
placeholder: "7890",
|
|
923
|
+
value: defaultView.port === 0 ? "" : String(defaultView.port),
|
|
924
|
+
onChange: (event) => {
|
|
925
|
+
const parsed = Number.parseInt(event.target.value, 10);
|
|
926
|
+
bindDefault("port")(Number.isNaN(parsed) ? 0 : parsed);
|
|
927
|
+
}
|
|
928
|
+
})
|
|
929
|
+
),
|
|
930
|
+
field(
|
|
931
|
+
t.username,
|
|
932
|
+
h("input", {
|
|
933
|
+
className: import_client.default.input,
|
|
934
|
+
style: S.input,
|
|
935
|
+
autocomplete: "off",
|
|
936
|
+
value: defaultView.username,
|
|
937
|
+
onChange: (event) => bindDefault("username")(event.target.value)
|
|
938
|
+
})
|
|
939
|
+
),
|
|
940
|
+
field(
|
|
941
|
+
t.password,
|
|
942
|
+
h("input", {
|
|
943
|
+
className: import_client.default.input,
|
|
944
|
+
style: S.input,
|
|
945
|
+
type: "password",
|
|
946
|
+
autocomplete: "off",
|
|
947
|
+
value: defaultView.password,
|
|
948
|
+
onChange: (event) => bindDefault("password")(event.target.value)
|
|
949
|
+
}),
|
|
950
|
+
t.passwordHintProxy
|
|
951
|
+
),
|
|
952
|
+
// Bulk opt-in, in ONE write: a per-card walk would rebuild the table N times.
|
|
953
|
+
// Disabled while the default configuration is incomplete, because pointing every card at an
|
|
954
|
+
// empty endpoint would just refuse them all.
|
|
955
|
+
h(
|
|
956
|
+
"div",
|
|
957
|
+
{ style: S.testRow, key: "default:apply" },
|
|
958
|
+
h(
|
|
959
|
+
"button",
|
|
960
|
+
{
|
|
961
|
+
className: import_client.default.testButton,
|
|
962
|
+
style: S.testButton,
|
|
963
|
+
type: "button",
|
|
964
|
+
disabled: !defaultComplete || rows.length === 0,
|
|
965
|
+
onClick: applyDefaultToAll
|
|
966
|
+
},
|
|
967
|
+
t.applyDefaultAll
|
|
968
|
+
)
|
|
969
|
+
)
|
|
970
|
+
);
|
|
971
|
+
const query = filter.trim().toLowerCase();
|
|
972
|
+
const shown = query.length === 0 ? rows : rows.filter((row) => `${row.id} ${row.name}`.toLowerCase().includes(query));
|
|
973
|
+
const filterRow = rows.length === 0 ? null : field(
|
|
974
|
+
t.filterLabel,
|
|
975
|
+
h("input", {
|
|
976
|
+
className: import_client.default.input,
|
|
977
|
+
style: S.input,
|
|
978
|
+
placeholder: t.filterPlaceholder,
|
|
979
|
+
value: filter,
|
|
980
|
+
onChange: (event) => setFilter(event.target.value)
|
|
981
|
+
})
|
|
982
|
+
);
|
|
983
|
+
const catalogueStatus = rows.length === 0 ? state.pending ? { text: t.catalogueLoading, error: false } : state.error !== void 0 ? { text: `${t.catalogueError}${state.error}`, error: true } : void 0 : void 0;
|
|
984
|
+
const catalogueNotice = state.error !== void 0 && rows.length > 0 ? h(
|
|
985
|
+
"p",
|
|
986
|
+
{ key: "catalogue-error", style: S.inheritWarning },
|
|
987
|
+
`${t.catalogueStale}${state.error}`
|
|
988
|
+
) : null;
|
|
989
|
+
const body = shown.length === 0 ? [
|
|
459
990
|
h(
|
|
460
991
|
"p",
|
|
461
992
|
{
|
|
@@ -468,60 +999,29 @@ function renderPage(ctx) {
|
|
|
468
999
|
borderRadius: "8px",
|
|
469
1000
|
fontSize: "12px",
|
|
470
1001
|
textAlign: "center",
|
|
471
|
-
color: "var(--dsw-alias-state-idle-primary)"
|
|
1002
|
+
color: catalogueStatus?.error === true ? "var(--dsw-alias-state-error-primary, #f85149)" : "var(--dsw-alias-state-idle-primary)",
|
|
1003
|
+
...catalogueStatus?.error === true ? { fontWeight: "500" } : {}
|
|
472
1004
|
}
|
|
473
1005
|
},
|
|
474
|
-
t.empty
|
|
1006
|
+
catalogueStatus?.text ?? (query.length > 0 ? t.noMatch : t.empty)
|
|
475
1007
|
)
|
|
476
|
-
] :
|
|
1008
|
+
] : shown.map((row) => {
|
|
477
1009
|
const route = view({ ...configured[row.id], ...draft[row.id] });
|
|
478
|
-
const
|
|
1010
|
+
const lastText = lastUseText(row.id) ?? (route.enabled ? t.noRecord : void 0);
|
|
479
1011
|
const result = tests[row.id];
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
),
|
|
1012
|
+
const onlyDead = deadRuleEntries(route.onlyHosts);
|
|
1013
|
+
const bypassDead = deadRuleEntries(route.bypassHosts);
|
|
1014
|
+
const useDefault = route.useDefault === true;
|
|
1015
|
+
const endpointStored = route.useDefault ? defaultComplete : route.host.trim().length > 0 && Number.isInteger(route.port) && route.port >= 1 && route.port <= 65535;
|
|
1016
|
+
const showTest = route.enabled || endpointStored;
|
|
1017
|
+
const problemText = (() => {
|
|
1018
|
+
const recorded = problems[row.id];
|
|
1019
|
+
if (recorded !== void 0) return recorded;
|
|
1020
|
+
if (useDefault) return defaultComplete ? void 0 : `${t.defaultMissing}${defaultWhy}`;
|
|
1021
|
+
return route.enabled ? defaultProxyProblem(route) : void 0;
|
|
1022
|
+
})();
|
|
1023
|
+
const rejected = route.enabled && problemText !== void 0;
|
|
1024
|
+
const endpointFields = [
|
|
525
1025
|
field(
|
|
526
1026
|
t.protocol,
|
|
527
1027
|
h(
|
|
@@ -529,7 +1029,6 @@ function renderPage(ctx) {
|
|
|
529
1029
|
{
|
|
530
1030
|
className: import_client.default.input,
|
|
531
1031
|
style: S.input,
|
|
532
|
-
disabled,
|
|
533
1032
|
value: route.protocol,
|
|
534
1033
|
onChange: (event) => bind(row.id, "protocol")(event.target.value)
|
|
535
1034
|
},
|
|
@@ -544,10 +1043,11 @@ function renderPage(ctx) {
|
|
|
544
1043
|
h("input", {
|
|
545
1044
|
className: import_client.default.input,
|
|
546
1045
|
style: S.input,
|
|
547
|
-
disabled,
|
|
548
1046
|
placeholder: "127.0.0.1",
|
|
549
1047
|
value: route.host,
|
|
550
|
-
onChange: (event) => bind(row.id, "host")(event.target.value)
|
|
1048
|
+
onChange: (event) => bind(row.id, "host")(event.target.value),
|
|
1049
|
+
// A pasted full proxy URL fills every transport field at once.
|
|
1050
|
+
onPaste: onProxyUrlPaste((parts) => applyCardUrl(row.id, parts))
|
|
551
1051
|
}),
|
|
552
1052
|
t.hostHint
|
|
553
1053
|
),
|
|
@@ -556,7 +1056,6 @@ function renderPage(ctx) {
|
|
|
556
1056
|
h("input", {
|
|
557
1057
|
className: import_client.default.input,
|
|
558
1058
|
style: S.input,
|
|
559
|
-
disabled,
|
|
560
1059
|
type: "number",
|
|
561
1060
|
min: 1,
|
|
562
1061
|
max: 65535,
|
|
@@ -573,13 +1072,12 @@ function renderPage(ctx) {
|
|
|
573
1072
|
h("input", {
|
|
574
1073
|
className: import_client.default.input,
|
|
575
1074
|
style: S.input,
|
|
576
|
-
disabled,
|
|
577
1075
|
autocomplete: "off",
|
|
578
1076
|
value: route.username,
|
|
579
1077
|
onChange: (event) => bind(row.id, "username")(event.target.value)
|
|
580
1078
|
})
|
|
581
1079
|
),
|
|
582
|
-
// Every transport accepts a password
|
|
1080
|
+
// Every transport accepts a password: SOCKS5 hands it to
|
|
583
1081
|
// Socks5ProxyAgent as an option, HTTP/HTTPS embed it in the proxy URL
|
|
584
1082
|
// userinfo (undici turns that into Proxy-Authorization). It is stored on
|
|
585
1083
|
// the route, so it lands in the settings file with the rest of the
|
|
@@ -589,19 +1087,93 @@ function renderPage(ctx) {
|
|
|
589
1087
|
h("input", {
|
|
590
1088
|
className: import_client.default.input,
|
|
591
1089
|
style: S.input,
|
|
592
|
-
disabled,
|
|
593
1090
|
type: "password",
|
|
594
1091
|
autocomplete: "off",
|
|
595
1092
|
value: route.password,
|
|
596
1093
|
onChange: (event) => bind(row.id, "password")(event.target.value)
|
|
597
1094
|
}),
|
|
598
1095
|
route.protocol === "socks5" ? t.passwordHint : t.passwordHintProxy
|
|
1096
|
+
)
|
|
1097
|
+
];
|
|
1098
|
+
const pill = rejected ? { className: import_client.default.pillRejected, style: { ...S.pill, ...S.pillRejected }, text: t.rejected } : route.enabled ? { className: import_client.default.pillOn, style: { ...S.pill, ...S.pillOn }, text: t.on } : { className: import_client.default.pillLive, style: { ...S.pill, ...S.pillLive }, text: t.off };
|
|
1099
|
+
return h(
|
|
1100
|
+
"section",
|
|
1101
|
+
{
|
|
1102
|
+
className: rejected ? import_client.default.cardRejected : route.enabled ? import_client.default.cardActive : import_client.default.card,
|
|
1103
|
+
style: rejected ? { ...S.card, ...S.cardRejected } : route.enabled ? { ...S.card, ...S.cardActive } : S.card,
|
|
1104
|
+
key: row.id
|
|
1105
|
+
},
|
|
1106
|
+
h(
|
|
1107
|
+
"header",
|
|
1108
|
+
{ className: import_client.default.cardHead, style: S.head },
|
|
1109
|
+
h(
|
|
1110
|
+
"div",
|
|
1111
|
+
{ className: import_client.default.cardTitles, style: S.titles },
|
|
1112
|
+
h("span", { className: import_client.default.name, style: S.name }, row.name),
|
|
1113
|
+
h("span", { className: import_client.default.id, style: S.id }, row.id),
|
|
1114
|
+
// A disabled route can never have hits, so "0" there would only add
|
|
1115
|
+
// noise; the count shows once the route is on (or has counted).
|
|
1116
|
+
route.enabled || (hits[row.id] ?? 0) > 0 ? h(
|
|
1117
|
+
"span",
|
|
1118
|
+
{ style: S.hits },
|
|
1119
|
+
`${t.hitCountPrefix}${hits[row.id] ?? 0}${t.hitCountSuffix}`
|
|
1120
|
+
) : null,
|
|
1121
|
+
// The last-use line. A failure takes the error colour, same rule as
|
|
1122
|
+
// a failed self-check: it is an answer, not decoration.
|
|
1123
|
+
lastText === void 0 ? null : h(
|
|
1124
|
+
"span",
|
|
1125
|
+
{
|
|
1126
|
+
style: {
|
|
1127
|
+
...S.hits,
|
|
1128
|
+
...lastUse[row.id]?.ok === false ? S.inheritWarning : {}
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
lastText
|
|
1132
|
+
)
|
|
1133
|
+
),
|
|
1134
|
+
h("span", { className: pill.className, style: pill.style }, pill.text)
|
|
599
1135
|
),
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1136
|
+
h(
|
|
1137
|
+
"label",
|
|
1138
|
+
{ className: import_client.default.toggle, style: S.toggle },
|
|
1139
|
+
h("input", {
|
|
1140
|
+
type: "checkbox",
|
|
1141
|
+
checked: route.enabled,
|
|
1142
|
+
onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
|
|
1143
|
+
}),
|
|
1144
|
+
h("span", null, t.useProxy)
|
|
1145
|
+
),
|
|
1146
|
+
// Opting into the shared endpoint. The card then shows the address it
|
|
1147
|
+
// will actually use — or, when the default is not configured, why the
|
|
1148
|
+
// route is refused — instead of a set of empty inputs the user never
|
|
1149
|
+
// filled and cannot interpret.
|
|
1150
|
+
h(
|
|
1151
|
+
"label",
|
|
1152
|
+
{ className: import_client.default.toggle, style: S.toggle, key: `${row.id}:usedefault` },
|
|
1153
|
+
h("input", {
|
|
1154
|
+
type: "checkbox",
|
|
1155
|
+
checked: useDefault,
|
|
1156
|
+
onChange: (event) => bind(row.id, "useDefault")(event.target.checked === true)
|
|
1157
|
+
}),
|
|
1158
|
+
h("span", null, t.useDefault)
|
|
1159
|
+
),
|
|
1160
|
+
problemText !== void 0 ? h("p", { key: `${row.id}:problem`, style: S.inheritWarning }, problemText) : useDefault ? h(
|
|
1161
|
+
"p",
|
|
1162
|
+
{ key: `${row.id}:inherit`, style: S.inheritNote },
|
|
1163
|
+
`${t.inheritingDefault}${defaultOrigin}`
|
|
1164
|
+
) : null,
|
|
1165
|
+
...useDefault ? [] : endpointFields,
|
|
1166
|
+
// The self-check is offered whenever there is something to check: an
|
|
1167
|
+
// enabled route (the Host's table carries it, and a rejected one still
|
|
1168
|
+
// reports WHY), or a disabled one whose endpoint is already complete —
|
|
1169
|
+
// the Host builds that one for the check alone. A disabled route with
|
|
1170
|
+
// no endpoint has nothing to test and would only answer "not enabled".
|
|
1171
|
+
showTest ? h(
|
|
603
1172
|
"div",
|
|
604
1173
|
{ style: S.testRow, key: `${row.id}:test` },
|
|
1174
|
+
// A check on a route that is still off is a trial run, and it says
|
|
1175
|
+
// so — otherwise the result reads as though the route were live.
|
|
1176
|
+
route.enabled ? null : h("span", { style: S.testResult }, t.testDisabledNote),
|
|
605
1177
|
h(
|
|
606
1178
|
"button",
|
|
607
1179
|
{
|
|
@@ -624,8 +1196,22 @@ function renderPage(ctx) {
|
|
|
624
1196
|
{ key: "lat", style: { color: latencyColor(result.ms) } },
|
|
625
1197
|
`\u2713 ${typeof result.ms === "number" ? result.ms : "\u2014"} ms`
|
|
626
1198
|
),
|
|
627
|
-
result.ip === void 0 ? null : ` \xB7 ${t.exitIpLabel}${result.ip}
|
|
1199
|
+
result.ip === void 0 ? null : ` \xB7 ${t.exitIpLabel}${result.ip}`,
|
|
1200
|
+
result.direct === true ? ` \xB7 ${t.directLabel}` : null
|
|
628
1201
|
] : h("span", { key: "err", style: S.testFailure }, `\u2717 ${result.error}`)
|
|
1202
|
+
) : null,
|
|
1203
|
+
// Promote this card's endpoint to the shared default. Offered only
|
|
1204
|
+
// when the card carries its OWN endpoint: an inheriting card would
|
|
1205
|
+
// just be copying the default onto itself.
|
|
1206
|
+
endpointStored && !useDefault ? h(
|
|
1207
|
+
"button",
|
|
1208
|
+
{
|
|
1209
|
+
className: import_client.default.testButton,
|
|
1210
|
+
style: S.testButton,
|
|
1211
|
+
type: "button",
|
|
1212
|
+
onClick: () => promoteToDefault(row.id)
|
|
1213
|
+
},
|
|
1214
|
+
t.setAsDefault
|
|
629
1215
|
) : null
|
|
630
1216
|
) : null,
|
|
631
1217
|
// Target-host rules, collapsed so the common case stays one short card.
|
|
@@ -638,24 +1224,38 @@ function renderPage(ctx) {
|
|
|
638
1224
|
h("input", {
|
|
639
1225
|
className: import_client.default.input,
|
|
640
1226
|
style: S.input,
|
|
641
|
-
disabled,
|
|
642
1227
|
placeholder: "api.example.com",
|
|
643
1228
|
value: route.onlyHosts,
|
|
644
1229
|
onChange: (event) => bind(row.id, "onlyHosts")(event.target.value)
|
|
645
1230
|
}),
|
|
646
|
-
t.onlyHostsHint
|
|
1231
|
+
onlyDead.length === 0 ? t.onlyHostsHint : `${t.ruleDeadHint}${onlyDead.join("\u3001")}`,
|
|
1232
|
+
onlyDead.length > 0
|
|
647
1233
|
),
|
|
648
1234
|
field(
|
|
649
1235
|
t.bypassHosts,
|
|
650
1236
|
h("input", {
|
|
651
1237
|
className: import_client.default.input,
|
|
652
1238
|
style: S.input,
|
|
653
|
-
disabled,
|
|
654
1239
|
placeholder: "localhost",
|
|
655
1240
|
value: route.bypassHosts,
|
|
656
1241
|
onChange: (event) => bind(row.id, "bypassHosts")(event.target.value)
|
|
657
1242
|
}),
|
|
658
|
-
t.bypassHostsHint
|
|
1243
|
+
bypassDead.length === 0 ? t.bypassHostsHint : `${t.ruleDeadHint}${bypassDead.join("\u3001")}`,
|
|
1244
|
+
bypassDead.length > 0
|
|
1245
|
+
),
|
|
1246
|
+
// The self-check target: empty uses the top-level testUrl, set makes
|
|
1247
|
+
// this card's Test request this URL — a real upstream proves the whole
|
|
1248
|
+
// path, not just the proxy hop.
|
|
1249
|
+
field(
|
|
1250
|
+
t.testTarget,
|
|
1251
|
+
h("input", {
|
|
1252
|
+
className: import_client.default.input,
|
|
1253
|
+
style: S.input,
|
|
1254
|
+
placeholder: "https://api.example.com",
|
|
1255
|
+
value: route.testTarget,
|
|
1256
|
+
onChange: (event) => bind(row.id, "testTarget")(event.target.value)
|
|
1257
|
+
}),
|
|
1258
|
+
t.testTargetHint
|
|
659
1259
|
)
|
|
660
1260
|
)
|
|
661
1261
|
);
|
|
@@ -664,6 +1264,9 @@ function renderPage(ctx) {
|
|
|
664
1264
|
"div",
|
|
665
1265
|
{ className: import_client.default.root, style: S.root },
|
|
666
1266
|
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),
|
|
1267
|
+
defaultBlock,
|
|
1268
|
+
catalogueNotice,
|
|
1269
|
+
filterRow,
|
|
667
1270
|
...body,
|
|
668
1271
|
h(
|
|
669
1272
|
"details",
|
|
@@ -699,62 +1302,118 @@ function renderPage(ctx) {
|
|
|
699
1302
|
);
|
|
700
1303
|
}
|
|
701
1304
|
var EN = {
|
|
702
|
-
lead: "Route selected model providers through your own forward proxy. Other providers stay direct.",
|
|
1305
|
+
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) go direct by default; set the top-level localDirect to false to proxy them instead \u2014 a forward proxy normally cannot reach your own machine.",
|
|
703
1306
|
empty: "No providers found yet.",
|
|
1307
|
+
catalogueLoading: "Loading providers\u2026",
|
|
1308
|
+
catalogueError: "Could not load the provider list: ",
|
|
1309
|
+
catalogueStale: "Could not load the provider list; showing the saved configuration: ",
|
|
704
1310
|
useProxy: "Use a proxy for this provider",
|
|
1311
|
+
useDefault: "Use the default proxy",
|
|
1312
|
+
defaultTitle: "Default proxy",
|
|
1313
|
+
defaultLead: 'One endpoint, reused by every provider that turns on "Use the default proxy". The status only reports configuration completeness; use the Test button on a card to verify reachability.',
|
|
1314
|
+
inheritingDefault: "Using the default proxy: ",
|
|
1315
|
+
defaultMissing: "The default proxy configuration is incomplete, so this provider is not proxied: ",
|
|
1316
|
+
filterLabel: "Filter",
|
|
1317
|
+
filterPlaceholder: "Filter by name or id\u2026",
|
|
1318
|
+
noMatch: "No provider matches the filter.",
|
|
1319
|
+
setAsDefault: "Set as the default proxy",
|
|
1320
|
+
applyDefaultAll: "Use the default proxy for all providers",
|
|
705
1321
|
protocol: "Type",
|
|
706
1322
|
host: "IP / host",
|
|
707
1323
|
port: "Port",
|
|
708
1324
|
username: "Username",
|
|
709
1325
|
password: "Password",
|
|
710
|
-
hostHint: "
|
|
1326
|
+
hostHint: "Only an IPv4, IPv6, or domain name is allowed. A full proxy URL (socks5://user:pass@host:1080) can be pasted instead \u2014 it is split into the fields.",
|
|
711
1327
|
passwordHint: "Sent to the SOCKS5 proxy for authentication; stored with this setting.",
|
|
712
1328
|
passwordHintProxy: "Sent to the proxy as an authentication password; stored with this setting.",
|
|
713
1329
|
hitCountPrefix: "",
|
|
714
1330
|
hitCountSuffix: " proxied",
|
|
1331
|
+
recentOk: "last ok ",
|
|
1332
|
+
recentFail: "last failure ",
|
|
1333
|
+
recentStarted: "last call ",
|
|
1334
|
+
noRecord: "no proxied request yet",
|
|
1335
|
+
justNow: "just now",
|
|
1336
|
+
minutesAgo: " min ago",
|
|
1337
|
+
hoursAgo: " h ago",
|
|
1338
|
+
daysAgo: " d ago",
|
|
715
1339
|
testButton: "Test",
|
|
716
1340
|
testRunning: "Testing\u2026",
|
|
1341
|
+
testDisabledNote: "Not enabled \u2014 test only: ",
|
|
717
1342
|
testFailed: "Test failed",
|
|
718
1343
|
exitIpLabel: "exit IP ",
|
|
1344
|
+
directLabel: "direct (not through the proxy)",
|
|
719
1345
|
advanced: "Advanced: target host rules",
|
|
720
1346
|
onlyHosts: "Proxy only",
|
|
721
1347
|
onlyHostsHint: "Comma/space separated; when set, only requests to these hosts are proxied (suffix match, * for all).",
|
|
722
1348
|
bypassHosts: "Bypass",
|
|
723
1349
|
bypassHostsHint: "Comma/space separated; requests to these hosts go direct, taking priority.",
|
|
1350
|
+
ruleDeadHint: "These entries never match \u2014 rules take host names only, without scheme or path: ",
|
|
1351
|
+
testTarget: "Self-check target",
|
|
1352
|
+
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
1353
|
socks5Hint: "SOCKS5 tunnels the connection; the proxy resolves the target host name.",
|
|
725
1354
|
on: "proxied",
|
|
726
|
-
|
|
1355
|
+
rejected: "config rejected",
|
|
727
1356
|
off: "direct",
|
|
1357
|
+
complete: "complete",
|
|
1358
|
+
notConfigured: "not configured",
|
|
728
1359
|
diagnostics: "Connection diagnostics",
|
|
729
1360
|
diagPending: "(collecting\u2026)"
|
|
730
1361
|
};
|
|
731
1362
|
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",
|
|
1363
|
+
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\u9ED8\u8BA4\u76F4\u8FDE\uFF0C\u9876\u5C42 localDirect \u8BBE\u4E3A false \u65F6\u6062\u590D\u8D70\u4EE3\u7406\u2014\u2014\u6B63\u5411\u4EE3\u7406\u901A\u5E38\u591F\u4E0D\u5230\u4F60\u81EA\u5DF1\u7684\u673A\u5668\u3002",
|
|
733
1364
|
empty: "\u6CA1\u6709\u68C0\u6D4B\u5230\u4EFB\u4F55 provider\u3002",
|
|
1365
|
+
catalogueLoading: "\u6B63\u5728\u8BFB\u53D6 provider \u5217\u8868\u2026",
|
|
1366
|
+
catalogueError: "\u8BFB\u53D6 provider \u5217\u8868\u5931\u8D25\uFF1A",
|
|
1367
|
+
catalogueStale: "provider \u5217\u8868\u8BFB\u53D6\u5931\u8D25\uFF0C\u4E0B\u9762\u663E\u793A\u7684\u662F\u5DF2\u4FDD\u5B58\u7684\u914D\u7F6E\uFF1A",
|
|
734
1368
|
useProxy: "\u4E3A\u8BE5 provider \u542F\u7528\u4EE3\u7406",
|
|
1369
|
+
useDefault: "\u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406",
|
|
1370
|
+
defaultTitle: "\u9ED8\u8BA4\u4EE3\u7406",
|
|
1371
|
+
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\u72B6\u6001\u53EA\u8868\u793A\u914D\u7F6E\u662F\u5426\u5B8C\u6574\uFF1B\u4EE3\u7406\u662F\u5426\u53EF\u8FBE\u8BF7\u7528\u5361\u7247\u4E0A\u7684\u300C\u6D4B\u8BD5\u300D\u9A8C\u8BC1\u3002",
|
|
1372
|
+
inheritingDefault: "\u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406\uFF1A",
|
|
1373
|
+
defaultMissing: "\u9876\u5C42\u9ED8\u8BA4\u4EE3\u7406\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF0C\u8BE5 provider \u4E0D\u4F1A\u8D70\u4EE3\u7406\uFF1A",
|
|
1374
|
+
filterLabel: "\u8FC7\u6EE4",
|
|
1375
|
+
filterPlaceholder: "\u6309\u540D\u79F0\u6216 id \u8FC7\u6EE4\u2026",
|
|
1376
|
+
noMatch: "\u6CA1\u6709\u5339\u914D\u7684 provider\u3002",
|
|
1377
|
+
setAsDefault: "\u8BBE\u4E3A\u9ED8\u8BA4\u4EE3\u7406",
|
|
1378
|
+
applyDefaultAll: "\u6240\u6709 provider \u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406",
|
|
735
1379
|
protocol: "\u7C7B\u578B",
|
|
736
1380
|
host: "IP / \u4E3B\u673A",
|
|
737
1381
|
port: "\u7AEF\u53E3",
|
|
738
1382
|
username: "\u7528\u6237\u540D",
|
|
739
1383
|
password: "\u5BC6\u7801",
|
|
740
|
-
hostHint: "\u53EA\u586B\
|
|
1384
|
+
hostHint: "\u53EA\u5141\u8BB8\u586B\u5199 IPv4\u3001IPv6 \u6216\u57DF\u540D\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
1385
|
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
1386
|
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
1387
|
hitCountPrefix: "\u5DF2\u4EE3\u7406 ",
|
|
744
1388
|
hitCountSuffix: " \u6B21",
|
|
1389
|
+
recentOk: "\u6700\u8FD1\u6210\u529F ",
|
|
1390
|
+
recentFail: "\u6700\u8FD1\u5931\u8D25 ",
|
|
1391
|
+
recentStarted: "\u6700\u8FD1\u8C03\u7528 ",
|
|
1392
|
+
noRecord: "\u5C1A\u65E0\u4EE3\u7406\u8BB0\u5F55",
|
|
1393
|
+
justNow: "\u521A\u521A",
|
|
1394
|
+
minutesAgo: " \u5206\u949F\u524D",
|
|
1395
|
+
hoursAgo: " \u5C0F\u65F6\u524D",
|
|
1396
|
+
daysAgo: " \u5929\u524D",
|
|
745
1397
|
testButton: "\u6D4B\u8BD5",
|
|
746
1398
|
testRunning: "\u6D4B\u8BD5\u4E2D\u2026",
|
|
1399
|
+
testDisabledNote: "\u672A\u542F\u7528\uFF0C\u4EC5\u6D4B\u8BD5\uFF1A",
|
|
747
1400
|
testFailed: "\u6D4B\u8BD5\u5931\u8D25",
|
|
748
1401
|
exitIpLabel: "\u51FA\u53E3 IP ",
|
|
1402
|
+
directLabel: "\u76F4\u8FDE\uFF08\u672A\u7ECF\u8FC7\u4EE3\u7406\uFF09",
|
|
749
1403
|
advanced: "\u9AD8\u7EA7\uFF1A\u76EE\u6807\u57DF\u540D\u89C4\u5219",
|
|
750
1404
|
onlyHosts: "\u4EC5\u8D70\u4EE3\u7406",
|
|
751
1405
|
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
1406
|
bypassHosts: "\u76F4\u8FDE",
|
|
753
1407
|
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",
|
|
1408
|
+
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",
|
|
1409
|
+
testTarget: "\u81EA\u68C0\u76EE\u6807",
|
|
1410
|
+
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
1411
|
socks5Hint: "SOCKS5 \u4F1A\u5EFA\u7ACB\u96A7\u9053\uFF0C\u76EE\u6807\u57DF\u540D\u7531\u4EE3\u7406\u7AEF\u89E3\u6790\u3002",
|
|
755
1412
|
on: "\u8D70\u4EE3\u7406",
|
|
756
|
-
|
|
1413
|
+
rejected: "\u914D\u7F6E\u88AB\u62D2",
|
|
757
1414
|
off: "\u76F4\u8FDE",
|
|
1415
|
+
complete: "\u914D\u7F6E\u5B8C\u6574",
|
|
1416
|
+
notConfigured: "\u672A\u914D\u7F6E",
|
|
758
1417
|
diagnostics: "\u8FDE\u63A5\u8BCA\u65AD",
|
|
759
1418
|
diagPending: "\uFF08\u6B63\u5728\u6536\u96C6\u2026\uFF09"
|
|
760
1419
|
};
|