Skip to content

Commit cd2951d

Browse files
committed
mac 更新适配
1 parent d68ee79 commit cd2951d

4 files changed

Lines changed: 216 additions & 25 deletions

File tree

src/main/ipc/app.ts

Lines changed: 141 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -751,40 +751,145 @@ function versionAsTag(version: string | undefined): string | undefined {
751751
return /^v/iu.test(trimmed) ? trimmed : `v${trimmed}`;
752752
}
753753

754-
function sanitizeDownloadFileName(value: string): string {
754+
function launcherUpdatePackageLabel(
755+
platform: NodeJS.Platform = process.platform,
756+
): string {
757+
if (platform === "darwin") {
758+
return "macOS DMG 安装包";
759+
}
760+
if (platform === "win32") {
761+
return "Windows 安装包";
762+
}
763+
return "当前平台安装包";
764+
}
765+
766+
function launcherUpdateDefaultFileName(
767+
platform: NodeJS.Platform = process.platform,
768+
): string {
769+
if (platform === "darwin") {
770+
return "MaiBot-OneKey-update.dmg";
771+
}
772+
if (platform === "win32") {
773+
return "MaiBot-OneKey-update.exe";
774+
}
775+
return "MaiBot-OneKey-update";
776+
}
777+
778+
function launcherAssetArchNames(arch: string): string[] {
779+
if (arch === "arm64") {
780+
return ["arm64", "aarch64"];
781+
}
782+
if (arch === "x64") {
783+
return ["x64", "x86_64", "amd64"];
784+
}
785+
return [arch.toLowerCase()];
786+
}
787+
788+
function scoreLauncherUpdateAsset(
789+
asset: GitHubReleaseAsset,
790+
platformNames: string[],
791+
arch: string,
792+
): number {
793+
const name = String(asset.name ?? "").toLowerCase();
794+
const archNames = launcherAssetArchNames(arch);
795+
let score = 0;
796+
if (platformNames.some((platformName) => name.includes(platformName))) {
797+
score += 20;
798+
}
799+
if (archNames.some((archName) => name.includes(archName))) {
800+
score += 10;
801+
}
802+
if (name.includes("universal")) {
803+
score += 5;
804+
}
805+
return score;
806+
}
807+
808+
function selectBestLauncherUpdateAsset(
809+
assets: GitHubReleaseAsset[],
810+
platformNames: string[],
811+
arch: string,
812+
): GitHubReleaseAsset | undefined {
813+
return assets
814+
.map((asset, index) => ({
815+
asset,
816+
index,
817+
score: scoreLauncherUpdateAsset(asset, platformNames, arch),
818+
}))
819+
.sort((left, right) => {
820+
if (right.score !== left.score) {
821+
return right.score - left.score;
822+
}
823+
return left.index - right.index;
824+
})[0]?.asset;
825+
}
826+
827+
function sanitizeDownloadFileName(
828+
value: string,
829+
platform: NodeJS.Platform = process.platform,
830+
): string {
755831
const sanitized = basename(value)
756832
.replace(/[<>:"/\\|?*\u0000-\u001F]/gu, "-")
757833
.replace(/\s+/gu, " ")
758834
.trim()
759835
.replace(/[. ]+$/u, "");
760-
return sanitized || "MaiBot-OneKey-update.exe";
836+
return sanitized || launcherUpdateDefaultFileName(platform);
761837
}
762838

763839
function selectLauncherUpdateAsset(
764840
rawAssets: unknown,
841+
platform: NodeJS.Platform = process.platform,
842+
arch: string = process.arch,
765843
): GitHubReleaseAsset | undefined {
766844
const assets = Array.isArray(rawAssets)
767845
? rawAssets.filter((item): item is GitHubReleaseAsset => {
768846
return Boolean(item && typeof item === "object");
769847
})
770848
: [];
771-
const executableAssets = assets.filter((asset) => {
849+
const downloadableAssets = assets.filter((asset) => {
772850
const name = typeof asset.name === "string" ? asset.name.toLowerCase() : "";
773851
const url =
774852
typeof asset.browser_download_url === "string"
775853
? asset.browser_download_url
776854
: "";
855+
return Boolean(name && url);
856+
});
857+
858+
if (platform === "darwin") {
859+
const dmgAssets = downloadableAssets.filter((asset) => {
860+
const name =
861+
typeof asset.name === "string" ? asset.name.toLowerCase() : "";
862+
return name.endsWith(".dmg");
863+
});
864+
return selectBestLauncherUpdateAsset(
865+
dmgAssets,
866+
["mac", "darwin", "osx"],
867+
arch,
868+
);
869+
}
870+
871+
if (platform === "win32") {
872+
const executableAssets = downloadableAssets.filter((asset) => {
873+
const name =
874+
typeof asset.name === "string" ? asset.name.toLowerCase() : "";
875+
return name.endsWith(".exe") && !name.includes("uninstaller");
876+
});
877+
return selectBestLauncherUpdateAsset(
878+
executableAssets,
879+
["win", "windows"],
880+
arch,
881+
);
882+
}
883+
884+
const linuxAssets = downloadableAssets.filter((asset) => {
885+
const name = typeof asset.name === "string" ? asset.name.toLowerCase() : "";
777886
return (
778-
name.endsWith(".exe") && !name.includes("uninstaller") && Boolean(url)
887+
name.endsWith(".appimage") ||
888+
name.endsWith(".deb") ||
889+
name.endsWith(".rpm")
779890
);
780891
});
781-
return (
782-
executableAssets.find((asset) =>
783-
String(asset.name ?? "")
784-
.toLowerCase()
785-
.includes("win"),
786-
) ?? executableAssets[0]
787-
);
892+
return selectBestLauncherUpdateAsset(linuxAssets, ["linux"], arch);
788893
}
789894

790895
async function fetchLauncherReleaseNotesInRange(
@@ -1156,7 +1261,7 @@ async function downloadLauncherUpdate(
11561261
onProgress?: LauncherUpdateDownloadProgressCallback,
11571262
): Promise<LauncherUpdateDownloadResult> {
11581263
if (!update.downloadUrl || !update.assetName) {
1159-
throw new Error("最新版本没有可下载的 Windows 安装包");
1264+
throw new Error(`最新版本没有可下载的${launcherUpdatePackageLabel()}`);
11601265
}
11611266

11621267
const controller = new AbortController();
@@ -3241,12 +3346,21 @@ export function registerAppIpc({
32413346
);
32423347
await assertLauncherUpdateAllowed(serviceManager);
32433348
const installerPath = download.installerPath;
3244-
const child = spawn(installerPath, [], {
3245-
detached: true,
3246-
stdio: "ignore",
3247-
windowsHide: false,
3248-
});
3249-
child.unref();
3349+
let willQuit = false;
3350+
if (process.platform === "win32") {
3351+
const child = spawn(installerPath, [], {
3352+
detached: true,
3353+
stdio: "ignore",
3354+
windowsHide: false,
3355+
});
3356+
child.unref();
3357+
willQuit = true;
3358+
} else {
3359+
const openError = await shell.openPath(installerPath);
3360+
if (openError) {
3361+
throw new Error(`安装包打开失败:${openError}`);
3362+
}
3363+
}
32503364
sendProgress({
32513365
phase: "completed",
32523366
assetName: update.assetName,
@@ -3257,18 +3371,24 @@ export function registerAppIpc({
32573371
logStore.append(
32583372
"desktop",
32593373
"system",
3260-
`启动器更新安装器已启动: ${installerPath}`,
3374+
`启动器更新安装包已打开: ${installerPath}`,
32613375
);
3262-
setTimeout(() => requestQuit(), 800);
3376+
if (willQuit) {
3377+
setTimeout(() => requestQuit(), 800);
3378+
}
32633379
return {
32643380
update: publicLauncherUpdateInfo(update),
32653381
installerPath,
32663382
started: true,
3267-
willQuit: true,
3383+
willQuit,
32683384
};
32693385
},
32703386
);
32713387

3388+
ipcMain.handle("launcher:quit", (): void => {
3389+
requestQuit();
3390+
});
3391+
32723392
ipcMain.handle(
32733393
"plugins:listMarket",
32743394
async (

src/preload/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ const desktopBridge: DesktopBridge = {
233233
ipcRenderer.invoke("launcher:checkUpdate") as Promise<LauncherUpdateInfo>,
234234
downloadAndInstallUpdate: () =>
235235
ipcRenderer.invoke("launcher:downloadAndInstallUpdate") as Promise<LauncherUpdateApplyResult>,
236+
quit: () =>
237+
ipcRenderer.invoke("launcher:quit") as Promise<void>,
236238
onDownloadProgress: (callback: (progress: LauncherUpdateDownloadProgress) => void) =>
237239
onIpc("launcher:update-download-progress", callback),
238240
resetSettings: () =>

src/renderer/src/components/app/HomePanel.tsx

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,6 +2221,7 @@ export function HomePanel({
22212221
const [busy, setBusy] = useState<string | null>(null);
22222222
const [error, setError] = useState<string | null>(null);
22232223
const [launcherUpdateInfo, setLauncherUpdateInfo] = useState<LauncherUpdateInfo | null>(null);
2224+
const [launcherQuitPromptOpen, setLauncherQuitPromptOpen] = useState(false);
22242225
const [maibotUpdateInfo, setMaibotUpdateInfo] = useState<MaiBotUpdateInfo | null>(null);
22252226
const [maibotUpdateInfoLoading, setMaibotUpdateInfoLoading] = useState(false);
22262227
const [maibotUpdateInfoError, setMaibotUpdateInfoError] = useState<string | null>(null);
@@ -2669,12 +2670,37 @@ export function HomePanel({
26692670
try {
26702671
const result = await window.maibotDesktop.launcher.downloadAndInstallUpdate();
26712672
setLauncherUpdateInfo(result.update);
2672-
toast.success(result.willQuit ? "安装器已启动,启动器即将退出" : "安装器已启动");
2673+
if (result.willQuit) {
2674+
toast.success("安装包已打开,启动器即将退出");
2675+
return;
2676+
}
2677+
setBusy(null);
2678+
if (snapshot.platform === "darwin") {
2679+
setUpdateDialog(null);
2680+
setLauncherQuitPromptOpen(true);
2681+
} else {
2682+
toast.success("安装包已打开");
2683+
}
2684+
} catch (nextError) {
2685+
setError(messageFromError(nextError));
2686+
setBusy(null);
2687+
}
2688+
}, [launcherUpdateBlocked, snapshot.platform]);
2689+
2690+
const quitAfterLauncherUpdate = useCallback(async () => {
2691+
if (!window.maibotDesktop?.launcher) {
2692+
setError("桌面桥未就绪,无法退出启动器");
2693+
return;
2694+
}
2695+
setBusy("launcher:quit");
2696+
setError(null);
2697+
try {
2698+
await window.maibotDesktop.launcher.quit();
26732699
} catch (nextError) {
26742700
setError(messageFromError(nextError));
26752701
setBusy(null);
26762702
}
2677-
}, [launcherUpdateBlocked]);
2703+
}, []);
26782704

26792705
const openMessagePlatformDialog = useCallback(() => {
26802706
setError(null);
@@ -3589,7 +3615,7 @@ export function HomePanel({
35893615
>
35903616
<DialogContent onInteractOutside={(event) => event.preventDefault()} size="lg">
35913617
<DialogHeader
3592-
description="检查 MaiBot OneKey 的最新安装包,并在确认后启动安装器。"
3618+
description="检查 MaiBot OneKey 的最新安装包,并在确认后下载打开。"
35933619
icon={<PackageCheck className="size-4" />}
35943620
title="更新一键包"
35953621
tone="primary"
@@ -3642,7 +3668,49 @@ export function HomePanel({
36423668
size="sm"
36433669
>
36443670
{busy === "launcher:update" ? <Loader2 className="animate-spin" /> : <Download />}
3645-
下载并安装
3671+
下载并打开
3672+
</Button>
3673+
</DialogFooter>
3674+
</DialogContent>
3675+
</Dialog>
3676+
3677+
<Dialog
3678+
open={launcherQuitPromptOpen}
3679+
onOpenChange={(next) => {
3680+
if (busy !== "launcher:quit") setLauncherQuitPromptOpen(next);
3681+
}}
3682+
>
3683+
<DialogContent onInteractOutside={(event) => event.preventDefault()} size="sm">
3684+
<DialogHeader
3685+
description="请关闭当前启动器并拖拽替换。安装完成后再重新打开 MaiBot OneKey。"
3686+
icon={<PackageCheck className="size-4" />}
3687+
title="安装包已打开"
3688+
tone="primary"
3689+
/>
3690+
<DialogBody className="space-y-3 text-sm text-muted-foreground">
3691+
{error ? (
3692+
<div className={cn("border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive", useRetroHome ? "rounded-sm" : "rounded-lg")}>
3693+
{error}
3694+
</div>
3695+
) : null}
3696+
<p>请在打开的 DMG 窗口中,把新版应用拖拽替换到 Applications。</p>
3697+
</DialogBody>
3698+
<DialogFooter>
3699+
<Button
3700+
disabled={busy === "launcher:quit"}
3701+
onClick={() => setLauncherQuitPromptOpen(false)}
3702+
size="sm"
3703+
variant="ghost"
3704+
>
3705+
稍后退出
3706+
</Button>
3707+
<Button
3708+
disabled={busy === "launcher:quit"}
3709+
onClick={() => void quitAfterLauncherUpdate()}
3710+
size="sm"
3711+
>
3712+
{busy === "launcher:quit" ? <Loader2 className="animate-spin" /> : <X />}
3713+
退出启动器
36463714
</Button>
36473715
</DialogFooter>
36483716
</DialogContent>

src/shared/contracts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,6 +1442,7 @@ export interface DesktopBridge {
14421442
selectAppIcon: (iconId: AppIconId) => Promise<AppIconSettings>;
14431443
checkUpdate: () => Promise<LauncherUpdateInfo>;
14441444
downloadAndInstallUpdate: () => Promise<LauncherUpdateApplyResult>;
1445+
quit: () => Promise<void>;
14451446
onDownloadProgress: (callback: (progress: LauncherUpdateDownloadProgress) => void) => () => void;
14461447
resetSettings: () => Promise<LauncherResetResult>;
14471448
resetAll: () => Promise<LauncherResetResult>;

0 commit comments

Comments
 (0)