cicy-desktop 2.1.42 → 2.1.44

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>CiCy Desktop</title>
7
- <script type="module" crossorigin src="./assets/index-BhLjpIIu.js"></script>
7
+ <script type="module" crossorigin src="./assets/index-BqRSij9W.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="./assets/index-CNVsvsZX.css">
9
9
  </head>
10
10
  <body>
@@ -1,26 +1,26 @@
1
1
  // Homepage window — primary CiCy Desktop window. Singleton; closing it
2
2
  // does NOT quit the app.
3
3
  //
4
- // URL selection:
5
- // 1. CICY_HOMEPAGE_URL env (Vite dev server)wins everywhere if set;
6
- // if the URL fails to load (tunnel down, server not running) the window
7
- // automatically falls back to the local file:// SPA.
8
- // 2. Windows: remote https://desktop.cicy-ai.comkeeps Win shipping
9
- // without rebuilding (Win release cadence is slower than render's)
10
- // 3. Mac/Linux: bundled local SPA file:// works offline, no mixed-
11
- // content concerns when embedding the team-assistant webview
12
- // (the cicy-desktop preload's IPC bridge still attaches).
4
+ // URL selection (HARDCODED — no env/dev-URL switch, no Vite):
5
+ // - Windows: the remote Cloudflare worker https://desktop.cicy-ai.com keeps
6
+ // Win shipping without rebuilding (Win release cadence is slower than
7
+ // render's). Falls back to the local file:// SPA if unreachable.
8
+ // - Mac/Linux: the bundled local SPA file:// — works offline, fast, no remote
9
+ // dependency, no mixed-content concerns when embedding the team-assistant
10
+ // webview (the cicy-desktop preload's IPC bridge still attaches).
13
11
 
14
12
  const path = require("path");
15
13
  const { BrowserWindow } = require("electron");
16
14
  const log = require("electron-log");
17
15
 
18
- const REMOTE_URL = "https://desktop.cicy-ai.com/";
19
- const DEV_URL = process.env.CICY_HOMEPAGE_URL || "";
16
+ // Hardcoded homepage source — no Vite / CICY_HOMEPAGE_URL dev-URL switch.
17
+ // - mac/linux: the bundled file:// SPA (offline, fast, no remote dependency).
18
+ // - Windows: the remote Cloudflare worker (Win release cadence is slower
19
+ // than render's, so it loads the homepage remotely).
20
+ const REMOTE_URL = "https://desktop.cicy-ai.com/"; // Cloudflare worker (remote homepage)
20
21
  const LOCAL_INDEX = path.join(__dirname, "homepage-react", "index.html");
21
22
 
22
23
  function pickHomepageURL() {
23
- if (DEV_URL) return DEV_URL;
24
24
  if (process.platform === "win32") return REMOTE_URL;
25
25
  return `file://${LOCAL_INDEX}`;
26
26
  }
@@ -88,8 +88,10 @@ async function openHomepage() {
88
88
  homepage.webContents.on("did-fail-load", (_e, code, desc, url) => {
89
89
  console.error(`[homepage] did-fail-load ${code} ${desc} ${url}`);
90
90
  const fallback = `file://${LOCAL_INDEX}`;
91
- if (url && DEV_URL && url.startsWith(DEV_URL.replace(/\/$/, "")) && url !== fallback) {
92
- log.warn(`[homepage] dev URL unreachable, falling back to ${fallback}`);
91
+ // If the remote (Windows) Cloudflare homepage is unreachable, fall back to
92
+ // the bundled local SPA so the window never stays blank.
93
+ if (url && url.startsWith(REMOTE_URL.replace(/\/$/, "")) && url !== fallback) {
94
+ log.warn(`[homepage] remote homepage unreachable, falling back to ${fallback}`);
93
95
  homepage.loadURL(fallback);
94
96
  }
95
97
  });
package/src/main.js CHANGED
@@ -547,17 +547,29 @@ function ensureWindowsDesktopLauncher() {
547
547
  // Honors `prefs.openAtLogin` if present; defaults to true on first run.
548
548
  function ensureAutoLaunch() {
549
549
  try {
550
- if (!electronApp.isPackaged) return; // dev mode: don't touch login items
551
550
  const prefs = readPrefs();
552
551
  const want = prefs.openAtLogin !== false; // default true
553
- if (process.platform === "darwin" || process.platform === "win32") {
552
+
553
+ if (process.platform === "darwin") {
554
+ // mac: register the STABLE Desktop applet as a login item — works for
555
+ // npx/global installs (isPackaged=false), not just packaged .apps. We
556
+ // register the applet (a fixed path that internally runs the global
557
+ // cicy-desktop bin) instead of process.execPath, which is a transient
558
+ // electron path that breaks on every version/cache change (the old auto-
559
+ // start bug). Skip a pure source checkout so devs aren't auto-added.
560
+ const installed = electronApp.isPackaged ||
561
+ __dirname.includes(`${path.sep}node_modules${path.sep}`);
562
+ if (!installed) return;
563
+ ensureMacLoginItem(want);
564
+ return;
565
+ }
566
+
567
+ // win/linux: unchanged — only manage login items for packaged builds.
568
+ if (!electronApp.isPackaged) return;
569
+ if (process.platform === "win32") {
554
570
  const cur = electronApp.getLoginItemSettings();
555
571
  if (cur.openAtLogin !== want) {
556
- electronApp.setLoginItemSettings({
557
- openAtLogin: want,
558
- // Windows: pass --hidden so the app starts to the tray, not foreground.
559
- args: process.platform === "win32" ? ["--hidden"] : undefined,
560
- });
572
+ electronApp.setLoginItemSettings({ openAtLogin: want, args: ["--hidden"] });
561
573
  log.info(`[autostart] openAtLogin → ${want}`);
562
574
  }
563
575
  } else if (process.platform === "linux") {
@@ -568,6 +580,28 @@ function ensureAutoLaunch() {
568
580
  }
569
581
  }
570
582
 
583
+ // mac login item pointing at the Desktop applet (~/Desktop/CiCy Desktop.app).
584
+ // The applet is created by the launcher (bin/cicy-desktop ensureMacDesktopApp)
585
+ // BEFORE electron spawns, so it exists by the time this runs. Idempotent:
586
+ // always clears a stale entry first, then (re)adds when wanted.
587
+ function ensureMacLoginItem(want) {
588
+ const name = "CiCy Desktop";
589
+ const appletPath = path.join(os.homedir(), "Desktop", "CiCy Desktop.app");
590
+ try {
591
+ const { execFileSync } = require("child_process");
592
+ const osa = (script) => execFileSync("osascript", ["-e", script], { stdio: "ignore" });
593
+ osa(`tell application "System Events" to if login item "${name}" exists then delete login item "${name}"`);
594
+ if (want && fs.existsSync(appletPath)) {
595
+ osa(`tell application "System Events" to make login item at end with properties {name:"${name}", path:"${appletPath}", hidden:false}`);
596
+ log.info(`[autostart] mac login item → ${appletPath}`);
597
+ } else {
598
+ log.info(`[autostart] mac login item ${want ? "skipped (applet missing)" : "removed"}`);
599
+ }
600
+ } catch (e) {
601
+ log.warn(`[autostart] mac login item failed: ${e.message}`);
602
+ }
603
+ }
604
+
571
605
  function readPrefs() {
572
606
  try {
573
607
  const p = path.join(electronApp.getPath("userData"), "prefs.json");
@@ -669,6 +703,10 @@ electronApp.whenReady().then(async () => {
669
703
  backendsIPC.register({ sidecarLogPath: path.join(os.homedir(), "logs", "cicy-code-sidecar.log") });
670
704
  require("./backends/sidecar-ipc").register({ sidecarLogPath: path.join(os.homedir(), "logs", "cicy-code-sidecar.log") });
671
705
 
706
+ // window.cicy.artifact bridge — CDP/webContents control of the cicy-code
707
+ // 产物 (artifact) <webview> guest. Injected renderer-side in window-utils.js.
708
+ require("./backends/artifact-ipc").register();
709
+
672
710
  // Browser-login loopback listener. Renderer calls auth:login-start when
673
711
  // the user clicks Login; main opens a 127.0.0.1 server + the browser,
674
712
  // and broadcasts auth:complete back to the homepage window once the
@@ -676,10 +714,38 @@ electronApp.whenReady().then(async () => {
676
714
  {
677
715
  const auth = require("./backends/auth-loopback");
678
716
  const { ipcMain: __ipcMainAuth } = require("electron");
717
+ const { readGlobalConfig, updateGlobalConfig } = require("./utils/global-json");
718
+ const GLOBAL_JSON = path.join(os.homedir(), "cicy-ai", "global.json");
719
+
720
+ // Persist the cloud login durably in the MAIN process (global.json),
721
+ // independent of the homepage renderer's origin. The renderer keeps the
722
+ // token in localStorage, which Chromium scopes to the homepage window's
723
+ // origin — and that origin drifts (file:// on mac, https://desktop.cicy-ai.com
724
+ // on Windows, http://<ip>:port or the team domain when CICY_HOMEPAGE_URL is
725
+ // set). A token saved under one origin is invisible after the URL changes,
726
+ // which forced the user to log in again and again. Storing it here and
727
+ // restoring it on every homepage load makes "logged in once" survive origin
728
+ // changes, restarts and public-URL switches. ONLY explicit logout clears it.
729
+ const saveDesktopAuth = (p) => {
730
+ try {
731
+ updateGlobalConfig(GLOBAL_JSON, (c) => {
732
+ c.desktopAuth = {
733
+ token: p.token || "",
734
+ accessToken: p.accessToken || "",
735
+ userId: p.userId != null ? String(p.userId) : "",
736
+ savedAt: Date.now(),
737
+ };
738
+ return c;
739
+ });
740
+ log.info("[auth] desktop login persisted to global.json (origin-independent)");
741
+ } catch (e) { log.warn(`[auth] persist failed: ${e.message}`); }
742
+ };
743
+
679
744
  __ipcMainAuth.handle("auth:login-start", async () => {
680
745
  try {
681
746
  await auth.startLogin({
682
747
  onResult: (payload) => {
748
+ if (payload && payload.token) saveDesktopAuth(payload);
683
749
  const hw = require("./backends/homepage-window");
684
750
  const w = hw.getHomepageWindow && hw.getHomepageWindow();
685
751
  if (w && !w.isDestroyed()) {
@@ -694,6 +760,29 @@ electronApp.whenReady().then(async () => {
694
760
  }
695
761
  });
696
762
  __ipcMainAuth.handle("auth:login-cancel", () => { auth.cancel(); return { ok: true }; });
763
+
764
+ // Origin-independent restore. The homepage SPA calls this on mount; if its
765
+ // own (origin-scoped) localStorage has no token, it adopts this one — so a
766
+ // homepage URL/origin change never forces a needless re-login.
767
+ __ipcMainAuth.handle("auth:get-saved", () => {
768
+ try {
769
+ const c = readGlobalConfig(GLOBAL_JSON);
770
+ const a = c && c.desktopAuth;
771
+ if (a && a.token) {
772
+ return { token: a.token, accessToken: a.accessToken || "", userId: a.userId || "" };
773
+ }
774
+ } catch (e) { log.warn(`[auth] get-saved failed: ${e.message}`); }
775
+ return null;
776
+ });
777
+
778
+ // Explicit logout is the ONLY thing that clears the durable store.
779
+ __ipcMainAuth.handle("auth:logout", () => {
780
+ try {
781
+ updateGlobalConfig(GLOBAL_JSON, (c) => { delete c.desktopAuth; return c; });
782
+ log.info("[auth] desktop login cleared (explicit logout)");
783
+ } catch (e) { log.warn(`[auth] logout clear failed: ${e.message}`); }
784
+ return { ok: true };
785
+ });
697
786
  }
698
787
 
699
788
  // Local-team discovery — reads ~/cicy-ai/global.json's cicyDesktopNodes
@@ -75,6 +75,44 @@ function setupWindowHandlers(win) {
75
75
  console.log('[RPC] electronRPC ready');
76
76
  } catch(e) {}
77
77
  }
78
+ // window.cicy.artifact — remote control of the 产物 (artifact) <webview>
79
+ // guest webContents for cicy-code's artifactBridge.ts. Targets the
80
+ // element id 'cicy-artifact-webview'; round-trips to artifact-ipc.js.
81
+ (function(){
82
+ try {
83
+ if (window.cicy && window.cicy.artifact) return;
84
+ const { ipcRenderer } = require('electron');
85
+ window.cicy = window.cicy || {};
86
+ const guestId = () => {
87
+ const el = document.getElementById('cicy-artifact-webview');
88
+ if (!el || typeof el.getWebContentsId !== 'function')
89
+ throw new Error('artifact webview not mounted (open the 产物 tab once)');
90
+ return el.getWebContentsId();
91
+ };
92
+ let _attached = false;
93
+ window.cicy.artifact = {
94
+ invoke: (method, args) =>
95
+ ipcRenderer.invoke('artifact:invoke', { guestId: guestId(), method, args: args || [] }),
96
+ cdp: {
97
+ attach: (protocolVersion) =>
98
+ ipcRenderer.invoke('artifact:cdp-attach', { guestId: guestId(), protocolVersion })
99
+ .then((r) => { _attached = true; return r; }),
100
+ detach: () =>
101
+ ipcRenderer.invoke('artifact:cdp-detach', { guestId: guestId() })
102
+ .then((r) => { _attached = false; return r; }),
103
+ isAttached: () => _attached,
104
+ send: (method, params) =>
105
+ ipcRenderer.invoke('artifact:cdp-send', { guestId: guestId(), method, params: params || {} }),
106
+ },
107
+ };
108
+ try { ipcRenderer.removeAllListeners('artifact:event'); } catch(e) {}
109
+ ipcRenderer.on('artifact:event', (_e, detail) => {
110
+ if (detail && detail.source === 'cdp' && detail.method === '__detached') _attached = false;
111
+ try { window.dispatchEvent(new CustomEvent('cicy-artifact-event', { detail: detail })); } catch(e) {}
112
+ });
113
+ console.log('[artifact] window.cicy.artifact ready');
114
+ } catch(e) { console.error('[artifact] bridge inject failed', e && e.message); }
115
+ })();
78
116
  `;
79
117
  if (win.webContents.debugger.isAttached()) {
80
118
  await win.webContents.debugger.sendCommand("Runtime.evaluate", { expression: rpcCode });
@@ -34,6 +34,9 @@ export default function App() {
34
34
  // Required as `New-Api-User: <id>` header on every console-API call —
35
35
  // middleware.UserAuth() rejects requests without it.
36
36
  const [userId, setUserId] = useState(() => safeGet(USER_ID_KEY));
37
+ // True while we ask main for a durably-saved login (origin-independent).
38
+ // Prevents the login card from flashing on every launch before restore.
39
+ const [authRestoring, setAuthRestoring] = useState(() => !safeGet(TOKEN_KEY));
37
40
  const [loggingIn, setLoggingIn] = useState(false);
38
41
  const [error, setError] = useState("");
39
42
  const [welcome, setWelcome] = useState("");
@@ -338,6 +341,38 @@ export default function App() {
338
341
  // happens INSIDE the agent via `agent-webpage exec-js navigator.language`
339
342
  // against the same client — see AGENTS.md.)
340
343
 
344
+ // Restore login from the main-process durable store when THIS origin's
345
+ // localStorage has none. The homepage origin drifts (file:// / the team
346
+ // domain / an IP:port), and localStorage is origin-scoped, so without this a
347
+ // token saved under a previous origin would force a needless re-login. Main
348
+ // persists the login origin-independently (global.json); we adopt it here so
349
+ // "logged in once" stays valid until an explicit logout.
350
+ useEffect(() => {
351
+ if (safeGet(TOKEN_KEY)) { setAuthRestoring(false); return; }
352
+ if (!window.cicy?.auth?.getSaved) { setAuthRestoring(false); return; }
353
+ let cancelled = false;
354
+ (async () => {
355
+ try {
356
+ const saved = await window.cicy.auth.getSaved();
357
+ if (cancelled) return;
358
+ if (saved?.token) {
359
+ try { localStorage.setItem(TOKEN_KEY, saved.token); } catch {}
360
+ setToken(saved.token);
361
+ if (saved.accessToken) {
362
+ try { localStorage.setItem(ACCESS_TOKEN_KEY, saved.accessToken); } catch {}
363
+ setAccessToken(saved.accessToken);
364
+ }
365
+ if (saved.userId) {
366
+ try { localStorage.setItem(USER_ID_KEY, String(saved.userId)); } catch {}
367
+ setUserId(String(saved.userId));
368
+ }
369
+ }
370
+ } catch {}
371
+ finally { if (!cancelled) setAuthRestoring(false); }
372
+ })();
373
+ return () => { cancelled = true; };
374
+ }, []);
375
+
341
376
  // auth:complete from main.
342
377
  useEffect(() => {
343
378
  if (!window.cicy?.auth?.onComplete) return;
@@ -385,6 +420,9 @@ export default function App() {
385
420
  localStorage.removeItem(ACCESS_TOKEN_KEY);
386
421
  localStorage.removeItem(USER_ID_KEY);
387
422
  } catch {}
423
+ // Clear the durable main-process store too — explicit logout is the ONLY
424
+ // path that should invalidate the persisted login.
425
+ try { window.cicy?.auth?.logout?.(); } catch {}
388
426
  setToken(null);
389
427
  setAccessToken(null);
390
428
  setUserId(null);
@@ -394,6 +432,20 @@ export default function App() {
394
432
  setProfileError("");
395
433
  }
396
434
 
435
+ // Still checking the durable store — show a minimal splash, not the login
436
+ // card, so we never flash "please log in" before restore completes.
437
+ if (!token && authRestoring) {
438
+ return (
439
+ <div className="shell" data-id="AuthRestoringSplash">
440
+ <div className="glow" aria-hidden />
441
+ <div className="card">
442
+ <Brand />
443
+ <div className="spinner-row"><Spinner /><span>正在恢复登录…</span></div>
444
+ </div>
445
+ </div>
446
+ );
447
+ }
448
+
397
449
  // Not logged in yet → centered login card.
398
450
  if (!token) {
399
451
  return (