dsh-provider-proxy 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -5
- package/index.js +5 -5
- package/lib/client.js +240 -21
- package/lib/client.js.map +3 -3
- package/lib/index.js +291 -14
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,8 +38,8 @@ dsh plugin --profile web add dsh-provider-proxy
|
|
|
38
38
|
| 类型 | `HTTP` / `HTTPS` / `SOCKS5` |
|
|
39
39
|
| IP / 主机 | 只填主机名或 IP,不要带 `http://` 或路径 |
|
|
40
40
|
| 端口 | 1–65535 的整数 |
|
|
41
|
-
| 用户名 |
|
|
42
|
-
| 密码 |
|
|
41
|
+
| 用户名 | 可选(与密码成对填写) |
|
|
42
|
+
| 密码 | 可选(与用户名成对填写)。作为代理认证口令发给代理(`HTTP` / `HTTPS` / `SOCKS5` 均支持) |
|
|
43
43
|
|
|
44
44
|
列表只包含**已激活**的 provider(`llm.listProviders()` 能列出的那些)。仅在配置里声明、
|
|
45
45
|
但没有实际注册的可配置 provider 不显示——它的请求根本不会被发出去。
|
|
@@ -47,6 +47,19 @@ dsh plugin --profile web add dsh-provider-proxy
|
|
|
47
47
|
|
|
48
48
|
保存立即对新请求生效,不需要重启;正在进行中的流会继续用它启动时的那条连接。
|
|
49
49
|
|
|
50
|
+
每张卡片在**该 provider 已启用代理**时还有一个 **测试** 按钮:点它会让 Host 通过**该 provider 自己的代理**发一次请求,回报延迟与(探测目标返回时)出口 IP。探测目标默认是 `https://api.ipify.org?format=json`,可用顶层设置 `testUrl` 换成自建的回显服务。代理不可达时它会明确失败,而不是假通过。延迟数值按快慢着色——≤500 ms 绿、≤1500 ms 黄、更慢红——但数值始终照常显示,颜色只是辅助。
|
|
51
|
+
|
|
52
|
+
卡片里还有一个折叠的 **高级:目标域名规则**,按**目标主机**决定是否走代理:
|
|
53
|
+
|
|
54
|
+
| 字段 | 说明 |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `仅走代理`(`onlyHosts`) | 逗号或空格分隔。非空时,只有匹配这些主机的请求走代理,其余直连。 |
|
|
57
|
+
| `直连`(`bypassHosts`) | 逗号或空格分隔。匹配这些主机的请求直连,优先于 `onlyHosts`。 |
|
|
58
|
+
|
|
59
|
+
两者都按后缀匹配(`example.com` 覆盖 `api.example.com`),`*` 表示全部,大小写不敏感。**默认是 fail-closed**:启用了代理的路由默认走代理,只有「`bypassHosts` 命中」与「`onlyHosts` 非空且未命中」两种显式例外才直连;解析不出目标主机时**仍走代理**,避免畸形 URL 把带凭据的请求悄悄直连出去。
|
|
60
|
+
|
|
61
|
+
当组成里存在 web 服务时,列表还会多出一张 **Web 搜索** 卡片(保留 id `web-search`,字段与其它卡片完全相同,也支持自检、计数与域名规则)。它承载 `web_search` 工具的流量;不启用它,搜索保持直连。
|
|
62
|
+
|
|
50
63
|
> **为什么是独立设置页,而不是内嵌在 Models 的 provider 卡片里?**
|
|
51
64
|
> 本插件最早按卡片内嵌(`settings.models.provider-card`)实现,但那个槽位是 keyed 的、
|
|
52
65
|
> 按 provider 命名空间派发,第三方注册在 0.2.0-rc.1 上始终匹配不上——不报错、不发请求、
|
|
@@ -76,14 +89,17 @@ dsh plugin --profile web add dsh-provider-proxy
|
|
|
76
89
|
## 已知限制
|
|
77
90
|
|
|
78
91
|
- **SOCKS5 由 `undici` 内置的 `Socks5ProxyAgent` 实现**(RFC 1928 / RFC 1929),API 稳定性标记为 *Experimental*;`undici` 若在未来移除,需要同步调整。
|
|
79
|
-
-
|
|
92
|
+
- **代理认证字段成对填写:用户名与密码要么都填,要么都不填。** 只填任一项都会被拒绝并给出提示。SOCKS5 侧的依据是 `Socks5ProxyAgent` 仅在两者齐备时才发起认证——只填用户名会静默广告无认证,只填密码则密码根本不会被发送(对开放代理能用,对要求认证的代理必然失败,且失败原因看起来像密码错误);HTTP/HTTPS 侧基本认证本可只发用户名(`alice:`,空密码),这里仍按成对拒绝:半填的凭据几乎总是漏填,三种传输共用一条规则比「哪种传输能承载哪种组合」更好记。代价如实写明——凭据真的使用空密码的代理无法在这里配置。不合规则的配置会被拒绝,原因列在设置页的「连接诊断」面板里(该路由的「测试」也会返回同一条),而不是被静默忽略——否则在强制认证的代理端,报错会指向代理而真正原因是设置缺项。
|
|
80
93
|
- **SOCKS5 不支持 `socks://` 别名与 SOCKS5 over TLS**(`proxyTls`)。
|
|
94
|
+
- **`bypassHosts` 命中时由本机解析并连接目标**(DNS 不经过代理)。SOCKS5 下这一点后果最明显:命中的域名不再由代理端解析,本机 DNS 会暴露该次查询。
|
|
95
|
+
- **`web_search` 可以走代理,`web_fetch` 不行。** 搜索 provider(`dsh-web-search-deepseek`)用全局 `fetch`,插件通过包裹 `ctx.web.search` 把它送进代理作用域;这条成立的前提是:工具每次调用都重新取 `ctx.web.search`,且 provider 的 `search()` 是 await-to-completion(不是惰性消费的流)。`web_fetch` 则用 `undici` 自带的 `dispatcher`(来自官方的进程级 env 策略),没有 per-provider 注入点,本插件覆盖不到——需要覆盖它请改用 `HTTP_PROXY` 之类的环境变量。
|
|
96
|
+
- **保留 id `web-search`。** 它的路由存在 `providers` 字典里;真实 provider 若恰好撞名,会与该路由共用同一份配置,无害。
|
|
81
97
|
- **上游若绕过 `globalThis.fetch` 自行建连,则不受影响。** `llm-deepseek` 确认是调用时解析全局 `fetch`;`llm-pi-ai` 走外部 SDK,抓取 `fetch` 的时机未能在源码层面确证。
|
|
82
98
|
- **不支持 PAC**,也不读取系统代理设置。
|
|
83
99
|
- **不改写 `baseURL`** —— 反向代理/中转请直接在 provider 上配置。
|
|
84
100
|
- **代理不可达时**请求照常失败,DSH 既有的重试与错误码语义不变。
|
|
85
|
-
-
|
|
86
|
-
-
|
|
101
|
+
- **连接自检依赖 `testUrl` 可达。** 默认的回显主机在部分网络下可能不可达,会让自检误报失败;请通过 `testUrl` 换成自建回显服务。自检只接受 `POST`,同一 provider 的多次点击会共享同一次在飞请求,不会被当成外呼放大器。
|
|
102
|
+
- **代理密码以明文随设置写入 `cordis.patch.yml`。** HTTP/HTTPS 的密码会拼进交给 `undici` 的代理 URL(`ProxyAgent` 由此生成 `Proxy-Authorization`),SOCKS5 的密码只作为选项传给 `Socks5ProxyAgent`;两者都不会出现在日志或诊断里的代理 URL——那里只显示不含凭据的 origin。
|
|
87
103
|
|
|
88
104
|
## 如何验证代理确实生效
|
|
89
105
|
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Loader compatibility shim.
|
|
2
|
-
//
|
|
3
|
-
// cordis-plugin-loader resolves an entry as "<package>/index.js" by convention
|
|
4
|
-
// and does NOT read package.json `main`/`exports`, so a package installed into
|
|
5
|
-
// a profile must expose this file. The implementation lives in ./lib/index.js.
|
|
1
|
+
// Loader compatibility shim.
|
|
2
|
+
//
|
|
3
|
+
// cordis-plugin-loader resolves an entry as "<package>/index.js" by convention
|
|
4
|
+
// and does NOT read package.json `main`/`exports`, so a package installed into
|
|
5
|
+
// a profile must expose this file. The implementation lives in ./lib/index.js.
|
|
6
6
|
export * from './lib/index.js'
|
package/lib/client.js
CHANGED
|
@@ -39,13 +39,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
39
39
|
var require_client = __commonJS({
|
|
40
40
|
"src/client.css"(exports, module2) {
|
|
41
41
|
"use strict";
|
|
42
|
-
var __dshMap = { "root": "
|
|
42
|
+
var __dshMap = { "root": "6d45ff27_root", "lead": "57c30143_lead", "empty": "b6600f2d_empty", "card": "0ba90fef_card", "cardActive": "923630a9_cardActive", "cardHead": "5af7e7c3_cardHead", "cardTitles": "8448919c_cardTitles", "name": "0b023e00_name", "id": "ffa6a1c5_id", "pillOn": "85972c09_pillOn", "pillLive": "d3644cdc_pillLive", "pillOff": "592bd7b2_pillOff", "toggle": "9efc135e_toggle", "field": "93a03683_field", "label": "0bd1d3ec_label", "hint": "52d56786_hint", "input": "f6748c5e_input", "testButton": "eb3b08bd_testButton", "diag": "4d4b11ef_diag", "diagSummary": "2e565ce0_diagSummary", "diagList": "fb9c7553_diagList" };
|
|
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.
|
|
48
|
+
__dshStyle.textContent = "/* dsh-provider-proxy \u2014 provider proxy settings page.\n Tokens only: no literal colours, matching the shared --dsw-* system. */\n\n.6d45ff27_root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n max-width: 640px;\n}\n\n.57c30143_lead {\n margin: 0 0 4px;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\n}\n\n.b6600f2d_empty {\n margin: 0;\n padding: 16px;\n border: 1px dashed var(--dsw-alias-border-l1);\n border-radius: 8px;\n font-size: 12px;\n text-align: center;\n color: var(--dsw-alias-state-idle-primary);\n}\n\n/* ---- provider card ---- */\n\n.0ba90fef_card,\n.923630a9_cardActive {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.923630a9_cardActive {\n border-color: var(--dsw-alias-state-success-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.5af7e7c3_cardHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.8448919c_cardTitles {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.0b023e00_name {\n overflow: hidden;\n font-size: 13px;\n font-weight: 600;\n white-space: nowrap;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.ffa6a1c5_id {\n flex: none;\n padding: 1px 6px;\n border-radius: 4px;\n background: var(--dsw-alias-bg-layer-2);\n font-family: ui-monospace, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* ---- status pill ---- */\n\n.85972c09_pillOn,\n.d3644cdc_pillLive,\n.592bd7b2_pillOff {\n flex: none;\n padding: 2px 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 500;\n white-space: nowrap;\n}\n\n.85972c09_pillOn {\n background: var(--dsw-alias-state-success-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.d3644cdc_pillLive {\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-secondary);\n}\n\n.592bd7b2_pillOff {\n color: var(--dsw-alias-state-idle-primary);\n}\n\n/* ---- toggle ---- */\n\n.9efc135e_toggle {\n display: flex;\n align-items: center;\n gap: 8px;\n padding-top: 2px;\n font-size: 12px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n}\n\n.9efc135e_toggle input {\n accent-color: var(--dsw-alias-brand-primary);\n cursor: pointer;\n}\n\n/* ---- fields ---- */\n\n.93a03683_field {\n display: grid;\n grid-template-columns: 96px 1fr;\n align-items: center;\n gap: 10px;\n font-size: 12px;\n}\n\n.0bd1d3ec_label {\n color: var(--dsw-alias-label-secondary);\n}\n\n.52d56786_hint {\n grid-column: 2;\n margin-top: -2px;\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.f6748c5e_input {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n padding: 5px 8px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n outline: none;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n/* The base look of every input is INLINE too, so these three state rules only\n ever take effect as author-important declarations \u2014 same reason as the\n self-check button above. `cursor` has no inline base and needs none, and the\n hover border uses `l3` because `l1`\u2192`l2` is a 4%\u219210% black step nobody can\n see. */\n\n.f6748c5e_input:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n}\n\n.f6748c5e_input:focus {\n border-color: var(--dsw-alias-brand-primary) !important;\n}\n\n.f6748c5e_input:disabled {\n color: var(--dsw-alias-state-idle-primary) !important;\n background: var(--dsw-alias-bg-layer-2) !important;\n cursor: not-allowed;\n}\n\n/* ---- self-check ---- */\n\n/* The hover state is the one thing an inline style cannot express, which is why\n this element carries a class at all. Layout stays inline.\n\n Two traps, both hit before this comment existed:\n - `!important` is load-bearing: the base border and background are INLINE, and\n the cascade sorts a style attribute after every author rule, so a plain\n declaration here would ship, match, and still lose.\n - the TOKENS must be visible ones. `bg-layer-1/2/3` all resolve to\n `neutral-bluish-00` in the light theme, so a layer swap is a no-op there, and\n `border-l1`\u2192`l2` is 4%\u219210% black on a 1px line. The app's own hovers use\n `interactive-bg-hover`; using it here is what makes the feedback exist. */\n.eb3b08bd_testButton {\n transition: border-color 120ms ease, background 120ms ease;\n}\n\n.eb3b08bd_testButton:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3) !important;\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\n/* ---- diagnostics ---- */\n\n.4d4b11ef_diag {\n margin-top: 2px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.2e565ce0_diagSummary {\n cursor: pointer;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.fb9c7553_diagList {\n margin: 6px 0 0;\n padding-left: 18px;\n font-family: ui-monospace, monospace;\n word-break: break-all;\n}\n";
|
|
49
49
|
document.head.appendChild(__dshStyle);
|
|
50
50
|
}
|
|
51
51
|
var __dshStyle;
|
|
@@ -159,18 +159,52 @@ function knownProviders(configured, published) {
|
|
|
159
159
|
return [...rows.values()].sort(byName);
|
|
160
160
|
}
|
|
161
161
|
var CATALOG_PATH = "/dsh-provider-proxy/providers";
|
|
162
|
+
var TEST_PATH = "/dsh-provider-proxy/test";
|
|
163
|
+
var LATENCY_FAST_MS = 500;
|
|
164
|
+
var LATENCY_SLOW_MS = 1500;
|
|
165
|
+
function latencyColor(ms) {
|
|
166
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return "var(--dsw-alias-label-secondary)";
|
|
167
|
+
if (ms <= LATENCY_FAST_MS) return "var(--dsw-alias-state-success-primary, #2ea043)";
|
|
168
|
+
if (ms <= LATENCY_SLOW_MS) return "var(--dsw-alias-state-warn-primary, #d29922)";
|
|
169
|
+
return "var(--dsw-alias-state-error-primary, #f85149)";
|
|
170
|
+
}
|
|
171
|
+
function testResultFrom(body, strings) {
|
|
172
|
+
const ok = body?.ok === true;
|
|
173
|
+
return {
|
|
174
|
+
pending: false,
|
|
175
|
+
ok,
|
|
176
|
+
ms: typeof body?.ms === "number" && Number.isFinite(body.ms) ? body.ms : void 0,
|
|
177
|
+
ip: typeof body?.exitIp === "string" && body.exitIp.length > 0 ? body.exitIp : void 0,
|
|
178
|
+
error: ok ? void 0 : typeof body?.error === "string" && body.error.length > 0 ? body.error : strings.testFailed
|
|
179
|
+
};
|
|
180
|
+
}
|
|
162
181
|
var catalogueSetter = () => {
|
|
163
182
|
};
|
|
164
|
-
async function
|
|
183
|
+
async function fetchCatalog() {
|
|
165
184
|
const response = await globalThis.fetch(CATALOG_PATH, { headers: { accept: "application/json" } });
|
|
166
185
|
if (!response.ok) throw new Error(`Host answered ${response.status}`);
|
|
167
186
|
const body = await response.json();
|
|
168
187
|
const list = Array.isArray(body?.providers) ? body.providers : [];
|
|
169
|
-
|
|
188
|
+
const rows = list.filter((row) => typeof row?.id === "string" && row.id.length > 0).map((row) => ({
|
|
170
189
|
id: row.id,
|
|
171
190
|
name: typeof row.name === "string" && row.name.length > 0 ? row.name : row.id,
|
|
172
191
|
live: row.live === true
|
|
173
192
|
}));
|
|
193
|
+
const hits = {};
|
|
194
|
+
const published = body?.hits;
|
|
195
|
+
if (published !== null && typeof published === "object") {
|
|
196
|
+
for (const [id, count] of Object.entries(published)) {
|
|
197
|
+
if (typeof count === "number" && Number.isFinite(count)) hits[id] = count;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const problems = {};
|
|
201
|
+
const rejected = body?.problems;
|
|
202
|
+
if (rejected !== null && typeof rejected === "object") {
|
|
203
|
+
for (const [id, message] of Object.entries(rejected)) {
|
|
204
|
+
if (typeof message === "string" && message.length > 0) problems[id] = message;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { rows, hits, problems };
|
|
174
208
|
}
|
|
175
209
|
var byName = (a, b) => a.name.localeCompare(b.name);
|
|
176
210
|
function view(route) {
|
|
@@ -180,7 +214,9 @@ function view(route) {
|
|
|
180
214
|
host: route?.host ?? "",
|
|
181
215
|
port: route?.port ?? 0,
|
|
182
216
|
username: route?.username ?? "",
|
|
183
|
-
password: route?.password ?? ""
|
|
217
|
+
password: route?.password ?? "",
|
|
218
|
+
onlyHosts: route?.onlyHosts ?? "",
|
|
219
|
+
bypassHosts: route?.bypassHosts ?? ""
|
|
184
220
|
};
|
|
185
221
|
}
|
|
186
222
|
var diagnostics = [];
|
|
@@ -232,6 +268,55 @@ var S = {
|
|
|
232
268
|
fontSize: "11px",
|
|
233
269
|
color: "var(--dsw-alias-label-secondary)"
|
|
234
270
|
},
|
|
271
|
+
hits: {
|
|
272
|
+
flex: "none",
|
|
273
|
+
fontSize: "11px",
|
|
274
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
275
|
+
whiteSpace: "nowrap"
|
|
276
|
+
},
|
|
277
|
+
testRow: {
|
|
278
|
+
display: "flex",
|
|
279
|
+
alignItems: "center",
|
|
280
|
+
gap: "10px",
|
|
281
|
+
flexWrap: "wrap"
|
|
282
|
+
},
|
|
283
|
+
testButton: {
|
|
284
|
+
padding: "5px 12px",
|
|
285
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
286
|
+
borderRadius: "6px",
|
|
287
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
288
|
+
color: "var(--dsw-alias-label-primary)",
|
|
289
|
+
font: "inherit",
|
|
290
|
+
fontSize: "12px",
|
|
291
|
+
cursor: "pointer"
|
|
292
|
+
},
|
|
293
|
+
testResult: {
|
|
294
|
+
fontSize: "11px",
|
|
295
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
296
|
+
wordBreak: "break-all"
|
|
297
|
+
},
|
|
298
|
+
/**
|
|
299
|
+
* A failed check is the answer, not decoration: it takes the error colour and
|
|
300
|
+
* a heavier weight, unlike the latency band colour which only tints a number
|
|
301
|
+
* that is printed anyway.
|
|
302
|
+
*/
|
|
303
|
+
testFailure: {
|
|
304
|
+
color: "var(--dsw-alias-state-error-primary, #f85149)",
|
|
305
|
+
fontWeight: "500"
|
|
306
|
+
},
|
|
307
|
+
/** A rejected route is a failure of the settings, not a warning about them. */
|
|
308
|
+
diagProblem: {
|
|
309
|
+
color: "var(--dsw-alias-state-error-primary, #f85149)"
|
|
310
|
+
},
|
|
311
|
+
advanced: {
|
|
312
|
+
marginTop: "4px",
|
|
313
|
+
fontSize: "12px"
|
|
314
|
+
},
|
|
315
|
+
advancedSummary: {
|
|
316
|
+
cursor: "pointer",
|
|
317
|
+
fontSize: "11px",
|
|
318
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
319
|
+
},
|
|
235
320
|
pill: {
|
|
236
321
|
flex: "none",
|
|
237
322
|
padding: "2px 10px",
|
|
@@ -296,20 +381,52 @@ function renderPage(ctx) {
|
|
|
296
381
|
);
|
|
297
382
|
const [state, setState] = (React.useState ?? ((fn) => [fn(), () => {
|
|
298
383
|
}]))(() => {
|
|
299
|
-
void
|
|
384
|
+
void fetchCatalog().then((catalog) => catalogueSetter?.(catalog)).catch((error) => {
|
|
300
385
|
note(`provider \u5217\u8868\u83B7\u53D6\u5931\u8D25\uFF1A${error?.message ?? error}`);
|
|
301
|
-
catalogueSetter?.([]);
|
|
386
|
+
catalogueSetter?.({ rows: [], hits: {}, problems: {} });
|
|
302
387
|
});
|
|
303
|
-
return {
|
|
388
|
+
return {
|
|
389
|
+
rows: void 0,
|
|
390
|
+
hits: {},
|
|
391
|
+
problems: {},
|
|
392
|
+
pending: true
|
|
393
|
+
};
|
|
304
394
|
});
|
|
305
|
-
catalogueSetter = (
|
|
395
|
+
catalogueSetter = (catalog) => setState({ rows: catalog.rows, hits: catalog.hits, problems: catalog.problems, pending: false });
|
|
306
396
|
const published = state.rows ?? [];
|
|
397
|
+
const hits = state.hits ?? {};
|
|
398
|
+
const problems = state.problems ?? {};
|
|
399
|
+
const problemLines = Object.entries(problems).map(
|
|
400
|
+
([id, message]) => h("li", { key: `p:${id}`, style: S.diagProblem }, `${id}\uFF1A${message}`)
|
|
401
|
+
);
|
|
402
|
+
const problemCount = problemLines.length;
|
|
307
403
|
note(
|
|
308
404
|
`provider \u5217\u8868\uFF1A\u6765\u81EA Host \u5171 ${published.length} \u4E2A\uFF08${published.map((r) => r.id).join("\u3001") || "\u65E0"}\uFF09`
|
|
309
405
|
);
|
|
310
406
|
const [draft, setDraft] = (React.useState ?? ((v) => [v, () => {
|
|
311
407
|
}]))({});
|
|
312
408
|
const edit = (id, field2, value) => setDraft((current) => ({ ...current, [id]: { ...current[id], [field2]: value } }));
|
|
409
|
+
const [tests, setTests] = (React.useState ?? ((v) => [v, () => {
|
|
410
|
+
}]))({});
|
|
411
|
+
const runTest = (id) => {
|
|
412
|
+
setTests((current) => ({ ...current, [id]: { pending: true } }));
|
|
413
|
+
void (async () => {
|
|
414
|
+
try {
|
|
415
|
+
const response = await globalThis.fetch(TEST_PATH, {
|
|
416
|
+
method: "POST",
|
|
417
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
418
|
+
body: JSON.stringify({ provider: id })
|
|
419
|
+
});
|
|
420
|
+
const body2 = await response.json();
|
|
421
|
+
setTests((current) => ({ ...current, [id]: testResultFrom(body2, t) }));
|
|
422
|
+
} catch (error) {
|
|
423
|
+
setTests((current) => ({
|
|
424
|
+
...current,
|
|
425
|
+
[id]: { pending: false, ok: false, error: String(error?.message ?? error) }
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
})();
|
|
429
|
+
};
|
|
313
430
|
const rows = knownProviders({ ...configured, ...draft }, published);
|
|
314
431
|
const save = (id, field2, value) => {
|
|
315
432
|
void writeConfig(ctx, [{ op: "set", path: ["providers", id, field2], value }]).then(() => {
|
|
@@ -359,6 +476,7 @@ function renderPage(ctx) {
|
|
|
359
476
|
] : rows.map((row) => {
|
|
360
477
|
const route = view({ ...configured[row.id], ...draft[row.id] });
|
|
361
478
|
const disabled = !route.enabled;
|
|
479
|
+
const result = tests[row.id];
|
|
362
480
|
return h(
|
|
363
481
|
"section",
|
|
364
482
|
{
|
|
@@ -373,7 +491,14 @@ function renderPage(ctx) {
|
|
|
373
491
|
"div",
|
|
374
492
|
{ className: import_client.default.cardTitles, style: S.titles },
|
|
375
493
|
h("span", { className: import_client.default.name, style: S.name }, row.name),
|
|
376
|
-
h("span", { className: import_client.default.id, style: S.id }, row.id)
|
|
494
|
+
h("span", { className: import_client.default.id, style: S.id }, row.id),
|
|
495
|
+
// A disabled route can never have hits, so "0" there would only add
|
|
496
|
+
// noise; the count shows once the route is on (or has counted).
|
|
497
|
+
route.enabled || (hits[row.id] ?? 0) > 0 ? h(
|
|
498
|
+
"span",
|
|
499
|
+
{ style: S.hits },
|
|
500
|
+
`${t.hitCountPrefix}${hits[row.id] ?? 0}${t.hitCountSuffix}`
|
|
501
|
+
) : null
|
|
377
502
|
),
|
|
378
503
|
h(
|
|
379
504
|
"span",
|
|
@@ -454,13 +579,12 @@ function renderPage(ctx) {
|
|
|
454
579
|
onChange: (event) => bind(row.id, "username")(event.target.value)
|
|
455
580
|
})
|
|
456
581
|
),
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
// settings file with the rest of the
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
route.protocol === "socks5" ? field(
|
|
582
|
+
// Every transport accepts a password now: SOCKS5 hands it to
|
|
583
|
+
// Socks5ProxyAgent as an option, HTTP/HTTPS embed it in the proxy URL
|
|
584
|
+
// userinfo (undici turns that into Proxy-Authorization). It is stored on
|
|
585
|
+
// the route, so it lands in the settings file with the rest of the
|
|
586
|
+
// config; the diagnostics only ever surface a credential-free origin.
|
|
587
|
+
field(
|
|
464
588
|
t.password,
|
|
465
589
|
h("input", {
|
|
466
590
|
className: import_client.default.input,
|
|
@@ -471,8 +595,69 @@ function renderPage(ctx) {
|
|
|
471
595
|
value: route.password,
|
|
472
596
|
onChange: (event) => bind(row.id, "password")(event.target.value)
|
|
473
597
|
}),
|
|
474
|
-
t.passwordHint
|
|
475
|
-
)
|
|
598
|
+
route.protocol === "socks5" ? t.passwordHint : t.passwordHintProxy
|
|
599
|
+
),
|
|
600
|
+
// The self-check only makes sense for a route that is ON; a disabled
|
|
601
|
+
// route is not in the Host's table, so there is nothing to test.
|
|
602
|
+
route.enabled ? h(
|
|
603
|
+
"div",
|
|
604
|
+
{ style: S.testRow, key: `${row.id}:test` },
|
|
605
|
+
h(
|
|
606
|
+
"button",
|
|
607
|
+
{
|
|
608
|
+
className: import_client.default.testButton,
|
|
609
|
+
style: S.testButton,
|
|
610
|
+
type: "button",
|
|
611
|
+
disabled: result?.pending === true,
|
|
612
|
+
onClick: () => runTest(row.id)
|
|
613
|
+
},
|
|
614
|
+
result?.pending === true ? t.testRunning : t.testButton
|
|
615
|
+
),
|
|
616
|
+
result?.pending === false ? h(
|
|
617
|
+
"span",
|
|
618
|
+
{ style: S.testResult },
|
|
619
|
+
result.ok === true ? [
|
|
620
|
+
// Only the latency carries the band colour; the exit IP
|
|
621
|
+
// stays in the secondary label colour.
|
|
622
|
+
h(
|
|
623
|
+
"span",
|
|
624
|
+
{ key: "lat", style: { color: latencyColor(result.ms) } },
|
|
625
|
+
`\u2713 ${typeof result.ms === "number" ? result.ms : "\u2014"} ms`
|
|
626
|
+
),
|
|
627
|
+
result.ip === void 0 ? null : ` \xB7 ${t.exitIpLabel}${result.ip}`
|
|
628
|
+
] : h("span", { key: "err", style: S.testFailure }, `\u2717 ${result.error}`)
|
|
629
|
+
) : null
|
|
630
|
+
) : null,
|
|
631
|
+
// Target-host rules, collapsed so the common case stays one short card.
|
|
632
|
+
h(
|
|
633
|
+
"details",
|
|
634
|
+
{ style: S.advanced, key: `${row.id}:rules` },
|
|
635
|
+
h("summary", { style: S.advancedSummary }, t.advanced),
|
|
636
|
+
field(
|
|
637
|
+
t.onlyHosts,
|
|
638
|
+
h("input", {
|
|
639
|
+
className: import_client.default.input,
|
|
640
|
+
style: S.input,
|
|
641
|
+
disabled,
|
|
642
|
+
placeholder: "api.example.com",
|
|
643
|
+
value: route.onlyHosts,
|
|
644
|
+
onChange: (event) => bind(row.id, "onlyHosts")(event.target.value)
|
|
645
|
+
}),
|
|
646
|
+
t.onlyHostsHint
|
|
647
|
+
),
|
|
648
|
+
field(
|
|
649
|
+
t.bypassHosts,
|
|
650
|
+
h("input", {
|
|
651
|
+
className: import_client.default.input,
|
|
652
|
+
style: S.input,
|
|
653
|
+
disabled,
|
|
654
|
+
placeholder: "localhost",
|
|
655
|
+
value: route.bypassHosts,
|
|
656
|
+
onChange: (event) => bind(row.id, "bypassHosts")(event.target.value)
|
|
657
|
+
}),
|
|
658
|
+
t.bypassHostsHint
|
|
659
|
+
)
|
|
660
|
+
)
|
|
476
661
|
);
|
|
477
662
|
});
|
|
478
663
|
return h(
|
|
@@ -494,11 +679,21 @@ function renderPage(ctx) {
|
|
|
494
679
|
color: "var(--dsw-alias-label-secondary)"
|
|
495
680
|
}
|
|
496
681
|
},
|
|
497
|
-
|
|
682
|
+
// The count is the only visible signal while the panel is collapsed: a
|
|
683
|
+
// rejected route is not a page that refuses to render, it is one that
|
|
684
|
+
// quietly does nothing.
|
|
685
|
+
h(
|
|
686
|
+
"summary",
|
|
687
|
+
{ className: import_client.default.diagSummary },
|
|
688
|
+
problemCount === 0 ? t.diagnostics : `${t.diagnostics} (${problemCount})`
|
|
689
|
+
),
|
|
498
690
|
h(
|
|
499
691
|
"ul",
|
|
500
692
|
{ className: import_client.default.diagList, style: { margin: "6px 0 0", paddingLeft: "18px" } },
|
|
501
|
-
...diagnostics.length === 0 ? [h("li", { key: "none" }, t.diagPending)] :
|
|
693
|
+
...problemCount === 0 && diagnostics.length === 0 ? [h("li", { key: "none" }, t.diagPending)] : [
|
|
694
|
+
...problemLines,
|
|
695
|
+
...diagnostics.map((line, index) => h("li", { key: `d${index}` }, line))
|
|
696
|
+
]
|
|
502
697
|
)
|
|
503
698
|
)
|
|
504
699
|
);
|
|
@@ -514,6 +709,18 @@ var EN = {
|
|
|
514
709
|
password: "Password",
|
|
515
710
|
hostHint: "Host or IP only, without scheme or path.",
|
|
516
711
|
passwordHint: "Sent to the SOCKS5 proxy for authentication; stored with this setting.",
|
|
712
|
+
passwordHintProxy: "Sent to the proxy as an authentication password; stored with this setting.",
|
|
713
|
+
hitCountPrefix: "",
|
|
714
|
+
hitCountSuffix: " proxied",
|
|
715
|
+
testButton: "Test",
|
|
716
|
+
testRunning: "Testing\u2026",
|
|
717
|
+
testFailed: "Test failed",
|
|
718
|
+
exitIpLabel: "exit IP ",
|
|
719
|
+
advanced: "Advanced: target host rules",
|
|
720
|
+
onlyHosts: "Proxy only",
|
|
721
|
+
onlyHostsHint: "Comma/space separated; when set, only requests to these hosts are proxied (suffix match, * for all).",
|
|
722
|
+
bypassHosts: "Bypass",
|
|
723
|
+
bypassHostsHint: "Comma/space separated; requests to these hosts go direct, taking priority.",
|
|
517
724
|
socks5Hint: "SOCKS5 tunnels the connection; the proxy resolves the target host name.",
|
|
518
725
|
on: "proxied",
|
|
519
726
|
live: "active",
|
|
@@ -532,6 +739,18 @@ var ZH = {
|
|
|
532
739
|
password: "\u5BC6\u7801",
|
|
533
740
|
hostHint: "\u53EA\u586B\u4E3B\u673A\u540D\u6216 IP\uFF0C\u4E0D\u8981\u5E26\u534F\u8BAE\u524D\u7F00\u6216\u8DEF\u5F84\u3002",
|
|
534
741
|
passwordHint: "\u4EC5\u4F5C\u4E3A SOCKS5 \u8BA4\u8BC1\u53E3\u4EE4\u53D1\u9001\u7ED9\u4EE3\u7406\uFF1B\u968F\u8BE5\u8BBE\u7F6E\u4E00\u8D77\u4FDD\u5B58\u3002",
|
|
742
|
+
passwordHintProxy: "\u4F5C\u4E3A\u4EE3\u7406\u8BA4\u8BC1\u53E3\u4EE4\u53D1\u9001\u7ED9\u4EE3\u7406\uFF1B\u968F\u8BE5\u8BBE\u7F6E\u4E00\u8D77\u4FDD\u5B58\u3002",
|
|
743
|
+
hitCountPrefix: "\u5DF2\u4EE3\u7406 ",
|
|
744
|
+
hitCountSuffix: " \u6B21",
|
|
745
|
+
testButton: "\u6D4B\u8BD5",
|
|
746
|
+
testRunning: "\u6D4B\u8BD5\u4E2D\u2026",
|
|
747
|
+
testFailed: "\u6D4B\u8BD5\u5931\u8D25",
|
|
748
|
+
exitIpLabel: "\u51FA\u53E3 IP ",
|
|
749
|
+
advanced: "\u9AD8\u7EA7\uFF1A\u76EE\u6807\u57DF\u540D\u89C4\u5219",
|
|
750
|
+
onlyHosts: "\u4EC5\u8D70\u4EE3\u7406",
|
|
751
|
+
onlyHostsHint: "\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694\uFF1B\u975E\u7A7A\u65F6\u53EA\u6709\u5339\u914D\u8FD9\u4E9B\u4E3B\u673A\u7684\u8BF7\u6C42\u8D70\u4EE3\u7406\uFF08\u540E\u7F00\u5339\u914D\uFF0C* \u8868\u793A\u5168\u90E8\uFF09\u3002",
|
|
752
|
+
bypassHosts: "\u76F4\u8FDE",
|
|
753
|
+
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",
|
|
535
754
|
socks5Hint: "SOCKS5 \u4F1A\u5EFA\u7ACB\u96A7\u9053\uFF0C\u76EE\u6807\u57DF\u540D\u7531\u4EE3\u7406\u7AEF\u89E3\u6790\u3002",
|
|
536
755
|
on: "\u8D70\u4EE3\u7406",
|
|
537
756
|
live: "\u542F\u7528\u4E2D",
|