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/warp-pool/index.js
CHANGED
|
@@ -1,358 +1,358 @@
|
|
|
1
|
-
// warp-pool · 装配入口。
|
|
2
|
-
//
|
|
3
|
-
// 定位(MODULES.md §0/§10):WARP 池是**增量**出口,不是替代成员自身出口;缺省**开**,MSLXDFF_WARP_POOL=0 关。
|
|
4
|
-
import { statSync } from "node:fs";
|
|
5
|
-
import { resolveWarpConfig } from "./config.js";
|
|
6
|
-
import { createRotatingDispatcher } from "./rotate.js";
|
|
7
|
-
import { classifyWarpSignal, SIGNAL_STATUSES } from "./verdict.js";
|
|
8
|
-
import { createHealthMonitor } from "./health.js";
|
|
9
|
-
import { ensureDir, loadNodes, removeNode, saveNode } from "./nodes.js";
|
|
10
|
-
import { registerWarpDevice } from "./register.js";
|
|
11
|
-
import { ensureBinary, probeTunnel, resolveAsset, startTunnel, writeTunnelConf } from "./wireproxy.js";
|
|
12
|
-
import { createReviver, labelExits, tailReason } from "./revive.js";
|
|
13
|
-
import { portInUse, portOwner, reclaimPort, waitPortFree } from "./ports.js";
|
|
14
|
-
import { fmtConf, fmtExit, fmtIdentityDetail, fmtPool, fmtPoolEmpty, fmtProbe, fmtProbeGiveUp, fmtPortBlocked, fmtSiblings, fmtSpawn, startupReport } from "./report.js";
|
|
15
|
-
import { wireRotation } from "./identity.js";
|
|
16
|
-
import { findDaemons } from "../runtime/proc-info.js";
|
|
17
|
-
|
|
18
|
-
/** 重拨节流:连续失败指数退避。网络不通的机器绝不能让重注册把 CF 刷到 429。 */
|
|
19
|
-
const REDIAL_MIN_GAP_MS = 5 * 60_000;
|
|
20
|
-
const REDIAL_MAX_GAP_MS = 30 * 60_000;
|
|
21
|
-
|
|
22
|
-
/** 注册间隔(同类项目实测:密集注册会让设备落同一网段,且触发 CF 429)。 */
|
|
23
|
-
function gapMs(cfg, rnd = Math.random) {
|
|
24
|
-
const span = Math.max(0, cfg.registerMaxGapMs - cfg.registerMinGapMs);
|
|
25
|
-
return Math.round(cfg.registerMinGapMs + rnd() * span);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 注册响应 → 节点对象(bootstrap 与重拨共用,避免同一份字段表写两遍)。 */
|
|
29
|
-
function nodeFromReg(port, reg, now) {
|
|
30
|
-
return { port, id: reg.id, token: reg.token, v4: reg.v4, v6: reg.v6, endpoint: reg.endpoint, peerKey: reg.peerKey, privateKey: reg.privateKey, registeredAt: new Date(now()).toISOString() };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 建池。开关缺省关,故「未启用」= 未设 MSLXDFF_WARP_POOL=1 或 非 leader;那时返回 null,调用方据此不注入。
|
|
35
|
-
* @returns {null | { dispatcher: object, ready: Promise<void>, close: () => Promise<void>, stats: () => object }}
|
|
36
|
-
*/
|
|
37
|
-
export function createWarpPool({
|
|
38
|
-
env = process.env,
|
|
39
|
-
baseUrl = "https://opencode.ai",
|
|
40
|
-
log = (m) => console.error(m),
|
|
41
|
-
register = registerWarpDevice,
|
|
42
|
-
ensureBin = ensureBinary,
|
|
43
|
-
start = startTunnel,
|
|
44
|
-
probe = probeTunnel,
|
|
45
|
-
reclaim = reclaimPort,
|
|
46
|
-
inUse = portInUse,
|
|
47
|
-
waitFree = waitPortFree,
|
|
48
|
-
now = Date.now,
|
|
49
|
-
rnd = Math.random,
|
|
50
|
-
wait = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
51
|
-
} = {}) {
|
|
52
|
-
const cfg = resolveWarpConfig(env);
|
|
53
|
-
if (!cfg.active) return null;
|
|
54
|
-
|
|
55
|
-
/** 身份轮换器:上游按出口 IP 计的限额(403 FreeTierError / 429 FreeUsage·GoUsage / 裸 429)时换一台 WARP 设备换出口。 */
|
|
56
|
-
let rotator = null;
|
|
57
|
-
|
|
58
|
-
const dispatcher = createRotatingDispatcher({
|
|
59
|
-
nodes: [],
|
|
60
|
-
strategy: cfg.strategy,
|
|
61
|
-
cooldownMs: cfg.cooldownMs,
|
|
62
|
-
log,
|
|
63
|
-
// 判决照抄参照实现 zen-router 的决策表(见 verdict.js 头注):403 FreeTierError 与
|
|
64
|
-
// 429 FreeUsage/GoUsage 是同一条件、必须**强制**换出口(跳过冷却);BlackUsageLimitError
|
|
65
|
-
// 是账号日配额、绝不换;其余状态码不参与判决、也不许刷日志。
|
|
66
|
-
onStatus: (signal, port) => {
|
|
67
|
-
const status = Number(signal?.status);
|
|
68
|
-
if (!SIGNAL_STATUSES.includes(status)) return;
|
|
69
|
-
const v = classifyWarpSignal(signal);
|
|
70
|
-
if (!cfg.rotate) {
|
|
71
|
-
log(`[warp-pool] :${port} 上游 ${status}(${v.kind})—— 轮换开关关着(MSLXDFF_WARP_ROTATE=0),不换出口`);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (!v.rotate || !rotator) {
|
|
75
|
-
log(`[warp-pool] :${port} 上游 ${status} 判为「${v.kind}」⇒ 不换出口:${v.why}`);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
log(`[warp-pool] :${port} 上游 ${status} 判为「${v.kind}」⇒ 换身份换出口${v.forced ? "(强制,跳过冷却)" : ""}:${v.why}`);
|
|
79
|
-
void rotator.rotate(port, { force: v.forced })
|
|
80
|
-
.then((r) => { if (r && r.skipped) log(`[warp-pool] :${port} 换出口被冷却掐掉(距上次不足最小间隔)—— 这一发仍走原出口`); })
|
|
81
|
-
.catch(() => {});
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
/** @type {Map<number, {stop: () => void}>} */
|
|
85
|
-
const tunnels = new Map();
|
|
86
|
-
|
|
87
|
-
/** 复活:用磁盘已有 conf 重起,不重注册(进程挂了 ≠ 设备失效,省 CF 配额)。 */
|
|
88
|
-
async function reviveTunnel(port) {
|
|
89
|
-
if (closed) return false;
|
|
90
|
-
const node = loadNodes(cfg.dir, [port])[0];
|
|
91
|
-
if (!node) {
|
|
92
|
-
log(`[warp-pool] :${port} 放弃复活:磁盘上没有该节点的身份(node-${port}.json 缺失或缺私钥)`);
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
await bringUp(node);
|
|
97
|
-
} catch (e) {
|
|
98
|
-
log(`[warp-pool] :${port} 复活起隧道失败:${String(e?.message || e)}`);
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
return true; // 真活没活由 createReviver 在 settleMs 后用 isAlive 复查,这里不许冒充成功
|
|
102
|
-
}
|
|
103
|
-
const reviver = createReviver({ revive: reviveTunnel, isAlive: (port) => { const t = tunnels.get(port); return !!t && t.alive !== false; }, log });
|
|
104
|
-
let binPath = "";
|
|
105
|
-
let closed = false;
|
|
106
|
-
let lastRedialAt = 0;
|
|
107
|
-
let redialGapMs = REDIAL_MIN_GAP_MS;
|
|
108
|
-
|
|
109
|
-
// 兜底清理:Node 退出不会回收 spawn 的子进程,daemon 突然退出时同步杀掉隧道。
|
|
110
|
-
// 同时置 closed:否则「杀子进程 → onExit → 立刻复活」会在退出过程中再 spawn 出一批孤儿(真机:每次重启漏一批)。
|
|
111
|
-
process.once("exit", () => {
|
|
112
|
-
closed = true;
|
|
113
|
-
try {
|
|
114
|
-
reviver.stop();
|
|
115
|
-
} catch {}
|
|
116
|
-
for (const t of tunnels.values()) {
|
|
117
|
-
try {
|
|
118
|
-
t.stop();
|
|
119
|
-
} catch {}
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
/** 起一条隧道的完整动作:确认不是重复起 → 清残留 → 写 conf → 等端口真空出来 → spawn → 记表。 */
|
|
124
|
-
async function bringUp(node) {
|
|
125
|
-
if (tunnels.has(node.port)) return; // 已归我们管:绝不再清残留(reclaim 会把自己这条隧道当残留杀掉)
|
|
126
|
-
await reclaim({ dir: cfg.dir, port: node.port, log }); // 清上一代孤儿(daemon 被强杀时子进程不会自己退)
|
|
127
|
-
const { conf, bytes } = writeTunnelConf(cfg.dir, node);
|
|
128
|
-
log(fmtConf({ port: node.port, conf, bytes }));
|
|
129
|
-
// SIGTERM 之后套接字要几百毫秒才释放,这期间连得上 ≠ 别人的进程占着——先等它真空出来再判
|
|
130
|
-
if (!(await waitFree(node.port, { inUse }))) {
|
|
131
|
-
const owner = await portOwner({ dir: cfg.dir, port: node.port }).catch(() => ({ ours: [], others: [] }));
|
|
132
|
-
throw new Error(fmtPortBlocked({ port: node.port, ...owner }));
|
|
133
|
-
}
|
|
134
|
-
const t = start({
|
|
135
|
-
binPath,
|
|
136
|
-
dir: cfg.dir,
|
|
137
|
-
port: node.port,
|
|
138
|
-
log,
|
|
139
|
-
onExit: ({ port, code, signal, tail, pid, aliveMs }) => {
|
|
140
|
-
tunnels.delete(port);
|
|
141
|
-
log(fmtExit({ port, code, signal, pid, aliveMs, tail: tailReason(tail) }));
|
|
142
|
-
if (!closed) void reviver.onExit(port); // 收摊中就别复活了,否则退出过程里又 spawn 出一批孤儿
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
tunnels.set(node.port, t);
|
|
146
|
-
log(fmtSpawn({ port: node.port, pid: t.pid, binPath, conf }));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 429 = 上游按出口 IP 计的名额耗尽 ⇒ 换一台设备的身份换出口(换身份=换桶)。
|
|
150
|
-
rotator = wireRotation({
|
|
151
|
-
dispatcher,
|
|
152
|
-
register,
|
|
153
|
-
tunnels,
|
|
154
|
-
bringUp,
|
|
155
|
-
swap: (port, node) => { removeNode(cfg.dir, port); saveNode(cfg.dir, node); },
|
|
156
|
-
log,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
/** 探活闸门参数:刚起的隧道要等 WireGuard 握手完成才通(真机:一起来就探 ⇒ 4 条全没通 ⇒ 池为空、WARP 白开)。 */
|
|
160
|
-
const PROBE_SETTLE_MS = 2_000;
|
|
161
|
-
const PROBE_ATTEMPTS = 2;
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* 入池前的实测闸门:给握手留时间 → 逐个探活 → 探不通的绝不进池。
|
|
165
|
-
* 为什么必须有:wireproxy 进程活着但隧道转不动流量时,请求会挂到超时(比直连更糟)。
|
|
166
|
-
* @returns {Promise<Array<object>>} 通过探活的节点
|
|
167
|
-
*/
|
|
168
|
-
async function probeGate(list) {
|
|
169
|
-
const live = [];
|
|
170
|
-
await wait(PROBE_SETTLE_MS);
|
|
171
|
-
for (const n of list) {
|
|
172
|
-
if (!tunnels.has(n.port)) continue;
|
|
173
|
-
const url = `${String(baseUrl).replace(/\/+$/, "")}${cfg.probePath}`;
|
|
174
|
-
let r = { ok: false };
|
|
175
|
-
for (let i = 1; i <= PROBE_ATTEMPTS; i++) {
|
|
176
|
-
const t0 = now();
|
|
177
|
-
r = await probe({ port: n.port, baseUrl, path: cfg.probePath });
|
|
178
|
-
const ms = Number.isFinite(r.ms) ? r.ms : now() - t0;
|
|
179
|
-
log(fmtProbe({ port: n.port, attempt: i, attempts: PROBE_ATTEMPTS, url, ok: r.ok, status: r.status, error: r.error, ms }));
|
|
180
|
-
if (r.ok) break;
|
|
181
|
-
if (i < PROBE_ATTEMPTS) await wait(PROBE_SETTLE_MS);
|
|
182
|
-
}
|
|
183
|
-
if (r.ok) {
|
|
184
|
-
live.push(n);
|
|
185
|
-
} else {
|
|
186
|
-
log(fmtProbeGiveUp({ port: n.port, attempts: PROBE_ATTEMPTS, url }));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return live;
|
|
190
|
-
}
|
|
191
|
-
async function bootstrap() {
|
|
192
|
-
if (closed) return;
|
|
193
|
-
ensureDir(cfg.dir);
|
|
194
|
-
try {
|
|
195
|
-
binPath = await ensureBin({ dir: cfg.dir, bin: cfg.bin, autodownload: cfg.autodownload, log });
|
|
196
|
-
} catch (e) {
|
|
197
|
-
const why = !cfg.bin && !resolveAsset()
|
|
198
|
-
? `本平台(${process.platform}-${process.arch})无预编译 wireproxy,如需启用请设 MSLXDFF_WARP_BIN`
|
|
199
|
-
: String(e?.message || e);
|
|
200
|
-
log(`[warp-pool] 出口池本次跳过(不启用,请求走直连):${why}`);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
if (closed) return;
|
|
204
|
-
|
|
205
|
-
// 1. 复用磁盘上已注册的节点
|
|
206
|
-
let nodes = loadNodes(cfg.dir, cfg.ports);
|
|
207
|
-
let binSize = 0;
|
|
208
|
-
try { binSize = statSync(binPath).size; } catch {}
|
|
209
|
-
await startupReport({ cfg, nodes, binPath, binSize, ownerOf: (port) => portOwner({ dir: cfg.dir, port }), log });
|
|
210
|
-
// 身份指纹:4 条隧道只拿到 2 个出口时,只有这组读数能区分「设备写重复」与「CF 共享 NAT 折叠」
|
|
211
|
-
for (const line of fmtIdentityDetail(nodes)) log(line);
|
|
212
|
-
// 同机其它 daemon:两个 daemon 会互相把对方的活隧道当「残留」杀掉(真机事故根因),必须一开池就报出来
|
|
213
|
-
const siblings = findDaemons({ self: process.pid });
|
|
214
|
-
if (siblings.length) log(fmtSiblings({ list: siblings }));
|
|
215
|
-
|
|
216
|
-
// 2. 补足:逐个注册(拉长间隔,避免 CF 注册限流与同网段撞车)
|
|
217
|
-
const used = new Set(nodes.map((n) => n.port));
|
|
218
|
-
const free = cfg.ports.filter((p) => !used.has(p));
|
|
219
|
-
for (const port of free) {
|
|
220
|
-
if (closed) return;
|
|
221
|
-
if (nodes.length >= cfg.count) break;
|
|
222
|
-
try {
|
|
223
|
-
if (nodes.length > 0) await wait(gapMs(cfg, rnd));
|
|
224
|
-
const reg = await register({ log });
|
|
225
|
-
const node = nodeFromReg(port, reg, now);
|
|
226
|
-
saveNode(cfg.dir, node);
|
|
227
|
-
nodes.push(node);
|
|
228
|
-
log(`[warp-pool] 注册新身份 :${port} 成功(设备 v6=${reg.v6},WireGuard 密钥对在本地生成)`);
|
|
229
|
-
} catch (e) {
|
|
230
|
-
log(`[warp-pool] :${port} 注册身份失败,本轮跳过这个端口:${String(e?.message || e)}`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// 3. 起隧道
|
|
235
|
-
for (const n of nodes) {
|
|
236
|
-
try {
|
|
237
|
-
await bringUp(n);
|
|
238
|
-
} catch (e) {
|
|
239
|
-
log(`[warp-pool] :${n.port} 起隧道失败,这个端口本轮不可用:${String(e?.message || e)}`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// 入池前实测探活:WARP 握手没完成时 wireproxy 在跑却转不动流量(请求会挂到超时),那种节点进池会黑洞全部请求
|
|
244
|
-
const live = await probeGate(nodes);
|
|
245
|
-
if (live.length) {
|
|
246
|
-
await dispatcher.setNodes(live.map((n) => ({ port: n.port, uri: `http://127.0.0.1:${n.port}` })));
|
|
247
|
-
// 出口取证:一次拿到每个端口的真实出口,既喂给轮换器做「换没换动」的比对,也直接写进池结论那一行
|
|
248
|
-
const exits = await labelExits({ ports: live.map((n) => n.port), dir: cfg.dir, log });
|
|
249
|
-
rotator.noteExits(exits);
|
|
250
|
-
log(fmtPool({ pooled: live.length, total: nodes.length, exits }));
|
|
251
|
-
} else {
|
|
252
|
-
log(fmtPoolEmpty({ total: nodes.length, ports: cfg.ports }));
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const monitor = createHealthMonitor({
|
|
257
|
-
dispatcher,
|
|
258
|
-
baseUrl,
|
|
259
|
-
probePath: cfg.probePath,
|
|
260
|
-
log,
|
|
261
|
-
infoOf: () => ({ alive: tunnels.size, pooled: dispatcher.size, exits: rotator ? rotator.stats() : [] }),
|
|
262
|
-
cooldownMs: cfg.cooldownMs,
|
|
263
|
-
onExhausted: async () => {
|
|
264
|
-
// 重拨 = 重新注册换出口。只重拨「进程已死」的隧道:全活却全挂 = 网络路径问题,重注册只会把 CF 刷到 429
|
|
265
|
-
const plan = redialPlan({ ports: cfg.ports, alivePorts: [...tunnels.keys()], lastRedialAt, redialGapMs, now });
|
|
266
|
-
if (plan.reason === "all-alive") {
|
|
267
|
-
log("[warp-pool] 隧道均存活但全池不通,判定为网络路径问题,跳过重拨");
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (plan.reason === "throttled") {
|
|
271
|
-
log(`[warp-pool] 距上次重拨不足 ${Math.round(redialGapMs / 1000)}s,跳过本轮`);
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
const dead = plan.dead;
|
|
275
|
-
lastRedialAt = now();
|
|
276
|
-
redialGapMs = Math.min(redialGapMs * 2, REDIAL_MAX_GAP_MS);
|
|
277
|
-
log(`[warp-pool] ${dead.length} 条隧道已死,重拨(下次最短间隔 ${Math.round(redialGapMs / 1000)}s)`);
|
|
278
|
-
for (const port of dead) {
|
|
279
|
-
if (closed) return;
|
|
280
|
-
try {
|
|
281
|
-
tunnels.get(port)?.stop();
|
|
282
|
-
tunnels.delete(port);
|
|
283
|
-
removeNode(cfg.dir, port);
|
|
284
|
-
const reg = await register({ log });
|
|
285
|
-
saveNode(cfg.dir, nodeFromReg(port, reg, now));
|
|
286
|
-
await bringUp({ port, ...reg });
|
|
287
|
-
} catch (e) {
|
|
288
|
-
log(`[warp-pool] 重拨 :${port} 失败:${String(e?.message || e)}`);
|
|
289
|
-
}
|
|
290
|
-
await wait(gapMs(cfg, rnd));
|
|
291
|
-
}
|
|
292
|
-
if (!closed && tunnels.size) {
|
|
293
|
-
// 重拨起来的隧道同样要过闸门:没过探活的隧道进池 = 把请求挂到超时(原先这里直接 setNodes,等于绕过闸门)
|
|
294
|
-
const alive = await probeGate([...tunnels.keys()].map((port) => ({ port })));
|
|
295
|
-
if (alive.length) {
|
|
296
|
-
redialGapMs = REDIAL_MIN_GAP_MS; // 确实有隧道能通 ⇒ 退避复位
|
|
297
|
-
await dispatcher.setNodes(alive.map((n) => ({ port: n.port, uri: `http://127.0.0.1:${n.port}` })));
|
|
298
|
-
} else {
|
|
299
|
-
log("[warp-pool] 重拨后仍没有一条隧道探通,保持直连(等下一轮健康检查)");
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
const ready = bootstrap()
|
|
306
|
-
.then(() => {
|
|
307
|
-
if (!closed) monitor.start();
|
|
308
|
-
})
|
|
309
|
-
.catch((e) => log(`[warp-pool] bootstrap 失败:${String(e?.message || e)}`));
|
|
310
|
-
|
|
311
|
-
return {
|
|
312
|
-
dispatcher,
|
|
313
|
-
ready,
|
|
314
|
-
config: cfg,
|
|
315
|
-
async close() {
|
|
316
|
-
if (closed) return;
|
|
317
|
-
closed = true;
|
|
318
|
-
monitor.stop();
|
|
319
|
-
reviver.stop();
|
|
320
|
-
for (const t of tunnels.values()) {
|
|
321
|
-
try {
|
|
322
|
-
t.stop();
|
|
323
|
-
} catch {}
|
|
324
|
-
}
|
|
325
|
-
tunnels.clear();
|
|
326
|
-
await dispatcher.close();
|
|
327
|
-
},
|
|
328
|
-
stats() {
|
|
329
|
-
return {
|
|
330
|
-
enabled: true,
|
|
331
|
-
mode: cfg.mode,
|
|
332
|
-
strategy: cfg.strategy,
|
|
333
|
-
dir: cfg.dir,
|
|
334
|
-
tunnels: tunnels.size,
|
|
335
|
-
nodes: dispatcher.stats(),
|
|
336
|
-
exits: rotator ? rotator.stats() : [],
|
|
337
|
-
};
|
|
338
|
-
},
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/** 只取轮换 dispatcher 的便捷入口;未启用时返回 undefined(调用方直接透传即"关闭态")。 */
|
|
343
|
-
/** 隧道进程由模块内的 process exit 钩子兜底清理。 */
|
|
344
|
-
export function createWarpDispatcher(opts) {
|
|
345
|
-
return createWarpPool(opts)?.dispatcher;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* 重拨门(纯函数,便于单测):只「有隧道进程已死」且「距上次重拨超过退避间隔」才放行——全活却全挂是网络问题。
|
|
350
|
-
* @returns {{ dead: number[], reason: "ok" | "all-alive" | "throttled" }}
|
|
351
|
-
*/
|
|
352
|
-
export function redialPlan({ ports = [], alivePorts = [], lastRedialAt = 0, redialGapMs = REDIAL_MIN_GAP_MS, now = Date.now } = {}) {
|
|
353
|
-
const alive = new Set(alivePorts);
|
|
354
|
-
const dead = ports.filter((p) => !alive.has(p));
|
|
355
|
-
if (!dead.length) return { dead, reason: "all-alive" };
|
|
356
|
-
if (now() - lastRedialAt < redialGapMs) return { dead, reason: "throttled" };
|
|
357
|
-
return { dead, reason: "ok" };
|
|
358
|
-
}
|
|
1
|
+
// warp-pool · 装配入口。
|
|
2
|
+
//
|
|
3
|
+
// 定位(MODULES.md §0/§10):WARP 池是**增量**出口,不是替代成员自身出口;缺省**开**,MSLXDFF_WARP_POOL=0 关。
|
|
4
|
+
import { statSync } from "node:fs";
|
|
5
|
+
import { resolveWarpConfig } from "./config.js";
|
|
6
|
+
import { createRotatingDispatcher } from "./rotate.js";
|
|
7
|
+
import { classifyWarpSignal, SIGNAL_STATUSES } from "./verdict.js";
|
|
8
|
+
import { createHealthMonitor } from "./health.js";
|
|
9
|
+
import { ensureDir, loadNodes, removeNode, saveNode } from "./nodes.js";
|
|
10
|
+
import { registerWarpDevice } from "./register.js";
|
|
11
|
+
import { ensureBinary, probeTunnel, resolveAsset, startTunnel, writeTunnelConf } from "./wireproxy.js";
|
|
12
|
+
import { createReviver, labelExits, tailReason } from "./revive.js";
|
|
13
|
+
import { portInUse, portOwner, reclaimPort, waitPortFree } from "./ports.js";
|
|
14
|
+
import { fmtConf, fmtExit, fmtIdentityDetail, fmtPool, fmtPoolEmpty, fmtProbe, fmtProbeGiveUp, fmtPortBlocked, fmtSiblings, fmtSpawn, startupReport } from "./report.js";
|
|
15
|
+
import { wireRotation } from "./identity.js";
|
|
16
|
+
import { findDaemons } from "../runtime/proc-info.js";
|
|
17
|
+
|
|
18
|
+
/** 重拨节流:连续失败指数退避。网络不通的机器绝不能让重注册把 CF 刷到 429。 */
|
|
19
|
+
const REDIAL_MIN_GAP_MS = 5 * 60_000;
|
|
20
|
+
const REDIAL_MAX_GAP_MS = 30 * 60_000;
|
|
21
|
+
|
|
22
|
+
/** 注册间隔(同类项目实测:密集注册会让设备落同一网段,且触发 CF 429)。 */
|
|
23
|
+
function gapMs(cfg, rnd = Math.random) {
|
|
24
|
+
const span = Math.max(0, cfg.registerMaxGapMs - cfg.registerMinGapMs);
|
|
25
|
+
return Math.round(cfg.registerMinGapMs + rnd() * span);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 注册响应 → 节点对象(bootstrap 与重拨共用,避免同一份字段表写两遍)。 */
|
|
29
|
+
function nodeFromReg(port, reg, now) {
|
|
30
|
+
return { port, id: reg.id, token: reg.token, v4: reg.v4, v6: reg.v6, endpoint: reg.endpoint, peerKey: reg.peerKey, privateKey: reg.privateKey, registeredAt: new Date(now()).toISOString() };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 建池。开关缺省关,故「未启用」= 未设 MSLXDFF_WARP_POOL=1 或 非 leader;那时返回 null,调用方据此不注入。
|
|
35
|
+
* @returns {null | { dispatcher: object, ready: Promise<void>, close: () => Promise<void>, stats: () => object }}
|
|
36
|
+
*/
|
|
37
|
+
export function createWarpPool({
|
|
38
|
+
env = process.env,
|
|
39
|
+
baseUrl = "https://opencode.ai",
|
|
40
|
+
log = (m) => console.error(m),
|
|
41
|
+
register = registerWarpDevice,
|
|
42
|
+
ensureBin = ensureBinary,
|
|
43
|
+
start = startTunnel,
|
|
44
|
+
probe = probeTunnel,
|
|
45
|
+
reclaim = reclaimPort,
|
|
46
|
+
inUse = portInUse,
|
|
47
|
+
waitFree = waitPortFree,
|
|
48
|
+
now = Date.now,
|
|
49
|
+
rnd = Math.random,
|
|
50
|
+
wait = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
51
|
+
} = {}) {
|
|
52
|
+
const cfg = resolveWarpConfig(env);
|
|
53
|
+
if (!cfg.active) return null;
|
|
54
|
+
|
|
55
|
+
/** 身份轮换器:上游按出口 IP 计的限额(403 FreeTierError / 429 FreeUsage·GoUsage / 裸 429)时换一台 WARP 设备换出口。 */
|
|
56
|
+
let rotator = null;
|
|
57
|
+
|
|
58
|
+
const dispatcher = createRotatingDispatcher({
|
|
59
|
+
nodes: [],
|
|
60
|
+
strategy: cfg.strategy,
|
|
61
|
+
cooldownMs: cfg.cooldownMs,
|
|
62
|
+
log,
|
|
63
|
+
// 判决照抄参照实现 zen-router 的决策表(见 verdict.js 头注):403 FreeTierError 与
|
|
64
|
+
// 429 FreeUsage/GoUsage 是同一条件、必须**强制**换出口(跳过冷却);BlackUsageLimitError
|
|
65
|
+
// 是账号日配额、绝不换;其余状态码不参与判决、也不许刷日志。
|
|
66
|
+
onStatus: (signal, port) => {
|
|
67
|
+
const status = Number(signal?.status);
|
|
68
|
+
if (!SIGNAL_STATUSES.includes(status)) return;
|
|
69
|
+
const v = classifyWarpSignal(signal);
|
|
70
|
+
if (!cfg.rotate) {
|
|
71
|
+
log(`[warp-pool] :${port} 上游 ${status}(${v.kind})—— 轮换开关关着(MSLXDFF_WARP_ROTATE=0),不换出口`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!v.rotate || !rotator) {
|
|
75
|
+
log(`[warp-pool] :${port} 上游 ${status} 判为「${v.kind}」⇒ 不换出口:${v.why}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
log(`[warp-pool] :${port} 上游 ${status} 判为「${v.kind}」⇒ 换身份换出口${v.forced ? "(强制,跳过冷却)" : ""}:${v.why}`);
|
|
79
|
+
void rotator.rotate(port, { force: v.forced })
|
|
80
|
+
.then((r) => { if (r && r.skipped) log(`[warp-pool] :${port} 换出口被冷却掐掉(距上次不足最小间隔)—— 这一发仍走原出口`); })
|
|
81
|
+
.catch(() => {});
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
/** @type {Map<number, {stop: () => void}>} */
|
|
85
|
+
const tunnels = new Map();
|
|
86
|
+
|
|
87
|
+
/** 复活:用磁盘已有 conf 重起,不重注册(进程挂了 ≠ 设备失效,省 CF 配额)。 */
|
|
88
|
+
async function reviveTunnel(port) {
|
|
89
|
+
if (closed) return false;
|
|
90
|
+
const node = loadNodes(cfg.dir, [port])[0];
|
|
91
|
+
if (!node) {
|
|
92
|
+
log(`[warp-pool] :${port} 放弃复活:磁盘上没有该节点的身份(node-${port}.json 缺失或缺私钥)`);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await bringUp(node);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
log(`[warp-pool] :${port} 复活起隧道失败:${String(e?.message || e)}`);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true; // 真活没活由 createReviver 在 settleMs 后用 isAlive 复查,这里不许冒充成功
|
|
102
|
+
}
|
|
103
|
+
const reviver = createReviver({ revive: reviveTunnel, isAlive: (port) => { const t = tunnels.get(port); return !!t && t.alive !== false; }, log });
|
|
104
|
+
let binPath = "";
|
|
105
|
+
let closed = false;
|
|
106
|
+
let lastRedialAt = 0;
|
|
107
|
+
let redialGapMs = REDIAL_MIN_GAP_MS;
|
|
108
|
+
|
|
109
|
+
// 兜底清理:Node 退出不会回收 spawn 的子进程,daemon 突然退出时同步杀掉隧道。
|
|
110
|
+
// 同时置 closed:否则「杀子进程 → onExit → 立刻复活」会在退出过程中再 spawn 出一批孤儿(真机:每次重启漏一批)。
|
|
111
|
+
process.once("exit", () => {
|
|
112
|
+
closed = true;
|
|
113
|
+
try {
|
|
114
|
+
reviver.stop();
|
|
115
|
+
} catch {}
|
|
116
|
+
for (const t of tunnels.values()) {
|
|
117
|
+
try {
|
|
118
|
+
t.stop();
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/** 起一条隧道的完整动作:确认不是重复起 → 清残留 → 写 conf → 等端口真空出来 → spawn → 记表。 */
|
|
124
|
+
async function bringUp(node) {
|
|
125
|
+
if (tunnels.has(node.port)) return; // 已归我们管:绝不再清残留(reclaim 会把自己这条隧道当残留杀掉)
|
|
126
|
+
await reclaim({ dir: cfg.dir, port: node.port, log }); // 清上一代孤儿(daemon 被强杀时子进程不会自己退)
|
|
127
|
+
const { conf, bytes } = writeTunnelConf(cfg.dir, node);
|
|
128
|
+
log(fmtConf({ port: node.port, conf, bytes }));
|
|
129
|
+
// SIGTERM 之后套接字要几百毫秒才释放,这期间连得上 ≠ 别人的进程占着——先等它真空出来再判
|
|
130
|
+
if (!(await waitFree(node.port, { inUse }))) {
|
|
131
|
+
const owner = await portOwner({ dir: cfg.dir, port: node.port }).catch(() => ({ ours: [], others: [] }));
|
|
132
|
+
throw new Error(fmtPortBlocked({ port: node.port, ...owner }));
|
|
133
|
+
}
|
|
134
|
+
const t = start({
|
|
135
|
+
binPath,
|
|
136
|
+
dir: cfg.dir,
|
|
137
|
+
port: node.port,
|
|
138
|
+
log,
|
|
139
|
+
onExit: ({ port, code, signal, tail, pid, aliveMs }) => {
|
|
140
|
+
tunnels.delete(port);
|
|
141
|
+
log(fmtExit({ port, code, signal, pid, aliveMs, tail: tailReason(tail) }));
|
|
142
|
+
if (!closed) void reviver.onExit(port); // 收摊中就别复活了,否则退出过程里又 spawn 出一批孤儿
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
tunnels.set(node.port, t);
|
|
146
|
+
log(fmtSpawn({ port: node.port, pid: t.pid, binPath, conf }));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 429 = 上游按出口 IP 计的名额耗尽 ⇒ 换一台设备的身份换出口(换身份=换桶)。
|
|
150
|
+
rotator = wireRotation({
|
|
151
|
+
dispatcher,
|
|
152
|
+
register,
|
|
153
|
+
tunnels,
|
|
154
|
+
bringUp,
|
|
155
|
+
swap: (port, node) => { removeNode(cfg.dir, port); saveNode(cfg.dir, node); },
|
|
156
|
+
log,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
/** 探活闸门参数:刚起的隧道要等 WireGuard 握手完成才通(真机:一起来就探 ⇒ 4 条全没通 ⇒ 池为空、WARP 白开)。 */
|
|
160
|
+
const PROBE_SETTLE_MS = 2_000;
|
|
161
|
+
const PROBE_ATTEMPTS = 2;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 入池前的实测闸门:给握手留时间 → 逐个探活 → 探不通的绝不进池。
|
|
165
|
+
* 为什么必须有:wireproxy 进程活着但隧道转不动流量时,请求会挂到超时(比直连更糟)。
|
|
166
|
+
* @returns {Promise<Array<object>>} 通过探活的节点
|
|
167
|
+
*/
|
|
168
|
+
async function probeGate(list) {
|
|
169
|
+
const live = [];
|
|
170
|
+
await wait(PROBE_SETTLE_MS);
|
|
171
|
+
for (const n of list) {
|
|
172
|
+
if (!tunnels.has(n.port)) continue;
|
|
173
|
+
const url = `${String(baseUrl).replace(/\/+$/, "")}${cfg.probePath}`;
|
|
174
|
+
let r = { ok: false };
|
|
175
|
+
for (let i = 1; i <= PROBE_ATTEMPTS; i++) {
|
|
176
|
+
const t0 = now();
|
|
177
|
+
r = await probe({ port: n.port, baseUrl, path: cfg.probePath });
|
|
178
|
+
const ms = Number.isFinite(r.ms) ? r.ms : now() - t0;
|
|
179
|
+
log(fmtProbe({ port: n.port, attempt: i, attempts: PROBE_ATTEMPTS, url, ok: r.ok, status: r.status, error: r.error, ms }));
|
|
180
|
+
if (r.ok) break;
|
|
181
|
+
if (i < PROBE_ATTEMPTS) await wait(PROBE_SETTLE_MS);
|
|
182
|
+
}
|
|
183
|
+
if (r.ok) {
|
|
184
|
+
live.push(n);
|
|
185
|
+
} else {
|
|
186
|
+
log(fmtProbeGiveUp({ port: n.port, attempts: PROBE_ATTEMPTS, url }));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return live;
|
|
190
|
+
}
|
|
191
|
+
async function bootstrap() {
|
|
192
|
+
if (closed) return;
|
|
193
|
+
ensureDir(cfg.dir);
|
|
194
|
+
try {
|
|
195
|
+
binPath = await ensureBin({ dir: cfg.dir, bin: cfg.bin, autodownload: cfg.autodownload, log });
|
|
196
|
+
} catch (e) {
|
|
197
|
+
const why = !cfg.bin && !resolveAsset()
|
|
198
|
+
? `本平台(${process.platform}-${process.arch})无预编译 wireproxy,如需启用请设 MSLXDFF_WARP_BIN`
|
|
199
|
+
: String(e?.message || e);
|
|
200
|
+
log(`[warp-pool] 出口池本次跳过(不启用,请求走直连):${why}`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (closed) return;
|
|
204
|
+
|
|
205
|
+
// 1. 复用磁盘上已注册的节点
|
|
206
|
+
let nodes = loadNodes(cfg.dir, cfg.ports);
|
|
207
|
+
let binSize = 0;
|
|
208
|
+
try { binSize = statSync(binPath).size; } catch {}
|
|
209
|
+
await startupReport({ cfg, nodes, binPath, binSize, ownerOf: (port) => portOwner({ dir: cfg.dir, port }), log });
|
|
210
|
+
// 身份指纹:4 条隧道只拿到 2 个出口时,只有这组读数能区分「设备写重复」与「CF 共享 NAT 折叠」
|
|
211
|
+
for (const line of fmtIdentityDetail(nodes)) log(line);
|
|
212
|
+
// 同机其它 daemon:两个 daemon 会互相把对方的活隧道当「残留」杀掉(真机事故根因),必须一开池就报出来
|
|
213
|
+
const siblings = findDaemons({ self: process.pid });
|
|
214
|
+
if (siblings.length) log(fmtSiblings({ list: siblings }));
|
|
215
|
+
|
|
216
|
+
// 2. 补足:逐个注册(拉长间隔,避免 CF 注册限流与同网段撞车)
|
|
217
|
+
const used = new Set(nodes.map((n) => n.port));
|
|
218
|
+
const free = cfg.ports.filter((p) => !used.has(p));
|
|
219
|
+
for (const port of free) {
|
|
220
|
+
if (closed) return;
|
|
221
|
+
if (nodes.length >= cfg.count) break;
|
|
222
|
+
try {
|
|
223
|
+
if (nodes.length > 0) await wait(gapMs(cfg, rnd));
|
|
224
|
+
const reg = await register({ log });
|
|
225
|
+
const node = nodeFromReg(port, reg, now);
|
|
226
|
+
saveNode(cfg.dir, node);
|
|
227
|
+
nodes.push(node);
|
|
228
|
+
log(`[warp-pool] 注册新身份 :${port} 成功(设备 v6=${reg.v6},WireGuard 密钥对在本地生成)`);
|
|
229
|
+
} catch (e) {
|
|
230
|
+
log(`[warp-pool] :${port} 注册身份失败,本轮跳过这个端口:${String(e?.message || e)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 3. 起隧道
|
|
235
|
+
for (const n of nodes) {
|
|
236
|
+
try {
|
|
237
|
+
await bringUp(n);
|
|
238
|
+
} catch (e) {
|
|
239
|
+
log(`[warp-pool] :${n.port} 起隧道失败,这个端口本轮不可用:${String(e?.message || e)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 入池前实测探活:WARP 握手没完成时 wireproxy 在跑却转不动流量(请求会挂到超时),那种节点进池会黑洞全部请求
|
|
244
|
+
const live = await probeGate(nodes);
|
|
245
|
+
if (live.length) {
|
|
246
|
+
await dispatcher.setNodes(live.map((n) => ({ port: n.port, uri: `http://127.0.0.1:${n.port}` })));
|
|
247
|
+
// 出口取证:一次拿到每个端口的真实出口,既喂给轮换器做「换没换动」的比对,也直接写进池结论那一行
|
|
248
|
+
const exits = await labelExits({ ports: live.map((n) => n.port), dir: cfg.dir, log });
|
|
249
|
+
rotator.noteExits(exits);
|
|
250
|
+
log(fmtPool({ pooled: live.length, total: nodes.length, exits }));
|
|
251
|
+
} else {
|
|
252
|
+
log(fmtPoolEmpty({ total: nodes.length, ports: cfg.ports }));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const monitor = createHealthMonitor({
|
|
257
|
+
dispatcher,
|
|
258
|
+
baseUrl,
|
|
259
|
+
probePath: cfg.probePath,
|
|
260
|
+
log,
|
|
261
|
+
infoOf: () => ({ alive: tunnels.size, pooled: dispatcher.size, exits: rotator ? rotator.stats() : [] }),
|
|
262
|
+
cooldownMs: cfg.cooldownMs,
|
|
263
|
+
onExhausted: async () => {
|
|
264
|
+
// 重拨 = 重新注册换出口。只重拨「进程已死」的隧道:全活却全挂 = 网络路径问题,重注册只会把 CF 刷到 429
|
|
265
|
+
const plan = redialPlan({ ports: cfg.ports, alivePorts: [...tunnels.keys()], lastRedialAt, redialGapMs, now });
|
|
266
|
+
if (plan.reason === "all-alive") {
|
|
267
|
+
log("[warp-pool] 隧道均存活但全池不通,判定为网络路径问题,跳过重拨");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (plan.reason === "throttled") {
|
|
271
|
+
log(`[warp-pool] 距上次重拨不足 ${Math.round(redialGapMs / 1000)}s,跳过本轮`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const dead = plan.dead;
|
|
275
|
+
lastRedialAt = now();
|
|
276
|
+
redialGapMs = Math.min(redialGapMs * 2, REDIAL_MAX_GAP_MS);
|
|
277
|
+
log(`[warp-pool] ${dead.length} 条隧道已死,重拨(下次最短间隔 ${Math.round(redialGapMs / 1000)}s)`);
|
|
278
|
+
for (const port of dead) {
|
|
279
|
+
if (closed) return;
|
|
280
|
+
try {
|
|
281
|
+
tunnels.get(port)?.stop();
|
|
282
|
+
tunnels.delete(port);
|
|
283
|
+
removeNode(cfg.dir, port);
|
|
284
|
+
const reg = await register({ log });
|
|
285
|
+
saveNode(cfg.dir, nodeFromReg(port, reg, now));
|
|
286
|
+
await bringUp({ port, ...reg });
|
|
287
|
+
} catch (e) {
|
|
288
|
+
log(`[warp-pool] 重拨 :${port} 失败:${String(e?.message || e)}`);
|
|
289
|
+
}
|
|
290
|
+
await wait(gapMs(cfg, rnd));
|
|
291
|
+
}
|
|
292
|
+
if (!closed && tunnels.size) {
|
|
293
|
+
// 重拨起来的隧道同样要过闸门:没过探活的隧道进池 = 把请求挂到超时(原先这里直接 setNodes,等于绕过闸门)
|
|
294
|
+
const alive = await probeGate([...tunnels.keys()].map((port) => ({ port })));
|
|
295
|
+
if (alive.length) {
|
|
296
|
+
redialGapMs = REDIAL_MIN_GAP_MS; // 确实有隧道能通 ⇒ 退避复位
|
|
297
|
+
await dispatcher.setNodes(alive.map((n) => ({ port: n.port, uri: `http://127.0.0.1:${n.port}` })));
|
|
298
|
+
} else {
|
|
299
|
+
log("[warp-pool] 重拨后仍没有一条隧道探通,保持直连(等下一轮健康检查)");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const ready = bootstrap()
|
|
306
|
+
.then(() => {
|
|
307
|
+
if (!closed) monitor.start();
|
|
308
|
+
})
|
|
309
|
+
.catch((e) => log(`[warp-pool] bootstrap 失败:${String(e?.message || e)}`));
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
dispatcher,
|
|
313
|
+
ready,
|
|
314
|
+
config: cfg,
|
|
315
|
+
async close() {
|
|
316
|
+
if (closed) return;
|
|
317
|
+
closed = true;
|
|
318
|
+
monitor.stop();
|
|
319
|
+
reviver.stop();
|
|
320
|
+
for (const t of tunnels.values()) {
|
|
321
|
+
try {
|
|
322
|
+
t.stop();
|
|
323
|
+
} catch {}
|
|
324
|
+
}
|
|
325
|
+
tunnels.clear();
|
|
326
|
+
await dispatcher.close();
|
|
327
|
+
},
|
|
328
|
+
stats() {
|
|
329
|
+
return {
|
|
330
|
+
enabled: true,
|
|
331
|
+
mode: cfg.mode,
|
|
332
|
+
strategy: cfg.strategy,
|
|
333
|
+
dir: cfg.dir,
|
|
334
|
+
tunnels: tunnels.size,
|
|
335
|
+
nodes: dispatcher.stats(),
|
|
336
|
+
exits: rotator ? rotator.stats() : [],
|
|
337
|
+
};
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** 只取轮换 dispatcher 的便捷入口;未启用时返回 undefined(调用方直接透传即"关闭态")。 */
|
|
343
|
+
/** 隧道进程由模块内的 process exit 钩子兜底清理。 */
|
|
344
|
+
export function createWarpDispatcher(opts) {
|
|
345
|
+
return createWarpPool(opts)?.dispatcher;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* 重拨门(纯函数,便于单测):只「有隧道进程已死」且「距上次重拨超过退避间隔」才放行——全活却全挂是网络问题。
|
|
350
|
+
* @returns {{ dead: number[], reason: "ok" | "all-alive" | "throttled" }}
|
|
351
|
+
*/
|
|
352
|
+
export function redialPlan({ ports = [], alivePorts = [], lastRedialAt = 0, redialGapMs = REDIAL_MIN_GAP_MS, now = Date.now } = {}) {
|
|
353
|
+
const alive = new Set(alivePorts);
|
|
354
|
+
const dead = ports.filter((p) => !alive.has(p));
|
|
355
|
+
if (!dead.length) return { dead, reason: "all-alive" };
|
|
356
|
+
if (now() - lastRedialAt < redialGapMs) return { dead, reason: "throttled" };
|
|
357
|
+
return { dead, reason: "ok" };
|
|
358
|
+
}
|