dsh-provider-proxy 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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": "7a51762d_root", "lead": "c71659b2_lead", "empty": "c42c4abe_empty", "card": "53acf569_card", "cardActive": "bd4546f8_cardActive", "defaultCard": "1103ba6e_defaultCard", "defaultLead": "0b907dc6_defaultLead", "cardHead": "d06dccdb_cardHead", "cardTitles": "ff20149f_cardTitles", "name": "4e36a10e_name", "id": "79208064_id", "pillOn": "3e0969fd_pillOn", "pillLive": "895a1b6e_pillLive", "pillOff": "f5c4d227_pillOff", "toggle": "94e6424f_toggle", "field": "5dfb5268_field", "label": "2e13439d_label", "hint": "cdd952c6_hint", "input": "31dd2d99_input", "testButton": "028bc91c_testButton", "diag": "5d114b0b_diag", "diagSummary": "27518b39_diagSummary", "diagList": "b3005b62_diagList" };
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.7a51762d_root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n max-width: 640px;\n}\n\n.c71659b2_lead {\n margin: 0 0 4px;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\n}\n\n.c42c4abe_empty {\n margin: 0;\n padding: 16px;\n border: 1px dashed var(--dsw-alias-border-l1);\n border-radius: 8px;\n font-size: 12px;\n text-align: center;\n color: var(--dsw-alias-state-idle-primary);\n}\n\n/* ---- provider card ---- */\n\n.53acf569_card,\n.bd4546f8_cardActive {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.bd4546f8_cardActive {\n border-color: var(--dsw-alias-state-success-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* ---- shared default endpoint ---- */\n\n/* A distinct outer layer, so it reads as "one setting for the page" rather than\n as one more provider card. */\n.1103ba6e_defaultCard {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 14px 16px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.0b907dc6_defaultLead {\n margin: -2px 0 0;\n font-size: 11px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n}\n\n.d06dccdb_cardHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.ff20149f_cardTitles {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.4e36a10e_name {\n overflow: hidden;\n font-size: 13px;\n font-weight: 600;\n white-space: nowrap;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.79208064_id {\n flex: none;\n padding: 1px 6px;\n border-radius: 4px;\n background: var(--dsw-alias-bg-layer-2);\n font-family: ui-monospace, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* ---- status pill ---- */\n\n.3e0969fd_pillOn,\n.895a1b6e_pillLive,\n.f5c4d227_pillOff {\n flex: none;\n padding: 2px 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n.3e0969fd_pillOn {\n background: var(--dsw-alias-state-success-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.895a1b6e_pillLive {\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-secondary);\n}\n\n.f5c4d227_pillOff {\n color: var(--dsw-alias-state-idle-primary);\n}\n\n/* ---- toggle ---- */\n\n.94e6424f_toggle {\n display: flex;\n align-items: center;\n gap: 8px;\n padding-top: 2px;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n}\n\n.94e6424f_toggle input {\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n/* ---- fields ---- */\n\n.5dfb5268_field {\n display: grid;\n grid-template-columns: 96px 1fr;\n align-items: center;\n gap: 10px;\n font-size: 12px;\n}\n\n.2e13439d_label {\n color: var(--dsw-alias-label-secondary);\n}\n\n.cdd952c6_hint {\n grid-column: 2;\n margin-top: -2px;\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.31dd2d99_input {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n padding: 5px 8px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n outline: none;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n/* The base look of every input is INLINE too, so these three state rules only\n ever take effect as author-important declarations \u2014 same reason as the\n self-check button above. `cursor` has no inline base and needs none, and the\n hover border uses `l3` because `l1`\u2192`l2` is a 4%\u219210% black step nobody can\n see. */\n\n.31dd2d99_input:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n}\n\n.31dd2d99_input:focus {\n border-color: var(--dsw-alias-brand-primary) !important;\n}\n\n.31dd2d99_input:disabled {\n color: var(--dsw-alias-state-idle-primary) !important;\n background: var(--dsw-alias-bg-layer-2) !important;\n cursor: not-allowed;\n}\n\n/* ---- self-check ---- */\n\n/* The hover state is the one thing an inline style cannot express, which is why\n this element carries a class at all. Layout stays inline.\n\n Two traps, both hit before this comment existed:\n - `!important` is load-bearing: the base border and background are INLINE, and\n the cascade sorts a style attribute after every author rule, so a plain\n declaration here would ship, match, and still lose.\n - the TOKENS must be visible ones. `bg-layer-1/2/3` all resolve to\n `neutral-bluish-00` in the light theme, so a layer swap is a no-op there, and\n `border-l1`\u2192`l2` is 4%\u219210% black on a 1px line. The app\'s own hovers use\n `interactive-bg-hover`; using it here is what makes the feedback exist. */\n.028bc91c_testButton {\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n.028bc91c_testButton:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\n/* ---- diagnostics ---- */\n\n.5d114b0b_diag {\n margin-top: 2px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.27518b39_diagSummary {\n cursor: pointer;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.b3005b62_diagList {\n margin: 6px 0 0;\n padding-left: 18px;\n font-family: ui-monospace, monospace;\n word-break: break-all;\n}\n';
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;
@@ -64,18 +64,60 @@ __export(client_exports, {
64
64
  module.exports = __toCommonJS(client_exports);
65
65
  var import_client = __toESM(require_client(), 1);
66
66
 
67
+ // src/host-rules.ts
68
+ function splitHostRules(raw) {
69
+ return raw.split(/[,\s]+/).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
70
+ }
71
+ function normalizeHostRule(raw) {
72
+ return raw.trim().replace(/^\*?\./, "").toLowerCase();
73
+ }
74
+ function isBracketedIpv6(token) {
75
+ return /^\[[0-9a-f:.]+\]$/i.test(token);
76
+ }
77
+ function hostRuleProblem(raw) {
78
+ const token = raw.trim();
79
+ if (token.length === 0) return void 0;
80
+ if (token === "*." || normalizeHostRule(token).length === 0) return "\u7A7A\u901A\u914D\u7B26";
81
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(token)) return "\u5E26\u534F\u8BAE\u524D\u7F00";
82
+ if (token.includes("/")) return "\u5E26\u8DEF\u5F84";
83
+ if (token.includes(":") && !isBracketedIpv6(token)) return "\u5E26\u7AEF\u53E3\u6216\u975E\u6CD5\u5192\u53F7";
84
+ const withoutLeadingWildcard = token.replace(/^\*\./, "");
85
+ if (withoutLeadingWildcard.includes("*") && token !== "*") return "\u6742\u6563\u901A\u914D\u7B26";
86
+ return void 0;
87
+ }
88
+ function deadRuleEntries(raw) {
89
+ return splitHostRules(raw).filter((entry) => hostRuleProblem(entry) !== void 0);
90
+ }
91
+
67
92
  // src/proxy-address.ts
68
- function defaultProxyProblem(endpoint) {
69
- const host = (endpoint.host ?? "").trim();
93
+ var PROXY_HOST_INVALID = "\u4EE3\u7406\u5730\u5740\u53EA\u80FD\u586B IPv4\u3001IPv6 \u6216\u57DF\u540D";
94
+ 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)$/;
95
+ 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;
96
+ var IPV6_CHARS = /^[0-9a-f:.]+$/i;
97
+ function proxyHostProblem(rawHost) {
98
+ const host = (rawHost ?? "").trim().replace(/\.$/, "");
70
99
  if (host.length === 0) return "\u672A\u586B\u5199\u4EE3\u7406\u5730\u5740";
71
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(host) || host.includes("/")) {
72
- return "\u4EE3\u7406\u5730\u5740\u53EA\u586B\u4E3B\u673A\u540D\u6216 IP\uFF0C\u4E0D\u8981\u5E26\u534F\u8BAE\u524D\u7F00\u6216\u8DEF\u5F84";
100
+ if (IPV4.test(host)) return void 0;
101
+ if (host.includes(":") || host.startsWith("[")) {
102
+ const literal = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
103
+ if (!IPV6_CHARS.test(literal)) return PROXY_HOST_INVALID;
104
+ try {
105
+ new URL(`http://[${literal}]:1`);
106
+ return void 0;
107
+ } catch {
108
+ return PROXY_HOST_INVALID;
109
+ }
73
110
  }
111
+ return DOMAIN.test(host) ? void 0 : PROXY_HOST_INVALID;
112
+ }
113
+ function defaultProxyProblem(endpoint) {
114
+ const hostProblem = proxyHostProblem(endpoint.host);
115
+ if (hostProblem !== void 0) return hostProblem;
74
116
  const port = endpoint.port ?? 0;
75
117
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
76
118
  return "\u4EE3\u7406\u7AEF\u53E3\u65E0\u6548\uFF08\u9700\u4E3A 1-65535 \u7684\u6574\u6570\uFF09";
77
119
  }
78
- const hasUser = (endpoint.username ?? "").length > 0;
120
+ const hasUser = (endpoint.username ?? "").trim().length > 0;
79
121
  const hasPassword = (endpoint.password ?? "").length > 0;
80
122
  if (hasUser !== hasPassword) {
81
123
  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";
@@ -170,12 +212,15 @@ async function writeConfig(ctx, ops) {
170
212
  }
171
213
  function knownProviders(configured, published) {
172
214
  const rows = /* @__PURE__ */ new Map();
215
+ const byId = /* @__PURE__ */ new Map();
173
216
  for (const row of published) {
217
+ byId.set(row.id, row);
174
218
  if (row.live) rows.set(row.id, row);
175
219
  }
176
- for (const [id, route] of Object.entries(configured)) {
220
+ for (const id of Object.keys(configured)) {
177
221
  if (rows.has(id)) continue;
178
- rows.set(id, { id, name: id, live: route?.enabled === true });
222
+ const known = byId.get(id);
223
+ rows.set(id, { id, name: known?.name ?? id, live: known?.live === true });
179
224
  }
180
225
  return [...rows.values()].sort(byName);
181
226
  }
@@ -183,6 +228,8 @@ var CATALOG_PATH = "/dsh-provider-proxy/providers";
183
228
  var TEST_PATH = "/dsh-provider-proxy/test";
184
229
  var LATENCY_FAST_MS = 500;
185
230
  var LATENCY_SLOW_MS = 1500;
231
+ var DEFAULT_TEST_URL = "https://api.ipify.org?format=json";
232
+ var MAX_TEST_URLS = 5;
186
233
  function latencyColor(ms) {
187
234
  if (typeof ms !== "number" || !Number.isFinite(ms)) return "var(--dsw-alias-label-secondary)";
188
235
  if (ms <= LATENCY_FAST_MS) return "var(--dsw-alias-state-success-primary, #2ea043)";
@@ -196,6 +243,10 @@ function testResultFrom(body, strings) {
196
243
  ok,
197
244
  ms: typeof body?.ms === "number" && Number.isFinite(body.ms) ? body.ms : void 0,
198
245
  ip: typeof body?.exitIp === "string" && body.exitIp.length > 0 ? body.exitIp : void 0,
246
+ status: typeof body?.status === "number" && Number.isInteger(body.status) ? body.status : void 0,
247
+ target: typeof body?.target === "string" && body.target.length > 0 ? body.target : void 0,
248
+ targetSource: body?.targetSource === "route" || body?.targetSource === "upstream" ? body.targetSource : body?.targetSource === "testUrl" ? "testUrl" : void 0,
249
+ cached: body?.cached === true,
199
250
  // The Host probes a target the way routing would: a loopback/private upstream
200
251
  // (or a bypassHosts match) is reached DIRECT, not through the proxy. The card
201
252
  // says so, so a green check is not read as "the proxy works".
@@ -203,6 +254,57 @@ function testResultFrom(body, strings) {
203
254
  error: ok ? void 0 : typeof body?.error === "string" && body.error.length > 0 ? body.error : strings.testFailed
204
255
  };
205
256
  }
257
+ function splitTestUrls(raw) {
258
+ return raw.split(/[,\s]+/).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
259
+ }
260
+ function testUrlHint(raw, t) {
261
+ const entries = splitTestUrls(raw);
262
+ if (entries.length === 0) return t.testUrlEmptyHint;
263
+ const invalid = entries.filter((entry) => !isAbsoluteHttpUrl(entry));
264
+ if (invalid.length > 0) return t.testUrlInvalidHint;
265
+ if (entries.length > MAX_TEST_URLS) return t.testUrlTooManyHint;
266
+ return void 0;
267
+ }
268
+ function isAbsoluteHttpUrl(value) {
269
+ try {
270
+ const url = new URL(value);
271
+ return (url.protocol === "http:" || url.protocol === "https:") && url.hostname.length > 0;
272
+ } catch {
273
+ return false;
274
+ }
275
+ }
276
+ function targetSourceSuffix(result, t) {
277
+ switch (result.targetSource) {
278
+ case "route":
279
+ return t.targetSourceRoute;
280
+ case "upstream":
281
+ return t.targetSourceUpstream;
282
+ case "testUrl":
283
+ return t.targetSourceTestUrl;
284
+ default:
285
+ return "";
286
+ }
287
+ }
288
+ function redactSensitiveText(text, secrets = []) {
289
+ let safe = text;
290
+ for (const secret of secrets) {
291
+ if (typeof secret !== "string" || secret.length === 0) continue;
292
+ safe = safe.split(secret).join("***");
293
+ }
294
+ safe = safe.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/gi, "$1");
295
+ safe = safe.replace(/([?&#])[^\s"'<>]*/g, "$1");
296
+ return safe.length > 500 ? `${safe.slice(0, 500)}\u2026` : safe;
297
+ }
298
+ function secretsFromOps(ops) {
299
+ const out = [];
300
+ for (const op of ops) {
301
+ const field = op?.path?.[op.path.length - 1];
302
+ if (field === "password" && typeof op?.value === "string" && op.value.length > 0) {
303
+ out.push(op.value);
304
+ }
305
+ }
306
+ return out;
307
+ }
206
308
  var catalogueSetter = () => {
207
309
  };
208
310
  function relativeTime(at, t) {
@@ -273,9 +375,6 @@ function touchCatalogPolling() {
273
375
  }, CATALOG_REFRESH_MS);
274
376
  }
275
377
  var byName = (a, b) => a.name.localeCompare(b.name);
276
- function deadRuleEntries(raw) {
277
- return raw.split(/[,\s]+/).map((entry) => entry.trim()).filter((entry) => /^[a-z][a-z0-9+.-]*:\/\//i.test(entry) || entry.includes("/"));
278
- }
279
378
  function view(route) {
280
379
  return {
281
380
  enabled: route?.enabled === true,
@@ -293,6 +392,14 @@ function view(route) {
293
392
  function unbracket(hostname) {
294
393
  return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
295
394
  }
395
+ function explicitPortOf(text) {
396
+ const authority = text.slice(text.indexOf("://") + 3).split(/[/?#]/, 1)[0];
397
+ const hostPort = authority.slice(authority.lastIndexOf("@") + 1);
398
+ const match = /^\[[^\]]+\]:(\d+)$/.exec(hostPort) ?? /^[^:]+:(\d+)$/.exec(hostPort);
399
+ if (match === null) return 0;
400
+ const port = Number.parseInt(match[1], 10);
401
+ return Number.isNaN(port) ? 0 : port;
402
+ }
296
403
  function splitProxyUrl(text) {
297
404
  const trimmed = text.trim();
298
405
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return void 0;
@@ -313,7 +420,7 @@ function splitProxyUrl(text) {
313
420
  return value;
314
421
  }
315
422
  };
316
- const parsedPort = url.port.length > 0 ? Number.parseInt(url.port, 10) : 0;
423
+ const parsedPort = url.port.length > 0 ? Number.parseInt(url.port, 10) : explicitPortOf(trimmed);
317
424
  return {
318
425
  protocol,
319
426
  host: unbracket(url.hostname),
@@ -324,6 +431,39 @@ function splitProxyUrl(text) {
324
431
  }
325
432
  var WRITE_DELAY_MS = 300;
326
433
  var pendingWrites = /* @__PURE__ */ new Map();
434
+ var writeQueue = Promise.resolve();
435
+ function enqueueWrite(run) {
436
+ const next = writeQueue.then(run, run);
437
+ writeQueue = next.catch(() => void 0);
438
+ return next;
439
+ }
440
+ function scheduleWrite(key, run) {
441
+ const pending = pendingWrites.get(key);
442
+ if (pending?.timer !== void 0) clearTimeout(pending.timer);
443
+ const entry = {
444
+ flush: () => {
445
+ if (entry.timer !== void 0) clearTimeout(entry.timer);
446
+ pendingWrites.delete(key);
447
+ return enqueueWrite(run);
448
+ }
449
+ };
450
+ entry.timer = setTimeout(() => {
451
+ void entry.flush();
452
+ }, WRITE_DELAY_MS);
453
+ pendingWrites.set(key, entry);
454
+ }
455
+ var testGeneration = /* @__PURE__ */ new Map();
456
+ var TEST_RELEVANT_FIELDS = /* @__PURE__ */ new Set([
457
+ "protocol",
458
+ "host",
459
+ "port",
460
+ "username",
461
+ "password",
462
+ "onlyHosts",
463
+ "bypassHosts",
464
+ "testTarget",
465
+ "useDefault"
466
+ ]);
327
467
  var diagnostics = [];
328
468
  function note(message) {
329
469
  if (!diagnostics.includes(message)) diagnostics.push(message);
@@ -349,6 +489,11 @@ var S = {
349
489
  borderColor: "var(--dsw-alias-state-success-primary)",
350
490
  background: "var(--dsw-alias-bg-layer-2)"
351
491
  },
492
+ /** An enabled route the Host refused: active-looking styling would be a lie. */
493
+ cardRejected: {
494
+ borderColor: "var(--dsw-alias-state-error-primary, #f85149)",
495
+ background: "var(--dsw-alias-bg-layer-2)"
496
+ },
352
497
  head: {
353
498
  display: "flex",
354
499
  alignItems: "center",
@@ -396,6 +541,14 @@ var S = {
396
541
  cursor: "pointer"
397
542
  },
398
543
  testResult: {
544
+ // A flex row so the target can drop onto its own line: the container was a
545
+ // plain span, where `flexBasis: 100%` does nothing and the target ran into
546
+ // the "cached" label.
547
+ display: "flex",
548
+ flexWrap: "wrap",
549
+ alignItems: "baseline",
550
+ columnGap: "6px",
551
+ rowGap: "2px",
399
552
  fontSize: "11px",
400
553
  color: "var(--dsw-alias-label-secondary)",
401
554
  wordBreak: "break-all"
@@ -439,6 +592,10 @@ var S = {
439
592
  color: "var(--dsw-alias-label-secondary)"
440
593
  },
441
594
  pillOff: { color: "var(--dsw-alias-state-idle-primary)" },
595
+ pillRejected: {
596
+ border: "1px solid var(--dsw-alias-state-error-primary, #f85149)",
597
+ color: "var(--dsw-alias-state-error-primary, #f85149)"
598
+ },
442
599
  /**
443
600
  * The shared default endpoint sits on its own layer so it reads as global
444
601
  * rather than as one more provider card — the mistake it exists to prevent is
@@ -500,6 +657,19 @@ var S = {
500
657
  color: "var(--dsw-alias-label-primary)",
501
658
  font: "inherit",
502
659
  fontSize: "12px"
660
+ },
661
+ diag: {
662
+ marginTop: "4px",
663
+ padding: "8px 10px",
664
+ border: "1px solid var(--dsw-alias-border-l1)",
665
+ borderRadius: "8px",
666
+ fontSize: "11px",
667
+ color: "var(--dsw-alias-label-secondary)"
668
+ },
669
+ hint: {
670
+ gridColumn: "2",
671
+ fontSize: "11px",
672
+ color: "var(--dsw-alias-label-secondary)"
503
673
  }
504
674
  };
505
675
  function renderPage(ctx) {
@@ -521,14 +691,20 @@ function renderPage(ctx) {
521
691
  const [state, setState] = (React.useState ?? ((fn) => [fn(), () => {
522
692
  }]))(() => {
523
693
  void fetchCatalog().then((catalog) => catalogueSetter?.(catalog)).catch((error) => {
524
- note(`provider \u5217\u8868\u83B7\u53D6\u5931\u8D25\uFF1A${error?.message ?? error}`);
525
- catalogueSetter?.({ rows: [], hits: {}, problems: {}, lastUse: {} });
694
+ catalogueSetter?.({
695
+ rows: [],
696
+ hits: {},
697
+ problems: {},
698
+ lastUse: {},
699
+ error: String(error?.message ?? error)
700
+ });
526
701
  });
527
702
  return {
528
703
  rows: void 0,
529
704
  hits: {},
530
705
  problems: {},
531
706
  lastUse: {},
707
+ error: void 0,
532
708
  pending: true
533
709
  };
534
710
  });
@@ -537,6 +713,7 @@ function renderPage(ctx) {
537
713
  hits: catalog.hits,
538
714
  problems: catalog.problems,
539
715
  lastUse: catalog.lastUse,
716
+ error: catalog.error,
540
717
  pending: false
541
718
  });
542
719
  touchCatalogPolling();
@@ -564,22 +741,56 @@ function renderPage(ctx) {
564
741
  const [defaultDraft, setDefaultDraft] = (React.useState ?? ((v) => [v, () => {
565
742
  }]))({});
566
743
  const editDefault = (field2, value) => setDefaultDraft((current) => ({ ...current, [field2]: value }));
744
+ const [globalDraft, setGlobalDraft] = (React.useState ?? ((v) => [v, () => {
745
+ }]))({});
746
+ const editGlobal = (field2, value) => setGlobalDraft((current) => ({ ...current, [field2]: value }));
747
+ const [saveError, setSaveError] = (React.useState ?? ((v) => [v, () => {
748
+ }]))(void 0);
749
+ const [savedNote, setSavedNote] = (React.useState ?? ((v) => [v, () => {
750
+ }]))(void 0);
567
751
  const [tests, setTests] = (React.useState ?? ((v) => [v, () => {
568
752
  }]))({});
753
+ const invalidateTests = (ids) => {
754
+ if (ids.length === 0) return;
755
+ for (const id of ids) {
756
+ testGeneration.set(id, (testGeneration.get(id) ?? 0) + 1);
757
+ }
758
+ setTests((current) => {
759
+ let changed = false;
760
+ const next = { ...current };
761
+ for (const id of ids) {
762
+ if (next[id] === void 0) continue;
763
+ delete next[id];
764
+ changed = true;
765
+ }
766
+ return changed ? next : current;
767
+ });
768
+ };
569
769
  const [filter, setFilter] = (React.useState ?? ((v) => [v, () => {
570
770
  }]))("");
571
771
  const runTest = (id) => {
772
+ const generation = (testGeneration.get(id) ?? 0) + 1;
773
+ testGeneration.set(id, generation);
572
774
  setTests((current) => ({ ...current, [id]: { pending: true } }));
573
775
  void (async () => {
574
776
  try {
777
+ for (const [key, entry] of [...pendingWrites]) {
778
+ if (key.startsWith(`${id}\0`) || key.startsWith("\0default\0") || key.startsWith("\0global\0")) {
779
+ void entry.flush();
780
+ }
781
+ }
782
+ await writeQueue;
783
+ if (testGeneration.get(id) !== generation) return;
575
784
  const response = await globalThis.fetch(TEST_PATH, {
576
785
  method: "POST",
577
786
  headers: { "content-type": "application/json", accept: "application/json" },
578
787
  body: JSON.stringify({ provider: id })
579
788
  });
580
789
  const body2 = await response.json();
790
+ if (testGeneration.get(id) !== generation) return;
581
791
  setTests((current) => ({ ...current, [id]: testResultFrom(body2, t) }));
582
792
  } catch (error) {
793
+ if (testGeneration.get(id) !== generation) return;
583
794
  setTests((current) => ({
584
795
  ...current,
585
796
  [id]: { pending: false, ok: false, error: String(error?.message ?? error) }
@@ -588,19 +799,25 @@ function renderPage(ctx) {
588
799
  })();
589
800
  };
590
801
  const rows = knownProviders({ ...configured, ...draft }, published);
802
+ const inheritingIds = () => rows.filter((row) => view({ ...configured[row.id], ...draft[row.id] }).useDefault === true).map((row) => row.id);
591
803
  const save = (key, path, label, value, onRefused) => {
592
- const pending = pendingWrites.get(key);
593
- if (pending !== void 0) clearTimeout(pending.timer);
594
- const timer = setTimeout(() => {
595
- pendingWrites.delete(key);
596
- void writeConfig(ctx, [{ op: "set", path, value }]).then(() => {
597
- note(`\u5DF2\u4FDD\u5B58 ${label} = ${JSON.stringify(value)}`);
804
+ const field2 = path[path.length - 1];
805
+ const secret = field2 === "password" && typeof value === "string" ? value : void 0;
806
+ const safeLabel = secret === void 0 ? label : `${label}\uFF08\u503C\u5DF2\u9690\u85CF\uFF09`;
807
+ scheduleWrite(
808
+ key,
809
+ () => writeConfig(ctx, [{ op: "set", path, value }]).then(() => {
810
+ setSaveError(void 0);
811
+ setSavedNote(secret === void 0 ? `\u5DF2\u4FDD\u5B58 ${label}` : `\u5DF2\u4FDD\u5B58 ${safeLabel}`);
598
812
  }).catch((error) => {
599
- note(`\u4FDD\u5B58\u5931\u8D25 ${label}\uFF1A${error?.message ?? error}`);
813
+ const message = redactSensitiveText(String(error?.message ?? error), [
814
+ ...secret === void 0 ? [] : [secret]
815
+ ]);
816
+ setSavedNote(void 0);
817
+ setSaveError({ label: safeLabel, message });
600
818
  onRefused();
601
- });
602
- }, WRITE_DELAY_MS);
603
- pendingWrites.set(key, { timer });
819
+ })
820
+ );
604
821
  };
605
822
  const rollbackRouteField = (id, field2) => {
606
823
  setDraft((current) => {
@@ -616,6 +833,7 @@ function renderPage(ctx) {
616
833
  };
617
834
  const bind = (id, field2) => (value) => {
618
835
  edit(id, field2, value);
836
+ if (TEST_RELEVANT_FIELDS.has(field2)) invalidateTests([id]);
619
837
  save(
620
838
  `${id}\0${field2}`,
621
839
  ["providers", id, field2],
@@ -626,6 +844,7 @@ function renderPage(ctx) {
626
844
  };
627
845
  const bindDefault = (field2) => (value) => {
628
846
  editDefault(field2, value);
847
+ invalidateTests(inheritingIds());
629
848
  save(`\0default\0${field2}`, ["defaultProxy", field2], `defaultProxy.${field2}`, value, () => {
630
849
  setDefaultDraft((current) => {
631
850
  if (current[field2] === void 0) return current;
@@ -635,19 +854,34 @@ function renderPage(ctx) {
635
854
  });
636
855
  });
637
856
  };
857
+ const bindGlobal = (field2) => (value) => {
858
+ editGlobal(field2, value);
859
+ invalidateTests(rows.map((row) => row.id));
860
+ save(`\0global\0${field2}`, [field2], field2, value, () => {
861
+ setGlobalDraft((current) => {
862
+ if (current[field2] === void 0) return current;
863
+ const next = { ...current };
864
+ delete next[field2];
865
+ return next;
866
+ });
867
+ });
868
+ };
638
869
  const saveMany = (key, ops, label, onRefused) => {
639
- const pending = pendingWrites.get(key);
640
- if (pending !== void 0) clearTimeout(pending.timer);
641
- const timer = setTimeout(() => {
642
- pendingWrites.delete(key);
643
- void writeConfig(ctx, ops).then(() => {
644
- note(`\u5DF2\u4FDD\u5B58 ${label}`);
870
+ const secrets = secretsFromOps(ops);
871
+ const hidesValues = ops.some((op) => op?.path?.[op.path.length - 1] === "password");
872
+ const safeLabel = hidesValues ? `${label}\uFF08\u503C\u5DF2\u9690\u85CF\uFF09` : label;
873
+ scheduleWrite(
874
+ key,
875
+ () => writeConfig(ctx, ops).then(() => {
876
+ setSaveError(void 0);
877
+ setSavedNote(`\u5DF2\u4FDD\u5B58 ${safeLabel}`);
645
878
  }).catch((error) => {
646
- note(`\u4FDD\u5B58\u5931\u8D25 ${label}\uFF1A${error?.message ?? error}`);
879
+ const message = redactSensitiveText(String(error?.message ?? error), secrets);
880
+ setSavedNote(void 0);
881
+ setSaveError({ label: safeLabel, message });
647
882
  onRefused();
648
- });
649
- }, WRITE_DELAY_MS);
650
- pendingWrites.set(key, { timer });
883
+ })
884
+ );
651
885
  };
652
886
  const rollbackRouteFields = (id, fields) => {
653
887
  setDraft((current) => {
@@ -663,6 +897,7 @@ function renderPage(ctx) {
663
897
  };
664
898
  const applyCardUrl = (id, parts) => {
665
899
  setDraft((current) => ({ ...current, [id]: { ...current[id], ...parts } }));
900
+ invalidateTests([id]);
666
901
  const fields = Object.keys(parts);
667
902
  saveMany(
668
903
  `${id}\0*paste`,
@@ -673,6 +908,7 @@ function renderPage(ctx) {
673
908
  };
674
909
  const applyDefaultUrl = (parts) => {
675
910
  setDefaultDraft((current) => ({ ...current, ...parts }));
911
+ invalidateTests(inheritingIds());
676
912
  const fields = Object.keys(parts);
677
913
  saveMany(
678
914
  "\0default\0*paste",
@@ -705,6 +941,7 @@ function renderPage(ctx) {
705
941
  for (const id of ids) next[id] = { ...next[id], useDefault: true };
706
942
  return next;
707
943
  });
944
+ invalidateTests(ids);
708
945
  saveMany(
709
946
  "\0default\0applyAll",
710
947
  ids.map((id) => ({ op: "set", path: ["providers", id, "useDefault"], value: true })),
@@ -754,7 +991,8 @@ function renderPage(ctx) {
754
991
  const storedDefault = snapshot.value?.defaultProxy ?? {};
755
992
  const defaultView = view({ ...storedDefault, ...defaultDraft });
756
993
  const defaultWhy = defaultProxyProblem(defaultView);
757
- const defaultUsable = defaultWhy === void 0;
994
+ const defaultHostWhy = defaultView.host.trim().length === 0 ? void 0 : proxyHostProblem(defaultView.host);
995
+ const defaultComplete = defaultWhy === void 0;
758
996
  const defaultOrigin = `${defaultView.protocol}://${defaultView.host}:${defaultView.port}`;
759
997
  const defaultBlock = h(
760
998
  "div",
@@ -770,10 +1008,10 @@ function renderPage(ctx) {
770
1008
  h(
771
1009
  "span",
772
1010
  {
773
- className: defaultUsable ? import_client.default.pillOn : import_client.default.pillOff,
774
- style: { ...S.pill, ...defaultUsable ? S.pillOn : S.pillOff }
1011
+ className: defaultComplete ? import_client.default.pillOn : import_client.default.pillOff,
1012
+ style: { ...S.pill, ...defaultComplete ? S.pillOn : S.pillOff }
775
1013
  },
776
- defaultUsable ? t.on : t.off
1014
+ defaultComplete ? t.complete : t.notConfigured
777
1015
  )
778
1016
  ),
779
1017
  h("p", { className: import_client.default.defaultLead, style: S.defaultLead }, t.defaultLead),
@@ -803,7 +1041,8 @@ function renderPage(ctx) {
803
1041
  // Same convenience as the cards: a full URL fills the whole block.
804
1042
  onPaste: onProxyUrlPaste(applyDefaultUrl)
805
1043
  }),
806
- t.hostHint
1044
+ defaultHostWhy ?? t.hostHint,
1045
+ defaultHostWhy !== void 0
807
1046
  ),
808
1047
  field(
809
1048
  t.port,
@@ -844,7 +1083,7 @@ function renderPage(ctx) {
844
1083
  t.passwordHintProxy
845
1084
  ),
846
1085
  // Bulk opt-in, in ONE write: a per-card walk would rebuild the table N times.
847
- // Disabled while the default is unusable, because pointing every card at an
1086
+ // Disabled while the default configuration is incomplete, because pointing every card at an
848
1087
  // empty endpoint would just refuse them all.
849
1088
  h(
850
1089
  "div",
@@ -855,13 +1094,82 @@ function renderPage(ctx) {
855
1094
  className: import_client.default.testButton,
856
1095
  style: S.testButton,
857
1096
  type: "button",
858
- disabled: !defaultUsable || rows.length === 0,
1097
+ disabled: !defaultComplete || rows.length === 0,
859
1098
  onClick: applyDefaultToAll
860
1099
  },
861
1100
  t.applyDefaultAll
862
1101
  )
863
1102
  )
864
1103
  );
1104
+ const storedTestUrl = typeof snapshot.value?.testUrl === "string" ? snapshot.value.testUrl : DEFAULT_TEST_URL;
1105
+ const storedLocalDirect = snapshot.value?.localDirect !== false;
1106
+ const globalTestUrl = typeof globalDraft.testUrl === "string" ? globalDraft.testUrl : storedTestUrl;
1107
+ const globalLocalDirect = typeof globalDraft.localDirect === "boolean" ? globalDraft.localDirect : storedLocalDirect;
1108
+ const globalTestUrlHint = testUrlHint(globalTestUrl, t);
1109
+ const globalBlock = h(
1110
+ "details",
1111
+ {
1112
+ className: import_client.default.diag,
1113
+ style: {
1114
+ ...S.diag,
1115
+ marginTop: "0"
1116
+ },
1117
+ key: "global-settings"
1118
+ },
1119
+ h("summary", { className: import_client.default.diagSummary }, t.globalTitle),
1120
+ h("p", { className: import_client.default.defaultLead, style: S.defaultLead }, t.globalLead),
1121
+ h(
1122
+ "label",
1123
+ { className: import_client.default.field, style: S.field },
1124
+ h("span", { className: import_client.default.label, style: S.label }, t.testUrl),
1125
+ h("input", {
1126
+ className: import_client.default.input,
1127
+ style: S.input,
1128
+ value: globalTestUrl,
1129
+ onChange: (event) => bindGlobal("testUrl")(event.target.value)
1130
+ }),
1131
+ globalTestUrlHint === void 0 ? h("span", { style: S.hint }, t.testUrlHint) : h(
1132
+ "span",
1133
+ {
1134
+ className: import_client.default.hint,
1135
+ style: {
1136
+ ...S.hint,
1137
+ color: "var(--dsw-alias-state-error-primary, #f85149)"
1138
+ }
1139
+ },
1140
+ globalTestUrlHint
1141
+ )
1142
+ ),
1143
+ h(
1144
+ "div",
1145
+ { style: S.testRow },
1146
+ h(
1147
+ "button",
1148
+ {
1149
+ className: import_client.default.testButton,
1150
+ style: S.testButton,
1151
+ type: "button",
1152
+ onClick: () => bindGlobal("testUrl")(DEFAULT_TEST_URL)
1153
+ },
1154
+ t.restoreDefaultTestUrl
1155
+ )
1156
+ ),
1157
+ h(
1158
+ "label",
1159
+ { className: import_client.default.toggle, style: S.toggle },
1160
+ h("input", {
1161
+ type: "checkbox",
1162
+ checked: globalLocalDirect,
1163
+ onChange: (event) => bindGlobal("localDirect")(event.target.checked === true)
1164
+ }),
1165
+ h("span", null, t.localDirect)
1166
+ ),
1167
+ h(
1168
+ "p",
1169
+ { style: S.hint },
1170
+ globalLocalDirect ? t.localDirectOnHint : t.localDirectOffHint
1171
+ )
1172
+ );
865
1173
  const query = filter.trim().toLowerCase();
866
1174
  const shown = query.length === 0 ? rows : rows.filter((row) => `${row.id} ${row.name}`.toLowerCase().includes(query));
867
1175
  const filterRow = rows.length === 0 ? null : field(
@@ -874,6 +1182,12 @@ function renderPage(ctx) {
874
1182
  onChange: (event) => setFilter(event.target.value)
875
1183
  })
876
1184
  );
1185
+ 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;
1186
+ const catalogueNotice = state.error !== void 0 && rows.length > 0 ? h(
1187
+ "p",
1188
+ { key: "catalogue-error", style: S.inheritWarning },
1189
+ `${t.catalogueStale}${state.error}`
1190
+ ) : null;
877
1191
  const body = shown.length === 0 ? [
878
1192
  h(
879
1193
  "p",
@@ -887,10 +1201,11 @@ function renderPage(ctx) {
887
1201
  borderRadius: "8px",
888
1202
  fontSize: "12px",
889
1203
  textAlign: "center",
890
- color: "var(--dsw-alias-state-idle-primary)"
1204
+ color: catalogueStatus?.error === true ? "var(--dsw-alias-state-error-primary, #f85149)" : "var(--dsw-alias-state-idle-primary)",
1205
+ ...catalogueStatus?.error === true ? { fontWeight: "500" } : {}
891
1206
  }
892
1207
  },
893
- query.length > 0 ? t.noMatch : t.empty
1208
+ catalogueStatus?.text ?? (query.length > 0 ? t.noMatch : t.empty)
894
1209
  )
895
1210
  ] : shown.map((row) => {
896
1211
  const route = view({ ...configured[row.id], ...draft[row.id] });
@@ -899,8 +1214,15 @@ function renderPage(ctx) {
899
1214
  const onlyDead = deadRuleEntries(route.onlyHosts);
900
1215
  const bypassDead = deadRuleEntries(route.bypassHosts);
901
1216
  const useDefault = route.useDefault === true;
902
- const endpointStored = route.useDefault ? defaultUsable : route.host.trim().length > 0 && Number.isInteger(route.port) && route.port >= 1 && route.port <= 65535;
1217
+ const endpointStored = route.useDefault ? defaultComplete : route.host.trim().length > 0 && Number.isInteger(route.port) && route.port >= 1 && route.port <= 65535;
903
1218
  const showTest = route.enabled || endpointStored;
1219
+ const problemText = (() => {
1220
+ const recorded = problems[row.id];
1221
+ if (recorded !== void 0) return recorded;
1222
+ if (useDefault) return defaultComplete ? void 0 : `${t.defaultMissing}${defaultWhy}`;
1223
+ return route.enabled ? defaultProxyProblem(route) : void 0;
1224
+ })();
1225
+ const rejected = route.enabled && problemText !== void 0;
904
1226
  const endpointFields = [
905
1227
  field(
906
1228
  t.protocol,
@@ -975,11 +1297,12 @@ function renderPage(ctx) {
975
1297
  route.protocol === "socks5" ? t.passwordHint : t.passwordHintProxy
976
1298
  )
977
1299
  ];
1300
+ 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 };
978
1301
  return h(
979
1302
  "section",
980
1303
  {
981
- className: route.enabled ? import_client.default.cardActive : import_client.default.card,
982
- style: route.enabled ? { ...S.card, ...S.cardActive } : S.card,
1304
+ className: rejected ? import_client.default.cardRejected : route.enabled ? import_client.default.cardActive : import_client.default.card,
1305
+ style: rejected ? { ...S.card, ...S.cardRejected } : route.enabled ? { ...S.card, ...S.cardActive } : S.card,
983
1306
  key: row.id
984
1307
  },
985
1308
  h(
@@ -1010,17 +1333,7 @@ function renderPage(ctx) {
1010
1333
  lastText
1011
1334
  )
1012
1335
  ),
1013
- h(
1014
- "span",
1015
- {
1016
- className: route.enabled ? import_client.default.pillOn : row.live ? import_client.default.pillLive : import_client.default.pillOff,
1017
- style: {
1018
- ...S.pill,
1019
- ...route.enabled ? S.pillOn : row.live ? S.pillLive : S.pillOff
1020
- }
1021
- },
1022
- route.enabled ? t.on : row.live ? t.live : t.off
1023
- )
1336
+ h("span", { className: pill.className, style: pill.style }, pill.text)
1024
1337
  ),
1025
1338
  h(
1026
1339
  "label",
@@ -1046,16 +1359,12 @@ function renderPage(ctx) {
1046
1359
  }),
1047
1360
  h("span", null, t.useDefault)
1048
1361
  ),
1049
- ...useDefault ? [
1050
- h(
1051
- "p",
1052
- {
1053
- key: `${row.id}:inherit`,
1054
- style: defaultUsable ? S.inheritNote : S.inheritWarning
1055
- },
1056
- defaultUsable ? `${t.inheritingDefault}${defaultOrigin}` : `${t.defaultMissing}${defaultWhy}`
1057
- )
1058
- ] : endpointFields,
1362
+ problemText !== void 0 ? h("p", { key: `${row.id}:problem`, style: S.inheritWarning }, problemText) : useDefault ? h(
1363
+ "p",
1364
+ { key: `${row.id}:inherit`, style: S.inheritNote },
1365
+ `${t.inheritingDefault}${defaultOrigin}`
1366
+ ) : null,
1367
+ ...useDefault ? [] : endpointFields,
1059
1368
  // The self-check is offered whenever there is something to check: an
1060
1369
  // enabled route (the Host's table carries it, and a rejected one still
1061
1370
  // reports WHY), or a disabled one whose endpoint is already complete —
@@ -1079,19 +1388,56 @@ function renderPage(ctx) {
1079
1388
  result?.pending === true ? t.testRunning : t.testButton
1080
1389
  ),
1081
1390
  result?.pending === false ? h(
1082
- "span",
1391
+ "div",
1083
1392
  { style: S.testResult },
1084
1393
  result.ok === true ? [
1085
- // Only the latency carries the band colour; the exit IP
1086
- // stays in the secondary label colour.
1394
+ // The band colour belongs to the LATENCY alone. The
1395
+ // status sits beside it in the plain label colour:
1396
+ // colouring the whole line made a successful check
1397
+ // over 1500 ms read as a failure.
1087
1398
  h(
1088
1399
  "span",
1089
1400
  { key: "lat", style: { color: latencyColor(result.ms) } },
1090
1401
  `\u2713 ${typeof result.ms === "number" ? result.ms : "\u2014"} ms`
1091
1402
  ),
1092
- result.ip === void 0 ? null : ` \xB7 ${t.exitIpLabel}${result.ip}`,
1093
- result.direct === true ? ` \xB7 ${t.directLabel}` : null
1094
- ] : h("span", { key: "err", style: S.testFailure }, `\u2717 ${result.error}`)
1403
+ typeof result.status === "number" ? h(
1404
+ "span",
1405
+ { key: "status" },
1406
+ `HTTP ${result.status}`
1407
+ ) : null,
1408
+ result.ip === void 0 ? null : h("span", { key: "ip" }, `${t.exitIpLabel}${result.ip}`),
1409
+ result.direct === true ? h("span", { key: "direct" }, t.directLabel) : null,
1410
+ result.cached === true ? h("span", { key: "cached" }, t.cachedLabel) : null,
1411
+ result.target === void 0 ? null : h(
1412
+ "span",
1413
+ {
1414
+ key: "target",
1415
+ style: {
1416
+ flexBasis: "100%",
1417
+ fontSize: "11px",
1418
+ color: "var(--dsw-alias-label-secondary)",
1419
+ wordBreak: "break-all"
1420
+ }
1421
+ },
1422
+ `${t.testTargetLabel}${result.target}${targetSourceSuffix(result, t)}`
1423
+ )
1424
+ ] : [
1425
+ h("span", { key: "err", style: S.testFailure }, `\u2717 ${result.error}`),
1426
+ typeof result.status === "number" ? h("span", { key: "status" }, `HTTP ${result.status}`) : null,
1427
+ result.target === void 0 ? null : h(
1428
+ "span",
1429
+ {
1430
+ key: "target",
1431
+ style: {
1432
+ flexBasis: "100%",
1433
+ fontSize: "11px",
1434
+ color: "var(--dsw-alias-label-secondary)",
1435
+ wordBreak: "break-all"
1436
+ }
1437
+ },
1438
+ `${t.testTargetLabel}${result.target}${targetSourceSuffix(result, t)}`
1439
+ )
1440
+ ]
1095
1441
  ) : null,
1096
1442
  // Promote this card's endpoint to the shared default. Offered only
1097
1443
  // when the card carries its OWN endpoint: an inheriting card would
@@ -1157,7 +1503,41 @@ function renderPage(ctx) {
1157
1503
  "div",
1158
1504
  { className: import_client.default.root, style: S.root },
1159
1505
  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),
1506
+ saveError === void 0 ? null : h(
1507
+ "div",
1508
+ {
1509
+ role: "alert",
1510
+ key: "save-error",
1511
+ style: {
1512
+ display: "flex",
1513
+ alignItems: "flex-start",
1514
+ justifyContent: "space-between",
1515
+ gap: "10px",
1516
+ padding: "8px 10px",
1517
+ border: "1px solid var(--dsw-alias-state-error-primary, #f85149)",
1518
+ borderRadius: "8px",
1519
+ background: "var(--dsw-alias-bg-layer-1)",
1520
+ color: "var(--dsw-alias-state-error-primary, #f85149)",
1521
+ fontSize: "12px",
1522
+ fontWeight: "500",
1523
+ wordBreak: "break-all"
1524
+ }
1525
+ },
1526
+ h("span", null, `${t.saveFailedPrefix}${saveError.label} \u2014 ${saveError.message}`),
1527
+ h(
1528
+ "button",
1529
+ {
1530
+ className: import_client.default.testButton,
1531
+ style: { ...S.testButton, flex: "none" },
1532
+ type: "button",
1533
+ onClick: () => setSaveError(void 0)
1534
+ },
1535
+ t.close
1536
+ )
1537
+ ),
1160
1538
  defaultBlock,
1539
+ globalBlock,
1540
+ catalogueNotice,
1161
1541
  filterRow,
1162
1542
  ...body,
1163
1543
  h(
@@ -1185,8 +1565,9 @@ function renderPage(ctx) {
1185
1565
  h(
1186
1566
  "ul",
1187
1567
  { className: import_client.default.diagList, style: { margin: "6px 0 0", paddingLeft: "18px" } },
1188
- ...problemCount === 0 && diagnostics.length === 0 ? [h("li", { key: "none" }, t.diagPending)] : [
1568
+ ...problemCount === 0 && diagnostics.length === 0 && savedNote === void 0 ? [h("li", { key: "none" }, t.diagPending)] : [
1189
1569
  ...problemLines,
1570
+ ...savedNote === void 0 ? [] : [h("li", { key: "saved" }, savedNote)],
1190
1571
  ...diagnostics.map((line, index) => h("li", { key: `d${index}` }, line))
1191
1572
  ]
1192
1573
  )
@@ -1194,14 +1575,17 @@ function renderPage(ctx) {
1194
1575
  );
1195
1576
  }
1196
1577
  var EN = {
1197
- lead: "Route selected model providers through your own forward proxy. Other providers stay direct. Requests to loopback or private addresses (localhost, 127.x, private ranges) always go direct \u2014 a forward proxy cannot reach your own machine.",
1578
+ 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.",
1198
1579
  empty: "No providers found yet.",
1580
+ catalogueLoading: "Loading providers\u2026",
1581
+ catalogueError: "Could not load the provider list: ",
1582
+ catalogueStale: "Could not load the provider list; showing the saved configuration: ",
1199
1583
  useProxy: "Use a proxy for this provider",
1200
1584
  useDefault: "Use the default proxy",
1201
1585
  defaultTitle: "Default proxy",
1202
- defaultLead: 'One endpoint, reused by every provider that turns on "Use the default proxy".',
1586
+ 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.',
1203
1587
  inheritingDefault: "Using the default proxy: ",
1204
- defaultMissing: "The default proxy cannot be used, so this provider is not proxied: ",
1588
+ defaultMissing: "The default proxy configuration is incomplete, so this provider is not proxied: ",
1205
1589
  filterLabel: "Filter",
1206
1590
  filterPlaceholder: "Filter by name or id\u2026",
1207
1591
  noMatch: "No provider matches the filter.",
@@ -1212,7 +1596,7 @@ var EN = {
1212
1596
  port: "Port",
1213
1597
  username: "Username",
1214
1598
  password: "Password",
1215
- hostHint: "Host or IP only, without scheme or path. A full proxy URL (socks5://user:pass@host:1080) can be pasted instead \u2014 it is split into the fields.",
1599
+ 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.",
1216
1600
  passwordHint: "Sent to the SOCKS5 proxy for authentication; stored with this setting.",
1217
1601
  passwordHintProxy: "Sent to the proxy as an authentication password; stored with this setting.",
1218
1602
  hitCountPrefix: "",
@@ -1231,30 +1615,53 @@ var EN = {
1231
1615
  testFailed: "Test failed",
1232
1616
  exitIpLabel: "exit IP ",
1233
1617
  directLabel: "direct (not through the proxy)",
1618
+ cachedLabel: "cached",
1619
+ testTargetLabel: "Target: ",
1620
+ targetSourceRoute: " (this card's own target)",
1621
+ targetSourceUpstream: " (this provider's real upstream)",
1622
+ targetSourceTestUrl: " (global self-check URL)",
1623
+ globalTitle: "Global settings (advanced)",
1624
+ globalLead: "These fields are saved with the plugin rather than per provider. Leave testUrl empty to use the built-in default; comma/space separated candidates are tried in order and only the first 5 are used. localDirect is on by default; turn it off only when the proxy itself is a loopback or private target.",
1625
+ testUrl: "Self-check URL",
1626
+ testUrlHint: "Comma/space separated; http(s) URLs in the order they should be tried.",
1627
+ testUrlEmptyHint: "Empty means the Host uses its built-in default URL.",
1628
+ testUrlInvalidHint: "Some candidates are not absolute http(s) URLs. They are saved, but those checks will fail.",
1629
+ testUrlTooManyHint: "More than 5 candidates are listed; only the first 5 take effect.",
1630
+ restoreDefaultTestUrl: "Restore default",
1631
+ localDirect: "Loopback / private targets go direct",
1632
+ localDirectOnHint: "On: localhost, 127.x and private ranges bypass the proxy.",
1633
+ localDirectOffHint: "Off: loopback/private targets also use the proxy. Enable only when the proxy itself is the target.",
1234
1634
  advanced: "Advanced: target host rules",
1235
1635
  onlyHosts: "Proxy only",
1236
1636
  onlyHostsHint: "Comma/space separated; when set, only requests to these hosts are proxied (suffix match, * for all).",
1237
1637
  bypassHosts: "Bypass",
1238
1638
  bypassHostsHint: "Comma/space separated; requests to these hosts go direct, taking priority.",
1239
- ruleDeadHint: "These entries never match \u2014 rules take host names only, without scheme or path: ",
1639
+ ruleDeadHint: "These entries never match \u2014 use bare host names (no port, scheme or path); a wildcard must be `*` or start with `*.`: ",
1240
1640
  testTarget: "Self-check target",
1241
1641
  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.",
1242
1642
  socks5Hint: "SOCKS5 tunnels the connection; the proxy resolves the target host name.",
1243
1643
  on: "proxied",
1244
- live: "active",
1644
+ rejected: "config rejected",
1245
1645
  off: "direct",
1646
+ complete: "complete",
1647
+ notConfigured: "not configured",
1246
1648
  diagnostics: "Connection diagnostics",
1247
- diagPending: "(collecting\u2026)"
1649
+ diagPending: "(collecting\u2026)",
1650
+ saveFailedPrefix: "Save failed: ",
1651
+ close: "Close"
1248
1652
  };
1249
1653
  var ZH = {
1250
- lead: "\u8BA9\u6307\u5B9A\u7684 provider \u8D70\u4F60\u81EA\u5DF1\u7684\u6B63\u5411\u4EE3\u7406\uFF0C\u5176\u5B83 provider \u4FDD\u6301\u76F4\u8FDE\u3002\u6307\u5411\u672C\u673A\u6216\u5185\u7F51\u5730\u5740\uFF08localhost\u3001127.x\u3001\u79C1\u6709\u7F51\u6BB5\uFF09\u7684\u8BF7\u6C42\u59CB\u7EC8\u76F4\u8FDE\u2014\u2014\u6B63\u5411\u4EE3\u7406\u591F\u4E0D\u5230\u4F60\u81EA\u5DF1\u7684\u673A\u5668\u3002",
1654
+ 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",
1251
1655
  empty: "\u6CA1\u6709\u68C0\u6D4B\u5230\u4EFB\u4F55 provider\u3002",
1656
+ catalogueLoading: "\u6B63\u5728\u8BFB\u53D6 provider \u5217\u8868\u2026",
1657
+ catalogueError: "\u8BFB\u53D6 provider \u5217\u8868\u5931\u8D25\uFF1A",
1658
+ catalogueStale: "provider \u5217\u8868\u8BFB\u53D6\u5931\u8D25\uFF0C\u4E0B\u9762\u663E\u793A\u7684\u662F\u5DF2\u4FDD\u5B58\u7684\u914D\u7F6E\uFF1A",
1252
1659
  useProxy: "\u4E3A\u8BE5 provider \u542F\u7528\u4EE3\u7406",
1253
1660
  useDefault: "\u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406",
1254
1661
  defaultTitle: "\u9ED8\u8BA4\u4EE3\u7406",
1255
- defaultLead: "\u4E3A\u52FE\u9009\u300C\u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406\u300D\u7684 provider \u590D\u7528\u7684\u7AEF\u70B9\uFF0C\u53EA\u586B\u4E00\u6B21\u3002",
1662
+ 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",
1256
1663
  inheritingDefault: "\u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406\uFF1A",
1257
- defaultMissing: "\u9876\u5C42\u9ED8\u8BA4\u4EE3\u7406\u4E0D\u53EF\u7528\uFF0C\u8BE5 provider \u4E0D\u4F1A\u8D70\u4EE3\u7406\uFF1A",
1664
+ defaultMissing: "\u9876\u5C42\u9ED8\u8BA4\u4EE3\u7406\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF0C\u8BE5 provider \u4E0D\u4F1A\u8D70\u4EE3\u7406\uFF1A",
1258
1665
  filterLabel: "\u8FC7\u6EE4",
1259
1666
  filterPlaceholder: "\u6309\u540D\u79F0\u6216 id \u8FC7\u6EE4\u2026",
1260
1667
  noMatch: "\u6CA1\u6709\u5339\u914D\u7684 provider\u3002",
@@ -1265,7 +1672,7 @@ var ZH = {
1265
1672
  port: "\u7AEF\u53E3",
1266
1673
  username: "\u7528\u6237\u540D",
1267
1674
  password: "\u5BC6\u7801",
1268
- hostHint: "\u53EA\u586B\u4E3B\u673A\u540D\u6216 IP\uFF0C\u4E0D\u8981\u5E26\u534F\u8BAE\u524D\u7F00\u6216\u8DEF\u5F84\uFF1B\u4E5F\u53EF\u4EE5\u76F4\u63A5\u7C98\u8D34\u5B8C\u6574\u4EE3\u7406 URL\uFF08\u5982 socks5://user:pass@host:1080\uFF09\uFF0C\u4F1A\u81EA\u52A8\u62C6\u5206\u5230\u5404\u5B57\u6BB5\u3002",
1675
+ 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",
1269
1676
  passwordHint: "\u4EC5\u4F5C\u4E3A SOCKS5 \u8BA4\u8BC1\u53E3\u4EE4\u53D1\u9001\u7ED9\u4EE3\u7406\uFF1B\u968F\u8BE5\u8BBE\u7F6E\u4E00\u8D77\u4FDD\u5B58\u3002",
1270
1677
  passwordHintProxy: "\u4F5C\u4E3A\u4EE3\u7406\u8BA4\u8BC1\u53E3\u4EE4\u53D1\u9001\u7ED9\u4EE3\u7406\uFF1B\u968F\u8BE5\u8BBE\u7F6E\u4E00\u8D77\u4FDD\u5B58\u3002",
1271
1678
  hitCountPrefix: "\u5DF2\u4EE3\u7406 ",
@@ -1284,20 +1691,40 @@ var ZH = {
1284
1691
  testFailed: "\u6D4B\u8BD5\u5931\u8D25",
1285
1692
  exitIpLabel: "\u51FA\u53E3 IP ",
1286
1693
  directLabel: "\u76F4\u8FDE\uFF08\u672A\u7ECF\u8FC7\u4EE3\u7406\uFF09",
1694
+ cachedLabel: "\u7F13\u5B58",
1695
+ testTargetLabel: "\u76EE\u6807 ",
1696
+ targetSourceRoute: "\uFF08\u672C\u5361\u7247\u81EA\u68C0\u76EE\u6807\uFF09",
1697
+ targetSourceUpstream: "\uFF08\u8BE5 provider \u7684\u771F\u5B9E\u4E0A\u6E38\uFF09",
1698
+ targetSourceTestUrl: "\uFF08\u5168\u5C40\u81EA\u68C0\u5730\u5740\uFF09",
1699
+ globalTitle: "\u5168\u5C40\u8BBE\u7F6E\uFF08\u9AD8\u7EA7\uFF09",
1700
+ globalLead: "\u8FD9\u91CC\u7684\u4E24\u9879\u5C5E\u4E8E\u63D2\u4EF6\u5168\u5C40\u8BBE\u7F6E\u3002testUrl \u7559\u7A7A\u65F6 Host \u4F7F\u7528\u5185\u7F6E\u9ED8\u8BA4\u503C\uFF1B\u591A\u4E2A\u5019\u9009\u7528\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694\uFF0C\u6309\u987A\u5E8F\u56DE\u9000\uFF0C\u53EA\u6709\u524D 5 \u4E2A\u751F\u6548\u3002localDirect \u9ED8\u8BA4\u5F00\u542F\uFF1B\u53EA\u6709\u4EE3\u7406\u672C\u8EAB\u5C31\u662F\u672C\u673A\u6216\u5185\u7F51\u76EE\u6807\u65F6\u624D\u9700\u8981\u5173\u95ED\u3002",
1701
+ testUrl: "\u81EA\u68C0\u5730\u5740",
1702
+ testUrlHint: "\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694\uFF1B\u6309\u987A\u5E8F\u5C1D\u8BD5\u7684 http(s) URL\u3002",
1703
+ testUrlEmptyHint: "\u7559\u7A7A\u65F6 Host \u4F7F\u7528\u5185\u7F6E\u9ED8\u8BA4\u5730\u5740\u3002",
1704
+ testUrlInvalidHint: "\u90E8\u5206\u5019\u9009\u4E0D\u662F\u7EDD\u5BF9\u7684 http(s) URL\u3002\u4ECD\u4F1A\u4FDD\u5B58\uFF0C\u4F46\u8FD9\u4E9B\u5019\u9009\u4F1A\u6D4B\u8BD5\u5931\u8D25\u3002",
1705
+ testUrlTooManyHint: "\u5019\u9009\u8D85\u8FC7 5 \u4E2A\uFF0C\u53EA\u6709\u524D 5 \u4E2A\u751F\u6548\u3002",
1706
+ restoreDefaultTestUrl: "\u6062\u590D\u9ED8\u8BA4",
1707
+ localDirect: "\u672C\u673A / \u5185\u7F51\u76EE\u6807\u76F4\u8FDE",
1708
+ localDirectOnHint: "\u5F00\u542F\uFF1Alocalhost\u3001127.x \u4E0E\u79C1\u6709\u7F51\u6BB5\u7ED5\u8FC7\u4EE3\u7406\u3002",
1709
+ localDirectOffHint: "\u5173\u95ED\uFF1A\u672C\u673A / \u5185\u7F51\u76EE\u6807\u4E5F\u8D70\u4EE3\u7406\uFF1B\u4EC5\u5728\u4EE3\u7406\u672C\u8EAB\u5C31\u662F\u76EE\u6807\u65F6\u4F7F\u7528\u3002",
1287
1710
  advanced: "\u9AD8\u7EA7\uFF1A\u76EE\u6807\u57DF\u540D\u89C4\u5219",
1288
1711
  onlyHosts: "\u4EC5\u8D70\u4EE3\u7406",
1289
1712
  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",
1290
1713
  bypassHosts: "\u76F4\u8FDE",
1291
1714
  bypassHostsHint: "\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694\uFF1B\u5339\u914D\u8FD9\u4E9B\u4E3B\u673A\u7684\u8BF7\u6C42\u76F4\u8FDE\uFF0C\u4F18\u5148\u4E8E\u4E0A\u4E00\u9879\u3002",
1292
- ruleDeadHint: "\u4EE5\u4E0B\u6761\u76EE\u6C38\u8FDC\u5339\u914D\u4E0D\u4E0A\u2014\u2014\u89C4\u5219\u53EA\u8BA4\u4E3B\u673A\u540D\uFF0C\u4E0D\u8981\u5E26\u534F\u8BAE\u524D\u7F00\u6216\u8DEF\u5F84\uFF1A",
1715
+ ruleDeadHint: "\u4EE5\u4E0B\u6761\u76EE\u6C38\u8FDC\u5339\u914D\u4E0D\u4E0A\u2014\u2014\u89C4\u5219\u53EA\u8BA4\u4E3B\u673A\u540D\uFF08\u4E0D\u8981\u5E26\u7AEF\u53E3\u3001\u534F\u8BAE\u524D\u7F00\u6216\u8DEF\u5F84\uFF09\uFF0C\u901A\u914D\u7B26\u53EA\u80FD\u662F\u5355\u72EC\u7684 * \u6216\u4EE5 *. \u5F00\u5934\uFF1A",
1293
1716
  testTarget: "\u81EA\u68C0\u76EE\u6807",
1294
1717
  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",
1295
1718
  socks5Hint: "SOCKS5 \u4F1A\u5EFA\u7ACB\u96A7\u9053\uFF0C\u76EE\u6807\u57DF\u540D\u7531\u4EE3\u7406\u7AEF\u89E3\u6790\u3002",
1296
1719
  on: "\u8D70\u4EE3\u7406",
1297
- live: "\u542F\u7528\u4E2D",
1720
+ rejected: "\u914D\u7F6E\u88AB\u62D2",
1298
1721
  off: "\u76F4\u8FDE",
1722
+ complete: "\u914D\u7F6E\u5B8C\u6574",
1723
+ notConfigured: "\u672A\u914D\u7F6E",
1299
1724
  diagnostics: "\u8FDE\u63A5\u8BCA\u65AD",
1300
- diagPending: "\uFF08\u6B63\u5728\u6536\u96C6\u2026\uFF09"
1725
+ diagPending: "\uFF08\u6B63\u5728\u6536\u96C6\u2026\uFF09",
1726
+ saveFailedPrefix: "\u4FDD\u5B58\u5931\u8D25\uFF1A",
1727
+ close: "\u5173\u95ED"
1301
1728
  };
1302
1729
  function stringsFor(ctx) {
1303
1730
  const language = localeOf(ctx);