dsh-provider-proxy 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,7 @@ dsh plugin --profile web add dsh-provider-proxy
40
40
  | IP / 主机 | 只填 IPv4、IPv6 或 ASCII 域名;不要带协议前缀、端口、路径或凭据。IDN 需先转 punycode |
41
41
  | 端口 | 1–65535 的整数 |
42
42
  | 用户名 | 可选(与密码成对填写) |
43
- | 密码 | 可选(与用户名成对填写)。作为代理认证口令发给代理(`HTTP` / `HTTPS` / `SOCKS5` 均支持) |
43
+ | 密码 | 可选(与用户名成对填写)。作为代理认证口令发给代理(`HTTP` / `HTTPS` / `SOCKS5` 均支持)。默认隐藏,可用密码框内右侧的眼睛图标临时查看,切换不会改写或保存密码 |
44
44
 
45
45
  同一个代理要给多张卡片用时,只在页面顶部的 **默认代理** 区块填一次,再在每张卡片勾上 **使用默认代理**。
46
46
  勾上之后该卡片的传输字段(类型 / 主机 / 端口 / 用户名 / 密码)**整体**取自默认区块,卡片不再显示这些输入框,
@@ -69,7 +69,9 @@ Host catalog 是否列出某个 provider(`live`)不影响已保存路由的
69
69
  协议 / 主机 / 端口 / 用户名 / 密码,并且是**一次**写入(不是五个字段各写一次)。显式写出的 `:80` / `:443`
70
70
  会保留;完全没写端口时保持「未配置」,不会自动补默认端口。
71
71
  - **设为默认代理**(卡片上,仅在该卡片有自己的端点时出现):把它当前的端点一次性提升为顶部的默认端点。
72
- - **所有 provider 使用默认代理**(默认区块里):一条批量写把每张卡片都指向默认端点;默认端点不可用时按钮禁用。
72
+ 如果默认端点已配置且有卡片正在继承,会先弹确认框;取消后不写入、不改草稿。
73
+ - **所有 provider 使用默认代理(N)**(默认区块里):N 是全部 provider 的数量,不受过滤框影响。
74
+ 一条批量写把每张卡片都指向默认端点;默认端点不可用时按钮禁用。
73
75
  - **最近一次调用**:卡片头上会显示「最近成功 3 分钟前」,失败时显示「最近失败 ECONNREFUSED」。
74
76
  命中计数只说明这条路由被用过,这一行说明它**现在是否还工作**。已启用但还没代理过请求的路由显示
75
77
  「尚无代理记录」;**测试按钮不计入**——它证明的是「代理这一跳通」,不是「这条路由承载过真实流量」,两者混在一起会让这一行的含义不可信。
@@ -77,6 +79,11 @@ Host catalog 是否列出某个 provider(`live`)不影响已保存路由的
77
79
  因为成功判据在响应头到达时就已成立(流式转发不能靠消费 body 来判断)。
78
80
  「最近调用」的时间取请求**发起**时刻;「最近成功 / 失败」的结果与时间来自最近一次**结束**的请求——
79
81
  两者分开记录,并发调用下不会把一次请求的发起时间配到另一次的结果上。
82
+ **这条记录只属于当前路由配置**:改代理端点、凭据、`useDefault` 的默认端点、目标域名规则或
83
+ `localDirect` 后,该 provider 的「最近调用」会变成「尚无代理记录」;`testUrl` / `testTarget`
84
+ 只影响测试,不会清掉它。仅关闭再打开代理开关也保留记录。修改配置前已经启动的请求不会把旧结果
85
+ 写回新配置;删除后重新添加同名 provider 同样视为一条新路由。`hits` 命中计数是历史累计,
86
+ 上述配置变化都不会清空它。
80
87
 
81
88
  页面顶部与顶层还有三项整页设置(写在 `cordis.patch.yml`,也可以直接编辑):
82
89
 
@@ -95,6 +102,11 @@ Host catalog 是否列出某个 provider(`live`)不影响已保存路由的
95
102
  已经在 `providers` 里配过代理的条目即使暂时不激活也仍然保留,避免已保存的设置悄悄消失;
96
103
  catalog 的 `live` 只影响未配置 provider 是否出现在列表里,不参与已保存卡片的状态判定。
97
104
 
105
+ 如果 Host 目录读取成功,但某个**已保存**的 provider 完全没有出现在目录里,页面不会隐藏它,
106
+ 而是在「连接诊断」里列出这些 provider,并说明代理配置仍会保留。目录读取中或读取失败时不会显示
107
+ 这条提示,避免把所有已保存 provider 误报为缺失。`live: false` 只表示目录中存在但当前未激活,
108
+ 不算缺失。
109
+
98
110
  保存立即对新请求生效,不需要重启;正在进行中的流会继续用它启动时的那条连接。
99
111
 
100
112
  设置写入失败时,字段会回滚,并在页面顶部显示「保存失败:<字段> — <原因>」和关闭按钮;下一次成功写入或点击关闭后消失。多次失败只保留最新一条。写入成功与失败的诊断都不会包含密码值,密码字段固定显示「值已隐藏」。
@@ -194,7 +206,7 @@ catalog 的 `live` 只影响未配置 provider 是否出现在列表里,不参
194
206
 
195
207
  ```sh
196
208
  npm run build # 构建 lib/index.js 与 lib/client.js
197
- npm test # 248 个用例,跑的是构建产物
209
+ npm test # 274 个用例,跑的是构建产物
198
210
  npm run typecheck # 类型检查(仅覆盖 src/)
199
211
  npm run verify # 上面三件事,按顺序
200
212
  ```
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": "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" };
42
+ var __dshMap = { "root": "b4bf55bf_root", "lead": "03b72018_lead", "empty": "9a9a9c27_empty", "card": "6d6907d5_card", "cardActive": "a287416b_cardActive", "cardRejected": "aabaa9b3_cardRejected", "defaultCard": "ebf26189_defaultCard", "defaultLead": "5b9e9eb5_defaultLead", "cardHead": "a3319282_cardHead", "cardTitles": "6f64c28d_cardTitles", "name": "847f7e47_name", "id": "7864474d_id", "pillOn": "7ed5399b_pillOn", "pillLive": "0ccb6da5_pillLive", "pillOff": "3accadc4_pillOff", "pillRejected": "86897991_pillRejected", "toggle": "81025292_toggle", "field": "5e2577de_field", "label": "d9087e9b_label", "hint": "aae2a8a2_hint", "input": "feb8c902_input", "passwordToggle": "5645283d_passwordToggle", "testButton": "de8ca09a_testButton", "diag": "4ad17b9d_diag", "diagSummary": "b2f0759b_diagSummary", "diagList": "f49699bb_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.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';
48
+ __dshStyle.textContent = '/* dsh-provider-proxy \u2014 provider proxy settings page.\n Tokens only: no literal colours, matching the shared --dsw-* system. */\n\n.b4bf55bf_root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n max-width: 640px;\n}\n\n.03b72018_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.9a9a9c27_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.6d6907d5_card,\n.a287416b_cardActive,\n.aabaa9b3_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.a287416b_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.aabaa9b3_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.ebf26189_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.5b9e9eb5_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.a3319282_cardHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.6f64c28d_cardTitles {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.847f7e47_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.7864474d_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.7ed5399b_pillOn,\n.0ccb6da5_pillLive,\n.3accadc4_pillOff,\n.86897991_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.7ed5399b_pillOn {\n background: var(--dsw-alias-state-success-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.0ccb6da5_pillLive {\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-secondary);\n}\n\n.3accadc4_pillOff {\n color: var(--dsw-alias-state-idle-primary);\n}\n\n.86897991_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.81025292_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.81025292_toggle input {\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n/* ---- fields ---- */\n\n.5e2577de_field {\n display: grid;\n grid-template-columns: 96px 1fr;\n align-items: center;\n gap: 10px;\n font-size: 12px;\n}\n\n.d9087e9b_label {\n color: var(--dsw-alias-label-secondary);\n}\n\n.aae2a8a2_hint {\n grid-column: 2;\n margin-top: -2px;\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.feb8c902_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.feb8c902_input:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n}\n\n.feb8c902_input:focus {\n border-color: var(--dsw-alias-brand-primary) !important;\n}\n\n.feb8c902_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/* The icon sits inside the input, so its hover/focus feedback must be local\n and must outrank the transparent inline base. */\n.5645283d_passwordToggle {\n transition: background 120ms ease, color 120ms ease;\n}\n\n.5645283d_passwordToggle:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover) !important;\n color: var(--dsw-alias-label-primary) !important;\n}\n\n.5645283d_passwordToggle:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\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.de8ca09a_testButton {\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n.de8ca09a_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.4ad17b9d_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.b2f0759b_diagSummary {\n cursor: pointer;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.f49699bb_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;
@@ -357,7 +357,7 @@ async function fetchCatalog() {
357
357
  };
358
358
  }
359
359
  }
360
- return { rows, hits, problems, lastUse };
360
+ return { rows, publishedRows: rows, hits, problems, lastUse };
361
361
  }
362
362
  var CATALOG_REFRESH_MS = 1e4;
363
363
  var catalogPoll;
@@ -540,6 +540,29 @@ var S = {
540
540
  fontSize: "12px",
541
541
  cursor: "pointer"
542
542
  },
543
+ /** The reveal/hide control sits inside the password field's right edge. */
544
+ passwordControl: {
545
+ position: "relative",
546
+ minWidth: "0"
547
+ },
548
+ passwordToggle: {
549
+ position: "absolute",
550
+ top: "50%",
551
+ right: "4px",
552
+ display: "flex",
553
+ alignItems: "center",
554
+ justifyContent: "center",
555
+ width: "26px",
556
+ height: "26px",
557
+ padding: "0",
558
+ border: "0",
559
+ borderRadius: "6px",
560
+ background: "transparent",
561
+ color: "var(--dsw-alias-label-secondary)",
562
+ lineHeight: "1",
563
+ transform: "translateY(-50%)",
564
+ cursor: "pointer"
565
+ },
543
566
  testResult: {
544
567
  // A flex row so the target can drop onto its own line: the container was a
545
568
  // plain span, where `flexBasis: 100%` does nothing and the target ran into
@@ -566,6 +589,10 @@ var S = {
566
589
  diagProblem: {
567
590
  color: "var(--dsw-alias-state-error-primary, #f85149)"
568
591
  },
592
+ /** Informational catalogue mismatch: not a rejected route, so no error red. */
593
+ diagNote: {
594
+ color: "var(--dsw-alias-label-secondary)"
595
+ },
569
596
  advanced: {
570
597
  marginTop: "4px",
571
598
  fontSize: "12px"
@@ -693,6 +720,7 @@ function renderPage(ctx) {
693
720
  void fetchCatalog().then((catalog) => catalogueSetter?.(catalog)).catch((error) => {
694
721
  catalogueSetter?.({
695
722
  rows: [],
723
+ publishedRows: [],
696
724
  hits: {},
697
725
  problems: {},
698
726
  lastUse: {},
@@ -701,6 +729,7 @@ function renderPage(ctx) {
701
729
  });
702
730
  return {
703
731
  rows: void 0,
732
+ publishedRows: void 0,
704
733
  hits: {},
705
734
  problems: {},
706
735
  lastUse: {},
@@ -710,6 +739,7 @@ function renderPage(ctx) {
710
739
  });
711
740
  catalogueSetter = (catalog) => setState({
712
741
  rows: catalog.rows,
742
+ publishedRows: catalog.publishedRows,
713
743
  hits: catalog.hits,
714
744
  problems: catalog.problems,
715
745
  lastUse: catalog.lastUse,
@@ -718,6 +748,7 @@ function renderPage(ctx) {
718
748
  });
719
749
  touchCatalogPolling();
720
750
  const published = state.rows ?? [];
751
+ const publishedRows = state.publishedRows ?? [];
721
752
  const hits = state.hits ?? {};
722
753
  const lastUse = state.lastUse ?? {};
723
754
  const lastUseText = (id) => {
@@ -731,7 +762,14 @@ function renderPage(ctx) {
731
762
  const problemLines = Object.entries(problems).map(
732
763
  ([id, message]) => h("li", { key: `p:${id}`, style: S.diagProblem }, `${id}\uFF1A${message}`)
733
764
  );
734
- const problemCount = problemLines.length;
765
+ const publishedIds = new Set(publishedRows.map((row) => row.id));
766
+ const missingFromCatalog = state.pending === true || state.error !== void 0 ? [] : Object.keys(configured).filter((id) => !publishedIds.has(id)).sort();
767
+ const catalogueMismatchLine = missingFromCatalog.length === 0 ? void 0 : h(
768
+ "li",
769
+ { key: "catalogue-mismatch", style: S.diagNote },
770
+ `${t.catalogueMismatch}${missingFromCatalog.join("\u3001")}${t.catalogueMismatchSuffix}`
771
+ );
772
+ const problemCount = problemLines.length + (catalogueMismatchLine === void 0 ? 0 : 1);
735
773
  note(
736
774
  `provider \u5217\u8868\uFF1A\u6765\u81EA Host \u5171 ${published.length} \u4E2A\uFF08${published.map((r) => r.id).join("\u3001") || "\u65E0"}\uFF09`
737
775
  );
@@ -741,6 +779,45 @@ function renderPage(ctx) {
741
779
  const [defaultDraft, setDefaultDraft] = (React.useState ?? ((v) => [v, () => {
742
780
  }]))({});
743
781
  const editDefault = (field2, value) => setDefaultDraft((current) => ({ ...current, [field2]: value }));
782
+ const [passwordVisible, setPasswordVisible] = (React.useState ?? ((v) => [v, () => {
783
+ }]))({});
784
+ const [defaultPasswordVisible, setDefaultPasswordVisible] = (React.useState ?? ((v) => [v, () => {
785
+ }]))(false);
786
+ const togglePassword = (id) => setPasswordVisible((current) => ({ ...current, [id]: current[id] !== true }));
787
+ const toggleDefaultPassword = () => setDefaultPasswordVisible((current) => current !== true);
788
+ const passwordToggle = (visible, onToggle) => h(
789
+ "button",
790
+ {
791
+ className: import_client.default.passwordToggle,
792
+ style: S.passwordToggle,
793
+ type: "button",
794
+ "aria-label": visible ? t.hidePassword : t.showPassword,
795
+ "aria-pressed": visible,
796
+ title: visible ? t.hidePassword : t.showPassword,
797
+ onClick: onToggle
798
+ },
799
+ h(
800
+ "svg",
801
+ {
802
+ width: 16,
803
+ height: 16,
804
+ viewBox: "0 0 24 24",
805
+ fill: "none",
806
+ stroke: "currentColor",
807
+ strokeWidth: 2,
808
+ strokeLinecap: "round",
809
+ strokeLinejoin: "round",
810
+ "aria-hidden": "true",
811
+ focusable: "false"
812
+ },
813
+ h("path", {
814
+ key: "eye",
815
+ d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"
816
+ }),
817
+ h("circle", { key: "pupil", cx: 12, cy: 12, r: 3 }),
818
+ visible ? h("path", { key: "slash", d: "m3 3 18 18" }) : null
819
+ )
820
+ );
744
821
  const [globalDraft, setGlobalDraft] = (React.useState ?? ((v) => [v, () => {
745
822
  }]))({});
746
823
  const editGlobal = (field2, value) => setGlobalDraft((current) => ({ ...current, [field2]: value }));
@@ -924,6 +1001,9 @@ function renderPage(ctx) {
924
1001
  );
925
1002
  };
926
1003
  const promoteToDefault = (id) => {
1004
+ if (defaultComplete && inheritingIds().length > 0 && globalThis.window?.confirm?.(t.confirmPromoteToDefault) !== true) {
1005
+ return;
1006
+ }
927
1007
  const route = view({ ...configured[id], ...draft[id] });
928
1008
  applyDefaultUrl({
929
1009
  protocol: route.protocol,
@@ -945,7 +1025,7 @@ function renderPage(ctx) {
945
1025
  saveMany(
946
1026
  "\0default\0applyAll",
947
1027
  ids.map((id) => ({ op: "set", path: ["providers", id, "useDefault"], value: true })),
948
- "\u6240\u6709 provider \u7684 useDefault",
1028
+ `\u6240\u6709 provider \u7684 useDefault\uFF08${ids.length} \u4E2A\uFF09`,
949
1029
  () => setDraft((current) => {
950
1030
  const next = { ...current };
951
1031
  for (const id of ids) {
@@ -1065,21 +1145,26 @@ function renderPage(ctx) {
1065
1145
  h("input", {
1066
1146
  className: import_client.default.input,
1067
1147
  style: S.input,
1068
- autocomplete: "off",
1148
+ autoComplete: "off",
1069
1149
  value: defaultView.username,
1070
1150
  onChange: (event) => bindDefault("username")(event.target.value)
1071
1151
  })
1072
1152
  ),
1073
1153
  field(
1074
1154
  t.password,
1075
- h("input", {
1076
- className: import_client.default.input,
1077
- style: S.input,
1078
- type: "password",
1079
- autocomplete: "off",
1080
- value: defaultView.password,
1081
- onChange: (event) => bindDefault("password")(event.target.value)
1082
- }),
1155
+ h(
1156
+ "div",
1157
+ { style: S.passwordControl },
1158
+ h("input", {
1159
+ className: import_client.default.input,
1160
+ style: { ...S.input, paddingRight: "36px" },
1161
+ type: defaultPasswordVisible === true ? "text" : "password",
1162
+ autoComplete: "off",
1163
+ value: defaultView.password,
1164
+ onChange: (event) => bindDefault("password")(event.target.value)
1165
+ }),
1166
+ passwordToggle(defaultPasswordVisible === true, toggleDefaultPassword)
1167
+ ),
1083
1168
  t.passwordHintProxy
1084
1169
  ),
1085
1170
  // Bulk opt-in, in ONE write: a per-card walk would rebuild the table N times.
@@ -1097,7 +1182,7 @@ function renderPage(ctx) {
1097
1182
  disabled: !defaultComplete || rows.length === 0,
1098
1183
  onClick: applyDefaultToAll
1099
1184
  },
1100
- t.applyDefaultAll
1185
+ `${t.applyDefaultAll}\uFF08${rows.length}\uFF09`
1101
1186
  )
1102
1187
  )
1103
1188
  );
@@ -1274,7 +1359,7 @@ function renderPage(ctx) {
1274
1359
  h("input", {
1275
1360
  className: import_client.default.input,
1276
1361
  style: S.input,
1277
- autocomplete: "off",
1362
+ autoComplete: "off",
1278
1363
  value: route.username,
1279
1364
  onChange: (event) => bind(row.id, "username")(event.target.value)
1280
1365
  })
@@ -1286,14 +1371,19 @@ function renderPage(ctx) {
1286
1371
  // config; the diagnostics only ever surface a credential-free origin.
1287
1372
  field(
1288
1373
  t.password,
1289
- h("input", {
1290
- className: import_client.default.input,
1291
- style: S.input,
1292
- type: "password",
1293
- autocomplete: "off",
1294
- value: route.password,
1295
- onChange: (event) => bind(row.id, "password")(event.target.value)
1296
- }),
1374
+ h(
1375
+ "div",
1376
+ { style: S.passwordControl },
1377
+ h("input", {
1378
+ className: import_client.default.input,
1379
+ style: { ...S.input, paddingRight: "36px" },
1380
+ type: passwordVisible[row.id] === true ? "text" : "password",
1381
+ autoComplete: "off",
1382
+ value: route.password,
1383
+ onChange: (event) => bind(row.id, "password")(event.target.value)
1384
+ }),
1385
+ passwordToggle(passwordVisible[row.id] === true, () => togglePassword(row.id))
1386
+ ),
1297
1387
  route.protocol === "socks5" ? t.passwordHint : t.passwordHintProxy
1298
1388
  )
1299
1389
  ];
@@ -1567,6 +1657,7 @@ function renderPage(ctx) {
1567
1657
  { className: import_client.default.diagList, style: { margin: "6px 0 0", paddingLeft: "18px" } },
1568
1658
  ...problemCount === 0 && diagnostics.length === 0 && savedNote === void 0 ? [h("li", { key: "none" }, t.diagPending)] : [
1569
1659
  ...problemLines,
1660
+ ...catalogueMismatchLine === void 0 ? [] : [catalogueMismatchLine],
1570
1661
  ...savedNote === void 0 ? [] : [h("li", { key: "saved" }, savedNote)],
1571
1662
  ...diagnostics.map((line, index) => h("li", { key: `d${index}` }, line))
1572
1663
  ]
@@ -1596,6 +1687,9 @@ var EN = {
1596
1687
  port: "Port",
1597
1688
  username: "Username",
1598
1689
  password: "Password",
1690
+ showPassword: "Show",
1691
+ hidePassword: "Hide",
1692
+ confirmPromoteToDefault: "The default proxy is already in use by one or more providers. Replace it with this card's endpoint?",
1599
1693
  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.",
1600
1694
  passwordHint: "Sent to the SOCKS5 proxy for authentication; stored with this setting.",
1601
1695
  passwordHintProxy: "Sent to the proxy as an authentication password; stored with this setting.",
@@ -1647,6 +1741,8 @@ var EN = {
1647
1741
  notConfigured: "not configured",
1648
1742
  diagnostics: "Connection diagnostics",
1649
1743
  diagPending: "(collecting\u2026)",
1744
+ catalogueMismatch: "Saved but not listed by the Host catalogue: ",
1745
+ catalogueMismatchSuffix: ". Their proxy settings are kept.",
1650
1746
  saveFailedPrefix: "Save failed: ",
1651
1747
  close: "Close"
1652
1748
  };
@@ -1672,6 +1768,9 @@ var ZH = {
1672
1768
  port: "\u7AEF\u53E3",
1673
1769
  username: "\u7528\u6237\u540D",
1674
1770
  password: "\u5BC6\u7801",
1771
+ showPassword: "\u663E\u793A",
1772
+ hidePassword: "\u9690\u85CF",
1773
+ confirmPromoteToDefault: "\u5DF2\u6709 provider \u6B63\u5728\u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406\u3002\u8981\u7528\u8FD9\u5F20\u5361\u7247\u7684\u7AEF\u70B9\u8986\u76D6\u5B83\u5417\uFF1F",
1675
1774
  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",
1676
1775
  passwordHint: "\u4EC5\u4F5C\u4E3A SOCKS5 \u8BA4\u8BC1\u53E3\u4EE4\u53D1\u9001\u7ED9\u4EE3\u7406\uFF1B\u968F\u8BE5\u8BBE\u7F6E\u4E00\u8D77\u4FDD\u5B58\u3002",
1677
1776
  passwordHintProxy: "\u4F5C\u4E3A\u4EE3\u7406\u8BA4\u8BC1\u53E3\u4EE4\u53D1\u9001\u7ED9\u4EE3\u7406\uFF1B\u968F\u8BE5\u8BBE\u7F6E\u4E00\u8D77\u4FDD\u5B58\u3002",
@@ -1723,6 +1822,8 @@ var ZH = {
1723
1822
  notConfigured: "\u672A\u914D\u7F6E",
1724
1823
  diagnostics: "\u8FDE\u63A5\u8BCA\u65AD",
1725
1824
  diagPending: "\uFF08\u6B63\u5728\u6536\u96C6\u2026\uFF09",
1825
+ catalogueMismatch: "\u5DF2\u4FDD\u5B58\u4F46\u672A\u51FA\u73B0\u5728 Host \u76EE\u5F55\u4E2D\u7684 provider\uFF1A",
1826
+ catalogueMismatchSuffix: "\u3002\u4EE3\u7406\u914D\u7F6E\u4ECD\u4F1A\u4FDD\u7559\u3002",
1726
1827
  saveFailedPrefix: "\u4FDD\u5B58\u5931\u8D25\uFF1A",
1727
1828
  close: "\u5173\u95ED"
1728
1829
  };