wip
This commit is contained in:
parent
461458e5ec
commit
b88f4a869e
@ -1,24 +0,0 @@
|
|||||||
import { dialog, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
|
||||||
import logger from 'electron-log'
|
|
||||||
import { writeFile } from 'fs'
|
|
||||||
|
|
||||||
export async function saveFile(_: Electron.IpcMainInvokeEvent, fileName: string, content: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
const options: SaveDialogOptions = {
|
|
||||||
title: '保存文件',
|
|
||||||
defaultPath: fileName
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: SaveDialogReturnValue = await dialog.showSaveDialog(options)
|
|
||||||
|
|
||||||
if (!result.canceled && result.filePath) {
|
|
||||||
writeFile(result.filePath, content, { encoding: 'utf-8' }, (err) => {
|
|
||||||
if (err) {
|
|
||||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +1,10 @@
|
|||||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||||
import * as Sentry from '@sentry/electron/main'
|
import * as Sentry from '@sentry/electron/main'
|
||||||
import { app, BrowserWindow, ipcMain, session, shell } from 'electron'
|
import { app, BrowserWindow } from 'electron'
|
||||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||||
|
|
||||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
import { registerIpc } from './ipc'
|
||||||
import { saveFile } from './event'
|
import { createMainWindow } from './window'
|
||||||
import AppUpdater from './updater'
|
|
||||||
import { createMainWindow, createMinappWindow } from './window'
|
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
@ -30,49 +28,7 @@ app.whenReady().then(() => {
|
|||||||
|
|
||||||
const mainWindow = createMainWindow()
|
const mainWindow = createMainWindow()
|
||||||
|
|
||||||
const { autoUpdater } = new AppUpdater(mainWindow)
|
registerIpc(mainWindow, app)
|
||||||
|
|
||||||
// IPC
|
|
||||||
ipcMain.handle('get-app-info', () => ({
|
|
||||||
version: app.getVersion(),
|
|
||||||
isPackaged: app.isPackaged,
|
|
||||||
appPath: app.getAppPath()
|
|
||||||
}))
|
|
||||||
|
|
||||||
ipcMain.handle('open-website', (_, url: string) => {
|
|
||||||
shell.openExternal(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('set-proxy', (_, proxy: string) => {
|
|
||||||
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('save-file', saveFile)
|
|
||||||
|
|
||||||
ipcMain.handle('minapp', (_, args) => {
|
|
||||||
createMinappWindow({
|
|
||||||
url: args.url,
|
|
||||||
windowOptions: {
|
|
||||||
...mainWindow.getBounds(),
|
|
||||||
...args.windowOptions
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
|
|
||||||
appConfig.set('theme', theme)
|
|
||||||
mainWindow?.setTitleBarOverlay &&
|
|
||||||
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
|
||||||
ipcMain.handle('check-for-update', async () => {
|
|
||||||
autoUpdater.logger?.info('触发检查更新')
|
|
||||||
return {
|
|
||||||
currentVersion: autoUpdater.currentVersion,
|
|
||||||
update: await autoUpdater.checkForUpdates()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
installExtension(REDUX_DEVTOOLS)
|
installExtension(REDUX_DEVTOOLS)
|
||||||
})
|
})
|
||||||
|
|||||||
59
src/main/ipc.ts
Normal file
59
src/main/ipc.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||||
|
|
||||||
|
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||||
|
import AppUpdater from './updater'
|
||||||
|
import { openFile, saveFile } from './utils/file'
|
||||||
|
import { compress, decompress } from './utils/zip'
|
||||||
|
import { createMinappWindow } from './window'
|
||||||
|
|
||||||
|
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||||
|
const { autoUpdater } = new AppUpdater(mainWindow)
|
||||||
|
|
||||||
|
// IPC
|
||||||
|
ipcMain.handle('get-app-info', () => ({
|
||||||
|
version: app.getVersion(),
|
||||||
|
isPackaged: app.isPackaged,
|
||||||
|
appPath: app.getAppPath()
|
||||||
|
}))
|
||||||
|
|
||||||
|
ipcMain.handle('open-website', (_, url: string) => {
|
||||||
|
shell.openExternal(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('set-proxy', (_, proxy: string) => {
|
||||||
|
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('save-file', saveFile)
|
||||||
|
ipcMain.handle('open-file', openFile)
|
||||||
|
ipcMain.handle('reload', () => mainWindow.reload())
|
||||||
|
|
||||||
|
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
|
||||||
|
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
|
||||||
|
|
||||||
|
ipcMain.handle('minapp', (_, args) => {
|
||||||
|
createMinappWindow({
|
||||||
|
url: args.url,
|
||||||
|
parent: mainWindow,
|
||||||
|
windowOptions: {
|
||||||
|
...mainWindow.getBounds(),
|
||||||
|
...args.windowOptions
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
|
||||||
|
appConfig.set('theme', theme)
|
||||||
|
mainWindow?.setTitleBarOverlay &&
|
||||||
|
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
||||||
|
ipcMain.handle('check-for-update', async () => {
|
||||||
|
autoUpdater.logger?.info('触发检查更新')
|
||||||
|
return {
|
||||||
|
currentVersion: autoUpdater.currentVersion,
|
||||||
|
update: await autoUpdater.checkForUpdates()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
24
src/main/utils/aes.ts
Normal file
24
src/main/utils/aes.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import * as crypto from 'crypto'
|
||||||
|
|
||||||
|
// 定义密钥和初始化向量(IV)
|
||||||
|
const secretKey = 'kDQvWz5slot3syfucoo53X6KKsEUJoeFikpiUWRJTLIo3zcUPpFvEa009kK13KCr'
|
||||||
|
const iv = Buffer.from('Cherry Studio', 'hex')
|
||||||
|
|
||||||
|
// 加密函数
|
||||||
|
export function encrypt(text: string): { iv: string; encryptedData: string } {
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(secretKey), iv)
|
||||||
|
let encrypted = cipher.update(text, 'utf8', 'hex')
|
||||||
|
encrypted += cipher.final('hex')
|
||||||
|
return {
|
||||||
|
iv: iv.toString('hex'),
|
||||||
|
encryptedData: encrypted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密函数
|
||||||
|
export function decrypt(encryptedData: string, iv: string): string {
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(secretKey), Buffer.from(iv, 'hex'))
|
||||||
|
let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
|
||||||
|
decrypted += decipher.final('utf8')
|
||||||
|
return decrypted
|
||||||
|
}
|
||||||
55
src/main/utils/file.ts
Normal file
55
src/main/utils/file.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { dialog, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
||||||
|
import logger from 'electron-log'
|
||||||
|
import { writeFile } from 'fs'
|
||||||
|
import { readFile } from 'fs/promises'
|
||||||
|
|
||||||
|
export async function saveFile(
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
fileName: string,
|
||||||
|
content: string,
|
||||||
|
options?: SaveDialogOptions
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const result: SaveDialogReturnValue = await dialog.showSaveDialog({
|
||||||
|
title: '保存文件',
|
||||||
|
defaultPath: fileName,
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePath) {
|
||||||
|
writeFile(result.filePath, content, { encoding: 'utf-8' }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openFile(
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
options: OpenDialogOptions
|
||||||
|
): Promise<{ fileName: string; content: Buffer } | null> {
|
||||||
|
try {
|
||||||
|
const result: OpenDialogReturnValue = await dialog.showOpenDialog({
|
||||||
|
title: '打开文件',
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [{ name: '所有文件', extensions: ['*'] }],
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
const filePath = result.filePaths[0]
|
||||||
|
const fileName = filePath.split('/').pop() || ''
|
||||||
|
const content = await readFile(filePath)
|
||||||
|
return { fileName, content }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[IPC - Error]', 'An error occurred opening the file:', err)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/main/utils/zip.ts
Normal file
37
src/main/utils/zip.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import util from 'node:util'
|
||||||
|
import zlib from 'node:zlib'
|
||||||
|
|
||||||
|
// 将 zlib 的 gzip 和 gunzip 方法转换为 Promise 版本
|
||||||
|
const gzipPromise = util.promisify(zlib.gzip)
|
||||||
|
const gunzipPromise = util.promisify(zlib.gunzip)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩字符串
|
||||||
|
* @param {string} string - 要压缩的 JSON 字符串
|
||||||
|
* @returns {Promise<Buffer>} 压缩后的 Buffer
|
||||||
|
*/
|
||||||
|
export async function compress(str) {
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.from(str, 'utf-8')
|
||||||
|
const compressedBuffer = await gzipPromise(buffer)
|
||||||
|
return compressedBuffer
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Compression failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压缩 Buffer 到 JSON 字符串
|
||||||
|
* @param {Buffer} compressedBuffer - 压缩的 Buffer
|
||||||
|
* @returns {Promise<string>} 解压缩后的 JSON 字符串
|
||||||
|
*/
|
||||||
|
export async function decompress(compressedBuffer) {
|
||||||
|
try {
|
||||||
|
const buffer = await gunzipPromise(compressedBuffer)
|
||||||
|
return buffer.toString('utf-8')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Decompression failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -94,9 +94,11 @@ export function createMainWindow() {
|
|||||||
|
|
||||||
export function createMinappWindow({
|
export function createMinappWindow({
|
||||||
url,
|
url,
|
||||||
|
parent,
|
||||||
windowOptions
|
windowOptions
|
||||||
}: {
|
}: {
|
||||||
url: string
|
url: string
|
||||||
|
parent?: BrowserWindow
|
||||||
windowOptions?: Electron.BrowserWindowConstructorOptions
|
windowOptions?: Electron.BrowserWindowConstructorOptions
|
||||||
}) {
|
}) {
|
||||||
const width = windowOptions?.width || 1000
|
const width = windowOptions?.width || 1000
|
||||||
@ -108,6 +110,7 @@ export function createMinappWindow({
|
|||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
title: 'Cherry Studio',
|
title: 'Cherry Studio',
|
||||||
...windowOptions,
|
...windowOptions,
|
||||||
|
parent,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/minapp.js'),
|
preload: join(__dirname, '../preload/minapp.js'),
|
||||||
sandbox: false,
|
sandbox: false,
|
||||||
|
|||||||
7
src/preload/index.d.ts
vendored
7
src/preload/index.d.ts
vendored
@ -1,4 +1,5 @@
|
|||||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||||
|
import type { OpenDialogOptions } from 'electron'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -12,9 +13,13 @@ declare global {
|
|||||||
checkForUpdate: () => void
|
checkForUpdate: () => void
|
||||||
openWebsite: (url: string) => void
|
openWebsite: (url: string) => void
|
||||||
setProxy: (proxy: string | undefined) => void
|
setProxy: (proxy: string | undefined) => void
|
||||||
saveFile: (path: string, content: string) => void
|
saveFile: (path: string, content: string | NodeJS.ArrayBufferView, options?: SaveDialogOptions) => void
|
||||||
|
openFile: (options?: OpenDialogOptions) => Promise<{ fileName: string; content: Buffer } | null>
|
||||||
setTheme: (theme: 'light' | 'dark') => void
|
setTheme: (theme: 'light' | 'dark') => void
|
||||||
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
||||||
|
reload: () => void
|
||||||
|
compress: (text: string) => Promise<Buffer>
|
||||||
|
decompress: (text: Buffer) => Promise<string>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,15 @@ const api = {
|
|||||||
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
||||||
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
|
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
|
||||||
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
|
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
|
||||||
saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content),
|
|
||||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme),
|
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme),
|
||||||
minApp: (url: string) => ipcRenderer.invoke('minapp', url)
|
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
||||||
|
openFile: (options?: { decompress: boolean }) => ipcRenderer.invoke('open-file', options),
|
||||||
|
reload: () => ipcRenderer.invoke('reload'),
|
||||||
|
saveFile: (path: string, content: string, options?: { compress: boolean }) => {
|
||||||
|
ipcRenderer.invoke('save-file', path, content, options)
|
||||||
|
},
|
||||||
|
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
||||||
|
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
|||||||
@ -28,7 +28,8 @@ const resources = {
|
|||||||
footnotes: 'References',
|
footnotes: 'References',
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
search: 'Search',
|
search: 'Search',
|
||||||
default: 'Default'
|
default: 'Default',
|
||||||
|
warning: 'Warning'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
add: 'Add',
|
add: 'Add',
|
||||||
@ -48,7 +49,11 @@ const resources = {
|
|||||||
'api.connection.failed': 'Connection failed',
|
'api.connection.failed': 'Connection failed',
|
||||||
'api.connection.success': 'Connection successful',
|
'api.connection.success': 'Connection successful',
|
||||||
'chat.completion.paused': 'Chat completion paused',
|
'chat.completion.paused': 'Chat completion paused',
|
||||||
'switch.disabled': 'Switching is disabled while the assistant is generating'
|
'switch.disabled': 'Switching is disabled while the assistant is generating',
|
||||||
|
'restore.success': 'Restored successfully',
|
||||||
|
'reset.confirm.content': 'Are you sure you want to clear all data?',
|
||||||
|
'reset.double.confirm.title': 'DATA LOST !!!',
|
||||||
|
'reset.double.confirm.content': 'All data will be lost, do you want to continue?'
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
@ -141,6 +146,9 @@ const resources = {
|
|||||||
'general.title': 'General Settings',
|
'general.title': 'General Settings',
|
||||||
'general.user_name': 'User Name',
|
'general.user_name': 'User Name',
|
||||||
'general.user_name.placeholder': 'Enter your name',
|
'general.user_name.placeholder': 'Enter your name',
|
||||||
|
'general.backup.title': 'Data Backup and Recovery',
|
||||||
|
'general.reset.title': 'Data Reset',
|
||||||
|
'general.reset.button': 'Reset',
|
||||||
'provider.api_key': 'API Key',
|
'provider.api_key': 'API Key',
|
||||||
'provider.check': 'Check',
|
'provider.check': 'Check',
|
||||||
'provider.get_api_key': 'Get API Key',
|
'provider.get_api_key': 'Get API Key',
|
||||||
@ -220,7 +228,8 @@ const resources = {
|
|||||||
'keep_alive_time.description': 'The time in minutes to keep the connection alive, default is 5 minutes.'
|
'keep_alive_time.description': 'The time in minutes to keep the connection alive, default is 5 minutes.'
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
'chat.response': 'Something went wrong. Please check if you have set your API key in the Settings > Providers'
|
'chat.response': 'Something went wrong. Please check if you have set your API key in the Settings > Providers',
|
||||||
|
'backup.file_format': 'Backup file format error'
|
||||||
},
|
},
|
||||||
words: {
|
words: {
|
||||||
knowledgeGraph: 'Knowledge Graph',
|
knowledgeGraph: 'Knowledge Graph',
|
||||||
@ -253,7 +262,8 @@ const resources = {
|
|||||||
footnote: '引用内容',
|
footnote: '引用内容',
|
||||||
select: '选择',
|
select: '选择',
|
||||||
search: '搜索',
|
search: '搜索',
|
||||||
default: '默认'
|
default: '默认',
|
||||||
|
warning: '警告'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
add: '添加',
|
add: '添加',
|
||||||
@ -273,7 +283,11 @@ const resources = {
|
|||||||
'api.connection.failed': '连接失败',
|
'api.connection.failed': '连接失败',
|
||||||
'api.connection.success': '连接成功',
|
'api.connection.success': '连接成功',
|
||||||
'chat.completion.paused': '会话已停止',
|
'chat.completion.paused': '会话已停止',
|
||||||
'switch.disabled': '模型回复完成后才能切换'
|
'switch.disabled': '模型回复完成后才能切换',
|
||||||
|
'restore.success': '恢复成功',
|
||||||
|
'reset.confirm.content': '确定要重置所有数据吗?',
|
||||||
|
'reset.double.confirm.title': '数据丢失!!!',
|
||||||
|
'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?'
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
save: '保存',
|
save: '保存',
|
||||||
@ -367,6 +381,9 @@ const resources = {
|
|||||||
'general.title': '常规设置',
|
'general.title': '常规设置',
|
||||||
'general.user_name': '用户名',
|
'general.user_name': '用户名',
|
||||||
'general.user_name.placeholder': '请输入用户名',
|
'general.user_name.placeholder': '请输入用户名',
|
||||||
|
'general.backup.title': '数据备份与恢复',
|
||||||
|
'general.reset.title': '重置数据',
|
||||||
|
'general.reset.button': '重置',
|
||||||
'provider.api_key': 'API 密钥',
|
'provider.api_key': 'API 密钥',
|
||||||
'provider.check': '检查',
|
'provider.check': '检查',
|
||||||
'provider.get_api_key': '点击这里获取密钥',
|
'provider.get_api_key': '点击这里获取密钥',
|
||||||
@ -446,7 +463,8 @@ const resources = {
|
|||||||
'keep_alive_time.description': '对话后模型在内存中保持的时间(默认:5分钟)'
|
'keep_alive_time.description': '对话后模型在内存中保持的时间(默认:5分钟)'
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
'chat.response': '出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥'
|
'chat.response': '出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥',
|
||||||
|
'backup.file_format': '备份文件格式错误'
|
||||||
},
|
},
|
||||||
words: {
|
words: {
|
||||||
knowledgeGraph: '知识图谱',
|
knowledgeGraph: '知识图谱',
|
||||||
|
|||||||
@ -51,6 +51,7 @@ const AppTitle = styled.div`
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
color: var(--color-text-soft);
|
color: var(--color-text-soft);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
import useAvatar from '@renderer/hooks/useAvatar'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
|
import { backup, reset, restore } from '@renderer/services/backup'
|
||||||
import LocalStorage from '@renderer/services/storage'
|
import LocalStorage from '@renderer/services/storage'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setAvatar } from '@renderer/store/runtime'
|
import { setAvatar } from '@renderer/store/runtime'
|
||||||
import { setFontSize, setLanguage, setUserName, ThemeMode } from '@renderer/store/settings'
|
import { setLanguage, setUserName, ThemeMode } from '@renderer/store/settings'
|
||||||
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
||||||
import { compressImage, isValidProxyUrl } from '@renderer/utils'
|
import { compressImage, isValidProxyUrl } from '@renderer/utils'
|
||||||
import { Avatar, Input, Select, Slider, Upload } from 'antd'
|
import { Avatar, Button, Input, Select, Upload } from 'antd'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -16,8 +18,7 @@ import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingT
|
|||||||
|
|
||||||
const GeneralSettings: FC = () => {
|
const GeneralSettings: FC = () => {
|
||||||
const avatar = useAvatar()
|
const avatar = useAvatar()
|
||||||
const { language, proxyUrl: storeProxyUrl, userName, theme, setTheme, fontSize } = useSettings()
|
const { language, proxyUrl: storeProxyUrl, userName, theme, setTheme } = useSettings()
|
||||||
const [fontSizeValue, setFontSizeValue] = useState(fontSize)
|
|
||||||
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -101,27 +102,6 @@ const GeneralSettings: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.font_size.title')}</SettingRowTitle>
|
|
||||||
<Slider
|
|
||||||
style={{ width: 290 }}
|
|
||||||
value={fontSizeValue}
|
|
||||||
onChange={(value) => setFontSizeValue(value)}
|
|
||||||
onChangeComplete={(value) => {
|
|
||||||
dispatch(setFontSize(value))
|
|
||||||
console.debug('set font size', value)
|
|
||||||
}}
|
|
||||||
min={12}
|
|
||||||
max={18}
|
|
||||||
step={1}
|
|
||||||
marks={{
|
|
||||||
12: <span style={{ fontSize: '12px' }}>A</span>,
|
|
||||||
14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
|
|
||||||
18: <span style={{ fontSize: '18px' }}>A</span>
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
|
||||||
<Input
|
<Input
|
||||||
@ -134,6 +114,23 @@ const GeneralSettings: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||||
|
<HStack gap="5px">
|
||||||
|
<Button onClick={backup}>备份</Button>
|
||||||
|
<Button onClick={restore}>恢复</Button>
|
||||||
|
</HStack>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
|
||||||
|
<HStack gap="5px">
|
||||||
|
<Button onClick={reset} danger>
|
||||||
|
{t('settings.general.reset.button')}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
74
src/renderer/src/services/backup.ts
Normal file
74
src/renderer/src/services/backup.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import i18n from '@renderer/i18n'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import localforage from 'localforage'
|
||||||
|
|
||||||
|
export async function backup() {
|
||||||
|
const indexedKeys = await localforage.keys()
|
||||||
|
const version = 1
|
||||||
|
const time = new Date().getTime()
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
time,
|
||||||
|
version,
|
||||||
|
localStorage,
|
||||||
|
indexedDB: [] as { key: string; value: any }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of indexedKeys) {
|
||||||
|
data.indexedDB.push({
|
||||||
|
key,
|
||||||
|
value: await localforage.getItem(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = `cherry-studio.${dayjs().format('YYYYMMDD')}.bak`
|
||||||
|
const fileContnet = JSON.stringify(data)
|
||||||
|
const file = await window.api.compress(fileContnet)
|
||||||
|
|
||||||
|
window.api.saveFile(filename, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restore() {
|
||||||
|
const file = await window.api.openFile()
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
try {
|
||||||
|
const content = await window.api.decompress(file.content)
|
||||||
|
const data = JSON.parse(content)
|
||||||
|
|
||||||
|
if (data.version === 1) {
|
||||||
|
localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio'])
|
||||||
|
|
||||||
|
for (const { key, value } of data.indexedDB) {
|
||||||
|
await localforage.setItem(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' })
|
||||||
|
setTimeout(() => window.api.reload(), 1500)
|
||||||
|
} else {
|
||||||
|
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reset() {
|
||||||
|
window.modal.confirm({
|
||||||
|
title: i18n.t('common.warning'),
|
||||||
|
content: i18n.t('message.reset.confirm.content'),
|
||||||
|
onOk: async () => {
|
||||||
|
window.modal.confirm({
|
||||||
|
title: i18n.t('message.reset.double.confirm.title'),
|
||||||
|
content: i18n.t('message.reset.double.confirm.content'),
|
||||||
|
onOk: async () => {
|
||||||
|
await localStorage.clear()
|
||||||
|
await localforage.clear()
|
||||||
|
window.api.reload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user