dsh-provider-proxy 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -44,10 +44,18 @@ function installSharedRouter() {
44
44
  return originalFetch.call(globalThis, input, init);
45
45
  }
46
46
  active.onHit?.();
47
- return undiciFetch(input, {
47
+ const inFlight = undiciFetch(input, {
48
48
  ...init,
49
49
  dispatcher: active.dispatcher
50
50
  });
51
+ if (active.onSettle !== void 0) {
52
+ const settle = active.onSettle;
53
+ void inFlight.then(
54
+ (response) => settle(response.status < 500, response),
55
+ (error) => settle(false, error)
56
+ ).catch(() => void 0);
57
+ }
58
+ return inFlight;
51
59
  };
52
60
  Object.defineProperty(globalThis, "fetch", {
53
61
  ...descriptor ?? { configurable: true, enumerable: true, writable: true },
@@ -126,6 +134,42 @@ import { ProxyAgent, Socks5ProxyAgent } from "undici";
126
134
  // src/config.ts
127
135
  import { createRequire } from "node:module";
128
136
 
137
+ // src/proxy-address.ts
138
+ var PROXY_HOST_INVALID = "\u4EE3\u7406\u5730\u5740\u53EA\u80FD\u586B IPv4\u3001IPv6 \u6216\u57DF\u540D";
139
+ 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)$/;
140
+ 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;
141
+ var IPV6_CHARS = /^[0-9a-f:.]+$/i;
142
+ function proxyHostProblem(rawHost) {
143
+ const host = (rawHost ?? "").trim().replace(/\.$/, "");
144
+ if (host.length === 0) return "\u672A\u586B\u5199\u4EE3\u7406\u5730\u5740";
145
+ if (IPV4.test(host)) return void 0;
146
+ if (host.includes(":") || host.startsWith("[")) {
147
+ const literal = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
148
+ if (!IPV6_CHARS.test(literal)) return PROXY_HOST_INVALID;
149
+ try {
150
+ new URL(`http://[${literal}]:1`);
151
+ return void 0;
152
+ } catch {
153
+ return PROXY_HOST_INVALID;
154
+ }
155
+ }
156
+ return DOMAIN.test(host) ? void 0 : PROXY_HOST_INVALID;
157
+ }
158
+ function defaultProxyProblem(endpoint) {
159
+ const hostProblem = proxyHostProblem(endpoint.host);
160
+ if (hostProblem !== void 0) return hostProblem;
161
+ const port = endpoint.port ?? 0;
162
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
163
+ return "\u4EE3\u7406\u7AEF\u53E3\u65E0\u6548\uFF08\u9700\u4E3A 1-65535 \u7684\u6574\u6570\uFF09";
164
+ }
165
+ const hasUser = (endpoint.username ?? "").trim().length > 0;
166
+ const hasPassword = (endpoint.password ?? "").length > 0;
167
+ if (hasUser !== hasPassword) {
168
+ 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";
169
+ }
170
+ return void 0;
171
+ }
172
+
129
173
  // node_modules/@deepseek-ai/schemastery/lib/index.mjs
130
174
  var lib_exports = {};
131
175
  __export(lib_exports, {
@@ -1017,9 +1061,27 @@ function vol(field) {
1017
1061
  return typeof volatile2 === "function" ? volatile2.call(field) : field;
1018
1062
  }
1019
1063
  var PROTOCOLS = ["http", "https", "socks5"];
1064
+ var DefaultProxy = z.object({
1065
+ protocol: z.union(PROTOCOLS).default("http"),
1066
+ /** Proxy host or IP, without scheme, port, or path. */
1067
+ host: z.string().default(""),
1068
+ /** Proxy port. 0 means "not configured". */
1069
+ port: z.number().default(0),
1070
+ username: z.string().default(""),
1071
+ password: z.string().default("")
1072
+ });
1020
1073
  var ProviderProxyRoute = z.object({
1021
1074
  /** Whether this provider's calls are routed through the proxy below. */
1022
1075
  enabled: z.boolean().default(false),
1076
+ /**
1077
+ * When true, this route's TRANSPORT fields (protocol/host/port/username/
1078
+ * password) come from the top-level `defaultProxy` instead of its own, so the
1079
+ * same endpoint is configured once for every provider that shares it. The
1080
+ * target-host rules and the self-check target stay the route's own. There is
1081
+ * no partial inheritance: to use a different endpoint, turn this off and fill
1082
+ * the fields in.
1083
+ */
1084
+ useDefault: z.boolean().default(false),
1023
1085
  /** Proxy transport. `socks5` is accepted but only honoured when the socks helper loaded. */
1024
1086
  protocol: z.union(PROTOCOLS).default("http"),
1025
1087
  /** Proxy host or IP, without scheme, port, or path. */
@@ -1036,11 +1098,31 @@ var ProviderProxyRoute = z.object({
1036
1098
  */
1037
1099
  onlyHosts: z.string().default(""),
1038
1100
  /** Comma/space-separated TARGET hosts that bypass the proxy; takes priority over `onlyHosts`. */
1039
- bypassHosts: z.string().default("")
1101
+ bypassHosts: z.string().default(""),
1102
+ /**
1103
+ * Optional self-check target for THIS provider's 测试 button. Empty falls back
1104
+ * to the top-level `testUrl`. Only the self-check reads it; routing never
1105
+ * touches it, so a bad value costs a failed check, never a broken route.
1106
+ */
1107
+ testTarget: z.string().default("")
1040
1108
  });
1041
1109
  var Config = z.object({
1042
1110
  /** Per-provider proxy routes, keyed by provider route id (e.g. `sensenova`). */
1043
1111
  providers: vol(z.dict(ProviderProxyRoute).default({})),
1112
+ /**
1113
+ * Shared endpoint for every route that sets `useDefault`. Volatile for the
1114
+ * same reason as `providers`: a settings write is committed into the running
1115
+ * fiber instead of remounting the plugin.
1116
+ */
1117
+ defaultProxy: vol(DefaultProxy.default({})),
1118
+ /**
1119
+ * Whether a loopback/private-address target is sent DIRECT instead of through
1120
+ * the proxy. On by default: such a target is normally a relay on this very
1121
+ * machine (LiteLLM, Ollama, a local mock), which a forward proxy cannot reach
1122
+ * at all — forcing it through one fails, and the failure reads as "the proxy
1123
+ * is broken". Turn this off only when the proxy itself is the target.
1124
+ */
1125
+ localDirect: vol(z.boolean().default(true)),
1044
1126
  /**
1045
1127
  * URL the per-provider connection self-check requests THROUGH the proxy.
1046
1128
  * Volatile, so a settings write does not remount the plugin; the default is an
@@ -1048,25 +1130,41 @@ var Config = z.object({
1048
1130
  */
1049
1131
  testUrl: vol(z.string().default("https://api.ipify.org?format=json"))
1050
1132
  });
1051
- function normalizeRoute(provider, raw) {
1052
- const route = ProviderProxyRoute(raw ?? {});
1053
- const value = typeof route.get === "function" ? route.get() : route;
1133
+ function plain(parsed) {
1134
+ const get = parsed.get;
1135
+ return typeof get === "function" ? get.call(parsed) : parsed;
1136
+ }
1137
+ function normalizeDefaultProxy(raw) {
1138
+ const value = plain(DefaultProxy(raw ?? {}));
1139
+ return {
1140
+ protocol: value.protocol,
1141
+ host: value.host.trim(),
1142
+ port: value.port,
1143
+ username: value.username.trim(),
1144
+ password: value.password
1145
+ };
1146
+ }
1147
+ function normalizeRoute(provider, raw, options) {
1148
+ const value = plain(ProviderProxyRoute(raw ?? {}));
1054
1149
  const onlyHosts = value.onlyHosts.trim().toLowerCase();
1055
1150
  const bypassHosts = value.bypassHosts.trim().toLowerCase();
1056
- if (!value.enabled) return { ...value, port: value.port || 0, onlyHosts, bypassHosts };
1057
- const host = value.host.trim();
1058
- if (host.length === 0) {
1059
- throw new TypeError(`dsh-provider-proxy: provider "${provider}" \u542F\u7528\u4E86\u4EE3\u7406\uFF0C\u4F46\u672A\u586B\u5199\u4EE3\u7406\u5730\u5740`);
1151
+ const testTarget = value.testTarget.trim();
1152
+ if (!value.enabled && options?.requireEnabled !== false) {
1153
+ return { ...value, port: value.port || 0, onlyHosts, bypassHosts, testTarget };
1060
1154
  }
1061
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(host) || host.includes("/")) {
1062
- throw new TypeError(`dsh-provider-proxy: provider "${provider}" \u7684\u4EE3\u7406\u5730\u5740\u53EA\u586B\u4E3B\u673A\u540D\u6216 IP\uFF0C\u4E0D\u8981\u5E26\u534F\u8BAE\u524D\u7F00\u6216\u8DEF\u5F84`);
1155
+ const host = value.host.trim();
1156
+ const hostProblem = proxyHostProblem(host);
1157
+ if (hostProblem !== void 0) {
1158
+ throw new TypeError(
1159
+ hostProblem === "\u672A\u586B\u5199\u4EE3\u7406\u5730\u5740" ? `dsh-provider-proxy: provider "${provider}" \u542F\u7528\u4E86\u4EE3\u7406\uFF0C\u4F46\u672A\u586B\u5199\u4EE3\u7406\u5730\u5740` : `dsh-provider-proxy: provider "${provider}" \u7684${hostProblem}`
1160
+ );
1063
1161
  }
1064
1162
  if (!Number.isInteger(value.port) || value.port < 1 || value.port > 65535) {
1065
1163
  throw new TypeError(
1066
1164
  `dsh-provider-proxy: provider "${provider}" \u7684\u4EE3\u7406\u7AEF\u53E3\u65E0\u6548\uFF08\u9700\u4E3A 1-65535 \u7684\u6574\u6570\uFF09`
1067
1165
  );
1068
1166
  }
1069
- return { ...value, host, username: value.username.trim(), onlyHosts, bypassHosts };
1167
+ return { ...value, host, username: value.username.trim(), onlyHosts, bypassHosts, testTarget };
1070
1168
  }
1071
1169
 
1072
1170
  // src/proxy-table.ts
@@ -1099,70 +1197,115 @@ function hostMatches(host, rules) {
1099
1197
  }
1100
1198
  return false;
1101
1199
  }
1102
- function buildDecide(route) {
1200
+ function isLocalHost(host) {
1201
+ const bare = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
1202
+ if (bare === "localhost" || bare.endsWith(".localhost")) return true;
1203
+ if (bare === "::1") return true;
1204
+ const octets = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(bare);
1205
+ if (octets === null) return false;
1206
+ const [a, b, c, d] = octets.slice(1).map(Number);
1207
+ if (a > 255 || b > 255 || c > 255 || d > 255) return false;
1208
+ if (a === 127) return true;
1209
+ if (a === 10) return true;
1210
+ if (a === 192 && b === 168) return true;
1211
+ if (a === 169 && b === 254) return true;
1212
+ if (a === 172 && b >= 16 && b <= 31) return true;
1213
+ return false;
1214
+ }
1215
+ function buildDecide(route, localDirect) {
1103
1216
  const only = parseHostList(route.onlyHosts);
1104
1217
  const bypass = parseHostList(route.bypassHosts);
1105
1218
  return (host) => {
1106
1219
  if (host === void 0) return "proxy";
1220
+ if (localDirect && isLocalHost(host)) return "direct";
1107
1221
  if (hostMatches(host, bypass)) return "direct";
1108
1222
  if (only.length > 0 && !hostMatches(host, only)) return "direct";
1109
1223
  return "proxy";
1110
1224
  };
1111
1225
  }
1112
- function createProxyTable(config) {
1113
- const entries = /* @__PURE__ */ new Map();
1114
- const problems = /* @__PURE__ */ new Map();
1115
- for (const [provider, raw] of Object.entries(config ?? {})) {
1116
- let route;
1117
- try {
1118
- route = normalizeRoute(provider, raw);
1119
- } catch (error) {
1120
- problems.set(provider, error instanceof Error ? error.message : String(error));
1121
- continue;
1122
- }
1123
- if (!route.enabled) continue;
1124
- if (route.protocol === "socks5" && route.username.length > 0 && route.password.length === 0) {
1125
- problems.set(
1126
- provider,
1127
- "dsh-provider-proxy: SOCKS5 \u914D\u7F6E\u4E86\u7528\u6237\u540D\u4F46\u6CA1\u6709\u5BC6\u7801\uFF1B\u8BF7\u8865\u5168\u5BC6\u7801\u6216\u6E05\u7A7A\u7528\u6237\u540D"
1128
- );
1129
- continue;
1130
- }
1131
- if (route.protocol === "socks5" && route.password.length > 0 && route.username.length === 0) {
1132
- problems.set(
1133
- provider,
1134
- "dsh-provider-proxy: SOCKS5 \u914D\u7F6E\u4E86\u5BC6\u7801\u4F46\u6CA1\u6709\u7528\u6237\u540D\uFF1B\u8BF7\u8865\u5168\u7528\u6237\u540D\u6216\u6E05\u7A7A\u5BC6\u7801"
1135
- );
1136
- continue;
1137
- }
1138
- if (route.protocol !== "socks5" && route.password.length > 0 && route.username.length === 0) {
1139
- problems.set(
1140
- provider,
1141
- "dsh-provider-proxy: \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"
1142
- );
1143
- continue;
1144
- }
1145
- if (route.protocol !== "socks5" && route.username.length > 0 && route.password.length === 0) {
1146
- problems.set(
1147
- provider,
1148
- "dsh-provider-proxy: \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"
1149
- );
1150
- continue;
1151
- }
1152
- const url = proxyUrl(route);
1153
- let dispatcher;
1154
- try {
1155
- dispatcher = createDispatcher(route, url, route.password);
1156
- } catch (error) {
1157
- problems.set(provider, error instanceof Error ? error.message : String(error));
1158
- continue;
1226
+ function withDefaultProxy(route, defaultProxy) {
1227
+ return {
1228
+ ...route,
1229
+ protocol: defaultProxy.protocol,
1230
+ host: defaultProxy.host,
1231
+ port: defaultProxy.port,
1232
+ username: defaultProxy.username,
1233
+ password: defaultProxy.password
1234
+ };
1235
+ }
1236
+ function buildRouteEntry(provider, raw, options) {
1237
+ let effective = raw;
1238
+ if (raw?.useDefault === true) {
1239
+ const why = defaultProxyProblem(options.defaultProxy);
1240
+ if (why !== void 0) {
1241
+ return {
1242
+ ok: false,
1243
+ problem: `dsh-provider-proxy: provider "${provider}" \u542F\u7528\u4E86\u300C\u4F7F\u7528\u9ED8\u8BA4\u4EE3\u7406\u300D\uFF0C\u4F46\u9876\u5C42\u9ED8\u8BA4\u4EE3\u7406\u4E0D\u53EF\u7528\uFF1A${why}`
1244
+ };
1159
1245
  }
1160
- entries.set(provider, {
1246
+ effective = withDefaultProxy(raw, options.defaultProxy);
1247
+ }
1248
+ let route;
1249
+ try {
1250
+ route = normalizeRoute(provider, effective, { requireEnabled: options.requireEnabled });
1251
+ } catch (error) {
1252
+ return { ok: false, problem: error instanceof Error ? error.message : String(error) };
1253
+ }
1254
+ if (route.protocol === "socks5" && route.username.length > 0 && route.password.length === 0) {
1255
+ return {
1256
+ ok: false,
1257
+ problem: "dsh-provider-proxy: SOCKS5 \u914D\u7F6E\u4E86\u7528\u6237\u540D\u4F46\u6CA1\u6709\u5BC6\u7801\uFF1B\u8BF7\u8865\u5168\u5BC6\u7801\u6216\u6E05\u7A7A\u7528\u6237\u540D"
1258
+ };
1259
+ }
1260
+ if (route.protocol === "socks5" && route.password.length > 0 && route.username.length === 0) {
1261
+ return {
1262
+ ok: false,
1263
+ problem: "dsh-provider-proxy: SOCKS5 \u914D\u7F6E\u4E86\u5BC6\u7801\u4F46\u6CA1\u6709\u7528\u6237\u540D\uFF1B\u8BF7\u8865\u5168\u7528\u6237\u540D\u6216\u6E05\u7A7A\u5BC6\u7801"
1264
+ };
1265
+ }
1266
+ if (route.protocol !== "socks5" && route.password.length > 0 && route.username.length === 0) {
1267
+ return {
1268
+ ok: false,
1269
+ problem: "dsh-provider-proxy: \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"
1270
+ };
1271
+ }
1272
+ if (route.protocol !== "socks5" && route.username.length > 0 && route.password.length === 0) {
1273
+ return {
1274
+ ok: false,
1275
+ problem: "dsh-provider-proxy: \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"
1276
+ };
1277
+ }
1278
+ const url = proxyUrl(route);
1279
+ let dispatcher;
1280
+ try {
1281
+ dispatcher = createDispatcher(route, url, route.password);
1282
+ } catch (error) {
1283
+ return { ok: false, problem: error instanceof Error ? error.message : String(error) };
1284
+ }
1285
+ return {
1286
+ ok: true,
1287
+ entry: {
1161
1288
  provider,
1162
1289
  proxyOrigin: displayOrigin(route, url),
1163
1290
  dispatcher,
1164
- decide: buildDecide(route)
1165
- });
1291
+ decide: buildDecide(route, options.localDirect),
1292
+ testTarget: route.testTarget
1293
+ }
1294
+ };
1295
+ }
1296
+ function createProxyTable(config, options = {}) {
1297
+ const entries = /* @__PURE__ */ new Map();
1298
+ const problems = /* @__PURE__ */ new Map();
1299
+ const buildOptions = {
1300
+ defaultProxy: normalizeDefaultProxy(options.defaultProxy),
1301
+ requireEnabled: true,
1302
+ localDirect: options.localDirect !== false
1303
+ };
1304
+ for (const [provider, raw] of Object.entries(config ?? {})) {
1305
+ if (raw?.enabled !== true) continue;
1306
+ const built = buildRouteEntry(provider, raw, buildOptions);
1307
+ if (!built.ok) problems.set(provider, built.problem);
1308
+ else entries.set(provider, built.entry);
1166
1309
  }
1167
1310
  return { entries, problems };
1168
1311
  }
@@ -1183,6 +1326,20 @@ function plainProviders(config) {
1183
1326
  const value = typeof snapshot2 === "function" ? snapshot2.call(raw) : raw;
1184
1327
  return value ?? {};
1185
1328
  }
1329
+ function plainDefaultProxy(config) {
1330
+ const raw = config?.defaultProxy;
1331
+ if (raw === void 0) return {};
1332
+ const snapshot2 = raw.get;
1333
+ const value = typeof snapshot2 === "function" ? snapshot2.call(raw) : raw;
1334
+ return value ?? {};
1335
+ }
1336
+ function plainLocalDirect(config) {
1337
+ const raw = config?.localDirect;
1338
+ if (raw === void 0) return true;
1339
+ const snapshot2 = raw.get;
1340
+ const value = typeof snapshot2 === "function" ? snapshot2.call(raw) : raw;
1341
+ return value !== false;
1342
+ }
1186
1343
  function apply(ctx, config) {
1187
1344
  const router = acquireFetchRouter();
1188
1345
  let current = /* @__PURE__ */ new Map();
@@ -1190,18 +1347,67 @@ function apply(ctx, config) {
1190
1347
  let closed = false;
1191
1348
  let webAvailable = false;
1192
1349
  const hits = /* @__PURE__ */ new Map();
1350
+ const lastUse = /* @__PURE__ */ new Map();
1193
1351
  const bump = (provider) => {
1194
1352
  hits.set(provider, (hits.get(provider) ?? 0) + 1);
1353
+ lastUse.set(provider, { ...lastUse.get(provider), startedAt: Date.now() });
1354
+ };
1355
+ const settle = (provider) => (ok, detail) => {
1356
+ const previous = lastUse.get(provider);
1357
+ lastUse.set(provider, {
1358
+ startedAt: previous?.startedAt ?? Date.now(),
1359
+ // `at` is the FINISH time, written together with the outcome it belongs to.
1360
+ at: Date.now(),
1361
+ ok,
1362
+ code: ok ? String(detail?.status ?? "") : failureCodeOf(detail)
1363
+ });
1195
1364
  };
1196
1365
  const DEFAULT_TEST_URL = "https://api.ipify.org?format=json";
1366
+ const MAX_TEST_TARGETS = 5;
1197
1367
  const TEST_TIMEOUT_MS = 1e4;
1198
1368
  const MAX_REQUEST_BYTES = 8 * 1024;
1199
1369
  const selfChecks = /* @__PURE__ */ new Map();
1200
- const plainTestUrl = () => {
1370
+ const SELF_CHECK_COOLDOWN_MS = 3e3;
1371
+ const lastChecks = /* @__PURE__ */ new Map();
1372
+ const plainTestUrls = () => {
1201
1373
  const raw = config?.testUrl;
1202
1374
  const snapshot2 = raw?.get;
1203
1375
  const value = typeof snapshot2 === "function" ? snapshot2.call(raw) : raw;
1204
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : DEFAULT_TEST_URL;
1376
+ const list = typeof value === "string" ? value.split(/[,\s]+/).map((entry) => entry.trim()).filter((entry) => entry.length > 0) : [];
1377
+ return list.length > 0 ? list.slice(0, MAX_TEST_TARGETS) : [DEFAULT_TEST_URL];
1378
+ };
1379
+ const walk = (value, path) => {
1380
+ let node = value;
1381
+ for (const key of path) {
1382
+ if (node === null || typeof node !== "object") return void 0;
1383
+ node = node[key];
1384
+ }
1385
+ return node;
1386
+ };
1387
+ const upstreamOf = (provider) => {
1388
+ let settings;
1389
+ try {
1390
+ settings = typeof ctx?.get === "function" ? ctx.get("settings") : void 0;
1391
+ } catch {
1392
+ return void 0;
1393
+ }
1394
+ if (settings === void 0 || typeof settings.get !== "function") return void 0;
1395
+ let entry;
1396
+ try {
1397
+ const entries = ctx.llm?.listConfigurableProviders?.() ?? [];
1398
+ entry = entries.find((candidate) => candidate?.provider === provider);
1399
+ } catch {
1400
+ return void 0;
1401
+ }
1402
+ if (entry === void 0) return void 0;
1403
+ let profile;
1404
+ try {
1405
+ profile = walk(settings.get(entry.settingsNs), entry.settingsPath ?? []);
1406
+ } catch {
1407
+ return void 0;
1408
+ }
1409
+ const baseURL = profile?.baseURL;
1410
+ return typeof baseURL === "string" && baseURL.trim().length > 0 ? baseURL.trim() : void 0;
1205
1411
  };
1206
1412
  const bareMessage = (message) => message.replace(/^dsh-provider-proxy:\s*/, "");
1207
1413
  const causesOf = (error) => {
@@ -1220,10 +1426,11 @@ function apply(ctx, config) {
1220
1426
  }
1221
1427
  return out;
1222
1428
  };
1223
- const classifyTestError = (error) => {
1429
+ const classifyTestError = (error, viaProxy) => {
1224
1430
  const chain = causesOf(error);
1225
1431
  const names = chain.map((node) => node.name).filter((name2) => typeof name2 === "string");
1226
1432
  const codes = chain.map((node) => node.code).filter((code2) => typeof code2 === "string");
1433
+ const messages = chain.map((node) => node.message).filter((text) => typeof text === "string");
1227
1434
  const message = error instanceof Error ? error.message : String(error);
1228
1435
  if (names.includes("TimeoutError") || names.includes("AbortError")) {
1229
1436
  return { kind: "timeout", error: `\u8D85\u65F6\uFF08${TEST_TIMEOUT_MS / 1e3}s\uFF09\uFF1A\u4EE3\u7406\u6216\u76EE\u6807\u65E0\u54CD\u5E94` };
@@ -1231,31 +1438,43 @@ function apply(ctx, config) {
1231
1438
  const connectivity = ["ECONNREFUSED", "ENOTFOUND", "EHOSTUNREACH", "ENETUNREACH", "ECONNRESET", "EAI_AGAIN"];
1232
1439
  const code = connectivity.find((candidate) => codes.includes(candidate));
1233
1440
  if (code !== void 0) {
1234
- return { kind: "proxy-unreachable", error: `\u4EE3\u7406\u4E0D\u53EF\u8FBE\uFF08${code}\uFF09\uFF1A${message}` };
1441
+ return viaProxy ? { kind: "proxy-unreachable", error: `\u4EE3\u7406\u4E0D\u53EF\u8FBE\uFF08${code}\uFF09\uFF1A${message}` } : { kind: "target", error: `\u76EE\u6807\u4E0D\u53EF\u8FBE\uFF08${code}\uFF09\uFF1A${message}` };
1235
1442
  }
1236
- if (names.includes("ProxyAuthentication") || message.includes("407")) {
1443
+ if (names.includes("ProxyAuthentication") || messages.some((text) => text.includes("407"))) {
1237
1444
  return { kind: "proxy-auth", error: `\u4EE3\u7406\u8981\u6C42\u8BA4\u8BC1\uFF08HTTP 407\uFF09\uFF1A${message}` };
1238
1445
  }
1239
1446
  return { kind: "target", error: `\u8BF7\u6C42\u5931\u8D25\uFF1A${message}` };
1240
1447
  };
1241
- const runSelfCheck = async (provider) => {
1242
- const route = current.get(provider);
1243
- if (route === void 0) {
1244
- const problem = problems.get(provider);
1245
- if (problem !== void 0) {
1246
- return { ok: false, kind: "route-rejected", error: bareMessage(problem) };
1247
- }
1248
- return { ok: false, kind: "no-route", error: `provider "${provider}" \u672A\u542F\u7528\u4EE3\u7406` };
1448
+ const failureCodeOf = (error) => {
1449
+ const chain = causesOf(error);
1450
+ const names = chain.map((node) => node.name).filter((name2) => typeof name2 === "string");
1451
+ if (names.includes("TimeoutError")) return "TIMEOUT";
1452
+ if (names.includes("AbortError")) return "ABORTED";
1453
+ for (const node of chain) {
1454
+ const code = node.code;
1455
+ if (typeof code === "string" && code.length > 0) return code;
1249
1456
  }
1457
+ return "ERROR";
1458
+ };
1459
+ const probeTarget = async (target, route) => {
1460
+ let host;
1461
+ try {
1462
+ host = new URL(target).hostname;
1463
+ } catch {
1464
+ return { ok: false, kind: "target", error: `\u81EA\u68C0\u76EE\u6807\u4E0D\u662F\u5408\u6CD5 URL\uFF1A${target}` };
1465
+ }
1466
+ const viaProxy = route.decide(host) === "proxy";
1250
1467
  const started = performance.now();
1251
1468
  try {
1252
- const response = await undiciFetch2(plainTestUrl(), {
1253
- dispatcher: route.dispatcher,
1469
+ const response = await undiciFetch2(target, {
1470
+ // A direct probe must NOT carry the dispatcher, or undici would force it
1471
+ // through the proxy anyway.
1472
+ ...viaProxy ? { dispatcher: route.dispatcher } : {},
1254
1473
  signal: AbortSignal.timeout(TEST_TIMEOUT_MS)
1255
1474
  });
1256
1475
  const ms = Math.round(performance.now() - started);
1257
- if (response.status === 407) {
1258
- return { ok: false, ms, status: 407, kind: "proxy-auth", error: "\u4EE3\u7406\u8981\u6C42\u8BA4\u8BC1\uFF08HTTP 407\uFF09" };
1476
+ if (viaProxy && response.status === 407) {
1477
+ return { ok: false, ms, status: 407, kind: "proxy-auth", viaProxy, error: "\u4EE3\u7406\u8981\u6C42\u8BA4\u8BC1\uFF08HTTP 407\uFF09" };
1259
1478
  }
1260
1479
  let exitIp;
1261
1480
  try {
@@ -1263,15 +1482,92 @@ function apply(ctx, config) {
1263
1482
  if (typeof body?.ip === "string" && body.ip.length > 0) exitIp = body.ip;
1264
1483
  } catch {
1265
1484
  }
1485
+ const reachable = response.status < 500;
1486
+ if (reachable) {
1487
+ return {
1488
+ ok: true,
1489
+ ms,
1490
+ status: response.status,
1491
+ viaProxy,
1492
+ ...exitIp === void 0 ? {} : { exitIp },
1493
+ kind: "ok"
1494
+ };
1495
+ }
1266
1496
  return {
1267
- ok: response.ok === true,
1497
+ ok: false,
1268
1498
  ms,
1269
1499
  status: response.status,
1270
- ...exitIp === void 0 ? {} : { exitIp },
1271
- kind: response.ok === true ? "ok" : "target"
1500
+ viaProxy,
1501
+ kind: "target",
1502
+ error: `\u76EE\u6807\u8FD4\u56DE HTTP ${response.status}\uFF085xx\uFF0C\u4E0A\u6E38\u6216\u94FE\u8DEF\u5F02\u5E38\uFF09`
1272
1503
  };
1273
1504
  } catch (error) {
1274
- return { ok: false, ms: Math.round(performance.now() - started), ...classifyTestError(error) };
1505
+ return {
1506
+ ok: false,
1507
+ ms: Math.round(performance.now() - started),
1508
+ viaProxy,
1509
+ ...classifyTestError(error, viaProxy)
1510
+ };
1511
+ }
1512
+ };
1513
+ const selfCheckTargets = (provider, testTarget) => {
1514
+ const own = (testTarget ?? "").trim();
1515
+ if (own.length > 0) return [own];
1516
+ const upstream = upstreamOf(provider);
1517
+ if (upstream !== void 0) return [upstream];
1518
+ return plainTestUrls();
1519
+ };
1520
+ const selfCheckKey = (provider) => {
1521
+ const raw = plainProviders(config)[provider] ?? {};
1522
+ const endpoint = raw.useDefault === true ? plainDefaultProxy(config) : raw;
1523
+ return JSON.stringify([
1524
+ provider,
1525
+ endpoint.protocol ?? "http",
1526
+ endpoint.host ?? "",
1527
+ endpoint.port ?? 0,
1528
+ endpoint.username ?? "",
1529
+ endpoint.password ?? "",
1530
+ raw.testTarget ?? "",
1531
+ raw.onlyHosts ?? "",
1532
+ raw.bypassHosts ?? "",
1533
+ plainLocalDirect(config),
1534
+ selfCheckTargets(provider, raw.testTarget)
1535
+ ]);
1536
+ };
1537
+ const runSelfCheck = async (provider) => {
1538
+ let route = current.get(provider);
1539
+ let temporary = false;
1540
+ if (route === void 0) {
1541
+ const problem = problems.get(provider);
1542
+ if (problem !== void 0) {
1543
+ return { ok: false, kind: "route-rejected", error: bareMessage(problem) };
1544
+ }
1545
+ const configured = plainProviders(config)[provider];
1546
+ if (configured === void 0 || configured.enabled === true) {
1547
+ return { ok: false, kind: "no-route", error: `provider "${provider}" \u672A\u542F\u7528\u4EE3\u7406` };
1548
+ }
1549
+ const built = buildRouteEntry(provider, configured, {
1550
+ defaultProxy: normalizeDefaultProxy(plainDefaultProxy(config)),
1551
+ requireEnabled: false,
1552
+ localDirect: plainLocalDirect(config)
1553
+ });
1554
+ if (!built.ok) {
1555
+ return { ok: false, kind: "route-rejected", error: bareMessage(built.problem) };
1556
+ }
1557
+ route = built.entry;
1558
+ temporary = true;
1559
+ }
1560
+ try {
1561
+ let last;
1562
+ for (const target of selfCheckTargets(provider, route.testTarget)) {
1563
+ const outcome = await probeTarget(target, route);
1564
+ if (outcome.ok === true) return outcome;
1565
+ last = outcome;
1566
+ if (outcome.kind === "proxy-auth") break;
1567
+ }
1568
+ return last ?? { ok: false, kind: "target", error: "\u6CA1\u6709\u53EF\u7528\u7684\u81EA\u68C0\u76EE\u6807" };
1569
+ } finally {
1570
+ if (temporary) await route.dispatcher.close().catch(() => void 0);
1275
1571
  }
1276
1572
  };
1277
1573
  try {
@@ -1279,7 +1575,10 @@ function apply(ctx, config) {
1279
1575
  } catch {
1280
1576
  }
1281
1577
  const rebuild = async () => {
1282
- const next = createProxyTable(plainProviders(config));
1578
+ const next = createProxyTable(plainProviders(config), {
1579
+ defaultProxy: plainDefaultProxy(config),
1580
+ localDirect: plainLocalDirect(config)
1581
+ });
1283
1582
  const previous = current;
1284
1583
  current = next.entries;
1285
1584
  problems = next.problems;
@@ -1331,7 +1630,8 @@ function apply(ctx, config) {
1331
1630
  problems: Object.fromEntries(
1332
1631
  [...problems].map(([id, message]) => [id, bareMessage(message)])
1333
1632
  ),
1334
- hits: Object.fromEntries(hits)
1633
+ hits: Object.fromEntries(hits),
1634
+ lastUse: Object.fromEntries(lastUse)
1335
1635
  });
1336
1636
  res.writeHead(200, {
1337
1637
  "content-type": "application/json; charset=utf-8",
@@ -1400,12 +1700,21 @@ function apply(ctx, config) {
1400
1700
  send(400, { ok: false, kind: "body", error: "\u7F3A\u5C11 provider" });
1401
1701
  return;
1402
1702
  }
1403
- let pending = selfChecks.get(provider);
1703
+ const key = selfCheckKey(provider);
1704
+ let pending = selfChecks.get(key);
1404
1705
  if (pending === void 0) {
1405
- pending = runSelfCheck(provider);
1406
- selfChecks.set(provider, pending);
1706
+ const last = lastChecks.get(key);
1707
+ if (last !== void 0 && Date.now() - last.at < SELF_CHECK_COOLDOWN_MS) {
1708
+ send(200, { ...last.result, cached: true });
1709
+ return;
1710
+ }
1711
+ pending = runSelfCheck(provider).then((result) => {
1712
+ lastChecks.set(key, { at: Date.now(), result });
1713
+ return result;
1714
+ });
1715
+ selfChecks.set(key, pending);
1407
1716
  void pending.finally(() => {
1408
- if (selfChecks.get(provider) === pending) selfChecks.delete(provider);
1717
+ if (selfChecks.get(key) === pending) selfChecks.delete(key);
1409
1718
  });
1410
1719
  }
1411
1720
  send(200, await pending);
@@ -1431,6 +1740,7 @@ function apply(ctx, config) {
1431
1740
  {
1432
1741
  dispatcher: route.dispatcher,
1433
1742
  onHit: () => bump(WEB_SEARCH_KEY),
1743
+ onSettle: settle(WEB_SEARCH_KEY),
1434
1744
  decide: route.decide
1435
1745
  },
1436
1746
  () => original.call(this, request, signal)
@@ -1454,6 +1764,7 @@ function apply(ctx, config) {
1454
1764
  {
1455
1765
  dispatcher: route.dispatcher,
1456
1766
  onHit: () => bump(provider),
1767
+ onSettle: settle(provider),
1457
1768
  decide: route.decide
1458
1769
  },
1459
1770
  next
@@ -1483,7 +1794,8 @@ function apply(ctx, config) {
1483
1794
  proxyOrigin: entry.proxyOrigin
1484
1795
  })),
1485
1796
  problems: Object.fromEntries(problems),
1486
- hits: Object.fromEntries(hits)
1797
+ hits: Object.fromEntries(hits),
1798
+ lastUse: Object.fromEntries(lastUse)
1487
1799
  })
1488
1800
  });
1489
1801
  }
@@ -1494,11 +1806,15 @@ export {
1494
1806
  WEB_SEARCH_KEY,
1495
1807
  acquireFetchRouter,
1496
1808
  apply,
1809
+ buildRouteEntry,
1497
1810
  closeTable,
1498
1811
  createProxyTable,
1812
+ defaultProxyProblem,
1499
1813
  inject,
1500
1814
  name,
1815
+ normalizeDefaultProxy,
1501
1816
  normalizeRoute,
1817
+ proxyHostProblem,
1502
1818
  proxyUrl
1503
1819
  };
1504
1820
  //# sourceMappingURL=index.js.map