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,232 +1,232 @@
1
- // warp-pool · 日志与自检报告(2026-10-16 四轮返工后新增;2026-10-17 七次修订补「同机其它 daemon」)。
2
- //
3
- // 为什么单独一个模块(五轮返工的教训):日志若只说「失败了」、不说「谁干的、为什么」,就只能靠猜——
4
- // 第 1 轮猜孤儿占端口、第 2 轮分不清旧 daemon 子进程与刚被杀还没释放的套接字、第 3 轮分不清握手未完成与上游拒绝、
5
- // 第 4~5 轮对着「退出 / 已复活」循环完全看不出真凶是**同机另一个 daemon**(两边都把对方的活隧道当残留杀)。
6
- // 故把可判定信息一次摆全:配置快照、**同机其它 daemon**、端口占用者(pid+ppid+命令行+本池残留/别人的活隧道/僵尸)、
7
- // 身份文件、每条隧道完整生命周期(清理→写配置→spawn pid→探活耗时/状态/错误→退出码/信号/存活时长/末行)、池子结论与「下一步敲什么命令」。
8
- import { fmtAlive } from "../runtime/proc-info.js";
9
- export const MARK = { step: "▸", ok: "✓", fail: "✗", warn: "⚠" };
10
-
11
- const KB = (n) => (n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)}MB` : `${Math.round(n / 1024)}KB`);
12
-
13
- /** 人类可读时长:412ms / 1.8s / 2m3s。 */
14
- export function dur(ms) {
15
- const n = Number(ms);
16
- if (!Number.isFinite(n) || n < 0) return "?";
17
- if (n < 1000) return `${Math.round(n)}ms`;
18
- if (n < 60_000) return `${(n / 1000).toFixed(1)}s`;
19
- return `${Math.floor(n / 60_000)}m${Math.round((n % 60_000) / 1000)}s`;
20
- }
21
-
22
- /** 配置快照:一行说清这次到底按什么参数在跑。 */
23
- export function fmtConfig(cfg) {
24
- return `[warp-pool] ${MARK.step} 启动自检:开关=${cfg.enabled ? "开" : "关"} 模式=${cfg.mode} 端口=${cfg.ports.join(",")} 策略=${cfg.strategy} 轮换=${cfg.rotate ? "开" : "关"} 冷却=${Math.round(cfg.cooldownMs / 1000)}s`;
25
- }
26
-
27
- /** 目录与二进制(带体积,顺手能看出二进制是不是被下坏了)。 */
28
- export function fmtEnv({ dir, binPath, binSize }) {
29
- const bin = binPath ? `${binPath}(${binSize ? KB(binSize) : "体积未知"})` : "未就绪";
30
- return `[warp-pool] ${MARK.step} 目录=${dir} 二进制=${bin}`;
31
- }
32
-
33
- /** 身份文件盘点:哪个端口有 id+token+私钥,哪个缺。 */
34
- export function fmtIdentities(nodes, ports) {
35
- const have = new Set(nodes.map((n) => n.port));
36
- const parts = ports.map((p) => `:${p}${have.has(p) ? " ✓" : " ✗缺"}`);
37
- return `[warp-pool] ${MARK.step} 身份文件 ${nodes.length}/${ports.length}:${parts.join(" ")}(✓=有 id+token+私钥,✗缺=现场注册)`;
38
- }
39
-
40
- /** 「另一个 daemon(pid 1558055,已运行 6h12m) 的活隧道 pid 33」——两个 daemon 互杀这类事故唯一能一眼看出结论的读数。 */
41
- export function fmtForeign(foreign = []) {
42
- return (foreign || []).map((f) => `另一个 daemon(pid ${f.ppid}${f.aliveSec != null ? `,已运行 ${fmtAlive(f.aliveSec)}` : ""}) 的活隧道 pid ${f.pid}`).join(";");
43
- }
44
-
45
- /** 单端口占用者:区分「本池残留」「另一个 daemon 的活隧道」「僵尸」「别人的进程」「空闲」——两轮返工缺的就是这一行。 */
46
- export function fmtPortState({ port, busy, ours = [], zombies = [], foreign = [], others = [] }) {
47
- if (!busy) return `[warp-pool] :${port} 空闲`;
48
- if (foreign.length) return `[warp-pool] :${port} 被 ${fmtForeign(foreign)} 占着:那是**别人的活隧道**,本进程不碰它(清了就是和另一个 daemon 互杀)`;
49
- if (ours.length) return `[warp-pool] :${port} 被 pid ${ours.join(",")} 占:本池 wireproxy 残留(上一代 daemon 留下的孤儿)→ 起隧道前清理`;
50
- if (zombies.length) return `[warp-pool] :${port} 有本池僵尸 pid ${zombies.join(",")}(已死等收尸:kill 无效,也不再占端口)`;
51
- const who = others.length ? others.map((o) => `pid ${o.pid} ${o.name}`).join("、") : "未知进程";
52
- return `[warp-pool] :${port} 被 ${who} 占:非本池进程 → 不碰它,这个端口本轮跳过(查明:ss -ltnp | grep :${port})`;
53
- }
54
-
55
- /** 清理残留的结果:扫了多少进程、命中谁、等了多久、清掉没、以及「另一个 daemon 占着」这种不该动的情形。 */
56
- export function fmtReclaim({ port, scanned, killed = [], zombies = [], foreign = [], waitedMs = 0, gone = true }) {
57
- const parts = [`扫 ${scanned} 个进程`];
58
- if (killed.length) parts.push(`命中本池残留 pid ${killed.join(",")} → ${gone ? "已清掉" : "清不掉(SIGTERM/SIGKILL 都试过)"}`);
59
- else parts.push("无本池残留");
60
- if (zombies.length) parts.push(`僵尸 pid ${zombies.join(",")}(已死等收尸,kill 无效)`);
61
- if (foreign.length) parts.push(`${MARK.warn} ${fmtForeign(foreign)}——不碰它(清了就是和另一个 daemon 互杀)`);
62
- const wait = killed.length ? `(等了 ${dur(waitedMs)})` : "";
63
- return `[warp-pool] :${port} ${MARK.step} 清理残留:${parts.join(";")}${wait}`;
64
- }
65
-
66
- /** 写出的 WireGuard 配置(体积能看出是不是缺字段:缺私钥那次就是 338B vs 373B)。 */
67
- export function fmtConf({ port, conf, bytes }) {
68
- return `[warp-pool] :${port} ${MARK.step} 写配置 ${conf}(${bytes}B,0600)`;
69
- }
70
-
71
- /** 起隧道:pid + 完整命令行,事后可 `ps -p <pid>` 对账。 */
72
- export function fmtSpawn({ port, pid, binPath, conf }) {
73
- return `[warp-pool] :${port} ${MARK.step} 起隧道 pid=${pid ?? "?"}:${binPath} -c ${conf}`;
74
- }
75
-
76
- /** 探活单次:URL、状态、耗时、错误原文——第 3 轮返工缺的就是「到底哪一步不通」。 */
77
- export function fmtProbe({ port, attempt, attempts, url, ok, status, error, ms }) {
78
- const head = `[warp-pool] :${port} ${ok ? MARK.ok : MARK.fail} 探活 ${attempt}/${attempts}:GET ${url} 经 127.0.0.1:${port}`;
79
- const tail = ok
80
- ? `HTTP ${status}(${dur(ms)})→ 进池`
81
- : `HTTP ${status || 0}(${dur(ms)})${error ? ` 错误=${String(error).slice(0, 160)}` : ""} → 稍后重试`;
82
- return `${head} → ${tail}`;
83
- }
84
-
85
- /** 探活彻底失败:说清「不进池」的后果与自检命令,而不是只丢一句「没探通」。 */
86
- export function fmtProbeGiveUp({ port, attempts, url }) {
87
- return `[warp-pool] :${port} ${MARK.fail} 探活 ${attempts} 次都不通 ⇒ 不进池(进程可能在跑但隧道转不动流量,进池会把请求挂到超时,比直连更糟)。自检:curl --proxy http://127.0.0.1:${port} ${url}`;
88
- }
89
-
90
- /** 隧道退出:退出码/信号/存活时长/pid/是否外部终止/stderr 末行(含「谁杀的」这一层推断)。 */
91
- export function fmtExit({ port, code, signal, pid, aliveMs, tail }) {
92
- const how = signal ? `信号 ${signal}` : `退出码 ${code}`;
93
- const killed = signal ? "本进程未调用 stop ⇒ 外部终止(别人杀了它 / 系统回收)" : "进程自行退出(多为配置被拒或端口被占)";
94
- return `[warp-pool] :${port} ${MARK.fail} 隧道退出:${how}|存活 ${aliveMs == null ? "?" : dur(aliveMs)}|pid ${pid ?? "?"}|${killed}|末行="${tail || "没有输出"}"`;
95
- }
96
-
97
- /** 端口最终被占着、拒绝硬起时,把「另一个 daemon 的活隧道 / 本池残留 / 僵尸 / 别人的进程 / 套接字还没释放」分开说。 */
98
- export function fmtPortBlocked({ port, ours = [], zombies = [], foreign = [], others = [] }) {
99
- const foreignTxt = foreign.length ? `${fmtForeign(foreign)}(清了就是互杀,故本进程不碰)——先停掉那个 daemon:kill ${[...new Set(foreign.map((f) => f.ppid))].join(" ")}` : "";
100
- const oursTxt = ours.length ? `本池残留 pid ${ours.join(",")}(SIGTERM/SIGKILL 都没清掉)` : "";
101
- const zombieTxt = zombies.length ? `本池僵尸 pid ${zombies.join(",")}(已死等收尸,不占端口)` : "";
102
- const othTxt = others.length ? others.map((o) => `pid ${o.pid} ${o.name}`).join("、") : "";
103
- const who = [foreignTxt, oursTxt, zombieTxt, othTxt].filter(Boolean).join(";") || "未知进程(已等套接字释放仍连得上)";
104
- return `[warp-pool] :${port} ${MARK.fail} 端口被 ${who} 占着,拒绝再起(硬起只会 bind 失败、无限重启刷屏)。查明:ss -ltnp | grep :${port}`;
105
- }
106
-
107
- /** 池子结论:入池几个、各端口出口是什么、几种不同地址——一句话判断「WARP 到底有没有在用」。 */
108
- export function fmtPool({ pooled, total, exits }) {
109
- const head = `[warp-pool] ${MARK.step} 出口池就绪:${pooled}/${total} 条入池`;
110
- const ipOf = new Map();
111
- for (const g of exits || []) for (const p of g.nodes || []) if (g.ip) ipOf.set(p, g.ip);
112
- if (!ipOf.size) return `${head}|出口地址未知(CF trace 取证失败,不影响使用)`;
113
- const table = [...ipOf.entries()].map(([p, ip]) => `:${p}=${ip}`).join(" ");
114
- return `${head}|出口:${table}|不同地址 ${new Set(ipOf.values()).size} 个`;
115
- }
116
-
117
- /**
118
- * 身份指纹:每个端口的短设备 id / WARP 内网 IPv6 尾段 / 对端端点 / 注册时间。
119
- * 为什么必须有:4 条隧道只拿到 2 个出口地址时(真机 2026-10-17),只有这组读数能区分
120
- * 「设备本身被写重复了」和「CF 把不同设备折叠成一个出口」——没有它就只能猜。
121
- *
122
- * ⚠ 判据**不看 IPv4**:`172.16.0.2` 是 WARP 的**固定接口地址**(6 次实测全是它:组长 4 台 +
123
- * 本机 2 台;公开的 wgcf 生成配置里同样是 `Address = 172.16.0.2/32`),按它判「重复」是误报
124
- * (2026-10-17 十一次修订前确实这么误报过)。每台设备真正唯一的是 **IPv6**(`2606:4700:110:…`)
125
- * 与密钥对,所以指纹打 v6 尾段。
126
- */
127
- /** v6 尾段做指纹(`2606:4700:110:81b5:56ec:72b3:9542:e3de` → `…:9542:e3de`);缺了给「无」。 */
128
- export function v6Tail(v6) {
129
- const s = String(v6 || "");
130
- if (!s) return "无";
131
- const parts = s.split(":");
132
- return parts.length > 2 ? `…:${parts.slice(-2).join(":")}` : s;
133
- }
134
- export function fmtIdentityDetail(nodes = []) {
135
- const out = [];
136
- const byId = new Map();
137
- const byEp = new Map();
138
- for (const n of nodes || []) {
139
- if (n?.id) byId.set(n.id, [...(byId.get(n.id) || []), n.port]);
140
- if (n?.endpoint) byEp.set(n.endpoint, [...(byEp.get(n.endpoint) || []), n.port]);
141
- out.push(`[warp-pool] :${n?.port} 设备=${n?.id ? String(n.id).slice(0, 8) : "无id"} 内网v6=${v6Tail(n?.v6)} 端点=${n?.endpoint || "?"} 注册=${String(n?.registeredAt || "?").slice(0, 19)}`);
142
- }
143
- const dupId = [...byId.entries()].filter(([, p]) => p.length > 1).map(([k, p]) => `${String(k).slice(0, 8)} ← :${p.join(",:")}`).join(";");
144
- if (dupId) out.push(`[warp-pool] ${MARK.warn} 设备 id 重复:${dupId} —— 同一个 WARP 设备被写到多个端口,出口必然相同;删掉重复端口的 node-<port>.json 再重启,它会重新注册`);
145
- // 端点全同 = 大家都从同一个 CF 入口出去(CF 对每次注册都回同一个 hostname):出口撞车由此而来
146
- const total = (nodes || []).length;
147
- const sameEp = [...byEp.entries()].find(([, p]) => total > 1 && p.length === total);
148
- if (sameEp) out.push(`[warp-pool] ${MARK.step} ${total} 条隧道都用同一个端点(${sameEp[0]})—— CF 对每次注册都回同一个 hostname,故大家都从同一个 CF 入口出去,出口地址撞车由此而来(想分散:把 node-<port>.json 的 endpoint 改成别的 WARP 入口 IP 后重启)`);
149
- return out;
150
- }
151
-
152
- /**
153
- * 出口分散结论:几个出口地址 / 几条隧道在线、撞车的是谁、以及「想多要一份该怎么办」。
154
- * 措辞必须自洽:免费 WARP 的 IPv4 是共享 NAT,**隧道数 ≠ 上游名额数**——旧措辞
155
- * 「同时在线就有 N 份名额」与紧邻的「出口不分散」分支自相矛盾(真机 4 条隧道只拿到 2 个出口)。
156
- */
157
- export function fmtExits({ groups = [], dir = "" } = {}) {
158
- if (!groups.length) return "";
159
- const online = groups.reduce((a, g) => a + g.nodes.length, 0);
160
- if (groups.length === 1) {
161
- return `[warp-pool] ${MARK.warn} 出口不分散:${online} 条隧道同时在线只有 1 个出口地址(${groups[0].ip}):免费版 WARP 的 IPv4 是共享 NAT,多开隧道换不到多份名额;多份名额靠撞 429 时的身份轮换`;
162
- }
163
- const collide = groups.filter((g) => g.nodes.length > 1);
164
- const drop = collide.flatMap((g) => g.nodes.slice(1));
165
- const base = dir || "$HOME/.config/mslxdff/warp-pool";
166
- const next = drop.length
167
- ? `|${MARK.warn} 撞车:${collide.map((g) => `${g.ip} ← :${g.nodes.join(",:")}`).join(";")}(它们共用同一份上游名额)⇒ 想多要一份:rm -f ${drop.map((p) => `${base}/node-${p}.json`).join(" ")} && mslxdff -restart(重新注册换出口;共享 NAT 下常换不动,要试几次)`
168
- : "";
169
- return `[warp-pool] ${MARK.step} 出口分散:${groups.length} 个出口地址 / ${online} 条隧道在线(上游按出口 IP 计名额 ⇒ 最多 ${groups.length} 份;隧道数 ≠ 名额数:免费版 WARP 的 IPv4 是共享 NAT)${next}`;
170
- }
171
-
172
- /** 池空告警:最容易被误读成「功能在跑」,必须把结论与逐条自检命令一起说。 */
173
- export function fmtPoolEmpty({ total, ports }) {
174
- return `[warp-pool] ${MARK.warn} 没有一条隧道探通 ⇒ 池为空,全部请求照旧直连(不影响可用性,但 WARP 出口等于没启用)。逐条自检:for p in ${ports.join(" ")}; do echo -n ":$p "; curl -s --max-time 8 --proxy http://127.0.0.1:$p https://cloudflare.com/cdn-cgi/trace | grep '^ip='; done`;
175
- }
176
-
177
- /** 健康检查一轮:只在状态变化/心跳时打(每分钟一行会把日志淹掉)。 */
178
- export function fmtHealth({ healthy, total, slowestMs, problems = [] }) {
179
- if (!problems.length) return `[warp-pool] ${MARK.ok} 健康检查:${healthy}/${total} 通(最慢 ${dur(slowestMs)})`;
180
- const detail = problems
181
- .map((p) => `:${p.port} 第 ${p.fails} 次不通(HTTP ${p.status || 0}${p.error ? ` ${String(p.error).slice(0, 80)}` : ""},${dur(p.ms)})冷却 ${Math.round(p.cooldownMs / 1000)}s`)
182
- .join(";");
183
- return `[warp-pool] ${MARK.warn} 健康检查:${healthy}/${total} 通|不通:${detail}`;
184
- }
185
-
186
- /** 心跳:一行给全部关键状态,省得「日志翻半天不知道现在啥情况」。 */
187
- export function fmtHeartbeat({ alive, total, pooled, exits, cooling, lastAllDown }) {
188
- const ips = [...new Set((exits || []).map((e) => e.ip).filter(Boolean))];
189
- return `[warp-pool] ${MARK.step} 心跳:隧道 ${alive}/${total} 活|入池 ${pooled}|出口 ${ips.length ? `${ips.length} 种(${ips.join(",")})` : "未知"}|冷却中 ${cooling}|全池不通 ${lastAllDown || "未发生"}`;
190
- }
191
-
192
- /** 同机其它 daemon:两个 daemon 抢同一批端口会互杀(真机事故根因),必须一次说清并给出精确 kill 命令。 */
193
- export function fmtSiblings({ list = [] } = {}) {
194
- if (!list.length) return "";
195
- const who = list.map((d) => `pid ${d.pid}(父 ${d.ppid}${d.aliveSec != null ? `,已运行 ${fmtAlive(d.aliveSec)}` : ""})`).join(";");
196
- return `[warp-pool] ${MARK.warn} 同机还有 ${list.length} 个别的 mslxdff daemon 在跑:${who} —— 它也在管同一个出口池目录,两边会把对方的隧道当「残留」杀掉(本池每个 conf 只允许一个拥有者)。先停掉它:kill ${list.map((d) => d.pid).join(" ")}`;
197
- }
198
-
199
- /**
200
- * 启动自检块:一次性把「这次按什么参数跑、二进制在哪多大、磁盘上有几个身份、端口分别被谁占着」打全。
201
- * 第 1 轮返工只看到「退出 code=1 / 已复活」循环,就是缺这一块——真因(孤儿占端口)当时只能靠猜。
202
- * @param {{ cfg: object, nodes: Array<object>, binPath?: string, binSize?: number,
203
- * ownerOf: (port:number)=>Promise<{busy:boolean, ours:number[], zombies:number[], foreign:Array<object>, others:Array<{pid:number,name:string}>}>,
204
- * log?: Function }} opts
205
- */
206
- export async function startupReport({ cfg, nodes, binPath = "", binSize = 0, ownerOf, log = () => {} }) {
207
- log(fmtConfig(cfg));
208
- log(fmtEnv({ dir: cfg.dir, binPath, binSize }));
209
- log(fmtIdentities(nodes, cfg.ports));
210
- const states = [];
211
- for (const port of cfg.ports) {
212
- try {
213
- states.push({ port, ...(await ownerOf(port)) });
214
- } catch (e) {
215
- states.push({ port, busy: false, ours: [], zombies: [], foreign: [], others: [], error: String(e?.message || e) });
216
- }
217
- }
218
- const busy = states.filter((s) => s.busy);
219
- log(`[warp-pool] ${MARK.step} 端口体检:${busy.length} 个被占 / ${states.length} 个端口`);
220
- for (const s of states) log(fmtPortState(s));
221
- // 另一个 daemon 占着端口 = 两个 daemon 互杀(真机事故根因):必须单独大声报,并给出精确 kill 命令
222
- const foreign = busy.flatMap((s) => s.foreign || []);
223
- if (foreign.length) {
224
- const pids = [...new Set(foreign.map((f) => f.ppid))];
225
- log(`[warp-pool] ${MARK.warn} ${foreign.length} 个端口被 ${fmtForeign(foreign)} 占着 —— 那不是残留,本进程不碰它(清了就是互杀)。若那是上一代的 daemon:kill ${pids.join(" ")}`);
226
- }
227
- const orphans = busy.filter((s) => (s.ours || []).length).length;
228
- if (orphans) log(`[warp-pool] ${MARK.warn} 有 ${orphans} 个端口被本池残留占着(上一代 daemon 被强杀留下的孤儿)——起隧道前逐个清理,日志会写清 pid`);
229
- const zombies = busy.filter((s) => (s.zombies || []).length).length;
230
- if (zombies) log(`[warp-pool] ${MARK.step} 有 ${zombies} 个端口上还有本池僵尸进程(已死等收尸:kill 无效,也不再占端口,无需处理)`);
231
- return states;
232
- }
1
+ // warp-pool · 日志与自检报告(2026-10-16 四轮返工后新增;2026-10-17 七次修订补「同机其它 daemon」)。
2
+ //
3
+ // 为什么单独一个模块(五轮返工的教训):日志若只说「失败了」、不说「谁干的、为什么」,就只能靠猜——
4
+ // 第 1 轮猜孤儿占端口、第 2 轮分不清旧 daemon 子进程与刚被杀还没释放的套接字、第 3 轮分不清握手未完成与上游拒绝、
5
+ // 第 4~5 轮对着「退出 / 已复活」循环完全看不出真凶是**同机另一个 daemon**(两边都把对方的活隧道当残留杀)。
6
+ // 故把可判定信息一次摆全:配置快照、**同机其它 daemon**、端口占用者(pid+ppid+命令行+本池残留/别人的活隧道/僵尸)、
7
+ // 身份文件、每条隧道完整生命周期(清理→写配置→spawn pid→探活耗时/状态/错误→退出码/信号/存活时长/末行)、池子结论与「下一步敲什么命令」。
8
+ import { fmtAlive } from "../runtime/proc-info.js";
9
+ export const MARK = { step: "▸", ok: "✓", fail: "✗", warn: "⚠" };
10
+
11
+ const KB = (n) => (n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)}MB` : `${Math.round(n / 1024)}KB`);
12
+
13
+ /** 人类可读时长:412ms / 1.8s / 2m3s。 */
14
+ export function dur(ms) {
15
+ const n = Number(ms);
16
+ if (!Number.isFinite(n) || n < 0) return "?";
17
+ if (n < 1000) return `${Math.round(n)}ms`;
18
+ if (n < 60_000) return `${(n / 1000).toFixed(1)}s`;
19
+ return `${Math.floor(n / 60_000)}m${Math.round((n % 60_000) / 1000)}s`;
20
+ }
21
+
22
+ /** 配置快照:一行说清这次到底按什么参数在跑。 */
23
+ export function fmtConfig(cfg) {
24
+ return `[warp-pool] ${MARK.step} 启动自检:开关=${cfg.enabled ? "开" : "关"} 模式=${cfg.mode} 端口=${cfg.ports.join(",")} 策略=${cfg.strategy} 轮换=${cfg.rotate ? "开" : "关"} 冷却=${Math.round(cfg.cooldownMs / 1000)}s`;
25
+ }
26
+
27
+ /** 目录与二进制(带体积,顺手能看出二进制是不是被下坏了)。 */
28
+ export function fmtEnv({ dir, binPath, binSize }) {
29
+ const bin = binPath ? `${binPath}(${binSize ? KB(binSize) : "体积未知"})` : "未就绪";
30
+ return `[warp-pool] ${MARK.step} 目录=${dir} 二进制=${bin}`;
31
+ }
32
+
33
+ /** 身份文件盘点:哪个端口有 id+token+私钥,哪个缺。 */
34
+ export function fmtIdentities(nodes, ports) {
35
+ const have = new Set(nodes.map((n) => n.port));
36
+ const parts = ports.map((p) => `:${p}${have.has(p) ? " ✓" : " ✗缺"}`);
37
+ return `[warp-pool] ${MARK.step} 身份文件 ${nodes.length}/${ports.length}:${parts.join(" ")}(✓=有 id+token+私钥,✗缺=现场注册)`;
38
+ }
39
+
40
+ /** 「另一个 daemon(pid 1558055,已运行 6h12m) 的活隧道 pid 33」——两个 daemon 互杀这类事故唯一能一眼看出结论的读数。 */
41
+ export function fmtForeign(foreign = []) {
42
+ return (foreign || []).map((f) => `另一个 daemon(pid ${f.ppid}${f.aliveSec != null ? `,已运行 ${fmtAlive(f.aliveSec)}` : ""}) 的活隧道 pid ${f.pid}`).join(";");
43
+ }
44
+
45
+ /** 单端口占用者:区分「本池残留」「另一个 daemon 的活隧道」「僵尸」「别人的进程」「空闲」——两轮返工缺的就是这一行。 */
46
+ export function fmtPortState({ port, busy, ours = [], zombies = [], foreign = [], others = [] }) {
47
+ if (!busy) return `[warp-pool] :${port} 空闲`;
48
+ if (foreign.length) return `[warp-pool] :${port} 被 ${fmtForeign(foreign)} 占着:那是**别人的活隧道**,本进程不碰它(清了就是和另一个 daemon 互杀)`;
49
+ if (ours.length) return `[warp-pool] :${port} 被 pid ${ours.join(",")} 占:本池 wireproxy 残留(上一代 daemon 留下的孤儿)→ 起隧道前清理`;
50
+ if (zombies.length) return `[warp-pool] :${port} 有本池僵尸 pid ${zombies.join(",")}(已死等收尸:kill 无效,也不再占端口)`;
51
+ const who = others.length ? others.map((o) => `pid ${o.pid} ${o.name}`).join("、") : "未知进程";
52
+ return `[warp-pool] :${port} 被 ${who} 占:非本池进程 → 不碰它,这个端口本轮跳过(查明:ss -ltnp | grep :${port})`;
53
+ }
54
+
55
+ /** 清理残留的结果:扫了多少进程、命中谁、等了多久、清掉没、以及「另一个 daemon 占着」这种不该动的情形。 */
56
+ export function fmtReclaim({ port, scanned, killed = [], zombies = [], foreign = [], waitedMs = 0, gone = true }) {
57
+ const parts = [`扫 ${scanned} 个进程`];
58
+ if (killed.length) parts.push(`命中本池残留 pid ${killed.join(",")} → ${gone ? "已清掉" : "清不掉(SIGTERM/SIGKILL 都试过)"}`);
59
+ else parts.push("无本池残留");
60
+ if (zombies.length) parts.push(`僵尸 pid ${zombies.join(",")}(已死等收尸,kill 无效)`);
61
+ if (foreign.length) parts.push(`${MARK.warn} ${fmtForeign(foreign)}——不碰它(清了就是和另一个 daemon 互杀)`);
62
+ const wait = killed.length ? `(等了 ${dur(waitedMs)})` : "";
63
+ return `[warp-pool] :${port} ${MARK.step} 清理残留:${parts.join(";")}${wait}`;
64
+ }
65
+
66
+ /** 写出的 WireGuard 配置(体积能看出是不是缺字段:缺私钥那次就是 338B vs 373B)。 */
67
+ export function fmtConf({ port, conf, bytes }) {
68
+ return `[warp-pool] :${port} ${MARK.step} 写配置 ${conf}(${bytes}B,0600)`;
69
+ }
70
+
71
+ /** 起隧道:pid + 完整命令行,事后可 `ps -p <pid>` 对账。 */
72
+ export function fmtSpawn({ port, pid, binPath, conf }) {
73
+ return `[warp-pool] :${port} ${MARK.step} 起隧道 pid=${pid ?? "?"}:${binPath} -c ${conf}`;
74
+ }
75
+
76
+ /** 探活单次:URL、状态、耗时、错误原文——第 3 轮返工缺的就是「到底哪一步不通」。 */
77
+ export function fmtProbe({ port, attempt, attempts, url, ok, status, error, ms }) {
78
+ const head = `[warp-pool] :${port} ${ok ? MARK.ok : MARK.fail} 探活 ${attempt}/${attempts}:GET ${url} 经 127.0.0.1:${port}`;
79
+ const tail = ok
80
+ ? `HTTP ${status}(${dur(ms)})→ 进池`
81
+ : `HTTP ${status || 0}(${dur(ms)})${error ? ` 错误=${String(error).slice(0, 160)}` : ""} → 稍后重试`;
82
+ return `${head} → ${tail}`;
83
+ }
84
+
85
+ /** 探活彻底失败:说清「不进池」的后果与自检命令,而不是只丢一句「没探通」。 */
86
+ export function fmtProbeGiveUp({ port, attempts, url }) {
87
+ return `[warp-pool] :${port} ${MARK.fail} 探活 ${attempts} 次都不通 ⇒ 不进池(进程可能在跑但隧道转不动流量,进池会把请求挂到超时,比直连更糟)。自检:curl --proxy http://127.0.0.1:${port} ${url}`;
88
+ }
89
+
90
+ /** 隧道退出:退出码/信号/存活时长/pid/是否外部终止/stderr 末行(含「谁杀的」这一层推断)。 */
91
+ export function fmtExit({ port, code, signal, pid, aliveMs, tail }) {
92
+ const how = signal ? `信号 ${signal}` : `退出码 ${code}`;
93
+ const killed = signal ? "本进程未调用 stop ⇒ 外部终止(别人杀了它 / 系统回收)" : "进程自行退出(多为配置被拒或端口被占)";
94
+ return `[warp-pool] :${port} ${MARK.fail} 隧道退出:${how}|存活 ${aliveMs == null ? "?" : dur(aliveMs)}|pid ${pid ?? "?"}|${killed}|末行="${tail || "没有输出"}"`;
95
+ }
96
+
97
+ /** 端口最终被占着、拒绝硬起时,把「另一个 daemon 的活隧道 / 本池残留 / 僵尸 / 别人的进程 / 套接字还没释放」分开说。 */
98
+ export function fmtPortBlocked({ port, ours = [], zombies = [], foreign = [], others = [] }) {
99
+ const foreignTxt = foreign.length ? `${fmtForeign(foreign)}(清了就是互杀,故本进程不碰)——先停掉那个 daemon:kill ${[...new Set(foreign.map((f) => f.ppid))].join(" ")}` : "";
100
+ const oursTxt = ours.length ? `本池残留 pid ${ours.join(",")}(SIGTERM/SIGKILL 都没清掉)` : "";
101
+ const zombieTxt = zombies.length ? `本池僵尸 pid ${zombies.join(",")}(已死等收尸,不占端口)` : "";
102
+ const othTxt = others.length ? others.map((o) => `pid ${o.pid} ${o.name}`).join("、") : "";
103
+ const who = [foreignTxt, oursTxt, zombieTxt, othTxt].filter(Boolean).join(";") || "未知进程(已等套接字释放仍连得上)";
104
+ return `[warp-pool] :${port} ${MARK.fail} 端口被 ${who} 占着,拒绝再起(硬起只会 bind 失败、无限重启刷屏)。查明:ss -ltnp | grep :${port}`;
105
+ }
106
+
107
+ /** 池子结论:入池几个、各端口出口是什么、几种不同地址——一句话判断「WARP 到底有没有在用」。 */
108
+ export function fmtPool({ pooled, total, exits }) {
109
+ const head = `[warp-pool] ${MARK.step} 出口池就绪:${pooled}/${total} 条入池`;
110
+ const ipOf = new Map();
111
+ for (const g of exits || []) for (const p of g.nodes || []) if (g.ip) ipOf.set(p, g.ip);
112
+ if (!ipOf.size) return `${head}|出口地址未知(CF trace 取证失败,不影响使用)`;
113
+ const table = [...ipOf.entries()].map(([p, ip]) => `:${p}=${ip}`).join(" ");
114
+ return `${head}|出口:${table}|不同地址 ${new Set(ipOf.values()).size} 个`;
115
+ }
116
+
117
+ /**
118
+ * 身份指纹:每个端口的短设备 id / WARP 内网 IPv6 尾段 / 对端端点 / 注册时间。
119
+ * 为什么必须有:4 条隧道只拿到 2 个出口地址时(真机 2026-10-17),只有这组读数能区分
120
+ * 「设备本身被写重复了」和「CF 把不同设备折叠成一个出口」——没有它就只能猜。
121
+ *
122
+ * ⚠ 判据**不看 IPv4**:`172.16.0.2` 是 WARP 的**固定接口地址**(6 次实测全是它:组长 4 台 +
123
+ * 本机 2 台;公开的 wgcf 生成配置里同样是 `Address = 172.16.0.2/32`),按它判「重复」是误报
124
+ * (2026-10-17 十一次修订前确实这么误报过)。每台设备真正唯一的是 **IPv6**(`2606:4700:110:…`)
125
+ * 与密钥对,所以指纹打 v6 尾段。
126
+ */
127
+ /** v6 尾段做指纹(`2606:4700:110:81b5:56ec:72b3:9542:e3de` → `…:9542:e3de`);缺了给「无」。 */
128
+ export function v6Tail(v6) {
129
+ const s = String(v6 || "");
130
+ if (!s) return "无";
131
+ const parts = s.split(":");
132
+ return parts.length > 2 ? `…:${parts.slice(-2).join(":")}` : s;
133
+ }
134
+ export function fmtIdentityDetail(nodes = []) {
135
+ const out = [];
136
+ const byId = new Map();
137
+ const byEp = new Map();
138
+ for (const n of nodes || []) {
139
+ if (n?.id) byId.set(n.id, [...(byId.get(n.id) || []), n.port]);
140
+ if (n?.endpoint) byEp.set(n.endpoint, [...(byEp.get(n.endpoint) || []), n.port]);
141
+ out.push(`[warp-pool] :${n?.port} 设备=${n?.id ? String(n.id).slice(0, 8) : "无id"} 内网v6=${v6Tail(n?.v6)} 端点=${n?.endpoint || "?"} 注册=${String(n?.registeredAt || "?").slice(0, 19)}`);
142
+ }
143
+ const dupId = [...byId.entries()].filter(([, p]) => p.length > 1).map(([k, p]) => `${String(k).slice(0, 8)} ← :${p.join(",:")}`).join(";");
144
+ if (dupId) out.push(`[warp-pool] ${MARK.warn} 设备 id 重复:${dupId} —— 同一个 WARP 设备被写到多个端口,出口必然相同;删掉重复端口的 node-<port>.json 再重启,它会重新注册`);
145
+ // 端点全同 = 大家都从同一个 CF 入口出去(CF 对每次注册都回同一个 hostname):出口撞车由此而来
146
+ const total = (nodes || []).length;
147
+ const sameEp = [...byEp.entries()].find(([, p]) => total > 1 && p.length === total);
148
+ if (sameEp) out.push(`[warp-pool] ${MARK.step} ${total} 条隧道都用同一个端点(${sameEp[0]})—— CF 对每次注册都回同一个 hostname,故大家都从同一个 CF 入口出去,出口地址撞车由此而来(想分散:把 node-<port>.json 的 endpoint 改成别的 WARP 入口 IP 后重启)`);
149
+ return out;
150
+ }
151
+
152
+ /**
153
+ * 出口分散结论:几个出口地址 / 几条隧道在线、撞车的是谁、以及「想多要一份该怎么办」。
154
+ * 措辞必须自洽:免费 WARP 的 IPv4 是共享 NAT,**隧道数 ≠ 上游名额数**——旧措辞
155
+ * 「同时在线就有 N 份名额」与紧邻的「出口不分散」分支自相矛盾(真机 4 条隧道只拿到 2 个出口)。
156
+ */
157
+ export function fmtExits({ groups = [], dir = "" } = {}) {
158
+ if (!groups.length) return "";
159
+ const online = groups.reduce((a, g) => a + g.nodes.length, 0);
160
+ if (groups.length === 1) {
161
+ return `[warp-pool] ${MARK.warn} 出口不分散:${online} 条隧道同时在线只有 1 个出口地址(${groups[0].ip}):免费版 WARP 的 IPv4 是共享 NAT,多开隧道换不到多份名额;多份名额靠撞 429 时的身份轮换`;
162
+ }
163
+ const collide = groups.filter((g) => g.nodes.length > 1);
164
+ const drop = collide.flatMap((g) => g.nodes.slice(1));
165
+ const base = dir || "$HOME/.config/mslxdff/warp-pool";
166
+ const next = drop.length
167
+ ? `|${MARK.warn} 撞车:${collide.map((g) => `${g.ip} ← :${g.nodes.join(",:")}`).join(";")}(它们共用同一份上游名额)⇒ 想多要一份:rm -f ${drop.map((p) => `${base}/node-${p}.json`).join(" ")} && mslxdff -restart(重新注册换出口;共享 NAT 下常换不动,要试几次)`
168
+ : "";
169
+ return `[warp-pool] ${MARK.step} 出口分散:${groups.length} 个出口地址 / ${online} 条隧道在线(上游按出口 IP 计名额 ⇒ 最多 ${groups.length} 份;隧道数 ≠ 名额数:免费版 WARP 的 IPv4 是共享 NAT)${next}`;
170
+ }
171
+
172
+ /** 池空告警:最容易被误读成「功能在跑」,必须把结论与逐条自检命令一起说。 */
173
+ export function fmtPoolEmpty({ total, ports }) {
174
+ return `[warp-pool] ${MARK.warn} 没有一条隧道探通 ⇒ 池为空,全部请求照旧直连(不影响可用性,但 WARP 出口等于没启用)。逐条自检:for p in ${ports.join(" ")}; do echo -n ":$p "; curl -s --max-time 8 --proxy http://127.0.0.1:$p https://cloudflare.com/cdn-cgi/trace | grep '^ip='; done`;
175
+ }
176
+
177
+ /** 健康检查一轮:只在状态变化/心跳时打(每分钟一行会把日志淹掉)。 */
178
+ export function fmtHealth({ healthy, total, slowestMs, problems = [] }) {
179
+ if (!problems.length) return `[warp-pool] ${MARK.ok} 健康检查:${healthy}/${total} 通(最慢 ${dur(slowestMs)})`;
180
+ const detail = problems
181
+ .map((p) => `:${p.port} 第 ${p.fails} 次不通(HTTP ${p.status || 0}${p.error ? ` ${String(p.error).slice(0, 80)}` : ""},${dur(p.ms)})冷却 ${Math.round(p.cooldownMs / 1000)}s`)
182
+ .join(";");
183
+ return `[warp-pool] ${MARK.warn} 健康检查:${healthy}/${total} 通|不通:${detail}`;
184
+ }
185
+
186
+ /** 心跳:一行给全部关键状态,省得「日志翻半天不知道现在啥情况」。 */
187
+ export function fmtHeartbeat({ alive, total, pooled, exits, cooling, lastAllDown }) {
188
+ const ips = [...new Set((exits || []).map((e) => e.ip).filter(Boolean))];
189
+ return `[warp-pool] ${MARK.step} 心跳:隧道 ${alive}/${total} 活|入池 ${pooled}|出口 ${ips.length ? `${ips.length} 种(${ips.join(",")})` : "未知"}|冷却中 ${cooling}|全池不通 ${lastAllDown || "未发生"}`;
190
+ }
191
+
192
+ /** 同机其它 daemon:两个 daemon 抢同一批端口会互杀(真机事故根因),必须一次说清并给出精确 kill 命令。 */
193
+ export function fmtSiblings({ list = [] } = {}) {
194
+ if (!list.length) return "";
195
+ const who = list.map((d) => `pid ${d.pid}(父 ${d.ppid}${d.aliveSec != null ? `,已运行 ${fmtAlive(d.aliveSec)}` : ""})`).join(";");
196
+ return `[warp-pool] ${MARK.warn} 同机还有 ${list.length} 个别的 mslxdff daemon 在跑:${who} —— 它也在管同一个出口池目录,两边会把对方的隧道当「残留」杀掉(本池每个 conf 只允许一个拥有者)。先停掉它:kill ${list.map((d) => d.pid).join(" ")}`;
197
+ }
198
+
199
+ /**
200
+ * 启动自检块:一次性把「这次按什么参数跑、二进制在哪多大、磁盘上有几个身份、端口分别被谁占着」打全。
201
+ * 第 1 轮返工只看到「退出 code=1 / 已复活」循环,就是缺这一块——真因(孤儿占端口)当时只能靠猜。
202
+ * @param {{ cfg: object, nodes: Array<object>, binPath?: string, binSize?: number,
203
+ * ownerOf: (port:number)=>Promise<{busy:boolean, ours:number[], zombies:number[], foreign:Array<object>, others:Array<{pid:number,name:string}>}>,
204
+ * log?: Function }} opts
205
+ */
206
+ export async function startupReport({ cfg, nodes, binPath = "", binSize = 0, ownerOf, log = () => {} }) {
207
+ log(fmtConfig(cfg));
208
+ log(fmtEnv({ dir: cfg.dir, binPath, binSize }));
209
+ log(fmtIdentities(nodes, cfg.ports));
210
+ const states = [];
211
+ for (const port of cfg.ports) {
212
+ try {
213
+ states.push({ port, ...(await ownerOf(port)) });
214
+ } catch (e) {
215
+ states.push({ port, busy: false, ours: [], zombies: [], foreign: [], others: [], error: String(e?.message || e) });
216
+ }
217
+ }
218
+ const busy = states.filter((s) => s.busy);
219
+ log(`[warp-pool] ${MARK.step} 端口体检:${busy.length} 个被占 / ${states.length} 个端口`);
220
+ for (const s of states) log(fmtPortState(s));
221
+ // 另一个 daemon 占着端口 = 两个 daemon 互杀(真机事故根因):必须单独大声报,并给出精确 kill 命令
222
+ const foreign = busy.flatMap((s) => s.foreign || []);
223
+ if (foreign.length) {
224
+ const pids = [...new Set(foreign.map((f) => f.ppid))];
225
+ log(`[warp-pool] ${MARK.warn} ${foreign.length} 个端口被 ${fmtForeign(foreign)} 占着 —— 那不是残留,本进程不碰它(清了就是互杀)。若那是上一代的 daemon:kill ${pids.join(" ")}`);
226
+ }
227
+ const orphans = busy.filter((s) => (s.ours || []).length).length;
228
+ if (orphans) log(`[warp-pool] ${MARK.warn} 有 ${orphans} 个端口被本池残留占着(上一代 daemon 被强杀留下的孤儿)——起隧道前逐个清理,日志会写清 pid`);
229
+ const zombies = busy.filter((s) => (s.zombies || []).length).length;
230
+ if (zombies) log(`[warp-pool] ${MARK.step} 有 ${zombies} 个端口上还有本池僵尸进程(已死等收尸:kill 无效,也不再占端口,无需处理)`);
231
+ return states;
232
+ }