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