mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 06:19:05 +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 材料:
|
在以下任何一种情况下,您需要联系我们并获得明确的书面商业授权后,方可继续使用 Cherry Studio 材料:
|
||||||
|
|
||||||
1. **修改与衍生**: 您对 Cherry Studio 材料进行修改或基于其进行衍生开发(包括但不限于修改应用名称、Logo、代码、功能、界面,数据等)。
|
1. **修改与衍生**: 您对 Cherry Studio 材料进行修改或基于其进行衍生开发(包括但不限于修改应用名称、Logo、代码、功能、界面,数据等)。
|
||||||
2. **企业服务**: 在您的企业内部,或为企业客户提供基于 CherryStudio 的服务,且该服务支持 10 人及以上累计用户使用。
|
2. **企业服务**: 在您的企业内部,或为企业客户提供基于 Cherry Studio 的服务,且该服务支持 10 人及以上累计用户使用。
|
||||||
3. **硬件捆绑销售**: 您将 Cherry Studio 预装或集成到硬件设备或产品中进行捆绑销售。
|
3. **硬件捆绑销售**: 您将 Cherry Studio 预装或集成到硬件设备或产品中进行捆绑销售。
|
||||||
4. **政府或教育机构大规模采购**: 您的使用场景属于政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。
|
4. **政府或教育机构大规模采购**: 您的使用场景属于政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。
|
||||||
5. **面向公众的公有云服务**:基于 Cherry Studio,提供面向公众的公有云服务。
|
5. **面向公众的公有云服务**:基于 Cherry Studio,提供面向公众的公有云服务。
|
||||||
@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
**License Agreement**
|
**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**
|
**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.
|
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",
|
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||||
"@xyflow/react": "^12.4.4",
|
"@xyflow/react": "^12.4.4",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
"async-mutex": "^0.5.0",
|
||||||
"color": "^5.0.0",
|
"color": "^5.0.0",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"docx": "^9.0.2",
|
"docx": "^9.0.2",
|
||||||
|
|||||||
@ -50,6 +50,8 @@ export enum IpcChannel {
|
|||||||
Mcp_StopServer = 'mcp:stop-server',
|
Mcp_StopServer = 'mcp:stop-server',
|
||||||
Mcp_ListTools = 'mcp:list-tools',
|
Mcp_ListTools = 'mcp:list-tools',
|
||||||
Mcp_CallTool = 'mcp:call-tool',
|
Mcp_CallTool = 'mcp:call-tool',
|
||||||
|
Mcp_ListPrompts = 'mcp:list-prompts',
|
||||||
|
Mcp_GetPrompt = 'mcp:get-prompt',
|
||||||
Mcp_GetInstallInfo = 'mcp:get-install-info',
|
Mcp_GetInstallInfo = 'mcp:get-install-info',
|
||||||
Mcp_ServersChanged = 'mcp:servers-changed',
|
Mcp_ServersChanged = 'mcp:servers-changed',
|
||||||
Mcp_ServersUpdated = 'mcp:servers-updated',
|
Mcp_ServersUpdated = 'mcp:servers-updated',
|
||||||
|
|||||||
@ -1,111 +1,109 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<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">
|
<head>
|
||||||
<div class="container mx-auto bg-white p-6 rounded shadow-lg">
|
<meta charset="UTF-8">
|
||||||
<h1 class="text-3xl font-bold mb-6 text-center">Cherry Studio 许可协议</h1>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<div class="mb-8">
|
<title>许可协议 | License Agreement</title>
|
||||||
<h2 class="text-2xl font-semibold mb-4">许可协议</h2>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<p class="mb-4">
|
</head>
|
||||||
本软件采用 <strong>Apache License 2.0</strong> 许可。除 Apache License 2.0 规定的条款外,您在使用 Cherry
|
|
||||||
Studio 时还应遵守以下附加条款:
|
<body class="bg-gray-50">
|
||||||
</p>
|
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||||
<h3 class="text-xl font-semibold mb-2">一. 商用许可</h3>
|
<!-- 中文版本 -->
|
||||||
<ol class="list-decimal list-inside mb-4">
|
<div class="mb-12">
|
||||||
<li><strong>免费商用</strong>:用户在不修改代码的情况下,可以免费用于商业目的。</li>
|
<h1 class="text-3xl font-bold mb-8 text-gray-900">许可协议</h1>
|
||||||
<li>
|
|
||||||
<strong>商业授权</strong>:如果您满足以下任意条件之一,需取得商业授权:
|
<p class="mb-6 text-gray-700">采用 Apache License 2.0 修改版许可,并附加以下条件:</p>
|
||||||
<ol class="list-decimal list-inside ml-4">
|
|
||||||
<li>对本软件进行二次修改、开发(包括但不限于修改应用名称、logo、代码以及功能)。</li>
|
<section class="mb-8">
|
||||||
<li>为企业客户提供多租户服务,且该服务支持 10 人或以上的使用。</li>
|
<h2 class="text-xl font-semibold mb-4 text-gray-900">一. 商用许可</h2>
|
||||||
<li>预装或集成到硬件设备或产品中进行捆绑销售。</li>
|
<p class="mb-4 text-gray-700">在以下任何一种情况下,您需要联系我们并获得明确的书面商业授权后,方可继续使用 Cherry Studio 材料:</p>
|
||||||
<li>政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。</li>
|
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||||
</ol>
|
<li><strong>修改与衍生</strong>: 您对 Cherry Studio 材料进行修改或基于其进行衍生开发(包括但不限于修改应用名称、Logo、代码、功能、界面,数据等)。</li>
|
||||||
</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>
|
</ol>
|
||||||
<h3 class="text-xl font-semibold mb-2">二. 贡献者协议</h3>
|
</section>
|
||||||
<ol class="list-decimal list-inside mb-4">
|
|
||||||
|
<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>
|
||||||
<li><strong>商业用途</strong>:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。</li>
|
<li><strong>商业用途</strong>:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。</li>
|
||||||
</ol>
|
</ol>
|
||||||
<h3 class="text-xl font-semibold mb-2">三. 其他条款</h3>
|
</section>
|
||||||
<ol class="list-decimal list-inside mb-4">
|
|
||||||
|
<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>本协议条款的解释权归 Cherry Studio 开发者所有。</li>
|
||||||
<li>本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。</li>
|
<li>本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p class="mb-4">如有任何问题或需申请商业授权,请联系 Cherry Studio 开发团队。</p>
|
</section>
|
||||||
<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>
|
|
||||||
</div>
|
</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_StopServer, mcpService.stopServer)
|
||||||
ipcMain.handle(IpcChannel.Mcp_ListTools, mcpService.listTools)
|
ipcMain.handle(IpcChannel.Mcp_ListTools, mcpService.listTools)
|
||||||
ipcMain.handle(IpcChannel.Mcp_CallTool, mcpService.callTool)
|
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.Mcp_GetInstallInfo, mcpService.getInstallInfo)
|
||||||
|
|
||||||
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
|
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 { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
|
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
|
||||||
import { nanoid } from '@reduxjs/toolkit'
|
import { nanoid } from '@reduxjs/toolkit'
|
||||||
import { MCPServer, MCPTool } from '@types'
|
import { GetMCPPromptResponse, MCPPrompt, MCPServer, MCPTool } from '@types'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
|
|
||||||
import { CacheService } from './CacheService'
|
import { CacheService } from './CacheService'
|
||||||
import { StreamableHTTPClientTransport, type StreamableHTTPClientTransportOptions } from './MCPStreamableHttpClient'
|
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 {
|
class McpService {
|
||||||
private clients: Map<string, Client> = new Map()
|
private clients: Map<string, Client> = new Map()
|
||||||
|
|
||||||
@ -35,6 +69,8 @@ class McpService {
|
|||||||
this.initClient = this.initClient.bind(this)
|
this.initClient = this.initClient.bind(this)
|
||||||
this.listTools = this.listTools.bind(this)
|
this.listTools = this.listTools.bind(this)
|
||||||
this.callTool = this.callTool.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.closeClient = this.closeClient.bind(this)
|
||||||
this.removeServer = this.removeServer.bind(this)
|
this.removeServer = this.removeServer.bind(this)
|
||||||
this.restartServer = this.restartServer.bind(this)
|
this.restartServer = this.restartServer.bind(this)
|
||||||
@ -216,31 +252,40 @@ class McpService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
private async listToolsImpl(server: MCPServer): Promise<MCPTool[]> {
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Logger.info(`[MCP] Listing tools for server: ${server.name}`)
|
Logger.info(`[MCP] Listing tools for server: ${server.name}`)
|
||||||
const { tools } = await client.listTools()
|
const client = await this.initClient(server)
|
||||||
const serverTools: MCPTool[] = []
|
try {
|
||||||
tools.map((tool: any) => {
|
const { tools } = await client.listTools()
|
||||||
const serverTool: MCPTool = {
|
const serverTools: MCPTool[] = []
|
||||||
...tool,
|
tools.map((tool: any) => {
|
||||||
id: `f${nanoid()}`,
|
const serverTool: MCPTool = {
|
||||||
serverId: server.id,
|
...tool,
|
||||||
serverName: server.name
|
id: `f${nanoid()}`,
|
||||||
}
|
serverId: server.id,
|
||||||
serverTools.push(serverTool)
|
serverName: server.name
|
||||||
})
|
}
|
||||||
CacheService.set(cacheKey, serverTools, 5 * 60 * 1000)
|
serverTools.push(serverTool)
|
||||||
return serverTools
|
})
|
||||||
|
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 }
|
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
|
* 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>
|
stopServer: (server: MCPServer) => Promise<void>
|
||||||
listTools: (server: MCPServer) => Promise<MCPTool[]>
|
listTools: (server: MCPServer) => Promise<MCPTool[]>
|
||||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) => Promise<any>
|
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 }>
|
getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }>
|
||||||
}
|
}
|
||||||
copilot: {
|
copilot: {
|
||||||
|
|||||||
@ -135,8 +135,11 @@ const api = {
|
|||||||
restartServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RestartServer, server),
|
restartServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RestartServer, server),
|
||||||
stopServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_StopServer, server),
|
stopServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_StopServer, server),
|
||||||
listTools: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListTools, 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 }),
|
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)
|
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo)
|
||||||
},
|
},
|
||||||
shell: {
|
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 { Collapse } from 'antd'
|
||||||
|
import { merge } from 'lodash'
|
||||||
import { FC, memo } from 'react'
|
import { FC, memo } from 'react'
|
||||||
|
|
||||||
interface CustomCollapseProps {
|
interface CustomCollapseProps {
|
||||||
@ -9,6 +10,11 @@ interface CustomCollapseProps {
|
|||||||
defaultActiveKey?: string[]
|
defaultActiveKey?: string[]
|
||||||
activeKey?: string[]
|
activeKey?: string[]
|
||||||
collapsible?: 'header' | 'icon' | 'disabled'
|
collapsible?: 'header' | 'icon' | 'disabled'
|
||||||
|
style?: React.CSSProperties
|
||||||
|
styles?: {
|
||||||
|
header?: React.CSSProperties
|
||||||
|
body?: React.CSSProperties
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomCollapse: FC<CustomCollapseProps> = ({
|
const CustomCollapse: FC<CustomCollapseProps> = ({
|
||||||
@ -18,14 +24,17 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
|
|||||||
destroyInactivePanel = false,
|
destroyInactivePanel = false,
|
||||||
defaultActiveKey = ['1'],
|
defaultActiveKey = ['1'],
|
||||||
activeKey,
|
activeKey,
|
||||||
collapsible = undefined
|
collapsible = undefined,
|
||||||
|
style,
|
||||||
|
styles
|
||||||
}) => {
|
}) => {
|
||||||
const CollapseStyle = {
|
const defaultCollapseStyle = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
border: '0.5px solid var(--color-border)'
|
border: '0.5px solid var(--color-border)'
|
||||||
}
|
}
|
||||||
const CollapseItemStyles = {
|
|
||||||
|
const defaultCollapseItemStyles = {
|
||||||
header: {
|
header: {
|
||||||
padding: '8px 16px',
|
padding: '8px 16px',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -38,17 +47,21 @@ const CustomCollapse: FC<CustomCollapseProps> = ({
|
|||||||
borderTop: '0.5px solid var(--color-border)'
|
borderTop: '0.5px solid var(--color-border)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const collapseStyle = merge({}, defaultCollapseStyle, style)
|
||||||
|
const collapseItemStyles = merge({}, defaultCollapseItemStyles, styles)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapse
|
<Collapse
|
||||||
bordered={false}
|
bordered={false}
|
||||||
style={CollapseStyle}
|
style={collapseStyle}
|
||||||
defaultActiveKey={defaultActiveKey}
|
defaultActiveKey={defaultActiveKey}
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
destroyInactivePanel={destroyInactivePanel}
|
destroyInactivePanel={destroyInactivePanel}
|
||||||
collapsible={collapsible}
|
collapsible={collapsible}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
styles: CollapseItemStyles,
|
styles: collapseItemStyles,
|
||||||
key: '1',
|
key: '1',
|
||||||
label,
|
label,
|
||||||
extra,
|
extra,
|
||||||
|
|||||||
@ -456,8 +456,7 @@ const StyledMenu = styled(Menu)`
|
|||||||
/* Simple animation that changes background color when sticky */
|
/* Simple animation that changes background color when sticky */
|
||||||
@keyframes background-change {
|
@keyframes background-change {
|
||||||
to {
|
to {
|
||||||
background-color: var(--color-background-soft);
|
background-color: var(--color-background);
|
||||||
opacity: 0.95;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -567,12 +567,12 @@ const QuickPanelFooterTips = styled.div<{ $footerWidth: number }>`
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
`
|
`
|
||||||
|
|
||||||
const QuickPanelFooterTitle = styled.div`
|
const QuickPanelFooterTitle = styled.div`
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -635,7 +635,8 @@ const QuickPanelItemIcon = styled.span`
|
|||||||
|
|
||||||
const QuickPanelItemLabel = styled.span`
|
const QuickPanelItemLabel = styled.span`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@ -26,3 +26,10 @@ export const useMCPServers = () => {
|
|||||||
updateMcpServers: (servers: MCPServer[]) => dispatch(setMCPServers(servers))
|
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",
|
"switch.disabled": "Please wait for the current reply to complete",
|
||||||
"tools": {
|
"tools": {
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"invoking": "Invoking"
|
"invoking": "Invoking",
|
||||||
|
"error": "Error occurred"
|
||||||
},
|
},
|
||||||
"topic.added": "New topic added",
|
"topic.added": "New topic added",
|
||||||
"upgrade.success.button": "Restart",
|
"upgrade.success.button": "Restart",
|
||||||
@ -1092,7 +1093,6 @@
|
|||||||
"newServer": "MCP Server",
|
"newServer": "MCP Server",
|
||||||
"npx_list": {
|
"npx_list": {
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"desc": "Search and add npm packages as MCP servers",
|
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"no_packages": "No packages found",
|
"no_packages": "No packages found",
|
||||||
"npm": "NPM",
|
"npm": "NPM",
|
||||||
@ -1101,7 +1101,6 @@
|
|||||||
"scope_required": "Please enter npm scope",
|
"scope_required": "Please enter npm scope",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"search_error": "Search error",
|
"search_error": "Search error",
|
||||||
"title": "NPX Package List",
|
|
||||||
"usage": "Usage",
|
"usage": "Usage",
|
||||||
"version": "Version"
|
"version": "Version"
|
||||||
},
|
},
|
||||||
@ -1118,10 +1117,25 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"editMcpJson": "Edit MCP Configuration",
|
"editMcpJson": "Edit MCP Configuration",
|
||||||
"installHelp": "Get Installation Help",
|
"installHelp": "Get Installation Help",
|
||||||
|
"tabs": {
|
||||||
|
"general": "General",
|
||||||
|
"tools": "Tools",
|
||||||
|
"prompts": "Prompts",
|
||||||
|
"resources": "Resources"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"inputSchema": "Input Schema",
|
"inputSchema": "Input Schema",
|
||||||
"availableTools": "Available Tools",
|
"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",
|
"deleteServer": "Delete Server",
|
||||||
"deleteServerConfirm": "Are you sure you want to delete this server?",
|
"deleteServerConfirm": "Are you sure you want to delete this server?",
|
||||||
|
|||||||
@ -562,7 +562,8 @@
|
|||||||
"switch.disabled": "現在の応答が完了するまで切り替えを無効にします",
|
"switch.disabled": "現在の応答が完了するまで切り替えを無効にします",
|
||||||
"tools": {
|
"tools": {
|
||||||
"completed": "完了",
|
"completed": "完了",
|
||||||
"invoking": "呼び出し中"
|
"invoking": "呼び出し中",
|
||||||
|
"error": "エラーが発生しました"
|
||||||
},
|
},
|
||||||
"topic.added": "新しいトピックが追加されました",
|
"topic.added": "新しいトピックが追加されました",
|
||||||
"upgrade.success.button": "再起動",
|
"upgrade.success.button": "再起動",
|
||||||
@ -1069,7 +1070,6 @@
|
|||||||
"newServer": "MCP サーバー",
|
"newServer": "MCP サーバー",
|
||||||
"npx_list": {
|
"npx_list": {
|
||||||
"actions": "アクション",
|
"actions": "アクション",
|
||||||
"desc": "npm パッケージを検索して MCP サーバーとして追加",
|
|
||||||
"description": "説明",
|
"description": "説明",
|
||||||
"no_packages": "パッケージが見つかりません",
|
"no_packages": "パッケージが見つかりません",
|
||||||
"npm": "NPM",
|
"npm": "NPM",
|
||||||
@ -1078,7 +1078,6 @@
|
|||||||
"scope_required": "npm スコープを入力してください",
|
"scope_required": "npm スコープを入力してください",
|
||||||
"search": "検索",
|
"search": "検索",
|
||||||
"search_error": "パッケージの検索に失敗しました",
|
"search_error": "パッケージの検索に失敗しました",
|
||||||
"title": "NPX パッケージリスト",
|
|
||||||
"usage": "使用法",
|
"usage": "使用法",
|
||||||
"version": "バージョン"
|
"version": "バージョン"
|
||||||
},
|
},
|
||||||
@ -1095,10 +1094,25 @@
|
|||||||
},
|
},
|
||||||
"editMcpJson": "MCP 設定を編集",
|
"editMcpJson": "MCP 設定を編集",
|
||||||
"installHelp": "インストールヘルプを取得",
|
"installHelp": "インストールヘルプを取得",
|
||||||
|
"tabs": {
|
||||||
|
"general": "一般",
|
||||||
|
"tools": "ツール",
|
||||||
|
"prompts": "プロンプト",
|
||||||
|
"resources": "リソース"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"inputSchema": "入力スキーマ",
|
"inputSchema": "入力スキーマ",
|
||||||
"availableTools": "利用可能なツール",
|
"availableTools": "利用可能なツール",
|
||||||
"noToolsAvailable": "利用可能なツールはありません"
|
"noToolsAvailable": "利用可能なツールなし",
|
||||||
|
"loadError": "ツール取得エラー"
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"availablePrompts": "利用可能なプロンプト",
|
||||||
|
"noPromptsAvailable": "利用可能なプロンプトはありません",
|
||||||
|
"arguments": "引数",
|
||||||
|
"requiredField": "必須フィールド",
|
||||||
|
"genericError": "プロンプト取得エラー",
|
||||||
|
"loadError": "プロンプト取得エラー"
|
||||||
},
|
},
|
||||||
"deleteServer": "サーバーを削除",
|
"deleteServer": "サーバーを削除",
|
||||||
"deleteServerConfirm": "このサーバーを削除してもよろしいですか?",
|
"deleteServerConfirm": "このサーバーを削除してもよろしいですか?",
|
||||||
|
|||||||
@ -563,7 +563,8 @@
|
|||||||
"switch.disabled": "Пожалуйста, дождитесь завершения текущего ответа",
|
"switch.disabled": "Пожалуйста, дождитесь завершения текущего ответа",
|
||||||
"tools": {
|
"tools": {
|
||||||
"completed": "Завершено",
|
"completed": "Завершено",
|
||||||
"invoking": "Вызов"
|
"invoking": "Вызов",
|
||||||
|
"error": "Произошла ошибка"
|
||||||
},
|
},
|
||||||
"topic.added": "Новый топик добавлен",
|
"topic.added": "Новый топик добавлен",
|
||||||
"upgrade.success.button": "Перезапустить",
|
"upgrade.success.button": "Перезапустить",
|
||||||
@ -1069,7 +1070,6 @@
|
|||||||
"newServer": "MCP сервер",
|
"newServer": "MCP сервер",
|
||||||
"npx_list": {
|
"npx_list": {
|
||||||
"actions": "Действия",
|
"actions": "Действия",
|
||||||
"desc": "Поиск и добавление npm пакетов в качестве MCP серверов",
|
|
||||||
"description": "Описание",
|
"description": "Описание",
|
||||||
"no_packages": "Ничего не найдено",
|
"no_packages": "Ничего не найдено",
|
||||||
"npm": "NPM",
|
"npm": "NPM",
|
||||||
@ -1078,7 +1078,6 @@
|
|||||||
"scope_required": "Пожалуйста, введите область npm",
|
"scope_required": "Пожалуйста, введите область npm",
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
"search_error": "Ошибка поиска",
|
"search_error": "Ошибка поиска",
|
||||||
"title": "Список пакетов NPX",
|
|
||||||
"usage": "Использование",
|
"usage": "Использование",
|
||||||
"version": "Версия"
|
"version": "Версия"
|
||||||
},
|
},
|
||||||
@ -1095,10 +1094,25 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"editMcpJson": "Редактировать MCP",
|
"editMcpJson": "Редактировать MCP",
|
||||||
"installHelp": "Получить помощь по установке",
|
"installHelp": "Получить помощь по установке",
|
||||||
|
"tabs": {
|
||||||
|
"general": "Общие",
|
||||||
|
"tools": "Инструменты",
|
||||||
|
"prompts": "Подсказки",
|
||||||
|
"resources": "Ресурсы"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"inputSchema": "входные параметры",
|
"inputSchema": "Схема ввода",
|
||||||
"availableTools": "доступные инструменты",
|
"availableTools": "Доступные инструменты",
|
||||||
"noToolsAvailable": "нет доступных инструментов"
|
"noToolsAvailable": "Нет доступных инструментов",
|
||||||
|
"loadError": "Ошибка получения инструментов"
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"availablePrompts": "Доступные подсказки",
|
||||||
|
"noPromptsAvailable": "Нет доступных подсказок",
|
||||||
|
"arguments": "Аргументы",
|
||||||
|
"requiredField": "Обязательное поле",
|
||||||
|
"genericError": "Ошибка получения подсказки",
|
||||||
|
"loadError": "Ошибка получения подсказок"
|
||||||
},
|
},
|
||||||
"deleteServer": "Удалить сервер",
|
"deleteServer": "Удалить сервер",
|
||||||
"deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?",
|
"deleteServerConfirm": "Вы уверены, что хотите удалить этот сервер?",
|
||||||
|
|||||||
@ -585,7 +585,8 @@
|
|||||||
"switch.disabled": "请等待当前回复完成后操作",
|
"switch.disabled": "请等待当前回复完成后操作",
|
||||||
"tools": {
|
"tools": {
|
||||||
"completed": "已完成",
|
"completed": "已完成",
|
||||||
"invoking": "调用中"
|
"invoking": "调用中",
|
||||||
|
"error": "发生错误"
|
||||||
},
|
},
|
||||||
"topic.added": "话题添加成功",
|
"topic.added": "话题添加成功",
|
||||||
"upgrade.success.button": "重启",
|
"upgrade.success.button": "重启",
|
||||||
@ -1092,7 +1093,6 @@
|
|||||||
"newServer": "MCP 服务器",
|
"newServer": "MCP 服务器",
|
||||||
"npx_list": {
|
"npx_list": {
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
"desc": "搜索并添加 npm 包作为 MCP 服务",
|
|
||||||
"description": "描述",
|
"description": "描述",
|
||||||
"no_packages": "未找到包",
|
"no_packages": "未找到包",
|
||||||
"npm": "NPM",
|
"npm": "NPM",
|
||||||
@ -1101,7 +1101,6 @@
|
|||||||
"scope_required": "请输入 npm 作用域",
|
"scope_required": "请输入 npm 作用域",
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"search_error": "搜索失败",
|
"search_error": "搜索失败",
|
||||||
"title": "NPX 包列表",
|
|
||||||
"usage": "用法",
|
"usage": "用法",
|
||||||
"version": "版本"
|
"version": "版本"
|
||||||
},
|
},
|
||||||
@ -1118,10 +1117,25 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"editMcpJson": "编辑 MCP 配置",
|
"editMcpJson": "编辑 MCP 配置",
|
||||||
"installHelp": "获取安装帮助",
|
"installHelp": "获取安装帮助",
|
||||||
|
"tabs": {
|
||||||
|
"general": "通用",
|
||||||
|
"tools": "工具",
|
||||||
|
"prompts": "提示",
|
||||||
|
"resources": "资源"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"inputSchema": "输入参数",
|
"inputSchema": "输入模式",
|
||||||
"availableTools": "可用工具",
|
"availableTools": "可用工具",
|
||||||
"noToolsAvailable": "没有可用工具"
|
"noToolsAvailable": "无可用工具",
|
||||||
|
"loadError": "获取工具失败"
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"availablePrompts": "可用提示",
|
||||||
|
"noPromptsAvailable": "无可用提示",
|
||||||
|
"arguments": "参数",
|
||||||
|
"requiredField": "必填字段",
|
||||||
|
"genericError": "获取提示错误",
|
||||||
|
"loadError": "获取提示失败"
|
||||||
},
|
},
|
||||||
"deleteServer": "删除服务器",
|
"deleteServer": "删除服务器",
|
||||||
"deleteServerConfirm": "确定要删除此服务器吗?",
|
"deleteServerConfirm": "确定要删除此服务器吗?",
|
||||||
|
|||||||
@ -563,7 +563,8 @@
|
|||||||
"switch.disabled": "請等待當前回覆完成",
|
"switch.disabled": "請等待當前回覆完成",
|
||||||
"tools": {
|
"tools": {
|
||||||
"completed": "已完成",
|
"completed": "已完成",
|
||||||
"invoking": "調用中"
|
"invoking": "調用中",
|
||||||
|
"error": "發生錯誤"
|
||||||
},
|
},
|
||||||
"topic.added": "新話題已新增",
|
"topic.added": "新話題已新增",
|
||||||
"upgrade.success.button": "重新啟動",
|
"upgrade.success.button": "重新啟動",
|
||||||
@ -1069,7 +1070,6 @@
|
|||||||
"newServer": "MCP 伺服器",
|
"newServer": "MCP 伺服器",
|
||||||
"npx_list": {
|
"npx_list": {
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
"desc": "搜索並添加 npm 包作為 MCP 服務",
|
|
||||||
"description": "描述",
|
"description": "描述",
|
||||||
"no_packages": "未找到包",
|
"no_packages": "未找到包",
|
||||||
"npm": "NPM",
|
"npm": "NPM",
|
||||||
@ -1078,7 +1078,6 @@
|
|||||||
"scope_required": "請輸入 npm 作用域",
|
"scope_required": "請輸入 npm 作用域",
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"search_error": "搜索失敗",
|
"search_error": "搜索失敗",
|
||||||
"title": "NPX 包列表",
|
|
||||||
"usage": "用法",
|
"usage": "用法",
|
||||||
"version": "版本"
|
"version": "版本"
|
||||||
},
|
},
|
||||||
@ -1095,10 +1094,25 @@
|
|||||||
"url": "URL",
|
"url": "URL",
|
||||||
"editMcpJson": "編輯 MCP 配置",
|
"editMcpJson": "編輯 MCP 配置",
|
||||||
"installHelp": "獲取安裝幫助",
|
"installHelp": "獲取安裝幫助",
|
||||||
|
"tabs": {
|
||||||
|
"general": "通用",
|
||||||
|
"tools": "工具",
|
||||||
|
"prompts": "提示",
|
||||||
|
"resources": "資源"
|
||||||
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"inputSchema": "輸入參數",
|
"inputSchema": "輸入模式",
|
||||||
"availableTools": "可用工具",
|
"availableTools": "可用工具",
|
||||||
"noToolsAvailable": "沒有可用工具"
|
"noToolsAvailable": "無可用工具",
|
||||||
|
"loadError": "獲取工具失敗"
|
||||||
|
},
|
||||||
|
"prompts": {
|
||||||
|
"availablePrompts": "可用提示",
|
||||||
|
"noPromptsAvailable": "無可用提示",
|
||||||
|
"arguments": "參數",
|
||||||
|
"requiredField": "必填欄位",
|
||||||
|
"genericError": "獲取提示錯誤",
|
||||||
|
"loadError": "獲取提示失敗"
|
||||||
},
|
},
|
||||||
"deleteServer": "刪除伺服器",
|
"deleteServer": "刪除伺服器",
|
||||||
"deleteServerConfirm": "確定要刪除此伺服器嗎?",
|
"deleteServerConfirm": "確定要刪除此伺服器嗎?",
|
||||||
|
|||||||
@ -912,7 +912,6 @@
|
|||||||
"noServers": "Δεν έχουν ρυθμιστεί διακομιστές",
|
"noServers": "Δεν έχουν ρυθμιστεί διακομιστές",
|
||||||
"npx_list": {
|
"npx_list": {
|
||||||
"actions": "Ενέργειες",
|
"actions": "Ενέργειες",
|
||||||
"desc": "Αναζητήστε και προσθέστε πακέτα npm ως υπηρεσίες MCP",
|
|
||||||
"description": "Περιγραφή",
|
"description": "Περιγραφή",
|
||||||
"no_packages": "Δεν βρέθηκαν πακέτα",
|
"no_packages": "Δεν βρέθηκαν πακέτα",
|
||||||
"npm": "NPM",
|
"npm": "NPM",
|
||||||
@ -921,7 +920,6 @@
|
|||||||
"scope_required": "Παρακαλώ εισαγάγετε το σκοπό του npm",
|
"scope_required": "Παρακαλώ εισαγάγετε το σκοπό του npm",
|
||||||
"search": "Αναζήτηση",
|
"search": "Αναζήτηση",
|
||||||
"search_error": "Η αναζήτηση απέτυχε",
|
"search_error": "Η αναζήτηση απέτυχε",
|
||||||
"title": "Λίστα πακέτων NPX",
|
|
||||||
"usage": "Χρήση",
|
"usage": "Χρήση",
|
||||||
"version": "Έκδοση"
|
"version": "Έκδοση"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -912,7 +912,6 @@
|
|||||||
"noServers": "No se han configurado servidores",
|
"noServers": "No se han configurado servidores",
|
||||||
"npx_list": {
|
"npx_list": {
|
||||||
"actions": "Acciones",
|
"actions": "Acciones",
|
||||||
"desc": "Buscar y agregar paquetes npm como servicios MCP",
|
|
||||||
"description": "Descripción",
|
"description": "Descripción",
|
||||||
"no_packages": "No se encontraron paquetes",
|
"no_packages": "No se encontraron paquetes",
|
||||||
"npm": "NPM",
|
"npm": "NPM",
|
||||||
@ -921,7 +920,6 @@
|
|||||||
"scope_required": "Por favor ingrese el ámbito npm",
|
"scope_required": "Por favor ingrese el ámbito npm",
|
||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
"search_error": "Error de búsqueda",
|
"search_error": "Error de búsqueda",
|
||||||
"title": "Lista de paquetes NPX",
|
|
||||||
"usage": "Uso",
|
"usage": "Uso",
|
||||||
"version": "Versión"
|
"version": "Versión"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -912,7 +912,6 @@
|
|||||||
"noServers": "Aucun serveur configuré",
|
"noServers": "Aucun serveur configuré",
|
||||||
"npx_list": {
|
"npx_list": {
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"desc": "Rechercher et ajouter un package npm en tant que service MCP",
|
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"no_packages": "Aucun package trouvé",
|
"no_packages": "Aucun package trouvé",
|
||||||
"npm": "NPM",
|
"npm": "NPM",
|
||||||
@ -921,7 +920,6 @@
|
|||||||
"scope_required": "Veuillez entrer le scope npm",
|
"scope_required": "Veuillez entrer le scope npm",
|
||||||
"search": "Rechercher",
|
"search": "Rechercher",
|
||||||
"search_error": "La recherche a échoué",
|
"search_error": "La recherche a échoué",
|
||||||
"title": "Liste des packages NPX",
|
|
||||||
"usage": "Utilisation",
|
"usage": "Utilisation",
|
||||||
"version": "Version"
|
"version": "Version"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -912,7 +912,6 @@
|
|||||||
"noServers": "Nenhum servidor configurado",
|
"noServers": "Nenhum servidor configurado",
|
||||||
"npx_list": {
|
"npx_list": {
|
||||||
"actions": "Ações",
|
"actions": "Ações",
|
||||||
"desc": "Pesquise e adicione pacotes npm como serviço MCP",
|
|
||||||
"description": "Descrição",
|
"description": "Descrição",
|
||||||
"no_packages": "Nenhum pacote encontrado",
|
"no_packages": "Nenhum pacote encontrado",
|
||||||
"npm": "NPM",
|
"npm": "NPM",
|
||||||
@ -921,7 +920,6 @@
|
|||||||
"scope_required": "Insira o escopo npm",
|
"scope_required": "Insira o escopo npm",
|
||||||
"search": "Pesquisar",
|
"search": "Pesquisar",
|
||||||
"search_error": "Falha na pesquisa",
|
"search_error": "Falha na pesquisa",
|
||||||
"title": "Lista de Pacotes NPX",
|
|
||||||
"usage": "Uso",
|
"usage": "Uso",
|
||||||
"version": "Versão"
|
"version": "Versão"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
HolderOutlined,
|
HolderOutlined,
|
||||||
PaperClipOutlined,
|
PaperClipOutlined,
|
||||||
PauseCircleOutlined,
|
PauseCircleOutlined,
|
||||||
QuestionCircleOutlined,
|
|
||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
TranslationOutlined
|
TranslationOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
@ -43,7 +42,7 @@ import { Assistant, FileType, KnowledgeBase, KnowledgeItem, MCPServer, Message,
|
|||||||
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
|
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
|
||||||
import { getFilesFromDropEvent } from '@renderer/utils/input'
|
import { getFilesFromDropEvent } from '@renderer/utils/input'
|
||||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
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 TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import Logger from 'electron-log/renderer'
|
import Logger from 'electron-log/renderer'
|
||||||
@ -363,6 +362,15 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
mcpToolsButtonRef.current?.openQuickPanel()
|
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'),
|
label: isVisionModel(model) ? t('chat.input.upload') : t('chat.input.upload.document'),
|
||||||
description: '',
|
description: '',
|
||||||
@ -691,9 +699,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
textareaRef.current?.focus()
|
textareaRef.current?.focus()
|
||||||
})
|
})
|
||||||
|
|
||||||
useShortcut('clear_topic', () => {
|
useShortcut('clear_topic', clearTopic)
|
||||||
clearTopic()
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
|
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
|
||||||
@ -1094,6 +1100,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
enabledMCPs={enabledMCPs}
|
enabledMCPs={enabledMCPs}
|
||||||
toggelEnableMCP={toggelEnableMCP}
|
toggelEnableMCP={toggelEnableMCP}
|
||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
|
setInputValue={setText}
|
||||||
|
resizeTextArea={resizeTextArea}
|
||||||
/>
|
/>
|
||||||
<GenerateImageButton
|
<GenerateImageButton
|
||||||
model={model}
|
model={model}
|
||||||
@ -1114,17 +1122,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
/>
|
/>
|
||||||
<Tooltip placement="top" title={t('chat.input.clear', { Command: cleanTopicShortcut })} arrow>
|
<Tooltip placement="top" title={t('chat.input.clear', { Command: cleanTopicShortcut })} arrow>
|
||||||
<Popconfirm
|
<ToolbarButton type="text" onClick={clearTopic}>
|
||||||
title={t('chat.input.clear.content')}
|
<ClearOutlined style={{ fontSize: 17 }} />
|
||||||
placement="top"
|
</ToolbarButton>
|
||||||
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>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement="top" title={isExpended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
<Tooltip placement="top" title={isExpended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
||||||
<ToolbarButton type="text" onClick={onToggleExpended}>
|
<ToolbarButton type="text" onClick={onToggleExpended}>
|
||||||
|
|||||||
@ -1,28 +1,40 @@
|
|||||||
import { CodeOutlined, PlusOutlined } from '@ant-design/icons'
|
import { CodeOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
|
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
import { MCPServer } from '@renderer/types'
|
import { MCPPrompt, MCPServer } from '@renderer/types'
|
||||||
import { Tooltip } from 'antd'
|
import { Form, Input, Modal, Tooltip } from 'antd'
|
||||||
import { FC, useCallback, useImperativeHandle, useMemo } from 'react'
|
import { FC, useCallback, useImperativeHandle, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router'
|
||||||
|
|
||||||
export interface MCPToolsButtonRef {
|
export interface MCPToolsButtonRef {
|
||||||
openQuickPanel: () => void
|
openQuickPanel: () => void
|
||||||
|
openPromptList: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ref?: React.RefObject<MCPToolsButtonRef | null>
|
ref?: React.RefObject<MCPToolsButtonRef | null>
|
||||||
enabledMCPs: MCPServer[]
|
enabledMCPs: MCPServer[]
|
||||||
|
setInputValue: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
resizeTextArea: () => void
|
||||||
toggelEnableMCP: (server: MCPServer) => void
|
toggelEnableMCP: (server: MCPServer) => void
|
||||||
ToolbarButton: any
|
ToolbarButton: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const MCPToolsButton: FC<Props> = ({ ref, enabledMCPs, toggelEnableMCP, ToolbarButton }) => {
|
const MCPToolsButton: FC<Props> = ({
|
||||||
|
ref,
|
||||||
|
setInputValue,
|
||||||
|
resizeTextArea,
|
||||||
|
enabledMCPs,
|
||||||
|
toggelEnableMCP,
|
||||||
|
ToolbarButton
|
||||||
|
}) => {
|
||||||
const { activedMcpServers } = useMCPServers()
|
const { activedMcpServers } = useMCPServers()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const quickPanel = useQuickPanel()
|
const quickPanel = useQuickPanel()
|
||||||
const navigate = useNavigate()
|
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))
|
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])
|
}, [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(() => {
|
const handleOpenQuickPanel = useCallback(() => {
|
||||||
if (quickPanel.isVisible && quickPanel.symbol === 'mcp') {
|
if (quickPanel.isVisible && quickPanel.symbol === 'mcp') {
|
||||||
@ -66,7 +292,8 @@ const MCPToolsButton: FC<Props> = ({ ref, enabledMCPs, toggelEnableMCP, ToolbarB
|
|||||||
}, [openQuickPanel, quickPanel])
|
}, [openQuickPanel, quickPanel])
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
openQuickPanel
|
openQuickPanel,
|
||||||
|
openPromptList
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (activedMcpServers.length === 0) {
|
if (activedMcpServers.length === 0) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
|||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onNewContext: () => void
|
onNewContext: () => void
|
||||||
@ -16,12 +17,20 @@ const NewContextButton: FC<Props> = ({ onNewContext, ToolbarButton }) => {
|
|||||||
useShortcut('toggle_new_context', onNewContext)
|
useShortcut('toggle_new_context', onNewContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip placement="top" title={t('chat.input.new.context', { Command: newContextShortcut })} arrow>
|
<Container>
|
||||||
<ToolbarButton type="text" onClick={onNewContext}>
|
<Tooltip placement="top" title={t('chat.input.new.context', { Command: newContextShortcut })} arrow>
|
||||||
<PicCenterOutlined />
|
<ToolbarButton type="text" onClick={onNewContext}>
|
||||||
</ToolbarButton>
|
<PicCenterOutlined />
|
||||||
</Tooltip>
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export default NewContextButton
|
export default NewContextButton
|
||||||
|
|||||||
@ -255,15 +255,19 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
|||||||
if (now - lastMoveTime.current < 50) return
|
if (now - lastMoveTime.current < 50) return
|
||||||
lastMoveTime.current = now
|
lastMoveTime.current = now
|
||||||
|
|
||||||
const triggerWidth = 10
|
// Calculate if the mouse is in the trigger area
|
||||||
let rightOffset = 5
|
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) {
|
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 rightPosition = window.innerWidth - rightOffset - triggerWidth
|
||||||
const topPosition = window.innerHeight * 0.35
|
const topPosition = window.innerHeight * 0.3 // 30% from top
|
||||||
const height = window.innerHeight * 0.3
|
const height = window.innerHeight * 0.4 // 40% of window height
|
||||||
|
|
||||||
const isInTriggerArea =
|
const isInTriggerArea =
|
||||||
e.clientX > rightPosition &&
|
e.clientX > rightPosition &&
|
||||||
@ -403,32 +407,31 @@ const ButtonGroup = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(8px);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
`
|
`
|
||||||
|
|
||||||
const NavigationButton = styled(Button)`
|
const NavigationButton = styled(Button)`
|
||||||
width: 32px;
|
width: 28px;
|
||||||
height: 32px;
|
height: 28px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
transition: all 0.25s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--color-hover);
|
background-color: var(--color-hover);
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.anticon {
|
.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 { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Message } from '@renderer/types'
|
import { Message } from '@renderer/types'
|
||||||
import { Collapse, message as antdMessage, Modal, Tooltip } from 'antd'
|
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 { id, tool, status, response } = toolResponse
|
||||||
const isInvoking = status === 'invoking'
|
const isInvoking = status === 'invoking'
|
||||||
const isDone = status === 'done'
|
const isDone = status === 'done'
|
||||||
|
const hasError = isDone && response?.isError === true
|
||||||
const result = {
|
const result = {
|
||||||
params: tool.inputSchema,
|
params: tool.inputSchema,
|
||||||
response: toolResponse.response
|
response: toolResponse.response
|
||||||
@ -59,10 +60,15 @@ const MessageTools: FC<Props> = ({ message }) => {
|
|||||||
<MessageTitleLabel>
|
<MessageTitleLabel>
|
||||||
<TitleContent>
|
<TitleContent>
|
||||||
<ToolName>{tool.name}</ToolName>
|
<ToolName>{tool.name}</ToolName>
|
||||||
<StatusIndicator $isInvoking={isInvoking}>
|
<StatusIndicator $isInvoking={isInvoking} $hasError={hasError}>
|
||||||
{isInvoking ? t('message.tools.invoking') : t('message.tools.completed')}
|
{isInvoking
|
||||||
|
? t('message.tools.invoking')
|
||||||
|
: hasError
|
||||||
|
? t('message.tools.error')
|
||||||
|
: t('message.tools.completed')}
|
||||||
{isInvoking && <LoadingOutlined spin style={{ marginLeft: 6 }} />}
|
{isInvoking && <LoadingOutlined spin style={{ marginLeft: 6 }} />}
|
||||||
{isDone && <CheckOutlined style={{ marginLeft: 6 }} />}
|
{isDone && !hasError && <CheckOutlined style={{ marginLeft: 6 }} />}
|
||||||
|
{hasError && <WarningOutlined style={{ marginLeft: 6 }} />}
|
||||||
</StatusIndicator>
|
</StatusIndicator>
|
||||||
</TitleContent>
|
</TitleContent>
|
||||||
<ActionButtonsContainer>
|
<ActionButtonsContainer>
|
||||||
@ -195,8 +201,12 @@ const ToolName = styled.span`
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const StatusIndicator = styled.span<{ $isInvoking: boolean }>`
|
const StatusIndicator = styled.span<{ $isInvoking: boolean; $hasError?: boolean }>`
|
||||||
color: ${(props) => (props.$isInvoking ? 'var(--color-primary)' : 'var(--color-success, #52c41a)')};
|
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;
|
font-size: 11px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -39,42 +39,6 @@ interface MessagesProps {
|
|||||||
setActiveTopic: (topic: Topic) => void
|
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 Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
|
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(() => {
|
useEffect(() => {
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, scrollToBottom),
|
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, scrollToBottom),
|
||||||
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, async (data: Topic) => {
|
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, async (data: Topic) => {
|
||||||
const defaultTopic = getDefaultTopic(assistant.id)
|
window.modal.confirm({
|
||||||
|
title: t('chat.input.clear.title'),
|
||||||
if (data && data.id !== topic.id) {
|
content: t('chat.input.clear.content'),
|
||||||
await clearTopicMessages(data.id)
|
centered: true,
|
||||||
updateTopic({ ...data, name: defaultTopic.name } as Topic)
|
onOk: () => clearTopic(data)
|
||||||
return
|
})
|
||||||
}
|
|
||||||
|
|
||||||
await clearTopicMessages()
|
|
||||||
setDisplayMessages([])
|
|
||||||
const _topic = getTopic(assistant, topic.id)
|
|
||||||
if (_topic) {
|
|
||||||
updateTopic({ ..._topic, name: defaultTopic.name } as Topic)
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.COPY_TOPIC_IMAGE, async () => {
|
EventEmitter.on(EVENT_NAMES.COPY_TOPIC_IMAGE, async () => {
|
||||||
await captureScrollableDivAsBlob(containerRef, async (blob) => {
|
await captureScrollableDivAsBlob(containerRef, async (blob) => {
|
||||||
@ -282,11 +258,43 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoaderProps {
|
const computeDisplayMessages = (messages: Message[], startIndex: number, displayCount: number) => {
|
||||||
$loading: boolean
|
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;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@ -300,6 +308,7 @@ const LoaderContainer = styled.div<LoaderProps>`
|
|||||||
const ScrollContainer = styled.div`
|
const ScrollContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
|
margin-bottom: -20px; // 添加负的底部外边距来减少空间
|
||||||
`
|
`
|
||||||
|
|
||||||
interface ContainerProps {
|
interface ContainerProps {
|
||||||
@ -309,7 +318,7 @@ interface ContainerProps {
|
|||||||
const Container = styled(Scrollbar)<ContainerProps>`
|
const Container = styled(Scrollbar)<ContainerProps>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
padding: 10px 0 20px;
|
padding: 10px 0 10px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|||||||
@ -31,6 +31,8 @@ const Container = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
margin-top: -10px;
|
margin-top: -10px;
|
||||||
|
padding: 0;
|
||||||
|
min-height: auto;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Button = styled(AntdButton)<{ $theme: ThemeMode }>`
|
const Button = styled(AntdButton)<{ $theme: ThemeMode }>`
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
|
import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
|
||||||
import { Center, VStack } from '@renderer/components/Layout'
|
import { Center, VStack } from '@renderer/components/Layout'
|
||||||
import { EventEmitter } from '@renderer/services/EventService'
|
|
||||||
import { Alert, Button } from 'antd'
|
import { Alert, Button } from 'antd'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useNavigate } from 'react-router'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { SettingDescription, SettingRow, SettingSubtitle } from '..'
|
import { SettingDescription, SettingRow, SettingSubtitle } from '..'
|
||||||
@ -21,6 +21,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
|||||||
const [bunPath, setBunPath] = useState<string | null>(null)
|
const [bunPath, setBunPath] = useState<string | null>(null)
|
||||||
const [binariesDir, setBinariesDir] = useState<string | null>(null)
|
const [binariesDir, setBinariesDir] = useState<string | null>(null)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const checkBinaries = async () => {
|
const checkBinaries = async () => {
|
||||||
const uvExists = await window.api.isBinaryExist('uv')
|
const uvExists = await window.api.isBinaryExist('uv')
|
||||||
@ -78,7 +79,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
|||||||
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}
|
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}
|
||||||
className="nodrag"
|
className="nodrag"
|
||||||
color={installed ? 'green' : 'danger'}
|
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 { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
import { MCPServer, MCPTool } from '@renderer/types'
|
import { MCPPrompt, MCPServer, MCPTool } from '@renderer/types'
|
||||||
import { Button, Flex, Form, Input, Radio, Switch } from 'antd'
|
import { Button, Flex, Form, Input, Radio, Switch, Tabs } from 'antd'
|
||||||
import TextArea from 'antd/es/input/TextArea'
|
import TextArea from 'antd/es/input/TextArea'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useNavigate } from 'react-router'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
|
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
|
||||||
|
import MCPPromptsSection from './McpPrompt'
|
||||||
import MCPToolsSection from './McpTool'
|
import MCPToolsSection from './McpTool'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -40,6 +42,8 @@ const PipRegistry: Registry[] = [
|
|||||||
{ name: '腾讯云', url: 'https://mirrors.cloud.tencent.com/pypi/simple/' }
|
{ name: '腾讯云', url: 'https://mirrors.cloud.tencent.com/pypi/simple/' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
type TabKey = 'settings' | 'tools' | 'prompts'
|
||||||
|
|
||||||
const McpSettings: React.FC<Props> = ({ server }) => {
|
const McpSettings: React.FC<Props> = ({ server }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { deleteMCPServer, updateMCPServer } = useMCPServers()
|
const { deleteMCPServer, updateMCPServer } = useMCPServers()
|
||||||
@ -48,11 +52,15 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [isFormChanged, setIsFormChanged] = useState(false)
|
const [isFormChanged, setIsFormChanged] = useState(false)
|
||||||
const [loadingServer, setLoadingServer] = useState<string | null>(null)
|
const [loadingServer, setLoadingServer] = useState<string | null>(null)
|
||||||
|
const [activeTab, setActiveTab] = useState<TabKey>('settings')
|
||||||
|
|
||||||
const [tools, setTools] = useState<MCPTool[]>([])
|
const [tools, setTools] = useState<MCPTool[]>([])
|
||||||
|
const [prompts, setPrompts] = useState<MCPPrompt[]>([])
|
||||||
const [isShowRegistry, setIsShowRegistry] = useState(false)
|
const [isShowRegistry, setIsShowRegistry] = useState(false)
|
||||||
const [registry, setRegistry] = useState<Registry[]>()
|
const [registry, setRegistry] = useState<Registry[]>()
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const serverType: MCPServer['type'] = server.type || (server.baseUrl ? 'sse' : 'stdio')
|
const serverType: MCPServer['type'] = server.type || (server.baseUrl ? 'sse' : 'stdio')
|
||||||
setServerType(serverType)
|
setServerType(serverType)
|
||||||
@ -109,10 +117,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
setLoadingServer(server.id)
|
setLoadingServer(server.id)
|
||||||
const localTools = await window.api.mcp.listTools(server)
|
const localTools = await window.api.mcp.listTools(server)
|
||||||
setTools(localTools)
|
setTools(localTools)
|
||||||
// window.message.success(t('settings.mcp.toolsLoaded'))
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.message.error({
|
window.message.error({
|
||||||
content: t('settings.mcp.toolsLoadError') + formatError(error),
|
content: t('settings.mcp.tools.loadError') + ' ' + formatError(error),
|
||||||
key: 'mcp-tools-error'
|
key: 'mcp-tools-error'
|
||||||
})
|
})
|
||||||
} finally {
|
} 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(() => {
|
useEffect(() => {
|
||||||
if (server.isActive) {
|
if (server.isActive) {
|
||||||
fetchTools()
|
fetchTools()
|
||||||
|
fetchPrompts()
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [server.id, server.isActive])
|
}, [server.id, server.isActive])
|
||||||
@ -234,6 +260,7 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
await window.api.mcp.removeServer(server)
|
await window.api.mcp.removeServer(server)
|
||||||
deleteMCPServer(server.id)
|
deleteMCPServer(server.id)
|
||||||
window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
|
window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
|
||||||
|
navigate('/settings/mcp')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -264,6 +291,9 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
if (active) {
|
if (active) {
|
||||||
const localTools = await window.api.mcp.listTools(server)
|
const localTools = await window.api.mcp.listTools(server)
|
||||||
setTools(localTools)
|
setTools(localTools)
|
||||||
|
|
||||||
|
const localPrompts = await window.api.mcp.listPrompts(server)
|
||||||
|
setPrompts(localPrompts)
|
||||||
} else {
|
} else {
|
||||||
await window.api.mcp.stopServer(server)
|
await window.api.mcp.stopServer(server)
|
||||||
}
|
}
|
||||||
@ -309,35 +339,16 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
[server, updateMCPServer]
|
[server, updateMCPServer]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
const tabs = [
|
||||||
<SettingContainer>
|
{
|
||||||
<SettingGroup style={{ marginBottom: 0 }}>
|
key: 'settings',
|
||||||
<SettingTitle>
|
label: t('settings.mcp.tabs.general'),
|
||||||
<Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}>
|
children: (
|
||||||
<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 />
|
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
onValuesChange={() => setIsFormChanged(true)}
|
onValuesChange={() => setIsFormChanged(true)}
|
||||||
style={{
|
style={{
|
||||||
// height: 'calc(100vh - var(--navbar-height) - 315px)',
|
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
width: 'calc(100% + 10px)',
|
width: 'calc(100% + 10px)',
|
||||||
paddingRight: '10px'
|
paddingRight: '10px'
|
||||||
@ -440,7 +451,58 @@ const McpSettings: React.FC<Props> = ({ server }) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</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>
|
</SettingGroup>
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,15 +2,16 @@ import { EditOutlined, ExportOutlined, SearchOutlined } from '@ant-design/icons'
|
|||||||
import { NavbarRight } from '@renderer/components/app/Navbar'
|
import { NavbarRight } from '@renderer/components/app/Navbar'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import { isWindows } from '@renderer/config/constant'
|
import { isWindows } from '@renderer/config/constant'
|
||||||
import { EventEmitter } from '@renderer/services/EventService'
|
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useNavigate } from 'react-router'
|
||||||
|
|
||||||
import EditMcpJsonPopup from './EditMcpJsonPopup'
|
import EditMcpJsonPopup from './EditMcpJsonPopup'
|
||||||
import InstallNpxUv from './InstallNpxUv'
|
import InstallNpxUv from './InstallNpxUv'
|
||||||
|
|
||||||
export const McpSettingsNavbar = () => {
|
export const McpSettingsNavbar = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
const onClick = () => window.open('https://mcp.so/', '_blank')
|
const onClick = () => window.open('https://mcp.so/', '_blank')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -19,7 +20,7 @@ export const McpSettingsNavbar = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
onClick={() => EventEmitter.emit('mcp:npx-search')}
|
onClick={() => navigate('/settings/mcp/npx-search')}
|
||||||
icon={<SearchOutlined />}
|
icon={<SearchOutlined />}
|
||||||
className="nodrag"
|
className="nodrag"
|
||||||
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
|
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
|
||||||
|
|||||||
@ -112,7 +112,7 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
|
|||||||
</Flex>
|
</Flex>
|
||||||
{tool.description && (
|
{tool.description && (
|
||||||
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
|
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
|
||||||
{tool.description}
|
{tool.description.length > 100 ? `${tool.description.substring(0, 100)}...` : tool.description}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -138,7 +138,6 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
|
|||||||
|
|
||||||
const Section = styled.div`
|
const Section = styled.div`
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
padding-top: 8px;
|
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 { nanoid } from '@reduxjs/toolkit'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import npmLogo from '@renderer/assets/images/mcp/npm.svg'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { Center, HStack } from '@renderer/components/Layout'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
import { builtinMCPServers } from '@renderer/store/mcp'
|
import { builtinMCPServers } from '@renderer/store/mcp'
|
||||||
import { MCPServer } from '@renderer/types'
|
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 { npxFinder } from 'npx-scope-finder'
|
||||||
import { type FC, useEffect, useState } from 'react'
|
import { type FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled, { css } from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { SettingDivider, SettingGroup, SettingTitle } from '..'
|
|
||||||
|
|
||||||
interface SearchResult {
|
interface SearchResult {
|
||||||
name: string
|
name: string
|
||||||
@ -27,8 +25,9 @@ const npmScopes = ['@cherry', '@modelcontextprotocol', '@gongrzhe', '@mcpmarket'
|
|||||||
|
|
||||||
let _searchResults: SearchResult[] = []
|
let _searchResults: SearchResult[] = []
|
||||||
|
|
||||||
const NpxSearch: FC = () => {
|
const NpxSearch: FC<{
|
||||||
const { theme } = useTheme()
|
setSelectedMcpServer: (server: MCPServer) => void
|
||||||
|
}> = ({ setSelectedMcpServer }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { Text, Link } = Typography
|
const { Text, Link } = Typography
|
||||||
|
|
||||||
@ -36,7 +35,7 @@ const NpxSearch: FC = () => {
|
|||||||
const [npmScope, setNpmScope] = useState('@cherry')
|
const [npmScope, setNpmScope] = useState('@cherry')
|
||||||
const [searchLoading, setSearchLoading] = useState(false)
|
const [searchLoading, setSearchLoading] = useState(false)
|
||||||
const [searchResults, setSearchResults] = useState<SearchResult[]>(_searchResults)
|
const [searchResults, setSearchResults] = useState<SearchResult[]>(_searchResults)
|
||||||
const { addMCPServer } = useMCPServers()
|
const { addMCPServer, mcpServers } = useMCPServers()
|
||||||
|
|
||||||
_searchResults = searchResults
|
_searchResults = searchResults
|
||||||
|
|
||||||
@ -116,119 +115,134 @@ const NpxSearch: FC = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingGroup theme={theme} css={SettingGroupCss}>
|
<Container>
|
||||||
<div>
|
<Center>
|
||||||
<SettingTitle>
|
<Space direction="vertical" style={{ marginBottom: 20, width: 500 }}>
|
||||||
{t('settings.mcp.npx_list.title')} <Text type="secondary">{t('settings.mcp.npx_list.desc')}</Text>
|
<Center style={{ marginBottom: 20 }}>
|
||||||
</SettingTitle>
|
<img src={npmLogo} alt="npm" width={100} />
|
||||||
<SettingDivider />
|
</Center>
|
||||||
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
|
||||||
<Space.Compact style={{ width: '100%', marginBottom: 10 }}>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('settings.mcp.npx_list.scope_placeholder')}
|
placeholder={t('settings.mcp.npx_list.scope_placeholder')}
|
||||||
value={npmScope}
|
value={npmScope}
|
||||||
onChange={(e) => setNpmScope(e.target.value)}
|
onChange={(e) => setNpmScope(e.target.value)}
|
||||||
onPressEnter={() => handleNpmSearch(npmScope)}
|
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>
|
</Space.Compact>
|
||||||
<HStack alignItems="center" mt="-5px" mb="5px">
|
<HStack alignItems="center" justifyContent="center">
|
||||||
{npmScopes.map((scope) => (
|
{npmScopes.map((scope) => (
|
||||||
<Tag
|
<Tag
|
||||||
key={scope}
|
key={scope}
|
||||||
|
bordered={false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setNpmScope(scope)
|
setNpmScope(scope)
|
||||||
handleNpmSearch(scope)
|
handleNpmSearch(scope)
|
||||||
}}
|
}}
|
||||||
style={{ cursor: searchLoading ? 'not-allowed' : 'pointer' }}>
|
style={{
|
||||||
|
cursor: searchLoading ? 'not-allowed' : 'pointer',
|
||||||
|
borderRadius: 100,
|
||||||
|
backgroundColor: 'var(--color-background-mute)'
|
||||||
|
}}>
|
||||||
{scope}
|
{scope}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
</HStack>
|
</HStack>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</Center>
|
||||||
|
{searchLoading && (
|
||||||
<ResultList>
|
<Center>
|
||||||
{searchLoading ? (
|
<Spin />
|
||||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
</Center>
|
||||||
<Spin />
|
)}
|
||||||
</div>
|
{!searchLoading && (
|
||||||
) : (
|
<ResultList>
|
||||||
searchResults?.map((record) => (
|
{searchResults?.map((record) => {
|
||||||
<Card
|
const isInstalled = mcpServers.some((server) => server.name === record.name)
|
||||||
size="small"
|
return (
|
||||||
key={record.name}
|
<Card
|
||||||
title={
|
size="small"
|
||||||
<Typography.Title level={5} style={{ margin: 0 }} className="selectable">
|
key={record.name}
|
||||||
{record.name}
|
title={
|
||||||
</Typography.Title>
|
<Typography.Title level={5} style={{ margin: 0 }} className="selectable">
|
||||||
}
|
{record.name}
|
||||||
extra={
|
</Typography.Title>
|
||||||
<Flex>
|
}
|
||||||
<Tag bordered={false} color="processing">
|
extra={
|
||||||
v{record.version}
|
<Flex>
|
||||||
</Tag>
|
<Tag bordered={false} color="processing">
|
||||||
<Button
|
v{record.version}
|
||||||
type="text"
|
</Tag>
|
||||||
icon={<PlusOutlined />}
|
<Button
|
||||||
size="small"
|
type="text"
|
||||||
onClick={() => {
|
icon={
|
||||||
const buildInServer = builtinMCPServers.find((server) => server.name === record.name)
|
isInstalled ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <PlusOutlined />
|
||||||
|
|
||||||
if (buildInServer) {
|
|
||||||
addMCPServer(buildInServer)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
if (isInstalled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
addMCPServer({
|
const buildInServer = builtinMCPServers.find((server) => server.name === record.name)
|
||||||
id: nanoid(),
|
|
||||||
name: record.name,
|
if (buildInServer) {
|
||||||
description: `${record.description}\n\n${t('settings.mcp.npx_list.usage')}: ${record.usage}\n${t('settings.mcp.npx_list.npm')}: ${record.npmLink}`,
|
addMCPServer(buildInServer)
|
||||||
command: 'npx',
|
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
|
||||||
args: ['-y', record.fullName],
|
setSelectedMcpServer(buildInServer)
|
||||||
isActive: false,
|
return
|
||||||
type: record.type
|
}
|
||||||
})
|
|
||||||
}}
|
const newServer = {
|
||||||
/>
|
id: nanoid(),
|
||||||
</Flex>
|
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}`,
|
||||||
<Space direction="vertical" size="small">
|
command: 'npx',
|
||||||
<Text className="selectable">{record.description}</Text>
|
args: ['-y', record.fullName],
|
||||||
<Text type="secondary" className="selectable">
|
isActive: false,
|
||||||
{t('settings.mcp.npx_list.usage')}: {record.usage}
|
type: record.type
|
||||||
</Text>
|
}
|
||||||
<Link href={record.npmLink} target="_blank" rel="noopener noreferrer">
|
|
||||||
{record.npmLink}
|
addMCPServer(newServer)
|
||||||
</Link>
|
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' })
|
||||||
</Space>
|
setSelectedMcpServer(newServer)
|
||||||
</Card>
|
}}
|
||||||
))
|
/>
|
||||||
)}
|
</Flex>
|
||||||
</ResultList>
|
}>
|
||||||
</SettingGroup>
|
<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`
|
const Container = styled.div`
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
margin-bottom: 0;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const ResultList = styled.div`
|
const ResultList = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 8px;
|
gap: 16px;
|
||||||
width: calc(100% + 10px);
|
width: 100%;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default NpxSearch
|
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 { nanoid } from '@reduxjs/toolkit'
|
||||||
import DragableList from '@renderer/components/DragableList'
|
|
||||||
import IndicatorLight from '@renderer/components/IndicatorLight'
|
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||||
import { HStack, VStack } from '@renderer/components/Layout'
|
import { VStack } from '@renderer/components/Layout'
|
||||||
import ListItem from '@renderer/components/ListItem'
|
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||||
import { EventEmitter } from '@renderer/services/EventService'
|
|
||||||
import { MCPServer } from '@renderer/types'
|
import { MCPServer } from '@renderer/types'
|
||||||
import { Dropdown, MenuProps } from 'antd'
|
import { FC, useCallback, useEffect, useState } from 'react'
|
||||||
import { isEmpty } from 'lodash'
|
|
||||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
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 styled from 'styled-components'
|
||||||
|
|
||||||
import { SettingContainer } from '..'
|
import { SettingContainer, SettingTitle } from '..'
|
||||||
import InstallNpxUv from './InstallNpxUv'
|
import InstallNpxUv from './InstallNpxUv'
|
||||||
import McpSettings from './McpSettings'
|
import McpSettings from './McpSettings'
|
||||||
import NpxSearch from './NpxSearch'
|
import NpxSearch from './NpxSearch'
|
||||||
|
|
||||||
const MCPSettings: FC = () => {
|
const MCPSettings: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { mcpServers, addMCPServer, updateMcpServers, deleteMCPServer } = useMCPServers()
|
const { mcpServers, addMCPServer } = useMCPServers()
|
||||||
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(mcpServers[0])
|
const [selectedMcpServer, setSelectedMcpServer] = useState<MCPServer | null>(null)
|
||||||
const [route, setRoute] = useState<'npx-search' | 'mcp-install' | null>(null)
|
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
const location = useLocation()
|
||||||
const unsubs = [
|
const pathname = location.pathname
|
||||||
EventEmitter.on('mcp:npx-search', () => setRoute('npx-search')),
|
|
||||||
EventEmitter.on('mcp:mcp-install', () => setRoute('mcp-install'))
|
|
||||||
]
|
|
||||||
return () => unsubs.forEach((unsub) => unsub())
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onAddMcpServer = async () => {
|
const onAddMcpServer = useCallback(async () => {
|
||||||
const newServer = {
|
const newServer = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
name: t('settings.mcp.newServer'),
|
name: t('settings.mcp.newServer'),
|
||||||
@ -49,136 +40,228 @@ const MCPSettings: FC = () => {
|
|||||||
addMCPServer(newServer)
|
addMCPServer(newServer)
|
||||||
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
|
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
|
||||||
setSelectedMcpServer(newServer)
|
setSelectedMcpServer(newServer)
|
||||||
}
|
}, [addMCPServer, t])
|
||||||
|
|
||||||
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]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const _selectedMcpServer = mcpServers.find((server) => server.id === selectedMcpServer?.id)
|
const _selectedMcpServer = mcpServers.find((server) => server.id === selectedMcpServer?.id)
|
||||||
setSelectedMcpServer(_selectedMcpServer || mcpServers[0])
|
setSelectedMcpServer(_selectedMcpServer || mcpServers[0])
|
||||||
}, [mcpServers, route, selectedMcpServer])
|
}, [mcpServers, selectedMcpServer])
|
||||||
|
|
||||||
const MainContent = useMemo(() => {
|
useEffect(() => {
|
||||||
if (route === 'npx-search' || isEmpty(mcpServers)) {
|
// Check if the selected server still exists in the updated mcpServers list
|
||||||
return (
|
|
||||||
<SettingContainer theme={theme}>
|
|
||||||
<NpxSearch />
|
|
||||||
</SettingContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route === 'mcp-install') {
|
|
||||||
return (
|
|
||||||
<SettingContainer theme={theme}>
|
|
||||||
<InstallNpxUv />
|
|
||||||
</SettingContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (selectedMcpServer) {
|
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 />
|
const McpServersList = useCallback(
|
||||||
}, [mcpServers, route, selectedMcpServer, theme])
|
() => (
|
||||||
|
<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 (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<McpListContainer>
|
{!isHome && (
|
||||||
<McpList>
|
<BackButtonContainer>
|
||||||
<ListItem
|
<Link to="/settings/mcp">
|
||||||
key="add"
|
<BackButton>
|
||||||
title={t('settings.mcp.addServer')}
|
<ArrowLeftOutlined /> {t('common.back')}
|
||||||
active={false}
|
</BackButton>
|
||||||
onClick={onAddMcpServer}
|
</Link>
|
||||||
icon={<PlusOutlined />}
|
</BackButtonContainer>
|
||||||
titleStyle={{ fontWeight: 500 }}
|
)}
|
||||||
style={{ width: '100%', marginTop: -2 }}
|
<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}>
|
<Route
|
||||||
{(server: MCPServer) => (
|
path="mcp-install"
|
||||||
<Dropdown menu={{ items: getMenuItems(server) }} trigger={['contextMenu']} key={server.id}>
|
element={
|
||||||
<div>
|
<SettingContainer theme={theme}>
|
||||||
<ListItem
|
<InstallNpxUv />
|
||||||
key={server.id}
|
</SettingContainer>
|
||||||
title={server.name}
|
}
|
||||||
active={selectedMcpServer?.id === server.id}
|
/>
|
||||||
onClick={() => {
|
</Routes>
|
||||||
setSelectedMcpServer(server)
|
</MainContainer>
|
||||||
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}
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled(HStack)`
|
const Container = styled(VStack)`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
`
|
`
|
||||||
|
|
||||||
const McpListContainer = styled(VStack)`
|
const GridContainer = 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;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
height: calc(100vh - var(--navbar-height));
|
||||||
.iconfont {
|
padding: 20px;
|
||||||
color: var(--color-text-2);
|
`
|
||||||
line-height: 16px;
|
|
||||||
|
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 (
|
return (
|
||||||
<CustomCollapse
|
<CustomCollapse
|
||||||
key={i}
|
key={i}
|
||||||
defaultActiveKey={i >= 5 ? [] : ['1']}
|
defaultActiveKey={['1']}
|
||||||
|
styles={{ body: { padding: '0 10px' } }}
|
||||||
label={
|
label={
|
||||||
<Flex align="center" gap={10}>
|
<Flex align="center" gap={10}>
|
||||||
<span style={{ fontWeight: 600 }}>{group}</span>
|
<span style={{ fontWeight: 600 }}>{group}</span>
|
||||||
@ -233,13 +234,15 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}>
|
}>
|
||||||
<FlexColumn>
|
<FlexColumn style={{ margin: '10px 0' }}>
|
||||||
{modelGroups[group].map((model) => (
|
{modelGroups[group].map((model) => (
|
||||||
<FileItem
|
<FileItem
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isModelInProvider(provider, model.id)
|
backgroundColor: isModelInProvider(provider, model.id)
|
||||||
? 'rgba(0, 126, 0, 0.06)'
|
? '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}
|
key={model.id}
|
||||||
fileInfo={{
|
fileInfo={{
|
||||||
|
|||||||
@ -48,7 +48,7 @@ const SettingsPage: FC = () => {
|
|||||||
<Container>
|
<Container>
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter>
|
<NavbarCenter style={{ borderRight: 'none' }}>{t('settings.title')}</NavbarCenter>
|
||||||
{pathname === '/settings/mcp' && <McpSettingsNavbar />}
|
{pathname.includes('/settings/mcp') && <McpSettingsNavbar />}
|
||||||
</Navbar>
|
</Navbar>
|
||||||
<ContentContainer id="content-container">
|
<ContentContainer id="content-container">
|
||||||
<SettingMenus>
|
<SettingMenus>
|
||||||
@ -142,7 +142,7 @@ const SettingsPage: FC = () => {
|
|||||||
<Route path="provider" element={<ProvidersList />} />
|
<Route path="provider" element={<ProvidersList />} />
|
||||||
<Route path="model" element={<ModelSettings />} />
|
<Route path="model" element={<ModelSettings />} />
|
||||||
<Route path="web-search" element={<WebSearchSettings />} />
|
<Route path="web-search" element={<WebSearchSettings />} />
|
||||||
<Route path="mcp" element={<MCPSettings />} />
|
<Route path="mcp/*" element={<MCPSettings />} />
|
||||||
<Route path="general/*" element={<GeneralSettings />} />
|
<Route path="general/*" element={<GeneralSettings />} />
|
||||||
<Route path="display" element={<DisplaySettings />} />
|
<Route path="display" element={<DisplaySettings />} />
|
||||||
{showMiniAppSettings && <Route path="miniapps" element={<MiniAppSettings />} />}
|
{showMiniAppSettings && <Route path="miniapps" element={<MiniAppSettings />} />}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
|
|||||||
height: calc(100vh - var(--navbar-height));
|
height: calc(100vh - var(--navbar-height));
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
|
padding-bottom: 75px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
font-family: Ubuntu;
|
font-family: Ubuntu;
|
||||||
background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};
|
background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};
|
||||||
|
|||||||
@ -402,6 +402,34 @@ export interface MCPTool {
|
|||||||
inputSchema: MCPToolInputSchema
|
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 {
|
export interface MCPConfig {
|
||||||
servers: MCPServer[]
|
servers: MCPServer[]
|
||||||
}
|
}
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@ -3942,6 +3942,7 @@ __metadata:
|
|||||||
analytics: "npm:^0.8.16"
|
analytics: "npm:^0.8.16"
|
||||||
antd: "npm:^5.22.5"
|
antd: "npm:^5.22.5"
|
||||||
applescript: "npm:^1.0.0"
|
applescript: "npm:^1.0.0"
|
||||||
|
async-mutex: "npm:^0.5.0"
|
||||||
axios: "npm:^1.7.3"
|
axios: "npm:^1.7.3"
|
||||||
babel-plugin-styled-components: "npm:^2.1.4"
|
babel-plugin-styled-components: "npm:^2.1.4"
|
||||||
browser-image-compression: "npm:^2.0.2"
|
browser-image-compression: "npm:^2.0.2"
|
||||||
@ -4493,6 +4494,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"async@npm:^3.2.3":
|
||||||
version: 3.2.6
|
version: 3.2.6
|
||||||
resolution: "async@npm:3.2.6"
|
resolution: "async@npm:3.2.6"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user