diff --git a/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch b/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch deleted file mode 100644 index 830f101d0..000000000 --- a/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/dist/cjs/client/stdio.js b/dist/cjs/client/stdio.js -index 2ada8771c5f76673b5021d6453c6bdd7e0b88013..89f6ea9ca7de86294d3d966f3454b98e19bfe534 100644 ---- a/dist/cjs/client/stdio.js -+++ b/dist/cjs/client/stdio.js -@@ -68,7 +68,7 @@ class StdioClientTransport { - this._process = (0, node_child_process_1.spawn)(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], { - env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(), - stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"], -- shell: false, -+ shell: process.platform === 'win32' ? true : false, - signal: this._abortController.signal, - windowsHide: node_process_1.default.platform === "win32" && isElectron(), - cwd: this._serverParams.cwd, -diff --git a/dist/esm/client/stdio.js b/dist/esm/client/stdio.js -index 387c982fd40fd8db9790a78e1a05c9ecb81501c0..7b7e60a306bca73149609015a27e904a0a68ca02 100644 ---- a/dist/esm/client/stdio.js -+++ b/dist/esm/client/stdio.js -@@ -61,7 +61,7 @@ export class StdioClientTransport { - this._process = spawn(this._serverParams.command, (_a = this._serverParams.args) !== null && _a !== void 0 ? _a : [], { - env: (_b = this._serverParams.env) !== null && _b !== void 0 ? _b : getDefaultEnvironment(), - stdio: ["pipe", "pipe", (_c = this._serverParams.stderr) !== null && _c !== void 0 ? _c : "inherit"], -- shell: false, -+ shell: process.platform === 'win32' ? true : false, - signal: this._abortController.signal, - windowsHide: process.platform === "win32" && isElectron(), - cwd: this._serverParams.cwd, diff --git a/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch b/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch new file mode 100644 index 000000000..c28db0e19 --- /dev/null +++ b/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch @@ -0,0 +1,18 @@ +diff --git a/dist/index.node.js b/dist/index.node.js +index bb108cbc210af5b99e864fd1dd8c555e948ecf7a..8ef8c1aab59215c21d161c0e52125724528ecab8 100644 +--- a/dist/index.node.js ++++ b/dist/index.node.js +@@ -1,8 +1,11 @@ + let crypto; + crypto = + globalThis.crypto?.webcrypto ?? // Node.js 16 REPL has globalThis.crypto as node:crypto +- globalThis.crypto ?? // Node.js 18+ +- (await import("node:crypto")).webcrypto; // Node.js 16 non-REPL ++ globalThis.crypto ?? // Node.js 18+ ++ (async() => { ++ const crypto = await import("node:crypto"); ++ return crypto.webcrypto; ++ })(); + /** + * Creates an array of length `size` of random bytes + * @param size diff --git a/package.json b/package.json index b9702590a..b74afe36d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.1.10", + "version": "1.1.12", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", @@ -67,7 +67,6 @@ "@google/generative-ai": "^0.21.0", "@langchain/community": "^0.3.36", "@mistralai/mistralai": "^1.5.2", - "@modelcontextprotocol/sdk": "patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch", "@notionhq/client": "^2.2.15", "@tryfabric/martian": "^1.2.4", "@types/react-infinite-scroll-component": "^5.0.0", @@ -109,6 +108,7 @@ "@google/genai": "^0.4.0", "@hello-pangea/dnd": "^16.6.0", "@kangfenmao/keyv-storage": "^0.1.0", + "@modelcontextprotocol/sdk": "^1.8.0", "@notionhq/client": "^2.2.15", "@reduxjs/toolkit": "^2.2.5", "@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch", @@ -189,7 +189,8 @@ "@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", "@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", "openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch", - "canvas": "3.1.0" + "canvas": "3.1.0", + "pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch" }, "packageManager": "yarn@4.6.0", "lint-staged": { diff --git a/resources/scripts/download.js b/resources/scripts/download.js index 270f8cbed..2e9d83a9e 100644 --- a/resources/scripts/download.js +++ b/resources/scripts/download.js @@ -1,8 +1,5 @@ -const { ProxyAgent } = require('undici') -const { SocksProxyAgent } = require('socks-proxy-agent') const https = require('https') const fs = require('fs') -const { pipeline } = require('stream/promises') /** * Downloads a file from a URL with redirect handling @@ -11,42 +8,28 @@ const { pipeline } = require('stream/promises') * @returns {Promise} Promise that resolves when download is complete */ async function downloadWithRedirects(url, destinationPath) { - const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY - if (proxyUrl.startsWith('socks')) { - const proxyAgent = new SocksProxyAgent(proxyUrl) - return new Promise((resolve, reject) => { - const request = (url) => { - https - .get(url, { agent: proxyAgent }, (response) => { - if (response.statusCode == 301 || response.statusCode == 302) { - request(response.headers.location) - return - } - if (response.statusCode !== 200) { - reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`)) - return - } - const file = fs.createWriteStream(destinationPath) - response.pipe(file) - file.on('finish', () => resolve()) - }) - .on('error', (err) => { - reject(err) - }) - } - request(url) - }) - } else { - const proxyAgent = new ProxyAgent(proxyUrl) - const response = await fetch(url, { - dispatcher: proxyAgent - }) - if (!response.ok) { - throw new Error(`Download failed: ${response.status} ${response.statusText}`) + return new Promise((resolve, reject) => { + const request = (url) => { + https + .get(url, (response) => { + if (response.statusCode == 301 || response.statusCode == 302) { + request(response.headers.location) + return + } + if (response.statusCode !== 200) { + reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`)) + return + } + const file = fs.createWriteStream(destinationPath) + response.pipe(file) + file.on('finish', () => resolve()) + }) + .on('error', (err) => { + reject(err) + }) } - const file = fs.createWriteStream(destinationPath) - await pipeline(response.body, file) - } + request(url) + }) } module.exports = { downloadWithRedirects } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index c08551503..4ee6ed753 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -2,7 +2,7 @@ import fs from 'node:fs' import { isMac, isWin } from '@main/constant' import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' -import { LocalFileSource, MCPServer, Shortcut, ThemeMode } from '@types' +import { LocalFileSource, Shortcut, ThemeMode } from '@types' import { BrowserWindow, ipcMain, session, shell } from 'electron' import log from 'electron-log' @@ -16,7 +16,7 @@ import { FileServiceManager } from './services/file/FileServiceManager' import FileService from './services/FileService' import FileStorage from './services/FileStorage' import KnowledgeService from './services/KnowledgeService' -import MCPService from './services/MCPService' +import mcpService from './services/MCPService' import * as NutstoreService from './services/NutstoreService' import ObsidianVaultService from './services/ObsidianVaultService' import { ProxyConfig, proxyManager } from './services/ProxyManager' @@ -31,7 +31,6 @@ import { compress, decompress } from './utils/zip' const fileManager = new FileStorage() const backupManager = new BackupManager() const exportService = new ExportService(fileManager) -const mcpService = new MCPService() const obsidianVaultService = new ObsidianVaultService() export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { @@ -280,36 +279,16 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ) // Register MCP handlers - ipcMain.on('mcp:servers-from-renderer', (_, servers) => mcpService.setServers(servers)) - ipcMain.handle('mcp:list-servers', async () => mcpService.listAvailableServices()) - ipcMain.handle('mcp:add-server', async (_, server: MCPServer) => mcpService.addServer(server)) - ipcMain.handle('mcp:update-server', async (_, server: MCPServer) => mcpService.updateServer(server)) - ipcMain.handle('mcp:delete-server', async (_, serverName: string) => mcpService.deleteServer(serverName)) - ipcMain.handle('mcp:set-server-active', async (_, { name, isActive }) => - mcpService.setServerActive({ name, isActive }) - ) - - // According to preload, this should take no parameters, but our implementation accepts - // an optional serverName for better flexibility - ipcMain.handle('mcp:list-tools', async (_, serverName?: string) => mcpService.listTools(serverName)) - ipcMain.handle('mcp:call-tool', async (_, params: { client: string; name: string; args: any }) => - mcpService.callTool(params) - ) - - ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup()) + ipcMain.handle('mcp:remove-server', mcpService.removeServer) + ipcMain.handle('mcp:list-tools', mcpService.listTools) + ipcMain.handle('mcp:call-tool', mcpService.callTool) + ipcMain.handle('mcp:get-install-info', mcpService.getInstallInfo) 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 - mcpService.on('servers-updated', (servers) => { - mainWindow?.webContents.send('mcp:servers-updated', servers) - }) - - app.on('before-quit', () => mcpService.cleanup()) - //copilot ipcMain.handle('copilot:get-auth-message', CopilotService.getAuthMessage) ipcMain.handle('copilot:get-copilot-token', CopilotService.getCopilotToken) diff --git a/src/main/reranker/BaseReranker.ts b/src/main/reranker/BaseReranker.ts index 543829089..58a642691 100644 --- a/src/main/reranker/BaseReranker.ts +++ b/src/main/reranker/BaseReranker.ts @@ -17,4 +17,15 @@ export default abstract class BaseReranker { 'Content-Type': 'application/json' } } + + public formatErrorMessage(url: string, error: any, requestBody: any) { + const errorDetails = { + url: url, + message: error.message, + status: error.response?.status, + statusText: error.response?.statusText, + requestBody: requestBody + } + return JSON.stringify(errorDetails, null, 2) + } } diff --git a/src/main/reranker/JinaReranker.ts b/src/main/reranker/JinaReranker.ts index ed14f4746..718774ee2 100644 --- a/src/main/reranker/JinaReranker.ts +++ b/src/main/reranker/JinaReranker.ts @@ -47,8 +47,10 @@ export default class JinaReranker extends BaseReranker { .filter((doc): doc is ExtractChunkData => doc !== undefined) .sort((a, b) => b.score - a.score) } catch (error: any) { - console.error('Jina Reranker API 错误:', error.status) - throw new Error(`${error} - BaseUrl: ${baseURL}`) + const errorDetails = this.formatErrorMessage(url, error, requestBody) + + console.error('Jina Reranker API Error:', errorDetails) + throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`) } } } diff --git a/src/main/reranker/SiliconFlowReranker.ts b/src/main/reranker/SiliconFlowReranker.ts index bba8d5405..d37f547b2 100644 --- a/src/main/reranker/SiliconFlowReranker.ts +++ b/src/main/reranker/SiliconFlowReranker.ts @@ -49,8 +49,10 @@ export default class SiliconFlowReranker extends BaseReranker { .filter((doc): doc is ExtractChunkData => doc !== undefined) .sort((a, b) => b.score - a.score) } catch (error: any) { - console.error('SiliconFlow Reranker API 错误:', error.status) - throw new Error(`${error} - BaseUrl: ${baseURL}`) + const errorDetails = this.formatErrorMessage(url, error, requestBody) + + console.error('SiliconFlow Reranker API 错误:', errorDetails) + throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`) } } } diff --git a/src/main/reranker/VoyageReranker.ts b/src/main/reranker/VoyageReranker.ts index c48fbb9bc..0cfc024ee 100644 --- a/src/main/reranker/VoyageReranker.ts +++ b/src/main/reranker/VoyageReranker.ts @@ -53,8 +53,10 @@ export default class VoyageReranker extends BaseReranker { .filter((doc): doc is ExtractChunkData => doc !== undefined) .sort((a, b) => b.score - a.score) } catch (error: any) { - console.error('Voyage Reranker API 错误:', error.message || error) - throw new Error(`${error} - BaseUrl: ${baseURL}`) + const errorDetails = this.formatErrorMessage(url, error, requestBody) + + console.error('Voyage Reranker API Error:', errorDetails) + throw new Error(`重排序请求失败: ${error.message}\n请求详情: ${errorDetails}`) } } } diff --git a/src/main/resources/icon.ico b/src/main/resources/icon.ico deleted file mode 100644 index 07f1f670c..000000000 Binary files a/src/main/resources/icon.ico and /dev/null differ diff --git a/src/main/services/KnowledgeService.ts b/src/main/services/KnowledgeService.ts index d78e3f81b..e157ded67 100644 --- a/src/main/services/KnowledgeService.ts +++ b/src/main/services/KnowledgeService.ts @@ -481,6 +481,9 @@ class KnowledgeService { _: Electron.IpcMainInvokeEvent, { search, base, results }: { search: string; base: KnowledgeBaseParams; results: ExtractChunkData[] } ): Promise => { + if (results.length === 0) { + return results + } return await new Reranker(base).rerank(search, results) } diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index f234d4d17..e37a9b4f5 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -1,615 +1,169 @@ -import { isLinux, isMac, isWin } from '@main/constant' -import { getBinaryPath } from '@main/utils/process' -import type { Client } from '@modelcontextprotocol/sdk/client/index.js' -import type { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' -import type { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' -import { MCPServer, MCPTool } from '@types' -import log from 'electron-log' -import { EventEmitter } from 'events' -import { v4 as uuidv4 } from 'uuid' +import os from 'node:os' +import path from 'node:path' -import { CacheService } from './CacheService' -import { windowService } from './WindowService' +import { getBinaryName, getBinaryPath } from '@main/utils/process' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' +import { MCPServer } from '@types' +import { app } from 'electron' +import Logger from 'electron-log' -/** - * Service for managing Model Context Protocol servers and tools - */ -export default class MCPService extends EventEmitter { - private servers: MCPServer[] = [] - private activeServers: Map = new Map() - private clients: { [key: string]: any } = {} - private Client: typeof Client | undefined - private stdioTransport: typeof StdioClientTransport | undefined - private sseTransport: typeof SSEClientTransport | undefined - private initialized = false - private initPromise: Promise | null = null +class McpService { + private client: Client | null = null + private clients: Map = new Map() - // Simplified server loading state management - private readyState = { - serversLoaded: false, - promise: null as Promise | null, - resolve: null as ((value: void) => void) | null - } - - constructor() { - super() - this.createServerLoadingPromise() - this.init().catch((err) => this.logError('Failed to initialize MCP service', err)) - } - - /** - * Create a promise that resolves when servers are loaded - */ - private createServerLoadingPromise(): void { - this.readyState.promise = new Promise((resolve) => { - this.readyState.resolve = resolve + private getServerKey(server: MCPServer): string { + return JSON.stringify({ + baseUrl: server.baseUrl, + command: server.command, + args: server.args, + env: server.env, + id: server.id }) } - /** - * Set servers received from Redux and trigger initialization if needed - */ - public setServers(servers: MCPServer[]): void { - this.servers = servers - log.info(`[MCP] Received ${servers.length} servers from Redux`) - - // Mark servers as loaded and resolve the waiting promise - if (!this.readyState.serversLoaded && this.readyState.resolve) { - this.readyState.serversLoaded = true - this.readyState.resolve() - this.readyState.resolve = null - } - - // Initialize if not already initialized - if (!this.initialized) { - this.init().catch((err) => this.logError('Failed to initialize MCP service', err)) - } + constructor() { + this.initClient = this.initClient.bind(this) + this.listTools = this.listTools.bind(this) + this.callTool = this.callTool.bind(this) + this.closeClient = this.closeClient.bind(this) + this.removeServer = this.removeServer.bind(this) } - /** - * Initialize the MCP service if not already initialized - */ - public async init(): Promise { - // If already initialized, return immediately - if (this.initialized) return + async initClient(server: MCPServer) { + const serverKey = this.getServerKey(server) - // If initialization is in progress, return that promise - if (this.initPromise) return this.initPromise - - this.initPromise = (async () => { - try { - log.info('[MCP] Starting initialization') - - // Wait for servers to be loaded from Redux - await this.waitForServers() - - // Load SDK components in parallel for better performance - const [Client, StdioTransport, SSETransport] = await Promise.all([ - this.importClient(), - this.importStdioClientTransport(), - this.importSSEClientTransport() - ]) - - this.Client = Client - this.stdioTransport = StdioTransport - this.sseTransport = SSETransport - - // Mark as initialized before loading servers - this.initialized = true - - // Load active servers - await this.loadActiveServers() - log.info('[MCP] Initialization successfully') - - return - } catch (err) { - this.initialized = false // Reset flag on error - log.error('[MCP] Failed to initialize:', err) - throw err - } finally { - this.initPromise = null - } - })() - - return this.initPromise - } - - /** - * Wait for servers to be loaded from Redux - */ - private async waitForServers(): Promise { - if (!this.readyState.serversLoaded && this.readyState.promise) { - log.info('[MCP] Waiting for servers data from Redux...') - await this.readyState.promise - log.info('[MCP] Servers received, continuing initialization') - } - } - - /** - * Helper to create consistent error logging functions - */ - private logError(message: string, err?: any): void { - log.error(`[MCP] ${message}`, err) - } - - /** - * Import the MCP client SDK - */ - private async importClient() { - try { - const { Client } = await import('@modelcontextprotocol/sdk/client/index.js') - return Client - } catch (err) { - this.logError('Failed to import Client:', err) - throw err - } - } - - /** - * Import the stdio transport - */ - private async importStdioClientTransport() { - try { - const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js') - return StdioClientTransport - } catch (err) { - log.error('[MCP] Failed to import StdioTransport:', err) - throw err - } - } - - /** - * Import the SSE transport - */ - private async importSSEClientTransport() { - try { - const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js') - return SSEClientTransport - } catch (err) { - log.error('[MCP] Failed to import SSETransport:', err) - throw err - } - } - - /** - * List all available MCP servers - */ - public async listAvailableServices(): Promise { - await this.ensureInitialized() - return this.servers - } - - /** - * Ensure the service is initialized before operations - */ - private async ensureInitialized() { - if (!this.initialized) { - log.debug('[MCP] Ensuring initialization') - await this.init() - } - } - - /** - * Add a new MCP server - */ - public async addServer(server: MCPServer): Promise { - await this.ensureInitialized() - - // Check for duplicate name - if (this.servers.some((s) => s.name === server.name)) { - throw new Error(`Server with name ${server.name} already exists`) - } - - // Activate if needed - if (server.isActive) { - await this.activate(server) - } - - // Add to servers list - this.servers = [...this.servers, server] - this.notifyReduxServersChanged(this.servers) - } - - /** - * Update an existing MCP server - */ - public async updateServer(server: MCPServer): Promise { - await this.ensureInitialized() - - const index = this.servers.findIndex((s) => s.name === server.name) - if (index === -1) { - throw new Error(`Server ${server.name} not found`) - } - - // Check activation status change - const wasActive = this.servers[index].isActive - if (wasActive && !server.isActive) { - await this.deactivate(server.name) - } else if (!wasActive && server.isActive) { - await this.activate(server) - } else { - await this.restartServer(server) - } - - // Update servers list - const updatedServers = [...this.servers] - updatedServers[index] = server - this.servers = updatedServers - - // Notify Redux - this.notifyReduxServersChanged(updatedServers) - } - - public async restartServer(_server: MCPServer): Promise { - await this.ensureInitialized() - - const server = this.servers.find((s) => s.name === _server.name) - - if (server) { - if (server.isActive) { - await this.deactivate(server.name) - } - await this.activate(server) - } - } - /** - * Delete an MCP server - */ - public async deleteServer(serverName: string): Promise { - await this.ensureInitialized() - - // Deactivate if running - if (this.clients[serverName]) { - await this.deactivate(serverName) - } - - // Update servers list - const filteredServers = this.servers.filter((s) => s.name !== serverName) - this.servers = filteredServers - this.notifyReduxServersChanged(filteredServers) - } - - /** - * Set a server's active state - */ - public async setServerActive(params: { name: string; isActive: boolean }): Promise { - await this.ensureInitialized() - - const { name, isActive } = params - const server = this.servers.find((s) => s.name === name) - - if (!server) { - throw new Error(`Server ${name} not found`) - } - - // Activate or deactivate as needed - if (isActive) { - await this.activate(server) - } else { - await this.deactivate(name) - } - - // Update server status - server.isActive = isActive - this.notifyReduxServersChanged([...this.servers]) - } - - /** - * Notify Redux in the renderer process about server changes - */ - private notifyReduxServersChanged(servers: MCPServer[]): void { - const mainWindow = windowService.getMainWindow() - if (mainWindow) { - mainWindow.webContents.send('mcp:servers-changed', servers) - } - } - - /** - * Activate an MCP server - */ - public async activate(server: MCPServer): Promise { - await this.ensureInitialized() - - const { name, baseUrl, command, env } = server - const args = [...(server.args || [])] - - // Skip if already running - if (this.clients[name]) { - log.info(`[MCP] Server ${name} is already running`) + // Check if we already have a client for this server configuration + const existingClient = this.clients.get(serverKey) + if (existingClient) { + this.client = existingClient return } + // If there's an existing client for a different server, close it + if (this.client) { + await this.closeClient() + } + + // Create new client instance for each connection + this.client = new Client({ name: 'Cherry Studio', version: app.getVersion() }, { capabilities: {} }) + + const args = [...(server.args || [])] + let transport: StdioClientTransport | SSEClientTransport try { // Create appropriate transport based on configuration - if (baseUrl) { - transport = new this.sseTransport!(new URL(baseUrl)) - } else if (command) { - let cmd: string = command - if (command === 'npx') { + if (server.baseUrl) { + transport = new SSEClientTransport(new URL(server.baseUrl)) + } else if (server.command) { + let cmd = server.command + + if (server.command === 'npx') { cmd = await getBinaryPath('bun') if (cmd === 'bun') { cmd = 'npx' } - log.info(`[MCP] Using command: ${cmd}`) + Logger.info(`[MCP] Using command: ${cmd}`) // add -x to args if args exist if (args && args.length > 0) { if (!args.includes('-y')) { - args.unshift('-y') + !args.includes('-y') && args.unshift('-y') } if (cmd.includes('bun') && !args.includes('x')) { args.unshift('x') } } - } else if (command === 'uvx') { + } + + if (server.command === 'uvx') { cmd = await getBinaryPath('uvx') } - log.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`) + Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`) - transport = new this.stdioTransport!({ + transport = new StdioClientTransport({ command: cmd, args, - stderr: 'pipe', - env: { - PATH: this.getEnhancedPath(process.env.PATH || ''), - ...env - } + env: server.env }) } else { throw new Error('Either baseUrl or command must be provided') } - // Create and connect client - const client = new this.Client!({ name, version: '1.0.0' }, { capabilities: {} }) + await this.client.connect(transport) - await client.connect(transport) + // Store the new client in the cache + this.clients.set(serverKey, this.client) - // Store client and server info - this.clients[name] = client - this.activeServers.set(name, { client, server }) - - log.info(`[MCP] Activated server: ${server.name}`) - this.emit('server-started', { name }) - } catch (error) { - log.error(`[MCP] Error activating server ${name}:`, error) - this.setServerActive({ name, isActive: false }) + Logger.info(`[MCP] Activated server: ${server.name}`) + } catch (error: any) { + Logger.error(`[MCP] Error activating server ${server.name}:`, error) throw error } } - /** - * Deactivate an MCP server - */ - public async deactivate(name: string): Promise { - await this.ensureInitialized() - - if (!this.clients[name]) { - log.warn(`[MCP] Server ${name} is not running`) - return - } - - try { - log.info(`[MCP] Stopping server: ${name}`) - await this.clients[name].close() - delete this.clients[name] - this.activeServers.delete(name) - this.emit('server-stopped', { name }) - } catch (error) { - log.error(`[MCP] Error deactivating server ${name}:`, error) - throw error - } - } - - /** - * List available tools from active MCP servers - */ - public async listTools(serverName?: string): Promise { - await this.ensureInitialized() - log.info(`[MCP] Listing tools from ${serverName || 'all active servers'}`) - - try { - // If server name provided, list tools for that server only - if (serverName) { - return await this.listToolsFromServer(serverName) - } - - // Otherwise list tools from all active servers - let allTools: MCPTool[] = [] - - for (const clientName in this.clients) { - log.info(`[MCP] Listing tools from ${clientName}`) - try { - const tools = await this.listToolsFromServer(clientName) - allTools = allTools.concat(tools) - } catch (error) { - this.logError(`Error listing tools for ${clientName}`, error) + async closeClient() { + if (this.client) { + // Remove the client from the cache + for (const [key, client] of this.clients.entries()) { + if (client === this.client) { + this.clients.delete(key) + break } } - log.info(`[MCP] Total tools listed: ${allTools.length}`) - return allTools - } catch (error) { - this.logError('Error listing tools:', error) - return [] + await this.client.close() + this.client = null } } - /** - * Helper method to list tools from a specific server - */ - private async listToolsFromServer(serverName: string): Promise { - log.info(`[MCP] start list tools from ${serverName}:`) - if (!this.clients[serverName]) { - throw new Error(`MCP Client ${serverName} not found`) - } - const cacheKey = `mcp:list_tool:${serverName}` + async removeServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) { + await this.closeClient() + this.clients.delete(this.getServerKey(server)) + } - if (CacheService.has(cacheKey)) { - log.info(`[MCP] Tools from ${serverName} loaded from cache`) - // Check if cache is still valid - const cachedTools = CacheService.get(cacheKey) - if (cachedTools && cachedTools.length > 0) { - return cachedTools - } - CacheService.remove(cacheKey) - } - - const { tools } = await this.clients[serverName].listTools() - - const transformedTools = tools.map((tool: any) => ({ + async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) { + await this.initClient(server) + const { tools } = await this.client!.listTools() + return tools.map((tool) => ({ ...tool, - serverName, - id: 'f' + uuidv4().replace(/-/g, '') + serverId: server.id, + serverName: server.name })) - - // Cache the tools for 5 minutes - if (transformedTools.length > 0) { - CacheService.set(cacheKey, transformedTools, 5 * 60 * 1000) - } - - log.info(`[MCP] Tools from ${serverName}:`, transformedTools) - return transformedTools } /** * Call a tool on an MCP server */ - public async callTool(params: { client: string; name: string; args: any }): Promise { - await this.ensureInitialized() - - const { client, name, args } = params - - if (!this.clients[client]) { - throw new Error(`MCP Client ${client} not found`) - } - - log.info('[MCP] Calling:', client, name, args) + public async callTool( + _: Electron.IpcMainInvokeEvent, + { server, name, args }: { server: MCPServer; name: string; args: any } + ): Promise { + await this.initClient(server) try { - return await this.clients[client].callTool({ - name, - arguments: args - }) + Logger.info('[MCP] Calling:', server.name, name, args) + const result = await this.client!.callTool({ name, arguments: args }) + return result } catch (error) { - log.error(`[MCP] Error calling tool ${name} on ${client}:`, error) + Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error) throw error } } - /** - * Clean up all MCP resources - */ - public async cleanup(): Promise { - const clientNames = Object.keys(this.clients) - - if (clientNames.length === 0) { - log.info('[MCP] No active servers to clean up') - return - } - - log.info(`[MCP] Cleaning up ${clientNames.length} active servers`) - - // Deactivate all clients - await Promise.allSettled( - clientNames.map((name) => - this.deactivate(name).catch((err) => { - log.error(`[MCP] Error during cleanup of ${name}:`, err) - }) - ) - ) - - this.clients = {} - this.activeServers.clear() - log.info('[MCP] All servers cleaned up') - } - - /** - * Load all active servers - */ - private async loadActiveServers(): Promise { - const activeServers = this.servers.filter((server) => server.isActive) - - if (activeServers.length === 0) { - log.info('[MCP] No active servers to load') - return - } - - log.info(`[MCP] Start loading ${activeServers.length} active servers`) - - // Activate servers in parallel for better performance - await Promise.allSettled( - activeServers.map(async (server) => { - try { - await this.activate(server) - } catch (error) { - this.logError(`Failed to activate server ${server.name}`, error) - this.emit('server-error', { name: server.name, error }) - } - }) - ) - - log.info(`[MCP] End loading ${Object.keys(this.clients).length} active servers`) - } - - /** - * Get enhanced PATH including common tool locations - */ - private getEnhancedPath(originalPath: string): string { - // 将原始 PATH 按分隔符分割成数组 - const pathSeparator = process.platform === 'win32' ? ';' : ':' - const existingPaths = new Set(originalPath.split(pathSeparator).filter(Boolean)) - const homeDir = process.env.HOME || process.env.USERPROFILE || '' - - // 定义要添加的新路径 - const newPaths: string[] = [] - - if (isMac) { - newPaths.push( - '/bin', - '/usr/bin', - '/usr/local/bin', - '/usr/local/sbin', - '/opt/homebrew/bin', - '/opt/homebrew/sbin', - '/usr/local/opt/node/bin', - `${homeDir}/.nvm/current/bin`, - `${homeDir}/.npm-global/bin`, - `${homeDir}/.yarn/bin`, - `${homeDir}/.cargo/bin`, - '/opt/local/bin' - ) - } - - if (isLinux) { - newPaths.push( - '/bin', - '/usr/bin', - '/usr/local/bin', - `${homeDir}/.nvm/current/bin`, - `${homeDir}/.npm-global/bin`, - `${homeDir}/.yarn/bin`, - `${homeDir}/.cargo/bin`, - '/snap/bin' - ) - } - - if (isWin) { - newPaths.push(`${process.env.APPDATA}\\npm`, `${homeDir}\\AppData\\Local\\Yarn\\bin`, `${homeDir}\\.cargo\\bin`) - } - - // 只添加不存在的路径 - newPaths.forEach((path) => { - if (path && !existingPaths.has(path)) { - existingPaths.add(path) - } - }) - - // 转换回字符串 - return Array.from(existingPaths).join(pathSeparator) + public async getInstallInfo() { + const dir = path.join(os.homedir(), '.cherrystudio', 'bin') + const uvName = await getBinaryName('uv') + const bunName = await getBinaryName('bun') + const uvPath = path.join(dir, uvName) + const bunPath = path.join(dir, bunName) + return { dir, uvPath, bunPath } } } + +export default new McpService() diff --git a/src/main/utils/index.ts b/src/main/utils/index.ts index 972364f58..295956216 100644 --- a/src/main/utils/index.ts +++ b/src/main/utils/index.ts @@ -42,3 +42,7 @@ export function dumpPersistState() { } return JSON.stringify(persistState) } + +export const runAsyncFunction = async (fn: () => void) => { + await fn() +} diff --git a/src/main/utils/process.ts b/src/main/utils/process.ts index e4151c01e..e10cdc4be 100644 --- a/src/main/utils/process.ts +++ b/src/main/utils/process.ts @@ -35,12 +35,18 @@ export function runInstallScript(scriptPath: string): Promise { }) } +export async function getBinaryName(name: string): Promise { + if (process.platform === 'win32') { + return `${name}.exe` + } + return name +} + export async function getBinaryPath(name: string): Promise { - let cmd = process.platform === 'win32' ? `${name}.exe` : name + const binaryName = await getBinaryName(name) const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin') const binariesDirExists = await fs.existsSync(binariesDir) - cmd = binariesDirExists ? path.join(binariesDir, cmd) : name - return cmd + return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName } export async function isBinaryExists(name: string): Promise { diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 659dbf0d1..c6cbf5c49 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -155,17 +155,10 @@ declare global { openExternal: (url: string, options?: OpenExternalOptions) => Promise } mcp: { - // servers - listServers: () => Promise - addServer: (server: MCPServer) => Promise - updateServer: (server: MCPServer) => Promise - deleteServer: (serverName: string) => Promise - setServerActive: (name: string, isActive: boolean) => Promise - // tools - listTools: () => Promise - callTool: ({ client, name, args }: { client: string; name: string; args: any }) => Promise - // status - cleanup: () => Promise + removeServer: (server: MCPServer) => Promise + listTools: (server: MCPServer) => Promise + callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise + getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }> } copilot: { getAuthMessage: ( diff --git a/src/preload/index.ts b/src/preload/index.ts index 625b0c4f3..23371f54f 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -124,15 +124,11 @@ const api = { ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey) }, mcp: { - listServers: () => ipcRenderer.invoke('mcp:list-servers'), - addServer: (server: MCPServer) => ipcRenderer.invoke('mcp:add-server', server), - updateServer: (server: MCPServer) => ipcRenderer.invoke('mcp:update-server', server), - deleteServer: (serverName: string) => ipcRenderer.invoke('mcp:delete-server', serverName), - setServerActive: (name: string, isActive: boolean) => - ipcRenderer.invoke('mcp:set-server-active', { name, isActive }), - listTools: (serverName?: string) => ipcRenderer.invoke('mcp:list-tools', serverName), - callTool: (params: { client: string; name: string; args: any }) => ipcRenderer.invoke('mcp:call-tool', params), - cleanup: () => ipcRenderer.invoke('mcp:cleanup') + removeServer: (server: MCPServer) => ipcRenderer.invoke('mcp:remove-server', server), + listTools: (server: MCPServer) => ipcRenderer.invoke('mcp:list-tools', server), + callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => + ipcRenderer.invoke('mcp:call-tool', { server, name, args }), + getInstallInfo: () => ipcRenderer.invoke('mcp:get-install-info') }, shell: { openExternal: shell.openExternal diff --git a/src/renderer/src/assets/images/avatar.png b/src/renderer/src/assets/images/avatar.png index 70fc62f49..c9c59363c 100644 Binary files a/src/renderer/src/assets/images/avatar.png and b/src/renderer/src/assets/images/avatar.png differ diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index 268658226..aebb4bd91 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -19,7 +19,7 @@ --color-gray-2: #414853; --color-gray-3: #32363f; - --color-text-1: rgba(255, 255, 245, 0.86); + --color-text-1: rgba(255, 255, 245, 0.9); --color-text-2: rgba(235, 235, 245, 0.6); --color-text-3: rgba(235, 235, 245, 0.38); diff --git a/src/renderer/src/components/DragableList/index.tsx b/src/renderer/src/components/DragableList/index.tsx index a5c2db91a..cc281e7b0 100644 --- a/src/renderer/src/components/DragableList/index.tsx +++ b/src/renderer/src/components/DragableList/index.tsx @@ -8,8 +8,8 @@ import { OnDragStartResponder, ResponderProvided } from '@hello-pangea/dnd' -import VirtualList from 'rc-virtual-list' import { droppableReorder } from '@renderer/utils' +import VirtualList from 'rc-virtual-list' import { FC } from 'react' interface Props { diff --git a/src/renderer/src/components/Icons/ToolsCallingIcon.tsx b/src/renderer/src/components/Icons/ToolsCallingIcon.tsx index 323c9570f..7a591d231 100644 --- a/src/renderer/src/components/Icons/ToolsCallingIcon.tsx +++ b/src/renderer/src/components/Icons/ToolsCallingIcon.tsx @@ -23,7 +23,7 @@ const Container = styled.div` ` const Icon = styled(ToolOutlined)` - color: #d97757; + color: var(--color-primary); font-size: 15px; margin-right: 6px; ` diff --git a/src/renderer/src/components/IndicatorLight.tsx b/src/renderer/src/components/IndicatorLight.tsx index c7eaa8bdc..edce665e6 100644 --- a/src/renderer/src/components/IndicatorLight.tsx +++ b/src/renderer/src/components/IndicatorLight.tsx @@ -4,15 +4,25 @@ import styled from 'styled-components' interface IndicatorLightProps { color: string + size?: number + shadow?: boolean + style?: React.CSSProperties + animation?: boolean } -const Light = styled.div<{ color: string }>` - width: 8px; - height: 8px; +const Light = styled.div<{ + color: string + size: number + shadow?: boolean + style?: React.CSSProperties + animation?: boolean +}>` + width: ${({ size }) => size}px; + height: ${({ size }) => size}px; border-radius: 50%; background-color: ${({ color }) => color}; - box-shadow: 0 0 6px ${({ color }) => color}; - animation: pulse 2s infinite; + box-shadow: ${({ shadow, color }) => (shadow ? `0 0 6px ${color}` : 'none')}; + animation: ${({ animation }) => (animation ? 'pulse 2s infinite' : 'none')}; @keyframes pulse { 0% { @@ -27,9 +37,9 @@ const Light = styled.div<{ color: string }>` } ` -const IndicatorLight: React.FC = ({ color }) => { +const IndicatorLight: React.FC = ({ color, size = 8, shadow = true, style, animation = true }) => { const actualColor = color === 'green' ? '#22c55e' : color - return + return } export default IndicatorLight diff --git a/src/renderer/src/components/ListItem/index.tsx b/src/renderer/src/components/ListItem/index.tsx index 7c2b785f2..7e2165c5d 100644 --- a/src/renderer/src/components/ListItem/index.tsx +++ b/src/renderer/src/components/ListItem/index.tsx @@ -8,17 +8,20 @@ interface ListItemProps { subtitle?: string titleStyle?: React.CSSProperties onClick?: () => void + rightContent?: ReactNode + style?: React.CSSProperties } -const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick }: ListItemProps) => { +const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick, rightContent, style }: ListItemProps) => { return ( - + {icon && {icon}} {title} {subtitle && {subtitle}} + {rightContent && {rightContent}} ) @@ -84,4 +87,8 @@ const SubtitleText = styled.div` color: var(--color-text-3); ` +const RightContentWrapper = styled.div` + margin-left: auto; +` + export default ListItem diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx index db6ed9fa7..9b0bb823a 100644 --- a/src/renderer/src/components/app/Navbar.tsx +++ b/src/renderer/src/components/app/Navbar.tsx @@ -1,4 +1,4 @@ -import { isMac } from '@renderer/config/constant' +import { isMac, isWindows } from '@renderer/config/constant' import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor' import type { FC, PropsWithChildren } from 'react' import type { HTMLAttributes } from 'react' @@ -63,4 +63,6 @@ const NavbarRightContainer = styled.div` display: flex; align-items: center; padding: 0 12px; + padding-right: ${isWindows ? '140px' : 12}; + justify-content: flex-end; ` diff --git a/src/renderer/src/context/SyntaxHighlighterProvider.tsx b/src/renderer/src/context/SyntaxHighlighterProvider.tsx index bbb4624d2..3933b7864 100644 --- a/src/renderer/src/context/SyntaxHighlighterProvider.tsx +++ b/src/renderer/src/context/SyntaxHighlighterProvider.tsx @@ -82,7 +82,7 @@ export const SyntaxHighlighterProvider: React.FC = ({ childre [highlighter, highlighterTheme] ) - return {children} + return {children} } export const useSyntaxHighlighter = () => { diff --git a/src/renderer/src/context/ThemeProvider.tsx b/src/renderer/src/context/ThemeProvider.tsx index 113898f4f..24c6bce1b 100644 --- a/src/renderer/src/context/ThemeProvider.tsx +++ b/src/renderer/src/context/ThemeProvider.tsx @@ -57,11 +57,7 @@ export const ThemeProvider: React.FC = ({ children, defaultT } }) - return ( - - {children} - - ) + return {children} } export const useTheme = () => use(ThemeContext) diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index 844060e52..752ed0719 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -11,7 +11,6 @@ import { useEffect } from 'react' import { useDefaultModel } from './useAssistant' import useFullScreenNotice from './useFullScreenNotice' -import { useInitMCPServers } from './useMCPServers' import { useRuntime } from './useRuntime' import { useSettings } from './useSettings' import useUpdateHandler from './useUpdateHandler' @@ -26,7 +25,6 @@ export function useAppInit() { useUpdateHandler() useFullScreenNotice() - useInitMCPServers() useEffect(() => { avatar?.value && dispatch(setAvatar(avatar.value)) diff --git a/src/renderer/src/hooks/useMCPServers.ts b/src/renderer/src/hooks/useMCPServers.ts index cd513977e..553df561e 100644 --- a/src/renderer/src/hooks/useMCPServers.ts +++ b/src/renderer/src/hooks/useMCPServers.ts @@ -1,7 +1,6 @@ -import store, { useAppSelector } from '@renderer/store' -import { setMCPServers } from '@renderer/store/mcp' +import store, { useAppDispatch, useAppSelector } from '@renderer/store' +import { addMCPServer, deleteMCPServer, setMCPServers, updateMCPServer } from '@renderer/store/mcp' import { MCPServer } from '@renderer/types' -import { useEffect } from 'react' const ipcRenderer = window.electron.ipcRenderer @@ -13,82 +12,16 @@ ipcRenderer.on('mcp:servers-changed', (_event, servers) => { export const useMCPServers = () => { const mcpServers = useAppSelector((state) => state.mcp.servers) const activedMcpServers = useAppSelector((state) => state.mcp.servers?.filter((server) => server.isActive)) - - const addMCPServer = async (server: MCPServer) => { - try { - await window.api.mcp.addServer(server) - // Main process will send back updated servers via mcp:servers-changed - } catch (error) { - console.error('Failed to add MCP server:', error) - throw error - } - } - - const updateMCPServer = async (server: MCPServer) => { - try { - await window.api.mcp.updateServer(server) - // Main process will send back updated servers via mcp:servers-changed - } catch (error) { - console.error('Failed to update MCP server:', error) - throw error - } - } - - const deleteMCPServer = async (name: string) => { - try { - await window.api.mcp.deleteServer(name) - // Main process will send back updated servers via mcp:servers-changed - } catch (error) { - console.error('Failed to delete MCP server:', error) - throw error - } - } - - const setMCPServerActive = async (name: string, isActive: boolean) => { - try { - await window.api.mcp.setServerActive(name, isActive) - // Main process will send back updated servers via mcp:servers-changed - } catch (error) { - console.error('Failed to set MCP server active status:', error) - throw error - } - } - - const getActiveMCPServers = () => { - return mcpServers.filter((server) => server.isActive) - } + const dispatch = useAppDispatch() return { mcpServers, activedMcpServers, - addMCPServer, - updateMCPServer, - deleteMCPServer, - setMCPServerActive, - getActiveMCPServers + addMCPServer: (server: MCPServer) => dispatch(addMCPServer(server)), + updateMCPServer: (server: MCPServer) => dispatch(updateMCPServer(server)), + deleteMCPServer: (id: string) => dispatch(deleteMCPServer(id)), + setMCPServerActive: (server: MCPServer, isActive: boolean) => dispatch(updateMCPServer({ ...server, isActive })), + getActiveMCPServers: () => mcpServers.filter((server) => server.isActive), + updateMcpServers: (servers: MCPServer[]) => dispatch(setMCPServers(servers)) } } - -export const useInitMCPServers = () => { - const mcpServers = useAppSelector((state) => state.mcp.servers) - // const dispatch = useAppDispatch() - - // Send servers to main process when they change in Redux - useEffect(() => { - ipcRenderer.send('mcp:servers-from-renderer', mcpServers) - }, [mcpServers]) - - // Initial load of MCP servers from main process - // useEffect(() => { - // const loadServers = async () => { - // try { - // const servers = await window.api.mcp.listServers() - // dispatch(setMCPServers(servers)) - // } catch (error) { - // console.error('Failed to load MCP servers:', error) - // } - // } - - // loadServers() - // }, [dispatch]) -} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 1de7892e8..54e56a87f 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -906,7 +906,8 @@ "pathSelector.return": "Return", "pathSelector.currentPath": "Current Path", "new_folder.button.confirm": "Confirm", - "new_folder.button.cancel": "Cancel" + "new_folder.button.cancel": "Cancel", + "new_folder.button": "New Folder" } }, "display.assistant.title": "Assistant Settings", @@ -965,10 +966,7 @@ "argsTooltip": "Each argument on a new line", "baseUrlTooltip": "Remote server base URL", "command": "Command", - "commandRequired": "Please enter a command", "config_description": "Configure Model Context Protocol servers", - "confirmDelete": "Delete Server", - "confirmDeleteMessage": "Are you sure you want to delete the server?", "deleteError": "Failed to delete server", "deleteSuccess": "Server deleted successfully", "dependenciesInstall": "Install Dependencies", @@ -979,7 +977,8 @@ "editServer": "Edit Server", "env": "Environment Variables", "envTooltip": "Format: KEY=value, one per line", - "findMore": "Find More MCP Servers", + "findMore": "Find More MCP", + "searchNpx": "Search MCP", "install": "Install", "installError": "Failed to install dependencies", "installSuccess": "Dependencies installed successfully", @@ -989,8 +988,8 @@ "jsonSaveSuccess": "JSON configuration has been saved.", "missingDependencies": "is Missing, please install it to continue.", "name": "Name", - "nameRequired": "Please enter a server name", "noServers": "No servers configured", + "newServer": "MCP Server", "npx_list": { "actions": "Actions", "desc": "Search and add npm packages as MCP servers", @@ -1006,14 +1005,19 @@ "usage": "Usage", "version": "Version" }, + "errors": { + "32000": "MCP server failed to start, please check the parameters according to the tutorial" + }, "serverPlural": "servers", "serverSingular": "server", "title": "MCP Servers", - "toggleError": "Toggle failed", + "startError": "Start failed", "type": "Type", "updateError": "Failed to update server", "updateSuccess": "Server updated successfully", - "url": "URL" + "url": "URL", + "editMcpJson": "Edit MCP Configuration", + "installHelp": "Get Installation Help" }, "messages.divider": "Show divider between messages", "messages.grid_columns": "Message grid display columns", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index f2c5826ff..715ffb4cb 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -906,7 +906,8 @@ "pathSelector.return": "戻る", "pathSelector.currentPath": "現在のパス", "new_folder.button.confirm": "確認", - "new_folder.button.cancel": "キャンセル" + "new_folder.button.cancel": "キャンセル", + "new_folder.button": "新しいフォルダー" } }, "display.assistant.title": "アシスタント設定", @@ -964,10 +965,7 @@ "argsTooltip": "1行に1つの引数を入力してください", "baseUrlTooltip": "リモートURLアドレス", "command": "コマンド", - "commandRequired": "コマンドを入力してください", "config_description": "モデルコンテキストプロトコルサーバーの設定", - "confirmDelete": "サーバーを削除", - "confirmDeleteMessage": "本当にこのサーバーを削除しますか?", "deleteError": "サーバーの削除に失敗しました", "deleteSuccess": "サーバーが正常に削除されました", "dependenciesInstall": "依存関係をインストール", @@ -978,7 +976,8 @@ "editServer": "サーバーを編集", "env": "環境変数", "envTooltip": "形式: KEY=value, 1行に1つ", - "findMore": "MCP サーバーを見つける", + "findMore": "MCP を見つける", + "searchNpx": "MCP を検索", "install": "インストール", "installError": "依存関係のインストールに失敗しました", "installSuccess": "依存関係のインストールに成功しました", @@ -988,8 +987,8 @@ "jsonSaveSuccess": "JSON設定が保存されました。", "missingDependencies": "が不足しています。続行するにはインストールしてください。", "name": "名前", - "nameRequired": "サーバー名を入力してください", "noServers": "サーバーが設定されていません", + "newServer": "MCP サーバー", "npx_list": { "actions": "アクション", "desc": "npm パッケージを検索して MCP サーバーとして追加", @@ -1008,11 +1007,16 @@ "serverPlural": "サーバー", "serverSingular": "サーバー", "title": "MCP サーバー", - "toggleError": "切り替えに失敗しました", + "startError": "起動に失敗しました", "type": "タイプ", "updateError": "サーバーの更新に失敗しました", "updateSuccess": "サーバーが正常に更新されました", - "url": "URL" + "url": "URL", + "errors": { + "32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください" + }, + "editMcpJson": "MCP 設定を編集", + "installHelp": "インストールヘルプを取得" }, "messages.divider": "メッセージ間に区切り線を表示", "messages.grid_columns": "メッセージグリッドの表示列数", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 1de6e574f..e36c44196 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -906,7 +906,8 @@ "pathSelector.return": "Назад", "pathSelector.currentPath": "Текущий путь", "new_folder.button.confirm": "Подтвердить", - "new_folder.button.cancel": "Отмена" + "new_folder.button.cancel": "Отмена", + "new_folder.button": "Новая папка" } }, "display.assistant.title": "Настройки ассистентов", @@ -964,10 +965,7 @@ "argsTooltip": "Каждый аргумент с новой строки", "baseUrlTooltip": "Адрес удаленного URL", "command": "Команда", - "commandRequired": "Пожалуйста, введите команду", "config_description": "Настройка серверов протокола контекста модели", - "confirmDelete": "Удалить сервер", - "confirmDeleteMessage": "Вы уверены, что хотите удалить этот сервер?", "deleteError": "Не удалось удалить сервер", "deleteSuccess": "Сервер успешно удален", "dependenciesInstall": "Установить зависимости", @@ -978,7 +976,8 @@ "editServer": "Редактировать сервер", "env": "Переменные окружения", "envTooltip": "Формат: KEY=value, по одной на строку", - "findMore": "Найти больше MCP серверов", + "findMore": "Найти больше MCP", + "searchNpx": "Найти MCP", "install": "Установить", "installError": "Не удалось установить зависимости", "installSuccess": "Зависимости успешно установлены", @@ -988,8 +987,8 @@ "jsonSaveSuccess": "JSON конфигурация сохранена", "missingDependencies": "отсутствует, пожалуйста, установите для продолжения.", "name": "Имя", - "nameRequired": "Пожалуйста, введите имя сервера", "noServers": "Серверы не настроены", + "newServer": "MCP сервер", "npx_list": { "actions": "Действия", "desc": "Поиск и добавление npm пакетов в качестве MCP серверов", @@ -1005,14 +1004,19 @@ "usage": "Использование", "version": "Версия" }, + "errors": { + "32000": "MCP сервер не запущен, пожалуйста, проверьте параметры" + }, "serverPlural": "серверы", "serverSingular": "сервер", "title": "Серверы MCP", - "toggleError": "Переключение не удалось", + "startError": "Запуск не удалось", "type": "Тип", "updateError": "Ошибка обновления сервера", "updateSuccess": "Сервер успешно обновлен", - "url": "URL" + "url": "URL", + "editMcpJson": "Редактировать MCP 配置", + "installHelp": "Получить помощь по установке" }, "messages.divider": "Показывать разделитель между сообщениями", "messages.grid_columns": "Количество столбцов сетки сообщений", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index a489ef139..a579a1836 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -902,7 +902,8 @@ "pathSelector.return": "返回", "pathSelector.currentPath": "当前路径", "new_folder.button.confirm": "确定", - "new_folder.button.cancel": "取消" + "new_folder.button.cancel": "取消", + "new_folder.button": "新建文件夹" } }, "display.assistant.title": "助手设置", @@ -968,10 +969,7 @@ "argsTooltip": "每个参数占一行", "baseUrlTooltip": "远程 URL 地址", "command": "命令", - "commandRequired": "请输入命令", "config_description": "配置模型上下文协议服务器", - "confirmDelete": "删除服务器", - "confirmDeleteMessage": "您确定要删除该服务器吗?", "deleteError": "删除服务器失败", "deleteSuccess": "服务器删除成功", "dependenciesInstall": "安装依赖项", @@ -982,7 +980,8 @@ "editServer": "编辑服务器", "env": "环境变量", "envTooltip": "格式:KEY=value,每行一个", - "findMore": "更多 MCP 服务器", + "findMore": "更多 MCP", + "searchNpx": "搜索 MCP", "install": "安装", "installError": "安装依赖项失败", "installSuccess": "依赖项安装成功", @@ -992,8 +991,8 @@ "jsonSaveSuccess": "JSON配置已保存", "missingDependencies": "缺失,请安装它以继续", "name": "名称", - "nameRequired": "请输入服务器名称", "noServers": "未配置服务器", + "newServer": "MCP 服务器", "npx_list": { "actions": "操作", "desc": "搜索并添加 npm 包作为 MCP 服务", @@ -1009,14 +1008,19 @@ "usage": "用法", "version": "版本" }, + "errors": { + "32000": "MCP 服务器启动失败,请根据教程检查参数是否填写完整" + }, "serverPlural": "服务器", "serverSingular": "服务器", "title": "MCP 服务器", - "toggleError": "切换失败", + "startError": "启动失败", "type": "类型", "updateError": "更新服务器失败", "updateSuccess": "服务器更新成功", - "url": "URL" + "url": "URL", + "editMcpJson": "编辑 MCP 配置", + "installHelp": "获取安装帮助" }, "messages.divider": "消息分割线", "messages.grid_columns": "消息网格展示列数", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 000cc968f..4b23c266d 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -906,7 +906,8 @@ "pathSelector.return": "返回", "pathSelector.currentPath": "當前路徑", "new_folder.button.confirm": "確定", - "new_folder.button.cancel": "取消" + "new_folder.button.cancel": "取消", + "new_folder.button": "新建文件夾" } }, "display.assistant.title": "助手設定", @@ -964,10 +965,7 @@ "argsTooltip": "每個參數佔一行", "baseUrlTooltip": "遠端 URL 地址", "command": "指令", - "commandRequired": "請輸入指令", "config_description": "設定模型上下文協議伺服器", - "confirmDelete": "刪除伺服器", - "confirmDeleteMessage": "您確定要刪除該伺服器嗎?", "deleteError": "刪除伺服器失敗", "deleteSuccess": "伺服器刪除成功", "dependenciesInstall": "安裝相依套件", @@ -978,7 +976,8 @@ "editServer": "編輯伺服器", "env": "環境變數", "envTooltip": "格式:KEY=value,每行一個", - "findMore": "更多 MCP 伺服器", + "findMore": "更多 MCP", + "searchNpx": "搜索 MCP", "install": "安裝", "installError": "安裝相依套件失敗", "installSuccess": "相依套件安裝成功", @@ -988,8 +987,8 @@ "jsonSaveSuccess": "JSON配置已儲存", "missingDependencies": "缺失,請安裝它以繼續", "name": "名稱", - "nameRequired": "請輸入伺服器名稱", "noServers": "未設定伺服器", + "newServer": "MCP 伺服器", "npx_list": { "actions": "操作", "desc": "搜索並添加 npm 包作為 MCP 服務", @@ -1005,14 +1004,19 @@ "usage": "用法", "version": "版本" }, + "errors": { + "32000": "MCP 伺服器啟動失敗,請根據教程檢查參數是否填寫完整" + }, "serverPlural": "伺服器", "serverSingular": "伺服器", "title": "MCP 伺服器", - "toggleError": "切換失敗", + "startError": "啟動失敗", "type": "類型", "updateError": "更新伺服器失敗", "updateSuccess": "伺服器更新成功", - "url": "URL" + "url": "URL", + "editMcpJson": "編輯 MCP 配置", + "installHelp": "獲取安裝幫助" }, "messages.divider": "訊息間顯示分隔線", "messages.grid_columns": "訊息網格展示列數", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 0af37342b..6b46257f2 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -886,10 +886,7 @@ "argsTooltip": "Κάθε παράμετρος σε μια γραμμή", "baseUrlTooltip": "Σύνδεσμος Απομακρυσμένης διεύθυνσης URL", "command": "Εντολή", - "commandRequired": "Παρακαλώ εισάγετε την εντολή", "config_description": "Ρυθμίζει το πλαίσιο συντονισμού πρωτοκόλλων διακομιστή", - "confirmDelete": "Διαγραφή διακομιστή", - "confirmDeleteMessage": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον διακομιστή;", "deleteError": "Αποτυχία διαγραφής διακομιστή", "deleteSuccess": "Ο διακομιστής διαγράφηκε επιτυχώς", "dependenciesInstall": "Εγκατάσταση εξαρτήσεων", @@ -910,7 +907,6 @@ "jsonSaveSuccess": "Η διαμορφωτική ρύθμιση JSON αποθηκεύτηκε επιτυχώς", "missingDependencies": "Απο缺失, παρακαλώ εγκαταστήστε το για να συνεχίσετε", "name": "Όνομα", - "nameRequired": "Παρακαλώ εισάγετε το όνομα του διακομιστή", "noServers": "Δεν έχουν ρυθμιστεί διακομιστές", "npx_list": { "actions": "Ενέργειες", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index f16124847..f97513058 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -886,10 +886,7 @@ "argsTooltip": "Cada argumento en una línea", "baseUrlTooltip": "Dirección URL remota", "command": "Comando", - "commandRequired": "Por favor ingrese el comando", "config_description": "Configurar modelo de contexto del protocolo del servidor", - "confirmDelete": "Eliminar servidor", - "confirmDeleteMessage": "¿Está seguro de que desea eliminar este servidor?", "deleteError": "Fallo al eliminar servidor", "deleteSuccess": "Servidor eliminado exitosamente", "dependenciesInstall": "Instalar dependencias", @@ -910,7 +907,6 @@ "jsonSaveSuccess": "Configuración JSON guardada exitosamente", "missingDependencies": "Faltan, instalelas para continuar", "name": "Nombre", - "nameRequired": "Por favor ingrese el nombre del servidor", "noServers": "No se han configurado servidores", "npx_list": { "actions": "Acciones", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 10cd4d4dd..9a1c96002 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -886,10 +886,7 @@ "argsTooltip": "Chaque argument sur une ligne", "baseUrlTooltip": "Adresse URL distante", "command": "Commande", - "commandRequired": "Veuillez entrer une commande", "config_description": "Configurer le modèle du protocole de contexte du serveur", - "confirmDelete": "Supprimer le serveur", - "confirmDeleteMessage": "Êtes-vous sûr de vouloir supprimer ce serveur ?", "deleteError": "Échec de la suppression du serveur", "deleteSuccess": "Serveur supprimé avec succès", "dependenciesInstall": "Installer les dépendances", @@ -910,7 +907,6 @@ "jsonSaveSuccess": "Configuration JSON sauvegardée", "missingDependencies": "Manquantes, veuillez les installer pour continuer", "name": "Nom", - "nameRequired": "Veuillez entrer le nom du serveur", "noServers": "Aucun serveur configuré", "npx_list": { "actions": "Actions", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index b4a7288f9..9dc210f94 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -886,10 +886,7 @@ "argsTooltip": "Cada argumento em uma linha", "baseUrlTooltip": "Endereço de URL remoto", "command": "Comando", - "commandRequired": "Digite o comando", "config_description": "Configurar modelo de protocolo de contexto do servidor", - "confirmDelete": "Excluir servidor", - "confirmDeleteMessage": "Tem certeza de que deseja excluir este servidor?", "deleteError": "Falha ao excluir servidor", "deleteSuccess": "Servidor excluído com sucesso", "dependenciesInstall": "Instalar dependências", @@ -910,7 +907,6 @@ "jsonSaveSuccess": "Configuração JSON salva com sucesso", "missingDependencies": "Ausente, instale para continuar", "name": "Nome", - "nameRequired": "Digite o nome do servidor", "noServers": "Nenhum servidor configurado", "npx_list": { "actions": "Ações", diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 5fe831bff..eb96e1384 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -618,9 +618,9 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const toggelEnableMCP = (mcp: MCPServer) => { setEnabledMCPs((prev) => { - const exists = prev.some((item) => item.name === mcp.name) + const exists = prev.some((item) => item.id === mcp.id) if (exists) { - return prev.filter((item) => item.name !== mcp.name) + return prev.filter((item) => item.id !== mcp.id) } else { return [...prev, mcp] } diff --git a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx index 0eaaef688..2d5e41f7a 100644 --- a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx @@ -27,19 +27,14 @@ const MCPToolsButton: FC = ({ enabledMCPs, toggelEnableMCP, ToolbarButton // Check if all active servers are enabled const activeServers = mcpServers.filter((s) => s.isActive) - const anyEnable = activeServers.some((server) => - enabledMCPs.some((enabledServer) => enabledServer.name === server.name) - ) + const anyEnable = activeServers.some((server) => enabledMCPs.some((enabledServer) => enabledServer.id === server.id)) - const enableAll = () => - mcpServers.forEach((s) => { - toggelEnableMCP(s) - }) + const enableAll = () => mcpServers.forEach(toggelEnableMCP) const disableAll = () => mcpServers.forEach((s) => { enabledMCPs.forEach((enabledServer) => { - if (enabledServer.name === s.name) { + if (enabledServer.id === s.id) { toggelEnableMCP(s) } }) @@ -64,32 +59,34 @@ const MCPToolsButton: FC = ({ enabledMCPs, toggelEnableMCP, ToolbarButton - {mcpServers.length > 0 ? ( - mcpServers - .filter((s) => s.isActive) - .map((server) => ( - -
-
{server.name}
- {server.description && ( - -
{truncateText(server.description)}
-
- )} - {server.baseUrl &&
{server.baseUrl}
} -
- s.name === server.name)} - onChange={() => toggelEnableMCP(server)} - /> -
- )) - ) : ( -
-
{t('settings.mcp.noServers')}
-
- )} + + {mcpServers.length > 0 ? ( + mcpServers + .filter((s) => s.isActive) + .map((server) => ( + +
+
{server.name}
+ {server.description && ( + +
{truncateText(server.description)}
+
+ )} + {server.baseUrl &&
{server.baseUrl}
} +
+ s.id === server.id)} + onChange={() => toggelEnableMCP(server)} + /> +
+ )) + ) : ( +
+
{t('settings.mcp.noServers')}
+
+ )} +
) @@ -106,7 +103,7 @@ const MCPToolsButton: FC = ({ enabledMCPs, toggelEnableMCP, ToolbarButton overlayClassName="mention-models-dropdown"> - 0 ? '#d97757' : 'var(--color-icon)' }} /> + 0 ? 'var(--color-primary)' : 'var(--color-icon)' }} /> @@ -127,6 +124,10 @@ const McpServerItems = styled.div` font-weight: 500; font-size: 14px; color: var(--color-text-1); + max-width: 400px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } .server-description { @@ -177,4 +178,8 @@ const DropdownHeader = styled.div` } ` +const DropdownBody = styled.div` + padding-bottom: 10px; +` + export default MCPToolsButton diff --git a/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx b/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx index c81d9adc5..695d3a05e 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx @@ -25,7 +25,9 @@ const MessageGroupModelList: FC = ({ messages, setSe return ( - dispatch(setFoldDisplayMode(isCompact ? 'expanded' : 'compact'))}> + dispatch(setFoldDisplayMode(isCompact ? 'expanded' : 'compact'))}> = ({ message }) => { ), children: isDone && result && ( - -
{JSON.stringify(result, null, 2)}
+ + {JSON.stringify(result, null, 2)} ) }) @@ -129,9 +129,8 @@ const MessageTools: FC = ({ message }) => { onCancel={() => setExpandedResponse(null)} footer={null} width="80%" - styles={{ - body: { maxHeight: '80vh', overflow: 'auto' } - }}> + centered + styles={{ body: { maxHeight: '80vh', overflow: 'auto' } }}> {expandedResponse && ( = ({ message }) => { aria-label={t('common.copy')}> -
{expandedResponse.content}
+ {expandedResponse.content}
)} @@ -157,7 +156,6 @@ const CollapseContainer = styled(Collapse)` margin-bottom: 15px; border-radius: 8px; overflow: hidden; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); .ant-collapse-header { background-color: var(--color-bg-2); @@ -257,13 +255,14 @@ const ToolResponseContainer = styled.div` max-height: 300px; border-top: 1px solid var(--color-border); position: relative; +` - pre { - margin: 0; - white-space: pre-wrap; - word-break: break-word; - color: var(--color-text); - } +const CodeBlock = styled.pre` + margin: 0; + white-space: pre-wrap; + word-break: break-word; + color: var(--color-text); + font-family: ubuntu; ` const ExpandedResponseContainer = styled.div` diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx index 8261bd414..dea594c8b 100644 --- a/src/renderer/src/pages/home/Navbar.tsx +++ b/src/renderer/src/pages/home/Navbar.tsx @@ -3,7 +3,7 @@ import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar import { HStack } from '@renderer/components/Layout' import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover' import SearchPopup from '@renderer/components/Popups/SearchPopup' -import { isMac, isWindows } from '@renderer/config/constant' +import { isMac } from '@renderer/config/constant' import { useAssistant } from '@renderer/hooks/useAssistant' import { modelGenerating } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' @@ -71,9 +71,7 @@ const HeaderNavbar: FC = ({ activeAssistant }) => {
)} - + {!showAssistants && ( diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 6d68ac28f..b638af15c 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -202,7 +202,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic allowClear: true } }) - prompt && updateTopic({ ...topic, prompt: prompt.trim() }) + prompt !== null && updateTopic({ ...topic, prompt: prompt.trim() }) } }, { diff --git a/src/renderer/src/pages/knowledge/KnowledgePage.tsx b/src/renderer/src/pages/knowledge/KnowledgePage.tsx index ecf5ab0cf..2c9612998 100644 --- a/src/renderer/src/pages/knowledge/KnowledgePage.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgePage.tsx @@ -6,13 +6,12 @@ import { SearchOutlined, SettingOutlined } from '@ant-design/icons' -import { Navbar, NavbarCenter, NavbarRight as NavbarRightFromComponents } from '@renderer/components/app/Navbar' +import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar' import DragableList from '@renderer/components/DragableList' import { HStack } from '@renderer/components/Layout' import ListItem from '@renderer/components/ListItem' import PromptPopup from '@renderer/components/Popups/PromptPopup' import Scrollbar from '@renderer/components/Scrollbar' -import { isWindows } from '@renderer/config/constant' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useShortcut } from '@renderer/hooks/useShortcuts' import { NavbarIcon } from '@renderer/pages/home/Navbar' @@ -252,9 +251,4 @@ const NarrowIcon = styled(NavbarIcon)` } ` -const NavbarRight = styled(NavbarRightFromComponents)` - min-width: auto; - padding-right: ${isWindows ? '140px' : 15}; -` - export default KnowledgePage diff --git a/src/renderer/src/pages/settings/MCPSettings/AddMcpServerPopup.tsx b/src/renderer/src/pages/settings/MCPSettings/AddMcpServerPopup.tsx deleted file mode 100644 index 5a729237a..000000000 --- a/src/renderer/src/pages/settings/MCPSettings/AddMcpServerPopup.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { TopView } from '@renderer/components/TopView' -import { useAppSelector } from '@renderer/store' -import { MCPServer } from '@renderer/types' -import { Form, Input, Modal, Radio, Switch } from 'antd' -import TextArea from 'antd/es/input/TextArea' -import React, { useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' - -interface ShowParams { - server?: MCPServer - create?: boolean -} - -interface Props extends ShowParams { - resolve: (data: any) => void -} - -interface MCPFormValues { - name: string - description?: string - serverType: 'sse' | 'stdio' - baseUrl?: string - command?: string - args?: string - env?: string - isActive: boolean -} - -const PopupContainer: React.FC = ({ server, create, resolve }) => { - const [open, setOpen] = useState(true) - const { t } = useTranslation() - const [serverType, setServerType] = useState<'sse' | 'stdio'>('stdio') - const mcpServers = useAppSelector((state) => state.mcp.servers) - const [form] = Form.useForm() - const [loading, setLoading] = useState(false) - - useEffect(() => { - if (server) { - // Determine server type based on server properties - const serverType = server.baseUrl ? 'sse' : 'stdio' - setServerType(serverType) - - form.setFieldsValue({ - name: server.name, - description: server.description, - serverType: serverType, - baseUrl: server.baseUrl || '', - command: server.command || '', - args: server.args ? server.args.join('\n') : '', - env: server.env - ? Object.entries(server.env) - .map(([key, value]) => `${key}=${value}`) - .join('\n') - : '', - isActive: server.isActive - }) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - // Watch the serverType field to update the form layout dynamically - useEffect(() => { - const type = form.getFieldValue('serverType') - type && setServerType(type) - }, [form]) - - const onOK = async () => { - setLoading(true) - try { - const values = await form.validateFields() - const mcpServer: MCPServer = { - name: values.name, - description: values.description, - isActive: values.isActive - } - - if (values.serverType === 'sse') { - mcpServer.baseUrl = values.baseUrl - } else { - mcpServer.command = values.command - mcpServer.args = values.args ? values.args.split('\n').filter((arg) => arg.trim() !== '') : [] - - const env: Record = {} - if (values.env) { - values.env.split('\n').forEach((line) => { - if (line.trim()) { - const [key, ...chunks] = line.split('=') - const value = chunks.join('=') - if (key && value) { - env[key.trim()] = value.trim() - } - } - }) - } - mcpServer.env = Object.keys(env).length > 0 ? env : undefined - } - - if (server && !create) { - try { - await window.api.mcp.updateServer(mcpServer) - window.message.success(t('settings.mcp.updateSuccess')) - setLoading(false) - setOpen(false) - form.resetFields() - } catch (error: any) { - window.message.error(`${t('settings.mcp.updateError')}: ${error.message}`) - setLoading(false) - } - } else { - // Check for duplicate name - if (mcpServers.some((server: MCPServer) => server.name === mcpServer.name)) { - window.message.error(t('settings.mcp.duplicateName')) - setLoading(false) - return - } - - try { - await window.api.mcp.addServer(mcpServer) - window.message.success(t('settings.mcp.addSuccess')) - setLoading(false) - setOpen(false) - form.resetFields() - } catch (error: any) { - window.message.error(`${t('settings.mcp.addError')}: ${error.message}`) - setLoading(false) - } - } - } catch (error: any) { - setLoading(false) - } - } - - const onCancel = () => { - setOpen(false) - } - - const onClose = () => { - resolve({}) - } - - AddMcpServerPopup.hide = onCancel - - return ( - -
- - - - - -