From 961ee2232780aefa54a2be3ab79c0ec8a0b825dd Mon Sep 17 00:00:00 2001 From: Kejiang Ma Date: Mon, 29 Sep 2025 18:36:54 +0800 Subject: [PATCH 01/22] feat: add new provider intel OVMS(openvino model server) (#9853) * add new provider: OVMS(openvino model server) Signed-off-by: Ma, Kejiang * remove useless comments * add note: support windows only * fix eslint error; add migrate for ovms provider Signed-off-by: Ma, Kejiang * fix ci error after rebase Signed-off-by: Ma, Kejiang * modifications base on reviewers' comments Signed-off-by: Ma, Kejiang * show intel-ovms provider only on windows and intel cpu Signed-off-by: Ma, Kejiang * complete i18n for intel ovms Signed-off-by: Ma, Kejiang * update ovms 2025.3; apply patch for model qwen3-8b on local Signed-off-by: Ma, Kejiang * fix lint issues Signed-off-by: Ma, Kejiang * fix issues for format, type checking Signed-off-by: Ma, Kejiang * remove test code Signed-off-by: Ma, Kejiang * fix issues after rebase Signed-off-by: Ma, Kejiang --------- Signed-off-by: Ma, Kejiang --- packages/shared/IpcChannel.ts | 11 + resources/scripts/download.js | 39 +- resources/scripts/install-ovms.js | 177 ++++++ src/main/ipc.ts | 15 + src/main/services/OvmsManager.ts | 586 ++++++++++++++++++ src/preload/index.ts | 14 +- .../aiCore/legacy/clients/ApiClientFactory.ts | 7 + .../aiCore/legacy/clients/ovms/OVMSClient.ts | 56 ++ .../src/assets/images/providers/intel.png | Bin 0 -> 3706 bytes src/renderer/src/config/models/default.ts | 1 + src/renderer/src/config/providers.ts | 22 + src/renderer/src/i18n/label.ts | 1 + src/renderer/src/i18n/locales/en-us.json | 53 ++ src/renderer/src/i18n/locales/zh-cn.json | 53 ++ src/renderer/src/i18n/locales/zh-tw.json | 53 ++ src/renderer/src/i18n/translate/el-gr.json | 53 ++ src/renderer/src/i18n/translate/es-es.json | 53 ++ src/renderer/src/i18n/translate/fr-fr.json | 53 ++ src/renderer/src/i18n/translate/ja-jp.json | 53 ++ src/renderer/src/i18n/translate/pt-pt.json | 53 ++ src/renderer/src/i18n/translate/ru-ru.json | 53 ++ .../ModelList/DownloadOVMSModelPopup.tsx | 353 +++++++++++ .../ProviderSettings/ModelList/ModelList.tsx | 18 +- .../ProviderSettings/OVMSSettings.tsx | 170 +++++ .../ProviderSettings/ProviderList.tsx | 6 + .../ProviderSettings/ProviderSetting.tsx | 2 + src/renderer/src/store/migrate.ts | 9 + src/renderer/src/types/index.ts | 1 + 28 files changed, 1960 insertions(+), 5 deletions(-) create mode 100644 resources/scripts/install-ovms.js create mode 100644 src/main/services/OvmsManager.ts create mode 100644 src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts create mode 100644 src/renderer/src/assets/images/providers/intel.png create mode 100644 src/renderer/src/pages/settings/ProviderSettings/ModelList/DownloadOVMSModelPopup.tsx create mode 100644 src/renderer/src/pages/settings/ProviderSettings/OVMSSettings.tsx diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 0eb0dd2797..1481d5acad 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -34,6 +34,7 @@ export enum IpcChannel { App_GetBinaryPath = 'app:get-binary-path', App_InstallUvBinary = 'app:install-uv-binary', App_InstallBunBinary = 'app:install-bun-binary', + App_InstallOvmsBinary = 'app:install-ovms-binary', App_LogToMain = 'app:log-to-main', App_SaveData = 'app:save-data', App_GetDiskInfo = 'app:get-disk-info', @@ -220,6 +221,7 @@ export enum IpcChannel { // system System_GetDeviceType = 'system:getDeviceType', System_GetHostname = 'system:getHostname', + System_GetCpuName = 'system:getCpuName', // DevTools System_ToggleDevTools = 'system:toggleDevTools', @@ -330,6 +332,15 @@ export enum IpcChannel { // OCR OCR_ocr = 'ocr:ocr', + // OVMS + Ovms_AddModel = 'ovms:add-model', + Ovms_StopAddModel = 'ovms:stop-addmodel', + Ovms_GetModels = 'ovms:get-models', + Ovms_IsRunning = 'ovms:is-running', + Ovms_GetStatus = 'ovms:get-status', + Ovms_RunOVMS = 'ovms:run-ovms', + Ovms_StopOVMS = 'ovms:stop-ovms', + // CherryAI Cherryai_GetSignature = 'cherryai:get-signature' } diff --git a/resources/scripts/download.js b/resources/scripts/download.js index 2e9d83a9e7..3a5766d0b2 100644 --- a/resources/scripts/download.js +++ b/resources/scripts/download.js @@ -1,5 +1,7 @@ const https = require('https') const fs = require('fs') +const path = require('path') +const { execSync } = require('child_process') /** * Downloads a file from a URL with redirect handling @@ -32,4 +34,39 @@ async function downloadWithRedirects(url, destinationPath) { }) } -module.exports = { downloadWithRedirects } +/** + * Downloads a file using PowerShell Invoke-WebRequest command + * @param {string} url The URL to download from + * @param {string} destinationPath The path to save the file to + * @returns {Promise} Promise that resolves to true if download succeeds + */ +async function downloadWithPowerShell(url, destinationPath) { + return new Promise((resolve, reject) => { + try { + // Only support windows platform for PowerShell download + if (process.platform !== 'win32') { + return reject(new Error('PowerShell download is only supported on Windows')) + } + + const outputDir = path.dirname(destinationPath) + fs.mkdirSync(outputDir, { recursive: true }) + + // PowerShell command to download the file with progress disabled for faster download + const psCommand = `powershell -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest '${url}' -OutFile '${destinationPath}'"` + + console.log(`Downloading with PowerShell: ${url}`) + execSync(psCommand, { stdio: 'inherit' }) + + if (fs.existsSync(destinationPath)) { + console.log(`Download completed: ${destinationPath}`) + resolve(true) + } else { + reject(new Error('Download failed: File not found after download')) + } + } catch (error) { + reject(new Error(`PowerShell download failed: ${error.message}`)) + } + }) +} + +module.exports = { downloadWithRedirects, downloadWithPowerShell } diff --git a/resources/scripts/install-ovms.js b/resources/scripts/install-ovms.js new file mode 100644 index 0000000000..57710e43f6 --- /dev/null +++ b/resources/scripts/install-ovms.js @@ -0,0 +1,177 @@ +const fs = require('fs') +const path = require('path') +const os = require('os') +const { execSync } = require('child_process') +const { downloadWithPowerShell } = require('./download') + +// Base URL for downloading OVMS binaries +const OVMS_PKG_NAME = 'ovms250911.zip' +const OVMS_RELEASE_BASE_URL = [`https://gitcode.com/gcw_ggDjjkY3/kjfile/releases/download/download/${OVMS_PKG_NAME}`] + +/** + * Downloads and extracts the OVMS binary for the specified platform + */ +async function downloadOvmsBinary() { + // Create output directory structure - OVMS goes into its own subdirectory + const csDir = path.join(os.homedir(), '.cherrystudio') + + // Ensure directories exist + fs.mkdirSync(csDir, { recursive: true }) + + const csOvmsDir = path.join(csDir, 'ovms') + // Delete existing OVMS directory if it exists + if (fs.existsSync(csOvmsDir)) { + fs.rmSync(csOvmsDir, { recursive: true }) + } + + const tempdir = os.tmpdir() + const tempFilename = path.join(tempdir, 'ovms.zip') + + // Try each URL until one succeeds + let downloadSuccess = false + let lastError = null + + for (let i = 0; i < OVMS_RELEASE_BASE_URL.length; i++) { + const downloadUrl = OVMS_RELEASE_BASE_URL[i] + console.log(`Attempting download from URL ${i + 1}/${OVMS_RELEASE_BASE_URL.length}: ${downloadUrl}`) + + try { + console.log(`Downloading OVMS from ${downloadUrl} to ${tempFilename}...`) + + // Try PowerShell download first, fallback to Node.js download if it fails + await downloadWithPowerShell(downloadUrl, tempFilename) + + // If we get here, download was successful + downloadSuccess = true + console.log(`Successfully downloaded from: ${downloadUrl}`) + break + } catch (error) { + console.warn(`Download failed from ${downloadUrl}: ${error.message}`) + lastError = error + + // Clean up failed download file if it exists + if (fs.existsSync(tempFilename)) { + try { + fs.unlinkSync(tempFilename) + } catch (cleanupError) { + console.warn(`Failed to clean up temporary file: ${cleanupError.message}`) + } + } + + // Continue to next URL if this one failed + if (i < OVMS_RELEASE_BASE_URL.length - 1) { + console.log(`Trying next URL...`) + } + } + } + + // Check if any download succeeded + if (!downloadSuccess) { + console.error(`All download URLs failed. Last error: ${lastError?.message || 'Unknown error'}`) + return 103 + } + + try { + console.log(`Extracting to ${csDir}...`) + + // Use tar.exe to extract the ZIP file + console.log(`Extracting OVMS to ${csDir}...`) + execSync(`tar -xf ${tempFilename} -C ${csDir}`, { stdio: 'inherit' }) + console.log(`OVMS extracted to ${csDir}`) + + // Clean up temporary file + fs.unlinkSync(tempFilename) + console.log(`Installation directory: ${csDir}`) + } catch (error) { + console.error(`Error installing OVMS: ${error.message}`) + if (fs.existsSync(tempFilename)) { + fs.unlinkSync(tempFilename) + } + + // Check if ovmsDir is empty and remove it if so + try { + const ovmsDir = path.join(csDir, 'ovms') + const files = fs.readdirSync(ovmsDir) + if (files.length === 0) { + fs.rmSync(ovmsDir, { recursive: true }) + console.log(`Removed empty directory: ${ovmsDir}`) + } + } catch (cleanupError) { + console.warn(`Warning: Failed to clean up directory: ${cleanupError.message}`) + return 105 + } + + return 104 + } + + return 0 +} + +/** + * Get the CPU Name and ID + */ +function getCpuInfo() { + const cpuInfo = { + name: '', + id: '' + } + + // Use PowerShell to get CPU information + try { + const psCommand = `powershell -Command "Get-CimInstance -ClassName Win32_Processor | Select-Object Name, DeviceID | ConvertTo-Json"` + const psOutput = execSync(psCommand).toString() + const cpuData = JSON.parse(psOutput) + + if (Array.isArray(cpuData)) { + cpuInfo.name = cpuData[0].Name || '' + cpuInfo.id = cpuData[0].DeviceID || '' + } else { + cpuInfo.name = cpuData.Name || '' + cpuInfo.id = cpuData.DeviceID || '' + } + } catch (error) { + console.error(`Failed to get CPU info: ${error.message}`) + } + + return cpuInfo +} + +/** + * Main function to install OVMS + */ +async function installOvms() { + const platform = os.platform() + console.log(`Detected platform: ${platform}`) + + const cpuName = getCpuInfo().name + console.log(`CPU Name: ${cpuName}`) + + // Check if CPU name contains "Ultra" + if (!cpuName.toLowerCase().includes('intel') || !cpuName.toLowerCase().includes('ultra')) { + console.error('OVMS installation requires an Intel(R) Core(TM) Ultra CPU.') + return 101 + } + + // only support windows + if (platform !== 'win32') { + console.error('OVMS installation is only supported on Windows.') + return 102 + } + + return await downloadOvmsBinary() +} + +// Run the installation +installOvms() + .then((retcode) => { + if (retcode === 0) { + console.log('OVMS installation successful') + } else { + console.error('OVMS installation failed') + } + process.exit(retcode) + }) + .catch((error) => { + console.error('OVMS installation failed:', error) + process.exit(100) + }) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 0ef9cca2fa..275b3df4f9 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -35,6 +35,7 @@ import NotificationService from './services/NotificationService' import * as NutstoreService from './services/NutstoreService' import ObsidianVaultService from './services/ObsidianVaultService' import { ocrService } from './services/ocr/OcrService' +import OvmsManager from './services/OvmsManager' import { proxyManager } from './services/ProxyManager' import { pythonService } from './services/PythonService' import { FileServiceManager } from './services/remotefile/FileServiceManager' @@ -81,6 +82,7 @@ const obsidianVaultService = new ObsidianVaultService() const vertexAIService = VertexAIService.getInstance() const memoryService = MemoryService.getInstance() const dxtService = new DxtService() +const ovmsManager = new OvmsManager() export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { const appUpdater = new AppUpdater() @@ -432,6 +434,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { // system ipcMain.handle(IpcChannel.System_GetDeviceType, () => (isMac ? 'mac' : isWin ? 'windows' : 'linux')) ipcMain.handle(IpcChannel.System_GetHostname, () => require('os').hostname()) + ipcMain.handle(IpcChannel.System_GetCpuName, () => require('os').cpus()[0].model) ipcMain.handle(IpcChannel.System_ToggleDevTools, (e) => { const win = BrowserWindow.fromWebContents(e.sender) win && win.webContents.toggleDevTools() @@ -710,6 +713,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name)) ipcMain.handle(IpcChannel.App_InstallUvBinary, () => runInstallScript('install-uv.js')) ipcMain.handle(IpcChannel.App_InstallBunBinary, () => runInstallScript('install-bun.js')) + ipcMain.handle(IpcChannel.App_InstallOvmsBinary, () => runInstallScript('install-ovms.js')) //copilot ipcMain.handle(IpcChannel.Copilot_GetAuthMessage, CopilotService.getAuthMessage.bind(CopilotService)) @@ -841,6 +845,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ocrService.ocr(file, provider) ) + // OVMS + ipcMain.handle(IpcChannel.Ovms_AddModel, (_, modelName: string, modelId: string, modelSource: string, task: string) => + ovmsManager.addModel(modelName, modelId, modelSource, task) + ) + ipcMain.handle(IpcChannel.Ovms_StopAddModel, () => ovmsManager.stopAddModel()) + ipcMain.handle(IpcChannel.Ovms_GetModels, () => ovmsManager.getModels()) + ipcMain.handle(IpcChannel.Ovms_IsRunning, () => ovmsManager.initializeOvms()) + ipcMain.handle(IpcChannel.Ovms_GetStatus, () => ovmsManager.getOvmsStatus()) + ipcMain.handle(IpcChannel.Ovms_RunOVMS, () => ovmsManager.runOvms()) + ipcMain.handle(IpcChannel.Ovms_StopOVMS, () => ovmsManager.stopOvms()) + // CherryAI ipcMain.handle(IpcChannel.Cherryai_GetSignature, (_, params) => generateSignature(params)) } diff --git a/src/main/services/OvmsManager.ts b/src/main/services/OvmsManager.ts new file mode 100644 index 0000000000..f319200ac3 --- /dev/null +++ b/src/main/services/OvmsManager.ts @@ -0,0 +1,586 @@ +import { exec } from 'node:child_process' +import { homedir } from 'node:os' +import { promisify } from 'node:util' + +import { loggerService } from '@logger' +import * as fs from 'fs-extra' +import * as path from 'path' + +const logger = loggerService.withContext('OvmsManager') + +const execAsync = promisify(exec) + +interface OvmsProcess { + pid: number + path: string + workingDirectory: string +} + +interface ModelConfig { + name: string + base_path: string +} + +interface OvmsConfig { + mediapipe_config_list: ModelConfig[] +} + +class OvmsManager { + private ovms: OvmsProcess | null = null + + /** + * Recursively terminate a process and all its child processes + * @param pid Process ID to terminate + * @returns Promise<{ success: boolean; message?: string }> + */ + private async terminalProcess(pid: number): Promise<{ success: boolean; message?: string }> { + try { + // Check if the process is running + const processCheckCommand = `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object Id | ConvertTo-Json` + const { stdout: processStdout } = await execAsync(`powershell -Command "${processCheckCommand}"`) + + if (!processStdout.trim()) { + logger.info(`Process with PID ${pid} is not running`) + return { success: true, message: `Process with PID ${pid} is not running` } + } + + // Find child processes + const childProcessCommand = `Get-WmiObject -Class Win32_Process | Where-Object { $_.ParentProcessId -eq ${pid} } | Select-Object ProcessId | ConvertTo-Json` + const { stdout: childStdout } = await execAsync(`powershell -Command "${childProcessCommand}"`) + + // If there are child processes, terminate them first + if (childStdout.trim()) { + const childProcesses = JSON.parse(childStdout) + const childList = Array.isArray(childProcesses) ? childProcesses : [childProcesses] + + logger.info(`Found ${childList.length} child processes for PID ${pid}`) + + // Recursively terminate each child process + for (const childProcess of childList) { + const childPid = childProcess.ProcessId + logger.info(`Terminating child process PID: ${childPid}`) + await this.terminalProcess(childPid) + } + } else { + logger.info(`No child processes found for PID ${pid}`) + } + + // Finally, terminate the parent process + const killCommand = `Stop-Process -Id ${pid} -Force -ErrorAction SilentlyContinue` + await execAsync(`powershell -Command "${killCommand}"`) + logger.info(`Terminated process with PID: ${pid}`) + + // Wait for the process to disappear with 5-second timeout + const timeout = 5000 // 5 seconds + const startTime = Date.now() + + while (Date.now() - startTime < timeout) { + const checkCommand = `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object Id | ConvertTo-Json` + const { stdout: checkStdout } = await execAsync(`powershell -Command "${checkCommand}"`) + + if (!checkStdout.trim()) { + logger.info(`Process with PID ${pid} has disappeared`) + return { success: true, message: `Process ${pid} and all child processes terminated successfully` } + } + + // Wait 300ms before checking again + await new Promise((resolve) => setTimeout(resolve, 300)) + } + + logger.warn(`Process with PID ${pid} did not disappear within timeout`) + return { success: false, message: `Process ${pid} did not disappear within 5 seconds` } + } catch (error) { + logger.error(`Failed to terminate process ${pid}:`, error as Error) + return { success: false, message: `Failed to terminate process ${pid}` } + } + } + + /** + * Stop OVMS process if it's running + * @returns Promise<{ success: boolean; message?: string }> + */ + public async stopOvms(): Promise<{ success: boolean; message?: string }> { + try { + // Check if OVMS process is running + const psCommand = `Get-Process -Name "ovms" -ErrorAction SilentlyContinue | Select-Object Id, Path | ConvertTo-Json` + const { stdout } = await execAsync(`powershell -Command "${psCommand}"`) + + if (!stdout.trim()) { + logger.info('OVMS process is not running') + return { success: true, message: 'OVMS process is not running' } + } + + const processes = JSON.parse(stdout) + const processList = Array.isArray(processes) ? processes : [processes] + + if (processList.length === 0) { + logger.info('OVMS process is not running') + return { success: true, message: 'OVMS process is not running' } + } + + // Terminate all OVMS processes using terminalProcess + for (const process of processList) { + const result = await this.terminalProcess(process.Id) + if (!result.success) { + logger.error(`Failed to terminate OVMS process with PID: ${process.Id}, ${result.message}`) + return { success: false, message: `Failed to terminate OVMS process: ${result.message}` } + } + logger.info(`Terminated OVMS process with PID: ${process.Id}`) + } + + // Reset the ovms instance + this.ovms = null + + logger.info('OVMS process stopped successfully') + return { success: true, message: 'OVMS process stopped successfully' } + } catch (error) { + logger.error(`Failed to stop OVMS process: ${error}`) + return { success: false, message: 'Failed to stop OVMS process' } + } + } + + /** + * Run OVMS by ensuring config.json exists and executing run.bat + * @returns Promise<{ success: boolean; message?: string }> + */ + public async runOvms(): Promise<{ success: boolean; message?: string }> { + const homeDir = homedir() + const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const configPath = path.join(ovmsDir, 'models', 'config.json') + const runBatPath = path.join(ovmsDir, 'run.bat') + + try { + // Check if config.json exists, if not create it with default content + if (!(await fs.pathExists(configPath))) { + logger.info(`Config file does not exist, creating: ${configPath}`) + + // Ensure the models directory exists + await fs.ensureDir(path.dirname(configPath)) + + // Create config.json with default content + const defaultConfig = { + mediapipe_config_list: [], + model_config_list: [] + } + + await fs.writeJson(configPath, defaultConfig, { spaces: 2 }) + logger.info(`Config file created: ${configPath}`) + } + + // Check if run.bat exists + if (!(await fs.pathExists(runBatPath))) { + logger.error(`run.bat not found at: ${runBatPath}`) + return { success: false, message: 'run.bat not found' } + } + + // Run run.bat without waiting for it to complete + logger.info(`Starting OVMS with run.bat: ${runBatPath}`) + exec(`"${runBatPath}"`, { cwd: ovmsDir }, (error) => { + if (error) { + logger.error(`Error running run.bat: ${error}`) + } + }) + + logger.info('OVMS started successfully') + return { success: true } + } catch (error) { + logger.error(`Failed to run OVMS: ${error}`) + return { success: false, message: 'Failed to run OVMS' } + } + } + + /** + * Get OVMS status - checks installation and running status + * @returns 'not-installed' | 'not-running' | 'running' + */ + public async getOvmsStatus(): Promise<'not-installed' | 'not-running' | 'running'> { + const homeDir = homedir() + const ovmsPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'ovms.exe') + + try { + // Check if OVMS executable exists + if (!(await fs.pathExists(ovmsPath))) { + logger.info(`OVMS executable not found at: ${ovmsPath}`) + return 'not-installed' + } + + // Check if OVMS process is running + //const psCommand = `Get-Process -Name "ovms" -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq "${ovmsPath.replace(/\\/g, '\\\\')}" } | Select-Object Id | ConvertTo-Json`; + //const { stdout } = await execAsync(`powershell -Command "${psCommand}"`); + const psCommand = `Get-Process -Name "ovms" -ErrorAction SilentlyContinue | Select-Object Id, Path | ConvertTo-Json` + const { stdout } = await execAsync(`powershell -Command "${psCommand}"`) + + if (!stdout.trim()) { + logger.info('OVMS process not running') + return 'not-running' + } + + const processes = JSON.parse(stdout) + const processList = Array.isArray(processes) ? processes : [processes] + + if (processList.length > 0) { + logger.info('OVMS process is running') + return 'running' + } else { + logger.info('OVMS process not running') + return 'not-running' + } + } catch (error) { + logger.info(`Failed to check OVMS status: ${error}`) + return 'not-running' + } + } + + /** + * Initialize OVMS by finding the executable path and working directory + */ + public async initializeOvms(): Promise { + // Use PowerShell to find ovms.exe processes with their paths + const psCommand = `Get-Process -Name "ovms" -ErrorAction SilentlyContinue | Select-Object Id, Path | ConvertTo-Json` + const { stdout } = await execAsync(`powershell -Command "${psCommand}"`) + + if (!stdout.trim()) { + logger.error('Command to find OVMS process returned no output') + return false + } + logger.debug(`OVMS process output: ${stdout}`) + + const processes = JSON.parse(stdout) + const processList = Array.isArray(processes) ? processes : [processes] + + // Find the first process with a valid path + for (const process of processList) { + this.ovms = { + pid: process.Id, + path: process.Path, + workingDirectory: path.dirname(process.Path) + } + return true + } + + return this.ovms !== null + } + + /** + * Check if the Model Name and ID are valid, they are valid only if they are not used in the config.json + * @param modelName Name of the model to check + * @param modelId ID of the model to check + */ + public async isNameAndIDAvalid(modelName: string, modelId: string): Promise { + if (!modelName || !modelId) { + logger.error('Model name and ID cannot be empty') + return false + } + + const homeDir = homedir() + const configPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'models', 'config.json') + try { + if (!(await fs.pathExists(configPath))) { + logger.warn(`Config file does not exist: ${configPath}`) + return false + } + + const config: OvmsConfig = await fs.readJson(configPath) + if (!config.mediapipe_config_list) { + logger.warn(`No mediapipe_config_list found in config: ${configPath}`) + return false + } + + // Check if the model name or ID already exists in the config + const exists = config.mediapipe_config_list.some( + (model) => model.name === modelName || model.base_path === modelId + ) + if (exists) { + logger.warn(`Model with name "${modelName}" or ID "${modelId}" already exists in the config`) + return false + } + } catch (error) { + logger.error(`Failed to check model existence: ${error}`) + return false + } + + return true + } + + private async applyModelPath(modelDirPath: string): Promise { + const homeDir = homedir() + const patchDir = path.join(homeDir, '.cherrystudio', 'ovms', 'patch') + if (!(await fs.pathExists(patchDir))) { + return true + } + + const modelId = path.basename(modelDirPath) + + // get all sub directories in patchDir + const patchs = await fs.readdir(patchDir) + for (const patch of patchs) { + const fullPatchPath = path.join(patchDir, patch) + + if (fs.lstatSync(fullPatchPath).isDirectory()) { + if (modelId.toLowerCase().includes(patch.toLowerCase())) { + // copy all files from fullPath to modelDirPath + try { + const files = await fs.readdir(fullPatchPath) + for (const file of files) { + const srcFile = path.join(fullPatchPath, file) + const destFile = path.join(modelDirPath, file) + await fs.copyFile(srcFile, destFile) + } + } catch (error) { + logger.error(`Failed to copy files from ${fullPatchPath} to ${modelDirPath}: ${error}`) + return false + } + logger.info(`Applied patchs for model ${modelId}`) + return true + } + } + } + + return true + } + + /** + * Add a model to OVMS by downloading it + * @param modelName Name of the model to add + * @param modelId ID of the model to download + * @param modelSource Model Source: huggingface, hf-mirror and modelscope, default is huggingface + * @param task Task type: text_generation, embedding, rerank, image_generation + */ + public async addModel( + modelName: string, + modelId: string, + modelSource: string, + task: string = 'text_generation' + ): Promise<{ success: boolean; message?: string }> { + logger.info(`Adding model: ${modelName} with ID: ${modelId}, Source: ${modelSource}, Task: ${task}`) + + const homeDir = homedir() + const ovdndDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const pathModel = path.join(ovdndDir, 'models', modelId) + + try { + // check the ovdnDir+'models'+modelId exist or not + if (await fs.pathExists(pathModel)) { + logger.error(`Model with ID ${modelId} already exists`) + return { success: false, message: 'Model ID already exists!' } + } + + // remove the model directory if it exists + if (await fs.pathExists(pathModel)) { + logger.info(`Removing existing model directory: ${pathModel}`) + await fs.remove(pathModel) + } + + // Use ovdnd.exe for downloading instead of ovms.exe + const ovdndPath = path.join(ovdndDir, 'ovdnd.exe') + const command = + `"${ovdndPath}" --pull ` + + `--model_repository_path "${ovdndDir}/models" ` + + `--source_model "${modelId}" ` + + `--model_name "${modelName}" ` + + `--target_device GPU ` + + `--task ${task} ` + + `--overwrite_models` + + const env: Record = { + ...process.env, + OVMS_DIR: ovdndDir, + PYTHONHOME: path.join(ovdndDir, 'python'), + PATH: `${process.env.PATH};${ovdndDir};${path.join(ovdndDir, 'python')}` + } + + if (modelSource) { + env.HF_ENDPOINT = modelSource + } + + logger.info(`Running command: ${command} from ${modelSource}`) + const { stdout } = await execAsync(command, { env: env, cwd: ovdndDir }) + + logger.info('Model download completed') + logger.debug(`Command output: ${stdout}`) + } catch (error) { + // remove ovdnDir+'models'+modelId if it exists + if (await fs.pathExists(pathModel)) { + logger.info(`Removing failed model directory: ${pathModel}`) + await fs.remove(pathModel) + } + logger.error(`Failed to add model: ${error}`) + return { + success: false, + message: `Download model ${modelId} failed, please check following items and try it again:

- the model id

- network connection and proxy

` + } + } + + // Update config file + if (!(await this.updateModelConfig(modelName, modelId))) { + logger.error('Failed to update model config') + return { success: false, message: 'Failed to update model config' } + } + + if (!(await this.applyModelPath(pathModel))) { + logger.error('Failed to apply model patchs') + return { success: false, message: 'Failed to apply model patchs' } + } + + logger.info(`Model ${modelName} added successfully with ID ${modelId}`) + return { success: true } + } + + /** + * Stop the model download process if it's running + * @returns Promise<{ success: boolean; message?: string }> + */ + public async stopAddModel(): Promise<{ success: boolean; message?: string }> { + try { + // Check if ovdnd.exe process is running + const psCommand = `Get-Process -Name "ovdnd" -ErrorAction SilentlyContinue | Select-Object Id, Path | ConvertTo-Json` + const { stdout } = await execAsync(`powershell -Command "${psCommand}"`) + + if (!stdout.trim()) { + logger.info('ovdnd process is not running') + return { success: true, message: 'Model download process is not running' } + } + + const processes = JSON.parse(stdout) + const processList = Array.isArray(processes) ? processes : [processes] + + if (processList.length === 0) { + logger.info('ovdnd process is not running') + return { success: true, message: 'Model download process is not running' } + } + + // Terminate all ovdnd processes + for (const process of processList) { + this.terminalProcess(process.Id) + } + + logger.info('Model download process stopped successfully') + return { success: true, message: 'Model download process stopped successfully' } + } catch (error) { + logger.error(`Failed to stop model download process: ${error}`) + return { success: false, message: 'Failed to stop model download process' } + } + } + + /** + * check if the model id exists in the OVMS configuration + * @param modelId ID of the model to check + */ + public async checkModelExists(modelId: string): Promise { + const homeDir = homedir() + const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const configPath = path.join(ovmsDir, 'models', 'config.json') + + try { + if (!(await fs.pathExists(configPath))) { + logger.warn(`Config file does not exist: ${configPath}`) + return false + } + + const config: OvmsConfig = await fs.readJson(configPath) + if (!config.mediapipe_config_list) { + logger.warn('No mediapipe_config_list found in config') + return false + } + + return config.mediapipe_config_list.some((model) => model.base_path === modelId) + } catch (error) { + logger.error(`Failed to check model existence: ${error}`) + return false + } + } + + /** + * Update the model configuration file + */ + public async updateModelConfig(modelName: string, modelId: string): Promise { + const homeDir = homedir() + const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const configPath = path.join(ovmsDir, 'models', 'config.json') + + try { + // Ensure the models directory exists + await fs.ensureDir(path.dirname(configPath)) + let config: OvmsConfig + + // Read existing config or create new one + if (await fs.pathExists(configPath)) { + config = await fs.readJson(configPath) + } else { + config = { mediapipe_config_list: [] } + } + + // Ensure mediapipe_config_list exists + if (!config.mediapipe_config_list) { + config.mediapipe_config_list = [] + } + + // Add new model config + const newModelConfig: ModelConfig = { + name: modelName, + base_path: modelId + } + + // Check if model already exists, if so, update it + const existingIndex = config.mediapipe_config_list.findIndex((model) => model.base_path === modelId) + + if (existingIndex >= 0) { + config.mediapipe_config_list[existingIndex] = newModelConfig + logger.info(`Updated existing model config: ${modelName}`) + } else { + config.mediapipe_config_list.push(newModelConfig) + logger.info(`Added new model config: ${modelName}`) + } + + // Write config back to file + await fs.writeJson(configPath, config, { spaces: 2 }) + logger.info(`Config file updated: ${configPath}`) + } catch (error) { + logger.error(`Failed to update model config: ${error}`) + return false + } + return true + } + + /** + * Get all models from OVMS config, filtered for image generation models + * @returns Array of model configurations + */ + public async getModels(): Promise { + const homeDir = homedir() + const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const configPath = path.join(ovmsDir, 'models', 'config.json') + + try { + if (!(await fs.pathExists(configPath))) { + logger.warn(`Config file does not exist: ${configPath}`) + return [] + } + + const config: OvmsConfig = await fs.readJson(configPath) + if (!config.mediapipe_config_list) { + logger.warn('No mediapipe_config_list found in config') + return [] + } + + // Filter models for image generation (SD, Stable-Diffusion, Stable Diffusion, FLUX) + const imageGenerationModels = config.mediapipe_config_list.filter((model) => { + const modelName = model.name.toLowerCase() + return ( + modelName.startsWith('sd') || + modelName.startsWith('stable-diffusion') || + modelName.startsWith('stable diffusion') || + modelName.startsWith('flux') + ) + }) + + logger.info(`Found ${imageGenerationModels.length} image generation models`) + return imageGenerationModels + } catch (error) { + logger.error(`Failed to get models: ${error}`) + return [] + } + } +} + +export default OvmsManager diff --git a/src/preload/index.ts b/src/preload/index.ts index faa0335aeb..9244076a9a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -95,7 +95,8 @@ const api = { }, system: { getDeviceType: () => ipcRenderer.invoke(IpcChannel.System_GetDeviceType), - getHostname: () => ipcRenderer.invoke(IpcChannel.System_GetHostname) + getHostname: () => ipcRenderer.invoke(IpcChannel.System_GetHostname), + getCpuName: () => ipcRenderer.invoke(IpcChannel.System_GetCpuName) }, devTools: { toggle: () => ipcRenderer.invoke(IpcChannel.System_ToggleDevTools) @@ -285,6 +286,16 @@ const api = { clearAuthCache: (projectId: string, clientEmail?: string) => ipcRenderer.invoke(IpcChannel.VertexAI_ClearAuthCache, projectId, clientEmail) }, + ovms: { + addModel: (modelName: string, modelId: string, modelSource: string, task: string) => + ipcRenderer.invoke(IpcChannel.Ovms_AddModel, modelName, modelId, modelSource, task), + stopAddModel: () => ipcRenderer.invoke(IpcChannel.Ovms_StopAddModel), + getModels: () => ipcRenderer.invoke(IpcChannel.Ovms_GetModels), + isRunning: () => ipcRenderer.invoke(IpcChannel.Ovms_IsRunning), + getStatus: () => ipcRenderer.invoke(IpcChannel.Ovms_GetStatus), + runOvms: () => ipcRenderer.invoke(IpcChannel.Ovms_RunOVMS), + stopOvms: () => ipcRenderer.invoke(IpcChannel.Ovms_StopOVMS) + }, config: { set: (key: string, value: any, isNotify: boolean = false) => ipcRenderer.invoke(IpcChannel.Config_Set, key, value, isNotify), @@ -350,6 +361,7 @@ const api = { getBinaryPath: (name: string) => ipcRenderer.invoke(IpcChannel.App_GetBinaryPath, name), installUVBinary: () => ipcRenderer.invoke(IpcChannel.App_InstallUvBinary), installBunBinary: () => ipcRenderer.invoke(IpcChannel.App_InstallBunBinary), + installOvmsBinary: () => ipcRenderer.invoke(IpcChannel.App_InstallOvmsBinary), protocol: { onReceiveData: (callback: (data: { url: string; params: any }) => void) => { const listener = (_event: Electron.IpcRendererEvent, data: { url: string; params: any }) => { diff --git a/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts b/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts index f7b4a80f46..e7194c240b 100644 --- a/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts +++ b/src/renderer/src/aiCore/legacy/clients/ApiClientFactory.ts @@ -12,6 +12,7 @@ import { VertexAPIClient } from './gemini/VertexAPIClient' import { NewAPIClient } from './newapi/NewAPIClient' import { OpenAIAPIClient } from './openai/OpenAIApiClient' import { OpenAIResponseAPIClient } from './openai/OpenAIResponseAPIClient' +import { OVMSClient } from './ovms/OVMSClient' import { PPIOAPIClient } from './ppio/PPIOAPIClient' import { ZhipuAPIClient } from './zhipu/ZhipuAPIClient' @@ -63,6 +64,12 @@ export class ApiClientFactory { return instance } + if (provider.id === 'ovms') { + logger.debug(`Creating OVMSClient for provider: ${provider.id}`) + instance = new OVMSClient(provider) as BaseApiClient + return instance + } + // 然后检查标准的 Provider Type switch (provider.type) { case 'openai': diff --git a/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts b/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts new file mode 100644 index 0000000000..d1fd4ed8ba --- /dev/null +++ b/src/renderer/src/aiCore/legacy/clients/ovms/OVMSClient.ts @@ -0,0 +1,56 @@ +import { loggerService } from '@logger' +import { isSupportedModel } from '@renderer/config/models' +import { objectKeys, Provider } from '@renderer/types' +import OpenAI from 'openai' + +import { OpenAIAPIClient } from '../openai/OpenAIApiClient' + +const logger = loggerService.withContext('OVMSClient') + +export class OVMSClient extends OpenAIAPIClient { + constructor(provider: Provider) { + super(provider) + } + + override async listModels(): Promise { + try { + const sdk = await this.getSdkInstance() + + const chatModelsResponse = await sdk.request({ + method: 'get', + path: '../v1/config' + }) + logger.debug(`Chat models response: ${JSON.stringify(chatModelsResponse)}`) + + // Parse the config response to extract model information + const config = chatModelsResponse as Record + const models = objectKeys(config) + .map((modelName) => { + const modelInfo = config[modelName] + + // Check if model has at least one version with "AVAILABLE" state + const hasAvailableVersion = modelInfo?.model_version_status?.some( + (versionStatus: any) => versionStatus?.state === 'AVAILABLE' + ) + + if (hasAvailableVersion) { + return { + id: modelName, + object: 'model' as const, + owned_by: 'ovms', + created: Date.now() + } + } + return null // Skip models without available versions + }) + .filter(Boolean) // Remove null entries + logger.debug(`Processed models: ${JSON.stringify(models)}`) + + // Filter out unsupported models + return models.filter((model): model is OpenAI.Models.Model => model !== null && isSupportedModel(model)) + } catch (error) { + logger.error(`Error listing OVMS models: ${error}`) + return [] + } + } +} diff --git a/src/renderer/src/assets/images/providers/intel.png b/src/renderer/src/assets/images/providers/intel.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d9e19d745763bc00c7337fa89605762c2778d3 GIT binary patch literal 3706 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 = { id: 'deepseek-r1', name: 'DeepSeek-R1', provider: 'burncloud', group: 'deepseek-ai' }, { id: 'deepseek-v3', name: 'DeepSeek-V3', provider: 'burncloud', group: 'deepseek-ai' } ], + ovms: [], ollama: [], lmstudio: [], silicon: [ diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 64e78e847a..ddaac53342 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -24,6 +24,7 @@ import GrokProviderLogo from '@renderer/assets/images/providers/grok.png' import GroqProviderLogo from '@renderer/assets/images/providers/groq.png' import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png' import InfiniProviderLogo from '@renderer/assets/images/providers/infini.png' +import IntelOvmsLogo from '@renderer/assets/images/providers/intel.png' import JinaProviderLogo from '@renderer/assets/images/providers/jina.png' import LanyunProviderLogo from '@renderer/assets/images/providers/lanyun.png' import LMStudioProviderLogo from '@renderer/assets/images/providers/lmstudio.png' @@ -109,6 +110,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = isSystem: true, enabled: false }, + ovms: { + id: 'ovms', + name: 'OpenVINO Model Server', + type: 'openai', + apiKey: '', + apiHost: 'http://localhost:8000/v3/', + models: SYSTEM_MODELS.ovms, + isSystem: true, + enabled: false + }, ocoolai: { id: 'ocoolai', name: 'ocoolAI', @@ -649,6 +660,7 @@ export const PROVIDER_LOGO_MAP: AtLeast = { yi: ZeroOneProviderLogo, groq: GroqProviderLogo, zhipu: ZhipuProviderLogo, + ovms: IntelOvmsLogo, ollama: OllamaProviderLogo, lmstudio: LMStudioProviderLogo, moonshot: MoonshotProviderLogo, @@ -1034,6 +1046,16 @@ export const PROVIDER_URLS: Record = { models: 'https://console.groq.com/docs/models' } }, + ovms: { + api: { + url: 'http://localhost:8000/v3/' + }, + websites: { + official: 'https://www.intel.com/content/www/us/en/developer/tools/openvino-toolkit/overview.html', + docs: 'https://docs.openvino.ai/2025/model-server/ovms_what_is_openvino_model_server.html', + models: 'https://www.modelscope.cn/organization/OpenVINO' + } + }, ollama: { api: { url: 'http://localhost:11434' diff --git a/src/renderer/src/i18n/label.ts b/src/renderer/src/i18n/label.ts index a07daa975f..ad3c326d9f 100644 --- a/src/renderer/src/i18n/label.ts +++ b/src/renderer/src/i18n/label.ts @@ -61,6 +61,7 @@ const providerKeyMap = { nvidia: 'provider.nvidia', o3: 'provider.o3', ocoolai: 'provider.ocoolai', + ovms: 'provider.ovms', ollama: 'provider.ollama', openai: 'provider.openai', openrouter: 'provider.openrouter', diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index dfd09049ed..94abc7667d 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -251,6 +251,7 @@ "added": "Added", "case_sensitive": "Case Sensitive", "collapse": "Collapse", + "download": "Download", "includes_user_questions": "Include Your Questions", "manage": "Manage", "select_model": "Select Model", @@ -1826,6 +1827,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "Install", + "installing": "Installing", + "reinstall": "Re-Install", + "run": "Run OVMS", + "starting": "Starting", + "stop": "Stop OVMS", + "stopping": "Stopping" + }, + "description": "

1. Download OV Models.

2. Add Models in 'Manager'.

Support Windows Only!

OVMS Install Path: '%USERPROFILE%\\.cherrystudio\\ovms' .

Please refer to Intel OVMS Guide

", + "download": { + "button": "Download", + "error": "Download Error", + "model_id": { + "label": "Model ID:", + "model_id_pattern": "Model ID must start with OpenVINO/", + "placeholder": "Required e.g. OpenVINO/Qwen3-8B-int4-ov", + "required": "Please enter the model ID" + }, + "model_name": { + "label": "Model Name:", + "placeholder": "Required e.g. Qwen3-8B-int4-ov", + "required": "Please enter the model name" + }, + "model_source": "Model Source:", + "model_task": "Model Task:", + "success": "Download successful", + "success_desc": "Model \"{{modelName}}\"-\"{{modelId}}\" downloaded successfully, please go to the OVMS management interface to add the model", + "tip": "The model is downloading, sometimes it takes hours. Please be patient...", + "title": "Download Intel OpenVINO Model" + }, + "failed": { + "install": "Install OVMS failed:", + "install_code_100": "Unknown Error", + "install_code_101": "Only supports Intel(R) Core(TM) Ultra CPU", + "install_code_102": "Only supports Windows", + "install_code_103": "Download OVMS runtime failed", + "install_code_104": "Uncompress OVMS runtime failed", + "install_code_105": "Clean OVMS runtime failed", + "run": "Run OVMS failed:", + "stop": "Stop OVMS failed:" + }, + "status": { + "not_installed": "OVMS is not installed", + "not_running": "OVMS is not running", + "running": "OVMS is running", + "unknown": "OVMS status unknown" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "Aspect Ratio", "aspect_ratios": { @@ -2057,6 +2109,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexity", "ph8": "PH8", "poe": "Poe", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index dd3fa37be1..c09c94cbbf 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -251,6 +251,7 @@ "added": "已添加", "case_sensitive": "区分大小写", "collapse": "收起", + "download": "下载", "includes_user_questions": "包含用户提问", "manage": "管理", "select_model": "选择模型", @@ -1826,6 +1827,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "安装", + "installing": "正在安装", + "reinstall": "重装", + "run": "运行 OVMS", + "starting": "启动中", + "stop": "停止 OVMS", + "stopping": "停止中" + }, + "description": "

1. 下载 OV 模型.

2. 在 'Manager' 中添加模型.

仅支持 Windows!

OVMS 安装路径: '%USERPROFILE%\\.cherrystudio\\ovms' .

请参考 Intel OVMS 指南

", + "download": { + "button": "下载", + "error": "选择失败", + "model_id": { + "label": "模型 ID", + "model_id_pattern": "模型 ID 必须以 OpenVINO/ 开头", + "placeholder": "必填,例如 OpenVINO/Qwen3-8B-int4-ov", + "required": "请输入模型 ID" + }, + "model_name": { + "label": "模型名称", + "placeholder": "必填,例如 Qwen3-8B-int4-ov", + "required": "请输入模型名称" + }, + "model_source": "模型来源:", + "model_task": "模型任务:", + "success": "下载成功", + "success_desc": "模型\"{{modelName}}\"-\"{{modelId}}\"下载成功,请前往 OVMS 管理界面添加模型", + "tip": "模型正在下载,有时需要几个小时。请耐心等待...", + "title": "下载 Intel OpenVINO 模型" + }, + "failed": { + "install": "安装 OVMS 失败:", + "install_code_100": "未知错误", + "install_code_101": "仅支持 Intel(R) Core(TM) Ultra CPU", + "install_code_102": "仅支持 Windows", + "install_code_103": "下载 OVMS runtime 失败", + "install_code_104": "解压 OVMS runtime 失败", + "install_code_105": "清理 OVMS runtime 失败", + "run": "运行 OVMS 失败:", + "stop": "停止 OVMS 失败:" + }, + "status": { + "not_installed": "OVMS 未安装", + "not_running": "OVMS 未运行", + "running": "OVMS 正在运行", + "unknown": "OVMS 状态未知" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "画幅比例", "aspect_ratios": { @@ -2057,6 +2109,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexity", "ph8": "PH8 大模型开放平台", "poe": "Poe", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 4e162abda3..a1aa1cc7fa 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -251,6 +251,7 @@ "added": "已新增", "case_sensitive": "區分大小寫", "collapse": "折疊", + "download": "下載", "includes_user_questions": "包含使用者提問", "manage": "管理", "select_model": "選擇模型", @@ -1826,6 +1827,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "安裝", + "installing": "正在安裝", + "reinstall": "重新安裝", + "run": "執行 OVMS", + "starting": "啟動中", + "stop": "停止 OVMS", + "stopping": "停止中" + }, + "description": "

1. 下載 OV 模型。

2. 在 'Manager' 中新增模型。

僅支援 Windows!

OVMS 安裝路徑: '%USERPROFILE%\\.cherrystudio\\ovms' 。

請參考 Intel OVMS 指南

", + "download": { + "button": "下載", + "error": "選擇失敗", + "model_id": { + "label": "模型 ID", + "model_id_pattern": "模型 ID 必須以 OpenVINO/ 開頭", + "placeholder": "必填,例如 OpenVINO/Qwen3-8B-int4-ov", + "required": "請輸入模型 ID" + }, + "model_name": { + "label": "模型名稱", + "placeholder": "必填,例如 Qwen3-8B-int4-ov", + "required": "請輸入模型名稱" + }, + "model_source": "模型來源:", + "model_task": "模型任務:", + "success": "下載成功", + "success_desc": "模型\"{{modelName}}\"-\"{{modelId}}\"下載成功,請前往 OVMS 管理界面添加模型", + "tip": "模型正在下載,有時需要幾個小時。請耐心等候...", + "title": "下載 Intel OpenVINO 模型" + }, + "failed": { + "install": "安裝 OVMS 失敗:", + "install_code_100": "未知錯誤", + "install_code_101": "僅支援 Intel(R) Core(TM) Ultra CPU", + "install_code_102": "僅支援 Windows", + "install_code_103": "下載 OVMS runtime 失敗", + "install_code_104": "解壓 OVMS runtime 失敗", + "install_code_105": "清理 OVMS runtime 失敗", + "run": "執行 OVMS 失敗:", + "stop": "停止 OVMS 失敗:" + }, + "status": { + "not_installed": "OVMS 未安裝", + "not_running": "OVMS 未執行", + "running": "OVMS 正在執行", + "unknown": "OVMS 狀態未知" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "畫幅比例", "aspect_ratios": { @@ -2057,6 +2109,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexity", "ph8": "PH8 大模型開放平台", "poe": "Poe", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 71aca53b82..a071f27783 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -251,6 +251,7 @@ "added": "προστέθηκε", "case_sensitive": "Διάκριση πεζών/κεφαλαίων", "collapse": "συμπεριλάβετε", + "download": "Λήψη", "includes_user_questions": "Περιλαμβάνει ερωτήσεις χρήστη", "manage": "χειριστείτε", "select_model": "επιλογή μοντέλου", @@ -1825,6 +1826,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "Εγκατάσταση", + "installing": "Εγκατάσταση σε εξέλιξη", + "reinstall": "Επανεγκατάσταση", + "run": "Εκτέλεση OVMS", + "starting": "Εκκίνηση σε εξέλιξη", + "stop": "Διακοπή OVMS", + "stopping": "Διακοπή σε εξέλιξη" + }, + "description": "

1. Λήψη μοντέλου OV.

2. Προσθήκη μοντέλου στο 'Manager'.

Υποστηρίζεται μόνο στα Windows!

Διαδρομή εγκατάστασης OVMS: '%USERPROFILE%\\.cherrystudio\\ovms' .

Ανατρέξτε στον Οδηγό Intel OVMS

", + "download": { + "button": "Λήψη", + "error": "Η επιλογή απέτυχε", + "model_id": { + "label": "Αναγνωριστικό μοντέλου:", + "model_id_pattern": "Το αναγνωριστικό μοντέλου πρέπει να ξεκινά με OpenVINO/", + "placeholder": "Απαιτείται, π.χ. OpenVINO/Qwen3-8B-int4-ov", + "required": "Παρακαλώ εισάγετε το αναγνωριστικό μοντέλου" + }, + "model_name": { + "label": "Όνομα μοντέλου:", + "placeholder": "Απαιτείται, π.χ. Qwen3-8B-int4-ov", + "required": "Παρακαλώ εισάγετε το όνομα του μοντέλου" + }, + "model_source": "Πηγή μοντέλου:", + "model_task": "Εργασία μοντέλου:", + "success": "Η λήψη ολοκληρώθηκε με επιτυχία", + "success_desc": "Το μοντέλο \"{{modelName}}\"-\"{{modelId}}\" λήφθηκε επιτυχώς, παρακαλώ μεταβείτε στη διεπαφή διαχείρισης OVMS για να προσθέσετε το μοντέλο", + "tip": "Το μοντέλο κατεβαίνει, μερικές φορές χρειάζονται αρκετές ώρες. Παρακαλώ περιμένετε υπομονετικά...", + "title": "Λήψη μοντέλου Intel OpenVINO" + }, + "failed": { + "install": "Η εγκατάσταση του OVMS απέτυχε:", + "install_code_100": "Άγνωστο σφάλμα", + "install_code_101": "Υποστηρίζεται μόνο σε Intel(R) Core(TM) Ultra CPU", + "install_code_102": "Υποστηρίζεται μόνο στα Windows", + "install_code_103": "Η λήψη του OVMS runtime απέτυχε", + "install_code_104": "Η αποσυμπίεση του OVMS runtime απέτυχε", + "install_code_105": "Ο καθαρισμός του OVMS runtime απέτυχε", + "run": "Η εκτέλεση του OVMS απέτυχε:", + "stop": "Η διακοπή του OVMS απέτυχε:" + }, + "status": { + "not_installed": "Το OVMS δεν έχει εγκατασταθεί", + "not_running": "Το OVMS δεν εκτελείται", + "running": "Το OVMS εκτελείται", + "unknown": "Άγνωστη κατάσταση OVMS" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "Λόγος διαστάσεων", "aspect_ratios": { @@ -2056,6 +2108,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexity", "ph8": "Πλατφόρμα Ανοιχτής Μεγάλης Μοντέλου PH8", "poe": "Poe", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index de3b5cb17f..7ae0a1bd3c 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -251,6 +251,7 @@ "added": "Agregado", "case_sensitive": "Distingue mayúsculas y minúsculas", "collapse": "Colapsar", + "download": "Descargar", "includes_user_questions": "Incluye preguntas del usuario", "manage": "Administrar", "select_model": "Seleccionar Modelo", @@ -1825,6 +1826,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "Instalar", + "installing": "Instalando", + "reinstall": "Reinstalar", + "run": "Ejecutar OVMS", + "starting": "Iniciando", + "stop": "Detener OVMS", + "stopping": "Deteniendo" + }, + "description": "

1. Descargar modelo OV.

2. Agregar modelo en 'Administrador'.

¡Solo compatible con Windows!

Ruta de instalación de OVMS: '%USERPROFILE%\\.cherrystudio\\ovms' .

Consulte la Guía de Intel OVMS

", + "download": { + "button": "Descargar", + "error": "Selección fallida", + "model_id": { + "label": "ID del modelo:", + "model_id_pattern": "El ID del modelo debe comenzar con OpenVINO/", + "placeholder": "Requerido, por ejemplo, OpenVINO/Qwen3-8B-int4-ov", + "required": "Por favor, ingrese el ID del modelo" + }, + "model_name": { + "label": "Nombre del modelo:", + "placeholder": "Requerido, por ejemplo, Qwen3-8B-int4-ov", + "required": "Por favor, ingrese el nombre del modelo" + }, + "model_source": "Fuente del modelo:", + "model_task": "Tarea del modelo:", + "success": "Descarga exitosa", + "success_desc": "El modelo \"{{modelName}}\"-\"{{modelId}}\" se descargó exitosamente, por favor vaya a la interfaz de administración de OVMS para agregar el modelo", + "tip": "El modelo se está descargando, a veces toma varias horas. Por favor espere pacientemente...", + "title": "Descargar modelo Intel OpenVINO" + }, + "failed": { + "install": "Error al instalar OVMS:", + "install_code_100": "Error desconocido", + "install_code_101": "Solo compatible con CPU Intel(R) Core(TM) Ultra", + "install_code_102": "Solo compatible con Windows", + "install_code_103": "Error al descargar el tiempo de ejecución de OVMS", + "install_code_104": "Error al descomprimir el tiempo de ejecución de OVMS", + "install_code_105": "Error al limpiar el tiempo de ejecución de OVMS", + "run": "Error al ejecutar OVMS:", + "stop": "Error al detener OVMS:" + }, + "status": { + "not_installed": "OVMS no instalado", + "not_running": "OVMS no está en ejecución", + "running": "OVMS en ejecución", + "unknown": "Estado de OVMS desconocido" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "Relación de aspecto", "aspect_ratios": { @@ -2056,6 +2108,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplejidad", "ph8": "Plataforma Abierta de Grandes Modelos PH8", "poe": "Poe", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 66a3a5b4ae..64a788f266 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -251,6 +251,7 @@ "added": "Ajouté", "case_sensitive": "Respecter la casse", "collapse": "Réduire", + "download": "Télécharger", "includes_user_questions": "Inclure les questions de l'utilisateur", "manage": "Gérer", "select_model": "Sélectionner le Modèle", @@ -1825,6 +1826,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "Installer", + "installing": "Installation en cours", + "reinstall": "Réinstaller", + "run": "Exécuter OVMS", + "starting": "Démarrage en cours", + "stop": "Arrêter OVMS", + "stopping": "Arrêt en cours" + }, + "description": "

1. Télécharger le modèle OV.

2. Ajouter le modèle dans 'Manager'.

Uniquement compatible avec Windows !

Chemin d'installation d'OVMS : '%USERPROFILE%\\.cherrystudio\\ovms' .

Veuillez vous référer au Guide Intel OVMS

", + "download": { + "button": "Télécharger", + "error": "Échec de la sélection", + "model_id": { + "label": "ID du modèle :", + "model_id_pattern": "L'ID du modèle doit commencer par OpenVINO/", + "placeholder": "Requis, par exemple OpenVINO/Qwen3-8B-int4-ov", + "required": "Veuillez saisir l'ID du modèle" + }, + "model_name": { + "label": "Nom du modèle :", + "placeholder": "Requis, par exemple Qwen3-8B-int4-ov", + "required": "Veuillez saisir le nom du modèle" + }, + "model_source": "Source du modèle :", + "model_task": "Tâche du modèle :", + "success": "Téléchargement réussi", + "success_desc": "Le modèle \"{{modelName}}\"-\"{{modelId}}\" a été téléchargé avec succès, veuillez vous rendre à l'interface de gestion OVMS pour ajouter le modèle", + "tip": "Le modèle est en cours de téléchargement, cela peut parfois prendre plusieurs heures. Veuillez patienter...", + "title": "Télécharger le modèle Intel OpenVINO" + }, + "failed": { + "install": "Échec de l'installation d'OVMS :", + "install_code_100": "Erreur inconnue", + "install_code_101": "Uniquement compatible avec les processeurs Intel(R) Core(TM) Ultra", + "install_code_102": "Uniquement compatible avec Windows", + "install_code_103": "Échec du téléchargement du runtime OVMS", + "install_code_104": "Échec de la décompression du runtime OVMS", + "install_code_105": "Échec du nettoyage du runtime OVMS", + "run": "Échec de l'exécution d'OVMS :", + "stop": "Échec de l'arrêt d'OVMS :" + }, + "status": { + "not_installed": "OVMS non installé", + "not_running": "OVMS n'est pas en cours d'exécution", + "running": "OVMS en cours d'exécution", + "unknown": "État d'OVMS inconnu" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "Format d'image", "aspect_ratios": { @@ -2056,6 +2108,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexité", "ph8": "Plateforme ouverte de grands modèles PH8", "poe": "Poe", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index bee5f54470..23eea05fe6 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -251,6 +251,7 @@ "added": "追加済み", "case_sensitive": "大文字と小文字の区別", "collapse": "折りたたむ", + "download": "ダウンロード", "includes_user_questions": "ユーザーからの質問を含む", "manage": "管理", "select_model": "モデルを選択", @@ -1825,6 +1826,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "インストール", + "installing": "インストール中", + "reinstall": "再インストール", + "run": "OVMSを実行", + "starting": "起動中", + "stop": "OVMSを停止", + "stopping": "停止中" + }, + "description": "

1. OVモデルをダウンロードします。

2. 'マネージャー'でモデルを追加します。

Windowsのみサポート!

OVMSインストールパス: '%USERPROFILE%\\.cherrystudio\\ovms' 。

詳細はIntel OVMSガイドをご参照ください。

", + "download": { + "button": "ダウンロード", + "error": "ダウンロードエラー", + "model_id": { + "label": "モデルID", + "model_id_pattern": "モデルIDはOpenVINO/で始まる必要があります", + "placeholder": "必須 例: OpenVINO/Qwen3-8B-int4-ov", + "required": "モデルIDを入力してください" + }, + "model_name": { + "label": "モデル名", + "placeholder": "必須 例: Qwen3-8B-int4-ov", + "required": "モデル名を入力してください" + }, + "model_source": "モデルソース:", + "model_task": "モデルタスク:", + "success": "ダウンロード成功", + "success_desc": "モデル\"{{modelName}}\"-\"{{modelId}}\"ダウンロード成功、OVMS管理インターフェースに移動してモデルを追加してください", + "tip": "モデルはダウンロードされていますが、時には数時間かかります。我慢してください...", + "title": "Intel OpenVINOモデルをダウンロード" + }, + "failed": { + "install": "OVMSのインストールに失敗しました:", + "install_code_100": "不明なエラー", + "install_code_101": "Intel(R) Core(TM) Ultra CPUのみサポート", + "install_code_102": "Windowsのみサポート", + "install_code_103": "OVMSランタイムのダウンロードに失敗しました", + "install_code_104": "OVMSランタイムの解凍に失敗しました", + "install_code_105": "OVMSランタイムのクリーンアップに失敗しました", + "run": "OVMSの実行に失敗しました:", + "stop": "OVMSの停止に失敗しました:" + }, + "status": { + "not_installed": "OVMSはインストールされていません", + "not_running": "OVMSは実行されていません", + "running": "OVMSは実行中です", + "unknown": "OVMSのステータスが不明です" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "画幅比例", "aspect_ratios": { @@ -2056,6 +2108,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexity", "ph8": "PH8", "poe": "Poe", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 5c9bbf566e..ab9bec0e66 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -251,6 +251,7 @@ "added": "Adicionado", "case_sensitive": "Diferenciar maiúsculas e minúsculas", "collapse": "Recolher", + "download": "Baixar", "includes_user_questions": "Incluir perguntas do usuário", "manage": "Gerenciar", "select_model": "Selecionar Modelo", @@ -1825,6 +1826,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "Instalar", + "installing": "Instalando", + "reinstall": "Reinstalar", + "run": "Executar OVMS", + "starting": "Iniciando", + "stop": "Parar OVMS", + "stopping": "Parando" + }, + "description": "

1. Baixe o modelo OV.

2. Adicione o modelo no 'Gerenciador'.

Compatível apenas com Windows!

Caminho de instalação do OVMS: '%USERPROFILE%\\.cherrystudio\\ovms' .

Consulte o Guia do Intel OVMS

", + "download": { + "button": "Baixar", + "error": "Falha na seleção", + "model_id": { + "label": "ID do modelo:", + "model_id_pattern": "O ID do modelo deve começar com OpenVINO/", + "placeholder": "Obrigatório, por exemplo, OpenVINO/Qwen3-8B-int4-ov", + "required": "Por favor, insira o ID do modelo" + }, + "model_name": { + "label": "Nome do modelo:", + "placeholder": "Obrigatório, por exemplo, Qwen3-8B-int4-ov", + "required": "Por favor, insira o nome do modelo" + }, + "model_source": "Fonte do modelo:", + "model_task": "Tarefa do modelo:", + "success": "Download concluído com sucesso", + "success_desc": "O modelo \"{{modelName}}\"-\"{{modelId}}\" foi baixado com sucesso, por favor vá para a interface de gerenciamento OVMS para adicionar o modelo", + "tip": "O modelo está sendo baixado, às vezes leva várias horas. Por favor aguarde pacientemente...", + "title": "Baixar modelo Intel OpenVINO" + }, + "failed": { + "install": "Falha na instalação do OVMS:", + "install_code_100": "Erro desconhecido", + "install_code_101": "Compatível apenas com CPU Intel(R) Core(TM) Ultra", + "install_code_102": "Compatível apenas com Windows", + "install_code_103": "Falha ao baixar o tempo de execução do OVMS", + "install_code_104": "Falha ao descompactar o tempo de execução do OVMS", + "install_code_105": "Falha ao limpar o tempo de execução do OVMS", + "run": "Falha ao executar o OVMS:", + "stop": "Falha ao parar o OVMS:" + }, + "status": { + "not_installed": "OVMS não instalado", + "not_running": "OVMS não está em execução", + "running": "OVMS em execução", + "unknown": "Status do OVMS desconhecido" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "Proporção da Imagem", "aspect_ratios": { @@ -2056,6 +2108,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexidade", "ph8": "Plataforma Aberta de Grandes Modelos PH8", "poe": "Poe", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index ec50afccd0..ccc1f49344 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -251,6 +251,7 @@ "added": "Добавлено", "case_sensitive": "Чувствительность к регистру", "collapse": "Свернуть", + "download": "Скачать", "includes_user_questions": "Включает вопросы пользователей", "manage": "Редактировать", "select_model": "Выбрать модель", @@ -1825,6 +1826,57 @@ }, "title": "Ollama" }, + "ovms": { + "action": { + "install": "Установить", + "installing": "Установка", + "reinstall": "Переустановить", + "run": "Запустить OVMS", + "starting": "Запуск", + "stop": "Остановить OVMS", + "stopping": "Остановка" + }, + "description": "

1. Загрузите модели OV.

2. Добавьте модели в 'Менеджер'.

Поддерживается только Windows!

Путь установки OVMS: '%USERPROFILE%\\.cherrystudio\\ovms'.

Пожалуйста, ознакомьтесь с руководством Intel OVMS

", + "download": { + "button": "Скачать", + "error": "Ошибка загрузки", + "model_id": { + "label": "ID модели", + "model_id_pattern": "ID модели должен начинаться с OpenVINO/", + "placeholder": "Обязательно, например: OpenVINO/Qwen3-8B-int4-ov", + "required": "Пожалуйста, введите ID модели" + }, + "model_name": { + "label": "Название модели:", + "placeholder": "Обязательно, например: Qwen3-8B-int4-ov", + "required": "Пожалуйста, введите название модели" + }, + "model_source": "Источник модели:", + "model_task": "Задача модели:", + "success": "Скачивание успешно", + "success_desc": "Модель \"{{modelName}}\"-\"{{modelId}}\" успешно скачана, пожалуйста, перейдите в интерфейс управления OVMS, чтобы добавить модель", + "tip": "Модель загружается, иногда это занимает часы. Пожалуйста, будьте терпеливы...", + "title": "Скачать модель Intel OpenVINO" + }, + "failed": { + "install": "Ошибка установки OVMS:", + "install_code_100": "Неизвестная ошибка", + "install_code_101": "Поддерживаются только процессоры Intel(R) Core(TM) Ultra CPU", + "install_code_102": "Поддерживается только Windows", + "install_code_103": "Ошибка загрузки среды выполнения OVMS", + "install_code_104": "Ошибка распаковки среды выполнения OVMS", + "install_code_105": "Ошибка очистки среды выполнения OVMS", + "run": "Ошибка запуска OVMS:", + "stop": "Ошибка остановки OVMS:" + }, + "status": { + "not_installed": "OVMS не установлен", + "not_running": "OVMS не запущен", + "running": "OVMS запущен", + "unknown": "Статус OVMS неизвестен" + }, + "title": "Intel OVMS" + }, "paintings": { "aspect_ratio": "Пропорции изображения", "aspect_ratios": { @@ -2056,6 +2108,7 @@ "ollama": "Ollama", "openai": "OpenAI", "openrouter": "OpenRouter", + "ovms": "Intel OVMS", "perplexity": "Perplexity", "ph8": "PH8", "poe": "Poe", diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList/DownloadOVMSModelPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList/DownloadOVMSModelPopup.tsx new file mode 100644 index 0000000000..0efe081a20 --- /dev/null +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList/DownloadOVMSModelPopup.tsx @@ -0,0 +1,353 @@ +import { loggerService } from '@logger' +import { TopView } from '@renderer/components/TopView' +import { Provider } from '@renderer/types' +import { AutoComplete, Button, Flex, Form, FormProps, Input, Modal, Progress, Select } from 'antd' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { useTimer } from '../../../../hooks/useTimer' + +const logger = loggerService.withContext('OVMSClient') + +interface ShowParams { + title: string + provider: Provider +} + +interface Props extends ShowParams { + resolve: (data: any) => unknown +} + +type FieldType = { + modelName: string + modelId: string + modelSource: string + task: string +} + +interface PresetModel { + modelId: string + modelName: string + modelSource: string + task: string + label: string +} + +const PRESET_MODELS: PresetModel[] = [ + { + modelId: 'OpenVINO/Qwen3-8B-int4-ov', + modelName: 'Qwen3-8B-int4-ov', + modelSource: 'https://www.modelscope.cn/models', + task: 'text_generation', + label: 'Qwen3-8B-int4-ov (Text Generation)' + }, + { + modelId: 'OpenVINO/bge-base-en-v1.5-fp16-ov', + modelName: 'bge-base-en-v1.5-fp16-ov', + modelSource: 'https://www.modelscope.cn/models', + task: 'embeddings', + label: 'bge-base-en-v1.5-fp16-ov (Embeddings)' + }, + { + modelId: 'OpenVINO/bge-reranker-base-fp16-ov', + modelName: 'bge-reranker-base-fp16-ov', + modelSource: 'https://www.modelscope.cn/models', + task: 'rerank', + label: 'bge-reranker-base-fp16-ov (Rerank)' + }, + { + modelId: 'OpenVINO/DeepSeek-R1-Distill-Qwen-7B-int4-ov', + modelName: 'DeepSeek-R1-Distill-Qwen-7B-int4-ov', + modelSource: 'https://www.modelscope.cn/models', + task: 'text_generation', + label: 'DeepSeek-R1-Distill-Qwen-7B-int4-ov (Text Generation)' + }, + { + modelId: 'OpenVINO/stable-diffusion-v1-5-int8-ov', + modelName: 'stable-diffusion-v1-5-int8-ov', + modelSource: 'https://www.modelscope.cn/models', + task: 'image_generation', + label: 'stable-diffusion-v1-5-int8-ov (Image Generation)' + }, + { + modelId: 'OpenVINO/FLUX.1-schnell-int4-ov', + modelName: 'FLUX.1-schnell-int4-ov', + modelSource: 'https://www.modelscope.cn/models', + task: 'image_generation', + label: 'FLUX.1-schnell-int4-ov (Image Generation)' + } +] + +const PopupContainer: React.FC = ({ title, resolve }) => { + const [open, setOpen] = useState(true) + const [loading, setLoading] = useState(false) + const [progress, setProgress] = useState(0) + const [cancelled, setCancelled] = useState(false) + const [form] = Form.useForm() + const { t } = useTranslation() + const { setIntervalTimer, clearIntervalTimer, setTimeoutTimer } = useTimer() + + const startFakeProgress = () => { + setProgress(0) + setIntervalTimer( + 'progress', + () => { + setProgress((prev) => { + if (prev >= 95) { + return prev // Stop at 95% until actual completion + } + // Simulate realistic download progress with slowing speed + const increment = + prev < 30 + ? Math.random() * 1 + 0.25 + : prev < 60 + ? Math.random() * 0.5 + 0.125 + : Math.random() * 0.25 + 0.03125 + + return Math.min(prev + increment, 95) + }) + }, + 500 + ) + } + + const stopFakeProgress = (complete = false) => { + clearIntervalTimer('progress') + if (complete) { + setProgress(100) + // Reset progress after a short delay + setTimeoutTimer('progress-reset', () => setProgress(0), 1500) + } else { + setProgress(0) + } + } + + const handlePresetSelect = (value: string) => { + const selectedPreset = PRESET_MODELS.find((model) => model.modelId === value) + if (selectedPreset) { + form.setFieldsValue({ + modelId: selectedPreset.modelId, + modelName: selectedPreset.modelName, + modelSource: selectedPreset.modelSource, + task: selectedPreset.task + }) + } + } + + const handleModelIdChange = (value: string) => { + if (value) { + // Extract model name from model ID (part after last '/') + const lastSlashIndex = value.lastIndexOf('/') + if (lastSlashIndex !== -1 && lastSlashIndex < value.length - 1) { + const modelName = value.substring(lastSlashIndex + 1) + form.setFieldValue('modelName', modelName) + } + } + } + + const onCancel = async () => { + if (loading) { + // Stop the download + try { + setCancelled(true) // Mark as cancelled by user + logger.info('Stopping download...') + await window.api.ovms.stopAddModel() + stopFakeProgress(false) + setLoading(false) + } catch (error) { + logger.error(`Failed to stop download: ${error}`) + } + return + } + setOpen(false) + } + + const onClose = () => { + resolve({}) + } + + const onFinish: FormProps['onFinish'] = async (values) => { + setLoading(true) + setCancelled(false) // Reset cancelled state + startFakeProgress() + try { + const { modelName, modelId, modelSource, task } = values + logger.info(`🔄 Downloading model: ${modelName} with ID: ${modelId}, source: ${modelSource}, task: ${task}`) + const result = await window.api.ovms.addModel(modelName, modelId, modelSource, task) + + if (result.success) { + stopFakeProgress(true) // Complete the progress bar + Modal.success({ + title: t('ovms.download.success'), + content: t('ovms.download.success_desc', { modelName: modelName, modelId: modelId }), + onOk: () => { + setOpen(false) + } + }) + } else { + stopFakeProgress(false) // Reset progress on error + logger.error(`Download failed, is it cancelled? ${cancelled}`) + // Only show error if not cancelled by user + if (!cancelled) { + Modal.error({ + title: t('ovms.download.error'), + content:
, + onOk: () => { + // Keep the form open for retry + } + }) + } + } + } catch (error: any) { + stopFakeProgress(false) // Reset progress on error + logger.error(`Download crashed, is it cancelled? ${cancelled}`) + // Only show error if not cancelled by user + if (!cancelled) { + Modal.error({ + title: t('ovms.download.error'), + content: error.message, + onOk: () => { + // Keep the form open for retry + } + }) + } + } finally { + setLoading(false) + } + } + + return ( + +
+ + ({ + value: model.modelId, + label: model.label + }))} + onSelect={handlePresetSelect} + onChange={handleModelIdChange} + disabled={loading} + allowClear + /> + + + + + + + + {loading && ( + + `${percent}%`} + /> +
+ {t('ovms.download.tip')} +
+
+ )} + + + + + +
+
+ ) +} + +export default class DownloadOVMSModelPopup { + static topviewId = 0 + static hide() { + TopView.hide('DownloadOVMSModelPopup') + } + static show(props: ShowParams) { + return new Promise((resolve) => { + TopView.show( + { + resolve(v) + this.hide() + }} + />, + 'DownloadOVMSModelPopup' + ) + }) + } +} diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx index 2d2af3788a..2e06cc6e73 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList/ModelList.tsx @@ -8,6 +8,7 @@ import { getProviderLabel } from '@renderer/i18n/label' import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '@renderer/pages/settings' import EditModelPopup from '@renderer/pages/settings/ProviderSettings/EditModelPopup/EditModelPopup' import AddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/AddModelPopup' +import DownloadOVMSModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/DownloadOVMSModelPopup' import ManageModelsPopup from '@renderer/pages/settings/ProviderSettings/ModelList/ManageModelsPopup' import NewApiAddModelPopup from '@renderer/pages/settings/ProviderSettings/ModelList/NewApiAddModelPopup' import { Model } from '@renderer/types' @@ -93,6 +94,11 @@ const ModelList: React.FC = ({ providerId }) => { } }, [provider, t]) + const onDownloadModel = useCallback( + () => DownloadOVMSModelPopup.show({ title: t('ovms.download.title'), provider }), + [provider, t] + ) + const isLoading = useMemo(() => displayedModelGroups === null, [displayedModelGroups]) return ( @@ -167,9 +173,15 @@ const ModelList: React.FC = ({ providerId }) => { - + {provider.id !== 'ovms' ? ( + + ) : ( + + )} ) diff --git a/src/renderer/src/pages/settings/ProviderSettings/OVMSSettings.tsx b/src/renderer/src/pages/settings/ProviderSettings/OVMSSettings.tsx new file mode 100644 index 0000000000..827e4097e0 --- /dev/null +++ b/src/renderer/src/pages/settings/ProviderSettings/OVMSSettings.tsx @@ -0,0 +1,170 @@ +import { VStack } from '@renderer/components/Layout' +import { Alert, Button } from 'antd' +import { FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { SettingRow, SettingSubtitle } from '..' + +const OVMSSettings: FC = () => { + const { t } = useTranslation() + + const [ovmsStatus, setOvmsStatus] = useState<'not-installed' | 'not-running' | 'running'>('not-running') + const [isInstallingOvms, setIsInstallingOvms] = useState(false) + const [isRunningOvms, setIsRunningOvms] = useState(false) + const [isStoppingOvms, setIsStoppingOvms] = useState(false) + + useEffect(() => { + const checkStatus = async () => { + const status = await window.api.ovms.getStatus() + setOvmsStatus(status) + } + checkStatus() + }, []) + + const installOvms = async () => { + try { + setIsInstallingOvms(true) + await window.api.installOvmsBinary() + // 安装成功后重新检查状态 + const status = await window.api.ovms.getStatus() + setOvmsStatus(status) + setIsInstallingOvms(false) + } catch (error: any) { + const errCodeMsg = { + '100': t('ovms.failed.install_code_100'), + '101': t('ovms.failed.install_code_101'), + '102': t('ovms.failed.install_code_102'), + '103': t('ovms.failed.install_code_103'), + '104': t('ovms.failed.install_code_104'), + '105': t('ovms.failed.install_code_105') + } + const match = error.message.match(/code (\d+)/) + const code = match ? match[1] : 'unknown' + const errorMsg = errCodeMsg[code as keyof typeof errCodeMsg] || error.message + + window.toast.error(t('ovms.failed.install') + errorMsg) + setIsInstallingOvms(false) + } + } + + const runOvms = async () => { + try { + setIsRunningOvms(true) + await window.api.ovms.runOvms() + // 运行成功后重新检查状态 + const status = await window.api.ovms.getStatus() + setOvmsStatus(status) + setIsRunningOvms(false) + } catch (error: any) { + window.toast.error(t('ovms.failed.run') + error.message) + setIsRunningOvms(false) + } + } + + const stopOvms = async () => { + try { + setIsStoppingOvms(true) + await window.api.ovms.stopOvms() + // 停止成功后重新检查状态 + const status = await window.api.ovms.getStatus() + setOvmsStatus(status) + setIsStoppingOvms(false) + } catch (error: any) { + window.toast.error(t('ovms.failed.stop') + error.message) + setIsStoppingOvms(false) + } + } + + const getAlertType = () => { + switch (ovmsStatus) { + case 'running': + return 'success' + case 'not-running': + return 'warning' + case 'not-installed': + return 'error' + default: + return 'warning' + } + } + + const getStatusMessage = () => { + switch (ovmsStatus) { + case 'running': + return t('ovms.status.running') + case 'not-running': + return t('ovms.status.not_running') + case 'not-installed': + return t('ovms.status.not_installed') + default: + return t('ovms.status.unknown') + } + } + + return ( + <> + + + {getStatusMessage()} + {ovmsStatus === 'not-installed' && ( + + )} + {ovmsStatus === 'not-running' && ( +
+ + +
+ )} + {ovmsStatus === 'running' && ( + + )} +
+ + } + /> +
} + showIcon + /> + + ) +} + +export default OVMSSettings diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx index d023f3d0d9..5dc53ca75a 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx @@ -27,6 +27,8 @@ import UrlSchemaInfoPopup from './UrlSchemaInfoPopup' const logger = loggerService.withContext('ProviderList') const BUTTON_WRAPPER_HEIGHT = 50 +const systemType = await window.api.system.getDeviceType() +const cpuName = await window.api.system.getCpuName() const ProviderList: FC = () => { const [searchParams, setSearchParams] = useSearchParams() @@ -273,6 +275,10 @@ const ProviderList: FC = () => { } const filteredProviders = providers.filter((provider) => { + if (provider.id === 'ovms' && (systemType !== 'windows' || !cpuName.toLowerCase().includes('intel'))) { + return false + } + const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean) const isProviderMatch = matchKeywordsInProvider(keywords, provider) const isModelMatch = provider.models.some((model) => matchKeywordsInModel(keywords, model)) diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index ea40f6d9ac..38d6f439e7 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -47,6 +47,7 @@ import DMXAPISettings from './DMXAPISettings' import GithubCopilotSettings from './GithubCopilotSettings' import GPUStackSettings from './GPUStackSettings' import LMStudioSettings from './LMStudioSettings' +import OVMSSettings from './OVMSSettings' import ProviderOAuth from './ProviderOAuth' import SelectProviderModelPopup from './SelectProviderModelPopup' import VertexAISettings from './VertexAISettings' @@ -282,6 +283,7 @@ const ProviderSetting: FC = ({ providerId }) => { {isProviderSupportAuth(provider) && } {provider.id === 'openai' && } + {provider.id === 'ovms' && } {isDmxapi && } {provider.id === 'anthropic' && ( <> diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 1c954ba27e..f1e76ed956 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2549,6 +2549,15 @@ const migrateConfig = { logger.error('migrate 158 error', error as Error) return state } + }, + '159': (state: RootState) => { + try { + addProvider(state, 'ovms') + return state + } catch (error) { + logger.error('migrate 159 error', error as Error) + return state + } } } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 2b9271d548..c3b7754e65 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -272,6 +272,7 @@ export const SystemProviderIds = { // cherryin: 'cherryin', silicon: 'silicon', aihubmix: 'aihubmix', + ovms: 'ovms', ocoolai: 'ocoolai', deepseek: 'deepseek', ppio: 'ppio', From 23f61b0d6211359571a46c0ace061e8d6c109939 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:22:25 +0800 Subject: [PATCH 02/22] feat: support gpt-5-codex (#10448) * feat(models): add gpt5_codex model support Add support for gpt5_codex model type in model configuration and type definitions. Update getThinkModelType to handle codex variant of gpt5 models. * feat(models): add gpt-5-codex model logo and update logo mapping Add new GPT-5-Codex model logo image and include it in the logo mapping configuration --- .../src/assets/images/models/gpt-5-codex.png | Bin 0 -> 25983 bytes src/renderer/src/config/models/logo.ts | 5 ++++- src/renderer/src/config/models/reasoning.ts | 9 ++++++++- src/renderer/src/types/index.ts | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/assets/images/models/gpt-5-codex.png diff --git a/src/renderer/src/assets/images/models/gpt-5-codex.png b/src/renderer/src/assets/images/models/gpt-5-codex.png new file mode 100644 index 0000000000000000000000000000000000000000..688d187349d1db41b72008b3da01ec01766253b3 GIT binary patch literal 25983 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 { let thinkingModelType: ThinkingModelType = 'default' + const modelId = getLowerBaseModelName(model.id) if (isGPT5SeriesModel(model)) { - thinkingModelType = 'gpt5' + if (modelId.includes('codex')) { + thinkingModelType = 'gpt5_codex' + } else { + thinkingModelType = 'gpt5' + } } else if (isSupportedReasoningEffortOpenAIModel(model)) { thinkingModelType = 'o' } else if (isSupportedThinkingTokenGeminiModel(model)) { diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index c3b7754e65..3f38b57749 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -76,6 +76,7 @@ const ThinkModelTypes = [ 'default', 'o', 'gpt5', + 'gpt5_codex', 'grok', 'gemini', 'gemini_pro', From d11a2cd95c4359b49047ae9e4a203c55ec087b72 Mon Sep 17 00:00:00 2001 From: MyPrototypeWhat Date: Tue, 30 Sep 2025 18:39:48 +0800 Subject: [PATCH 03/22] chore: update dependencies and versioning across packages (#10471) - Bumped versions for several @ai-sdk packages in package.json and yarn.lock to their latest releases, including @ai-sdk/amazon-bedrock, @ai-sdk/google-vertex, @ai-sdk/mistral, and @ai-sdk/perplexity. - Updated ai package version from 5.0.44 to 5.0.59. - Updated aiCore package version from 1.0.0-alpha.18 to 1.0.1 and adjusted dependencies accordingly. - Ensured compatibility with the latest zod version in multiple packages. --- package.json | 10 +- packages/aiCore/package.json | 16 +-- yarn.lock | 216 +++++++++++++++++------------------ 3 files changed, 115 insertions(+), 127 deletions(-) diff --git a/package.json b/package.json index 2e6b6251e6..907f872009 100644 --- a/package.json +++ b/package.json @@ -97,10 +97,10 @@ "@agentic/exa": "^7.3.3", "@agentic/searxng": "^7.3.3", "@agentic/tavily": "^7.3.3", - "@ai-sdk/amazon-bedrock": "^3.0.21", - "@ai-sdk/google-vertex": "^3.0.27", - "@ai-sdk/mistral": "^2.0.14", - "@ai-sdk/perplexity": "^2.0.9", + "@ai-sdk/amazon-bedrock": "^3.0.29", + "@ai-sdk/google-vertex": "^3.0.33", + "@ai-sdk/mistral": "^2.0.17", + "@ai-sdk/perplexity": "^2.0.11", "@ant-design/v5-patch-for-react-19": "^1.0.3", "@anthropic-ai/sdk": "^0.41.0", "@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch", @@ -215,7 +215,7 @@ "@viz-js/lang-dot": "^1.0.5", "@viz-js/viz": "^3.14.0", "@xyflow/react": "^12.4.4", - "ai": "^5.0.44", + "ai": "^5.0.59", "antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch", "archiver": "^7.0.1", "async-mutex": "^0.5.0", diff --git a/packages/aiCore/package.json b/packages/aiCore/package.json index 93bf7b6414..7210dcebb9 100644 --- a/packages/aiCore/package.json +++ b/packages/aiCore/package.json @@ -1,6 +1,6 @@ { "name": "@cherrystudio/ai-core", - "version": "1.0.0-alpha.18", + "version": "1.0.1", "description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK", "main": "dist/index.js", "module": "dist/index.mjs", @@ -36,14 +36,14 @@ "ai": "^5.0.26" }, "dependencies": { - "@ai-sdk/anthropic": "^2.0.17", - "@ai-sdk/azure": "^2.0.30", - "@ai-sdk/deepseek": "^1.0.17", - "@ai-sdk/openai": "^2.0.30", - "@ai-sdk/openai-compatible": "^1.0.17", + "@ai-sdk/anthropic": "^2.0.22", + "@ai-sdk/azure": "^2.0.42", + "@ai-sdk/deepseek": "^1.0.20", + "@ai-sdk/openai": "^2.0.42", + "@ai-sdk/openai-compatible": "^1.0.19", "@ai-sdk/provider": "^2.0.0", - "@ai-sdk/provider-utils": "^3.0.9", - "@ai-sdk/xai": "^2.0.18", + "@ai-sdk/provider-utils": "^3.0.10", + "@ai-sdk/xai": "^2.0.23", "zod": "^4.1.5" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 9252c911de..93029954a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -74,169 +74,157 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/amazon-bedrock@npm:^3.0.21": - version: 3.0.21 - resolution: "@ai-sdk/amazon-bedrock@npm:3.0.21" +"@ai-sdk/amazon-bedrock@npm:^3.0.29": + version: 3.0.29 + resolution: "@ai-sdk/amazon-bedrock@npm:3.0.29" dependencies: - "@ai-sdk/anthropic": "npm:2.0.17" + "@ai-sdk/anthropic": "npm:2.0.22" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" "@smithy/eventstream-codec": "npm:^4.0.1" "@smithy/util-utf8": "npm:^4.0.0" aws4fetch: "npm:^1.0.20" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/2d15baaad53e389666cede9673e2b43f5299e2cedb70f5b7afc656b7616e73775a9108c2cc1beee4644ff4c66ad41c8dd0b412373dd05caa4fc3d477c4343ea8 + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/7add02e6c13774943929bb5d568b3110f6badc6d95cb56c6d3011cafc45778e27c0133417dd7fe835e7f0b1ae7767c22a7d5e3d39f725e2aa44e2b6e47d95fb7 languageName: node linkType: hard -"@ai-sdk/anthropic@npm:2.0.17, @ai-sdk/anthropic@npm:^2.0.17": - version: 2.0.17 - resolution: "@ai-sdk/anthropic@npm:2.0.17" +"@ai-sdk/anthropic@npm:2.0.22, @ai-sdk/anthropic@npm:^2.0.22": + version: 2.0.22 + resolution: "@ai-sdk/anthropic@npm:2.0.22" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/783b6a953f3854c4303ad7c30dd56d4706486c7d1151adb17071d87933418c59c26bce53d5c26d34c4d4728eaac4a856ce49a336caed26a7216f982fea562814 + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/d922d2ff606b2429fb14c099628ba6734ef7c9b0e9225635f3faaf2d067362dea6ae0e920a35c05ccf15a01c59fef93ead5f147a9609dd3dd8c3ac18a3123b85 languageName: node linkType: hard -"@ai-sdk/azure@npm:^2.0.30": - version: 2.0.30 - resolution: "@ai-sdk/azure@npm:2.0.30" +"@ai-sdk/azure@npm:^2.0.42": + version: 2.0.42 + resolution: "@ai-sdk/azure@npm:2.0.42" dependencies: - "@ai-sdk/openai": "npm:2.0.30" + "@ai-sdk/openai": "npm:2.0.42" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/22af450e28026547badc891a627bcb3cfa2d030864089947172506810f06cfa4c74c453aabd6a0d5c05ede5ffdee381b9278772ce781eca0c7c826c7d7ae3dc3 + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/14d3d6edac691df57879a9a7efc46d5d00b6bde5b64cd62a67a7668455c341171119ae90a431e57ac37009bced19add50b3da26998376b7e56e080bc2c997c00 languageName: node linkType: hard -"@ai-sdk/deepseek@npm:^1.0.17": - version: 1.0.17 - resolution: "@ai-sdk/deepseek@npm:1.0.17" +"@ai-sdk/deepseek@npm:^1.0.20": + version: 1.0.20 + resolution: "@ai-sdk/deepseek@npm:1.0.20" dependencies: - "@ai-sdk/openai-compatible": "npm:1.0.17" + "@ai-sdk/openai-compatible": "npm:1.0.19" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/c408701343bb28ed0b3e034b8789e6de1dfd6cfc6a9b53feb68f155889e29a9fbbcf05bd99e63f60809cf05ee4b158abaccdf1cbcd9df92c0987094220a61d08 + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/e66ece8cf6371c2bac5436ed82cd1e2bb5c367fae6df60090f91cff62bf241f4df0abded99c33558013f8dc0bcc7d962f2126086eba8587ba929da50afd3d806 languageName: node linkType: hard -"@ai-sdk/gateway@npm:1.0.23": - version: 1.0.23 - resolution: "@ai-sdk/gateway@npm:1.0.23" +"@ai-sdk/gateway@npm:1.0.32": + version: 1.0.32 + resolution: "@ai-sdk/gateway@npm:1.0.32" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/b1e1a6ab63b9191075eed92c586cd927696f8997ad24f056585aee3f5fffd283d981aa6b071a2560ecda4295445b80a4cfd321fa63c06e7ac54a06bc4c84887f + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/82c98db6e4e8e235e1ff66410318ebe77cc1518ebf06d8d4757b4f30aaa3bf7075d3028816438551fef2f89e2d4c8c26e4efcd9913a06717aee1308dad3ddc30 languageName: node linkType: hard -"@ai-sdk/google-vertex@npm:^3.0.27": - version: 3.0.27 - resolution: "@ai-sdk/google-vertex@npm:3.0.27" +"@ai-sdk/google-vertex@npm:^3.0.33": + version: 3.0.33 + resolution: "@ai-sdk/google-vertex@npm:3.0.33" dependencies: - "@ai-sdk/anthropic": "npm:2.0.17" - "@ai-sdk/google": "npm:2.0.14" + "@ai-sdk/anthropic": "npm:2.0.22" + "@ai-sdk/google": "npm:2.0.17" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" google-auth-library: "npm:^9.15.0" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/7017838aef9c04c18ce9acec52eb602ee0a38d68a7496977a3898411f1ac235b2d7776011fa686084b90b0881e65c69596014e5465b8ed0d0e313b5db1f967a7 + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/d440e46f702385985a34f2260074eb41cf2516036598039c8c72d6155825114452942c3c012a181da7661341bee9a38958e5f9a53bba145b9c5dc4446411a651 languageName: node linkType: hard -"@ai-sdk/google@npm:2.0.14": - version: 2.0.14 - resolution: "@ai-sdk/google@npm:2.0.14" +"@ai-sdk/google@npm:2.0.17": + version: 2.0.17 + resolution: "@ai-sdk/google@npm:2.0.17" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/2c04839cf58c33514a54c9de8190c363b5cacfbfc8404fea5d2ec36ad0af5ced4fc571f978e7aa35876bd9afae138f4c700d2bc1f64a78a37d0401f6797bf8f3 + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/174bcde507e5bf4bf95f20dbe4eaba73870715b13779e320f3df44995606e4d7ccd1e1f4b759d224deaf58bdfc6aa2e43a24dcbe5fa335ddfe91df1b06114218 languageName: node linkType: hard -"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch": - version: 2.0.14 - resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch::version=2.0.14&hash=351f1a" +"@ai-sdk/mistral@npm:^2.0.17": + version: 2.0.17 + resolution: "@ai-sdk/mistral@npm:2.0.17" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/1ed5a0732a82b981d51f63c6241ed8ee94d5c29a842764db770305cfc2f49ab6e528cac438b5357fc7b02194104c7b76d4390a1dc1d019ace9c174b0849e0da6 + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/58a129357c93cc7f2b15b2ba6ccfb9df3fb72e06163641602ea41c858f835cd76985d66665a56e4ed3fa1eb19ca75a83ae12986d466ec41942e9bf13d558c441 languageName: node linkType: hard -"@ai-sdk/mistral@npm:^2.0.14": - version: 2.0.14 - resolution: "@ai-sdk/mistral@npm:2.0.14" +"@ai-sdk/openai-compatible@npm:1.0.19, @ai-sdk/openai-compatible@npm:^1.0.19": + version: 1.0.19 + resolution: "@ai-sdk/openai-compatible@npm:1.0.19" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/420be3a039095830aaf59b6f82c1f986ff4800ba5b9438e1dd85530026a42c9454a6e632b6a1a1839816609f4752d0a19140d8943ad78bb976fb5d6a37714e16 + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/5b7b21fb515e829c3d8a499a5760ffc035d9b8220695996110e361bd79e9928859da4ecf1ea072735bcbe4977c6dd0661f543871921692e86f8b5bfef14fe0e5 languageName: node linkType: hard -"@ai-sdk/openai-compatible@npm:1.0.17, @ai-sdk/openai-compatible@npm:^1.0.17": - version: 1.0.17 - resolution: "@ai-sdk/openai-compatible@npm:1.0.17" +"@ai-sdk/openai@npm:2.0.42, @ai-sdk/openai@npm:^2.0.42": + version: 2.0.42 + resolution: "@ai-sdk/openai@npm:2.0.42" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/53ab6111e0f44437a2e268a51fb747600844d85b0cd0d170fb87a7b68af3eb21d7728d7bbf14d71c9fcf36e7a0f94ad75f0ad6b1070e473c867ab08ef84f6564 + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/b1ab158aafc86735e53c4621ffe125d469bc1732c533193652768a9f66ecd4d169303ce7ca59069b7baf725da49e55bcf81210848f09f66deaf2a8335399e6d7 languageName: node linkType: hard -"@ai-sdk/openai@npm:2.0.30, @ai-sdk/openai@npm:^2.0.30": - version: 2.0.30 - resolution: "@ai-sdk/openai@npm:2.0.30" +"@ai-sdk/perplexity@npm:^2.0.11": + version: 2.0.11 + resolution: "@ai-sdk/perplexity@npm:2.0.11" dependencies: "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/90a57c1b10dac46c0bbe7e16cf9202557fb250d9f0e94a2a5fb7d95b5ea77815a56add78b00238d3823f0313c9b2c42abe865478d28a6196f72b341d32dd40af + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/a8722b68f529b3d1baaa1ba4624c61efe732f22b24dfc20e27afae07bb25d72532bcb62d022191ab5e49df24496af619eabc092a4e6ad293b3fe231ef61b6467 languageName: node linkType: hard -"@ai-sdk/perplexity@npm:^2.0.9": - version: 2.0.9 - resolution: "@ai-sdk/perplexity@npm:2.0.9" - dependencies: - "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" - peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/2023aadc26c41430571c4897df79074e7a95a12f2238ad57081355484066bcf9e8dfde1da60fa6af12fc9fb2a195899326f753c69f4913dc005a33367f150349 - languageName: node - linkType: hard - -"@ai-sdk/provider-utils@npm:3.0.9, @ai-sdk/provider-utils@npm:^3.0.9": - version: 3.0.9 - resolution: "@ai-sdk/provider-utils@npm:3.0.9" +"@ai-sdk/provider-utils@npm:3.0.10, @ai-sdk/provider-utils@npm:^3.0.10": + version: 3.0.10 + resolution: "@ai-sdk/provider-utils@npm:3.0.10" dependencies: "@ai-sdk/provider": "npm:2.0.0" "@standard-schema/spec": "npm:^1.0.0" eventsource-parser: "npm:^3.0.5" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/f8b659343d7e22ae099f7b6fc514591c0408012eb0aa00f7a912798b6d7d7305cafa8f18a07c7adec0bb5d39d9b6256b76d65c5393c3fc843d1361c52f1f8080 + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/d2c16abdb84ba4ef48c9f56190b5ffde224b9e6ae5147c5c713d2623627732d34b96aa9aef2a2ea4b0c49e1b863cc963c7d7ff964a1dc95f0f036097aaaaaa98 languageName: node linkType: hard @@ -249,16 +237,16 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/xai@npm:^2.0.18": - version: 2.0.18 - resolution: "@ai-sdk/xai@npm:2.0.18" +"@ai-sdk/xai@npm:^2.0.23": + version: 2.0.23 + resolution: "@ai-sdk/xai@npm:2.0.23" dependencies: - "@ai-sdk/openai-compatible": "npm:1.0.17" + "@ai-sdk/openai-compatible": "npm:1.0.19" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/7134501a2d315ec13605558aa24d7f5662885fe8b0491a634abefeb0c5c88517149677d1beff0c8abeec78a6dcd14573a2f57d96fa54a1d63d03820ac7ff827a + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/4cf6b3bc71024797d1b2e37b57fb746f7387f9a7c1da530fd040aad1a840603a1a86fb7df7e428c723eba9b1547f89063d68f84e6e08444d2d4f152dee321dc3 languageName: node linkType: hard @@ -2325,14 +2313,14 @@ __metadata: version: 0.0.0-use.local resolution: "@cherrystudio/ai-core@workspace:packages/aiCore" dependencies: - "@ai-sdk/anthropic": "npm:^2.0.17" - "@ai-sdk/azure": "npm:^2.0.30" - "@ai-sdk/deepseek": "npm:^1.0.17" - "@ai-sdk/openai": "npm:^2.0.30" - "@ai-sdk/openai-compatible": "npm:^1.0.17" + "@ai-sdk/anthropic": "npm:^2.0.22" + "@ai-sdk/azure": "npm:^2.0.42" + "@ai-sdk/deepseek": "npm:^1.0.20" + "@ai-sdk/openai": "npm:^2.0.42" + "@ai-sdk/openai-compatible": "npm:^1.0.19" "@ai-sdk/provider": "npm:^2.0.0" - "@ai-sdk/provider-utils": "npm:^3.0.9" - "@ai-sdk/xai": "npm:^2.0.18" + "@ai-sdk/provider-utils": "npm:^3.0.10" + "@ai-sdk/xai": "npm:^2.0.23" tsdown: "npm:^0.12.9" typescript: "npm:^5.0.0" vitest: "npm:^3.2.4" @@ -13195,10 +13183,10 @@ __metadata: "@agentic/exa": "npm:^7.3.3" "@agentic/searxng": "npm:^7.3.3" "@agentic/tavily": "npm:^7.3.3" - "@ai-sdk/amazon-bedrock": "npm:^3.0.21" - "@ai-sdk/google-vertex": "npm:^3.0.27" - "@ai-sdk/mistral": "npm:^2.0.14" - "@ai-sdk/perplexity": "npm:^2.0.9" + "@ai-sdk/amazon-bedrock": "npm:^3.0.29" + "@ai-sdk/google-vertex": "npm:^3.0.33" + "@ai-sdk/mistral": "npm:^2.0.17" + "@ai-sdk/perplexity": "npm:^2.0.11" "@ant-design/v5-patch-for-react-19": "npm:^1.0.3" "@anthropic-ai/sdk": "npm:^0.41.0" "@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch" @@ -13317,7 +13305,7 @@ __metadata: "@viz-js/lang-dot": "npm:^1.0.5" "@viz-js/viz": "npm:^3.14.0" "@xyflow/react": "npm:^12.4.4" - ai: "npm:^5.0.44" + ai: "npm:^5.0.59" antd: "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch" archiver: "npm:^7.0.1" async-mutex: "npm:^0.5.0" @@ -13575,17 +13563,17 @@ __metadata: languageName: node linkType: hard -"ai@npm:^5.0.44": - version: 5.0.44 - resolution: "ai@npm:5.0.44" +"ai@npm:^5.0.59": + version: 5.0.59 + resolution: "ai@npm:5.0.59" dependencies: - "@ai-sdk/gateway": "npm:1.0.23" + "@ai-sdk/gateway": "npm:1.0.32" "@ai-sdk/provider": "npm:2.0.0" - "@ai-sdk/provider-utils": "npm:3.0.9" + "@ai-sdk/provider-utils": "npm:3.0.10" "@opentelemetry/api": "npm:1.9.0" peerDependencies: - zod: ^3.25.76 || ^4 - checksum: 10c0/528c7e165f75715194204051ce0aa341d8dca7d5536c2abcf3df83ccda7399ed5d91deaa45a81340f93d2461b1c2fc5f740f7804dfd396927c71b0667403569b + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/daa956e753b93fbc30afbfba5be2ebb73e3c280dae3064e13949f04d5a22c0f4ea5698cc87e24a23ed6585d9cf7febee61b915292dbbd4286dc40c449cf2b845 languageName: node linkType: hard From 38ac42af8c52de5f85809f955835ac8cf1dea82b Mon Sep 17 00:00:00 2001 From: LeaderOnePro Date: Tue, 30 Sep 2025 23:43:19 +0800 Subject: [PATCH 04/22] feat: add GitHub Copilot CLI integration to coding tools (#10403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add GitHub Copilot CLI integration to coding tools - Add githubCopilotCli to codeTools enum - Support @github/copilot package installation - Add 'copilot' executable command mapping - Update Redux store to include GitHub Copilot CLI state - Add GitHub Copilot CLI option to UI with proper provider mapping - Implement environment variable handling for GitHub authentication - Fix model selection logic to disable model choice for GitHub Copilot CLI - Update launch validation to not require model selection for GitHub Copilot CLI - Fix prepareLaunchEnvironment and executeLaunch to handle no-model scenario This enables users to launch GitHub Copilot CLI directly from Cherry Studio's code tools interface without needing to select a model, as GitHub Copilot CLI uses GitHub's built-in models and authentication. Signed-off-by: LeaderOnePro * style: apply code formatting for GitHub Copilot CLI integration Auto-fix code style inconsistencies using project's Biome formatter. Resolves semicolon, comma, and quote style issues to match project standards. Signed-off-by: LeaderOnePro * feat: conditionally render model selector for GitHub Copilot CLI - Hide model selector component when GitHub Copilot CLI is selected - Maintain validation logic to allow GitHub Copilot CLI without model selection - Improve UX by removing empty model dropdown for GitHub Copilot CLI 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --------- Signed-off-by: LeaderOnePro Co-authored-by: Claude --- packages/shared/config/constant.ts | 3 +- src/main/services/CodeToolsService.ts | 22 ++- src/renderer/src/hooks/useCodeTools.ts | 6 +- src/renderer/src/pages/code/CodeToolsPage.tsx | 156 ++++++++++++------ src/renderer/src/pages/code/index.ts | 10 +- src/renderer/src/store/codeTools.ts | 14 +- 6 files changed, 153 insertions(+), 58 deletions(-) diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index 3ffe88f08a..3b38592005 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -217,7 +217,8 @@ export enum codeTools { claudeCode = 'claude-code', geminiCli = 'gemini-cli', openaiCodex = 'openai-codex', - iFlowCli = 'iflow-cli' + iFlowCli = 'iflow-cli', + githubCopilotCli = 'github-copilot-cli' } export enum terminalApps { diff --git a/src/main/services/CodeToolsService.ts b/src/main/services/CodeToolsService.ts index 486e58c212..d6eea8a9e6 100644 --- a/src/main/services/CodeToolsService.ts +++ b/src/main/services/CodeToolsService.ts @@ -31,7 +31,10 @@ interface VersionInfo { class CodeToolsService { private versionCache: Map = new Map() - private terminalsCache: { terminals: TerminalConfig[]; timestamp: number } | null = null + private terminalsCache: { + terminals: TerminalConfig[] + timestamp: number + } | null = null private customTerminalPaths: Map = new Map() // Store user-configured terminal paths private readonly CACHE_DURATION = 1000 * 60 * 30 // 30 minutes cache private readonly TERMINALS_CACHE_DURATION = 1000 * 60 * 5 // 5 minutes cache for terminals @@ -82,6 +85,8 @@ class CodeToolsService { return '@qwen-code/qwen-code' case codeTools.iFlowCli: return '@iflow-ai/iflow-cli' + case codeTools.githubCopilotCli: + return '@github/copilot' default: throw new Error(`Unsupported CLI tool: ${cliTool}`) } @@ -99,6 +104,8 @@ class CodeToolsService { return 'qwen' case codeTools.iFlowCli: return 'iflow' + case codeTools.githubCopilotCli: + return 'copilot' default: throw new Error(`Unsupported CLI tool: ${cliTool}`) } @@ -144,7 +151,9 @@ class CodeToolsService { case terminalApps.powershell: // Check for PowerShell in PATH try { - await execAsync('powershell -Command "Get-Host"', { timeout: 3000 }) + await execAsync('powershell -Command "Get-Host"', { + timeout: 3000 + }) return terminal } catch { try { @@ -384,7 +393,9 @@ class CodeToolsService { const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) - const { stdout } = await execAsync(`"${executablePath}" --version`, { timeout: 10000 }) + const { stdout } = await execAsync(`"${executablePath}" --version`, { + timeout: 10000 + }) // Extract version number from output (format may vary by tool) const versionMatch = stdout.trim().match(/\d+\.\d+\.\d+/) installedVersion = versionMatch ? versionMatch[0] : stdout.trim().split(' ')[0] @@ -425,7 +436,10 @@ class CodeToolsService { logger.info(`${packageName} latest version: ${latestVersion}`) // Cache the result - this.versionCache.set(cacheKey, { version: latestVersion!, timestamp: now }) + this.versionCache.set(cacheKey, { + version: latestVersion!, + timestamp: now + }) logger.debug(`Cached latest version for ${packageName}`) } catch (error) { logger.warn(`Failed to get latest version for ${packageName}:`, error as Error) diff --git a/src/renderer/src/hooks/useCodeTools.ts b/src/renderer/src/hooks/useCodeTools.ts index 4d1527ed98..44ffd29d9c 100644 --- a/src/renderer/src/hooks/useCodeTools.ts +++ b/src/renderer/src/hooks/useCodeTools.ts @@ -108,7 +108,11 @@ export const useCodeTools = () => { const environmentVariables = codeToolsState?.environmentVariables?.[codeToolsState.selectedCliTool] || '' // 检查是否可以启动(所有必需字段都已填写) - const canLaunch = Boolean(codeToolsState.selectedCliTool && selectedModel && codeToolsState.currentDirectory) + const canLaunch = Boolean( + codeToolsState.selectedCliTool && + codeToolsState.currentDirectory && + (codeToolsState.selectedCliTool === codeTools.githubCopilotCli || selectedModel) + ) return { // 状态 diff --git a/src/renderer/src/pages/code/CodeToolsPage.tsx b/src/renderer/src/pages/code/CodeToolsPage.tsx index b64833f6d6..06f2ef064b 100644 --- a/src/renderer/src/pages/code/CodeToolsPage.tsx +++ b/src/renderer/src/pages/code/CodeToolsPage.tsx @@ -98,6 +98,10 @@ const CodeToolsPage: FC = () => { return m.id.includes('openai') || OPENAI_CODEX_SUPPORTED_PROVIDERS.includes(m.provider) } + if (selectedCliTool === codeTools.githubCopilotCli) { + return false + } + if (selectedCliTool === codeTools.qwenCode || selectedCliTool === codeTools.iFlowCli) { if (m.supported_endpoint_types) { return ['openai', 'openai-response'].some((type) => @@ -196,7 +200,7 @@ const CodeToolsPage: FC = () => { } } - if (!selectedModel) { + if (!selectedModel && selectedCliTool !== codeTools.githubCopilotCli) { return { isValid: false, message: t('code.model_required') } } @@ -205,6 +209,11 @@ const CodeToolsPage: FC = () => { // 准备启动环境 const prepareLaunchEnvironment = async (): Promise | null> => { + if (selectedCliTool === codeTools.githubCopilotCli) { + const userEnv = parseEnvironmentVariables(environmentVariables) + return userEnv + } + if (!selectedModel) return null const modelProvider = getProviderByModel(selectedModel) @@ -229,7 +238,9 @@ const CodeToolsPage: FC = () => { // 执行启动操作 const executeLaunch = async (env: Record) => { - window.api.codeTools.run(selectedCliTool, selectedModel?.id!, currentDirectory, env, { + const modelId = selectedCliTool === codeTools.githubCopilotCli ? '' : selectedModel?.id! + + window.api.codeTools.run(selectedCliTool, modelId, currentDirectory, env, { autoUpdateToLatest, terminal: selectedTerminal }) @@ -316,7 +327,12 @@ const CodeToolsPage: FC = () => { banner style={{ borderRadius: 'var(--list-item-border-radius)' }} message={ -
+
{t('code.bun_required_message')}
+ + + )}
{t('code.working_directory')}
@@ -403,11 +437,27 @@ const CodeToolsPage: FC = () => { options={directories.map((dir) => ({ value: dir, label: ( -
- {dir} +
+ + {dir} + handleRemoveDirectory(dir, e)} />
@@ -429,7 +479,14 @@ const CodeToolsPage: FC = () => { rows={2} style={{ fontFamily: 'monospace' }} /> -
{t('code.env_vars_help')}
+
+ {t('code.env_vars_help')} +
{/* 终端选择 (macOS 和 Windows) */} @@ -464,7 +521,12 @@ const CodeToolsPage: FC = () => { selectedTerminal !== terminalApps.cmd && selectedTerminal !== terminalApps.powershell && selectedTerminal !== terminalApps.windowsTerminal && ( -
+
{terminalCustomPaths[selectedTerminal] ? `${t('code.custom_path')}: ${terminalCustomPaths[selectedTerminal]}` : t('code.custom_path_required')} diff --git a/src/renderer/src/pages/code/index.ts b/src/renderer/src/pages/code/index.ts index 531a7f5f01..06c7991039 100644 --- a/src/renderer/src/pages/code/index.ts +++ b/src/renderer/src/pages/code/index.ts @@ -20,7 +20,8 @@ export const CLI_TOOLS = [ { value: codeTools.qwenCode, label: 'Qwen Code' }, { value: codeTools.geminiCli, label: 'Gemini CLI' }, { value: codeTools.openaiCodex, label: 'OpenAI Codex' }, - { value: codeTools.iFlowCli, label: 'iFlow CLI' } + { value: codeTools.iFlowCli, label: 'iFlow CLI' }, + { value: codeTools.githubCopilotCli, label: 'GitHub Copilot CLI' } ] export const GEMINI_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api', 'cherryin'] @@ -43,7 +44,8 @@ export const CLI_TOOL_PROVIDER_MAP: Record Pr [codeTools.qwenCode]: (providers) => providers.filter((p) => p.type.includes('openai')), [codeTools.openaiCodex]: (providers) => providers.filter((p) => p.id === 'openai' || OPENAI_CODEX_SUPPORTED_PROVIDERS.includes(p.id)), - [codeTools.iFlowCli]: (providers) => providers.filter((p) => p.type.includes('openai')) + [codeTools.iFlowCli]: (providers) => providers.filter((p) => p.type.includes('openai')), + [codeTools.githubCopilotCli]: () => [] } export const getCodeToolsApiBaseUrl = (model: Model, type: EndpointType) => { @@ -158,6 +160,10 @@ export const generateToolEnvironment = ({ env.IFLOW_BASE_URL = baseUrl env.IFLOW_MODEL_NAME = model.id break + + case codeTools.githubCopilotCli: + env.GITHUB_TOKEN = apiKey || '' + break } return env diff --git a/src/renderer/src/store/codeTools.ts b/src/renderer/src/store/codeTools.ts index 471a31113e..fd23d9fff8 100644 --- a/src/renderer/src/store/codeTools.ts +++ b/src/renderer/src/store/codeTools.ts @@ -26,12 +26,17 @@ export const initialState: CodeToolsState = { [codeTools.qwenCode]: null, [codeTools.claudeCode]: null, [codeTools.geminiCli]: null, - [codeTools.openaiCodex]: null + [codeTools.openaiCodex]: null, + [codeTools.iFlowCli]: null, + [codeTools.githubCopilotCli]: null }, environmentVariables: { 'qwen-code': '', 'claude-code': '', - 'gemini-cli': '' + 'gemini-cli': '', + 'openai-codex': '', + 'iflow-cli': '', + 'github-copilot-cli': '' }, directories: [], currentDirectory: '', @@ -63,7 +68,10 @@ const codeToolsSlice = createSlice({ state.environmentVariables = { 'qwen-code': '', 'claude-code': '', - 'gemini-cli': '' + 'gemini-cli': '', + 'openai-codex': '', + 'iflow-cli': '', + 'github-copilot-cli': '' } } state.environmentVariables[state.selectedCliTool] = action.payload From 932b1d529a7a351be3b58d3490be7d840411c7b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:15:14 +0800 Subject: [PATCH 05/22] ci(deps): bump actions/setup-node from 4 to 5 (#10478) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-i18n.yml | 2 +- .github/workflows/nightly-build.yml | 2 +- .github/workflows/pr-ci.yml | 2 +- .github/workflows/release.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto-i18n.yml b/.github/workflows/auto-i18n.yml index 140d6208fc..204d8ac437 100644 --- a/.github/workflows/auto-i18n.yml +++ b/.github/workflows/auto-i18n.yml @@ -26,7 +26,7 @@ jobs: ref: ${{ github.event.pull_request.head.ref }} - name: 📦 Setting Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 20 diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 42d0d66150..9e1608b13e 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -56,7 +56,7 @@ jobs: ref: main - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 20 diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 4f462db95c..9108d71fc1 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v5 - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 20 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ca1eb0146..c54504de07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: npm version "$VERSION" --no-git-tag-version --allow-same-version - name: Install Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 20 From 1e4902b267e283cc84ef58496b1a79f63fdde26b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:15:38 +0800 Subject: [PATCH 06/22] ci(deps): bump actions/checkout from 4 to 5 (#10479) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/claude-code-review.yml | 2 +- .github/workflows/claude-translator.yml | 2 +- .github/workflows/claude.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 5ed414b1fe..cc6d28817f 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/.github/workflows/claude-translator.yml b/.github/workflows/claude-translator.yml index ff317f8532..c474afeb8e 100644 --- a/.github/workflows/claude-translator.yml +++ b/.github/workflows/claude-translator.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index ba1fcb97aa..82c7b4393b 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -37,7 +37,7 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 From 74db4c46466db3e7ebbd670b99dc5d99aac6e185 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:16:02 +0800 Subject: [PATCH 07/22] ci(deps): bump actions/github-script from 7 to 8 (#10480) Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/delete-branch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/delete-branch.yml b/.github/workflows/delete-branch.yml index fae32c7477..033ab4bfa0 100644 --- a/.github/workflows/delete-branch.yml +++ b/.github/workflows/delete-branch.yml @@ -12,7 +12,7 @@ jobs: if: github.event.pull_request.merged == true && github.event.pull_request.head.repo.full_name == github.repository steps: - name: Delete merged branch - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | github.rest.git.deleteRef({ From f91e7da0a1fbd2ef794543b732171e5affb26619 Mon Sep 17 00:00:00 2001 From: Tristan Zhang <82869104+ABucket@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:09:11 +0800 Subject: [PATCH 08/22] feat: add notes export (#10488) * feat: add notes export * chore: fix lint error * feat: unified export interface for notes * fix: hide export reasoning when exporting notes * chore: fix lint error * chore: remove debug log --- .../src/components/ObsidianExportDialog.tsx | 17 +++--- .../components/Popups/ObsidianExportPopup.tsx | 2 + src/renderer/src/pages/notes/NotesSidebar.tsx | 53 +++++++++++++++++-- src/renderer/src/utils/export.ts | 48 +++++++++++++++++ 4 files changed, 111 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/components/ObsidianExportDialog.tsx b/src/renderer/src/components/ObsidianExportDialog.tsx index 0c3d1c0038..b55105c599 100644 --- a/src/renderer/src/components/ObsidianExportDialog.tsx +++ b/src/renderer/src/components/ObsidianExportDialog.tsx @@ -38,6 +38,7 @@ interface PopupContainerProps { message?: Message messages?: Message[] topic?: Topic + rawContent?: string } // 转换文件信息数组为树形结构 @@ -140,7 +141,8 @@ const PopupContainer: React.FC = ({ resolve, message, messages, - topic + topic, + rawContent }) => { const defaultObsidianVault = store.getState().settings.defaultObsidianVault const [state, setState] = useState({ @@ -229,7 +231,9 @@ const PopupContainer: React.FC = ({ return } let markdown = '' - if (topic) { + if (rawContent) { + markdown = rawContent + } else if (topic) { markdown = await topicToMarkdown(topic, exportReasoning) } else if (messages && messages.length > 0) { markdown = messagesToMarkdown(messages, exportReasoning) @@ -299,7 +303,6 @@ const PopupContainer: React.FC = ({ } } } - return ( = ({ - - - + {!rawContent && ( + + + + )} ) diff --git a/src/renderer/src/components/Popups/ObsidianExportPopup.tsx b/src/renderer/src/components/Popups/ObsidianExportPopup.tsx index aec5fcbaa8..9a00e311cf 100644 --- a/src/renderer/src/components/Popups/ObsidianExportPopup.tsx +++ b/src/renderer/src/components/Popups/ObsidianExportPopup.tsx @@ -9,6 +9,7 @@ interface ObsidianExportOptions { topic?: Topic message?: Message messages?: Message[] + rawContent?: string } export default class ObsidianExportPopup { @@ -24,6 +25,7 @@ export default class ObsidianExportPopup { topic={options.topic} message={options.message} messages={options.messages} + rawContent={options.rawContent} obsidianTags={''} open={true} resolve={(v) => { diff --git a/src/renderer/src/pages/notes/NotesSidebar.tsx b/src/renderer/src/pages/notes/NotesSidebar.tsx index 4588c37611..54a3f80cdb 100644 --- a/src/renderer/src/pages/notes/NotesSidebar.tsx +++ b/src/renderer/src/pages/notes/NotesSidebar.tsx @@ -6,11 +6,13 @@ import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useActiveNode } from '@renderer/hooks/useNotesQuery' import NotesSidebarHeader from '@renderer/pages/notes/NotesSidebarHeader' -import { useAppSelector } from '@renderer/store' +import { RootState, useAppSelector } from '@renderer/store' import { selectSortType } from '@renderer/store/note' import { NotesSortType, NotesTreeNode } from '@renderer/types/note' +import { exportNote } from '@renderer/utils/export' import { useVirtualizer } from '@tanstack/react-virtual' import { Dropdown, Input, InputRef, MenuProps } from 'antd' +import { ItemType, MenuItemType } from 'antd/es/menu/interface' import { ChevronDown, ChevronRight, @@ -21,10 +23,12 @@ import { Folder, FolderOpen, Star, - StarOff + StarOff, + UploadIcon } from 'lucide-react' import { FC, memo, Ref, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' import styled from 'styled-components' interface NotesSidebarProps { @@ -213,6 +217,7 @@ const NotesSidebar: FC = ({ const { bases } = useKnowledgeBases() const { activeNode } = useActiveNode(notesTree) const sortType = useAppSelector(selectSortType) + const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions) const [editingNodeId, setEditingNodeId] = useState(null) const [draggedNodeId, setDraggedNodeId] = useState(null) const [dragOverNodeId, setDragOverNodeId] = useState(null) @@ -525,6 +530,48 @@ const NotesSidebar: FC = ({ onClick: () => { handleExportKnowledge(node) } + }, + { + label: t('chat.topics.export.title'), + key: 'export', + icon: , + children: [ + exportMenuOptions.markdown && { + label: t('chat.topics.export.md.label'), + key: 'markdown', + onClick: () => exportNote({ node, platform: 'markdown' }) + }, + exportMenuOptions.docx && { + label: t('chat.topics.export.word'), + key: 'word', + onClick: () => exportNote({ node, platform: 'docx' }) + }, + exportMenuOptions.notion && { + label: t('chat.topics.export.notion'), + key: 'notion', + onClick: () => exportNote({ node, platform: 'notion' }) + }, + exportMenuOptions.yuque && { + label: t('chat.topics.export.yuque'), + key: 'yuque', + onClick: () => exportNote({ node, platform: 'yuque' }) + }, + exportMenuOptions.obsidian && { + label: t('chat.topics.export.obsidian'), + key: 'obsidian', + onClick: () => exportNote({ node, platform: 'obsidian' }) + }, + exportMenuOptions.joplin && { + label: t('chat.topics.export.joplin'), + key: 'joplin', + onClick: () => exportNote({ node, platform: 'joplin' }) + }, + exportMenuOptions.siyuan && { + label: t('chat.topics.export.siyuan'), + key: 'siyuan', + onClick: () => exportNote({ node, platform: 'siyuan' }) + } + ].filter(Boolean) as ItemType[] } ) } @@ -543,7 +590,7 @@ const NotesSidebar: FC = ({ return baseMenuItems }, - [t, handleStartEdit, onToggleStar, handleExportKnowledge, handleDeleteNode] + [t, handleStartEdit, onToggleStar, handleExportKnowledge, handleDeleteNode, exportMenuOptions] ) const handleDropFiles = useCallback( diff --git a/src/renderer/src/utils/export.ts b/src/renderer/src/utils/export.ts index f3ce321d63..d50b5b0e5d 100644 --- a/src/renderer/src/utils/export.ts +++ b/src/renderer/src/utils/export.ts @@ -1082,3 +1082,51 @@ export const exportTopicToNotes = async (topic: Topic, folderPath: string): Prom throw error } } + +const exportNoteAsMarkdown = async (noteName: string, content: string): Promise => { + const markdown = `# ${noteName}\n\n${content}` + const fileName = removeSpecialCharactersForFileName(noteName) + '.md' + const result = await window.api.file.save(fileName, markdown) + if (result) { + window.toast.success(i18n.t('message.success.markdown.export.specified')) + } +} + +interface NoteExportOptions { + node: { name: string; externalPath: string } + platform: 'markdown' | 'docx' | 'notion' | 'yuque' | 'obsidian' | 'joplin' | 'siyuan' +} + +export const exportNote = async ({ node, platform }: NoteExportOptions): Promise => { + try { + const content = await window.api.file.readExternal(node.externalPath) + + switch (platform) { + case 'markdown': + return await exportNoteAsMarkdown(node.name, content) + case 'docx': + window.api.export.toWord(`# ${node.name}\n\n${content}`, removeSpecialCharactersForFileName(node.name)) + return + case 'notion': + await exportMessageToNotion(node.name, content) + return + case 'yuque': + await exportMarkdownToYuque(node.name, `# ${node.name}\n\n${content}`) + return + case 'obsidian': { + const { default: ObsidianExportPopup } = await import('@renderer/components/Popups/ObsidianExportPopup') + await ObsidianExportPopup.show({ title: node.name, processingMethod: '1', rawContent: content }) + return + } + case 'joplin': + await exportMarkdownToJoplin(node.name, content) + return + case 'siyuan': + await exportMarkdownToSiyuan(node.name, `# ${node.name}\n\n${content}`) + return + } + } catch (error) { + logger.error(`Failed to export note to ${platform}:`, error as Error) + throw error + } +} From 53e38ed1aa8bf04d38f1d6a6cfbeb0855d376afb Mon Sep 17 00:00:00 2001 From: purefkh Date: Thu, 2 Oct 2025 17:45:42 +0800 Subject: [PATCH 09/22] feat(models): update Gemini regex (#10463) * feat(models): update Gemini regex * fix: lint * fix format --- src/renderer/src/config/models/reasoning.ts | 6 +++++- src/renderer/src/config/models/vision.ts | 1 + src/renderer/src/config/models/websearch.ts | 7 +++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/config/models/reasoning.ts b/src/renderer/src/config/models/reasoning.ts index 3f55f13a3a..545d759363 100644 --- a/src/renderer/src/config/models/reasoning.ts +++ b/src/renderer/src/config/models/reasoning.ts @@ -178,9 +178,13 @@ export function isGeminiReasoningModel(model?: Model): boolean { return false } +// Gemini 支持思考模式的模型正则 +export const GEMINI_THINKING_MODEL_REGEX = + /gemini-(?:2\.5.*(?:-latest)?|flash-latest|pro-latest|flash-lite-latest)(?:-[\w-]+)*$/i + export const isSupportedThinkingTokenGeminiModel = (model: Model): boolean => { const modelId = getLowerBaseModelName(model.id, '/') - if (modelId.includes('gemini-2.5')) { + if (GEMINI_THINKING_MODEL_REGEX.test(modelId)) { if (modelId.includes('image') || modelId.includes('tts')) { return false } diff --git a/src/renderer/src/config/models/vision.ts b/src/renderer/src/config/models/vision.ts index 02ed3cd6fc..85eca139c4 100644 --- a/src/renderer/src/config/models/vision.ts +++ b/src/renderer/src/config/models/vision.ts @@ -12,6 +12,7 @@ const visionAllowedModels = [ 'gemini-1\\.5', 'gemini-2\\.0', 'gemini-2\\.5', + 'gemini-(flash|pro|flash-lite)-latest', 'gemini-exp', 'claude-3', 'claude-sonnet-4', diff --git a/src/renderer/src/config/models/websearch.ts b/src/renderer/src/config/models/websearch.ts index 4acc8a7836..ecdf1bec37 100644 --- a/src/renderer/src/config/models/websearch.ts +++ b/src/renderer/src/config/models/websearch.ts @@ -11,9 +11,12 @@ export const CLAUDE_SUPPORTED_WEBSEARCH_REGEX = new RegExp( 'i' ) -export const GEMINI_FLASH_MODEL_REGEX = new RegExp('gemini-.*-flash.*$') +export const GEMINI_FLASH_MODEL_REGEX = new RegExp('gemini.*-flash.*$') -export const GEMINI_SEARCH_REGEX = new RegExp('gemini-2\\..*', 'i') +export const GEMINI_SEARCH_REGEX = new RegExp( + 'gemini-(?:2.*(?:-latest)?|flash-latest|pro-latest|flash-lite-latest)(?:-[\\w-]+)*$', + 'i' +) export const PERPLEXITY_SEARCH_MODELS = [ 'sonar-pro', From e7e5c0456f7fab4d289cd23c53cf4bc0d61c420d Mon Sep 17 00:00:00 2001 From: Tristan Zhang <82869104+ABucket@users.noreply.github.com> Date: Thu, 2 Oct 2025 20:45:46 +0800 Subject: [PATCH 10/22] feat: allowing notes to be renamed using LLM (#10487) * feat: implement auto-renaming feature for notes * feat: motion effects for auto renaming in notes * feat: add i18n for zh-tw for auto renaming in notes * chore: lint --- src/renderer/src/i18n/locales/en-us.json | 6 + src/renderer/src/i18n/locales/zh-cn.json | 6 + src/renderer/src/i18n/locales/zh-tw.json | 6 + src/renderer/src/pages/notes/NotesSidebar.tsx | 137 +++++++++++++++++- src/renderer/src/services/ApiService.ts | 62 ++++++++ 5 files changed, 213 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 94abc7667d..0f3d2a3f24 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1697,6 +1697,12 @@ "provider_settings": "Go to provider settings" }, "notes": { + "auto_rename": { + "empty_note": "Note is empty, cannot generate name", + "failed": "Failed to generate note name", + "label": "Generate Note Name", + "success": "Note name generated successfully" + }, "characters": "Characters", "collapse": "Collapse", "content_placeholder": "Please enter the note content...", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index c09c94cbbf..ce2dc5c222 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1697,6 +1697,12 @@ "provider_settings": "跳转到服务商设置界面" }, "notes": { + "auto_rename": { + "empty_note": "笔记为空,无法生成名称", + "failed": "生成笔记名称失败", + "label": "生成笔记名称", + "success": "笔记名称生成成功" + }, "characters": "字符", "collapse": "收起", "content_placeholder": "请输入笔记内容...", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index a1aa1cc7fa..e4f45288ee 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1697,6 +1697,12 @@ "provider_settings": "跳轉到服務商設置界面" }, "notes": { + "auto_rename": { + "empty_note": "筆記為空,無法生成名稱", + "failed": "生成筆記名稱失敗", + "label": "生成筆記名稱", + "success": "筆記名稱生成成功" + }, "characters": "字符", "collapse": "收起", "content_placeholder": "請輸入筆記內容...", diff --git a/src/renderer/src/pages/notes/NotesSidebar.tsx b/src/renderer/src/pages/notes/NotesSidebar.tsx index 54a3f80cdb..a53fc5b5f7 100644 --- a/src/renderer/src/pages/notes/NotesSidebar.tsx +++ b/src/renderer/src/pages/notes/NotesSidebar.tsx @@ -6,6 +6,7 @@ import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useActiveNode } from '@renderer/hooks/useNotesQuery' import NotesSidebarHeader from '@renderer/pages/notes/NotesSidebarHeader' +import { fetchNoteSummary } from '@renderer/services/ApiService' import { RootState, useAppSelector } from '@renderer/store' import { selectSortType } from '@renderer/store/note' import { NotesSortType, NotesTreeNode } from '@renderer/types/note' @@ -22,6 +23,7 @@ import { FileSearch, Folder, FolderOpen, + Sparkles, Star, StarOff, UploadIcon @@ -54,6 +56,8 @@ interface TreeNodeProps { selectedFolderId?: string | null activeNodeId?: string editingNodeId: string | null + renamingNodeIds: Set + newlyRenamedNodeIds: Set draggedNodeId: string | null dragOverNodeId: string | null dragPosition: 'before' | 'inside' | 'after' @@ -76,6 +80,8 @@ const TreeNode = memo( selectedFolderId, activeNodeId, editingNodeId, + renamingNodeIds, + newlyRenamedNodeIds, draggedNodeId, dragOverNodeId, dragPosition, @@ -96,6 +102,8 @@ const TreeNode = memo( ? node.type === 'folder' && node.id === selectedFolderId : node.id === activeNodeId const isEditing = editingNodeId === node.id && inPlaceEdit.isEditing + const isRenaming = renamingNodeIds.has(node.id) + const isNewlyRenamed = newlyRenamedNodeIds.has(node.id) const hasChildren = node.children && node.children.length > 0 const isDragging = draggedNodeId === node.id const isDragOver = dragOverNodeId === node.id @@ -103,6 +111,12 @@ const TreeNode = memo( const isDragInside = isDragOver && dragPosition === 'inside' const isDragAfter = isDragOver && dragPosition === 'after' + const getNodeNameClassName = () => { + if (isRenaming) return 'shimmer' + if (isNewlyRenamed) return 'typing' + return '' + } + return (
@@ -160,7 +174,7 @@ const TreeNode = memo( size="small" /> ) : ( - {node.name} + {node.name} )} @@ -177,6 +191,8 @@ const TreeNode = memo( selectedFolderId={selectedFolderId} activeNodeId={activeNodeId} editingNodeId={editingNodeId} + renamingNodeIds={renamingNodeIds} + newlyRenamedNodeIds={newlyRenamedNodeIds} draggedNodeId={draggedNodeId} dragOverNodeId={dragOverNodeId} dragPosition={dragPosition} @@ -219,6 +235,8 @@ const NotesSidebar: FC = ({ const sortType = useAppSelector(selectSortType) const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions) const [editingNodeId, setEditingNodeId] = useState(null) + const [renamingNodeIds, setRenamingNodeIds] = useState>(new Set()) + const [newlyRenamedNodeIds, setNewlyRenamedNodeIds] = useState>(new Set()) const [draggedNodeId, setDraggedNodeId] = useState(null) const [dragOverNodeId, setDragOverNodeId] = useState(null) const [dragPosition, setDragPosition] = useState<'before' | 'inside' | 'after'>('inside') @@ -341,6 +359,49 @@ const NotesSidebar: FC = ({ [bases.length, t] ) + const handleAutoRename = useCallback( + async (note: NotesTreeNode) => { + if (note.type !== 'file') return + + setRenamingNodeIds((prev) => new Set(prev).add(note.id)) + try { + const content = await window.api.file.readExternal(note.externalPath) + if (!content || content.trim().length === 0) { + window.toast.warning(t('notes.auto_rename.empty_note')) + return + } + + const summaryText = await fetchNoteSummary({ content }) + if (summaryText) { + onRenameNode(note.id, summaryText) + window.toast.success(t('notes.auto_rename.success')) + } else { + window.toast.error(t('notes.auto_rename.failed')) + } + } catch (error) { + window.toast.error(t('notes.auto_rename.failed')) + logger.error(`Failed to auto-rename note: ${error}`) + } finally { + setRenamingNodeIds((prev) => { + const next = new Set(prev) + next.delete(note.id) + return next + }) + + setNewlyRenamedNodeIds((prev) => new Set(prev).add(note.id)) + + setTimeout(() => { + setNewlyRenamedNodeIds((prev) => { + const next = new Set(prev) + next.delete(note.id) + return next + }) + }, 700) + } + }, + [onRenameNode, t] + ) + const handleDragStart = useCallback((e: React.DragEvent, node: NotesTreeNode) => { setDraggedNodeId(node.id) e.dataTransfer.effectAllowed = 'move' @@ -495,7 +556,22 @@ const NotesSidebar: FC = ({ const getMenuItems = useCallback( (node: NotesTreeNode) => { - const baseMenuItems: MenuProps['items'] = [ + const baseMenuItems: MenuProps['items'] = [] + + // only show auto rename for file for now + if (node.type !== 'folder') { + baseMenuItems.push({ + label: t('notes.auto_rename.label'), + key: 'auto-rename', + icon: , + disabled: renamingNodeIds.has(node.id), + onClick: () => { + handleAutoRename(node) + } + }) + } + + baseMenuItems.push( { label: t('notes.rename'), key: 'rename', @@ -512,7 +588,7 @@ const NotesSidebar: FC = ({ window.api.openPath(node.externalPath) } } - ] + ) if (node.type !== 'folder') { baseMenuItems.push( { @@ -590,7 +666,16 @@ const NotesSidebar: FC = ({ return baseMenuItems }, - [t, handleStartEdit, onToggleStar, handleExportKnowledge, handleDeleteNode, exportMenuOptions] + [ + t, + handleStartEdit, + onToggleStar, + handleExportKnowledge, + handleDeleteNode, + renamingNodeIds, + handleAutoRename, + exportMenuOptions + ] ) const handleDropFiles = useCallback( @@ -727,6 +812,8 @@ const NotesSidebar: FC = ({ selectedFolderId={selectedFolderId} activeNodeId={activeNode?.id} editingNodeId={editingNodeId} + renamingNodeIds={renamingNodeIds} + newlyRenamedNodeIds={newlyRenamedNodeIds} draggedNodeId={draggedNodeId} dragOverNodeId={dragOverNodeId} dragPosition={dragPosition} @@ -771,6 +858,8 @@ const NotesSidebar: FC = ({ selectedFolderId={selectedFolderId} activeNodeId={activeNode?.id} editingNodeId={editingNodeId} + renamingNodeIds={renamingNodeIds} + newlyRenamedNodeIds={newlyRenamedNodeIds} draggedNodeId={draggedNodeId} dragOverNodeId={dragOverNodeId} dragPosition={dragPosition} @@ -793,6 +882,8 @@ const NotesSidebar: FC = ({ selectedFolderId={selectedFolderId} activeNodeId={activeNode?.id} editingNodeId={editingNodeId} + renamingNodeIds={renamingNodeIds} + newlyRenamedNodeIds={newlyRenamedNodeIds} draggedNodeId={draggedNodeId} dragOverNodeId={dragOverNodeId} dragPosition={dragPosition} @@ -980,6 +1071,44 @@ const NodeName = styled.div` text-overflow: ellipsis; font-size: 13px; color: var(--color-text); + position: relative; + will-change: background-position, width; + + --color-shimmer-mid: var(--color-text-1); + --color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent); + + &.shimmer { + background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end)); + background-size: 200% 100%; + background-clip: text; + color: transparent; + animation: shimmer 3s linear infinite; + } + + &.typing { + display: block; + white-space: nowrap; + overflow: hidden; + animation: typewriter 0.5s steps(40, end); + } + + @keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } + } + + @keyframes typewriter { + from { + width: 0; + } + to { + width: 100%; + } + } ` const EditInput = styled(Input)` diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 64e2c1ae31..6ab07662cb 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -251,6 +251,68 @@ export async function fetchMessagesSummary({ messages, assistant }: { messages: } } +export async function fetchNoteSummary({ content, assistant }: { content: string; assistant?: Assistant }) { + let prompt = (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title') + const resolvedAssistant = assistant || getDefaultAssistant() + const model = getQuickModel() || resolvedAssistant.model || getDefaultModel() + + if (prompt && containsSupportedVariables(prompt)) { + prompt = await replacePromptVariables(prompt, model.name) + } + + const provider = getProviderByModel(model) + + if (!hasApiKey(provider)) { + return null + } + + const AI = new AiProviderNew(model) + + // only 2000 char and no images + const truncatedContent = content.substring(0, 2000) + const purifiedContent = purifyMarkdownImages(truncatedContent) + + const summaryAssistant = { + ...resolvedAssistant, + settings: { + ...resolvedAssistant.settings, + reasoning_effort: undefined, + qwenThinkMode: false + }, + prompt, + model + } + + const llmMessages = { + system: prompt, + prompt: purifiedContent + } + + const middlewareConfig: AiSdkMiddlewareConfig = { + streamOutput: false, + enableReasoning: false, + isPromptToolUse: false, + isSupportedToolUse: false, + isImageGenerationEndpoint: false, + enableWebSearch: false, + enableGenerateImage: false, + enableUrlContext: false, + mcpTools: [] + } + + try { + const { getText } = await AI.completions(model.id, llmMessages, { + ...middlewareConfig, + assistant: summaryAssistant, + callType: 'summary' + }) + const text = getText() + return removeSpecialCharactersForTopicName(text) || null + } catch (error: any) { + return null + } +} + // export async function fetchSearchSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) { // const model = getQuickModel() || assistant.model || getDefaultModel() // const provider = getProviderByModel(model) From b7e7174f3d6261cf9b9fa9b39f15d1fd91ad5cdb Mon Sep 17 00:00:00 2001 From: Tristan Zhang <82869104+ABucket@users.noreply.github.com> Date: Thu, 2 Oct 2025 20:56:53 +0800 Subject: [PATCH 11/22] feat: add middle-click tab closing (#10498) --- src/renderer/src/components/Tab/TabContainer.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx index 3930819c6e..a83621f4b0 100644 --- a/src/renderer/src/components/Tab/TabContainer.tsx +++ b/src/renderer/src/components/Tab/TabContainer.tsx @@ -237,7 +237,17 @@ const TabsContainer: React.FC = ({ children }) => { onSortEnd={onSortEnd} className="tabs-sortable" renderItem={(tab) => ( - handleTabClick(tab)}> + handleTabClick(tab)} + onAuxClick={(e) => { + if (e.button === 1 && tab.id !== 'home') { + e.preventDefault() + e.stopPropagation() + closeTab(tab.id) + } + }}> {tab.id && {getTabIcon(tab.id, minapps, minAppsCache)}} {getTabTitle(tab.id)} From 2aedbf5702aed4df2ece6aaff2abba56c9683c44 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:36:18 +0800 Subject: [PATCH 12/22] fix(reasoning): support deepseek v3.2, claude 4.5, glm 4.6 (#10475) * fix(reasoning): update deepseek model id regex pattern to match more variants The previous regex pattern was too restrictive and didn't account for all possible deepseek model id formats. This change expands the pattern to support more variants while maintaining the same functionality. * fix(reasoning): update deepseek model id regex pattern to match more variants * fix(reasoning): improve regex pattern for deepseek model matching Update the regex pattern to be more precise in matching deepseek model versions. Add detailed comments explaining the pattern and note future improvements. * feat(models): add GLM-4.6 model to supported list Update model configuration to include new GLM-4.6 model and add it to the supported models for thinking token functionality * feat(models): add claude sonnet 4.5 model to anthropic provider --- src/renderer/src/config/models/default.ts | 12 ++++++++++++ src/renderer/src/config/models/reasoning.ts | 10 ++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/config/models/default.ts b/src/renderer/src/config/models/default.ts index abcb38f9b3..4fa60ed548 100644 --- a/src/renderer/src/config/models/default.ts +++ b/src/renderer/src/config/models/default.ts @@ -430,6 +430,12 @@ export const SYSTEM_MODELS: Record = } ], anthropic: [ + { + id: 'claude-sonnet-4-5-20250929', + provider: 'anthropic', + name: 'Claude Sonnet 4.5', + group: 'Claude 4.5' + }, { id: 'claude-sonnet-4-20250514', provider: 'anthropic', @@ -698,6 +704,12 @@ export const SYSTEM_MODELS: Record = name: 'GLM-4.5-Flash', group: 'GLM-4.5' }, + { + id: 'glm-4.6', + provider: 'zhipu', + name: 'GLM-4.6', + group: 'GLM-4.6' + }, { id: 'glm-4.5', provider: 'zhipu', diff --git a/src/renderer/src/config/models/reasoning.ts b/src/renderer/src/config/models/reasoning.ts index 545d759363..10cb64156b 100644 --- a/src/renderer/src/config/models/reasoning.ts +++ b/src/renderer/src/config/models/reasoning.ts @@ -339,14 +339,20 @@ export const isSupportedReasoningEffortPerplexityModel = (model: Model): boolean export const isSupportedThinkingTokenZhipuModel = (model: Model): boolean => { const modelId = getLowerBaseModelName(model.id, '/') - return modelId.includes('glm-4.5') + return ['glm-4.5', 'glm-4.6'].some((id) => modelId.includes(id)) } export const isDeepSeekHybridInferenceModel = (model: Model) => { const modelId = getLowerBaseModelName(model.id) // deepseek官方使用chat和reasoner做推理控制,其他provider需要单独判断,id可能会有所差别 // openrouter: deepseek/deepseek-chat-v3.1 不知道会不会有其他provider仿照ds官方分出一个同id的作为非思考模式的模型,这里有风险 - return /deepseek-v3(?:\.1|-1-\d+)/.test(modelId) || modelId.includes('deepseek-chat-v3.1') + // Matches: "deepseek-v3" followed by ".digit" or "-digit". + // Optionally, this can be followed by ".alphanumeric_sequence" or "-alphanumeric_sequence" + // until the end of the string. + // Examples: deepseek-v3.1, deepseek-v3-1, deepseek-v3.1.2, deepseek-v3.1-alpha + // Does NOT match: deepseek-v3.123 (missing separator after '1'), deepseek-v3.x (x isn't a digit) + // TODO: move to utils and add test cases + return /deepseek-v3(?:\.\d|-\d)(?:(\.|-)\w+)?$/.test(modelId) || modelId.includes('deepseek-chat-v3.1') } export const isSupportedThinkingTokenDeepSeekModel = isDeepSeekHybridInferenceModel From a436ab1d785ee102d49c5917785dd8343c6eae21 Mon Sep 17 00:00:00 2001 From: Tristan Zhang <82869104+ABucket@users.noreply.github.com> Date: Fri, 3 Oct 2025 19:23:49 +0800 Subject: [PATCH 13/22] fix(TextFilePreview): make editor read-only but can be copied (#10499) * fix(TextFilePreview): make editor read-only but can be copied * feat: add table auto-wrap feature for notes * Revert "feat: add table auto-wrap feature for notes" This reverts commit 7785f480b1f585a27e08f6fbf1829a30cad97a4a. --- src/renderer/src/components/Popups/TextFilePreview.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/Popups/TextFilePreview.tsx b/src/renderer/src/components/Popups/TextFilePreview.tsx index f5fa787d0b..9c74eef0d1 100644 --- a/src/renderer/src/components/Popups/TextFilePreview.tsx +++ b/src/renderer/src/components/Popups/TextFilePreview.tsx @@ -1,3 +1,4 @@ +import { EditorState } from '@codemirror/state' import { Modal } from 'antd' import { useState } from 'react' import styled from 'styled-components' @@ -55,12 +56,12 @@ const PopupContainer: React.FC = ({ text, title, extension, resolve }) => footer={null}> {extension !== undefined ? ( ) : ( {text} From 78eacccf6efa9c82236f9c05f9e6e2cf29f3b8f5 Mon Sep 17 00:00:00 2001 From: PP Kun Date: Sat, 4 Oct 2025 12:42:07 +0800 Subject: [PATCH 14/22] chore(build): Upgrade electron (#10525) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 electron 从 37.4.0 升级到 37.6.0 - 解决旧版本导致macOS 26卡顿问题 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 907f872009..ce586b5afe 100644 --- a/package.json +++ b/package.json @@ -238,7 +238,7 @@ "docx": "^9.0.2", "dompurify": "^3.2.6", "dotenv-cli": "^7.4.2", - "electron": "37.4.0", + "electron": "37.6.0", "electron-builder": "26.0.15", "electron-devtools-installer": "^3.2.0", "electron-store": "^8.2.0", diff --git a/yarn.lock b/yarn.lock index 93029954a6..afb54da011 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16809,16 +16809,16 @@ __metadata: languageName: node linkType: hard -"electron@npm:37.4.0": - version: 37.4.0 - resolution: "electron@npm:37.4.0" +"electron@npm:37.6.0": + version: 37.6.0 + resolution: "electron@npm:37.6.0" dependencies: "@electron/get": "npm:^2.0.0" "@types/node": "npm:^22.7.7" extract-zip: "npm:^2.0.1" bin: electron: cli.js - checksum: 10c0/92a0c41190e234d302bc612af6cce9af08cd07f6699c1ff21a9365297e73dc9d88c6c4c25ddabf352447e3e555878d2ab0f2f31a14e210dda6de74d2787ff323 + checksum: 10c0/d67b7f0ff902f9184c2a7445507746343f8b39f3616d9d26128e7515e0184252cfc8ac97a3f1458f9ea9b4af6ab5b3208282014e8d91c0e1505ff21f5fa57ce6 languageName: node linkType: hard From 2048f210e7fbacf71c8f761be98b63a74084a83a Mon Sep 17 00:00:00 2001 From: one Date: Sat, 4 Oct 2025 23:24:50 +0800 Subject: [PATCH 15/22] feat(CodeEditor): add a prop to enable the readOnly extension (#10516) * feat(CodeEditor): add a prop to enable the readOnly extension * feat: enable keymap for TextFilePreview --- src/renderer/src/components/CodeEditor/index.tsx | 9 ++++++++- src/renderer/src/components/Popups/TextFilePreview.tsx | 6 ++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/components/CodeEditor/index.tsx b/src/renderer/src/components/CodeEditor/index.tsx index 4304ec324e..64c387ffd0 100644 --- a/src/renderer/src/components/CodeEditor/index.tsx +++ b/src/renderer/src/components/CodeEditor/index.tsx @@ -75,10 +75,15 @@ export interface CodeEditorProps { /** CSS class name appended to the default `code-editor` class. */ className?: string /** - * Whether the editor is editable. + * Whether the editor view is editable. * @default true */ editable?: boolean + /** + * Set the editor state to read only but keep some user interactions, e.g., keymaps. + * @default false + */ + readOnly?: boolean /** * Whether the editor is expanded. * If true, the height and maxHeight props are ignored. @@ -114,6 +119,7 @@ const CodeEditor = ({ style, className, editable = true, + readOnly = false, expanded = true, wrapped = true }: CodeEditorProps) => { @@ -189,6 +195,7 @@ const CodeEditor = ({ maxHeight={expanded ? undefined : maxHeight} minHeight={minHeight} editable={editable} + readOnly={readOnly} // @ts-ignore 强制使用,见 react-codemirror 的 Example.tsx theme={activeCmTheme} extensions={customExtensions} diff --git a/src/renderer/src/components/Popups/TextFilePreview.tsx b/src/renderer/src/components/Popups/TextFilePreview.tsx index 9c74eef0d1..584929401c 100644 --- a/src/renderer/src/components/Popups/TextFilePreview.tsx +++ b/src/renderer/src/components/Popups/TextFilePreview.tsx @@ -1,4 +1,3 @@ -import { EditorState } from '@codemirror/state' import { Modal } from 'antd' import { useState } from 'react' import styled from 'styled-components' @@ -56,12 +55,15 @@ const PopupContainer: React.FC = ({ text, title, extension, resolve }) => footer={null}> {extension !== undefined ? ( ) : ( {text} From fcf53f06efa8780451a4c1b2e0ec01e994a68886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=BF=E9=80=94=E9=A3=8E=E6=B5=AA?= <116719594+FLC-ytfl@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:39:32 +0800 Subject: [PATCH 16/22] fix(models vision) (#10530) --- src/renderer/src/config/models/vision.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderer/src/config/models/vision.ts b/src/renderer/src/config/models/vision.ts index 85eca139c4..c7d78b90ab 100644 --- a/src/renderer/src/config/models/vision.ts +++ b/src/renderer/src/config/models/vision.ts @@ -22,7 +22,9 @@ const visionAllowedModels = [ 'qwen-vl', 'qwen2-vl', 'qwen2.5-vl', + 'qwen3-vl', 'qwen2.5-omni', + 'qwen3-omni', 'qvq', 'internvl2', 'grok-vision-beta', From 8bec7640fa7002c77130d56065cda2cf31d8b51d Mon Sep 17 00:00:00 2001 From: rebecca554owen Date: Mon, 6 Oct 2025 22:19:09 +0800 Subject: [PATCH 17/22] fix(metrics): restore first token latency reporting (#10538) --- .../src/aiCore/chunk/AiSdkToChunkAdapter.ts | 96 +++++++++++++------ 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts index fb68cedb23..6d7070ce85 100644 --- a/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts +++ b/src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts @@ -22,6 +22,8 @@ export class AiSdkToChunkAdapter { private accumulate: boolean | undefined private isFirstChunk = true private enableWebSearch: boolean = false + private responseStartTimestamp: number | null = null + private firstTokenTimestamp: number | null = null constructor( private onChunk: (chunk: Chunk) => void, @@ -34,6 +36,17 @@ export class AiSdkToChunkAdapter { this.enableWebSearch = enableWebSearch || false } + private markFirstTokenIfNeeded() { + if (this.firstTokenTimestamp === null && this.responseStartTimestamp !== null) { + this.firstTokenTimestamp = Date.now() + } + } + + private resetTimingState() { + this.responseStartTimestamp = null + this.firstTokenTimestamp = null + } + /** * 处理 AI SDK 流结果 * @param aiSdkResult AI SDK 的流结果对象 @@ -61,6 +74,8 @@ export class AiSdkToChunkAdapter { webSearchResults: [], reasoningId: '' } + this.resetTimingState() + this.responseStartTimestamp = Date.now() // Reset link converter state at the start of stream this.isFirstChunk = true @@ -73,6 +88,7 @@ export class AiSdkToChunkAdapter { if (this.enableWebSearch) { const remainingText = flushLinkConverterBuffer() if (remainingText) { + this.markFirstTokenIfNeeded() this.onChunk({ type: ChunkType.TEXT_DELTA, text: remainingText @@ -87,6 +103,7 @@ export class AiSdkToChunkAdapter { } } finally { reader.releaseLock() + this.resetTimingState() } } @@ -137,6 +154,7 @@ export class AiSdkToChunkAdapter { // Only emit chunk if there's text to send if (finalText) { + this.markFirstTokenIfNeeded() this.onChunk({ type: ChunkType.TEXT_DELTA, text: this.accumulate ? final.text : finalText @@ -161,6 +179,9 @@ export class AiSdkToChunkAdapter { break case 'reasoning-delta': final.reasoningContent += chunk.text || '' + if (chunk.text) { + this.markFirstTokenIfNeeded() + } this.onChunk({ type: ChunkType.THINKING_DELTA, text: final.reasoningContent || '' @@ -260,44 +281,37 @@ export class AiSdkToChunkAdapter { break } - case 'finish': + case 'finish': { + const usage = { + completion_tokens: chunk.totalUsage?.outputTokens || 0, + prompt_tokens: chunk.totalUsage?.inputTokens || 0, + total_tokens: chunk.totalUsage?.totalTokens || 0 + } + const metrics = this.buildMetrics(chunk.totalUsage) + const baseResponse = { + text: final.text || '', + reasoning_content: final.reasoningContent || '' + } + this.onChunk({ type: ChunkType.BLOCK_COMPLETE, response: { - text: final.text || '', - reasoning_content: final.reasoningContent || '', - usage: { - completion_tokens: chunk.totalUsage.outputTokens || 0, - prompt_tokens: chunk.totalUsage.inputTokens || 0, - total_tokens: chunk.totalUsage.totalTokens || 0 - }, - metrics: chunk.totalUsage - ? { - completion_tokens: chunk.totalUsage.outputTokens || 0, - time_completion_millsec: 0 - } - : undefined + ...baseResponse, + usage: { ...usage }, + metrics: metrics ? { ...metrics } : undefined } }) this.onChunk({ type: ChunkType.LLM_RESPONSE_COMPLETE, response: { - text: final.text || '', - reasoning_content: final.reasoningContent || '', - usage: { - completion_tokens: chunk.totalUsage.outputTokens || 0, - prompt_tokens: chunk.totalUsage.inputTokens || 0, - total_tokens: chunk.totalUsage.totalTokens || 0 - }, - metrics: chunk.totalUsage - ? { - completion_tokens: chunk.totalUsage.outputTokens || 0, - time_completion_millsec: 0 - } - : undefined + ...baseResponse, + usage: { ...usage }, + metrics: metrics ? { ...metrics } : undefined } }) + this.resetTimingState() break + } // === 源和文件相关事件 === case 'source': @@ -333,6 +347,34 @@ export class AiSdkToChunkAdapter { default: } } + + private buildMetrics(totalUsage?: { + inputTokens?: number | null + outputTokens?: number | null + totalTokens?: number | null + }) { + if (!totalUsage) { + return undefined + } + + const completionTokens = totalUsage.outputTokens ?? 0 + const now = Date.now() + const start = this.responseStartTimestamp ?? now + const firstToken = this.firstTokenTimestamp + const timeFirstToken = Math.max(firstToken != null ? firstToken - start : 0, 0) + const baseForCompletion = firstToken ?? start + let timeCompletion = Math.max(now - baseForCompletion, 0) + + if (timeCompletion === 0 && completionTokens > 0) { + timeCompletion = 1 + } + + return { + completion_tokens: completionTokens, + time_first_token_millsec: timeFirstToken, + time_completion_millsec: timeCompletion + } + } } export default AiSdkToChunkAdapter From d2d5064eedef411eade75e099fabf819a23e5893 Mon Sep 17 00:00:00 2001 From: Murphy <69335326+MurphyLo@users.noreply.github.com> Date: Tue, 7 Oct 2025 00:02:48 +0800 Subject: [PATCH 18/22] fix: forked topic and rename modal retaining old name after rename (#10528) fix: sync active topic metadata after rename --- src/renderer/src/hooks/useTopic.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/renderer/src/hooks/useTopic.ts b/src/renderer/src/hooks/useTopic.ts index d2a71622fd..990cb12db8 100644 --- a/src/renderer/src/hooks/useTopic.ts +++ b/src/renderer/src/hooks/useTopic.ts @@ -48,6 +48,17 @@ export function useActiveTopic(assistantId: string, topic?: Topic) { } }, [activeTopic?.id, assistant]) + useEffect(() => { + if (!assistant?.topics?.length || !activeTopic) { + return + } + + const latestTopic = assistant.topics.find((item) => item.id === activeTopic.id) + if (latestTopic && latestTopic !== activeTopic) { + setActiveTopic(latestTopic) + } + }, [assistant?.topics, activeTopic]) + return { activeTopic, setActiveTopic } } From a9843b4128c984aaf3a1f7d38b7ff9ee1a866ff9 Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:24:29 +0800 Subject: [PATCH 19/22] feat: expand clickable area of topic in-place renaming (#10548) * chore: update electron dependency from 37.4.0 to 37.6.0 * feat(TopicsTab): add double click to edit topic name Move double click handler from TopicName component to parent div to improve UX * fix(TopicsTab): prevent topic edit on double click when already editing --- src/renderer/src/pages/home/Tabs/TopicsTab.tsx | 16 ++++++++-------- yarn.lock | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index f0cdda7e6e..df35bc2e92 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -525,6 +525,11 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, onContextMenu={() => setTargetTopic(topic)} className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')} onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)} + onDoubleClick={() => { + if (editingTopicId === topic.id && topicEdit.isEditing) return + setEditingTopicId(topic.id) + topicEdit.startEdit(topic.name) + }} style={{ borderRadius, cursor: editingTopicId === topic.id && topicEdit.isEditing ? 'default' : 'pointer' @@ -541,13 +546,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, onClick={(e) => e.stopPropagation()} /> ) : ( - { - setEditingTopicId(topic.id) - topicEdit.startEdit(topic.name) - }}> + {topicName} )} @@ -571,7 +570,8 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, } else { handleDeleteClick(topic.id, e) } - }}> + }} + onDoubleClick={(e) => e.stopPropagation()}> {deletingTopicId === topic.id ? ( ) : ( diff --git a/yarn.lock b/yarn.lock index afb54da011..3309a1c5f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13328,7 +13328,7 @@ __metadata: docx: "npm:^9.0.2" dompurify: "npm:^3.2.6" dotenv-cli: "npm:^7.4.2" - electron: "npm:37.4.0" + electron: "npm:37.6.0" electron-builder: "npm:26.0.15" electron-devtools-installer: "npm:^3.2.0" electron-store: "npm:^8.2.0" From cd881ceb34860e3a72f2cad86f3b80fe1326a462 Mon Sep 17 00:00:00 2001 From: Daniel Hofheinz Date: Tue, 7 Oct 2025 10:55:21 -0700 Subject: [PATCH 20/22] fix(ui): remove redundant scrollbar in side-by-side view & fix message menubar overflow (#10543) * fix(ui): remove redundant scrollbar in side-by-side view Changed GridContainer from styled(Scrollbar) to styled.div to eliminate redundant horizontal scrollbar in multi-model horizontal layout mode. The Scrollbar component is designed for vertical scrolling and conflicts with horizontal layouts. Fixes #10520 * fix(ui): restore vertical scrollbar for grid mode while preserving horizontal fix Optimal solution: Use Scrollbar component as base to preserve auto-hide behavior for vertical modes (grid, vertical, fold) while overriding its overflow-y behavior for horizontal mode only. This approach: - Preserves the June 2025 UX optimization (auto-hide scrollbars) - Fixes horizontal scrollbar issue from #10520 - Restores vertical scrolling for grid mode - Maintains auto-hide behavior for all vertical scrolling modes - Minimal change with no code duplication The Scrollbar component provides scrollbar thumb auto-hide after 1.5s, which enhances UX for vertical scrolling. By using CSS overrides only for horizontal mode, we get the best of both worlds. * chore: fix import sorting in MessageGroup.tsx Unrelated to PR scope - fixing to unblock CI. Auto-fixed via eslint --fix (moved Scrollbar import to correct position). Also updated yarn.lock to resolve dependency sync. * fix(ui): add explicit overflow declarations for all grid modes Previous fix relied on CSS inheritance from Scrollbar base component, but display: grid interferes with overflow property inheritance. This iteration adds explicit overflow-y: auto and overflow-x: hidden to grid, fold, vertical, and multi-select modes to ensure vertical scrolling works reliably across all layouts. - horizontal mode: overflow-y visible, overflow-x auto (unchanged) - grid/fold/vertical modes: explicit overflow-y auto, overflow-x hidden - multi-select mode: explicit overflow-y auto, overflow-x hidden Fixes vertical scrollbar missing in grid mode reported by @EurFelux * fix(Messages): adjust overflow behavior in message groups Fix scrollbar issues by hiding vertical overflow in horizontal layout and simplifying overflow handling in grid layout * feat(HorizontalScrollContainer): add classNames prop for container and content styling allow custom styling of container and content via classNames prop --------- Co-authored-by: icarus --- .../HorizontalScrollContainer/index.tsx | 15 ++++++- .../src/pages/home/Messages/Message.tsx | 42 +++++++++++-------- .../src/pages/home/Messages/MessageGroup.tsx | 24 +++++++++-- 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/src/renderer/src/components/HorizontalScrollContainer/index.tsx b/src/renderer/src/components/HorizontalScrollContainer/index.tsx index ed5cdc52de..fdc890d2e2 100644 --- a/src/renderer/src/components/HorizontalScrollContainer/index.tsx +++ b/src/renderer/src/components/HorizontalScrollContainer/index.tsx @@ -1,3 +1,4 @@ +import { cn } from '@heroui/react' import Scrollbar from '@renderer/components/Scrollbar' import { ChevronRight } from 'lucide-react' import { useEffect, useRef, useState } from 'react' @@ -17,6 +18,10 @@ export interface HorizontalScrollContainerProps { dependencies?: readonly unknown[] scrollDistance?: number className?: string + classNames?: { + container?: string + content?: string + } gap?: string expandable?: boolean } @@ -26,6 +31,7 @@ const HorizontalScrollContainer: React.FC = ({ dependencies = [], scrollDistance = 200, className, + classNames, gap = '8px', expandable = false }) => { @@ -95,11 +101,16 @@ const HorizontalScrollContainer: React.FC = ({ return ( - + {children} {canScroll && !isExpanded && !isScrolledToEnd && ( diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 0e86e9b232..d4a7ceef71 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -1,4 +1,6 @@ +import { cn } from '@heroui/react' import { loggerService } from '@logger' +import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer' import Scrollbar from '@renderer/components/Scrollbar' import { useMessageEditing } from '@renderer/context/MessageEditingContext' import { useAssistant } from '@renderer/hooks/useAssistant' @@ -225,20 +227,28 @@ const MessageItem: FC = ({ {showMenubar && ( - - } - setModel={setModel} - onUpdateUseful={onUpdateUseful} - /> + + + } + setModel={setModel} + onUpdateUseful={onUpdateUseful} + /> + )} @@ -282,10 +292,8 @@ const MessageContentContainer = styled(Scrollbar)` overflow-y: auto; ` -const MessageFooter = styled.div<{ $isLastMessage: boolean; $messageStyle: 'plain' | 'bubble' }>` +const MessageFooter = styled.div` display: flex; - flex-direction: ${({ $isLastMessage, $messageStyle }) => - $isLastMessage && $messageStyle === 'plain' ? 'row-reverse' : 'row'}; align-items: center; justify-content: space-between; gap: 10px; diff --git a/src/renderer/src/pages/home/Messages/MessageGroup.tsx b/src/renderer/src/pages/home/Messages/MessageGroup.tsx index 4632c9ffb9..94c9672eda 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroup.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroup.tsx @@ -337,17 +337,30 @@ const GroupContainer = styled.div` const GridContainer = styled(Scrollbar)<{ $count: number; $gridColumns: number }>` width: 100%; display: grid; - overflow-y: visible; gap: 16px; + &.horizontal { padding-bottom: 4px; grid-template-columns: repeat(${({ $count }) => $count}, minmax(420px, 1fr)); + overflow-y: hidden; overflow-x: auto; + &::-webkit-scrollbar { + height: 6px; + } + &::-webkit-scrollbar-thumb { + background: var(--color-scrollbar-thumb); + border-radius: var(--scrollbar-thumb-radius); + } + &::-webkit-scrollbar-thumb:hover { + background: var(--color-scrollbar-thumb-hover); + } } &.fold, &.vertical { grid-template-columns: repeat(1, minmax(0, 1fr)); gap: 8px; + overflow-y: auto; + overflow-x: hidden; } &.grid { grid-template-columns: repeat( @@ -355,11 +368,15 @@ const GridContainer = styled(Scrollbar)<{ $count: number; $gridColumns: number } minmax(0, 1fr) ); grid-template-rows: auto; + overflow-y: auto; + overflow-x: hidden; } &.multi-select-mode { grid-template-columns: repeat(1, minmax(0, 1fr)); gap: 10px; + overflow-y: auto; + overflow-x: hidden; .grid { height: auto; } @@ -385,7 +402,7 @@ interface MessageWrapperProps { const MessageWrapper = styled.div` &.horizontal { padding: 1px; - overflow-y: auto; + /* overflow-y: auto; */ .message { height: 100%; border: 0.5px solid var(--color-border); @@ -405,8 +422,9 @@ const MessageWrapper = styled.div` } } &.grid { + display: block; height: 300px; - overflow-y: hidden; + overflow: hidden; border: 0.5px solid var(--color-border); border-radius: 10px; cursor: pointer; From d4b34281604c62bd54acb27bc855993234c095c1 Mon Sep 17 00:00:00 2001 From: Tristan Zhang Date: Wed, 8 Oct 2025 01:57:00 +0800 Subject: [PATCH 21/22] feat: Support automatic line wrapping for tables in notes (#10503) * feat: add table auto-wrap feature for notes * chore: lint * feat: remove settings for auto wrap --- .../src/components/RichEditor/styles.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/renderer/src/components/RichEditor/styles.ts b/src/renderer/src/components/RichEditor/styles.ts index b184063d08..6acb943528 100644 --- a/src/renderer/src/components/RichEditor/styles.ts +++ b/src/renderer/src/components/RichEditor/styles.ts @@ -14,6 +14,31 @@ export const RichEditorWrapper = styled.div<{ border-radius: 6px; background: var(--color-background); overflow-y: hidden; + .ProseMirror table, + .tiptap table { + table-layout: auto !important; + } + + .ProseMirror table th, + .ProseMirror table td, + .tiptap th, + .tiptap td { + white-space: normal !important; + word-wrap: break-word !important; + word-break: break-word !important; + overflow-wrap: break-word !important; + overflow: visible !important; + text-overflow: clip !important; + } + + .ProseMirror table th > *, + .ProseMirror table td > *, + .tiptap td > *, + .tiptap th > * { + white-space: normal !important; + overflow: visible !important; + text-overflow: clip !important; + } width: ${({ $isFullWidth }) => ($isFullWidth ? '100%' : '60%')}; margin: ${({ $isFullWidth }) => ($isFullWidth ? '0' : '0 auto')}; font-family: ${({ $fontFamily }) => ($fontFamily === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)')}; @@ -21,6 +46,7 @@ export const RichEditorWrapper = styled.div<{ ${({ $minHeight }) => $minHeight && `min-height: ${$minHeight}px;`} ${({ $maxHeight }) => $maxHeight && `max-height: ${$maxHeight}px;`} + ` export const ToolbarWrapper = styled.div` From 504531d4d5f3399ab5f7b762fcafdec817e45c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=C2=B7Dong?= <98630204+GeorgeDong32@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:48:26 +0800 Subject: [PATCH 22/22] feat(notes): add spell-check control (#10507) * feat(notes): add spell-check control * feat(notes): add spell-check toggle to preview mode toolbar * feat(settings): move spellcheck to global and use hook --- .../src/components/RichEditor/index.tsx | 4 ++- .../src/components/RichEditor/types.ts | 2 ++ .../components/RichEditor/useRichEditor.ts | 7 +++++- src/renderer/src/i18n/locales/en-us.json | 2 ++ src/renderer/src/i18n/locales/zh-cn.json | 2 ++ src/renderer/src/i18n/locales/zh-tw.json | 2 ++ src/renderer/src/i18n/translate/el-gr.json | 9 +++++++ src/renderer/src/i18n/translate/es-es.json | 9 +++++++ src/renderer/src/i18n/translate/fr-fr.json | 9 +++++++ src/renderer/src/i18n/translate/ja-jp.json | 9 +++++++ src/renderer/src/i18n/translate/pt-pt.json | 9 +++++++ src/renderer/src/i18n/translate/ru-ru.json | 9 +++++++ src/renderer/src/pages/notes/NotesEditor.tsx | 25 +++++++++++++++++-- 13 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/RichEditor/index.tsx b/src/renderer/src/components/RichEditor/index.tsx index 0b9e3876ac..83023dab9a 100644 --- a/src/renderer/src/components/RichEditor/index.tsx +++ b/src/renderer/src/components/RichEditor/index.tsx @@ -48,7 +48,8 @@ const RichEditor = ({ enableContentSearch = false, isFullWidth = false, fontFamily = 'default', - fontSize = 16 + fontSize = 16, + enableSpellCheck = false // toolbarItems: _toolbarItems // TODO: Implement custom toolbar items }: RichEditorProps & { ref?: React.RefObject }) => { // Use the rich editor hook for complete editor management @@ -71,6 +72,7 @@ const RichEditor = ({ onBlur, placeholder, editable, + enableSpellCheck, scrollParent: () => scrollContainerRef.current, onShowTableActionMenu: ({ position, actions }) => { const iconMap: Record = { diff --git a/src/renderer/src/components/RichEditor/types.ts b/src/renderer/src/components/RichEditor/types.ts index 8804210aef..48ae5bb112 100644 --- a/src/renderer/src/components/RichEditor/types.ts +++ b/src/renderer/src/components/RichEditor/types.ts @@ -50,6 +50,8 @@ export interface RichEditorProps { fontFamily?: 'default' | 'serif' /** Font size in pixels */ fontSize?: number + /** Whether to enable spell check */ + enableSpellCheck?: boolean } export interface ToolbarItem { diff --git a/src/renderer/src/components/RichEditor/useRichEditor.ts b/src/renderer/src/components/RichEditor/useRichEditor.ts index 7dae176068..1ece36fb00 100644 --- a/src/renderer/src/components/RichEditor/useRichEditor.ts +++ b/src/renderer/src/components/RichEditor/useRichEditor.ts @@ -57,6 +57,8 @@ export interface UseRichEditorOptions { editable?: boolean /** Whether to enable table of contents functionality */ enableTableOfContents?: boolean + /** Whether to enable spell check */ + enableSpellCheck?: boolean /** Show table action menu (row/column) with concrete actions and position */ onShowTableActionMenu?: (payload: { type: 'row' | 'column' @@ -126,6 +128,7 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor previewLength = 50, placeholder = '', editable = true, + enableSpellCheck = false, onShowTableActionMenu, scrollParent } = options @@ -410,7 +413,9 @@ export const useRichEditor = (options: UseRichEditorOptions = {}): UseRichEditor // Allow text selection even when not editable style: editable ? '' - : 'user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;' + : 'user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;', + // Set spellcheck attribute on the contenteditable element + spellcheck: enableSpellCheck ? 'true' : 'false' } }, onUpdate: ({ editor }) => { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 0f3d2a3f24..00ab42128b 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1784,6 +1784,8 @@ "sort_updated_asc": "Update time (oldest first)", "sort_updated_desc": "Update time (newest first)", "sort_z2a": "File name (Z-A)", + "spell_check": "Spell Check", + "spell_check_tooltip": "Enable/Disable spell check", "star": "Favorite note", "starred_notes": "Collected notes", "title": "Notes", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index ce2dc5c222..af2389dbe2 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1784,6 +1784,8 @@ "sort_updated_asc": "更新时间(从旧到新)", "sort_updated_desc": "更新时间(从新到旧)", "sort_z2a": "文件名(Z-A)", + "spell_check": "拼写检查", + "spell_check_tooltip": "启用/禁用拼写检查", "star": "收藏笔记", "starred_notes": "收藏的笔记", "title": "笔记", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index e4f45288ee..d0c08124ea 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1784,6 +1784,8 @@ "sort_updated_asc": "更新時間(從舊到新)", "sort_updated_desc": "更新時間(從新到舊)", "sort_z2a": "文件名(Z-A)", + "spell_check": "拼寫檢查", + "spell_check_tooltip": "啟用/禁用拼寫檢查", "star": "收藏筆記", "starred_notes": "收藏的筆記", "title": "筆記", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index a071f27783..57edf11f96 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -334,6 +334,7 @@ "new_topic": "Νέο θέμα {{Command}}", "pause": "Παύση", "placeholder": "Εισάγετε μήνυμα εδώ...", + "placeholder_without_triggers": "Εδώ εισαγάγετε το μήνυμα, πατήστε {{key}} για αποστολή", "send": "Αποστολή", "settings": "Ρυθμίσεις", "thinking": { @@ -1696,6 +1697,12 @@ "provider_settings": "Μετάβαση στις ρυθμίσεις παρόχου" }, "notes": { + "auto_rename": { + "empty_note": "Το σημείωμα είναι κενό, δεν μπορεί να δημιουργηθεί όνομα", + "failed": "Αποτυχία δημιουργίας ονόματος σημείωσης", + "label": "Δημιουργία ονόματος σημείωσης", + "success": "Η δημιουργία του ονόματος σημειώσεων ολοκληρώθηκε με επιτυχία" + }, "characters": "χαρακτήρας", "collapse": "σύμπτυξη", "content_placeholder": "Παρακαλώ εισαγάγετε το περιεχόμενο των σημειώσεων...", @@ -1777,6 +1784,8 @@ "sort_updated_asc": "χρόνος ενημέρωσης (από παλιά στα νέα)", "sort_updated_desc": "χρόνος ενημέρωσης (από νεώτερο σε παλαιότερο)", "sort_z2a": "όνομα αρχείου (Z-A)", + "spell_check": "Έλεγχος ορθογραφίας", + "spell_check_tooltip": "Ενεργοποίηση/Απενεργοποίηση ελέγχου ορθογραφίας", "star": "Αγαπημένες σημειώσεις", "starred_notes": "Σημειώσεις συλλογής", "title": "σημειώσεις", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 7ae0a1bd3c..739943ffc7 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -334,6 +334,7 @@ "new_topic": "Nuevo tema {{Command}}", "pause": "Pausar", "placeholder": "Escribe aquí tu mensaje...", + "placeholder_without_triggers": "Escriba un mensaje aquí y presione {{key}} para enviar", "send": "Enviar", "settings": "Configuración", "thinking": { @@ -1696,6 +1697,12 @@ "provider_settings": "Ir a la configuración del proveedor" }, "notes": { + "auto_rename": { + "empty_note": "La nota está vacía, no se puede generar un nombre", + "failed": "Error al generar el nombre de la nota", + "label": "Generar nombre de nota", + "success": "Se ha generado correctamente el nombre de la nota" + }, "characters": "carácter", "collapse": "ocultar", "content_placeholder": "Introduzca el contenido de la nota...", @@ -1777,6 +1784,8 @@ "sort_updated_asc": "Fecha de actualización (de más antigua a más reciente)", "sort_updated_desc": "Fecha de actualización (de más nuevo a más antiguo)", "sort_z2a": "Nombre de archivo (Z-A)", + "spell_check": "comprobación ortográfica", + "spell_check_tooltip": "Habilitar/deshabilitar revisión ortográfica", "star": "Notas guardadas", "starred_notes": "notas guardadas", "title": "notas", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 64a788f266..d5eda2e61e 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -334,6 +334,7 @@ "new_topic": "Nouveau sujet {{Command}}", "pause": "Pause", "placeholder": "Entrez votre message ici...", + "placeholder_without_triggers": "Entrez votre message ici, appuyez sur {{key}} pour envoyer", "send": "Envoyer", "settings": "Paramètres", "thinking": { @@ -1696,6 +1697,12 @@ "provider_settings": "Aller aux paramètres du fournisseur" }, "notes": { + "auto_rename": { + "empty_note": "La note est vide, impossible de générer un nom", + "failed": "Échec de la génération du nom de note", + "label": "Générer un nom de note", + "success": "La génération du nom de note a réussi" + }, "characters": "caractère", "collapse": "réduire", "content_placeholder": "Veuillez saisir le contenu de la note...", @@ -1777,6 +1784,8 @@ "sort_updated_asc": "Heure de mise à jour (du plus ancien au plus récent)", "sort_updated_desc": "Date de mise à jour (du plus récent au plus ancien)", "sort_z2a": "Nom de fichier (Z-A)", + "spell_check": "Vérification orthographique", + "spell_check_tooltip": "Activer/Désactiver la vérification orthographique", "star": "Notes enregistrées", "starred_notes": "notes de collection", "title": "notes", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 23eea05fe6..f5cde82e28 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -334,6 +334,7 @@ "new_topic": "新しいトピック {{Command}}", "pause": "一時停止", "placeholder": "ここにメッセージを入力し、{{key}} を押して送信...", + "placeholder_without_triggers": "ここにメッセージを入力し、{{key}} を押して送信してください", "send": "送信", "settings": "設定", "thinking": { @@ -1696,6 +1697,12 @@ "provider_settings": "プロバイダー設定に移動" }, "notes": { + "auto_rename": { + "empty_note": "ノートが空です。名前を生成できません。", + "failed": "ノート名の生成に失敗しました", + "label": "ノート名の生成", + "success": "ノート名の生成に成功しました" + }, "characters": "文字", "collapse": "閉じる", "content_placeholder": "メモの内容を入力してください...", @@ -1777,6 +1784,8 @@ "sort_updated_asc": "更新日時(古い順)", "sort_updated_desc": "更新日時(新しい順)", "sort_z2a": "ファイル名(Z-A)", + "spell_check": "スペルチェック", + "spell_check_tooltip": "スペルチェックの有効/無効", "star": "お気に入りのノート", "starred_notes": "収集したノート", "title": "ノート", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index ab9bec0e66..befcedf381 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -334,6 +334,7 @@ "new_topic": "Novo tópico {{Command}}", "pause": "Pausar", "placeholder": "Digite sua mensagem aqui...", + "placeholder_without_triggers": "Digite a mensagem aqui, pressione {{key}} para enviar", "send": "Enviar", "settings": "Configurações", "thinking": { @@ -1696,6 +1697,12 @@ "provider_settings": "Ir para as configurações do provedor" }, "notes": { + "auto_rename": { + "empty_note": "A nota está vazia, não é possível gerar um nome", + "failed": "Falha ao gerar o nome da nota", + "label": "Gerar nome da nota", + "success": "Nome da nota gerado com sucesso" + }, "characters": "caractere", "collapse": "[minimizar]", "content_placeholder": "Introduza o conteúdo da nota...", @@ -1777,6 +1784,8 @@ "sort_updated_asc": "Tempo de atualização (do mais antigo para o mais recente)", "sort_updated_desc": "atualização de tempo (do mais novo para o mais antigo)", "sort_z2a": "Nome do arquivo (Z-A)", + "spell_check": "verificação ortográfica", + "spell_check_tooltip": "Ativar/Desativar verificação ortográfica", "star": "Notas favoritas", "starred_notes": "notas salvas", "title": "nota", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index ccc1f49344..f74529300d 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -334,6 +334,7 @@ "new_topic": "Новый топик {{Command}}", "pause": "Остановить", "placeholder": "Введите ваше сообщение здесь, нажмите {{key}} для отправки...", + "placeholder_without_triggers": "Введите сообщение здесь, нажмите {{key}}, чтобы отправить", "send": "Отправить", "settings": "Настройки", "thinking": { @@ -1696,6 +1697,12 @@ "provider_settings": "Перейти к настройкам поставщика" }, "notes": { + "auto_rename": { + "empty_note": "Заметки пусты, имя невозможно сгенерировать", + "failed": "Создание названия заметки не удалось", + "label": "Создать название заметки", + "success": "Имя заметки успешно создано" + }, "characters": "Символы", "collapse": "Свернуть", "content_placeholder": "Введите содержимое заметки...", @@ -1777,6 +1784,8 @@ "sort_updated_asc": "Время обновления (от старого к новому)", "sort_updated_desc": "Время обновления (от нового к старому)", "sort_z2a": "Имя файла (Я-А)", + "spell_check": "Проверка орфографии", + "spell_check_tooltip": "Включить/отключить проверку орфографии", "star": "Избранные заметки", "starred_notes": "Сохраненные заметки", "title": "заметки", diff --git a/src/renderer/src/pages/notes/NotesEditor.tsx b/src/renderer/src/pages/notes/NotesEditor.tsx index 8bdd44d12c..18c2cfe9d5 100644 --- a/src/renderer/src/pages/notes/NotesEditor.tsx +++ b/src/renderer/src/pages/notes/NotesEditor.tsx @@ -1,11 +1,16 @@ +import ActionIconButton from '@renderer/components/Buttons/ActionIconButton' import CodeEditor from '@renderer/components/CodeEditor' import { HSpaceBetweenStack } from '@renderer/components/Layout' import RichEditor from '@renderer/components/RichEditor' import { RichEditorRef } from '@renderer/components/RichEditor/types' import Selector from '@renderer/components/Selector' import { useNotesSettings } from '@renderer/hooks/useNotesSettings' +import { useSettings } from '@renderer/hooks/useSettings' +import { useAppDispatch } from '@renderer/store' +import { setEnableSpellCheck } from '@renderer/store/settings' import { EditorView } from '@renderer/types' -import { Empty } from 'antd' +import { Empty, Tooltip } from 'antd' +import { SpellCheck } from 'lucide-react' import { FC, memo, RefObject, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -21,7 +26,9 @@ interface NotesEditorProps { const NotesEditor: FC = memo( ({ activeNodeId, currentContent, tokenCount, onMarkdownChange, editorRef }) => { const { t } = useTranslation() + const dispatch = useAppDispatch() const { settings } = useNotesSettings() + const { enableSpellCheck } = useSettings() const currentViewMode = useMemo(() => { if (settings.defaultViewMode === 'edit') { return settings.defaultEditMode @@ -78,6 +85,7 @@ const NotesEditor: FC = memo( isFullWidth={settings.isFullWidth} fontFamily={settings.fontFamily} fontSize={settings.fontSize} + enableSpellCheck={enableSpellCheck} /> )} @@ -92,8 +100,21 @@ const NotesEditor: FC = memo( color: 'var(--color-text-3)', display: 'flex', alignItems: 'center', - gap: 8 + gap: 12 }}> + {tmpViewMode === 'preview' && ( + + { + const newValue = !enableSpellCheck + dispatch(setEnableSpellCheck(newValue)) + window.api.setEnableSpellCheck(newValue) + }}> + + + + )} setTmpViewMode(value)}