cicy-desktop 2.1.342 → 2.1.358

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.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/scripts/build-homepage.cjs +16 -1
  3. package/src/app-updater.js +30 -0
  4. package/src/backends/homepage-preload.js +3 -1
  5. package/src/backends/homepage-react/assets/index-Buyk0RlW.js +655 -0
  6. package/src/backends/homepage-react/assets/index-CVk0uc2g.css +1 -0
  7. package/src/backends/homepage-react/index.html +5 -3
  8. package/src/backends/homepage-react/panel/facebook-matrix.html +305 -87
  9. package/src/backends/homepage-react/panel/ipcheck.html +112 -0
  10. package/src/backends/homepage-react/panel/matrix.html +595 -0
  11. package/src/backends/homepage-react/panel/telegram-matrix.html +96 -22
  12. package/src/backends/homepage-react/panel/tiktok-matrix.html +599 -0
  13. package/src/backends/hub-client.js +108 -2
  14. package/src/backends/sidecar-ipc.js +33 -6
  15. package/src/main.js +67 -11
  16. package/src/sidecar/host-mihomo.js +47 -2
  17. package/src/tabbrowser/facebook-identity.js +12 -1
  18. package/src/tabbrowser/facebook-matrix.html +305 -87
  19. package/src/tabbrowser/fb-login.js +115 -0
  20. package/src/tabbrowser/newtab-protocol.js +6 -1
  21. package/src/tabbrowser/panel-cells.js +14 -1
  22. package/src/tabbrowser/telegram-matrix.html +96 -22
  23. package/src/tabbrowser/telegram-web-preferences.js +5 -1
  24. package/src/tabbrowser/tiktok-matrix.html +305 -87
  25. package/src/tools/tab-browser-tools.js +21 -100
  26. package/src/tray.js +22 -0
  27. package/test/bootstrap-failure-reporting.test.js +1 -1
  28. package/test/facebook-identity.test.js +3 -1
  29. package/test/facebook-matrix.test.js +2 -1
  30. package/test/telegram-matrix.test.js +2 -2
  31. package/test/tiktok-matrix.test.js +2 -1
  32. package/tools/arm-fblogin.js +32 -0
  33. package/tools/wsd-ws +114 -0
  34. package/workers/render/index.html +3 -1
  35. package/workers/render/package.json +2 -3
  36. package/workers/render/public/panel/facebook-matrix.html +305 -87
  37. package/workers/render/public/panel/ipcheck.html +112 -0
  38. package/workers/render/public/panel/matrix.html +595 -0
  39. package/workers/render/public/panel/telegram-matrix.html +96 -22
  40. package/workers/render/public/panel/tiktok-matrix.html +599 -0
  41. package/workers/render/src/App.css +4 -0
  42. package/workers/render/src/App.jsx +917 -52
  43. package/workers/render/stamp-panel-version.mjs +16 -0
  44. package/workers/render/worker.js +1 -1
  45. package/src/backends/homepage-react/assets/index-C6Zrdgc0.css +0 -1
  46. package/src/backends/homepage-react/assets/index-OG6JB73F.js +0 -411
  47. package/src/tabbrowser/cicy-code-tab-restore.js +0 -10
  48. package/test/cicy-code-tab-restore.test.js +0 -21
@@ -12,6 +12,10 @@
12
12
  // hub:open {id} → one-time grant URL → opened as a team tab; the
13
13
  // grant sets the domain-wide hub session cookie so
14
14
  // later opens need no login at all
15
+ // hub:projects {id} → that node's projects, each with its agents —
16
+ // read straight off the node's own API through its
17
+ // hub hostname (the gateway accepts the owner's hub
18
+ // token as bearer and swaps in the node's api token)
15
19
  //
16
20
  // The desktop registers itself as a hub instance (id `code-desktop-…`) because
17
21
  // hub tokens are bound to (owner, instance). It never heartbeats, so it is
@@ -333,7 +337,8 @@ async function instances() {
333
337
  const out = list
334
338
  // direct: `self` is this desktop's hidden pseudo-instance; via sidecar: `self` is the
335
339
  // local node itself, which IS a real instance the user may want to open.
336
- .filter((i) => (viaSidecar || !i.self) && !String(i.platform || "").startsWith("desktop"))
340
+ // Viewer logins (this desktop, other desktops, phones) are not machines.
341
+ .filter((i) => (viaSidecar || !i.self) && !/^(desktop|mobile)/.test(String(i.platform || "")))
337
342
  .map((i) => ({
338
343
  id: i.instanceId,
339
344
  name: i.name || (i.proxyHost ? String(i.proxyHost).split(".")[0] : i.instanceId),
@@ -350,6 +355,8 @@ async function instances() {
350
355
  resources: i.resources || null,
351
356
  ports: Array.isArray(i.ports) ? i.ports : [],
352
357
  agents: Array.isArray(i.agents) ? i.agents.length : undefined,
358
+ // The node's last agent snapshot (agentId/title/status/model/working…).
359
+ agentList: Array.isArray(i.agents) ? i.agents : [],
353
360
  }))
354
361
  .sort((x, y) => Number(y.online) - Number(x.online) || x.name.localeCompare(y.name));
355
362
  // Every host in this list was fetched with the owner's own token, so each one
@@ -358,9 +365,98 @@ async function instances() {
358
365
  try {
359
366
  hubTrust.recordOwnerHubHosts(out.map((i) => i.host));
360
367
  } catch {}
368
+ _lastInstances = out;
361
369
  return { ok: true, owner: res.json.owner || a.owner, viaSidecar, instances: out };
362
370
  }
363
371
 
372
+ // ── node drill-down: projects → agents ──────────────────────────────────────
373
+ // Every instance host is the node's own cicy-code API; the hub gateway lets the
374
+ // owner's hub token through (swapping in the node's api token), so the desktop
375
+ // can read /api/groups (projects), /api/panes and /api/poll directly — no
376
+ // cookie session, no per-node credential.
377
+ let _lastInstances = [];
378
+ const NODE_TIMEOUT_MS = 15_000;
379
+
380
+ async function nodeFetch(host, route, token) {
381
+ const ctrl = new AbortController();
382
+ const t = setTimeout(() => ctrl.abort(), NODE_TIMEOUT_MS);
383
+ try {
384
+ const r = await pickFetch()("https://" + host + route, {
385
+ headers: { accept: "application/json", authorization: "Bearer " + token },
386
+ signal: ctrl.signal,
387
+ cache: "no-store",
388
+ });
389
+ const text = await r.text();
390
+ let json = null;
391
+ try {
392
+ json = text ? JSON.parse(text) : null;
393
+ } catch {}
394
+ if (!r.ok) throw new Error(`${route}: HTTP ${r.status}`);
395
+ return json;
396
+ } finally {
397
+ clearTimeout(t);
398
+ }
399
+ }
400
+
401
+ const shortWid = (paneId) => String(paneId || "").split(":")[0];
402
+
403
+ async function projects({ id } = {}) {
404
+ const a = readAuth();
405
+ if (!a) return { ok: false, error: "not_logged_in", projects: [] };
406
+ let inst = _lastInstances.find((i) => i.id === id);
407
+ if (!inst) {
408
+ const r = await instances();
409
+ inst = (r.instances || []).find((i) => i.id === id);
410
+ }
411
+ if (!inst || !inst.host) return { ok: false, error: "instance_not_found", projects: [] };
412
+ const [groupsRes, panesRes, pollRes] = await Promise.all([
413
+ nodeFetch(inst.host, "/api/groups", a.token).catch(() => null),
414
+ nodeFetch(inst.host, "/api/panes", a.token),
415
+ nodeFetch(inst.host, "/api/poll", a.token).catch(() => null),
416
+ ]);
417
+ const groups = Array.isArray(groupsRes) ? groupsRes : (groupsRes && groupsRes.groups) || [];
418
+ const panes = (Array.isArray(panesRes) ? panesRes : (panesRes && panesRes.panes) || []).filter(
419
+ (p) => p && typeof p.pane_id === "string" && p.pane_id
420
+ );
421
+ const statuses = (pollRes && pollRes.statuses) || {};
422
+ const pollRows = new Map();
423
+ for (const row of (pollRes && pollRes.agents) || [])
424
+ if (row && row.name) pollRows.set(String(row.name), row);
425
+ // Every pane on the node is an agent (masters included).
426
+ const agents = new Map();
427
+ for (const p of panes) {
428
+ const wid = shortWid(p.pane_id);
429
+ if (!wid || agents.has(wid)) continue;
430
+ const row = pollRows.get(wid) || {};
431
+ agents.set(wid, {
432
+ wid,
433
+ title: p.title || row.title || wid,
434
+ agentType: p.agent_type || row.agent_type || "",
435
+ role: p.role || "",
436
+ status: String(statuses[p.pane_id] || statuses[wid] || row.status || ""),
437
+ model: p.default_model || "",
438
+ workspace: p.workspace || "",
439
+ });
440
+ }
441
+ const placed = new Set();
442
+ const out = [];
443
+ for (const g of groups) {
444
+ const ids = new Set((g.pane_ids || []).map(shortWid));
445
+ const members = [...agents.values()].filter((x) => ids.has(x.wid) && !placed.has(x.wid));
446
+ for (const m of members) placed.add(m.wid);
447
+ if (!members.length && !g.is_default) continue;
448
+ out.push({
449
+ id: String(g.id),
450
+ name: g.name || String(g.project_template || g.id),
451
+ slug: g.project_template || "",
452
+ agents: members,
453
+ });
454
+ }
455
+ const rest = [...agents.values()].filter((x) => !placed.has(x.wid));
456
+ if (rest.length) out.push({ id: "ungrouped", name: "", slug: "", agents: rest });
457
+ return { ok: true, host: inst.host, projects: out };
458
+ }
459
+
364
460
  // One-time hand-off URL for an instance (optionally one of its local ports).
365
461
  async function grantUrl({ id, port = 0, next = "/" } = {}) {
366
462
  const a = readAuth();
@@ -410,4 +506,14 @@ async function logout() {
410
506
  return { ok: true };
411
507
  }
412
508
 
413
- module.exports = { status, loginStart, loginCode, cancel, instances, grantUrl, logout, hubOrigin };
509
+ module.exports = {
510
+ status,
511
+ loginStart,
512
+ loginCode,
513
+ cancel,
514
+ instances,
515
+ projects,
516
+ grantUrl,
517
+ logout,
518
+ hubOrigin,
519
+ };
@@ -149,6 +149,16 @@ function register({ sidecarLogPath } = {}) {
149
149
  // 退避:子进程被拦(安全软件开机后一段时间会自己放行)2 分钟一试;其余 15 分钟一试。
150
150
  const AUTO_RETRY_MS = { spawn_blocked: 2 * 60 * 1000, windows_component_store_broken: 6 * 60 * 60 * 1000, virtualization_disabled: 60 * 60 * 1000, default: 15 * 60 * 1000 };
151
151
  const HARD_BOOTSTRAP_REASONS = new Set(["spawn_blocked", "virtualization_disabled", "wsl_enable_failed", "windows_component_store_broken", "wsl_kernel_missing", "wsl_reboot_required"]);
152
+ // cicy-desktop 本质是 client,cicy-code/WSL「能装就装、不能装也能用」:仍会自动尝试安装,
153
+ // 但硬失败(这台机根本装不了,如不支持嵌套虚拟化)后不再每 15 分钟强装弹 UAC,也不自动
154
+ // 重启整机——改为永久挂起,app 照常当 client;用户在 UI 点「安装/重试」可解除挂起。
155
+ // 要恢复旧的「强制自动重试 + 自动重启」行为:设 env CICY_WSL_AUTOINSTALL=1 或建标记文件
156
+ // ~/cicy-ai/db/wsl-autoinstall.flag。
157
+ const WSL_AUTOINSTALL_FLAG = path.join(os.homedir(), "cicy-ai", "db", "wsl-autoinstall.flag");
158
+ function wslAutoInstallAllowed() {
159
+ if (process.env.CICY_WSL_AUTOINSTALL === "1") return true;
160
+ try { return fs.existsSync(WSL_AUTOINSTALL_FLAG); } catch { return false; }
161
+ }
152
162
  function recordBootstrapResult(result, err) {
153
163
  if (err) {
154
164
  const code = err && err.code;
@@ -160,10 +170,19 @@ function register({ sidecarLogPath } = {}) {
160
170
  _lastBootstrapError = { reason: result.reason || result.error || "bootstrap_failed", message: result.message || "", ts: Date.now() };
161
171
  } else return;
162
172
  _softRetryAt = Date.now() + 2 * 60 * 1000;
163
- if (_lastBootstrapError.reason === "wsl_reboot_required") scheduleReboot(90);
173
+ // 需要重启才能继续装 WSL 时,默认不自动重启整机(这些机器在跑账号矩阵,强制重启代价大),
174
+ // 只挂起等用户手动重启/重试;CICY_WSL_AUTOINSTALL=1 时才恢复自动重启。
175
+ if (_lastBootstrapError.reason === "wsl_reboot_required") scheduleReboot(90, "", { auto: true });
164
176
  if (HARD_BOOTSTRAP_REASONS.has(_lastBootstrapError.reason)) {
165
177
  _autoBootstrapPaused = _lastBootstrapError.reason;
166
- _autoBootstrapRetryAt = Date.now() + (AUTO_RETRY_MS[_lastBootstrapError.reason] || AUTO_RETRY_MS.default);
178
+ // 硬失败(如 vmPlatform=false,这台根本装不了)→ 永久挂起,不再每 15 分钟强装;
179
+ // app 照常当 client。用户点「安装/重试」(docker:app-bootstrap)会清挂起再试一次。
180
+ // 例外:spawn_blocked 是安全软件开机临时拦子进程、会自愈 → 保留短重试(符合「能装就装」),
181
+ // 不永久挂起。
182
+ const transient = _lastBootstrapError.reason === "spawn_blocked";
183
+ _autoBootstrapRetryAt = (wslAutoInstallAllowed() || transient)
184
+ ? Date.now() + (AUTO_RETRY_MS[_lastBootstrapError.reason] || AUTO_RETRY_MS.default)
185
+ : Number.MAX_SAFE_INTEGER;
167
186
  }
168
187
  log.error(`[docker] bootstrap failed reason=${_lastBootstrapError.reason}${_lastBootstrapError.message ? ` — ${_lastBootstrapError.message}` : ""}${_autoBootstrapPaused ? " (auto-bootstrap paused until user retries)" : ""}`);
169
188
  }
@@ -191,7 +210,10 @@ function register({ sidecarLogPath } = {}) {
191
210
  if (delta) { c = Math.max(0, c + delta); try { fs.mkdirSync(path.dirname(REBOOT_COUNT_FILE), { recursive: true }); fs.writeFileSync(REBOOT_COUNT_FILE, JSON.stringify({ count: c, ts: Date.now() })); } catch {} }
192
211
  return c;
193
212
  }
194
- function scheduleReboot(delaySec = 90, why = "") {
213
+ function scheduleReboot(delaySec = 90, why = "", { auto = false } = {}) {
214
+ // client 优先:自动触发的整机重启默认抑制(只有用户手动「立即重启」或开启
215
+ // CICY_WSL_AUTOINSTALL 才真的重启),避免为可选的 cicy-code sidecar 重启正在跑账号的机器。
216
+ if (auto && !wslAutoInstallAllowed()) { log.info("[docker] auto-reboot 已抑制(client 模式;设 CICY_WSL_AUTOINSTALL=1 或 wsl-autoinstall.flag 可恢复)"); return false; }
195
217
  if (_rebootScheduled || process.platform !== "win32") return false;
196
218
  if (delaySec > 10 && rebootCount() >= 2) { log.warn("[docker] auto-reboot skipped: already rebooted 2× for WSL without success"); return false; }
197
219
  _rebootScheduled = true;
@@ -234,6 +256,11 @@ function register({ sidecarLogPath } = {}) {
234
256
  _dockerDaemonBusy = true;
235
257
  try {
236
258
  const s = await refreshDockerStatus();
259
+ // client 优先:host mihomo(Chrome/TG 代理)必须在 docker/WSL 起不来时也能自启。
260
+ // 它原本只在下方 `else if (s.running)` 分支里被拉起 —— 于是 docker 装不上的机器
261
+ // 重启后 mihomo 永不启动 → 面板全白板。这里每轮无条件确保一次:standalone 分支
262
+ // 不依赖 docker;幂等(已在跑则只做配置同步/直接返回)。
263
+ try { await maybeStartChromeProxy(); } catch (e) { log.warn(`[chrome-proxy] ensure-running failed: ${e.message}`); }
237
264
  // 打开 cicy-desktop 就保证 Windows/WSL 的 :8008 可用:
238
265
  // - 已安装但停止 → 启动 WSL、dockerd 和容器;
239
266
  // - 从未安装 → bootstrap 自动启用 WSL2、导入专用 distro、安装 Docker
@@ -262,12 +289,12 @@ function register({ sidecarLogPath } = {}) {
262
289
  if (dk.modernWslInstalled()) { try { rebootCount(-99); } catch {} }
263
290
  }
264
291
  _lastBootstrapError = { reason: "wsl_wedged", message: dk.modernWslInstalled && dk.modernWslInstalled() ? "已安装新版 WSL,将自动重启 Windows 生效" : "WSL 卡死且服务重启无效,将自动重启 Windows 修复", ts: Date.now() };
265
- scheduleReboot(90);
292
+ scheduleReboot(90, "", { auto: true });
266
293
  }
267
294
  } catch (e) { log.warn(`[docker-daemon] auto repairWsl failed: ${e.message}`); }
268
295
  await refreshDockerStatus(); return;
269
296
  }
270
- if (_unknownStreak >= 6 && !_rebootScheduled) { _lastBootstrapError = { reason: "wsl_wedged", message: "WSL 持续无响应,将自动重启 Windows 修复", ts: Date.now() }; scheduleReboot(90); }
297
+ if (_unknownStreak >= 6 && !_rebootScheduled) { _lastBootstrapError = { reason: "wsl_wedged", message: "WSL 持续无响应,将自动重启 Windows 修复", ts: Date.now() }; scheduleReboot(90, "", { auto: true }); }
271
298
  return;
272
299
  } else if (!s.unknown) { _unknownStreak = 0; }
273
300
  // 容器在里面健康、Windows 侧却连不通 = WSL localhost 转发坏了(实测 wsl --shutdown 后恢复)。
@@ -310,7 +337,7 @@ function register({ sidecarLogPath } = {}) {
310
337
  if (_relayMissesAfterRepair >= 3 && !_rebootScheduled && rebootCount() < 2) {
311
338
  _lastBootstrapError = { reason: "relay_unreachable", message: "容器正常但 Windows 连不上 :8008,端口修复无效 → 90 秒后自动重启 Windows 修复", ts: Date.now() };
312
339
  log.warn("[docker-daemon] relay still unreachable after tcp repair → auto reboot");
313
- scheduleReboot(90, "Windows 无法连接本机 :8008(端口异常),自动重启修复");
340
+ scheduleReboot(90, "Windows 无法连接本机 :8008(端口异常),自动重启修复", { auto: true });
314
341
  await refreshDockerStatus();
315
342
  return;
316
343
  }
package/src/main.js CHANGED
@@ -1043,6 +1043,11 @@ function ensureWindowsWatchdog(want) {
1043
1043
  "For Each p In procs",
1044
1044
  " n = n + 1",
1045
1045
  "Next",
1046
+ // 更新安装期间让位:updating.flag 新鲜(<180s)就不拉起 app,免得和安装器
1047
+ // 抢 exe 文件锁 → 装不上 → 版本停滞 → 反复重启的循环。mtime 过期即自动恢复。
1048
+ 'Set fso = CreateObject("Scripting.FileSystemObject")',
1049
+ 'flag = sh.ExpandEnvironmentStrings("%LOCALAPPDATA%") & "\\cicy-desktop\\updating.flag"',
1050
+ 'If fso.FileExists(flag) Then If DateDiff("s", fso.GetFile(flag).DateLastModified, Now) < 180 Then WScript.Quit',
1046
1051
  `If n = 0 Then sh.Run """${exe}"" --hidden", 0, False`,
1047
1052
  "",
1048
1053
  ];
@@ -1564,6 +1569,14 @@ electronApp.whenReady().then(async () => {
1564
1569
  }
1565
1570
  });
1566
1571
  __ipcMainAuth.handle("hub:logout", () => hub.logout());
1572
+ // Drill-down: one node's projects, each with its agents.
1573
+ __ipcMainAuth.handle("hub:projects", async (_e, arg) => {
1574
+ try {
1575
+ return await hub.projects(arg || {});
1576
+ } catch (e) {
1577
+ return { ok: false, error: String((e && e.message) || e), projects: [] };
1578
+ }
1579
+ });
1567
1580
  // "+ 面板" dropdown config, edited from the homepage: which panels show, in
1568
1581
  // what order, under what name. panel-menu-store validates every id against
1569
1582
  // the built-in set, so the homepage can reorder/rename/disable but can never
@@ -2024,6 +2037,19 @@ electronApp.whenReady().then(async () => {
2024
2037
  };
2025
2038
  installApplicationMenu();
2026
2039
 
2040
+ // 实时读:profile 列表(chrome.json / electron account-*.json)变化时重建原生菜单,
2041
+ // 修复"只在启动时读一次"导致外部改动不反映的 bug。
2042
+ try {
2043
+ let menuWatchTimer = null;
2044
+ const rebuildMenu = () => { clearTimeout(menuWatchTimer); menuWatchTimer = setTimeout(() => { try { installApplicationMenu(); } catch (e) {} }, 300); };
2045
+ for (const dir of [path.join(os.homedir(), "cicy-ai", "db"), path.join(os.homedir(), "cicy-ai", "electron")]) {
2046
+ try {
2047
+ fs.mkdirSync(dir, { recursive: true });
2048
+ fs.watch(dir, { persistent: false }, (evt, name) => { if (!name || /^chrome\.json$/.test(name) || /^account-\d+\.json$/.test(name)) rebuildMenu(); });
2049
+ } catch (e) {}
2050
+ }
2051
+ } catch (e) {}
2052
+
2027
2053
  // Always open the homepage unless launched at login with --hidden.
2028
2054
  const hidden = process.argv.includes("--hidden");
2029
2055
  if (!hidden) {
@@ -2035,6 +2061,16 @@ electronApp.whenReady().then(async () => {
2035
2061
  const hw = require("./backends/homepage-window");
2036
2062
  appUpdater.init(hw.getHomepageWindow && hw.getHomepageWindow());
2037
2063
 
2064
+ // 保险:默认会话(profile 0)可能在 session-created 监听器挂上之前就已创建,
2065
+ // 启动时再显式直连一次。幂等,失败只记日志。
2066
+ try {
2067
+ const { session } = require("electron");
2068
+ session.defaultSession
2069
+ .setProxy({ mode: "direct" })
2070
+ .then(() => log.info("[Proxy] 启动时确认:默认会话(profile 0)直连"))
2071
+ .catch((err) => log.error("[Proxy] 启动时默认会话直连失败:", err));
2072
+ } catch (err) { log.error("[Proxy] 启动时默认会话直连异常:", err); }
2073
+
2038
2074
  // 为 webview partition 设置代理
2039
2075
  if (config.proxy) {
2040
2076
  const { session } = require("electron");
@@ -2042,6 +2078,8 @@ electronApp.whenReady().then(async () => {
2042
2078
  mainSession
2043
2079
  .setProxy({
2044
2080
  proxyRules: config.proxy,
2081
+ // localhost bypass:到本机 cicy-code / 终端 ws 的连接不能被代理拦
2082
+ proxyBypassRules: "127.0.0.1,localhost,[::1]",
2045
2083
  })
2046
2084
  .then(() => {
2047
2085
  log.info(`[Proxy] persist:main partition 已设置代理: ${config.proxy}`);
@@ -2200,18 +2238,36 @@ process.on("SIGINT", () => {
2200
2238
  electronApp.quit();
2201
2239
  });
2202
2240
 
2203
- // 为所有 session(包括 webview partition)设置代理
2241
+ // 给 session 设代理 —— 但**默认会话(profile 0)永远直连**。
2242
+ //
2243
+ // 以前这里对「所有 session」一视同仁地套 --proxy,默认会话也被代理了。而首页、
2244
+ // CiCy Hub 登录、自动更新全都跑在默认会话上,于是代理一挂,整台机器对外彻底断联:
2245
+ // 登录报 net::ERR_PROXY_CONNECTION_FAILED、机器掉出车队、连新版本都拉不到 ——
2246
+ // 变成「只能有人到机器跟前才能救」(实测 xs-master:mihomo 没起来就这样)。
2247
+ // window-utils 里本来就写明 profile 0 强制直连,但那段只在建窗口时对特定会话生效,
2248
+ // 盖不住这个全局钩子。
2249
+ //
2250
+ // 注意:Electron 的 Session 读不到 .partition(恒为 undefined,见 window-utils 的说明),
2251
+ // 所以只能拿对象和 defaultSession 比,不能靠 partition 字符串判断。
2204
2252
  electronApp.on("session-created", (session) => {
2205
- if (config.proxy) {
2253
+ const { session: sessions } = require("electron");
2254
+ // 默认会话**无条件**直连 —— 不只是「不套 --proxy」,还要挡住 Electron 默认的
2255
+ // 「跟随系统代理」。首页窗口根本不走 window-utils 那段 profile 0 直连逻辑
2256
+ // (homepage-window.js 里没有任何 session 处理),所以以前 config.proxy 为空时
2257
+ // 默认会话就静默跟随 Windows 系统代理:系统代理指着挂掉的 mihomo,
2258
+ // 首页/Hub 登录/车队通道/自动更新全断,机器还拉不到修复版本。
2259
+ if (session === sessions.defaultSession) {
2206
2260
  session
2207
- .setProxy({
2208
- proxyRules: config.proxy,
2209
- })
2210
- .then(() => {
2211
- log.info(`[Proxy] Session ${session.partition || "default"} 已设置代理: ${config.proxy}`);
2212
- })
2213
- .catch((err) => {
2214
- log.error(`[Proxy] Session ${session.partition || "default"} 设置代理失败:`, err);
2215
- });
2261
+ .setProxy({ mode: "direct" })
2262
+ .then(() => log.info("[Proxy] 默认会话(profile 0)强制直连(不跟随系统代理)"))
2263
+ .catch((err) => log.error("[Proxy] 默认会话直连设置失败:", err));
2264
+ return;
2216
2265
  }
2266
+ if (!config.proxy) return;
2267
+ // 其余会话走代理,但 localhost 一律 bypass —— 到本机 cicy-code / gotty 终端的连接
2268
+ // 不能被代理拦(以前漏了 bypass,终端白板就是这么来的)。
2269
+ session
2270
+ .setProxy({ proxyRules: config.proxy, proxyBypassRules: "127.0.0.1,localhost,[::1]" })
2271
+ .then(() => log.info(`[Proxy] 会话已设置代理: ${config.proxy}(bypass localhost)`))
2272
+ .catch((err) => log.error("[Proxy] 会话设置代理失败:", err));
2217
2273
  });
@@ -77,14 +77,50 @@ const HOST_CTRL = Number(process.env.CICY_HOST_MIHOMO_CTRL || 19011);
77
77
 
78
78
  function binPresent() { return valid(NEW_BIN) || valid(LEGACY_BIN); }
79
79
 
80
- function download(url, dest, redirects = 0) {
80
+ // Node's own TLS stack gets ECONNRESET on the R2 host from some CN networks
81
+ // (TLS-fingerprint reset) while Chromium's stack on the very same machine gets
82
+ // 200 — so prefer Electron `net` (Chromium) and keep Node https as the fallback
83
+ // for non-Electron callers (tests / scripts).
84
+ function downloadViaNet(url, dest) {
85
+ let net;
86
+ try { net = require("electron").net; } catch { net = null; }
87
+ if (!net || typeof net.fetch !== "function") return null;
88
+ return (async () => {
89
+ const stg = dest + ".part";
90
+ try { fs.rmSync(stg, { force: true }); } catch {}
91
+ const res = await net.fetch(url, { signal: AbortSignal.timeout(10 * 60 * 1000) });
92
+ if (res.status !== 200) throw new Error(`HTTP ${res.status} for ${url}`);
93
+ const total = Number(res.headers.get("content-length") || 0);
94
+ const f = fs.createWriteStream(stg);
95
+ const reader = res.body.getReader();
96
+ let got = 0;
97
+ try {
98
+ for (;;) {
99
+ const { done, value } = await reader.read();
100
+ if (done) break;
101
+ const b = Buffer.from(value);
102
+ got += b.length;
103
+ await new Promise((ok, no) => f.write(b, (e) => (e ? no(e) : ok())));
104
+ }
105
+ await new Promise((ok) => f.end(ok));
106
+ if (total && got !== total) throw new Error(`short download ${got}/${total}`);
107
+ fs.renameSync(stg, dest);
108
+ } catch (e) {
109
+ try { f.destroy(); } catch {}
110
+ try { fs.rmSync(stg, { force: true }); } catch {}
111
+ throw e;
112
+ }
113
+ })();
114
+ }
115
+
116
+ function downloadViaNode(url, dest, redirects = 0) {
81
117
  return new Promise((resolve, reject) => {
82
118
  if (redirects > 5) return reject(new Error("too many redirects"));
83
119
  const f = fs.createWriteStream(dest);
84
120
  const req = https.get(url, (res) => {
85
121
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
86
122
  f.close(); try { fs.unlinkSync(dest); } catch {}
87
- return download(res.headers.location, dest, redirects + 1).then(resolve, reject);
123
+ return downloadViaNode(res.headers.location, dest, redirects + 1).then(resolve, reject);
88
124
  }
89
125
  if (res.statusCode !== 200) { f.close(); try { fs.unlinkSync(dest); } catch {} return reject(new Error(`HTTP ${res.statusCode} for ${url}`)); }
90
126
  res.pipe(f);
@@ -95,6 +131,15 @@ function download(url, dest, redirects = 0) {
95
131
  });
96
132
  }
97
133
 
134
+ async function download(url, dest) {
135
+ const viaNet = downloadViaNet(url, dest);
136
+ if (viaNet) {
137
+ try { await viaNet; return; }
138
+ catch (e) { console.warn(`[host-mihomo] net.fetch download failed (${e.message}), falling back to https`); }
139
+ }
140
+ await downloadViaNode(url, dest);
141
+ }
142
+
98
143
  // Ensure the mihomo binary is in ~/.local/bin/mihomo-<ver> (idempotent).
99
144
  // Already there → done. Only a legacy runtime copy → migrate it (no network).
100
145
  // Nothing → pull from OSS.
@@ -49,7 +49,18 @@ function facebookIdentityFromProfile(profile) {
49
49
  const logins = profile && Array.isArray(profile.logins) ? profile.logins : [];
50
50
  const l = logins.find((x) => String((x && x.name) || "").toLowerCase() === "facebook");
51
51
  if (!l) return null;
52
- return { id: String(l.username || ""), username: "", displayName: String(l.note || ""), phone: "" };
52
+ // email / twofa / secondEmail 原来在这里被丢掉了,面板的 ⚙ 里那几行只能显示「—」。
53
+ // 它们在 logins[] 里一直存着(实测 2026-09-11,xs-1001 #1 三项都有值)。
54
+ // password 这个字段 logins[] 里没有,FB 密码仍只能从备注串 `FB密码:xxx` 解析。
55
+ return {
56
+ id: String(l.username || ""),
57
+ username: "",
58
+ displayName: String(l.note || ""),
59
+ phone: "",
60
+ email: String(l.email || ""),
61
+ twofa: String(l.twofa || ""),
62
+ secondEmail: String(l.secondEmail || ""),
63
+ };
53
64
  }
54
65
 
55
66
  module.exports = { isFacebookUrl, FACEBOOK_IDENTITY_SCRIPT, normalizeFacebookIdentity, facebookLoginRecord, facebookIdentityFromProfile };