cicy-desktop 2.1.83 → 2.1.85

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.83",
3
+ "version": "2.1.85",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -133,11 +133,11 @@
133
133
  },
134
134
  "//optionalDependencies": "Runtime Bundle v1 (主人指令): platform binaries delivered by `npm i -g cicy-desktop` itself — npm installs only the current-platform subpackage (os/cpu pinned in each), so first start seeds the runtime store with ZERO network, ZERO npx. Windows packages are named *-windows-* (npm spam filter 403s new names containing win32). cicy-msys2 added once published.",
135
135
  "optionalDependencies": {
136
- "cicy-code-darwin-x64": "2.3.2",
137
- "cicy-code-darwin-arm64": "2.3.2",
138
- "cicy-code-linux-x64": "2.3.2",
139
- "cicy-code-linux-arm64": "2.3.2",
140
- "cicy-code-windows-x64": "2.3.2",
136
+ "cicy-code-darwin-x64": "2.3.4",
137
+ "cicy-code-darwin-arm64": "2.3.4",
138
+ "cicy-code-linux-x64": "2.3.4",
139
+ "cicy-code-linux-arm64": "2.3.4",
140
+ "cicy-code-windows-x64": "2.3.4",
141
141
  "cicy-mihomo-darwin-x64": "1.10.4",
142
142
  "cicy-mihomo-darwin-arm64": "1.10.4",
143
143
  "cicy-mihomo-linux-x64": "1.10.4",
package/src/main.js CHANGED
@@ -1049,15 +1049,17 @@ electronApp.whenReady().then(async () => {
1049
1049
  }
1050
1050
 
1051
1051
  // Periodic WHOLE-desktop snapshot → ~/cicy-files/desktop-snapshot/desktop.b64
1052
- // (≤600px wide JPEG). WINDOWS ONLY: the cloud (cicy-code) live-captures mac/
1053
- // linux via the OS grabber, but on Windows live PowerShell capture fails under
1054
- // 360/AppLocker/RDP, so there it reads this file instead. The capture runs in a
1055
- // --disable-gpu child electron (GDI path, works over RDP; main app GPU intact).
1056
- if (process.platform === "win32" && !global.__cicyDesktopSnapStarted) {
1052
+ // (≤600px wide JPEG). ALL PLATFORMS: the cloud (cicy-code) fetches it via the
1053
+ // dedicated non-dangerous `desktop_snapshot` RPC tool, which reads this fresh
1054
+ // file so there's no per-call screen capture, hence no macOS Screen-Recording
1055
+ // prompt and no consent dialog. On Windows the capture runs in a --disable-gpu
1056
+ // child electron (GDI path, works over RDP; main app GPU intact); on mac/linux
1057
+ // it's an in-process native-capture loop (screencapture / scrot).
1058
+ if (!global.__cicyDesktopSnapStarted) {
1057
1059
  global.__cicyDesktopSnapStarted = true;
1058
1060
  try {
1059
1061
  const info = require("./utils/desktop-snapshot").startDesktopSnapshots();
1060
- log.info(`[desktop-snap] desktop snapshots → ${info.dir} (every ${info.intervalMs}ms, maxW ${info.maxWidth})`);
1062
+ log.info(`[desktop-snap] desktop snapshots → ${info.dir} (every ${info.intervalMs}ms, maxW ${info.maxWidth}, mode ${info.mode})`);
1061
1063
  } catch (e) { log.warn(`[desktop-snap] start failed: ${e.message}`); }
1062
1064
  }
1063
1065
 
@@ -62,6 +62,15 @@
62
62
  .tab.active::before,.tab.active::after{content:"";position:absolute;bottom:0;width:5px;height:5px}
63
63
  .tab.active::before{left:-5px;background:radial-gradient(circle at top left,var(--strip) 4.5px,var(--toolbar) 5px)}
64
64
  .tab.active::after{right:-5px;background:radial-gradient(circle at top right,var(--strip) 4.5px,var(--toolbar) 5px)}
65
+ /* Chrome-style hairline separator between adjacent INACTIVE tabs: a faint 1px
66
+ vertical line sitting in the gap to the LEFT of each tab. Suppressed for the
67
+ first tab and around the active/hovered tab (Chrome hides the seam that would
68
+ touch an active or hovered tab on either side). Inactive tabs don't use
69
+ ::before (that's the active tab's ear), so it's free here. */
70
+ .tab:not(.active)::before{content:"";position:absolute;left:-3.5px;top:50%;transform:translateY(-50%);width:1px;height:16px;background:rgba(255,255,255,.12);pointer-events:none}
71
+ #tabs>.tab:first-child::before{display:none} /* nothing left of the first tab */
72
+ .tab:hover::before{display:none} /* no line on the hovered tab's left */
73
+ .tab.active+.tab::before,.tab:hover+.tab::before{display:none} /* none on the active/hovered tab's right */
65
74
  .fav{width:16px;height:16px;flex:0 0 16px;border-radius:4px;display:flex;align-items:center;justify-content:center;overflow:hidden}
66
75
  .fav img{width:16px;height:16px;object-fit:contain}
67
76
  .spin{width:14px;height:14px;border:2px solid rgba(255,255,255,.25);border-top-color:var(--accent);border-radius:50%;animation:sp .7s linear infinite}
@@ -0,0 +1,72 @@
1
+ const { z } = require("zod");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+
5
+ // desktop_snapshot — DEDICATED, NON-dangerous full-desktop screenshot tool for the
6
+ // cloud (cicy-code). It returns a base64 JPEG of the whole screen so cicy-code can
7
+ // preview a device's desktop WITHOUT going through exec_shell / file_read.
8
+ //
9
+ // Why a dedicated tool (主人令 + w-10135):
10
+ // exec_*/file_* are in rpc-guard's DANGEROUS_TOOLS, so each call pops the
11
+ // "敏感操作请求" consent dialog, and on macOS the live `screencapture` shell also
12
+ // trips the OS Screen-Recording prompt. This tool is NOT dangerous (deliberately
13
+ // absent from DANGEROUS_TOOLS), and it reuses cicy-desktop's own native capturer
14
+ // (which already holds the OS grant) — so: no shell, no consent dialog, no
15
+ // per-call permission prompt.
16
+ //
17
+ // Source of the image (preferred → fallback):
18
+ // 1) ~/cicy-files/desktop-snapshot/desktop.b64 — written every few seconds by the
19
+ // desktop-snapshot daemon (src/utils/desktop-snapshot.js, started in main.js).
20
+ // On Windows this is the ONLY valid source (in-process desktopCapturer fails
21
+ // over RDP without --disable-gpu; the daemon is a --disable-gpu child).
22
+ // 2) live in-process capture (mac/linux only) when the file is stale/missing.
23
+ const snap = require("../utils/desktop-snapshot");
24
+
25
+ // Treat the daemon file as good if written within this window. The daemon ticks
26
+ // every ~8s, so 30s tolerates a couple of missed/slow ticks before we live-capture.
27
+ const FRESH_MS = 30_000;
28
+
29
+ function readDaemonB64() {
30
+ try {
31
+ const p = path.join(snap.snapDir(), "desktop.b64");
32
+ const st = fs.statSync(p);
33
+ const ageMs = Date.now() - st.mtimeMs;
34
+ const b64 = fs.readFileSync(p, "utf8").trim();
35
+ if (b64.length > 256) return { b64, ageMs };
36
+ } catch (_) {}
37
+ return null;
38
+ }
39
+
40
+ module.exports = (registerTool) => {
41
+ registerTool(
42
+ "desktop_snapshot",
43
+ "返回整屏桌面截图(base64 JPEG,≤600px 宽)。优先读 desktop-snapshot daemon 写的 desktop.b64(秒回、不弹 consent / 屏幕录制),过期或缺失时 mac/linux 即时抓屏。",
44
+ z.object({ maxWidth: z.number().optional().describe("最大宽度(px),默认 600") }),
45
+ async ({ maxWidth }) => {
46
+ try {
47
+ const fresh = readDaemonB64();
48
+ if (fresh && fresh.ageMs <= FRESH_MS) {
49
+ return { content: [{ type: "text", text: fresh.b64 }] };
50
+ }
51
+
52
+ // Stale/missing. mac/linux can capture live in-process; win32 cannot (needs
53
+ // the --disable-gpu daemon), so there we fall back to whatever file exists.
54
+ if (process.platform !== "win32") {
55
+ try {
56
+ const r = await snap.captureB64(maxWidth);
57
+ return { content: [{ type: "text", text: r.b64 }] };
58
+ } catch (e) {
59
+ if (fresh) return { content: [{ type: "text", text: fresh.b64 }] }; // stale but real
60
+ throw e;
61
+ }
62
+ }
63
+
64
+ if (fresh) return { content: [{ type: "text", text: fresh.b64 }] }; // win32: stale is better than nothing
65
+ throw new Error("no desktop snapshot yet (daemon warming up?)");
66
+ } catch (error) {
67
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
68
+ }
69
+ },
70
+ { tag: "Desktop" }
71
+ );
72
+ };
@@ -14,6 +14,7 @@ module.exports = [
14
14
  require("./automation-tools"),
15
15
  require("./account-tools"),
16
16
  require("./device-tools"),
17
+ require("./desktop-snapshot-tools"),
17
18
  require("./download-tools"),
18
19
  require("./ipc-bridge"),
19
20
  require("./hook-gemini"),
@@ -104,9 +104,14 @@ class TabManager {
104
104
  managerByHost.set(this.win.webContents.id, this);
105
105
  // Fullscreen hides the OS window controls (mac traffic lights / win caption
106
106
  // buttons) → tell the shell so it reclaims the reserved gutter (CSS
107
- // body.is-fullscreen). Fires on both mac and Windows.
108
- this.win.on("enter-full-screen", () => { try { this.win.webContents.send("window:fullscreen", true); } catch (e) {} });
109
- this.win.on("leave-full-screen", () => { try { this.win.webContents.send("window:fullscreen", false); } catch (e) {} });
107
+ // body.is-fullscreen). Fires on both mac and Windows. MUST also reach every
108
+ // TAB's renderer: the homepage SPA (homepage-preload) toggles its own
109
+ // data-fullscreen attr to drop the 34px traffic-light gutter and the
110
+ // homepage now runs as a resident BrowserView TAB, not this.win.webContents.
111
+ // Without forwarding to the tab, the gutter stays in fullscreen = a blank
112
+ // strip across the top of 我的团队 (reported on mac fullscreen).
113
+ this.win.on("enter-full-screen", () => this.sendFullscreen(true));
114
+ this.win.on("leave-full-screen", () => this.sendFullscreen(false));
110
115
  this.win.on("resize", () => this.layout());
111
116
  this.win.on("closed", () => {
112
117
  managers.delete(accountIdx);
@@ -114,6 +119,16 @@ class TabManager {
114
119
  });
115
120
  }
116
121
 
122
+ // Broadcast the window's fullscreen state to the shell chrome AND every tab's
123
+ // renderer. The homepage tab's SPA needs it to collapse the mac traffic-light
124
+ // gutter; other tabs simply ignore the message.
125
+ sendFullscreen(isFs) {
126
+ try { this.win.webContents.send("window:fullscreen", isFs); } catch (e) {}
127
+ for (const t of this.tabs) {
128
+ try { t.view.webContents.send("window:fullscreen", isFs); } catch (e) {}
129
+ }
130
+ }
131
+
117
132
  pushState() {
118
133
  const active = this.tabs.find((t) => t.id === this.activeId);
119
134
  let wc = null;
@@ -177,6 +192,10 @@ class TabManager {
177
192
  wc.on("page-favicon-updated", (_e, favs) => { tab.favicon = (favs && favs[0]) || ""; this.pushState(); });
178
193
  wc.on("did-start-loading", () => { tab.loading = true; this.pushState(); });
179
194
  wc.on("did-stop-loading", () => { tab.loading = false; this.pushState(); });
195
+ // Re-sync fullscreen state after each (re)load: the SPA resets data-fullscreen
196
+ // to "0" on mount, so a homepage reload while the window is fullscreen would
197
+ // otherwise re-show the 34px traffic-light gutter (blank top strip).
198
+ wc.on("did-finish-load", () => { try { wc.send("window:fullscreen", !!this.win.isFullScreen()); } catch (e) {} });
180
199
  wc.on("did-navigate", (_e, u) => { tab.url = u; tab.favicon = ""; this.pushState(); });
181
200
  wc.on("did-navigate-in-page", (_e, u) => { tab.url = u; this.pushState(); });
182
201
  // popups / window.open → open as a new tab. In profile 0 the new tab carries
@@ -247,13 +266,10 @@ function findManagerByTab(webContentsId) {
247
266
  }
248
267
 
249
268
  // ── programmatic API (team-open reroute / homepage) ──────────────────────────
250
- // profile 0 is the system tab window: tabs may ONLY be added by the homepage
251
- // (team open passes systemOpen:true). The "+" button, the electron_tab_open
252
- // tool and the panel can't add tabs to it.
269
+ // profile 0 is the system tab window. It used to accept tabs ONLY from the
270
+ // homepage (team open, systemOpen:true); that restriction is lifted so the "+"
271
+ // button / electron_tab_open / the panel can add tabs to profile 0 too.
253
272
  async function openTab(accountIdx, url, opts = {}) {
254
- if (accountIdx === 0 && !opts.systemOpen) {
255
- throw new Error("profile 0 的标签只能从首页点开 team");
256
- }
257
273
  const m = ensureManager(accountIdx);
258
274
  const id = m.addTab(url, { trusted: !!opts.trusted, home: !!opts.home, title: opts.title || "" });
259
275
  try { m.win.show(); m.win.focus(); } catch (e) {}
@@ -280,7 +296,7 @@ function installIpc() {
280
296
  ipcInstalled = true;
281
297
  const mgr = (e) => managerByHost.get(e.sender.id);
282
298
  ipcMain.on("tabwin:ready", (e) => { const m = mgr(e); if (m) m.pushState(); });
283
- ipcMain.on("tabwin:new", (e, { url }) => { const m = mgr(e); if (m && m.accountIdx !== 0) m.addTab(url || ""); });
299
+ ipcMain.on("tabwin:new", (e, { url }) => { const m = mgr(e); if (m) m.addTab(url || ""); });
284
300
  ipcMain.on("tabwin:activate", (e, { id }) => { const m = mgr(e); if (m) m.activate(id); });
285
301
  ipcMain.on("tabwin:close", (e, { id }) => { const m = mgr(e); if (m) m.close(id); });
286
302
  ipcMain.on("tabwin:navigate", (e, { url }) => { const m = mgr(e); const wc = m && m.activeWc(); if (wc && url) wc.loadURL(String(url)); });
@@ -92,6 +92,21 @@ async function captureOnce() {
92
92
  return { dir, w: o.width, h: o.height, bytes: jpeg.length };
93
93
  }
94
94
 
95
+ // One-shot in-process capture that RETURNS the base64 JPEG (does not touch disk).
96
+ // Used by the `desktop_snapshot` RPC tool as the live fallback when the daemon's
97
+ // desktop.b64 file is missing/stale. NOT valid on win32 in the main process —
98
+ // desktopCapturer needs the --disable-gpu daemon there (see grabScreenImage);
99
+ // the tool guards that and reads the daemon file instead.
100
+ async function captureB64(maxWidth) {
101
+ const mw = maxWidth > 0 ? maxWidth : MAX_W;
102
+ let img = await grabScreenImage();
103
+ const o = img.getSize();
104
+ if (o.width > mw) img = img.resize({ width: mw, quality: "good" });
105
+ const jpeg = img.toJPEG(QUALITY);
106
+ if (!jpeg || jpeg.length < 256) throw new Error("encoded jpeg too small");
107
+ return { b64: jpeg.toString("base64"), w: o.width, h: o.height, bytes: jpeg.length };
108
+ }
109
+
95
110
  // ── parent (started from main.js) ─────────────────────────────────────────────
96
111
  let child = null;
97
112
  let timer = null;
@@ -172,4 +187,4 @@ if (process.env.CICY_SNAP_DAEMON === "1") {
172
187
  app.on("render-process-gone", () => app.quit());
173
188
  }
174
189
 
175
- module.exports = { startDesktopSnapshots, stopDesktopSnapshots, snapDir, captureOnce };
190
+ module.exports = { startDesktopSnapshots, stopDesktopSnapshots, snapDir, captureOnce, captureB64, MAX_W };