mslxdff 0.1.199 → 0.1.203
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 +3 -3
- package/bin/mslxdff.js +1 -1
- package/package.json +38 -38
- package/plugins/README.md +14 -14
- package/plugins/prefer-model.mjs.example +14 -14
- package/src/anthropic/count-tokens.js +61 -61
- package/src/anthropic/request-body.js +126 -126
- package/src/anthropic/shared.js +18 -18
- package/src/anthropic/sse-aggregate.js +56 -56
- package/src/anthropic/translate.js +287 -287
- package/src/anthropic/web-search.js +361 -361
- package/src/api-kind.js +31 -31
- package/src/autostart.js +288 -288
- package/src/bench/probe.js +1 -1
- package/src/bench/runner.js +1 -1
- package/src/bench/via-probe.js +1 -1
- package/src/bench/via.js +144 -144
- package/src/bench/workbuddy-bench.js +1 -1
- package/src/chat/config.js +11 -11
- package/src/chat/cooling.js +81 -81
- package/src/chat/direct.js +58 -58
- package/src/chat/index.js +6 -6
- package/src/chat/spinner.js +38 -38
- package/src/chat/sse.js +69 -69
- package/src/chat/stats.js +247 -247
- package/src/chat/store.js +48 -48
- package/src/chat/tool-handlers.js +67 -67
- package/src/chat/tools.js +1 -1
- package/src/chat/upstream.js +59 -59
- package/src/chat-pipeline/auto-race.js +158 -158
- package/src/chat-pipeline/empty-turn.js +147 -147
- package/src/chat-pipeline/engine.js +17 -17
- package/src/chat-pipeline/serial-trial.js +283 -283
- package/src/chat-pipeline/terminal-verdict.js +147 -147
- package/src/chooser.js +54 -54
- package/src/claude-desktop/apply.js +101 -101
- package/src/claude-desktop/context1m.js +38 -38
- package/src/claude-desktop/doctor.js +146 -146
- package/src/claude-desktop/slots.js +152 -152
- package/src/cli/bootstrap.js +1 -1
- package/src/cli/commands/claude-desktop.js +284 -284
- package/src/cli/commands/daemon.js +231 -231
- package/src/cli/commands/model/list-live.js +22 -22
- package/src/cli/commands/model/list-providers.js +76 -76
- package/src/cli/commands/model/list-render.js +77 -77
- package/src/cli/commands/model/list-sort.js +52 -52
- package/src/cli/commands/model/list-tty.js +76 -76
- package/src/cli/commands/model/list.js +166 -166
- package/src/cli/commands/model/live-models.js +24 -24
- package/src/cli/commands/model/stats.js +43 -43
- package/src/cli/commands/model/status.js +47 -47
- package/src/cli/commands/model.js +51 -51
- package/src/cli/commands/provider/add.js +108 -108
- package/src/cli/commands/provider/allowlist.js +99 -99
- package/src/cli/commands/provider/cline-free.js +99 -99
- package/src/cli/commands/provider/cline-login.js +218 -218
- package/src/cli/commands/provider/cline-quota.js +65 -65
- package/src/cli/commands/provider/codearts-login.js +45 -45
- package/src/cli/commands/provider/config.js +119 -119
- package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
- package/src/cli/commands/provider/keys-guard.js +50 -50
- package/src/cli/commands/provider/keys.js +161 -161
- package/src/cli/commands/provider/qoder-checkin.js +81 -81
- package/src/cli/commands/provider/qoder-login.js +102 -102
- package/src/cli/commands/provider/raccoon-login.js +180 -180
- package/src/cli/commands/provider/raccoon-password.js +162 -162
- package/src/cli/commands/provider/raccoon-quota.js +109 -109
- package/src/cli/commands/provider/traework-checkin.js +144 -144
- package/src/cli/commands/provider/traework-login.js +164 -164
- package/src/cli/commands/provider/workbuddy-login.js +153 -153
- package/src/cli/commands/provider/zcode-login.js +77 -77
- package/src/cli/commands/provider/zcode-quota.js +55 -55
- package/src/cli/commands/provider.js +1 -1
- package/src/cli/commands/stats.js +149 -149
- package/src/cli/commands/system.js +261 -261
- package/src/cli/commands/timezone.js +47 -47
- package/src/cli/commands/use-group.js +60 -60
- package/src/cli/commands/workbuddy.js +189 -189
- package/src/cli/format.js +141 -141
- package/src/cli/group-helpers.js +70 -70
- package/src/cli/help.js +80 -80
- package/src/cli/index.js +70 -70
- package/src/cli/interactive.js +121 -121
- package/src/cli/join-core.js +99 -99
- package/src/cli/join-wizard.js +175 -175
- package/src/cli/policy.js +136 -136
- package/src/cli/provider-row.js +126 -126
- package/src/cli/util.js +24 -24
- package/src/compat.js +53 -53
- package/src/events.js +38 -38
- package/src/free-lane.js +131 -131
- package/src/free-watcher.js +63 -63
- package/src/groups.js +262 -262
- package/src/logs.js +195 -195
- package/src/metrics.js +62 -62
- package/src/model-capabilities/context-lookup.js +65 -65
- package/src/model-capabilities/merge.js +142 -142
- package/src/model-trace.js +187 -187
- package/src/models.js +1 -1
- package/src/opencode-identity.js +72 -72
- package/src/peers.js +254 -251
- package/src/plugins.js +107 -107
- package/src/providers/base.js +164 -164
- package/src/providers/classify.js +22 -22
- package/src/providers/cline/allowlist-sync.js +93 -93
- package/src/providers/cline/free-catalog.js +71 -71
- package/src/providers/cline/headers.js +37 -37
- package/src/providers/cline/index.js +130 -130
- package/src/providers/cline/models.js +150 -150
- package/src/providers/cline/usage.js +274 -274
- package/src/providers/codearts/auth-pool.js +148 -148
- package/src/providers/codearts/concurrency.js +34 -34
- package/src/providers/codearts/const.js +65 -65
- package/src/providers/codearts/dpop.js +95 -95
- package/src/providers/codearts/index.js +175 -175
- package/src/providers/codearts/login.js +214 -214
- package/src/providers/codearts/models.js +182 -182
- package/src/providers/codearts/sign.js +96 -96
- package/src/providers/codearts/sse.js +204 -204
- package/src/providers/codearts/stream.js +209 -209
- package/src/providers/codearts/sts.js +124 -124
- package/src/providers/codearts.js +1 -1
- package/src/providers/dispatcher.js +146 -146
- package/src/providers/generic.js +156 -156
- package/src/providers/globalqwenwork/account-store.js +141 -141
- package/src/providers/globalqwenwork/constants.js +73 -73
- package/src/providers/globalqwenwork/crypto.js +2 -2
- package/src/providers/globalqwenwork/http.js +2 -2
- package/src/providers/globalqwenwork/index.js +331 -331
- package/src/providers/globalqwenwork/payload.js +147 -147
- package/src/providers/globalqwenwork/rsa.js +2 -2
- package/src/providers/globalqwenwork/sse.js +2 -2
- package/src/providers/globalqwenwork/stream.js +2 -2
- package/src/providers/globalqwenwork.js +1 -1
- package/src/providers/globalworkbuddy/account-store.js +198 -198
- package/src/providers/globalworkbuddy/auth.js +97 -97
- package/src/providers/globalworkbuddy/catalog.js +138 -138
- package/src/providers/globalworkbuddy/constants.js +73 -73
- package/src/providers/globalworkbuddy/device-auth.js +120 -120
- package/src/providers/globalworkbuddy/errors.js +98 -98
- package/src/providers/globalworkbuddy/headers.js +89 -89
- package/src/providers/globalworkbuddy/models.js +96 -96
- package/src/providers/globalworkbuddy/payload.js +80 -80
- package/src/providers/index.js +5 -5
- package/src/providers/model-id.js +172 -172
- package/src/providers/opencode.js +19 -19
- package/src/providers/openrouter.js +31 -31
- package/src/providers/qoder/account-store.js +106 -106
- package/src/providers/qoder/aggregate.js +60 -60
- package/src/providers/qoder/baseprompt.json +591 -591
- package/src/providers/qoder/checkin.js +160 -160
- package/src/providers/qoder/constants.js +44 -44
- package/src/providers/qoder/encode.js +41 -41
- package/src/providers/qoder/fingerprint.js +38 -38
- package/src/providers/qoder/models.js +77 -77
- package/src/providers/qoder/oauth.js +84 -84
- package/src/providers/qoder/payload.js +162 -162
- package/src/providers/qoder/request.js +27 -27
- package/src/providers/qoder/session.js +134 -134
- package/src/providers/qoder/sticky.js +38 -38
- package/src/providers/qoder/stream.js +116 -116
- package/src/providers/qoder.js +1 -1
- package/src/providers/raccoon/account-store.js +216 -216
- package/src/providers/raccoon/auth.js +67 -67
- package/src/providers/raccoon/chat.js +228 -228
- package/src/providers/raccoon/const.js +180 -165
- package/src/providers/raccoon/credits.js +185 -175
- package/src/providers/raccoon/crypto.js +43 -0
- package/src/providers/raccoon/envelope.js +31 -31
- package/src/providers/raccoon/headers.js +45 -45
- package/src/providers/raccoon/index.js +216 -216
- package/src/providers/raccoon/login.js +279 -279
- package/src/providers/raccoon/models.js +160 -160
- package/src/providers/raccoon/password-login.js +102 -87
- package/src/providers/raccoon/qr-codewords.js +115 -115
- package/src/providers/raccoon/qr.js +236 -236
- package/src/providers/raccoon/sse.js +135 -135
- package/src/providers/responses-channel.js +120 -120
- package/src/providers/share-keys.js +53 -53
- package/src/providers/traework/account-store.js +234 -234
- package/src/providers/traework/chat.js +217 -217
- package/src/providers/traework/checkin.js +77 -77
- package/src/providers/traework/constants.js +41 -41
- package/src/providers/traework/errors.js +52 -52
- package/src/providers/traework/headers.js +79 -79
- package/src/providers/traework/index.js +132 -132
- package/src/providers/traework/market-identity.js +116 -116
- package/src/providers/traework/models.js +98 -98
- package/src/providers/traework/payload.js +86 -86
- package/src/providers/traework/sse.js +184 -184
- package/src/providers/traework/token.js +52 -52
- package/src/providers/traework.js +2 -2
- package/src/providers/workbuddy/account-store.js +186 -186
- package/src/providers/workbuddy/auth.js +158 -158
- package/src/providers/workbuddy/balance.js +86 -86
- package/src/providers/workbuddy/cat-travel.js +125 -125
- package/src/providers/workbuddy/checkin.js +75 -75
- package/src/providers/workbuddy/desktop-info.js +78 -78
- package/src/providers/workbuddy/growth-api.js +67 -67
- package/src/providers/workbuddy/growth-plans.js +63 -63
- package/src/providers/workbuddy/growth.js +181 -181
- package/src/providers/workbuddy/index.js +217 -217
- package/src/providers/workbuddy/models.js +126 -126
- package/src/providers/workbuddy/payload.js +141 -141
- package/src/providers/workbuddy/rotation-log.js +83 -83
- package/src/providers/workbuddy/sanitize-tools.js +61 -61
- package/src/providers/workbuddy/sdk-chat.js +26 -26
- package/src/providers/workbuddy-balance.js +78 -78
- package/src/providers/zcode/account-store.js +129 -129
- package/src/providers/zcode/auth.js +28 -28
- package/src/providers/zcode/chat.js +179 -179
- package/src/providers/zcode/const.js +54 -54
- package/src/providers/zcode/context-shape.js +162 -162
- package/src/providers/zcode/headers.js +93 -93
- package/src/providers/zcode/index.js +124 -124
- package/src/providers/zcode/models.js +65 -65
- package/src/providers/zcode/oauth.js +120 -120
- package/src/providers/zcode/quota.js +176 -176
- package/src/providers/zcode/sse.js +196 -196
- package/src/readline-compat.js +25 -25
- package/src/responses/translate.js +311 -311
- package/src/routes/chat/broadband-handler.js +72 -72
- package/src/routes/chat/empty-turn-report.js +31 -31
- package/src/routes/chat/exhausted-handler.js +87 -87
- package/src/routes/chat/hedge-handler.js +130 -130
- package/src/routes/chat/index.js +1 -1
- package/src/routes/chat/local-handler.js +63 -63
- package/src/routes/chat/peer-handler.js +81 -81
- package/src/routes/chat.js +3 -3
- package/src/routes/fallback.js +88 -88
- package/src/routes/groups-relay.js +214 -214
- package/src/routes/groups.js +122 -122
- package/src/routes/helpers.js +83 -83
- package/src/routes/index.js +147 -147
- package/src/routes/messages-route.js +365 -365
- package/src/routes/peers.js +23 -1
- package/src/routes/relay-queue.js +181 -181
- package/src/routes/relay.js +60 -60
- package/src/routes/responses-route.js +213 -213
- package/src/routes/stream-body.js +84 -84
- package/src/routes/stream-hold.js +337 -337
- package/src/routes/stream-scan.js +366 -366
- package/src/routes.js +3 -3
- package/src/runtime/auto-update.js +113 -113
- package/src/runtime/bootstrap.js +8 -0
- package/src/runtime/broadband-stream.js +79 -79
- package/src/runtime/broadband.js +103 -103
- package/src/runtime/group-sync.js +81 -81
- package/src/runtime/keep-warm.js +120 -0
- package/src/runtime/lifecycle-forensics.js +116 -116
- package/src/runtime/lifecycle-log.js +87 -87
- package/src/runtime/log-stamp.js +73 -73
- package/src/runtime/proc-info.js +137 -137
- package/src/runtime/qoder-checkin.js +93 -93
- package/src/runtime/raccoon-checkin.js +126 -126
- package/src/runtime/server-lifecycle.js +189 -189
- package/src/runtime/traework-checkin.js +118 -118
- package/src/runtime/workbuddy-checkin.js +120 -120
- package/src/runtime/workbuddy-growth.js +133 -133
- package/src/server.js +7 -0
- package/src/state/facade.js +58 -58
- package/src/state/memory.js +162 -162
- package/src/state/merge.js +25 -25
- package/src/state/migrations/cline-unify.js +94 -94
- package/src/state/migrations.js +33 -33
- package/src/state/persist.js +42 -42
- package/src/state/provider-config.js +142 -142
- package/src/state/schemas/group.js +31 -31
- package/src/state/schemas/model.js +94 -94
- package/src/state/schemas/peer.js +31 -31
- package/src/state/schemas/port.js +10 -10
- package/src/state/schemas/provider.js +202 -202
- package/src/state/schemas/timezone.js +70 -70
- package/src/state/schemas/token.js +48 -48
- package/src/state/schemas/use-group.js +102 -102
- package/src/state/store.js +176 -176
- package/src/state.js +1 -1
- package/src/sync-claude-desktop.js +234 -234
- package/src/sync-claude.js +152 -152
- package/src/sync-codex.js +116 -116
- package/src/sync-workbuddy.js +158 -158
- package/src/talk-full-redact.js +129 -129
- package/src/talk-full.js +131 -131
- package/src/talk-log.js +257 -257
- package/src/text-tokens.js +52 -52
- package/src/thinking-effort.js +67 -67
- package/src/time.js +106 -106
- package/src/timeline.js +26 -26
- package/src/transport/index.js +1 -1
- package/src/upstream-engine/index.js +74 -72
- package/src/upstream-engine/mode.js +15 -15
- package/src/upstream-engine/sdk/attempt.js +221 -221
- package/src/upstream-engine/sdk/chat.js +28 -28
- package/src/upstream-engine/sdk/convert.js +192 -192
- package/src/upstream-engine/sdk/diagnose.js +62 -62
- package/src/upstream-engine/sdk/dispatch.js +55 -55
- package/src/upstream-engine/sdk/responses.js +176 -176
- package/src/upstream-engine/sdk/sse.js +166 -166
- package/src/upstream-responses.js +305 -305
- package/src/upstream.js +53 -1
- package/src/usage/record.js +106 -106
- package/src/usage/report.js +195 -195
- package/src/v6pool/claim.js +157 -157
- package/src/v6pool/config.js +65 -65
- package/src/v6pool/detect.js +130 -130
- package/src/v6pool/index.js +152 -152
- package/src/v6pool/prefix.js +140 -140
- package/src/v6pool/probe.js +40 -40
- package/src/v6pool/report.js +104 -104
- package/src/v6pool/rotate.js +263 -239
- package/src/warp-pool/config.js +74 -74
- package/src/warp-pool/health.js +106 -106
- package/src/warp-pool/identity.js +141 -141
- package/src/warp-pool/index.js +358 -358
- package/src/warp-pool/keys.js +29 -29
- package/src/warp-pool/nodes.js +95 -95
- package/src/warp-pool/ports.js +160 -160
- package/src/warp-pool/register.js +113 -113
- package/src/warp-pool/report.js +232 -232
- package/src/warp-pool/revive.js +168 -168
- package/src/warp-pool/rotate.js +214 -214
- package/src/warp-pool/strategy.js +43 -43
- package/src/warp-pool/verdict.js +69 -69
- package/src/warp-pool/wireproxy.js +162 -162
- package/docs/cli_help_mini.md +0 -146
- package/src/providers/AGENTS.md +0 -49
- package/src/routes/AGENTS.md +0 -37
package/src/v6pool/detect.js
CHANGED
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
// v6pool · 自动判断本机有没有可用的 IPv6 池(纯解析 + 可注入 IO,便于单测)。
|
|
2
|
-
//
|
|
3
|
-
// 四关,任一不过就诚实跳过(并给出「下一步该跑什么」):
|
|
4
|
-
// ① 平台:只有 Linux 能读 /proc 并用 ip 命令(与 wireproxy 预编译平台限制同口径);
|
|
5
|
-
// ② 地址:/proc/net/if_inet6 里必须有 scope=global 的地址;
|
|
6
|
-
// ③ 前缀:该地址的前缀主机位 ≥ minHostBits(/128 只有 1 个地址,不构成池);
|
|
7
|
-
// ④ 认领:ip_nonlocal_bind=1(直接 bind,零改动)或 root(能 ip addr add)。
|
|
8
|
-
//
|
|
9
|
-
// ⚠ 本模块**不假装知道**「整段有没有真路由到本机」——那只能靠 probe.js 从池里的地址实测。
|
|
10
|
-
// 内核接受地址(bind 成功)≠ 机房把回包送回来;两者混为一谈正是「池看起来就绪、请求全黑洞」的根因。
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
12
|
-
import { formatV6, hostBitsOf, networkOf, parsePrefix, parseV6 } from "./prefix.js";
|
|
13
|
-
|
|
14
|
-
/** /proc/net/if_inet6 的 scope 列:00=global、10=host、20=link、40=site(都是 hex)。 */
|
|
15
|
-
export const SCOPE_GLOBAL = 0;
|
|
16
|
-
|
|
17
|
-
/** 解析 /proc/net/if_inet6:`<32 位 hex 地址> <ifindex> <prefixlen> <scope> <flags> <网卡>`。 */
|
|
18
|
-
export function parseIfInet6(raw) {
|
|
19
|
-
const out = [];
|
|
20
|
-
for (const line of String(raw ?? "").split("\n")) {
|
|
21
|
-
const f = line.trim().split(/\s+/);
|
|
22
|
-
if (f.length < 6 || !/^[0-9a-f]{32}$/i.test(f[0])) continue;
|
|
23
|
-
const addr = parseV6(f[0].toLowerCase().match(/.{4}/g).join(":"));
|
|
24
|
-
if (addr === null) continue;
|
|
25
|
-
out.push({
|
|
26
|
-
addr,
|
|
27
|
-
text: formatV6(addr),
|
|
28
|
-
ifindex: parseInt(f[1], 16),
|
|
29
|
-
bits: parseInt(f[2], 16),
|
|
30
|
-
scope: parseInt(f[3], 16),
|
|
31
|
-
iface: f[5],
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
return out;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function skip(reason, hint) {
|
|
38
|
-
return { ok: false, reason, hint };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 四关判定:把「本机事实」变成「可用池 或 具体跳过原因」。
|
|
43
|
-
* 全部依赖注入(测试不用真读 /proc、不用真 root)。
|
|
44
|
-
*/
|
|
45
|
-
export function pickPool({
|
|
46
|
-
entries = [],
|
|
47
|
-
nonlocalBind = 0,
|
|
48
|
-
uid = -1,
|
|
49
|
-
platform = process.platform,
|
|
50
|
-
explicitPrefix = "",
|
|
51
|
-
explicitIface = "",
|
|
52
|
-
minHostBits = 8,
|
|
53
|
-
claim = "auto",
|
|
54
|
-
} = {}) {
|
|
55
|
-
if (platform !== "linux") {
|
|
56
|
-
return skip(`本平台(${platform})不支持:需要 Linux 的 /proc 与 ip 命令`, "非 Linux 机器不需要本功能(WARP 池同样只在 Linux 可用)");
|
|
57
|
-
}
|
|
58
|
-
const globals = entries.filter((e) => e.scope === SCOPE_GLOBAL);
|
|
59
|
-
let addr;
|
|
60
|
-
let bits;
|
|
61
|
-
let iface;
|
|
62
|
-
let source;
|
|
63
|
-
if (explicitPrefix) {
|
|
64
|
-
const p = parsePrefix(explicitPrefix);
|
|
65
|
-
if (!p) return skip(`MSLXDFF_V6_PREFIX 解析失败:${explicitPrefix}`, "格式形如 2001:db8:1:2::/64");
|
|
66
|
-
addr = p.addr;
|
|
67
|
-
bits = p.bits;
|
|
68
|
-
source = "env";
|
|
69
|
-
iface = explicitIface || globals[0]?.iface || "";
|
|
70
|
-
} else {
|
|
71
|
-
if (!globals.length) {
|
|
72
|
-
return skip("本机没有任何全局 IPv6 地址(只有 link-local / loopback)", "确认网卡上启用了 IPv6,且运营商或机房确实给了 IPv6");
|
|
73
|
-
}
|
|
74
|
-
const picked = (explicitIface && globals.find((e) => e.iface === explicitIface)) || globals[0];
|
|
75
|
-
addr = picked.addr;
|
|
76
|
-
bits = picked.bits;
|
|
77
|
-
iface = explicitIface || picked.iface;
|
|
78
|
-
source = "proc";
|
|
79
|
-
}
|
|
80
|
-
const hostBits = hostBitsOf(bits);
|
|
81
|
-
if (hostBits < minHostBits) {
|
|
82
|
-
return skip(
|
|
83
|
-
`前缀 /${bits} 只有 ${hostBits} 位主机位(< ${minHostBits}),不构成池`,
|
|
84
|
-
`该地址是 /${bits}:整段没路由到本机时换不了 IP。若确认整段可用,用 MSLXDFF_V6_PREFIX=<段>/<位数> 显式指定`,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
const mode = claim === "auto" ? (nonlocalBind === 1 ? "bind" : uid === 0 ? "add" : "none") : claim;
|
|
88
|
-
if (mode === "bind" && nonlocalBind !== 1) {
|
|
89
|
-
return skip("MSLXDFF_V6_CLAIM=bind 但 net.ipv6.ip_nonlocal_bind≠1", "sysctl -w net.ipv6.ip_nonlocal_bind=1,或改用 MSLXDFF_V6_CLAIM=add");
|
|
90
|
-
}
|
|
91
|
-
if (mode === "add" && uid !== 0) {
|
|
92
|
-
return skip(`MSLXDFF_V6_CLAIM=add 需要 root(当前 uid=${uid})`, "以 root 跑 daemon,或 sysctl -w net.ipv6.ip_nonlocal_bind=1 后设 MSLXDFF_V6_CLAIM=bind");
|
|
93
|
-
}
|
|
94
|
-
if (mode === "none") {
|
|
95
|
-
return skip("既没有 ip_nonlocal_bind=1,也不是 root ⇒ 内核会拒绝 bind 池里的地址(EADDRNOTAVAIL)", "二选一:sysctl -w net.ipv6.ip_nonlocal_bind=1,或以 root 跑(可 ip addr add)");
|
|
96
|
-
}
|
|
97
|
-
return {
|
|
98
|
-
ok: true,
|
|
99
|
-
addr,
|
|
100
|
-
bits,
|
|
101
|
-
iface,
|
|
102
|
-
hostBits,
|
|
103
|
-
mode,
|
|
104
|
-
source,
|
|
105
|
-
prefix: `${formatV6(networkOf(addr, bits))}/${bits}`,
|
|
106
|
-
/** 本机自己的全局地址:造池时避开它们(别把机器正在用的地址当池里的"新"地址) */
|
|
107
|
-
mine: globals.map((e) => e.text),
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** 真机路径:读 /proc 与 /proc/sys(可整体注入,测试零 IO)。 */
|
|
112
|
-
export function detectPool({ cfg = {}, entries, nonlocalBind, uid, platform = process.platform, readFile = (p) => readFileSync(p, "utf8") } = {}) {
|
|
113
|
-
const read = (p, dflt) => {
|
|
114
|
-
try {
|
|
115
|
-
return readFile(p);
|
|
116
|
-
} catch {
|
|
117
|
-
return dflt;
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
return pickPool({
|
|
121
|
-
entries: entries ?? parseIfInet6(read("/proc/net/if_inet6", "")),
|
|
122
|
-
nonlocalBind: nonlocalBind ?? (Number(String(read("/proc/sys/net/ipv6/ip_nonlocal_bind", "0")).trim()) || 0),
|
|
123
|
-
uid: uid ?? (typeof process.getuid === "function" ? process.getuid() : -1),
|
|
124
|
-
platform,
|
|
125
|
-
explicitPrefix: cfg.prefix || "",
|
|
126
|
-
explicitIface: cfg.iface || "",
|
|
127
|
-
minHostBits: cfg.minHostBits || 8,
|
|
128
|
-
claim: cfg.claim || "auto",
|
|
129
|
-
});
|
|
130
|
-
}
|
|
1
|
+
// v6pool · 自动判断本机有没有可用的 IPv6 池(纯解析 + 可注入 IO,便于单测)。
|
|
2
|
+
//
|
|
3
|
+
// 四关,任一不过就诚实跳过(并给出「下一步该跑什么」):
|
|
4
|
+
// ① 平台:只有 Linux 能读 /proc 并用 ip 命令(与 wireproxy 预编译平台限制同口径);
|
|
5
|
+
// ② 地址:/proc/net/if_inet6 里必须有 scope=global 的地址;
|
|
6
|
+
// ③ 前缀:该地址的前缀主机位 ≥ minHostBits(/128 只有 1 个地址,不构成池);
|
|
7
|
+
// ④ 认领:ip_nonlocal_bind=1(直接 bind,零改动)或 root(能 ip addr add)。
|
|
8
|
+
//
|
|
9
|
+
// ⚠ 本模块**不假装知道**「整段有没有真路由到本机」——那只能靠 probe.js 从池里的地址实测。
|
|
10
|
+
// 内核接受地址(bind 成功)≠ 机房把回包送回来;两者混为一谈正是「池看起来就绪、请求全黑洞」的根因。
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { formatV6, hostBitsOf, networkOf, parsePrefix, parseV6 } from "./prefix.js";
|
|
13
|
+
|
|
14
|
+
/** /proc/net/if_inet6 的 scope 列:00=global、10=host、20=link、40=site(都是 hex)。 */
|
|
15
|
+
export const SCOPE_GLOBAL = 0;
|
|
16
|
+
|
|
17
|
+
/** 解析 /proc/net/if_inet6:`<32 位 hex 地址> <ifindex> <prefixlen> <scope> <flags> <网卡>`。 */
|
|
18
|
+
export function parseIfInet6(raw) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const line of String(raw ?? "").split("\n")) {
|
|
21
|
+
const f = line.trim().split(/\s+/);
|
|
22
|
+
if (f.length < 6 || !/^[0-9a-f]{32}$/i.test(f[0])) continue;
|
|
23
|
+
const addr = parseV6(f[0].toLowerCase().match(/.{4}/g).join(":"));
|
|
24
|
+
if (addr === null) continue;
|
|
25
|
+
out.push({
|
|
26
|
+
addr,
|
|
27
|
+
text: formatV6(addr),
|
|
28
|
+
ifindex: parseInt(f[1], 16),
|
|
29
|
+
bits: parseInt(f[2], 16),
|
|
30
|
+
scope: parseInt(f[3], 16),
|
|
31
|
+
iface: f[5],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function skip(reason, hint) {
|
|
38
|
+
return { ok: false, reason, hint };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 四关判定:把「本机事实」变成「可用池 或 具体跳过原因」。
|
|
43
|
+
* 全部依赖注入(测试不用真读 /proc、不用真 root)。
|
|
44
|
+
*/
|
|
45
|
+
export function pickPool({
|
|
46
|
+
entries = [],
|
|
47
|
+
nonlocalBind = 0,
|
|
48
|
+
uid = -1,
|
|
49
|
+
platform = process.platform,
|
|
50
|
+
explicitPrefix = "",
|
|
51
|
+
explicitIface = "",
|
|
52
|
+
minHostBits = 8,
|
|
53
|
+
claim = "auto",
|
|
54
|
+
} = {}) {
|
|
55
|
+
if (platform !== "linux") {
|
|
56
|
+
return skip(`本平台(${platform})不支持:需要 Linux 的 /proc 与 ip 命令`, "非 Linux 机器不需要本功能(WARP 池同样只在 Linux 可用)");
|
|
57
|
+
}
|
|
58
|
+
const globals = entries.filter((e) => e.scope === SCOPE_GLOBAL);
|
|
59
|
+
let addr;
|
|
60
|
+
let bits;
|
|
61
|
+
let iface;
|
|
62
|
+
let source;
|
|
63
|
+
if (explicitPrefix) {
|
|
64
|
+
const p = parsePrefix(explicitPrefix);
|
|
65
|
+
if (!p) return skip(`MSLXDFF_V6_PREFIX 解析失败:${explicitPrefix}`, "格式形如 2001:db8:1:2::/64");
|
|
66
|
+
addr = p.addr;
|
|
67
|
+
bits = p.bits;
|
|
68
|
+
source = "env";
|
|
69
|
+
iface = explicitIface || globals[0]?.iface || "";
|
|
70
|
+
} else {
|
|
71
|
+
if (!globals.length) {
|
|
72
|
+
return skip("本机没有任何全局 IPv6 地址(只有 link-local / loopback)", "确认网卡上启用了 IPv6,且运营商或机房确实给了 IPv6");
|
|
73
|
+
}
|
|
74
|
+
const picked = (explicitIface && globals.find((e) => e.iface === explicitIface)) || globals[0];
|
|
75
|
+
addr = picked.addr;
|
|
76
|
+
bits = picked.bits;
|
|
77
|
+
iface = explicitIface || picked.iface;
|
|
78
|
+
source = "proc";
|
|
79
|
+
}
|
|
80
|
+
const hostBits = hostBitsOf(bits);
|
|
81
|
+
if (hostBits < minHostBits) {
|
|
82
|
+
return skip(
|
|
83
|
+
`前缀 /${bits} 只有 ${hostBits} 位主机位(< ${minHostBits}),不构成池`,
|
|
84
|
+
`该地址是 /${bits}:整段没路由到本机时换不了 IP。若确认整段可用,用 MSLXDFF_V6_PREFIX=<段>/<位数> 显式指定`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const mode = claim === "auto" ? (nonlocalBind === 1 ? "bind" : uid === 0 ? "add" : "none") : claim;
|
|
88
|
+
if (mode === "bind" && nonlocalBind !== 1) {
|
|
89
|
+
return skip("MSLXDFF_V6_CLAIM=bind 但 net.ipv6.ip_nonlocal_bind≠1", "sysctl -w net.ipv6.ip_nonlocal_bind=1,或改用 MSLXDFF_V6_CLAIM=add");
|
|
90
|
+
}
|
|
91
|
+
if (mode === "add" && uid !== 0) {
|
|
92
|
+
return skip(`MSLXDFF_V6_CLAIM=add 需要 root(当前 uid=${uid})`, "以 root 跑 daemon,或 sysctl -w net.ipv6.ip_nonlocal_bind=1 后设 MSLXDFF_V6_CLAIM=bind");
|
|
93
|
+
}
|
|
94
|
+
if (mode === "none") {
|
|
95
|
+
return skip("既没有 ip_nonlocal_bind=1,也不是 root ⇒ 内核会拒绝 bind 池里的地址(EADDRNOTAVAIL)", "二选一:sysctl -w net.ipv6.ip_nonlocal_bind=1,或以 root 跑(可 ip addr add)");
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
addr,
|
|
100
|
+
bits,
|
|
101
|
+
iface,
|
|
102
|
+
hostBits,
|
|
103
|
+
mode,
|
|
104
|
+
source,
|
|
105
|
+
prefix: `${formatV6(networkOf(addr, bits))}/${bits}`,
|
|
106
|
+
/** 本机自己的全局地址:造池时避开它们(别把机器正在用的地址当池里的"新"地址) */
|
|
107
|
+
mine: globals.map((e) => e.text),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 真机路径:读 /proc 与 /proc/sys(可整体注入,测试零 IO)。 */
|
|
112
|
+
export function detectPool({ cfg = {}, entries, nonlocalBind, uid, platform = process.platform, readFile = (p) => readFileSync(p, "utf8") } = {}) {
|
|
113
|
+
const read = (p, dflt) => {
|
|
114
|
+
try {
|
|
115
|
+
return readFile(p);
|
|
116
|
+
} catch {
|
|
117
|
+
return dflt;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
return pickPool({
|
|
121
|
+
entries: entries ?? parseIfInet6(read("/proc/net/if_inet6", "")),
|
|
122
|
+
nonlocalBind: nonlocalBind ?? (Number(String(read("/proc/sys/net/ipv6/ip_nonlocal_bind", "0")).trim()) || 0),
|
|
123
|
+
uid: uid ?? (typeof process.getuid === "function" ? process.getuid() : -1),
|
|
124
|
+
platform,
|
|
125
|
+
explicitPrefix: cfg.prefix || "",
|
|
126
|
+
explicitIface: cfg.iface || "",
|
|
127
|
+
minHostBits: cfg.minHostBits || 8,
|
|
128
|
+
claim: cfg.claim || "auto",
|
|
129
|
+
});
|
|
130
|
+
}
|
package/src/v6pool/index.js
CHANGED
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
// v6pool · 装配入口(MODULES.md §0/§1)。
|
|
2
|
-
//
|
|
3
|
-
// 定位:**自动判断**本机有没有可用的 IPv6 池;有就启用(出口 = 池里的源地址),
|
|
4
|
-
// 没有就诚实跳过一行日志(行为与今天完全一致)。缺省开,MSLXDFF_V6_POOL=0 关。
|
|
5
|
-
//
|
|
6
|
-
// 与 WARP 池的关系:v6 池可用 ⇒ 不建 WARP 池(v6 严格更优:地址无限、零子进程、换址成本≈0);
|
|
7
|
-
// 检测通过但探活全失败 ⇒ 池为空 ⇒ dispatcher 走直连兜底,日志点名 MSLXDFF_V6_POOL=0 可恢复 WARP。
|
|
8
|
-
import { getUndici } from "../compat.js";
|
|
9
|
-
import { resolveV6Config } from "./config.js";
|
|
10
|
-
import { detectPool } from "./detect.js";
|
|
11
|
-
import { createClaimer } from "./claim.js";
|
|
12
|
-
import { createV6Dispatcher } from "./rotate.js";
|
|
13
|
-
import { probeFrom } from "./probe.js";
|
|
14
|
-
import { randomAddress } from "./prefix.js";
|
|
15
|
-
import { fmtClose, fmtConfig, fmtDetect, fmtDisabled, fmtPool, fmtPoolEmpty, fmtProbe, fmtReadyFail, fmtSkip } from "./report.js";
|
|
16
|
-
import { createWarpDispatcher } from "../warp-pool/index.js";
|
|
17
|
-
|
|
18
|
-
/** 每槽 Agent 的连接池参数。注入 dispatcher 后 transport 不建自带池(transport/index.js L28),所以必须自带。 */
|
|
19
|
-
export const AGENT_OPTIONS = { connections: 4, pipelining: 1, keepAliveTimeout: 30_000, keepAliveMaxTimeout: 60_000 };
|
|
20
|
-
|
|
21
|
-
/** 初始地址集的去重重试上限(前缀足够大时一次就成)。 */
|
|
22
|
-
export const DISTINCT_TRIES = 16;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 建池。检测不过 / 平台不支持 / 无权限 / undici 缺失 ⇒ 返回 null(调用方据此不注入、退回原行为)。
|
|
26
|
-
* @returns {null | { dispatcher: object, ready: Promise<void>, config: object, detect: object,
|
|
27
|
-
* close: () => Promise<void>, stats: () => object }}
|
|
28
|
-
*/
|
|
29
|
-
export function createV6Pool({ env = process.env, baseUrl = "https://opencode.ai", log = (m) => console.error(m), deps = {} } = {}) {
|
|
30
|
-
const cfg = resolveV6Config(env);
|
|
31
|
-
if (!cfg.enabled) {
|
|
32
|
-
log(fmtDisabled());
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
log(fmtConfig(cfg));
|
|
36
|
-
|
|
37
|
-
const d = deps.detect ? deps.detect(cfg) : detectPool({ cfg, env });
|
|
38
|
-
if (!d.ok) {
|
|
39
|
-
log(fmtSkip(d));
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
log(fmtDetect(d));
|
|
43
|
-
|
|
44
|
-
const { Agent } = getUndici();
|
|
45
|
-
if (typeof Agent !== "function") {
|
|
46
|
-
log(fmtReadyFail({ error: "undici Agent 不可用(未安装 undici 且 Node <18)" }));
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const prefix = { addr: d.addr, bits: d.bits };
|
|
51
|
-
const mine = new Set(d.mine || []);
|
|
52
|
-
const agentFactory = deps.agentFactory || ((addr) => new Agent({ connect: { localAddress: addr }, ...AGENT_OPTIONS }));
|
|
53
|
-
const claimer = deps.claimer ? deps.claimer(d) : createClaimer({ iface: d.iface, mode: d.mode, dir: cfg.dir, log });
|
|
54
|
-
const probe = deps.probe || probeFrom;
|
|
55
|
-
|
|
56
|
-
/** 挑一个池内地址:避开本机自己的全局地址(别把机器正在用的地址当"新"地址)。 */
|
|
57
|
-
function pickAddress() {
|
|
58
|
-
for (let i = 0; i < DISTINCT_TRIES; i++) {
|
|
59
|
-
const a = randomAddress(prefix);
|
|
60
|
-
if (!mine.has(a)) return a;
|
|
61
|
-
}
|
|
62
|
-
return randomAddress(prefix);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const dispatcher = createV6Dispatcher({
|
|
66
|
-
addrs: [],
|
|
67
|
-
strategy: cfg.strategy,
|
|
68
|
-
retireMs: cfg.retireMs,
|
|
69
|
-
rotate: cfg.rotate,
|
|
70
|
-
debug: cfg.debug,
|
|
71
|
-
log,
|
|
72
|
-
claimer,
|
|
73
|
-
pickAddress,
|
|
74
|
-
agentFactory,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
let closed = false;
|
|
78
|
-
const ready = (async () => {
|
|
79
|
-
try {
|
|
80
|
-
await claimer.sweepStale();
|
|
81
|
-
const addrs = [];
|
|
82
|
-
const taken = new Set();
|
|
83
|
-
for (let i = 0; i < cfg.count; i++) {
|
|
84
|
-
if (closed) return;
|
|
85
|
-
let a = pickAddress();
|
|
86
|
-
for (let k = 0; k < DISTINCT_TRIES && taken.has(a); k++) a = pickAddress();
|
|
87
|
-
await claimer.ensure(a);
|
|
88
|
-
taken.add(a);
|
|
89
|
-
addrs.push(a);
|
|
90
|
-
}
|
|
91
|
-
// 逐个实证探活:内核接受了地址 ≠ 机房把整段路由到本机。探不通的当场释放,绝不进池。
|
|
92
|
-
const passed = [];
|
|
93
|
-
for (let i = 0; i < addrs.length; i++) {
|
|
94
|
-
if (closed) return;
|
|
95
|
-
const r = await probe({ addr: addrs[i], baseUrl, path: cfg.probePath, agentFactory });
|
|
96
|
-
log(fmtProbe({ addr: addrs[i], i: i + 1, n: addrs.length, ...r }));
|
|
97
|
-
if (r.ok) passed.push(addrs[i]);
|
|
98
|
-
else await claimer.release(addrs[i]);
|
|
99
|
-
}
|
|
100
|
-
if (!passed.length) {
|
|
101
|
-
log(fmtPoolEmpty({ tried: addrs.length }));
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
dispatcher.setAddresses(passed);
|
|
105
|
-
log(fmtPool({ prefix: d.prefix, addrs: passed, claimed: addrs.length, mode: d.mode }));
|
|
106
|
-
} catch (e) {
|
|
107
|
-
log(fmtReadyFail({ error: String(e?.message || e) }));
|
|
108
|
-
}
|
|
109
|
-
})();
|
|
110
|
-
|
|
111
|
-
// 退出钩子:`process.on("exit")` 里只能跑同步代码,但地址必须清——加进网卡的地址会改变内核的
|
|
112
|
-
// 默认源地址选择,影响同机其它服务(本轮手工实测踩过)。清不掉就交给下次启动的 sweepStale。
|
|
113
|
-
if (deps.exitHook !== false) {
|
|
114
|
-
process.once("exit", () => {
|
|
115
|
-
try {
|
|
116
|
-
claimer.releaseAllSync?.();
|
|
117
|
-
} catch {
|
|
118
|
-
/* 清残留兜底见 claim.js 的 sweepStale */
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
dispatcher,
|
|
125
|
-
ready,
|
|
126
|
-
config: cfg,
|
|
127
|
-
detect: d,
|
|
128
|
-
async close() {
|
|
129
|
-
if (closed) return;
|
|
130
|
-
closed = true;
|
|
131
|
-
await dispatcher.close();
|
|
132
|
-
const released = await claimer.releaseAll();
|
|
133
|
-
log(fmtClose({ released }));
|
|
134
|
-
},
|
|
135
|
-
stats() {
|
|
136
|
-
return { enabled: true, mode: d.mode, prefix: d.prefix, iface: d.iface, claimed: claimer.claimed(), ...dispatcher.stats() };
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 出口池二选一(唯一接线点用):v6 池可用就用它,否则退回 WARP 池。
|
|
143
|
-
* 两者都只产出一个 dispatcher 对象,注入点与契约完全一致(见 MODULES.md §1)。
|
|
144
|
-
* 放在这里而不是 providers-setup:那里是 10KB 硬门下的文件,多一行都是风险。
|
|
145
|
-
*/
|
|
146
|
-
export function createPreferredDispatcher({ baseUrl = "https://opencode.ai", env = process.env, log, deps = {} } = {}) {
|
|
147
|
-
const pool = createV6Pool({ env, baseUrl, log, deps });
|
|
148
|
-
if (pool) return pool.dispatcher;
|
|
149
|
-
// 单向依赖:warp-pool 不 import v6pool(见 MODULES.md §2「已知耦合」)。
|
|
150
|
-
return createWarpDispatcher({ baseUrl, env, log });
|
|
151
|
-
}
|
|
152
|
-
|
|
1
|
+
// v6pool · 装配入口(MODULES.md §0/§1)。
|
|
2
|
+
//
|
|
3
|
+
// 定位:**自动判断**本机有没有可用的 IPv6 池;有就启用(出口 = 池里的源地址),
|
|
4
|
+
// 没有就诚实跳过一行日志(行为与今天完全一致)。缺省开,MSLXDFF_V6_POOL=0 关。
|
|
5
|
+
//
|
|
6
|
+
// 与 WARP 池的关系:v6 池可用 ⇒ 不建 WARP 池(v6 严格更优:地址无限、零子进程、换址成本≈0);
|
|
7
|
+
// 检测通过但探活全失败 ⇒ 池为空 ⇒ dispatcher 走直连兜底,日志点名 MSLXDFF_V6_POOL=0 可恢复 WARP。
|
|
8
|
+
import { getUndici } from "../compat.js";
|
|
9
|
+
import { resolveV6Config } from "./config.js";
|
|
10
|
+
import { detectPool } from "./detect.js";
|
|
11
|
+
import { createClaimer } from "./claim.js";
|
|
12
|
+
import { createV6Dispatcher } from "./rotate.js";
|
|
13
|
+
import { probeFrom } from "./probe.js";
|
|
14
|
+
import { randomAddress } from "./prefix.js";
|
|
15
|
+
import { fmtClose, fmtConfig, fmtDetect, fmtDisabled, fmtPool, fmtPoolEmpty, fmtProbe, fmtReadyFail, fmtSkip } from "./report.js";
|
|
16
|
+
import { createWarpDispatcher } from "../warp-pool/index.js";
|
|
17
|
+
|
|
18
|
+
/** 每槽 Agent 的连接池参数。注入 dispatcher 后 transport 不建自带池(transport/index.js L28),所以必须自带。 */
|
|
19
|
+
export const AGENT_OPTIONS = { connections: 4, pipelining: 1, keepAliveTimeout: 30_000, keepAliveMaxTimeout: 60_000 };
|
|
20
|
+
|
|
21
|
+
/** 初始地址集的去重重试上限(前缀足够大时一次就成)。 */
|
|
22
|
+
export const DISTINCT_TRIES = 16;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 建池。检测不过 / 平台不支持 / 无权限 / undici 缺失 ⇒ 返回 null(调用方据此不注入、退回原行为)。
|
|
26
|
+
* @returns {null | { dispatcher: object, ready: Promise<void>, config: object, detect: object,
|
|
27
|
+
* close: () => Promise<void>, stats: () => object }}
|
|
28
|
+
*/
|
|
29
|
+
export function createV6Pool({ env = process.env, baseUrl = "https://opencode.ai", log = (m) => console.error(m), deps = {} } = {}) {
|
|
30
|
+
const cfg = resolveV6Config(env);
|
|
31
|
+
if (!cfg.enabled) {
|
|
32
|
+
log(fmtDisabled());
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
log(fmtConfig(cfg));
|
|
36
|
+
|
|
37
|
+
const d = deps.detect ? deps.detect(cfg) : detectPool({ cfg, env });
|
|
38
|
+
if (!d.ok) {
|
|
39
|
+
log(fmtSkip(d));
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
log(fmtDetect(d));
|
|
43
|
+
|
|
44
|
+
const { Agent } = getUndici();
|
|
45
|
+
if (typeof Agent !== "function") {
|
|
46
|
+
log(fmtReadyFail({ error: "undici Agent 不可用(未安装 undici 且 Node <18)" }));
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const prefix = { addr: d.addr, bits: d.bits };
|
|
51
|
+
const mine = new Set(d.mine || []);
|
|
52
|
+
const agentFactory = deps.agentFactory || ((addr) => new Agent({ connect: { localAddress: addr }, ...AGENT_OPTIONS }));
|
|
53
|
+
const claimer = deps.claimer ? deps.claimer(d) : createClaimer({ iface: d.iface, mode: d.mode, dir: cfg.dir, log });
|
|
54
|
+
const probe = deps.probe || probeFrom;
|
|
55
|
+
|
|
56
|
+
/** 挑一个池内地址:避开本机自己的全局地址(别把机器正在用的地址当"新"地址)。 */
|
|
57
|
+
function pickAddress() {
|
|
58
|
+
for (let i = 0; i < DISTINCT_TRIES; i++) {
|
|
59
|
+
const a = randomAddress(prefix);
|
|
60
|
+
if (!mine.has(a)) return a;
|
|
61
|
+
}
|
|
62
|
+
return randomAddress(prefix);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const dispatcher = createV6Dispatcher({
|
|
66
|
+
addrs: [],
|
|
67
|
+
strategy: cfg.strategy,
|
|
68
|
+
retireMs: cfg.retireMs,
|
|
69
|
+
rotate: cfg.rotate,
|
|
70
|
+
debug: cfg.debug,
|
|
71
|
+
log,
|
|
72
|
+
claimer,
|
|
73
|
+
pickAddress,
|
|
74
|
+
agentFactory,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
let closed = false;
|
|
78
|
+
const ready = (async () => {
|
|
79
|
+
try {
|
|
80
|
+
await claimer.sweepStale();
|
|
81
|
+
const addrs = [];
|
|
82
|
+
const taken = new Set();
|
|
83
|
+
for (let i = 0; i < cfg.count; i++) {
|
|
84
|
+
if (closed) return;
|
|
85
|
+
let a = pickAddress();
|
|
86
|
+
for (let k = 0; k < DISTINCT_TRIES && taken.has(a); k++) a = pickAddress();
|
|
87
|
+
await claimer.ensure(a);
|
|
88
|
+
taken.add(a);
|
|
89
|
+
addrs.push(a);
|
|
90
|
+
}
|
|
91
|
+
// 逐个实证探活:内核接受了地址 ≠ 机房把整段路由到本机。探不通的当场释放,绝不进池。
|
|
92
|
+
const passed = [];
|
|
93
|
+
for (let i = 0; i < addrs.length; i++) {
|
|
94
|
+
if (closed) return;
|
|
95
|
+
const r = await probe({ addr: addrs[i], baseUrl, path: cfg.probePath, agentFactory });
|
|
96
|
+
log(fmtProbe({ addr: addrs[i], i: i + 1, n: addrs.length, ...r }));
|
|
97
|
+
if (r.ok) passed.push(addrs[i]);
|
|
98
|
+
else await claimer.release(addrs[i]);
|
|
99
|
+
}
|
|
100
|
+
if (!passed.length) {
|
|
101
|
+
log(fmtPoolEmpty({ tried: addrs.length }));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
dispatcher.setAddresses(passed);
|
|
105
|
+
log(fmtPool({ prefix: d.prefix, addrs: passed, claimed: addrs.length, mode: d.mode }));
|
|
106
|
+
} catch (e) {
|
|
107
|
+
log(fmtReadyFail({ error: String(e?.message || e) }));
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
|
|
111
|
+
// 退出钩子:`process.on("exit")` 里只能跑同步代码,但地址必须清——加进网卡的地址会改变内核的
|
|
112
|
+
// 默认源地址选择,影响同机其它服务(本轮手工实测踩过)。清不掉就交给下次启动的 sweepStale。
|
|
113
|
+
if (deps.exitHook !== false) {
|
|
114
|
+
process.once("exit", () => {
|
|
115
|
+
try {
|
|
116
|
+
claimer.releaseAllSync?.();
|
|
117
|
+
} catch {
|
|
118
|
+
/* 清残留兜底见 claim.js 的 sweepStale */
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
dispatcher,
|
|
125
|
+
ready,
|
|
126
|
+
config: cfg,
|
|
127
|
+
detect: d,
|
|
128
|
+
async close() {
|
|
129
|
+
if (closed) return;
|
|
130
|
+
closed = true;
|
|
131
|
+
await dispatcher.close();
|
|
132
|
+
const released = await claimer.releaseAll();
|
|
133
|
+
log(fmtClose({ released }));
|
|
134
|
+
},
|
|
135
|
+
stats() {
|
|
136
|
+
return { enabled: true, mode: d.mode, prefix: d.prefix, iface: d.iface, claimed: claimer.claimed(), ...dispatcher.stats() };
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 出口池二选一(唯一接线点用):v6 池可用就用它,否则退回 WARP 池。
|
|
143
|
+
* 两者都只产出一个 dispatcher 对象,注入点与契约完全一致(见 MODULES.md §1)。
|
|
144
|
+
* 放在这里而不是 providers-setup:那里是 10KB 硬门下的文件,多一行都是风险。
|
|
145
|
+
*/
|
|
146
|
+
export function createPreferredDispatcher({ baseUrl = "https://opencode.ai", env = process.env, log, deps = {} } = {}) {
|
|
147
|
+
const pool = createV6Pool({ env, baseUrl, log, deps });
|
|
148
|
+
if (pool) return pool.dispatcher;
|
|
149
|
+
// 单向依赖:warp-pool 不 import v6pool(见 MODULES.md §2「已知耦合」)。
|
|
150
|
+
return createWarpDispatcher({ baseUrl, env, log });
|
|
151
|
+
}
|
|
152
|
+
|