cicy-desktop 2.1.330 → 2.1.332
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 +1 -1
- package/src/backends/homepage-window.js +67 -6
- package/src/main.js +33 -5
- package/test/autostart-run-key.test.js +59 -0
- package/test/homepage-remote.test.js +106 -0
package/package.json
CHANGED
|
@@ -4,11 +4,24 @@
|
|
|
4
4
|
// Homepage window — primary CiCy Desktop window. Singleton; closing it
|
|
5
5
|
// does NOT quit the app.
|
|
6
6
|
//
|
|
7
|
-
// URL selection
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// URL selection: the bundled local file:// SPA by DEFAULT — works offline, fast,
|
|
8
|
+
// no remote dependency, no mixed-content concerns when embedding the
|
|
9
|
+
// team-assistant webview (the cicy-desktop preload's IPC bridge still attaches).
|
|
10
|
+
//
|
|
11
|
+
// The same SPA is also deployed as the desktop-render Worker on
|
|
12
|
+
// https://desktop.cicy-ai.com, which is what makes "ship UI without shipping the
|
|
13
|
+
// app" possible. Pointing the homepage at it is OPT-IN, and deliberately not the
|
|
14
|
+
// default: the home tab runs homepage-preload, whose bridge is the UNGUARDED
|
|
15
|
+
// "rpc" channel (no origin gate, no dangerous-tool gate — see utils/rpc-guard),
|
|
16
|
+
// because it was only ever loaded from file://. A remote origin on that channel
|
|
17
|
+
// gets ungated exec_*/file_* on every desktop, so whoever controls the domain
|
|
18
|
+
// controls the fleet. Turn it on per machine with CICY_HOMEPAGE_URL (any URL) or
|
|
19
|
+
// prefs.homepageRemote:true (uses REMOTE_HOMEPAGE), and only for an origin you
|
|
20
|
+
// trust exactly as much as the bundled bundle itself.
|
|
21
|
+
//
|
|
22
|
+
// Either way the remote load is never a one-way door: a main-frame failure (or a
|
|
23
|
+
// load that never commits) falls back to the bundled snapshot, so a CDN outage
|
|
24
|
+
// cannot leave the app with no homepage.
|
|
12
25
|
|
|
13
26
|
const path = require("path");
|
|
14
27
|
const { BrowserWindow } = require("electron");
|
|
@@ -19,9 +32,55 @@ const FIXED_WIDTH = 930;
|
|
|
19
32
|
const FIXED_HEIGHT = 640;
|
|
20
33
|
|
|
21
34
|
const LOCAL_INDEX = path.join(__dirname, "homepage-react", "index.html");
|
|
35
|
+
const LOCAL_URL = `file://${LOCAL_INDEX}`;
|
|
36
|
+
const REMOTE_HOMEPAGE = "https://desktop.cicy-ai.com/";
|
|
37
|
+
const REMOTE_FALLBACK_MS = 15000;
|
|
38
|
+
|
|
39
|
+
function readHomepagePrefs() {
|
|
40
|
+
try {
|
|
41
|
+
const os = require("os");
|
|
42
|
+
const p = path.join(os.homedir(), "cicy-ai", "db", "prefs.json");
|
|
43
|
+
return JSON.parse(require("fs").readFileSync(p, "utf8")) || {};
|
|
44
|
+
} catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
22
48
|
|
|
23
49
|
function pickHomepageURL() {
|
|
24
|
-
|
|
50
|
+
const env = String(process.env.CICY_HOMEPAGE_URL || "").trim();
|
|
51
|
+
if (env) return env;
|
|
52
|
+
try {
|
|
53
|
+
if (readHomepagePrefs().homepageRemote === true) return REMOTE_HOMEPAGE;
|
|
54
|
+
} catch {}
|
|
55
|
+
return LOCAL_URL;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fall back to the bundled snapshot when a remote homepage fails to load or
|
|
59
|
+
// never commits. Idempotent per webContents; a no-op once we are already local.
|
|
60
|
+
function wireRemoteFallback(wc) {
|
|
61
|
+
if (!wc || wc.isDestroyed() || wc.__cicyHomeFallbackWired) return;
|
|
62
|
+
let current = "";
|
|
63
|
+
try { current = wc.getURL(); } catch {}
|
|
64
|
+
if (!current || current.startsWith("file://")) {
|
|
65
|
+
// getURL() is empty until the first commit, so only skip a known-local load.
|
|
66
|
+
if (current) return;
|
|
67
|
+
}
|
|
68
|
+
wc.__cicyHomeFallbackWired = true;
|
|
69
|
+
let settled = false;
|
|
70
|
+
const toLocal = (reason) => {
|
|
71
|
+
if (settled || wc.isDestroyed()) return;
|
|
72
|
+
let url = "";
|
|
73
|
+
try { url = wc.getURL(); } catch {}
|
|
74
|
+
if (url.startsWith("file://")) { settled = true; return; }
|
|
75
|
+
settled = true;
|
|
76
|
+
log.warn(`[homepage] remote homepage unusable (${reason}) → bundled snapshot`);
|
|
77
|
+
try { wc.loadURL(LOCAL_URL); } catch (e) { log.error(`[homepage] fallback failed: ${e.message}`); }
|
|
78
|
+
};
|
|
79
|
+
wc.on("did-fail-load", (_e, code, desc, _url, isMainFrame) => {
|
|
80
|
+
if (isMainFrame) toLocal(`${code} ${desc}`);
|
|
81
|
+
});
|
|
82
|
+
wc.once("did-finish-load", () => { settled = true; });
|
|
83
|
+
setTimeout(() => toLocal(`no load within ${REMOTE_FALLBACK_MS}ms`), REMOTE_FALLBACK_MS);
|
|
25
84
|
}
|
|
26
85
|
|
|
27
86
|
let homepage = null; // standalone fallback window (only if the tab engine fails)
|
|
@@ -40,6 +99,7 @@ async function openHomepage(opts = {}) {
|
|
|
40
99
|
if (wc) {
|
|
41
100
|
homeTabWc = wc;
|
|
42
101
|
try { wc.once("destroyed", () => { if (homeTabWc === wc) homeTabWc = null; }); } catch {}
|
|
102
|
+
try { wireRemoteFallback(wc); } catch (e) { log.warn(`[homepage] fallback wiring: ${e.message}`); }
|
|
43
103
|
}
|
|
44
104
|
log.info(`[homepage] opened as profile-0 resident tab (wc=${wc && wc.id})`);
|
|
45
105
|
return win;
|
|
@@ -103,6 +163,7 @@ async function openHomepageStandalone() {
|
|
|
103
163
|
|
|
104
164
|
const target = pickHomepageURL();
|
|
105
165
|
log.info(`[homepage] loading ${target}`);
|
|
166
|
+
try { wireRemoteFallback(homepage.webContents); } catch (e) { log.warn(`[homepage] fallback wiring: ${e.message}`); }
|
|
106
167
|
homepage.loadURL(target);
|
|
107
168
|
|
|
108
169
|
// Pipe renderer console + load failures to main-process stdout so we can
|
package/src/main.js
CHANGED
|
@@ -949,22 +949,29 @@ function ensureAutoLaunch() {
|
|
|
949
949
|
// (它先起 master 再起 worker,直接起 electron.exe 会缺 master)。登录项指向一个
|
|
950
950
|
// wscript 隐藏启动的 VBS → `cmd /c node .\bin\cicy-desktop`,全程无控制台窗口。
|
|
951
951
|
const fromSource = !electronApp.isPackaged;
|
|
952
|
-
let opts;
|
|
952
|
+
let opts, runCmd;
|
|
953
953
|
if (fromSource) {
|
|
954
954
|
const vbs = writeSourceAutostartVbs();
|
|
955
955
|
opts = { openAtLogin: want, path: "wscript.exe", args: ["//B", "//Nologo", vbs] };
|
|
956
|
+
runCmd = `wscript.exe //B //Nologo "${vbs}"`;
|
|
956
957
|
} else {
|
|
957
958
|
opts = { openAtLogin: want, args: ["--hidden"] };
|
|
959
|
+
runCmd = `"${process.execPath}" --hidden`;
|
|
958
960
|
}
|
|
959
|
-
const
|
|
960
|
-
fromSource ? { path: opts.path, args: opts.args } : undefined
|
|
961
|
-
)
|
|
962
|
-
if (cur.openAtLogin !== want) {
|
|
961
|
+
const probe = () =>
|
|
962
|
+
electronApp.getLoginItemSettings(fromSource ? { path: opts.path, args: opts.args } : undefined);
|
|
963
|
+
if (probe().openAtLogin !== want) {
|
|
963
964
|
electronApp.setLoginItemSettings(opts);
|
|
964
965
|
log.info(
|
|
965
966
|
`[autostart] openAtLogin → ${want}${fromSource ? ` (source: ${opts.path} ${opts.args.join(" ")})` : ""}`
|
|
966
967
|
);
|
|
967
968
|
}
|
|
969
|
+
// Read it back. setLoginItemSettings() returns without throwing on every
|
|
970
|
+
// Windows node of this fleet and still leaves openAtLogin false, with
|
|
971
|
+
// HKCU\...\Run untouched — so autostart silently never existed, and a node
|
|
972
|
+
// whose app went away (e.g. an installer closed it) never came back, not
|
|
973
|
+
// even after a reboot. When the API did not take, write the Run key.
|
|
974
|
+
if (probe().openAtLogin !== want) ensureWindowsRunKey(want, runCmd);
|
|
968
975
|
} else if (process.platform === "linux") {
|
|
969
976
|
if (!electronApp.isPackaged) return;
|
|
970
977
|
ensureLinuxAutostart(want);
|
|
@@ -974,6 +981,27 @@ function ensureAutoLaunch() {
|
|
|
974
981
|
}
|
|
975
982
|
}
|
|
976
983
|
|
|
984
|
+
// Fallback for the silently-failing setLoginItemSettings (see ensureAutoLaunch):
|
|
985
|
+
// own the HKCU Run entry directly. Async so a slow reg.exe never delays startup;
|
|
986
|
+
// failures are logged, never thrown — autostart is best-effort.
|
|
987
|
+
const WIN_RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
988
|
+
const WIN_RUN_NAME = "CiCy Desktop";
|
|
989
|
+
function ensureWindowsRunKey(want, command) {
|
|
990
|
+
try {
|
|
991
|
+
const { execFile } = require("child_process");
|
|
992
|
+
const args = want
|
|
993
|
+
? ["add", WIN_RUN_KEY, "/v", WIN_RUN_NAME, "/t", "REG_SZ", "/d", command, "/f"]
|
|
994
|
+
: ["delete", WIN_RUN_KEY, "/v", WIN_RUN_NAME, "/f"];
|
|
995
|
+
execFile("reg", args, { windowsHide: true }, (err) => {
|
|
996
|
+
// deleting an absent value exits non-zero — that is the wanted end state.
|
|
997
|
+
if (err && want) log.warn(`[autostart] Run-key fallback failed: ${err.message}`);
|
|
998
|
+
else if (want) log.info(`[autostart] Run-key fallback wrote ${WIN_RUN_NAME} → ${command}`);
|
|
999
|
+
});
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
log.warn(`[autostart] Run-key fallback threw: ${e.message}`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
977
1005
|
// 源码模式 Windows 登录自启的隐藏启动器(VBS)。写到 %LOCALAPPDATA%\cicy-desktop,幂等覆盖。
|
|
978
1006
|
// 继承当前的 CICY_DEBUG(用 start-cicy-desktop-win.bat 起的就带 =1),其余环境用登录时的用户环境。
|
|
979
1007
|
function writeSourceAutostartVbs() {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Windows autostart had no test and never worked. Measured across the fleet:
|
|
5
|
+
// app.setLoginItemSettings({openAtLogin:true}) returns without throwing, yet
|
|
6
|
+
// getLoginItemSettings() still reports false and HKCU\...\Run stays untouched
|
|
7
|
+
// (the key exists and is writable — OneDrive lives there). So every Windows node
|
|
8
|
+
// ran with no autostart at all, which is why one whose app went away never came
|
|
9
|
+
// back, not even after a reboot. ensureAutoLaunch() now reads the setting back
|
|
10
|
+
// and owns the Run entry itself when the API did not take.
|
|
11
|
+
const test = require("node:test");
|
|
12
|
+
const assert = require("node:assert/strict");
|
|
13
|
+
const fs = require("node:fs");
|
|
14
|
+
const path = require("node:path");
|
|
15
|
+
|
|
16
|
+
const src = fs.readFileSync(path.join(__dirname, "..", "src", "main.js"), "utf8");
|
|
17
|
+
const fn = src.slice(
|
|
18
|
+
src.indexOf("function ensureAutoLaunch()"),
|
|
19
|
+
src.indexOf("function ensureWindowsRunKey")
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
test("windows autostart verifies the setting instead of trusting the API", () => {
|
|
23
|
+
// A single getLoginItemSettings() call before the write is not enough — the
|
|
24
|
+
// regression is entirely in what happens AFTER setLoginItemSettings().
|
|
25
|
+
assert.match(fn, /const probe = \(\) =>/);
|
|
26
|
+
assert.match(fn, /if \(probe\(\)\.openAtLogin !== want\) \{[^]*setLoginItemSettings\(opts\)/);
|
|
27
|
+
assert.match(fn, /if \(probe\(\)\.openAtLogin !== want\) ensureWindowsRunKey\(want, runCmd\)/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("the Run-key command differs for packaged vs source runs", () => {
|
|
31
|
+
// Packaged: the exe itself, hidden. Source: wscript running the VBS launcher,
|
|
32
|
+
// because starting electron.exe directly skips bin/cicy-desktop's master boot.
|
|
33
|
+
assert.match(fn, /runCmd = `"\$\{process\.execPath\}" --hidden`/);
|
|
34
|
+
assert.match(fn, /runCmd = `wscript\.exe \/\/B \/\/Nologo "\$\{vbs\}"`/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("ensureWindowsRunKey adds or removes the HKCU Run value, async and non-fatal", () => {
|
|
38
|
+
const helper = src.slice(
|
|
39
|
+
src.indexOf("function ensureWindowsRunKey"),
|
|
40
|
+
src.indexOf("// 源码模式 Windows 登录自启")
|
|
41
|
+
);
|
|
42
|
+
assert.match(
|
|
43
|
+
src,
|
|
44
|
+
/WIN_RUN_KEY = "HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run"/
|
|
45
|
+
);
|
|
46
|
+
assert.match(
|
|
47
|
+
helper,
|
|
48
|
+
/\["add", WIN_RUN_KEY, "\/v", WIN_RUN_NAME, "\/t", "REG_SZ", "\/d", command, "\/f"\]/
|
|
49
|
+
);
|
|
50
|
+
assert.match(helper, /\["delete", WIN_RUN_KEY, "\/v", WIN_RUN_NAME, "\/f"\]/);
|
|
51
|
+
assert.match(helper, /execFile\("reg", args, \{ windowsHide: true \}/); // no console flash, no startup stall
|
|
52
|
+
assert.match(helper, /catch \(e\) \{[^]*log\.warn/); // best-effort: never throws into startup
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("autostart still defaults to on and is honoured per-platform", () => {
|
|
56
|
+
assert.match(fn, /const want = prefs\.openAtLogin !== false/);
|
|
57
|
+
assert.match(fn, /if \(process\.platform === "darwin"\)/);
|
|
58
|
+
assert.match(fn, /else if \(process\.platform === "linux"\)/);
|
|
59
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The homepage URL was hardcoded to file://, so deploying the same SPA as the
|
|
5
|
+
// desktop-render Worker (desktop.cicy-ai.com) changed nothing on any desktop —
|
|
6
|
+
// the app never fetched it. It can now be pointed at the web build, but only
|
|
7
|
+
// deliberately: the home tab runs homepage-preload, whose bridge is the
|
|
8
|
+
// UNGUARDED "rpc" channel, so a remote origin there gets ungated exec_*/file_*.
|
|
9
|
+
// Hence opt-in + an automatic fall back to the bundled snapshot.
|
|
10
|
+
const test = require("node:test");
|
|
11
|
+
const assert = require("node:assert/strict");
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const os = require("node:os");
|
|
14
|
+
const path = require("node:path");
|
|
15
|
+
|
|
16
|
+
const SRC = path.join(__dirname, "..", "src", "backends", "homepage-window.js");
|
|
17
|
+
const src = fs.readFileSync(SRC, "utf8");
|
|
18
|
+
|
|
19
|
+
function withHome(fn) {
|
|
20
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "cicy-home-"));
|
|
21
|
+
const prevHome = process.env.HOME,
|
|
22
|
+
prevUp = process.env.USERPROFILE;
|
|
23
|
+
const prevUrl = process.env.CICY_HOMEPAGE_URL;
|
|
24
|
+
process.env.HOME = home;
|
|
25
|
+
process.env.USERPROFILE = home;
|
|
26
|
+
delete process.env.CICY_HOMEPAGE_URL;
|
|
27
|
+
fs.mkdirSync(path.join(home, "cicy-ai", "db"), { recursive: true });
|
|
28
|
+
try {
|
|
29
|
+
return fn(home);
|
|
30
|
+
} finally {
|
|
31
|
+
process.env.HOME = prevHome;
|
|
32
|
+
if (prevUp === undefined) delete process.env.USERPROFILE;
|
|
33
|
+
else process.env.USERPROFILE = prevUp;
|
|
34
|
+
if (prevUrl === undefined) delete process.env.CICY_HOMEPAGE_URL;
|
|
35
|
+
else process.env.CICY_HOMEPAGE_URL = prevUrl;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// homepage-window requires electron; exercise the real pickHomepageURL by
|
|
40
|
+
// evaluating just that function plus the helper it depends on.
|
|
41
|
+
function loadPicker() {
|
|
42
|
+
const start = src.indexOf("const LOCAL_INDEX");
|
|
43
|
+
const end = src.indexOf("// Fall back to the bundled snapshot");
|
|
44
|
+
const body = src.slice(start, end);
|
|
45
|
+
const mod = { exports: {} };
|
|
46
|
+
const fn = new Function(
|
|
47
|
+
"require",
|
|
48
|
+
"path",
|
|
49
|
+
"module",
|
|
50
|
+
"__dirname",
|
|
51
|
+
body + "\nmodule.exports = { pickHomepageURL, LOCAL_URL, REMOTE_HOMEPAGE };"
|
|
52
|
+
);
|
|
53
|
+
fn(require, path, mod, path.dirname(SRC));
|
|
54
|
+
return mod.exports;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test("defaults to the bundled file:// snapshot", () => {
|
|
58
|
+
withHome(() => {
|
|
59
|
+
const { pickHomepageURL, LOCAL_URL } = loadPicker();
|
|
60
|
+
assert.equal(pickHomepageURL(), LOCAL_URL);
|
|
61
|
+
assert.match(pickHomepageURL(), /^file:\/\//);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("prefs.homepageRemote opts in to the deployed Worker", () => {
|
|
66
|
+
withHome((home) => {
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.join(home, "cicy-ai", "db", "prefs.json"),
|
|
69
|
+
JSON.stringify({ homepageRemote: true })
|
|
70
|
+
);
|
|
71
|
+
const { pickHomepageURL, REMOTE_HOMEPAGE } = loadPicker();
|
|
72
|
+
assert.equal(pickHomepageURL(), REMOTE_HOMEPAGE);
|
|
73
|
+
assert.equal(REMOTE_HOMEPAGE, "https://desktop.cicy-ai.com/");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("CICY_HOMEPAGE_URL wins over prefs and the default", () => {
|
|
78
|
+
withHome((home) => {
|
|
79
|
+
fs.writeFileSync(
|
|
80
|
+
path.join(home, "cicy-ai", "db", "prefs.json"),
|
|
81
|
+
JSON.stringify({ homepageRemote: true })
|
|
82
|
+
);
|
|
83
|
+
process.env.CICY_HOMEPAGE_URL = "http://127.0.0.1:5173/";
|
|
84
|
+
try {
|
|
85
|
+
assert.equal(loadPicker().pickHomepageURL(), "http://127.0.0.1:5173/");
|
|
86
|
+
} finally {
|
|
87
|
+
delete process.env.CICY_HOMEPAGE_URL;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("a malformed prefs file falls back to local rather than throwing", () => {
|
|
93
|
+
withHome((home) => {
|
|
94
|
+
fs.writeFileSync(path.join(home, "cicy-ai", "db", "prefs.json"), "{ not json");
|
|
95
|
+
const { pickHomepageURL, LOCAL_URL } = loadPicker();
|
|
96
|
+
assert.equal(pickHomepageURL(), LOCAL_URL);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a remote homepage always has a way back to the bundled snapshot", () => {
|
|
101
|
+
assert.match(src, /wc\.on\("did-fail-load"/);
|
|
102
|
+
assert.match(src, /setTimeout\(\(\) => toLocal\(`no load within/); // blank page that never commits
|
|
103
|
+
assert.match(src, /if \(url\.startsWith\("file:\/\/"\)\) \{ settled = true; return; \}/); // never loops on local
|
|
104
|
+
assert.match(src, /wireRemoteFallback\(wc\)/); // resident home tab
|
|
105
|
+
assert.match(src, /wireRemoteFallback\(homepage\.webContents\)/); // standalone window
|
|
106
|
+
});
|