cicy-desktop 2.1.243 → 2.1.245

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-DAGqtb2n.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-DqFJ0g1T.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="./assets/index-C6Zrdgc0.css">
11
11
  </head>
12
12
  <body>
@@ -540,13 +540,6 @@
540
540
  this.state = "error";
541
541
  this.error = e.message;
542
542
  }
543
- // Zero-trust tunnel: tier cap (gates the "开启隧道" item) + current on/off.
544
- try {
545
- const ts = await window.cicy.sidecar.tunnelStatus();
546
- this.tunnelLimit = (ts && typeof ts.tunnelLimit === "number") ? ts.tunnelLimit : 0;
547
- const gw = await window.cicy.sidecar.getGateway();
548
- this.gwEnabled = !!(gw && gw.gateway && gw.gateway.enabled);
549
- } catch { /* keep prior tier */ }
550
543
  this.render();
551
544
  },
552
545
 
@@ -802,10 +795,6 @@
802
795
  <div class="more-item" data-act="check-update">
803
796
  ${iconUse("i-refresh","sm")} 检查更新
804
797
  </div>
805
- ${sidecarUI.tunnelLimit > 0 ? `<div class="more-divider"></div>
806
- <div class="more-item" data-act="tunnel">
807
- ${iconUse("i-open","sm")} ${sidecarUI.gwEnabled ? "隧道运行中" : "开启隧道"}
808
- </div>` : ""}
809
798
  </div>
810
799
  </div>
811
800
  <button class="open local" data-act="open" ${openDisabled ? `disabled title="${esc(disabledReason)}"` : ""}>${iconUse("i-open","sm")} Open</button>
@@ -959,26 +948,6 @@
959
948
  // Re-render local rows so the more menu reflects the new state.
960
949
  if (typeof refreshLocalRows === "function") refreshLocalRows();
961
950
  };
962
- } else if (act === "tunnel") {
963
- // Zero-trust tunnel: expose this machine's :8008 via the gateway. The
964
- // cloud returns 402 for the free tier (which uses Cloudflare directly).
965
- btn.onclick = async (e) => {
966
- e.stopPropagation();
967
- btn.closest(".more-menu").hidden = true;
968
- document.querySelectorAll(".btn-more").forEach(b => b.classList.remove("active"));
969
- if (sidecarUI.gwEnabled) { showToast("隧道已在运行"); return; }
970
- showToast("正在开启隧道…");
971
- const r = await window.cicy.sidecar.enrollTunnel(null); // null → this machine's local team
972
- if (r && r.ok) {
973
- sidecarUI.gwEnabled = true;
974
- showToast(`隧道已开启: ${r.slug ? r.slug + ".gw.cicy-ai.com" : "已连接"}`);
975
- if (typeof refreshLocalRows === "function") refreshLocalRows();
976
- } else if (r && r.status === 402) {
977
- showToast("隧道是付费功能,请升级到团队档", true);
978
- } else {
979
- showToast(`开启失败: ${(r && (r.reason || r.error)) || "unknown"}`, true);
980
- }
981
- };
982
951
  } else if (act === "update") {
983
952
  btn.onclick = (e) => {
984
953
  e.stopPropagation();
@@ -626,88 +626,13 @@ function register({ sidecarLogPath } = {}) {
626
626
  }
627
627
  });
628
628
 
629
- // 「Cloudflare Tunnel」配置: 读/写 {enabled, token, host}。set 后重启 cicy-code 让
630
- // 新的 CICY_CFT_* 环境变量生效(mac native: sidecar.restart;win: 重建容器,runContainer
631
- // 会注入 cftEnv)。全程把进度推到抽屉(op:restart)。
632
- ipcMain.handle("sidecar:get-cft", () => {
633
- try { return { ok: true, cft: sidecar.getCft() }; } catch (e) { return { ok: false, cft: { enabled: false, token: "", host: "" }, error: e.message }; }
634
- });
635
-
636
- // ── 自托管隧道 cicy-tunnel(url+token 自填面板,替代原云端 enroll)—— 流程同 cft ──────
637
- ipcMain.handle("sidecar:get-tunnel", () => {
638
- try { return { ok: true, tunnel: sidecar.getTunnel() }; } catch (e) { return { ok: false, tunnel: { enabled: false, url: "", token: "" }, error: e.message }; }
639
- });
640
629
  // 账号套餐等级(徽章用)—— 从云端取 tier,忽略 tunnels 列表;与自填隧道解耦。
641
630
  ipcMain.handle("tunnel:status", async () => {
642
631
  try { return cc ? await cc.listTunnels() : { ok: false, tunnelLimit: 0 }; } catch (e) { return { ok: false, tunnelLimit: 0, error: e.message }; }
643
632
  });
644
- // 存 url+token(desktop-flags.json)→ 重启 cicy-code(mac)/重建容器(win)让新的
645
- // CICY_TUNNEL_URL/TOKEN 生效,节点据此主动拨出到 <slug>.gw.<域名>。透明转发,不鉴权。
646
- ipcMain.handle("sidecar:set-tunnel", async (e, cfg) => {
647
- const emit = (ev) => { try { e.sender.send("sidecar:op-progress", { op: "restart", ...ev }); } catch {} };
648
- try {
649
- const saved = sidecar.setTunnel(cfg || {});
650
- emit({ phase: "swap", status: "running", message: `隧道已${saved.enabled ? "开启" : "关闭"},正在重启 cicy-code…` });
651
- if (process.platform === "win32") {
652
- try { await appDocker.recreate({ ...(await appOpts()), onProgress: emit }); } catch { try { await appDocker.dockerRestart?.({ onProgress: emit }); } catch {} }
653
- } else {
654
- await sidecar.restart({ logPath: sidecarLogPath });
655
- }
656
- let up = false;
657
- for (let i = 0; i < 240; i++) { if ((await sidecar.probeExisting(PORT)) || (await appDocker.probeHealth?.(PORT))) { up = true; break; } await new Promise((r) => setTimeout(r, 500)); }
658
- emit({ phase: "done", status: up ? "done" : "error", message: up ? `隧道已${saved.enabled ? "开启" : "关闭"}` : "重启后 :8008 未就绪——稍等或重试" });
659
- return { ok: up, tunnel: saved };
660
- } catch (err) {
661
- emit({ phase: "done", status: "error", message: `设置失败:${err.message}` });
662
- return { ok: false, error: err.message };
663
- }
664
- });
665
- ipcMain.handle("sidecar:set-cft", async (e, cfg) => {
666
- const emit = (ev) => { try { e.sender.send("sidecar:op-progress", { op: "restart", ...ev }); } catch {} };
667
- try {
668
- const saved = sidecar.setCft(cfg || {});
669
- emit({ phase: "swap", status: "running", message: `Cloudflare Tunnel 已${saved.enabled ? "开启" : "关闭"},正在重启 cicy-code…` });
670
- if (process.platform === "win32") {
671
- // Windows: recreate the container so runContainer re-injects the new
672
- // CICY_CFT_* env (recreate's progress cb is `onProgress`, not `emit`).
673
- try { await appDocker.recreate({ onProgress: emit }); } catch (err) { try { await appDocker.dockerRestart({ onProgress: emit }); } catch {} }
674
- } else {
675
- await sidecar.restart({ logPath: sidecarLogPath });
676
- }
677
- let up = false;
678
- for (let i = 0; i < 240; i++) { if (await sidecar.probeExisting(PORT) || await appDocker.probeHealth?.(PORT)) { up = true; break; } await new Promise((r) => setTimeout(r, 500)); }
679
- // 校验 token 是否有效:开启后轮询 /api/health 的 tunnel_url —— 有值 = cloudflared
680
- // 真的连上了(token 有效);超时没有 = token 无效/连不上。cft.json 是本地写的,不算数。
681
- let tunnelUrl = "";
682
- if (up && saved.enabled) {
683
- const http = require("http");
684
- emit({ phase: "swap", status: "running", message: "校验隧道连接(token 是否有效)…" });
685
- for (let i = 0; i < 40; i++) { // ~20s
686
- try {
687
- const h = await new Promise((res, rej) => {
688
- const rq = http.get(`http://127.0.0.1:${PORT}/api/health`, { timeout: 3000 }, (r) => { let b = ""; r.on("data", (d) => (b += d)); r.on("end", () => { try { res(JSON.parse(b)); } catch (er) { rej(er); } }); });
689
- rq.on("error", rej); rq.on("timeout", () => { rq.destroy(); rej(new Error("t")); });
690
- });
691
- if (h && h.tunnel_url) { tunnelUrl = h.tunnel_url; break; }
692
- } catch {}
693
- await new Promise((r) => setTimeout(r, 500));
694
- }
695
- }
696
- const tunnelUp = !!tunnelUrl;
697
- const okMsg = !saved.enabled ? "已关闭 Cloudflare Tunnel"
698
- : tunnelUp ? `隧道已连接 ✅ ${tunnelUrl}`
699
- : "cicy-code 已重启,但隧道未连上——Token 可能无效/过期,或域名未在 Cloudflare 配好路由";
700
- emit({ phase: "done", status: up && (!saved.enabled || tunnelUp) ? "done" : "error", message: up ? okMsg : "重启后 :8008 未就绪——稍等或重试" });
701
- return { ok: up, cft: saved, tunnelUp, url: tunnelUrl };
702
- } catch (err) {
703
- emit({ phase: "done", status: "error", message: `设置失败:${err.message}` });
704
- return { ok: false, error: err.message };
705
- }
706
- });
707
633
 
708
634
  // 「容器内使用 Docker」(Docker-outside-of-Docker)开关: 读/写 dood flag。set 后重建容器
709
- // 让 docker.sock + WSL 的 docker 客户端挂载生效(runContainer 内部读 dood flag)。不再下载
710
- // docker CLI(改为直接挂 WSL 现成的二进制,秒生效,避免卡在"保存中")。进度推抽屉(op:restart)。
635
+ // 让 docker.sock + WSL 的 docker 客户端挂载生效(runContainer 内部读 dood flag)
711
636
  ipcMain.handle("sidecar:get-dood", () => {
712
637
  try { return { ok: true, dood: sidecar.isDood() }; } catch (e) { return { ok: false, dood: false, error: e.message }; }
713
638
  });
@@ -279,50 +279,6 @@ function setDood(on) {
279
279
  return f.dood;
280
280
  }
281
281
 
282
- // 「Cloudflare Tunnel」配置持久化(同 desktop-flags.json 的 cft 字段):
283
- // { enabled, token, host }。enabled 且 token 非空时,start() 注入 CICY_CFT_TOKEN/
284
- // CICY_CFT_HOST 环境变量 —— cicy-code 据此跑一条命名隧道(固定域名)。host 是对外
285
- // 公布的 FQDN(可空,命名隧道的 hostname 也可在 Cloudflare 面板里配)。
286
- function getCft() {
287
- const c = readFlags().cft || {};
288
- return { enabled: !!c.enabled, token: c.token || "", host: c.host || "" };
289
- }
290
- function setCft(cfg = {}) {
291
- const f = readFlags();
292
- f.cft = { enabled: !!cfg.enabled, token: String(cfg.token || "").trim(), host: String(cfg.host || "").trim() };
293
- try { fs.mkdirSync(path.dirname(FLAGS_FILE), { recursive: true }); fs.writeFileSync(FLAGS_FILE, JSON.stringify(f, null, 2)); } catch (e) { console.warn(`[cicy-code-sidecar] setCft write failed: ${e.message}`); }
294
- return getCft();
295
- }
296
- // Env the tunnel needs when enabled (shared by native spawn + the Windows container).
297
- function cftEnv() {
298
- const c = getCft();
299
- if (!c.enabled || !c.token) return {};
300
- return { CICY_CFT_TOKEN: c.token, ...(c.host ? { CICY_CFT_HOST: c.host } : {}) };
301
- }
302
-
303
- // 自托管隧道 cicy-tunnel(替代原 cloud enroll gateway)—— 面板同 cft,就 { enabled, url, token }。
304
- // 用户从自己的 cicy-tunnel `enroll` 拿到 wss url + node token,填进来;enabled 且 url+token 非空
305
- // 时,start()/容器注入 CICY_TUNNEL_URL/TOKEN(cicy-code 据此主动拨出到隧道)。同时注入旧名
306
- // CICY_GATEWAY_* 作兼容,老 cicy-code 也能拨。Mirrors getCft/setCft/cftEnv。
307
- // 读取时兼容旧 f.gateway(老配置无缝迁移)。
308
- function getTunnel() {
309
- const c = readFlags().tunnel || readFlags().gateway || {};
310
- return { enabled: !!c.enabled, url: c.url || "", token: c.token || "" };
311
- }
312
- function setTunnel(cfg = {}) {
313
- const f = readFlags();
314
- f.tunnel = { enabled: !!cfg.enabled, url: String(cfg.url || "").trim(), token: String(cfg.token || "").trim() };
315
- delete f.gateway; // 迁移到 tunnel,清掉旧键
316
- try { fs.mkdirSync(path.dirname(FLAGS_FILE), { recursive: true }); fs.writeFileSync(FLAGS_FILE, JSON.stringify(f, null, 2)); } catch (e) { console.warn(`[cicy-code-sidecar] setTunnel write failed: ${e.message}`); }
317
- return getTunnel();
318
- }
319
- function tunnelEnv() {
320
- const c = getTunnel();
321
- if (!c.enabled || !c.url || !c.token) return {};
322
- return { CICY_TUNNEL_URL: c.url, CICY_TUNNEL_TOKEN: c.token, CICY_GATEWAY_URL: c.url, CICY_GATEWAY_TOKEN: c.token };
323
- }
324
- // 兼容别名(旧调用方)。
325
- const getGateway = getTunnel, setGateway = setTunnel, gatewayEnv = tunnelEnv;
326
282
 
327
283
 
328
284
  async function start({ logPath, port = DEFAULT_PORT, force = false, version = null, emit = null } = {}) {
@@ -385,13 +341,6 @@ async function start({ logPath, port = DEFAULT_PORT, force = false, version = nu
385
341
  // mihomo 二进制 desktop 已用 node24 预装到 runtime store(避开 node13 的 npm6 装不上的坑)→
386
342
  // 注入 MIHOMO_BIN,cicy-code 的 cicy-mihomo 包装器直接用,不再自己 npm pack。
387
343
  ...(mihomoBin ? { MIHOMO_BIN: mihomoBin } : {}),
388
- // 「Cloudflare Tunnel」开启时注入 connector token(+ 可选公网 host)→ cicy-code
389
- // 起一条命名隧道。关闭时不注入,cicy-code 不起隧道。
390
- ...cftEnv(),
391
- ...tunnelEnv(),
392
- // Zero-trust gateway (paid tiers): when a tunnel is enrolled, cicy-code dials
393
- // OUT to <slug>.gw.cicy-ai.com. Off → not injected, no gateway dial.
394
- ...gatewayEnv(),
395
344
  };
396
345
  // 「局域网访问」开关: 开 → 加 --public,cicy-code 绑 0.0.0.0(同局域网设备可访问,
397
346
  // api_token 仍把关);关 → 默认只绑 127.0.0.1。flag 存 runtime/desktop-flags.json。
@@ -603,4 +552,4 @@ async function update({ logPath, port = DEFAULT_PORT, emit } = {}) {
603
552
  }
604
553
  }
605
554
 
606
- module.exports = { start, stop, restart, update, probeExisting, clearNpxCache, isUpdating, isBusy, ensureEnv, ensureNode, isPublic, setPublicFlag, isDood, setDood, getCft, setCft, cftEnv, getTunnel, setTunnel, tunnelEnv, getGateway, setGateway, gatewayEnv };
555
+ module.exports = { start, stop, restart, update, probeExisting, clearNpxCache, isUpdating, isBusy, ensureEnv, ensureNode, isPublic, setPublicFlag, isDood, setDood };
@@ -606,13 +606,6 @@ async function runContainer({ port = 8008, container = "cicy-code-docker", volum
606
606
  if (await probeHealth(port)) { ensureDesktopShortcut(volume, port).catch(() => {}); return { adopted: true }; }
607
607
  // Replace any stale same-named container.
608
608
  try { await wslRun(`docker rm -f ${container}`, { timeout: 20000 }); } catch {}
609
- // 「Cloudflare Tunnel」开启时,把 connector token(+ 可选 host)作为容器环境变量注入
610
- // → 容器内 cicy-code 起命名隧道。配置存在宿主 desktop-flags.json,与 mac native 同源。
611
- let cftE = {}; try { cftE = require("./cicy-code").cftEnv(); } catch {}
612
- // 自托管隧道 cicy-tunnel:开启时注入 CICY_TUNNEL_URL/TOKEN(+旧 CICY_GATEWAY_* 兼容),
613
- // 容器内 cicy-code 主动拨出到隧道。配置同源 desktop-flags.json。
614
- let tunE = {}; try { tunE = require("./cicy-code").tunnelEnv(); } catch {}
615
- env = { ...(env || {}), ...cftE, ...tunE };
616
609
  const envArgs = Object.entries(env || {})
617
610
  .filter(([, v]) => v != null && v !== "")
618
611
  .map(([k, v]) => `-e ${k}='${String(v).replace(/'/g, "'\\''")}'`)
@@ -2051,9 +2051,7 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
2051
2051
  const [status, setStatus] = useState(null);
2052
2052
  const [busy, setBusy] = useState(""); // "" | bootstrap | restart | stop | upgrade | probe
2053
2053
  const [menuOpen, setMenuOpen] = useState(false);
2054
- const [cftOpen, setCftOpen] = useState(false); // Cloudflare 隧道 modal(卡片层,菜单外)
2055
2054
  const [doodOpen, setDoodOpen] = useState(false); // 容器内使用 Docker(DooD)modal(卡片层,菜单外)
2056
- const [tunnelOpen, setTunnelOpen] = useState(false); // 自托管隧道(cicy-tunnel)配置 modal(卡片层,菜单外)
2057
2055
  const [confirmRecreate, setConfirmRecreate] = useState(false); // 重建容器 in-app 确认弹窗(不用 native confirm)
2058
2056
  const [portsOpen, setPortsOpen] = useState(false); // 端口设置 modal
2059
2057
  const [portList, setPortList] = useState([]); // 编辑中的额外端口(字符串数组,便于输入)
@@ -2436,16 +2434,6 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
2436
2434
  onClick={(e) => { e.stopPropagation(); setMenuOpen(false); window.cicy?.tabs?.reloadIfOpen?.("http://127.0.0.1:8008", "Docker 团队"); }}>
2437
2435
  {tr("docker.reloadWindow", "刷新窗口")}
2438
2436
  </button>
2439
- <button type="button" data-id="DockerCard-cft" className="bcard__menu-item"
2440
- title={tr("sidecar.cftHint", "用 Cloudflare 命名隧道把 cicy-code 暴露到固定公网域名(需要 Cloudflare connector token);切换会自动重启 cicy-code")}
2441
- onClick={(e) => { e.stopPropagation(); setMenuOpen(false); setCftOpen(true); }}>
2442
- {tr("sidecar.cftMenu", "Cloudflare 隧道")}
2443
- </button>
2444
- <button type="button" data-id="DockerCard-tunnel-cfg" className="bcard__menu-item"
2445
- title={tr("tunnel.hint", "把本机 cicy-code 通过你自建的 cicy-tunnel 暴露到 <slug>.gw.<域名>(填 url+token,透明转发、不需 --public);切换会重启 cicy-code")}
2446
- onClick={(e) => { e.stopPropagation(); setMenuOpen(false); setTunnelOpen(true); }}>
2447
- {tr("tunnel.menu", "自托管隧道")}
2448
- </button>
2449
2437
  <button type="button" data-id="DockerCard-open-dir" className="bcard__menu-item"
2450
2438
  onClick={(e) => { e.stopPropagation(); setMenuOpen(false); window.cicy?.docker?.openDir?.(); }}>
2451
2439
  {tr("docker.openWslDir", "打开 WSL 目录")}
@@ -2534,8 +2522,6 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
2534
2522
  {isBusy ? <Spinner /> : <ArrowIcon />}
2535
2523
  <span>{ctaLabel}</span>
2536
2524
  </button>
2537
- <CftModal open={cftOpen} onClose={() => setCftOpen(false)} toastId="docker-op" />
2538
- <TunnelModal open={tunnelOpen} onClose={() => setTunnelOpen(false)} toastId="docker-op" />
2539
2525
  <DoodModal open={doodOpen} onClose={() => setDoodOpen(false)} toastId="docker-op" />
2540
2526
  {confirmRecreate && createPortal(
2541
2527
  <div data-id="DockerCard-recreate-modal"
@@ -2660,14 +2646,12 @@ function LocalTeamCard({ team, cloudCode, onOpen, onRename, onRefresh }) {
2660
2646
  const running = team.status === "running";
2661
2647
  const [busy, setBusy] = useState(""); // "" | start | restart | update | stop | lan
2662
2648
  const [menuOpen, setMenuOpen] = useState(false);
2663
- const [cftOpen, setCftOpen] = useState(false); // Cloudflare 隧道 modal(卡片层,菜单外)
2664
2649
  // 局域网访问开关: cicy-code --public 状态。仅本地团队;初始从 sidecar.getPublic() 读。
2665
2650
  const [lanOn, setLanOn] = useState(false);
2666
2651
  useEffect(() => {
2667
2652
  if (!local || !window.cicy?.sidecar?.getPublic) return;
2668
2653
  window.cicy.sidecar.getPublic().then((r) => setLanOn(!!r?.public)).catch(() => {});
2669
2654
  }, [local]);
2670
- const [tunnelOpen, setTunnelOpen] = useState(false); // 自托管隧道(cicy-tunnel)配置 modal(卡片层,菜单外)
2671
2655
  // cicy-code 版本统一从 sidecar.versions() 一处拿("拿版本就一个方法")。
2672
2656
  // running===undefined = 还没查到(用于区分"加载中" vs "停了/拿不到");区别于
2673
2657
  // running===null(查过了但 daemon 没报版本)。latest/installed 同源。
@@ -2980,16 +2964,6 @@ function LocalTeamCard({ team, cloudCode, onOpen, onRename, onRefresh }) {
2980
2964
  >
2981
2965
  {tr("sidecar.lanAccess", "局域网访问")} · {lanOn ? tr("common.on", "开") : tr("common.off", "关")}
2982
2966
  </button>
2983
- <button type="button" data-id="LocalTeamCard-cft" className="bcard__menu-item"
2984
- title={tr("sidecar.cftHint", "用 Cloudflare 命名隧道把本机 cicy-code 暴露到固定公网域名(需要 Cloudflare connector token);切换会自动重启 cicy-code")}
2985
- onClick={(e) => { e.stopPropagation(); setMenuOpen(false); setCftOpen(true); }}>
2986
- {tr("sidecar.cftMenu", "Cloudflare 隧道")}
2987
- </button>
2988
- <button type="button" data-id="LocalTeamCard-tunnel-cfg" className="bcard__menu-item"
2989
- title={tr("tunnel.hint", "把本机 cicy-code 通过你自建的 cicy-tunnel 暴露到 <slug>.gw.<域名>(填 url+token,透明转发、不需 --public);切换会重启 cicy-code")}
2990
- onClick={(e) => { e.stopPropagation(); setMenuOpen(false); setTunnelOpen(true); }}>
2991
- {tr("tunnel.menu", "自托管隧道")}
2992
- </button>
2993
2967
  </>
2994
2968
  )}
2995
2969
  {team.cloud_team_id && (
@@ -3112,8 +3086,6 @@ function LocalTeamCard({ team, cloudCode, onOpen, onRename, onRefresh }) {
3112
3086
  </div>,
3113
3087
  document.body,
3114
3088
  )}
3115
- <CftModal open={cftOpen} onClose={() => setCftOpen(false)} toastId={opToastId} />
3116
- <TunnelModal open={tunnelOpen} onClose={() => setTunnelOpen(false)} toastId={opToastId} />
3117
3089
  <ConfirmModal open={confirmDel}
3118
3090
  title={tr("localTeams.deleteTitle", "删除团队")}
3119
3091
  message={tr("localTeams.deleteMsg", "确定删除「{{name}}」?此操作不可撤销。", { name: team.name })}
@@ -3510,151 +3482,9 @@ const AVATAR_PALETTE = [
3510
3482
  ];
3511
3483
  function hashStr(s) { s = String(s == null ? "" : s); let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 100003; return h; }
3512
3484
  function avatarBg(key) { return AVATAR_PALETTE[hashStr(key) % AVATAR_PALETTE.length]; }
3513
- // Cloudflare 隧道 modal —— open 受控组件,**渲染在卡片层(菜单外)**,否则点菜单项关掉
3514
- // 菜单会把 modal 一起卸载(Windows 上就是这样点不开)。LocalTeamCard(mac native)和
3515
- // DockerCard(Windows 容器)共用。保存 → sidecar.setCft → cicy-code 带 CICY_CFT_TOKEN/
3516
- // CICY_CFT_HOST 重启(mac spawn env / win 容器 -e)。
3517
- function CftModal({ open, onClose, toastId = "cft-op" }) {
3518
- const [busy, setBusy] = useState(false);
3519
- const [err, setErr] = useState("");
3520
- const [cfg, setCfg] = useState({ enabled: false, token: "", host: "" });
3521
- useEffect(() => {
3522
- if (!open || !window.cicy?.sidecar?.getCft) return;
3523
- setErr("");
3524
- window.cicy.sidecar.getCft().then((r) => { if (r?.cft) setCfg({ enabled: !!r.cft.enabled, token: r.cft.token || "", host: r.cft.host || "" }); }).catch(() => {});
3525
- }, [open]);
3526
- const save = async () => {
3527
- if (busy) return;
3528
- if (cfg.enabled) {
3529
- if (!String(cfg.token).trim()) { setErr(tr("sidecar.cftNeedToken", "开启需要填 Tunnel Token")); return; }
3530
- if (!String(cfg.host).trim()) { setErr(tr("sidecar.cftNeedHost", "公网域名 Host 不能为空")); return; }
3531
- }
3532
- setBusy(true); setErr("");
3533
- toast.show({ id: toastId, message: tr("sidecar.cftApplying", "应用 Cloudflare Tunnel,重启中…"), status: "running", progress: undefined });
3534
- try {
3535
- const r = await window.cicy.sidecar.setCft(cfg);
3536
- if (r?.ok) {
3537
- if (r.cft) setCfg(r.cft);
3538
- if (r.cft?.enabled && !r.tunnelUp) {
3539
- // 重启成功但隧道没连上 = token 无效/连不上,留在弹窗报错,别静默关掉
3540
- setErr(tr("sidecar.cftTunnelDown", "隧道未连上:Token 可能无效/过期,或域名未在 Cloudflare 配好路由"));
3541
- toast.show({ id: toastId, message: tr("sidecar.cftTunnelDown", "隧道未连上:Token 可能无效…"), status: "error", ttl: 7000 });
3542
- } else {
3543
- onClose && onClose();
3544
- toast.show({ id: toastId, message: r.cft?.enabled ? (tr("sidecar.cftConnected", "隧道已连接") + (r.url ? `: ${r.url}` : "")) : tr("sidecar.cftOff", "Cloudflare Tunnel 已关闭"), status: "done", ttl: 4000 });
3545
- }
3546
- }
3547
- else { setErr(r?.error || tr("sidecar.cftFailed", "设置失败")); toast.show({ id: toastId, message: tr("sidecar.cftFailed", "设置失败") + (r?.error ? `: ${r.error}` : ""), status: "error", ttl: 6000 }); }
3548
- } catch (e) { setErr(e?.message || String(e)); toast.show({ id: toastId, message: tr("sidecar.cftFailed", "设置失败") + `: ${e?.message || e}`, status: "error", ttl: 6000 }); }
3549
- finally { setBusy(false); }
3550
- };
3551
- const setOpen = (v) => { if (!v) onClose && onClose(); }; // modal 内部沿用 setOpen(false) 关闭
3552
- if (!open) return null;
3553
- return (
3554
- <>
3555
- {createPortal(
3556
- <div data-id="cft-modal"
3557
- style={{ position: "fixed", inset: 0, zIndex: 66, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,.5)" }}
3558
- onMouseDown={(e) => { if (!busy && e.target === e.currentTarget) setOpen(false); }}>
3559
- <div onClick={(e) => e.stopPropagation()}
3560
- style={{ width: 440, maxWidth: "92vw", background: "var(--card, #1b1d22)", border: "1px solid var(--border, #2c2f36)", borderRadius: 14, padding: 22, boxShadow: "0 20px 60px rgba(0,0,0,.5)" }}>
3561
- <div style={{ fontSize: 16, fontWeight: 700, marginBottom: 4 }}>{tr("sidecar.cftTitle", "Cloudflare 隧道")}</div>
3562
- <div style={{ fontSize: 12, opacity: .6, marginBottom: 16 }}>{tr("sidecar.cftSubtitle", "把本机 cicy-code 通过 Cloudflare 命名隧道暴露到固定公网域名")}</div>
3563
- <label data-id="cft-toggle-row" style={{ display: "flex", alignItems: "center", gap: 10, cursor: busy ? "default" : "pointer", marginBottom: 16 }}>
3564
- <input type="checkbox" data-id="cft-toggle" checked={cfg.enabled} disabled={busy}
3565
- onChange={(e) => setCfg((c) => ({ ...c, enabled: e.target.checked }))} />
3566
- <span style={{ fontSize: 14, fontWeight: 600 }}>{tr("sidecar.cftEnable", "开启隧道")}</span>
3567
- </label>
3568
- <div style={{ fontSize: 12, opacity: .7, marginBottom: 6 }}>{tr("sidecar.cftToken", "Tunnel Token")}</div>
3569
- <textarea data-id="cft-token" rows={3} className="login-email-input" style={{ width: "100%", resize: "vertical", lineHeight: 1.4, fontFamily: "var(--mono)", wordBreak: "break-all" }}
3570
- value={cfg.token} placeholder="eyJhIjoi…(粘贴 Cloudflare connector token)" spellCheck={false} disabled={busy}
3571
- onChange={(e) => setCfg((c) => ({ ...c, token: e.target.value.replace(/\s+/g, "") }))} />
3572
- <div style={{ fontSize: 12, opacity: .7, margin: "12px 0 6px" }}>{tr("sidecar.cftHost", "公网域名 Host")}</div>
3573
- <input data-id="cft-host" className="login-email-input" style={{ width: "100%", fontFamily: "var(--mono)" }}
3574
- value={cfg.host} placeholder="cloudshell.cicy-ai.com" spellCheck={false} disabled={busy}
3575
- onChange={(e) => setCfg((c) => ({ ...c, host: e.target.value.replace(/[\r\n\s]+/g, "").trim() }))}
3576
- onKeyDown={(e) => { if (e.key === "Escape" && !busy) setOpen(false); }} />
3577
- {err && <div className="error" style={{ marginTop: 8, fontSize: 12 }}>{err}</div>}
3578
- <div className="modal-actions">
3579
- <button type="button" className="btn-ghost" disabled={busy} onClick={() => setOpen(false)}>{tr("common.cancel", "取消")}</button>
3580
- <button type="button" className="btn-primary" data-id="cft-save" disabled={busy} onClick={save}>{busy ? tr("common.saving", "保存中…") : tr("common.save", "保存")}</button>
3581
- </div>
3582
- </div>
3583
- </div>,
3584
- document.body,
3585
- )}
3586
- </>
3587
- );
3588
- }
3589
3485
  // 容器内使用 Docker(Docker-outside-of-Docker)modal —— 单 checkbox,像 CftModal 一样渲染在
3590
3486
  // 卡片层(菜单外)。开启 → sidecar.setDood → 重建容器挂 docker.sock + 把 docker CLI 装进容器
3591
3487
  // 持久卷,下载进度实时显示在弹窗里。DockerCard(Windows 容器)用。
3592
- // 自托管隧道 cicy-tunnel modal —— url + token + 开关,和 CftModal 一模一样(渲染在卡片层)。
3593
- // 保存 → sidecar.setTunnel → cicy-code 带 CICY_TUNNEL_URL/TOKEN 重启,主动拨出到隧道。透明转发。
3594
- function TunnelModal({ open, onClose, toastId = "tunnel-op" }) {
3595
- const [busy, setBusy] = useState(false);
3596
- const [err, setErr] = useState("");
3597
- const [cfg, setCfg] = useState({ enabled: false, url: "", token: "" });
3598
- useEffect(() => {
3599
- if (!open || !window.cicy?.sidecar?.getTunnel) return;
3600
- setErr("");
3601
- window.cicy.sidecar.getTunnel().then((r) => { if (r?.tunnel) setCfg({ enabled: !!r.tunnel.enabled, url: r.tunnel.url || "", token: r.tunnel.token || "" }); }).catch(() => {});
3602
- }, [open]);
3603
- const save = async () => {
3604
- if (busy) return;
3605
- if (cfg.enabled) {
3606
- if (!String(cfg.url).trim()) { setErr(tr("tunnel.needUrl", "开启需要填隧道地址(wss://…/_tunnel/connect)")); return; }
3607
- if (!String(cfg.token).trim()) { setErr(tr("tunnel.needToken", "开启需要填节点 token")); return; }
3608
- }
3609
- setBusy(true); setErr("");
3610
- toast.show({ id: toastId, message: tr("tunnel.applying", "应用隧道配置,重启 cicy-code 中…"), status: "running", progress: undefined });
3611
- try {
3612
- const r = await window.cicy.sidecar.setTunnel(cfg);
3613
- if (r?.ok) {
3614
- if (r.tunnel) setCfg(r.tunnel);
3615
- onClose && onClose();
3616
- toast.show({ id: toastId, message: cfg.enabled ? tr("tunnel.on", "隧道已开启") : tr("tunnel.off", "隧道已关闭"), status: "done", ttl: 4000 });
3617
- } else { setErr(r?.error || tr("tunnel.failed", "设置失败")); toast.show({ id: toastId, message: tr("tunnel.failed", "设置失败") + (r?.error ? `: ${r.error}` : ""), status: "error", ttl: 6000 }); }
3618
- } catch (e) { setErr(e?.message || String(e)); toast.show({ id: toastId, message: tr("tunnel.failed", "设置失败") + `: ${e?.message || e}`, status: "error", ttl: 6000 }); }
3619
- finally { setBusy(false); }
3620
- };
3621
- const setOpen = (v) => { if (!v) onClose && onClose(); };
3622
- if (!open) return null;
3623
- return (
3624
- <>
3625
- {createPortal(
3626
- <div data-id="tunnel-modal"
3627
- style={{ position: "fixed", inset: 0, zIndex: 66, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,.5)" }}
3628
- onMouseDown={(e) => { if (!busy && e.target === e.currentTarget) setOpen(false); }}>
3629
- <div onClick={(e) => e.stopPropagation()}
3630
- style={{ width: 440, maxWidth: "92vw", background: "var(--card, #1b1d22)", border: "1px solid var(--border, #2c2f36)", borderRadius: 14, padding: 22, boxShadow: "0 20px 60px rgba(0,0,0,.5)" }}>
3631
- <div style={{ fontSize: 16, fontWeight: 700, marginBottom: 4 }}>{tr("tunnel.title", "自托管隧道 · cicy-tunnel")}</div>
3632
- <div style={{ fontSize: 12, opacity: .6, marginBottom: 16 }}>{tr("tunnel.subtitle", "把本机 cicy-code 通过你自建的 cicy-tunnel 暴露到 <slug>.gw.<域名>,透明转发。url+token 从隧道 enroll 拿。")}</div>
3633
- <label data-id="tunnel-toggle-row" style={{ display: "flex", alignItems: "center", gap: 10, cursor: busy ? "default" : "pointer", marginBottom: 16 }}>
3634
- <input type="checkbox" data-id="tunnel-toggle" checked={cfg.enabled} disabled={busy}
3635
- onChange={(e) => setCfg((c) => ({ ...c, enabled: e.target.checked }))} />
3636
- <span style={{ fontSize: 14, fontWeight: 600 }}>{tr("tunnel.enable", "开启隧道")}</span>
3637
- </label>
3638
- <div style={{ fontSize: 12, opacity: .7, marginBottom: 6 }}>{tr("tunnel.url", "隧道地址 URL")}</div>
3639
- <input data-id="tunnel-url" className="login-email-input" style={{ width: "100%", fontFamily: "var(--mono)" }}
3640
- value={cfg.url} placeholder="wss://my-mac.gw.example.com/_tunnel/connect" spellCheck={false} disabled={busy}
3641
- onChange={(e) => setCfg((c) => ({ ...c, url: e.target.value.replace(/[\r\n\s]+/g, "").trim() }))} />
3642
- <div style={{ fontSize: 12, opacity: .7, margin: "12px 0 6px" }}>{tr("tunnel.token", "节点 Token")}</div>
3643
- <textarea data-id="tunnel-token" rows={3} className="login-email-input" style={{ width: "100%", resize: "vertical", lineHeight: 1.4, fontFamily: "var(--mono)", wordBreak: "break-all" }}
3644
- value={cfg.token} placeholder={tr("tunnel.tokenPh", "粘贴 enroll 输出的 token")} spellCheck={false} disabled={busy}
3645
- onChange={(e) => setCfg((c) => ({ ...c, token: e.target.value.replace(/\s+/g, "") }))} />
3646
- {err && <div className="error" style={{ marginTop: 8, fontSize: 12 }}>{err}</div>}
3647
- <div className="modal-actions">
3648
- <button type="button" className="btn-ghost" disabled={busy} onClick={() => setOpen(false)}>{tr("common.cancel", "取消")}</button>
3649
- <button type="button" className="btn-primary" data-id="tunnel-save" disabled={busy} onClick={save}>{busy ? tr("common.saving", "保存中…") : tr("common.save", "保存")}</button>
3650
- </div>
3651
- </div>
3652
- </div>,
3653
- document.body,
3654
- )}
3655
- </>
3656
- );
3657
- }
3658
3488
  function DoodModal({ open, onClose, toastId = "dood-op" }) {
3659
3489
  const [busy, setBusy] = useState(false);
3660
3490
  const [err, setErr] = useState("");