mslxdff 0.1.203 → 0.1.207
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/docs/cli_help_mini.md +146 -0
- package/package.json +38 -38
- package/plugins/README.md +14 -14
- package/plugins/prefer-model.mjs.example +14 -14
- package/src/anthropic/canonical-json.js +55 -0
- package/src/anthropic/count-tokens.js +61 -61
- package/src/anthropic/request-body.js +183 -126
- package/src/anthropic/request-shape.js +60 -0
- package/src/anthropic/session-id.js +58 -0
- package/src/anthropic/shared.js +18 -18
- package/src/anthropic/sse-aggregate.js +56 -56
- package/src/anthropic/tool-media.js +75 -0
- package/src/anthropic/translate.js +296 -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/index.js +8 -40
- package/src/chat-pipeline/serial-trial.js +279 -283
- package/src/chat-pipeline/terminal-verdict.js +132 -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 +158 -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 +71 -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 -254
- package/src/plugins.js +107 -107
- package/src/providers/AGENTS.md +49 -0
- 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 -180
- package/src/providers/raccoon/credits.js +185 -185
- package/src/providers/raccoon/crypto.js +43 -43
- 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 -102
- 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/AGENTS.md +37 -0
- 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 +93 -83
- package/src/routes/index.js +147 -147
- package/src/routes/messages-route.js +375 -365
- package/src/routes/peers.js +10 -2
- 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/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 -120
- 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/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 -74
- 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/usage/cache-fields.js +58 -0
- package/src/usage/record.js +109 -106
- package/src/usage/report.js +209 -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 -263
- 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/src/warp-pool/verdict.js
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
// warp-pool · 上游失败信号的「换不换出口」判决(纯函数,零 IO)。
|
|
2
|
-
//
|
|
3
|
-
// 判据逐字对齐参照实现 kyomufs/zen-router:
|
|
4
|
-
// - internal/zen/errors.go 的 Kind 分类表(哪些错误算「按出口 IP 计的限额」);
|
|
5
|
-
// - internal/router/stage.go 的 NextAttempt 决策表(每种 Kind 该换什么、要不要绕过冷却)。
|
|
6
|
-
//
|
|
7
|
-
// 立此模块的原因(2026-10-17 十次修订):我原先只在 rotate.js 的 onStatus 里认 `status === 429`,
|
|
8
|
-
// 漏掉了参照实现表里**最重要的一格**——`403 FreeTierError` 与 `429 FreeUsageLimitError` 是
|
|
9
|
-
// **同一个条件的两种状态码**(上游按自己的窗口状态二选一,文案完全相同)。参照实现实测:
|
|
10
|
-
// 同一身份连发 3 次 429,**身份一换同一个请求立刻 200**(errors.go:32-42);
|
|
11
|
-
// 而它最初把 403 当通用 403 不重试,代价是整轮请求直接死掉(errors.go:50-61 记录得很清楚)。
|
|
12
|
-
// 我这边更糟:`terminal-verdict.js` 把 FreeTierError 判成**终局**(跳过接力),
|
|
13
|
-
// warp-pool 又只认 429 ⇒ **403 FreeTierError 从来没有任何换出口动作**。
|
|
14
|
-
//
|
|
15
|
-
// 反向护栏同样照抄:`BlackUsageLimitError` 是真正的**账号日配额**,换出口抬不动,
|
|
16
|
-
// 参照实现明确「绝不旋转身份」(stage.go:125-128、errors.go:28-31);换 key 才是它的正解。
|
|
17
|
-
// 裸 403(正文没有 FreeTier 判决)也不旋转:参照实现的 kindByStatus 把 403 归为
|
|
18
|
-
// KindClient(不可重试),换出口对形状/区域/凭据类 403 毫无用处。
|
|
19
|
-
//
|
|
20
|
-
// 读不到正文时的退路也照抄 kindByStatus:429 → 当出口 IP 限流(换);403 → 不换。
|
|
21
|
-
|
|
22
|
-
/** 上游失败正文里判定免费层资格的具名判决(与 terminal-verdict.js 的 FREE_TIER_PATTERNS 同源口径)。 */
|
|
23
|
-
const FREE_TIER_RE = /FreeTierError|free tier can only be used/i;
|
|
24
|
-
/** 匿名免费层 429(参照实现实测:按出口 IP 计,换身份即换桶)。 */
|
|
25
|
-
const FREE_QUOTA_RE = /FreeUsageLimitError|GoUsageLimitError/i;
|
|
26
|
-
/** 黑名单层:真账号日配额,换出口抬不动。 */
|
|
27
|
-
const BLACK_QUOTA_RE = /BlackUsageLimitError/i;
|
|
28
|
-
|
|
29
|
-
/** 只这两类状态码参与判决;其余(含 200)一律不参与、也不许刷日志。 */
|
|
30
|
-
export const SIGNAL_STATUSES = [403, 429];
|
|
31
|
-
|
|
32
|
-
/** 正文捕获上限(判据只需要文案里的类名,2KB 足够,且防大 body 拖成慢扫描)。 */
|
|
33
|
-
export const SIGNAL_MAX_BYTES = 2048;
|
|
34
|
-
|
|
35
|
-
/** 正文里出现了具名判决(供 tapHandler 一凑够就提前判决,不必等整条 body 读完)。 */
|
|
36
|
-
export function looksLikeQuotaSignal(text) {
|
|
37
|
-
const t = typeof text === "string" ? text : "";
|
|
38
|
-
return FREE_TIER_RE.test(t) || FREE_QUOTA_RE.test(t) || BLACK_QUOTA_RE.test(t);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 一次上游失败该不该换出口。
|
|
43
|
-
* @param {{ status?: number, text?: string }} signal 上游状态码 + 正文(≤ SIGNAL_MAX_BYTES;读不到给空串)
|
|
44
|
-
* @returns {{ kind: "free-tier"|"free-quota"|"black-quota"|"ip-limit"|"none", rotate: boolean,
|
|
45
|
-
* forced: boolean, why: string }}
|
|
46
|
-
* forced=true 表示必须**跳过**冷却(参照实现的 RotateForced:冷却曾把第一次真实反应吞掉,
|
|
47
|
-
* 于是重试复用了同一个已限额的出口 —— stage.go:315-324)。
|
|
48
|
-
*/
|
|
49
|
-
export function classifyWarpSignal({ status, text = "" } = {}) {
|
|
50
|
-
const code = Number(status);
|
|
51
|
-
const body = typeof text === "string" ? text : "";
|
|
52
|
-
if (!SIGNAL_STATUSES.includes(code)) {
|
|
53
|
-
return { kind: "none", rotate: false, forced: false, why: `上游 ${code || "?"} 不是 403/429,与出口限额无关` };
|
|
54
|
-
}
|
|
55
|
-
// 顺序即优先级,照抄 errors.go 的 classify():先认正文里的具名判决,再退回状态码。
|
|
56
|
-
if (BLACK_QUOTA_RE.test(body)) {
|
|
57
|
-
return { kind: "black-quota", rotate: false, forced: false, why: "BlackUsageLimitError 是真正的账号日配额,换出口抬不动(参照实现同样绝不旋转)——该换 key,不是换 IP" };
|
|
58
|
-
}
|
|
59
|
-
if (FREE_TIER_RE.test(body)) {
|
|
60
|
-
return { kind: "free-tier", rotate: true, forced: true, why: "403 FreeTierError 与 429 FreeUsageLimitError 是同一条件的两种状态码;参照实现实测换身份后同一个请求立刻 200 ⇒ 强制换出口,跳过冷却" };
|
|
61
|
-
}
|
|
62
|
-
if (FREE_QUOTA_RE.test(body)) {
|
|
63
|
-
return { kind: "free-quota", rotate: true, forced: true, why: "匿名免费层 429 实测是**按出口 IP** 计的限额(同一身份连发 3 次全 429,换身份立刻 200)⇒ 强制换出口,跳过冷却" };
|
|
64
|
-
}
|
|
65
|
-
if (code === 429) {
|
|
66
|
-
return { kind: "ip-limit", rotate: true, forced: false, why: "裸 429(正文无配额判决)= 出口 IP 限流信号 ⇒ 换出口;保留冷却,防 429 风暴把 CF 注册刷爆" };
|
|
67
|
-
}
|
|
68
|
-
return { kind: "none", rotate: false, forced: false, why: "403 但正文没有 FreeTier 判决 ⇒ 不是配额信号(形状/区域/凭据类),换出口无用" };
|
|
69
|
-
}
|
|
1
|
+
// warp-pool · 上游失败信号的「换不换出口」判决(纯函数,零 IO)。
|
|
2
|
+
//
|
|
3
|
+
// 判据逐字对齐参照实现 kyomufs/zen-router:
|
|
4
|
+
// - internal/zen/errors.go 的 Kind 分类表(哪些错误算「按出口 IP 计的限额」);
|
|
5
|
+
// - internal/router/stage.go 的 NextAttempt 决策表(每种 Kind 该换什么、要不要绕过冷却)。
|
|
6
|
+
//
|
|
7
|
+
// 立此模块的原因(2026-10-17 十次修订):我原先只在 rotate.js 的 onStatus 里认 `status === 429`,
|
|
8
|
+
// 漏掉了参照实现表里**最重要的一格**——`403 FreeTierError` 与 `429 FreeUsageLimitError` 是
|
|
9
|
+
// **同一个条件的两种状态码**(上游按自己的窗口状态二选一,文案完全相同)。参照实现实测:
|
|
10
|
+
// 同一身份连发 3 次 429,**身份一换同一个请求立刻 200**(errors.go:32-42);
|
|
11
|
+
// 而它最初把 403 当通用 403 不重试,代价是整轮请求直接死掉(errors.go:50-61 记录得很清楚)。
|
|
12
|
+
// 我这边更糟:`terminal-verdict.js` 把 FreeTierError 判成**终局**(跳过接力),
|
|
13
|
+
// warp-pool 又只认 429 ⇒ **403 FreeTierError 从来没有任何换出口动作**。
|
|
14
|
+
//
|
|
15
|
+
// 反向护栏同样照抄:`BlackUsageLimitError` 是真正的**账号日配额**,换出口抬不动,
|
|
16
|
+
// 参照实现明确「绝不旋转身份」(stage.go:125-128、errors.go:28-31);换 key 才是它的正解。
|
|
17
|
+
// 裸 403(正文没有 FreeTier 判决)也不旋转:参照实现的 kindByStatus 把 403 归为
|
|
18
|
+
// KindClient(不可重试),换出口对形状/区域/凭据类 403 毫无用处。
|
|
19
|
+
//
|
|
20
|
+
// 读不到正文时的退路也照抄 kindByStatus:429 → 当出口 IP 限流(换);403 → 不换。
|
|
21
|
+
|
|
22
|
+
/** 上游失败正文里判定免费层资格的具名判决(与 terminal-verdict.js 的 FREE_TIER_PATTERNS 同源口径)。 */
|
|
23
|
+
const FREE_TIER_RE = /FreeTierError|free tier can only be used/i;
|
|
24
|
+
/** 匿名免费层 429(参照实现实测:按出口 IP 计,换身份即换桶)。 */
|
|
25
|
+
const FREE_QUOTA_RE = /FreeUsageLimitError|GoUsageLimitError/i;
|
|
26
|
+
/** 黑名单层:真账号日配额,换出口抬不动。 */
|
|
27
|
+
const BLACK_QUOTA_RE = /BlackUsageLimitError/i;
|
|
28
|
+
|
|
29
|
+
/** 只这两类状态码参与判决;其余(含 200)一律不参与、也不许刷日志。 */
|
|
30
|
+
export const SIGNAL_STATUSES = [403, 429];
|
|
31
|
+
|
|
32
|
+
/** 正文捕获上限(判据只需要文案里的类名,2KB 足够,且防大 body 拖成慢扫描)。 */
|
|
33
|
+
export const SIGNAL_MAX_BYTES = 2048;
|
|
34
|
+
|
|
35
|
+
/** 正文里出现了具名判决(供 tapHandler 一凑够就提前判决,不必等整条 body 读完)。 */
|
|
36
|
+
export function looksLikeQuotaSignal(text) {
|
|
37
|
+
const t = typeof text === "string" ? text : "";
|
|
38
|
+
return FREE_TIER_RE.test(t) || FREE_QUOTA_RE.test(t) || BLACK_QUOTA_RE.test(t);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 一次上游失败该不该换出口。
|
|
43
|
+
* @param {{ status?: number, text?: string }} signal 上游状态码 + 正文(≤ SIGNAL_MAX_BYTES;读不到给空串)
|
|
44
|
+
* @returns {{ kind: "free-tier"|"free-quota"|"black-quota"|"ip-limit"|"none", rotate: boolean,
|
|
45
|
+
* forced: boolean, why: string }}
|
|
46
|
+
* forced=true 表示必须**跳过**冷却(参照实现的 RotateForced:冷却曾把第一次真实反应吞掉,
|
|
47
|
+
* 于是重试复用了同一个已限额的出口 —— stage.go:315-324)。
|
|
48
|
+
*/
|
|
49
|
+
export function classifyWarpSignal({ status, text = "" } = {}) {
|
|
50
|
+
const code = Number(status);
|
|
51
|
+
const body = typeof text === "string" ? text : "";
|
|
52
|
+
if (!SIGNAL_STATUSES.includes(code)) {
|
|
53
|
+
return { kind: "none", rotate: false, forced: false, why: `上游 ${code || "?"} 不是 403/429,与出口限额无关` };
|
|
54
|
+
}
|
|
55
|
+
// 顺序即优先级,照抄 errors.go 的 classify():先认正文里的具名判决,再退回状态码。
|
|
56
|
+
if (BLACK_QUOTA_RE.test(body)) {
|
|
57
|
+
return { kind: "black-quota", rotate: false, forced: false, why: "BlackUsageLimitError 是真正的账号日配额,换出口抬不动(参照实现同样绝不旋转)——该换 key,不是换 IP" };
|
|
58
|
+
}
|
|
59
|
+
if (FREE_TIER_RE.test(body)) {
|
|
60
|
+
return { kind: "free-tier", rotate: true, forced: true, why: "403 FreeTierError 与 429 FreeUsageLimitError 是同一条件的两种状态码;参照实现实测换身份后同一个请求立刻 200 ⇒ 强制换出口,跳过冷却" };
|
|
61
|
+
}
|
|
62
|
+
if (FREE_QUOTA_RE.test(body)) {
|
|
63
|
+
return { kind: "free-quota", rotate: true, forced: true, why: "匿名免费层 429 实测是**按出口 IP** 计的限额(同一身份连发 3 次全 429,换身份立刻 200)⇒ 强制换出口,跳过冷却" };
|
|
64
|
+
}
|
|
65
|
+
if (code === 429) {
|
|
66
|
+
return { kind: "ip-limit", rotate: true, forced: false, why: "裸 429(正文无配额判决)= 出口 IP 限流信号 ⇒ 换出口;保留冷却,防 429 风暴把 CF 注册刷爆" };
|
|
67
|
+
}
|
|
68
|
+
return { kind: "none", rotate: false, forced: false, why: "403 但正文没有 FreeTier 判决 ⇒ 不是配额信号(形状/区域/凭据类),换出口无用" };
|
|
69
|
+
}
|
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
// warp-pool · wireproxy 进程管理:取二进制 → 生成配置 → 启动 → 守护/销毁。
|
|
2
|
-
// wireproxy(octeep/wireproxy,Go,用户态 WireGuard,无需 root/TUN)把 WARP 隧道暴露为本地 HTTP 代理。
|
|
3
|
-
// 为什么是 [http] 而非 [Socks5]:项目 undici 为 v5(实测 5.29.0),其 ProxyAgent 只接受 http/https,
|
|
4
|
-
// 「SOCKS5 支持」是 undici 后续版本才加的(实测 socks5:// 直接抛 Invalid URL protocol)。
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import { chmodSync, existsSync, createWriteStream, readdirSync, rmSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import { pipeline } from "node:stream/promises";
|
|
9
|
-
import { compatFetch, getUndici } from "../compat.js";
|
|
10
|
-
import { ensureDir, nodePaths, renderWireproxyConf, writeSecret } from "./nodes.js";
|
|
11
|
-
|
|
12
|
-
export const WIREPROXY_VERSION = "v1.1.3";
|
|
13
|
-
const RELEASE_BASE = "https://github.com/octeep/wireproxy/releases/download";
|
|
14
|
-
|
|
15
|
-
const ASSETS = {
|
|
16
|
-
"linux-x64": "wireproxy_linux_amd64.tar.gz",
|
|
17
|
-
"linux-arm64": "wireproxy_linux_arm64.tar.gz",
|
|
18
|
-
"darwin-x64": "wireproxy_darwin_amd64.tar.gz",
|
|
19
|
-
"darwin-arm64": "wireproxy_darwin_arm64.tar.gz",
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/** 依平台选发布包;不支持的平台返回 null(改用 MSLXDFF_WARP_BIN 自备二进制)。 */
|
|
23
|
-
export function resolveAsset(platform = process.platform, arch = process.arch) {
|
|
24
|
-
const key = `${platform}-${arch}`;
|
|
25
|
-
return ASSETS[key] || null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 本地二进制路径(bin/ 下按平台固定名)。 */
|
|
29
|
-
export function binaryPath(dir) {
|
|
30
|
-
return join(dir, "bin", process.platform === "win32" ? "wireproxy.exe" : "wireproxy");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 确保 wireproxy 可用:优先 env 指定 → 本地已下载 → 自动下载解包。
|
|
35
|
-
* @returns {Promise<string>} 可执行文件绝对路径
|
|
36
|
-
*/
|
|
37
|
-
export async function ensureBinary({ dir, bin = "", autodownload = true, fetchImpl = compatFetch, log = () => {}, platform = process.platform, arch = process.arch } = {}) {
|
|
38
|
-
if (bin) {
|
|
39
|
-
if (!existsSync(bin)) throw new Error(`MSLXDFF_WARP_BIN 指向的文件不存在:${bin}`);
|
|
40
|
-
return bin;
|
|
41
|
-
}
|
|
42
|
-
const local = binaryPath(dir);
|
|
43
|
-
if (existsSync(local)) return local;
|
|
44
|
-
|
|
45
|
-
if (!autodownload) throw new Error(`未找到 wireproxy(${local}),且已关闭自动下载:请设 MSLXDFF_WARP_BIN 或开启 MSLXDFF_WARP_AUTODOWNLOAD`);
|
|
46
|
-
const asset = resolveAsset(platform, arch);
|
|
47
|
-
if (!asset) throw new Error(`暂不支持自动下载 wireproxy(平台 ${platform}-${arch}):请自备二进制并设 MSLXDFF_WARP_BIN`);
|
|
48
|
-
|
|
49
|
-
ensureDir(join(dir, "bin"));
|
|
50
|
-
const url = `${RELEASE_BASE}/${WIREPROXY_VERSION}/${asset}`;
|
|
51
|
-
const tarball = join(dir, "bin", asset);
|
|
52
|
-
log(`[warp-pool] 下载 wireproxy ${WIREPROXY_VERSION}(${asset})`);
|
|
53
|
-
const res = await fetchImpl(url, { signal: AbortSignal.timeout(120_000) });
|
|
54
|
-
if (!res.ok) throw new Error(`下载 wireproxy 失败:HTTP ${res.status}`);
|
|
55
|
-
await pipeline(res.body, createWriteStream(tarball));
|
|
56
|
-
|
|
57
|
-
// 解包用系统 tar(Linux/macOS 必带;不引入 npm 依赖去解析 tar 格式)。
|
|
58
|
-
await runCommand("tar", ["-xzf", tarball, "-C", join(dir, "bin")], { log });
|
|
59
|
-
try { rmSync(tarball, { force: true }); } catch {}
|
|
60
|
-
|
|
61
|
-
const extracted = findWireproxy(join(dir, "bin"));
|
|
62
|
-
if (!extracted) throw new Error(`解包后未找到 wireproxy 可执行文件(目录:${join(dir, "bin")})`);
|
|
63
|
-
if (extracted !== local) {
|
|
64
|
-
try { chmodSync(extracted, 0o755); } catch {}
|
|
65
|
-
}
|
|
66
|
-
try { chmodSync(local, 0o755); } catch {}
|
|
67
|
-
return existsSync(local) ? local : extracted;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function findWireproxy(binDir) {
|
|
71
|
-
try {
|
|
72
|
-
const hit = readdirSync(binDir).find((f) => f === "wireproxy" || f === "wireproxy.exe");
|
|
73
|
-
return hit ? join(binDir, hit) : null;
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function runCommand(cmd, args, { log = () => {} } = {}) {
|
|
80
|
-
return new Promise((resolve, reject) => {
|
|
81
|
-
let err = "";
|
|
82
|
-
const p = spawn(cmd, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
83
|
-
p.stderr.on("data", (d) => { err += String(d); });
|
|
84
|
-
p.on("error", reject);
|
|
85
|
-
p.on("close", (code) => {
|
|
86
|
-
if (code === 0) resolve();
|
|
87
|
-
else reject(new Error(`${cmd} 退出码 ${code}${err ? `:${err.slice(0, 200)}` : ""}`));
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** 写出某节点的 wireproxy 配置(含私钥 ⇒ 0600)。 */
|
|
93
|
-
export function writeTunnelConf(dir, node) {
|
|
94
|
-
if (!node?.privateKey) throw new Error(`节点 :${node?.port} 缺 WireGuard 私钥,拒绝写出一份跑不起来的配置`);
|
|
95
|
-
const { conf } = nodePaths(dir, node.port);
|
|
96
|
-
const text = renderWireproxyConf({ ...node, port: node.port });
|
|
97
|
-
writeSecret(conf, text);
|
|
98
|
-
return { conf, bytes: Buffer.byteLength(text) };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* 启动一条隧道。返回 { port, conf, stop() }。
|
|
103
|
-
* 进程退出即视为隧道失效(不自动重启:由 health 层决策重拨换 IP)。
|
|
104
|
-
*/
|
|
105
|
-
export function startTunnel({ binPath: exe, dir, port, log = () => {}, onExit = () => {}, now = Date.now } = {}) {
|
|
106
|
-
const { conf } = nodePaths(dir, port);
|
|
107
|
-
const child = spawn(exe, ["-c", conf], { stdio: ["ignore", "pipe", "pipe"] });
|
|
108
|
-
const startedAt = now();
|
|
109
|
-
let tail = "";
|
|
110
|
-
const keepTail = (d) => {
|
|
111
|
-
tail = (tail + String(d)).slice(-800);
|
|
112
|
-
};
|
|
113
|
-
child.stdout.on("data", keepTail);
|
|
114
|
-
child.stderr.on("data", keepTail);
|
|
115
|
-
|
|
116
|
-
let stopped = false;
|
|
117
|
-
child.on("exit", (code, signal) => {
|
|
118
|
-
if (!stopped) onExit({ port, code, signal, tail, pid: child.pid, aliveMs: now() - startedAt });
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
port,
|
|
123
|
-
conf,
|
|
124
|
-
get pid() {
|
|
125
|
-
return child.pid;
|
|
126
|
-
},
|
|
127
|
-
get alive() {
|
|
128
|
-
return !stopped && child.exitCode === null;
|
|
129
|
-
},
|
|
130
|
-
get tail() {
|
|
131
|
-
return tail;
|
|
132
|
-
},
|
|
133
|
-
stop() {
|
|
134
|
-
if (stopped) return;
|
|
135
|
-
stopped = true;
|
|
136
|
-
try { child.kill("SIGTERM"); } catch {}
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** 探活一个已起的隧道:经该端口访问上游 models 路径(不烧 token)。 */
|
|
142
|
-
export async function probeTunnel({ port, baseUrl = "https://opencode.ai", path = "/zen/v1/models", fetchImpl = compatFetch, timeoutMs = 8000, headers = {}, wantText = false } = {}) {
|
|
143
|
-
const { ProxyAgent } = getUndici();
|
|
144
|
-
const agent = new ProxyAgent({ uri: `http://127.0.0.1:${port}`, connections: 2, keepAliveTimeout: 10_000 });
|
|
145
|
-
const url = `${String(baseUrl).replace(/\/+$/, "")}${path}`;
|
|
146
|
-
const t0 = Date.now();
|
|
147
|
-
try {
|
|
148
|
-
const res = await fetchImpl(url, {
|
|
149
|
-
headers,
|
|
150
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
151
|
-
dispatcher: agent,
|
|
152
|
-
});
|
|
153
|
-
const base = { ok: res.ok, status: res.status, ms: Date.now() - t0, url };
|
|
154
|
-
if (!wantText) return base;
|
|
155
|
-
const text = await res.text().catch(() => "");
|
|
156
|
-
return { ...base, text: String(text).slice(0, 2000) };
|
|
157
|
-
} catch (e) {
|
|
158
|
-
return { ok: false, status: 0, error: String(e?.message || e), ms: Date.now() - t0, url };
|
|
159
|
-
} finally {
|
|
160
|
-
try { await agent.close(); } catch {}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
1
|
+
// warp-pool · wireproxy 进程管理:取二进制 → 生成配置 → 启动 → 守护/销毁。
|
|
2
|
+
// wireproxy(octeep/wireproxy,Go,用户态 WireGuard,无需 root/TUN)把 WARP 隧道暴露为本地 HTTP 代理。
|
|
3
|
+
// 为什么是 [http] 而非 [Socks5]:项目 undici 为 v5(实测 5.29.0),其 ProxyAgent 只接受 http/https,
|
|
4
|
+
// 「SOCKS5 支持」是 undici 后续版本才加的(实测 socks5:// 直接抛 Invalid URL protocol)。
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { chmodSync, existsSync, createWriteStream, readdirSync, rmSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { pipeline } from "node:stream/promises";
|
|
9
|
+
import { compatFetch, getUndici } from "../compat.js";
|
|
10
|
+
import { ensureDir, nodePaths, renderWireproxyConf, writeSecret } from "./nodes.js";
|
|
11
|
+
|
|
12
|
+
export const WIREPROXY_VERSION = "v1.1.3";
|
|
13
|
+
const RELEASE_BASE = "https://github.com/octeep/wireproxy/releases/download";
|
|
14
|
+
|
|
15
|
+
const ASSETS = {
|
|
16
|
+
"linux-x64": "wireproxy_linux_amd64.tar.gz",
|
|
17
|
+
"linux-arm64": "wireproxy_linux_arm64.tar.gz",
|
|
18
|
+
"darwin-x64": "wireproxy_darwin_amd64.tar.gz",
|
|
19
|
+
"darwin-arm64": "wireproxy_darwin_arm64.tar.gz",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** 依平台选发布包;不支持的平台返回 null(改用 MSLXDFF_WARP_BIN 自备二进制)。 */
|
|
23
|
+
export function resolveAsset(platform = process.platform, arch = process.arch) {
|
|
24
|
+
const key = `${platform}-${arch}`;
|
|
25
|
+
return ASSETS[key] || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 本地二进制路径(bin/ 下按平台固定名)。 */
|
|
29
|
+
export function binaryPath(dir) {
|
|
30
|
+
return join(dir, "bin", process.platform === "win32" ? "wireproxy.exe" : "wireproxy");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 确保 wireproxy 可用:优先 env 指定 → 本地已下载 → 自动下载解包。
|
|
35
|
+
* @returns {Promise<string>} 可执行文件绝对路径
|
|
36
|
+
*/
|
|
37
|
+
export async function ensureBinary({ dir, bin = "", autodownload = true, fetchImpl = compatFetch, log = () => {}, platform = process.platform, arch = process.arch } = {}) {
|
|
38
|
+
if (bin) {
|
|
39
|
+
if (!existsSync(bin)) throw new Error(`MSLXDFF_WARP_BIN 指向的文件不存在:${bin}`);
|
|
40
|
+
return bin;
|
|
41
|
+
}
|
|
42
|
+
const local = binaryPath(dir);
|
|
43
|
+
if (existsSync(local)) return local;
|
|
44
|
+
|
|
45
|
+
if (!autodownload) throw new Error(`未找到 wireproxy(${local}),且已关闭自动下载:请设 MSLXDFF_WARP_BIN 或开启 MSLXDFF_WARP_AUTODOWNLOAD`);
|
|
46
|
+
const asset = resolveAsset(platform, arch);
|
|
47
|
+
if (!asset) throw new Error(`暂不支持自动下载 wireproxy(平台 ${platform}-${arch}):请自备二进制并设 MSLXDFF_WARP_BIN`);
|
|
48
|
+
|
|
49
|
+
ensureDir(join(dir, "bin"));
|
|
50
|
+
const url = `${RELEASE_BASE}/${WIREPROXY_VERSION}/${asset}`;
|
|
51
|
+
const tarball = join(dir, "bin", asset);
|
|
52
|
+
log(`[warp-pool] 下载 wireproxy ${WIREPROXY_VERSION}(${asset})`);
|
|
53
|
+
const res = await fetchImpl(url, { signal: AbortSignal.timeout(120_000) });
|
|
54
|
+
if (!res.ok) throw new Error(`下载 wireproxy 失败:HTTP ${res.status}`);
|
|
55
|
+
await pipeline(res.body, createWriteStream(tarball));
|
|
56
|
+
|
|
57
|
+
// 解包用系统 tar(Linux/macOS 必带;不引入 npm 依赖去解析 tar 格式)。
|
|
58
|
+
await runCommand("tar", ["-xzf", tarball, "-C", join(dir, "bin")], { log });
|
|
59
|
+
try { rmSync(tarball, { force: true }); } catch {}
|
|
60
|
+
|
|
61
|
+
const extracted = findWireproxy(join(dir, "bin"));
|
|
62
|
+
if (!extracted) throw new Error(`解包后未找到 wireproxy 可执行文件(目录:${join(dir, "bin")})`);
|
|
63
|
+
if (extracted !== local) {
|
|
64
|
+
try { chmodSync(extracted, 0o755); } catch {}
|
|
65
|
+
}
|
|
66
|
+
try { chmodSync(local, 0o755); } catch {}
|
|
67
|
+
return existsSync(local) ? local : extracted;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function findWireproxy(binDir) {
|
|
71
|
+
try {
|
|
72
|
+
const hit = readdirSync(binDir).find((f) => f === "wireproxy" || f === "wireproxy.exe");
|
|
73
|
+
return hit ? join(binDir, hit) : null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function runCommand(cmd, args, { log = () => {} } = {}) {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
let err = "";
|
|
82
|
+
const p = spawn(cmd, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
83
|
+
p.stderr.on("data", (d) => { err += String(d); });
|
|
84
|
+
p.on("error", reject);
|
|
85
|
+
p.on("close", (code) => {
|
|
86
|
+
if (code === 0) resolve();
|
|
87
|
+
else reject(new Error(`${cmd} 退出码 ${code}${err ? `:${err.slice(0, 200)}` : ""}`));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 写出某节点的 wireproxy 配置(含私钥 ⇒ 0600)。 */
|
|
93
|
+
export function writeTunnelConf(dir, node) {
|
|
94
|
+
if (!node?.privateKey) throw new Error(`节点 :${node?.port} 缺 WireGuard 私钥,拒绝写出一份跑不起来的配置`);
|
|
95
|
+
const { conf } = nodePaths(dir, node.port);
|
|
96
|
+
const text = renderWireproxyConf({ ...node, port: node.port });
|
|
97
|
+
writeSecret(conf, text);
|
|
98
|
+
return { conf, bytes: Buffer.byteLength(text) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 启动一条隧道。返回 { port, conf, stop() }。
|
|
103
|
+
* 进程退出即视为隧道失效(不自动重启:由 health 层决策重拨换 IP)。
|
|
104
|
+
*/
|
|
105
|
+
export function startTunnel({ binPath: exe, dir, port, log = () => {}, onExit = () => {}, now = Date.now } = {}) {
|
|
106
|
+
const { conf } = nodePaths(dir, port);
|
|
107
|
+
const child = spawn(exe, ["-c", conf], { stdio: ["ignore", "pipe", "pipe"] });
|
|
108
|
+
const startedAt = now();
|
|
109
|
+
let tail = "";
|
|
110
|
+
const keepTail = (d) => {
|
|
111
|
+
tail = (tail + String(d)).slice(-800);
|
|
112
|
+
};
|
|
113
|
+
child.stdout.on("data", keepTail);
|
|
114
|
+
child.stderr.on("data", keepTail);
|
|
115
|
+
|
|
116
|
+
let stopped = false;
|
|
117
|
+
child.on("exit", (code, signal) => {
|
|
118
|
+
if (!stopped) onExit({ port, code, signal, tail, pid: child.pid, aliveMs: now() - startedAt });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
port,
|
|
123
|
+
conf,
|
|
124
|
+
get pid() {
|
|
125
|
+
return child.pid;
|
|
126
|
+
},
|
|
127
|
+
get alive() {
|
|
128
|
+
return !stopped && child.exitCode === null;
|
|
129
|
+
},
|
|
130
|
+
get tail() {
|
|
131
|
+
return tail;
|
|
132
|
+
},
|
|
133
|
+
stop() {
|
|
134
|
+
if (stopped) return;
|
|
135
|
+
stopped = true;
|
|
136
|
+
try { child.kill("SIGTERM"); } catch {}
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 探活一个已起的隧道:经该端口访问上游 models 路径(不烧 token)。 */
|
|
142
|
+
export async function probeTunnel({ port, baseUrl = "https://opencode.ai", path = "/zen/v1/models", fetchImpl = compatFetch, timeoutMs = 8000, headers = {}, wantText = false } = {}) {
|
|
143
|
+
const { ProxyAgent } = getUndici();
|
|
144
|
+
const agent = new ProxyAgent({ uri: `http://127.0.0.1:${port}`, connections: 2, keepAliveTimeout: 10_000 });
|
|
145
|
+
const url = `${String(baseUrl).replace(/\/+$/, "")}${path}`;
|
|
146
|
+
const t0 = Date.now();
|
|
147
|
+
try {
|
|
148
|
+
const res = await fetchImpl(url, {
|
|
149
|
+
headers,
|
|
150
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
151
|
+
dispatcher: agent,
|
|
152
|
+
});
|
|
153
|
+
const base = { ok: res.ok, status: res.status, ms: Date.now() - t0, url };
|
|
154
|
+
if (!wantText) return base;
|
|
155
|
+
const text = await res.text().catch(() => "");
|
|
156
|
+
return { ...base, text: String(text).slice(0, 2000) };
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return { ok: false, status: 0, error: String(e?.message || e), ms: Date.now() - t0, url };
|
|
159
|
+
} finally {
|
|
160
|
+
try { await agent.close(); } catch {}
|
|
161
|
+
}
|
|
162
|
+
}
|