cicy-desktop 2.1.310 → 2.1.313
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 +5 -5
- package/src/main.js +19 -1
- package/src/tabbrowser/telegram-matrix.html +2 -34
- package/src/tools/ipc-bridge.js +1 -1
- package/src/tools/tab-browser-tools.js +10 -3
- package/src/tools/window-tools.js +1 -1
- package/src/utils/window-utils.js +1 -1
- package/test/no-focus-steal.test.js +27 -0
- package/test/telegram-matrix.test.js +1 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cicy-desktop",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.313",
|
|
4
4
|
"description": "CiCy - AI-powered operating system browser",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -144,10 +144,10 @@
|
|
|
144
144
|
"//optionalDependencies": "(2026-06 回调): mac/linux 改回 native cicy-code(:8008,colima 在 16G mac 上压垮内存)→ 重新内置 cicy-code-<plat> / cicy-mihomo-<plat>,localbin.fromBundle 零网络 seed(npm 仅作更新通道,带 npmmirror→npmjs 回退)。这些由 scripts/sync-runtime-deps.cjs 在 tag-push 时同步到最新版。npm 的 os/cpu 字段保证每个平台只装自己那份。Windows 仍走 docker(WSL :8009),它那份 cicy-code-windows 用不到但 bundle 着无害。",
|
|
145
145
|
"optionalDependencies": {
|
|
146
146
|
"electron": "41.0.3",
|
|
147
|
-
"cicy-code-darwin-x64": "2.3.
|
|
148
|
-
"cicy-code-darwin-arm64": "2.3.
|
|
149
|
-
"cicy-code-linux-x64": "2.3.
|
|
150
|
-
"cicy-code-linux-arm64": "2.3.
|
|
147
|
+
"cicy-code-darwin-x64": "2.3.581",
|
|
148
|
+
"cicy-code-darwin-arm64": "2.3.581",
|
|
149
|
+
"cicy-code-linux-x64": "2.3.581",
|
|
150
|
+
"cicy-code-linux-arm64": "2.3.581",
|
|
151
151
|
"cicy-code-windows-x64": "2.3.193",
|
|
152
152
|
"cicy-mihomo-darwin-x64": "1.10.4",
|
|
153
153
|
"cicy-mihomo-darwin-arm64": "1.10.4",
|
package/src/main.js
CHANGED
|
@@ -358,6 +358,24 @@ if (oneWindow) {
|
|
|
358
358
|
// Setup logging
|
|
359
359
|
setupLogging(config);
|
|
360
360
|
wrapLogger();
|
|
361
|
+
// ── focus audit ──────────────────────────────────────────────────────────────
|
|
362
|
+
// 「cicy-desktop 莫名其妙跳到前台」修了多次都没根治,因为事后无法知道是谁调的 show/focus。
|
|
363
|
+
// 这里给 BrowserWindow 的抢前台方法打点:每次调用记一行 [focus-audit] + 调用栈前几帧。
|
|
364
|
+
// 只记日志,不改行为。排查时 grep 主日志里的 focus-audit 即可。
|
|
365
|
+
try {
|
|
366
|
+
const { BrowserWindow: _BW } = require("electron");
|
|
367
|
+
for (const method of ["show", "focus", "restore", "moveTop"]) {
|
|
368
|
+
const orig = _BW.prototype[method];
|
|
369
|
+
if (typeof orig !== "function") continue;
|
|
370
|
+
_BW.prototype[method] = function (...args) {
|
|
371
|
+
try {
|
|
372
|
+
const frames = String(new Error().stack || "").split("\n").slice(2, 6).map((l) => l.trim().replace(/^at /, "")).join(" ← ");
|
|
373
|
+
log.info(`[focus-audit] ${method}() win=${this.id} title="${(this.getTitle && this.getTitle()) || ""}" visible=${this.isVisible && this.isVisible()} focused=${this.isFocused && this.isFocused()} ← ${frames}`);
|
|
374
|
+
} catch (e) {}
|
|
375
|
+
return orig.apply(this, args);
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
} catch (e) {}
|
|
361
379
|
|
|
362
380
|
log.info("[MCP] Server starting at", new Date().toISOString());
|
|
363
381
|
|
|
@@ -1579,7 +1597,7 @@ electronApp.whenReady().then(async () => {
|
|
|
1579
1597
|
if (!e.url) continue;
|
|
1580
1598
|
if (liveSet.has(`${e.accountIdx || 0}::${registry.normalizeUrl(e.url)}`)) continue;
|
|
1581
1599
|
log.info(`[WindowRegistry] Reopening ${e.url} (account ${e.accountIdx || 0})`);
|
|
1582
|
-
const opts = { url: e.url };
|
|
1600
|
+
const opts = { url: e.url, background: true }; // 启动时自动重开:后台,不抢焦点
|
|
1583
1601
|
if (e.bounds && typeof e.bounds === "object") Object.assign(opts, e.bounds);
|
|
1584
1602
|
createWindow(opts, e.accountIdx || 0, true);
|
|
1585
1603
|
}
|
|
@@ -39,11 +39,7 @@
|
|
|
39
39
|
.line2 .tg { flex: none; max-width: 55%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #b4b4bb; }
|
|
40
40
|
.line2 .tg b { color: var(--fg); font-weight: 600; }
|
|
41
41
|
.line2 .tg.none { color: #6b6b72; }
|
|
42
|
-
.line2 .note { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #9a9aa2; cursor: text; outline: 0; border-radius: 4px; padding: 0 4px; margin: 0 -4px; }
|
|
43
|
-
.line2 .note.empty { color: #55555c; font-style: italic; }
|
|
44
|
-
.line2 .note:hover { color: #fff; }
|
|
45
42
|
/* 编辑态就地变成可输入,尺寸/位置与只读态完全一致,不发生位移 */
|
|
46
|
-
.line2 .note.editing { color: var(--fg); font-style: normal; background: #111113; box-shadow: 0 0 0 1px var(--accent); text-overflow: clip; }
|
|
47
43
|
.line2 .ip { flex: none; max-width: 45%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 0 6px; border-radius: 999px; background: var(--head); color: #b4b4bb; font: 11px ui-monospace, monospace; line-height: 16px; cursor: pointer; }
|
|
48
44
|
.line2 .ip.none { color: #6b6b72; font-family: inherit; }
|
|
49
45
|
.line2 .ip.busy { color: #f0b429; }
|
|
@@ -229,7 +225,7 @@
|
|
|
229
225
|
row.className = 'row'; row.dataset.id = String(p.accountIdx); row.title = `persist:sandbox-${p.accountIdx}`;
|
|
230
226
|
row.innerHTML = `
|
|
231
227
|
<div class="line1"><span class="dot"></span><span class="name">${esc(p.name)}</span><span class="chip">#${p.accountIdx}</span><span class="st"></span><button class="icon gear" title="代理设置">⚙</button><button class="icon reload" title="重新加载">⟳</button></div>
|
|
232
|
-
<div class="line2"><span class="tg none">—</span><span class="
|
|
228
|
+
<div class="line2"><span class="tg none">—</span><span class="ip none" title="经该 Profile 的代理探测出口 IP / 地区">测 IP</span></div>
|
|
233
229
|
<div class="cfg">
|
|
234
230
|
<div class="f"><label>代理</label><input data-role="proxy" placeholder="留空 = 直连,例如 http://127.0.0.1:20001" spellcheck="false"><button class="sm save">保存</button><button class="sm ghost test">测 IP</button></div>
|
|
235
231
|
<div class="hint">保存后已打开的会话会重新加载;<b>测 IP</b> 经该代理访问 IP 服务,结果显示在上方。</div>
|
|
@@ -238,35 +234,7 @@
|
|
|
238
234
|
</div>
|
|
239
235
|
<div class="row-err"></div>`;
|
|
240
236
|
const errEl = row.querySelector('.row-err');
|
|
241
|
-
|
|
242
|
-
const showNote = () => { noteEl.textContent = p.note || '添加备注'; noteEl.classList.toggle('empty', !p.note); };
|
|
243
|
-
showNote();
|
|
244
|
-
let editing = false;
|
|
245
|
-
const startEdit = (e) => {
|
|
246
|
-
e && e.stopPropagation();
|
|
247
|
-
if (editing) return;
|
|
248
|
-
editing = true;
|
|
249
|
-
noteEl.classList.add('editing'); noteEl.classList.remove('empty');
|
|
250
|
-
noteEl.textContent = p.note || '';
|
|
251
|
-
noteEl.contentEditable = 'plaintext-only';
|
|
252
|
-
noteEl.focus();
|
|
253
|
-
const range = document.createRange(); range.selectNodeContents(noteEl);
|
|
254
|
-
const sel = getSelection(); sel.removeAllRanges(); sel.addRange(range);
|
|
255
|
-
};
|
|
256
|
-
const stopEdit = () => { editing = false; noteEl.contentEditable = 'false'; noteEl.classList.remove('editing'); showNote(); };
|
|
257
|
-
const saveNote = async () => {
|
|
258
|
-
if (!editing) return;
|
|
259
|
-
const value = noteEl.textContent.replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
260
|
-
stopEdit();
|
|
261
|
-
if (value === (p.note || '')) return;
|
|
262
|
-
try {
|
|
263
|
-
const updated = await window.panelAPI.setProfileNote(p.accountIdx, value);
|
|
264
|
-
p.note = updated.note; showNote();
|
|
265
|
-
} catch (e) { errEl.textContent = e.message || String(e); errEl.classList.add('on'); }
|
|
266
|
-
};
|
|
267
|
-
noteEl.onclick = (e) => { e.stopPropagation(); startEdit(e); };
|
|
268
|
-
noteEl.onkeydown = (e) => { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); saveNote(); } if (e.key === 'Escape') { e.preventDefault(); stopEdit(); } };
|
|
269
|
-
noteEl.onblur = () => saveNote();
|
|
237
|
+
// 备注功能已按需求移除(列表只保留 名称 / @用户名 / 状态 / IP)
|
|
270
238
|
// 代理设置抽屉(⚙):不常用,默认收起,列表保持干净。
|
|
271
239
|
const proxyInput = row.querySelector('[data-role="proxy"]');
|
|
272
240
|
const saveBtn = row.querySelector('.cfg .save');
|
package/src/tools/ipc-bridge.js
CHANGED
|
@@ -14,7 +14,7 @@ if (!global._cicyIpcBridge) {
|
|
|
14
14
|
const { url, title } = data || {};
|
|
15
15
|
if (!url) return;
|
|
16
16
|
log.info(`[IPC Bridge] open-window: ${url} ${title || ""}`);
|
|
17
|
-
const win = createWindow({ url }, 0, true);
|
|
17
|
+
const win = createWindow({ url, background: true }, 0, true); // 渲染进程请求:后台开,不抢焦点
|
|
18
18
|
if (title) win.setTitle(title);
|
|
19
19
|
});
|
|
20
20
|
|
|
@@ -268,6 +268,9 @@ class TabManager {
|
|
|
268
268
|
const winOpts = {
|
|
269
269
|
width: 1180,
|
|
270
270
|
height: 820,
|
|
271
|
+
// 先不显示:由 openTab 决定 show+focus(用户明确要求)还是 showInactive(程序/页面触发)。
|
|
272
|
+
// 默认 show:true 会在新建 profile 窗口的瞬间把 app 抢到前台。
|
|
273
|
+
show: false,
|
|
271
274
|
backgroundColor: STRIP_BG,
|
|
272
275
|
title: accountIdx === 0 ? "CiCy Desktop" : `CiCy Browser · sandbox-${accountIdx}`,
|
|
273
276
|
// Drop the native title-bar row — the tab strip becomes the top of the window
|
|
@@ -305,6 +308,8 @@ class TabManager {
|
|
|
305
308
|
} catch (e) {}
|
|
306
309
|
}
|
|
307
310
|
this.win = new BrowserWindow(winOpts);
|
|
311
|
+
// 兜底:无论谁建的窗口,3s 内没人 show/showInactive 就静默显示,窗口绝不会永远隐身。
|
|
312
|
+
setTimeout(() => { try { if (!this.win.isDestroyed() && !this.win.isVisible()) this.win.showInactive(); } catch (e) {} }, 3000);
|
|
308
313
|
try {
|
|
309
314
|
this.win.cicyAccountIdx = accountIdx;
|
|
310
315
|
this.win.webContents.cicyAccountIdx = accountIdx;
|
|
@@ -490,7 +495,9 @@ class TabManager {
|
|
|
490
495
|
// 沙箱 profile(accountIdx ≥ 1)自身的 window.open 本就没有桥,仍开成本 profile 的 tab。
|
|
491
496
|
try { wc.setWindowOpenHandler(({ url: u }) => {
|
|
492
497
|
try {
|
|
493
|
-
|
|
498
|
+
// activate:false —— 这是页面自己(window.open/重定向)触发的,不是用户在看 cicy-desktop;
|
|
499
|
+
// 不能因此把窗口抢到前台(用户报的「莫名其妙跳到前台」主因之一)。
|
|
500
|
+
if (this.accountIdx === 0) openTab(1, u, { activate: false }); // 特权 profile 的链接 → 沙箱 profile 1
|
|
494
501
|
else this.addTab(u); // 沙箱 profile → 本 profile 内开 tab
|
|
495
502
|
} catch (e) {}
|
|
496
503
|
return { action: "deny" };
|
|
@@ -508,7 +515,7 @@ class TabManager {
|
|
|
508
515
|
let curOrigin = ""; try { curOrigin = new URL(wc.getURL()).origin; } catch {}
|
|
509
516
|
if (tgt.origin !== curOrigin && !(tab.team && isInstanceAuthNavigation(wc.getURL(), u))) {
|
|
510
517
|
e.preventDefault();
|
|
511
|
-
openTab(1, u);
|
|
518
|
+
openTab(1, u, { activate: false }); // 页面驱动,不抢焦点
|
|
512
519
|
}
|
|
513
520
|
} catch (err) {}
|
|
514
521
|
});
|
|
@@ -1024,7 +1031,7 @@ function registerTabBrowserTools(registerTool) {
|
|
|
1024
1031
|
if (success) {
|
|
1025
1032
|
// agent 驱动的切标签:只切视图、静默保证窗口可见,绝不从用户当前 app 抢焦点
|
|
1026
1033
|
// (之前这里 show()+focus() + mac app.focus({steal:true}) 会让 cicy-desktop 突然跳到前台)。
|
|
1027
|
-
|
|
1034
|
+
// 不 restore():Windows 上 restore() 会激活窗口(=抢前台)。最小化就保持最小化。
|
|
1028
1035
|
m.surfaceQuiet();
|
|
1029
1036
|
}
|
|
1030
1037
|
return ok({ success, webContentsId });
|
|
@@ -206,7 +206,7 @@ function registerTools(registerTool) {
|
|
|
206
206
|
const registry = require("../utils/window-registry");
|
|
207
207
|
const entry = registry.getByKey(window_key);
|
|
208
208
|
if (!entry) throw new Error(`windowKey ${window_key} not found in registry`);
|
|
209
|
-
const opts = { url: entry.url };
|
|
209
|
+
const opts = { url: entry.url, background: true }; // agent 驱动:后台开,不抢焦点
|
|
210
210
|
if (entry.bounds && typeof entry.bounds === "object") Object.assign(opts, entry.bounds);
|
|
211
211
|
const win = createWindow(opts, entry.accountIdx || 0, true);
|
|
212
212
|
return {
|
|
@@ -83,7 +83,7 @@ function windowOpenDecision(url, { wc } = {}) {
|
|
|
83
83
|
// its own profile N. Never in-session for profile 0.
|
|
84
84
|
const openerIdx = accountIdxOfWebContents(wc);
|
|
85
85
|
const target = openerIdx === 0 ? 1 : openerIdx;
|
|
86
|
-
try { createWindow({ url }, target, true); }
|
|
86
|
+
try { createWindow({ url, background: true }, target, true); } // 页面驱动:后台开,不抢焦点
|
|
87
87
|
catch (e) { log.warn(`[WindowOpen] open external in profile ${target} failed: ${e && e.message}`); }
|
|
88
88
|
return { action: "deny" };
|
|
89
89
|
}
|
|
@@ -18,3 +18,30 @@ test("agent-driven tab activate does not bring the window to front", () => {
|
|
|
18
18
|
assert.doesNotMatch(block, /m\.win\.focus\(\)|app\.focus\(\{ steal: true \}\)/);
|
|
19
19
|
assert.match(block, /m\.surfaceQuiet\(\)/);
|
|
20
20
|
});
|
|
21
|
+
|
|
22
|
+
test("page-driven opens (window.open / cross-origin navigate) never activate the window", () => {
|
|
23
|
+
const src = read("src/tools/tab-browser-tools.js");
|
|
24
|
+
assert.match(src, /if \(this\.accountIdx === 0\) openTab\(1, u, \{ activate: false \}\)/);
|
|
25
|
+
assert.match(src, /e\.preventDefault\(\);\s*openTab\(1, u, \{ activate: false \}\)/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("tab windows are created hidden and only surfaced by openTab / a quiet fallback", () => {
|
|
29
|
+
const src = read("src/tools/tab-browser-tools.js");
|
|
30
|
+
const i = src.indexOf("const winOpts = {");
|
|
31
|
+
assert.match(src.slice(i, i + 600), /show: false,/);
|
|
32
|
+
const j = src.indexOf("this.win = new BrowserWindow(winOpts);");
|
|
33
|
+
assert.match(src.slice(j, j + 400), /showInactive\(\)/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("non-click createWindow paths open in the background", () => {
|
|
37
|
+
assert.match(read("src/utils/window-utils.js"), /createWindow\(\{ url, background: true \}, target, true\)/);
|
|
38
|
+
assert.match(read("src/tools/ipc-bridge.js"), /createWindow\(\{ url, background: true \}, 0, true\)/);
|
|
39
|
+
assert.match(read("src/tools/window-tools.js"), /const opts = \{ url: entry\.url, background: true \}/);
|
|
40
|
+
assert.match(read("src/main.js"), /const opts = \{ url: e\.url, background: true \}/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("focus audit hooks show/focus/restore/moveTop", () => {
|
|
44
|
+
const main = read("src/main.js");
|
|
45
|
+
assert.match(main, /\[focus-audit\]/);
|
|
46
|
+
assert.match(main, /\["show", "focus", "restore", "moveTop"\]/);
|
|
47
|
+
});
|
|
@@ -80,7 +80,7 @@ test("reloading an unopened profile creates its cell instead of a bare reload",
|
|
|
80
80
|
assert.match(reload, /if \(!fresh\)[^]*panelAPI\.reload/);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
test("matrix page exposes profile table, phone preview,
|
|
83
|
+
test("matrix page exposes profile table, phone preview, proxy drawer and ip probe", () => {
|
|
84
84
|
const html = fs.readFileSync(path.join(__dirname, "..", "src", "tabbrowser", "telegram-matrix.html"), "utf8");
|
|
85
85
|
assert.match(html, /id="rows"/);
|
|
86
86
|
assert.match(html, /id="ov-error"/);
|
|
@@ -95,8 +95,6 @@ test("matrix page exposes profile table, phone preview, note, proxy drawer and i
|
|
|
95
95
|
assert.match(html, /id="add-profile"/);
|
|
96
96
|
assert.match(html, /class="cfg"[^]*data-role="proxy"/);
|
|
97
97
|
assert.match(html, /\.row\.cfg-open \.cfg \{ display: block; \}/);
|
|
98
|
-
assert.match(html, /panelAPI\.setProfileNote/);
|
|
99
|
-
assert.match(html, /data-role="note"/);
|
|
100
98
|
assert.match(html, /class="tg none"/);
|
|
101
99
|
assert.match(html, /id="phone-preview"/);
|
|
102
100
|
assert.match(html, /https:\/\/web\.telegram\.org\/k\//);
|