fix: canvas image/video download - fix data URL parsing in cvDownloadPreview (v1.5.21, hot 1.5.23)

This commit is contained in:
mac
2026-07-20 08:48:56 +08:00
parent 42e23663ee
commit aadd2b04ae
28 changed files with 5881 additions and 329 deletions
+259
View File
@@ -0,0 +1,259 @@
// 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,
};