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/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": "6d45ff27_root", "lead": "57c30143_lead", "empty": "b6600f2d_empty", "card": "0ba90fef_card", "cardActive": "923630a9_cardActive", "cardHead": "5af7e7c3_cardHead", "cardTitles": "8448919c_cardTitles", "name": "0b023e00_name", "id": "ffa6a1c5_id", "pillOn": "85972c09_pillOn", "pillLive": "d3644cdc_pillLive", "pillOff": "592bd7b2_pillOff", "toggle": "9efc135e_toggle", "field": "93a03683_field", "label": "0bd1d3ec_label", "hint": "52d56786_hint", "input": "f6748c5e_input", "testButton": "eb3b08bd_testButton", "diag": "4d4b11ef_diag", "diagSummary": "2e565ce0_diagSummary", "diagList": "fb9c7553_diagList" };
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 = "/* dsh-provider-proxy \u2014 provider proxy settings page.\n Tokens only: no literal colours, matching the shared --dsw-* system. */\n\n.6d45ff27_root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n max-width: 640px;\n}\n\n.57c30143_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.b6600f2d_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.0ba90fef_card,\n.923630a9_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.923630a9_cardActive {\n border-color: var(--dsw-alias-state-success-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.5af7e7c3_cardHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.8448919c_cardTitles {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.0b023e00_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.ffa6a1c5_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.85972c09_pillOn,\n.d3644cdc_pillLive,\n.592bd7b2_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.85972c09_pillOn {\n background: var(--dsw-alias-state-success-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.d3644cdc_pillLive {\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-secondary);\n}\n\n.592bd7b2_pillOff {\n color: var(--dsw-alias-state-idle-primary);\n}\n\n/* ---- toggle ---- */\n\n.9efc135e_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.9efc135e_toggle input {\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n/* ---- fields ---- */\n\n.93a03683_field {\n display: grid;\n grid-template-columns: 96px 1fr;\n align-items: center;\n gap: 10px;\n font-size: 12px;\n}\n\n.0bd1d3ec_label {\n color: var(--dsw-alias-label-secondary);\n}\n\n.52d56786_hint {\n grid-column: 2;\n margin-top: -2px;\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.f6748c5e_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.f6748c5e_input:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n}\n\n.f6748c5e_input:focus {\n border-color: var(--dsw-alias-brand-primary) !important;\n}\n\n.f6748c5e_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.eb3b08bd_testButton {\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n.eb3b08bd_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.4d4b11ef_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.2e565ce0_diagSummary {\n cursor: pointer;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.fb9c7553_diagList {\n margin: 6px 0 0;\n padding-left: 18px;\n font-family: ui-monospace, monospace;\n word-break: break-all;\n}\n";
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: () => localeOf(ctx) === "zh" ? "Provider \u4EE3\u7406" : "Provider \u4EE3\u7406"
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 [id, route] of Object.entries(configured)) {
195
+ for (const id of Object.keys(configured)) {
156
196
  if (rows.has(id)) continue;
157
- rows.set(id, { id, name: id, live: route?.enabled === true });
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
- return { rows, hits, problems };
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
- note(`provider \u5217\u8868\u83B7\u53D6\u5931\u8D25\uFF1A${error?.message ?? error}`);
386
- catalogueSetter?.({ rows: [], hits: {}, problems: {} });
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({ rows: catalog.rows, hits: catalog.hits, problems: catalog.problems, pending: false });
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 save = (id, field2, value) => {
432
- void writeConfig(ctx, [{ op: "set", path: ["providers", id, field2], value }]).then(() => {
433
- note(`\u5DF2\u4FDD\u5B58 ${id}.${field2} = ${JSON.stringify(value)}`);
434
- }).catch((error) => {
435
- note(`\u4FDD\u5B58\u5931\u8D25 ${id}.${field2}\uFF1A${error?.message ?? error}`);
436
- setDraft((current) => {
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[id];
737
+ delete next[field2];
439
738
  return next;
440
739
  });
441
740
  });
442
741
  };
443
- const bind = (id, field2) => (value) => {
444
- edit(id, field2, value);
445
- save(id, field2, value);
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
- { className: import_client.default.hint, style: { gridColumn: "2", fontSize: "11px" } },
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 body = rows.length === 0 ? [
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
- ] : rows.map((row) => {
1008
+ ] : shown.map((row) => {
477
1009
  const route = view({ ...configured[row.id], ...draft[row.id] });
478
- const disabled = !route.enabled;
1010
+ const lastText = lastUseText(row.id) ?? (route.enabled ? t.noRecord : void 0);
479
1011
  const result = tests[row.id];
480
- return h(
481
- "section",
482
- {
483
- className: route.enabled ? import_client.default.cardActive : import_client.default.card,
484
- style: route.enabled ? { ...S.card, ...S.cardActive } : S.card,
485
- key: row.id
486
- },
487
- h(
488
- "header",
489
- { className: import_client.default.cardHead, style: S.head },
490
- h(
491
- "div",
492
- { className: import_client.default.cardTitles, style: S.titles },
493
- h("span", { className: import_client.default.name, style: S.name }, row.name),
494
- h("span", { className: import_client.default.id, style: S.id }, row.id),
495
- // A disabled route can never have hits, so "0" there would only add
496
- // noise; the count shows once the route is on (or has counted).
497
- route.enabled || (hits[row.id] ?? 0) > 0 ? h(
498
- "span",
499
- { style: S.hits },
500
- `${t.hitCountPrefix}${hits[row.id] ?? 0}${t.hitCountSuffix}`
501
- ) : null
502
- ),
503
- h(
504
- "span",
505
- {
506
- className: route.enabled ? import_client.default.pillOn : row.live ? import_client.default.pillLive : import_client.default.pillOff,
507
- style: {
508
- ...S.pill,
509
- ...route.enabled ? S.pillOn : row.live ? S.pillLive : S.pillOff
510
- }
511
- },
512
- route.enabled ? t.on : row.live ? t.live : t.off
513
- )
514
- ),
515
- h(
516
- "label",
517
- { className: import_client.default.toggle, style: S.toggle },
518
- h("input", {
519
- type: "checkbox",
520
- checked: route.enabled,
521
- onChange: (event) => bind(row.id, "enabled")(event.target.checked === true)
522
- }),
523
- h("span", null, t.useProxy)
524
- ),
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 now: SOCKS5 hands it to
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
- // The self-check only makes sense for a route that is ON; a disabled
601
- // route is not in the Host's table, so there is nothing to test.
602
- route.enabled ? h(
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: "Host or IP only, without scheme or path.",
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
- live: "active",
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\u4E3B\u673A\u540D\u6216 IP\uFF0C\u4E0D\u8981\u5E26\u534F\u8BAE\u524D\u7F00\u6216\u8DEF\u5F84\u3002",
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
- live: "\u542F\u7528\u4E2D",
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
  };