// claudedesk.js —— 把中转分组接入 Claude 桌面 App(图形客户端),免登录直用中转。 // 关键:走 App 自己的「已保存配置库」(configLibrary),不是托管偏好(defaults/.mobileconfig)。 // 托管偏好那条路是企业 MDM 通道,会强制 SSO 登录,个人账号卡登录页 —— 早期踩过坑,已弃用。 // 做法对照 cc-switch 源码 src-tauri/src/claude_desktop_config.rs(farion1231/cc-switch)核实。 // 详见 memory/claude-desktop-3p-config.md。 "use strict"; const fs = require("fs"); const os = require("os"); const path = require("path"); const router = require("./router"); const PLATFORM = process.platform; // 'darwin' | 'win32' | ... const CONFIG_FILE = "claude_desktop_config.json"; const CONFIG_LIBRARY_DIR = "configLibrary"; const PROFILE_ID = "00000000-0000-4000-8000-000000157210"; const PROFILE_NAME = "TokenDun"; // 备份根:复用现有 ~/.codex/backups_state 约定 const BACKUP_DIR = path.join(os.homedir(), ".codex", "backups_state", "claudedesk", "official"); // —— 平台路径 —— // 返回 { normalConfig, threepConfig, libDir, profilePath, metaPath } 或 null(不支持) function platformPaths() { if (PLATFORM === "darwin") { const appSupport = path.join(os.homedir(), "Library", "Application Support"); return dirsToPaths(path.join(appSupport, "Claude"), path.join(appSupport, "Claude-3p")); } if (PLATFORM === "win32") { const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"); return dirsToPaths(pickWinDir(localAppData, "Claude"), pickWinDir(localAppData, "Claude-3p")); } return null; } // Windows 下 App 目录名可能带后缀,先精确后模糊匹配 function pickWinDir(base, exact) { const p = path.join(base, exact); if (fs.existsSync(p)) return p; try { const hit = fs.readdirSync(base).find((n) => { const full = path.join(base, n); const isDir = (() => { try { return fs.statSync(full).isDirectory(); } catch { return false; } })(); if (!isDir) return false; return exact === "Claude-3p" ? /claude.*3p/i.test(n) : (/claude/i.test(n) && !/3p/i.test(n)); }); if (hit) return path.join(base, hit); } catch {} return p; } function dirsToPaths(normalDir, threepDir) { const libDir = path.join(threepDir, CONFIG_LIBRARY_DIR); return { normalConfig: path.join(normalDir, CONFIG_FILE), threepConfig: path.join(threepDir, CONFIG_FILE), libDir, profilePath: path.join(libDir, `${PROFILE_ID}.json`), metaPath: path.join(libDir, "_meta.json"), }; } // —— JSON 读写小工具 —— const readJson = (p) => { try { return JSON.parse(fs.readFileSync(p, "utf-8")); } catch { return null; } }; const readJsonOrEmpty = (p) => { const v = readJson(p); return v && typeof v === "object" && !Array.isArray(v) ? v : {}; }; function writeJson(p, obj) { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(obj, null, 2) + "\n"); } const isSupported = () => PLATFORM === "darwin" || PLATFORM === "win32"; // —— gateway profile 本体(写进 configLibrary/.json)—— function buildGatewayProfile(group) { const oneM = new Set(Array.isArray(group.oneMillionModels) ? group.oneMillionModels : []); const profile = { coworkEgressAllowedHosts: ["*"], disableDeploymentModeChooser: true, inferenceGatewayApiKey: group.key || "", inferenceGatewayAuthScheme: "bearer", inferenceGatewayBaseUrl: String(group.baseUrl || "").replace(/\/+$/, ""), inferenceProvider: "gateway", }; if (group.model) { const item = { id: group.model }; if (oneM.has(group.model)) item.supports1m = true; // 1M 长上下文标记(不是 beta 头) profile.inferenceModels = [item]; } return profile; } // —— deploymentMode 写入(就地改,不动其它字段,尤其保留官方账号绑定 preferences)—— function writeDeploymentMode(p, mode) { const v = readJsonOrEmpty(p); v.deploymentMode = mode; writeJson(p, v); } // —— _meta.json:登记/清除本 profile —— function writeMeta(metaPath, applied) { const v = readJsonOrEmpty(metaPath); let entries = Array.isArray(v.entries) ? v.entries : []; entries = entries.filter((e) => !(e && e.id === PROFILE_ID)); // 去重 if (applied) { entries.push({ id: PROFILE_ID, name: PROFILE_NAME }); v.appliedId = PROFILE_ID; } else if (v.appliedId === PROFILE_ID) { // 取消:若当前 applied 指向本 profile,切到剩余任一或清空 const next = entries.find((e) => e && e.id); if (next) v.appliedId = next.id; else delete v.appliedId; } v.entries = entries; writeJson(metaPath, v); } // ============================================================================ // 备份 / 还原(防「回不去官方登录」)—— 幂等 write-once 快照那 4 个文件 // ============================================================================ const SNAP_FILES = (p) => [ ["normalConfig", p.normalConfig], ["threepConfig", p.threepConfig], ["profile", p.profilePath], ["meta", p.metaPath], ]; function snapshotOfficial() { fs.mkdirSync(BACKUP_DIR, { recursive: true }); const marker = path.join(BACKUP_DIR, "done.json"); if (fs.existsSync(marker)) return { firstTime: false, dir: BACKUP_DIR }; const p = platformPaths(); const meta = { ts: Date.now(), platform: PLATFORM, files: {} }; if (p) { for (const [key, src] of SNAP_FILES(p)) { const dst = path.join(BACKUP_DIR, key + ".json"); if (fs.existsSync(src)) { try { fs.copyFileSync(src, dst); meta.files[key] = { existed: true }; try { fs.chmodSync(dst, 0o600); } catch {} } catch { meta.files[key] = { existed: true, copyFailed: true }; } } else { meta.files[key] = { existed: false }; // 记录原本不存在 → 还原时删除 } } } fs.writeFileSync(marker, JSON.stringify(meta, null, 2)); return { firstTime: true, dir: BACKUP_DIR }; } // 还原官方:把中转配置抹掉,deploymentMode 回 1p,App 恢复官方账号登录。 // 优先用首份快照精确复原;无快照则按规则清理(删 profile、meta 清 entry、config 回 1p)。 function restoreOfficial() { if (!isSupported()) return { ok: false, error: "当前系统不支持(仅 macOS / Windows)" }; const p = platformPaths(); if (!p) return { ok: false, error: "无法定位 Claude 桌面 App 配置目录" }; const marker = path.join(BACKUP_DIR, "done.json"); const meta = readJson(marker); try { if (meta && meta.files) { // 精确复原:有备份写回,标记「原本不存在」的删掉 for (const [key, src] of SNAP_FILES(p)) { const info = meta.files[key] || {}; const bak = path.join(BACKUP_DIR, key + ".json"); if (info.existed && fs.existsSync(bak)) { fs.mkdirSync(path.dirname(src), { recursive: true }); fs.copyFileSync(bak, src); } else if (!info.existed && fs.existsSync(src)) { try { fs.unlinkSync(src); } catch {} } } } else { // 无备份:按规则清理回官方 writeDeploymentMode(p.normalConfig, "1p"); writeDeploymentMode(p.threepConfig, "1p"); if (fs.existsSync(p.profilePath)) { try { fs.unlinkSync(p.profilePath); } catch {} } writeMeta(p.metaPath, null); } } catch (e) { return { ok: false, error: "还原失败:" + (e.message || e) }; } return { ok: true, message: "已还原 Claude 官方配置,请重启 Claude 桌面 App(会回到账号登录)" }; } // ============================================================================ // 一键接入(写 configLibrary,免登录) // ============================================================================ function applyGroup(groupId) { if (!isSupported()) return { ok: false, error: "当前系统暂不支持(仅 macOS / Windows)" }; const g = router.groupById(groupId); if (!g) return { ok: false, error: "未找到该分组" }; if (g.isDefault) return { ok: false, error: "默认直连分组不能接入" }; if (!g.baseUrl || !g.key) return { ok: false, error: "该分组缺少 API 地址或密钥" }; if ((g.mode || "responses") !== "claude") { return { ok: false, error: "Claude 桌面 App 只接受 Anthropic 协议(/v1/messages)分组,请把该分组的接口类型改成 Claude Code" }; } const p = platformPaths(); if (!p) return { ok: false, error: "无法定位 Claude 桌面 App 配置目录(未安装?)" }; // 写前强制备份官方现状(幂等 write-once) let backup; try { backup = snapshotOfficial(); } catch (e) { return { ok: false, error: "备份官方配置失败:" + (e.message || e) }; } // 失败回滚:先记住本次要写的 4 个文件的当前内容 const rollback = SNAP_FILES(p).map(([k, f]) => ({ f, content: fs.existsSync(f) ? fs.readFileSync(f) : null })); try { const profile = buildGatewayProfile(g); writeDeploymentMode(p.normalConfig, "3p"); writeDeploymentMode(p.threepConfig, "3p"); writeJson(p.profilePath, profile); writeMeta(p.metaPath, true); } catch (e) { // 回滚 for (const r of rollback) { try { if (r.content === null) { if (fs.existsSync(r.f)) fs.unlinkSync(r.f); } else fs.writeFileSync(r.f, r.content); } catch {} } return { ok: false, error: "写入 Claude 桌面配置失败:" + (e.message || e) }; } const has1m = Array.isArray(g.oneMillionModels) && g.model && g.oneMillionModels.includes(g.model); return { ok: true, firstTimeBackup: backup.firstTime, has1m: !!has1m, message: "已接入 Claude 桌面 App,请重启 App 生效(无需登录,直接用中转)。官方配置已备份,可随时一键还原。", }; } // 导出 profile JSON(供高级用户手动放置 / 备份) function exportConfig(groupId, format) { const g = router.groupById(groupId); if (!g) return { ok: false, error: "未找到该分组" }; if (!g.baseUrl || !g.key) return { ok: false, error: "该分组缺少 API 地址或密钥" }; const profile = buildGatewayProfile(g); const content = JSON.stringify(profile, null, 2) + "\n"; return { ok: true, content, filename: `${PROFILE_ID}.json`, ext: "json" }; } // 复制片段(configLibrary profile 内容) function snippet(groupId) { const r = exportConfig(groupId, "json"); return r.ok ? { ok: true, text: r.content } : r; } function status() { const marker = path.join(BACKUP_DIR, "done.json"); const meta = readJson(marker); const p = platformPaths(); let applied = false; if (p) { const cfg = readJson(p.threepConfig) || readJson(p.normalConfig); const m = readJson(p.metaPath); applied = !!(cfg && cfg.deploymentMode === "3p" && m && m.appliedId === PROFILE_ID); } return { supported: isSupported(), platform: PLATFORM, hasBackup: !!meta, backupTs: meta ? meta.ts : 0, currentApplied: applied, }; } module.exports = { status, applyGroup, restoreOfficial, exportConfig, snippet, // 供测试 buildGatewayProfile, platformPaths, snapshotOfficial, PROFILE_ID, };