cicy-desktop 2.1.41 → 2.1.43
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 +55 -0
- package/src/tools/index.js +1 -0
- package/src/tools/list-tools.js +46 -0
- 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
|
@@ -669,6 +669,10 @@ electronApp.whenReady().then(async () => {
|
|
|
669
669
|
backendsIPC.register({ sidecarLogPath: path.join(os.homedir(), "logs", "cicy-code-sidecar.log") });
|
|
670
670
|
require("./backends/sidecar-ipc").register({ sidecarLogPath: path.join(os.homedir(), "logs", "cicy-code-sidecar.log") });
|
|
671
671
|
|
|
672
|
+
// window.cicy.artifact bridge — CDP/webContents control of the cicy-code
|
|
673
|
+
// 产物 (artifact) <webview> guest. Injected renderer-side in window-utils.js.
|
|
674
|
+
require("./backends/artifact-ipc").register();
|
|
675
|
+
|
|
672
676
|
// Browser-login loopback listener. Renderer calls auth:login-start when
|
|
673
677
|
// the user clicks Login; main opens a 127.0.0.1 server + the browser,
|
|
674
678
|
// and broadcasts auth:complete back to the homepage window once the
|
|
@@ -676,10 +680,38 @@ electronApp.whenReady().then(async () => {
|
|
|
676
680
|
{
|
|
677
681
|
const auth = require("./backends/auth-loopback");
|
|
678
682
|
const { ipcMain: __ipcMainAuth } = require("electron");
|
|
683
|
+
const { readGlobalConfig, updateGlobalConfig } = require("./utils/global-json");
|
|
684
|
+
const GLOBAL_JSON = path.join(os.homedir(), "cicy-ai", "global.json");
|
|
685
|
+
|
|
686
|
+
// Persist the cloud login durably in the MAIN process (global.json),
|
|
687
|
+
// independent of the homepage renderer's origin. The renderer keeps the
|
|
688
|
+
// token in localStorage, which Chromium scopes to the homepage window's
|
|
689
|
+
// origin — and that origin drifts (file:// on mac, https://desktop.cicy-ai.com
|
|
690
|
+
// on Windows, http://<ip>:port or the team domain when CICY_HOMEPAGE_URL is
|
|
691
|
+
// set). A token saved under one origin is invisible after the URL changes,
|
|
692
|
+
// which forced the user to log in again and again. Storing it here and
|
|
693
|
+
// restoring it on every homepage load makes "logged in once" survive origin
|
|
694
|
+
// changes, restarts and public-URL switches. ONLY explicit logout clears it.
|
|
695
|
+
const saveDesktopAuth = (p) => {
|
|
696
|
+
try {
|
|
697
|
+
updateGlobalConfig(GLOBAL_JSON, (c) => {
|
|
698
|
+
c.desktopAuth = {
|
|
699
|
+
token: p.token || "",
|
|
700
|
+
accessToken: p.accessToken || "",
|
|
701
|
+
userId: p.userId != null ? String(p.userId) : "",
|
|
702
|
+
savedAt: Date.now(),
|
|
703
|
+
};
|
|
704
|
+
return c;
|
|
705
|
+
});
|
|
706
|
+
log.info("[auth] desktop login persisted to global.json (origin-independent)");
|
|
707
|
+
} catch (e) { log.warn(`[auth] persist failed: ${e.message}`); }
|
|
708
|
+
};
|
|
709
|
+
|
|
679
710
|
__ipcMainAuth.handle("auth:login-start", async () => {
|
|
680
711
|
try {
|
|
681
712
|
await auth.startLogin({
|
|
682
713
|
onResult: (payload) => {
|
|
714
|
+
if (payload && payload.token) saveDesktopAuth(payload);
|
|
683
715
|
const hw = require("./backends/homepage-window");
|
|
684
716
|
const w = hw.getHomepageWindow && hw.getHomepageWindow();
|
|
685
717
|
if (w && !w.isDestroyed()) {
|
|
@@ -694,6 +726,29 @@ electronApp.whenReady().then(async () => {
|
|
|
694
726
|
}
|
|
695
727
|
});
|
|
696
728
|
__ipcMainAuth.handle("auth:login-cancel", () => { auth.cancel(); return { ok: true }; });
|
|
729
|
+
|
|
730
|
+
// Origin-independent restore. The homepage SPA calls this on mount; if its
|
|
731
|
+
// own (origin-scoped) localStorage has no token, it adopts this one — so a
|
|
732
|
+
// homepage URL/origin change never forces a needless re-login.
|
|
733
|
+
__ipcMainAuth.handle("auth:get-saved", () => {
|
|
734
|
+
try {
|
|
735
|
+
const c = readGlobalConfig(GLOBAL_JSON);
|
|
736
|
+
const a = c && c.desktopAuth;
|
|
737
|
+
if (a && a.token) {
|
|
738
|
+
return { token: a.token, accessToken: a.accessToken || "", userId: a.userId || "" };
|
|
739
|
+
}
|
|
740
|
+
} catch (e) { log.warn(`[auth] get-saved failed: ${e.message}`); }
|
|
741
|
+
return null;
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// Explicit logout is the ONLY thing that clears the durable store.
|
|
745
|
+
__ipcMainAuth.handle("auth:logout", () => {
|
|
746
|
+
try {
|
|
747
|
+
updateGlobalConfig(GLOBAL_JSON, (c) => { delete c.desktopAuth; return c; });
|
|
748
|
+
log.info("[auth] desktop login cleared (explicit logout)");
|
|
749
|
+
} catch (e) { log.warn(`[auth] logout clear failed: ${e.message}`); }
|
|
750
|
+
return { ok: true };
|
|
751
|
+
});
|
|
697
752
|
}
|
|
698
753
|
|
|
699
754
|
// Local-team discovery — reads ~/cicy-ai/global.json's cicyDesktopNodes
|
package/src/tools/index.js
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { z } = require("zod");
|
|
2
|
+
const { loadToolCatalog } = require("../server/tool-catalog");
|
|
3
|
+
const { zodToJsonSchema } = require("../server/tool-registry");
|
|
4
|
+
|
|
5
|
+
// A meta-tool: lets any electronRPC caller (renderer, <webview>, agent-desktop
|
|
6
|
+
// `rpc list_tools`) discover the full set of registered tools at runtime —
|
|
7
|
+
// previously the only index was HTTP /openapi.json, unreachable over the IPC /
|
|
8
|
+
// desktop_event bridge.
|
|
9
|
+
module.exports = (registerTool) => {
|
|
10
|
+
registerTool(
|
|
11
|
+
"list_tools",
|
|
12
|
+
"列出本机 cicy-desktop 当前注册的所有 electronRPC 工具(name/description/tag;可选 inputSchema)。用于运行时发现可调用工具全集。",
|
|
13
|
+
z.object({
|
|
14
|
+
tag: z.string().optional().describe("只列某个 tag 下的工具,如 Chrome / System / General"),
|
|
15
|
+
schema: z.boolean().optional().describe("为 true 时附带每个工具的 inputSchema(JSON Schema)"),
|
|
16
|
+
names_only: z.boolean().optional().describe("为 true 时只返回工具名数组"),
|
|
17
|
+
}),
|
|
18
|
+
async ({ tag, schema, names_only } = {}) => {
|
|
19
|
+
const cat = loadToolCatalog();
|
|
20
|
+
let records = Array.from(cat.toolsByName.values());
|
|
21
|
+
if (tag) records = records.filter((r) => (r.tag || "General") === tag);
|
|
22
|
+
records.sort(
|
|
23
|
+
(a, b) => (a.tag || "General").localeCompare(b.tag || "General") || a.name.localeCompare(b.name)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
let payload;
|
|
27
|
+
if (names_only) {
|
|
28
|
+
payload = { count: records.length, tools: records.map((r) => r.name) };
|
|
29
|
+
} else {
|
|
30
|
+
payload = {
|
|
31
|
+
count: records.length,
|
|
32
|
+
tags: [...new Set(records.map((r) => r.tag || "General"))].sort(),
|
|
33
|
+
tools: records.map((r) => {
|
|
34
|
+
const t = { name: r.name, description: r.description, tag: r.tag || "General" };
|
|
35
|
+
if (schema) {
|
|
36
|
+
try { t.inputSchema = zodToJsonSchema(r.schema); } catch { t.inputSchema = null; }
|
|
37
|
+
}
|
|
38
|
+
return t;
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
43
|
+
},
|
|
44
|
+
{ tag: "System" }
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -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 (
|