cicy-desktop 2.1.176 → 2.1.178
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/package.json +1 -1
- package/src/backends/homepage-preload.js +1 -0
- package/src/backends/homepage-react/assets/index-CH3x7Ecg.js +377 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/sidecar-ipc.js +12 -1
- package/src/sidecar/colima-docker.js +98 -7
- package/workers/render/src/App.jsx +23 -1
- package/src/backends/homepage-react/assets/index-Cirmfhyz.js +0 -377
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
|
7
7
|
<link rel="icon" type="image/png" sizes="256x256" href="./favicon-256.png" />
|
|
8
8
|
<title>CiCy Desktop</title>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-CH3x7Ecg.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-CGcN1St7.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -78,7 +78,7 @@ async function ensureDockerTeam() {
|
|
|
78
78
|
const r = await cc.registerDockerTeam({ port: APP_PORT });
|
|
79
79
|
if (r && r.ok && r.apiKey) {
|
|
80
80
|
dockerTeamReg = { teamId: r.teamId, title: r.title || `Docker :${APP_PORT}`, apiKey: r.apiKey };
|
|
81
|
-
try { const store = readDockerTeams(); store[APP_VOLUME] = { teamId: r.teamId, title: dockerTeamReg.title, apiKey: r.apiKey }; writeDockerTeams(store); } catch {}
|
|
81
|
+
try { const store = readDockerTeams(); store[APP_VOLUME] = { ...(store[APP_VOLUME] || {}), teamId: r.teamId, title: dockerTeamReg.title, apiKey: r.apiKey }; writeDockerTeams(store); } catch {}
|
|
82
82
|
return dockerTeamReg;
|
|
83
83
|
}
|
|
84
84
|
return cachedFallback(); // cloud 返回异常 → offline 兜底
|
|
@@ -331,6 +331,17 @@ function register({ sidecarLogPath } = {}) {
|
|
|
331
331
|
} catch (e) { return { ok: false, error: e.message }; }
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
+
// ⋯ menu(仅 macOS)→ 「授权容器访问 Mac」:不挂 docker,改走 SSH。colima 自带
|
|
335
|
+
// host.docker.internal 指向 Mac 主机,Mac sshd 已在 :22。把容器公钥写进 Mac 的
|
|
336
|
+
// ~/.ssh/authorized_keys(容器没 key 就先生成),并在容器里写 ~/.ssh/config 加个 `mac`
|
|
337
|
+
// 别名 → 容器内 `ssh mac` 即可上 Mac 主机跑命令。比挂 docker.sock 更通用、不碰 GID 权限。
|
|
338
|
+
ipcMain.handle("docker:app-authorize-host-ssh", async () => {
|
|
339
|
+
if (process.platform !== "darwin") return { ok: false, error: "仅 macOS 支持(容器经 host.docker.internal 访问 Mac)" };
|
|
340
|
+
try {
|
|
341
|
+
return await appDocker.authorizeHostSsh({ container: APP_CONTAINER });
|
|
342
|
+
} catch (err) { return { ok: false, error: err.message }; }
|
|
343
|
+
});
|
|
344
|
+
|
|
334
345
|
// ⋯ menu → 更新 cicy-code: pull the latest cicy-code into the container +
|
|
335
346
|
// restart it (no container recreate). Streams progress to the drawer.
|
|
336
347
|
ipcMain.handle("docker:app-update", async (e) => {
|
|
@@ -54,6 +54,13 @@ function baseImagePath() { return path.join(os.homedir(), "cicy-ai", "colima", `
|
|
|
54
54
|
const BREW_PATHS = "/opt/homebrew/bin:/usr/local/bin:/opt/homebrew/sbin:/usr/local/sbin";
|
|
55
55
|
function shEnv() { return { ...process.env, PATH: `${BREW_PATHS}:${process.env.PATH || ""}` }; }
|
|
56
56
|
|
|
57
|
+
// Homebrew 默认前缀(按 arch):Apple Silicon=/opt/homebrew,Intel=/usr/local。
|
|
58
|
+
// brew 二进制装完落在 <prefix>/bin/brew —— 已经在 BREW_PATHS 里,装完即可被 command -v 找到。
|
|
59
|
+
const BREW_PREFIX = IS_ARM ? "/opt/homebrew" : "/usr/local";
|
|
60
|
+
// Homebrew 源码 tarball:官方「Untar anywhere」装法(解压即用,不走 git clone,不需要
|
|
61
|
+
// Xcode/CLT —— colima/docker 都是预编译 bottle,无编译步骤)。可用环境变量换 CN 镜像。
|
|
62
|
+
const BREW_TARBALL = process.env.CICY_BREW_TARBALL || "https://github.com/Homebrew/brew/tarball/master";
|
|
63
|
+
|
|
57
64
|
// 在 Mac 宿主跑一条 bash 命令(execFile,无壳注入;命令串原样交给 bash -lc)。
|
|
58
65
|
function sh(cmd, { timeout = 60000 } = {}) {
|
|
59
66
|
return new Promise((resolve, reject) => {
|
|
@@ -101,14 +108,61 @@ function brewInstalled() { return hasCmd("brew"); }
|
|
|
101
108
|
function colimaInstalled() { return hasCmd("colima"); }
|
|
102
109
|
function dockerCliInstalled(){ return hasCmd("docker"); }
|
|
103
110
|
|
|
104
|
-
// Homebrew
|
|
105
|
-
//
|
|
111
|
+
// 自动装 Homebrew —— 纯 App 内,不甩给用户一条命令、也不假装在装。
|
|
112
|
+
// 难点:GUI 启动的 Electron 没有 tty,sudo 弹不出密码框。所以唯一的特权操作(建前缀 +
|
|
113
|
+
// chown 给当前用户)用 osascript「with administrator privileges」弹一次原生系统密码框完成;
|
|
114
|
+
// 之后用官方「Untar anywhere」把 brew 解压进自己拥有的前缀,全程不再需要 sudo。
|
|
115
|
+
async function autoInstallBrew({ emit } = {}) {
|
|
116
|
+
const user = os.userInfo().username;
|
|
117
|
+
|
|
118
|
+
// 1) 一次性提权:建前缀 + chown(弹系统密码框)。AppleScript 写临时文件再 osascript,
|
|
119
|
+
// 避开 bash→osascript→shell 三层嵌套引号。
|
|
120
|
+
emit && emit({ phase: "install-docker", status: "running",
|
|
121
|
+
message: "安装 Homebrew:即将弹出系统密码框,请输入你的 Mac 登录密码授权…" });
|
|
122
|
+
const scpt = path.join(os.tmpdir(), "cicy-brew-prep.applescript");
|
|
123
|
+
fs.writeFileSync(scpt,
|
|
124
|
+
`do shell script "mkdir -p '${BREW_PREFIX}' && chown -R '${user}':admin '${BREW_PREFIX}'" ` +
|
|
125
|
+
`with administrator privileges with prompt "CiCy Desktop 需要管理员权限来安装 Homebrew(只需输入一次 Mac 登录密码)"`);
|
|
126
|
+
try {
|
|
127
|
+
await sh(`osascript ${JSON.stringify(scpt)}`, { timeout: 180000 });
|
|
128
|
+
} catch (e) {
|
|
129
|
+
const why = String(e.stderr || e.message || "");
|
|
130
|
+
const cancelled = /-128|User canceled|用户.*取消/i.test(why);
|
|
131
|
+
emit && emit({ phase: "install-docker", status: "error",
|
|
132
|
+
message: cancelled ? "已取消授权 —— 安装 Homebrew 需要管理员密码,点「重试」再来一次。"
|
|
133
|
+
: `获取管理员权限失败:${why.slice(0, 160)}(点重试)` });
|
|
134
|
+
return false;
|
|
135
|
+
} finally { try { fs.unlinkSync(scpt); } catch {} }
|
|
136
|
+
|
|
137
|
+
// 2) 解压 Homebrew 进前缀(Untar anywhere,无 sudo)。brew 落在 <prefix>/bin/brew。
|
|
138
|
+
emit && emit({ phase: "install-docker", status: "running", message: "下载并解压 Homebrew…" });
|
|
139
|
+
try {
|
|
140
|
+
await shStream(
|
|
141
|
+
`curl -fL --retry 3 ${JSON.stringify(BREW_TARBALL)} | tar xz --strip-components 1 -C ${JSON.stringify(BREW_PREFIX)} 2>&1`,
|
|
142
|
+
{ emit, phase: "install-docker", timeout: 600000 });
|
|
143
|
+
} catch (e) {
|
|
144
|
+
emit && emit({ phase: "install-docker", status: "error",
|
|
145
|
+
message: `Homebrew 下载/解压失败:${String(e.stdout || e.message || "").slice(-160)}(点重试)` });
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 3) 验证能跑(顺手关掉首次 install 的隐式 auto-update,tarball 装法不是 git 仓库)。
|
|
150
|
+
try { await sh(`HOMEBREW_NO_AUTO_UPDATE=1 ${BREW_PREFIX}/bin/brew --version`, { timeout: 60000 }); }
|
|
151
|
+
catch {
|
|
152
|
+
emit && emit({ phase: "install-docker", status: "error", message: "Homebrew 解压完成但无法运行(点重试)" });
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
emit && emit({ phase: "install-docker", status: "running", message: "Homebrew 安装完成 ✅" });
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
106
159
|
async function ensureBrew({ emit } = {}) {
|
|
107
160
|
if (await brewInstalled()) return true;
|
|
161
|
+
if (await autoInstallBrew({ emit }) && (await brewInstalled())) return true;
|
|
162
|
+
// 兜底:自动装没成,给手动指引(用户自己装好后点「重试」仍可继续)。
|
|
108
163
|
emit && emit({ phase: "install-docker", status: "error",
|
|
109
|
-
message: "
|
|
110
|
-
'/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
111
|
-
"装好后回来点「重试」。" });
|
|
164
|
+
message: "Homebrew 自动安装未完成。可在「终端」手动安装后点「重试」:\n" +
|
|
165
|
+
'/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' });
|
|
112
166
|
return false;
|
|
113
167
|
}
|
|
114
168
|
|
|
@@ -116,7 +170,8 @@ async function ensureBrew({ emit } = {}) {
|
|
|
116
170
|
// Docker 引擎本体由 colima 在 VM 里提供。
|
|
117
171
|
async function installColima({ emit } = {}) {
|
|
118
172
|
emit && emit({ phase: "install-docker", status: "running", message: "通过 Homebrew 安装 Colima 与 docker CLI(几分钟,下面是实时进度)…" });
|
|
119
|
-
|
|
173
|
+
// HOMEBREW_NO_AUTO_UPDATE:tarball 装法不是 git 仓库,跳过隐式 auto-update(走 API 装 bottle)。
|
|
174
|
+
await shStream("HOMEBREW_NO_AUTO_UPDATE=1 brew install colima docker 2>&1", { emit, phase: "install-docker", timeout: 1200000 });
|
|
120
175
|
}
|
|
121
176
|
|
|
122
177
|
// ---- Colima VM 生命周期 --------------------------------------------------
|
|
@@ -474,7 +529,43 @@ async function hasGatewayKey(container = "cicy-code-docker-8009") {
|
|
|
474
529
|
catch { return false; }
|
|
475
530
|
}
|
|
476
531
|
|
|
532
|
+
// 「授权容器访问 Mac」(不挂 docker):colima 自带 host.docker.internal 指向 Mac 主机,Mac
|
|
533
|
+
// sshd 已在 :22。把容器公钥写进 Mac 的 ~/.ssh/authorized_keys(容器没 key 就先 ssh-keygen),
|
|
534
|
+
// 并在容器里写 ~/.ssh/config 加 `mac` 别名(→ host.docker.internal)→ 容器内 `ssh mac` 即可
|
|
535
|
+
// 上 Mac 主机跑命令。Electron 主进程就以 Mac 用户跑,直接 fs 写 authorized_keys;容器侧走
|
|
536
|
+
// docker exec。比挂 docker.sock 更通用,且不碰 socket 的 GID 权限那摊事。
|
|
537
|
+
async function authorizeHostSsh({ container = "cicy-code-docker-8009" } = {}) {
|
|
538
|
+
const user = os.userInfo().username; // Electron 跑在哪个 Mac 用户下 → 容器要 ssh 的目标用户
|
|
539
|
+
// 1) 容器里确保有 keypair(都没有就生成 ed25519)
|
|
540
|
+
await dk(`exec ${container} sh -c 'mkdir -p $HOME/.ssh && chmod 700 $HOME/.ssh; { [ -f $HOME/.ssh/id_ed25519 ] || [ -f $HOME/.ssh/id_rsa ]; } || ssh-keygen -t ed25519 -N "" -f $HOME/.ssh/id_ed25519 -q'`, { timeout: 30000 });
|
|
541
|
+
// 2) 读容器公钥(ed25519 + rsa 都收)
|
|
542
|
+
const { stdout } = await dk(`exec ${container} sh -c 'cat $HOME/.ssh/id_ed25519.pub $HOME/.ssh/id_rsa.pub 2>/dev/null'`, { timeout: 15000 });
|
|
543
|
+
const pubs = String(stdout).split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
544
|
+
if (!pubs.length) throw new Error("容器公钥读取失败");
|
|
545
|
+
// 3) 写进 Mac 主机 ~/.ssh/authorized_keys(去重)。主进程就是 Mac 用户,直接 fs。
|
|
546
|
+
const sshDir = path.join(os.homedir(), ".ssh");
|
|
547
|
+
fs.mkdirSync(sshDir, { recursive: true }); try { fs.chmodSync(sshDir, 0o700); } catch {}
|
|
548
|
+
const ak = path.join(sshDir, "authorized_keys");
|
|
549
|
+
let cur = ""; try { cur = fs.readFileSync(ak, "utf8"); } catch {}
|
|
550
|
+
const have = new Set(cur.split(/\r?\n/).map((l) => l.trim()));
|
|
551
|
+
const add = pubs.filter((k) => !have.has(k));
|
|
552
|
+
if (add.length) fs.appendFileSync(ak, (cur && !cur.endsWith("\n") ? "\n" : "") + add.join("\n") + "\n");
|
|
553
|
+
try { fs.chmodSync(ak, 0o600); } catch {}
|
|
554
|
+
// 4) 容器里写 ~/.ssh/config 加 `mac` 别名(base64 传,免三层嵌套引号)
|
|
555
|
+
const cfg = `Host mac\n HostName host.docker.internal\n User ${user}\n StrictHostKeyChecking accept-new\n ConnectTimeout 10\n`;
|
|
556
|
+
const b64 = Buffer.from(cfg).toString("base64");
|
|
557
|
+
await dk(`exec ${container} sh -c 'echo ${b64} | base64 -d > $HOME/.ssh/config && chmod 600 $HOME/.ssh/config'`, { timeout: 15000 });
|
|
558
|
+
// 5) 端到端验证:容器内 ssh mac 跑一条
|
|
559
|
+
let verified = false, detail = "";
|
|
560
|
+
try {
|
|
561
|
+
const v = await dk(`exec ${container} ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 mac 'echo CICY_SSH_OK; hostname'`, { timeout: 25000 });
|
|
562
|
+
detail = String(v.stdout || "").trim();
|
|
563
|
+
verified = /CICY_SSH_OK/.test(detail);
|
|
564
|
+
} catch (e) { detail = String(e.stderr || e.stdout || e.message || "").trim().slice(-200); }
|
|
565
|
+
return { ok: true, user, added: add.length, verified, detail };
|
|
566
|
+
}
|
|
567
|
+
|
|
477
568
|
module.exports = {
|
|
478
569
|
bootstrap, status, restart, stop, dockerRestart, recreate, update, upgrade, runContainer, readContainerToken,
|
|
479
|
-
vmExists, colimaInstalled, dockerCliInstalled, engineUp, imagePresent, probeHealth, hasGatewayKey,
|
|
570
|
+
vmExists, colimaInstalled, dockerCliInstalled, engineUp, imagePresent, probeHealth, hasGatewayKey, authorizeHostSsh,
|
|
480
571
|
};
|
|
@@ -1896,6 +1896,20 @@ function DockerCard({ dockerTeam, cloudTitle, onOpen, onRename, onRefresh }) {
|
|
|
1896
1896
|
} finally { setBusy(""); checkStatus(); }
|
|
1897
1897
|
}, [checkStatus]);
|
|
1898
1898
|
|
|
1899
|
+
// 「授权容器访问 Mac」(仅 macOS,不挂 docker):把容器公钥写进 Mac 的 authorized_keys +
|
|
1900
|
+
// 容器里写 ssh config 的 `mac` 别名,之后容器内 `ssh mac` 即可访问 Mac 主机跑命令。
|
|
1901
|
+
const authorizeHostSsh = useCallback(async () => {
|
|
1902
|
+
setMenuOpen(false); setBusy("authssh");
|
|
1903
|
+
toast.show({ id: "docker-op", message: tr("docker.authorizingHost", "授权容器访问 Mac(配置 SSH)…"), status: "running" });
|
|
1904
|
+
try {
|
|
1905
|
+
const r = await window.cicy.docker.appAuthorizeHostSsh();
|
|
1906
|
+
if (r?.ok) toast.show({ id: "docker-op", message: r.verified ? tr("docker.authorizedHostOk", "已授权:容器内 `ssh mac` 可访问 Mac ✅") : tr("docker.authorizedHostPartial", "公钥已写入 Mac(未验证成功,可在容器内手动 `ssh mac` 试)"), status: r.verified ? "done" : "error", ttl: 5000 });
|
|
1907
|
+
else toast.show({ id: "docker-op", message: (r?.error || tr("docker.opFailed", "操作失败")), status: "error", ttl: 6000 });
|
|
1908
|
+
} catch (e) {
|
|
1909
|
+
toast.show({ id: "docker-op", message: e.message, status: "error", ttl: 6000 });
|
|
1910
|
+
} finally { setBusy(""); checkStatus(); }
|
|
1911
|
+
}, [checkStatus]);
|
|
1912
|
+
|
|
1899
1913
|
// Render on Windows (WSL2) and macOS (Colima) — the two platforms the
|
|
1900
1914
|
// Docker-版 backend supports. window.cicy.platform is sync, so we can decide
|
|
1901
1915
|
// immediately without waiting on the async appStatus probe.
|
|
@@ -2024,8 +2038,16 @@ function DockerCard({ dockerTeam, cloudTitle, onOpen, onRename, onRefresh }) {
|
|
|
2024
2038
|
onClick={() => { setMenuOpen(false); openCloudPage(dockerTeam?.cloud_team_id ? `?team=${encodeURIComponent(dockerTeam.cloud_team_id)}` : "?view=usage"); }}>
|
|
2025
2039
|
{tr("docker.billing", "帐单")}
|
|
2026
2040
|
</button>
|
|
2027
|
-
{/* 分隔线:下面是操作整个 Docker 容器的(重启 Docker / 重建 Docker)*/}
|
|
2041
|
+
{/* 分隔线:下面是操作整个 Docker 容器的(授权访问 Mac / 重启 Docker / 重建 Docker)*/}
|
|
2028
2042
|
<div className="bcard__menu-sep" data-id="DockerCard-menu-sep" role="separator" aria-hidden />
|
|
2043
|
+
{/* 仅 macOS:授权容器经 SSH 访问 Mac 主机(host.docker.internal),不挂 docker */}
|
|
2044
|
+
{platform === "darwin" && (
|
|
2045
|
+
<button type="button" data-id="DockerCard-authorize-host-ssh" className="bcard__menu-item"
|
|
2046
|
+
title={tr("docker.authorizeHostHint", "把容器公钥加到 Mac 的 authorized_keys,容器内 `ssh mac` 即可访问本机")}
|
|
2047
|
+
onClick={authorizeHostSsh}>
|
|
2048
|
+
{tr("docker.authorizeHost", "授权容器访问 Mac")}
|
|
2049
|
+
</button>
|
|
2050
|
+
)}
|
|
2029
2051
|
<button type="button" data-id="DockerCard-docker-restart" className="bcard__menu-item"
|
|
2030
2052
|
onClick={() => runOp("restart", () => window.cicy.docker.appDockerRestart(), tr("docker.dockerRestarted", "已重启 Docker 容器"))}>
|
|
2031
2053
|
{tr("docker.dockerRestart", "重启 Docker")}
|