cicy-desktop 2.1.55 → 2.1.57
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 +3 -2
- package/src/backends/local-teams.js +69 -48
- package/src/main.js +20 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cicy-desktop",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.57",
|
|
4
4
|
"description": "CiCy - AI-powered operating system browser",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
"productName": "CiCy Desktop",
|
|
27
27
|
"protocols": [
|
|
28
28
|
{
|
|
29
|
-
"name": "CiCy URL",
|
|
29
|
+
"name": "CiCy Desktop URL",
|
|
30
30
|
"schemes": [
|
|
31
|
+
"cicy-desktop",
|
|
31
32
|
"cicy"
|
|
32
33
|
]
|
|
33
34
|
}
|
|
@@ -26,7 +26,12 @@ try { __t = require("../i18n").t; } catch { __t = null; }
|
|
|
26
26
|
const unnamedName = () => { try { return (__t && __t("localTeams.unnamed")) || "Unnamed"; } catch { return "Unnamed"; } };
|
|
27
27
|
const log = require("electron-log");
|
|
28
28
|
|
|
29
|
+
// global.json is now only read for the local sidecar's api_token (auto-fill).
|
|
30
|
+
// The TEAM LIST lives in its own file: ~/cicy-ai/db/teams.json — decoupled from
|
|
31
|
+
// global.json (which cicy-code + the helper self-register into, which used to
|
|
32
|
+
// leak "Unnamed" ghosts into the team list). Shape: a flat { "<id>": {node} } map.
|
|
29
33
|
const GLOBAL_JSON = path.join(os.homedir(), "cicy-ai", "global.json");
|
|
34
|
+
const TEAMS_JSON = path.join(os.homedir(), "cicy-ai", "db", "teams.json");
|
|
30
35
|
const HEALTH_TIMEOUT_MS = 1500;
|
|
31
36
|
const CACHE_MS = 4000; // small dedupe so rapid renderer polls don't fan-out
|
|
32
37
|
|
|
@@ -44,6 +49,50 @@ function readGlobal() {
|
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
// The team map, from teams.json. One-time migration: if teams.json is absent
|
|
53
|
+
// but the legacy global.json.cicyDesktopNodes still has teams, seed teams.json
|
|
54
|
+
// from it (global.json is left untouched). Returns a { "<id>": {node} } map.
|
|
55
|
+
function readNodes() {
|
|
56
|
+
try {
|
|
57
|
+
const raw = fs.readFileSync(TEAMS_JSON, "utf8");
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (e && e.code !== "ENOENT") log.info(`[local-teams] teams.json read failed: ${e.message}`);
|
|
62
|
+
}
|
|
63
|
+
const legacy = (readGlobal()?.cicyDesktopNodes) || {};
|
|
64
|
+
if (Object.keys(legacy).length) {
|
|
65
|
+
try {
|
|
66
|
+
fs.mkdirSync(path.dirname(TEAMS_JSON), { recursive: true });
|
|
67
|
+
fs.writeFileSync(TEAMS_JSON, JSON.stringify(legacy, null, 2), { mode: 0o600 });
|
|
68
|
+
log.info(`[local-teams] migrated ${Object.keys(legacy).length} team(s) global.json → teams.json`);
|
|
69
|
+
} catch (e) { log.info(`[local-teams] teams.json migrate failed: ${e.message}`); }
|
|
70
|
+
return legacy;
|
|
71
|
+
}
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Atomic read-modify-write of teams.json. The updater gets the node map and
|
|
76
|
+
// returns the next map. Seeds from legacy global.json on first write too.
|
|
77
|
+
async function writeNodes(updater) {
|
|
78
|
+
let nodes = {};
|
|
79
|
+
try {
|
|
80
|
+
const raw = await fs.promises.readFile(TEAMS_JSON, "utf8");
|
|
81
|
+
nodes = JSON.parse(raw);
|
|
82
|
+
if (!nodes || typeof nodes !== "object") nodes = {};
|
|
83
|
+
} catch (e) {
|
|
84
|
+
if (e.code !== "ENOENT") throw e;
|
|
85
|
+
nodes = (readGlobal()?.cicyDesktopNodes) || {}; // seed from legacy
|
|
86
|
+
}
|
|
87
|
+
const next = updater(nodes) || nodes;
|
|
88
|
+
const tmp = `${TEAMS_JSON}.tmp.${process.pid}.${Date.now()}`;
|
|
89
|
+
await fs.promises.mkdir(path.dirname(TEAMS_JSON), { recursive: true });
|
|
90
|
+
await fs.promises.writeFile(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
91
|
+
await fs.promises.rename(tmp, TEAMS_JSON);
|
|
92
|
+
_cacheUntil = 0; // invalidate list() cache
|
|
93
|
+
return next;
|
|
94
|
+
}
|
|
95
|
+
|
|
47
96
|
function probeHealth(baseUrl, token) {
|
|
48
97
|
return new Promise((resolve) => {
|
|
49
98
|
let parsed;
|
|
@@ -93,8 +142,7 @@ function classify(health) {
|
|
|
93
142
|
|
|
94
143
|
async function list({ refresh = false } = {}) {
|
|
95
144
|
if (!refresh && _cache && Date.now() < _cacheUntil) return _cache;
|
|
96
|
-
const
|
|
97
|
-
const nodes = (g && g.cicyDesktopNodes) || {};
|
|
145
|
+
const nodes = readNodes();
|
|
98
146
|
const slugs = Object.keys(nodes);
|
|
99
147
|
const teams = await Promise.all(slugs.map(async (slug) => {
|
|
100
148
|
const node = nodes[slug] || {};
|
|
@@ -133,8 +181,7 @@ async function list({ refresh = false } = {}) {
|
|
|
133
181
|
// the SPA of every desktop tool, which was the regression in the
|
|
134
182
|
// previous implementation.
|
|
135
183
|
function openTeam(id) {
|
|
136
|
-
const
|
|
137
|
-
const node = g?.cicyDesktopNodes?.[id];
|
|
184
|
+
const node = readNodes()[id];
|
|
138
185
|
if (!node) return { ok: false, error: "team not found" };
|
|
139
186
|
const baseUrl = (node.base_url || "").replace(/\/$/, "");
|
|
140
187
|
if (!baseUrl) return { ok: false, error: "no base_url" };
|
|
@@ -179,8 +226,7 @@ function stripVolatile(u) {
|
|
|
179
226
|
// 刷新 action). Matches the window the same way openTeam reuses one — by
|
|
180
227
|
// origin+pathname. No-op-with-error if no window is open for the team.
|
|
181
228
|
function reloadTeam(id) {
|
|
182
|
-
const
|
|
183
|
-
const node = g?.cicyDesktopNodes?.[id];
|
|
229
|
+
const node = readNodes()[id];
|
|
184
230
|
if (!node) return { ok: false, error: "team not found" };
|
|
185
231
|
const baseUrl = (node.base_url || "").replace(/\/$/, "");
|
|
186
232
|
if (!baseUrl) return { ok: false, error: "no base_url" };
|
|
@@ -222,23 +268,6 @@ function slugifyId(input) {
|
|
|
222
268
|
.slice(0, 40);
|
|
223
269
|
}
|
|
224
270
|
|
|
225
|
-
async function writeGlobal(updater) {
|
|
226
|
-
let parsed = {};
|
|
227
|
-
try {
|
|
228
|
-
const raw = await fsp.readFile(GLOBAL_JSON, "utf8");
|
|
229
|
-
parsed = JSON.parse(raw);
|
|
230
|
-
if (!parsed || typeof parsed !== "object") parsed = {};
|
|
231
|
-
} catch (e) {
|
|
232
|
-
if (e.code !== "ENOENT") throw e;
|
|
233
|
-
}
|
|
234
|
-
const next = updater(parsed);
|
|
235
|
-
const tmp = `${GLOBAL_JSON}.tmp.${process.pid}.${Date.now()}`;
|
|
236
|
-
await fsp.mkdir(path.dirname(GLOBAL_JSON), { recursive: true });
|
|
237
|
-
await fsp.writeFile(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
238
|
-
await fsp.rename(tmp, GLOBAL_JSON);
|
|
239
|
-
_cacheUntil = 0; // invalidate list() cache
|
|
240
|
-
return next;
|
|
241
|
-
}
|
|
242
271
|
|
|
243
272
|
// Dedupe key for a team: host:port only. The same cicy-code node is the same
|
|
244
273
|
// node across platforms/protocols, so protocol, path and token never affect
|
|
@@ -286,8 +315,7 @@ async function addTeam(spec) {
|
|
|
286
315
|
// This is what the user sees in the helper flow — "rerun the installer
|
|
287
316
|
// on the same port" should rotate the token in place, not pile on a
|
|
288
317
|
// second card.
|
|
289
|
-
const
|
|
290
|
-
const existing = g?.cicyDesktopNodes || {};
|
|
318
|
+
const existing = readNodes();
|
|
291
319
|
let existingId = null;
|
|
292
320
|
for (const [k, v] of Object.entries(existing)) {
|
|
293
321
|
if (normaliseUrl(v?.base_url || "") === baseUrlKey) { existingId = k; break; }
|
|
@@ -318,10 +346,9 @@ async function addTeam(spec) {
|
|
|
318
346
|
// Drop undefined keys so we never overwrite existing fields with null.
|
|
319
347
|
Object.keys(patch).forEach(k => patch[k] === undefined && delete patch[k]);
|
|
320
348
|
|
|
321
|
-
await
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
gNext.cicyDesktopNodes[id] = {
|
|
349
|
+
await writeNodes((nodes) => {
|
|
350
|
+
const prev = nodes[id] || {};
|
|
351
|
+
nodes[id] = {
|
|
325
352
|
...prev,
|
|
326
353
|
...patch,
|
|
327
354
|
// Upsert by base_url: an EXISTING team keeps its (possibly user-renamed)
|
|
@@ -332,10 +359,10 @@ async function addTeam(spec) {
|
|
|
332
359
|
added_at: prev.added_at || now,
|
|
333
360
|
updated_at: now,
|
|
334
361
|
};
|
|
335
|
-
return
|
|
362
|
+
return nodes;
|
|
336
363
|
});
|
|
337
364
|
log.info(`[local-teams] ${existingId ? "upsert" : "add"} ${id} → ${baseUrl} (source=${patch.install_source || "n/a"})`);
|
|
338
|
-
const next = (
|
|
365
|
+
const next = readNodes()[id] || {};
|
|
339
366
|
return { ok: true, id, upserted: !!existingId, team: { id, ...next, port } };
|
|
340
367
|
}
|
|
341
368
|
|
|
@@ -370,9 +397,8 @@ async function updateTeam(id, patch) {
|
|
|
370
397
|
|
|
371
398
|
// If base_url is changing, enforce dedupe — refuse to merge two teams.
|
|
372
399
|
if (filtered.base_url) {
|
|
373
|
-
const g = readGlobal();
|
|
374
400
|
const nextKey = normaliseUrl(filtered.base_url);
|
|
375
|
-
for (const [k, v] of Object.entries(
|
|
401
|
+
for (const [k, v] of Object.entries(readNodes())) {
|
|
376
402
|
if (k === id) continue;
|
|
377
403
|
if (normaliseUrl(v?.base_url || "") === nextKey) {
|
|
378
404
|
return { ok: false, error: `another team (id=${k}) already uses that base_url` };
|
|
@@ -381,20 +407,16 @@ async function updateTeam(id, patch) {
|
|
|
381
407
|
}
|
|
382
408
|
|
|
383
409
|
let existed = false;
|
|
384
|
-
await
|
|
385
|
-
if (
|
|
410
|
+
await writeNodes((nodes) => {
|
|
411
|
+
if (Object.prototype.hasOwnProperty.call(nodes, id)) {
|
|
386
412
|
existed = true;
|
|
387
|
-
|
|
388
|
-
...g.cicyDesktopNodes[id],
|
|
389
|
-
...filtered,
|
|
390
|
-
updated_at: new Date().toISOString(),
|
|
391
|
-
};
|
|
413
|
+
nodes[id] = { ...nodes[id], ...filtered, updated_at: new Date().toISOString() };
|
|
392
414
|
}
|
|
393
|
-
return
|
|
415
|
+
return nodes;
|
|
394
416
|
});
|
|
395
417
|
if (!existed) return { ok: false, error: "team not found" };
|
|
396
418
|
log.info(`[local-teams] update ${id} → ${Object.keys(filtered).join(",")}`);
|
|
397
|
-
const next = (
|
|
419
|
+
const next = readNodes()[id] || {};
|
|
398
420
|
let port = null;
|
|
399
421
|
try { port = parseInt(new URL(next.base_url || "").port, 10) || null; } catch {}
|
|
400
422
|
return { ok: true, id, team: { id, ...next, port } };
|
|
@@ -403,12 +425,12 @@ async function updateTeam(id, patch) {
|
|
|
403
425
|
async function removeTeam(id) {
|
|
404
426
|
if (!id) return { ok: false, error: "id required" };
|
|
405
427
|
let existed = false;
|
|
406
|
-
await
|
|
407
|
-
if (
|
|
428
|
+
await writeNodes((nodes) => {
|
|
429
|
+
if (Object.prototype.hasOwnProperty.call(nodes, id)) {
|
|
408
430
|
existed = true;
|
|
409
|
-
delete
|
|
431
|
+
delete nodes[id];
|
|
410
432
|
}
|
|
411
|
-
return
|
|
433
|
+
return nodes;
|
|
412
434
|
});
|
|
413
435
|
log.info(`[local-teams] remove ${id} (existed=${existed})`);
|
|
414
436
|
return { ok: true, removed: existed };
|
|
@@ -651,8 +673,7 @@ async function upgradeDocker(node) {
|
|
|
651
673
|
|
|
652
674
|
async function upgradeTeam(id) {
|
|
653
675
|
if (!id) return { ok: false, error: "id required" };
|
|
654
|
-
const
|
|
655
|
-
const node = g?.cicyDesktopNodes?.[id];
|
|
676
|
+
const node = readNodes()[id];
|
|
656
677
|
if (!node) return { ok: false, error: "team not found" };
|
|
657
678
|
const src = String(node.install_source || "").toLowerCase();
|
|
658
679
|
const isDocker = src.includes("docker") || (!!node.container_name && !node.install_path);
|
package/src/main.js
CHANGED
|
@@ -117,12 +117,20 @@ if (!__singleLock) {
|
|
|
117
117
|
electronApp.exit(0);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
// Register cicy:// as
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
// Register cicy-desktop:// as the desktop's URL protocol. We MOVED off the bare
|
|
121
|
+
// `cicy://` scheme because it collides: the CiCy mobile/Expo app (com.cicy-ai
|
|
122
|
+
// .mobile) and a generic com.github.Electron both claim `cicy:`, so a browser
|
|
123
|
+
// click routes the deeplink to the wrong app (the desktop only got it when an
|
|
124
|
+
// already-running Electron instance happened to intercept its own scheme).
|
|
125
|
+
// `cicy-desktop://` is desktop-only → browsers route it unambiguously here.
|
|
126
|
+
// `cicy://` stays registered + accepted for back-compat with old links.
|
|
127
|
+
// On macOS the OS calls open-url; on Windows/Linux the URL arrives in argv
|
|
128
|
+
// (second-instance below, or process.argv on cold start).
|
|
129
|
+
const DEEPLINK_SCHEMES = ["cicy-desktop", "cicy"]; // primary, then legacy
|
|
130
|
+
const isDeepLink = (u) =>
|
|
131
|
+
typeof u === "string" && DEEPLINK_SCHEMES.some((s) => u.startsWith(`${s}://`));
|
|
132
|
+
for (const s of DEEPLINK_SCHEMES) {
|
|
133
|
+
try { if (!electronApp.isDefaultProtocolClient(s)) electronApp.setAsDefaultProtocolClient(s); } catch {}
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
// Deep links can arrive before any BrowserWindow exists (cold start via
|
|
@@ -165,9 +173,10 @@ electronApp.on("browser-window-created", (_e, win) => {
|
|
|
165
173
|
|
|
166
174
|
async function handleDeepLink(url) {
|
|
167
175
|
log.info(`[deeplink] handleDeepLink got: ${url}`);
|
|
168
|
-
if (!url
|
|
176
|
+
if (!isDeepLink(url)) return;
|
|
169
177
|
try {
|
|
170
|
-
// cicy://addTeam?title=My+Team&url=https://...&token=xxx
|
|
178
|
+
// cicy-desktop://addTeam?title=My+Team&url=https://...&token=xxx
|
|
179
|
+
// (legacy cicy://addTeam?... still accepted)
|
|
171
180
|
const u = new URL(url);
|
|
172
181
|
const action = (u.hostname || "").toLowerCase();
|
|
173
182
|
if (action === "addteam") {
|
|
@@ -219,13 +228,13 @@ electronApp.on("open-url", (_e, url) => {
|
|
|
219
228
|
// Cold start on Windows/Linux: protocol URL is the last argv element. macOS
|
|
220
229
|
// also gets the argv copy on some launchers, so this is harmless there.
|
|
221
230
|
{
|
|
222
|
-
const coldUrl = process.argv.find(a =>
|
|
231
|
+
const coldUrl = process.argv.find(a => isDeepLink(a));
|
|
223
232
|
if (coldUrl) handleDeepLink(coldUrl);
|
|
224
233
|
}
|
|
225
234
|
|
|
226
235
|
electronApp.on("second-instance", (_e, argv) => {
|
|
227
|
-
// argv may include cicy:// URL on Windows/Linux
|
|
228
|
-
const cicyUrl = argv.find(a => a
|
|
236
|
+
// argv may include a cicy-desktop:// (or legacy cicy://) URL on Windows/Linux
|
|
237
|
+
const cicyUrl = argv.find(a => isDeepLink(a));
|
|
229
238
|
if (cicyUrl) handleDeepLink(cicyUrl);
|
|
230
239
|
|
|
231
240
|
try {
|