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