cicy-desktop 2.1.329 → 2.1.330
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
package/src/app-updater.js
CHANGED
|
@@ -21,7 +21,7 @@ const os = require("os");
|
|
|
21
21
|
const { readGlobalConfig, updateGlobalConfig } = require("./utils/global-json");
|
|
22
22
|
const https = require("https");
|
|
23
23
|
const log = require("electron-log");
|
|
24
|
-
const { R2_RELEASES_BASE } = require("./sidecar/mirrors");
|
|
24
|
+
const { R2_RELEASES_BASE, buildUrlList } = require("./sidecar/mirrors");
|
|
25
25
|
|
|
26
26
|
// ── 版本比较:>0 a 新于 b ─────────────────────────────────────────────────────
|
|
27
27
|
function cmpVer(a, b) {
|
|
@@ -74,25 +74,76 @@ function headOk(url, redirects = 0) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// ── 最新版本号 ────────────────────────────────────────────────────────────────
|
|
77
|
+
// R2 优先(win/mac/linux 各自指针,两条 CI 版本可能不同步,所以各读各的),GitHub 兜底。
|
|
78
|
+
//
|
|
79
|
+
// 原先这里是「一律读 OSS,彻底不碰 GitHub」,理由是仓库可能私有。代价是 R2 一旦不可达,
|
|
80
|
+
// 自更新就整条断掉 —— 而 fleet 里确实有一批 Windows 节点连 r2.deepfetch.de5.net 直接
|
|
81
|
+
// ECONNRESET(GitHub 反而 ~300ms 可达),它们因此永远停在旧版、只能人工推包。
|
|
82
|
+
// 现在 R2 仍是首选(仓库若再转私有,这条路不受影响),失败才回退 GitHub Release;
|
|
83
|
+
// 两边都不通才算真失败。
|
|
84
|
+
const GH_REPO = "cicy-ai/cicy-desktop";
|
|
85
|
+
|
|
86
|
+
function fetchLatestVersionFromGitHub() {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const req = https.get(
|
|
89
|
+
`https://api.github.com/repos/${GH_REPO}/releases/latest`,
|
|
90
|
+
{ headers: { "User-Agent": "cicy-desktop-updater", Accept: "application/vnd.github+json" }, timeout: 10000 },
|
|
91
|
+
(res) => {
|
|
92
|
+
if (res.statusCode !== 200) { res.resume(); return reject(new Error(`GitHub HTTP ${res.statusCode}`)); }
|
|
93
|
+
let body = "";
|
|
94
|
+
res.setEncoding("utf8");
|
|
95
|
+
res.on("data", (d) => (body += d));
|
|
96
|
+
res.on("end", () => {
|
|
97
|
+
try {
|
|
98
|
+
const tag = String((JSON.parse(body) || {}).tag_name || "").replace(/^v/, "");
|
|
99
|
+
tag ? resolve(tag) : reject(new Error("no tag_name"));
|
|
100
|
+
} catch (e) { reject(e); }
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
req.on("error", reject);
|
|
105
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
async function fetchLatestVersion() {
|
|
78
|
-
// 版本清单**一律读 OSS**(win/mac/linux 各自指针),与网络判定无关 —— 彻底不碰 GitHub,
|
|
79
|
-
// 这样 cicy-desktop 仓库私有也不影响更新检查。win/mac 是两条独立 CI、版本可能不同步,读错
|
|
80
|
-
// 平台会 404,所以各读各的指针文件。
|
|
81
110
|
const file = process.platform === "win32" ? "win-latest-version.txt"
|
|
82
111
|
: process.platform === "darwin" ? "mac-latest-version.txt"
|
|
83
112
|
: "linux-latest-version.txt";
|
|
84
|
-
|
|
113
|
+
try {
|
|
114
|
+
return (await getText(`${R2_RELEASES_BASE}/${file}`)).trim().replace(/^v/, "");
|
|
115
|
+
} catch (e) {
|
|
116
|
+
log.warn(`[app-updater] R2 版本指针不可达(${e.message}),回退 GitHub Release`);
|
|
117
|
+
return await fetchLatestVersionFromGitHub();
|
|
118
|
+
}
|
|
85
119
|
}
|
|
86
120
|
|
|
87
|
-
// ──
|
|
121
|
+
// ── 安装包来源(有序候选)+ 本地文件名 ──────────────────────────────────────
|
|
122
|
+
// 返回 { file, urls } —— urls 是按优先级排好的候选:R2 → GitHub 直连 → ghproxy 镜像。
|
|
123
|
+
// 调用方依次尝试,第一个能下的算数(见 check() 的 HEAD 探测和 downloadUpdate 的重试)。
|
|
124
|
+
//
|
|
125
|
+
// 注意 Windows 两边**文件名不同**:R2 上是 cicy-desktop-<ver>.exe(CI 传的版本化副本),
|
|
126
|
+
// GitHub Release 里是 electron-builder 产出的 CiCy-Desktop-Setup-<ver>.exe。
|
|
127
|
+
// mac 的 pkg 和 linux 的 AppImage 两边同名。`file` 只是本地落盘名,保持原样不变。
|
|
88
128
|
function assetFor(version) {
|
|
89
129
|
const plat = process.platform;
|
|
90
130
|
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
91
|
-
// 三端安装包**全部从 OSS 拉**,与网络无关 —— 不再拼 GitHub 下载 URL,仓库私有也能更新。
|
|
92
131
|
const file = plat === "win32" ? `cicy-desktop-${version}.exe`
|
|
93
132
|
: plat === "darwin" ? `cicy-desktop-${version}-${arch}.pkg`
|
|
94
133
|
: `CiCy-Desktop-${version}.AppImage`;
|
|
95
|
-
|
|
134
|
+
const ghName = plat === "win32" ? `CiCy-Desktop-Setup-${version}.exe` : file;
|
|
135
|
+
const ghUrl = `https://github.com/${GH_REPO}/releases/download/v${version}/${ghName}`;
|
|
136
|
+
// buildUrlList("global") = [直连, ...镜像];R2 排在最前面。
|
|
137
|
+
const urls = [`${R2_RELEASES_BASE}/${file}`, ...buildUrlList(ghUrl, "global")];
|
|
138
|
+
return { file, urls, url: urls[0] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 第一个 HEAD 得到 200/206 的候选;都不行返回 null。
|
|
142
|
+
async function firstReachable(urls) {
|
|
143
|
+
for (const u of urls) {
|
|
144
|
+
if (await headOk(u)) return u;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
96
147
|
}
|
|
97
148
|
|
|
98
149
|
// ── 下载(带进度,跟随重定向)──────────────────────────────────────────────────
|
|
@@ -179,9 +230,10 @@ async function check() {
|
|
|
179
230
|
if (latest && cmpVer(latest, current) > 0) {
|
|
180
231
|
// 「先出包再更新版本」客户端保险:版本号涨了不代表包传完了。先 HEAD 确认本平台安装包
|
|
181
232
|
// 真能下(200),否则当成「还没就绪」继续显已是最新,避免用户点下载 404。
|
|
182
|
-
const {
|
|
183
|
-
const ready = await
|
|
233
|
+
const { urls } = assetFor(latest);
|
|
234
|
+
const ready = await firstReachable(urls);
|
|
184
235
|
if (ready) {
|
|
236
|
+
log.info(`[app-updater] ${latest} 安装包就绪:${ready}`);
|
|
185
237
|
broadcast({ status: "available", version: latest, current, progress: null, filePath: null, autoUpdate: getAutoUpdate(), auto: false });
|
|
186
238
|
if (getAutoUpdate()) {
|
|
187
239
|
log.info(`[app-updater] auto-update on → downloading ${latest} and installing without asking`);
|
|
@@ -189,7 +241,7 @@ async function check() {
|
|
|
189
241
|
await downloadUpdate();
|
|
190
242
|
if (_state.status === "ready") installNow();
|
|
191
243
|
}
|
|
192
|
-
} else { log.info(`[app-updater] ${latest}
|
|
244
|
+
} else { log.info(`[app-updater] ${latest} 版本号已更新但所有来源都拿不到安装包(HEAD 非 200):${urls.join(" , ")} — 暂不提示更新`); broadcast({ status: "up-to-date", version: current, current }); }
|
|
193
245
|
} else {
|
|
194
246
|
broadcast({ status: "up-to-date", version: latest || current, current });
|
|
195
247
|
}
|
|
@@ -208,16 +260,26 @@ async function downloadUpdate() {
|
|
|
208
260
|
if (!version) { broadcast({ status: "error", error: "no version" }); return _state; }
|
|
209
261
|
_downloading = true;
|
|
210
262
|
try {
|
|
211
|
-
const {
|
|
263
|
+
const { urls, file } = assetFor(version);
|
|
212
264
|
// 下到 ~/Downloads(用户能直接找到安装包);目录不存在则建。
|
|
213
265
|
const dir = app.getPath("downloads");
|
|
214
266
|
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
215
267
|
const dest = path.join(dir, file);
|
|
216
|
-
log.info(`[app-updater] downloading ${url} → ${dest}`);
|
|
217
268
|
broadcast({ status: "downloading", progress: { percent: 0, transferred: 0, total: 0 }, filePath: null });
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
269
|
+
// 逐个候选试(R2 → GitHub → 镜像):一个源断了就换下一个,全断才算失败。
|
|
270
|
+
let lastErr = null;
|
|
271
|
+
for (const url of urls) {
|
|
272
|
+
try {
|
|
273
|
+
log.info(`[app-updater] downloading ${url} → ${dest}`);
|
|
274
|
+
await download(url, dest, (p) => broadcast({ status: "downloading", progress: p }));
|
|
275
|
+
broadcast({ status: "ready", filePath: dest, progress: { percent: 100 } });
|
|
276
|
+
return _state;
|
|
277
|
+
} catch (e) {
|
|
278
|
+
lastErr = e;
|
|
279
|
+
log.warn(`[app-updater] 源失败(${url}):${e.message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
throw lastErr || new Error("no source available");
|
|
221
283
|
} catch (e) {
|
|
222
284
|
log.warn("[app-updater] download failed:", e.message);
|
|
223
285
|
broadcast({ status: "error", error: e.message });
|
|
@@ -233,14 +233,23 @@ function start({ force = false } = {}) {
|
|
|
233
233
|
|
|
234
234
|
// Full enable: ensure binary → copy+adapt container config → (re)start.
|
|
235
235
|
// containerYaml is fetched by the caller (sidecar-ipc) via appDocker.
|
|
236
|
+
// A missing container config is NOT fatal on its own: when the WSL/docker
|
|
237
|
+
// cicy-code isn't installed (or is down) we run STANDALONE on the host's own
|
|
238
|
+
// mihomo-host.yaml if one is already there, so the per-profile proxies keep
|
|
239
|
+
// working instead of dying with the container — that's what lets Windows run
|
|
240
|
+
// without WSL at all. Only "no container config AND no host config" leaves us
|
|
241
|
+
// nothing to start. Standalone has no authoritative selection source, so the
|
|
242
|
+
// selection sync is skipped rather than overriding whatever the host holds.
|
|
236
243
|
async function enable({ containerYaml, selections = {}, emit } = {}) {
|
|
237
244
|
await ensureBinary({ emit });
|
|
238
|
-
|
|
239
|
-
|
|
245
|
+
let changed = false, standalone = false;
|
|
246
|
+
if (containerYaml) changed = writeConfig(containerYaml);
|
|
247
|
+
else if (fs.existsSync(HOST_CONFIG)) standalone = true;
|
|
248
|
+
else throw new Error(tt("noContainerConfig"));
|
|
240
249
|
const res = start({ force: changed });
|
|
241
|
-
const synced = await syncSelections(selections);
|
|
250
|
+
const synced = standalone ? { updated: [] } : await syncSelections(selections);
|
|
242
251
|
emit && emit({ phase: "chrome-proxy", status: "running", message: tt("ready") });
|
|
243
|
-
return { ok: true, ...res, ...synced };
|
|
252
|
+
return { ok: true, standalone, ...res, ...synced };
|
|
244
253
|
}
|
|
245
254
|
|
|
246
255
|
module.exports = {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The self-updater used to read BOTH the version pointer and the installer from
|
|
5
|
+
// R2 only ("彻底不碰 GitHub"). That made auto-update fail closed on every node
|
|
6
|
+
// that cannot reach r2.deepfetch.de5.net — a real slice of the fleet answers
|
|
7
|
+
// ECONNRESET there while GitHub responds in ~300ms, so those boxes sat on an old
|
|
8
|
+
// version until someone pushed a package by hand. R2 stays FIRST (a private repo
|
|
9
|
+
// must keep working); GitHub is the fallback.
|
|
10
|
+
//
|
|
11
|
+
// app-updater requires electron at load time, so this asserts on source shape —
|
|
12
|
+
// the same convention as app-updater-auto.test.js.
|
|
13
|
+
const test = require("node:test");
|
|
14
|
+
const assert = require("node:assert/strict");
|
|
15
|
+
const fs = require("node:fs");
|
|
16
|
+
const path = require("node:path");
|
|
17
|
+
|
|
18
|
+
const src = fs.readFileSync(path.join(__dirname, "..", "src", "app-updater.js"), "utf8");
|
|
19
|
+
|
|
20
|
+
test("version pointer falls back to the GitHub release when R2 is unreachable", () => {
|
|
21
|
+
assert.match(src, /function fetchLatestVersionFromGitHub\(\)/);
|
|
22
|
+
assert.match(src, /api\.github\.com\/repos\/\$\{GH_REPO\}\/releases\/latest/);
|
|
23
|
+
assert.match(src, /tag_name/);
|
|
24
|
+
// R2 first, GitHub only in the catch — not the other way round.
|
|
25
|
+
const fn = src.slice(
|
|
26
|
+
src.indexOf("async function fetchLatestVersion()"),
|
|
27
|
+
src.indexOf("// ── 安装包来源")
|
|
28
|
+
);
|
|
29
|
+
assert.match(fn, /R2_RELEASES_BASE\}\/\$\{file\}/);
|
|
30
|
+
assert.match(fn, /catch \(e\) \{[^]*fetchLatestVersionFromGitHub\(\)/);
|
|
31
|
+
assert.ok(
|
|
32
|
+
fn.indexOf("R2_RELEASES_BASE") < fn.indexOf("fetchLatestVersionFromGitHub"),
|
|
33
|
+
"R2 must be tried first"
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("assetFor returns ordered candidates: R2 → GitHub → mirrors", () => {
|
|
38
|
+
const fn = src.slice(
|
|
39
|
+
src.indexOf("function assetFor(version)"),
|
|
40
|
+
src.indexOf("async function firstReachable")
|
|
41
|
+
);
|
|
42
|
+
assert.match(
|
|
43
|
+
fn,
|
|
44
|
+
/const urls = \[`\$\{R2_RELEASES_BASE\}\/\$\{file\}`, \.\.\.buildUrlList\(ghUrl, "global"\)\]/
|
|
45
|
+
);
|
|
46
|
+
// Windows is the one platform whose asset is named differently on each source:
|
|
47
|
+
// R2 keeps the CI's versioned copy, GitHub has electron-builder's Setup exe.
|
|
48
|
+
assert.match(fn, /ghName = plat === "win32" \? `CiCy-Desktop-Setup-\$\{version\}\.exe` : file/);
|
|
49
|
+
assert.match(fn, /releases\/download\/v\$\{version\}\/\$\{ghName\}/);
|
|
50
|
+
assert.match(fn, /return \{ file, urls, url: urls\[0\] \}/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("check() probes every candidate before declaring the package missing", () => {
|
|
54
|
+
assert.match(src, /async function firstReachable\(urls\)/);
|
|
55
|
+
assert.match(src, /for \(const u of urls\) \{[^]*if \(await headOk\(u\)\) return u;/);
|
|
56
|
+
const chk = src.slice(
|
|
57
|
+
src.indexOf("async function check()"),
|
|
58
|
+
src.indexOf("async function downloadUpdate()")
|
|
59
|
+
);
|
|
60
|
+
assert.match(chk, /const \{ urls \} = assetFor\(latest\)/);
|
|
61
|
+
assert.match(chk, /const ready = await firstReachable\(urls\)/);
|
|
62
|
+
assert.doesNotMatch(chk, /await headOk\(url\)/); // the old single-source probe is gone
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("downloadUpdate() retries the next source instead of failing on the first", () => {
|
|
66
|
+
const dl = src.slice(
|
|
67
|
+
src.indexOf("async function downloadUpdate()"),
|
|
68
|
+
src.indexOf("function installNow()")
|
|
69
|
+
);
|
|
70
|
+
assert.match(dl, /const \{ urls, file \} = assetFor\(version\)/);
|
|
71
|
+
assert.match(dl, /for \(const url of urls\) \{/);
|
|
72
|
+
assert.match(dl, /catch \(e\) \{[^]*lastErr = e/);
|
|
73
|
+
assert.match(dl, /throw lastErr \|\| new Error\("no source available"\)/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("mirrors helper is imported so CN keeps its ghproxy fallback", () => {
|
|
77
|
+
assert.match(
|
|
78
|
+
src,
|
|
79
|
+
/const \{ R2_RELEASES_BASE, buildUrlList \} = require\("\.\/sidecar\/mirrors"\)/
|
|
80
|
+
);
|
|
81
|
+
});
|