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.
- package/package.json +1 -1
- package/scripts/build-homepage.cjs +16 -1
- package/src/app-updater.js +30 -0
- package/src/backends/homepage-preload.js +3 -1
- package/src/backends/homepage-react/assets/index-Buyk0RlW.js +655 -0
- package/src/backends/homepage-react/assets/index-CVk0uc2g.css +1 -0
- package/src/backends/homepage-react/index.html +5 -3
- package/src/backends/homepage-react/panel/facebook-matrix.html +305 -87
- package/src/backends/homepage-react/panel/ipcheck.html +112 -0
- package/src/backends/homepage-react/panel/matrix.html +595 -0
- package/src/backends/homepage-react/panel/telegram-matrix.html +96 -22
- package/src/backends/homepage-react/panel/tiktok-matrix.html +599 -0
- package/src/backends/hub-client.js +108 -2
- package/src/backends/sidecar-ipc.js +33 -6
- package/src/main.js +67 -11
- package/src/sidecar/host-mihomo.js +47 -2
- package/src/tabbrowser/facebook-identity.js +12 -1
- package/src/tabbrowser/facebook-matrix.html +305 -87
- package/src/tabbrowser/fb-login.js +115 -0
- package/src/tabbrowser/newtab-protocol.js +6 -1
- package/src/tabbrowser/panel-cells.js +14 -1
- package/src/tabbrowser/telegram-matrix.html +96 -22
- package/src/tabbrowser/telegram-web-preferences.js +5 -1
- package/src/tabbrowser/tiktok-matrix.html +305 -87
- package/src/tools/tab-browser-tools.js +21 -100
- package/src/tray.js +22 -0
- package/test/bootstrap-failure-reporting.test.js +1 -1
- package/test/facebook-identity.test.js +3 -1
- package/test/facebook-matrix.test.js +2 -1
- package/test/telegram-matrix.test.js +2 -2
- package/test/tiktok-matrix.test.js +2 -1
- package/tools/arm-fblogin.js +32 -0
- package/tools/wsd-ws +114 -0
- package/workers/render/index.html +3 -1
- package/workers/render/package.json +2 -3
- package/workers/render/public/panel/facebook-matrix.html +305 -87
- package/workers/render/public/panel/ipcheck.html +112 -0
- package/workers/render/public/panel/matrix.html +595 -0
- package/workers/render/public/panel/telegram-matrix.html +96 -22
- package/workers/render/public/panel/tiktok-matrix.html +599 -0
- package/workers/render/src/App.css +4 -0
- package/workers/render/src/App.jsx +917 -52
- package/workers/render/stamp-panel-version.mjs +16 -0
- package/workers/render/worker.js +1 -1
- package/src/backends/homepage-react/assets/index-C6Zrdgc0.css +0 -1
- package/src/backends/homepage-react/assets/index-OG6JB73F.js +0 -411
- package/src/tabbrowser/cicy-code-tab-restore.js +0 -10
- 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
|
-
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 };
|