mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 14:59:27 +08:00
Merge branch 'main' into node-store
This commit is contained in:
commit
a97c3d9695
@ -52,6 +52,7 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
|
|||||||
- 🔤 AI-powered Translation
|
- 🔤 AI-powered Translation
|
||||||
- 🎯 Drag-and-drop Sorting
|
- 🎯 Drag-and-drop Sorting
|
||||||
- 🔌 Mini Program Support
|
- 🔌 Mini Program Support
|
||||||
|
- ⚙️ MCP(Model Context Protocol) Server
|
||||||
|
|
||||||
5. **Enhanced User Experience**:
|
5. **Enhanced User Experience**:
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,7 @@ Cherry Studio は、複数の LLM プロバイダーをサポートするデス
|
|||||||
- 🔤 AI による翻訳機能
|
- 🔤 AI による翻訳機能
|
||||||
- 🎯 ドラッグ&ドロップによる整理
|
- 🎯 ドラッグ&ドロップによる整理
|
||||||
- 🔌 ミニプログラム対応
|
- 🔌 ミニプログラム対応
|
||||||
|
- ⚙️ MCP(モデルコンテキストプロトコル) サービス
|
||||||
|
|
||||||
5. **優れたユーザー体験**:
|
5. **優れたユーザー体験**:
|
||||||
|
|
||||||
|
|||||||
@ -52,6 +52,7 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客
|
|||||||
- 🔤 AI 驱动的翻译功能
|
- 🔤 AI 驱动的翻译功能
|
||||||
- 🎯 拖拽排序
|
- 🎯 拖拽排序
|
||||||
- 🔌 小程序支持
|
- 🔌 小程序支持
|
||||||
|
- ⚙️ MCP(模型上下文协议) 服务
|
||||||
|
|
||||||
5. **优质使用体验**:
|
5. **优质使用体验**:
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,6 @@ export default defineConfig([
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
ignores: ['node_modules/**', 'dist/**', 'out/**', '.gitignore', 'scripts/cloudflare-worker.js']
|
ignores: ['node_modules/**', 'dist/**', 'out/**', 'local/**', '.gitignore', 'scripts/cloudflare-worker.js']
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|||||||
@ -85,6 +85,7 @@
|
|||||||
"npx-scope-finder": "^1.2.0",
|
"npx-scope-finder": "^1.2.0",
|
||||||
"officeparser": "^4.1.1",
|
"officeparser": "^4.1.1",
|
||||||
"p-queue": "^8.1.0",
|
"p-queue": "^8.1.0",
|
||||||
|
"socks-proxy-agent": "^8.0.3",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
"tokenx": "^0.4.1",
|
"tokenx": "^0.4.1",
|
||||||
"undici": "^7.4.0",
|
"undici": "^7.4.0",
|
||||||
|
|||||||
@ -103,7 +103,10 @@ export const textExts = [
|
|||||||
'.cxx', // C++ 源文件
|
'.cxx', // C++ 源文件
|
||||||
'.cppm', // C++20 模块接口文件
|
'.cppm', // C++20 模块接口文件
|
||||||
'.ipp', // 模板实现文件
|
'.ipp', // 模板实现文件
|
||||||
'.ixx' // C++20 模块实现文件
|
'.ixx', // C++20 模块实现文件
|
||||||
|
'.f90', // Fortran 90 源文件
|
||||||
|
'.f', // Fortran 固定格式源代码文件
|
||||||
|
'.f03' // Fortran 2003+ 源代码文件
|
||||||
]
|
]
|
||||||
|
|
||||||
export const ZOOM_SHORTCUTS = [
|
export const ZOOM_SHORTCUTS = [
|
||||||
|
|||||||
52
resources/scripts/download.js
Normal file
52
resources/scripts/download.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const { ProxyAgent } = require('undici')
|
||||||
|
const { SocksProxyAgent } = require('socks-proxy-agent')
|
||||||
|
const https = require('https')
|
||||||
|
const fs = require('fs')
|
||||||
|
const { pipeline } = require('stream/promises')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a file from a URL with redirect handling
|
||||||
|
* @param {string} url The URL to download from
|
||||||
|
* @param {string} destinationPath The path to save the file to
|
||||||
|
* @returns {Promise<void>} Promise that resolves when download is complete
|
||||||
|
*/
|
||||||
|
async function downloadWithRedirects(url, destinationPath) {
|
||||||
|
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY
|
||||||
|
if (proxyUrl.startsWith('socks')) {
|
||||||
|
const proxyAgent = new SocksProxyAgent(proxyUrl)
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = (url) => {
|
||||||
|
https
|
||||||
|
.get(url, { agent: proxyAgent }, (response) => {
|
||||||
|
if (response.statusCode == 301 || response.statusCode == 302) {
|
||||||
|
request(response.headers.location)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const file = fs.createWriteStream(destinationPath)
|
||||||
|
response.pipe(file)
|
||||||
|
file.on('finish', () => resolve())
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
request(url)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const proxyAgent = new ProxyAgent(proxyUrl)
|
||||||
|
const response = await fetch(url, {
|
||||||
|
dispatcher: proxyAgent
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
const file = fs.createWriteStream(destinationPath)
|
||||||
|
await pipeline(response.body, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { downloadWithRedirects }
|
||||||
@ -2,8 +2,8 @@ const fs = require('fs')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const os = require('os')
|
const os = require('os')
|
||||||
const { execSync } = require('child_process')
|
const { execSync } = require('child_process')
|
||||||
const https = require('https')
|
|
||||||
const AdmZip = require('adm-zip')
|
const AdmZip = require('adm-zip')
|
||||||
|
const { downloadWithRedirects } = require('./download')
|
||||||
|
|
||||||
// Base URL for downloading bun binaries
|
// Base URL for downloading bun binaries
|
||||||
const BUN_RELEASE_BASE_URL = 'https://github.com/oven-sh/bun/releases/download'
|
const BUN_RELEASE_BASE_URL = 'https://github.com/oven-sh/bun/releases/download'
|
||||||
@ -24,41 +24,6 @@ const BUN_PACKAGES = {
|
|||||||
'linux-musl-arm64': 'bun-linux-aarch64-musl.zip'
|
'linux-musl-arm64': 'bun-linux-aarch64-musl.zip'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads a file from a URL with redirect handling
|
|
||||||
* @param {string} url The URL to download from
|
|
||||||
* @param {string} destinationPath The path to save the file to
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function downloadWithRedirects(url, destinationPath) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const file = fs.createWriteStream(destinationPath)
|
|
||||||
const request = (url) => {
|
|
||||||
https
|
|
||||||
.get(url, (response) => {
|
|
||||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
||||||
// Handle redirect
|
|
||||||
request(response.headers.location)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode !== 200) {
|
|
||||||
reject(new Error(`Failed to download: ${response.statusCode}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.pipe(file)
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close(resolve)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.on('error', reject)
|
|
||||||
}
|
|
||||||
|
|
||||||
request(url)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads and extracts the bun binary for the specified platform and architecture
|
* Downloads and extracts the bun binary for the specified platform and architecture
|
||||||
* @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux')
|
* @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux')
|
||||||
|
|||||||
@ -2,9 +2,9 @@ const fs = require('fs')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const os = require('os')
|
const os = require('os')
|
||||||
const { execSync } = require('child_process')
|
const { execSync } = require('child_process')
|
||||||
const https = require('https')
|
|
||||||
const tar = require('tar')
|
const tar = require('tar')
|
||||||
const AdmZip = require('adm-zip')
|
const AdmZip = require('adm-zip')
|
||||||
|
const { downloadWithRedirects } = require('./download')
|
||||||
|
|
||||||
// Base URL for downloading uv binaries
|
// Base URL for downloading uv binaries
|
||||||
const UV_RELEASE_BASE_URL = 'https://github.com/astral-sh/uv/releases/download'
|
const UV_RELEASE_BASE_URL = 'https://github.com/astral-sh/uv/releases/download'
|
||||||
@ -32,41 +32,6 @@ const UV_PACKAGES = {
|
|||||||
'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.tar.gz'
|
'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.tar.gz'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads a file from a URL with redirect handling
|
|
||||||
* @param {string} url The URL to download from
|
|
||||||
* @param {string} destinationPath The path to save the file to
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function downloadWithRedirects(url, destinationPath) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const file = fs.createWriteStream(destinationPath)
|
|
||||||
const request = (url) => {
|
|
||||||
https
|
|
||||||
.get(url, (response) => {
|
|
||||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
||||||
// Handle redirect
|
|
||||||
request(response.headers.location)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode !== 200) {
|
|
||||||
reject(new Error(`Failed to download: ${response.statusCode}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.pipe(file)
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close(resolve)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.on('error', reject)
|
|
||||||
}
|
|
||||||
|
|
||||||
request(url)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads and extracts the uv binary for the specified platform and architecture
|
* Downloads and extracts the uv binary for the specified platform and architecture
|
||||||
* @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux')
|
* @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux')
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||||
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
||||||
import { app } from 'electron'
|
import { app, ipcMain } from 'electron'
|
||||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||||
|
|
||||||
import { registerIpc } from './ipc'
|
import { registerIpc } from './ipc'
|
||||||
@ -44,6 +44,9 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
.then((name) => console.log(`Added Extension: ${name}`))
|
.then((name) => console.log(`Added Extension: ${name}`))
|
||||||
.catch((err) => console.log('An error occurred: ', err))
|
.catch((err) => console.log('An error occurred: ', err))
|
||||||
}
|
}
|
||||||
|
ipcMain.handle('system:getDeviceType', () => {
|
||||||
|
return process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'windows' : 'linux'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Listen for second instance
|
// Listen for second instance
|
||||||
|
|||||||
@ -85,8 +85,21 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// theme
|
// theme
|
||||||
ipcMain.handle('app:set-theme', (_, theme: ThemeMode) => {
|
ipcMain.handle('app:set-theme', (event, theme: ThemeMode) => {
|
||||||
|
if (theme === configManager.getTheme()) return
|
||||||
|
|
||||||
configManager.setTheme(theme)
|
configManager.setTheme(theme)
|
||||||
|
|
||||||
|
// should sync theme change to all windows
|
||||||
|
const senderWindowId = event.sender.id
|
||||||
|
const windows = BrowserWindow.getAllWindows()
|
||||||
|
// 向其他窗口广播主题变化
|
||||||
|
windows.forEach((win) => {
|
||||||
|
if (win.webContents.id !== senderWindowId) {
|
||||||
|
win.webContents.send('theme:change', theme)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
mainWindow?.setTitleBarOverlay &&
|
mainWindow?.setTitleBarOverlay &&
|
||||||
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||||
})
|
})
|
||||||
@ -131,6 +144,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle('backup:restore', backupManager.restore)
|
ipcMain.handle('backup:restore', backupManager.restore)
|
||||||
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
|
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
|
||||||
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
|
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
|
||||||
|
ipcMain.handle('backup:listWebdavFiles', backupManager.listWebdavFiles)
|
||||||
|
|
||||||
// file
|
// file
|
||||||
ipcMain.handle('file:open', fileManager.open)
|
ipcMain.handle('file:open', fileManager.open)
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export default abstract class BaseReranker {
|
|||||||
|
|
||||||
public defaultHeaders() {
|
public defaultHeaders() {
|
||||||
return {
|
return {
|
||||||
Authorization: `Bearer ${this.base.apiKey}`,
|
Authorization: `Bearer ${this.base.rerankApiKey}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/main/reranker/JinaReranker.ts
Normal file
48
src/main/reranker/JinaReranker.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||||
|
import { KnowledgeBaseParams } from '@types'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import BaseReranker from './BaseReranker'
|
||||||
|
|
||||||
|
export default class JinaReranker extends BaseReranker {
|
||||||
|
constructor(base: KnowledgeBaseParams) {
|
||||||
|
super(base)
|
||||||
|
}
|
||||||
|
|
||||||
|
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
|
||||||
|
const baseURL = this.base?.rerankBaseURL?.endsWith('/')
|
||||||
|
? this.base.rerankBaseURL.slice(0, -1)
|
||||||
|
: this.base.rerankBaseURL
|
||||||
|
const url = `${baseURL}/rerank`
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
model: this.base.rerankModel,
|
||||||
|
query,
|
||||||
|
documents: searchResults.map((doc) => doc.pageContent),
|
||||||
|
top_n: this.base.topN
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() })
|
||||||
|
|
||||||
|
const rerankResults = data.results
|
||||||
|
console.log(rerankResults)
|
||||||
|
const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0]))
|
||||||
|
return searchResults
|
||||||
|
.map((doc: ExtractChunkData, index: number) => {
|
||||||
|
const score = resultMap.get(index)
|
||||||
|
if (score === undefined) return undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
score
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Jina Reranker API 错误:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,12 +2,15 @@ import { KnowledgeBaseParams } from '@types'
|
|||||||
|
|
||||||
import BaseReranker from './BaseReranker'
|
import BaseReranker from './BaseReranker'
|
||||||
import DefaultReranker from './DefaultReranker'
|
import DefaultReranker from './DefaultReranker'
|
||||||
|
import JinaReranker from './JinaReranker'
|
||||||
import SiliconFlowReranker from './SiliconFlowReranker'
|
import SiliconFlowReranker from './SiliconFlowReranker'
|
||||||
|
|
||||||
export default class RerankerFactory {
|
export default class RerankerFactory {
|
||||||
static create(base: KnowledgeBaseParams): BaseReranker {
|
static create(base: KnowledgeBaseParams): BaseReranker {
|
||||||
if (base.rerankModelProvider === 'silicon') {
|
if (base.rerankModelProvider === 'silicon') {
|
||||||
return new SiliconFlowReranker(base)
|
return new SiliconFlowReranker(base)
|
||||||
|
} else if (base.rerankModelProvider === 'jina') {
|
||||||
|
return new JinaReranker(base)
|
||||||
}
|
}
|
||||||
return new DefaultReranker(base)
|
return new DefaultReranker(base)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,37 +10,41 @@ export default class SiliconFlowReranker extends BaseReranker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
|
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
|
||||||
const url = `${this.base.baseURL}/rerank`
|
const baseURL = this.base?.rerankBaseURL?.endsWith('/')
|
||||||
|
? this.base.rerankBaseURL.slice(0, -1)
|
||||||
|
: this.base.rerankBaseURL
|
||||||
|
const url = `${baseURL}/rerank`
|
||||||
|
|
||||||
const { data } = await axios.post(
|
const requestBody = {
|
||||||
url,
|
model: this.base.rerankModel,
|
||||||
{
|
query,
|
||||||
model: this.base.rerankModel,
|
documents: searchResults.map((doc) => doc.pageContent),
|
||||||
query,
|
top_n: this.base.topN,
|
||||||
documents: searchResults.map((doc) => doc.pageContent),
|
max_chunks_per_doc: this.base.chunkSize,
|
||||||
top_n: this.base.topN,
|
overlap_tokens: this.base.chunkOverlap
|
||||||
max_chunks_per_doc: this.base.chunkSize,
|
}
|
||||||
overlap_tokens: this.base.chunkOverlap
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: this.defaultHeaders()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const rerankResults = data.results
|
try {
|
||||||
const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0]))
|
const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() })
|
||||||
|
|
||||||
return searchResults
|
const rerankResults = data.results
|
||||||
.map((doc: ExtractChunkData, index: number) => {
|
const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0]))
|
||||||
const score = resultMap.get(index)
|
|
||||||
if (score === undefined) return undefined
|
|
||||||
|
|
||||||
return {
|
return searchResults
|
||||||
...doc,
|
.map((doc: ExtractChunkData, index: number) => {
|
||||||
score
|
const score = resultMap.get(index)
|
||||||
}
|
if (score === undefined) return undefined
|
||||||
})
|
|
||||||
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
return {
|
||||||
.sort((a, b) => b.score - a.score)
|
...doc,
|
||||||
|
score
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SiliconFlow Reranker API 错误:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { app } from 'electron'
|
|||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
import * as fs from 'fs-extra'
|
import * as fs from 'fs-extra'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
import { createClient, FileStat } from 'webdav'
|
||||||
|
|
||||||
import WebDav from './WebDav'
|
import WebDav from './WebDav'
|
||||||
import { windowService } from './WindowService'
|
import { windowService } from './WindowService'
|
||||||
@ -18,6 +19,7 @@ class BackupManager {
|
|||||||
this.restore = this.restore.bind(this)
|
this.restore = this.restore.bind(this)
|
||||||
this.backupToWebdav = this.backupToWebdav.bind(this)
|
this.backupToWebdav = this.backupToWebdav.bind(this)
|
||||||
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
|
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
|
||||||
|
this.listWebdavFiles = this.listWebdavFiles.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setWritableRecursive(dirPath: string): Promise<void> {
|
private async setWritableRecursive(dirPath: string): Promise<void> {
|
||||||
@ -117,10 +119,10 @@ class BackupManager {
|
|||||||
await fs.remove(this.tempDir)
|
await fs.remove(this.tempDir)
|
||||||
onProgress({ stage: 'completed', progress: 100, total: 100 })
|
onProgress({ stage: 'completed', progress: 100, total: 100 })
|
||||||
|
|
||||||
Logger.log('Backup completed successfully')
|
Logger.log('[BackupManager] Backup completed successfully')
|
||||||
return backupedFilePath
|
return backupedFilePath
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('Backup failed:', error)
|
Logger.error('[BackupManager] Backup failed:', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,7 +188,7 @@ class BackupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
|
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
|
||||||
const filename = 'cherry-studio.backup.zip'
|
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
|
||||||
const backupedFilePath = await this.backup(_, filename, data)
|
const backupedFilePath = await this.backup(_, filename, data)
|
||||||
const webdavClient = new WebDav(webdavConfig)
|
const webdavClient = new WebDav(webdavConfig)
|
||||||
return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
|
return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
|
||||||
@ -195,18 +197,48 @@ class BackupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
|
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
|
||||||
const filename = 'cherry-studio.backup.zip'
|
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
|
||||||
const webdavClient = new WebDav(webdavConfig)
|
const webdavClient = new WebDav(webdavConfig)
|
||||||
const retrievedFile = await webdavClient.getFileContents(filename)
|
try {
|
||||||
const backupedFilePath = path.join(this.backupDir, filename)
|
const retrievedFile = await webdavClient.getFileContents(filename)
|
||||||
|
const backupedFilePath = path.join(this.backupDir, filename)
|
||||||
|
|
||||||
if (!fs.existsSync(this.backupDir)) {
|
if (!fs.existsSync(this.backupDir)) {
|
||||||
fs.mkdirSync(this.backupDir, { recursive: true })
|
fs.mkdirSync(this.backupDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync为同步写,无须await
|
||||||
|
fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
|
||||||
|
|
||||||
|
return await this.restore(_, backupedFilePath)
|
||||||
|
} catch (error: any) {
|
||||||
|
Logger.error('[backup] Failed to restore from WebDAV:', error)
|
||||||
|
throw new Error(error.message || 'Failed to restore backup file')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
|
listWebdavFiles = async (_: Electron.IpcMainInvokeEvent, config: WebDavConfig) => {
|
||||||
|
try {
|
||||||
|
const client = createClient(config.webdavHost, {
|
||||||
|
username: config.webdavUser,
|
||||||
|
password: config.webdavPass
|
||||||
|
})
|
||||||
|
|
||||||
return await this.restore(_, backupedFilePath)
|
const response = await client.getDirectoryContents(config.webdavPath)
|
||||||
|
const files = Array.isArray(response) ? response : response.data
|
||||||
|
|
||||||
|
return files
|
||||||
|
.filter((file: FileStat) => file.type === 'file' && file.basename.endsWith('.zip'))
|
||||||
|
.map((file: FileStat) => ({
|
||||||
|
fileName: file.basename,
|
||||||
|
modifiedTime: file.lastmod,
|
||||||
|
size: file.size
|
||||||
|
}))
|
||||||
|
.sort((a, b) => new Date(b.modifiedTime).getTime() - new Date(a.modifiedTime).getTime())
|
||||||
|
} catch (error: any) {
|
||||||
|
Logger.error('Failed to list WebDAV files:', error)
|
||||||
|
throw new Error(error.message || 'Failed to list backup files')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDirSize(dirPath: string): Promise<number> {
|
private async getDirSize(dirPath: string): Promise<number> {
|
||||||
|
|||||||
15
src/preload/index.d.ts
vendored
15
src/preload/index.d.ts
vendored
@ -6,7 +6,12 @@ import { AppInfo, FileType, KnowledgeBaseParams, KnowledgeItem, LanguageVarious,
|
|||||||
import type { LoaderReturn } from '@shared/config/types'
|
import type { LoaderReturn } from '@shared/config/types'
|
||||||
import type { OpenDialogOptions } from 'electron'
|
import type { OpenDialogOptions } from 'electron'
|
||||||
import type { UpdateInfo } from 'electron-updater'
|
import type { UpdateInfo } from 'electron-updater'
|
||||||
import { Readable } from 'stream'
|
|
||||||
|
interface BackupFile {
|
||||||
|
fileName: string
|
||||||
|
modifiedTime: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -24,6 +29,9 @@ declare global {
|
|||||||
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
||||||
reload: () => void
|
reload: () => void
|
||||||
clearCache: () => Promise<{ success: boolean; error?: string }>
|
clearCache: () => Promise<{ success: boolean; error?: string }>
|
||||||
|
system: {
|
||||||
|
getDeviceType: () => Promise<'mac' | 'windows' | 'linux'>
|
||||||
|
}
|
||||||
zip: {
|
zip: {
|
||||||
compress: (text: string) => Promise<Buffer>
|
compress: (text: string) => Promise<Buffer>
|
||||||
decompress: (text: Buffer) => Promise<string>
|
decompress: (text: Buffer) => Promise<string>
|
||||||
@ -33,6 +41,7 @@ declare global {
|
|||||||
restore: (backupPath: string) => Promise<string>
|
restore: (backupPath: string) => Promise<string>
|
||||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
|
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
|
||||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<string>
|
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<string>
|
||||||
|
listWebdavFiles: (webdavConfig: WebDavConfig) => Promise<BackupFile[]>
|
||||||
}
|
}
|
||||||
file: {
|
file: {
|
||||||
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
||||||
@ -68,8 +77,8 @@ declare global {
|
|||||||
update: (shortcuts: Shortcut[]) => Promise<void>
|
update: (shortcuts: Shortcut[]) => Promise<void>
|
||||||
}
|
}
|
||||||
knowledgeBase: {
|
knowledgeBase: {
|
||||||
create: ({ id, model, apiKey, baseURL }: KnowledgeBaseParams) => Promise<void>
|
create: (base: KnowledgeBaseParams) => Promise<void>
|
||||||
reset: ({ base }: { base: KnowledgeBaseParams }) => Promise<void>
|
reset: (base: KnowledgeBaseParams) => Promise<void>
|
||||||
delete: (id: string) => Promise<void>
|
delete: (id: string) => Promise<void>
|
||||||
add: ({
|
add: ({
|
||||||
base,
|
base,
|
||||||
|
|||||||
@ -17,6 +17,9 @@ const api = {
|
|||||||
openWebsite: (url: string) => ipcRenderer.invoke('open:website', url),
|
openWebsite: (url: string) => ipcRenderer.invoke('open:website', url),
|
||||||
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
||||||
clearCache: () => ipcRenderer.invoke('app:clear-cache'),
|
clearCache: () => ipcRenderer.invoke('app:clear-cache'),
|
||||||
|
system: {
|
||||||
|
getDeviceType: () => ipcRenderer.invoke('system:getDeviceType')
|
||||||
|
},
|
||||||
zip: {
|
zip: {
|
||||||
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
||||||
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
|
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
|
||||||
@ -27,7 +30,8 @@ const api = {
|
|||||||
restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath),
|
restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath),
|
||||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
|
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
|
||||||
ipcRenderer.invoke('backup:backupToWebdav', data, webdavConfig),
|
ipcRenderer.invoke('backup:backupToWebdav', data, webdavConfig),
|
||||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig)
|
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig),
|
||||||
|
listWebdavFiles: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:listWebdavFiles', webdavConfig)
|
||||||
},
|
},
|
||||||
nodeapp: {
|
nodeapp: {
|
||||||
list: () => ipcRenderer.invoke('nodeapp:list'),
|
list: () => ipcRenderer.invoke('nodeapp:list'),
|
||||||
@ -92,9 +96,8 @@ const api = {
|
|||||||
update: (shortcuts: Shortcut[]) => ipcRenderer.invoke('shortcuts:update', shortcuts)
|
update: (shortcuts: Shortcut[]) => ipcRenderer.invoke('shortcuts:update', shortcuts)
|
||||||
},
|
},
|
||||||
knowledgeBase: {
|
knowledgeBase: {
|
||||||
create: ({ id, model, apiKey, baseURL }: KnowledgeBaseParams) =>
|
create: (base: KnowledgeBaseParams) => ipcRenderer.invoke('knowledge-base:create', base),
|
||||||
ipcRenderer.invoke('knowledge-base:create', { id, model, apiKey, baseURL }),
|
reset: (base: KnowledgeBaseParams) => ipcRenderer.invoke('knowledge-base:reset', base),
|
||||||
reset: ({ base }: { base: KnowledgeBaseParams }) => ipcRenderer.invoke('knowledge-base:reset', { base }),
|
|
||||||
delete: (id: string) => ipcRenderer.invoke('knowledge-base:delete', id),
|
delete: (id: string) => ipcRenderer.invoke('knowledge-base:delete', id),
|
||||||
add: ({
|
add: ({
|
||||||
base,
|
base,
|
||||||
|
|||||||
@ -45,7 +45,15 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultT
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.setAttribute('os', isMac ? 'mac' : 'windows')
|
document.body.setAttribute('os', isMac ? 'mac' : 'windows')
|
||||||
}, [])
|
|
||||||
|
// listen theme change from main process from other windows
|
||||||
|
const themeChangeListenerRemover = window.electron.ipcRenderer.on('theme:change', (_, newTheme) => {
|
||||||
|
setTheme(newTheme)
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
themeChangeListenerRemover()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return <ThemeContext.Provider value={{ theme: _theme, toggleTheme }}>{children}</ThemeContext.Provider>
|
return <ThemeContext.Provider value={{ theme: _theme, toggleTheme }}>{children}</ThemeContext.Provider>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -237,7 +237,8 @@
|
|||||||
"copied": "Copied",
|
"copied": "Copied",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
"advanced_settings": "Advanced Settings"
|
"advanced_settings": "Advanced Settings",
|
||||||
|
"expand": "Expand"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "Docs"
|
"title": "Docs"
|
||||||
@ -484,6 +485,10 @@
|
|||||||
"upgrade.success.title": "Upgrade successfully",
|
"upgrade.success.title": "Upgrade successfully",
|
||||||
"warn.notion.exporting": "Exporting to Notion, please do not request export repeatedly!",
|
"warn.notion.exporting": "Exporting to Notion, please do not request export repeatedly!",
|
||||||
"warning.rate.limit": "Too many requests. Please wait {{seconds}} seconds before trying again.",
|
"warning.rate.limit": "Too many requests. Please wait {{seconds}} seconds before trying again.",
|
||||||
|
"tools": {
|
||||||
|
"invoking": "Invoking",
|
||||||
|
"completed": "Completed"
|
||||||
|
},
|
||||||
"nextJsInfo": "Next.js Application Note",
|
"nextJsInfo": "Next.js Application Note",
|
||||||
"nextJsDescription": "Next.js applications require a build step before they can be started. Make sure to check 'This is a Next.js application' or manually add 'npm run build' as the build command."
|
"nextJsDescription": "Next.js applications require a build step before they can be started. Make sure to check 'This is a Next.js application' or manually add 'npm run build' as the build command."
|
||||||
},
|
},
|
||||||
@ -766,6 +771,12 @@
|
|||||||
"password": "WebDAV Password",
|
"password": "WebDAV Password",
|
||||||
"path": "WebDAV Path",
|
"path": "WebDAV Path",
|
||||||
"path.placeholder": "/backup",
|
"path.placeholder": "/backup",
|
||||||
|
"backup.modal.title": "Backup to WebDAV",
|
||||||
|
"backup.modal.filename.placeholder": "Please enter backup filename",
|
||||||
|
"restore.modal.title": "Restore from WebDAV",
|
||||||
|
"restore.modal.select.placeholder": "Please select a backup file to restore",
|
||||||
|
"restore.confirm.title": "Confirm Restore",
|
||||||
|
"restore.confirm.content": "Restoring from WebDAV will overwrite current data. Do you want to continue?",
|
||||||
"restore.button": "Restore from WebDAV",
|
"restore.button": "Restore from WebDAV",
|
||||||
"restore.content": "Restore from WebDAV will overwrite the current data, continue?",
|
"restore.content": "Restore from WebDAV will overwrite the current data, continue?",
|
||||||
"restore.title": "Restore from WebDAV",
|
"restore.title": "Restore from WebDAV",
|
||||||
|
|||||||
@ -237,7 +237,8 @@
|
|||||||
"copied": "コピーされました",
|
"copied": "コピーされました",
|
||||||
"confirm": "確認",
|
"confirm": "確認",
|
||||||
"more": "もっと",
|
"more": "もっと",
|
||||||
"advanced_settings": "詳細設定"
|
"advanced_settings": "詳細設定",
|
||||||
|
"expand": "展開"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "ドキュメント"
|
"title": "ドキュメント"
|
||||||
@ -483,7 +484,11 @@
|
|||||||
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
||||||
"upgrade.success.title": "アップグレードに成功しました",
|
"upgrade.success.title": "アップグレードに成功しました",
|
||||||
"warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! ",
|
"warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! ",
|
||||||
"warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。"
|
"warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。",
|
||||||
|
"tools": {
|
||||||
|
"invoking": "呼び出し中",
|
||||||
|
"completed": "完了"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "サイドバーに追加",
|
"sidebar.add.title": "サイドバーに追加",
|
||||||
@ -763,7 +768,13 @@
|
|||||||
"syncError": "バックアップエラー",
|
"syncError": "バックアップエラー",
|
||||||
"syncStatus": "バックアップ状態",
|
"syncStatus": "バックアップ状態",
|
||||||
"title": "WebDAV",
|
"title": "WebDAV",
|
||||||
"user": "WebDAVユーザー"
|
"user": "WebDAVユーザー",
|
||||||
|
"backup.modal.title": "WebDAV にバックアップ",
|
||||||
|
"backup.modal.filename.placeholder": "バックアップファイル名を入力してください",
|
||||||
|
"restore.modal.title": "WebDAV から復元",
|
||||||
|
"restore.modal.select.placeholder": "復元するバックアップファイルを選択してください",
|
||||||
|
"restore.confirm.title": "復元を確認",
|
||||||
|
"restore.confirm.content": "WebDAV から復元すると現在のデータが上書きされます。続行しますか?"
|
||||||
},
|
},
|
||||||
"yuque": {
|
"yuque": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -237,7 +237,8 @@
|
|||||||
"confirm": "Подтверждение",
|
"confirm": "Подтверждение",
|
||||||
"copied": "Скопировано",
|
"copied": "Скопировано",
|
||||||
"more": "Ещё",
|
"more": "Ещё",
|
||||||
"advanced_settings": "Дополнительные настройки"
|
"advanced_settings": "Дополнительные настройки",
|
||||||
|
"expand": "Развернуть"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "Документация"
|
"title": "Документация"
|
||||||
@ -489,7 +490,11 @@
|
|||||||
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
||||||
"upgrade.success.title": "Обновление успешно",
|
"upgrade.success.title": "Обновление успешно",
|
||||||
"warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!",
|
"warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!",
|
||||||
"warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова."
|
"warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова.",
|
||||||
|
"tools": {
|
||||||
|
"invoking": "Вызов",
|
||||||
|
"completed": "Завершено"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "Добавить в боковую панель",
|
"sidebar.add.title": "Добавить в боковую панель",
|
||||||
@ -763,7 +768,13 @@
|
|||||||
"syncError": "Ошибка резервного копирования",
|
"syncError": "Ошибка резервного копирования",
|
||||||
"syncStatus": "Статус резервного копирования",
|
"syncStatus": "Статус резервного копирования",
|
||||||
"title": "WebDAV",
|
"title": "WebDAV",
|
||||||
"user": "Пользователь WebDAV"
|
"user": "Пользователь WebDAV",
|
||||||
|
"backup.modal.title": "Резервное копирование на WebDAV",
|
||||||
|
"backup.modal.filename.placeholder": "Введите имя файла резервной копии",
|
||||||
|
"restore.modal.title": "Восстановление с WebDAV",
|
||||||
|
"restore.modal.select.placeholder": "Выберите файл резервной копии для восстановления",
|
||||||
|
"restore.confirm.title": "Подтверждение восстановления",
|
||||||
|
"restore.confirm.content": "Восстановление с WebDAV перезапишет текущие данные, продолжить?"
|
||||||
},
|
},
|
||||||
"yuque": {
|
"yuque": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -237,7 +237,8 @@
|
|||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"you": "用户",
|
"you": "用户",
|
||||||
"more": "更多",
|
"more": "更多",
|
||||||
"advanced_settings": "高级设置"
|
"advanced_settings": "高级设置",
|
||||||
|
"expand": "展开"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "帮助文档"
|
"title": "帮助文档"
|
||||||
@ -483,7 +484,11 @@
|
|||||||
"upgrade.success.content": "重启用以完成升级",
|
"upgrade.success.content": "重启用以完成升级",
|
||||||
"upgrade.success.title": "升级成功",
|
"upgrade.success.title": "升级成功",
|
||||||
"warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!",
|
"warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!",
|
||||||
"warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试"
|
"warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试",
|
||||||
|
"tools": {
|
||||||
|
"invoking": "调用中",
|
||||||
|
"completed": "已完成"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "添加到侧边栏",
|
"sidebar.add.title": "添加到侧边栏",
|
||||||
@ -757,6 +762,12 @@
|
|||||||
"password": "WebDAV 密码",
|
"password": "WebDAV 密码",
|
||||||
"path": "WebDAV 路径",
|
"path": "WebDAV 路径",
|
||||||
"path.placeholder": "/backup",
|
"path.placeholder": "/backup",
|
||||||
|
"backup.modal.title": "备份到 WebDAV",
|
||||||
|
"backup.modal.filename.placeholder": "请输入备份文件名",
|
||||||
|
"restore.modal.title": "从 WebDAV 恢复",
|
||||||
|
"restore.modal.select.placeholder": "请选择要恢复的备份文件",
|
||||||
|
"restore.confirm.title": "确认恢复",
|
||||||
|
"restore.confirm.content": "从 WebDAV 恢复将会覆盖当前数据,是否继续?",
|
||||||
"restore.button": "从 WebDAV 恢复",
|
"restore.button": "从 WebDAV 恢复",
|
||||||
"restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?",
|
"restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?",
|
||||||
"restore.title": "从 WebDAV 恢复",
|
"restore.title": "从 WebDAV 恢复",
|
||||||
|
|||||||
@ -237,7 +237,8 @@
|
|||||||
"copied": "已複製",
|
"copied": "已複製",
|
||||||
"confirm": "確認",
|
"confirm": "確認",
|
||||||
"more": "更多",
|
"more": "更多",
|
||||||
"advanced_settings": "進階設定"
|
"advanced_settings": "進階設定",
|
||||||
|
"expand": "展開"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "說明文件"
|
"title": "說明文件"
|
||||||
@ -483,7 +484,11 @@
|
|||||||
"upgrade.success.content": "請重新啟動程式以完成升級",
|
"upgrade.success.content": "請重新啟動程式以完成升級",
|
||||||
"upgrade.success.title": "升級成功",
|
"upgrade.success.title": "升級成功",
|
||||||
"warn.notion.exporting": "正在匯出到 Notion,請勿重複請求匯出!",
|
"warn.notion.exporting": "正在匯出到 Notion,請勿重複請求匯出!",
|
||||||
"warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試"
|
"warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試",
|
||||||
|
"tools": {
|
||||||
|
"invoking": "調用中",
|
||||||
|
"completed": "已完成"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "新增到側邊欄",
|
"sidebar.add.title": "新增到側邊欄",
|
||||||
@ -763,7 +768,13 @@
|
|||||||
"syncError": "備份錯誤",
|
"syncError": "備份錯誤",
|
||||||
"syncStatus": "備份狀態",
|
"syncStatus": "備份狀態",
|
||||||
"title": "WebDAV",
|
"title": "WebDAV",
|
||||||
"user": "WebDAV 使用者名稱"
|
"user": "WebDAV 使用者名稱",
|
||||||
|
"backup.modal.title": "備份到 WebDAV",
|
||||||
|
"backup.modal.filename.placeholder": "請輸入備份文件名",
|
||||||
|
"restore.modal.title": "從 WebDAV 恢復",
|
||||||
|
"restore.modal.select.placeholder": "請選擇要恢復的備份文件",
|
||||||
|
"restore.confirm.title": "復元確認",
|
||||||
|
"restore.confirm.content": "從 WebDAV 恢復將覆蓋目前資料,是否繼續?"
|
||||||
},
|
},
|
||||||
"yuque": {
|
"yuque": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -70,7 +70,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
const onCopy = useCallback(
|
const onCopy = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
|
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content.trimStart()))
|
||||||
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
|||||||
@ -60,7 +60,7 @@ const MessageTools: FC<Props> = ({ message }) => {
|
|||||||
<TitleContent>
|
<TitleContent>
|
||||||
<ToolName>{tool.name}</ToolName>
|
<ToolName>{tool.name}</ToolName>
|
||||||
<StatusIndicator $isInvoking={isInvoking}>
|
<StatusIndicator $isInvoking={isInvoking}>
|
||||||
{isInvoking ? t('tools.invoking') : t('tools.completed')}
|
{isInvoking ? t('message.tools.invoking') : t('message.tools.completed')}
|
||||||
{isInvoking && <LoadingOutlined spin style={{ marginLeft: 6 }} />}
|
{isInvoking && <LoadingOutlined spin style={{ marginLeft: 6 }} />}
|
||||||
{isDone && <CheckOutlined style={{ marginLeft: 6 }} />}
|
{isDone && <CheckOutlined style={{ marginLeft: 6 }} />}
|
||||||
</StatusIndicator>
|
</StatusIndicator>
|
||||||
|
|||||||
@ -142,10 +142,10 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
|
|||||||
name="rerankModel"
|
name="rerankModel"
|
||||||
label={t('models.rerank_model')}
|
label={t('models.rerank_model')}
|
||||||
tooltip={{ title: t('models.rerank_model_tooltip'), placement: 'right' }}
|
tooltip={{ title: t('models.rerank_model_tooltip'), placement: 'right' }}
|
||||||
|
initialValue={getModelUniqId(base.rerankModel) || undefined}
|
||||||
rules={[{ required: false, message: t('message.error.enter.model') }]}>
|
rules={[{ required: false, message: t('message.error.enter.model') }]}>
|
||||||
<Select
|
<Select
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
defaultValue={getModelUniqId(base.rerankModel) || undefined}
|
|
||||||
options={rerankSelectOptions}
|
options={rerankSelectOptions}
|
||||||
placeholder={t('settings.models.empty')}
|
placeholder={t('settings.models.empty')}
|
||||||
allowClear
|
allowClear
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { FolderOpenOutlined, SaveOutlined, SyncOutlined } from '@ant-design/icons'
|
import { FolderOpenOutlined, SaveOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { backupToWebdav, restoreFromWebdav, startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
|
import { backupToWebdav, restoreFromWebdav, startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
setWebdavAutoSync,
|
setWebdavAutoSync,
|
||||||
setWebdavHost as _setWebdavHost,
|
setWebdavHost as _setWebdavHost,
|
||||||
@ -13,13 +12,19 @@ import {
|
|||||||
setWebdavSyncInterval as _setWebdavSyncInterval,
|
setWebdavSyncInterval as _setWebdavSyncInterval,
|
||||||
setWebdavUser as _setWebdavUser
|
setWebdavUser as _setWebdavUser
|
||||||
} from '@renderer/store/settings'
|
} from '@renderer/store/settings'
|
||||||
import { Button, Input, Select } from 'antd'
|
import { Button, Input, Modal, Select, Spin, Tooltip } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||||
|
|
||||||
|
interface BackupFile {
|
||||||
|
fileName: string
|
||||||
|
modifiedTime: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
const WebDavSettings: FC = () => {
|
const WebDavSettings: FC = () => {
|
||||||
const {
|
const {
|
||||||
webdavHost: webDAVHost,
|
webdavHost: webDAVHost,
|
||||||
@ -38,45 +43,22 @@ const WebDavSettings: FC = () => {
|
|||||||
|
|
||||||
const [backuping, setBackuping] = useState(false)
|
const [backuping, setBackuping] = useState(false)
|
||||||
const [restoring, setRestoring] = useState(false)
|
const [restoring, setRestoring] = useState(false)
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||||
|
const [customFileName, setCustomFileName] = useState('')
|
||||||
|
const [isRestoreModalVisible, setIsRestoreModalVisible] = useState(false)
|
||||||
|
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([])
|
||||||
|
const [selectedFile, setSelectedFile] = useState<string>('')
|
||||||
|
const [loadingFiles, setLoadingFiles] = useState(false)
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { webdavSync } = useRuntime()
|
const { webdavSync } = useAppSelector((state) => state.backup)
|
||||||
|
|
||||||
// 把之前备份的文件定时上传到 webdav,首先先配置 webdav 的 host, port, user, pass, path
|
// 把之前备份的文件定时上传到 webdav,首先先配置 webdav 的 host, port, user, pass, path
|
||||||
|
|
||||||
const onBackup = async () => {
|
|
||||||
if (!webdavHost) {
|
|
||||||
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setBackuping(true)
|
|
||||||
await backupToWebdav({ showMessage: true })
|
|
||||||
setBackuping(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRestore = async () => {
|
|
||||||
if (!webdavHost) {
|
|
||||||
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setRestoring(true)
|
|
||||||
await restoreFromWebdav()
|
|
||||||
setRestoring(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPressRestore = () => {
|
|
||||||
window.modal.confirm({
|
|
||||||
title: t('settings.data.webdav.restore.title'),
|
|
||||||
content: t('settings.data.webdav.restore.content'),
|
|
||||||
centered: true,
|
|
||||||
onOk: onRestore
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSyncIntervalChange = (value: number) => {
|
const onSyncIntervalChange = (value: number) => {
|
||||||
setSyncInterval(value)
|
setSyncInterval(value)
|
||||||
dispatch(_setWebdavSyncInterval(value))
|
dispatch(_setWebdavSyncInterval(value))
|
||||||
@ -99,20 +81,102 @@ const WebDavSettings: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<HStack gap="5px" alignItems="center">
|
<HStack gap="5px" alignItems="center">
|
||||||
{webdavSync.syncing && <SyncOutlined spin />}
|
{webdavSync.syncing && <SyncOutlined spin />}
|
||||||
|
{!webdavSync.syncing && webdavSync.lastSyncError && (
|
||||||
|
<Tooltip title={`${t('settings.data.webdav.syncError')}: ${webdavSync.lastSyncError}`}>
|
||||||
|
<WarningOutlined style={{ color: 'red' }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{webdavSync.lastSyncTime && (
|
{webdavSync.lastSyncTime && (
|
||||||
<span style={{ color: 'var(--text-secondary)' }}>
|
<span style={{ color: 'var(--text-secondary)' }}>
|
||||||
{t('settings.data.webdav.lastSync')}: {dayjs(webdavSync.lastSyncTime).format('HH:mm:ss')}
|
{t('settings.data.webdav.lastSync')}: {dayjs(webdavSync.lastSyncTime).format('HH:mm:ss')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{webdavSync.lastSyncError && (
|
|
||||||
<span style={{ color: 'var(--error-color)' }}>
|
|
||||||
{t('settings.data.webdav.syncError')}: {webdavSync.lastSyncError}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</HStack>
|
</HStack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showBackupModal = async () => {
|
||||||
|
// 获取默认文件名
|
||||||
|
const deviceType = await window.api.system.getDeviceType()
|
||||||
|
const timestamp = dayjs().format('YYYYMMDDHHmmss')
|
||||||
|
const defaultFileName = `cherry-studio.${timestamp}.${deviceType}.zip`
|
||||||
|
setCustomFileName(defaultFileName)
|
||||||
|
setIsModalVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBackup = async () => {
|
||||||
|
setBackuping(true)
|
||||||
|
try {
|
||||||
|
await backupToWebdav({ showMessage: true, customFileName })
|
||||||
|
} finally {
|
||||||
|
setBackuping(false)
|
||||||
|
setIsModalVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsModalVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const showRestoreModal = async () => {
|
||||||
|
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
|
||||||
|
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRestoreModalVisible(true)
|
||||||
|
setLoadingFiles(true)
|
||||||
|
try {
|
||||||
|
const files = await window.api.backup.listWebdavFiles({
|
||||||
|
webdavHost,
|
||||||
|
webdavUser,
|
||||||
|
webdavPass,
|
||||||
|
webdavPath
|
||||||
|
})
|
||||||
|
setBackupFiles(files)
|
||||||
|
} catch (error: any) {
|
||||||
|
window.message.error({ content: error.message, key: 'list-files-error' })
|
||||||
|
} finally {
|
||||||
|
setLoadingFiles(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRestore = async () => {
|
||||||
|
if (!selectedFile || !webdavHost || !webdavUser || !webdavPass || !webdavPath) {
|
||||||
|
window.message.error({
|
||||||
|
content: !selectedFile ? t('message.error.no.file.selected') : t('message.error.invalid.webdav'),
|
||||||
|
key: 'restore-error'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.modal.confirm({
|
||||||
|
title: t('settings.data.webdav.restore.confirm.title'),
|
||||||
|
content: t('settings.data.webdav.restore.confirm.content'),
|
||||||
|
centered: true,
|
||||||
|
onOk: async () => {
|
||||||
|
setRestoring(true)
|
||||||
|
try {
|
||||||
|
await restoreFromWebdav(selectedFile)
|
||||||
|
setIsRestoreModalVisible(false)
|
||||||
|
} catch (error: any) {
|
||||||
|
window.message.error({ content: error.message, key: 'restore-error' })
|
||||||
|
} finally {
|
||||||
|
setRestoring(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFileOption = (file: BackupFile) => {
|
||||||
|
const date = dayjs(file.modifiedTime).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
const size = `${(file.size / 1024).toFixed(2)} KB`
|
||||||
|
return {
|
||||||
|
label: `${file.fileName} (${date}, ${size})`,
|
||||||
|
value: file.fileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingGroup theme={theme}>
|
<SettingGroup theme={theme}>
|
||||||
<SettingTitle>{t('settings.data.webdav.title')}</SettingTitle>
|
<SettingTitle>{t('settings.data.webdav.title')}</SettingTitle>
|
||||||
@ -165,10 +229,10 @@ const WebDavSettings: FC = () => {
|
|||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||||
<HStack gap="5px" justifyContent="space-between">
|
<HStack gap="5px" justifyContent="space-between">
|
||||||
<Button onClick={onBackup} icon={<SaveOutlined />} loading={backuping}>
|
<Button onClick={showBackupModal} icon={<SaveOutlined />} loading={backuping}>
|
||||||
{t('settings.data.webdav.backup.button')}
|
{t('settings.data.webdav.backup.button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onPressRestore} icon={<FolderOpenOutlined />} loading={restoring}>
|
<Button onClick={showRestoreModal} icon={<FolderOpenOutlined />} loading={restoring}>
|
||||||
{t('settings.data.webdav.restore.button')}
|
{t('settings.data.webdav.restore.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
@ -198,6 +262,46 @@ const WebDavSettings: FC = () => {
|
|||||||
</SettingRow>
|
</SettingRow>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
title={t('settings.data.webdav.backup.modal.title')}
|
||||||
|
open={isModalVisible}
|
||||||
|
onOk={handleBackup}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
okButtonProps={{ loading: backuping }}>
|
||||||
|
<Input
|
||||||
|
value={customFileName}
|
||||||
|
onChange={(e) => setCustomFileName(e.target.value)}
|
||||||
|
placeholder={t('settings.data.webdav.backup.modal.filename.placeholder')}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={t('settings.data.webdav.restore.modal.title')}
|
||||||
|
open={isRestoreModalVisible}
|
||||||
|
onOk={handleRestore}
|
||||||
|
onCancel={() => setIsRestoreModalVisible(false)}
|
||||||
|
okButtonProps={{ loading: restoring }}
|
||||||
|
width={600}>
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<Select
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder={t('settings.data.webdav.restore.modal.select.placeholder')}
|
||||||
|
value={selectedFile}
|
||||||
|
onChange={setSelectedFile}
|
||||||
|
options={backupFiles.map(formatFileOption)}
|
||||||
|
loading={loadingFiles}
|
||||||
|
showSearch
|
||||||
|
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
|
||||||
|
/>
|
||||||
|
{loadingFiles && (
|
||||||
|
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}>
|
||||||
|
<Spin />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -306,7 +306,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
{apiKeyWebsite && (
|
{apiKeyWebsite && (
|
||||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||||
<HStack gap={5}>
|
<HStack>
|
||||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||||
{t('settings.provider.get_api_key')}
|
{t('settings.provider.get_api_key')}
|
||||||
</SettingHelpLink>
|
</SettingHelpLink>
|
||||||
|
|||||||
@ -112,8 +112,8 @@ const TranslatePage: FC = () => {
|
|||||||
message,
|
message,
|
||||||
assistant,
|
assistant,
|
||||||
onResponse: (text) => {
|
onResponse: (text) => {
|
||||||
translatedText = text
|
translatedText = text.replace(/^\s*\n+/g, '')
|
||||||
setResult(text)
|
setResult(translatedText)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -497,7 +497,7 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finishReason === 'tool_calls') {
|
if (finishReason === 'tool_calls' || (finishReason === 'stop' && Object.keys(final_tool_calls).length > 0)) {
|
||||||
const toolCalls = Object.values(final_tool_calls).map(this.cleanToolCallArgs)
|
const toolCalls = Object.values(final_tool_calls).map(this.cleanToolCallArgs)
|
||||||
console.log('start invoke tools', toolCalls)
|
console.log('start invoke tools', toolCalls)
|
||||||
if (this.isZhipuTool(model)) {
|
if (this.isZhipuTool(model)) {
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { setWebDAVSyncState } from '@renderer/store/runtime'
|
import { setWebDAVSyncState } from '@renderer/store/backup'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import Logger from 'electron-log'
|
||||||
|
|
||||||
export async function backup() {
|
export async function backup() {
|
||||||
const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}.zip`
|
const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}.zip`
|
||||||
@ -59,16 +60,27 @@ export async function reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 备份到 webdav
|
// 备份到 webdav
|
||||||
export async function backupToWebdav({ showMessage = false }: { showMessage?: boolean } = {}) {
|
export async function backupToWebdav({
|
||||||
|
showMessage = false,
|
||||||
|
customFileName = ''
|
||||||
|
}: { showMessage?: boolean; customFileName?: string } = {}) {
|
||||||
if (isManualBackupRunning) {
|
if (isManualBackupRunning) {
|
||||||
console.log('[Backup] Manual backup already in progress')
|
Logger.log('[Backup] Manual backup already in progress')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null }))
|
store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null }))
|
||||||
|
|
||||||
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
||||||
|
let deviceType = 'unknown'
|
||||||
|
try {
|
||||||
|
deviceType = (await window.api.system.getDeviceType()) || 'unknown'
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[Backup] Failed to get device type:', error)
|
||||||
|
}
|
||||||
|
const timestamp = dayjs().format('YYYYMMDDHHmmss')
|
||||||
|
const backupFileName = customFileName || `cherry-studio.${timestamp}.${deviceType}.zip`
|
||||||
|
const finalFileName = backupFileName.endsWith('.zip') ? backupFileName : `${backupFileName}.zip`
|
||||||
const backupData = await getBackupData()
|
const backupData = await getBackupData()
|
||||||
|
|
||||||
// 上传文件
|
// 上传文件
|
||||||
@ -77,43 +89,47 @@ export async function backupToWebdav({ showMessage = false }: { showMessage?: bo
|
|||||||
webdavHost,
|
webdavHost,
|
||||||
webdavUser,
|
webdavUser,
|
||||||
webdavPass,
|
webdavPass,
|
||||||
webdavPath
|
webdavPath,
|
||||||
|
fileName: finalFileName
|
||||||
})
|
})
|
||||||
if (success) {
|
if (success) {
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
setWebDAVSyncState({
|
setWebDAVSyncState({
|
||||||
lastSyncTime: Date.now(),
|
|
||||||
lastSyncError: null
|
lastSyncError: null
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
||||||
} else {
|
} else {
|
||||||
store.dispatch(setWebDAVSyncState({ lastSyncError: 'Backup failed' }))
|
store.dispatch(setWebDAVSyncState({ lastSyncError: 'Backup failed' }))
|
||||||
showMessage && window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' })
|
window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' })
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
store.dispatch(setWebDAVSyncState({ lastSyncError: error.message }))
|
store.dispatch(setWebDAVSyncState({ lastSyncError: error.message }))
|
||||||
console.error('[backup] backupToWebdav: Error uploading file to WebDAV:', error)
|
console.error('[Backup] backupToWebdav: Error uploading file to WebDAV:', error)
|
||||||
showMessage &&
|
window.modal.error({
|
||||||
window.modal.error({
|
title: i18n.t('message.backup.failed'),
|
||||||
title: i18n.t('message.backup.failed'),
|
content: error.message
|
||||||
content: error.message
|
})
|
||||||
})
|
|
||||||
} finally {
|
} finally {
|
||||||
store.dispatch(setWebDAVSyncState({ syncing: false }))
|
store.dispatch(
|
||||||
|
setWebDAVSyncState({
|
||||||
|
lastSyncTime: Date.now(),
|
||||||
|
syncing: false
|
||||||
|
})
|
||||||
|
)
|
||||||
isManualBackupRunning = false
|
isManualBackupRunning = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 webdav 恢复
|
// 从 webdav 恢复
|
||||||
export async function restoreFromWebdav() {
|
export async function restoreFromWebdav(fileName?: string) {
|
||||||
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
||||||
let data = ''
|
let data = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
data = await window.api.backup.restoreFromWebdav({ webdavHost, webdavUser, webdavPass, webdavPath })
|
data = await window.api.backup.restoreFromWebdav({ webdavHost, webdavUser, webdavPass, webdavPath, fileName })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[backup] restoreFromWebdav: Error downloading file from WebDAV:', error)
|
console.error('[Backup] restoreFromWebdav: Error downloading file from WebDAV:', error)
|
||||||
window.modal.error({
|
window.modal.error({
|
||||||
title: i18n.t('message.restore.failed'),
|
title: i18n.t('message.restore.failed'),
|
||||||
content: error.message
|
content: error.message
|
||||||
@ -123,7 +139,7 @@ export async function restoreFromWebdav() {
|
|||||||
try {
|
try {
|
||||||
await handleData(JSON.parse(data))
|
await handleData(JSON.parse(data))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[backup] Error downloading file from WebDAV:', error)
|
console.error('[Backup] Error downloading file from WebDAV:', error)
|
||||||
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,6 +174,7 @@ export function startAutoSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { webdavSyncInterval } = store.getState().settings
|
const { webdavSyncInterval } = store.getState().settings
|
||||||
|
const { webdavSync } = store.getState().backup
|
||||||
|
|
||||||
if (webdavSyncInterval <= 0) {
|
if (webdavSyncInterval <= 0) {
|
||||||
console.log('[AutoSync] Invalid sync interval, auto sync disabled')
|
console.log('[AutoSync] Invalid sync interval, auto sync disabled')
|
||||||
@ -165,9 +182,21 @@ export function startAutoSync() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
syncTimeout = setTimeout(performAutoBackup, webdavSyncInterval * 60 * 1000)
|
// 用户指定的自动备份时间间隔(毫秒)
|
||||||
|
const requiredInterval = webdavSyncInterval * 60 * 1000
|
||||||
|
|
||||||
console.log(`[AutoSync] Next sync scheduled in ${webdavSyncInterval} minutes`)
|
// 如果存在最后一次同步WebDAV的时间,以它为参考计算下一次同步的时间
|
||||||
|
const timeUntilNextSync = webdavSync?.lastSyncTime
|
||||||
|
? Math.max(1000, webdavSync.lastSyncTime + requiredInterval - Date.now())
|
||||||
|
: requiredInterval
|
||||||
|
|
||||||
|
syncTimeout = setTimeout(performAutoBackup, timeUntilNextSync)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[AutoSync] Next sync scheduled in ${Math.floor(timeUntilNextSync / 1000 / 60)} minutes ${Math.floor(
|
||||||
|
(timeUntilNextSync / 1000) % 60
|
||||||
|
)} seconds`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performAutoBackup() {
|
async function performAutoBackup() {
|
||||||
@ -179,7 +208,7 @@ export function startAutoSync() {
|
|||||||
|
|
||||||
isAutoBackupRunning = true
|
isAutoBackupRunning = true
|
||||||
try {
|
try {
|
||||||
console.log('[AutoSync] Performing auto backup...')
|
console.log('[AutoSync] Starting auto backup...')
|
||||||
await backupToWebdav({ showMessage: false })
|
await backupToWebdav({ showMessage: false })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AutoSync] Auto backup failed:', error)
|
console.error('[AutoSync] Auto backup failed:', error)
|
||||||
|
|||||||
@ -11,10 +11,12 @@ import FileManager from './FileManager'
|
|||||||
|
|
||||||
export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams => {
|
export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams => {
|
||||||
const provider = getProviderByModel(base.model)
|
const provider = getProviderByModel(base.model)
|
||||||
|
const rerankProvider = getProviderByModel(base.rerankModel)
|
||||||
const aiProvider = new AiProvider(provider)
|
const aiProvider = new AiProvider(provider)
|
||||||
|
const rerankAiProvider = new AiProvider(rerankProvider)
|
||||||
|
|
||||||
let host = aiProvider.getBaseURL()
|
let host = aiProvider.getBaseURL()
|
||||||
|
const rerankHost = rerankAiProvider.getBaseURL()
|
||||||
if (provider.type === 'gemini') {
|
if (provider.type === 'gemini') {
|
||||||
host = host + '/v1beta/openai/'
|
host = host + '/v1beta/openai/'
|
||||||
}
|
}
|
||||||
@ -40,6 +42,8 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
|
|||||||
baseURL: host,
|
baseURL: host,
|
||||||
chunkSize,
|
chunkSize,
|
||||||
chunkOverlap: base.chunkOverlap,
|
chunkOverlap: base.chunkOverlap,
|
||||||
|
rerankBaseURL: rerankHost,
|
||||||
|
rerankApiKey: rerankAiProvider.getApiKey() || 'secret',
|
||||||
rerankModel: base.rerankModel?.id,
|
rerankModel: base.rerankModel?.id,
|
||||||
rerankModelProvider: base.rerankModel?.provider,
|
rerankModelProvider: base.rerankModel?.provider,
|
||||||
topN: base.topN
|
topN: base.topN
|
||||||
|
|||||||
32
src/renderer/src/store/backup.ts
Normal file
32
src/renderer/src/store/backup.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
export interface WebDAVSyncState {
|
||||||
|
lastSyncTime: number | null
|
||||||
|
syncing: boolean
|
||||||
|
lastSyncError: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupState {
|
||||||
|
webdavSync: WebDAVSyncState
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: BackupState = {
|
||||||
|
webdavSync: {
|
||||||
|
lastSyncTime: null,
|
||||||
|
syncing: false,
|
||||||
|
lastSyncError: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupSlice = createSlice({
|
||||||
|
name: 'backup',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setWebDAVSyncState: (state, action: PayloadAction<Partial<WebDAVSyncState>>) => {
|
||||||
|
state.webdavSync = { ...state.webdavSync, ...action.payload }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const { setWebDAVSyncState } = backupSlice.actions
|
||||||
|
export default backupSlice.reducer
|
||||||
@ -5,6 +5,7 @@ import storage from 'redux-persist/lib/storage'
|
|||||||
|
|
||||||
import agents from './agents'
|
import agents from './agents'
|
||||||
import assistants from './assistants'
|
import assistants from './assistants'
|
||||||
|
import backup from './backup'
|
||||||
import copilot from './copilot'
|
import copilot from './copilot'
|
||||||
import knowledge from './knowledge'
|
import knowledge from './knowledge'
|
||||||
import llm from './llm'
|
import llm from './llm'
|
||||||
@ -22,6 +23,7 @@ import websearch from './websearch'
|
|||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
assistants,
|
assistants,
|
||||||
agents,
|
agents,
|
||||||
|
backup,
|
||||||
paintings,
|
paintings,
|
||||||
llm,
|
llm,
|
||||||
settings,
|
settings,
|
||||||
@ -40,7 +42,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 81,
|
version: 82,
|
||||||
blacklist: ['runtime', 'messages'],
|
blacklist: ['runtime', 'messages'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -28,31 +28,6 @@ const initialState: MessagesState = {
|
|||||||
error: null
|
error: null
|
||||||
}
|
}
|
||||||
|
|
||||||
// const MAX_RECENT_TOPICS = 10
|
|
||||||
|
|
||||||
// // 只初始化最近的会话消息
|
|
||||||
// export const initializeMessagesState = createAsyncThunk('messages/initialize', async () => {
|
|
||||||
// try {
|
|
||||||
// // 获取所有会话的基本信息
|
|
||||||
// const recentTopics = await TopicManager.getTopicLimit(MAX_RECENT_TOPICS)
|
|
||||||
// console.log('recentTopics', recentTopics)
|
|
||||||
// const messagesByTopic: Record<string, Message[]> = {}
|
|
||||||
|
|
||||||
// // 只加载最近会话的消息
|
|
||||||
// for (const topic of recentTopics) {
|
|
||||||
// if (topic.messages && topic.messages.length > 0) {
|
|
||||||
// const messages = topic.messages.map((msg) => ({ ...msg }))
|
|
||||||
// messagesByTopic[topic.id] = messages
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return messagesByTopic
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Failed to initialize recent messages:', error)
|
|
||||||
// return {}
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// 新增准备会话消息的函数,实现懒加载机制
|
// 新增准备会话消息的函数,实现懒加载机制
|
||||||
export const prepareTopicMessages = createAsyncThunk(
|
export const prepareTopicMessages = createAsyncThunk(
|
||||||
'messages/prepareTopic',
|
'messages/prepareTopic',
|
||||||
@ -144,7 +119,7 @@ const messagesSlice = createSlice({
|
|||||||
if (message) {
|
if (message) {
|
||||||
Object.assign(message, updates)
|
Object.assign(message, updates)
|
||||||
db.topics.update(topicId, {
|
db.topics.update(topicId, {
|
||||||
messages: topicMessages.map((m) => (m.id === message.id ? cloneDeep(message) : m))
|
messages: topicMessages.map((m) => (m.id === message.id ? cloneDeep(message) : cloneDeep(m)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,19 +185,6 @@ const messagesSlice = createSlice({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// extraReducers: (builder) => {
|
|
||||||
// builder
|
|
||||||
// .addCase(initializeMessagesState.pending, (state) => {
|
|
||||||
// state.error = null
|
|
||||||
// })
|
|
||||||
// .addCase(initializeMessagesState.fulfilled, (state, action) => {
|
|
||||||
// console.log('initializeMessagesState.fulfilled', action.payload)
|
|
||||||
// state.messagesByTopic = action.payload
|
|
||||||
// })
|
|
||||||
// .addCase(initializeMessagesState.rejected, (state, action) => {
|
|
||||||
// state.error = action.error.message || 'Failed to load messages'
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleResponseMessageUpdate = (
|
const handleResponseMessageUpdate = (
|
||||||
|
|||||||
@ -772,6 +772,22 @@ const migrateConfig = {
|
|||||||
'81': (state: RootState) => {
|
'81': (state: RootState) => {
|
||||||
addProvider(state, 'copilot')
|
addProvider(state, 'copilot')
|
||||||
return state
|
return state
|
||||||
|
},
|
||||||
|
'82': (state: RootState) => {
|
||||||
|
const runtimeState = state.runtime as any
|
||||||
|
if (runtimeState?.webdavSync) {
|
||||||
|
state.backup = state.backup || {}
|
||||||
|
state.backup = {
|
||||||
|
...state.backup,
|
||||||
|
webdavSync: {
|
||||||
|
lastSyncTime: runtimeState.webdavSync.lastSyncTime || null,
|
||||||
|
syncing: runtimeState.webdavSync.syncing || false,
|
||||||
|
lastSyncError: runtimeState.webdavSync.lastSyncError || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete runtimeState.webdavSync
|
||||||
|
}
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,12 +11,6 @@ export interface UpdateState {
|
|||||||
available: boolean
|
available: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebDAVSyncState {
|
|
||||||
lastSyncTime: number | null
|
|
||||||
syncing: boolean
|
|
||||||
lastSyncError: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RuntimeState {
|
export interface RuntimeState {
|
||||||
avatar: string
|
avatar: string
|
||||||
generating: boolean
|
generating: boolean
|
||||||
@ -25,7 +19,6 @@ export interface RuntimeState {
|
|||||||
filesPath: string
|
filesPath: string
|
||||||
resourcesPath: string
|
resourcesPath: string
|
||||||
update: UpdateState
|
update: UpdateState
|
||||||
webdavSync: WebDAVSyncState
|
|
||||||
export: ExportState
|
export: ExportState
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,11 +41,6 @@ const initialState: RuntimeState = {
|
|||||||
downloadProgress: 0,
|
downloadProgress: 0,
|
||||||
available: false
|
available: false
|
||||||
},
|
},
|
||||||
webdavSync: {
|
|
||||||
lastSyncTime: null,
|
|
||||||
syncing: false,
|
|
||||||
lastSyncError: null
|
|
||||||
},
|
|
||||||
export: {
|
export: {
|
||||||
isExporting: false
|
isExporting: false
|
||||||
}
|
}
|
||||||
@ -83,9 +71,6 @@ const runtimeSlice = createSlice({
|
|||||||
setUpdateState: (state, action: PayloadAction<Partial<UpdateState>>) => {
|
setUpdateState: (state, action: PayloadAction<Partial<UpdateState>>) => {
|
||||||
state.update = { ...state.update, ...action.payload }
|
state.update = { ...state.update, ...action.payload }
|
||||||
},
|
},
|
||||||
setWebDAVSyncState: (state, action: PayloadAction<Partial<WebDAVSyncState>>) => {
|
|
||||||
state.webdavSync = { ...state.webdavSync, ...action.payload }
|
|
||||||
},
|
|
||||||
setExportState: (state, action: PayloadAction<Partial<ExportState>>) => {
|
setExportState: (state, action: PayloadAction<Partial<ExportState>>) => {
|
||||||
state.export = { ...state.export, ...action.payload }
|
state.export = { ...state.export, ...action.payload }
|
||||||
}
|
}
|
||||||
@ -100,7 +85,6 @@ export const {
|
|||||||
setFilesPath,
|
setFilesPath,
|
||||||
setResourcesPath,
|
setResourcesPath,
|
||||||
setUpdateState,
|
setUpdateState,
|
||||||
setWebDAVSyncState,
|
|
||||||
setExportState
|
setExportState
|
||||||
} = runtimeSlice.actions
|
} = runtimeSlice.actions
|
||||||
|
|
||||||
|
|||||||
@ -218,6 +218,7 @@ export type WebDavConfig = {
|
|||||||
webdavUser: string
|
webdavUser: string
|
||||||
webdavPass: string
|
webdavPass: string
|
||||||
webdavPath: string
|
webdavPath: string
|
||||||
|
fileName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppInfo = {
|
export type AppInfo = {
|
||||||
@ -285,6 +286,8 @@ export type KnowledgeBaseParams = {
|
|||||||
baseURL: string
|
baseURL: string
|
||||||
chunkSize?: number
|
chunkSize?: number
|
||||||
chunkOverlap?: number
|
chunkOverlap?: number
|
||||||
|
rerankApiKey?: string
|
||||||
|
rerankBaseURL?: string
|
||||||
rerankModel?: string
|
rerankModel?: string
|
||||||
rerankModelProvider?: string
|
rerankModelProvider?: string
|
||||||
topN?: number
|
topN?: number
|
||||||
|
|||||||
@ -18,7 +18,10 @@ const supportedAttributes = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
function filterPropertieAttributes(tool: MCPTool, filterNestedObj = false) {
|
function filterPropertieAttributes(tool: MCPTool, filterNestedObj = false) {
|
||||||
const roperties = tool.inputSchema.properties
|
const properties = tool.inputSchema.properties
|
||||||
|
if (!properties) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
const getSubMap = (obj: Record<string, any>, keys: string[]) => {
|
const getSubMap = (obj: Record<string, any>, keys: string[]) => {
|
||||||
const filtered = Object.fromEntries(Object.entries(obj).filter(([key]) => keys.includes(key)))
|
const filtered = Object.fromEntries(Object.entries(obj).filter(([key]) => keys.includes(key)))
|
||||||
|
|
||||||
@ -46,10 +49,10 @@ function filterPropertieAttributes(tool: MCPTool, filterNestedObj = false) {
|
|||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, val] of Object.entries(roperties)) {
|
for (const [key, val] of Object.entries(properties)) {
|
||||||
roperties[key] = getSubMap(val, supportedAttributes)
|
properties[key] = getSubMap(val, supportedAttributes)
|
||||||
}
|
}
|
||||||
return roperties
|
return properties
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mcpToolsToOpenAITools(mcpTools: MCPTool[]): Array<ChatCompletionTool> {
|
export function mcpToolsToOpenAITools(mcpTools: MCPTool[]): Array<ChatCompletionTool> {
|
||||||
|
|||||||
@ -29,7 +29,7 @@ const HomeWindow: FC = () => {
|
|||||||
const textChange = useState(() => {})[1]
|
const textChange = useState(() => {})[1]
|
||||||
const { defaultAssistant } = useDefaultAssistant()
|
const { defaultAssistant } = useDefaultAssistant()
|
||||||
const { defaultModel: model } = useDefaultModel()
|
const { defaultModel: model } = useDefaultModel()
|
||||||
const { language, readClipboardAtStartup } = useSettings()
|
const { language, readClipboardAtStartup, windowStyle, theme } = useSettings()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const inputBarRef = useRef<HTMLDivElement>(null)
|
const inputBarRef = useRef<HTMLDivElement>(null)
|
||||||
const featureMenusRef = useRef<FeatureMenusRef>(null)
|
const featureMenusRef = useRef<FeatureMenusRef>(null)
|
||||||
@ -201,9 +201,24 @@ const HomeWindow: FC = () => {
|
|||||||
}
|
}
|
||||||
}, [route])
|
}, [route])
|
||||||
|
|
||||||
|
const backgroundColor = () => {
|
||||||
|
// ONLY MAC: when transparent style + light theme: use vibrancy effect
|
||||||
|
// because the dark style under mac's vibrancy effect has not been implemented
|
||||||
|
if (
|
||||||
|
isMac &&
|
||||||
|
windowStyle === 'transparent' &&
|
||||||
|
theme === 'light' &&
|
||||||
|
!window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
) {
|
||||||
|
return 'transparent'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'var(--color-background)'
|
||||||
|
}
|
||||||
|
|
||||||
if (['chat', 'summary', 'explanation'].includes(route)) {
|
if (['chat', 'summary', 'explanation'].includes(route)) {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container style={{ backgroundColor: backgroundColor() }}>
|
||||||
{route === 'chat' && (
|
{route === 'chat' && (
|
||||||
<>
|
<>
|
||||||
<InputBar
|
<InputBar
|
||||||
@ -232,7 +247,7 @@ const HomeWindow: FC = () => {
|
|||||||
|
|
||||||
if (route === 'translate') {
|
if (route === 'translate') {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container style={{ backgroundColor: backgroundColor() }}>
|
||||||
<TranslateWindow text={referenceText} />
|
<TranslateWindow text={referenceText} />
|
||||||
<Divider style={{ margin: '10px 0' }} />
|
<Divider style={{ margin: '10px 0' }} />
|
||||||
<Footer route={route} onExit={() => setRoute('home')} />
|
<Footer route={route} onExit={() => setRoute('home')} />
|
||||||
@ -241,7 +256,7 @@ const HomeWindow: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container style={{ backgroundColor: backgroundColor() }}>
|
||||||
<InputBar
|
<InputBar
|
||||||
text={text}
|
text={text}
|
||||||
model={model}
|
model={model}
|
||||||
@ -280,7 +295,6 @@ const Container = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
background-color: ${isMac ? 'transparent' : 'var(--color-background)'};
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const Main = styled.main`
|
const Main = styled.main`
|
||||||
|
|||||||
@ -3411,6 +3411,7 @@ __metadata:
|
|||||||
rollup-plugin-visualizer: "npm:^5.12.0"
|
rollup-plugin-visualizer: "npm:^5.12.0"
|
||||||
sass: "npm:^1.77.2"
|
sass: "npm:^1.77.2"
|
||||||
shiki: "npm:^1.22.2"
|
shiki: "npm:^1.22.2"
|
||||||
|
socks-proxy-agent: "npm:^8.0.3"
|
||||||
string-width: "npm:^7.2.0"
|
string-width: "npm:^7.2.0"
|
||||||
styled-components: "npm:^6.1.11"
|
styled-components: "npm:^6.1.11"
|
||||||
tar: "npm:^7.4.3"
|
tar: "npm:^7.4.3"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user