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 +1 -1
- package/src/backends/sidecar-ipc.js +33 -9
- package/src/main.js +15 -7
- package/src/sidecar/host-mihomo.js +39 -2
- package/src/tabbrowser/panel-cells.js +19 -1
- package/src/tools/tab-browser-tools.js +12 -4
package/package.json
CHANGED
|
@@ -392,29 +392,53 @@ function register({ sidecarLogPath } = {}) {
|
|
|
392
392
|
setInterval(() => { reconcileDocker().catch(() => {}); }, 60000); // keep fresh + self-heal
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
// 宿主 Chrome
|
|
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()
|
|
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
|
-
|
|
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
|
|
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:
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
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
|