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.
@@ -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-Cirmfhyz.js"></script>
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 缺失:没法静默装(需要交互 + Xcode Command Line Tools)。给出明确指引,
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: "未检测到 Homebrew —— 请先在「终端」执行安装:\n" +
110
- '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"\n' +
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
- await shStream("brew install colima docker 2>&1", { emit, phase: "install-docker", timeout: 1200000 });
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")}