dsh-provider-proxy 0.4.0 → 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 +61 -7
- package/lib/client.js +676 -99
- package/lib/client.js.map +4 -4
- package/lib/index.js +408 -73
- 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,56 +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: \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"
|
|
1135
|
-
);
|
|
1136
|
-
continue;
|
|
1137
|
-
}
|
|
1138
|
-
const url = proxyUrl(route);
|
|
1139
|
-
let dispatcher;
|
|
1140
|
-
try {
|
|
1141
|
-
dispatcher = createDispatcher(route, url, route.password);
|
|
1142
|
-
} catch (error) {
|
|
1143
|
-
problems.set(provider, error instanceof Error ? error.message : String(error));
|
|
1144
|
-
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
|
+
};
|
|
1145
1228
|
}
|
|
1146
|
-
|
|
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: {
|
|
1147
1271
|
provider,
|
|
1148
1272
|
proxyOrigin: displayOrigin(route, url),
|
|
1149
1273
|
dispatcher,
|
|
1150
|
-
decide: buildDecide(route)
|
|
1151
|
-
|
|
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);
|
|
1152
1292
|
}
|
|
1153
1293
|
return { entries, problems };
|
|
1154
1294
|
}
|
|
@@ -1169,6 +1309,20 @@ function plainProviders(config) {
|
|
|
1169
1309
|
const value = typeof snapshot2 === "function" ? snapshot2.call(raw) : raw;
|
|
1170
1310
|
return value ?? {};
|
|
1171
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
|
+
}
|
|
1172
1326
|
function apply(ctx, config) {
|
|
1173
1327
|
const router = acquireFetchRouter();
|
|
1174
1328
|
let current = /* @__PURE__ */ new Map();
|
|
@@ -1176,18 +1330,69 @@ function apply(ctx, config) {
|
|
|
1176
1330
|
let closed = false;
|
|
1177
1331
|
let webAvailable = false;
|
|
1178
1332
|
const hits = /* @__PURE__ */ new Map();
|
|
1333
|
+
const lastUse = /* @__PURE__ */ new Map();
|
|
1179
1334
|
const bump = (provider) => {
|
|
1180
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
|
+
});
|
|
1181
1347
|
};
|
|
1182
1348
|
const DEFAULT_TEST_URL = "https://api.ipify.org?format=json";
|
|
1349
|
+
const MAX_TEST_TARGETS = 5;
|
|
1183
1350
|
const TEST_TIMEOUT_MS = 1e4;
|
|
1351
|
+
const MAX_REQUEST_BYTES = 8 * 1024;
|
|
1184
1352
|
const selfChecks = /* @__PURE__ */ new Map();
|
|
1185
|
-
const
|
|
1353
|
+
const SELF_CHECK_COOLDOWN_MS = 3e3;
|
|
1354
|
+
const lastChecks = /* @__PURE__ */ new Map();
|
|
1355
|
+
const plainTestUrls = () => {
|
|
1186
1356
|
const raw = config?.testUrl;
|
|
1187
1357
|
const snapshot2 = raw?.get;
|
|
1188
1358
|
const value = typeof snapshot2 === "function" ? snapshot2.call(raw) : raw;
|
|
1189
|
-
|
|
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;
|
|
1190
1394
|
};
|
|
1395
|
+
const bareMessage = (message) => message.replace(/^dsh-provider-proxy:\s*/, "");
|
|
1191
1396
|
const causesOf = (error) => {
|
|
1192
1397
|
const out = [];
|
|
1193
1398
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1204,10 +1409,11 @@ function apply(ctx, config) {
|
|
|
1204
1409
|
}
|
|
1205
1410
|
return out;
|
|
1206
1411
|
};
|
|
1207
|
-
const classifyTestError = (error) => {
|
|
1412
|
+
const classifyTestError = (error, viaProxy) => {
|
|
1208
1413
|
const chain = causesOf(error);
|
|
1209
1414
|
const names = chain.map((node) => node.name).filter((name2) => typeof name2 === "string");
|
|
1210
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");
|
|
1211
1417
|
const message = error instanceof Error ? error.message : String(error);
|
|
1212
1418
|
if (names.includes("TimeoutError") || names.includes("AbortError")) {
|
|
1213
1419
|
return { kind: "timeout", error: `\u8D85\u65F6\uFF08${TEST_TIMEOUT_MS / 1e3}s\uFF09\uFF1A\u4EE3\u7406\u6216\u76EE\u6807\u65E0\u54CD\u5E94` };
|
|
@@ -1215,27 +1421,43 @@ function apply(ctx, config) {
|
|
|
1215
1421
|
const connectivity = ["ECONNREFUSED", "ENOTFOUND", "EHOSTUNREACH", "ENETUNREACH", "ECONNRESET", "EAI_AGAIN"];
|
|
1216
1422
|
const code = connectivity.find((candidate) => codes.includes(candidate));
|
|
1217
1423
|
if (code !== void 0) {
|
|
1218
|
-
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}` };
|
|
1219
1425
|
}
|
|
1220
|
-
if (names.includes("ProxyAuthentication") ||
|
|
1426
|
+
if (names.includes("ProxyAuthentication") || messages.some((text) => text.includes("407"))) {
|
|
1221
1427
|
return { kind: "proxy-auth", error: `\u4EE3\u7406\u8981\u6C42\u8BA4\u8BC1\uFF08HTTP 407\uFF09\uFF1A${message}` };
|
|
1222
1428
|
}
|
|
1223
1429
|
return { kind: "target", error: `\u8BF7\u6C42\u5931\u8D25\uFF1A${message}` };
|
|
1224
1430
|
};
|
|
1225
|
-
const
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
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;
|
|
1229
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}` };
|
|
1448
|
+
}
|
|
1449
|
+
const viaProxy = route.decide(host) === "proxy";
|
|
1230
1450
|
const started = performance.now();
|
|
1231
1451
|
try {
|
|
1232
|
-
const response = await undiciFetch2(
|
|
1233
|
-
|
|
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 } : {},
|
|
1234
1456
|
signal: AbortSignal.timeout(TEST_TIMEOUT_MS)
|
|
1235
1457
|
});
|
|
1236
1458
|
const ms = Math.round(performance.now() - started);
|
|
1237
|
-
if (response.status === 407) {
|
|
1238
|
-
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" };
|
|
1239
1461
|
}
|
|
1240
1462
|
let exitIp;
|
|
1241
1463
|
try {
|
|
@@ -1243,15 +1465,92 @@ function apply(ctx, config) {
|
|
|
1243
1465
|
if (typeof body?.ip === "string" && body.ip.length > 0) exitIp = body.ip;
|
|
1244
1466
|
} catch {
|
|
1245
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
|
+
}
|
|
1246
1479
|
return {
|
|
1247
|
-
ok:
|
|
1480
|
+
ok: false,
|
|
1248
1481
|
ms,
|
|
1249
1482
|
status: response.status,
|
|
1250
|
-
|
|
1251
|
-
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`
|
|
1252
1486
|
};
|
|
1253
1487
|
} catch (error) {
|
|
1254
|
-
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);
|
|
1255
1554
|
}
|
|
1256
1555
|
};
|
|
1257
1556
|
try {
|
|
@@ -1259,7 +1558,10 @@ function apply(ctx, config) {
|
|
|
1259
1558
|
} catch {
|
|
1260
1559
|
}
|
|
1261
1560
|
const rebuild = async () => {
|
|
1262
|
-
const next = createProxyTable(plainProviders(config)
|
|
1561
|
+
const next = createProxyTable(plainProviders(config), {
|
|
1562
|
+
defaultProxy: plainDefaultProxy(config),
|
|
1563
|
+
localDirect: plainLocalDirect(config)
|
|
1564
|
+
});
|
|
1263
1565
|
const previous = current;
|
|
1264
1566
|
current = next.entries;
|
|
1265
1567
|
problems = next.problems;
|
|
@@ -1308,8 +1610,11 @@ function apply(ctx, config) {
|
|
|
1308
1610
|
try {
|
|
1309
1611
|
const text = JSON.stringify({
|
|
1310
1612
|
...catalog(),
|
|
1311
|
-
problems: Object.fromEntries(
|
|
1312
|
-
|
|
1613
|
+
problems: Object.fromEntries(
|
|
1614
|
+
[...problems].map(([id, message]) => [id, bareMessage(message)])
|
|
1615
|
+
),
|
|
1616
|
+
hits: Object.fromEntries(hits),
|
|
1617
|
+
lastUse: Object.fromEntries(lastUse)
|
|
1313
1618
|
});
|
|
1314
1619
|
res.writeHead(200, {
|
|
1315
1620
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -1327,7 +1632,10 @@ function apply(ctx, config) {
|
|
|
1327
1632
|
kind: "exact",
|
|
1328
1633
|
path: "/dsh-provider-proxy/test",
|
|
1329
1634
|
handler: (req, res) => {
|
|
1635
|
+
let replied = false;
|
|
1330
1636
|
const send = (status, payload) => {
|
|
1637
|
+
if (replied) return;
|
|
1638
|
+
replied = true;
|
|
1331
1639
|
const text = JSON.stringify(payload);
|
|
1332
1640
|
res.writeHead(status, {
|
|
1333
1641
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -1344,10 +1652,24 @@ function apply(ctx, config) {
|
|
|
1344
1652
|
send(400, { ok: false, kind: "body", error: "\u65E0\u6CD5\u8BFB\u53D6\u8BF7\u6C42\u4F53" });
|
|
1345
1653
|
return;
|
|
1346
1654
|
}
|
|
1655
|
+
let bytes = 0;
|
|
1347
1656
|
const chunks = [];
|
|
1348
|
-
req.on("data", (chunk) =>
|
|
1657
|
+
req.on("data", (chunk) => {
|
|
1658
|
+
if (replied) return;
|
|
1659
|
+
bytes += chunk.length;
|
|
1660
|
+
if (bytes > MAX_REQUEST_BYTES) {
|
|
1661
|
+
send(413, {
|
|
1662
|
+
ok: false,
|
|
1663
|
+
kind: "body",
|
|
1664
|
+
error: `\u8BF7\u6C42\u4F53\u8D85\u8FC7 ${MAX_REQUEST_BYTES} \u5B57\u8282\u4E0A\u9650`
|
|
1665
|
+
});
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
chunks.push(chunk);
|
|
1669
|
+
});
|
|
1349
1670
|
req.on("error", () => send(400, { ok: false, kind: "body", error: "\u8BFB\u53D6\u8BF7\u6C42\u4F53\u5931\u8D25" }));
|
|
1350
1671
|
req.on("end", () => {
|
|
1672
|
+
if (replied) return;
|
|
1351
1673
|
void (async () => {
|
|
1352
1674
|
let provider;
|
|
1353
1675
|
try {
|
|
@@ -1361,12 +1683,21 @@ function apply(ctx, config) {
|
|
|
1361
1683
|
send(400, { ok: false, kind: "body", error: "\u7F3A\u5C11 provider" });
|
|
1362
1684
|
return;
|
|
1363
1685
|
}
|
|
1364
|
-
|
|
1686
|
+
const key = selfCheckKey(provider);
|
|
1687
|
+
let pending = selfChecks.get(key);
|
|
1365
1688
|
if (pending === void 0) {
|
|
1366
|
-
|
|
1367
|
-
|
|
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);
|
|
1368
1699
|
void pending.finally(() => {
|
|
1369
|
-
if (selfChecks.get(
|
|
1700
|
+
if (selfChecks.get(key) === pending) selfChecks.delete(key);
|
|
1370
1701
|
});
|
|
1371
1702
|
}
|
|
1372
1703
|
send(200, await pending);
|
|
@@ -1391,8 +1722,8 @@ function apply(ctx, config) {
|
|
|
1391
1722
|
return router.run(
|
|
1392
1723
|
{
|
|
1393
1724
|
dispatcher: route.dispatcher,
|
|
1394
|
-
provider: WEB_SEARCH_KEY,
|
|
1395
1725
|
onHit: () => bump(WEB_SEARCH_KEY),
|
|
1726
|
+
onSettle: settle(WEB_SEARCH_KEY),
|
|
1396
1727
|
decide: route.decide
|
|
1397
1728
|
},
|
|
1398
1729
|
() => original.call(this, request, signal)
|
|
@@ -1401,7 +1732,7 @@ function apply(ctx, config) {
|
|
|
1401
1732
|
web.search = patched;
|
|
1402
1733
|
const registerEffect = typeof wctx?.effect === "function" ? wctx.effect.bind(wctx) : ctx.effect?.bind(ctx);
|
|
1403
1734
|
registerEffect?.(() => () => {
|
|
1404
|
-
if (web
|
|
1735
|
+
if (Object.getOwnPropertyDescriptor(web, "search")?.value === patched) delete web.search;
|
|
1405
1736
|
});
|
|
1406
1737
|
});
|
|
1407
1738
|
ctx.effect(() => {
|
|
@@ -1415,8 +1746,8 @@ function apply(ctx, config) {
|
|
|
1415
1746
|
return router.wrap(
|
|
1416
1747
|
{
|
|
1417
1748
|
dispatcher: route.dispatcher,
|
|
1418
|
-
provider,
|
|
1419
1749
|
onHit: () => bump(provider),
|
|
1750
|
+
onSettle: settle(provider),
|
|
1420
1751
|
decide: route.decide
|
|
1421
1752
|
},
|
|
1422
1753
|
next
|
|
@@ -1446,7 +1777,8 @@ function apply(ctx, config) {
|
|
|
1446
1777
|
proxyOrigin: entry.proxyOrigin
|
|
1447
1778
|
})),
|
|
1448
1779
|
problems: Object.fromEntries(problems),
|
|
1449
|
-
hits: Object.fromEntries(hits)
|
|
1780
|
+
hits: Object.fromEntries(hits),
|
|
1781
|
+
lastUse: Object.fromEntries(lastUse)
|
|
1450
1782
|
})
|
|
1451
1783
|
});
|
|
1452
1784
|
}
|
|
@@ -1457,10 +1789,13 @@ export {
|
|
|
1457
1789
|
WEB_SEARCH_KEY,
|
|
1458
1790
|
acquireFetchRouter,
|
|
1459
1791
|
apply,
|
|
1792
|
+
buildRouteEntry,
|
|
1460
1793
|
closeTable,
|
|
1461
1794
|
createProxyTable,
|
|
1795
|
+
defaultProxyProblem,
|
|
1462
1796
|
inject,
|
|
1463
1797
|
name,
|
|
1798
|
+
normalizeDefaultProxy,
|
|
1464
1799
|
normalizeRoute,
|
|
1465
1800
|
proxyUrl
|
|
1466
1801
|
};
|