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.
- package/README.md +3 -3
- package/bin/mslxdff.js +1 -1
- package/docs/cli_help_mini.md +146 -0
- package/package.json +38 -38
- package/plugins/README.md +14 -14
- package/plugins/prefer-model.mjs.example +14 -14
- package/src/anthropic/canonical-json.js +55 -0
- package/src/anthropic/count-tokens.js +61 -61
- package/src/anthropic/request-body.js +183 -126
- package/src/anthropic/request-shape.js +60 -0
- package/src/anthropic/session-id.js +58 -0
- package/src/anthropic/shared.js +18 -18
- package/src/anthropic/sse-aggregate.js +56 -56
- package/src/anthropic/tool-media.js +75 -0
- package/src/anthropic/translate.js +296 -287
- package/src/anthropic/web-search.js +361 -361
- package/src/api-kind.js +31 -31
- package/src/autostart.js +288 -288
- package/src/bench/probe.js +1 -1
- package/src/bench/runner.js +1 -1
- package/src/bench/via-probe.js +1 -1
- package/src/bench/via.js +144 -144
- package/src/bench/workbuddy-bench.js +1 -1
- package/src/chat/config.js +11 -11
- package/src/chat/cooling.js +81 -81
- package/src/chat/direct.js +58 -58
- package/src/chat/index.js +6 -6
- package/src/chat/spinner.js +38 -38
- package/src/chat/sse.js +69 -69
- package/src/chat/stats.js +247 -247
- package/src/chat/store.js +48 -48
- package/src/chat/tool-handlers.js +67 -67
- package/src/chat/tools.js +1 -1
- package/src/chat/upstream.js +59 -59
- package/src/chat-pipeline/auto-race.js +158 -158
- package/src/chat-pipeline/empty-turn.js +147 -147
- package/src/chat-pipeline/engine.js +17 -17
- package/src/chat-pipeline/index.js +8 -40
- package/src/chat-pipeline/serial-trial.js +279 -283
- package/src/chat-pipeline/terminal-verdict.js +132 -147
- package/src/chooser.js +54 -54
- package/src/claude-desktop/apply.js +101 -101
- package/src/claude-desktop/context1m.js +38 -38
- package/src/claude-desktop/doctor.js +146 -146
- package/src/claude-desktop/slots.js +152 -152
- package/src/cli/bootstrap.js +1 -1
- package/src/cli/commands/claude-desktop.js +284 -284
- package/src/cli/commands/daemon.js +231 -231
- package/src/cli/commands/model/list-live.js +22 -22
- package/src/cli/commands/model/list-providers.js +76 -76
- package/src/cli/commands/model/list-render.js +77 -77
- package/src/cli/commands/model/list-sort.js +52 -52
- package/src/cli/commands/model/list-tty.js +76 -76
- package/src/cli/commands/model/list.js +166 -166
- package/src/cli/commands/model/live-models.js +24 -24
- package/src/cli/commands/model/stats.js +43 -43
- package/src/cli/commands/model/status.js +47 -47
- package/src/cli/commands/model.js +51 -51
- package/src/cli/commands/provider/add.js +108 -108
- package/src/cli/commands/provider/allowlist.js +99 -99
- package/src/cli/commands/provider/cline-free.js +99 -99
- package/src/cli/commands/provider/cline-login.js +218 -218
- package/src/cli/commands/provider/cline-quota.js +65 -65
- package/src/cli/commands/provider/codearts-login.js +45 -45
- package/src/cli/commands/provider/config.js +119 -119
- package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
- package/src/cli/commands/provider/keys-guard.js +50 -50
- package/src/cli/commands/provider/keys.js +161 -161
- package/src/cli/commands/provider/qoder-checkin.js +81 -81
- package/src/cli/commands/provider/qoder-login.js +102 -102
- package/src/cli/commands/provider/raccoon-login.js +180 -180
- package/src/cli/commands/provider/raccoon-password.js +162 -162
- package/src/cli/commands/provider/raccoon-quota.js +109 -109
- package/src/cli/commands/provider/traework-checkin.js +144 -144
- package/src/cli/commands/provider/traework-login.js +164 -164
- package/src/cli/commands/provider/workbuddy-login.js +153 -153
- package/src/cli/commands/provider/zcode-login.js +77 -77
- package/src/cli/commands/provider/zcode-quota.js +55 -55
- package/src/cli/commands/provider.js +1 -1
- package/src/cli/commands/stats.js +158 -149
- package/src/cli/commands/system.js +261 -261
- package/src/cli/commands/timezone.js +47 -47
- package/src/cli/commands/use-group.js +60 -60
- package/src/cli/commands/workbuddy.js +189 -189
- package/src/cli/format.js +141 -141
- package/src/cli/group-helpers.js +70 -70
- package/src/cli/help.js +80 -80
- package/src/cli/index.js +70 -70
- package/src/cli/interactive.js +121 -121
- package/src/cli/join-core.js +99 -99
- package/src/cli/join-wizard.js +175 -175
- package/src/cli/policy.js +136 -136
- package/src/cli/provider-row.js +126 -126
- package/src/cli/util.js +24 -24
- package/src/compat.js +53 -53
- package/src/events.js +38 -38
- package/src/free-lane.js +131 -131
- package/src/free-watcher.js +63 -63
- package/src/groups.js +262 -262
- package/src/logs.js +195 -195
- package/src/metrics.js +71 -62
- package/src/model-capabilities/context-lookup.js +65 -65
- package/src/model-capabilities/merge.js +142 -142
- package/src/model-trace.js +187 -187
- package/src/models.js +1 -1
- package/src/opencode-identity.js +72 -72
- package/src/peers.js +254 -254
- package/src/plugins.js +107 -107
- package/src/providers/AGENTS.md +49 -0
- package/src/providers/base.js +164 -164
- package/src/providers/classify.js +22 -22
- package/src/providers/cline/allowlist-sync.js +93 -93
- package/src/providers/cline/free-catalog.js +71 -71
- package/src/providers/cline/headers.js +37 -37
- package/src/providers/cline/index.js +130 -130
- package/src/providers/cline/models.js +150 -150
- package/src/providers/cline/usage.js +274 -274
- package/src/providers/codearts/auth-pool.js +148 -148
- package/src/providers/codearts/concurrency.js +34 -34
- package/src/providers/codearts/const.js +65 -65
- package/src/providers/codearts/dpop.js +95 -95
- package/src/providers/codearts/index.js +175 -175
- package/src/providers/codearts/login.js +214 -214
- package/src/providers/codearts/models.js +182 -182
- package/src/providers/codearts/sign.js +96 -96
- package/src/providers/codearts/sse.js +204 -204
- package/src/providers/codearts/stream.js +209 -209
- package/src/providers/codearts/sts.js +124 -124
- package/src/providers/codearts.js +1 -1
- package/src/providers/dispatcher.js +146 -146
- package/src/providers/generic.js +156 -156
- package/src/providers/globalqwenwork/account-store.js +141 -141
- package/src/providers/globalqwenwork/constants.js +73 -73
- package/src/providers/globalqwenwork/crypto.js +2 -2
- package/src/providers/globalqwenwork/http.js +2 -2
- package/src/providers/globalqwenwork/index.js +331 -331
- package/src/providers/globalqwenwork/payload.js +147 -147
- package/src/providers/globalqwenwork/rsa.js +2 -2
- package/src/providers/globalqwenwork/sse.js +2 -2
- package/src/providers/globalqwenwork/stream.js +2 -2
- package/src/providers/globalqwenwork.js +1 -1
- package/src/providers/globalworkbuddy/account-store.js +198 -198
- package/src/providers/globalworkbuddy/auth.js +97 -97
- package/src/providers/globalworkbuddy/catalog.js +138 -138
- package/src/providers/globalworkbuddy/constants.js +73 -73
- package/src/providers/globalworkbuddy/device-auth.js +120 -120
- package/src/providers/globalworkbuddy/errors.js +98 -98
- package/src/providers/globalworkbuddy/headers.js +89 -89
- package/src/providers/globalworkbuddy/models.js +96 -96
- package/src/providers/globalworkbuddy/payload.js +80 -80
- package/src/providers/index.js +5 -5
- package/src/providers/model-id.js +172 -172
- package/src/providers/opencode.js +19 -19
- package/src/providers/openrouter.js +31 -31
- package/src/providers/qoder/account-store.js +106 -106
- package/src/providers/qoder/aggregate.js +60 -60
- package/src/providers/qoder/baseprompt.json +591 -591
- package/src/providers/qoder/checkin.js +160 -160
- package/src/providers/qoder/constants.js +44 -44
- package/src/providers/qoder/encode.js +41 -41
- package/src/providers/qoder/fingerprint.js +38 -38
- package/src/providers/qoder/models.js +77 -77
- package/src/providers/qoder/oauth.js +84 -84
- package/src/providers/qoder/payload.js +162 -162
- package/src/providers/qoder/request.js +27 -27
- package/src/providers/qoder/session.js +134 -134
- package/src/providers/qoder/sticky.js +38 -38
- package/src/providers/qoder/stream.js +116 -116
- package/src/providers/qoder.js +1 -1
- package/src/providers/raccoon/account-store.js +216 -216
- package/src/providers/raccoon/auth.js +67 -67
- package/src/providers/raccoon/chat.js +228 -228
- package/src/providers/raccoon/const.js +180 -180
- package/src/providers/raccoon/credits.js +185 -185
- package/src/providers/raccoon/crypto.js +43 -43
- package/src/providers/raccoon/envelope.js +31 -31
- package/src/providers/raccoon/headers.js +45 -45
- package/src/providers/raccoon/index.js +216 -216
- package/src/providers/raccoon/login.js +279 -279
- package/src/providers/raccoon/models.js +160 -160
- package/src/providers/raccoon/password-login.js +102 -102
- package/src/providers/raccoon/qr-codewords.js +115 -115
- package/src/providers/raccoon/qr.js +236 -236
- package/src/providers/raccoon/sse.js +135 -135
- package/src/providers/responses-channel.js +120 -120
- package/src/providers/share-keys.js +53 -53
- package/src/providers/traework/account-store.js +234 -234
- package/src/providers/traework/chat.js +217 -217
- package/src/providers/traework/checkin.js +77 -77
- package/src/providers/traework/constants.js +41 -41
- package/src/providers/traework/errors.js +52 -52
- package/src/providers/traework/headers.js +79 -79
- package/src/providers/traework/index.js +132 -132
- package/src/providers/traework/market-identity.js +116 -116
- package/src/providers/traework/models.js +98 -98
- package/src/providers/traework/payload.js +86 -86
- package/src/providers/traework/sse.js +184 -184
- package/src/providers/traework/token.js +52 -52
- package/src/providers/traework.js +2 -2
- package/src/providers/workbuddy/account-store.js +186 -186
- package/src/providers/workbuddy/auth.js +158 -158
- package/src/providers/workbuddy/balance.js +86 -86
- package/src/providers/workbuddy/cat-travel.js +125 -125
- package/src/providers/workbuddy/checkin.js +75 -75
- package/src/providers/workbuddy/desktop-info.js +78 -78
- package/src/providers/workbuddy/growth-api.js +67 -67
- package/src/providers/workbuddy/growth-plans.js +63 -63
- package/src/providers/workbuddy/growth.js +181 -181
- package/src/providers/workbuddy/index.js +217 -217
- package/src/providers/workbuddy/models.js +126 -126
- package/src/providers/workbuddy/payload.js +141 -141
- package/src/providers/workbuddy/rotation-log.js +83 -83
- package/src/providers/workbuddy/sanitize-tools.js +61 -61
- package/src/providers/workbuddy/sdk-chat.js +26 -26
- package/src/providers/workbuddy-balance.js +78 -78
- package/src/providers/zcode/account-store.js +129 -129
- package/src/providers/zcode/auth.js +28 -28
- package/src/providers/zcode/chat.js +179 -179
- package/src/providers/zcode/const.js +54 -54
- package/src/providers/zcode/context-shape.js +162 -162
- package/src/providers/zcode/headers.js +93 -93
- package/src/providers/zcode/index.js +124 -124
- package/src/providers/zcode/models.js +65 -65
- package/src/providers/zcode/oauth.js +120 -120
- package/src/providers/zcode/quota.js +176 -176
- package/src/providers/zcode/sse.js +196 -196
- package/src/readline-compat.js +25 -25
- package/src/responses/translate.js +311 -311
- package/src/routes/AGENTS.md +37 -0
- package/src/routes/chat/broadband-handler.js +72 -72
- package/src/routes/chat/empty-turn-report.js +31 -31
- package/src/routes/chat/exhausted-handler.js +87 -87
- package/src/routes/chat/hedge-handler.js +130 -130
- package/src/routes/chat/index.js +1 -1
- package/src/routes/chat/local-handler.js +63 -63
- package/src/routes/chat/peer-handler.js +81 -81
- package/src/routes/chat.js +3 -3
- package/src/routes/fallback.js +88 -88
- package/src/routes/groups-relay.js +214 -214
- package/src/routes/groups.js +122 -122
- package/src/routes/helpers.js +93 -83
- package/src/routes/index.js +147 -147
- package/src/routes/messages-route.js +375 -365
- package/src/routes/peers.js +10 -2
- package/src/routes/relay-queue.js +181 -181
- package/src/routes/relay.js +60 -60
- package/src/routes/responses-route.js +213 -213
- package/src/routes/stream-body.js +84 -84
- package/src/routes/stream-hold.js +337 -337
- package/src/routes/stream-scan.js +366 -366
- package/src/routes.js +3 -3
- package/src/runtime/auto-update.js +113 -113
- package/src/runtime/broadband-stream.js +79 -79
- package/src/runtime/broadband.js +103 -103
- package/src/runtime/group-sync.js +81 -81
- package/src/runtime/keep-warm.js +120 -120
- package/src/runtime/lifecycle-forensics.js +116 -116
- package/src/runtime/lifecycle-log.js +87 -87
- package/src/runtime/log-stamp.js +73 -73
- package/src/runtime/proc-info.js +137 -137
- package/src/runtime/qoder-checkin.js +93 -93
- package/src/runtime/raccoon-checkin.js +126 -126
- package/src/runtime/server-lifecycle.js +189 -189
- package/src/runtime/traework-checkin.js +118 -118
- package/src/runtime/workbuddy-checkin.js +120 -120
- package/src/runtime/workbuddy-growth.js +133 -133
- package/src/state/facade.js +58 -58
- package/src/state/memory.js +162 -162
- package/src/state/merge.js +25 -25
- package/src/state/migrations/cline-unify.js +94 -94
- package/src/state/migrations.js +33 -33
- package/src/state/persist.js +42 -42
- package/src/state/provider-config.js +142 -142
- package/src/state/schemas/group.js +31 -31
- package/src/state/schemas/model.js +94 -94
- package/src/state/schemas/peer.js +31 -31
- package/src/state/schemas/port.js +10 -10
- package/src/state/schemas/provider.js +202 -202
- package/src/state/schemas/timezone.js +70 -70
- package/src/state/schemas/token.js +48 -48
- package/src/state/schemas/use-group.js +102 -102
- package/src/state/store.js +176 -176
- package/src/state.js +1 -1
- package/src/sync-claude-desktop.js +234 -234
- package/src/sync-claude.js +152 -152
- package/src/sync-codex.js +116 -116
- package/src/sync-workbuddy.js +158 -158
- package/src/talk-full-redact.js +129 -129
- package/src/talk-full.js +131 -131
- package/src/talk-log.js +257 -257
- package/src/text-tokens.js +52 -52
- package/src/thinking-effort.js +67 -67
- package/src/time.js +106 -106
- package/src/timeline.js +26 -26
- package/src/transport/index.js +1 -1
- package/src/upstream-engine/index.js +74 -74
- package/src/upstream-engine/mode.js +15 -15
- package/src/upstream-engine/sdk/attempt.js +221 -221
- package/src/upstream-engine/sdk/chat.js +28 -28
- package/src/upstream-engine/sdk/convert.js +192 -192
- package/src/upstream-engine/sdk/diagnose.js +62 -62
- package/src/upstream-engine/sdk/dispatch.js +55 -55
- package/src/upstream-engine/sdk/responses.js +176 -176
- package/src/upstream-engine/sdk/sse.js +166 -166
- package/src/upstream-responses.js +305 -305
- package/src/usage/cache-fields.js +58 -0
- package/src/usage/record.js +109 -106
- package/src/usage/report.js +209 -195
- package/src/v6pool/claim.js +157 -157
- package/src/v6pool/config.js +65 -65
- package/src/v6pool/detect.js +130 -130
- package/src/v6pool/index.js +152 -152
- package/src/v6pool/prefix.js +140 -140
- package/src/v6pool/probe.js +40 -40
- package/src/v6pool/report.js +104 -104
- package/src/v6pool/rotate.js +263 -263
- package/src/warp-pool/config.js +74 -74
- package/src/warp-pool/health.js +106 -106
- package/src/warp-pool/identity.js +141 -141
- package/src/warp-pool/index.js +358 -358
- package/src/warp-pool/keys.js +29 -29
- package/src/warp-pool/nodes.js +95 -95
- package/src/warp-pool/ports.js +160 -160
- package/src/warp-pool/register.js +113 -113
- package/src/warp-pool/report.js +232 -232
- package/src/warp-pool/revive.js +168 -168
- package/src/warp-pool/rotate.js +214 -214
- package/src/warp-pool/strategy.js +43 -43
- package/src/warp-pool/verdict.js +69 -69
- 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
|
+
}
|