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.
Files changed (330) hide show
  1. package/README.md +3 -3
  2. package/bin/mslxdff.js +1 -1
  3. package/docs/cli_help_mini.md +146 -0
  4. package/package.json +38 -38
  5. package/plugins/README.md +14 -14
  6. package/plugins/prefer-model.mjs.example +14 -14
  7. package/src/anthropic/canonical-json.js +55 -0
  8. package/src/anthropic/count-tokens.js +61 -61
  9. package/src/anthropic/request-body.js +183 -126
  10. package/src/anthropic/request-shape.js +60 -0
  11. package/src/anthropic/session-id.js +58 -0
  12. package/src/anthropic/shared.js +18 -18
  13. package/src/anthropic/sse-aggregate.js +56 -56
  14. package/src/anthropic/tool-media.js +75 -0
  15. package/src/anthropic/translate.js +296 -287
  16. package/src/anthropic/web-search.js +361 -361
  17. package/src/api-kind.js +31 -31
  18. package/src/autostart.js +288 -288
  19. package/src/bench/probe.js +1 -1
  20. package/src/bench/runner.js +1 -1
  21. package/src/bench/via-probe.js +1 -1
  22. package/src/bench/via.js +144 -144
  23. package/src/bench/workbuddy-bench.js +1 -1
  24. package/src/chat/config.js +11 -11
  25. package/src/chat/cooling.js +81 -81
  26. package/src/chat/direct.js +58 -58
  27. package/src/chat/index.js +6 -6
  28. package/src/chat/spinner.js +38 -38
  29. package/src/chat/sse.js +69 -69
  30. package/src/chat/stats.js +247 -247
  31. package/src/chat/store.js +48 -48
  32. package/src/chat/tool-handlers.js +67 -67
  33. package/src/chat/tools.js +1 -1
  34. package/src/chat/upstream.js +59 -59
  35. package/src/chat-pipeline/auto-race.js +158 -158
  36. package/src/chat-pipeline/empty-turn.js +147 -147
  37. package/src/chat-pipeline/engine.js +17 -17
  38. package/src/chat-pipeline/index.js +8 -40
  39. package/src/chat-pipeline/serial-trial.js +279 -283
  40. package/src/chat-pipeline/terminal-verdict.js +132 -147
  41. package/src/chooser.js +54 -54
  42. package/src/claude-desktop/apply.js +101 -101
  43. package/src/claude-desktop/context1m.js +38 -38
  44. package/src/claude-desktop/doctor.js +146 -146
  45. package/src/claude-desktop/slots.js +152 -152
  46. package/src/cli/bootstrap.js +1 -1
  47. package/src/cli/commands/claude-desktop.js +284 -284
  48. package/src/cli/commands/daemon.js +231 -231
  49. package/src/cli/commands/model/list-live.js +22 -22
  50. package/src/cli/commands/model/list-providers.js +76 -76
  51. package/src/cli/commands/model/list-render.js +77 -77
  52. package/src/cli/commands/model/list-sort.js +52 -52
  53. package/src/cli/commands/model/list-tty.js +76 -76
  54. package/src/cli/commands/model/list.js +166 -166
  55. package/src/cli/commands/model/live-models.js +24 -24
  56. package/src/cli/commands/model/stats.js +43 -43
  57. package/src/cli/commands/model/status.js +47 -47
  58. package/src/cli/commands/model.js +51 -51
  59. package/src/cli/commands/provider/add.js +108 -108
  60. package/src/cli/commands/provider/allowlist.js +99 -99
  61. package/src/cli/commands/provider/cline-free.js +99 -99
  62. package/src/cli/commands/provider/cline-login.js +218 -218
  63. package/src/cli/commands/provider/cline-quota.js +65 -65
  64. package/src/cli/commands/provider/codearts-login.js +45 -45
  65. package/src/cli/commands/provider/config.js +119 -119
  66. package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
  67. package/src/cli/commands/provider/keys-guard.js +50 -50
  68. package/src/cli/commands/provider/keys.js +161 -161
  69. package/src/cli/commands/provider/qoder-checkin.js +81 -81
  70. package/src/cli/commands/provider/qoder-login.js +102 -102
  71. package/src/cli/commands/provider/raccoon-login.js +180 -180
  72. package/src/cli/commands/provider/raccoon-password.js +162 -162
  73. package/src/cli/commands/provider/raccoon-quota.js +109 -109
  74. package/src/cli/commands/provider/traework-checkin.js +144 -144
  75. package/src/cli/commands/provider/traework-login.js +164 -164
  76. package/src/cli/commands/provider/workbuddy-login.js +153 -153
  77. package/src/cli/commands/provider/zcode-login.js +77 -77
  78. package/src/cli/commands/provider/zcode-quota.js +55 -55
  79. package/src/cli/commands/provider.js +1 -1
  80. package/src/cli/commands/stats.js +158 -149
  81. package/src/cli/commands/system.js +261 -261
  82. package/src/cli/commands/timezone.js +47 -47
  83. package/src/cli/commands/use-group.js +60 -60
  84. package/src/cli/commands/workbuddy.js +189 -189
  85. package/src/cli/format.js +141 -141
  86. package/src/cli/group-helpers.js +70 -70
  87. package/src/cli/help.js +80 -80
  88. package/src/cli/index.js +70 -70
  89. package/src/cli/interactive.js +121 -121
  90. package/src/cli/join-core.js +99 -99
  91. package/src/cli/join-wizard.js +175 -175
  92. package/src/cli/policy.js +136 -136
  93. package/src/cli/provider-row.js +126 -126
  94. package/src/cli/util.js +24 -24
  95. package/src/compat.js +53 -53
  96. package/src/events.js +38 -38
  97. package/src/free-lane.js +131 -131
  98. package/src/free-watcher.js +63 -63
  99. package/src/groups.js +262 -262
  100. package/src/logs.js +195 -195
  101. package/src/metrics.js +71 -62
  102. package/src/model-capabilities/context-lookup.js +65 -65
  103. package/src/model-capabilities/merge.js +142 -142
  104. package/src/model-trace.js +187 -187
  105. package/src/models.js +1 -1
  106. package/src/opencode-identity.js +72 -72
  107. package/src/peers.js +254 -254
  108. package/src/plugins.js +107 -107
  109. package/src/providers/AGENTS.md +49 -0
  110. package/src/providers/base.js +164 -164
  111. package/src/providers/classify.js +22 -22
  112. package/src/providers/cline/allowlist-sync.js +93 -93
  113. package/src/providers/cline/free-catalog.js +71 -71
  114. package/src/providers/cline/headers.js +37 -37
  115. package/src/providers/cline/index.js +130 -130
  116. package/src/providers/cline/models.js +150 -150
  117. package/src/providers/cline/usage.js +274 -274
  118. package/src/providers/codearts/auth-pool.js +148 -148
  119. package/src/providers/codearts/concurrency.js +34 -34
  120. package/src/providers/codearts/const.js +65 -65
  121. package/src/providers/codearts/dpop.js +95 -95
  122. package/src/providers/codearts/index.js +175 -175
  123. package/src/providers/codearts/login.js +214 -214
  124. package/src/providers/codearts/models.js +182 -182
  125. package/src/providers/codearts/sign.js +96 -96
  126. package/src/providers/codearts/sse.js +204 -204
  127. package/src/providers/codearts/stream.js +209 -209
  128. package/src/providers/codearts/sts.js +124 -124
  129. package/src/providers/codearts.js +1 -1
  130. package/src/providers/dispatcher.js +146 -146
  131. package/src/providers/generic.js +156 -156
  132. package/src/providers/globalqwenwork/account-store.js +141 -141
  133. package/src/providers/globalqwenwork/constants.js +73 -73
  134. package/src/providers/globalqwenwork/crypto.js +2 -2
  135. package/src/providers/globalqwenwork/http.js +2 -2
  136. package/src/providers/globalqwenwork/index.js +331 -331
  137. package/src/providers/globalqwenwork/payload.js +147 -147
  138. package/src/providers/globalqwenwork/rsa.js +2 -2
  139. package/src/providers/globalqwenwork/sse.js +2 -2
  140. package/src/providers/globalqwenwork/stream.js +2 -2
  141. package/src/providers/globalqwenwork.js +1 -1
  142. package/src/providers/globalworkbuddy/account-store.js +198 -198
  143. package/src/providers/globalworkbuddy/auth.js +97 -97
  144. package/src/providers/globalworkbuddy/catalog.js +138 -138
  145. package/src/providers/globalworkbuddy/constants.js +73 -73
  146. package/src/providers/globalworkbuddy/device-auth.js +120 -120
  147. package/src/providers/globalworkbuddy/errors.js +98 -98
  148. package/src/providers/globalworkbuddy/headers.js +89 -89
  149. package/src/providers/globalworkbuddy/models.js +96 -96
  150. package/src/providers/globalworkbuddy/payload.js +80 -80
  151. package/src/providers/index.js +5 -5
  152. package/src/providers/model-id.js +172 -172
  153. package/src/providers/opencode.js +19 -19
  154. package/src/providers/openrouter.js +31 -31
  155. package/src/providers/qoder/account-store.js +106 -106
  156. package/src/providers/qoder/aggregate.js +60 -60
  157. package/src/providers/qoder/baseprompt.json +591 -591
  158. package/src/providers/qoder/checkin.js +160 -160
  159. package/src/providers/qoder/constants.js +44 -44
  160. package/src/providers/qoder/encode.js +41 -41
  161. package/src/providers/qoder/fingerprint.js +38 -38
  162. package/src/providers/qoder/models.js +77 -77
  163. package/src/providers/qoder/oauth.js +84 -84
  164. package/src/providers/qoder/payload.js +162 -162
  165. package/src/providers/qoder/request.js +27 -27
  166. package/src/providers/qoder/session.js +134 -134
  167. package/src/providers/qoder/sticky.js +38 -38
  168. package/src/providers/qoder/stream.js +116 -116
  169. package/src/providers/qoder.js +1 -1
  170. package/src/providers/raccoon/account-store.js +216 -216
  171. package/src/providers/raccoon/auth.js +67 -67
  172. package/src/providers/raccoon/chat.js +228 -228
  173. package/src/providers/raccoon/const.js +180 -180
  174. package/src/providers/raccoon/credits.js +185 -185
  175. package/src/providers/raccoon/crypto.js +43 -43
  176. package/src/providers/raccoon/envelope.js +31 -31
  177. package/src/providers/raccoon/headers.js +45 -45
  178. package/src/providers/raccoon/index.js +216 -216
  179. package/src/providers/raccoon/login.js +279 -279
  180. package/src/providers/raccoon/models.js +160 -160
  181. package/src/providers/raccoon/password-login.js +102 -102
  182. package/src/providers/raccoon/qr-codewords.js +115 -115
  183. package/src/providers/raccoon/qr.js +236 -236
  184. package/src/providers/raccoon/sse.js +135 -135
  185. package/src/providers/responses-channel.js +120 -120
  186. package/src/providers/share-keys.js +53 -53
  187. package/src/providers/traework/account-store.js +234 -234
  188. package/src/providers/traework/chat.js +217 -217
  189. package/src/providers/traework/checkin.js +77 -77
  190. package/src/providers/traework/constants.js +41 -41
  191. package/src/providers/traework/errors.js +52 -52
  192. package/src/providers/traework/headers.js +79 -79
  193. package/src/providers/traework/index.js +132 -132
  194. package/src/providers/traework/market-identity.js +116 -116
  195. package/src/providers/traework/models.js +98 -98
  196. package/src/providers/traework/payload.js +86 -86
  197. package/src/providers/traework/sse.js +184 -184
  198. package/src/providers/traework/token.js +52 -52
  199. package/src/providers/traework.js +2 -2
  200. package/src/providers/workbuddy/account-store.js +186 -186
  201. package/src/providers/workbuddy/auth.js +158 -158
  202. package/src/providers/workbuddy/balance.js +86 -86
  203. package/src/providers/workbuddy/cat-travel.js +125 -125
  204. package/src/providers/workbuddy/checkin.js +75 -75
  205. package/src/providers/workbuddy/desktop-info.js +78 -78
  206. package/src/providers/workbuddy/growth-api.js +67 -67
  207. package/src/providers/workbuddy/growth-plans.js +63 -63
  208. package/src/providers/workbuddy/growth.js +181 -181
  209. package/src/providers/workbuddy/index.js +217 -217
  210. package/src/providers/workbuddy/models.js +126 -126
  211. package/src/providers/workbuddy/payload.js +141 -141
  212. package/src/providers/workbuddy/rotation-log.js +83 -83
  213. package/src/providers/workbuddy/sanitize-tools.js +61 -61
  214. package/src/providers/workbuddy/sdk-chat.js +26 -26
  215. package/src/providers/workbuddy-balance.js +78 -78
  216. package/src/providers/zcode/account-store.js +129 -129
  217. package/src/providers/zcode/auth.js +28 -28
  218. package/src/providers/zcode/chat.js +179 -179
  219. package/src/providers/zcode/const.js +54 -54
  220. package/src/providers/zcode/context-shape.js +162 -162
  221. package/src/providers/zcode/headers.js +93 -93
  222. package/src/providers/zcode/index.js +124 -124
  223. package/src/providers/zcode/models.js +65 -65
  224. package/src/providers/zcode/oauth.js +120 -120
  225. package/src/providers/zcode/quota.js +176 -176
  226. package/src/providers/zcode/sse.js +196 -196
  227. package/src/readline-compat.js +25 -25
  228. package/src/responses/translate.js +311 -311
  229. package/src/routes/AGENTS.md +37 -0
  230. package/src/routes/chat/broadband-handler.js +72 -72
  231. package/src/routes/chat/empty-turn-report.js +31 -31
  232. package/src/routes/chat/exhausted-handler.js +87 -87
  233. package/src/routes/chat/hedge-handler.js +130 -130
  234. package/src/routes/chat/index.js +1 -1
  235. package/src/routes/chat/local-handler.js +63 -63
  236. package/src/routes/chat/peer-handler.js +81 -81
  237. package/src/routes/chat.js +3 -3
  238. package/src/routes/fallback.js +88 -88
  239. package/src/routes/groups-relay.js +214 -214
  240. package/src/routes/groups.js +122 -122
  241. package/src/routes/helpers.js +93 -83
  242. package/src/routes/index.js +147 -147
  243. package/src/routes/messages-route.js +375 -365
  244. package/src/routes/peers.js +10 -2
  245. package/src/routes/relay-queue.js +181 -181
  246. package/src/routes/relay.js +60 -60
  247. package/src/routes/responses-route.js +213 -213
  248. package/src/routes/stream-body.js +84 -84
  249. package/src/routes/stream-hold.js +337 -337
  250. package/src/routes/stream-scan.js +366 -366
  251. package/src/routes.js +3 -3
  252. package/src/runtime/auto-update.js +113 -113
  253. package/src/runtime/broadband-stream.js +79 -79
  254. package/src/runtime/broadband.js +103 -103
  255. package/src/runtime/group-sync.js +81 -81
  256. package/src/runtime/keep-warm.js +120 -120
  257. package/src/runtime/lifecycle-forensics.js +116 -116
  258. package/src/runtime/lifecycle-log.js +87 -87
  259. package/src/runtime/log-stamp.js +73 -73
  260. package/src/runtime/proc-info.js +137 -137
  261. package/src/runtime/qoder-checkin.js +93 -93
  262. package/src/runtime/raccoon-checkin.js +126 -126
  263. package/src/runtime/server-lifecycle.js +189 -189
  264. package/src/runtime/traework-checkin.js +118 -118
  265. package/src/runtime/workbuddy-checkin.js +120 -120
  266. package/src/runtime/workbuddy-growth.js +133 -133
  267. package/src/state/facade.js +58 -58
  268. package/src/state/memory.js +162 -162
  269. package/src/state/merge.js +25 -25
  270. package/src/state/migrations/cline-unify.js +94 -94
  271. package/src/state/migrations.js +33 -33
  272. package/src/state/persist.js +42 -42
  273. package/src/state/provider-config.js +142 -142
  274. package/src/state/schemas/group.js +31 -31
  275. package/src/state/schemas/model.js +94 -94
  276. package/src/state/schemas/peer.js +31 -31
  277. package/src/state/schemas/port.js +10 -10
  278. package/src/state/schemas/provider.js +202 -202
  279. package/src/state/schemas/timezone.js +70 -70
  280. package/src/state/schemas/token.js +48 -48
  281. package/src/state/schemas/use-group.js +102 -102
  282. package/src/state/store.js +176 -176
  283. package/src/state.js +1 -1
  284. package/src/sync-claude-desktop.js +234 -234
  285. package/src/sync-claude.js +152 -152
  286. package/src/sync-codex.js +116 -116
  287. package/src/sync-workbuddy.js +158 -158
  288. package/src/talk-full-redact.js +129 -129
  289. package/src/talk-full.js +131 -131
  290. package/src/talk-log.js +257 -257
  291. package/src/text-tokens.js +52 -52
  292. package/src/thinking-effort.js +67 -67
  293. package/src/time.js +106 -106
  294. package/src/timeline.js +26 -26
  295. package/src/transport/index.js +1 -1
  296. package/src/upstream-engine/index.js +74 -74
  297. package/src/upstream-engine/mode.js +15 -15
  298. package/src/upstream-engine/sdk/attempt.js +221 -221
  299. package/src/upstream-engine/sdk/chat.js +28 -28
  300. package/src/upstream-engine/sdk/convert.js +192 -192
  301. package/src/upstream-engine/sdk/diagnose.js +62 -62
  302. package/src/upstream-engine/sdk/dispatch.js +55 -55
  303. package/src/upstream-engine/sdk/responses.js +176 -176
  304. package/src/upstream-engine/sdk/sse.js +166 -166
  305. package/src/upstream-responses.js +305 -305
  306. package/src/usage/cache-fields.js +58 -0
  307. package/src/usage/record.js +109 -106
  308. package/src/usage/report.js +209 -195
  309. package/src/v6pool/claim.js +157 -157
  310. package/src/v6pool/config.js +65 -65
  311. package/src/v6pool/detect.js +130 -130
  312. package/src/v6pool/index.js +152 -152
  313. package/src/v6pool/prefix.js +140 -140
  314. package/src/v6pool/probe.js +40 -40
  315. package/src/v6pool/report.js +104 -104
  316. package/src/v6pool/rotate.js +263 -263
  317. package/src/warp-pool/config.js +74 -74
  318. package/src/warp-pool/health.js +106 -106
  319. package/src/warp-pool/identity.js +141 -141
  320. package/src/warp-pool/index.js +358 -358
  321. package/src/warp-pool/keys.js +29 -29
  322. package/src/warp-pool/nodes.js +95 -95
  323. package/src/warp-pool/ports.js +160 -160
  324. package/src/warp-pool/register.js +113 -113
  325. package/src/warp-pool/report.js +232 -232
  326. package/src/warp-pool/revive.js +168 -168
  327. package/src/warp-pool/rotate.js +214 -214
  328. package/src/warp-pool/strategy.js +43 -43
  329. package/src/warp-pool/verdict.js +69 -69
  330. package/src/warp-pool/wireproxy.js +162 -162
@@ -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
+ }