Compare commits

...

4 Commits

Author SHA1 Message Date
kangfenmao
bc17dcb911 chore(version): 1.5.10 2025-09-11 16:28:24 +08:00
Konv Suu
44e93671fa fix: 对齐模型设置中 avatar 的样式 (#9829)
* fix: 对齐模型设置中 avatar 的样式

* update

* update

* fix: 修复上传弹出两次文件夹的问题

* update
2025-09-11 15:21:27 +08:00
Phantom
a5bfd8f3db fix: handle multiple content source when pasting to translate input (#9919)
* fix(translate): 处理粘贴事件时增加处理中状态检查

* fix(translate): 修复粘贴文本时未阻止默认行为的问题

添加event.preventDefault()以防止粘贴文本时触发默认行为
同时优化粘贴逻辑,优先处理文本内容
2025-09-11 15:20:46 +08:00
LiuVaayne
07c3c33acc refactor(mcp): enhance MCPService logging and error handling (#9878) 2025-09-11 15:19:31 +08:00
10 changed files with 318 additions and 142 deletions

View File

@@ -122,23 +122,20 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
✨ 重要更新: ✨ 重要更新:
- 新增笔记模块,支持富文本编辑和管理 - 新增标签页拖拽重新排序功能
- 内置 GLM-4.5-Flash 免费模型(由智谱开放平台提供) - 增强笔记编辑器同步功能
- 内置 Qwen3-8B 免费模型(由硅基流动提供) - 知识库支持文件选择和上传
- 新增 Nano BananaGemini 2.5 Flash Image模型支持 - 链接预览支持解析 OG 数据
- 新增系统 OCR 功能 (macOS & Windows) - 新增"重试失败消息"按钮
- 新增图片 OCR 识别和翻译功能
- 模型切换支持通过标签筛选
- 翻译功能增强:历史搜索和收藏
🔧 性能优化: 🔧 性能优化:
- 优化历史页面搜索性能 - 优化 MCP 服务日志和错误处理
- 优化拖拽列表组件交互 - 改进构建配置和依赖管理
- 升级 Electron 到 37.4.0 - 增强 Linux 系统 OCR 构建支持
🐛 修复问题: 🐛 修复问题:
- 修复知识库加密 PDF 文档处理 - 修复翻译功能相关问题
- 修复导航栏在左侧时笔记侧边栏按钮缺失 - 修复 MCP 服务相关问题
- 修复多个模型兼容性问题 - 修复导航和标签页显示问题
- 修复 MCP 相关问题 - 修复 Obsidian 集成检测
- 其他稳定性改进 - 其他界面和稳定性改进

View File

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

View File

@@ -56,6 +56,45 @@ type CallToolArgs = { server: MCPServer; name: string; args: any; callId?: strin
const logger = loggerService.withContext('MCPService') const logger = loggerService.withContext('MCPService')
// Redact potentially sensitive fields in objects (headers, tokens, api keys)
function redactSensitive(input: any): any {
const SENSITIVE_KEYS = ['authorization', 'Authorization', 'apiKey', 'api_key', 'apikey', 'token', 'access_token']
const MAX_STRING = 300
const redact = (val: any): any => {
if (val == null) return val
if (typeof val === 'string') {
return val.length > MAX_STRING ? `${val.slice(0, MAX_STRING)}…<${val.length - MAX_STRING} more>` : val
}
if (Array.isArray(val)) return val.map((v) => redact(v))
if (typeof val === 'object') {
const out: Record<string, any> = {}
for (const [k, v] of Object.entries(val)) {
if (SENSITIVE_KEYS.includes(k)) {
out[k] = '<redacted>'
} else {
out[k] = redact(v)
}
}
return out
}
return val
}
return redact(input)
}
// Create a context-aware logger for a server
function getServerLogger(server: MCPServer, extra?: Record<string, any>) {
const base = {
serverName: server?.name,
serverId: server?.id,
baseUrl: server?.baseUrl,
type: server?.type || (server?.command ? 'stdio' : server?.baseUrl ? 'http' : 'inmemory')
}
return loggerService.withContext('MCPService', { ...base, ...(extra || {}) })
}
/** /**
* Higher-order function to add caching capability to any async function * Higher-order function to add caching capability to any async function
* @param fn The original function to be wrapped with caching * @param fn The original function to be wrapped with caching
@@ -74,15 +113,17 @@ function withCache<T extends unknown[], R>(
const cacheKey = getCacheKey(...args) const cacheKey = getCacheKey(...args)
if (CacheService.has(cacheKey)) { if (CacheService.has(cacheKey)) {
logger.debug(`${logPrefix} loaded from cache`) logger.debug(`${logPrefix} loaded from cache`, { cacheKey })
const cachedData = CacheService.get<R>(cacheKey) const cachedData = CacheService.get<R>(cacheKey)
if (cachedData) { if (cachedData) {
return cachedData return cachedData
} }
} }
const start = Date.now()
const result = await fn(...args) const result = await fn(...args)
CacheService.set(cacheKey, result, ttl) CacheService.set(cacheKey, result, ttl)
logger.debug(`${logPrefix} cached`, { cacheKey, ttlMs: ttl, durationMs: Date.now() - start })
return result return result
} }
} }
@@ -128,6 +169,7 @@ class McpService {
// If there's a pending initialization, wait for it // If there's a pending initialization, wait for it
const pendingClient = this.pendingClients.get(serverKey) const pendingClient = this.pendingClients.get(serverKey)
if (pendingClient) { if (pendingClient) {
getServerLogger(server).silly(`Waiting for pending client initialization`)
return pendingClient return pendingClient
} }
@@ -136,8 +178,11 @@ class McpService {
if (existingClient) { if (existingClient) {
try { try {
// Check if the existing client is still connected // Check if the existing client is still connected
const pingResult = await existingClient.ping() const pingResult = await existingClient.ping({
logger.debug(`Ping result for ${server.name}:`, pingResult) // add short timeout to prevent hanging
timeout: 1000
})
getServerLogger(server).debug(`Ping result`, { ok: !!pingResult })
// If the ping fails, remove the client from the cache // If the ping fails, remove the client from the cache
// and create a new one // and create a new one
if (!pingResult) { if (!pingResult) {
@@ -146,7 +191,7 @@ class McpService {
return existingClient return existingClient
} }
} catch (error: any) { } catch (error: any) {
logger.error(`Error pinging server ${server.name}:`, error?.message) getServerLogger(server).error(`Error pinging server`, error as Error)
this.clients.delete(serverKey) this.clients.delete(serverKey)
} }
} }
@@ -172,15 +217,15 @@ class McpService {
> => { > => {
// Create appropriate transport based on configuration // Create appropriate transport based on configuration
if (isBuiltinMCPServer(server) && server.name !== BuiltinMCPServerNames.mcpAutoInstall) { if (isBuiltinMCPServer(server) && server.name !== BuiltinMCPServerNames.mcpAutoInstall) {
logger.debug(`Using in-memory transport for server: ${server.name}`) getServerLogger(server).debug(`Using in-memory transport`)
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair() const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
// start the in-memory server with the given name and environment variables // start the in-memory server with the given name and environment variables
const inMemoryServer = createInMemoryMCPServer(server.name, args, server.env || {}) const inMemoryServer = createInMemoryMCPServer(server.name, args, server.env || {})
try { try {
await inMemoryServer.connect(serverTransport) await inMemoryServer.connect(serverTransport)
logger.debug(`In-memory server started: ${server.name}`) getServerLogger(server).debug(`In-memory server started`)
} catch (error: Error | any) { } catch (error: Error | any) {
logger.error(`Error starting in-memory server: ${error}`) getServerLogger(server).error(`Error starting in-memory server`, error as Error)
throw new Error(`Failed to start in-memory server: ${error.message}`) throw new Error(`Failed to start in-memory server: ${error.message}`)
} }
// set the client transport to the client // set the client transport to the client
@@ -193,7 +238,10 @@ class McpService {
}, },
authProvider authProvider
} }
logger.debug(`StreamableHTTPClientTransport options:`, options) // redact headers before logging
getServerLogger(server).debug(`StreamableHTTPClientTransport options`, {
options: redactSensitive(options)
})
return new StreamableHTTPClientTransport(new URL(server.baseUrl!), options) return new StreamableHTTPClientTransport(new URL(server.baseUrl!), options)
} else if (server.type === 'sse') { } else if (server.type === 'sse') {
const options: SSEClientTransportOptions = { const options: SSEClientTransportOptions = {
@@ -209,7 +257,7 @@ class McpService {
headers['Authorization'] = `Bearer ${tokens.access_token}` headers['Authorization'] = `Bearer ${tokens.access_token}`
} }
} catch (error) { } catch (error) {
logger.error('Failed to fetch tokens:', error as Error) getServerLogger(server).error('Failed to fetch tokens:', error as Error)
} }
} }
@@ -239,15 +287,18 @@ class McpService {
...server.env, ...server.env,
...resolvedConfig.env ...resolvedConfig.env
} }
logger.debug(`Using resolved DXT config - command: ${cmd}, args: ${args?.join(' ')}`) getServerLogger(server).debug(`Using resolved DXT config`, {
command: cmd,
args
})
} else { } else {
logger.warn(`Failed to resolve DXT config for ${server.name}, falling back to manifest values`) getServerLogger(server).warn(`Failed to resolve DXT config, falling back to manifest values`)
} }
} }
if (server.command === 'npx') { if (server.command === 'npx') {
cmd = await getBinaryPath('bun') cmd = await getBinaryPath('bun')
logger.debug(`Using command: ${cmd}`) getServerLogger(server).debug(`Using command`, { command: cmd })
// add -x to args if args exist // add -x to args if args exist
if (args && args.length > 0) { if (args && args.length > 0) {
@@ -282,7 +333,7 @@ class McpService {
} }
} }
logger.debug(`Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`) getServerLogger(server).debug(`Starting server`, { command: cmd, args })
// Logger.info(`[MCP] Environment variables for server:`, server.env) // Logger.info(`[MCP] Environment variables for server:`, server.env)
const loginShellEnv = await this.getLoginShellEnv() const loginShellEnv = await this.getLoginShellEnv()
@@ -304,12 +355,14 @@ class McpService {
// For DXT servers, set the working directory to the extracted path // For DXT servers, set the working directory to the extracted path
if (server.dxtPath) { if (server.dxtPath) {
transportOptions.cwd = server.dxtPath transportOptions.cwd = server.dxtPath
logger.debug(`Setting working directory for DXT server: ${server.dxtPath}`) getServerLogger(server).debug(`Setting working directory for DXT server`, {
cwd: server.dxtPath
})
} }
const stdioTransport = new StdioClientTransport(transportOptions) const stdioTransport = new StdioClientTransport(transportOptions)
stdioTransport.stderr?.on('data', (data) => stdioTransport.stderr?.on('data', (data) =>
logger.debug(`Stdio stderr for server: ${server.name}` + data.toString()) getServerLogger(server).debug(`Stdio stderr`, { data: data.toString() })
) )
return stdioTransport return stdioTransport
} else { } else {
@@ -318,7 +371,7 @@ class McpService {
} }
const handleAuth = async (client: Client, transport: SSEClientTransport | StreamableHTTPClientTransport) => { const handleAuth = async (client: Client, transport: SSEClientTransport | StreamableHTTPClientTransport) => {
logger.debug(`Starting OAuth flow for server: ${server.name}`) getServerLogger(server).debug(`Starting OAuth flow`)
// Create an event emitter for the OAuth callback // Create an event emitter for the OAuth callback
const events = new EventEmitter() const events = new EventEmitter()
@@ -331,27 +384,27 @@ class McpService {
// Set a timeout to close the callback server // Set a timeout to close the callback server
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
logger.warn(`OAuth flow timed out for server: ${server.name}`) getServerLogger(server).warn(`OAuth flow timed out`)
callbackServer.close() callbackServer.close()
}, 300000) // 5 minutes timeout }, 300000) // 5 minutes timeout
try { try {
// Wait for the authorization code // Wait for the authorization code
const authCode = await callbackServer.waitForAuthCode() const authCode = await callbackServer.waitForAuthCode()
logger.debug(`Received auth code: ${authCode}`) getServerLogger(server).debug(`Received auth code`)
// Complete the OAuth flow // Complete the OAuth flow
await transport.finishAuth(authCode) await transport.finishAuth(authCode)
logger.debug(`OAuth flow completed for server: ${server.name}`) getServerLogger(server).debug(`OAuth flow completed`)
const newTransport = await initTransport() const newTransport = await initTransport()
// Try to connect again // Try to connect again
await client.connect(newTransport) await client.connect(newTransport)
logger.debug(`Successfully authenticated with server: ${server.name}`) getServerLogger(server).debug(`Successfully authenticated`)
} catch (oauthError) { } catch (oauthError) {
logger.error(`OAuth authentication failed for server ${server.name}:`, oauthError as Error) getServerLogger(server).error(`OAuth authentication failed`, oauthError as Error)
throw new Error( throw new Error(
`OAuth authentication failed: ${oauthError instanceof Error ? oauthError.message : String(oauthError)}` `OAuth authentication failed: ${oauthError instanceof Error ? oauthError.message : String(oauthError)}`
) )
@@ -390,7 +443,7 @@ class McpService {
logger.debug(`Activated server: ${server.name}`) logger.debug(`Activated server: ${server.name}`)
return client return client
} catch (error: any) { } catch (error: any) {
logger.error(`Error activating server ${server.name}:`, error?.message) getServerLogger(server).error(`Error activating server`, error as Error)
throw new Error(`[MCP] Error activating server ${server.name}: ${error.message}`) throw new Error(`[MCP] Error activating server ${server.name}: ${error.message}`)
} }
} finally { } finally {
@@ -450,9 +503,9 @@ class McpService {
logger.debug(`Message from server ${server.name}:`, notification.params) logger.debug(`Message from server ${server.name}:`, notification.params)
}) })
logger.debug(`Set up notification handlers for server: ${server.name}`) getServerLogger(server).debug(`Set up notification handlers`)
} catch (error) { } catch (error) {
logger.error(`Failed to set up notification handlers for server ${server.name}:`, error as Error) getServerLogger(server).error(`Failed to set up notification handlers`, error as Error)
} }
} }
@@ -470,7 +523,7 @@ class McpService {
CacheService.remove(`mcp:list_tool:${serverKey}`) CacheService.remove(`mcp:list_tool:${serverKey}`)
CacheService.remove(`mcp:list_prompts:${serverKey}`) CacheService.remove(`mcp:list_prompts:${serverKey}`)
CacheService.remove(`mcp:list_resources:${serverKey}`) CacheService.remove(`mcp:list_resources:${serverKey}`)
logger.debug(`Cleared all caches for server: ${serverKey}`) logger.debug(`Cleared all caches for server`, { serverKey })
} }
async closeClient(serverKey: string) { async closeClient(serverKey: string) {
@@ -478,18 +531,18 @@ class McpService {
if (client) { if (client) {
// Remove the client from the cache // Remove the client from the cache
await client.close() await client.close()
logger.debug(`Closed server: ${serverKey}`) logger.debug(`Closed server`, { serverKey })
this.clients.delete(serverKey) this.clients.delete(serverKey)
// Clear all caches for this server // Clear all caches for this server
this.clearServerCache(serverKey) this.clearServerCache(serverKey)
} else { } else {
logger.warn(`No client found for server: ${serverKey}`) logger.warn(`No client found for server`, { serverKey })
} }
} }
async stopServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) { async stopServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
const serverKey = this.getServerKey(server) const serverKey = this.getServerKey(server)
logger.debug(`Stopping server: ${server.name}`) getServerLogger(server).debug(`Stopping server`)
await this.closeClient(serverKey) await this.closeClient(serverKey)
} }
@@ -505,16 +558,16 @@ class McpService {
try { try {
const cleaned = this.dxtService.cleanupDxtServer(server.name) const cleaned = this.dxtService.cleanupDxtServer(server.name)
if (cleaned) { if (cleaned) {
logger.debug(`Cleaned up DXT server directory for: ${server.name}`) getServerLogger(server).debug(`Cleaned up DXT server directory`)
} }
} catch (error) { } catch (error) {
logger.error(`Failed to cleanup DXT server: ${server.name}`, error as Error) getServerLogger(server).error(`Failed to cleanup DXT server`, error as Error)
} }
} }
} }
async restartServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) { async restartServer(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
logger.debug(`Restarting server: ${server.name}`) getServerLogger(server).debug(`Restarting server`)
const serverKey = this.getServerKey(server) const serverKey = this.getServerKey(server)
await this.closeClient(serverKey) await this.closeClient(serverKey)
// Clear cache before restarting to ensure fresh data // Clear cache before restarting to ensure fresh data
@@ -527,7 +580,7 @@ class McpService {
try { try {
await this.closeClient(key) await this.closeClient(key)
} catch (error: any) { } catch (error: any) {
logger.error(`Failed to close client: ${error?.message}`) logger.error(`Failed to close client`, error as Error)
} }
} }
} }
@@ -536,9 +589,9 @@ class McpService {
* Check connectivity for an MCP server * Check connectivity for an MCP server
*/ */
public async checkMcpConnectivity(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise<boolean> { public async checkMcpConnectivity(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise<boolean> {
logger.debug(`Checking connectivity for server: ${server.name}`) getServerLogger(server).debug(`Checking connectivity`)
try { try {
logger.debug(`About to call initClient for server: ${server.name}`, { hasInitClient: !!this.initClient }) getServerLogger(server).debug(`About to call initClient`, { hasInitClient: !!this.initClient })
if (!this.initClient) { if (!this.initClient) {
throw new Error('initClient method is not available') throw new Error('initClient method is not available')
@@ -547,10 +600,10 @@ class McpService {
const client = await this.initClient(server) const client = await this.initClient(server)
// Attempt to list tools as a way to check connectivity // Attempt to list tools as a way to check connectivity
await client.listTools() await client.listTools()
logger.debug(`Connectivity check successful for server: ${server.name}`) getServerLogger(server).debug(`Connectivity check successful`)
return true return true
} catch (error) { } catch (error) {
logger.error(`Connectivity check failed for server: ${server.name}`, error as Error) getServerLogger(server).error(`Connectivity check failed`, error as Error)
// Close the client if connectivity check fails to ensure a clean state for the next attempt // Close the client if connectivity check fails to ensure a clean state for the next attempt
const serverKey = this.getServerKey(server) const serverKey = this.getServerKey(server)
await this.closeClient(serverKey) await this.closeClient(serverKey)
@@ -559,9 +612,8 @@ class McpService {
} }
private async listToolsImpl(server: MCPServer): Promise<MCPTool[]> { private async listToolsImpl(server: MCPServer): Promise<MCPTool[]> {
logger.debug(`Listing tools for server: ${server.name}`) getServerLogger(server).debug(`Listing tools`)
const client = await this.initClient(server) const client = await this.initClient(server)
logger.debug(`Client for server: ${server.name}`, client)
try { try {
const { tools } = await client.listTools() const { tools } = await client.listTools()
const serverTools: MCPTool[] = [] const serverTools: MCPTool[] = []
@@ -576,7 +628,7 @@ class McpService {
}) })
return serverTools return serverTools
} catch (error: any) { } catch (error: any) {
logger.error(`Failed to list tools for server: ${server.name}`, error?.message) getServerLogger(server).error(`Failed to list tools`, error as Error)
return [] return []
} }
} }
@@ -613,12 +665,16 @@ class McpService {
const callToolFunc = async ({ server, name, args }: CallToolArgs) => { const callToolFunc = async ({ server, name, args }: CallToolArgs) => {
try { try {
logger.debug(`Calling: ${server.name} ${name} ${JSON.stringify(args)} callId: ${toolCallId}`, server) getServerLogger(server, { tool: name, callId: toolCallId }).debug(`Calling tool`, {
args: redactSensitive(args)
})
if (typeof args === 'string') { if (typeof args === 'string') {
try { try {
args = JSON.parse(args) args = JSON.parse(args)
} catch (e) { } catch (e) {
logger.error('args parse error', args) getServerLogger(server, { tool: name, callId: toolCallId }).error('args parse error', e as Error, {
args
})
} }
if (args === '') { if (args === '') {
args = {} args = {}
@@ -627,8 +683,9 @@ class McpService {
const client = await this.initClient(server) const client = await this.initClient(server)
const result = await client.callTool({ name, arguments: args }, undefined, { const result = await client.callTool({ name, arguments: args }, undefined, {
onprogress: (process) => { onprogress: (process) => {
logger.debug(`Progress: ${process.progress / (process.total || 1)}`) getServerLogger(server, { tool: name, callId: toolCallId }).debug(`Progress`, {
logger.debug(`Progress notification received for server: ${server.name}`, process) ratio: process.progress / (process.total || 1)
})
const mainWindow = windowService.getMainWindow() const mainWindow = windowService.getMainWindow()
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.send('mcp-progress', process.progress / (process.total || 1)) mainWindow.webContents.send('mcp-progress', process.progress / (process.total || 1))
@@ -643,7 +700,7 @@ class McpService {
}) })
return result as MCPCallToolResponse return result as MCPCallToolResponse
} catch (error) { } catch (error) {
logger.error(`Error calling tool ${name} on ${server.name}:`, error as Error) getServerLogger(server, { tool: name, callId: toolCallId }).error(`Error calling tool`, error as Error)
throw error throw error
} finally { } finally {
this.activeToolCalls.delete(toolCallId) this.activeToolCalls.delete(toolCallId)
@@ -667,7 +724,7 @@ class McpService {
*/ */
private async listPromptsImpl(server: MCPServer): Promise<MCPPrompt[]> { private async listPromptsImpl(server: MCPServer): Promise<MCPPrompt[]> {
const client = await this.initClient(server) const client = await this.initClient(server)
logger.debug(`Listing prompts for server: ${server.name}`) getServerLogger(server).debug(`Listing prompts`)
try { try {
const { prompts } = await client.listPrompts() const { prompts } = await client.listPrompts()
return prompts.map((prompt: any) => ({ return prompts.map((prompt: any) => ({
@@ -679,7 +736,7 @@ class McpService {
} catch (error: any) { } catch (error: any) {
// -32601 is the code for the method not found // -32601 is the code for the method not found
if (error?.code !== -32601) { if (error?.code !== -32601) {
logger.error(`Failed to list prompts for server: ${server.name}`, error?.message) getServerLogger(server).error(`Failed to list prompts`, error as Error)
} }
return [] return []
} }
@@ -748,7 +805,7 @@ class McpService {
} catch (error: any) { } catch (error: any) {
// -32601 is the code for the method not found // -32601 is the code for the method not found
if (error?.code !== -32601) { if (error?.code !== -32601) {
logger.error(`Failed to list resources for server: ${server.name}`, error?.message) getServerLogger(server).error(`Failed to list resources`, error as Error)
} }
return [] return []
} }
@@ -774,7 +831,7 @@ class McpService {
* Get a specific resource from an MCP server (implementation) * Get a specific resource from an MCP server (implementation)
*/ */
private async getResourceImpl(server: MCPServer, uri: string): Promise<GetResourceResponse> { private async getResourceImpl(server: MCPServer, uri: string): Promise<GetResourceResponse> {
logger.debug(`Getting resource ${uri} from server: ${server.name}`) getServerLogger(server, { uri }).debug(`Getting resource`)
const client = await this.initClient(server) const client = await this.initClient(server)
try { try {
const result = await client.readResource({ uri: uri }) const result = await client.readResource({ uri: uri })
@@ -792,7 +849,7 @@ class McpService {
contents: contents contents: contents
} }
} catch (error: Error | any) { } catch (error: Error | any) {
logger.error(`Failed to get resource ${uri} from server: ${server.name}`, error.message) getServerLogger(server, { uri }).error(`Failed to get resource`, error as Error)
throw new Error(`Failed to get resource ${uri} from server: ${server.name}: ${error.message}`) throw new Error(`Failed to get resource ${uri} from server: ${server.name}: ${error.message}`)
} }
} }
@@ -837,10 +894,10 @@ class McpService {
if (activeToolCall) { if (activeToolCall) {
activeToolCall.abort() activeToolCall.abort()
this.activeToolCalls.delete(callId) this.activeToolCalls.delete(callId)
logger.debug(`Aborted tool call: ${callId}`) logger.debug(`Aborted tool call`, { callId })
return true return true
} else { } else {
logger.warn(`No active tool call found for callId: ${callId}`) logger.warn(`No active tool call found for callId`, { callId })
return false return false
} }
} }
@@ -850,22 +907,22 @@ class McpService {
*/ */
public async getServerVersion(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise<string | null> { public async getServerVersion(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise<string | null> {
try { try {
logger.debug(`Getting server version for: ${server.name}`) getServerLogger(server).debug(`Getting server version`)
const client = await this.initClient(server) const client = await this.initClient(server)
// Try to get server information which may include version // Try to get server information which may include version
const serverInfo = client.getServerVersion() const serverInfo = client.getServerVersion()
logger.debug(`Server info for ${server.name}:`, serverInfo) getServerLogger(server).debug(`Server info`, redactSensitive(serverInfo))
if (serverInfo && serverInfo.version) { if (serverInfo && serverInfo.version) {
logger.debug(`Server version for ${server.name}: ${serverInfo.version}`) getServerLogger(server).debug(`Server version`, { version: serverInfo.version })
return serverInfo.version return serverInfo.version
} }
logger.warn(`No version information available for server: ${server.name}`) getServerLogger(server).warn(`No version information available`)
return null return null
} catch (error: any) { } catch (error: any) {
logger.error(`Failed to get server version for ${server.name}:`, error?.message) getServerLogger(server).error(`Failed to get server version`, error as Error)
return null return null
} }
} }

View File

@@ -0,0 +1,144 @@
import { PoeLogo } from '@renderer/components/Icons'
import { getProviderLogo } from '@renderer/config/providers'
import { Provider } from '@renderer/types'
import { generateColorFromChar, getFirstCharacter, getForegroundColor } from '@renderer/utils'
import { Avatar } from 'antd'
import React from 'react'
import styled from 'styled-components'
interface ProviderAvatarPrimitiveProps {
providerId: string
providerName: string
logoSrc?: string
size?: number
className?: string
style?: React.CSSProperties
}
interface ProviderAvatarProps {
provider: Provider
customLogos?: Record<string, string>
size?: number
className?: string
style?: React.CSSProperties
}
const ProviderSvgLogo = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border: 0.5px solid var(--color-border);
border-radius: 100%;
& > svg {
width: 80%;
height: 80%;
}
`
const ProviderLogo = styled(Avatar)`
width: 100%;
height: 100%;
border: 0.5px solid var(--color-border);
`
export const ProviderAvatarPrimitive: React.FC<ProviderAvatarPrimitiveProps> = ({
providerId,
providerName,
logoSrc,
size,
className,
style
}) => {
if (providerId === 'poe') {
return (
<ProviderSvgLogo className={className} style={style}>
<PoeLogo fontSize={size} />
</ProviderSvgLogo>
)
}
if (logoSrc) {
return (
<ProviderLogo draggable="false" shape="circle" src={logoSrc} className={className} style={style} size={size} />
)
}
const backgroundColor = generateColorFromChar(providerName)
const color = providerName ? getForegroundColor(backgroundColor) : 'white'
return (
<ProviderLogo
size={size}
shape="circle"
className={className}
style={{
backgroundColor,
color,
...style
}}>
{getFirstCharacter(providerName)}
</ProviderLogo>
)
}
export const ProviderAvatar: React.FC<ProviderAvatarProps> = ({
provider,
customLogos = {},
className,
style,
size
}) => {
const systemLogoSrc = getProviderLogo(provider.id)
if (systemLogoSrc) {
return (
<ProviderAvatarPrimitive
size={size}
providerId={provider.id}
providerName={provider.name}
logoSrc={systemLogoSrc}
className={className}
style={style}
/>
)
}
const customLogo = customLogos[provider.id]
if (customLogo) {
if (customLogo === 'poe') {
return (
<ProviderAvatarPrimitive
size={size}
providerId="poe"
providerName={provider.name}
className={className}
style={style}
/>
)
}
return (
<ProviderAvatarPrimitive
providerId={provider.id}
providerName={provider.name}
logoSrc={customLogo}
size={size}
className={className}
style={style}
/>
)
}
return (
<ProviderAvatarPrimitive
providerId={provider.id}
providerName={provider.name}
size={size}
className={className}
style={style}
/>
)
}

View File

@@ -1,4 +1,5 @@
import { SearchOutlined } from '@ant-design/icons' import { SearchOutlined } from '@ant-design/icons'
import { ProviderAvatarPrimitive } from '@renderer/components/ProviderAvatar'
import { PROVIDER_LOGO_MAP } from '@renderer/config/providers' import { PROVIDER_LOGO_MAP } from '@renderer/config/providers'
import { getProviderLabel } from '@renderer/i18n/label' import { getProviderLabel } from '@renderer/i18n/label'
import { Input, Tooltip } from 'antd' import { Input, Tooltip } from 'antd'
@@ -48,10 +49,10 @@ const ProviderLogoPicker: FC<Props> = ({ onProviderClick }) => {
/> />
</SearchContainer> </SearchContainer>
<LogoGrid> <LogoGrid>
{filteredProviders.map(({ id, logo, name }) => ( {filteredProviders.map(({ id, name, logo }) => (
<Tooltip key={id} title={name} placement="top" mouseLeaveDelay={0}> <Tooltip key={id} title={name} placement="top" mouseLeaveDelay={0}>
<LogoItem onClick={(e) => handleProviderClick(e, id)}> <LogoItem onClick={(e) => handleProviderClick(e, id)}>
<img src={logo} alt={name} draggable={false} /> <ProviderAvatarPrimitive providerId={id} size={52} providerName={name} logoSrc={logo} />
</LogoItem> </LogoItem>
</Tooltip> </Tooltip>
))} ))}
@@ -86,11 +87,12 @@ const LogoGrid = styled.div`
const LogoItem = styled.div` const LogoItem = styled.div`
width: 52px; width: 52px;
height: 52px; height: 52px;
border-radius: 100%;
overflow: hidden;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
border-radius: 8px;
transition: all 0.2s ease; transition: all 0.2s ease;
background: var(--color-background-soft); background: var(--color-background-soft);
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);
@@ -102,8 +104,8 @@ const LogoItem = styled.div`
} }
img { img {
width: 32px; width: 100%;
height: 32px; height: 100%;
object-fit: contain; object-fit: contain;
user-select: none; user-select: none;
-webkit-user-drag: none; -webkit-user-drag: none;

View File

@@ -661,7 +661,7 @@ export const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
vertexai: VertexAIProviderLogo, vertexai: VertexAIProviderLogo,
'new-api': NewAPIProviderLogo, 'new-api': NewAPIProviderLogo,
'aws-bedrock': AwsProviderLogo, 'aws-bedrock': AwsProviderLogo,
poe: 'svg' // use svg icon component poe: 'poe' // use svg icon component
} as const } as const
export function getProviderLogo(providerId: string) { export function getProviderLogo(providerId: string) {

View File

@@ -1,3 +1,4 @@
import { loggerService } from '@logger'
import { nanoid } from '@reduxjs/toolkit' import { nanoid } from '@reduxjs/toolkit'
import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar' import CollapsibleSearchBar from '@renderer/components/CollapsibleSearchBar'
import { Sortable, useDndReorder } from '@renderer/components/dnd' import { Sortable, useDndReorder } from '@renderer/components/dnd'
@@ -23,6 +24,8 @@ import McpMarketList from './McpMarketList'
import McpServerCard from './McpServerCard' import McpServerCard from './McpServerCard'
import SyncServersPopup from './SyncServersPopup' import SyncServersPopup from './SyncServersPopup'
const logger = loggerService.withContext('McpServersList')
const McpServersList: FC = () => { const McpServersList: FC = () => {
const { mcpServers, addMCPServer, deleteMCPServer, updateMcpServers, updateMCPServer } = useMCPServers() const { mcpServers, addMCPServer, deleteMCPServer, updateMcpServers, updateMCPServer } = useMCPServers()
const { t } = useTranslation() const { t } = useTranslation()
@@ -158,12 +161,11 @@ const McpServersList: FC = () => {
const handleToggleActive = async (server: MCPServer, active: boolean) => { const handleToggleActive = async (server: MCPServer, active: boolean) => {
setLoadingServerIds((prev) => new Set(prev).add(server.id)) setLoadingServerIds((prev) => new Set(prev).add(server.id))
const oldActiveState = server.isActive const oldActiveState = server.isActive
logger.silly('toggle activate', { serverId: server.id, active })
try { try {
if (active) { if (active) {
await window.api.mcp.listTools(server)
// Fetch version when server is activated // Fetch version when server is activated
fetchServerVersion({ ...server, isActive: active }) await fetchServerVersion({ ...server, isActive: active })
} else { } else {
await window.api.mcp.stopServer(server) await window.api.mcp.stopServer(server)
// Clear version when server is deactivated // Clear version when server is deactivated
@@ -259,7 +261,7 @@ const McpServersList: FC = () => {
server={server} server={server}
version={serverVersions[server.id]} version={serverVersions[server.id]}
isLoading={loadingServerIds.has(server.id)} isLoading={loadingServerIds.has(server.id)}
onToggle={(active) => handleToggleActive(server, active)} onToggle={async (active) => await handleToggleActive(server, active)}
onDelete={() => onDeleteMcpServer(server)} onDelete={() => onDeleteMcpServer(server)}
onEdit={() => navigate(`/settings/mcp/settings/${encodeURIComponent(server.id)}`)} onEdit={() => navigate(`/settings/mcp/settings/${encodeURIComponent(server.id)}`)}
onOpenUrl={(url) => window.open(url, '_blank')} onOpenUrl={(url) => window.open(url, '_blank')}

View File

@@ -1,5 +1,6 @@
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { Center, VStack } from '@renderer/components/Layout' import { Center, VStack } from '@renderer/components/Layout'
import { ProviderAvatarPrimitive } from '@renderer/components/ProviderAvatar'
import ProviderLogoPicker from '@renderer/components/ProviderLogoPicker' import ProviderLogoPicker from '@renderer/components/ProviderLogoPicker'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { PROVIDER_LOGO_MAP } from '@renderer/config/providers' import { PROVIDER_LOGO_MAP } from '@renderer/config/providers'
@@ -143,7 +144,6 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
}) })
setLogo(tempUrl) setLogo(tempUrl)
} }
setDropdownOpen(false) setDropdownOpen(false)
} catch (error: any) { } catch (error: any) {
window.message.error(error.message) window.message.error(error.message)
@@ -152,7 +152,8 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
<MenuItem ref={uploadRef}>{t('settings.general.image_upload')}</MenuItem> <MenuItem ref={uploadRef}>{t('settings.general.image_upload')}</MenuItem>
</Upload> </Upload>
), ),
onClick: () => { onClick: (e: any) => {
e.stopPropagation()
uploadRef.current?.click() uploadRef.current?.click()
} }
}, },
@@ -215,7 +216,9 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
}} }}
placement="bottom"> placement="bottom">
{logo ? ( {logo ? (
<ProviderLogo src={logo} /> <ProviderLogo>
<ProviderAvatarPrimitive providerId={logo} providerName={name} logoSrc={logo} size={60} />
</ProviderLogo>
) : ( ) : (
<ProviderInitialsLogo style={name ? { backgroundColor, color } : undefined}> <ProviderInitialsLogo style={name ? { backgroundColor, color } : undefined}>
{getInitials()} {getInitials()}
@@ -258,16 +261,17 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
) )
} }
const ProviderLogo = styled.img` const ProviderLogo = styled.div`
cursor: pointer; cursor: pointer;
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 12px; border-radius: 100%;
object-fit: contain; overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
background-color: var(--color-background-soft);
padding: 5px;
border: 0.5px solid var(--color-border);
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
} }
@@ -277,7 +281,7 @@ const ProviderInitialsLogo = styled.div`
cursor: pointer; cursor: pointer;
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 12px; border-radius: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@@ -5,22 +5,14 @@ import {
type DraggableVirtualListRef, type DraggableVirtualListRef,
useDraggableReorder useDraggableReorder
} from '@renderer/components/DraggableList' } from '@renderer/components/DraggableList'
import { DeleteIcon, EditIcon, PoeLogo } from '@renderer/components/Icons' import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
import { getProviderLogo } from '@renderer/config/providers' import { ProviderAvatar } from '@renderer/components/ProviderAvatar'
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider' import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import ImageStorage from '@renderer/services/ImageStorage' import ImageStorage from '@renderer/services/ImageStorage'
import { isSystemProvider, Provider, ProviderType } from '@renderer/types' import { isSystemProvider, Provider, ProviderType } from '@renderer/types'
import { import { getFancyProviderName, matchKeywordsInModel, matchKeywordsInProvider, uuid } from '@renderer/utils'
generateColorFromChar, import { Button, Dropdown, Input, MenuProps, Tag } from 'antd'
getFancyProviderName,
getFirstCharacter,
getForegroundColor,
matchKeywordsInModel,
matchKeywordsInProvider,
uuid
} from '@renderer/utils'
import { Avatar, Button, Dropdown, Input, MenuProps, Tag } from 'antd'
import { GripVertical, PlusIcon, Search, UserPen } from 'lucide-react' import { GripVertical, PlusIcon, Search, UserPen } from 'lucide-react'
import { FC, startTransition, useCallback, useEffect, useRef, useState } from 'react' import { FC, startTransition, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -280,36 +272,6 @@ const ProviderList: FC = () => {
} }
} }
const getProviderAvatar = (provider: Provider, size: number = 25) => {
// 特殊处理一下svg格式
if (isSystemProvider(provider)) {
switch (provider.id) {
case 'poe':
return <PoeLogo fontSize={size} />
}
}
const logoSrc = getProviderLogo(provider.id)
if (logoSrc) {
return <ProviderLogo draggable="false" shape="circle" src={logoSrc} size={size} />
}
const customLogo = providerLogos[provider.id]
if (customLogo) {
return <ProviderLogo draggable="false" shape="square" src={customLogo} size={size} />
}
// generate color for custom provider
const backgroundColor = generateColorFromChar(provider.name)
const color = provider.name ? getForegroundColor(backgroundColor) : 'white'
return (
<ProviderLogo size={size} shape="square" style={{ backgroundColor, color, minWidth: size }}>
{getFirstCharacter(provider.name)}
</ProviderLogo>
)
}
const filteredProviders = providers.filter((provider) => { const filteredProviders = providers.filter((provider) => {
const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean) const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean)
const isProviderMatch = matchKeywordsInProvider(keywords, provider) const isProviderMatch = matchKeywordsInProvider(keywords, provider)
@@ -382,7 +344,14 @@ const ProviderList: FC = () => {
<DragHandle> <DragHandle>
<GripVertical size={12} /> <GripVertical size={12} />
</DragHandle> </DragHandle>
{getProviderAvatar(provider)} <ProviderAvatar
style={{
width: 24,
height: 24
}}
provider={provider}
customLogos={providerLogos}
/>
<ProviderItemName className="text-nowrap">{getFancyProviderName(provider)}</ProviderItemName> <ProviderItemName className="text-nowrap">{getFancyProviderName(provider)}</ProviderItemName>
{provider.enabled && ( {provider.enabled && (
<Tag color="green" style={{ marginLeft: 'auto', marginRight: 0, borderRadius: 16 }}> <Tag color="green" style={{ marginLeft: 'auto', marginRight: 0, borderRadius: 16 }}>
@@ -466,10 +435,6 @@ const DragHandle = styled.div`
} }
` `
const ProviderLogo = styled(Avatar)`
border: 0.5px solid var(--color-border);
`
const ProviderItemName = styled.div` const ProviderItemName = styled.div`
margin-left: 10px; margin-left: 10px;
font-weight: 500; font-weight: 500;

View File

@@ -612,9 +612,14 @@ const TranslatePage: FC = () => {
// 粘贴上传文件 // 粘贴上传文件
const onPaste = useCallback( const onPaste = useCallback(
async (event: React.ClipboardEvent<HTMLTextAreaElement>) => { async (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
event.preventDefault()
if (isProcessing) return
setIsProcessing(true) setIsProcessing(true)
logger.debug('event', event) logger.debug('event', event)
if (event.clipboardData?.files && event.clipboardData.files.length > 0) { const text = event.clipboardData.getData('text')
if (!isEmpty(text)) {
setText(text)
} else if (event.clipboardData.files && event.clipboardData.files.length > 0) {
event.preventDefault() event.preventDefault()
const files = event.clipboardData.files const files = event.clipboardData.files
const file = getSingleFile(files) as File const file = getSingleFile(files) as File
@@ -659,7 +664,7 @@ const TranslatePage: FC = () => {
} }
setIsProcessing(false) setIsProcessing(false)
}, },
[getSingleFile, processFile, t] [getSingleFile, isProcessing, processFile, t]
) )
return ( return (
<Container <Container