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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.310",
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.580",
148
- "cicy-code-darwin-arm64": "2.3.580",
149
- "cicy-code-linux-x64": "2.3.580",
150
- "cicy-code-linux-arm64": "2.3.580",
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="note" data-role="note" title="点击编辑备注,Enter 保存 / Esc 取消" spellcheck="false"></span><span class="ip none" title="经该 Profile 的代理探测出口 IP / 地区">测 IP</span></div>
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
- const noteEl = row.querySelector('.note');
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');
@@ -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
- if (this.accountIdx === 0) openTab(1, u); // 特权 profile 的链接 → 沙箱 profile 1
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
- try { if (m.win.isMinimized()) m.win.restore(); } catch (e) {}
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, note, proxy drawer and ip probe", () => {
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\//);