feat: refactor IPC handlers for binary management and update localization strings

commit 97d251569690462763810270ad850ad6b0057ac9
Author: kangfenmao <kangfenmao@qq.com>
Date:   Mon Mar 17 10:24:43 2025 +0800

    feat: refactor IPC handlers for binary management and update localization strings

    - Simplified IPC handlers for checking binary existence and retrieving binary paths by removing unnecessary await statements.
    - Updated localization strings in English, Japanese, Russian, Simplified Chinese, and Traditional Chinese to change "Install Dependencies" to "Install".
    - Removed the MCPSettings component, replacing it with a new InstallNpxUv component for better management of binary installations.

commit d0f6039c7659a0f4cc97555434999c731ea07f9f
Author: Vaayne <liu.vaayne@gmail.com>
Date:   Sun Mar 16 23:52:18 2025 +0800

    feat: enhance showAddModal to pre-fill form with server details

commit dde8253dc8bdffb482b9af19a07bc89886a19d3a
Author: Vaayne <liu.vaayne@gmail.com>
Date:   Sun Mar 16 23:27:17 2025 +0800

    feat: add binary management APIs and enhance MCP service for dependency installation

commit d8fda4b7b0e238097f1811850517bd56fe0de0df
Author: Vaayne <liu.vaayne@gmail.com>
Date:   Sun Mar 16 21:57:34 2025 +0800

    fix: improve error logging in MCPService and streamline tool call response handling in OpenAIProvider

commit e7af2085a66989d9be546701e4f5308e1008cb18
Author: Vaayne <liu.vaayne@gmail.com>
Date:   Sun Mar 16 15:14:32 2025 +0800

    fix: lint

commit 2ef7d16298a1270df26974158140015b8cbd91bc
Author: Vaayne <liu.vaayne@gmail.com>
Date:   Sat Mar 15 21:11:26 2025 +0800

    feat: implement uv binary installation script and integrate with MCP service

commit d318b4e5fc8b506e6d4b08490a9e7ceffe9add80
Author: Vaayne <liu.vaayne@gmail.com>
Date:   Sat Mar 15 20:28:58 2025 +0800

    feat: add uv binary installation script and enhance MCP service command handling
This commit is contained in:
kangfenmao 2025-03-17 10:25:48 +08:00
parent 1e830c0613
commit 857bb02e50
16 changed files with 685 additions and 14 deletions

View File

@ -0,0 +1,221 @@
const fs = require('fs')
const path = require('path')
const os = require('os')
const { execSync } = require('child_process')
const https = require('https')
// Base URL for downloading bun binaries
const BUN_RELEASE_BASE_URL = 'https://github.com/oven-sh/bun/releases/download'
const DEFAULT_BUN_VERSION = '1.2.5' // Default fallback version
// Mapping of platform+arch to binary package name
const BUN_PACKAGES = {
'darwin-arm64': 'bun-darwin-aarch64.zip',
'darwin-x64': 'bun-darwin-x64.zip',
'win32-x64': 'bun-windows-x64.zip',
'win32-x64-baseline': 'bun-windows-x64-baseline.zip',
'linux-x64': 'bun-linux-x64.zip',
'linux-x64-baseline': 'bun-linux-x64-baseline.zip',
'linux-arm64': 'bun-linux-aarch64.zip',
// MUSL variants
'linux-musl-x64': 'bun-linux-x64-musl.zip',
'linux-musl-x64-baseline': 'bun-linux-x64-musl-baseline.zip',
'linux-musl-arm64': 'bun-linux-aarch64-musl.zip'
}
/**
* Fetches the latest version of bun from GitHub API
* @returns {Promise<string>} The latest version tag (without 'bun-v' prefix)
*/
async function getLatestBunVersion() {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: '/repos/oven-sh/bun/releases/latest',
headers: {
'User-Agent': 'cherry-studio-install-script'
}
}
const req = https.get(options, (res) => {
if (res.statusCode !== 200) {
reject(new Error(`Request failed with status code ${res.statusCode}`))
return
}
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
try {
const release = JSON.parse(data)
// Remove the 'bun-v' prefix if present
const version = release.tag_name.startsWith('bun-v')
? release.tag_name.substring(5)
: release.tag_name.startsWith('v')
? release.tag_name.substring(1)
: release.tag_name
resolve(version)
} catch (error) {
reject(new Error(`Failed to parse GitHub API response: ${error.message}`))
}
})
})
req.on('error', (error) => {
reject(new Error(`Failed to fetch latest version: ${error.message}`))
})
req.end()
})
}
/**
* Downloads and extracts the bun binary for the specified platform and architecture
* @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux')
* @param {string} arch Architecture to download for (e.g., 'x64', 'arm64')
* @param {string} version Version of bun to download
* @param {boolean} isMusl Whether to use MUSL variant for Linux
* @param {boolean} isBaseline Whether to use baseline variant
*/
async function downloadBunBinary(platform, arch, version = DEFAULT_BUN_VERSION, isMusl = false, isBaseline = false) {
let platformKey = isMusl ? `${platform}-musl-${arch}` : `${platform}-${arch}`
if (isBaseline) {
platformKey += '-baseline'
}
const packageName = BUN_PACKAGES[platformKey]
if (!packageName) {
console.error(`No binary available for ${platformKey}`)
return false
}
// Create output directory structure
const archDir = path.join(os.homedir(), '.cherrystudio', 'bin')
// Ensure directories exist
fs.mkdirSync(archDir, { recursive: true })
// Download URL for the specific binary
const downloadUrl = `${BUN_RELEASE_BASE_URL}/bun-v${version}/${packageName}`
const tempdir = os.tmpdir()
// Create a temporary file for the downloaded binary
const localFilename = path.join(tempdir, packageName)
try {
console.log(`Downloading bun ${version} for ${platformKey}...`)
console.log(`URL: ${downloadUrl}`)
// Download the file
execSync(`curl --fail -L -o "${localFilename}" "${downloadUrl}"`, { stdio: 'inherit' })
// Extract the zip file
console.log(`Extracting ${packageName} to ${archDir}...`)
execSync(`unzip -o "${localFilename}" -d "${tempdir}"`, { stdio: 'inherit' })
execSync(`mv ${tempdir}/${packageName.split('.')[0]}/* ${archDir}/`, { stdio: 'inherit' })
// Clean up the downloaded file
fs.unlinkSync(localFilename)
console.log(`Successfully installed bun ${version} for ${platformKey}`)
return true
} catch (error) {
console.error(`Error installing bun for ${platformKey}: ${error.message}`)
if (fs.existsSync(localFilename)) {
fs.unlinkSync(localFilename)
}
return false
}
}
/**
* Detects current platform and architecture
*/
function detectPlatformAndArch() {
const platform = os.platform()
const arch = os.arch()
const isMusl = platform === 'linux' && detectIsMusl()
return { platform, arch, isMusl }
}
/**
* Attempts to detect if running on MUSL libc
*/
function detectIsMusl() {
try {
// Simple check for Alpine Linux which uses MUSL
const output = execSync('cat /etc/os-release').toString()
return output.toLowerCase().includes('alpine')
} catch (error) {
return false
}
}
/**
* Main function to install bun
*/
async function installBun() {
const args = process.argv.slice(2)
const specifiedVersion = args.find((arg) => !arg.startsWith('--'))
// Get the latest version if no specific version is provided
const version = specifiedVersion || (await getLatestBunVersion())
console.log(`Using bun version: ${version}`)
const specificPlatform = args.find((arg) => arg.startsWith('--platform='))?.split('=')[1]
const specificArch = args.find((arg) => arg.startsWith('--arch='))?.split('=')[1]
const specificMusl = args.includes('--musl')
const specificBaseline = args.includes('--baseline')
const installAll = args.includes('--all')
if (installAll) {
console.log(`Installing all bun ${version} binaries...`)
for (const platformKey in BUN_PACKAGES) {
let platform,
arch,
isMusl = false,
isBaseline = false
if (platformKey.includes('-musl-')) {
const [platformPart, archPart] = platformKey.split('-musl-')
platform = platformPart
isMusl = true
if (archPart.includes('-baseline')) {
;[arch] = archPart.split('-baseline')
isBaseline = true
} else {
arch = archPart
}
} else if (platformKey.includes('-baseline')) {
const [platformPart, archPart] = platformKey.split('-')
platform = platformPart
arch = archPart.replace('-baseline', '')
isBaseline = true
} else {
;[platform, arch] = platformKey.split('-')
}
await downloadBunBinary(platform, arch, version, isMusl, isBaseline)
}
} else {
const { platform, arch, isMusl } = detectPlatformAndArch()
const targetPlatform = specificPlatform || platform
const targetArch = specificArch || arch
const targetMusl = specificMusl || isMusl
const targetBaseline = specificBaseline || false
console.log(
`Installing bun ${version} for ${targetPlatform}-${targetArch}${targetMusl ? ' (MUSL)' : ''}${targetBaseline ? ' (baseline)' : ''}...`
)
await downloadBunBinary(targetPlatform, targetArch, version, targetMusl, targetBaseline)
}
}
// Run the installation
installBun().catch((error) => {
console.error('Installation failed:', error)
process.exit(1)
})

View File

@ -0,0 +1,202 @@
const fs = require('fs')
const path = require('path')
const os = require('os')
const { execSync } = require('child_process')
const https = require('https')
// Base URL for downloading uv binaries
const UV_RELEASE_BASE_URL = 'https://github.com/astral-sh/uv/releases/download'
const DEFAULT_UV_VERSION = '0.6.6'
// Mapping of platform+arch to binary package name
const UV_PACKAGES = {
'darwin-arm64': 'uv-aarch64-apple-darwin.tar.gz',
'darwin-x64': 'uv-x86_64-apple-darwin.tar.gz',
'win32-arm64': 'uv-aarch64-pc-windows-msvc.zip',
'win32-ia32': 'uv-i686-pc-windows-msvc.zip',
'win32-x64': 'uv-x86_64-pc-windows-msvc.zip',
'linux-arm64': 'uv-aarch64-unknown-linux-gnu.tar.gz',
'linux-ia32': 'uv-i686-unknown-linux-gnu.tar.gz',
'linux-ppc64': 'uv-powerpc64-unknown-linux-gnu.tar.gz',
'linux-ppc64le': 'uv-powerpc64le-unknown-linux-gnu.tar.gz',
'linux-s390x': 'uv-s390x-unknown-linux-gnu.tar.gz',
'linux-x64': 'uv-x86_64-unknown-linux-gnu.tar.gz',
'linux-armv7l': 'uv-armv7-unknown-linux-gnueabihf.tar.gz',
// MUSL variants
'linux-musl-arm64': 'uv-aarch64-unknown-linux-musl.tar.gz',
'linux-musl-ia32': 'uv-i686-unknown-linux-musl.tar.gz',
'linux-musl-x64': 'uv-x86_64-unknown-linux-musl.tar.gz',
'linux-musl-armv6l': 'uv-arm-unknown-linux-musleabihf.tar.gz',
'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.tar.gz'
}
/**
* Fetches the latest version of uv from GitHub API
* @returns {Promise<string>} The latest version tag (without 'v' prefix)
*/
async function getLatestUvVersion() {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: '/repos/astral-sh/uv/releases/latest',
headers: {
'User-Agent': 'cherry-studio-install-script'
}
}
const req = https.get(options, (res) => {
if (res.statusCode !== 200) {
reject(new Error(`Request failed with status code ${res.statusCode}`))
return
}
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
try {
const release = JSON.parse(data)
// Remove the 'v' prefix if present
const version = release.tag_name.startsWith('v') ? release.tag_name.substring(1) : release.tag_name
resolve(version)
} catch (error) {
reject(new Error(`Failed to parse GitHub API response: ${error.message}`))
}
})
})
req.on('error', (error) => {
reject(new Error(`Failed to fetch latest version: ${error.message}`))
})
req.end()
})
}
/**
* Downloads and extracts the uv binary for the specified platform and architecture
* @param {string} platform Platform to download for (e.g., 'darwin', 'win32', 'linux')
* @param {string} arch Architecture to download for (e.g., 'x64', 'arm64')
* @param {string} version Version of uv to download
* @param {boolean} isMusl Whether to use MUSL variant for Linux
*/
async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, isMusl = false) {
const platformKey = isMusl ? `${platform}-musl-${arch}` : `${platform}-${arch}`
const packageName = UV_PACKAGES[platformKey]
if (!packageName) {
console.error(`No binary available for ${platformKey}`)
return false
}
// Create output directory structure
const archDir = path.join(os.homedir(), '.cherrystudio', 'bin')
// Ensure directories exist
fs.mkdirSync(archDir, { recursive: true })
// Download URL for the specific binary
const downloadUrl = `${UV_RELEASE_BASE_URL}/${version}/${packageName}`
const tempdir = os.tmpdir()
const localFilename = path.join(tempdir, packageName)
try {
console.log(`Downloading uv ${version} for ${platformKey}...`)
console.log(`URL: ${downloadUrl}`)
// Download the file
execSync(`curl --fail -L -o "${localFilename}" "${downloadUrl}"`, { stdio: 'inherit' })
// Extract based on file extension
console.log(`Extracting ${packageName} to ${archDir}...`)
if (packageName.endsWith('.tar.gz')) {
execSync(`tar -xzf "${localFilename}" -C "${tempdir}"`, { stdio: 'inherit' })
} else if (packageName.endsWith('.zip')) {
execSync(`unzip -o "${localFilename}" -d "${tempdir}"`, { stdio: 'inherit' })
}
execSync(`mv ${tempdir}/${packageName.split('.')[0]}/* ${archDir}/`, { stdio: 'inherit' })
// Clean up the downloaded file
fs.unlinkSync(localFilename)
console.log(`Successfully installed uv ${version} for ${platform}-${arch}`)
return true
} catch (error) {
console.error(`Error installing uv for ${platformKey}: ${error.message}`)
if (fs.existsSync(localFilename)) {
fs.unlinkSync(localFilename)
}
return false
}
}
/**
* Detects current platform and architecture
*/
function detectPlatformAndArch() {
const platform = os.platform()
const arch = os.arch()
const isMusl = platform === 'linux' && detectIsMusl()
return { platform, arch, isMusl }
}
/**
* Attempts to detect if running on MUSL libc
*/
function detectIsMusl() {
try {
// Simple check for Alpine Linux which uses MUSL
const output = execSync('cat /etc/os-release').toString()
return output.toLowerCase().includes('alpine')
} catch (error) {
return false
}
}
/**
* Main function to install uv
*/
async function installUv() {
const args = process.argv.slice(2)
const specifiedVersion = args.find((arg) => !arg.startsWith('--'))
// Get the latest version if no specific version is provided
const version = specifiedVersion || (await getLatestUvVersion())
console.log(`Using uv version: ${version}`)
const specificPlatform = args.find((arg) => arg.startsWith('--platform='))?.split('=')[1]
const specificArch = args.find((arg) => arg.startsWith('--arch='))?.split('=')[1]
const specificMusl = args.includes('--musl')
const installAll = args.includes('--all')
if (installAll) {
console.log(`Installing all uv ${version} binaries...`)
for (const platformKey in UV_PACKAGES) {
const [platformArch, musl] = platformKey.split('-musl-')
if (musl) {
const [platform, arch] = platformArch.split('-')
await downloadUvBinary(platform, arch, version, true)
} else {
const [platform, arch] = platformKey.split('-')
await downloadUvBinary(platform, arch, version, false)
}
}
} else {
const { platform, arch, isMusl } = detectPlatformAndArch()
const targetPlatform = specificPlatform || platform
const targetArch = specificArch || arch
const targetMusl = specificMusl || isMusl
console.log(`Installing uv ${version} for ${targetPlatform}-${targetArch}${targetMusl ? ' (MUSL)' : ''}...`)
await downloadUvBinary(targetPlatform, targetArch, version, targetMusl)
}
}
// Run the installation
installUv().catch((error) => {
console.error('Installation failed:', error)
process.exit(1)
})

View File

@ -1,5 +1,6 @@
import fs from 'node:fs' import fs from 'node:fs'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import { MCPServer, Shortcut, ThemeMode } from '@types' import { MCPServer, Shortcut, ThemeMode } from '@types'
import { BrowserWindow, ipcMain, session, shell } from 'electron' import { BrowserWindow, ipcMain, session, shell } from 'electron'
import log from 'electron-log' import log from 'electron-log'
@ -232,6 +233,11 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup()) ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup())
ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name))
ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name))
ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js'))
ipcMain.handle('app:install-bun-binary', () => runInstallScript('install-bun.js'))
// Listen for changes in MCP servers and notify renderer // Listen for changes in MCP servers and notify renderer
mcpService.on('servers-updated', (servers) => { mcpService.on('servers-updated', (servers) => {
mainWindow?.webContents.send('mcp:servers-updated', servers) mainWindow?.webContents.send('mcp:servers-updated', servers)

View File

@ -1,10 +1,13 @@
import { isLinux, isMac, isWin } from '@main/constant' import { isLinux, isMac, isWin } from '@main/constant'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import type { Client } from '@modelcontextprotocol/sdk/client/index.js' import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
import type { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import type { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { MCPServer, MCPTool } from '@types' import { MCPServer, MCPTool } from '@types'
import log from 'electron-log' import log from 'electron-log'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import os from 'os'
import path from 'path'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { windowService } from './WindowService' import { windowService } from './WindowService'
@ -32,6 +35,7 @@ export default class MCPService extends EventEmitter {
constructor() { constructor() {
super() super()
this.createServerLoadingPromise() this.createServerLoadingPromise()
this.init().catch((err) => this.logError('Failed to initialize MCP service', err))
} }
/** /**
@ -59,7 +63,7 @@ export default class MCPService extends EventEmitter {
// Initialize if not already initialized // Initialize if not already initialized
if (!this.initialized) { if (!this.initialized) {
this.init().catch(this.logError('Failed to initialize MCP service')) this.init().catch((err) => this.logError('Failed to initialize MCP service', err))
} }
} }
@ -125,8 +129,8 @@ export default class MCPService extends EventEmitter {
/** /**
* Helper to create consistent error logging functions * Helper to create consistent error logging functions
*/ */
private logError(message: string) { private logError(message: string, err?: any): void {
return (err: Error) => log.error(`[MCP] ${message}:`, err) log.error(`[MCP] ${message}`, err)
} }
/** /**
@ -137,7 +141,7 @@ export default class MCPService extends EventEmitter {
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js') const { Client } = await import('@modelcontextprotocol/sdk/client/index.js')
return Client return Client
} catch (err) { } catch (err) {
log.error('[MCP] Failed to import Client:', err) this.logError('Failed to import Client:', err)
throw err throw err
} }
} }
@ -323,10 +327,38 @@ export default class MCPService extends EventEmitter {
transport = new this.sseTransport!(new URL(baseUrl)) transport = new this.sseTransport!(new URL(baseUrl))
} else if (command) { } else if (command) {
let cmd: string = command let cmd: string = command
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin')
log.info(`[MCP] Using binaries directory: ${binariesDir}`)
if (command === 'npx') { if (command === 'npx') {
cmd = process.platform === 'win32' ? `${command}.cmd` : command // check if cmd exists, if not exist, install it using `node scripts/install-bun.js`
const isBunExist = await isBinaryExists('bun')
if (!isBunExist) {
log.info(`[MCP] Installing bun...`)
await runInstallScript('install-bun.js')
}
cmd = getBinaryPath('bun')
log.info(`[MCP] Using command: ${cmd}`)
// add -x to args if args exist
if (args && args.length > 0) {
if (!args.includes('-y')) {
args.unshift('-y')
}
if (!args.includes('x')) {
args.unshift('x')
}
}
} else if (command === 'uvx') {
// check if cmd exists, if not exist, install it using `node scripts/install-uv.js`
const isUvxExist = await isBinaryExists('uvx')
if (!isUvxExist) {
log.info(`[MCP] Installing uvx...`)
await runInstallScript('install-uv.js')
}
cmd = getBinaryPath('uvx')
} }
log.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`)
transport = new this.stdioTransport!({ transport = new this.stdioTransport!({
command: cmd, command: cmd,
args, args,
@ -386,6 +418,7 @@ export default class MCPService extends EventEmitter {
*/ */
public async listTools(serverName?: string): Promise<MCPTool[]> { public async listTools(serverName?: string): Promise<MCPTool[]> {
await this.ensureInitialized() await this.ensureInitialized()
log.info(`[MCP] Listing tools from ${serverName || 'all active servers'}`)
try { try {
// If server name provided, list tools for that server only // If server name provided, list tools for that server only
@ -397,18 +430,19 @@ export default class MCPService extends EventEmitter {
let allTools: MCPTool[] = [] let allTools: MCPTool[] = []
for (const clientName in this.clients) { for (const clientName in this.clients) {
log.info(`[MCP] Listing tools from ${clientName}`)
try { try {
const tools = await this.listToolsFromServer(clientName) const tools = await this.listToolsFromServer(clientName)
allTools = allTools.concat(tools) allTools = allTools.concat(tools)
} catch (error) { } catch (error) {
this.logError(`[MCP] Error listing tools for ${clientName}`) this.logError(`Error listing tools for ${clientName}`, error)
} }
} }
log.info(`[MCP] Total tools listed: ${allTools.length}`) log.info(`[MCP] Total tools listed: ${allTools.length}`)
return allTools return allTools
} catch (error) { } catch (error) {
this.logError('Error listing tools:') this.logError('Error listing tools:', error)
return [] return []
} }
} }
@ -417,11 +451,13 @@ export default class MCPService extends EventEmitter {
* Helper method to list tools from a specific server * Helper method to list tools from a specific server
*/ */
private async listToolsFromServer(serverName: string): Promise<MCPTool[]> { private async listToolsFromServer(serverName: string): Promise<MCPTool[]> {
log.info(`[MCP] start list tools from ${serverName}:`)
if (!this.clients[serverName]) { if (!this.clients[serverName]) {
throw new Error(`MCP Client ${serverName} not found`) throw new Error(`MCP Client ${serverName} not found`)
} }
const { tools } = await this.clients[serverName].listTools() const { tools } = await this.clients[serverName].listTools()
log.info(`[MCP] Tools from ${serverName}:`, tools)
return tools.map((tool: any) => ({ return tools.map((tool: any) => ({
...tool, ...tool,
serverName, serverName,
@ -500,7 +536,7 @@ export default class MCPService extends EventEmitter {
try { try {
await this.activate(server) await this.activate(server)
} catch (error) { } catch (error) {
this.logError(`Failed to activate server ${server.name}`) this.logError(`Failed to activate server ${server.name}`, error)
this.emit('server-error', { name: server.name, error }) this.emit('server-error', { name: server.name, error })
} }
}) })

50
src/main/utils/process.ts Normal file
View File

@ -0,0 +1,50 @@
import { spawn } from 'child_process'
import log from 'electron-log'
import fs from 'fs'
import os from 'os'
import path from 'path'
import { getResourcePath } from '.'
export function runInstallScript(scriptPath: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
const installScriptPath = path.join(getResourcePath(), 'scripts', scriptPath)
log.info(`Running script at: ${installScriptPath}`)
const nodeProcess = spawn(process.execPath, [installScriptPath], {
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
})
nodeProcess.stdout.on('data', (data) => {
log.info(`Script output: ${data}`)
})
nodeProcess.stderr.on('data', (data) => {
log.error(`Script error: ${data}`)
})
nodeProcess.on('close', (code) => {
if (code === 0) {
log.info('Script completed successfully')
resolve()
} else {
log.error(`Script exited with code ${code}`)
reject(new Error(`Process exited with code ${code}`))
}
})
})
}
export function getBinaryPath(name: string): string {
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin')
let cmd = path.join(binariesDir, name)
cmd = process.platform === 'win32' ? `${cmd}.exe` : cmd
return cmd
}
export function isBinaryExists(name: string): Promise<boolean> {
return new Promise((resolve) => {
const cmd = getBinaryPath(name)
resolve(fs.existsSync(cmd))
})
}

View File

@ -135,6 +135,10 @@ declare global {
// status // status
cleanup: () => Promise<void> cleanup: () => Promise<void>
} }
isBinaryExist: (name: string) => Promise<boolean>
getBinaryPath: (name: string) => Promise<string>
installUVBinary: () => Promise<void>
installBunBinary: () => Promise<void>
} }
} }
} }

View File

@ -119,7 +119,13 @@ const api = {
}, },
shell: { shell: {
openExternal: shell.openExternal openExternal: shell.openExternal
} },
// Binary related APIs
isBinaryExist: (name: string) => ipcRenderer.invoke('app:is-binary-exist', name),
getBinaryPath: (name: string) => ipcRenderer.invoke('app:get-binary-path', name),
installUVBinary: () => ipcRenderer.invoke('app:install-uv-binary'),
installBunBinary: () => ipcRenderer.invoke('app:install-bun-binary')
} }
// Use `contextBridge` APIs to expose Electron APIs to // Use `contextBridge` APIs to expose Electron APIs to

View File

@ -825,6 +825,12 @@
"updateError": "Failed to update server", "updateError": "Failed to update server",
"url": "URL", "url": "URL",
"toggleError": "Toggle failed", "toggleError": "Toggle failed",
"dependenciesInstalling": "Installing dependencies...",
"dependenciesInstall": "Install Dependencies",
"installSuccess": "Dependencies installed successfully",
"installError": "Failed to install dependencies",
"missingDependencies": "is Missing, please install it to continue.",
"install": "Install",
"npx_list": { "npx_list": {
"title": "NPX Package List", "title": "NPX Package List",
"desc": "Search and add npm packages as MCP servers", "desc": "Search and add npm packages as MCP servers",

View File

@ -825,6 +825,12 @@
"updateError": "サーバーの更新に失敗しました", "updateError": "サーバーの更新に失敗しました",
"url": "URL", "url": "URL",
"toggleError": "切り替えに失敗しました", "toggleError": "切り替えに失敗しました",
"dependenciesInstalling": "依存関係をインストール中...",
"dependenciesInstall": "依存関係をインストール",
"installSuccess": "依存関係のインストールに成功しました",
"installError": "依存関係のインストールに失敗しました",
"missingDependencies": "が不足しています。続行するにはインストールしてください。",
"install": "インストール",
"npx_list": { "npx_list": {
"title": "NPX パッケージリスト", "title": "NPX パッケージリスト",
"desc": "npm パッケージを検索して MCP サーバーとして追加", "desc": "npm パッケージを検索して MCP サーバーとして追加",

View File

@ -825,6 +825,12 @@
"updateError": "Ошибка обновления сервера", "updateError": "Ошибка обновления сервера",
"url": "URL", "url": "URL",
"toggleError": "Переключение не удалось", "toggleError": "Переключение не удалось",
"dependenciesInstalling": "Установка зависимостей...",
"dependenciesInstall": "Установить зависимости",
"installSuccess": "Зависимости успешно установлены",
"installError": "Не удалось установить зависимости",
"missingDependencies": "отсутствует, пожалуйста, установите для продолжения.",
"install": "Установить",
"npx_list": { "npx_list": {
"title": "Список пакетов NPX", "title": "Список пакетов NPX",
"desc": "Поиск и добавление npm пакетов в качестве MCP серверов", "desc": "Поиск и добавление npm пакетов в качестве MCP серверов",

View File

@ -825,6 +825,12 @@
"updateError": "更新服务器失败", "updateError": "更新服务器失败",
"url": "URL", "url": "URL",
"toggleError": "切换失败", "toggleError": "切换失败",
"dependenciesInstalling": "正在安装依赖项...",
"dependenciesInstall": "安装依赖项",
"installSuccess": "依赖项安装成功",
"installError": "安装依赖项失败",
"missingDependencies": "缺失,请安装它以继续",
"install": "安装",
"npx_list": { "npx_list": {
"title": "NPX 包列表", "title": "NPX 包列表",
"desc": "搜索并添加 npm 包作为 MCP 服务", "desc": "搜索并添加 npm 包作为 MCP 服务",

View File

@ -825,6 +825,12 @@
"updateError": "更新伺服器失敗", "updateError": "更新伺服器失敗",
"url": "URL", "url": "URL",
"toggleError": "切換失敗", "toggleError": "切換失敗",
"dependenciesInstalling": "正在安裝相依套件...",
"dependenciesInstall": "安裝相依套件",
"installSuccess": "相依套件安裝成功",
"installError": "安裝相依套件失敗",
"missingDependencies": "缺失,請安裝它以繼續",
"install": "安裝",
"npx_list": { "npx_list": {
"title": "NPX 包列表", "title": "NPX 包列表",
"desc": "搜索並添加 npm 包作為 MCP 服務", "desc": "搜索並添加 npm 包作為 MCP 服務",

View File

@ -0,0 +1,114 @@
import { Alert, Button } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingRow, SettingSubtitle } from '..'
const InstallNpxUv: FC = () => {
const [isUvInstalled, setIsUvInstalled] = useState(false)
const [isBunInstalled, setIsBunInstalled] = useState(false)
const [isInstallingUv, setIsInstallingUv] = useState(false)
const [isInstallingBun, setIsInstallingBun] = useState(false)
const { t } = useTranslation()
const checkBinaries = async () => {
const uvExists = await window.api.isBinaryExist('uv')
const bunExists = await window.api.isBinaryExist('bun')
setIsUvInstalled(uvExists)
setIsBunInstalled(bunExists)
}
const installUV = async () => {
try {
setIsInstallingUv(true)
await window.api.installUVBinary()
setIsUvInstalled(true)
setIsInstallingUv(false)
} catch (error: any) {
window.message.error(`${t('settings.mcp.installError')}: ${error.message}`)
setIsInstallingUv(false)
checkBinaries()
}
}
const installBun = async () => {
try {
setIsInstallingBun(true)
await window.api.installBunBinary()
setIsBunInstalled(true)
setIsInstallingBun(false)
} catch (error: any) {
window.message.error(`${t('settings.mcp.installError')}: ${error.message}`)
setIsInstallingBun(false)
checkBinaries()
}
}
useEffect(() => {
checkBinaries()
}, [])
if (isUvInstalled && isBunInstalled) {
return null
}
return (
<Container>
{!isUvInstalled && (
<Alert
type="warning"
banner
style={{ padding: 8 }}
description={
<SettingRow>
<SettingSubtitle style={{ margin: 0 }}>
{isUvInstalled ? 'UV Installed' : `UV ${t('settings.mcp.missingDependencies')}`}
</SettingSubtitle>
<Button
type="primary"
onClick={installUV}
loading={isInstallingUv}
disabled={isInstallingUv}
size="small">
{isInstallingUv ? t('settings.mcp.dependenciesInstalling') : t('settings.mcp.install')}
</Button>
</SettingRow>
}
/>
)}
{!isBunInstalled && (
<Alert
type="warning"
banner
style={{ padding: 8 }}
description={
<SettingRow>
<SettingSubtitle style={{ margin: 0 }}>
{isBunInstalled ? 'Bun Installed' : `Bun ${t('settings.mcp.missingDependencies')}`}
</SettingSubtitle>
<Button
type="primary"
onClick={installBun}
loading={isInstallingBun}
disabled={isInstallingBun}
size="small">
{isInstallingBun ? t('settings.mcp.dependenciesInstalling') : t('settings.mcp.install')}
</Button>
</SettingRow>
}
/>
)}
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 20px;
gap: 10px;
`
export default InstallNpxUv

View File

@ -36,6 +36,7 @@ const NpxSearch: FC = () => {
} }
setSearchLoading(true) setSearchLoading(true)
try { try {
// Call npxFinder to search for packages // Call npxFinder to search for packages
const packages = await npxFinder(npmScope) const packages = await npxFinder(npmScope)

View File

@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..' import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
import AddMcpServerPopup from './AddMcpServerPopup' import AddMcpServerPopup from './AddMcpServerPopup'
import InstallNpxUv from './InstallNpxUv'
import NpxSearch from './NpxSearch' import NpxSearch from './NpxSearch'
const MCPSettings: FC = () => { const MCPSettings: FC = () => {
@ -199,6 +200,7 @@ const MCPSettings: FC = () => {
return ( return (
<SettingContainer theme={theme}> <SettingContainer theme={theme}>
<InstallNpxUv />
<SettingGroup theme={theme}> <SettingGroup theme={theme}>
<SettingTitle> <SettingTitle>
{t('settings.mcp.title')} {t('settings.mcp.title')}
@ -207,7 +209,7 @@ const MCPSettings: FC = () => {
</Tooltip> </Tooltip>
</SettingTitle> </SettingTitle>
<SettingDivider /> <SettingDivider />
<Paragraph type="secondary" style={{ margin: '0 0 20px 0' }}> <Paragraph type="secondary" style={{ margin: 0 }}>
{t('settings.mcp.config_description')} {t('settings.mcp.config_description')}
</Paragraph> </Paragraph>
@ -239,6 +241,7 @@ const MCPSettings: FC = () => {
columns={columns} columns={columns}
rowKey="name" rowKey="name"
pagination={false} pagination={false}
size="small"
locale={{ emptyText: t('settings.mcp.noServers') }} locale={{ emptyText: t('settings.mcp.noServers') }}
rowClassName={(record) => (!record.isActive ? 'inactive-row' : '')} rowClassName={(record) => (!record.isActive ? 'inactive-row' : '')}
onRow={(record) => ({ onRow={(record) => ({

View File

@ -516,9 +516,7 @@ export default class OpenAIProvider extends BaseProvider {
reqMessages.push({ reqMessages.push({
role: 'tool', role: 'tool',
content: isString(toolCallResponse.content) content: toolCallResponse.content,
? toolCallResponse.content
: JSON.stringify(toolCallResponse.content),
tool_call_id: toolCall.id tool_call_id: toolCall.id
} as ChatCompletionToolMessageParam) } as ChatCompletionToolMessageParam)