From 3403adfb54d5b5efbd142fc33d9ccc069f64338e Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Wed, 11 Mar 2026 12:06:51 +0800 Subject: [PATCH 1/8] fix: add manual download option for macOS users with old code signing Due to developer signing certificate change, macOS users with the old Team ID (Q24M7JR2C4) cannot auto-update. This adds a manual download button that directs them to the official website. Also refactors AppUpdater headers into a shared getCommonHeaders() function and adds App-Name, App-Version, and OS headers for better update server analytics. Signed-off-by: kangfenmao --- packages/shared/IpcChannel.ts | 1 + packages/shared/config/constant.ts | 2 + src/main/ipc.ts | 28 +++++++++++++ src/main/services/AppUpdater.ts | 25 +++++++----- src/preload/index.ts | 2 + .../components/Popups/UpdateDialogPopup.tsx | 40 ++++++++++++++++--- src/renderer/src/i18n/locales/en-us.json | 2 + src/renderer/src/i18n/locales/zh-cn.json | 2 + src/renderer/src/i18n/locales/zh-tw.json | 2 + src/renderer/src/i18n/translate/de-de.json | 2 + src/renderer/src/i18n/translate/el-gr.json | 2 + src/renderer/src/i18n/translate/es-es.json | 2 + src/renderer/src/i18n/translate/fr-fr.json | 2 + src/renderer/src/i18n/translate/ja-jp.json | 2 + src/renderer/src/i18n/translate/pt-pt.json | 2 + src/renderer/src/i18n/translate/ro-ro.json | 2 + src/renderer/src/i18n/translate/ru-ru.json | 2 + 17 files changed, 105 insertions(+), 15 deletions(-) diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 85d8fd9261d..349eae11f7f 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -10,6 +10,7 @@ export enum IpcChannel { App_Reload = 'app:reload', App_Quit = 'app:quit', App_Info = 'app:info', + App_GetSigningInfo = 'app:get-signing-info', App_Proxy = 'app:proxy', App_SetLaunchToTray = 'app:set-launch-to-tray', App_SetTray = 'app:set-tray', diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index 75bb853f57d..47b681bf9e4 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -502,3 +502,5 @@ export const CHERRYIN_CONFIG = { REDIRECT_URI: 'cherrystudio://oauth/callback', SCOPES: 'openid profile email offline_access balance:read usage:read tokens:read tokens:write' } + +export const APP_NAME = 'Cherry Studio' diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 587c7393a21..658caac4150 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -166,6 +166,34 @@ export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App) installPath: path.dirname(app.getPath('exe')) })) + ipcMain.handle(IpcChannel.App_GetSigningInfo, async () => { + if (!isMac) { + return { teamId: null, bundleId: null, authority: null } + } + + const exePath = app.getPath('exe') + // /path/to/App.app/Contents/MacOS/AppName -> /path/to/App.app + const appPath = exePath.replace(/\/Contents\/MacOS\/.*$/, '') + + try { + const { execSync } = await import('child_process') + const output = execSync(`codesign -dv --verbose=4 "${appPath}" 2>&1`, { encoding: 'utf-8' }) + + const teamIdMatch = output.match(/TeamIdentifier=(.+)/) + const identifierMatch = output.match(/Identifier=(.+)/) + const authorityMatch = output.match(/Authority=([^\n]+)/) + + return { + teamId: teamIdMatch?.[1] || null, + bundleId: identifierMatch?.[1] || null, + authority: authorityMatch?.[1] || null + } + } catch (error) { + logger.error('Failed to get signing info', error as Error) + return { teamId: null, bundleId: null, authority: null } + } + }) + ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string, bypassRules?: string) => { let proxyConfig: ProxyConfig diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts index 57dc3fb2a83..c1b9f89f143 100644 --- a/src/main/services/AppUpdater.ts +++ b/src/main/services/AppUpdater.ts @@ -2,7 +2,7 @@ import { loggerService } from '@logger' import { isWin } from '@main/constant' import { getIpCountry } from '@main/utils/ipService' import { generateUserAgent } from '@main/utils/systemInfo' -import { FeedUrl, UpdateConfigUrl, UpdateMirror, UpgradeChannel } from '@shared/config/constant' +import { APP_NAME, FeedUrl, UpdateConfigUrl, UpdateMirror, UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import type { UpdateInfo } from 'builder-util-runtime' import { CancellationToken } from 'builder-util-runtime' @@ -17,6 +17,17 @@ import { windowService } from './WindowService' const logger = loggerService.withContext('AppUpdater') +function getCommonHeaders() { + return { + 'User-Agent': generateUserAgent(), + 'Cache-Control': 'no-cache', + 'Client-Id': configManager.getClientId(), + 'App-Name': APP_NAME, + 'App-Version': `v${app.getVersion()}`, + OS: process.platform + } +} + // Language markers constants for multi-language release notes const LANG_MARKERS = { EN_START: '', @@ -58,10 +69,7 @@ export default class AppUpdater { autoUpdater.autoInstallOnAppQuit = configManager.getAutoUpdate() autoUpdater.requestHeaders = { ...autoUpdater.requestHeaders, - 'User-Agent': generateUserAgent(), - 'X-Client-Id': configManager.getClientId(), - // no-cache - 'Cache-Control': 'no-cache' + ...getCommonHeaders() } autoUpdater.on('error', (error) => { @@ -142,11 +150,8 @@ export default class AppUpdater { logger.info(`Fetching update config from ${configUrl} (mirror: ${mirror})`) const response = await net.fetch(configUrl, { headers: { - 'User-Agent': generateUserAgent(), - Accept: 'application/json', - 'X-Client-Id': configManager.getClientId(), - // no-cache - 'Cache-Control': 'no-cache' + ...getCommonHeaders(), + Accept: 'application/json' } }) diff --git a/src/preload/index.ts b/src/preload/index.ts index ba90d273683..ac407963b80 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -103,6 +103,8 @@ export function tracedInvoke(channel: string, spanContext: SpanContext | undefin // Custom APIs for renderer const api = { getAppInfo: () => ipcRenderer.invoke(IpcChannel.App_Info), + getSigningInfo: (): Promise<{ teamId: string | null; bundleId: string | null; authority: string | null }> => + ipcRenderer.invoke(IpcChannel.App_GetSigningInfo), getDiskInfo: (directoryPath: string): Promise<{ free: number; size: number } | null> => ipcRenderer.invoke(IpcChannel.App_GetDiskInfo, directoryPath), reload: () => ipcRenderer.invoke(IpcChannel.App_Reload), diff --git a/src/renderer/src/components/Popups/UpdateDialogPopup.tsx b/src/renderer/src/components/Popups/UpdateDialogPopup.tsx index 593c882bf51..647c45b7b41 100644 --- a/src/renderer/src/components/Popups/UpdateDialogPopup.tsx +++ b/src/renderer/src/components/Popups/UpdateDialogPopup.tsx @@ -2,7 +2,7 @@ import { loggerService } from '@logger' import { TopView } from '@renderer/components/TopView' import { handleSaveData, useAppDispatch } from '@renderer/store' import { setUpdateState } from '@renderer/store/runtime' -import { Button, Modal } from 'antd' +import { Alert, Button, Modal } from 'antd' import type { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -11,6 +11,10 @@ import styled from 'styled-components' const logger = loggerService.withContext('UpdateDialog') +// Old Team ID that requires manual download after v1.8.0 +const OLD_TEAM_ID = 'Q24M7JR2C4' +const DOWNLOAD_URL = 'https://www.cherry-ai.com/download' + interface ShowParams { releaseInfo: UpdateInfo | null } @@ -23,13 +27,26 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { const { t } = useTranslation() const [open, setOpen] = useState(true) const [isInstalling, setIsInstalling] = useState(false) + const [requiresManualDownload, setRequiresManualDownload] = useState(false) const dispatch = useAppDispatch() + const isMac = window.electron.process.platform === 'darwin' + useEffect(() => { if (releaseInfo) { logger.info('Update dialog opened', { version: releaseInfo.version }) } - }, [releaseInfo]) + + // Check if macOS user with old Team ID needs manual download + if (isMac) { + window.api.getSigningInfo().then((info) => { + if (info.teamId === OLD_TEAM_ID) { + setRequiresManualDownload(true) + logger.info('Manual download required due to signing change', { teamId: info.teamId }) + } + }) + } + }, [releaseInfo, isMac]) const handleInstall = async () => { setIsInstalling(true) @@ -44,6 +61,10 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { } } + const handleDownload = () => { + window.api.openWebsite(DOWNLOAD_URL) + } + const onCancel = () => { setOpen(false) } @@ -79,11 +100,20 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { , - + requiresManualDownload ? ( + + ) : ( + + ) ]}> + {requiresManualDownload && ( + + )} {typeof releaseNotes === 'string' diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 3c8bb80a947..685a174816a 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -5556,8 +5556,10 @@ "show_window": "Show Window" }, "update": { + "download": "Download", "install": "Install", "later": "Later", + "manualDownloadRequired": "Automatic updates are not available for this version. Please download the new version manually from the official website.", "message": "New version {{version}} is ready, do you want to install it now?", "noReleaseNotes": "No release notes", "saveDataError": "Failed to save data, please try again.", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 36d90127dd1..16ed4600d6f 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -5556,8 +5556,10 @@ "show_window": "显示窗口" }, "update": { + "download": "前往下载", "install": "立即安装", "later": "稍后", + "manualDownloadRequired": "此版本不支持自动更新,请前往官网手动下载新版本。", "message": "发现新版本 {{version}},是否立即安装?", "noReleaseNotes": "暂无更新日志", "saveDataError": "保存数据失败,请重试", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index daf44c07540..33a12ffca85 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -5556,8 +5556,10 @@ "show_window": "顯示視窗" }, "update": { + "download": "前往下載", "install": "立即安裝", "later": "稍後", + "manualDownloadRequired": "此版本不支援自動更新,請前往官網手動下載新版本。", "message": "新版本 {{version}} 已準備就緒,是否立即安裝?", "noReleaseNotes": "暫無更新日誌", "saveDataError": "儲存資料失敗,請重試", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index 705fe271d1a..a7c7ff242e1 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -5556,8 +5556,10 @@ "show_window": "Fenster anzeigen" }, "update": { + "download": "Herunterladen", "install": "Jetzt installieren", "later": "Später", + "manualDownloadRequired": "Automatische Updates sind für diese Version nicht verfügbar. Bitte laden Sie die neue Version manuell von der offiziellen Website herunter.", "message": "Neue Version {{version}} gefunden. Jetzt installieren?", "noReleaseNotes": "Kein Changelog verfügbar", "saveDataError": "Speichern fehlgeschlagen, bitte erneut versuchen", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index befdd6ba4b3..f793ad95f08 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -5556,8 +5556,10 @@ "show_window": "Εμφάνιση παραθύρου" }, "update": { + "download": "Λήψη", "install": "Εγκατάσταση", "later": "Μετά", + "manualDownloadRequired": "Οι αυτόματες ενημερώσεις δεν είναι διαθέσιμες για αυτήν την έκδοση. Παρακαλούμε κατεβάστε τη νέα έκδοση χειροκίνητα από την επίσημη ιστοσελίδα.", "message": "Νέα έκδοση {{version}} είναι έτοιμη, θέλετε να την εγκαταστήσετε τώρα;", "noReleaseNotes": "Χωρίς σημειώσεις", "saveDataError": "Η αποθήκευση των δεδομένων απέτυχε, δοκιμάστε ξανά", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index a90f760b8e8..df0d89ab1b0 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -5556,8 +5556,10 @@ "show_window": "Mostrar ventana" }, "update": { + "download": "Descargar", "install": "Instalar", "later": "Más tarde", + "manualDownloadRequired": "Las actualizaciones automáticas no están disponibles para esta versión. Por favor, descarga la nueva versión manualmente desde el sitio web oficial.", "message": "Nueva versión {{version}} disponible, ¿desea instalarla ahora?", "noReleaseNotes": "Sin notas de la versión", "saveDataError": "Error al guardar los datos, inténtalo de nuevo", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index b3c3bd6ba74..dc3f87200a0 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -5556,8 +5556,10 @@ "show_window": "Afficher la fenêtre" }, "update": { + "download": "Télécharger", "install": "Installer", "later": "Plus tard", + "manualDownloadRequired": "Les mises à jour automatiques ne sont pas disponibles pour cette version. Veuillez télécharger la nouvelle version manuellement depuis le site officiel.", "message": "Nouvelle version {{version}} disponible, voulez-vous l'installer maintenant ?", "noReleaseNotes": "Aucune note de version", "saveDataError": "Échec de la sauvegarde des données, veuillez réessayer", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 85b8de18821..f4632ffee6d 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -5556,8 +5556,10 @@ "show_window": "ウィンドウを表示" }, "update": { + "download": "ダウンロード", "install": "今すぐインストール", "later": "後で", + "manualDownloadRequired": "このバージョンでは自動更新がサポートされていません。公式ウェブサイトから新しいバージョンを手動でダウンロードしてください。", "message": "新バージョン {{version}} が利用可能です。今すぐインストールしますか?", "noReleaseNotes": "暫無更新日誌", "saveDataError": "データの保存に失敗しました。もう一度お試しください。", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 3e2e0dd46c0..1ce993caf92 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -5556,8 +5556,10 @@ "show_window": "Exibir Janela" }, "update": { + "download": "Baixar", "install": "Instalar", "later": "Mais tarde", + "manualDownloadRequired": "As atualizações automáticas não estão disponíveis para esta versão. Por favor, baixe a nova versão manualmente no site oficial.", "message": "Nova versão {{version}} disponível, deseja instalar agora?", "noReleaseNotes": "Sem notas de versão", "saveDataError": "Falha ao salvar os dados, tente novamente", diff --git a/src/renderer/src/i18n/translate/ro-ro.json b/src/renderer/src/i18n/translate/ro-ro.json index 7eb3065811d..e46ca3f21e8 100644 --- a/src/renderer/src/i18n/translate/ro-ro.json +++ b/src/renderer/src/i18n/translate/ro-ro.json @@ -5556,8 +5556,10 @@ "show_window": "Afișează fereastra" }, "update": { + "download": "Descărcare", "install": "Instalează", "later": "Mai târziu", + "manualDownloadRequired": "Actualizările automate nu sunt disponibile pentru această versiune. Vă rugăm să descărcați noua versiune manual de pe site-ul oficial.", "message": "Noua versiune {{version}} este gata, vrei să o instalezi acum?", "noReleaseNotes": "Nicio notă de lansare", "saveDataError": "Salvarea datelor a eșuat, te rugăm să încerci din nou.", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index c5e2c98a3f6..4c0e923be99 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -5556,8 +5556,10 @@ "show_window": "Показать окно" }, "update": { + "download": "Скачать", "install": "Установить", "later": "Позже", + "manualDownloadRequired": "Автоматические обновления недоступны для этой версии. Пожалуйста, загрузите новую версию вручную с официального сайта.", "message": "Новая версия {{version}} готова, установить сейчас?", "noReleaseNotes": "Нет заметок об обновлении", "saveDataError": "Ошибка сохранения данных, повторите попытку", From cde4fd76870679d5c710f6a5fd45edbe47d33d94 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Wed, 11 Mar 2026 18:45:53 +0800 Subject: [PATCH 2/8] fix: refactor signing info retrieval to use AppService method --- src/main/ipc.ts | 28 +---------------------- src/main/services/AppService.ts | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 658caac4150..10838b02b63 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -166,33 +166,7 @@ export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App) installPath: path.dirname(app.getPath('exe')) })) - ipcMain.handle(IpcChannel.App_GetSigningInfo, async () => { - if (!isMac) { - return { teamId: null, bundleId: null, authority: null } - } - - const exePath = app.getPath('exe') - // /path/to/App.app/Contents/MacOS/AppName -> /path/to/App.app - const appPath = exePath.replace(/\/Contents\/MacOS\/.*$/, '') - - try { - const { execSync } = await import('child_process') - const output = execSync(`codesign -dv --verbose=4 "${appPath}" 2>&1`, { encoding: 'utf-8' }) - - const teamIdMatch = output.match(/TeamIdentifier=(.+)/) - const identifierMatch = output.match(/Identifier=(.+)/) - const authorityMatch = output.match(/Authority=([^\n]+)/) - - return { - teamId: teamIdMatch?.[1] || null, - bundleId: identifierMatch?.[1] || null, - authority: authorityMatch?.[1] || null - } - } catch (error) { - logger.error('Failed to get signing info', error as Error) - return { teamId: null, bundleId: null, authority: null } - } - }) + ipcMain.handle(IpcChannel.App_GetSigningInfo, () => appService.getSigningInfo()) ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string, bypassRules?: string) => { let proxyConfig: ProxyConfig diff --git a/src/main/services/AppService.ts b/src/main/services/AppService.ts index a7e1fa95351..12a177d2ca4 100644 --- a/src/main/services/AppService.ts +++ b/src/main/services/AppService.ts @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import { isDev, isLinux, isMac, isWin } from '@main/constant' +import { spawnSync } from 'child_process' import { app } from 'electron' import fs from 'fs' import os from 'os' @@ -7,6 +8,12 @@ import path from 'path' const logger = loggerService.withContext('AppService') +export interface SigningInfo { + teamId: string | null + bundleId: string | null + authority: string | null +} + export class AppService { private static instance: AppService @@ -21,6 +28,39 @@ export class AppService { return AppService.instance } + /** + * Get macOS app signing information (team ID, bundle ID, authority) + * Returns null values for non-macOS platforms or unsigned apps + */ + public getSigningInfo(): SigningInfo { + if (!isMac) { + return { teamId: null, bundleId: null, authority: null } + } + + const exePath = app.getPath('exe') + // /path/to/App.app/Contents/MacOS/AppName -> /path/to/App.app + const appPath = exePath.replace(/\/Contents\/MacOS\/.*$/, '') + + try { + const result = spawnSync('codesign', ['-dv', '--verbose=4', appPath], { encoding: 'utf-8' }) + // codesign outputs signing info to stderr + const output = result.stderr || result.stdout + + const teamIdMatch = output.match(/TeamIdentifier=(.+)/) + const identifierMatch = output.match(/Identifier=(.+)/) + const authorityMatch = output.match(/Authority=([^\n]+)/) + + return { + teamId: teamIdMatch?.[1] || null, + bundleId: identifierMatch?.[1] || null, + authority: authorityMatch?.[1] || null + } + } catch (error) { + logger.error('Failed to get signing info', error as Error) + return { teamId: null, bundleId: null, authority: null } + } + } + public async setAppLaunchOnBoot(isLaunchOnBoot: boolean): Promise { // Set login item settings for windows and mac // linux is not supported because it requires more file operations From 93878669d6b8ca5cb004c2b5435db42f336b3f68 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 12 Mar 2026 10:06:52 +0800 Subject: [PATCH 3/8] fix: add timeout to codesign command to prevent main process blocking Signed-off-by: kangfenmao --- src/main/services/AppService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/services/AppService.ts b/src/main/services/AppService.ts index 12a177d2ca4..ca699eeeb03 100644 --- a/src/main/services/AppService.ts +++ b/src/main/services/AppService.ts @@ -42,7 +42,7 @@ export class AppService { const appPath = exePath.replace(/\/Contents\/MacOS\/.*$/, '') try { - const result = spawnSync('codesign', ['-dv', '--verbose=4', appPath], { encoding: 'utf-8' }) + const result = spawnSync('codesign', ['-dv', '--verbose=4', appPath], { encoding: 'utf-8', timeout: 5000 }) // codesign outputs signing info to stderr const output = result.stderr || result.stdout From 7ddeb8c225ae94c61b517fcfd6268855418a0098 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 12 Mar 2026 10:56:26 +0800 Subject: [PATCH 4/8] fix: update app upgrade segments for legacy and current v1 versions --- config/app-upgrade-segments.json | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/config/app-upgrade-segments.json b/config/app-upgrade-segments.json index 70c8ac25f0a..af590480f21 100644 --- a/config/app-upgrade-segments.json +++ b/config/app-upgrade-segments.json @@ -1,13 +1,31 @@ { "segments": [ { - "id": "legacy-v1", + "id": "legacy-v1-locked", "type": "legacy", "match": { - "range": ">=1.0.0 <2.0.0" + "range": ">=1.0.0 <1.7.25" }, + "lockedVersion": "1.7.25", "minCompatibleVersion": "1.0.0", - "description": "Last stable v1.7.x release - required intermediate version for users below v1.7", + "description": "Gateway version for users below v1.7.25 - locked to v1.7.25", + "channelTemplates": { + "latest": { + "feedTemplates": { + "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", + "gitcode": "https://releases.cherry-ai.com" + } + } + } + }, + { + "id": "current-v1", + "type": "latest", + "match": { + "range": ">=1.7.25 <2.0.0" + }, + "minCompatibleVersion": "1.7.25", + "description": "Current latest v1.x release for users on v1.7.25+", "channelTemplates": { "latest": { "feedTemplates": { From e0770d6b26a40fc91cc0d106aef09898e0184581 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 12 Mar 2026 15:12:36 +0800 Subject: [PATCH 5/8] fix: exclude config/app-upgrade-segments.json from includes in biome.jsonc --- biome.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biome.jsonc b/biome.jsonc index 1568fec7286..0a7e4b03c1c 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -14,7 +14,7 @@ } }, "enabled": true, - "includes": ["**/*.json", "!*.json", "!**/package.json", "!coverage/**"] + "includes": ["**/*.json", "!*.json", "!**/package.json", "!coverage/**", "!config/app-upgrade-segments.json"] }, "css": { "formatter": { From 034e98eeaf5f233c7f06c1832584b207c4e0da25 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 12 Mar 2026 15:12:59 +0800 Subject: [PATCH 6/8] fix: update gitcode feedTemplates URLs in app-upgrade-segments.json --- config/app-upgrade-segments.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/app-upgrade-segments.json b/config/app-upgrade-segments.json index af590480f21..d0148b263b4 100644 --- a/config/app-upgrade-segments.json +++ b/config/app-upgrade-segments.json @@ -13,7 +13,7 @@ "latest": { "feedTemplates": { "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", - "gitcode": "https://releases.cherry-ai.com" + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" } } } @@ -30,7 +30,7 @@ "latest": { "feedTemplates": { "github": "https://github.com/CherryHQ/cherry-studio/releases/download/{{tag}}", - "gitcode": "https://releases.cherry-ai.com" + "gitcode": "https://gitcode.com/CherryHQ/cherry-studio/releases/download/{{tag}}" } }, "rc": { From f4da3f46245cbd69f1731ff6143ae1bebbfaf2cf Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 12 Mar 2026 15:16:10 +0800 Subject: [PATCH 7/8] fix: enhance codesign error handling and update regex for signing info extraction --- src/main/services/AppService.ts | 13 +++++++++---- .../src/components/Popups/UpdateDialogPopup.tsx | 7 +++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/services/AppService.ts b/src/main/services/AppService.ts index ca699eeeb03..2d473ef2596 100644 --- a/src/main/services/AppService.ts +++ b/src/main/services/AppService.ts @@ -43,12 +43,17 @@ export class AppService { try { const result = spawnSync('codesign', ['-dv', '--verbose=4', appPath], { encoding: 'utf-8', timeout: 5000 }) - // codesign outputs signing info to stderr + + if (result.error || result.status !== 0) { + logger.warn('codesign check failed', { error: result.error, status: result.status }) + return { teamId: null, bundleId: null, authority: null } + } + const output = result.stderr || result.stdout - const teamIdMatch = output.match(/TeamIdentifier=(.+)/) - const identifierMatch = output.match(/Identifier=(.+)/) - const authorityMatch = output.match(/Authority=([^\n]+)/) + const teamIdMatch = output.match(/^TeamIdentifier=(.+)$/m) + const identifierMatch = output.match(/^Identifier=(.+)$/m) + const authorityMatch = output.match(/^Authority=([^\n]+)$/m) return { teamId: teamIdMatch?.[1] || null, diff --git a/src/renderer/src/components/Popups/UpdateDialogPopup.tsx b/src/renderer/src/components/Popups/UpdateDialogPopup.tsx index 647c45b7b41..024966c9612 100644 --- a/src/renderer/src/components/Popups/UpdateDialogPopup.tsx +++ b/src/renderer/src/components/Popups/UpdateDialogPopup.tsx @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import { TopView } from '@renderer/components/TopView' +import { isMac } from '@renderer/config/constant' import { handleSaveData, useAppDispatch } from '@renderer/store' import { setUpdateState } from '@renderer/store/runtime' import { Alert, Button, Modal } from 'antd' @@ -11,7 +12,7 @@ import styled from 'styled-components' const logger = loggerService.withContext('UpdateDialog') -// Old Team ID that requires manual download after v1.8.0 +// Old Team ID that requires manual download const OLD_TEAM_ID = 'Q24M7JR2C4' const DOWNLOAD_URL = 'https://www.cherry-ai.com/download' @@ -30,8 +31,6 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { const [requiresManualDownload, setRequiresManualDownload] = useState(false) const dispatch = useAppDispatch() - const isMac = window.electron.process.platform === 'darwin' - useEffect(() => { if (releaseInfo) { logger.info('Update dialog opened', { version: releaseInfo.version }) @@ -46,7 +45,7 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { } }) } - }, [releaseInfo, isMac]) + }, [releaseInfo]) const handleInstall = async () => { setIsInstalling(true) From f40d3062643d13541625a20644008fb49872dddf Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 12 Mar 2026 15:47:55 +0800 Subject: [PATCH 8/8] fix: add manual install update functionality for macOS users with old code signing --- packages/shared/IpcChannel.ts | 1 + src/main/ipc.ts | 1 + src/main/services/AppUpdater.ts | 119 +++++++++++++++++- src/preload/index.ts | 2 + .../components/Popups/UpdateDialogPopup.tsx | 50 +++++--- src/renderer/src/i18n/locales/en-us.json | 3 +- src/renderer/src/i18n/locales/zh-cn.json | 3 +- src/renderer/src/i18n/locales/zh-tw.json | 3 +- src/renderer/src/i18n/translate/de-de.json | 3 +- src/renderer/src/i18n/translate/el-gr.json | 3 +- src/renderer/src/i18n/translate/es-es.json | 3 +- src/renderer/src/i18n/translate/fr-fr.json | 3 +- src/renderer/src/i18n/translate/ja-jp.json | 3 +- src/renderer/src/i18n/translate/pt-pt.json | 3 +- src/renderer/src/i18n/translate/ro-ro.json | 3 +- src/renderer/src/i18n/translate/ru-ru.json | 3 +- 16 files changed, 180 insertions(+), 26 deletions(-) diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 349eae11f7f..12444832c4b 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -7,6 +7,7 @@ export enum IpcChannel { App_SetSpellCheckLanguages = 'app:set-spell-check-languages', App_CheckForUpdate = 'app:check-for-update', App_QuitAndInstall = 'app:quit-and-install', + App_ManualInstallUpdate = 'app:manual-install-update', App_Reload = 'app:reload', App_Quit = 'app:quit', App_Info = 'app:info', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 10838b02b63..1d3ae10215c 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -189,6 +189,7 @@ export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App) // Update ipcMain.handle(IpcChannel.App_QuitAndInstall, () => appUpdater.quitAndInstall()) + ipcMain.handle(IpcChannel.App_ManualInstallUpdate, () => appUpdater.manualInstallUpdate()) // language ipcMain.handle(IpcChannel.App_SetLanguage, (_, language) => { diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts index c1b9f89f143..e18bc52e325 100644 --- a/src/main/services/AppUpdater.ts +++ b/src/main/services/AppUpdater.ts @@ -1,14 +1,19 @@ import { loggerService } from '@logger' -import { isWin } from '@main/constant' +import { isMac, isWin } from '@main/constant' import { getIpCountry } from '@main/utils/ipService' import { generateUserAgent } from '@main/utils/systemInfo' import { APP_NAME, FeedUrl, UpdateConfigUrl, UpdateMirror, UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import type { UpdateInfo } from 'builder-util-runtime' import { CancellationToken } from 'builder-util-runtime' +import { exec, execSync } from 'child_process' +import { promisify } from 'util' + +const execAsync = promisify(exec) import { app, net } from 'electron' import type { AppUpdater as _AppUpdater, Logger, NsisUpdater, UpdateCheckResult } from 'electron-updater' import { autoUpdater } from 'electron-updater' +import fs from 'fs' import path from 'path' import semver from 'semver' @@ -315,6 +320,118 @@ export default class AppUpdater { setImmediate(() => autoUpdater.quitAndInstall()) } + /** + * Manual install update for macOS users with old code signing + * This bypasses Squirrel.Mac which validates code signing + */ + public async manualInstallUpdate(): Promise<{ success: boolean; error?: string }> { + if (!isMac) { + return { success: false, error: 'Manual install is only supported on macOS' } + } + + // Constants + const ZIP_PATTERN = /^Cherry-Studio-\d+\.\d+\.\d+(-arm64)?\.zip$/ + const APP_NAME = 'Cherry Studio.app' + const TARGET_PATH = `/Applications/${APP_NAME}` + const cacheDir = path.join(app.getPath('home'), 'Library/Caches', 'cherrystudio-updater', 'pending') + const extractDir = path.join(app.getPath('temp'), 'cherry-studio-update') + const newAppPath = path.join(extractDir, APP_NAME) + + // Helper functions + const findUpdateZip = (): string | null => { + if (!fs.existsSync(cacheDir)) return null + const zipFile = fs.readdirSync(cacheDir).find((f) => ZIP_PATTERN.test(f)) + return zipFile ? path.join(cacheDir, zipFile) : null + } + + const extractZip = (zipPath: string): void => { + fs.rmSync(extractDir, { recursive: true, force: true }) + fs.mkdirSync(extractDir, { recursive: true }) + execSync(`unzip -o "${zipPath}" -d "${extractDir}"`) + } + + const isValidApp = (): boolean => { + return ( + fs.existsSync(newAppPath) && + fs.existsSync(path.join(newAppPath, 'Contents', 'Info.plist')) && + fs.existsSync(path.join(newAppPath, 'Contents', 'MacOS')) + ) + } + + const replaceAppWithAdminPrivileges = async (): Promise<{ success: boolean; error?: string }> => { + const language = configManager.getLanguage() + const prompt = + language === 'zh-CN' || language === 'zh-TW' + ? 'Cherry Studio 需要管理员权限来安装更新' + : 'Cherry Studio needs administrator privileges to install the update.' + + const scriptPath = path.join(app.getPath('temp'), `cherry-update-${Date.now()}.scpt`) + const scriptContent = `do shell script "rm -rf \\"${TARGET_PATH}\\" && mv \\"${newAppPath}\\" \\"${TARGET_PATH}\\"" with administrator privileges with prompt "${prompt}"` + + try { + fs.writeFileSync(scriptPath, scriptContent, { encoding: 'utf-8', mode: 0o600 }) + await execAsync(`osascript "${scriptPath}"`, { timeout: 60000 }) + logger.info('Manual install: app replaced successfully') + return { success: true } + } catch (error) { + const msg = (error as Error).message + logger.error('Manual install: replace failed', error as Error) + if (msg.includes('User canceled') || msg.includes('-128')) { + return { success: false, error: 'User cancelled' } + } + return { success: false, error: msg } + } finally { + fs.rmSync(scriptPath, { force: true }) + } + } + + const scheduleRelaunch = (): void => { + const pid = process.pid + const scriptPath = path.join(app.getPath('temp'), `cherry-relaunch-${Date.now()}.sh`) + const script = `#!/bin/sh +sleep 1 +kill -9 ${pid} 2>/dev/null +# Wait for process exit (max 30s) +for i in $(seq 1 60); do kill -0 ${pid} 2>/dev/null || break; sleep 0.5; done +open "${TARGET_PATH}" +rm -f "${scriptPath}" +` + fs.writeFileSync(scriptPath, script, { mode: 0o755 }) + + const { spawn } = require('child_process') + spawn('/bin/sh', [scriptPath], { detached: true, stdio: 'ignore' }).unref() + } + + // Main logic + const updateZip = findUpdateZip() + if (!updateZip) { + logger.error('Manual install: valid zip file not found', { cacheDir }) + return { success: false, error: 'Update file not found' } + } + + logger.info('Manual install: found update zip', { updateZip }) + + try { + extractZip(updateZip) + + if (!isValidApp()) { + logger.error('Manual install: extracted app invalid', { newAppPath }) + return { success: false, error: 'Extracted app not found' } + } + + const result = await replaceAppWithAdminPrivileges() + if (!result.success) { + return result + } + + scheduleRelaunch() + return { success: true } + } catch (error) { + logger.error('Manual install failed', error as Error) + return { success: false, error: (error as Error).message } + } + } + /** * Check if release notes contain multi-language markers */ diff --git a/src/preload/index.ts b/src/preload/index.ts index ac407963b80..f5983ecd856 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -113,6 +113,8 @@ const api = { ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules), checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate), quitAndInstall: () => ipcRenderer.invoke(IpcChannel.App_QuitAndInstall), + manualInstallUpdate: (): Promise<{ success: boolean; error?: string }> => + ipcRenderer.invoke(IpcChannel.App_ManualInstallUpdate), setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang), setEnableSpellCheck: (isEnable: boolean) => ipcRenderer.invoke(IpcChannel.App_SetEnableSpellCheck, isEnable), setSpellCheckLanguages: (languages: string[]) => ipcRenderer.invoke(IpcChannel.App_SetSpellCheckLanguages, languages), diff --git a/src/renderer/src/components/Popups/UpdateDialogPopup.tsx b/src/renderer/src/components/Popups/UpdateDialogPopup.tsx index 024966c9612..14930f9bcb3 100644 --- a/src/renderer/src/components/Popups/UpdateDialogPopup.tsx +++ b/src/renderer/src/components/Popups/UpdateDialogPopup.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components' const logger = loggerService.withContext('UpdateDialog') -// Old Team ID that requires manual download +// Old Team ID that requires manual install const OLD_TEAM_ID = 'Q24M7JR2C4' const DOWNLOAD_URL = 'https://www.cherry-ai.com/download' @@ -28,7 +28,7 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { const { t } = useTranslation() const [open, setOpen] = useState(true) const [isInstalling, setIsInstalling] = useState(false) - const [requiresManualDownload, setRequiresManualDownload] = useState(false) + const [requiresManualInstall, setRequiresManualInstall] = useState(false) const dispatch = useAppDispatch() useEffect(() => { @@ -36,12 +36,12 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { logger.info('Update dialog opened', { version: releaseInfo.version }) } - // Check if macOS user with old Team ID needs manual download + // Check if macOS user with old Team ID needs manual install if (isMac) { - window.api.getSigningInfo().then((info) => { - if (info.teamId === OLD_TEAM_ID) { - setRequiresManualDownload(true) - logger.info('Manual download required due to signing change', { teamId: info.teamId }) + window.api.getSigningInfo().then((signingInfo) => { + if (signingInfo.teamId === OLD_TEAM_ID) { + setRequiresManualInstall(true) + logger.info('Manual install required', { teamId: signingInfo.teamId }) } }) } @@ -60,8 +60,30 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { } } - const handleDownload = () => { - window.api.openWebsite(DOWNLOAD_URL) + const handleManualInstall = async () => { + setIsInstalling(true) + try { + await handleSaveData() + const result = await window.api.manualInstallUpdate() + + if (!result.success) { + setIsInstalling(false) + if (result.error === 'User cancelled') { + // User cancelled password dialog, do nothing + return + } + logger.error('Manual install failed', { error: result.error }) + window.toast.error(t('update.manualInstallError')) + // Fallback to download page + window.api.openWebsite(DOWNLOAD_URL) + } + // If success, app will relaunch automatically + } catch (error) { + logger.error('Manual install error', error as Error) + setIsInstalling(false) + window.toast.error(t('update.manualInstallError')) + window.api.openWebsite(DOWNLOAD_URL) + } } const onCancel = () => { @@ -99,9 +121,9 @@ const PopupContainer: React.FC = ({ releaseInfo, resolve }) => { , - requiresManualDownload ? ( - ) : (