mslxdff 0.1.203 → 0.1.207

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