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.
- package/CLAUDE.md +35 -29
- package/README.md +2 -3
- package/bin/cicy-desktop +50 -14
- package/package.json +1 -1
- package/src/backends/artifact-ipc.js +142 -0
- package/src/backends/homepage-preload.js +5 -0
- package/src/backends/homepage-react/assets/index-BqRSij9W.js +49 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/homepage-window.js +16 -14
- package/src/main.js +96 -7
- package/src/utils/window-utils.js +38 -0
- package/workers/render/src/App.jsx +52 -0
- package/src/backends/homepage-react/assets/index-BhLjpIIu.js +0 -49
|
@@ -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-
|
|
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
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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 (
|