mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
Merge branch 'main' into 1600822305-patch-2
This commit is contained in:
commit
3ac8fe6861
8
LICENSE
8
LICENSE
@ -1,13 +1,13 @@
|
||||
**许可协议**
|
||||
|
||||
本软件采用 Apache License 2.0 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry Studio 时还应遵守以下附加条款:
|
||||
采用 Apache License 2.0 修改版许可,并附加以下条件:
|
||||
|
||||
**一. 商用许可**
|
||||
|
||||
在以下任何一种情况下,您需要联系我们并获得明确的书面商业授权后,方可继续使用 Cherry Studio 材料:
|
||||
|
||||
1. **修改与衍生**: 您对 Cherry Studio 材料进行修改或基于其进行衍生开发(包括但不限于修改应用名称、Logo、代码、功能、界面,数据等)。
|
||||
2. **企业服务**: 在您的企业内部,或为企业客户提供基于 CherryStudio 的服务,且该服务支持 10 人及以上累计用户使用。
|
||||
2. **企业服务**: 在您的企业内部,或为企业客户提供基于 Cherry Studio 的服务,且该服务支持 10 人及以上累计用户使用。
|
||||
3. **硬件捆绑销售**: 您将 Cherry Studio 预装或集成到硬件设备或产品中进行捆绑销售。
|
||||
4. **政府或教育机构大规模采购**: 您的使用场景属于政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。
|
||||
5. **面向公众的公有云服务**:基于 Cherry Studio,提供面向公众的公有云服务。
|
||||
@ -33,7 +33,7 @@
|
||||
|
||||
**License Agreement**
|
||||
|
||||
This software is licensed under the Apache License 2.0. In addition to the terms stipulated by the Apache License 2.0, you must comply with the following supplementary terms when using Cherry Studio:
|
||||
This software is licensed under a modified version of the Apache License 2.0, with the following additional conditions。
|
||||
|
||||
**I. Commercial Licensing**
|
||||
|
||||
@ -59,4 +59,4 @@ As a contributor to Cherry Studio, you must agree to the following terms:
|
||||
|
||||
If you have any questions or need to apply for commercial authorization, please contact the Cherry Studio development team.
|
||||
|
||||
Other than these specific conditions, all remaining rights and restrictions follow the Apache License 2.0. For more detailed information regarding Apache License 2.0, please visit http://www.apache.org/licenses/LICENSE-2.0.
|
||||
Other than these specific conditions, all remaining rights and restrictions follow the Apache License 2.0. For more detailed information regarding Apache License 2.0, please visit http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
@ -73,6 +73,7 @@
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"adm-zip": "^0.5.16",
|
||||
"async-mutex": "^0.5.0",
|
||||
"color": "^5.0.0",
|
||||
"diff": "^7.0.0",
|
||||
"docx": "^9.0.2",
|
||||
|
||||
@ -50,6 +50,8 @@ export enum IpcChannel {
|
||||
Mcp_StopServer = 'mcp:stop-server',
|
||||
Mcp_ListTools = 'mcp:list-tools',
|
||||
Mcp_CallTool = 'mcp:call-tool',
|
||||
Mcp_ListPrompts = 'mcp:list-prompts',
|
||||
Mcp_GetPrompt = 'mcp:get-prompt',
|
||||
Mcp_GetInstallInfo = 'mcp:get-install-info',
|
||||
Mcp_ServersChanged = 'mcp:servers-changed',
|
||||
Mcp_ServersUpdated = 'mcp:servers-updated',
|
||||
|
||||
@ -1,111 +1,109 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CherryStudio 许可协议-ZH/EN</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 p-8">
|
||||
<div class="container mx-auto bg-white p-6 rounded shadow-lg">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio 许可协议</h1>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-4">许可协议</h2>
|
||||
<p class="mb-4">
|
||||
本软件采用 <strong>Apache License 2.0</strong> 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry
|
||||
Studio 时还应遵守以下附加条款:
|
||||
</p>
|
||||
<h3 class="text-xl font-semibold mb-2">一. 商用许可</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li><strong>免费商用</strong>:用户在不修改代码的情况下,可以免费用于商业目的。</li>
|
||||
<li>
|
||||
<strong>商业授权</strong>:如果您满足以下任意条件之一,需取得商业授权:
|
||||
<ol class="list-decimal list-inside ml-4">
|
||||
<li>对本软件进行二次修改、开发(包括但不限于修改应用名称、logo、代码以及功能)。</li>
|
||||
<li>为企业客户提供多租户服务,且该服务支持 10 人或以上的使用。</li>
|
||||
<li>预装或集成到硬件设备或产品中进行捆绑销售。</li>
|
||||
<li>政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。</li>
|
||||
</ol>
|
||||
</li>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>许可协议 | License Agreement</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50">
|
||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||
<!-- 中文版本 -->
|
||||
<div class="mb-12">
|
||||
<h1 class="text-3xl font-bold mb-8 text-gray-900">许可协议</h1>
|
||||
|
||||
<p class="mb-6 text-gray-700">采用 Apache License 2.0 修改版许可,并附加以下条件:</p>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">一. 商用许可</h2>
|
||||
<p class="mb-4 text-gray-700">在以下任何一种情况下,您需要联系我们并获得明确的书面商业授权后,方可继续使用 Cherry Studio 材料:</p>
|
||||
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>修改与衍生</strong>: 您对 Cherry Studio 材料进行修改或基于其进行衍生开发(包括但不限于修改应用名称、Logo、代码、功能、界面,数据等)。</li>
|
||||
<li><strong>企业服务</strong>: 在您的企业内部,或为企业客户提供基于 Cherry Studio 的服务,且该服务支持 10 人及以上累计用户使用。</li>
|
||||
<li><strong>硬件捆绑销售</strong>: 您将 Cherry Studio 预装或集成到硬件设备或产品中进行捆绑销售。</li>
|
||||
<li><strong>政府或教育机构大规模采购</strong>: 您的使用场景属于政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。</li>
|
||||
<li><strong>面向公众的公有云服务</strong>:基于 Cherry Studio,提供面向公众的公有云服务。</li>
|
||||
</ol>
|
||||
<h3 class="text-xl font-semibold mb-2">二. 贡献者协议</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">二. 贡献者协议</h2>
|
||||
<p class="mb-4 text-gray-700">作为 Cherry Studio 的贡献者,您应当同意以下条款:</p>
|
||||
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>许可调整</strong>:生产者有权根据需要对开源协议进行调整,使其更加严格或宽松。</li>
|
||||
<li><strong>商业用途</strong>:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。</li>
|
||||
</ol>
|
||||
<h3 class="text-xl font-semibold mb-2">三. 其他条款</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">三. 其他条款</h2>
|
||||
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||
<li>本协议条款的解释权归 Cherry Studio 开发者所有。</li>
|
||||
<li>本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。</li>
|
||||
</ol>
|
||||
<p class="mb-4">如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。</p>
|
||||
<p>
|
||||
除上述特定条件外,其他所有权利和限制均遵循 Apache License 2.0。有关 Apache License 2.0 的详细信息,请访问
|
||||
<a href="http://www.apache.org/licenses/LICENSE-2.0" class="text-blue-500 underline"
|
||||
>http://www.apache.org/licenses/LICENSE-2.0</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio License</h1>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-4">License Agreement</h2>
|
||||
<p class="mb-4">
|
||||
This software is licensed under the <strong>Apache License 2.0</strong>. In addition to the terms of the
|
||||
Apache License 2.0, the following additional terms apply to the use of Cherry Studio:
|
||||
</p>
|
||||
<h3 class="text-xl font-semibold mb-2">I. Commercial Use License</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li>
|
||||
<strong>Free Commercial Use</strong>: Users can use the software for commercial purposes without modifying
|
||||
the code.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Commercial License Required</strong>: A commercial license is required if any of the following
|
||||
conditions are met:
|
||||
<ol class="list-decimal list-inside ml-4">
|
||||
<li>
|
||||
You modify, develop, or alter the software, including but not limited to changes to the application
|
||||
name, logo, code, or functionality.
|
||||
</li>
|
||||
<li>You provide multi-tenant services to enterprise customers with 10 or more users.</li>
|
||||
<li>
|
||||
You pre-install or integrate the software into hardware devices or products and bundle it for sale.
|
||||
</li>
|
||||
<li>
|
||||
You are engaging in large-scale procurement for government or educational institutions, especially
|
||||
involving security, data privacy, or other sensitive requirements.
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
<h3 class="text-xl font-semibold mb-2">II. Contributor Agreement</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li>
|
||||
<strong>License Adjustment</strong>: The producer reserves the right to adjust the open-source license as
|
||||
needed, making it stricter or more lenient.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Commercial Use</strong>: Any code you contribute may be used for commercial purposes, including but
|
||||
not limited to cloud business operations.
|
||||
</li>
|
||||
</ol>
|
||||
<h3 class="text-xl font-semibold mb-2">III. Other Terms</h3>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li>The interpretation of these terms is subject to the discretion of Cherry Studio developers.</li>
|
||||
<li>These terms may be updated, and users will be notified through the software when changes occur.</li>
|
||||
</ol>
|
||||
<p class="mb-4">
|
||||
For any questions or to request a commercial license, please contact the Cherry Studio development team.
|
||||
</p>
|
||||
<p>
|
||||
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache
|
||||
License 2.0. Detailed information about the Apache License 2.0 can be found at
|
||||
<a href="http://www.apache.org/licenses/LICENSE-2.0" class="text-blue-500 underline"
|
||||
>http://www.apache.org/licenses/LICENSE-2.0</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<hr class="my-12 border-gray-300">
|
||||
|
||||
<!-- English Version -->
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-8 text-gray-900">License Agreement</h1>
|
||||
|
||||
<p class="mb-6 text-gray-700">This software is licensed under a modified version of the Apache License 2.0, with
|
||||
the following additional conditions.</p>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">I. Commercial Licensing</h2>
|
||||
<p class="mb-4 text-gray-700">You must contact us and obtain explicit written commercial authorization to
|
||||
continue using Cherry Studio materials under any of the following circumstances:</p>
|
||||
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>Modifications and Derivatives:</strong> You modify Cherry Studio materials or perform derivative
|
||||
development based on them (including but not limited to changing the application's name, logo, code,
|
||||
functionality, user interface, data, etc.).</li>
|
||||
<li><strong>Enterprise Services:</strong> You use Cherry Studio internally within your enterprise, or you
|
||||
provide Cherry Studio-based services for enterprise customers, and such services support cumulative usage by
|
||||
10 or more users.</li>
|
||||
<li><strong>Hardware Bundling and Sales:</strong> You pre-install or integrate Cherry Studio into hardware
|
||||
devices or products for bundled sale.</li>
|
||||
<li><strong>Large-scale Procurement by Government or Educational Institutions:</strong> Your usage scenario
|
||||
involves large-scale procurement projects by government or educational institutions, especially in cases
|
||||
involving sensitive requirements such as security and data privacy.</li>
|
||||
<li><strong>Public Cloud Services:</strong> You provide public cloud-based product services utilizing Cherry
|
||||
Studio.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">II. Contributor Agreement</h2>
|
||||
<p class="mb-4 text-gray-700">As a contributor to Cherry Studio, you must agree to the following terms:</p>
|
||||
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>License Adjustments:</strong> The producer reserves the right to adjust the open-source license as
|
||||
necessary, making it more strict or permissive.</li>
|
||||
<li><strong>Commercial Usage:</strong> Your contributed code may be used commercially, including but not
|
||||
limited to cloud business operations.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">III. Other Terms</h2>
|
||||
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||
<li>Cherry Studio developers reserve the right of final interpretation of these agreement terms.</li>
|
||||
<li>This agreement may be updated according to practical circumstances, and users will be notified of updates
|
||||
through this software.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<p class="mt-8 text-gray-700">
|
||||
Other than these specific conditions, all remaining rights and restrictions follow the Apache License 2.0. For
|
||||
more detailed information regarding Apache License 2.0, please visit
|
||||
<a href="http://www.apache.org/licenses/LICENSE-2.0"
|
||||
class="text-blue-600 hover:underline">http://www.apache.org/licenses/LICENSE-2.0</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -264,6 +264,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.Mcp_StopServer, mcpService.stopServer)
|
||||
ipcMain.handle(IpcChannel.Mcp_ListTools, mcpService.listTools)
|
||||
ipcMain.handle(IpcChannel.Mcp_CallTool, mcpService.callTool)
|
||||
ipcMain.handle(IpcChannel.Mcp_ListPrompts, mcpService.listPrompts)
|
||||
ipcMain.handle(IpcChannel.Mcp_GetPrompt, mcpService.getPrompt)
|
||||
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
|
||||
|
||||
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -10,13 +10,47 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { MCPServer, MCPTool } from '@types'
|
||||
import { GetMCPPromptResponse, MCPPrompt, MCPServer, MCPTool } from '@types'
|
||||
import { app } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { CacheService } from './CacheService'
|
||||
import { StreamableHTTPClientTransport, type StreamableHTTPClientTransportOptions } from './MCPStreamableHttpClient'
|
||||
|
||||
// Generic type for caching wrapped functions
|
||||
type CachedFunction<T extends unknown[], R> = (...args: T) => Promise<R>
|
||||
|
||||
/**
|
||||
* Higher-order function to add caching capability to any async function
|
||||
* @param fn The original function to be wrapped with caching
|
||||
* @param getCacheKey Function to generate a cache key from the function arguments
|
||||
* @param ttl Time to live for the cache entry in milliseconds
|
||||
* @param logPrefix Prefix for log messages
|
||||
* @returns The wrapped function with caching capability
|
||||
*/
|
||||
function withCache<T extends unknown[], R>(
|
||||
fn: (...args: T) => Promise<R>,
|
||||
getCacheKey: (...args: T) => string,
|
||||
ttl: number,
|
||||
logPrefix: string
|
||||
): CachedFunction<T, R> {
|
||||
return async (...args: T): Promise<R> => {
|
||||
const cacheKey = getCacheKey(...args)
|
||||
|
||||
if (CacheService.has(cacheKey)) {
|
||||
Logger.info(`${logPrefix} loaded from cache`)
|
||||
const cachedData = CacheService.get<R>(cacheKey)
|
||||
if (cachedData) {
|
||||
return cachedData
|
||||
}
|
||||
}
|
||||
|
||||
const result = await fn(...args)
|
||||
CacheService.set(cacheKey, result, ttl)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class McpService {
|
||||
private clients: Map<string, Client> = new Map()
|
||||
|
||||
@ -35,6 +69,8 @@ class McpService {
|
||||
this.initClient = this.initClient.bind(this)
|
||||
this.listTools = this.listTools.bind(this)
|
||||
this.callTool = this.callTool.bind(this)
|
||||
this.listPrompts = this.listPrompts.bind(this)
|
||||
this.getPrompt = this.getPrompt.bind(this)
|
||||
this.closeClient = this.closeClient.bind(this)
|
||||
this.removeServer = this.removeServer.bind(this)
|
||||
this.restartServer = this.restartServer.bind(this)
|
||||
@ -216,31 +252,40 @@ class McpService {
|
||||
}
|
||||
}
|
||||
|
||||
async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
const client = await this.initClient(server)
|
||||
const serverKey = this.getServerKey(server)
|
||||
const cacheKey = `mcp:list_tool:${serverKey}`
|
||||
if (CacheService.has(cacheKey)) {
|
||||
Logger.info(`[MCP] Tools from ${server.name} loaded from cache`)
|
||||
const cachedTools = CacheService.get<MCPTool[]>(cacheKey)
|
||||
if (cachedTools && cachedTools.length > 0) {
|
||||
return cachedTools
|
||||
}
|
||||
}
|
||||
private async listToolsImpl(server: MCPServer): Promise<MCPTool[]> {
|
||||
Logger.info(`[MCP] Listing tools for server: ${server.name}`)
|
||||
const { tools } = await client.listTools()
|
||||
const serverTools: MCPTool[] = []
|
||||
tools.map((tool: any) => {
|
||||
const serverTool: MCPTool = {
|
||||
...tool,
|
||||
id: `f${nanoid()}`,
|
||||
serverId: server.id,
|
||||
serverName: server.name
|
||||
}
|
||||
serverTools.push(serverTool)
|
||||
})
|
||||
CacheService.set(cacheKey, serverTools, 5 * 60 * 1000)
|
||||
return serverTools
|
||||
const client = await this.initClient(server)
|
||||
try {
|
||||
const { tools } = await client.listTools()
|
||||
const serverTools: MCPTool[] = []
|
||||
tools.map((tool: any) => {
|
||||
const serverTool: MCPTool = {
|
||||
...tool,
|
||||
id: `f${nanoid()}`,
|
||||
serverId: server.id,
|
||||
serverName: server.name
|
||||
}
|
||||
serverTools.push(serverTool)
|
||||
})
|
||||
return serverTools
|
||||
} catch (error) {
|
||||
Logger.error(`[MCP] Failed to list tools for server: ${server.name}`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||
const cachedListTools = withCache<[MCPServer], MCPTool[]>(
|
||||
this.listToolsImpl.bind(this),
|
||||
(server) => {
|
||||
const serverKey = this.getServerKey(server)
|
||||
return `mcp:list_tool:${serverKey}`
|
||||
},
|
||||
5 * 60 * 1000, // 5 minutes TTL
|
||||
`[MCP] Tools from ${server.name}`
|
||||
)
|
||||
|
||||
return cachedListTools(server)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -270,6 +315,76 @@ class McpService {
|
||||
return { dir, uvPath, bunPath }
|
||||
}
|
||||
|
||||
/**
|
||||
* List prompts available on an MCP server
|
||||
*/
|
||||
private async listPromptsImpl(server: MCPServer): Promise<MCPPrompt[]> {
|
||||
Logger.info(`[MCP] Listing prompts for server: ${server.name}`)
|
||||
const client = await this.initClient(server)
|
||||
try {
|
||||
const { prompts } = await client.listPrompts()
|
||||
const serverPrompts = prompts.map((prompt: any) => ({
|
||||
...prompt,
|
||||
id: `p${nanoid()}`,
|
||||
serverId: server.id,
|
||||
serverName: server.name
|
||||
}))
|
||||
return serverPrompts
|
||||
} catch (error) {
|
||||
Logger.error(`[MCP] Failed to list prompts for server: ${server.name}`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List prompts available on an MCP server with caching
|
||||
*/
|
||||
public async listPrompts(_: Electron.IpcMainInvokeEvent, server: MCPServer): Promise<MCPPrompt[]> {
|
||||
const cachedListPrompts = withCache<[MCPServer], MCPPrompt[]>(
|
||||
this.listPromptsImpl.bind(this),
|
||||
(server) => {
|
||||
const serverKey = this.getServerKey(server)
|
||||
return `mcp:list_prompts:${serverKey}`
|
||||
},
|
||||
60 * 60 * 1000, // 60 minutes TTL
|
||||
`[MCP] Prompts from ${server.name}`
|
||||
)
|
||||
return cachedListPrompts(server)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific prompt from an MCP server (implementation)
|
||||
*/
|
||||
private async getPromptImpl(
|
||||
server: MCPServer,
|
||||
name: string,
|
||||
args?: Record<string, any>
|
||||
): Promise<GetMCPPromptResponse> {
|
||||
Logger.info(`[MCP] Getting prompt ${name} from server: ${server.name}`)
|
||||
const client = await this.initClient(server)
|
||||
return await client.getPrompt({ name, arguments: args })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific prompt from an MCP server with caching
|
||||
*/
|
||||
public async getPrompt(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
{ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }
|
||||
): Promise<GetMCPPromptResponse> {
|
||||
const cachedGetPrompt = withCache<[MCPServer, string, Record<string, any> | undefined], GetMCPPromptResponse>(
|
||||
this.getPromptImpl.bind(this),
|
||||
(server, name, args) => {
|
||||
const serverKey = this.getServerKey(server)
|
||||
const argsKey = args ? JSON.stringify(args) : 'no-args'
|
||||
return `mcp:get_prompt:${serverKey}:${name}:${argsKey}`
|
||||
},
|
||||
30 * 60 * 1000, // 30 minutes TTL
|
||||
`[MCP] Prompt ${name} from ${server.name}`
|
||||
)
|
||||
return await cachedGetPrompt(server, name, args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enhanced PATH including common tool locations
|
||||
*/
|
||||
|
||||
10
src/preload/index.d.ts
vendored
10
src/preload/index.d.ts
vendored
@ -151,6 +151,16 @@ declare global {
|
||||
stopServer: (server: MCPServer) => Promise<void>
|
||||
listTools: (server: MCPServer) => Promise<MCPTool[]>
|
||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise<any>
|
||||
listPrompts: (server: MCPServer) => Promise<MCPPrompt[]>
|
||||
getPrompt: ({
|
||||
server,
|
||||
name,
|
||||
args
|
||||
}: {
|
||||
server: MCPServer
|
||||
name: string
|
||||
args?: Record<string, any>
|
||||
}) => Promise<GetMCPPromptResponse>
|
||||
getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }>
|
||||
}
|
||||
copilot: {
|
||||
|
||||
@ -135,8 +135,11 @@ const api = {
|
||||
restartServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RestartServer, server),
|
||||
stopServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_StopServer, server),
|
||||
listTools: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListTools, server),
|
||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) =>
|
||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }) =>
|
||||
ipcRenderer.invoke(IpcChannel.Mcp_CallTool, { server, name, args }),
|
||||
listPrompts: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListPrompts, server),
|
||||
getPrompt: ({ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }) =>
|
||||
ipcRenderer.invoke(IpcChannel.Mcp_GetPrompt, { server, name, args }),
|
||||
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo)
|
||||
},
|
||||
shell: {
|
||||
|
||||
1
src/renderer/src/assets/images/mcp/npm.svg
Normal file
1
src/renderer/src/assets/images/mcp/npm.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744456106953" class="icon" viewBox="0 0 2633 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1486" xmlns:xlink="http://www.w3.org/1999/xlink" width="514.2578125" height="200"><path d="M0 0v877.843607h731.440328v146.156393H1316.724263v-146.156393h1316.724262V0z m731.440328 731.196014h-146.156393V292.312786h-146.485574v438.883228H146.485574V146.238688h584.954754z m438.880656 0v146.567869h-292.405369V146.238688H1463.209837v585.037049H1170.320984z m1316.888853 0H2341.30033V292.312786h-146.56787v438.883228h-146.485574V292.312786h-145.909508v438.883228H1609.283935V146.238688h878.008197zM1170.238688 292.477377H1316.724263v292.644539h-146.485575z" fill="#CB3837" p-id="1487"></path></svg>
|
||||
|
After Width: | Height: | Size: 845 B |
@ -1,4 +1,5 @@
|
||||
import { Collapse } from 'antd'
|
||||
import { merge } from 'lodash'
|
||||
import { FC, memo } from 'react'
|
||||
|
||||
interface CustomCollapseProps {
|
||||
@ -9,6 +10,11 @@ interface CustomCollapseProps {
|
||||
defaultActiveKey?: string[]
|
||||
activeKey?: string[]
|
||||
collapsible?: 'header' | 'icon' | 'disabled'
|
||||
style?: React.CSSProperties
|
||||
styles?: {
|
||||
header?: React.CSSProperties
|
||||
body?: React.CSSProperties
|
||||
}
|
||||
}
|
||||
|
||||
const CustomCollapse: FC<CustomCollapseProps> = ({
|
||||
@ -18,14 +24,17 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
|
||||
destroyInactivePanel = false,
|
||||
defaultActiveKey = ['1'],
|
||||
activeKey,
|
||||
collapsible = undefined
|
||||
collapsible = undefined,
|
||||
style,
|
||||
styles
|
||||
}) => {
|
||||
const CollapseStyle = {
|
||||
const defaultCollapseStyle = {
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: '0.5px solid var(--color-border)'
|
||||
}
|
||||
const CollapseItemStyles = {
|
||||
|
||||
const defaultCollapseItemStyles = {
|
||||
header: {
|
||||
padding: '8px 16px',
|
||||
alignItems: 'center',
|
||||
@ -38,17 +47,21 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
|
||||
borderTop: '0.5px solid var(--color-border)'
|
||||
}
|
||||
}
|
||||
|
||||
const collapseStyle = merge({}, defaultCollapseStyle, style)
|
||||
const collapseItemStyles = merge({}, defaultCollapseItemStyles, styles)
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
bordered={false}
|
||||
style={CollapseStyle}
|
||||
style={collapseStyle}
|
||||
defaultActiveKey={defaultActiveKey}
|
||||
activeKey={activeKey}
|
||||
destroyInactivePanel={destroyInactivePanel}
|
||||
collapsible={collapsible}
|
||||
items={[
|
||||
{
|
||||
styles: CollapseItemStyles,
|
||||
styles: collapseItemStyles,
|
||||
key: '1',
|
||||
label,
|
||||
extra,
|
||||
|
||||
@ -456,8 +456,7 @@ const StyledMenu = styled(Menu)`
|
||||
/* Simple animation that changes background color when sticky */
|
||||
@keyframes background-change {
|
||||
to {
|
||||
background-color: var(--color-background-soft);
|
||||
opacity: 0.95;
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -567,12 +567,12 @@ const QuickPanelFooterTips = styled.div<{ $footerWidth: number }>`
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
gap: 16px;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const QuickPanelFooterTitle = styled.div`
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -635,7 +635,8 @@ const QuickPanelItemIcon = styled.span`
|
||||
|
||||
const QuickPanelItemLabel = styled.span`
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@ -26,3 +26,10 @@ export const useMCPServers = () => {
|
||||
updateMcpServers: (servers: MCPServer[]) => dispatch(setMCPServers(servers))
|
||||
}
|
||||
}
|
||||
|
||||
export const useMCPServer = (id: string) => {
|
||||
const { mcpServers } = useMCPServers()
|
||||
return {
|
||||
server: mcpServers.find((server) => server.id === id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -585,7 +585,8 @@
|
||||
"switch.disabled": "Please wait for the current reply to complete",
|
||||
"tools": {
|
||||
"completed": "Completed",
|
||||
"invoking": "Invoking"
|
||||
"invoking": "Invoking",
|
||||
"error": "Error occurred"
|
||||
},
|
||||
"topic.added": "New topic added",
|
||||
"upgrade.success.button": "Restart",
|
||||
@ -1092,7 +1093,6 @@
|
||||
"newServer": "MCP Server",
|
||||
"npx_list": {
|
||||
"actions": "Actions",
|
||||
"desc": "Search and add npm packages as MCP servers",
|
||||
"description": "Description",
|
||||
"no_packages": "No packages found",
|
||||
"npm": "NPM",
|
||||
@ -1101,7 +1101,6 @@
|
||||
"scope_required": "Please enter npm scope",
|
||||
"search": "Search",
|
||||
"search_error": "Search error",
|
||||
"title": "NPX Package List",
|
||||
"usage": "Usage",
|
||||
"version": "Version"
|
||||
},
|
||||
@ -1118,10 +1117,25 @@
|
||||
"url": "URL",
|
||||
"editMcpJson": "Edit MCP Configuration",
|
||||
"installHelp": "Get Installation Help",
|
||||
"tabs": {
|
||||
"general": "General",
|
||||
"tools": "Tools",
|
||||
"prompts": "Prompts",
|
||||
"resources": "Resources"
|
||||
},
|
||||
"tools": {
|
||||
"inputSchema": "Input Schema",
|
||||
"availableTools": "Available Tools",
|
||||
"noToolsAvailable": "No tools available"
|
||||
"noToolsAvailable": "No tools available",
|
||||
"loadError": "Get tools Error"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "Available Prompts",
|
||||
"noPromptsAvailable": "No prompts available",
|
||||
"arguments": "Arguments",
|
||||
"requiredField": "Required Field",
|
||||
"genericError": "Get prompt Error",
|
||||
"loadError": "Get prompts Error"
|
||||
},
|
||||
"deleteServer": "Delete Server",
|
||||
"deleteServerConfirm": "Are you sure you want to delete this server?",
|
||||
|
||||
@ -562,7 +562,8 @@
|
||||
"switch.disabled": "現在の応答が完了するまで切り替えを無効にします",
|
||||
"tools": {
|
||||
"completed": "完了",
|
||||
"invoking": "呼び出し中"
|
||||
"invoking": "呼び出し中",
|
||||
"error": "エラーが発生しました"
|
||||
},
|
||||
"topic.added": "新しいトピックが追加されました",
|
||||
"upgrade.success.button": "再起動",
|
||||
@ -1069,7 +1070,6 @@
|
||||
"newServer": "MCP サーバー",
|
||||
"npx_list": {
|
||||
"actions": "アクション",
|
||||
"desc": "npm パッケージを検索して MCP サーバーとして追加",
|
||||
"description": "説明",
|
||||
"no_packages": "パッケージが見つかりません",
|
||||
"npm": "NPM",
|
||||
@ -1078,7 +1078,6 @@
|
||||
"scope_required": "npm スコープを入力してください",
|
||||
"search": "検索",
|
||||
"search_error": "パッケージの検索に失敗しました",
|
||||
"title": "NPX パッケージリスト",
|
||||
"usage": "使用法",
|
||||
"version": "バージョン"
|
||||
},
|
||||
@ -1095,10 +1094,25 @@
|
||||
},
|
||||
"editMcpJson": "MCP 設定を編集",
|
||||
"installHelp": "インストールヘルプを取得",
|
||||
"tabs": {
|
||||
"general": "一般",
|
||||
"tools": "ツール",
|
||||
"prompts": "プロンプト",
|
||||
"resources": "リソース"
|
||||
},
|
||||
"tools": {
|
||||
"inputSchema": "入力スキーマ",
|
||||
"availableTools": "利用可能なツール",
|
||||
"noToolsAvailable": "利用可能なツールはありません"
|
||||
"noToolsAvailable": "利用可能なツールなし",
|
||||
"loadError": "ツール取得エラー"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "利用可能なプロンプト",
|
||||
"noPromptsAvailable": "利用可能なプロンプトはありません",
|
||||
"arguments": "引数",
|
||||
"requiredField": "必須フィールド",
|
||||
"genericError": "プロンプト取得エラー",
|
||||
"loadError": "プロンプト取得エラー"
|
||||
},
|
||||
"deleteServer": "サーバーを削除",
|
||||
"deleteServerConfirm": "このサーバーを削除してもよろしいですか?",
|
||||
|
||||
@ -563,7 +563,8 @@
|
||||
"switch.disabled": "Пожалуйста, дождитесь завершения текущего ответа",
|
||||
"tools": {
|
||||
"completed": "Завершено",
|
||||
"invoking": "Вызов"
|
||||
"invoking": "Вызов",
|
||||
"error": "Произошла ошибка"
|
||||
},
|
||||
"topic.added": "Новый топик добавлен",
|
||||
"upgrade.success.button": "Перезапустить",
|
||||
@ -1069,7 +1070,6 @@
|
||||
"newServer": "MCP сервер",
|
||||
"npx_list": {
|
||||
"actions": "Действия",
|
||||
"desc": "Поиск и добавление npm пакетов в качестве MCP серверов",
|
||||
"description": "Описание",
|
||||
"no_packages": "Ничего не найдено",
|
||||
"npm": "NPM",
|
||||
@ -1078,7 +1078,6 @@
|
||||
"scope_required": "Пожалуйста, введите область npm",
|
||||
"search": "Поиск",
|
||||
"search_error": "Ошибка поиска",
|
||||
"title": "Список пакетов NPX",
|
||||
"usage": "Использование",
|
||||
"version": "Версия"
|
||||
},
|
||||
@ -1095,10 +1094,25 @@
|
||||
"url": "URL",
|
||||
"editMcpJson": "Редактировать MCP",
|
||||
"installHelp": "Получить помощь по установке",
|
||||
"tabs": {
|
||||
"general": "Общие",
|
||||
"tools": "Инструменты",
|
||||
"prompts": "Подсказки",
|
||||
"resources": "Ресурсы"
|
||||
},
|
||||
"tools": {
|
||||
"inputSchema": "входные параметры",
|
||||
"availableTools": "доступные инструменты",
|
||||
"noToolsAvailable": "нет доступных инструментов"
|
||||
"inputSchema": "Схема ввода",
|
||||
"availableTools": "Доступные инструменты",
|
||||
"noToolsAvailable": "Нет доступных инструментов",
|
||||
"loadError": "Ошибка получения инструментов"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "Доступные подсказки",
|
||||
"noPromptsAvailable": "Нет доступных подсказок",
|
||||
"arguments": "Аргументы",
|
||||
"requiredField": "Обязательное поле",
|
||||
"genericError": "Ошибка получения подсказки",
|
||||
"loadError": "Ошибка получения подсказок"
|
||||
},
|
||||
"deleteServer": "Удалить сервер",
|
||||
"deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?",
|
||||
|
||||
@ -585,7 +585,8 @@
|
||||
"switch.disabled": "请等待当前回复完成后操作",
|
||||
"tools": {
|
||||
"completed": "已完成",
|
||||
"invoking": "调用中"
|
||||
"invoking": "调用中",
|
||||
"error": "发生错误"
|
||||
},
|
||||
"topic.added": "话题添加成功",
|
||||
"upgrade.success.button": "重启",
|
||||
@ -1092,7 +1093,6 @@
|
||||
"newServer": "MCP 服务器",
|
||||
"npx_list": {
|
||||
"actions": "操作",
|
||||
"desc": "搜索并添加 npm 包作为 MCP 服务",
|
||||
"description": "描述",
|
||||
"no_packages": "未找到包",
|
||||
"npm": "NPM",
|
||||
@ -1101,7 +1101,6 @@
|
||||
"scope_required": "请输入 npm 作用域",
|
||||
"search": "搜索",
|
||||
"search_error": "搜索失败",
|
||||
"title": "NPX 包列表",
|
||||
"usage": "用法",
|
||||
"version": "版本"
|
||||
},
|
||||
@ -1118,10 +1117,25 @@
|
||||
"url": "URL",
|
||||
"editMcpJson": "编辑 MCP 配置",
|
||||
"installHelp": "获取安装帮助",
|
||||
"tabs": {
|
||||
"general": "通用",
|
||||
"tools": "工具",
|
||||
"prompts": "提示",
|
||||
"resources": "资源"
|
||||
},
|
||||
"tools": {
|
||||
"inputSchema": "输入参数",
|
||||
"inputSchema": "输入模式",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "没有可用工具"
|
||||
"noToolsAvailable": "无可用工具",
|
||||
"loadError": "获取工具失败"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "可用提示",
|
||||
"noPromptsAvailable": "无可用提示",
|
||||
"arguments": "参数",
|
||||
"requiredField": "必填字段",
|
||||
"genericError": "获取提示错误",
|
||||
"loadError": "获取提示失败"
|
||||
},
|
||||
"deleteServer": "删除服务器",
|
||||
"deleteServerConfirm": "确定要删除此服务器吗?",
|
||||
|
||||
@ -563,7 +563,8 @@
|
||||
"switch.disabled": "請等待當前回覆完成",
|
||||
"tools": {
|
||||
"completed": "已完成",
|
||||
"invoking": "調用中"
|
||||
"invoking": "調用中",
|
||||
"error": "發生錯誤"
|
||||
},
|
||||
"topic.added": "新話題已新增",
|
||||
"upgrade.success.button": "重新啟動",
|
||||
@ -1069,7 +1070,6 @@
|
||||
"newServer": "MCP 伺服器",
|
||||
"npx_list": {
|
||||
"actions": "操作",
|
||||
"desc": "搜索並添加 npm 包作為 MCP 服務",
|
||||
"description": "描述",
|
||||
"no_packages": "未找到包",
|
||||
"npm": "NPM",
|
||||
@ -1078,7 +1078,6 @@
|
||||
"scope_required": "請輸入 npm 作用域",
|
||||
"search": "搜索",
|
||||
"search_error": "搜索失敗",
|
||||
"title": "NPX 包列表",
|
||||
"usage": "用法",
|
||||
"version": "版本"
|
||||
},
|
||||
@ -1095,10 +1094,25 @@
|
||||
"url": "URL",
|
||||
"editMcpJson": "編輯 MCP 配置",
|
||||
"installHelp": "獲取安裝幫助",
|
||||
"tabs": {
|
||||
"general": "通用",
|
||||
"tools": "工具",
|
||||
"prompts": "提示",
|
||||
"resources": "資源"
|
||||
},
|
||||
"tools": {
|
||||
"inputSchema": "輸入參數",
|
||||
"inputSchema": "輸入模式",
|
||||
"availableTools": "可用工具",
|
||||
"noToolsAvailable": "沒有可用工具"
|
||||
"noToolsAvailable": "無可用工具",
|
||||
"loadError": "獲取工具失敗"
|
||||
},
|
||||
"prompts": {
|
||||
"availablePrompts": "可用提示",
|
||||
"noPromptsAvailable": "無可用提示",
|
||||
"arguments": "參數",
|
||||
"requiredField": "必填欄位",
|
||||
"genericError": "獲取提示錯誤",
|
||||
"loadError": "獲取提示失敗"
|
||||
},
|
||||
"deleteServer": "刪除伺服器",
|
||||
"deleteServerConfirm": "確定要刪除此伺服器嗎?",
|
||||
|
||||
@ -912,7 +912,6 @@
|
||||
"noServers": "Δεν έχουν ρυθμιστεί διακομιστές",
|
||||
"npx_list": {
|
||||
"actions": "Ενέργειες",
|
||||
"desc": "Αναζητήστε και προσθέστε πακέτα npm ως υπηρεσίες MCP",
|
||||
"description": "Περιγραφή",
|
||||
"no_packages": "Δεν βρέθηκαν πακέτα",
|
||||
"npm": "NPM",
|
||||
@ -921,7 +920,6 @@
|
||||
"scope_required": "Παρακαλώ εισαγάγετε το σκοπό του npm",
|
||||
"search": "Αναζήτηση",
|
||||
"search_error": "Η αναζήτηση απέτυχε",
|
||||
"title": "Λίστα πακέτων NPX",
|
||||
"usage": "Χρήση",
|
||||
"version": "Έκδοση"
|
||||
},
|
||||
|
||||
@ -912,7 +912,6 @@
|
||||
"noServers": "No se han configurado servidores",
|
||||
"npx_list": {
|
||||
"actions": "Acciones",
|
||||
"desc": "Buscar y agregar paquetes npm como servicios MCP",
|
||||
"description": "Descripción",
|
||||
"no_packages": "No se encontraron paquetes",
|
||||
"npm": "NPM",
|
||||
@ -921,7 +920,6 @@
|
||||
"scope_required": "Por favor ingrese el ámbito npm",
|
||||
"search": "Buscar",
|
||||
"search_error": "Error de búsqueda",
|
||||
"title": "Lista de paquetes NPX",
|
||||
"usage": "Uso",
|
||||
"version": "Versión"
|
||||
},
|
||||
|
||||
@ -912,7 +912,6 @@
|
||||
"noServers": "Aucun serveur configuré",
|
||||
"npx_list": {
|
||||
"actions": "Actions",
|
||||
"desc": "Rechercher et ajouter un package npm en tant que service MCP",
|
||||
"description": "Description",
|
||||
"no_packages": "Aucun package trouvé",
|
||||
"npm": "NPM",
|
||||
@ -921,7 +920,6 @@
|
||||
"scope_required": "Veuillez entrer le scope npm",
|
||||
"search": "Rechercher",
|
||||
"search_error": "La recherche a échoué",
|
||||
"title": "Liste des packages NPX",
|
||||
"usage": "Utilisation",
|
||||
"version": "Version"
|
||||
},
|
||||
|
||||
@ -912,7 +912,6 @@
|
||||
"noServers": "Nenhum servidor configurado",
|
||||
"npx_list": {
|
||||
"actions": "Ações",
|
||||
"desc": "Pesquise e adicione pacotes npm como serviço MCP",
|
||||
"description": "Descrição",
|
||||
"no_packages": "Nenhum pacote encontrado",
|
||||
"npm": "NPM",
|
||||
@ -921,7 +920,6 @@
|
||||
"scope_required": "Insira o escopo npm",
|
||||
"search": "Pesquisar",
|
||||
"search_error": "Falha na pesquisa",
|
||||
"title": "Lista de Pacotes NPX",
|
||||
"usage": "Uso",
|
||||
"version": "Versão"
|
||||
},
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
HolderOutlined,
|
||||
PaperClipOutlined,
|
||||
PauseCircleOutlined,
|
||||
QuestionCircleOutlined,
|
||||
ThunderboltOutlined,
|
||||
TranslationOutlined
|
||||
} from '@ant-design/icons'
|
||||
@ -43,7 +42,7 @@ import { Assistant, FileType, KnowledgeBase, KnowledgeItem, MCPServer, Message,
|
||||
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
|
||||
import { getFilesFromDropEvent } from '@renderer/utils/input'
|
||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import dayjs from 'dayjs'
|
||||
import Logger from 'electron-log/renderer'
|
||||
@ -363,6 +362,15 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
mcpToolsButtonRef.current?.openQuickPanel()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'MCP Prompt',
|
||||
description: '',
|
||||
icon: <CodeOutlined />,
|
||||
isMenu: true,
|
||||
action: () => {
|
||||
mcpToolsButtonRef.current?.openPromptList()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'),
|
||||
description: '',
|
||||
@ -691,9 +699,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
textareaRef.current?.focus()
|
||||
})
|
||||
|
||||
useShortcut('clear_topic', () => {
|
||||
clearTopic()
|
||||
})
|
||||
useShortcut('clear_topic', clearTopic)
|
||||
|
||||
useEffect(() => {
|
||||
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
|
||||
@ -1094,6 +1100,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
enabledMCPs={enabledMCPs}
|
||||
toggelEnableMCP={toggelEnableMCP}
|
||||
ToolbarButton={ToolbarButton}
|
||||
setInputValue={setText}
|
||||
resizeTextArea={resizeTextArea}
|
||||
/>
|
||||
<GenerateImageButton
|
||||
model={model}
|
||||
@ -1114,17 +1122,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
ToolbarButton={ToolbarButton}
|
||||
/>
|
||||
<Tooltip placement="top" title={t('chat.input.clear', { Command: cleanTopicShortcut })} arrow>
|
||||
<Popconfirm
|
||||
title={t('chat.input.clear.content')}
|
||||
placement="top"
|
||||
onConfirm={clearTopic}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
okText={t('chat.input.clear.title')}>
|
||||
<ToolbarButton type="text">
|
||||
<ClearOutlined style={{ fontSize: 17 }} />
|
||||
</ToolbarButton>
|
||||
</Popconfirm>
|
||||
<ToolbarButton type="text" onClick={clearTopic}>
|
||||
<ClearOutlined style={{ fontSize: 17 }} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={isExpended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
||||
<ToolbarButton type="text" onClick={onToggleExpended}>
|
||||
|
||||
@ -1,28 +1,40 @@
|
||||
import { CodeOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { MCPPrompt, MCPServer } from '@renderer/types'
|
||||
import { Form, Input, Modal, Tooltip } from 'antd'
|
||||
import { FC, useCallback, useImperativeHandle, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
export interface MCPToolsButtonRef {
|
||||
openQuickPanel: () => void
|
||||
openPromptList: () => void
|
||||
}
|
||||
|
||||
interface Props {
|
||||
ref?: React.RefObject<MCPToolsButtonRef | null>
|
||||
enabledMCPs: MCPServer[]
|
||||
setInputValue: React.Dispatch<React.SetStateAction<string>>
|
||||
resizeTextArea: () => void
|
||||
toggelEnableMCP: (server: MCPServer) => void
|
||||
ToolbarButton: any
|
||||
}
|
||||
|
||||
const MCPToolsButton: FC<Props> = ({ ref, enabledMCPs, toggelEnableMCP, ToolbarButton }) => {
|
||||
const MCPToolsButton: FC<Props> = ({
|
||||
ref,
|
||||
setInputValue,
|
||||
resizeTextArea,
|
||||
enabledMCPs,
|
||||
toggelEnableMCP,
|
||||
ToolbarButton
|
||||
}) => {
|
||||
const { activedMcpServers } = useMCPServers()
|
||||
const { t } = useTranslation()
|
||||
const quickPanel = useQuickPanel()
|
||||
const navigate = useNavigate()
|
||||
// Create form instance at the top level
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const availableMCPs = activedMcpServers.filter((server) => enabledMCPs.some((s) => s.id === server.id))
|
||||
|
||||
@ -56,6 +68,220 @@ const MCPToolsButton: FC<Props> = ({ ref, enabledMCPs, toggelEnableMCP, ToolbarB
|
||||
}
|
||||
})
|
||||
}, [menuItems, quickPanel, t])
|
||||
// Extract and format all content from the prompt response
|
||||
const extractPromptContent = useCallback((response: any): string | null => {
|
||||
// Handle string response (backward compatibility)
|
||||
if (typeof response === 'string') {
|
||||
return response
|
||||
}
|
||||
|
||||
// Handle GetMCPPromptResponse format
|
||||
if (response && Array.isArray(response.messages)) {
|
||||
let formattedContent = ''
|
||||
|
||||
for (const message of response.messages) {
|
||||
if (!message.content) continue
|
||||
|
||||
// Add role prefix if available
|
||||
const rolePrefix = message.role ? `**${message.role.charAt(0).toUpperCase() + message.role.slice(1)}:** ` : ''
|
||||
|
||||
// Process different content types
|
||||
switch (message.content.type) {
|
||||
case 'text':
|
||||
// Add formatted text content with role
|
||||
formattedContent += `${rolePrefix}${message.content.text}\n\n`
|
||||
break
|
||||
|
||||
case 'image':
|
||||
// Format image as markdown with proper attribution
|
||||
if (message.content.data && message.content.mimeType) {
|
||||
const imageData = message.content.data
|
||||
const mimeType = message.content.mimeType
|
||||
// Include role if available
|
||||
if (rolePrefix) {
|
||||
formattedContent += `${rolePrefix}\n`
|
||||
}
|
||||
formattedContent += `\n\n`
|
||||
}
|
||||
break
|
||||
|
||||
case 'audio':
|
||||
// Add indicator for audio content with role
|
||||
formattedContent += `${rolePrefix}[Audio content available]\n\n`
|
||||
break
|
||||
|
||||
case 'resource':
|
||||
// Add indicator for resource content with role
|
||||
if (message.content.text) {
|
||||
formattedContent += `${rolePrefix}${message.content.text}\n\n`
|
||||
} else {
|
||||
formattedContent += `${rolePrefix}[Resource content available]\n\n`
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
// Add text content if available with role, otherwise show placeholder
|
||||
if (message.content.text) {
|
||||
formattedContent += `${rolePrefix}${message.content.text}\n\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return formattedContent.trim()
|
||||
}
|
||||
|
||||
// Fallback handling for single message format
|
||||
if (response && response.messages && response.messages.length > 0) {
|
||||
const message = response.messages[0]
|
||||
if (message.content && message.content.text) {
|
||||
const rolePrefix = message.role ? `**${message.role.charAt(0).toUpperCase() + message.role.slice(1)}:** ` : ''
|
||||
return `${rolePrefix}${message.content.text}`
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}, [])
|
||||
|
||||
// Helper function to insert prompt into text area
|
||||
const insertPromptIntoTextArea = useCallback(
|
||||
(promptText: string) => {
|
||||
setInputValue((prev) => {
|
||||
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement
|
||||
if (!textArea) return prev + promptText // Fallback if we can't find the textarea
|
||||
|
||||
const cursorPosition = textArea.selectionStart
|
||||
const selectionStart = cursorPosition
|
||||
const selectionEndPosition = cursorPosition + promptText.length
|
||||
const newText = prev.slice(0, cursorPosition) + promptText + prev.slice(cursorPosition)
|
||||
|
||||
setTimeout(() => {
|
||||
textArea.focus()
|
||||
textArea.setSelectionRange(selectionStart, selectionEndPosition)
|
||||
resizeTextArea()
|
||||
}, 10)
|
||||
return newText
|
||||
})
|
||||
},
|
||||
[setInputValue, resizeTextArea]
|
||||
)
|
||||
|
||||
const handlePromptSelect = useCallback(
|
||||
(prompt: MCPPrompt) => {
|
||||
setTimeout(async () => {
|
||||
const server = enabledMCPs.find((s) => s.id === prompt.serverId)
|
||||
if (server) {
|
||||
try {
|
||||
// Check if the prompt has arguments
|
||||
if (prompt.arguments && prompt.arguments.length > 0) {
|
||||
// Reset form when opening a new modal
|
||||
form.resetFields()
|
||||
|
||||
Modal.confirm({
|
||||
title: `${t('settings.mcp.prompts.arguments')}: ${prompt.name}`,
|
||||
content: (
|
||||
<Form form={form} layout="vertical">
|
||||
{prompt.arguments.map((arg, index) => (
|
||||
<Form.Item
|
||||
key={index}
|
||||
name={arg.name}
|
||||
label={`${arg.name}${arg.required ? ' *' : ''}`}
|
||||
tooltip={arg.description}
|
||||
rules={
|
||||
arg.required ? [{ required: true, message: t('settings.mcp.prompts.requiredField') }] : []
|
||||
}>
|
||||
<Input placeholder={arg.description || arg.name} />
|
||||
</Form.Item>
|
||||
))}
|
||||
</Form>
|
||||
),
|
||||
onOk: async () => {
|
||||
try {
|
||||
// Validate and get form values
|
||||
const values = await form.validateFields()
|
||||
|
||||
const response = await window.api.mcp.getPrompt({
|
||||
server,
|
||||
name: prompt.name,
|
||||
args: values
|
||||
})
|
||||
|
||||
// Extract and format prompt content from the response
|
||||
const promptContent = extractPromptContent(response)
|
||||
if (promptContent) {
|
||||
insertPromptIntoTextArea(promptContent)
|
||||
} else {
|
||||
throw new Error('Invalid prompt response format')
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
} catch (error: Error | any) {
|
||||
if (error.errorFields) {
|
||||
// This is a form validation error, handled by Ant Design
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
Modal.error({
|
||||
title: t('common.error'),
|
||||
content: error.message || t('settings.mcp.prompts.genericError')
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
},
|
||||
okText: t('common.confirm'),
|
||||
cancelText: t('common.cancel')
|
||||
})
|
||||
} else {
|
||||
// If no arguments, get the prompt directly
|
||||
const response = await window.api.mcp.getPrompt({
|
||||
server,
|
||||
name: prompt.name
|
||||
})
|
||||
|
||||
// Extract and format prompt content from the response
|
||||
const promptContent = extractPromptContent(response)
|
||||
if (promptContent) {
|
||||
insertPromptIntoTextArea(promptContent)
|
||||
} else {
|
||||
throw new Error('Invalid prompt response format')
|
||||
}
|
||||
}
|
||||
} catch (error: Error | any) {
|
||||
Modal.error({
|
||||
title: t('common.error'),
|
||||
content: error.message || t('settings.mcp.prompt.genericError')
|
||||
})
|
||||
}
|
||||
}
|
||||
}, 10)
|
||||
},
|
||||
[enabledMCPs, form, t, extractPromptContent, insertPromptIntoTextArea] // Add form to dependencies
|
||||
)
|
||||
|
||||
const promptList = useMemo(async () => {
|
||||
const prompts: MCPPrompt[] = []
|
||||
|
||||
for (const server of enabledMCPs) {
|
||||
const serverPrompts = await window.api.mcp.listPrompts(server)
|
||||
prompts.push(...serverPrompts)
|
||||
}
|
||||
|
||||
return prompts.map((prompt) => ({
|
||||
label: prompt.name,
|
||||
description: prompt.description,
|
||||
icon: <CodeOutlined />,
|
||||
action: () => handlePromptSelect(prompt)
|
||||
}))
|
||||
}, [handlePromptSelect, enabledMCPs])
|
||||
|
||||
const openPromptList = useCallback(async () => {
|
||||
const prompts = await promptList
|
||||
quickPanel.open({
|
||||
title: t('settings.mcp.title'),
|
||||
list: prompts,
|
||||
symbol: 'mcp-prompt',
|
||||
multiple: true
|
||||
})
|
||||
}, [promptList, quickPanel, t])
|
||||
|
||||
const handleOpenQuickPanel = useCallback(() => {
|
||||
if (quickPanel.isVisible && quickPanel.symbol === 'mcp') {
|
||||
@ -66,7 +292,8 @@ const MCPToolsButton: FC<Props> = ({ ref, enabledMCPs, toggelEnableMCP, ToolbarB
|
||||
}, [openQuickPanel, quickPanel])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
openQuickPanel
|
||||
openQuickPanel,
|
||||
openPromptList
|
||||
}))
|
||||
|
||||
if (activedMcpServers.length === 0) {
|
||||
|
||||
@ -3,6 +3,7 @@ import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
import { Tooltip } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
onNewContext: () => void
|
||||
@ -16,12 +17,20 @@ const NewContextButton: FC<Props> = ({ onNewContext, ToolbarButton }) => {
|
||||
useShortcut('toggle_new_context', onNewContext)
|
||||
|
||||
return (
|
||||
<Tooltip placement="top" title={t('chat.input.new.context', { Command: newContextShortcut })} arrow>
|
||||
<ToolbarButton type="text" onClick={onNewContext}>
|
||||
<PicCenterOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Container>
|
||||
<Tooltip placement="top" title={t('chat.input.new.context', { Command: newContextShortcut })} arrow>
|
||||
<ToolbarButton type="text" onClick={onNewContext}>
|
||||
<PicCenterOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
@media (max-width: 800px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default NewContextButton
|
||||
|
||||
@ -255,15 +255,19 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
if (now - lastMoveTime.current < 50) return
|
||||
lastMoveTime.current = now
|
||||
|
||||
const triggerWidth = 10
|
||||
let rightOffset = 5
|
||||
// Calculate if the mouse is in the trigger area
|
||||
const triggerWidth = 80 // Same as the width in styled component
|
||||
|
||||
// Safe way to calculate position when using calc expressions
|
||||
let rightOffset = 16 // Default right offset
|
||||
if (showRightTopics) {
|
||||
rightOffset = 5 + 300
|
||||
// When topics are shown on right, we need to account for topic list width
|
||||
rightOffset = 16 + 300 // Assuming topic list width is 300px, adjust if different
|
||||
}
|
||||
|
||||
const rightPosition = window.innerWidth - rightOffset - triggerWidth
|
||||
const topPosition = window.innerHeight * 0.35
|
||||
const height = window.innerHeight * 0.3
|
||||
const topPosition = window.innerHeight * 0.3 // 30% from top
|
||||
const height = window.innerHeight * 0.4 // 40% of window height
|
||||
|
||||
const isInTriggerArea =
|
||||
e.clientX > rightPosition &&
|
||||
@ -403,32 +407,31 @@ const ButtonGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--color-border);
|
||||
`
|
||||
|
||||
const NavigationButton = styled(Button)`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
color: var(--color-text);
|
||||
transition: all 0.25s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-hover);
|
||||
color: var(--color-primary);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { CheckOutlined, ExpandOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import { CheckOutlined, ExpandOutlined, LoadingOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Message } from '@renderer/types'
|
||||
import { Collapse, message as antdMessage, Modal, Tooltip } from 'antd'
|
||||
@ -48,6 +48,7 @@ const MessageTools: FC<Props> = ({ message }) => {
|
||||
const { id, tool, status, response } = toolResponse
|
||||
const isInvoking = status === 'invoking'
|
||||
const isDone = status === 'done'
|
||||
const hasError = isDone && response?.isError === true
|
||||
const result = {
|
||||
params: tool.inputSchema,
|
||||
response: toolResponse.response
|
||||
@ -59,10 +60,15 @@ const MessageTools: FC<Props> = ({ message }) => {
|
||||
<MessageTitleLabel>
|
||||
<TitleContent>
|
||||
<ToolName>{tool.name}</ToolName>
|
||||
<StatusIndicator $isInvoking={isInvoking}>
|
||||
{isInvoking ? t('message.tools.invoking') : t('message.tools.completed')}
|
||||
<StatusIndicator $isInvoking={isInvoking} $hasError={hasError}>
|
||||
{isInvoking
|
||||
? t('message.tools.invoking')
|
||||
: hasError
|
||||
? t('message.tools.error')
|
||||
: t('message.tools.completed')}
|
||||
{isInvoking && <LoadingOutlined spin style={{ marginLeft: 6 }} />}
|
||||
{isDone && <CheckOutlined style={{ marginLeft: 6 }} />}
|
||||
{isDone && !hasError && <CheckOutlined style={{ marginLeft: 6 }} />}
|
||||
{hasError && <WarningOutlined style={{ marginLeft: 6 }} />}
|
||||
</StatusIndicator>
|
||||
</TitleContent>
|
||||
<ActionButtonsContainer>
|
||||
@ -195,8 +201,12 @@ const ToolName = styled.span`
|
||||
font-size: 13px;
|
||||
`
|
||||
|
||||
const StatusIndicator = styled.span<{ $isInvoking: boolean }>`
|
||||
color: ${(props) => (props.$isInvoking ? 'var(--color-primary)' : 'var(--color-success, #52c41a)')};
|
||||
const StatusIndicator = styled.span<{ $isInvoking: boolean; $hasError?: boolean }>`
|
||||
color: ${(props) => {
|
||||
if (props.$hasError) return 'var(--color-error, #ff4d4f)'
|
||||
if (props.$isInvoking) return 'var(--color-primary)'
|
||||
return 'var(--color-success, #52c41a)'
|
||||
}};
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -39,42 +39,6 @@ interface MessagesProps {
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
const computeDisplayMessages = (messages: Message[], startIndex: number, displayCount: number) => {
|
||||
const reversedMessages = [...messages].reverse()
|
||||
|
||||
// 如果剩余消息数量小于 displayCount,直接返回所有剩余消息
|
||||
if (reversedMessages.length - startIndex <= displayCount) {
|
||||
return reversedMessages.slice(startIndex)
|
||||
}
|
||||
|
||||
const userIdSet = new Set() // 用户消息 id 集合
|
||||
const assistantIdSet = new Set() // 助手消息 askId 集合
|
||||
const displayMessages: Message[] = []
|
||||
|
||||
// 处理单条消息的函数
|
||||
const processMessage = (message: Message) => {
|
||||
if (!message) return
|
||||
|
||||
const idSet = message.role === 'user' ? userIdSet : assistantIdSet
|
||||
const messageId = message.role === 'user' ? message.id : message.askId
|
||||
|
||||
if (!idSet.has(messageId)) {
|
||||
idSet.add(messageId)
|
||||
displayMessages.push(message)
|
||||
return
|
||||
}
|
||||
// 如果是相同 askId 的助手消息,也要显示
|
||||
displayMessages.push(message)
|
||||
}
|
||||
|
||||
// 遍历消息直到满足显示数量要求
|
||||
for (let i = startIndex; i < reversedMessages.length && userIdSet.size + assistantIdSet.size < displayCount; i++) {
|
||||
processMessage(reversedMessages[i])
|
||||
}
|
||||
|
||||
return displayMessages
|
||||
}
|
||||
|
||||
const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic }) => {
|
||||
const { t } = useTranslation()
|
||||
const { showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
|
||||
@ -119,24 +83,36 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
}
|
||||
}, [])
|
||||
|
||||
const clearTopic = useCallback(
|
||||
async (data: Topic) => {
|
||||
const defaultTopic = getDefaultTopic(assistant.id)
|
||||
|
||||
if (data && data.id !== topic.id) {
|
||||
await clearTopicMessages(data.id)
|
||||
updateTopic({ ...data, name: defaultTopic.name } as Topic)
|
||||
return
|
||||
}
|
||||
|
||||
await clearTopicMessages()
|
||||
|
||||
setDisplayMessages([])
|
||||
|
||||
const _topic = getTopic(assistant, topic.id)
|
||||
_topic && updateTopic({ ..._topic, name: defaultTopic.name } as Topic)
|
||||
},
|
||||
[assistant, clearTopicMessages, topic.id, updateTopic]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, scrollToBottom),
|
||||
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, async (data: Topic) => {
|
||||
const defaultTopic = getDefaultTopic(assistant.id)
|
||||
|
||||
if (data && data.id !== topic.id) {
|
||||
await clearTopicMessages(data.id)
|
||||
updateTopic({ ...data, name: defaultTopic.name } as Topic)
|
||||
return
|
||||
}
|
||||
|
||||
await clearTopicMessages()
|
||||
setDisplayMessages([])
|
||||
const _topic = getTopic(assistant, topic.id)
|
||||
if (_topic) {
|
||||
updateTopic({ ..._topic, name: defaultTopic.name } as Topic)
|
||||
}
|
||||
window.modal.confirm({
|
||||
title: t('chat.input.clear.title'),
|
||||
content: t('chat.input.clear.content'),
|
||||
centered: true,
|
||||
onOk: () => clearTopic(data)
|
||||
})
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.COPY_TOPIC_IMAGE, async () => {
|
||||
await captureScrollableDivAsBlob(containerRef, async (blob) => {
|
||||
@ -282,11 +258,43 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
)
|
||||
}
|
||||
|
||||
interface LoaderProps {
|
||||
$loading: boolean
|
||||
const computeDisplayMessages = (messages: Message[], startIndex: number, displayCount: number) => {
|
||||
const reversedMessages = [...messages].reverse()
|
||||
|
||||
// 如果剩余消息数量小于 displayCount,直接返回所有剩余消息
|
||||
if (reversedMessages.length - startIndex <= displayCount) {
|
||||
return reversedMessages.slice(startIndex)
|
||||
}
|
||||
|
||||
const userIdSet = new Set() // 用户消息 id 集合
|
||||
const assistantIdSet = new Set() // 助手消息 askId 集合
|
||||
const displayMessages: Message[] = []
|
||||
|
||||
// 处理单条消息的函数
|
||||
const processMessage = (message: Message) => {
|
||||
if (!message) return
|
||||
|
||||
const idSet = message.role === 'user' ? userIdSet : assistantIdSet
|
||||
const messageId = message.role === 'user' ? message.id : message.askId
|
||||
|
||||
if (!idSet.has(messageId)) {
|
||||
idSet.add(messageId)
|
||||
displayMessages.push(message)
|
||||
return
|
||||
}
|
||||
// 如果是相同 askId 的助手消息,也要显示
|
||||
displayMessages.push(message)
|
||||
}
|
||||
|
||||
// 遍历消息直到满足显示数量要求
|
||||
for (let i = startIndex; i < reversedMessages.length && userIdSet.size + assistantIdSet.size < displayCount; i++) {
|
||||
processMessage(reversedMessages[i])
|
||||
}
|
||||
|
||||
return displayMessages
|
||||
}
|
||||
|
||||
const LoaderContainer = styled.div<LoaderProps>`
|
||||
const LoaderContainer = styled.div<{ $loading: boolean }>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
@ -300,6 +308,7 @@ const LoaderContainer = styled.div<LoaderProps>`
|
||||
const ScrollContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
margin-bottom: -20px; // 添加负的底部外边距来减少空间
|
||||
`
|
||||
|
||||
interface ContainerProps {
|
||||
@ -309,7 +318,7 @@ interface ContainerProps {
|
||||
const Container = styled(Scrollbar)<ContainerProps>`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 10px 0 20px;
|
||||
padding: 10px 0 10px;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--color-background);
|
||||
z-index: 1;
|
||||
|
||||
@ -31,6 +31,8 @@ const Container = styled.div`
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
margin-top: -10px;
|
||||
padding: 0;
|
||||
min-height: auto;
|
||||
`
|
||||
|
||||
const Button = styled(AntdButton)<{ $theme: ThemeMode }>`
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
import { Center, VStack } from '@renderer/components/Layout'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { Alert, Button } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingDescription, SettingRow, SettingSubtitle } from '..'
|
||||
@ -21,6 +21,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
const [bunPath, setBunPath] = useState<string | null>(null)
|
||||
const [binariesDir, setBinariesDir] = useState<string | null>(null)
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const checkBinaries = async () => {
|
||||
const uvExists = await window.api.isBinaryExist('uv')
|
||||
@ -78,7 +79,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
||||
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}
|
||||
className="nodrag"
|
||||
color={installed ? 'green' : 'danger'}
|
||||
onClick={() => EventEmitter.emit('mcp:mcp-install')}
|
||||
onClick={() => navigate('/settings/mcp/mcp-install')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
96
src/renderer/src/pages/settings/MCPSettings/McpPrompt.tsx
Normal file
96
src/renderer/src/pages/settings/MCPSettings/McpPrompt.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { MCPPrompt } from '@renderer/types'
|
||||
import { Collapse, Descriptions, Empty, Flex, Tag, Tooltip, Typography } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface MCPPromptsSectionProps {
|
||||
prompts: MCPPrompt[]
|
||||
}
|
||||
|
||||
const MCPPromptsSection = ({ prompts }: MCPPromptsSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Render prompt arguments
|
||||
const renderPromptArguments = (prompt: MCPPrompt) => {
|
||||
if (!prompt.arguments || prompt.arguments.length === 0) return null
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Typography.Title level={5}>{t('settings.mcp.tools.inputSchema')}:</Typography.Title>
|
||||
<Descriptions bordered size="small" column={1} style={{ marginTop: 8 }}>
|
||||
{prompt.arguments.map((arg, index) => (
|
||||
<Descriptions.Item
|
||||
key={index}
|
||||
label={
|
||||
<Flex align="center" gap={8}>
|
||||
<Typography.Text strong>{arg.name}</Typography.Text>
|
||||
{arg.required && (
|
||||
<Tooltip title="Required field">
|
||||
<Tag color="red">Required</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
}>
|
||||
<Flex vertical gap={4}>
|
||||
{arg.description && (
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
|
||||
{arg.description}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
</Flex>
|
||||
</Descriptions.Item>
|
||||
))}
|
||||
</Descriptions>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<SectionTitle>{t('settings.mcp.prompts.availablePrompts')}</SectionTitle>
|
||||
{prompts.length > 0 ? (
|
||||
<Collapse bordered={false} ghost>
|
||||
{prompts.map((prompt) => (
|
||||
<Collapse.Panel
|
||||
key={prompt.id || prompt.name}
|
||||
header={
|
||||
<Flex vertical align="flex-start">
|
||||
<Flex align="center" style={{ width: '100%' }}>
|
||||
<Typography.Text strong>{prompt.name}</Typography.Text>
|
||||
</Flex>
|
||||
{prompt.description && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
|
||||
{prompt.description}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Flex>
|
||||
}>
|
||||
<SelectableContent>{renderPromptArguments(prompt)}</SelectableContent>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
) : (
|
||||
<Empty description={t('settings.mcp.prompts.noPromptsAvailable')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
const Section = styled.div`
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
`
|
||||
|
||||
const SectionTitle = styled.h3`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: var(--color-text-secondary);
|
||||
`
|
||||
|
||||
const SelectableContent = styled.div`
|
||||
user-select: text;
|
||||
padding: 0 12px;
|
||||
`
|
||||
|
||||
export default MCPPromptsSection
|
||||
@ -1,13 +1,15 @@
|
||||
import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { MCPServer, MCPTool } from '@renderer/types'
|
||||
import { Button, Flex, Form, Input, Radio, Switch } from 'antd'
|
||||
import { MCPPrompt, MCPServer, MCPTool } from '@renderer/types'
|
||||
import { Button, Flex, Form, Input, Radio, Switch, Tabs } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
|
||||
import MCPPromptsSection from './McpPrompt'
|
||||
import MCPToolsSection from './McpTool'
|
||||
|
||||
interface Props {
|
||||
@ -40,6 +42,8 @@ const PipRegistry: Registry[] = [
|
||||
{ name: '腾讯云', url: 'https://mirrors.cloud.tencent.com/pypi/simple/' }
|
||||
]
|
||||
|
||||
type TabKey = 'settings' | 'tools' | 'prompts'
|
||||
|
||||
const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
const { t } = useTranslation()
|
||||
const { deleteMCPServer, updateMCPServer } = useMCPServers()
|
||||
@ -48,11 +52,15 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isFormChanged, setIsFormChanged] = useState(false)
|
||||
const [loadingServer, setLoadingServer] = useState<string | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<TabKey>('settings')
|
||||
|
||||
const [tools, setTools] = useState<MCPTool[]>([])
|
||||
const [prompts, setPrompts] = useState<MCPPrompt[]>([])
|
||||
const [isShowRegistry, setIsShowRegistry] = useState(false)
|
||||
const [registry, setRegistry] = useState<Registry[]>()
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
const serverType: MCPServer['type'] = server.type || (server.baseUrl ? 'sse' : 'stdio')
|
||||
setServerType(serverType)
|
||||
@ -109,10 +117,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
setLoadingServer(server.id)
|
||||
const localTools = await window.api.mcp.listTools(server)
|
||||
setTools(localTools)
|
||||
// window.message.success(t('settings.mcp.toolsLoaded'))
|
||||
} catch (error) {
|
||||
window.message.error({
|
||||
content: t('settings.mcp.toolsLoadError') + formatError(error),
|
||||
content: t('settings.mcp.tools.loadError') + ' ' + formatError(error),
|
||||
key: 'mcp-tools-error'
|
||||
})
|
||||
} finally {
|
||||
@ -121,9 +128,28 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPrompts = async () => {
|
||||
if (server.isActive) {
|
||||
try {
|
||||
setLoadingServer(server.id)
|
||||
const localPrompts = await window.api.mcp.listPrompts(server)
|
||||
setPrompts(localPrompts)
|
||||
} catch (error) {
|
||||
window.message.error({
|
||||
content: t('settings.mcp.prompts.loadError') + ' ' + formatError(error),
|
||||
key: 'mcp-prompts-error'
|
||||
})
|
||||
setPrompts([])
|
||||
} finally {
|
||||
setLoadingServer(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (server.isActive) {
|
||||
fetchTools()
|
||||
fetchPrompts()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [server.id, server.isActive])
|
||||
@ -234,6 +260,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
await window.api.mcp.removeServer(server)
|
||||
deleteMCPServer(server.id)
|
||||
window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
|
||||
navigate('/settings/mcp')
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
@ -264,6 +291,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
if (active) {
|
||||
const localTools = await window.api.mcp.listTools(server)
|
||||
setTools(localTools)
|
||||
|
||||
const localPrompts = await window.api.mcp.listPrompts(server)
|
||||
setPrompts(localPrompts)
|
||||
} else {
|
||||
await window.api.mcp.stopServer(server)
|
||||
}
|
||||
@ -309,35 +339,16 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
[server, updateMCPServer]
|
||||
)
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<SettingGroup style={{ marginBottom: 0 }}>
|
||||
<SettingTitle>
|
||||
<Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}>
|
||||
<ServerName className="text-nowrap">{server?.name}</ServerName>
|
||||
{!(server.type === 'inMemory') && (
|
||||
<Button danger icon={<DeleteOutlined />} type="text" onClick={() => onDeleteMcpServer(server)} />
|
||||
)}
|
||||
</Flex>
|
||||
<Flex align="center" gap={16}>
|
||||
<Switch
|
||||
value={server.isActive}
|
||||
key={server.id}
|
||||
loading={loadingServer === server.id}
|
||||
onChange={onToggleActive}
|
||||
/>
|
||||
<Button type="primary" icon={<SaveOutlined />} onClick={onSave} loading={loading} disabled={!isFormChanged}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
const tabs = [
|
||||
{
|
||||
key: 'settings',
|
||||
label: t('settings.mcp.tabs.general'),
|
||||
children: (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onValuesChange={() => setIsFormChanged(true)}
|
||||
style={{
|
||||
// height: 'calc(100vh - var(--navbar-height) - 315px)',
|
||||
overflowY: 'auto',
|
||||
width: 'calc(100% + 10px)',
|
||||
paddingRight: '10px'
|
||||
@ -440,7 +451,58 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
{server.isActive && <MCPToolsSection tools={tools} server={server} onToggleTool={handleToggleTool} />}
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
if (server.isActive) {
|
||||
tabs.push(
|
||||
{
|
||||
key: 'tools',
|
||||
label: t('settings.mcp.tabs.tools'),
|
||||
children: <MCPToolsSection tools={tools} server={server} onToggleTool={handleToggleTool} />
|
||||
},
|
||||
{
|
||||
key: 'prompts',
|
||||
label: t('settings.mcp.tabs.prompts'),
|
||||
children: <MCPPromptsSection prompts={prompts} />
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<SettingGroup style={{ marginBottom: 0 }}>
|
||||
<SettingTitle>
|
||||
<Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}>
|
||||
<ServerName className="text-nowrap">{server?.name}</ServerName>
|
||||
<Button danger icon={<DeleteOutlined />} type="text" onClick={() => onDeleteMcpServer(server)} />
|
||||
</Flex>
|
||||
<Flex align="center" gap={16}>
|
||||
<Switch
|
||||
value={server.isActive}
|
||||
key={server.id}
|
||||
loading={loadingServer === server.id}
|
||||
onChange={onToggleActive}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={onSave}
|
||||
loading={loading}
|
||||
disabled={!isFormChanged || activeTab !== 'settings'}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
|
||||
<Tabs
|
||||
defaultActiveKey="settings"
|
||||
items={tabs}
|
||||
onChange={(key) => setActiveTab(key as TabKey)}
|
||||
style={{ marginTop: 8 }}
|
||||
/>
|
||||
</SettingGroup>
|
||||
</SettingContainer>
|
||||
)
|
||||
|
||||
@ -2,15 +2,16 @@ import { EditOutlined, ExportOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { isWindows } from '@renderer/config/constant'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { Button } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
|
||||
import EditMcpJsonPopup from './EditMcpJsonPopup'
|
||||
import InstallNpxUv from './InstallNpxUv'
|
||||
|
||||
export const McpSettingsNavbar = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const onClick = () => window.open('https://mcp.so/', '_blank')
|
||||
|
||||
return (
|
||||
@ -19,7 +20,7 @@ export const McpSettingsNavbar = () => {
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={() => EventEmitter.emit('mcp:npx-search')}
|
||||
onClick={() => navigate('/settings/mcp/npx-search')}
|
||||
icon={<SearchOutlined />}
|
||||
className="nodrag"
|
||||
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
|
||||
|
||||
@ -112,7 +112,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
|
||||
</Flex>
|
||||
{tool.description && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
|
||||
{tool.description}
|
||||
{tool.description.length > 100 ? `${tool.description.substring(0, 100)}...` : tool.description}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Flex>
|
||||
@ -138,7 +138,6 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
|
||||
|
||||
const Section = styled.div`
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: 8px;
|
||||
`
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { PlusOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { CheckOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import npmLogo from '@renderer/assets/images/mcp/npm.svg'
|
||||
import { Center, HStack } from '@renderer/components/Layout'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { builtinMCPServers } from '@renderer/store/mcp'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
@ -9,9 +9,7 @@ import { Button, Card, Flex, Input, Space, Spin, Tag, Typography } from 'antd'
|
||||
import { npxFinder } from 'npx-scope-finder'
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingTitle } from '..'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface SearchResult {
|
||||
name: string
|
||||
@ -27,8 +25,9 @@ const npmScopes = ['@cherry', '@modelcontextprotocol', '@gongrzhe', '@mcpmarket'
|
||||
|
||||
let _searchResults: SearchResult[] = []
|
||||
|
||||
const NpxSearch: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
const NpxSearch: FC<{
|
||||
setSelectedMcpServer: (server: MCPServer) => void
|
||||
}> = ({ setSelectedMcpServer }) => {
|
||||
const { t } = useTranslation()
|
||||
const { Text, Link } = Typography
|
||||
|
||||
@ -36,7 +35,7 @@ const NpxSearch: FC = () => {
|
||||
const [npmScope, setNpmScope] = useState('@cherry')
|
||||
const [searchLoading, setSearchLoading] = useState(false)
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>(_searchResults)
|
||||
const { addMCPServer } = useMCPServers()
|
||||
const { addMCPServer, mcpServers } = useMCPServers()
|
||||
|
||||
_searchResults = searchResults
|
||||
|
||||
@ -116,119 +115,134 @@ const NpxSearch: FC = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme} css={SettingGroupCss}>
|
||||
<div>
|
||||
<SettingTitle>
|
||||
{t('settings.mcp.npx_list.title')} <Text type="secondary">{t('settings.mcp.npx_list.desc')}</Text>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space.Compact style={{ width: '100%', marginBottom: 10 }}>
|
||||
<Container>
|
||||
<Center>
|
||||
<Space direction="vertical" style={{ marginBottom: 20, width: 500 }}>
|
||||
<Center style={{ marginBottom: 20 }}>
|
||||
<img src={npmLogo} alt="npm" width={100} />
|
||||
</Center>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input
|
||||
placeholder={t('settings.mcp.npx_list.scope_placeholder')}
|
||||
value={npmScope}
|
||||
onChange={(e) => setNpmScope(e.target.value)}
|
||||
onPressEnter={() => handleNpmSearch(npmScope)}
|
||||
size="large"
|
||||
styles={{ input: { borderRadius: 100 } }}
|
||||
/>
|
||||
<Button icon={<SearchOutlined />} onClick={() => handleNpmSearch(npmScope)} disabled={searchLoading}>
|
||||
{t('settings.mcp.npx_list.search')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<HStack alignItems="center" mt="-5px" mb="5px">
|
||||
<HStack alignItems="center" justifyContent="center">
|
||||
{npmScopes.map((scope) => (
|
||||
<Tag
|
||||
key={scope}
|
||||
bordered={false}
|
||||
onClick={() => {
|
||||
setNpmScope(scope)
|
||||
handleNpmSearch(scope)
|
||||
}}
|
||||
style={{ cursor: searchLoading ? 'not-allowed' : 'pointer' }}>
|
||||
style={{
|
||||
cursor: searchLoading ? 'not-allowed' : 'pointer',
|
||||
borderRadius: 100,
|
||||
backgroundColor: 'var(--color-background-mute)'
|
||||
}}>
|
||||
{scope}
|
||||
</Tag>
|
||||
))}
|
||||
</HStack>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<ResultList>
|
||||
{searchLoading ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
searchResults?.map((record) => (
|
||||
<Card
|
||||
size="small"
|
||||
key={record.name}
|
||||
title={
|
||||
<Typography.Title level={5} style={{ margin: 0 }} className="selectable">
|
||||
{record.name}
|
||||
</Typography.Title>
|
||||
}
|
||||
extra={
|
||||
<Flex>
|
||||
<Tag bordered={false} color="processing">
|
||||
v{record.version}
|
||||
</Tag>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const buildInServer = builtinMCPServers.find((server) => server.name === record.name)
|
||||
|
||||
if (buildInServer) {
|
||||
addMCPServer(buildInServer)
|
||||
return
|
||||
</Center>
|
||||
{searchLoading && (
|
||||
<Center>
|
||||
<Spin />
|
||||
</Center>
|
||||
)}
|
||||
{!searchLoading && (
|
||||
<ResultList>
|
||||
{searchResults?.map((record) => {
|
||||
const isInstalled = mcpServers.some((server) => server.name === record.name)
|
||||
return (
|
||||
<Card
|
||||
size="small"
|
||||
key={record.name}
|
||||
title={
|
||||
<Typography.Title level={5} style={{ margin: 0 }} className="selectable">
|
||||
{record.name}
|
||||
</Typography.Title>
|
||||
}
|
||||
extra={
|
||||
<Flex>
|
||||
<Tag bordered={false} color="processing">
|
||||
v{record.version}
|
||||
</Tag>
|
||||
<Button
|
||||
type="text"
|
||||
icon={
|
||||
isInstalled ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <PlusOutlined />
|
||||
}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (isInstalled) {
|
||||
return
|
||||
}
|
||||
|
||||
addMCPServer({
|
||||
id: nanoid(),
|
||||
name: record.name,
|
||||
description: `${record.description}\n\n${t('settings.mcp.npx_list.usage')}: ${record.usage}\n${t('settings.mcp.npx_list.npm')}: ${record.npmLink}`,
|
||||
command: 'npx',
|
||||
args: ['-y', record.fullName],
|
||||
isActive: false,
|
||||
type: record.type
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Text className="selectable">{record.description}</Text>
|
||||
<Text type="secondary" className="selectable">
|
||||
{t('settings.mcp.npx_list.usage')}: {record.usage}
|
||||
</Text>
|
||||
<Link href={record.npmLink} target="_blank" rel="noopener noreferrer">
|
||||
{record.npmLink}
|
||||
</Link>
|
||||
</Space>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</ResultList>
|
||||
</SettingGroup>
|
||||
const buildInServer = builtinMCPServers.find((server) => server.name === record.name)
|
||||
|
||||
if (buildInServer) {
|
||||
addMCPServer(buildInServer)
|
||||
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
|
||||
setSelectedMcpServer(buildInServer)
|
||||
return
|
||||
}
|
||||
|
||||
const newServer = {
|
||||
id: nanoid(),
|
||||
name: record.name,
|
||||
description: `${record.description}\n\n${t('settings.mcp.npx_list.usage')}: ${record.usage}\n${t('settings.mcp.npx_list.npm')}: ${record.npmLink}`,
|
||||
command: 'npx',
|
||||
args: ['-y', record.fullName],
|
||||
isActive: false,
|
||||
type: record.type
|
||||
}
|
||||
|
||||
addMCPServer(newServer)
|
||||
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
|
||||
setSelectedMcpServer(newServer)
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Text className="selectable">{record.description}</Text>
|
||||
<Text type="secondary" className="selectable">
|
||||
{t('settings.mcp.npx_list.usage')}: {record.usage}
|
||||
</Text>
|
||||
<Link href={record.npmLink} target="_blank" rel="noopener noreferrer">
|
||||
{record.npmLink}
|
||||
</Link>
|
||||
</Space>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</ResultList>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const SettingGroupCss = css`
|
||||
height: 100%;
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 0;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const ResultList = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: calc(100% + 10px);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
padding-right: 4px;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
export default NpxSearch
|
||||
|
||||
@ -1,41 +1,32 @@
|
||||
import { CodeOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { ArrowLeftOutlined, CodeOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import DragableList from '@renderer/components/DragableList'
|
||||
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||
import { HStack, VStack } from '@renderer/components/Layout'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { VStack } from '@renderer/components/Layout'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer } from '..'
|
||||
import { SettingContainer, SettingTitle } from '..'
|
||||
import InstallNpxUv from './InstallNpxUv'
|
||||
import McpSettings from './McpSettings'
|
||||
import NpxSearch from './NpxSearch'
|
||||
|
||||
const MCPSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { mcpServers, addMCPServer, updateMcpServers, deleteMCPServer } = useMCPServers()
|
||||
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0])
|
||||
const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null)
|
||||
const { mcpServers, addMCPServer } = useMCPServers()
|
||||
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(null)
|
||||
const { theme } = useTheme()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
const unsubs = [
|
||||
EventEmitter.on('mcp:npx-search', () => setRoute('npx-search')),
|
||||
EventEmitter.on('mcp:mcp-install', () => setRoute('mcp-install'))
|
||||
]
|
||||
return () => unsubs.forEach((unsub) => unsub())
|
||||
}, [])
|
||||
const location = useLocation()
|
||||
const pathname = location.pathname
|
||||
|
||||
const onAddMcpServer = async () => {
|
||||
const onAddMcpServer = useCallback(async () => {
|
||||
const newServer = {
|
||||
id: nanoid(),
|
||||
name: t('settings.mcp.newServer'),
|
||||
@ -49,136 +40,228 @@ const MCPSettings: FC = () => {
|
||||
addMCPServer(newServer)
|
||||
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
|
||||
setSelectedMcpServer(newServer)
|
||||
}
|
||||
|
||||
const onDeleteMcpServer = useCallback(
|
||||
async (server: MCPServer) => {
|
||||
try {
|
||||
await window.api.mcp.removeServer(server)
|
||||
await deleteMCPServer(server.id)
|
||||
window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
|
||||
} catch (error: any) {
|
||||
window.message.error({
|
||||
content: `${t('settings.mcp.deleteError')}: ${error.message}`,
|
||||
key: 'mcp-list'
|
||||
})
|
||||
}
|
||||
},
|
||||
[deleteMCPServer, t]
|
||||
)
|
||||
|
||||
const getMenuItems = useCallback(
|
||||
(server: MCPServer) => {
|
||||
const menus: MenuProps['items'] = [
|
||||
{
|
||||
label: t('common.delete'),
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: () => onDeleteMcpServer(server)
|
||||
}
|
||||
]
|
||||
return menus
|
||||
},
|
||||
[onDeleteMcpServer, t]
|
||||
)
|
||||
}, [addMCPServer, t])
|
||||
|
||||
useEffect(() => {
|
||||
const _selectedMcpServer = mcpServers.find((server) => server.id === selectedMcpServer?.id)
|
||||
setSelectedMcpServer(_selectedMcpServer || mcpServers[0])
|
||||
}, [mcpServers, route, selectedMcpServer])
|
||||
}, [mcpServers, selectedMcpServer])
|
||||
|
||||
const MainContent = useMemo(() => {
|
||||
if (route === 'npx-search' || isEmpty(mcpServers)) {
|
||||
return (
|
||||
<SettingContainer theme={theme}>
|
||||
<NpxSearch />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
if (route === 'mcp-install') {
|
||||
return (
|
||||
<SettingContainer theme={theme}>
|
||||
<InstallNpxUv />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
useEffect(() => {
|
||||
// Check if the selected server still exists in the updated mcpServers list
|
||||
if (selectedMcpServer) {
|
||||
return <McpSettings server={selectedMcpServer} />
|
||||
const serverExists = mcpServers.some((server) => server.id === selectedMcpServer.id)
|
||||
if (!serverExists) {
|
||||
setSelectedMcpServer(null)
|
||||
}
|
||||
} else {
|
||||
setSelectedMcpServer(null)
|
||||
}
|
||||
}, [mcpServers, selectedMcpServer])
|
||||
|
||||
return <NpxSearch />
|
||||
}, [mcpServers, route, selectedMcpServer, theme])
|
||||
const McpServersList = useCallback(
|
||||
() => (
|
||||
<GridContainer>
|
||||
<GridHeader>
|
||||
<SettingTitle>{t('settings.mcp.newServer')}</SettingTitle>
|
||||
</GridHeader>
|
||||
<ServersGrid>
|
||||
<AddServerCard onClick={onAddMcpServer}>
|
||||
<PlusOutlined style={{ fontSize: 24 }} />
|
||||
<AddServerText>{t('settings.mcp.addServer')}</AddServerText>
|
||||
</AddServerCard>
|
||||
{mcpServers.map((server) => (
|
||||
<ServerCard
|
||||
key={server.id}
|
||||
onClick={() => {
|
||||
setSelectedMcpServer(server)
|
||||
navigate(`/settings/mcp/server/${server.id}`)
|
||||
}}>
|
||||
<ServerHeader>
|
||||
<ServerIcon>
|
||||
<CodeOutlined />
|
||||
</ServerIcon>
|
||||
<ServerName>{server.name}</ServerName>
|
||||
<StatusIndicator>
|
||||
<IndicatorLight
|
||||
size={6}
|
||||
color={server.isActive ? 'green' : 'var(--color-text-3)'}
|
||||
animation={server.isActive}
|
||||
shadow={false}
|
||||
/>
|
||||
</StatusIndicator>
|
||||
</ServerHeader>
|
||||
<ServerDescription>
|
||||
{server.description &&
|
||||
server.description.substring(0, 60) + (server.description.length > 60 ? '...' : '')}
|
||||
</ServerDescription>
|
||||
</ServerCard>
|
||||
))}
|
||||
</ServersGrid>
|
||||
</GridContainer>
|
||||
),
|
||||
[mcpServers, navigate, onAddMcpServer, t]
|
||||
)
|
||||
|
||||
const isHome = pathname === '/settings/mcp'
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<McpListContainer>
|
||||
<McpList>
|
||||
<ListItem
|
||||
key="add"
|
||||
title={t('settings.mcp.addServer')}
|
||||
active={false}
|
||||
onClick={onAddMcpServer}
|
||||
icon={<PlusOutlined />}
|
||||
titleStyle={{ fontWeight: 500 }}
|
||||
style={{ width: '100%', marginTop: -2 }}
|
||||
{!isHome && (
|
||||
<BackButtonContainer>
|
||||
<Link to="/settings/mcp">
|
||||
<BackButton>
|
||||
<ArrowLeftOutlined /> {t('common.back')}
|
||||
</BackButton>
|
||||
</Link>
|
||||
</BackButtonContainer>
|
||||
)}
|
||||
<MainContainer>
|
||||
<Routes>
|
||||
<Route path="/" element={<McpServersList />} />
|
||||
<Route path="server/:id" element={selectedMcpServer ? <McpSettings server={selectedMcpServer} /> : null} />
|
||||
<Route
|
||||
path="npx-search"
|
||||
element={
|
||||
<SettingContainer theme={theme}>
|
||||
<NpxSearch setSelectedMcpServer={setSelectedMcpServer} />
|
||||
</SettingContainer>
|
||||
}
|
||||
/>
|
||||
<DragableList list={mcpServers} onUpdate={updateMcpServers}>
|
||||
{(server: MCPServer) => (
|
||||
<Dropdown menu={{ items: getMenuItems(server) }} trigger={['contextMenu']} key={server.id}>
|
||||
<div>
|
||||
<ListItem
|
||||
key={server.id}
|
||||
title={server.name}
|
||||
active={selectedMcpServer?.id === server.id}
|
||||
onClick={() => {
|
||||
setSelectedMcpServer(server)
|
||||
setRoute(null)
|
||||
}}
|
||||
titleStyle={{ fontWeight: 500 }}
|
||||
icon={<CodeOutlined />}
|
||||
rightContent={
|
||||
<IndicatorLight
|
||||
size={6}
|
||||
color={server.isActive ? 'green' : 'var(--color-text-3)'}
|
||||
animation={server.isActive}
|
||||
shadow={false}
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
)}
|
||||
</DragableList>
|
||||
</McpList>
|
||||
</McpListContainer>
|
||||
{MainContent}
|
||||
<Route
|
||||
path="mcp-install"
|
||||
element={
|
||||
<SettingContainer theme={theme}>
|
||||
<InstallNpxUv />
|
||||
</SettingContainer>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</MainContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled(HStack)`
|
||||
const Container = styled(VStack)`
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const McpListContainer = styled(VStack)`
|
||||
width: var(--settings-width);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
`
|
||||
|
||||
const McpList = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
const GridContainer = styled(VStack)`
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
.iconfont {
|
||||
color: var(--color-text-2);
|
||||
line-height: 16px;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
padding: 20px;
|
||||
`
|
||||
|
||||
const GridHeader = styled.div`
|
||||
width: 100%;
|
||||
padding-bottom: 16px;
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
`
|
||||
|
||||
const ServersGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 2px;
|
||||
`
|
||||
|
||||
const ServerCard = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
height: 140px;
|
||||
background-color: var(--color-bg-1);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
`
|
||||
|
||||
const ServerHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
`
|
||||
|
||||
const ServerIcon = styled.div`
|
||||
font-size: 18px;
|
||||
color: var(--color-primary);
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
const ServerName = styled.div`
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const StatusIndicator = styled.div`
|
||||
margin-left: 8px;
|
||||
`
|
||||
|
||||
const ServerDescription = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
`
|
||||
|
||||
const AddServerCard = styled(ServerCard)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-style: dashed;
|
||||
background-color: transparent;
|
||||
color: var(--color-text-2);
|
||||
`
|
||||
|
||||
const AddServerText = styled.div`
|
||||
margin-top: 12px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const BackButtonContainer = styled.div`
|
||||
padding: 12px 0 0 12px;
|
||||
width: 100%;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
const MainContainer = styled.div`
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const BackButton = styled.div`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--color-text-1);
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
background-color: var(--color-bg-1);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-bg-2);
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -199,7 +199,8 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
return (
|
||||
<CustomCollapse
|
||||
key={i}
|
||||
defaultActiveKey={i >= 5 ? [] : ['1']}
|
||||
defaultActiveKey={['1']}
|
||||
styles={{ body: { padding: '0 10px' } }}
|
||||
label={
|
||||
<Flex align="center" gap={10}>
|
||||
<span style={{ fontWeight: 600 }}>{group}</span>
|
||||
@ -233,13 +234,15 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
/>
|
||||
</Tooltip>
|
||||
}>
|
||||
<FlexColumn>
|
||||
<FlexColumn style={{ margin: '10px 0' }}>
|
||||
{modelGroups[group].map((model) => (
|
||||
<FileItem
|
||||
style={{
|
||||
backgroundColor: isModelInProvider(provider, model.id)
|
||||
? 'rgba(0, 126, 0, 0.06)'
|
||||
: 'rgba(255, 255, 255, 0.04)'
|
||||
: 'rgba(255, 255, 255, 0.04)',
|
||||
border: 'none',
|
||||
boxShadow: 'none'
|
||||
}}
|
||||
key={model.id}
|
||||
fileInfo={{
|
||||
|
||||
@ -48,7 +48,7 @@ const SettingsPage: FC = () => {
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter>
|
||||
{pathname === '/settings/mcp' && <McpSettingsNavbar />}
|
||||
{pathname.includes('/settings/mcp') && <McpSettingsNavbar />}
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<SettingMenus>
|
||||
@ -142,7 +142,7 @@ const SettingsPage: FC = () => {
|
||||
<Route path="provider" element={<ProvidersList />} />
|
||||
<Route path="model" element={<ModelSettings />} />
|
||||
<Route path="web-search" element={<WebSearchSettings />} />
|
||||
<Route path="mcp" element={<MCPSettings />} />
|
||||
<Route path="mcp/*" element={<MCPSettings />} />
|
||||
<Route path="general/*" element={<GeneralSettings />} />
|
||||
<Route path="display" element={<DisplaySettings />} />
|
||||
{showMiniAppSettings && <Route path="miniapps" element={<MiniAppSettings />} />}
|
||||
|
||||
@ -10,6 +10,7 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
padding: 20px;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 75px;
|
||||
overflow-y: scroll;
|
||||
font-family: Ubuntu;
|
||||
background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};
|
||||
|
||||
@ -402,6 +402,34 @@ export interface MCPTool {
|
||||
inputSchema: MCPToolInputSchema
|
||||
}
|
||||
|
||||
export interface MCPPromptArguments {
|
||||
name: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export interface MCPPrompt {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
arguments?: MCPPromptArguments[]
|
||||
serverId: string
|
||||
serverName: string
|
||||
}
|
||||
|
||||
export interface GetMCPPromptResponse {
|
||||
description?: string
|
||||
messages: {
|
||||
role: string
|
||||
content: {
|
||||
type: 'text' | 'image' | 'audio' | 'resource'
|
||||
text?: string
|
||||
data?: string
|
||||
mimeType?: string
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface MCPConfig {
|
||||
servers: MCPServer[]
|
||||
}
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@ -3942,6 +3942,7 @@ __metadata:
|
||||
analytics: "npm:^0.8.16"
|
||||
antd: "npm:^5.22.5"
|
||||
applescript: "npm:^1.0.0"
|
||||
async-mutex: "npm:^0.5.0"
|
||||
axios: "npm:^1.7.3"
|
||||
babel-plugin-styled-components: "npm:^2.1.4"
|
||||
browser-image-compression: "npm:^2.0.2"
|
||||
@ -4493,6 +4494,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"async-mutex@npm:^0.5.0":
|
||||
version: 0.5.0
|
||||
resolution: "async-mutex@npm:0.5.0"
|
||||
dependencies:
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10c0/9096e6ad6b674c894d8ddd5aa4c512b09bb05931b8746ebd634952b05685608b2b0820ed5c406e6569919ff5fe237ab3c491e6f2887d6da6b6ba906db3ee9c32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"async@npm:^3.2.3":
|
||||
version: 3.2.6
|
||||
resolution: "async@npm:3.2.6"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user