feat: check update

This commit is contained in:
kangfenmao 2024-07-16 19:57:25 +08:00
parent 80e34688b1
commit e962351b13
14 changed files with 214 additions and 74 deletions

View File

@ -1,3 +1,6 @@
provider: generic # provider: generic
url: http://127.0.0.1:8080 # url: http://127.0.0.1:8080
updaterCacheDirName: cherry-studio-updater # updaterCacheDirName: cherry-studio-updater
provider: github
repo: cherry-studio
owner: kangfenmao

View File

@ -56,7 +56,5 @@ electronDownload:
afterSign: scripts/notarize.js afterSign: scripts/notarize.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
- 修复多语言提示错误 - 修复更新日志页面不能滚动问题
- 修复智谱AI默认模型错误问题 - 新增检查更新按钮
- 修复 OpenRouter API 检测出错问题
- 修复模型提供商多语言翻译错误问题

View File

@ -1,13 +1,13 @@
import { electronApp, is, optimizer } from '@electron-toolkit/utils' import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import * as Sentry from '@sentry/electron/main'
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron' import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import windowStateKeeper from 'electron-window-state' import windowStateKeeper from 'electron-window-state'
import { join } from 'path' import { join } from 'path'
import icon from '../../resources/icon.png?asset' import icon from '../../resources/icon.png?asset'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import AppUpdater from './updater' import AppUpdater from './updater'
import * as Sentry from '@sentry/electron/main'
function createWindow(): void { function createWindow() {
// Load the previous state with fallback to defaults // Load the previous state with fallback to defaults
const mainWindowState = windowStateKeeper({ const mainWindowState = windowStateKeeper({
defaultWidth: 1080, defaultWidth: 1080,
@ -62,6 +62,8 @@ function createWindow(): void {
} else { } else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html')) mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
} }
return mainWindow
} }
// This method will be called when Electron has finished // This method will be called when Electron has finished
@ -78,28 +80,38 @@ app.whenReady().then(() => {
optimizer.watchWindowShortcuts(window) optimizer.watchWindowShortcuts(window)
}) })
// IPC
ipcMain.handle('get-app-info', () => ({
version: app.getVersion()
}))
createWindow()
app.on('activate', function () { app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the // On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open. // dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow() if (BrowserWindow.getAllWindows().length === 0) createWindow()
}) })
installExtension(REDUX_DEVTOOLS) const mainWindow = createWindow()
.then((name) => console.log(`Added Extension: ${name}`))
.catch((err) => console.log('An error occurred: ', err))
if (app.isPackaged) { const { autoUpdater } = new AppUpdater(mainWindow)
setTimeout(() => new AppUpdater(), 3000)
// IPC
ipcMain.handle('get-app-info', () => ({
version: app.getVersion(),
isPackaged: app.isPackaged
}))
ipcMain.handle('open-website', (_, url: string) => {
shell.openExternal(url)
})
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
ipcMain.handle('check-for-update', async () => {
autoUpdater.logger?.info('触发检查更新')
return {
currentVersion: autoUpdater.currentVersion,
update: await autoUpdater.checkForUpdates()
} }
}) })
installExtension(REDUX_DEVTOOLS)
})
// Quit when all windows are closed, except on macOS. There, it's common // Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits // for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q. // explicitly with Cmd + Q.

View File

@ -1,24 +1,20 @@
import { autoUpdater, UpdateInfo } from 'electron-updater' import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
import logger from 'electron-log' import logger from 'electron-log'
import { dialog, ipcMain } from 'electron' import { BrowserWindow, dialog } from 'electron'
export default class AppUpdater { export default class AppUpdater {
constructor() { autoUpdater: _AppUpdater = autoUpdater
constructor(mainWindow: BrowserWindow) {
logger.transports.file.level = 'debug' logger.transports.file.level = 'debug'
autoUpdater.logger = logger autoUpdater.logger = logger
autoUpdater.forceDevUpdateConfig = true autoUpdater.forceDevUpdateConfig = true
autoUpdater.autoDownload = false autoUpdater.autoDownload = false
autoUpdater.checkForUpdates()
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
ipcMain.on('check-for-update', () => {
logger.info('触发检查更新')
return autoUpdater.checkForUpdates()
})
// 检测下载错误 // 检测下载错误
autoUpdater.on('error', (error) => { autoUpdater.on('error', (error) => {
logger.error('更新异常', error) logger.error('更新异常', error)
mainWindow.webContents.send('update-error', error)
}) })
// 检测是否需要更新 // 检测是否需要更新
@ -28,6 +24,7 @@ export default class AppUpdater {
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => { autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
autoUpdater.logger?.info('检测到新版本,确认是否下载') autoUpdater.logger?.info('检测到新版本,确认是否下载')
mainWindow.webContents.send('update-available', releaseInfo)
const releaseNotes = releaseInfo.releaseNotes const releaseNotes = releaseInfo.releaseNotes
let releaseContent = '' let releaseContent = ''
if (releaseNotes) { if (releaseNotes) {
@ -49,10 +46,12 @@ export default class AppUpdater {
title: '应用有新的更新', title: '应用有新的更新',
detail: releaseContent, detail: releaseContent,
message: '发现新版本,是否现在更新?', message: '发现新版本,是否现在更新?',
buttons: ['否', '是'] buttons: ['下次再说', '更新']
}) })
.then(({ response }) => { .then(({ response }) => {
if (response === 1) { if (response === 1) {
logger.info('用户选择更新,准备下载更新')
mainWindow.webContents.send('download-update')
autoUpdater.downloadUpdate() autoUpdater.downloadUpdate()
} }
}) })
@ -61,11 +60,13 @@ export default class AppUpdater {
// 检测到不需要更新时 // 检测到不需要更新时
autoUpdater.on('update-not-available', () => { autoUpdater.on('update-not-available', () => {
logger.info('现在使用的就是最新版本,不用更新') logger.info('现在使用的就是最新版本,不用更新')
mainWindow.webContents.send('update-not-available')
}) })
// 更新下载进度 // 更新下载进度
autoUpdater.on('download-progress', (progress) => { autoUpdater.on('download-progress', (progress) => {
logger.info('下载进度', progress) logger.info('下载进度', progress)
mainWindow.webContents.send('download-progress', progress)
}) })
// 当需要更新的内容下载完成后 // 当需要更新的内容下载完成后
@ -80,5 +81,7 @@ export default class AppUpdater {
setImmediate(() => autoUpdater.quitAndInstall()) setImmediate(() => autoUpdater.quitAndInstall())
}) })
}) })
this.autoUpdater = autoUpdater
} }
} }

View File

@ -6,7 +6,10 @@ declare global {
api: { api: {
getAppInfo: () => Promise<{ getAppInfo: () => Promise<{
version: string version: string
isPackaged: boolean
}> }>
checkForUpdate: () => void
openWebsite: (url: string) => void
} }
} }
} }

View File

@ -4,7 +4,8 @@ import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer // Custom APIs for renderer
const api = { const api = {
getAppInfo: () => ipcRenderer.invoke('get-app-info'), getAppInfo: () => ipcRenderer.invoke('get-app-info'),
checkForUpdate: () => ipcRenderer.invoke('check-for-update') checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url)
} }
// Use `contextBridge` APIs to expose Electron APIs to // Use `contextBridge` APIs to expose Electron APIs to

View File

@ -1,20 +1,25 @@
# CHANGES LOG # CHANGES LOG
### v0.2.4 - 2024-07-16
- Fixed the issue of the update log page cannot be scrolled
- Added a check for updates button
### v0.2.3 - 2024-07-16 ### v0.2.3 - 2024-07-16
1. Fixed multi-language prompt errors - Fixed multi-language prompt errors
2. Fixed default model error issues with ZHIPU AI - Fixed default model error issues with ZHIPU AI
3. Fixed OpenRouter API detection error issues - Fixed OpenRouter API detection error issues
4. Fixed multi-language translation errors with model providers - Fixed multi-language translation errors with model providers
### v0.2.2 - 2024-07-15 ### v0.2.2 - 2024-07-15
1. Fix the issue where the default assistant name is empty. - Fix the issue where the default assistant name is empty.
2. Fix the problem with default language detection during the first installation. - Fix the problem with default language detection during the first installation.
3. Adjust the changelog style. - Adjust the changelog style.
### v0.2.1 - 2024-07-15 ### v0.2.1 - 2024-07-15
1. **Feature**: Add new feature for pausing message sending - **Feature**: Add new feature for pausing message sending
2. **Fix**: Resolve incomplete translation issue upon language switch - **Fix**: Resolve incomplete translation issue upon language switch
3. **Build**: Support for macOS Intel architecture - **Build**: Support for macOS Intel architecture

View File

@ -1,21 +1,26 @@
# 更新日志 # 更新日志
### v0.2.4 - 2024-07-16
- 修复更新日志页面不能滚动问题
- 新增检查更新按钮
### v0.2.3 - 2024-07-16 ### v0.2.3 - 2024-07-16
1. 修复多语言提示错误 - 修复多语言提示错误
2. 修复智谱AI默认模型错误问题 - 修复智谱AI默认模型错误问题
3. 修复 OpenRouter API 检测出错问题 - 修复 OpenRouter API 检测出错问题
4. 修复模型提供商多语言翻译错误问题 - 修复模型提供商多语言翻译错误问题
### v0.2.2 - 2024-07-15 ### v0.2.2 - 2024-07-15
1. 修复默认助理名称为空的问题 - 修复默认助理名称为空的问题
2. 修复首次安装默认语言检测问题 - 修复首次安装默认语言检测问题
3. 更新日志样式微调 - 更新日志样式微调
### v0.2.1 - 2024-07-15 ### v0.2.1 - 2024-07-15
1. 【功能】新增消息暂停发送功能 - 【功能】新增消息暂停发送功能
2. 【修复】修复多语言切换不彻底问题 - 【修复】修复多语言切换不彻底问题
3. 【构建】支持 macOS Intel 架构 - 【构建】支持 macOS Intel 架构

View File

@ -15,4 +15,11 @@ export function useAppInitEffect() {
}) })
i18nInit() i18nInit()
}, [dispatch]) }, [dispatch])
useEffect(() => {
runAsyncFunction(async () => {
const { isPackaged } = await window.api.getAppInfo()
isPackaged && setTimeout(window.api.checkForUpdate, 3000)
})
}, [])
} }

View File

@ -108,7 +108,12 @@ const resources = {
'models.add.group_name.placeholder': 'Optional e.g. ChatGPT', 'models.add.group_name.placeholder': 'Optional e.g. ChatGPT',
'models.empty': 'No models found', 'models.empty': 'No models found',
'assistant.title': 'Default Assistant', 'assistant.title': 'Default Assistant',
'about.description': 'A powerful AI assistant for producer' 'about.description': 'A powerful AI assistant for producer',
'about.updateNotAvailable': 'You are using the latest version',
'about.checkingUpdate': 'Checking for updates...',
'about.updateError': 'Update error',
'about.checkUpdate': 'Check Update',
'about.downloading': 'Downloading...'
} }
} }
}, },
@ -217,7 +222,12 @@ const resources = {
'models.add.group_name.placeholder': '例如 ChatGPT', 'models.add.group_name.placeholder': '例如 ChatGPT',
'models.empty': '没有模型', 'models.empty': '没有模型',
'assistant.title': '默认助手', 'assistant.title': '默认助手',
'about.description': '一个为创造者而生的 AI 助手' 'about.description': '一个为创造者而生的 AI 助手',
'about.updateNotAvailable': '你的软件已是最新版本',
'about.checkingUpdate': '正在检查更新...',
'about.updateError': '更新出错',
'about.checkUpdate': '检查更新',
'about.downloading': '正在下载更新...'
} }
} }
} }

View File

@ -2,17 +2,11 @@ import localforage from 'localforage'
import KeyvStorage from '@kangfenmao/keyv-storage' import KeyvStorage from '@kangfenmao/keyv-storage'
import * as Sentry from '@sentry/electron/renderer' import * as Sentry from '@sentry/electron/renderer'
function init() { function initSentry() {
localforage.config({ // Disable sentry in development mode
driver: localforage.INDEXEDDB, if (process?.env?.NODE_ENV === 'development') {
name: 'CherryAI', return
version: 1.0, }
storeName: 'cherryai',
description: 'Cherry Studio Storage'
})
window.keyv = new KeyvStorage()
window.keyv.init()
Sentry.init({ Sentry.init({
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()], integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
@ -29,4 +23,19 @@ function init() {
}) })
} }
function init() {
localforage.config({
driver: localforage.INDEXEDDB,
name: 'CherryAI',
version: 1.0,
storeName: 'cherryai',
description: 'Cherry Studio Storage'
})
window.keyv = new KeyvStorage()
window.keyv.init()
initSentry()
}
init() init()

View File

@ -1,14 +1,34 @@
import { Avatar } from 'antd' import { Avatar, Button, Progress } from 'antd'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Logo from '@renderer/assets/images/logo.png' import Logo from '@renderer/assets/images/logo.png'
import { runAsyncFunction } from '@renderer/utils' import { runAsyncFunction } from '@renderer/utils'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Changelog from './components/Changelog' import Changelog from './components/Changelog'
import { debounce } from 'lodash'
import { ProgressInfo } from 'electron-updater'
const AboutSettings: FC = () => { const AboutSettings: FC = () => {
const [version, setVersion] = useState('') const [version, setVersion] = useState('')
const { t } = useTranslation() const { t } = useTranslation()
const [percent, setPercent] = useState(0)
const [checkUpdateLoading, setCheckUpdateLoading] = useState(false)
const [downloading, setDownloading] = useState(false)
const onCheckUpdate = debounce(
async () => {
if (checkUpdateLoading || downloading) return
setCheckUpdateLoading(true)
await window.api.checkForUpdate()
setCheckUpdateLoading(false)
},
2000,
{ leading: true, trailing: false }
)
const onOpenWebsite = (suffix = '') => {
window.api.openWebsite('https://github.com/kangfenmao/cherry-studio' + suffix)
}
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
@ -17,20 +37,65 @@ const AboutSettings: FC = () => {
}) })
}, []) }, [])
useEffect(() => {
const ipcRenderer = window.electron.ipcRenderer
const removers = [
ipcRenderer.on('update-not-available', () => {
setCheckUpdateLoading(false)
window.message.success(t('settings.about.updateNotAvailable'))
}),
ipcRenderer.on('update-available', () => {
setCheckUpdateLoading(false)
}),
ipcRenderer.on('download-update', () => {
setCheckUpdateLoading(false)
setDownloading(true)
}),
ipcRenderer.on('download-progress', (_, progress: ProgressInfo) => {
setPercent(progress.percent)
}),
ipcRenderer.on('update-error', (_, error) => {
setCheckUpdateLoading(false)
setDownloading(false)
setPercent(0)
window.modal.info({
title: t('settings.about.updateError'),
content: error?.message || t('settings.about.updateError'),
icon: null
})
})
]
return () => removers.forEach((remover) => remover())
}, [t])
return ( return (
<Container> <Container>
<AvatarWrapper onClick={() => onOpenWebsite()}>
{percent > 0 && (
<ProgressCircle
type="circle"
size={104}
percent={percent}
showInfo={false}
strokeLinecap="butt"
strokeColor="#67ad5b"
/>
)}
<Avatar src={Logo} size={100} style={{ marginTop: 50, minHeight: 100 }} /> <Avatar src={Logo} size={100} style={{ marginTop: 50, minHeight: 100 }} />
</AvatarWrapper>
<Title> <Title>
Cherry Studio <Version>(v{version})</Version> Cherry Studio <Version onClick={() => onOpenWebsite('/releases')}>(v{version})</Version>
</Title> </Title>
<Description>{t('settings.about.description')}</Description> <Description>{t('settings.about.description')}</Description>
<CheckUpdateButton onClick={onCheckUpdate} loading={checkUpdateLoading}>
{downloading ? t('settings.about.downloading') : t('settings.about.checkUpdate')}
</CheckUpdateButton>
<Changelog /> <Changelog />
</Container> </Container>
) )
} }
const Container = styled.div` const Container = styled.div`
padding: 20px;
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
@ -38,6 +103,8 @@ const Container = styled.div`
justify-content: flex-start; justify-content: flex-start;
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
overflow-y: scroll; overflow-y: scroll;
padding: 0;
padding-bottom: 50px;
` `
const Title = styled.div` const Title = styled.div`
@ -52,6 +119,7 @@ const Version = styled.span`
color: var(--color-text-2); color: var(--color-text-2);
margin: 10px 0; margin: 10px 0;
text-align: center; text-align: center;
cursor: pointer;
` `
const Description = styled.div` const Description = styled.div`
@ -60,4 +128,19 @@ const Description = styled.div`
text-align: center; text-align: center;
` `
const CheckUpdateButton = styled(Button)`
margin-top: 10px;
`
const AvatarWrapper = styled.div`
position: relative;
cursor: pointer;
`
const ProgressCircle = styled(Progress)`
position: absolute;
top: 48px;
left: -2px;
`
export default AboutSettings export default AboutSettings

View File

@ -3,7 +3,7 @@ import changelogZh from '@renderer/CHANGELOG.zh.md?raw'
import { FC } from 'react' import { FC } from 'react'
import Markdown from 'react-markdown' import Markdown from 'react-markdown'
import styled from 'styled-components' import styled from 'styled-components'
import styles from '@renderer/assets/styles/changelog.module.scss' import styles from './changelog.module.scss'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
const Changelog: FC = () => { const Changelog: FC = () => {

View File

@ -69,7 +69,8 @@ $code-color: #f0e7db;
ul, ul,
ol { ol {
padding-left: 30px; padding-left: 20px;
list-style: disc;
} }
li { li {