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,234 +1,234 @@
1
- import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync, unlinkSync, copyFileSync, chmodSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import os from "node:os";
4
- import { toInferenceModels } from "./claude-desktop/slots.js";
5
-
6
- /**
7
- * -setto claude-desktop:把本机网关写成 **Claude Desktop on 3P** 的 profile。
8
- *
9
- * 落点与键面(官方 `claude.com/docs/third-party/claude-desktop/configuration` 「How keys are read」):
10
- * - Windows 本地(非托管):`%LOCALAPPDATA%\Claude-3p\configLibrary\`
11
- * - macOS:`~/Library/Application Support/Claude-3p/configLibrary/`
12
- * - Linux:`~/.config/Claude-3p/configLibrary/`(`$XDG_CONFIG_HOME` 优先)
13
- * 目录里 `_meta.json` 记「哪份配置生效」(`appliedId` + `entries[]`),每份配置是平级的 `<uuid>.json`。
14
- * in-app 配置窗口(Developer → Configure Third-Party Inference…)写的就是这里,我们代劳。
15
- *
16
- * 三条硬红线(照 `src/sync-claude.js` 的既有纪律,不许松):
17
- * 1. 只登记**自己那一条** entry(uuid 由端口派生 → 重跑原地更新、不堆条目),用户/窗口建的其它 entry 一字不动;
18
- * 2. 现有 JSON 解析失败 → 一律拒写(那是用户在 App 里配过的东西,不是我们的草稿);
19
- * 3. 文件里有明文 token → `0600`(新建带 mode + 存量补 chmod),半截 tmp 清理失败要把路径如实交出去。
20
- *
21
- * 不碰 `claude_desktop_config.json`(App 自己的 prefs),不碰托管策略(HKLM/HKCU\SOFTWARE\Policies\Claude
22
- * 一旦存在,App 会整体忽略本地 configLibrary —— 那种情况 `-claude-desktop status` 会明说)。
23
- */
24
-
25
- /** configLibrary 目录(三平台 + env 覆盖,覆盖位专为单测隔离 HOME)。 */
26
- export function claudeDesktopConfigDir() {
27
- const env = process.env.MSLXDFF_CLAUDE_DESKTOP_DIR;
28
- if (typeof env === "string" && env.trim()) return env.trim();
29
- const home = os.homedir();
30
- if (process.platform === "win32") {
31
- const local = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
32
- return join(local, "Claude-3p", "configLibrary");
33
- }
34
- if (process.platform === "darwin") return join(home, "Library", "Application Support", "Claude-3p", "configLibrary");
35
- const xdg = (process.env.XDG_CONFIG_HOME || "").trim() || join(home, ".config");
36
- return join(xdg, "Claude-3p", "configLibrary");
37
- }
38
-
39
- /** 配置条目 id:由端口派生的确定性 UUID 形态(同 cc-switch 的路子,端口不同即不撞车)。 */
40
- export function desktopConfigId(port) {
41
- const p = String(Math.max(0, Number(port) || 0) % 1000000).padStart(12, "0");
42
- return `00000000-0000-4000-8000-${p}`;
43
- }
44
-
45
- /** 纯函数:profile 对象(App 侧读的键面)。 */
46
- export function buildDesktopProfile({ baseUrl = "", apiKey = "", rows = [], maxEffort } = {}) {
47
- return {
48
- inferenceProvider: "gateway",
49
- inferenceCredentialKind: "static",
50
- inferenceGatewayAuthScheme: "bearer", // 本仓鉴权只认 Authorization: Bearer(src/routes/helpers.js authorized())
51
- inferenceGatewayBaseUrl: String(baseUrl),
52
- inferenceGatewayApiKey: String(apiKey),
53
- inferenceModels: toInferenceModels(rows, { maxEffort }),
54
- };
55
- }
56
-
57
- function norm(text) {
58
- return String(text || "").replace(/\r\n/g, "\n").trim();
59
- }
60
-
61
- function parseJsonOrThrow(text, file) {
62
- try {
63
- return JSON.parse(text);
64
- } catch {
65
- throw new Error(`JSON 解析失败,拒绝覆盖:${file}`);
66
- }
67
- }
68
-
69
- function readJsonOr(file, { allowMissing = false } = {}) {
70
- let raw;
71
- try {
72
- raw = readFileSync(file, "utf8");
73
- } catch {
74
- if (allowMissing) return null;
75
- throw new Error(`读不到 ${file}`);
76
- }
77
- return parseJsonOrThrow(raw, file);
78
- }
79
-
80
- /** 一次性备份(已存在不覆盖):那份是「mslxdff 首次接管前」的原文。 */
81
- function backupOnce(dir, file, backupDir) {
82
- if (!existsSync(file)) return null;
83
- const target = join(backupDir, `${desktopBase(file)}.bak`);
84
- if (existsSync(target)) return null; // 已有更早的备份,不覆盖
85
- try {
86
- mkdirSync(backupDir, { recursive: true });
87
- copyFileSync(file, target);
88
- return target;
89
- } catch {
90
- return null; // 复制失败不阻塞写入,但绝不谎报「已备份」
91
- }
92
- }
93
- function desktopBase(file) {
94
- return String(file).split(/[\\/]/).pop().replace(/\.json$/i, "");
95
- }
96
-
97
- /** 原子写(tmp → rename,Windows 占用时退回直写;tmp 里带明文 token 必须先擦再删)。 */
98
- function writeFileSecure(file, text) {
99
- const tmp = `${file}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
100
- let leftover = null;
101
- const sweep = () => {
102
- if (!existsSync(tmp)) return;
103
- try { unlinkSync(tmp); return; } catch { /* 占用,走擦除 */ }
104
- try { writeFileSync(tmp, "", "utf8"); } catch { /* 擦不动也继续试删 */ }
105
- try { unlinkSync(tmp); } catch { leftover = tmp; }
106
- };
107
- let written = false;
108
- try {
109
- try { writeFileSync(tmp, text, { encoding: "utf8", mode: 0o600 }); } catch { writeFileSync(tmp, text, "utf8"); }
110
- try { chmodSync(tmp, 0o600); } catch { /* Windows 无 posix mode */ }
111
- try {
112
- renameSync(tmp, file);
113
- written = true;
114
- } catch {
115
- try { writeFileSync(file, text, { encoding: "utf8", mode: 0o600 }); written = true; } catch { /* 交给下面 throw */ }
116
- }
117
- } finally {
118
- sweep();
119
- }
120
- if (!written) throw new Error(`无法写入 ${file}(文件可能被其他进程占用)——配置未更新`);
121
- try { chmodSync(file, 0o600); } catch { /* ignore */ }
122
- return leftover;
123
- }
124
-
125
- /**
126
- * 写 profile + 登记 `_meta.json`。
127
- * @param {{port:number, token:string, rows:{slot:string,model:string,label?:string}[], dir?:string, baseUrl?:string, maxEffort?:string}} p
128
- */
129
- export function syncToClaudeDesktop(p = {}) {
130
- const dir = p.dir || claudeDesktopConfigDir();
131
- const token = String(p.token || "");
132
- if (!token) throw new Error("claude-desktop: token required(先跑 mslxdff -showtoken)");
133
- const rows = Array.isArray(p.rows) ? p.rows : [];
134
- if (!rows.length) throw new Error("claude-desktop: rows required(模型槽位为空)");
135
- const port = Number(p.port) || 8989;
136
- const baseUrl = p.baseUrl || `http://127.0.0.1:${port}`;
137
- const id = desktopConfigId(port);
138
- const configFile = join(dir, `${id}.json`);
139
- const metaFile = join(dir, "_meta.json");
140
- const backupDir = join(dirname(dir), "configLibrary.pre-mslxdff");
141
-
142
- const nextProfile = buildDesktopProfile({ baseUrl, apiKey: token, rows, maxEffort: p.maxEffort });
143
- const nextProfileText = `${JSON.stringify(nextProfile, null, 2)}\n`;
144
- // 先读后写:任一份现有文件坏了就整体不动(避免「profile 换了、_meta 没登记」的半截状态)
145
- // 幂等比较一律用**原文 vs 原文**:解析后的对象 stringify 是紧凑形,与盘上缩进形永远不等 → 会把「没变化」误报成「已更新」
146
- const oldProfileText = existsSync(configFile) ? readFileSync(configFile, "utf8") : null;
147
- const oldMetaText = existsSync(metaFile) ? readFileSync(metaFile, "utf8") : null;
148
- const oldProfile = oldProfileText === null ? null : parseJsonOrThrow(oldProfileText, configFile);
149
- const oldMeta = oldMetaText === null ? null : parseJsonOrThrow(oldMetaText, metaFile);
150
- const nextMeta = mergeMeta(oldMeta, { id, name: `mslxdff:${port}` });
151
- const nextMetaText = `${JSON.stringify(nextMeta, null, 2)}\n`;
152
- const unchanged = oldProfileText !== null && oldMetaText !== null
153
- && norm(oldProfileText) === norm(nextProfileText) && norm(oldMetaText) === norm(nextMetaText);
154
- if (unchanged) {
155
- return { action: "updated", id, dir, configFile, metaFile, backup: null, changed: false, rows: rows.length, profile: nextProfile, meta: nextMeta };
156
- }
157
- const b1 = backupOnce(dir, configFile, backupDir);
158
- const b2 = backupOnce(dir, metaFile, backupDir);
159
- mkdirSync(dir, { recursive: true });
160
- const l1 = writeFileSecure(configFile, nextProfileText);
161
- const l2 = writeFileSecure(metaFile, nextMetaText);
162
- return {
163
- action: oldProfile ? "updated" : "inserted",
164
- id, dir, configFile, metaFile,
165
- backup: b1 || b2 || null,
166
- backups: [b1, b2].filter(Boolean),
167
- changed: true,
168
- rows: rows.length,
169
- profile: nextProfile,
170
- meta: nextMeta,
171
- tmpLeftover: [l1, l2].filter(Boolean),
172
- };
173
- }
174
-
175
- /** `_meta.json` 合并:只动我们那一条,其它 entry 与 appliedId 语义原样保留。 */
176
- export function mergeMeta(existing, { id, name }) {
177
- const base = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
178
- const list = Array.isArray(base.entries) ? base.entries.slice() : [];
179
- const entries = [];
180
- let replaced = false;
181
- for (const e of list) {
182
- if (!e || typeof e !== "object") continue;
183
- if (String(e.id || "") === String(id)) { entries.push({ ...e, name: String(name) }); replaced = true; }
184
- else entries.push(e);
185
- }
186
- if (!replaced) entries.push({ id: String(id), name: String(name) });
187
- return { ...base, appliedId: String(id), entries };
188
- }
189
-
190
- /** 读当前生效配置(体检与 `--official` 用)。不抛错,一切异常如实回结构。 */
191
- export function readClaudeDesktopProfile({ dir } = {}) {
192
- const d = dir || claudeDesktopConfigDir();
193
- const metaFile = join(d, "_meta.json");
194
- const out = { dir: d, exists: existsSync(d), meta: null, appliedId: "", appliedName: "", appliedFile: "", profile: null, entries: [], error: "" };
195
- if (!out.exists) { out.error = "目录不存在(App 从未进过 3P 模式,或路径不是本机默认)"; return out; }
196
- let meta;
197
- try { meta = readJsonOr(metaFile); } catch (e) { out.error = String(e?.message || e); return out; }
198
- if (!meta || typeof meta !== "object") { out.error = "_meta.json 不是对象"; return out; }
199
- out.meta = meta;
200
- out.entries = Array.isArray(meta.entries) ? meta.entries : [];
201
- out.appliedId = String(meta.appliedId || "");
202
- const hit = out.entries.find((e) => String(e?.id || "") === out.appliedId) || null;
203
- out.appliedName = hit ? String(hit.name || "") : "";
204
- out.appliedFile = join(d, `${out.appliedId}.json`);
205
- if (!out.appliedId) { out.error = "appliedId 为空(App 里没选任何配置)"; return out; }
206
- try { out.profile = readJsonOr(out.appliedFile, { allowMissing: true }); } catch (e) { out.error = String(e?.message || e); }
207
- if (!out.profile) out.error = out.error || `生效配置 ${out.appliedId}.json 读不到`;
208
- return out;
209
- }
210
-
211
- /**
212
- * `--official`:把 mslxdff 那条登记从 `_meta.json` 摘掉(不删配置、不吞别人的 entry)。
213
- * 摘后若 `appliedId` 正是我们那条 → 置空,让 App 回到「未选配置」,用户在登录界面自选官方或别的配置。
214
- */
215
- export function retireClaudeDesktopProfile({ port, dir } = {}) {
216
- const d = dir || claudeDesktopConfigDir();
217
- const metaFile = join(d, "_meta.json");
218
- const id = desktopConfigId(port);
219
- const rawText = existsSync(metaFile) ? readFileSync(metaFile, "utf8") : null;
220
- const meta = rawText === null ? null : parseJsonOrThrow(rawText, metaFile);
221
- if (!meta || !Array.isArray(meta.entries)) return { action: "none", dir: d, removed: false, remaining: 0 };
222
- const before = meta.entries.length;
223
- const entries = meta.entries.filter((e) => String(e?.id || "") !== id);
224
- const appliedId = String(meta.appliedId || "") === id ? "" : String(meta.appliedId || "");
225
- const next = { ...meta, entries, appliedId };
226
- const nextText = `${JSON.stringify(next, null, 2)}\n`;
227
- // 幂等判据一律「原文 vs 原文」:对象 stringify 是紧凑形,与盘上缩进形永远不等 → 会把「没变化」报成「已摘除」
228
- if (rawText !== null && norm(rawText) === norm(nextText)) {
229
- return { action: "unchanged", dir: d, removed: false, remaining: entries.length, appliedId };
230
- }
231
- backupOnce(d, metaFile, join(dirname(d), "configLibrary.pre-mslxdff"));
232
- writeFileSecure(metaFile, nextText);
233
- return { action: "retired", dir: d, removed: before !== entries.length, remaining: entries.length, appliedId, id };
234
- }
1
+ import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync, unlinkSync, copyFileSync, chmodSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import os from "node:os";
4
+ import { toInferenceModels } from "./claude-desktop/slots.js";
5
+
6
+ /**
7
+ * -setto claude-desktop:把本机网关写成 **Claude Desktop on 3P** 的 profile。
8
+ *
9
+ * 落点与键面(官方 `claude.com/docs/third-party/claude-desktop/configuration` 「How keys are read」):
10
+ * - Windows 本地(非托管):`%LOCALAPPDATA%\Claude-3p\configLibrary\`
11
+ * - macOS:`~/Library/Application Support/Claude-3p/configLibrary/`
12
+ * - Linux:`~/.config/Claude-3p/configLibrary/`(`$XDG_CONFIG_HOME` 优先)
13
+ * 目录里 `_meta.json` 记「哪份配置生效」(`appliedId` + `entries[]`),每份配置是平级的 `<uuid>.json`。
14
+ * in-app 配置窗口(Developer → Configure Third-Party Inference…)写的就是这里,我们代劳。
15
+ *
16
+ * 三条硬红线(照 `src/sync-claude.js` 的既有纪律,不许松):
17
+ * 1. 只登记**自己那一条** entry(uuid 由端口派生 → 重跑原地更新、不堆条目),用户/窗口建的其它 entry 一字不动;
18
+ * 2. 现有 JSON 解析失败 → 一律拒写(那是用户在 App 里配过的东西,不是我们的草稿);
19
+ * 3. 文件里有明文 token → `0600`(新建带 mode + 存量补 chmod),半截 tmp 清理失败要把路径如实交出去。
20
+ *
21
+ * 不碰 `claude_desktop_config.json`(App 自己的 prefs),不碰托管策略(HKLM/HKCU\SOFTWARE\Policies\Claude
22
+ * 一旦存在,App 会整体忽略本地 configLibrary —— 那种情况 `-claude-desktop status` 会明说)。
23
+ */
24
+
25
+ /** configLibrary 目录(三平台 + env 覆盖,覆盖位专为单测隔离 HOME)。 */
26
+ export function claudeDesktopConfigDir() {
27
+ const env = process.env.MSLXDFF_CLAUDE_DESKTOP_DIR;
28
+ if (typeof env === "string" && env.trim()) return env.trim();
29
+ const home = os.homedir();
30
+ if (process.platform === "win32") {
31
+ const local = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
32
+ return join(local, "Claude-3p", "configLibrary");
33
+ }
34
+ if (process.platform === "darwin") return join(home, "Library", "Application Support", "Claude-3p", "configLibrary");
35
+ const xdg = (process.env.XDG_CONFIG_HOME || "").trim() || join(home, ".config");
36
+ return join(xdg, "Claude-3p", "configLibrary");
37
+ }
38
+
39
+ /** 配置条目 id:由端口派生的确定性 UUID 形态(同 cc-switch 的路子,端口不同即不撞车)。 */
40
+ export function desktopConfigId(port) {
41
+ const p = String(Math.max(0, Number(port) || 0) % 1000000).padStart(12, "0");
42
+ return `00000000-0000-4000-8000-${p}`;
43
+ }
44
+
45
+ /** 纯函数:profile 对象(App 侧读的键面)。 */
46
+ export function buildDesktopProfile({ baseUrl = "", apiKey = "", rows = [], maxEffort } = {}) {
47
+ return {
48
+ inferenceProvider: "gateway",
49
+ inferenceCredentialKind: "static",
50
+ inferenceGatewayAuthScheme: "bearer", // 本仓鉴权只认 Authorization: Bearer(src/routes/helpers.js authorized())
51
+ inferenceGatewayBaseUrl: String(baseUrl),
52
+ inferenceGatewayApiKey: String(apiKey),
53
+ inferenceModels: toInferenceModels(rows, { maxEffort }),
54
+ };
55
+ }
56
+
57
+ function norm(text) {
58
+ return String(text || "").replace(/\r\n/g, "\n").trim();
59
+ }
60
+
61
+ function parseJsonOrThrow(text, file) {
62
+ try {
63
+ return JSON.parse(text);
64
+ } catch {
65
+ throw new Error(`JSON 解析失败,拒绝覆盖:${file}`);
66
+ }
67
+ }
68
+
69
+ function readJsonOr(file, { allowMissing = false } = {}) {
70
+ let raw;
71
+ try {
72
+ raw = readFileSync(file, "utf8");
73
+ } catch {
74
+ if (allowMissing) return null;
75
+ throw new Error(`读不到 ${file}`);
76
+ }
77
+ return parseJsonOrThrow(raw, file);
78
+ }
79
+
80
+ /** 一次性备份(已存在不覆盖):那份是「mslxdff 首次接管前」的原文。 */
81
+ function backupOnce(dir, file, backupDir) {
82
+ if (!existsSync(file)) return null;
83
+ const target = join(backupDir, `${desktopBase(file)}.bak`);
84
+ if (existsSync(target)) return null; // 已有更早的备份,不覆盖
85
+ try {
86
+ mkdirSync(backupDir, { recursive: true });
87
+ copyFileSync(file, target);
88
+ return target;
89
+ } catch {
90
+ return null; // 复制失败不阻塞写入,但绝不谎报「已备份」
91
+ }
92
+ }
93
+ function desktopBase(file) {
94
+ return String(file).split(/[\\/]/).pop().replace(/\.json$/i, "");
95
+ }
96
+
97
+ /** 原子写(tmp → rename,Windows 占用时退回直写;tmp 里带明文 token 必须先擦再删)。 */
98
+ function writeFileSecure(file, text) {
99
+ const tmp = `${file}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
100
+ let leftover = null;
101
+ const sweep = () => {
102
+ if (!existsSync(tmp)) return;
103
+ try { unlinkSync(tmp); return; } catch { /* 占用,走擦除 */ }
104
+ try { writeFileSync(tmp, "", "utf8"); } catch { /* 擦不动也继续试删 */ }
105
+ try { unlinkSync(tmp); } catch { leftover = tmp; }
106
+ };
107
+ let written = false;
108
+ try {
109
+ try { writeFileSync(tmp, text, { encoding: "utf8", mode: 0o600 }); } catch { writeFileSync(tmp, text, "utf8"); }
110
+ try { chmodSync(tmp, 0o600); } catch { /* Windows 无 posix mode */ }
111
+ try {
112
+ renameSync(tmp, file);
113
+ written = true;
114
+ } catch {
115
+ try { writeFileSync(file, text, { encoding: "utf8", mode: 0o600 }); written = true; } catch { /* 交给下面 throw */ }
116
+ }
117
+ } finally {
118
+ sweep();
119
+ }
120
+ if (!written) throw new Error(`无法写入 ${file}(文件可能被其他进程占用)——配置未更新`);
121
+ try { chmodSync(file, 0o600); } catch { /* ignore */ }
122
+ return leftover;
123
+ }
124
+
125
+ /**
126
+ * 写 profile + 登记 `_meta.json`。
127
+ * @param {{port:number, token:string, rows:{slot:string,model:string,label?:string}[], dir?:string, baseUrl?:string, maxEffort?:string}} p
128
+ */
129
+ export function syncToClaudeDesktop(p = {}) {
130
+ const dir = p.dir || claudeDesktopConfigDir();
131
+ const token = String(p.token || "");
132
+ if (!token) throw new Error("claude-desktop: token required(先跑 mslxdff -showtoken)");
133
+ const rows = Array.isArray(p.rows) ? p.rows : [];
134
+ if (!rows.length) throw new Error("claude-desktop: rows required(模型槽位为空)");
135
+ const port = Number(p.port) || 8989;
136
+ const baseUrl = p.baseUrl || `http://127.0.0.1:${port}`;
137
+ const id = desktopConfigId(port);
138
+ const configFile = join(dir, `${id}.json`);
139
+ const metaFile = join(dir, "_meta.json");
140
+ const backupDir = join(dirname(dir), "configLibrary.pre-mslxdff");
141
+
142
+ const nextProfile = buildDesktopProfile({ baseUrl, apiKey: token, rows, maxEffort: p.maxEffort });
143
+ const nextProfileText = `${JSON.stringify(nextProfile, null, 2)}\n`;
144
+ // 先读后写:任一份现有文件坏了就整体不动(避免「profile 换了、_meta 没登记」的半截状态)
145
+ // 幂等比较一律用**原文 vs 原文**:解析后的对象 stringify 是紧凑形,与盘上缩进形永远不等 → 会把「没变化」误报成「已更新」
146
+ const oldProfileText = existsSync(configFile) ? readFileSync(configFile, "utf8") : null;
147
+ const oldMetaText = existsSync(metaFile) ? readFileSync(metaFile, "utf8") : null;
148
+ const oldProfile = oldProfileText === null ? null : parseJsonOrThrow(oldProfileText, configFile);
149
+ const oldMeta = oldMetaText === null ? null : parseJsonOrThrow(oldMetaText, metaFile);
150
+ const nextMeta = mergeMeta(oldMeta, { id, name: `mslxdff:${port}` });
151
+ const nextMetaText = `${JSON.stringify(nextMeta, null, 2)}\n`;
152
+ const unchanged = oldProfileText !== null && oldMetaText !== null
153
+ && norm(oldProfileText) === norm(nextProfileText) && norm(oldMetaText) === norm(nextMetaText);
154
+ if (unchanged) {
155
+ return { action: "updated", id, dir, configFile, metaFile, backup: null, changed: false, rows: rows.length, profile: nextProfile, meta: nextMeta };
156
+ }
157
+ const b1 = backupOnce(dir, configFile, backupDir);
158
+ const b2 = backupOnce(dir, metaFile, backupDir);
159
+ mkdirSync(dir, { recursive: true });
160
+ const l1 = writeFileSecure(configFile, nextProfileText);
161
+ const l2 = writeFileSecure(metaFile, nextMetaText);
162
+ return {
163
+ action: oldProfile ? "updated" : "inserted",
164
+ id, dir, configFile, metaFile,
165
+ backup: b1 || b2 || null,
166
+ backups: [b1, b2].filter(Boolean),
167
+ changed: true,
168
+ rows: rows.length,
169
+ profile: nextProfile,
170
+ meta: nextMeta,
171
+ tmpLeftover: [l1, l2].filter(Boolean),
172
+ };
173
+ }
174
+
175
+ /** `_meta.json` 合并:只动我们那一条,其它 entry 与 appliedId 语义原样保留。 */
176
+ export function mergeMeta(existing, { id, name }) {
177
+ const base = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
178
+ const list = Array.isArray(base.entries) ? base.entries.slice() : [];
179
+ const entries = [];
180
+ let replaced = false;
181
+ for (const e of list) {
182
+ if (!e || typeof e !== "object") continue;
183
+ if (String(e.id || "") === String(id)) { entries.push({ ...e, name: String(name) }); replaced = true; }
184
+ else entries.push(e);
185
+ }
186
+ if (!replaced) entries.push({ id: String(id), name: String(name) });
187
+ return { ...base, appliedId: String(id), entries };
188
+ }
189
+
190
+ /** 读当前生效配置(体检与 `--official` 用)。不抛错,一切异常如实回结构。 */
191
+ export function readClaudeDesktopProfile({ dir } = {}) {
192
+ const d = dir || claudeDesktopConfigDir();
193
+ const metaFile = join(d, "_meta.json");
194
+ const out = { dir: d, exists: existsSync(d), meta: null, appliedId: "", appliedName: "", appliedFile: "", profile: null, entries: [], error: "" };
195
+ if (!out.exists) { out.error = "目录不存在(App 从未进过 3P 模式,或路径不是本机默认)"; return out; }
196
+ let meta;
197
+ try { meta = readJsonOr(metaFile); } catch (e) { out.error = String(e?.message || e); return out; }
198
+ if (!meta || typeof meta !== "object") { out.error = "_meta.json 不是对象"; return out; }
199
+ out.meta = meta;
200
+ out.entries = Array.isArray(meta.entries) ? meta.entries : [];
201
+ out.appliedId = String(meta.appliedId || "");
202
+ const hit = out.entries.find((e) => String(e?.id || "") === out.appliedId) || null;
203
+ out.appliedName = hit ? String(hit.name || "") : "";
204
+ out.appliedFile = join(d, `${out.appliedId}.json`);
205
+ if (!out.appliedId) { out.error = "appliedId 为空(App 里没选任何配置)"; return out; }
206
+ try { out.profile = readJsonOr(out.appliedFile, { allowMissing: true }); } catch (e) { out.error = String(e?.message || e); }
207
+ if (!out.profile) out.error = out.error || `生效配置 ${out.appliedId}.json 读不到`;
208
+ return out;
209
+ }
210
+
211
+ /**
212
+ * `--official`:把 mslxdff 那条登记从 `_meta.json` 摘掉(不删配置、不吞别人的 entry)。
213
+ * 摘后若 `appliedId` 正是我们那条 → 置空,让 App 回到「未选配置」,用户在登录界面自选官方或别的配置。
214
+ */
215
+ export function retireClaudeDesktopProfile({ port, dir } = {}) {
216
+ const d = dir || claudeDesktopConfigDir();
217
+ const metaFile = join(d, "_meta.json");
218
+ const id = desktopConfigId(port);
219
+ const rawText = existsSync(metaFile) ? readFileSync(metaFile, "utf8") : null;
220
+ const meta = rawText === null ? null : parseJsonOrThrow(rawText, metaFile);
221
+ if (!meta || !Array.isArray(meta.entries)) return { action: "none", dir: d, removed: false, remaining: 0 };
222
+ const before = meta.entries.length;
223
+ const entries = meta.entries.filter((e) => String(e?.id || "") !== id);
224
+ const appliedId = String(meta.appliedId || "") === id ? "" : String(meta.appliedId || "");
225
+ const next = { ...meta, entries, appliedId };
226
+ const nextText = `${JSON.stringify(next, null, 2)}\n`;
227
+ // 幂等判据一律「原文 vs 原文」:对象 stringify 是紧凑形,与盘上缩进形永远不等 → 会把「没变化」报成「已摘除」
228
+ if (rawText !== null && norm(rawText) === norm(nextText)) {
229
+ return { action: "unchanged", dir: d, removed: false, remaining: entries.length, appliedId };
230
+ }
231
+ backupOnce(d, metaFile, join(dirname(d), "configLibrary.pre-mslxdff"));
232
+ writeFileSecure(metaFile, nextText);
233
+ return { action: "retired", dir: d, removed: before !== entries.length, remaining: entries.length, appliedId, id };
234
+ }