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,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
+ }