mslxdff 0.1.203 → 0.1.208

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 (332) hide show
  1. package/README.md +3 -3
  2. package/bin/mslxdff.js +1 -1
  3. package/docs/cli_help_mini.md +147 -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/v6-pool.js +123 -0
  85. package/src/cli/commands/workbuddy.js +189 -189
  86. package/src/cli/format.js +141 -141
  87. package/src/cli/group-helpers.js +70 -70
  88. package/src/cli/help.js +80 -80
  89. package/src/cli/index.js +73 -70
  90. package/src/cli/interactive.js +121 -121
  91. package/src/cli/join-core.js +99 -99
  92. package/src/cli/join-wizard.js +175 -175
  93. package/src/cli/policy.js +136 -136
  94. package/src/cli/provider-row.js +126 -126
  95. package/src/cli/util.js +24 -24
  96. package/src/compat.js +53 -53
  97. package/src/events.js +38 -38
  98. package/src/free-lane.js +131 -131
  99. package/src/free-watcher.js +63 -63
  100. package/src/groups.js +262 -262
  101. package/src/logs.js +195 -195
  102. package/src/metrics.js +71 -62
  103. package/src/model-capabilities/context-lookup.js +65 -65
  104. package/src/model-capabilities/merge.js +142 -142
  105. package/src/model-trace.js +187 -187
  106. package/src/models.js +1 -1
  107. package/src/opencode-identity.js +72 -72
  108. package/src/peers.js +254 -254
  109. package/src/plugins.js +107 -107
  110. package/src/providers/AGENTS.md +49 -0
  111. package/src/providers/base.js +164 -164
  112. package/src/providers/classify.js +22 -22
  113. package/src/providers/cline/allowlist-sync.js +93 -93
  114. package/src/providers/cline/free-catalog.js +71 -71
  115. package/src/providers/cline/headers.js +37 -37
  116. package/src/providers/cline/index.js +130 -130
  117. package/src/providers/cline/models.js +150 -150
  118. package/src/providers/cline/usage.js +274 -274
  119. package/src/providers/codearts/auth-pool.js +148 -148
  120. package/src/providers/codearts/concurrency.js +34 -34
  121. package/src/providers/codearts/const.js +65 -65
  122. package/src/providers/codearts/dpop.js +95 -95
  123. package/src/providers/codearts/index.js +175 -175
  124. package/src/providers/codearts/login.js +214 -214
  125. package/src/providers/codearts/models.js +182 -182
  126. package/src/providers/codearts/sign.js +96 -96
  127. package/src/providers/codearts/sse.js +204 -204
  128. package/src/providers/codearts/stream.js +209 -209
  129. package/src/providers/codearts/sts.js +124 -124
  130. package/src/providers/codearts.js +1 -1
  131. package/src/providers/dispatcher.js +146 -146
  132. package/src/providers/generic.js +156 -156
  133. package/src/providers/globalqwenwork/account-store.js +141 -141
  134. package/src/providers/globalqwenwork/constants.js +73 -73
  135. package/src/providers/globalqwenwork/crypto.js +2 -2
  136. package/src/providers/globalqwenwork/http.js +2 -2
  137. package/src/providers/globalqwenwork/index.js +331 -331
  138. package/src/providers/globalqwenwork/payload.js +147 -147
  139. package/src/providers/globalqwenwork/rsa.js +2 -2
  140. package/src/providers/globalqwenwork/sse.js +2 -2
  141. package/src/providers/globalqwenwork/stream.js +2 -2
  142. package/src/providers/globalqwenwork.js +1 -1
  143. package/src/providers/globalworkbuddy/account-store.js +198 -198
  144. package/src/providers/globalworkbuddy/auth.js +97 -97
  145. package/src/providers/globalworkbuddy/catalog.js +138 -138
  146. package/src/providers/globalworkbuddy/constants.js +73 -73
  147. package/src/providers/globalworkbuddy/device-auth.js +120 -120
  148. package/src/providers/globalworkbuddy/errors.js +98 -98
  149. package/src/providers/globalworkbuddy/headers.js +89 -89
  150. package/src/providers/globalworkbuddy/models.js +96 -96
  151. package/src/providers/globalworkbuddy/payload.js +80 -80
  152. package/src/providers/index.js +5 -5
  153. package/src/providers/model-id.js +172 -172
  154. package/src/providers/opencode.js +19 -19
  155. package/src/providers/openrouter.js +31 -31
  156. package/src/providers/qoder/account-store.js +106 -106
  157. package/src/providers/qoder/aggregate.js +60 -60
  158. package/src/providers/qoder/baseprompt.json +591 -591
  159. package/src/providers/qoder/checkin.js +160 -160
  160. package/src/providers/qoder/constants.js +44 -44
  161. package/src/providers/qoder/encode.js +41 -41
  162. package/src/providers/qoder/fingerprint.js +38 -38
  163. package/src/providers/qoder/models.js +77 -77
  164. package/src/providers/qoder/oauth.js +84 -84
  165. package/src/providers/qoder/payload.js +162 -162
  166. package/src/providers/qoder/request.js +27 -27
  167. package/src/providers/qoder/session.js +134 -134
  168. package/src/providers/qoder/sticky.js +38 -38
  169. package/src/providers/qoder/stream.js +116 -116
  170. package/src/providers/qoder.js +1 -1
  171. package/src/providers/raccoon/account-store.js +216 -216
  172. package/src/providers/raccoon/auth.js +67 -67
  173. package/src/providers/raccoon/chat.js +228 -228
  174. package/src/providers/raccoon/const.js +180 -180
  175. package/src/providers/raccoon/credits.js +185 -185
  176. package/src/providers/raccoon/crypto.js +43 -43
  177. package/src/providers/raccoon/envelope.js +31 -31
  178. package/src/providers/raccoon/headers.js +45 -45
  179. package/src/providers/raccoon/index.js +216 -216
  180. package/src/providers/raccoon/login.js +279 -279
  181. package/src/providers/raccoon/models.js +160 -160
  182. package/src/providers/raccoon/password-login.js +102 -102
  183. package/src/providers/raccoon/qr-codewords.js +115 -115
  184. package/src/providers/raccoon/qr.js +236 -236
  185. package/src/providers/raccoon/sse.js +135 -135
  186. package/src/providers/responses-channel.js +120 -120
  187. package/src/providers/share-keys.js +53 -53
  188. package/src/providers/traework/account-store.js +234 -234
  189. package/src/providers/traework/chat.js +217 -217
  190. package/src/providers/traework/checkin.js +77 -77
  191. package/src/providers/traework/constants.js +41 -41
  192. package/src/providers/traework/errors.js +52 -52
  193. package/src/providers/traework/headers.js +79 -79
  194. package/src/providers/traework/index.js +132 -132
  195. package/src/providers/traework/market-identity.js +116 -116
  196. package/src/providers/traework/models.js +98 -98
  197. package/src/providers/traework/payload.js +86 -86
  198. package/src/providers/traework/sse.js +184 -184
  199. package/src/providers/traework/token.js +52 -52
  200. package/src/providers/traework.js +2 -2
  201. package/src/providers/workbuddy/account-store.js +186 -186
  202. package/src/providers/workbuddy/auth.js +158 -158
  203. package/src/providers/workbuddy/balance.js +86 -86
  204. package/src/providers/workbuddy/cat-travel.js +125 -125
  205. package/src/providers/workbuddy/checkin.js +75 -75
  206. package/src/providers/workbuddy/desktop-info.js +78 -78
  207. package/src/providers/workbuddy/growth-api.js +67 -67
  208. package/src/providers/workbuddy/growth-plans.js +63 -63
  209. package/src/providers/workbuddy/growth.js +181 -181
  210. package/src/providers/workbuddy/index.js +217 -217
  211. package/src/providers/workbuddy/models.js +126 -126
  212. package/src/providers/workbuddy/payload.js +141 -141
  213. package/src/providers/workbuddy/rotation-log.js +83 -83
  214. package/src/providers/workbuddy/sanitize-tools.js +61 -61
  215. package/src/providers/workbuddy/sdk-chat.js +26 -26
  216. package/src/providers/workbuddy-balance.js +78 -78
  217. package/src/providers/zcode/account-store.js +129 -129
  218. package/src/providers/zcode/auth.js +28 -28
  219. package/src/providers/zcode/chat.js +179 -179
  220. package/src/providers/zcode/const.js +54 -54
  221. package/src/providers/zcode/context-shape.js +162 -162
  222. package/src/providers/zcode/headers.js +93 -93
  223. package/src/providers/zcode/index.js +124 -124
  224. package/src/providers/zcode/models.js +65 -65
  225. package/src/providers/zcode/oauth.js +120 -120
  226. package/src/providers/zcode/quota.js +176 -176
  227. package/src/providers/zcode/sse.js +196 -196
  228. package/src/readline-compat.js +25 -25
  229. package/src/responses/translate.js +311 -311
  230. package/src/routes/AGENTS.md +37 -0
  231. package/src/routes/chat/broadband-handler.js +72 -72
  232. package/src/routes/chat/empty-turn-report.js +31 -31
  233. package/src/routes/chat/exhausted-handler.js +87 -87
  234. package/src/routes/chat/hedge-handler.js +130 -130
  235. package/src/routes/chat/index.js +1 -1
  236. package/src/routes/chat/local-handler.js +63 -63
  237. package/src/routes/chat/peer-handler.js +81 -81
  238. package/src/routes/chat.js +3 -3
  239. package/src/routes/fallback.js +88 -88
  240. package/src/routes/groups-relay.js +214 -214
  241. package/src/routes/groups.js +122 -122
  242. package/src/routes/helpers.js +93 -83
  243. package/src/routes/index.js +147 -147
  244. package/src/routes/messages-route.js +375 -365
  245. package/src/routes/peers.js +10 -2
  246. package/src/routes/relay-queue.js +181 -181
  247. package/src/routes/relay.js +60 -60
  248. package/src/routes/responses-route.js +213 -213
  249. package/src/routes/stream-body.js +84 -84
  250. package/src/routes/stream-hold.js +337 -337
  251. package/src/routes/stream-scan.js +366 -366
  252. package/src/routes.js +3 -3
  253. package/src/runtime/auto-update.js +113 -113
  254. package/src/runtime/broadband-stream.js +79 -79
  255. package/src/runtime/broadband.js +103 -103
  256. package/src/runtime/group-sync.js +81 -81
  257. package/src/runtime/keep-warm.js +120 -120
  258. package/src/runtime/lifecycle-forensics.js +116 -116
  259. package/src/runtime/lifecycle-log.js +87 -87
  260. package/src/runtime/log-stamp.js +73 -73
  261. package/src/runtime/proc-info.js +137 -137
  262. package/src/runtime/qoder-checkin.js +93 -93
  263. package/src/runtime/raccoon-checkin.js +126 -126
  264. package/src/runtime/server-lifecycle.js +189 -189
  265. package/src/runtime/traework-checkin.js +118 -118
  266. package/src/runtime/workbuddy-checkin.js +120 -120
  267. package/src/runtime/workbuddy-growth.js +133 -133
  268. package/src/state/facade.js +58 -58
  269. package/src/state/memory.js +162 -162
  270. package/src/state/merge.js +25 -25
  271. package/src/state/migrations/cline-unify.js +94 -94
  272. package/src/state/migrations.js +33 -33
  273. package/src/state/persist.js +42 -42
  274. package/src/state/provider-config.js +142 -142
  275. package/src/state/schemas/group.js +31 -31
  276. package/src/state/schemas/model.js +94 -94
  277. package/src/state/schemas/peer.js +31 -31
  278. package/src/state/schemas/port.js +10 -10
  279. package/src/state/schemas/provider.js +202 -202
  280. package/src/state/schemas/timezone.js +70 -70
  281. package/src/state/schemas/token.js +48 -48
  282. package/src/state/schemas/use-group.js +102 -102
  283. package/src/state/store.js +176 -176
  284. package/src/state.js +1 -1
  285. package/src/sync-claude-desktop.js +234 -234
  286. package/src/sync-claude.js +152 -152
  287. package/src/sync-codex.js +116 -116
  288. package/src/sync-workbuddy.js +158 -158
  289. package/src/talk-full-redact.js +129 -129
  290. package/src/talk-full.js +131 -131
  291. package/src/talk-log.js +257 -257
  292. package/src/text-tokens.js +52 -52
  293. package/src/thinking-effort.js +67 -67
  294. package/src/time.js +106 -106
  295. package/src/timeline.js +26 -26
  296. package/src/transport/index.js +1 -1
  297. package/src/upstream-engine/index.js +74 -74
  298. package/src/upstream-engine/mode.js +15 -15
  299. package/src/upstream-engine/sdk/attempt.js +221 -221
  300. package/src/upstream-engine/sdk/chat.js +28 -28
  301. package/src/upstream-engine/sdk/convert.js +192 -192
  302. package/src/upstream-engine/sdk/diagnose.js +62 -62
  303. package/src/upstream-engine/sdk/dispatch.js +55 -55
  304. package/src/upstream-engine/sdk/responses.js +176 -176
  305. package/src/upstream-engine/sdk/sse.js +166 -166
  306. package/src/upstream-responses.js +305 -305
  307. package/src/usage/cache-fields.js +58 -0
  308. package/src/usage/record.js +109 -106
  309. package/src/usage/report.js +209 -195
  310. package/src/v6pool/claim.js +157 -157
  311. package/src/v6pool/config.js +65 -65
  312. package/src/v6pool/detect.js +130 -130
  313. package/src/v6pool/index.js +152 -152
  314. package/src/v6pool/prefix.js +140 -140
  315. package/src/v6pool/probe.js +40 -40
  316. package/src/v6pool/report.js +104 -104
  317. package/src/v6pool/rotate.js +263 -263
  318. package/src/v6pool/selftest.js +154 -0
  319. package/src/warp-pool/config.js +74 -74
  320. package/src/warp-pool/health.js +106 -106
  321. package/src/warp-pool/identity.js +141 -141
  322. package/src/warp-pool/index.js +358 -358
  323. package/src/warp-pool/keys.js +29 -29
  324. package/src/warp-pool/nodes.js +95 -95
  325. package/src/warp-pool/ports.js +160 -160
  326. package/src/warp-pool/register.js +113 -113
  327. package/src/warp-pool/report.js +232 -232
  328. package/src/warp-pool/revive.js +168 -168
  329. package/src/warp-pool/rotate.js +214 -214
  330. package/src/warp-pool/strategy.js +43 -43
  331. package/src/warp-pool/verdict.js +69 -69
  332. package/src/warp-pool/wireproxy.js +162 -162
@@ -1,29 +1,29 @@
1
- // warp-pool · WireGuard 密钥(`wg genkey` / `wg pubkey` 的 Node 原生等价物)。
2
- // WireGuard 用 Curve25519(x25519):密钥是 32 字节 raw,编码为 44 字符 base64。
3
- // Node 原生 generateKeyPairSync('x25519') 导出的 DER 末 32 字节即 raw key(本机实测通过)。
4
- import crypto from "node:crypto";
5
-
6
- const RAW_BYTES = 32;
7
-
8
- /** DER(SPKI/PKCS8) 的末 32 字节就是 Curve25519 raw key。 */
9
- function rawTail(der) {
10
- const raw = der.subarray(-RAW_BYTES);
11
- if (raw.length !== RAW_BYTES) throw new Error("x25519 密钥导出异常:DER 长度不足 32 字节");
12
- return raw;
13
- }
14
-
15
- /**
16
- * 生成一对 WireGuard 密钥(base64,44 字符)。
17
- * 注册 WARP 设备时把 publicKey 填进请求体的 `key` 字段。
18
- */
19
- export function generateWgKeyPair() {
20
- const { publicKey, privateKey } = crypto.generateKeyPairSync("x25519");
21
- const privateKeyB64 = rawTail(privateKey.export({ type: "pkcs8", format: "der" })).toString("base64");
22
- const publicKeyB64 = rawTail(publicKey.export({ type: "spki", format: "der" })).toString("base64");
23
- return { privateKey: privateKeyB64, publicKey: publicKeyB64 };
24
- }
25
-
26
- /** WireGuard base64 密钥形状自检:恒 44 字符、以 '=' 收尾(Nothing-up-my-sleeve 检查用)。 */
27
- export function isWgKey(v) {
28
- return typeof v === "string" && /^[A-Za-z0-9+/]{43}=$/.test(v);
29
- }
1
+ // warp-pool · WireGuard 密钥(`wg genkey` / `wg pubkey` 的 Node 原生等价物)。
2
+ // WireGuard 用 Curve25519(x25519):密钥是 32 字节 raw,编码为 44 字符 base64。
3
+ // Node 原生 generateKeyPairSync('x25519') 导出的 DER 末 32 字节即 raw key(本机实测通过)。
4
+ import crypto from "node:crypto";
5
+
6
+ const RAW_BYTES = 32;
7
+
8
+ /** DER(SPKI/PKCS8) 的末 32 字节就是 Curve25519 raw key。 */
9
+ function rawTail(der) {
10
+ const raw = der.subarray(-RAW_BYTES);
11
+ if (raw.length !== RAW_BYTES) throw new Error("x25519 密钥导出异常:DER 长度不足 32 字节");
12
+ return raw;
13
+ }
14
+
15
+ /**
16
+ * 生成一对 WireGuard 密钥(base64,44 字符)。
17
+ * 注册 WARP 设备时把 publicKey 填进请求体的 `key` 字段。
18
+ */
19
+ export function generateWgKeyPair() {
20
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("x25519");
21
+ const privateKeyB64 = rawTail(privateKey.export({ type: "pkcs8", format: "der" })).toString("base64");
22
+ const publicKeyB64 = rawTail(publicKey.export({ type: "spki", format: "der" })).toString("base64");
23
+ return { privateKey: privateKeyB64, publicKey: publicKeyB64 };
24
+ }
25
+
26
+ /** WireGuard base64 密钥形状自检:恒 44 字符、以 '=' 收尾(Nothing-up-my-sleeve 检查用)。 */
27
+ export function isWgKey(v) {
28
+ return typeof v === "string" && /^[A-Za-z0-9+/]{43}=$/.test(v);
29
+ }
@@ -1,95 +1,95 @@
1
- // warp-pool · 节点元数据持久化。
2
- // 含 WireGuard 私钥文件与设备 token ⇒ 一律 0600,且只落在日志根(MSLXDFF_WARP_DIR),不进工作树。
3
- import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
- import { join } from "node:path";
5
-
6
- const SECRET_MODE = 0o600;
7
-
8
- /** 节点两件套:WireGuard 配置(含私钥)与元数据(含设备 token)。 */
9
- export function nodePaths(dir, port) {
10
- return {
11
- conf: join(dir, `node-${port}.conf`),
12
- meta: join(dir, `node-${port}.json`),
13
- };
14
- }
15
-
16
- /** 写敏感文件:新建用 0600,存量补 chmod(mode 只在新建时生效)。 */
17
- export function writeSecret(file, content) {
18
- const existed = existsSync(file);
19
- writeFileSync(file, content, existed ? undefined : { mode: SECRET_MODE });
20
- try { chmodSync(file, SECRET_MODE); } catch {}
21
- return file;
22
- }
23
-
24
- export function ensureDir(dir) {
25
- mkdirSync(dir, { recursive: true, mode: 0o700 });
26
- return dir;
27
- }
28
-
29
- /** 读单个节点元数据;文件缺失/损坏返回 null,绝不抛。 */
30
- export function loadNode(dir, port) {
31
- const { meta } = nodePaths(dir, port);
32
- try {
33
- const j = JSON.parse(readFileSync(meta, "utf8"));
34
- if (!j || typeof j !== "object") return null;
35
- return {
36
- port,
37
- id: String(j.id || ""),
38
- token: String(j.token || ""),
39
- v4: String(j.v4 || ""),
40
- v6: String(j.v6 || ""),
41
- endpoint: String(j.endpoint || ""),
42
- peerKey: String(j.peerKey || ""),
43
- privateKey: String(j.privateKey || ""), // 必须读回:bringUp 靠它渲染 conf,漏了就是 PrivateKey = undefined(2026-10-16 真机事故:复用节点的隧道全起不来)
44
- registeredAt: String(j.registeredAt || ""),
45
- ...(j.cooldownUntil ? { cooldownUntil: Number(j.cooldownUntil) } : {}),
46
- };
47
- } catch {
48
- return null;
49
- }
50
- }
51
-
52
- export function saveNode(dir, node) {
53
- const { meta } = nodePaths(dir, node.port);
54
- return writeSecret(meta, `${JSON.stringify(node, null, 2)}\n`);
55
- }
56
-
57
- export function removeNode(dir, port) {
58
- const { conf, meta } = nodePaths(dir, port);
59
- for (const f of [conf, meta]) {
60
- try { if (existsSync(f)) unlinkSync(f); } catch {}
61
- }
62
- }
63
-
64
- /** 读回配置目录里已注册的节点(按端口升序)。 */
65
- export function loadNodes(dir, ports) {
66
- const out = [];
67
- for (const p of ports) {
68
- const n = loadNode(dir, p);
69
- if (n && n.id && n.token && n.privateKey) out.push(n);
70
- }
71
- return out;
72
- }
73
-
74
- /** WireGuard + wireproxy INI([http] 段把隧道暴露成本地 HTTP 代理出口;undici v5 的 ProxyAgent 只吃 http/https)。 */
75
- export function renderWireproxyConf({ privateKey, v4, v6, endpoint, peerKey, port }) {
76
- const addresses = [
77
- v4 ? `Address = ${v4}/32` : "",
78
- v6 ? `Address = ${v6}/128` : "",
79
- ].filter(Boolean).join("\n");
80
- return `[Interface]
81
- PrivateKey = ${privateKey}
82
- ${addresses}
83
- DNS = 1.1.1.1
84
- MTU = 1280
85
-
86
- [Peer]
87
- PublicKey = ${peerKey}
88
- AllowedIPs = 0.0.0.0/0, ::/0
89
- Endpoint = ${endpoint}
90
- PersistentKeepalive = 25
91
-
92
- [http]
93
- BindAddress = 127.0.0.1:${port}
94
- `;
95
- }
1
+ // warp-pool · 节点元数据持久化。
2
+ // 含 WireGuard 私钥文件与设备 token ⇒ 一律 0600,且只落在日志根(MSLXDFF_WARP_DIR),不进工作树。
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ const SECRET_MODE = 0o600;
7
+
8
+ /** 节点两件套:WireGuard 配置(含私钥)与元数据(含设备 token)。 */
9
+ export function nodePaths(dir, port) {
10
+ return {
11
+ conf: join(dir, `node-${port}.conf`),
12
+ meta: join(dir, `node-${port}.json`),
13
+ };
14
+ }
15
+
16
+ /** 写敏感文件:新建用 0600,存量补 chmod(mode 只在新建时生效)。 */
17
+ export function writeSecret(file, content) {
18
+ const existed = existsSync(file);
19
+ writeFileSync(file, content, existed ? undefined : { mode: SECRET_MODE });
20
+ try { chmodSync(file, SECRET_MODE); } catch {}
21
+ return file;
22
+ }
23
+
24
+ export function ensureDir(dir) {
25
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
26
+ return dir;
27
+ }
28
+
29
+ /** 读单个节点元数据;文件缺失/损坏返回 null,绝不抛。 */
30
+ export function loadNode(dir, port) {
31
+ const { meta } = nodePaths(dir, port);
32
+ try {
33
+ const j = JSON.parse(readFileSync(meta, "utf8"));
34
+ if (!j || typeof j !== "object") return null;
35
+ return {
36
+ port,
37
+ id: String(j.id || ""),
38
+ token: String(j.token || ""),
39
+ v4: String(j.v4 || ""),
40
+ v6: String(j.v6 || ""),
41
+ endpoint: String(j.endpoint || ""),
42
+ peerKey: String(j.peerKey || ""),
43
+ privateKey: String(j.privateKey || ""), // 必须读回:bringUp 靠它渲染 conf,漏了就是 PrivateKey = undefined(2026-10-16 真机事故:复用节点的隧道全起不来)
44
+ registeredAt: String(j.registeredAt || ""),
45
+ ...(j.cooldownUntil ? { cooldownUntil: Number(j.cooldownUntil) } : {}),
46
+ };
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ export function saveNode(dir, node) {
53
+ const { meta } = nodePaths(dir, node.port);
54
+ return writeSecret(meta, `${JSON.stringify(node, null, 2)}\n`);
55
+ }
56
+
57
+ export function removeNode(dir, port) {
58
+ const { conf, meta } = nodePaths(dir, port);
59
+ for (const f of [conf, meta]) {
60
+ try { if (existsSync(f)) unlinkSync(f); } catch {}
61
+ }
62
+ }
63
+
64
+ /** 读回配置目录里已注册的节点(按端口升序)。 */
65
+ export function loadNodes(dir, ports) {
66
+ const out = [];
67
+ for (const p of ports) {
68
+ const n = loadNode(dir, p);
69
+ if (n && n.id && n.token && n.privateKey) out.push(n);
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /** WireGuard + wireproxy INI([http] 段把隧道暴露成本地 HTTP 代理出口;undici v5 的 ProxyAgent 只吃 http/https)。 */
75
+ export function renderWireproxyConf({ privateKey, v4, v6, endpoint, peerKey, port }) {
76
+ const addresses = [
77
+ v4 ? `Address = ${v4}/32` : "",
78
+ v6 ? `Address = ${v6}/128` : "",
79
+ ].filter(Boolean).join("\n");
80
+ return `[Interface]
81
+ PrivateKey = ${privateKey}
82
+ ${addresses}
83
+ DNS = 1.1.1.1
84
+ MTU = 1280
85
+
86
+ [Peer]
87
+ PublicKey = ${peerKey}
88
+ AllowedIPs = 0.0.0.0/0, ::/0
89
+ Endpoint = ${endpoint}
90
+ PersistentKeepalive = 25
91
+
92
+ [http]
93
+ BindAddress = 127.0.0.1:${port}
94
+ `;
95
+ }
@@ -1,160 +1,160 @@
1
- // warp-pool · 端口占用与「上一代进程残留」清理(2026-10-16 真机事故后新增;2026-10-17 七次修订)。
2
- //
3
- // 事故一(0.1.179):daemon 被强杀时 `process.once("exit")` 钩子不执行,wireproxy 子进程变孤儿继续占着端口。
4
- // 事故二(七次修订,真机铁证):**两个 daemon 同时活着**(一个从昨天起活了一整天),两者都用同一份
5
- // `node-<port>.conf` 判「本池残留」⇒ 互相把对方的**活隧道**当残留杀掉 ⇒ 无限互杀、池永远不稳。
6
- // 两条教训写进判据:
7
- // ① 父进程是**另一个活着的 daemon** ⇒ 那不是残留,是「别人的活隧道」,清了就是互杀 ⇒ 不许清,只报出来;
8
- // ② 「SIGKILL 都清不掉」多半是**僵尸**(state=Z:kill 无效,但 kill(pid,0) 仍返回成功)⇒ 僵尸算已死。
9
- import { connect } from "node:net";
10
- import { defaultCmdline, defaultPids, defaultStat, isZombie, otherDaemonParent, parseCmdline, pidAlive } from "../runtime/proc-info.js";
11
- import { nodePaths } from "./nodes.js";
12
- import { fmtReclaim } from "./report.js";
13
-
14
- // /proc 读取层已上移到 runtime/proc-info.js(daemon 侧也要用同一套判据);这里原样再导出,既有调用方路径不变。
15
- export { parseCmdline, pidAlive };
16
-
17
- /**
18
- * 该进程是不是「正在跑我们这条 conf」的 wireproxy。
19
- * 判据刻意收紧(argv 里得有 wireproxy 这个可执行文件名,且 conf 路径逐字相等),
20
- * 免得误杀别人的进程。缺一不可。
21
- */
22
- export function isOurWireproxy(argv, conf) {
23
- const list = Array.isArray(argv) ? argv : [];
24
- const isBin = list.some((a) => /(^|[\\/])wireproxy(\.exe)?$/i.test(a));
25
- return isBin && list.includes(conf);
26
- }
27
-
28
- /**
29
- * 清掉占着该端口那条 conf 的**真残留**:自家丢引用的子进程、父进程已死的孤儿。
30
- * 不动的三类:僵尸(kill 无效,也不是真占用者)、**父进程是另一个活 daemon 的活隧道**(清了就是互杀)、
31
- * argv 不匹配的别人的进程。
32
- * 非 Linux 直接返回空(读不到 /proc,别拿别的平台做危险猜测)。
33
- * @returns {Promise<{scanned:number, killed:number[], zombies:number[],
34
- * foreign:Array<{pid:number,ppid:number,aliveSec:number|null}>, waitedMs:number, gone:boolean}>}
35
- */
36
- export async function reclaimPort({
37
- dir,
38
- port,
39
- log = () => {},
40
- platform = process.platform,
41
- pids = defaultPids,
42
- cmdline = defaultCmdline,
43
- stat = defaultStat,
44
- kill = process.kill,
45
- sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
46
- waitRounds = 15,
47
- } = {}) {
48
- const startedAt = Date.now();
49
- const shape = (scanned, killed, zombies, foreign) => ({
50
- scanned,
51
- killed,
52
- zombies,
53
- foreign,
54
- waitedMs: Date.now() - startedAt,
55
- gone: !killed.some((p) => pidAlive(p, kill)),
56
- });
57
- if (platform !== "linux") return shape(0, [], [], []);
58
-
59
- const conf = nodePaths(dir, port).conf;
60
- const all = pids();
61
- const zombies = [];
62
- const foreign = [];
63
- const targets = [];
64
- for (const pid of all) {
65
- if (!isOurWireproxy(cmdline(pid), conf)) continue;
66
- const st = stat(pid);
67
- if (st && isZombie(st.state)) {
68
- zombies.push(pid);
69
- continue;
70
- }
71
- const other = otherDaemonParent(pid, { stat, cmdline, kill });
72
- if (other) {
73
- foreign.push({ pid, ppid: other.pid, aliveSec: other.aliveSec });
74
- continue;
75
- }
76
- targets.push(pid);
77
- }
78
-
79
- if (targets.length) {
80
- for (const pid of targets) {
81
- try {
82
- kill(pid, "SIGTERM");
83
- } catch {}
84
- }
85
- for (let i = 0; i < waitRounds && targets.some((p) => pidAlive(p, kill)); i++) await sleep(100);
86
- for (const pid of targets.filter((p) => pidAlive(p, kill))) {
87
- try {
88
- kill(pid, "SIGKILL");
89
- } catch {}
90
- }
91
- }
92
- const r = shape(all.length, targets, zombies, foreign);
93
- log(fmtReclaim({ port, ...r }));
94
- return r;
95
- }
96
-
97
- /** 该端口是否已有人在监听(TCP 连得上 = 占用;连不上/被拒 = 空闲)。 */
98
- export function portInUse(port, { host = "127.0.0.1", timeoutMs = 800, connectFn = connect } = {}) {
99
- return new Promise((resolve) => {
100
- let done = false;
101
- const sock = connectFn({ host, port });
102
- const finish = (v) => {
103
- if (done) return;
104
- done = true;
105
- try {
106
- sock.destroy();
107
- } catch {}
108
- resolve(v);
109
- };
110
- sock.setTimeout(timeoutMs, () => finish(false));
111
- sock.once("connect", () => finish(true));
112
- sock.once("error", () => finish(false));
113
- });
114
- }
115
-
116
- /**
117
- * 等端口真正空出来(SIGTERM 之后套接字要几百毫秒才释放,这期间连得上 ≠ 别人的进程占着)。
118
- * 返回 true = 空了;false = 等了 rounds 轮还是被占。
119
- */
120
- export async function waitPortFree(port, { inUse = portInUse, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), rounds = 12, stepMs = 250 } = {}) {
121
- for (let i = 0; i < rounds; i++) {
122
- if (!(await inUse(port))) return true;
123
- await sleep(stepMs);
124
- }
125
- return !(await inUse(port));
126
- }
127
-
128
- /**
129
- * 端口占用者取证:谁在监听——本池残留?另一个 daemon 的活隧道?僵尸?别人的进程?还是空闲。
130
- * 「孤儿占端口」「两个 daemon 互杀」这两类事故唯一能一眼看出结论的读数:只报「占用 / 拒绝再起」是查不出问题的。
131
- * @returns {Promise<{busy:boolean, ours:number[], zombies:number[],
132
- * foreign:Array<{pid:number,ppid:number,aliveSec:number|null}>, others:Array<{pid:number,name:string}>}>}
133
- */
134
- export async function portOwner({ dir, port, platform = process.platform, pids = defaultPids, cmdline = defaultCmdline, stat = defaultStat, kill = process.kill, inUse = portInUse } = {}) {
135
- const busy = await inUse(port);
136
- const out = { busy, ours: [], zombies: [], foreign: [], others: [] };
137
- if (!busy || platform !== "linux") return out;
138
- const conf = nodePaths(dir, port).conf;
139
- for (const pid of pids()) {
140
- const argv = cmdline(pid);
141
- if (!argv.length) continue;
142
- if (isOurWireproxy(argv, conf)) {
143
- const st = stat(pid);
144
- if (st && isZombie(st.state)) {
145
- out.zombies.push(pid);
146
- continue;
147
- }
148
- const other = otherDaemonParent(pid, { stat, cmdline, kill });
149
- if (other) {
150
- out.foreign.push({ pid, ppid: other.pid, aliveSec: other.aliveSec });
151
- continue;
152
- }
153
- out.ours.push(pid);
154
- continue;
155
- }
156
- // 非 root 看不到别人的 listen 表,退一步:命令行里带着这个端口号的进程也值得报出来
157
- if (argv.some((a) => String(a).includes(`:${port}`))) out.others.push({ pid, name: String(argv[0]).split("/").pop() });
158
- }
159
- return out;
160
- }
1
+ // warp-pool · 端口占用与「上一代进程残留」清理(2026-10-16 真机事故后新增;2026-10-17 七次修订)。
2
+ //
3
+ // 事故一(0.1.179):daemon 被强杀时 `process.once("exit")` 钩子不执行,wireproxy 子进程变孤儿继续占着端口。
4
+ // 事故二(七次修订,真机铁证):**两个 daemon 同时活着**(一个从昨天起活了一整天),两者都用同一份
5
+ // `node-<port>.conf` 判「本池残留」⇒ 互相把对方的**活隧道**当残留杀掉 ⇒ 无限互杀、池永远不稳。
6
+ // 两条教训写进判据:
7
+ // ① 父进程是**另一个活着的 daemon** ⇒ 那不是残留,是「别人的活隧道」,清了就是互杀 ⇒ 不许清,只报出来;
8
+ // ② 「SIGKILL 都清不掉」多半是**僵尸**(state=Z:kill 无效,但 kill(pid,0) 仍返回成功)⇒ 僵尸算已死。
9
+ import { connect } from "node:net";
10
+ import { defaultCmdline, defaultPids, defaultStat, isZombie, otherDaemonParent, parseCmdline, pidAlive } from "../runtime/proc-info.js";
11
+ import { nodePaths } from "./nodes.js";
12
+ import { fmtReclaim } from "./report.js";
13
+
14
+ // /proc 读取层已上移到 runtime/proc-info.js(daemon 侧也要用同一套判据);这里原样再导出,既有调用方路径不变。
15
+ export { parseCmdline, pidAlive };
16
+
17
+ /**
18
+ * 该进程是不是「正在跑我们这条 conf」的 wireproxy。
19
+ * 判据刻意收紧(argv 里得有 wireproxy 这个可执行文件名,且 conf 路径逐字相等),
20
+ * 免得误杀别人的进程。缺一不可。
21
+ */
22
+ export function isOurWireproxy(argv, conf) {
23
+ const list = Array.isArray(argv) ? argv : [];
24
+ const isBin = list.some((a) => /(^|[\\/])wireproxy(\.exe)?$/i.test(a));
25
+ return isBin && list.includes(conf);
26
+ }
27
+
28
+ /**
29
+ * 清掉占着该端口那条 conf 的**真残留**:自家丢引用的子进程、父进程已死的孤儿。
30
+ * 不动的三类:僵尸(kill 无效,也不是真占用者)、**父进程是另一个活 daemon 的活隧道**(清了就是互杀)、
31
+ * argv 不匹配的别人的进程。
32
+ * 非 Linux 直接返回空(读不到 /proc,别拿别的平台做危险猜测)。
33
+ * @returns {Promise<{scanned:number, killed:number[], zombies:number[],
34
+ * foreign:Array<{pid:number,ppid:number,aliveSec:number|null}>, waitedMs:number, gone:boolean}>}
35
+ */
36
+ export async function reclaimPort({
37
+ dir,
38
+ port,
39
+ log = () => {},
40
+ platform = process.platform,
41
+ pids = defaultPids,
42
+ cmdline = defaultCmdline,
43
+ stat = defaultStat,
44
+ kill = process.kill,
45
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
46
+ waitRounds = 15,
47
+ } = {}) {
48
+ const startedAt = Date.now();
49
+ const shape = (scanned, killed, zombies, foreign) => ({
50
+ scanned,
51
+ killed,
52
+ zombies,
53
+ foreign,
54
+ waitedMs: Date.now() - startedAt,
55
+ gone: !killed.some((p) => pidAlive(p, kill)),
56
+ });
57
+ if (platform !== "linux") return shape(0, [], [], []);
58
+
59
+ const conf = nodePaths(dir, port).conf;
60
+ const all = pids();
61
+ const zombies = [];
62
+ const foreign = [];
63
+ const targets = [];
64
+ for (const pid of all) {
65
+ if (!isOurWireproxy(cmdline(pid), conf)) continue;
66
+ const st = stat(pid);
67
+ if (st && isZombie(st.state)) {
68
+ zombies.push(pid);
69
+ continue;
70
+ }
71
+ const other = otherDaemonParent(pid, { stat, cmdline, kill });
72
+ if (other) {
73
+ foreign.push({ pid, ppid: other.pid, aliveSec: other.aliveSec });
74
+ continue;
75
+ }
76
+ targets.push(pid);
77
+ }
78
+
79
+ if (targets.length) {
80
+ for (const pid of targets) {
81
+ try {
82
+ kill(pid, "SIGTERM");
83
+ } catch {}
84
+ }
85
+ for (let i = 0; i < waitRounds && targets.some((p) => pidAlive(p, kill)); i++) await sleep(100);
86
+ for (const pid of targets.filter((p) => pidAlive(p, kill))) {
87
+ try {
88
+ kill(pid, "SIGKILL");
89
+ } catch {}
90
+ }
91
+ }
92
+ const r = shape(all.length, targets, zombies, foreign);
93
+ log(fmtReclaim({ port, ...r }));
94
+ return r;
95
+ }
96
+
97
+ /** 该端口是否已有人在监听(TCP 连得上 = 占用;连不上/被拒 = 空闲)。 */
98
+ export function portInUse(port, { host = "127.0.0.1", timeoutMs = 800, connectFn = connect } = {}) {
99
+ return new Promise((resolve) => {
100
+ let done = false;
101
+ const sock = connectFn({ host, port });
102
+ const finish = (v) => {
103
+ if (done) return;
104
+ done = true;
105
+ try {
106
+ sock.destroy();
107
+ } catch {}
108
+ resolve(v);
109
+ };
110
+ sock.setTimeout(timeoutMs, () => finish(false));
111
+ sock.once("connect", () => finish(true));
112
+ sock.once("error", () => finish(false));
113
+ });
114
+ }
115
+
116
+ /**
117
+ * 等端口真正空出来(SIGTERM 之后套接字要几百毫秒才释放,这期间连得上 ≠ 别人的进程占着)。
118
+ * 返回 true = 空了;false = 等了 rounds 轮还是被占。
119
+ */
120
+ export async function waitPortFree(port, { inUse = portInUse, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), rounds = 12, stepMs = 250 } = {}) {
121
+ for (let i = 0; i < rounds; i++) {
122
+ if (!(await inUse(port))) return true;
123
+ await sleep(stepMs);
124
+ }
125
+ return !(await inUse(port));
126
+ }
127
+
128
+ /**
129
+ * 端口占用者取证:谁在监听——本池残留?另一个 daemon 的活隧道?僵尸?别人的进程?还是空闲。
130
+ * 「孤儿占端口」「两个 daemon 互杀」这两类事故唯一能一眼看出结论的读数:只报「占用 / 拒绝再起」是查不出问题的。
131
+ * @returns {Promise<{busy:boolean, ours:number[], zombies:number[],
132
+ * foreign:Array<{pid:number,ppid:number,aliveSec:number|null}>, others:Array<{pid:number,name:string}>}>}
133
+ */
134
+ export async function portOwner({ dir, port, platform = process.platform, pids = defaultPids, cmdline = defaultCmdline, stat = defaultStat, kill = process.kill, inUse = portInUse } = {}) {
135
+ const busy = await inUse(port);
136
+ const out = { busy, ours: [], zombies: [], foreign: [], others: [] };
137
+ if (!busy || platform !== "linux") return out;
138
+ const conf = nodePaths(dir, port).conf;
139
+ for (const pid of pids()) {
140
+ const argv = cmdline(pid);
141
+ if (!argv.length) continue;
142
+ if (isOurWireproxy(argv, conf)) {
143
+ const st = stat(pid);
144
+ if (st && isZombie(st.state)) {
145
+ out.zombies.push(pid);
146
+ continue;
147
+ }
148
+ const other = otherDaemonParent(pid, { stat, cmdline, kill });
149
+ if (other) {
150
+ out.foreign.push({ pid, ppid: other.pid, aliveSec: other.aliveSec });
151
+ continue;
152
+ }
153
+ out.ours.push(pid);
154
+ continue;
155
+ }
156
+ // 非 root 看不到别人的 listen 表,退一步:命令行里带着这个端口号的进程也值得报出来
157
+ if (argv.some((a) => String(a).includes(`:${port}`))) out.others.push({ pid, name: String(argv[0]).split("/").pop() });
158
+ }
159
+ return out;
160
+ }