dsh-provider-proxy 0.4.1 → 0.5.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 +58 -4
- package/lib/client.js +628 -87
- package/lib/client.js.map +4 -4
- package/lib/index.js +384 -86
- package/lib/index.js.map +3 -3
- package/package.json +1 -1
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
|
-
|
|
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 },
|
|
@@ -1017,9 +1025,27 @@ function vol(field) {
|
|
|
1017
1025
|
return typeof volatile2 === "function" ? volatile2.call(field) : field;
|
|
1018
1026
|
}
|
|
1019
1027
|
var PROTOCOLS = ["http", "https", "socks5"];
|
|
1028
|
+
var DefaultProxy = z.object({
|
|
1029
|
+
protocol: z.union(PROTOCOLS).default("http"),
|
|
1030
|
+
/** Proxy host or IP, without scheme, port, or path. */
|
|
1031
|
+
host: z.string().default(""),
|
|
1032
|
+
/** Proxy port. 0 means "not configured". */
|
|
1033
|
+
port: z.number().default(0),
|
|
1034
|
+
username: z.string().default(""),
|
|
1035
|
+
password: z.string().default("")
|
|
1036
|
+
});
|
|
1020
1037
|
var ProviderProxyRoute = z.object({
|
|
1021
1038
|
/** Whether this provider's calls are routed through the proxy below. */
|
|
1022
1039
|
enabled: z.boolean().default(false),
|
|
1040
|
+
/**
|
|
1041
|
+
* When true, this route's TRANSPORT fields (protocol/host/port/username/
|
|
1042
|
+
* password) come from the top-level `defaultProxy` instead of its own, so the
|
|
1043
|
+
* same endpoint is configured once for every provider that shares it. The
|
|
1044
|
+
* target-host rules and the self-check target stay the route's own. There is
|
|
1045
|
+
* no partial inheritance: to use a different endpoint, turn this off and fill
|
|
1046
|
+
* the fields in.
|
|
1047
|
+
*/
|
|
1048
|
+
useDefault: z.boolean().default(false),
|
|
1023
1049
|
/** Proxy transport. `socks5` is accepted but only honoured when the socks helper loaded. */
|
|
1024
1050
|
protocol: z.union(PROTOCOLS).default("http"),
|
|
1025
1051
|
/** Proxy host or IP, without scheme, port, or path. */
|
|
@@ -1036,11 +1062,31 @@ var ProviderProxyRoute = z.object({
|
|
|
1036
1062
|
*/
|
|
1037
1063
|
onlyHosts: z.string().default(""),
|
|
1038
1064
|
/** Comma/space-separated TARGET hosts that bypass the proxy; takes priority over `onlyHosts`. */
|
|
1039
|
-
bypassHosts: z.string().default("")
|
|
1065
|
+
bypassHosts: z.string().default(""),
|
|
1066
|
+
/**
|
|
1067
|
+
* Optional self-check target for THIS provider's 测试 button. Empty falls back
|
|
1068
|
+
* to the top-level `testUrl`. Only the self-check reads it; routing never
|
|
1069
|
+
* touches it, so a bad value costs a failed check, never a broken route.
|
|
1070
|
+
*/
|
|
1071
|
+
testTarget: z.string().default("")
|
|
1040
1072
|
});
|
|
1041
1073
|
var Config = z.object({
|
|
1042
1074
|
/** Per-provider proxy routes, keyed by provider route id (e.g. `sensenova`). */
|
|
1043
1075
|
providers: vol(z.dict(ProviderProxyRoute).default({})),
|
|
1076
|
+
/**
|
|
1077
|
+
* Shared endpoint for every route that sets `useDefault`. Volatile for the
|
|
1078
|
+
* same reason as `providers`: a settings write is committed into the running
|
|
1079
|
+
* fiber instead of remounting the plugin.
|
|
1080
|
+
*/
|
|
1081
|
+
defaultProxy: vol(DefaultProxy.default({})),
|
|
1082
|
+
/**
|
|
1083
|
+
* Whether a loopback/private-address target is sent DIRECT instead of through
|
|
1084
|
+
* the proxy. On by default: such a target is normally a relay on this very
|
|
1085
|
+
* machine (LiteLLM, Ollama, a local mock), which a forward proxy cannot reach
|
|
1086
|
+
* at all — forcing it through one fails, and the failure reads as "the proxy
|
|
1087
|
+
* is broken". Turn this off only when the proxy itself is the target.
|
|
1088
|
+
*/
|
|
1089
|
+
localDirect: vol(z.boolean().default(true)),
|
|
1044
1090
|
/**
|
|
1045
1091
|
* URL the per-provider connection self-check requests THROUGH the proxy.
|
|
1046
1092
|
* Volatile, so a settings write does not remount the plugin; the default is an
|
|
@@ -1048,12 +1094,28 @@ var Config = z.object({
|
|
|
1048
1094
|
*/
|
|
1049
1095
|
testUrl: vol(z.string().default("https://api.ipify.org?format=json"))
|
|
1050
1096
|
});
|
|
1051
|
-
function
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1097
|
+
function plain(parsed) {
|
|
1098
|
+
const get = parsed.get;
|
|
1099
|
+
return typeof get === "function" ? get.call(parsed) : parsed;
|
|
1100
|
+
}
|
|
1101
|
+
function normalizeDefaultProxy(raw) {
|
|
1102
|
+
const value = plain(DefaultProxy(raw ?? {}));
|
|
1103
|
+
return {
|
|
1104
|
+
protocol: value.protocol,
|
|
1105
|
+
host: value.host.trim(),
|
|
1106
|
+
port: value.port,
|
|
1107
|
+
username: value.username.trim(),
|
|
1108
|
+
password: value.password
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
function normalizeRoute(provider, raw, options) {
|
|
1112
|
+
const value = plain(ProviderProxyRoute(raw ?? {}));
|
|
1054
1113
|
const onlyHosts = value.onlyHosts.trim().toLowerCase();
|
|
1055
1114
|
const bypassHosts = value.bypassHosts.trim().toLowerCase();
|
|
1056
|
-
|
|
1115
|
+
const testTarget = value.testTarget.trim();
|
|
1116
|
+
if (!value.enabled && options?.requireEnabled !== false) {
|
|
1117
|
+
return { ...value, port: value.port || 0, onlyHosts, bypassHosts, testTarget };
|
|
1118
|
+
}
|
|
1057
1119
|
const host = value.host.trim();
|
|
1058
1120
|
if (host.length === 0) {
|
|
1059
1121
|
throw new TypeError(`dsh-provider-proxy: provider "${provider}" \u542F\u7528\u4E86\u4EE3\u7406\uFF0C\u4F46\u672A\u586B\u5199\u4EE3\u7406\u5730\u5740`);
|
|
@@ -1066,7 +1128,26 @@ function normalizeRoute(provider, raw) {
|
|
|
1066
1128
|
`dsh-provider-proxy: provider "${provider}" \u7684\u4EE3\u7406\u7AEF\u53E3\u65E0\u6548\uFF08\u9700\u4E3A 1-65535 \u7684\u6574\u6570\uFF09`
|
|
1067
1129
|
);
|
|
1068
1130
|
}
|
|
1069
|
-
return { ...value, host, username: value.username.trim(), onlyHosts, bypassHosts };
|
|
1131
|
+
return { ...value, host, username: value.username.trim(), onlyHosts, bypassHosts, testTarget };
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// src/proxy-address.ts
|
|
1135
|
+
function defaultProxyProblem(endpoint) {
|
|
1136
|
+
const host = (endpoint.host ?? "").trim();
|
|
1137
|
+
if (host.length === 0) return "\u672A\u586B\u5199\u4EE3\u7406\u5730\u5740";
|
|
1138
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(host) || host.includes("/")) {
|
|
1139
|
+
return "\u4EE3\u7406\u5730\u5740\u53EA\u586B\u4E3B\u673A\u540D\u6216 IP\uFF0C\u4E0D\u8981\u5E26\u534F\u8BAE\u524D\u7F00\u6216\u8DEF\u5F84";
|
|
1140
|
+
}
|
|
1141
|
+
const port = endpoint.port ?? 0;
|
|
1142
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1143
|
+
return "\u4EE3\u7406\u7AEF\u53E3\u65E0\u6548\uFF08\u9700\u4E3A 1-65535 \u7684\u6574\u6570\uFF09";
|
|
1144
|
+
}
|
|
1145
|
+
const hasUser = (endpoint.username ?? "").length > 0;
|
|
1146
|
+
const hasPassword = (endpoint.password ?? "").length > 0;
|
|
1147
|
+
if (hasUser !== hasPassword) {
|
|
1148
|
+
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";
|
|
1149
|
+
}
|
|
1150
|
+
return void 0;
|
|
1070
1151
|
}
|
|
1071
1152
|
|
|
1072
1153
|
// src/proxy-table.ts
|
|
@@ -1099,70 +1180,115 @@ function hostMatches(host, rules) {
|
|
|
1099
1180
|
}
|
|
1100
1181
|
return false;
|
|
1101
1182
|
}
|
|
1102
|
-
function
|
|
1183
|
+
function isLocalHost(host) {
|
|
1184
|
+
const bare = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
1185
|
+
if (bare === "localhost" || bare.endsWith(".localhost")) return true;
|
|
1186
|
+
if (bare === "::1") return true;
|
|
1187
|
+
const octets = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(bare);
|
|
1188
|
+
if (octets === null) return false;
|
|
1189
|
+
const [a, b, c, d] = octets.slice(1).map(Number);
|
|
1190
|
+
if (a > 255 || b > 255 || c > 255 || d > 255) return false;
|
|
1191
|
+
if (a === 127) return true;
|
|
1192
|
+
if (a === 10) return true;
|
|
1193
|
+
if (a === 192 && b === 168) return true;
|
|
1194
|
+
if (a === 169 && b === 254) return true;
|
|
1195
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
function buildDecide(route, localDirect) {
|
|
1103
1199
|
const only = parseHostList(route.onlyHosts);
|
|
1104
1200
|
const bypass = parseHostList(route.bypassHosts);
|
|
1105
1201
|
return (host) => {
|
|
1106
1202
|
if (host === void 0) return "proxy";
|
|
1203
|
+
if (localDirect && isLocalHost(host)) return "direct";
|
|
1107
1204
|
if (hostMatches(host, bypass)) return "direct";
|
|
1108
1205
|
if (only.length > 0 && !hostMatches(host, only)) return "direct";
|
|
1109
1206
|
return "proxy";
|
|
1110
1207
|
};
|
|
1111
1208
|
}
|
|
1112
|
-
function
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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;
|
|
1209
|
+
function withDefaultProxy(route, defaultProxy) {
|
|
1210
|
+
return {
|
|
1211
|
+
...route,
|
|
1212
|
+
protocol: defaultProxy.protocol,
|
|
1213
|
+
host: defaultProxy.host,
|
|
1214
|
+
port: defaultProxy.port,
|
|
1215
|
+
username: defaultProxy.username,
|
|
1216
|
+
password: defaultProxy.password
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function buildRouteEntry(provider, raw, options) {
|
|
1220
|
+
let effective = raw;
|
|
1221
|
+
if (raw?.useDefault === true) {
|
|
1222
|
+
const why = defaultProxyProblem(options.defaultProxy);
|
|
1223
|
+
if (why !== void 0) {
|
|
1224
|
+
return {
|
|
1225
|
+
ok: false,
|
|
1226
|
+
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}`
|
|
1227
|
+
};
|
|
1159
1228
|
}
|
|
1160
|
-
|
|
1229
|
+
effective = withDefaultProxy(raw, options.defaultProxy);
|
|
1230
|
+
}
|
|
1231
|
+
let route;
|
|
1232
|
+
try {
|
|
1233
|
+
route = normalizeRoute(provider, effective, { requireEnabled: options.requireEnabled });
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
return { ok: false, problem: error instanceof Error ? error.message : String(error) };
|
|
1236
|
+
}
|
|
1237
|
+
if (route.protocol === "socks5" && route.username.length > 0 && route.password.length === 0) {
|
|
1238
|
+
return {
|
|
1239
|
+
ok: false,
|
|
1240
|
+
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"
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
if (route.protocol === "socks5" && route.password.length > 0 && route.username.length === 0) {
|
|
1244
|
+
return {
|
|
1245
|
+
ok: false,
|
|
1246
|
+
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"
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
if (route.protocol !== "socks5" && route.password.length > 0 && route.username.length === 0) {
|
|
1250
|
+
return {
|
|
1251
|
+
ok: false,
|
|
1252
|
+
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"
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
if (route.protocol !== "socks5" && route.username.length > 0 && route.password.length === 0) {
|
|
1256
|
+
return {
|
|
1257
|
+
ok: false,
|
|
1258
|
+
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"
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const url = proxyUrl(route);
|
|
1262
|
+
let dispatcher;
|
|
1263
|
+
try {
|
|
1264
|
+
dispatcher = createDispatcher(route, url, route.password);
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
return { ok: false, problem: error instanceof Error ? error.message : String(error) };
|
|
1267
|
+
}
|
|
1268
|
+
return {
|
|
1269
|
+
ok: true,
|
|
1270
|
+
entry: {
|
|
1161
1271
|
provider,
|
|
1162
1272
|
proxyOrigin: displayOrigin(route, url),
|
|
1163
1273
|
dispatcher,
|
|
1164
|
-
decide: buildDecide(route)
|
|
1165
|
-
|
|
1274
|
+
decide: buildDecide(route, options.localDirect),
|
|
1275
|
+
testTarget: route.testTarget
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
function createProxyTable(config, options = {}) {
|
|
1280
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1281
|
+
const problems = /* @__PURE__ */ new Map();
|
|
1282
|
+
const buildOptions = {
|
|
1283
|
+
defaultProxy: normalizeDefaultProxy(options.defaultProxy),
|
|
1284
|
+
requireEnabled: true,
|
|
1285
|
+
localDirect: options.localDirect !== false
|
|
1286
|
+
};
|
|
1287
|
+
for (const [provider, raw] of Object.entries(config ?? {})) {
|
|
1288
|
+
if (raw?.enabled !== true) continue;
|
|
1289
|
+
const built = buildRouteEntry(provider, raw, buildOptions);
|
|
1290
|
+
if (!built.ok) problems.set(provider, built.problem);
|
|
1291
|
+
else entries.set(provider, built.entry);
|
|
1166
1292
|
}
|
|
1167
1293
|
return { entries, problems };
|
|
1168
1294
|
}
|
|
@@ -1183,6 +1309,20 @@ function plainProviders(config) {
|
|
|
1183
1309
|
const value = typeof snapshot2 === "function" ? snapshot2.call(raw) : raw;
|
|
1184
1310
|
return value ?? {};
|
|
1185
1311
|
}
|
|
1312
|
+
function plainDefaultProxy(config) {
|
|
1313
|
+
const raw = config?.defaultProxy;
|
|
1314
|
+
if (raw === void 0) return {};
|
|
1315
|
+
const snapshot2 = raw.get;
|
|
1316
|
+
const value = typeof snapshot2 === "function" ? snapshot2.call(raw) : raw;
|
|
1317
|
+
return value ?? {};
|
|
1318
|
+
}
|
|
1319
|
+
function plainLocalDirect(config) {
|
|
1320
|
+
const raw = config?.localDirect;
|
|
1321
|
+
if (raw === void 0) return true;
|
|
1322
|
+
const snapshot2 = raw.get;
|
|
1323
|
+
const value = typeof snapshot2 === "function" ? snapshot2.call(raw) : raw;
|
|
1324
|
+
return value !== false;
|
|
1325
|
+
}
|
|
1186
1326
|
function apply(ctx, config) {
|
|
1187
1327
|
const router = acquireFetchRouter();
|
|
1188
1328
|
let current = /* @__PURE__ */ new Map();
|
|
@@ -1190,18 +1330,67 @@ function apply(ctx, config) {
|
|
|
1190
1330
|
let closed = false;
|
|
1191
1331
|
let webAvailable = false;
|
|
1192
1332
|
const hits = /* @__PURE__ */ new Map();
|
|
1333
|
+
const lastUse = /* @__PURE__ */ new Map();
|
|
1193
1334
|
const bump = (provider) => {
|
|
1194
1335
|
hits.set(provider, (hits.get(provider) ?? 0) + 1);
|
|
1336
|
+
lastUse.set(provider, { ...lastUse.get(provider), startedAt: Date.now() });
|
|
1337
|
+
};
|
|
1338
|
+
const settle = (provider) => (ok, detail) => {
|
|
1339
|
+
const previous = lastUse.get(provider);
|
|
1340
|
+
lastUse.set(provider, {
|
|
1341
|
+
startedAt: previous?.startedAt ?? Date.now(),
|
|
1342
|
+
// `at` is the FINISH time, written together with the outcome it belongs to.
|
|
1343
|
+
at: Date.now(),
|
|
1344
|
+
ok,
|
|
1345
|
+
code: ok ? String(detail?.status ?? "") : failureCodeOf(detail)
|
|
1346
|
+
});
|
|
1195
1347
|
};
|
|
1196
1348
|
const DEFAULT_TEST_URL = "https://api.ipify.org?format=json";
|
|
1349
|
+
const MAX_TEST_TARGETS = 5;
|
|
1197
1350
|
const TEST_TIMEOUT_MS = 1e4;
|
|
1198
1351
|
const MAX_REQUEST_BYTES = 8 * 1024;
|
|
1199
1352
|
const selfChecks = /* @__PURE__ */ new Map();
|
|
1200
|
-
const
|
|
1353
|
+
const SELF_CHECK_COOLDOWN_MS = 3e3;
|
|
1354
|
+
const lastChecks = /* @__PURE__ */ new Map();
|
|
1355
|
+
const plainTestUrls = () => {
|
|
1201
1356
|
const raw = config?.testUrl;
|
|
1202
1357
|
const snapshot2 = raw?.get;
|
|
1203
1358
|
const value = typeof snapshot2 === "function" ? snapshot2.call(raw) : raw;
|
|
1204
|
-
|
|
1359
|
+
const list = typeof value === "string" ? value.split(/[,\s]+/).map((entry) => entry.trim()).filter((entry) => entry.length > 0) : [];
|
|
1360
|
+
return list.length > 0 ? list.slice(0, MAX_TEST_TARGETS) : [DEFAULT_TEST_URL];
|
|
1361
|
+
};
|
|
1362
|
+
const walk = (value, path) => {
|
|
1363
|
+
let node = value;
|
|
1364
|
+
for (const key of path) {
|
|
1365
|
+
if (node === null || typeof node !== "object") return void 0;
|
|
1366
|
+
node = node[key];
|
|
1367
|
+
}
|
|
1368
|
+
return node;
|
|
1369
|
+
};
|
|
1370
|
+
const upstreamOf = (provider) => {
|
|
1371
|
+
let settings;
|
|
1372
|
+
try {
|
|
1373
|
+
settings = typeof ctx?.get === "function" ? ctx.get("settings") : void 0;
|
|
1374
|
+
} catch {
|
|
1375
|
+
return void 0;
|
|
1376
|
+
}
|
|
1377
|
+
if (settings === void 0 || typeof settings.get !== "function") return void 0;
|
|
1378
|
+
let entry;
|
|
1379
|
+
try {
|
|
1380
|
+
const entries = ctx.llm?.listConfigurableProviders?.() ?? [];
|
|
1381
|
+
entry = entries.find((candidate) => candidate?.provider === provider);
|
|
1382
|
+
} catch {
|
|
1383
|
+
return void 0;
|
|
1384
|
+
}
|
|
1385
|
+
if (entry === void 0) return void 0;
|
|
1386
|
+
let profile;
|
|
1387
|
+
try {
|
|
1388
|
+
profile = walk(settings.get(entry.settingsNs), entry.settingsPath ?? []);
|
|
1389
|
+
} catch {
|
|
1390
|
+
return void 0;
|
|
1391
|
+
}
|
|
1392
|
+
const baseURL = profile?.baseURL;
|
|
1393
|
+
return typeof baseURL === "string" && baseURL.trim().length > 0 ? baseURL.trim() : void 0;
|
|
1205
1394
|
};
|
|
1206
1395
|
const bareMessage = (message) => message.replace(/^dsh-provider-proxy:\s*/, "");
|
|
1207
1396
|
const causesOf = (error) => {
|
|
@@ -1220,10 +1409,11 @@ function apply(ctx, config) {
|
|
|
1220
1409
|
}
|
|
1221
1410
|
return out;
|
|
1222
1411
|
};
|
|
1223
|
-
const classifyTestError = (error) => {
|
|
1412
|
+
const classifyTestError = (error, viaProxy) => {
|
|
1224
1413
|
const chain = causesOf(error);
|
|
1225
1414
|
const names = chain.map((node) => node.name).filter((name2) => typeof name2 === "string");
|
|
1226
1415
|
const codes = chain.map((node) => node.code).filter((code2) => typeof code2 === "string");
|
|
1416
|
+
const messages = chain.map((node) => node.message).filter((text) => typeof text === "string");
|
|
1227
1417
|
const message = error instanceof Error ? error.message : String(error);
|
|
1228
1418
|
if (names.includes("TimeoutError") || names.includes("AbortError")) {
|
|
1229
1419
|
return { kind: "timeout", error: `\u8D85\u65F6\uFF08${TEST_TIMEOUT_MS / 1e3}s\uFF09\uFF1A\u4EE3\u7406\u6216\u76EE\u6807\u65E0\u54CD\u5E94` };
|
|
@@ -1231,31 +1421,43 @@ function apply(ctx, config) {
|
|
|
1231
1421
|
const connectivity = ["ECONNREFUSED", "ENOTFOUND", "EHOSTUNREACH", "ENETUNREACH", "ECONNRESET", "EAI_AGAIN"];
|
|
1232
1422
|
const code = connectivity.find((candidate) => codes.includes(candidate));
|
|
1233
1423
|
if (code !== void 0) {
|
|
1234
|
-
return { kind: "proxy-unreachable", error: `\u4EE3\u7406\u4E0D\u53EF\u8FBE\uFF08${code}\uFF09\uFF1A${message}` };
|
|
1424
|
+
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
1425
|
}
|
|
1236
|
-
if (names.includes("ProxyAuthentication") ||
|
|
1426
|
+
if (names.includes("ProxyAuthentication") || messages.some((text) => text.includes("407"))) {
|
|
1237
1427
|
return { kind: "proxy-auth", error: `\u4EE3\u7406\u8981\u6C42\u8BA4\u8BC1\uFF08HTTP 407\uFF09\uFF1A${message}` };
|
|
1238
1428
|
}
|
|
1239
1429
|
return { kind: "target", error: `\u8BF7\u6C42\u5931\u8D25\uFF1A${message}` };
|
|
1240
1430
|
};
|
|
1241
|
-
const
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1431
|
+
const failureCodeOf = (error) => {
|
|
1432
|
+
const chain = causesOf(error);
|
|
1433
|
+
const names = chain.map((node) => node.name).filter((name2) => typeof name2 === "string");
|
|
1434
|
+
if (names.includes("TimeoutError")) return "TIMEOUT";
|
|
1435
|
+
if (names.includes("AbortError")) return "ABORTED";
|
|
1436
|
+
for (const node of chain) {
|
|
1437
|
+
const code = node.code;
|
|
1438
|
+
if (typeof code === "string" && code.length > 0) return code;
|
|
1439
|
+
}
|
|
1440
|
+
return "ERROR";
|
|
1441
|
+
};
|
|
1442
|
+
const probeTarget = async (target, route) => {
|
|
1443
|
+
let host;
|
|
1444
|
+
try {
|
|
1445
|
+
host = new URL(target).hostname;
|
|
1446
|
+
} catch {
|
|
1447
|
+
return { ok: false, kind: "target", error: `\u81EA\u68C0\u76EE\u6807\u4E0D\u662F\u5408\u6CD5 URL\uFF1A${target}` };
|
|
1249
1448
|
}
|
|
1449
|
+
const viaProxy = route.decide(host) === "proxy";
|
|
1250
1450
|
const started = performance.now();
|
|
1251
1451
|
try {
|
|
1252
|
-
const response = await undiciFetch2(
|
|
1253
|
-
|
|
1452
|
+
const response = await undiciFetch2(target, {
|
|
1453
|
+
// A direct probe must NOT carry the dispatcher, or undici would force it
|
|
1454
|
+
// through the proxy anyway.
|
|
1455
|
+
...viaProxy ? { dispatcher: route.dispatcher } : {},
|
|
1254
1456
|
signal: AbortSignal.timeout(TEST_TIMEOUT_MS)
|
|
1255
1457
|
});
|
|
1256
1458
|
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" };
|
|
1459
|
+
if (viaProxy && response.status === 407) {
|
|
1460
|
+
return { ok: false, ms, status: 407, kind: "proxy-auth", viaProxy, error: "\u4EE3\u7406\u8981\u6C42\u8BA4\u8BC1\uFF08HTTP 407\uFF09" };
|
|
1259
1461
|
}
|
|
1260
1462
|
let exitIp;
|
|
1261
1463
|
try {
|
|
@@ -1263,15 +1465,92 @@ function apply(ctx, config) {
|
|
|
1263
1465
|
if (typeof body?.ip === "string" && body.ip.length > 0) exitIp = body.ip;
|
|
1264
1466
|
} catch {
|
|
1265
1467
|
}
|
|
1468
|
+
const reachable = response.status < 500;
|
|
1469
|
+
if (reachable) {
|
|
1470
|
+
return {
|
|
1471
|
+
ok: true,
|
|
1472
|
+
ms,
|
|
1473
|
+
status: response.status,
|
|
1474
|
+
viaProxy,
|
|
1475
|
+
...exitIp === void 0 ? {} : { exitIp },
|
|
1476
|
+
kind: "ok"
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1266
1479
|
return {
|
|
1267
|
-
ok:
|
|
1480
|
+
ok: false,
|
|
1268
1481
|
ms,
|
|
1269
1482
|
status: response.status,
|
|
1270
|
-
|
|
1271
|
-
kind:
|
|
1483
|
+
viaProxy,
|
|
1484
|
+
kind: "target",
|
|
1485
|
+
error: `\u76EE\u6807\u8FD4\u56DE HTTP ${response.status}\uFF085xx\uFF0C\u4E0A\u6E38\u6216\u94FE\u8DEF\u5F02\u5E38\uFF09`
|
|
1272
1486
|
};
|
|
1273
1487
|
} catch (error) {
|
|
1274
|
-
return {
|
|
1488
|
+
return {
|
|
1489
|
+
ok: false,
|
|
1490
|
+
ms: Math.round(performance.now() - started),
|
|
1491
|
+
viaProxy,
|
|
1492
|
+
...classifyTestError(error, viaProxy)
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
const selfCheckTargets = (provider, testTarget) => {
|
|
1497
|
+
const own = (testTarget ?? "").trim();
|
|
1498
|
+
if (own.length > 0) return [own];
|
|
1499
|
+
const upstream = upstreamOf(provider);
|
|
1500
|
+
if (upstream !== void 0) return [upstream];
|
|
1501
|
+
return plainTestUrls();
|
|
1502
|
+
};
|
|
1503
|
+
const selfCheckKey = (provider) => {
|
|
1504
|
+
const raw = plainProviders(config)[provider] ?? {};
|
|
1505
|
+
const endpoint = raw.useDefault === true ? plainDefaultProxy(config) : raw;
|
|
1506
|
+
return JSON.stringify([
|
|
1507
|
+
provider,
|
|
1508
|
+
endpoint.protocol ?? "http",
|
|
1509
|
+
endpoint.host ?? "",
|
|
1510
|
+
endpoint.port ?? 0,
|
|
1511
|
+
endpoint.username ?? "",
|
|
1512
|
+
endpoint.password ?? "",
|
|
1513
|
+
raw.testTarget ?? "",
|
|
1514
|
+
raw.onlyHosts ?? "",
|
|
1515
|
+
raw.bypassHosts ?? "",
|
|
1516
|
+
plainLocalDirect(config),
|
|
1517
|
+
selfCheckTargets(provider, raw.testTarget)
|
|
1518
|
+
]);
|
|
1519
|
+
};
|
|
1520
|
+
const runSelfCheck = async (provider) => {
|
|
1521
|
+
let route = current.get(provider);
|
|
1522
|
+
let temporary = false;
|
|
1523
|
+
if (route === void 0) {
|
|
1524
|
+
const problem = problems.get(provider);
|
|
1525
|
+
if (problem !== void 0) {
|
|
1526
|
+
return { ok: false, kind: "route-rejected", error: bareMessage(problem) };
|
|
1527
|
+
}
|
|
1528
|
+
const configured = plainProviders(config)[provider];
|
|
1529
|
+
if (configured === void 0 || configured.enabled === true) {
|
|
1530
|
+
return { ok: false, kind: "no-route", error: `provider "${provider}" \u672A\u542F\u7528\u4EE3\u7406` };
|
|
1531
|
+
}
|
|
1532
|
+
const built = buildRouteEntry(provider, configured, {
|
|
1533
|
+
defaultProxy: normalizeDefaultProxy(plainDefaultProxy(config)),
|
|
1534
|
+
requireEnabled: false,
|
|
1535
|
+
localDirect: plainLocalDirect(config)
|
|
1536
|
+
});
|
|
1537
|
+
if (!built.ok) {
|
|
1538
|
+
return { ok: false, kind: "route-rejected", error: bareMessage(built.problem) };
|
|
1539
|
+
}
|
|
1540
|
+
route = built.entry;
|
|
1541
|
+
temporary = true;
|
|
1542
|
+
}
|
|
1543
|
+
try {
|
|
1544
|
+
let last;
|
|
1545
|
+
for (const target of selfCheckTargets(provider, route.testTarget)) {
|
|
1546
|
+
const outcome = await probeTarget(target, route);
|
|
1547
|
+
if (outcome.ok === true) return outcome;
|
|
1548
|
+
last = outcome;
|
|
1549
|
+
if (outcome.kind === "proxy-auth") break;
|
|
1550
|
+
}
|
|
1551
|
+
return last ?? { ok: false, kind: "target", error: "\u6CA1\u6709\u53EF\u7528\u7684\u81EA\u68C0\u76EE\u6807" };
|
|
1552
|
+
} finally {
|
|
1553
|
+
if (temporary) await route.dispatcher.close().catch(() => void 0);
|
|
1275
1554
|
}
|
|
1276
1555
|
};
|
|
1277
1556
|
try {
|
|
@@ -1279,7 +1558,10 @@ function apply(ctx, config) {
|
|
|
1279
1558
|
} catch {
|
|
1280
1559
|
}
|
|
1281
1560
|
const rebuild = async () => {
|
|
1282
|
-
const next = createProxyTable(plainProviders(config)
|
|
1561
|
+
const next = createProxyTable(plainProviders(config), {
|
|
1562
|
+
defaultProxy: plainDefaultProxy(config),
|
|
1563
|
+
localDirect: plainLocalDirect(config)
|
|
1564
|
+
});
|
|
1283
1565
|
const previous = current;
|
|
1284
1566
|
current = next.entries;
|
|
1285
1567
|
problems = next.problems;
|
|
@@ -1331,7 +1613,8 @@ function apply(ctx, config) {
|
|
|
1331
1613
|
problems: Object.fromEntries(
|
|
1332
1614
|
[...problems].map(([id, message]) => [id, bareMessage(message)])
|
|
1333
1615
|
),
|
|
1334
|
-
hits: Object.fromEntries(hits)
|
|
1616
|
+
hits: Object.fromEntries(hits),
|
|
1617
|
+
lastUse: Object.fromEntries(lastUse)
|
|
1335
1618
|
});
|
|
1336
1619
|
res.writeHead(200, {
|
|
1337
1620
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -1400,12 +1683,21 @@ function apply(ctx, config) {
|
|
|
1400
1683
|
send(400, { ok: false, kind: "body", error: "\u7F3A\u5C11 provider" });
|
|
1401
1684
|
return;
|
|
1402
1685
|
}
|
|
1403
|
-
|
|
1686
|
+
const key = selfCheckKey(provider);
|
|
1687
|
+
let pending = selfChecks.get(key);
|
|
1404
1688
|
if (pending === void 0) {
|
|
1405
|
-
|
|
1406
|
-
|
|
1689
|
+
const last = lastChecks.get(key);
|
|
1690
|
+
if (last !== void 0 && Date.now() - last.at < SELF_CHECK_COOLDOWN_MS) {
|
|
1691
|
+
send(200, { ...last.result, cached: true });
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
pending = runSelfCheck(provider).then((result) => {
|
|
1695
|
+
lastChecks.set(key, { at: Date.now(), result });
|
|
1696
|
+
return result;
|
|
1697
|
+
});
|
|
1698
|
+
selfChecks.set(key, pending);
|
|
1407
1699
|
void pending.finally(() => {
|
|
1408
|
-
if (selfChecks.get(
|
|
1700
|
+
if (selfChecks.get(key) === pending) selfChecks.delete(key);
|
|
1409
1701
|
});
|
|
1410
1702
|
}
|
|
1411
1703
|
send(200, await pending);
|
|
@@ -1431,6 +1723,7 @@ function apply(ctx, config) {
|
|
|
1431
1723
|
{
|
|
1432
1724
|
dispatcher: route.dispatcher,
|
|
1433
1725
|
onHit: () => bump(WEB_SEARCH_KEY),
|
|
1726
|
+
onSettle: settle(WEB_SEARCH_KEY),
|
|
1434
1727
|
decide: route.decide
|
|
1435
1728
|
},
|
|
1436
1729
|
() => original.call(this, request, signal)
|
|
@@ -1454,6 +1747,7 @@ function apply(ctx, config) {
|
|
|
1454
1747
|
{
|
|
1455
1748
|
dispatcher: route.dispatcher,
|
|
1456
1749
|
onHit: () => bump(provider),
|
|
1750
|
+
onSettle: settle(provider),
|
|
1457
1751
|
decide: route.decide
|
|
1458
1752
|
},
|
|
1459
1753
|
next
|
|
@@ -1483,7 +1777,8 @@ function apply(ctx, config) {
|
|
|
1483
1777
|
proxyOrigin: entry.proxyOrigin
|
|
1484
1778
|
})),
|
|
1485
1779
|
problems: Object.fromEntries(problems),
|
|
1486
|
-
hits: Object.fromEntries(hits)
|
|
1780
|
+
hits: Object.fromEntries(hits),
|
|
1781
|
+
lastUse: Object.fromEntries(lastUse)
|
|
1487
1782
|
})
|
|
1488
1783
|
});
|
|
1489
1784
|
}
|
|
@@ -1494,10 +1789,13 @@ export {
|
|
|
1494
1789
|
WEB_SEARCH_KEY,
|
|
1495
1790
|
acquireFetchRouter,
|
|
1496
1791
|
apply,
|
|
1792
|
+
buildRouteEntry,
|
|
1497
1793
|
closeTable,
|
|
1498
1794
|
createProxyTable,
|
|
1795
|
+
defaultProxyProblem,
|
|
1499
1796
|
inject,
|
|
1500
1797
|
name,
|
|
1798
|
+
normalizeDefaultProxy,
|
|
1501
1799
|
normalizeRoute,
|
|
1502
1800
|
proxyUrl
|
|
1503
1801
|
};
|