Files
codex-market-app/electron/lib/claudedesk.js
T

260 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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/<id>.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,
};