diff --git a/README.md b/README.md index be9b20db92..733aa511ac 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai - 🔤 AI-powered Translation - 🎯 Drag-and-drop Sorting - 🔌 Mini Program Support +- ⚙️ MCP(Model Context Protocol) Server 5. **Enhanced User Experience**: diff --git a/docs/README.ja.md b/docs/README.ja.md index a6dbadd693..1625ddbb44 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -53,6 +53,7 @@ Cherry Studio は、複数の LLM プロバイダーをサポートするデス - 🔤 AI による翻訳機能 - 🎯 ドラッグ&ドロップによる整理 - 🔌 ミニプログラム対応 +- ⚙️ MCP(モデルコンテキストプロトコル) サービス 5. **優れたユーザー体験**: diff --git a/docs/README.zh.md b/docs/README.zh.md index 0fd38f1fa5..fff32d4b81 100644 --- a/docs/README.zh.md +++ b/docs/README.zh.md @@ -52,6 +52,7 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客 - 🔤 AI 驱动的翻译功能 - 🎯 拖拽排序 - 🔌 小程序支持 +- ⚙️ MCP(模型上下文协议) 服务 5. **优质使用体验**: diff --git a/eslint.config.mjs b/eslint.config.mjs index 72775324c3..a196af52df 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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'] } ]) diff --git a/package.json b/package.json index 1716cc92b0..6dcc69a3cb 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "npx-scope-finder": "^1.2.0", "officeparser": "^4.1.1", "p-queue": "^8.1.0", + "socks-proxy-agent": "^8.0.3", "tar": "^7.4.3", "tokenx": "^0.4.1", "undici": "^7.4.0", diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index a5fca5afc5..34f7d1658e 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -103,7 +103,10 @@ export const textExts = [ '.cxx', // C++ 源文件 '.cppm', // C++20 模块接口文件 '.ipp', // 模板实现文件 - '.ixx' // C++20 模块实现文件 + '.ixx', // C++20 模块实现文件 + '.f90', // Fortran 90 源文件 + '.f', // Fortran 固定格式源代码文件 + '.f03' // Fortran 2003+ 源代码文件 ] export const ZOOM_SHORTCUTS = [ diff --git a/resources/scripts/download.js b/resources/scripts/download.js new file mode 100644 index 0000000000..270f8cbedd --- /dev/null +++ b/resources/scripts/download.js @@ -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} 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 } diff --git a/resources/scripts/install-bun.js b/resources/scripts/install-bun.js index 6087c19118..246128474a 100644 --- a/resources/scripts/install-bun.js +++ b/resources/scripts/install-bun.js @@ -2,8 +2,8 @@ const fs = require('fs') const path = require('path') const os = require('os') const { execSync } = require('child_process') -const https = require('https') const AdmZip = require('adm-zip') +const { downloadWithRedirects } = require('./download') // Base URL for downloading bun binaries 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' } -/** - * 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} - */ -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 * @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux') diff --git a/resources/scripts/install-uv.js b/resources/scripts/install-uv.js index 773ff24c64..0b3dfe3a33 100644 --- a/resources/scripts/install-uv.js +++ b/resources/scripts/install-uv.js @@ -2,9 +2,9 @@ const fs = require('fs') const path = require('path') const os = require('os') const { execSync } = require('child_process') -const https = require('https') const tar = require('tar') const AdmZip = require('adm-zip') +const { downloadWithRedirects } = require('./download') // Base URL for downloading uv binaries 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' } -/** - * 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} - */ -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 * @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux') diff --git a/src/main/index.ts b/src/main/index.ts index 19a121ad46..907ef2b649 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,6 +1,6 @@ import { electronApp, optimizer } from '@electron-toolkit/utils' import { replaceDevtoolsFont } from '@main/utils/windowUtil' -import { app } from 'electron' +import { app, ipcMain } from 'electron' import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer' import { registerIpc } from './ipc' @@ -44,6 +44,9 @@ if (!app.requestSingleInstanceLock()) { .then((name) => console.log(`Added Extension: ${name}`)) .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 diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 3eca63c014..aadff47ac0 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -85,8 +85,21 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { }) // theme - ipcMain.handle('app:set-theme', (_, theme: ThemeMode) => { + ipcMain.handle('app:set-theme', (event, theme: ThemeMode) => { + if (theme === configManager.getTheme()) return + 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(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:backupToWebdav', backupManager.backupToWebdav) ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav) + ipcMain.handle('backup:listWebdavFiles', backupManager.listWebdavFiles) // file ipcMain.handle('file:open', fileManager.open) diff --git a/src/main/reranker/BaseReranker.ts b/src/main/reranker/BaseReranker.ts index 469ed2e62a..d31bb40b2d 100644 --- a/src/main/reranker/BaseReranker.ts +++ b/src/main/reranker/BaseReranker.ts @@ -13,7 +13,7 @@ export default abstract class BaseReranker { public defaultHeaders() { return { - Authorization: `Bearer ${this.base.apiKey}`, + Authorization: `Bearer ${this.base.rerankApiKey}`, 'Content-Type': 'application/json' } } diff --git a/src/main/reranker/JinaReranker.ts b/src/main/reranker/JinaReranker.ts new file mode 100644 index 0000000000..dbee063cc6 --- /dev/null +++ b/src/main/reranker/JinaReranker.ts @@ -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 => { + 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 + } + } +} diff --git a/src/main/reranker/RerankerFactory.ts b/src/main/reranker/RerankerFactory.ts index 2c15fe6cc7..0c2e8d7dd3 100644 --- a/src/main/reranker/RerankerFactory.ts +++ b/src/main/reranker/RerankerFactory.ts @@ -2,12 +2,15 @@ import { KnowledgeBaseParams } from '@types' import BaseReranker from './BaseReranker' import DefaultReranker from './DefaultReranker' +import JinaReranker from './JinaReranker' import SiliconFlowReranker from './SiliconFlowReranker' export default class RerankerFactory { static create(base: KnowledgeBaseParams): BaseReranker { if (base.rerankModelProvider === 'silicon') { return new SiliconFlowReranker(base) + } else if (base.rerankModelProvider === 'jina') { + return new JinaReranker(base) } return new DefaultReranker(base) } diff --git a/src/main/reranker/SiliconFlowReranker.ts b/src/main/reranker/SiliconFlowReranker.ts index 8fa3de35a1..ee82362e20 100644 --- a/src/main/reranker/SiliconFlowReranker.ts +++ b/src/main/reranker/SiliconFlowReranker.ts @@ -10,37 +10,41 @@ export default class SiliconFlowReranker extends BaseReranker { } public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise => { - 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( - url, - { - model: this.base.rerankModel, - query, - documents: searchResults.map((doc) => doc.pageContent), - top_n: this.base.topN, - max_chunks_per_doc: this.base.chunkSize, - overlap_tokens: this.base.chunkOverlap - }, - { - headers: this.defaultHeaders() - } - ) + const requestBody = { + model: this.base.rerankModel, + query, + documents: searchResults.map((doc) => doc.pageContent), + top_n: this.base.topN, + max_chunks_per_doc: this.base.chunkSize, + overlap_tokens: this.base.chunkOverlap + } - const rerankResults = data.results - const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0])) + try { + const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() }) - return searchResults - .map((doc: ExtractChunkData, index: number) => { - const score = resultMap.get(index) - if (score === undefined) return undefined + const rerankResults = data.results + const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0])) - return { - ...doc, - score - } - }) - .filter((doc): doc is ExtractChunkData => doc !== undefined) - .sort((a, b) => b.score - a.score) + 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('SiliconFlow Reranker API 错误:', error) + throw error + } } } diff --git a/src/main/services/BackupManager.ts b/src/main/services/BackupManager.ts index cbccdc90a1..92b5ef6549 100644 --- a/src/main/services/BackupManager.ts +++ b/src/main/services/BackupManager.ts @@ -5,6 +5,7 @@ import { app } from 'electron' import Logger from 'electron-log' import * as fs from 'fs-extra' import * as path from 'path' +import { createClient, FileStat } from 'webdav' import WebDav from './WebDav' import { windowService } from './WindowService' @@ -18,6 +19,7 @@ class BackupManager { this.restore = this.restore.bind(this) this.backupToWebdav = this.backupToWebdav.bind(this) this.restoreFromWebdav = this.restoreFromWebdav.bind(this) + this.listWebdavFiles = this.listWebdavFiles.bind(this) } private async setWritableRecursive(dirPath: string): Promise { @@ -117,10 +119,10 @@ class BackupManager { await fs.remove(this.tempDir) onProgress({ stage: 'completed', progress: 100, total: 100 }) - Logger.log('Backup completed successfully') + Logger.log('[BackupManager] Backup completed successfully') return backupedFilePath } catch (error) { - Logger.error('Backup failed:', error) + Logger.error('[BackupManager] Backup failed:', error) throw error } } @@ -186,7 +188,7 @@ class BackupManager { } 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 webdavClient = new WebDav(webdavConfig) return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), { @@ -195,18 +197,48 @@ class BackupManager { } 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 retrievedFile = await webdavClient.getFileContents(filename) - const backupedFilePath = path.join(this.backupDir, filename) + try { + const retrievedFile = await webdavClient.getFileContents(filename) + const backupedFilePath = path.join(this.backupDir, filename) - if (!fs.existsSync(this.backupDir)) { - fs.mkdirSync(this.backupDir, { recursive: true }) + if (!fs.existsSync(this.backupDir)) { + 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 { diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 7a20ae202b..b73c725ed7 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -6,7 +6,12 @@ import { AppInfo, FileType, KnowledgeBaseParams, KnowledgeItem, LanguageVarious, import type { LoaderReturn } from '@shared/config/types' import type { OpenDialogOptions } from 'electron' import type { UpdateInfo } from 'electron-updater' -import { Readable } from 'stream' + +interface BackupFile { + fileName: string + modifiedTime: string + size: number +} declare global { interface Window { @@ -24,6 +29,9 @@ declare global { minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void reload: () => void clearCache: () => Promise<{ success: boolean; error?: string }> + system: { + getDeviceType: () => Promise<'mac' | 'windows' | 'linux'> + } zip: { compress: (text: string) => Promise decompress: (text: Buffer) => Promise @@ -33,6 +41,7 @@ declare global { restore: (backupPath: string) => Promise backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise + listWebdavFiles: (webdavConfig: WebDavConfig) => Promise } file: { select: (options?: OpenDialogOptions) => Promise @@ -68,8 +77,8 @@ declare global { update: (shortcuts: Shortcut[]) => Promise } knowledgeBase: { - create: ({ id, model, apiKey, baseURL }: KnowledgeBaseParams) => Promise - reset: ({ base }: { base: KnowledgeBaseParams }) => Promise + create: (base: KnowledgeBaseParams) => Promise + reset: (base: KnowledgeBaseParams) => Promise delete: (id: string) => Promise add: ({ base, diff --git a/src/preload/index.ts b/src/preload/index.ts index 6d6b167217..4fbc8a6571 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -17,6 +17,9 @@ const api = { openWebsite: (url: string) => ipcRenderer.invoke('open:website', url), minApp: (url: string) => ipcRenderer.invoke('minapp', url), clearCache: () => ipcRenderer.invoke('app:clear-cache'), + system: { + getDeviceType: () => ipcRenderer.invoke('system:getDeviceType') + }, zip: { compress: (text: string) => ipcRenderer.invoke('zip:compress', text), decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text) @@ -27,7 +30,8 @@ const api = { restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath), backupToWebdav: (data: string, webdavConfig: 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: { list: () => ipcRenderer.invoke('nodeapp:list'), @@ -92,9 +96,8 @@ const api = { update: (shortcuts: Shortcut[]) => ipcRenderer.invoke('shortcuts:update', shortcuts) }, knowledgeBase: { - create: ({ id, model, apiKey, baseURL }: KnowledgeBaseParams) => - ipcRenderer.invoke('knowledge-base:create', { id, model, apiKey, baseURL }), - reset: ({ base }: { base: KnowledgeBaseParams }) => ipcRenderer.invoke('knowledge-base:reset', { base }), + create: (base: KnowledgeBaseParams) => ipcRenderer.invoke('knowledge-base:create', base), + reset: (base: KnowledgeBaseParams) => ipcRenderer.invoke('knowledge-base:reset', base), delete: (id: string) => ipcRenderer.invoke('knowledge-base:delete', id), add: ({ base, diff --git a/src/renderer/src/context/ThemeProvider.tsx b/src/renderer/src/context/ThemeProvider.tsx index e9242b3b1d..95478b35d9 100644 --- a/src/renderer/src/context/ThemeProvider.tsx +++ b/src/renderer/src/context/ThemeProvider.tsx @@ -45,7 +45,15 @@ export const ThemeProvider: React.FC = ({ children, defaultT useEffect(() => { 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 {children} } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 8bd67e8473..639281c987 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -237,7 +237,8 @@ "copied": "Copied", "confirm": "Confirm", "more": "More", - "advanced_settings": "Advanced Settings" + "advanced_settings": "Advanced Settings", + "expand": "Expand" }, "docs": { "title": "Docs" @@ -484,6 +485,10 @@ "upgrade.success.title": "Upgrade successfully", "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.", + "tools": { + "invoking": "Invoking", + "completed": "Completed" + }, "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." }, @@ -766,6 +771,12 @@ "password": "WebDAV Password", "path": "WebDAV Path", "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.content": "Restore from WebDAV will overwrite the current data, continue?", "restore.title": "Restore from WebDAV", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index e6ddc746dc..9a5678c747 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -237,7 +237,8 @@ "copied": "コピーされました", "confirm": "確認", "more": "もっと", - "advanced_settings": "詳細設定" + "advanced_settings": "詳細設定", + "expand": "展開" }, "docs": { "title": "ドキュメント" @@ -483,7 +484,11 @@ "upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください", "upgrade.success.title": "アップグレードに成功しました", "warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! ", - "warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。" + "warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。", + "tools": { + "invoking": "呼び出し中", + "completed": "完了" + } }, "minapp": { "sidebar.add.title": "サイドバーに追加", @@ -763,7 +768,13 @@ "syncError": "バックアップエラー", "syncStatus": "バックアップ状態", "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": { "check": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index d364031745..1624ef18a8 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -237,7 +237,8 @@ "confirm": "Подтверждение", "copied": "Скопировано", "more": "Ещё", - "advanced_settings": "Дополнительные настройки" + "advanced_settings": "Дополнительные настройки", + "expand": "Развернуть" }, "docs": { "title": "Документация" @@ -489,7 +490,11 @@ "upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления", "upgrade.success.title": "Обновление успешно", "warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!", - "warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова." + "warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова.", + "tools": { + "invoking": "Вызов", + "completed": "Завершено" + } }, "minapp": { "sidebar.add.title": "Добавить в боковую панель", @@ -763,7 +768,13 @@ "syncError": "Ошибка резервного копирования", "syncStatus": "Статус резервного копирования", "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": { "check": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index d173e5bb5f..a768c7cf89 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -237,7 +237,8 @@ "warning": "警告", "you": "用户", "more": "更多", - "advanced_settings": "高级设置" + "advanced_settings": "高级设置", + "expand": "展开" }, "docs": { "title": "帮助文档" @@ -483,7 +484,11 @@ "upgrade.success.content": "重启用以完成升级", "upgrade.success.title": "升级成功", "warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!", - "warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试" + "warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试", + "tools": { + "invoking": "调用中", + "completed": "已完成" + } }, "minapp": { "sidebar.add.title": "添加到侧边栏", @@ -757,6 +762,12 @@ "password": "WebDAV 密码", "path": "WebDAV 路径", "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.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?", "restore.title": "从 WebDAV 恢复", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index e9c2f5dec5..0e48c2bc27 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -237,7 +237,8 @@ "copied": "已複製", "confirm": "確認", "more": "更多", - "advanced_settings": "進階設定" + "advanced_settings": "進階設定", + "expand": "展開" }, "docs": { "title": "說明文件" @@ -483,7 +484,11 @@ "upgrade.success.content": "請重新啟動程式以完成升級", "upgrade.success.title": "升級成功", "warn.notion.exporting": "正在匯出到 Notion,請勿重複請求匯出!", - "warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試" + "warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試", + "tools": { + "invoking": "調用中", + "completed": "已完成" + } }, "minapp": { "sidebar.add.title": "新增到側邊欄", @@ -763,7 +768,13 @@ "syncError": "備份錯誤", "syncStatus": "備份狀態", "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": { "check": { diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index 03374989fa..1894eca41c 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -70,7 +70,7 @@ const MessageMenubar: FC = (props) => { const onCopy = useCallback( (e: React.MouseEvent) => { 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' }) setCopied(true) setTimeout(() => setCopied(false), 2000) diff --git a/src/renderer/src/pages/home/Messages/MessageTools.tsx b/src/renderer/src/pages/home/Messages/MessageTools.tsx index 73d73cfc4b..22b638a917 100644 --- a/src/renderer/src/pages/home/Messages/MessageTools.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTools.tsx @@ -60,7 +60,7 @@ const MessageTools: FC = ({ message }) => { {tool.name} - {isInvoking ? t('tools.invoking') : t('tools.completed')} + {isInvoking ? t('message.tools.invoking') : t('message.tools.completed')} {isInvoking && } {isDone && } diff --git a/src/renderer/src/pages/knowledge/components/KnowledgeSettingsPopup.tsx b/src/renderer/src/pages/knowledge/components/KnowledgeSettingsPopup.tsx index e1c1e8c186..56c00b66f9 100644 --- a/src/renderer/src/pages/knowledge/components/KnowledgeSettingsPopup.tsx +++ b/src/renderer/src/pages/knowledge/components/KnowledgeSettingsPopup.tsx @@ -142,10 +142,10 @@ const PopupContainer: React.FC = ({ base: _base, resolve }) => { name="rerankModel" label={t('models.rerank_model')} tooltip={{ title: t('models.rerank_model_tooltip'), placement: 'right' }} + initialValue={getModelUniqId(base.rerankModel) || undefined} rules={[{ required: false, message: t('message.error.enter.model') }]}> setCustomFileName(e.target.value)} + placeholder={t('settings.data.webdav.backup.modal.filename.placeholder')} + /> + + + setIsRestoreModalVisible(false)} + okButtonProps={{ loading: restoring }} + width={600}> +
+