Skip to content
Open
24 changes: 21 additions & 3 deletions config/app-upgrade-segments.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
1 change: 1 addition & 0 deletions packages/shared/IpcChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/config/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
2 changes: 2 additions & 0 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App)
installPath: path.dirname(app.getPath('exe'))
}))

ipcMain.handle(IpcChannel.App_GetSigningInfo, () => appService.getSigningInfo())

ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string, bypassRules?: string) => {
let proxyConfig: ProxyConfig

Expand Down
40 changes: 40 additions & 0 deletions src/main/services/AppService.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
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'
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

Expand All @@ -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', timeout: 5000 })
// 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<void> {
// Set login item settings for windows and mac
// linux is not supported because it requires more file operations
Expand Down
25 changes: 15 additions & 10 deletions src/main/services/AppUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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: '<!--LANG:en-->',
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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'
}
})

Expand Down
2 changes: 2 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
40 changes: 35 additions & 5 deletions src/renderer/src/components/Popups/UpdateDialogPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
}
Expand All @@ -23,13 +27,26 @@ const PopupContainer: React.FC<Props> = ({ 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)
Expand All @@ -44,6 +61,10 @@ const PopupContainer: React.FC<Props> = ({ releaseInfo, resolve }) => {
}
}

const handleDownload = () => {
window.api.openWebsite(DOWNLOAD_URL)
}

const onCancel = () => {
setOpen(false)
}
Expand Down Expand Up @@ -79,11 +100,20 @@ const PopupContainer: React.FC<Props> = ({ releaseInfo, resolve }) => {
<Button key="later" onClick={onIgnore} disabled={isInstalling}>
{t('update.later')}
</Button>,
<Button key="install" type="primary" onClick={handleInstall} loading={isInstalling}>
{t('update.install')}
</Button>
requiresManualDownload ? (
<Button key="download" type="primary" onClick={handleDownload}>
{t('update.download')}
</Button>
) : (
<Button key="install" type="primary" onClick={handleInstall} loading={isInstalling}>
{t('update.install')}
</Button>
)
]}>
<ModalBodyWrapper>
{requiresManualDownload && (
<Alert type="warning" showIcon message={t('update.manualDownloadRequired')} style={{ marginBottom: 16 }} />
)}
<ReleaseNotesWrapper className="markdown">
<Markdown>
{typeof releaseNotes === 'string'
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/i18n/locales/en-us.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/i18n/locales/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -5556,8 +5556,10 @@
"show_window": "显示窗口"
},
"update": {
"download": "前往下载",
"install": "立即安装",
"later": "稍后",
"manualDownloadRequired": "此版本不支持自动更新,请前往官网手动下载新版本。",
"message": "发现新版本 {{version}},是否立即安装?",
"noReleaseNotes": "暂无更新日志",
"saveDataError": "保存数据失败,请重试",
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/i18n/locales/zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -5556,8 +5556,10 @@
"show_window": "顯示視窗"
},
"update": {
"download": "前往下載",
"install": "立即安裝",
"later": "稍後",
"manualDownloadRequired": "此版本不支援自動更新,請前往官網手動下載新版本。",
"message": "新版本 {{version}} 已準備就緒,是否立即安裝?",
"noReleaseNotes": "暫無更新日誌",
"saveDataError": "儲存資料失敗,請重試",
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/i18n/translate/de-de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/i18n/translate/el-gr.json
Original file line number Diff line number Diff line change
Expand Up @@ -5556,8 +5556,10 @@
"show_window": "Εμφάνιση παραθύρου"
},
"update": {
"download": "Λήψη",
"install": "Εγκατάσταση",
"later": "Μετά",
"manualDownloadRequired": "Οι αυτόματες ενημερώσεις δεν είναι διαθέσιμες για αυτήν την έκδοση. Παρακαλούμε κατεβάστε τη νέα έκδοση χειροκίνητα από την επίσημη ιστοσελίδα.",
"message": "Νέα έκδοση {{version}} είναι έτοιμη, θέλετε να την εγκαταστήσετε τώρα;",
"noReleaseNotes": "Χωρίς σημειώσεις",
"saveDataError": "Η αποθήκευση των δεδομένων απέτυχε, δοκιμάστε ξανά",
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/i18n/translate/es-es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/i18n/translate/fr-fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/i18n/translate/ja-jp.json
Original file line number Diff line number Diff line change
Expand Up @@ -5556,8 +5556,10 @@
"show_window": "ウィンドウを表示"
},
"update": {
"download": "ダウンロード",
"install": "今すぐインストール",
"later": "後で",
"manualDownloadRequired": "このバージョンでは自動更新がサポートされていません。公式ウェブサイトから新しいバージョンを手動でダウンロードしてください。",
"message": "新バージョン {{version}} が利用可能です。今すぐインストールしますか?",
"noReleaseNotes": "暫無更新日誌",
"saveDataError": "データの保存に失敗しました。もう一度お試しください。",
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/i18n/translate/pt-pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/i18n/translate/ro-ro.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/i18n/translate/ru-ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -5556,8 +5556,10 @@
"show_window": "Показать окно"
},
"update": {
"download": "Скачать",
"install": "Установить",
"later": "Позже",
"manualDownloadRequired": "Автоматические обновления недоступны для этой версии. Пожалуйста, загрузите новую версию вручную с официального сайта.",
"message": "Новая версия {{version}} готова, установить сейчас?",
"noReleaseNotes": "Нет заметок об обновлении",
"saveDataError": "Ошибка сохранения данных, повторите попытку",
Expand Down
Loading