cicy-desktop 2.1.359 → 2.1.361

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.359",
3
+ "version": "2.1.361",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -392,29 +392,53 @@ function register({ sidecarLogPath } = {}) {
392
392
  setInterval(() => { reconcileDocker().catch(() => {}); }, 60000); // keep fresh + self-heal
393
393
  }
394
394
 
395
- // 宿主 Chrome 代理(始终开启):从容器 cp mihomo 配置 → host-mihomo 在宿主起/续。
396
- // 已在跑也同步一次配置(容器侧节点可能被云端更新),变了才重启。幂等。二进制不在会自动下。
395
+ // 宿主 Chrome / Electron profile 代理(始终开启,且**不依赖 WSL 或容器**)。
396
+ //
397
+ // 每个 profile 的会话被指向 127.0.0.1:(20000+idx),所以这个 mihomo 必须存在,
398
+ // 否则那些 webview 一律 ERR_PROXY_CONNECTION_FAILED 白板。以前这里要先从
399
+ // cicy-code 容器里 cp 出 mihomo.yaml,没有容器(没装 WSL / WSL 坏了)就什么都不做 ——
400
+ // xs-master 就是这样:没有 WSL、没有容器,mihomo 从来没被装过,profile 1 的
401
+ // Telegram webview 一直白板(2026-09-13 实测)。
402
+ //
403
+ // 现在的语义:**默认装、默认起**。容器配置能读到就用它(云端下发的真实节点),
404
+ // 读不到就用宿主已有的 mihomo-host.yaml;都没有就按本机实际 profile 生成一份
405
+ // DIRECT 兜底配置先跑起来。等容器/云端配置到位,下一轮 reconcile 会覆盖它。
406
+ function hostProfileIdxs() {
407
+ try {
408
+ const store = require("../profiles/profile-store");
409
+ const ids = [];
410
+ for (const b of ["chrome", "electron"]) {
411
+ for (const p of store.listProfiles(b) || []) {
412
+ const n = Number(p && (p.accountIdx ?? p.idx));
413
+ if (Number.isInteger(n) && n >= 1) ids.push(n);
414
+ }
415
+ }
416
+ return [...new Set(ids)];
417
+ } catch (e) { return []; }
418
+ }
419
+
397
420
  async function maybeStartChromeProxy() {
398
421
  if (!APP_DOCKER_SUPPORTED) return;
399
422
  // 「独立管理」开关:标记文件在 → 不从容器同步覆盖宿主配置,host mihomo
400
423
  // 用宿主自己的 mihomo-host.yaml 独立跑(没跑就用现有宿主配置起来)。幂等。
401
424
  if (hostMihomo.standalonePinned()) {
402
- if (!hostMihomo.running() && hostMihomo.binPresent()) {
403
- try { await hostMihomo.enable({ containerYaml: null }); }
425
+ if (!hostMihomo.running()) {
426
+ try { await hostMihomo.enable({ containerYaml: null, profileIdxs: hostProfileIdxs() }); }
404
427
  catch (e) { log.warn(`[chrome-proxy] standalone start failed: ${e.message}`); }
405
428
  }
406
429
  return;
407
430
  }
431
+ // 容器读不到不再是致命的 —— catch 成 null,继续往独立模式走。
408
432
  const [yaml, selections] = await Promise.all([
409
- appDocker.readMihomoConfig(APP_CONTAINER),
410
- appDocker.readMihomoSelections(APP_CONTAINER),
433
+ appDocker.readMihomoConfig(APP_CONTAINER).catch(() => null),
434
+ appDocker.readMihomoSelections(APP_CONTAINER).catch(() => ({})),
411
435
  ]);
412
436
  if (hostMihomo.binPresent() && hostMihomo.running()) {
413
- if (hostMihomo.writeConfig(yaml)) hostMihomo.start({ force: true });
414
- await hostMihomo.syncSelections(selections);
437
+ if (yaml) { if (hostMihomo.writeConfig(yaml)) hostMihomo.start({ force: true }); await hostMihomo.syncSelections(selections); }
415
438
  return;
416
439
  }
417
- await hostMihomo.enable({ containerYaml: yaml, selections });
440
+ try { await hostMihomo.enable({ containerYaml: yaml, selections, profileIdxs: hostProfileIdxs() }); }
441
+ catch (e) { log.warn(`[chrome-proxy] enable failed: ${e.message}`); }
418
442
  }
419
443
 
420
444
  ipcMain.handle("sidecar:status", async () => {
package/src/main.js CHANGED
@@ -44,15 +44,23 @@ electronApp.on("web-contents-created", (_e, wc) => {
44
44
  // attachContextMenu). Wire copy directly on the guest. COPY-ONLY and WITHOUT
45
45
  // preventDefault, so the gotty terminal's Ctrl+C=SIGINT and any web app's own
46
46
  // copy handler still fire; wc.copy() is a no-op when there's no selection.
47
+ // 域名注入的**唯一统一入口**:凡是承载站点页面的 webContents 都走 domain-inject。
48
+ // · webview —— <webview> guest(如搜群后台里内嵌的 Telegram)
49
+ // · browserView —— 浏览器标签页 和 矩阵面板的格子(panel-cells.js 建的)
50
+ // BrowserWindow 自己的主 webContents 由 window-utils.js 挂,这里不重复。
51
+ //
52
+ // 以前这里只判 webview,于是 **标签页和面板格子一个都没注入**(都是 BrowserView):
53
+ // 实测 xs-master 的 desktop.cicy-ai.com / hub / 矩阵面板格子,__cicyInjected 全是 false,
54
+ // electronRPC 未定义 —— 放进 inject 目录的脚本在这些页面里从来不生效(2026-09-13)。
55
+ try {
56
+ const _t = wc.getType && wc.getType();
57
+ if (_t === "webview" || _t === "browserView") {
58
+ const { applyDomainInject } = require("./utils/domain-inject");
59
+ wc.on("dom-ready", () => applyDomainInject(wc));
60
+ }
61
+ } catch (_) {}
47
62
  try {
48
63
  if (wc.getType && wc.getType() === "webview") {
49
- // 所有 <webview> guest 都走 domain-inject:inject 文件读取逻辑原本只挂在
50
- // BrowserWindow 的主 webContents 上(window-utils.js),tab 里嵌套的 webview 拿不到。
51
- // 这里在 app 级 web-contents-created 上统一给每个 webview 挂 dom-ready 注入。
52
- try {
53
- const { applyDomainInject } = require("./utils/domain-inject");
54
- wc.on("dom-ready", () => applyDomainInject(wc));
55
- } catch (_) {}
56
64
  wc.on("before-input-event", (_ev, input) => {
57
65
  if (
58
66
  input.type === "keyDown" &&
@@ -290,12 +290,47 @@ function start({ force = false } = {}) {
290
290
  // without WSL at all. Only "no container config AND no host config" leaves us
291
291
  // nothing to start. Standalone has no authoritative selection source, so the
292
292
  // selection sync is skipped rather than overriding whatever the host holds.
293
- async function enable({ containerYaml, selections = {}, emit } = {}) {
293
+ // 没有任何配置可用时的兜底:按机器上实际存在的 profile 生成 listener
294
+ // (约定 端口 = 20000 + accountIdx,免鉴权、只绑 127.0.0.1),规则 MATCH,DIRECT。
295
+ //
296
+ // 为什么要有它:没有 WSL / 没有 cicy-code 容器的机器(xs-master 就是)拿不到云端
297
+ // 下发的 mihomo.yaml,原来这里直接抛 noContainerConfig —— 于是 mihomo 连装都不装,
298
+ // 那些 profile 的会话指向 127.0.0.1:2000N 却无人监听,webview 全部
299
+ // ERR_PROXY_CONNECTION_FAILED 白板(2026-09-13 在 xs-master 实测)。
300
+ // 先 DIRECT 让它通,之后容器/云端真配置一到,reconcile 循环会覆盖掉这份兜底。
301
+ function defaultHostConfig(idxs) {
302
+ const list = (Array.isArray(idxs) && idxs.length ? idxs : [1])
303
+ .map((n) => Number(n)).filter((n) => Number.isInteger(n) && n >= 1 && n <= 999);
304
+ const host = {
305
+ "mixed-port": HOST_MIXED,
306
+ "allow-lan": false,
307
+ "log-level": "warning",
308
+ "external-controller": `127.0.0.1:${HOST_CTRL}`,
309
+ secret: "",
310
+ dns: { enable: false },
311
+ listeners: [...new Set(list)].sort((a, b) => a - b).map((n) => ({
312
+ name: `chrome-profile-${n}`, type: "mixed", port: 20000 + n, listen: "127.0.0.1",
313
+ })),
314
+ proxies: [],
315
+ "proxy-groups": [],
316
+ rules: ["MATCH,DIRECT"],
317
+ };
318
+ return yaml.dump(host, { lineWidth: -1 });
319
+ }
320
+
321
+ function writeDefaultConfig(idxs) {
322
+ const next = defaultHostConfig(idxs);
323
+ fs.mkdirSync(path.dirname(HOST_CONFIG), { recursive: true });
324
+ fs.writeFileSync(HOST_CONFIG, next);
325
+ return true;
326
+ }
327
+
328
+ async function enable({ containerYaml, selections = {}, emit, profileIdxs } = {}) {
294
329
  await ensureBinary({ emit });
295
330
  let changed = false, standalone = false;
296
331
  if (containerYaml) changed = writeConfig(containerYaml);
297
332
  else if (fs.existsSync(HOST_CONFIG)) standalone = true;
298
- else throw new Error(tt("noContainerConfig"));
333
+ else { changed = writeDefaultConfig(profileIdxs); standalone = true; } // 完全独立:自己造一份能跑的
299
334
  const res = start({ force: changed });
300
335
  const synced = standalone ? { updated: [] } : await syncSelections(selections);
301
336
  emit && emit({ phase: "chrome-proxy", status: "running", message: tt("ready") });
@@ -303,6 +338,8 @@ async function enable({ containerYaml, selections = {}, emit } = {}) {
303
338
  }
304
339
 
305
340
  module.exports = {
341
+ defaultHostConfig,
342
+ writeDefaultConfig,
306
343
  VER, assetUrl, binPath, binPresent, ensureBinary,
307
344
  buildHostConfig, writeConfig, planSelectionUpdates, syncSelections,
308
345
  start, stop, running, enable, standalonePinned,
@@ -385,9 +385,23 @@ class PanelCells {
385
385
  }
386
386
  }
387
387
 
388
+ // 面板页里的 <webview partition="persist:sandbox-N"> 用的是同一批 partition,但它不走
389
+ // PanelCells,没人给它设代理 —— 不设就是直连,账号的真实出口 IP 直接暴露。
390
+ // 面板一打开就把所有 electron profile 的 partition 代理都绑上,webview 和 BrowserView
391
+ // 两种格子都覆盖到。ensureCellSessionProxy 自带去重,重复调用无副作用。
392
+ function ensureAllProfileProxies() {
393
+ try {
394
+ const profileStore = require("../profiles/profile-store");
395
+ for (const p of profileStore.listProfiles("electron") || []) {
396
+ const idx = Number(p && p.accountIdx);
397
+ if (Number.isInteger(idx) && idx > 0) ensureCellSessionProxy(idx);
398
+ }
399
+ } catch (e) {}
400
+ }
401
+
388
402
  function cellsForTab(manager, tab) {
389
403
  let pc = registry.get(tab.id);
390
- if (!pc) { pc = new PanelCells(manager, tab); registry.set(tab.id, pc); }
404
+ if (!pc) { pc = new PanelCells(manager, tab); registry.set(tab.id, pc); ensureAllProfileProxies(); }
391
405
  return pc;
392
406
  }
393
407
 
@@ -414,6 +428,10 @@ function installIpc(findTab) {
414
428
  ipcMain.handle("panelcells:states", (e) => { const pc = ctx(e); return pc ? pc.states() : []; });
415
429
  ipcMain.handle("panelcells:profiles", (e) => {
416
430
  if (!ctx(e)) return [];
431
+ // 面板页开局必调这个接口。借它把所有 profile 的 partition 代理绑一遍 ——
432
+ // 页面里的 <webview partition="persist:sandbox-N"> 不走 PanelCells,没人给它设代理,
433
+ // 不设就是直连,账号真实出口 IP 直接暴露。ensureCellSessionProxy 自带去重。
434
+ ensureAllProfileProxies();
417
435
  try {
418
436
  return require("../profiles/profile-store").listProfiles("electron")
419
437
  .filter((p) => Number.isInteger(Number(p.accountIdx)) && Number(p.accountIdx) > 0 && Number(p.accountIdx) !== 9)
@@ -160,10 +160,18 @@ function buildTabWebPreferences(accountIdx, partition, target, opts = {}) {
160
160
  // home is system-driven (openHomeWindow), never caller-URL-reachable, so it
161
161
  // keeps its privileges without a URL check.
162
162
  if (opts.home) { wp.preload = HOMEPAGE_PRELOAD; wp.webviewTag = true; wp.allowRunningInsecureContent = true; wp.sandbox = false; }
163
- // split panel page: its cells are main-managed BrowserViews (panel-cells.js),
164
- // driven over panelAPI IPC. No webviewTag needed (that path is the file://-dev
165
- // fallback below). Sandbox stays ON the preload only uses contextBridge/ipc.
166
- else if (typeof target === "string" && (target.startsWith(PANEL_URL_BASE) || target.startsWith("cicyui://panel"))) { wp.preload = PANEL_PRELOAD; }
163
+ // split panel page: cells may be main-managed BrowserViews (panel-cells.js, over
164
+ // panelAPI IPC) OR in-page <webview>. BrowserViews are painted by the main process
165
+ // at viewport coordinates, so they do not scroll with the page and are not clipped
166
+ // by the grid's overflow with more than a screenful of cells that is unusable.
167
+ // <webview> lives in the DOM and scrolls/clips for free, so the panel needs
168
+ // webviewTag on. sandbox must go off for the tag to be usable; the preload still
169
+ // only exposes contextBridge/ipcRenderer, and the panel page is our own origin.
170
+ else if (typeof target === "string" && (target.startsWith(PANEL_URL_BASE) || target.startsWith("cicyui://panel"))) {
171
+ wp.preload = PANEL_PRELOAD;
172
+ wp.webviewTag = true;
173
+ wp.sandbox = false;
174
+ }
167
175
  // Every other profile-0 tab carries the electronRPC bridge (WEBVIEW_PRELOAD),
168
176
  // but the bridge is INERT until the page's origin is authorized: the first
169
177
  // rpc:guarded call from a non-allowlisted origin pops a consent modal