Merge remote-tracking branch 'origin/main' into feat/agents-new

This commit is contained in:
Vaayne
2025-09-21 17:25:02 +08:00
36 changed files with 907 additions and 113 deletions

2
.gitignore vendored
View File

@@ -54,6 +54,8 @@ local
.qwen/*
.trae/*
.claude-code-router/*
.codebuddy/*
.zed/*
CLAUDE.local.md
# vitest

View File

@@ -125,16 +125,20 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
✨ 新功能:
- 支持在对话中显示 AI 生成的图片
- 代码编辑工具支持更多终端类型
- 新增 Azure AI 服务支持
- 新增通义千问 Plus 模型
🐛 问题修复:
- 修复 Anthropic API URL 处理,移除尾部斜杠并添加端点路径处理
- 修复 MessageEditor 缺少 QuickPanelProvider 包装的问题
- 修复 MiniWindow 高度问题
- 修复翻译功能中选中文本未正确使用的问题
- 修复文件管理中空格键误删文件的问题
- 修复部分 AI 服务连接不稳定的问题
- 修复翻译页面长文本显示异常
- 优化列表显示样式
🚀 性能优化:
- 优化输入栏提及模型状态缓存,在渲染间保持状态
- 重构网络搜索参数支持模型内置搜索,新增 OpenAI Chat 和 OpenRouter 支持
🔧 重构改进:
- 更新 HeroUIProvider 导入路径,改善上下文管理
- 更新依赖项和 VSCode 开发环境配置
- 升级 @cherrystudio/ai-core 到 v1.0.0-alpha.17
- 提升输入框响应速度
- 优化模型切换性能
- 改进翻译功能的引用和邮件格式处理

View File

@@ -48,6 +48,27 @@ export default defineConfig([
'@eslint-react/no-children-to-array': 'off'
}
},
{
ignores: [
'node_modules/**',
'build/**',
'dist/**',
'out/**',
'local/**',
'.yarn/**',
'.gitignore',
'scripts/cloudflare-worker.js',
'src/main/integration/nutstore/sso/lib/**',
'src/main/integration/cherryin/index.js',
'src/main/integration/nutstore/sso/lib/**',
'src/renderer/src/ui/**',
'packages/**/dist'
]
},
// turn off oxlint supported rules.
...oxlint.configs['flat/eslint'],
...oxlint.configs['flat/typescript'],
...oxlint.configs['flat/unicorn'],
{
// LoggerService Custom Rules - only apply to src directory
files: ['src/**/*.{ts,tsx,js,jsx}'],
@@ -110,25 +131,4 @@ export default defineConfig([
'i18n/no-template-in-t': 'warn'
}
},
{
ignores: [
'node_modules/**',
'build/**',
'dist/**',
'out/**',
'local/**',
'.yarn/**',
'.gitignore',
'scripts/cloudflare-worker.js',
'src/main/integration/nutstore/sso/lib/**',
'src/main/integration/cherryin/index.js',
'src/main/integration/nutstore/sso/lib/**',
'src/renderer/src/ui/**',
'packages/**/dist'
]
},
// turn off oxlint supported rules.
...oxlint.configs['flat/eslint'],
...oxlint.configs['flat/typescript'],
...oxlint.configs['flat/unicorn']
])

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.6.0-rc.2",
"version": "1.6.0-rc.3",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",

View File

@@ -324,6 +324,10 @@ export enum IpcChannel {
// CodeTools
CodeTools_Run = 'code-tools:run',
CodeTools_GetAvailableTerminals = 'code-tools:get-available-terminals',
CodeTools_SetCustomTerminalPath = 'code-tools:set-custom-terminal-path',
CodeTools_GetCustomTerminalPath = 'code-tools:get-custom-terminal-path',
CodeTools_RemoveCustomTerminalPath = 'code-tools:remove-custom-terminal-path',
// OCR
OCR_ocr = 'ocr:ocr',

View File

@@ -219,3 +219,242 @@ export enum codeTools {
openaiCodex = 'openai-codex',
iFlowCli = 'iflow-cli'
}
export enum terminalApps {
systemDefault = 'Terminal',
iterm2 = 'iTerm2',
kitty = 'kitty',
alacritty = 'Alacritty',
wezterm = 'WezTerm',
ghostty = 'Ghostty',
tabby = 'Tabby',
// Windows terminals
windowsTerminal = 'WindowsTerminal',
powershell = 'PowerShell',
cmd = 'CMD',
wsl = 'WSL'
}
export interface TerminalConfig {
id: string
name: string
bundleId?: string
customPath?: string // For user-configured terminal paths on Windows
}
export interface TerminalConfigWithCommand extends TerminalConfig {
command: (directory: string, fullCommand: string) => { command: string; args: string[] }
}
export const MACOS_TERMINALS: TerminalConfig[] = [
{
id: terminalApps.systemDefault,
name: 'Terminal',
bundleId: 'com.apple.Terminal'
},
{
id: terminalApps.iterm2,
name: 'iTerm2',
bundleId: 'com.googlecode.iterm2'
},
{
id: terminalApps.kitty,
name: 'kitty',
bundleId: 'net.kovidgoyal.kitty'
},
{
id: terminalApps.alacritty,
name: 'Alacritty',
bundleId: 'org.alacritty'
},
{
id: terminalApps.wezterm,
name: 'WezTerm',
bundleId: 'com.github.wez.wezterm'
},
{
id: terminalApps.ghostty,
name: 'Ghostty',
bundleId: 'com.mitchellh.ghostty'
},
{
id: terminalApps.tabby,
name: 'Tabby',
bundleId: 'org.tabby'
}
]
export const WINDOWS_TERMINALS: TerminalConfig[] = [
{
id: terminalApps.cmd,
name: 'Command Prompt'
},
{
id: terminalApps.powershell,
name: 'PowerShell'
},
{
id: terminalApps.windowsTerminal,
name: 'Windows Terminal'
},
{
id: terminalApps.wsl,
name: 'WSL (Ubuntu/Debian)'
},
{
id: terminalApps.alacritty,
name: 'Alacritty'
},
{
id: terminalApps.wezterm,
name: 'WezTerm'
}
]
export const WINDOWS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
{
id: terminalApps.cmd,
name: 'Command Prompt',
command: (_: string, fullCommand: string) => ({
command: 'cmd',
args: ['/c', 'start', 'cmd', '/k', fullCommand]
})
},
{
id: terminalApps.powershell,
name: 'PowerShell',
command: (_: string, fullCommand: string) => ({
command: 'cmd',
args: ['/c', 'start', 'powershell', '-NoExit', '-Command', `& '${fullCommand}'`]
})
},
{
id: terminalApps.windowsTerminal,
name: 'Windows Terminal',
command: (_: string, fullCommand: string) => ({
command: 'wt',
args: ['cmd', '/k', fullCommand]
})
},
{
id: terminalApps.wsl,
name: 'WSL (Ubuntu/Debian)',
command: (_: string, fullCommand: string) => {
// Start WSL in a new window and execute the batch file from within WSL using cmd.exe
// The batch file will run in Windows context but output will be in WSL terminal
return {
command: 'cmd',
args: ['/c', 'start', 'wsl', '-e', 'bash', '-c', `cmd.exe /c '${fullCommand}' ; exec bash`]
}
}
},
{
id: terminalApps.alacritty,
name: 'Alacritty',
customPath: '', // Will be set by user in settings
command: (_: string, fullCommand: string) => ({
command: 'alacritty', // Will be replaced with customPath if set
args: ['-e', 'cmd', '/k', fullCommand]
})
},
{
id: terminalApps.wezterm,
name: 'WezTerm',
customPath: '', // Will be set by user in settings
command: (_: string, fullCommand: string) => ({
command: 'wezterm', // Will be replaced with customPath if set
args: ['start', 'cmd', '/k', fullCommand]
})
}
]
export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
{
id: terminalApps.systemDefault,
name: 'Terminal',
bundleId: 'com.apple.Terminal',
command: (directory: string, fullCommand: string) => ({
command: 'sh',
args: [
'-c',
`open -na Terminal && sleep 0.5 && osascript -e 'tell application "Terminal" to activate' -e 'tell application "Terminal" to do script "cd '${directory.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' && clear && ${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}" in front window'`
]
})
},
{
id: terminalApps.iterm2,
name: 'iTerm2',
bundleId: 'com.googlecode.iterm2',
command: (directory: string, fullCommand: string) => ({
command: 'sh',
args: [
'-c',
`open -na iTerm && sleep 0.8 && osascript -e 'on waitUntilRunning()\n repeat 50 times\n tell application "System Events"\n if (exists process "iTerm2") then exit repeat\n end tell\n delay 0.1\n end repeat\nend waitUntilRunning\n\nwaitUntilRunning()\n\ntell application "iTerm2"\n if (count of windows) = 0 then\n create window with default profile\n delay 0.3\n else\n tell current window\n create tab with default profile\n end tell\n delay 0.3\n end if\n tell current session of current window to write text "cd '${directory.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' && clear && ${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"\n activate\nend tell'`
]
})
},
{
id: terminalApps.kitty,
name: 'kitty',
bundleId: 'net.kovidgoyal.kitty',
command: (directory: string, fullCommand: string) => ({
command: 'sh',
args: [
'-c',
`cd "${directory}" && open -na kitty --args --directory="${directory}" sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "kitty" to activate'`
]
})
},
{
id: terminalApps.alacritty,
name: 'Alacritty',
bundleId: 'org.alacritty',
command: (directory: string, fullCommand: string) => ({
command: 'sh',
args: [
'-c',
`open -na Alacritty --args --working-directory "${directory}" -e sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "Alacritty" to activate'`
]
})
},
{
id: terminalApps.wezterm,
name: 'WezTerm',
bundleId: 'com.github.wez.wezterm',
command: (directory: string, fullCommand: string) => ({
command: 'sh',
args: [
'-c',
`open -na WezTerm --args start --new-tab --cwd "${directory}" -- sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "WezTerm" to activate'`
]
})
},
{
id: terminalApps.ghostty,
name: 'Ghostty',
bundleId: 'com.mitchellh.ghostty',
command: (directory: string, fullCommand: string) => ({
command: 'sh',
args: [
'-c',
`cd "${directory}" && open -na Ghostty --args --working-directory="${directory}" -e sh -c "${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}; exec \\$SHELL" && sleep 0.5 && osascript -e 'tell application "Ghostty" to activate'`
]
})
},
{
id: terminalApps.tabby,
name: 'Tabby',
bundleId: 'org.tabby',
command: (directory: string, fullCommand: string) => ({
command: 'sh',
args: [
'-c',
`if pgrep -x "Tabby" > /dev/null; then
open -na Tabby --args open && sleep 0.3
else
open -na Tabby --args open && sleep 2
fi && osascript -e 'tell application "Tabby" to activate' -e 'set the clipboard to "cd \\"${directory.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}\\" && clear && ${fullCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"' -e 'tell application "System Events" to tell process "Tabby" to keystroke "v" using {command down}' -e 'tell application "System Events" to key code 36'`
]
})
}
]

View File

@@ -834,6 +834,16 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// CodeTools
ipcMain.handle(IpcChannel.CodeTools_Run, codeToolsService.run)
ipcMain.handle(IpcChannel.CodeTools_GetAvailableTerminals, () => codeToolsService.getAvailableTerminalsForPlatform())
ipcMain.handle(IpcChannel.CodeTools_SetCustomTerminalPath, (_, terminalId: string, path: string) =>
codeToolsService.setCustomTerminalPath(terminalId, path)
)
ipcMain.handle(IpcChannel.CodeTools_GetCustomTerminalPath, (_, terminalId: string) =>
codeToolsService.getCustomTerminalPath(terminalId)
)
ipcMain.handle(IpcChannel.CodeTools_RemoveCustomTerminalPath, (_, terminalId: string) =>
codeToolsService.removeCustomTerminalPath(terminalId)
)
// OCR
ipcMain.handle(IpcChannel.OCR_ocr, (_, file: SupportedOcrFile, provider: OcrProvider) =>

View File

@@ -3,11 +3,20 @@ import os from 'node:os'
import path from 'node:path'
import { loggerService } from '@logger'
import { isWin } from '@main/constant'
import { isMac, isWin } from '@main/constant'
import { removeEnvProxy } from '@main/utils'
import { isUserInChina } from '@main/utils/ipService'
import { getBinaryName } from '@main/utils/process'
import { codeTools } from '@shared/config/constant'
import {
codeTools,
MACOS_TERMINALS,
MACOS_TERMINALS_WITH_COMMANDS,
terminalApps,
TerminalConfig,
TerminalConfigWithCommand,
WINDOWS_TERMINALS,
WINDOWS_TERMINALS_WITH_COMMANDS
} from '@shared/config/constant'
import { spawn } from 'child_process'
import { promisify } from 'util'
@@ -22,7 +31,10 @@ interface VersionInfo {
class CodeToolsService {
private versionCache: Map<string, { version: string; timestamp: number }> = new Map()
private terminalsCache: { terminals: TerminalConfig[]; timestamp: number } | null = null
private customTerminalPaths: Map<string, string> = 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
constructor() {
this.getBunPath = this.getBunPath.bind(this)
@@ -32,6 +44,23 @@ class CodeToolsService {
this.getVersionInfo = this.getVersionInfo.bind(this)
this.updatePackage = this.updatePackage.bind(this)
this.run = this.run.bind(this)
if (isMac || isWin) {
this.preloadTerminals()
}
}
/**
* Preload available terminals in background
*/
private async preloadTerminals(): Promise<void> {
try {
logger.info('Preloading available terminals...')
await this.getAvailableTerminals()
logger.info('Terminal preloading completed')
} catch (error) {
logger.warn('Terminal preloading failed:', error as Error)
}
}
public async getBunPath() {
@@ -75,10 +104,258 @@ class CodeToolsService {
}
}
/**
* Check if a single terminal is available
*/
private async checkTerminalAvailability(terminal: TerminalConfig): Promise<TerminalConfig | null> {
try {
if (isMac && terminal.bundleId) {
// macOS: Check if application is installed via bundle ID with timeout
const { stdout } = await execAsync(`mdfind "kMDItemCFBundleIdentifier == '${terminal.bundleId}'"`, {
timeout: 3000
})
if (stdout.trim()) {
return terminal
}
} else if (isWin) {
// Windows: Check terminal availability
return await this.checkWindowsTerminalAvailability(terminal)
} else {
// TODO: Check if terminal is available in linux
await execAsync(`which ${terminal.id}`, { timeout: 2000 })
return terminal
}
} catch (error) {
logger.debug(`Terminal ${terminal.id} not available:`, error as Error)
}
return null
}
/**
* Check Windows terminal availability (simplified - user configured paths)
*/
private async checkWindowsTerminalAvailability(terminal: TerminalConfig): Promise<TerminalConfig | null> {
try {
switch (terminal.id) {
case terminalApps.cmd:
// CMD is always available on Windows
return terminal
case terminalApps.powershell:
// Check for PowerShell in PATH
try {
await execAsync('powershell -Command "Get-Host"', { timeout: 3000 })
return terminal
} catch {
try {
await execAsync('pwsh -Command "Get-Host"', { timeout: 3000 })
return terminal
} catch {
return null
}
}
case terminalApps.windowsTerminal:
// Check for Windows Terminal via where command (doesn't launch the terminal)
try {
await execAsync('where wt', { timeout: 3000 })
return terminal
} catch {
return null
}
case terminalApps.wsl:
// Check for WSL
try {
await execAsync('wsl --status', { timeout: 3000 })
return terminal
} catch {
return null
}
default:
// For other terminals (Alacritty, WezTerm), check if user has configured custom path
return await this.checkCustomTerminalPath(terminal)
}
} catch (error) {
logger.debug(`Windows terminal ${terminal.id} not available:`, error as Error)
return null
}
}
/**
* Check if user has configured custom path for terminal
*/
private async checkCustomTerminalPath(terminal: TerminalConfig): Promise<TerminalConfig | null> {
// Check if user has configured custom path
const customPath = this.customTerminalPaths.get(terminal.id)
if (customPath && fs.existsSync(customPath)) {
try {
await execAsync(`"${customPath}" --version`, { timeout: 3000 })
return { ...terminal, customPath }
} catch {
return null
}
}
// Fallback to PATH check
try {
const command = terminal.id === terminalApps.alacritty ? 'alacritty' : 'wezterm'
await execAsync(`${command} --version`, { timeout: 3000 })
return terminal
} catch {
return null
}
}
/**
* Set custom path for a terminal (called from settings UI)
*/
public setCustomTerminalPath(terminalId: string, path: string): void {
logger.info(`Setting custom path for terminal ${terminalId}: ${path}`)
this.customTerminalPaths.set(terminalId, path)
// Clear terminals cache to force refresh
this.terminalsCache = null
}
/**
* Get custom path for a terminal
*/
public getCustomTerminalPath(terminalId: string): string | undefined {
return this.customTerminalPaths.get(terminalId)
}
/**
* Remove custom path for a terminal
*/
public removeCustomTerminalPath(terminalId: string): void {
logger.info(`Removing custom path for terminal ${terminalId}`)
this.customTerminalPaths.delete(terminalId)
// Clear terminals cache to force refresh
this.terminalsCache = null
}
/**
* Get available terminals (with caching and parallel checking)
*/
private async getAvailableTerminals(): Promise<TerminalConfig[]> {
const now = Date.now()
// Check cache first
if (this.terminalsCache && now - this.terminalsCache.timestamp < this.TERMINALS_CACHE_DURATION) {
logger.info(`Using cached terminals list (${this.terminalsCache.terminals.length} terminals)`)
return this.terminalsCache.terminals
}
logger.info('Checking available terminals in parallel...')
const startTime = Date.now()
// Get terminal list based on platform
const terminalList = isWin ? WINDOWS_TERMINALS : MACOS_TERMINALS
// Check all terminals in parallel
const terminalPromises = terminalList.map((terminal) => this.checkTerminalAvailability(terminal))
try {
// Wait for all checks to complete with a global timeout
const results = await Promise.allSettled(
terminalPromises.map((p) =>
Promise.race([p, new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))])
)
)
const availableTerminals: TerminalConfig[] = []
results.forEach((result, index) => {
if (result.status === 'fulfilled' && result.value) {
availableTerminals.push(result.value as TerminalConfig)
} else if (result.status === 'rejected') {
logger.debug(`Terminal check failed for ${MACOS_TERMINALS[index].id}:`, result.reason)
}
})
const endTime = Date.now()
logger.info(
`Terminal availability check completed in ${endTime - startTime}ms, found ${availableTerminals.length} terminals`
)
// Cache the results
this.terminalsCache = {
terminals: availableTerminals,
timestamp: now
}
return availableTerminals
} catch (error) {
logger.error('Error checking terminal availability:', error as Error)
// Return cached result if available, otherwise empty array
return this.terminalsCache?.terminals || []
}
}
/**
* Get terminal config by ID, fallback to system default
*/
private async getTerminalConfig(terminalId?: string): Promise<TerminalConfigWithCommand> {
const availableTerminals = await this.getAvailableTerminals()
const terminalCommands = isWin ? WINDOWS_TERMINALS_WITH_COMMANDS : MACOS_TERMINALS_WITH_COMMANDS
const defaultTerminal = isWin ? terminalApps.cmd : terminalApps.systemDefault
if (terminalId) {
let requestedTerminal = terminalCommands.find(
(t) => t.id === terminalId && availableTerminals.some((at) => at.id === t.id)
)
if (requestedTerminal) {
// Apply custom path if configured
const customPath = this.customTerminalPaths.get(terminalId)
if (customPath && isWin) {
requestedTerminal = this.applyCustomPath(requestedTerminal, customPath)
}
return requestedTerminal
} else {
logger.warn(`Requested terminal ${terminalId} not available, falling back to system default`)
}
}
// Fallback to system default Terminal
const systemTerminal = terminalCommands.find(
(t) => t.id === defaultTerminal && availableTerminals.some((at) => at.id === t.id)
)
if (systemTerminal) {
return systemTerminal
}
// If even system Terminal is not found, return the first available
const firstAvailable = terminalCommands.find((t) => availableTerminals.some((at) => at.id === t.id))
if (firstAvailable) {
return firstAvailable
}
// Last resort fallback
return terminalCommands.find((t) => t.id === defaultTerminal)!
}
/**
* Apply custom path to terminal configuration
*/
private applyCustomPath(terminal: TerminalConfigWithCommand, customPath: string): TerminalConfigWithCommand {
return {
...terminal,
customPath,
command: (directory: string, fullCommand: string) => {
const originalCommand = terminal.command(directory, fullCommand)
return {
...originalCommand,
command: customPath // Replace command with custom path
}
}
}
}
private async isPackageInstalled(cliTool: string): Promise<boolean> {
const executableName = await this.getCliExecutableName(cliTool)
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
const executablePath = path.join(binDir, executableName + (process.platform === 'win32' ? '.exe' : ''))
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
// Ensure bin directory exists
if (!fs.existsSync(binDir)) {
@@ -105,7 +382,7 @@ class CodeToolsService {
try {
const executableName = await this.getCliExecutableName(cliTool)
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
const executablePath = path.join(binDir, executableName + (process.platform === 'win32' ? '.exe' : ''))
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
const { stdout } = await execAsync(`"${executablePath}" --version`, { timeout: 10000 })
// Extract version number from output (format may vary by tool)
@@ -191,6 +468,17 @@ class CodeToolsService {
}
}
/**
* Get available terminals for the current platform
*/
public async getAvailableTerminalsForPlatform(): Promise<TerminalConfig[]> {
if (isMac || isWin) {
return this.getAvailableTerminals()
}
// For other platforms, return empty array for now
return []
}
/**
* Update a CLI tool to the latest version
*/
@@ -202,8 +490,7 @@ class CodeToolsService {
const bunInstallPath = path.join(os.homedir(), '.cherrystudio')
const registryUrl = await this.getNpmRegistryUrl()
const installEnvPrefix =
process.platform === 'win32'
const installEnvPrefix = isWin
? `set "BUN_INSTALL=${bunInstallPath}" && set "NPM_CONFIG_REGISTRY=${registryUrl}" &&`
: `export BUN_INSTALL="${bunInstallPath}" && export NPM_CONFIG_REGISTRY="${registryUrl}" &&`
@@ -241,7 +528,7 @@ class CodeToolsService {
_model: string,
directory: string,
env: Record<string, string>,
options: { autoUpdateToLatest?: boolean } = {}
options: { autoUpdateToLatest?: boolean; terminal?: string } = {}
) {
logger.info(`Starting CLI tool launch: ${cliTool} in directory: ${directory}`)
logger.debug(`Environment variables:`, Object.keys(env))
@@ -251,7 +538,7 @@ class CodeToolsService {
const bunPath = await this.getBunPath()
const executableName = await this.getCliExecutableName(cliTool)
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
const executablePath = path.join(binDir, executableName + (process.platform === 'win32' ? '.exe' : ''))
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
logger.debug(`Package name: ${packageName}`)
logger.debug(`Bun path: ${bunPath}`)
@@ -295,7 +582,13 @@ class CodeToolsService {
// Build environment variable prefix (based on platform)
const buildEnvPrefix = (isWindows: boolean) => {
if (Object.keys(env).length === 0) return ''
if (Object.keys(env).length === 0) {
logger.info('No environment variables to set')
return ''
}
logger.info('Setting environment variables:', Object.keys(env))
logger.info('Environment variable values:', env)
if (isWindows) {
// Windows uses set command
@@ -304,13 +597,29 @@ class CodeToolsService {
.join(' && ')
} else {
// Unix-like systems use export command
return Object.entries(env)
.map(([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`)
const validEntries = Object.entries(env).filter(([key, value]) => {
if (!key || key.trim() === '') {
return false
}
if (value === undefined || value === null) {
return false
}
return true
})
const envCommands = validEntries
.map(([key, value]) => {
const sanitizedValue = String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
const exportCmd = `export ${key}="${sanitizedValue}"`
logger.info(`Setting env var: ${key}="${sanitizedValue}"`)
logger.info(`Export command: ${exportCmd}`)
return exportCmd
})
.join(' && ')
return envCommands
}
}
// Build command to execute
let baseCommand = isWin ? `"${executablePath}"` : `"${bunPath}" "${executablePath}"`
// Add configuration parameters for OpenAI Codex
@@ -351,20 +660,20 @@ class CodeToolsService {
switch (platform) {
case 'darwin': {
// macOS - Use osascript to launch terminal and execute command directly, without showing startup command
// macOS - Support multiple terminals
const envPrefix = buildEnvPrefix(false)
const command = envPrefix ? `${envPrefix} && ${baseCommand}` : baseCommand
// Combine directory change with the main command to ensure they execute in the same shell session
const fullCommand = `cd '${directory.replace(/'/g, "\\'")}' && clear && ${command}`
terminalCommand = 'osascript'
terminalArgs = [
'-e',
`tell application "Terminal"
do script "${fullCommand.replace(/"/g, '\\"')}"
activate
end tell`
]
const terminalConfig = await this.getTerminalConfig(options.terminal)
logger.info(`Using terminal: ${terminalConfig.name} (${terminalConfig.id})`)
const { command: cmd, args } = terminalConfig.command(directory, fullCommand)
terminalCommand = cmd
terminalArgs = args
break
}
case 'win32': {
@@ -424,9 +733,23 @@ end tell`
throw new Error(`Failed to create launch script: ${error}`)
}
// Launch bat file - Use safest start syntax, no title parameter
terminalCommand = 'cmd'
terminalArgs = ['/c', 'start', batFilePath]
// Use selected terminal configuration
const terminalConfig = await this.getTerminalConfig(options.terminal)
logger.info(`Using terminal: ${terminalConfig.name} (${terminalConfig.id})`)
// Get command and args from terminal configuration
// Pass the bat file path as the command to execute
const fullCommand = batFilePath
const { command: cmd, args } = terminalConfig.command(directory, fullCommand)
// Override if it's a custom terminal with a custom path
if (terminalConfig.customPath) {
terminalCommand = terminalConfig.customPath
terminalArgs = args
} else {
terminalCommand = cmd
terminalArgs = args
}
// Set cleanup task (delete temp file after 5 minutes)
setTimeout(() => {

View File

@@ -1,7 +1,7 @@
import { electronAPI } from '@electron-toolkit/preload'
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
import { SpanContext } from '@opentelemetry/api'
import { UpgradeChannel } from '@shared/config/constant'
import { TerminalConfig, UpgradeChannel } from '@shared/config/constant'
import type { LogLevel, LogSourceWithContext } from '@shared/config/logger'
import type { FileChangeEvent } from '@shared/config/types'
import { IpcChannel } from '@shared/IpcChannel'
@@ -439,8 +439,16 @@ const api = {
model: string,
directory: string,
env: Record<string, string>,
options?: { autoUpdateToLatest?: boolean }
) => ipcRenderer.invoke(IpcChannel.CodeTools_Run, cliTool, model, directory, env, options)
options?: { autoUpdateToLatest?: boolean; terminal?: string }
) => ipcRenderer.invoke(IpcChannel.CodeTools_Run, cliTool, model, directory, env, options),
getAvailableTerminals: (): Promise<TerminalConfig[]> =>
ipcRenderer.invoke(IpcChannel.CodeTools_GetAvailableTerminals),
setCustomTerminalPath: (terminalId: string, path: string): Promise<void> =>
ipcRenderer.invoke(IpcChannel.CodeTools_SetCustomTerminalPath, terminalId, path),
getCustomTerminalPath: (terminalId: string): Promise<string | undefined> =>
ipcRenderer.invoke(IpcChannel.CodeTools_GetCustomTerminalPath, terminalId),
removeCustomTerminalPath: (terminalId: string): Promise<void> =>
ipcRenderer.invoke(IpcChannel.CodeTools_RemoveCustomTerminalPath, terminalId)
},
ocr: {
ocr: (file: SupportedOcrFile, provider: OcrProvider): Promise<OcrResult> =>

View File

@@ -13,16 +13,6 @@ import { ToolCallChunkHandler } from './handleToolCallChunk'
const logger = loggerService.withContext('AiSdkToChunkAdapter')
export interface CherryStudioChunk {
type: 'text-delta' | 'text-complete' | 'tool-call' | 'tool-result' | 'finish' | 'error'
text?: string
toolCall?: any
toolResult?: any
finishReason?: string
usage?: any
error?: any
}
/**
* AI SDK 到 Cherry Studio Chunk 适配器类
* 处理 fullStream 到 Cherry Studio chunk 的转换

View File

@@ -298,8 +298,29 @@ export class ToolCallChunkHandler {
type: ChunkType.MCP_TOOL_COMPLETE,
responses: [toolResponse]
})
const images: string[] = []
for (const content of toolResponse.response?.content || []) {
if (content.type === 'image' && content.data) {
images.push(`data:${content.mimeType};base64,${content.data}`)
}
}
if (images.length) {
this.onChunk({
type: ChunkType.IMAGE_CREATED
})
this.onChunk({
type: ChunkType.IMAGE_COMPLETE,
image: {
type: 'base64',
images: images
}
})
}
}
}
handleToolError(
chunk: {
type: 'tool-error'

View File

@@ -140,7 +140,17 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo
return builder.build()
}
const tagNameArray = ['think', 'thought', 'reasoning']
const tagName = {
reasoning: 'reasoning',
think: 'think',
thought: 'thought'
}
function getReasoningTagName(modelId: string | undefined): string {
if (modelId?.includes('gpt-oss')) return tagName.reasoning
if (modelId?.includes('gemini')) return tagName.thought
return tagName.think
}
/**
* 添加provider特定的中间件
@@ -156,7 +166,7 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
case 'openai':
case 'azure-openai': {
if (config.enableReasoning) {
const tagName = config.model?.id.includes('gemini') ? tagNameArray[1] : tagNameArray[0]
const tagName = getReasoningTagName(config.model?.id.toLowerCase())
builder.add({
name: 'thinking-tag-extraction',
middleware: extractReasoningMiddleware({ tagName })
@@ -168,13 +178,6 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
// Gemini特定中间件
break
case 'aws-bedrock': {
if (config.model?.id.includes('gpt-oss')) {
const tagName = tagNameArray[2]
builder.add({
name: 'thinking-tag-extraction',
middleware: extractReasoningMiddleware({ tagName })
})
}
break
}
default:

View File

@@ -213,7 +213,8 @@ export function providerToAiSdkConfig(
options: {
...options,
name: actualProvider.id,
...extraOptions
...extraOptions,
includeUsage: true
}
}
}

View File

@@ -84,6 +84,7 @@ export function buildProviderOptions(
case 'openai':
case 'openai-chat':
case 'azure':
case 'azure-responses':
providerSpecificOptions = {
...buildOpenAIProviderOptions(assistant, model, capabilities),
serviceTier: serviceTierSetting

View File

@@ -44,7 +44,7 @@ function mapMaxResultToOpenAIContextSize(maxResults: number): OpenAISearchConfig
export function buildProviderBuiltinWebSearchConfig(
providerId: BaseProviderId,
webSearchConfig: CherryWebSearchConfig
): WebSearchPluginConfig {
): WebSearchPluginConfig | undefined {
switch (providerId) {
case 'openai': {
return {
@@ -99,7 +99,7 @@ export function buildProviderBuiltinWebSearchConfig(
}
}
default: {
throw new Error(`Unsupported provider: ${providerId}`)
return {}
}
}
}

View File

@@ -76,6 +76,10 @@
list-style: initial;
}
.markdown ol {
list-style: decimal;
}
.markdown ul,
.markdown ol {
padding-left: 1.5em;

View File

@@ -255,7 +255,9 @@ const PopupContainer: React.FC<Props> = ({ source, title, resolve }) => {
try {
if (isNoteMode) {
const note = source.data as NotesTreeNode
const content = await window.api.file.read(note.id + '.md')
const content = note.externalPath
? await window.api.file.readExternal(note.externalPath)
: await window.api.file.read(note.id + '.md')
logger.debug('Note content:', content)
await addNote(content)
savedCount = 1

View File

@@ -225,6 +225,8 @@ export function isSupportedThinkingTokenQwenModel(model?: Model): boolean {
'qwen-plus-2025-04-28',
'qwen-plus-0714',
'qwen-plus-2025-07-14',
'qwen-plus-2025-07-28',
'qwen-plus-2025-09-11',
'qwen-turbo',
'qwen-turbo-latest',
'qwen-turbo-0428',
@@ -410,13 +412,14 @@ export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> =
'gemini-.*-pro.*$': { min: 128, max: 32768 },
// Qwen models
// qwen-plus-x 系列自 qwen-plus-2025-07-28 后模型最长思维链变为 81_920, qwen-plus 模型于 2025.9.16 同步变更
'qwen3-235b-a22b-thinking-2507$': { min: 0, max: 81_920 },
'qwen3-30b-a3b-thinking-2507$': { min: 0, max: 81_920 },
'qwen-plus-2025-07-28$': { min: 0, max: 81_920 },
'qwen-plus-latest$': { min: 0, max: 81_920 },
'qwen-plus-2025-07-14$': { min: 0, max: 38_912 },
'qwen-plus-2025-04-28$': { min: 0, max: 38_912 },
'qwen3-1\\.7b$': { min: 0, max: 30_720 },
'qwen3-0\\.6b$': { min: 0, max: 30_720 },
'qwen-plus.*$': { min: 0, max: 38_912 },
'qwen-plus.*$': { min: 0, max: 81_920 },
'qwen-turbo.*$': { min: 0, max: 38_912 },
'qwen-flash.*$': { min: 0, max: 81_920 },
'qwen3-(?!max).*$': { min: 1024, max: 38_912 },

View File

@@ -8,7 +8,8 @@ import {
setCurrentDirectory,
setEnvironmentVariables,
setSelectedCliTool,
setSelectedModel
setSelectedModel,
setSelectedTerminal
} from '@renderer/store/codeTools'
import { Model } from '@renderer/types'
import { codeTools } from '@shared/config/constant'
@@ -35,6 +36,14 @@ export const useCodeTools = () => {
[dispatch]
)
// 设置选择的终端
const setTerminal = useCallback(
(terminal: string) => {
dispatch(setSelectedTerminal(terminal))
},
[dispatch]
)
// 设置环境变量
const setEnvVars = useCallback(
(envVars: string) => {
@@ -105,6 +114,7 @@ export const useCodeTools = () => {
// 状态
selectedCliTool: codeToolsState.selectedCliTool,
selectedModel: selectedModel,
selectedTerminal: codeToolsState.selectedTerminal,
environmentVariables: environmentVariables,
directories: codeToolsState.directories,
currentDirectory: codeToolsState.currentDirectory,
@@ -113,6 +123,7 @@ export const useCodeTools = () => {
// 操作函数
setCliTool,
setModel,
setTerminal,
setEnvVars,
addDir,
removeDir,

View File

@@ -67,7 +67,8 @@ export const useKnowledge = (baseId: string) => {
// 添加笔记
const addNote = async (content: string) => {
await dispatch(addNoteThunk(baseId, content))
checkAllBases()
// 确保数据库写入完成后再触发队列检查
setTimeout(() => KnowledgeQueue.checkAllBases(), 100)
}
// 添加URL

View File

@@ -743,6 +743,10 @@
"bun_required_message": "Bun environment is required to run CLI tools",
"cli_tool": "CLI Tool",
"cli_tool_placeholder": "Select the CLI tool to use",
"custom_path": "Custom path",
"custom_path_error": "Failed to set custom terminal path",
"custom_path_required": "Custom path required for this terminal",
"custom_path_set": "Custom terminal path set successfully",
"description": "Quickly launch multiple code CLI tools to improve development efficiency",
"env_vars_help": "Enter custom environment variables (one per line, format: KEY=value)",
"environment_variables": "Environment Variables",
@@ -761,7 +765,10 @@
"model_placeholder": "Select the model to use",
"model_required": "Please select a model",
"select_folder": "Select Folder",
"set_custom_path": "Set custom terminal path",
"supported_providers": "Supported Providers",
"terminal": "Terminal",
"terminal_placeholder": "Select terminal application",
"title": "Code Tools",
"update_options": "Update Options",
"working_directory": "Working Directory"

View File

@@ -743,6 +743,10 @@
"bun_required_message": "运行 CLI 工具需要安装 Bun 环境",
"cli_tool": "CLI 工具",
"cli_tool_placeholder": "选择要使用的 CLI 工具",
"custom_path": "自定义路径",
"custom_path_error": "设置自定义终端路径失败",
"custom_path_required": "此终端需要设置自定义路径",
"custom_path_set": "自定义终端路径设置成功",
"description": "快速启动多个代码 CLI 工具,提高开发效率",
"env_vars_help": "输入自定义环境变量每行一个格式KEY=value",
"environment_variables": "环境变量",
@@ -761,7 +765,10 @@
"model_placeholder": "选择要使用的模型",
"model_required": "请选择模型",
"select_folder": "选择文件夹",
"set_custom_path": "设置自定义终端路径",
"supported_providers": "支持的服务商",
"terminal": "终端",
"terminal_placeholder": "选择终端应用",
"title": "代码工具",
"update_options": "更新选项",
"working_directory": "工作目录"

View File

@@ -743,6 +743,10 @@
"bun_required_message": "運行 CLI 工具需要安裝 Bun 環境",
"cli_tool": "CLI 工具",
"cli_tool_placeholder": "選擇要使用的 CLI 工具",
"custom_path": "自訂路徑",
"custom_path_error": "設定自訂終端機路徑失敗",
"custom_path_required": "此終端機需要設定自訂路徑",
"custom_path_set": "自訂終端機路徑設定成功",
"description": "快速啟動多個程式碼 CLI 工具,提高開發效率",
"env_vars_help": "輸入自定義環境變數每行一個格式KEY=value",
"environment_variables": "環境變數",
@@ -761,7 +765,10 @@
"model_placeholder": "選擇要使用的模型",
"model_required": "請選擇模型",
"select_folder": "選擇資料夾",
"set_custom_path": "設定自訂終端機路徑",
"supported_providers": "支援的供應商",
"terminal": "終端機",
"terminal_placeholder": "選擇終端機應用程式",
"title": "程式碼工具",
"update_options": "更新選項",
"working_directory": "工作目錄"

View File

@@ -743,6 +743,10 @@
"bun_required_message": "Για τη λειτουργία του εργαλείου CLI πρέπει να εγκαταστήσετε το περιβάλλον Bun",
"cli_tool": "Εργαλείο CLI",
"cli_tool_placeholder": "Επιλέξτε το CLI εργαλείο που θέλετε να χρησιμοποιήσετε",
"custom_path": "Προσαρμοσμένη διαδρομή",
"custom_path_error": "Η ρύθμιση της προσαρμοσμένης διαδρομής τερματικού απέτυχε",
"custom_path_required": "Αυτό το τερματικό απαιτεί τη ρύθμιση προσαρμοσμένης διαδρομής",
"custom_path_set": "Η προσαρμοσμένη διαδρομή τερματικού ορίστηκε με επιτυχία",
"description": "Εκκίνηση γρήγορα πολλών εργαλείων CLI κώδικα, για αύξηση της αποδοτικότητας ανάπτυξης",
"env_vars_help": "Εισαγάγετε προσαρμοσμένες μεταβλητές περιβάλλοντος (μία ανά γραμμή, με τη μορφή: KEY=value)",
"environment_variables": "Μεταβλητές περιβάλλοντος",
@@ -761,7 +765,10 @@
"model_placeholder": "Επιλέξτε το μοντέλο που θα χρησιμοποιήσετε",
"model_required": "Επιλέξτε μοντέλο",
"select_folder": "Επιλογή φακέλου",
"set_custom_path": "Ρύθμιση προσαρμοσμένης διαδρομής τερματικού",
"supported_providers": "υποστηριζόμενοι πάροχοι",
"terminal": "τερματικό",
"terminal_placeholder": "Επιλέξτε εφαρμογή τερματικού",
"title": "Εργαλεία κώδικα",
"update_options": "Ενημέρωση επιλογών",
"working_directory": "κατάλογος εργασίας"

View File

@@ -743,6 +743,10 @@
"bun_required_message": "Se requiere instalar el entorno Bun para ejecutar la herramienta de línea de comandos",
"cli_tool": "Herramienta de línea de comandos",
"cli_tool_placeholder": "Seleccione la herramienta de línea de comandos que desea utilizar",
"custom_path": "Ruta personalizada",
"custom_path_error": "Error al establecer la ruta de terminal personalizada",
"custom_path_required": "此终端需要设置自定义路径",
"custom_path_set": "Configuración de ruta de terminal personalizada exitosa",
"description": "Inicia rápidamente múltiples herramientas de línea de comandos para código, aumentando la eficiencia del desarrollo",
"env_vars_help": "Introduzca variables de entorno personalizadas (una por línea, formato: CLAVE=valor)",
"environment_variables": "Variables de entorno",
@@ -761,7 +765,10 @@
"model_placeholder": "Seleccionar el modelo que se va a utilizar",
"model_required": "Seleccione el modelo",
"select_folder": "Seleccionar carpeta",
"set_custom_path": "Establecer ruta de terminal personalizada",
"supported_providers": "Proveedores de servicios compatibles",
"terminal": "terminal",
"terminal_placeholder": "Seleccionar aplicación de terminal",
"title": "Herramientas de código",
"update_options": "Opciones de actualización",
"working_directory": "directorio de trabajo"

View File

@@ -743,6 +743,10 @@
"bun_required_message": "L'exécution de l'outil en ligne de commande nécessite l'installation de l'environnement Bun",
"cli_tool": "Outil CLI",
"cli_tool_placeholder": "Sélectionnez l'outil CLI à utiliser",
"custom_path": "Chemin personnalisé",
"custom_path_error": "Échec de la définition du chemin de terminal personnalisé",
"custom_path_required": "Ce terminal nécessite la configuration dun chemin personnalisé",
"custom_path_set": "Paramétrage personnalisé du chemin du terminal réussi",
"description": "Lancer rapidement plusieurs outils CLI de code pour améliorer l'efficacité du développement",
"env_vars_help": "Saisissez les variables d'environnement personnalisées (une par ligne, format : KEY=value)",
"environment_variables": "variables d'environnement",
@@ -761,7 +765,10 @@
"model_placeholder": "Sélectionnez le modèle à utiliser",
"model_required": "Veuillez sélectionner le modèle",
"select_folder": "Sélectionner le dossier",
"set_custom_path": "Définir un chemin de terminal personnalisé",
"supported_providers": "fournisseurs pris en charge",
"terminal": "Terminal",
"terminal_placeholder": "Choisir une application de terminal",
"title": "Outils de code",
"update_options": "Options de mise à jour",
"working_directory": "répertoire de travail"

View File

@@ -743,6 +743,10 @@
"bun_required_message": "CLI ツールを実行するには Bun 環境が必要です",
"cli_tool": "CLI ツール",
"cli_tool_placeholder": "使用する CLI ツールを選択してください",
"custom_path": "カスタムパス",
"custom_path_error": "カスタムターミナルパスの設定に失敗しました",
"custom_path_required": "この端末にはカスタムパスを設定する必要があります",
"custom_path_set": "カスタムターミナルパスの設定が成功しました",
"description": "開発効率を向上させるために、複数のコード CLI ツールを迅速に起動します",
"env_vars_help": "環境変数を設定して、CLI ツールの実行時に使用します。各変数は 1 行ごとに設定してください。",
"environment_variables": "環境変数",
@@ -761,7 +765,10 @@
"model_placeholder": "使用するモデルを選択してください",
"model_required": "モデルを選択してください",
"select_folder": "フォルダを選択",
"set_custom_path": "カスタムターミナルパスを設定",
"supported_providers": "サポートされているプロバイダー",
"terminal": "端末",
"terminal_placeholder": "ターミナルアプリを選択",
"title": "コードツール",
"update_options": "更新オプション",
"working_directory": "作業ディレクトリ"

View File

@@ -743,6 +743,10 @@
"bun_required_message": "Executar a ferramenta CLI requer a instalação do ambiente Bun",
"cli_tool": "Ferramenta de linha de comando",
"cli_tool_placeholder": "Selecione a ferramenta de linha de comando a ser utilizada",
"custom_path": "Caminho personalizado",
"custom_path_error": "Falha ao definir caminho de terminal personalizado",
"custom_path_required": "Este terminal requer a definição de um caminho personalizado",
"custom_path_set": "Configuração personalizada do caminho do terminal bem-sucedida",
"description": "Inicie rapidamente várias ferramentas de linha de comando de código, aumentando a eficiência do desenvolvimento",
"env_vars_help": "Insira variáveis de ambiente personalizadas (uma por linha, formato: CHAVE=valor)",
"environment_variables": "variáveis de ambiente",
@@ -761,7 +765,10 @@
"model_placeholder": "Selecione o modelo a ser utilizado",
"model_required": "Selecione o modelo",
"select_folder": "Selecionar pasta",
"set_custom_path": "Definir caminho personalizado do terminal",
"supported_providers": "Provedores de serviço suportados",
"terminal": "terminal",
"terminal_placeholder": "Selecionar aplicativo de terminal",
"title": "Ferramenta de código",
"update_options": "Opções de atualização",
"working_directory": "diretório de trabalho"

View File

@@ -743,6 +743,10 @@
"bun_required_message": "Запуск CLI-инструментов требует установки среды Bun",
"cli_tool": "Инструмент",
"cli_tool_placeholder": "Выберите CLI-инструмент для использования",
"custom_path": "Пользовательский путь",
"custom_path_error": "Не удалось задать пользовательский путь терминала",
"custom_path_required": "Этот терминал требует установки пользовательского пути",
"custom_path_set": "Пользовательский путь терминала успешно установлен",
"description": "Быстро запускает несколько CLI-инструментов для кода, повышая эффективность разработки",
"env_vars_help": "Установите переменные окружения для использования при запуске CLI-инструментов. Каждая переменная должна быть на отдельной строке в формате KEY=value",
"environment_variables": "Переменные окружения",
@@ -761,7 +765,10 @@
"model_placeholder": "Выберите модель для использования",
"model_required": "Пожалуйста, выберите модель",
"select_folder": "Выберите папку",
"set_custom_path": "Настройка пользовательского пути терминала",
"supported_providers": "Поддерживаемые поставщики",
"terminal": "терминал",
"terminal_placeholder": "Выбор приложения терминала",
"title": "Инструменты кода",
"update_options": "Параметры обновления",
"working_directory": "Рабочая директория"

View File

@@ -1,6 +1,7 @@
import AiProvider from '@renderer/aiCore'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import ModelSelector from '@renderer/components/ModelSelector'
import { isMac, isWin } from '@renderer/config/constant'
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
import { getProviderLogo } from '@renderer/config/providers'
import { useCodeTools } from '@renderer/hooks/useCodeTools'
@@ -14,9 +15,9 @@ import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setIsBunInstalled } from '@renderer/store/mcp'
import { Model } from '@renderer/types'
import { getClaudeSupportedProviders } from '@renderer/utils/provider'
import { codeTools } from '@shared/config/constant'
import { Alert, Avatar, Button, Checkbox, Input, Popover, Select, Space } from 'antd'
import { ArrowUpRight, Download, HelpCircle, Terminal, X } from 'lucide-react'
import { codeTools, terminalApps, TerminalConfig } from '@shared/config/constant'
import { Alert, Avatar, Button, Checkbox, Input, Popover, Select, Space, Tooltip } from 'antd'
import { ArrowUpRight, Download, FolderOpen, HelpCircle, Terminal, X } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
@@ -42,12 +43,14 @@ const CodeToolsPage: FC = () => {
const {
selectedCliTool,
selectedModel,
selectedTerminal,
environmentVariables,
directories,
currentDirectory,
canLaunch,
setCliTool,
setModel,
setTerminal,
setEnvVars,
setCurrentDir,
removeDir,
@@ -58,6 +61,9 @@ const CodeToolsPage: FC = () => {
const [isLaunching, setIsLaunching] = useState(false)
const [isInstallingBun, setIsInstallingBun] = useState(false)
const [autoUpdateToLatest, setAutoUpdateToLatest] = useState(false)
const [availableTerminals, setAvailableTerminals] = useState<TerminalConfig[]>([])
const [isLoadingTerminals, setIsLoadingTerminals] = useState(false)
const [terminalCustomPaths, setTerminalCustomPaths] = useState<Record<string, string>>({})
const modelPredicate = useCallback(
(m: Model) => {
@@ -119,6 +125,26 @@ const CodeToolsPage: FC = () => {
}
}, [dispatch])
// 获取可用终端
const loadAvailableTerminals = useCallback(async () => {
if (!isMac && !isWin) return // 仅 macOS 和 Windows 支持
try {
setIsLoadingTerminals(true)
const terminals = await window.api.codeTools.getAvailableTerminals()
setAvailableTerminals(terminals)
logger.info(
`Found ${terminals.length} available terminals:`,
terminals.map((t) => t.name)
)
} catch (error) {
logger.error('Failed to load available terminals:', error as Error)
setAvailableTerminals([])
} finally {
setIsLoadingTerminals(false)
}
}, [])
// 安装 bun
const handleInstallBun = async () => {
try {
@@ -179,11 +205,37 @@ const CodeToolsPage: FC = () => {
// 执行启动操作
const executeLaunch = async (env: Record<string, string>) => {
window.api.codeTools.run(selectedCliTool, selectedModel?.id!, currentDirectory, env, {
autoUpdateToLatest
autoUpdateToLatest,
terminal: selectedTerminal
})
window.toast.success(t('code.launch.success'))
}
// 设置终端自定义路径
const handleSetCustomPath = async (terminalId: string) => {
try {
const result = await window.api.file.select({
properties: ['openFile'],
filters: [
{ name: 'Executable', extensions: ['exe'] },
{ name: 'All Files', extensions: ['*'] }
]
})
if (result && result.length > 0) {
const path = result[0].path
await window.api.codeTools.setCustomTerminalPath(terminalId, path)
setTerminalCustomPaths((prev) => ({ ...prev, [terminalId]: path }))
window.toast.success(t('code.custom_path_set'))
// Reload terminals to reflect changes
loadAvailableTerminals()
}
} catch (error) {
logger.error('Failed to set custom terminal path:', error as Error)
window.toast.error(t('code.custom_path_error'))
}
}
// 处理启动
const handleLaunch = async () => {
const validation = validateLaunch()
@@ -216,6 +268,11 @@ const CodeToolsPage: FC = () => {
checkBunInstallation()
}, [checkBunInstallation])
// 页面加载时获取可用终端
useEffect(() => {
loadAvailableTerminals()
}, [loadAvailableTerminals])
return (
<Container>
<Navbar>
@@ -350,6 +407,47 @@ const CodeToolsPage: FC = () => {
<div style={{ fontSize: 12, color: 'var(--color-text-3)', marginTop: 4 }}>{t('code.env_vars_help')}</div>
</SettingsItem>
{/* 终端选择 (macOS 和 Windows) */}
{(isMac || isWin) && availableTerminals.length > 0 && (
<SettingsItem>
<div className="settings-label">{t('code.terminal')}</div>
<Space.Compact style={{ width: '100%', display: 'flex' }}>
<Select
style={{ flex: 1 }}
placeholder={t('code.terminal_placeholder')}
value={selectedTerminal}
onChange={setTerminal}
loading={isLoadingTerminals}
options={availableTerminals.map((terminal) => ({
value: terminal.id,
label: terminal.name
}))}
/>
{/* Show custom path button for Windows terminals except cmd/powershell */}
{isWin &&
selectedTerminal &&
selectedTerminal !== terminalApps.cmd &&
selectedTerminal !== terminalApps.powershell &&
selectedTerminal !== terminalApps.windowsTerminal && (
<Tooltip title={terminalCustomPaths[selectedTerminal] || t('code.set_custom_path')}>
<Button icon={<FolderOpen size={16} />} onClick={() => handleSetCustomPath(selectedTerminal)} />
</Tooltip>
)}
</Space.Compact>
{isWin &&
selectedTerminal &&
selectedTerminal !== terminalApps.cmd &&
selectedTerminal !== terminalApps.powershell &&
selectedTerminal !== terminalApps.windowsTerminal && (
<div style={{ fontSize: 12, color: 'var(--color-text-3)', marginTop: 4 }}>
{terminalCustomPaths[selectedTerminal]
? `${t('code.custom_path')}: ${terminalCustomPaths[selectedTerminal]}`
: t('code.custom_path_required')}
</div>
)}
</SettingsItem>
)}
<SettingsItem>
<div className="settings-label">{t('code.update_options')}</div>
<Checkbox checked={autoUpdateToLatest} onChange={(e) => setAutoUpdateToLatest(e.target.checked)}>

View File

@@ -377,7 +377,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
}
}
if (event.key === 'Backspace' && text.trim() === '' && files.length > 0) {
if (event.key === 'Backspace' && text.length === 0 && files.length > 0) {
setFiles((prev) => prev.slice(0, -1))
return event.preventDefault()
}

View File

@@ -6,7 +6,6 @@ import {
MAX_CONTEXT_COUNT,
UNLIMITED_CONTEXT_COUNT
} from '@renderer/config/constant'
import { isQwenMTModel } from '@renderer/config/models'
import { UNKNOWN } from '@renderer/config/translate'
import i18n from '@renderer/i18n'
import store from '@renderer/store'
@@ -70,24 +69,16 @@ export function getDefaultTranslateAssistant(targetLanguage: TranslateLanguage,
temperature: 0.7
}
let prompt: string
let content: string
if (isQwenMTModel(model)) {
content = text
prompt = ''
} else {
content = 'follow system instruction'
prompt = store
const content = store
.getState()
.settings.translateModelPrompt.replaceAll('{{target_language}}', targetLanguage.value)
.replaceAll('{{text}}', text)
}
const translateAssistant = {
...assistant,
model,
settings,
prompt,
prompt: '',
targetLanguage,
content
} satisfies TranslateAssistant

View File

@@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { Model } from '@renderer/types'
import { codeTools } from '@shared/config/constant'
import { codeTools, terminalApps } from '@shared/config/constant'
// 常量定义
const MAX_DIRECTORIES = 10 // 最多保存10个目录
@@ -16,6 +16,8 @@ export interface CodeToolsState {
directories: string[]
// 当前选择的目录
currentDirectory: string
// 选择的终端 ( macOS 和 Windows)
selectedTerminal: string
}
export const initialState: CodeToolsState = {
@@ -32,7 +34,8 @@ export const initialState: CodeToolsState = {
'gemini-cli': ''
},
directories: [],
currentDirectory: ''
currentDirectory: '',
selectedTerminal: terminalApps.systemDefault
}
const codeToolsSlice = createSlice({
@@ -44,6 +47,11 @@ const codeToolsSlice = createSlice({
state.selectedCliTool = action.payload
},
// 设置选择的终端
setSelectedTerminal: (state, action: PayloadAction<string>) => {
state.selectedTerminal = action.payload
},
// 设置选择的模型(为当前 CLI 工具设置)
setSelectedModel: (state, action: PayloadAction<Model | null>) => {
state.selectedModels[state.selectedCliTool] = action.payload
@@ -113,12 +121,14 @@ const codeToolsSlice = createSlice({
state.environmentVariables = initialState.environmentVariables
state.directories = initialState.directories
state.currentDirectory = initialState.currentDirectory
state.selectedTerminal = initialState.selectedTerminal
}
}
})
export const {
setSelectedCliTool,
setSelectedTerminal,
setSelectedModel,
setEnvironmentVariables,
addDirectory,

View File

@@ -25,7 +25,7 @@ describe('api', () => {
})
it('should handle empty string gracefully', () => {
expect(formatApiHost('')).toBe('/v1/')
expect(formatApiHost('')).toBe('')
})
})

View File

@@ -20,6 +20,10 @@ export function formatApiKeys(value: string): string {
* @returns {string} 格式化后的 API 主机地址。
*/
export function formatApiHost(host: string, apiVersion: string = 'v1'): string {
if (!host) {
return ''
}
const forceUseOriginalHost = () => {
if (host.endsWith('/')) {
return true

View File

@@ -138,8 +138,9 @@ const ActionTranslate: FC<Props> = ({ action, scrollToBottom }) => {
}
}
assistantRef.current = getDefaultTranslateAssistant(translateLang, action.selectedText)
processMessages(assistantRef.current, topicRef.current, action.selectedText, setAskId, onStream, onFinish, onError)
const assistant = getDefaultTranslateAssistant(translateLang, action.selectedText)
assistantRef.current = assistant
processMessages(assistant, topicRef.current, assistant.content, setAskId, onStream, onFinish, onError)
}, [action, targetLanguage, alterLanguage, scrollToBottom])
useEffect(() => {