diff --git a/backend/app/agent/factory/browser.py b/backend/app/agent/factory/browser.py index 4aa9f9a00..c825378f3 100644 --- a/backend/app/agent/factory/browser.py +++ b/backend/app/agent/factory/browser.py @@ -25,7 +25,10 @@ attach_remote_sub_agent_if_enabled, ) from app.agent.listen_chat_agent import logger -from app.agent.prompt import BROWSER_SYS_PROMPT +from app.agent.prompt import ( + BROWSER_SYS_PROMPT, + append_connected_app_mcp_notice, +) from app.agent.toolkit.human_toolkit import HumanToolkit from app.agent.toolkit.hybrid_browser_toolkit import HybridBrowserToolkit @@ -382,6 +385,7 @@ def browser_agent( now_str=NOW_STR, external_browser_notice=external_browser_notice, ) + system_message = append_connected_app_mcp_notice(system_message) system_message = attach_remote_sub_agent_if_enabled( options=options, agent_name=Agents.browser_agent, diff --git a/backend/app/agent/factory/developer.py b/backend/app/agent/factory/developer.py index 6cd1afbe2..53b6552e0 100644 --- a/backend/app/agent/factory/developer.py +++ b/backend/app/agent/factory/developer.py @@ -22,7 +22,10 @@ attach_remote_sub_agent_if_enabled, ) from app.agent.listen_chat_agent import logger -from app.agent.prompt import DEVELOPER_SYS_PROMPT +from app.agent.prompt import ( + DEVELOPER_SYS_PROMPT, + append_connected_app_mcp_notice, +) from app.agent.toolkit.human_toolkit import HumanToolkit # TODO: Remove NoteTakingToolkit and use TerminalToolkit instead @@ -128,6 +131,7 @@ async def developer_agent( working_directory=working_directory, now_str=NOW_STR, ) + system_message = append_connected_app_mcp_notice(system_message) system_message = attach_remote_sub_agent_if_enabled( options=options, agent_name=Agents.developer_agent, diff --git a/backend/app/agent/factory/document.py b/backend/app/agent/factory/document.py index 709e2f35d..83b973530 100644 --- a/backend/app/agent/factory/document.py +++ b/backend/app/agent/factory/document.py @@ -21,7 +21,10 @@ attach_remote_sub_agent_if_enabled, ) from app.agent.listen_chat_agent import logger -from app.agent.prompt import DOCUMENT_SYS_PROMPT +from app.agent.prompt import ( + DOCUMENT_SYS_PROMPT, + append_connected_app_mcp_notice, +) from app.agent.toolkit.excel_toolkit import ExcelToolkit from app.agent.toolkit.file_write_toolkit import FileToolkit from app.agent.toolkit.google_drive_mcp_toolkit import GoogleDriveMCPToolkit @@ -154,6 +157,7 @@ async def document_agent( working_directory=working_directory, now_str=NOW_STR, ) + system_message = append_connected_app_mcp_notice(system_message) system_message = attach_remote_sub_agent_if_enabled( options=options, agent_name=Agents.document_agent, diff --git a/backend/app/agent/factory/mcp.py b/backend/app/agent/factory/mcp.py index d066b1586..c5832c5bb 100644 --- a/backend/app/agent/factory/mcp.py +++ b/backend/app/agent/factory/mcp.py @@ -19,7 +19,7 @@ remote_sub_agent_enabled, ) from app.agent.listen_chat_agent import logger -from app.agent.prompt import MCP_SYS_PROMPT +from app.agent.prompt import MCP_SYS_PROMPT, append_connected_app_mcp_notice from app.agent.toolkit.human_toolkit import HumanToolkit from app.agent.toolkit.mcp_search_toolkit import McpSearchToolkit from app.agent.tools import get_mcp_tools @@ -73,7 +73,7 @@ async def mcp_agent(options: Chat): working_directory=working_directory, tools=tools, tool_names=tool_names, - system_message=MCP_SYS_PROMPT, + system_message=append_connected_app_mcp_notice(MCP_SYS_PROMPT), local_tool_description="local MCP or search tools", message_integration=message_integration, ) diff --git a/backend/app/agent/factory/multi_modal.py b/backend/app/agent/factory/multi_modal.py index 9b13c8432..56d244460 100644 --- a/backend/app/agent/factory/multi_modal.py +++ b/backend/app/agent/factory/multi_modal.py @@ -23,7 +23,10 @@ attach_remote_sub_agent_if_enabled, ) from app.agent.listen_chat_agent import logger -from app.agent.prompt import MULTI_MODAL_SYS_PROMPT +from app.agent.prompt import ( + MULTI_MODAL_SYS_PROMPT, + append_connected_app_mcp_notice, +) from app.agent.toolkit.audio_analysis_toolkit import AudioAnalysisToolkit from app.agent.toolkit.human_toolkit import HumanToolkit @@ -202,6 +205,7 @@ def multi_modal_agent( working_directory=working_directory, now_str=NOW_STR, ) + system_message = append_connected_app_mcp_notice(system_message) system_message = attach_remote_sub_agent_if_enabled( options=options, agent_name=Agents.multi_modal_agent, diff --git a/backend/app/agent/factory/question_confirm.py b/backend/app/agent/factory/question_confirm.py index 3461f5238..dba387810 100644 --- a/backend/app/agent/factory/question_confirm.py +++ b/backend/app/agent/factory/question_confirm.py @@ -13,7 +13,10 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= from app.agent.agent_model import agent_model -from app.agent.prompt import QUESTION_CONFIRM_SYS_PROMPT +from app.agent.prompt import ( + QUESTION_CONFIRM_SYS_PROMPT, + append_connected_app_mcp_notice, +) from app.agent.utils import NOW_STR from app.model.chat import Chat @@ -21,6 +24,8 @@ def question_confirm_agent(options: Chat): return agent_model( "question_confirm_agent", - QUESTION_CONFIRM_SYS_PROMPT.format(now_str=NOW_STR), + append_connected_app_mcp_notice( + QUESTION_CONFIRM_SYS_PROMPT.format(now_str=NOW_STR) + ), options, ) diff --git a/backend/app/agent/factory/single_agent.py b/backend/app/agent/factory/single_agent.py index ac5e3a747..229c50f4c 100644 --- a/backend/app/agent/factory/single_agent.py +++ b/backend/app/agent/factory/single_agent.py @@ -21,7 +21,10 @@ from app.agent.agent_model import agent_model from app.agent.factory.toolkit_assembler import assemble_single_agent_toolkits -from app.agent.prompt import SINGLE_AGENT_SYS_PROMPT +from app.agent.prompt import ( + SINGLE_AGENT_SYS_PROMPT, + append_connected_app_mcp_notice, +) from app.agent.utils import NOW_STR from app.hands.interface import IHands from app.model.chat import Chat @@ -70,6 +73,7 @@ async def single_agent( working_directory=working_directory, now_str=NOW_STR, ) + system_message = append_connected_app_mcp_notice(system_message) agent = agent_model( Agents.single_agent, diff --git a/backend/app/agent/factory/social_media.py b/backend/app/agent/factory/social_media.py index d1cb32349..6b42ea031 100644 --- a/backend/app/agent/factory/social_media.py +++ b/backend/app/agent/factory/social_media.py @@ -19,7 +19,10 @@ attach_remote_sub_agent_if_enabled, ) from app.agent.listen_chat_agent import logger -from app.agent.prompt import SOCIAL_MEDIA_SYS_PROMPT +from app.agent.prompt import ( + SOCIAL_MEDIA_SYS_PROMPT, + append_connected_app_mcp_notice, +) from app.agent.toolkit.google_calendar_toolkit import GoogleCalendarToolkit from app.agent.toolkit.google_gmail_mcp_toolkit import GoogleGmailMCPToolkit from app.agent.toolkit.human_toolkit import HumanToolkit @@ -110,6 +113,7 @@ async def social_media_agent(options: Chat): system_message = SOCIAL_MEDIA_SYS_PROMPT.format( working_directory=working_directory, now_str=NOW_STR ) + system_message = append_connected_app_mcp_notice(system_message) system_message = attach_remote_sub_agent_if_enabled( options=options, agent_name=Agents.social_media_agent, diff --git a/backend/app/agent/prompt.py b/backend/app/agent/prompt.py index 7ee1f905b..127551f0f 100644 --- a/backend/app/agent/prompt.py +++ b/backend/app/agent/prompt.py @@ -109,6 +109,29 @@ def build_remote_sub_agent_planning_notice() -> str: return REMOTE_SUB_AGENT_PLANNING_NOTICE +CONNECTED_APP_MCP_NOTICE = """\ + +When the user asks to query or operate a third-party app that may already be +connected, use available MCP or connector tools before browser/manual-login +flows. Examples include Clerk, Slack, Gmail, Notion, GitHub, Google Calendar, +Google Drive, Jira, Linear, Lark, Airtable, HubSpot, and similar SaaS apps. + +- Search or list the app's available actions first. +- Execute read/list/search/count actions directly when the user's intent is + clear and all required input is available or optional. +- Execute write/send/update/delete/admin actions only when the user explicitly + asked for that change and the target details are clear. +- Ask for dashboard links, API keys, or manual browser login only after the + connector tools show the app/account/action is unavailable or required input + is missing. + +""" + + +def append_connected_app_mcp_notice(system_message: str) -> str: + return f"{system_message.rstrip()}\n\n{CONNECTED_APP_MCP_NOTICE}" + + SOCIAL_MEDIA_SYS_PROMPT = """\ You are a Social Media Management Assistant with comprehensive capabilities across multiple platforms. You MUST use the `send_message_to_user` tool to diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index 2b977df8c..1ebc1f08b 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -49,7 +49,10 @@ remote_sub_agent_enabled, ) from app.agent.listen_chat_agent import ListenChatAgent -from app.agent.prompt import build_remote_sub_agent_planning_notice +from app.agent.prompt import ( + append_connected_app_mcp_notice, + build_remote_sub_agent_planning_notice, +) from app.agent.toolkit.human_toolkit import HumanToolkit from app.agent.toolkit.note_taking_toolkit import NoteTakingToolkit from app.agent.toolkit.skill_toolkit import SkillToolkit @@ -2867,6 +2870,9 @@ async def new_agent_model( For any date-related tasks, you MUST use this as \ the current date. """ + enhanced_description = append_connected_app_mcp_notice( + enhanced_description + ) message_integration = ToolkitMessageIntegration( message_handler=HumanToolkit( options.project_id, data.name diff --git a/src/api/connectors.ts b/src/api/connectors.ts new file mode 100644 index 000000000..dd3d41350 --- /dev/null +++ b/src/api/connectors.ts @@ -0,0 +1,338 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { + proxyFetchDelete, + proxyFetchGet, + proxyFetchPost, + proxyFetchPut, +} from '@/api/http'; + +export type ConnectorAuthType = + | 'no_auth' + | 'api_key' + | 'custom_credential' + | 'oauth2' + | string; + +export interface ConnectorCredentialField { + key: string; + label: string; + inputType: 'text' | 'password' | 'textarea' | 'json' | string; + required: boolean; + secret: boolean; + placeholder?: string | null; + description?: string | null; +} + +export interface ConnectorAuthDefinition { + type: ConnectorAuthType; + label?: string | null; + placeholder?: string | null; + description?: string | null; + extraFields?: ConnectorCredentialField[]; + fields?: ConnectorCredentialField[]; + scopes?: string[]; + clientConfigFields?: ConnectorCredentialField[]; +} + +export interface ConnectorAction { + id?: string; + name?: string; + description?: string; +} + +export interface ConnectorConnection { + id?: string | null; + service: string; + connectionName: string; + authType: ConnectorAuthType; + configured: boolean; + virtual: boolean; + default: boolean; + profile?: { + displayName?: string | null; + grantedScopes?: string[]; + } | null; +} + +export interface ConnectorProvider { + service: string; + displayName?: string; + description?: string | null; + iconUrl?: string | null; + homepageUrl?: string | null; + categories?: string[]; + authTypes?: ConnectorAuthType[]; + auth?: ConnectorAuthDefinition[]; + action_count?: number; + locally_executable_action_count?: number; + catalog_only_action_count?: number; + recommended?: boolean; + sort_rank?: number | null; + connection?: ConnectorConnection | null; + actions?: ConnectorAction[]; +} + +export interface ConnectorProvidersResponse { + enabled: boolean; + source: 'connector_gateway'; + provider_count: number; + filtered_count: number; + connected_count: number; + page: number; + page_size: number; + total_pages: number; + providers: ConnectorProvider[]; +} + +export interface ConnectorProviderResponse { + enabled: boolean; + source: 'connector_gateway'; + provider: ConnectorProvider; +} + +export interface ConnectProviderRequest { + auth_type: ConnectorAuthType; + values?: Record; + connection_name?: string; +} + +export interface ConnectorOAuthAuthorization { + service: string; + authorizationUrl: string; + state?: string; +} + +export interface FetchConnectorProvidersOptions { + page?: number; + pageSize?: number; + query?: string; +} + +export function providerLabel(provider: ConnectorProvider): string { + return provider.displayName || provider.service; +} + +export function isConnectedProvider( + provider: ConnectorProvider | null | undefined +): boolean { + const connection = provider?.connection; + return connection?.configured === true && connection.virtual !== true; +} + +export interface FetchConnectorProvidersRequestOptions { + /** Skip the short-lived list cache and force a network fetch. */ + bypassCache?: boolean; +} + +const PROVIDERS_LIST_CACHE_TTL_MS = 60_000; + +type ProvidersListCacheEntry = { + expiresAt: number; + data: ConnectorProvidersResponse; +}; + +const providersListCache = new Map(); +const providersListInflight = new Map< + string, + Promise +>(); + +function providersListCacheKey( + options: FetchConnectorProvidersOptions = {} +): string { + return [ + options.page || 1, + options.pageSize || 24, + options.query?.trim() || '', + ].join('::'); +} + +function normalizeProvidersResponse( + response: any, + options: FetchConnectorProvidersOptions = {} +): ConnectorProvidersResponse { + const providers = Array.isArray(response?.providers) + ? response.providers + : []; + const providerCount = + typeof response?.provider_count === 'number' + ? response.provider_count + : providers.length; + const filteredCount = + typeof response?.filtered_count === 'number' + ? response.filtered_count + : providerCount; + const pageSize = + typeof response?.page_size === 'number' + ? response.page_size + : options.pageSize || 24; + return { + enabled: response?.enabled === true, + source: 'connector_gateway', + provider_count: providerCount, + filtered_count: filteredCount, + connected_count: + typeof response?.connected_count === 'number' + ? response.connected_count + : 0, + page: + typeof response?.page === 'number' ? response.page : options.page || 1, + page_size: pageSize, + total_pages: + typeof response?.total_pages === 'number' + ? response.total_pages + : Math.max(1, Math.ceil(filteredCount / pageSize)), + providers, + }; +} + +/** Synchronous cache read for instant UI hydration. */ +export function getCachedConnectorProviders( + options: FetchConnectorProvidersOptions = {} +): ConnectorProvidersResponse | null { + const entry = providersListCache.get(providersListCacheKey(options)); + if (!entry || entry.expiresAt <= Date.now()) return null; + return entry.data; +} + +export function invalidateConnectorProvidersCache(): void { + providersListCache.clear(); + providersListInflight.clear(); +} + +/** Warm the list cache without waiting for dialog open. */ +export function prefetchConnectorProviders( + options: FetchConnectorProvidersOptions = {} +): Promise { + return fetchConnectorProviders(options); +} + +export async function fetchConnectorProviders( + options: FetchConnectorProvidersOptions = {}, + requestOptions: FetchConnectorProvidersRequestOptions = {} +): Promise { + const cacheKey = providersListCacheKey(options); + if (!requestOptions.bypassCache) { + const cached = getCachedConnectorProviders(options); + if (cached) return cached; + } + // Always coalesce concurrent identical requests, even when bypassing cache. + const inflight = providersListInflight.get(cacheKey); + if (inflight) return inflight; + + const request = (async () => { + const params: Record = { + page: options.page || 1, + page_size: options.pageSize || 24, + }; + const query = options.query?.trim(); + if (query) { + params.q = query; + } + const response = await proxyFetchGet( + '/api/v1/connectors/providers', + params + ); + const normalized = normalizeProvidersResponse(response, options); + providersListCache.set(cacheKey, { + expiresAt: Date.now() + PROVIDERS_LIST_CACHE_TTL_MS, + data: normalized, + }); + return normalized; + })(); + + providersListInflight.set(cacheKey, request); + try { + return await request; + } finally { + if (providersListInflight.get(cacheKey) === request) { + providersListInflight.delete(cacheKey); + } + } +} + +/** Fetch every provider page and return only connected providers. */ +export async function fetchConnectedProviders(): Promise { + const pageSize = 60; + const first = await fetchConnectorProviders({ page: 1, pageSize }); + let providers = first.providers; + const connectedPages = Math.ceil( + first.connected_count / (first.page_size || pageSize) + ); + for (let page = 2; page <= connectedPages; page += 1) { + const response = await fetchConnectorProviders({ + page, + pageSize: first.page_size || pageSize, + }); + providers = providers.concat(response.providers); + } + const unique = new Map( + providers.map((provider) => [provider.service, provider]) + ); + return Array.from(unique.values()).filter(isConnectedProvider); +} + +export async function fetchConnectorProvider( + service: string +): Promise { + const response = await proxyFetchGet( + `/api/v1/connectors/providers/${encodeURIComponent(service)}` + ); + return { + enabled: response?.enabled === true, + source: 'connector_gateway', + provider: response?.provider, + }; +} + +export async function connectProvider( + service: string, + request: ConnectProviderRequest +) { + return proxyFetchPut( + `/api/v1/connectors/connections/${encodeURIComponent(service)}`, + request + ); +} + +export async function disconnectProvider( + service: string, + connectionName?: string +) { + return proxyFetchDelete( + `/api/v1/connectors/connections/${encodeURIComponent(service)}`, + connectionName ? { connection_name: connectionName } : undefined + ); +} + +export async function createConnectorOAuthAuthorization( + service: string, + connectionName?: string +): Promise { + const response = await proxyFetchPost( + '/api/v1/connectors/oauth/authorizations', + { + service, + connection_name: connectionName, + } + ); + const authorization = response?.authorization; + return { + service: authorization?.service || service, + authorizationUrl: authorization?.authorizationUrl || '', + state: authorization?.state, + }; +} diff --git a/src/components/AddWorker/ToolSelect.tsx b/src/components/AddWorker/ToolSelect.tsx index 2fbe8ad89..930cef73a 100644 --- a/src/components/AddWorker/ToolSelect.tsx +++ b/src/components/AddWorker/ToolSelect.tsx @@ -22,6 +22,7 @@ import { } from '@/api/http'; import IntegrationList from '@/components/Dashboard/IntegrationList'; import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; import { useIntegrationManagement, type IntegrationItem, @@ -42,6 +43,7 @@ import { useState, } from 'react'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; import { Checkbox } from '../ui/checkbox'; import { Textarea } from '../ui/textarea'; import { TooltipSimple } from '../ui/tooltip'; @@ -258,6 +260,7 @@ const ToolSelect = forwardRef< const host = useHost(); const electronAPI = host?.electronAPI; const { t } = useTranslation(); + const navigate = useNavigate(); // state management - remove internal selected state, use parent passed initialSelectedTools const [keyword, setKeyword] = useState(''); const { email } = useAuthStore(); @@ -783,36 +786,22 @@ const ToolSelect = forwardRef< }); }, [integrations, webInstalled, keyword]); - const webNotConnectedItems = useMemo(() => { + // Align with the Connectors page: only connected built-ins and enabled + // custom MCPs are selectable as agent tools. + const ownPicks = useMemo(() => { const kw = keyword.trim().toLowerCase(); - return integrations - .filter((i: IntegrationItem) => !webInstalled[i.key]) - .filter((i: IntegrationItem) => { + return userMcpList + .filter((opt) => Number(opt.status) === 1) + .filter((opt) => { if (!kw) return true; - const descStr = typeof i.desc === 'string' ? i.desc.toLowerCase() : ''; - return ( - (i.key || '').toLowerCase().includes(kw) || - (i.name || '').toLowerCase().includes(kw) || - descStr.includes(kw) - ); + const name = String(opt.mcp_name || '').toLowerCase(); + const desc = String(opt.mcp_desc || '').toLowerCase(); + const key = String(opt.mcp_key || '').toLowerCase(); + return name.includes(kw) || desc.includes(kw) || key.includes(kw); }); - }, [integrations, webInstalled, keyword]); - - const ownPicks = useMemo(() => { - const kw = keyword.trim().toLowerCase(); - return userMcpList.filter((opt) => { - if (!kw) return true; - const name = String(opt.mcp_name || '').toLowerCase(); - const desc = String(opt.mcp_desc || '').toLowerCase(); - const key = String(opt.mcp_key || '').toLowerCase(); - return name.includes(kw) || desc.includes(kw) || key.includes(kw); - }); }, [userMcpList, keyword]); - const listHasItems = - webConnectedItems.length > 0 || - webNotConnectedItems.length > 0 || - ownPicks.length > 0; + const listHasItems = webConnectedItems.length > 0 || ownPicks.length > 0; const showSearchPlaceholder = keyword.length === 0 && (initialSelectedTools?.length ?? 0) === 0; @@ -944,29 +933,21 @@ const ToolSelect = forwardRef< )} - {webNotConnectedItems.length > 0 && ( -
-
- {t('setting.mcp-sidebar-not-connected')} -
- -
- )} ) : ( -

- {t('dashboard.no-results')} -

+
+

+ {t('dashboard.no-results')} +

+ +
)} diff --git a/src/components/ChatBox/BottomBox/PickerPanel.tsx b/src/components/ChatBox/BottomBox/PickerPanel.tsx index 7a19e4b2b..4ce5cc3d0 100644 --- a/src/components/ChatBox/BottomBox/PickerPanel.tsx +++ b/src/components/ChatBox/BottomBox/PickerPanel.tsx @@ -12,9 +12,14 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +import { fetchConnectedProviders, providerLabel } from '@/api/connectors'; import { proxyFetchGet } from '@/api/http'; import ellipseIcon from '@/assets/mcp/Ellipse-25.svg'; import { Button } from '@/components/ui/button'; +import { + useIntegrationManagement, + type IntegrationItem, +} from '@/hooks/useIntegrationManagement'; import { integrationLeadingIconUrl } from '@/lib/connectorIcons'; import { RICH_CONNECTOR_STYLE_CLASSES, @@ -24,6 +29,7 @@ import { } from '@/lib/richText'; import { skillNameToDirName } from '@/lib/skillToolkit'; import { cn } from '@/lib/utils'; +import { useServerCapabilityStore } from '@/store/serverCapabilityStore'; import { useSkillsStore } from '@/store/skillsStore'; import { Check, Plus, Wrench } from 'lucide-react'; import { Fragment, useEffect, useMemo, useState, type ReactNode } from 'react'; @@ -38,6 +44,8 @@ export interface PickerItem { id: string; name: string; token: string; + /** Provider icon URL for hosted connector items. */ + iconUrl?: string; } /** A labelled section within a picker (e.g. built-in vs. your own connectors). */ @@ -205,9 +213,10 @@ interface WiredPickerPanelProps { const EXCLUDED_BUILTIN_CONNECTORS = ['Search', 'RAG']; /** - * Full connector list matching the Connectors settings page: built-in - * integrations (`/api/v1/config/info`) plus the user's own MCPs - * (`/api/v1/mcp/users`), shown as two labelled sections. + * Connected connectors only, matching the Connectors page sidebar: connected + * hosted connectors (when the Connector Gateway is enabled), connected built-in + * integrations (`/api/v1/config/info` + configs), and the user's enabled MCPs + * (`/api/v1/mcp/users`), shown as labelled sections. */ export function ConnectorPickerPanel({ inputValue, @@ -215,10 +224,27 @@ export function ConnectorPickerPanel({ }: WiredPickerPanelProps) { const { t } = useTranslation(); const navigate = useNavigate(); - const [builtIn, setBuiltIn] = useState([]); + const [builtInItems, setBuiltInItems] = useState([]); + const [openItems, setOpenItems] = useState([]); const [yourMcps, setYourMcps] = useState([]); const [loading, setLoading] = useState(true); + const capabilities = useServerCapabilityStore((state) => state.capabilities); + const capabilityStatus = useServerCapabilityStore((state) => state.status); + const fetchCapabilities = useServerCapabilityStore( + (state) => state.fetchCapabilities + ); + const gatewayEnabled = + capabilities.features.connector_gateway.enabled === true; + + // Reuse the Connectors page's connected-state rules (OAuth tokens, config + // groups, Google Search defaults) instead of duplicating them here. + const { installed } = useIntegrationManagement(builtInItems); + + useEffect(() => { + void fetchCapabilities(); + }, [fetchCapabilities]); + useEffect(() => { let cancelled = false; Promise.allSettled([ @@ -232,13 +258,15 @@ export function ConnectorPickerPanel({ infoRes.value && typeof infoRes.value === 'object' ) { - setBuiltIn( + setBuiltInItems( Object.keys(infoRes.value) .filter((key) => !EXCLUDED_BUILTIN_CONNECTORS.includes(key)) .map((key) => ({ - id: `builtin-${key}`, + key, name: key, - token: connectorNameToToken(key), + desc: '', + env_vars: [], + onInstall: () => undefined, })) ); } @@ -247,11 +275,13 @@ export function ConnectorPickerPanel({ ? usersRes.value : (usersRes.value?.items ?? []); setYourMcps( - list.map((item: { id: number; mcp_name: string }) => ({ - id: `user-${item.id}`, - name: item.mcp_name, - token: connectorNameToToken(item.mcp_name), - })) + list + .filter((item: { status?: number }) => Number(item.status) === 1) + .map((item: { id: number; mcp_name: string }) => ({ + id: `user-${item.id}`, + name: item.mcp_name, + token: connectorNameToToken(item.mcp_name), + })) ); } }) @@ -263,7 +293,46 @@ export function ConnectorPickerPanel({ }; }, []); + useEffect(() => { + if (capabilityStatus !== 'ready' || !gatewayEnabled) { + setOpenItems([]); + return; + } + let cancelled = false; + fetchConnectedProviders() + .then((providers) => { + if (cancelled) return; + setOpenItems( + providers.map((provider) => ({ + id: `open-${provider.service}`, + name: providerLabel(provider), + token: connectorNameToToken(providerLabel(provider)), + iconUrl: provider.iconUrl || undefined, + })) + ); + }) + .catch(() => { + if (!cancelled) setOpenItems([]); + }); + return () => { + cancelled = true; + }; + }, [capabilityStatus, gatewayEnabled]); + + const builtIn = useMemo( + () => + builtInItems + .filter((item) => installed[item.key]) + .map((item) => ({ + id: `builtin-${item.key}`, + name: item.name, + token: connectorNameToToken(item.key), + })), + [builtInItems, installed] + ); + const groups: PickerGroup[] = [ + { id: 'open', label: t('connectors.gateway-connectors'), items: openItems }, { id: 'builtin', label: t('setting.mcp-sidebar-built-in'), @@ -289,6 +358,13 @@ export function ConnectorPickerPanel({ )} renderLogo={(item) => { + if (item.id.startsWith('open-')) { + return item.iconUrl ? ( + + ) : ( + + ); + } if (!item.id.startsWith('builtin-')) { return ( diff --git a/src/i18n/locales/ar/connectors.json b/src/i18n/locales/ar/connectors.json new file mode 100644 index 000000000..cec69c494 --- /dev/null +++ b/src/i18n/locales/ar/connectors.json @@ -0,0 +1,132 @@ +{ + "title": "الموصلات", + "search-placeholder": "البحث في موصلاتك", + "browse": "تصفح الموصلات", + "add-custom": "إضافة موصل مخصص", + "retry": "إعادة المحاولة", + "your-connectors": "موصلاتك", + "no-matching": "لا توجد موصلات مطابقة.", + "count-one": "موصل", + "count-other": "موصلات", + "recommended": "موصلات موصى بها لك.", + "no-recommended": "لا توجد موصلات موصى بها متاحة.", + "gateway-unavailable": "الموصلات غير متاحة.", + "gateway-unavailable-desc": "لم يتم تمكين Connector Gateway في هذا النشر.", + "gateway-connectors": "الموصلات", + "source-open": "السحابة", + "source-built-in": "مدمج", + "source-local": "محلي", + "source-remote": "بعيد", + "load-built-in-failed": "تعذر تحميل الموصلات المدمجة", + "load-custom-failed": "تعذر تحميل الموصلات المخصصة", + "load-gateway-failed": "تعذر تحميل الموصلات", + "disconnected": "تم قطع اتصال {{name}}", + "disconnect-failed": "تعذر قطع اتصال الموصل", + "update-failed": "تعذر تحديث الموصل", + "status-save-failed-enabled": "تم تمكين الموصل، ولكن تعذر حفظ حالته. حدّث الصفحة وحاول مرة أخرى.", + "status-save-failed-disabled": "تم تعطيل الموصل، ولكن تعذر حفظ حالته. حدّث الصفحة وحاول مرة أخرى.", + "updated": "تم تحديث الموصل", + "save-failed": "تعذر حفظ الموصل", + "deleted": "تم حذف الموصل", + "delete-failed": "تعذر حذف الموصل", + "enable-connector": "تمكين الموصل", + "disable-connector": "تعطيل الموصل", + "more-actions": "المزيد من الإجراءات", + "open": "فتح", + "edit": "تعديل", + "delete": "حذف", + "connected-account": "الحساب المتصل", + "supported-actions": "الإجراءات المدعومة", + "supported-actions-count": "{{num}} من الإجراءات المدعومة", + "show-more": "عرض المزيد", + "show-less": "عرض أقل", + "provider-website": "موقع المزود", + "built-in-title": "موصل توافق محلي", + "built-in-desc": "يستخدم هذا الموصل بيئة التكامل المحلية في Eigent. يُعد Connector Gateway السوق الأساسي للتكاملات المستضافة الجديدة.", + "configuration": "الإعداد: {{vars}}", + "status": "الحالة", + "active": "نشط", + "disabled": "معطّل", + "server-url": "عنوان URL للخادم", + "not-configured": "غير معدّ", + "command": "الأمر", + "arguments": "الوسائط", + "requires": "يتطلب {{vars}}", + "notion-desc": "اتصل بمساحة عمل Notion.", + "google-calendar-desc": "إدارة أحداث Google Calendar وجداوله.", + "generic-desc": "ربط {{name}} بـ Eigent.", + "google-search": "بحث Google", + "google-search-desc": "ربط Google Custom Search بمهام التصفح والبحث.", + "notion-install-failed": "تعذر تثبيت Notion", + "google-calendar-install-failed": "تعذر تثبيت Google Calendar", + "search-connectors": "البحث عن موصلات", + "auth-api-key": "مفتاح API", + "auth-credential": "بيانات الاعتماد", + "auth-oauth": "OAuth", + "auth-none": "دون مصادقة", + "unnamed-action": "إجراء بلا اسم", + "try-again": "حاول مرة أخرى", + "no-gateway-found": "لم يتم العثور على موصلات.", + "no-built-in-found": "لم يتم العثور على موصلات مدمجة.", + "loading": "جارٍ التحميل…", + "updating": "جارٍ التحديث…", + "loading-more": "جارٍ تحميل المزيد…", + "new": "جديد", + "installed": "مثبّت", + "local-integration": "تكامل محلي", + "authentication": "المصادقة", + "oauth-title": "الاتصال باستخدام OAuth", + "oauth-desc": "سيفتح Eigent صفحة تفويض المزود. ستُكمل نافذة الحوار هذه التثبيت عند اكتمال التفويض.", + "oauth-scopes": "الأذونات المطلوبة: {{scopes}}", + "no-auth-desc": "لا يتطلب هذا الموصل بيانات اعتماد.", + "waiting-authorization": "في انتظار اكتمال التفويض…", + "authorization-started": "بدأ التفويض", + "installed-toast": "تم تثبيت {{name}}", + "no-authorization-url": "لم يُرجع الموصل عنوان URL للتفويض", + "authorization-pending": "لا يزال التفويض معلقًا. أكمله في نافذة المزود، ثم حاول مرة أخرى.", + "install-failed": "تعذر تثبيت الموصل", + "detail-load-failed": "تعذر تحميل تفاصيل الموصل", + "authorization-incomplete": "لم يكتمل التفويض بعد. أكمله في نافذة المزود ثم حدّث الحالة مرة أخرى.", + "refresh-status-failed": "تعذر تحديث حالة الموصل", + "field-required": "الحقل {{field}} مطلوب", + "complete-authorization": "أكمل التفويض في نافذة المزود.", + "refresh-status": "تحديث الحالة", + "built-in-auth-desc": "يفتح التثبيت مسار تفويض المزود في نافذة منفصلة.", + "built-in-generic-desc": "تكامل Eigent محلي.", + "cancel": "إلغاء", + "install": "تثبيت", + "installing": "جارٍ التثبيت…", + "save": "حفظ", + "enter-value": "أدخل {{field}}", + "custom-title": "إضافة موصل مخصص", + "custom-subtitle": "ثبّت خادم MCP محليًا أو بعيدًا تثق به.", + "custom-warning": "يمكن لخوادم MCP المخصصة تنفيذ الأوامر أو الوصول إلى خدمات بعيدة. ثبّت الإعدادات التي تثق بها فقط.", + "local-json-desc": "أضف خادم MCP محليًا من خلال تقديم إعداد JSON صالح.", + "learn-more": "معرفة المزيد", + "connector-name": "اسم الموصل", + "remote-name-placeholder": "خادم MCP البعيد الخاص بي", + "remote-url": "عنوان URL لخادم MCP البعيد", + "remote-url-note": "استخدم HTTPS للخوادم الموجودة خارج شبكة محلية موثوقة.", + "invalid-json": "JSON غير صالح: {{message}}", + "json-format-error": "خطأ في تنسيق JSON: {{message}}", + "parse-failed": "فشل التحليل", + "missing-mcp-servers": "يجب أن يحتوي الإعداد على كائن mcpServers", + "add-at-least-one": "أضف خادم MCP واحدًا على الأقل", + "already-exists": "{{name}} موجود بالفعل", + "name-required": "اسم الموصل مطلوب", + "invalid-remote-url": "أدخل عنوان URL صالحًا لخادم MCP بعيد", + "remote-url-protocol": "يجب أن تستخدم عناوين URL لخوادم MCP البعيدة بروتوكول HTTP أو HTTPS", + "remote-missing-id": "تم إنشاء الموصل البعيد دون معرّف", + "custom-installed": "الموصلات المخصصة المثبّتة: {{num}}", + "install-custom-failed": "تعذر تثبيت الموصل المخصص", + "env-key-value": "متغيرات البيئة (مفتاح-قيمة)", + "value-placeholder": "القيمة", + "configuration-title": "الإعداد", + "google-api-key": "مفتاح Google API", + "google-api-key-placeholder": "أدخل مفتاح Google API من Google Cloud Console", + "google-api-key-note": "تعرّف على كيفية الحصول على مفتاح Google API ← https://developers.google.com/custom-search/v1/overview", + "search-engine-id": "معرّف محرك البحث", + "search-engine-id-placeholder": "أدخل معرّف محرك البحث المخصص المرتبط بمفتاح API", + "google-search-custom-desc": "اتصل بـ Google Custom Search. يتطلب مفتاح Google API ومعرّف محرك بحث مخصص (CSE).", + "google-search-default-desc": "بحث Google مفعّل افتراضيًا. لا يلزم مفتاح API." +} diff --git a/src/i18n/locales/ar/index.ts b/src/i18n/locales/ar/index.ts index efa753446..ae235fb7b 100644 --- a/src/i18n/locales/ar/index.ts +++ b/src/i18n/locales/ar/index.ts @@ -14,6 +14,7 @@ import agents from './agents.json'; import chat from './chat.json'; +import connectors from './connectors.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; import setting from './setting.json'; @@ -26,6 +27,7 @@ export default { dashboard, workforce, chat, + connectors, setting, update, triggers, diff --git a/src/i18n/locales/de/connectors.json b/src/i18n/locales/de/connectors.json new file mode 100644 index 000000000..44c847cf5 --- /dev/null +++ b/src/i18n/locales/de/connectors.json @@ -0,0 +1,132 @@ +{ + "title": "Konnektoren", + "search-placeholder": "Deine Konnektoren durchsuchen", + "browse": "Konnektoren durchsuchen", + "add-custom": "Benutzerdefinierten hinzufügen", + "retry": "Erneut versuchen", + "your-connectors": "Deine Konnektoren", + "no-matching": "Keine passenden Konnektoren.", + "count-one": "Konnektor", + "count-other": "Konnektoren", + "recommended": "Für dich empfohlene Konnektoren.", + "no-recommended": "Keine empfohlenen Konnektoren verfügbar.", + "gateway-unavailable": "Connectors ist nicht verfügbar.", + "gateway-unavailable-desc": "Für diese Bereitstellung ist das Connector Gateway nicht aktiviert.", + "gateway-connectors": "Connectors", + "source-open": "Cloud", + "source-built-in": "Integriert", + "source-local": "Lokal", + "source-remote": "Remote", + "load-built-in-failed": "Integrierte Konnektoren konnten nicht geladen werden", + "load-custom-failed": "Benutzerdefinierte Konnektoren konnten nicht geladen werden", + "load-gateway-failed": "Connectors konnten nicht geladen werden", + "disconnected": "Verbindung zu {{name}} getrennt", + "disconnect-failed": "Konnektor konnte nicht getrennt werden", + "update-failed": "Konnektor konnte nicht aktualisiert werden", + "status-save-failed-enabled": "Der Konnektor wurde aktiviert, sein Status konnte jedoch nicht gespeichert werden. Aktualisiere die Seite und versuche es erneut.", + "status-save-failed-disabled": "Der Konnektor wurde deaktiviert, sein Status konnte jedoch nicht gespeichert werden. Aktualisiere die Seite und versuche es erneut.", + "updated": "Konnektor aktualisiert", + "save-failed": "Konnektor konnte nicht gespeichert werden", + "deleted": "Konnektor gelöscht", + "delete-failed": "Konnektor konnte nicht gelöscht werden", + "enable-connector": "Konnektor aktivieren", + "disable-connector": "Konnektor deaktivieren", + "more-actions": "Weitere Aktionen", + "open": "Öffnen", + "edit": "Bearbeiten", + "delete": "Löschen", + "connected-account": "Verbundenes Konto", + "supported-actions": "Unterstützte Aktionen", + "supported-actions-count": "{{num}} unterstützte Aktionen", + "show-more": "Mehr anzeigen", + "show-less": "Weniger anzeigen", + "provider-website": "Website des Anbieters", + "built-in-title": "Lokaler Kompatibilitätskonnektor", + "built-in-desc": "Dieser Konnektor verwendet die lokale Integrationslaufzeit von Eigent. Connector Gateway ist der primäre Marktplatz für neue gehostete Integrationen.", + "configuration": "Konfiguration: {{vars}}", + "status": "Status", + "active": "Aktiv", + "disabled": "Deaktiviert", + "server-url": "Server-URL", + "not-configured": "Nicht konfiguriert", + "command": "Befehl", + "arguments": "Argumente", + "requires": "Erfordert {{vars}}", + "notion-desc": "Einen Notion-Arbeitsbereich verbinden.", + "google-calendar-desc": "Google-Kalendertermine und -zeitpläne verwalten.", + "generic-desc": "{{name}} mit Eigent verbinden.", + "google-search": "Google-Suche", + "google-search-desc": "Google Custom Search für Browser- und Rechercheaufgaben verbinden.", + "notion-install-failed": "Notion konnte nicht installiert werden", + "google-calendar-install-failed": "Google Kalender konnte nicht installiert werden", + "search-connectors": "Konnektoren suchen", + "auth-api-key": "API-Schlüssel", + "auth-credential": "Anmeldedaten", + "auth-oauth": "OAuth", + "auth-none": "Keine Authentifizierung", + "unnamed-action": "Unbenannte Aktion", + "try-again": "Erneut versuchen", + "no-gateway-found": "Keine Connectors gefunden.", + "no-built-in-found": "Keine integrierten Konnektoren gefunden.", + "loading": "Wird geladen…", + "updating": "Wird aktualisiert…", + "loading-more": "Weitere werden geladen…", + "new": "Neu", + "installed": "Installiert", + "local-integration": "Lokale Integration", + "authentication": "Authentifizierung", + "oauth-title": "Mit OAuth verbinden", + "oauth-desc": "Eigent öffnet die Autorisierungsseite des Anbieters. Dieses Dialogfeld schließt die Installation ab, sobald die Autorisierung abgeschlossen ist.", + "oauth-scopes": "Angeforderte Berechtigungen: {{scopes}}", + "no-auth-desc": "Dieser Konnektor benötigt keine Anmeldedaten.", + "waiting-authorization": "Warten auf Abschluss der Autorisierung…", + "authorization-started": "Autorisierung gestartet", + "installed-toast": "{{name}} installiert", + "no-authorization-url": "Der Konnektor hat keine Autorisierungs-URL zurückgegeben", + "authorization-pending": "Die Autorisierung steht noch aus. Schließe sie im Anbieterfenster ab und versuche es erneut.", + "install-failed": "Konnektor konnte nicht installiert werden", + "detail-load-failed": "Konnektordetails konnten nicht geladen werden", + "authorization-incomplete": "Die Autorisierung ist noch nicht abgeschlossen. Schließe sie im Anbieterfenster ab und aktualisiere den Status erneut.", + "refresh-status-failed": "Konnektorstatus konnte nicht aktualisiert werden", + "field-required": "{{field}} ist erforderlich", + "complete-authorization": "Schließe die Autorisierung im Anbieterfenster ab.", + "refresh-status": "Status aktualisieren", + "built-in-auth-desc": "Bei der Installation wird der Autorisierungsvorgang des Anbieters in einem separaten Fenster geöffnet.", + "built-in-generic-desc": "Eine lokale Eigent-Integration.", + "cancel": "Abbrechen", + "install": "Installieren", + "installing": "Wird installiert…", + "save": "Speichern", + "enter-value": "{{field}} eingeben", + "custom-title": "Benutzerdefinierten Konnektor hinzufügen", + "custom-subtitle": "Installiere einen vertrauenswürdigen lokalen oder Remote-MCP-Server.", + "custom-warning": "Benutzerdefinierte MCP-Server können Befehle ausführen oder auf Remote-Dienste zugreifen. Installiere nur Konfigurationen, denen du vertraust.", + "local-json-desc": "Füge einen lokalen MCP-Server mit einer gültigen JSON-Konfiguration hinzu.", + "learn-more": "Mehr erfahren", + "connector-name": "Konnektorname", + "remote-name-placeholder": "Mein Remote-MCP", + "remote-url": "Remote-MCP-URL", + "remote-url-note": "Verwende HTTPS für Server außerhalb eines vertrauenswürdigen lokalen Netzwerks.", + "invalid-json": "Ungültiges JSON: {{message}}", + "json-format-error": "JSON-Formatfehler: {{message}}", + "parse-failed": "Analyse fehlgeschlagen", + "missing-mcp-servers": "Die Konfiguration muss ein mcpServers-Objekt enthalten", + "add-at-least-one": "Füge mindestens einen MCP-Server hinzu", + "already-exists": "{{name}} ist bereits vorhanden", + "name-required": "Der Konnektorname ist erforderlich", + "invalid-remote-url": "Gib eine gültige Remote-MCP-URL ein", + "remote-url-protocol": "Remote-MCP-URLs müssen HTTP oder HTTPS verwenden", + "remote-missing-id": "Der Remote-Konnektor wurde ohne ID erstellt", + "custom-installed": "Installierte benutzerdefinierte Konnektoren: {{num}}", + "install-custom-failed": "Benutzerdefinierter Konnektor konnte nicht installiert werden", + "env-key-value": "Umgebungsvariablen (Schlüssel-Wert)", + "value-placeholder": "Wert", + "configuration-title": "Konfiguration", + "google-api-key": "Google-API-Schlüssel", + "google-api-key-placeholder": "Google-API-Schlüssel aus der Google Cloud Console eingeben", + "google-api-key-note": "So erhältst du deinen Google-API-Schlüssel → https://developers.google.com/custom-search/v1/overview", + "search-engine-id": "Suchmaschinen-ID", + "search-engine-id-placeholder": "Die mit deinem API-Schlüssel verknüpfte ID der benutzerdefinierten Suchmaschine eingeben", + "google-search-custom-desc": "Mit Google Custom Search verbinden. Erfordert einen Google-API-Schlüssel und eine ID der benutzerdefinierten Suchmaschine (CSE).", + "google-search-default-desc": "Die Google-Suche ist standardmäßig aktiviert. Kein API-Schlüssel erforderlich." +} diff --git a/src/i18n/locales/de/index.ts b/src/i18n/locales/de/index.ts index efa753446..ae235fb7b 100644 --- a/src/i18n/locales/de/index.ts +++ b/src/i18n/locales/de/index.ts @@ -14,6 +14,7 @@ import agents from './agents.json'; import chat from './chat.json'; +import connectors from './connectors.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; import setting from './setting.json'; @@ -26,6 +27,7 @@ export default { dashboard, workforce, chat, + connectors, setting, update, triggers, diff --git a/src/i18n/locales/en-us/connectors.json b/src/i18n/locales/en-us/connectors.json new file mode 100644 index 000000000..bd4ff2040 --- /dev/null +++ b/src/i18n/locales/en-us/connectors.json @@ -0,0 +1,132 @@ +{ + "title": "Connectors", + "search-placeholder": "Search your connectors", + "browse": "Browse connectors", + "add-custom": "Add custom", + "retry": "Retry", + "your-connectors": "Your connectors", + "no-matching": "No matching connectors.", + "count-one": "connector", + "count-other": "connectors", + "recommended": "Recommended connectors for you.", + "no-recommended": "No recommended connectors available.", + "gateway-unavailable": "Connectors is unavailable.", + "gateway-unavailable-desc": "This deployment does not have the Connector Gateway enabled.", + "gateway-connectors": "Connectors", + "source-open": "Cloud", + "source-built-in": "Built-in", + "source-local": "Local", + "source-remote": "Remote", + "load-built-in-failed": "Failed to load Built-in connectors", + "load-custom-failed": "Failed to load Custom connectors", + "load-gateway-failed": "Failed to load connectors", + "disconnected": "{{name}} disconnected", + "disconnect-failed": "Failed to disconnect connector", + "update-failed": "Failed to update connector", + "status-save-failed-enabled": "Connector enabled, but saving its state failed. Refresh and try again.", + "status-save-failed-disabled": "Connector disabled, but saving its state failed. Refresh and try again.", + "updated": "Connector updated", + "save-failed": "Failed to save connector", + "deleted": "Connector deleted", + "delete-failed": "Failed to delete connector", + "enable-connector": "Enable connector", + "disable-connector": "Disable connector", + "more-actions": "More actions", + "open": "Open", + "edit": "Edit", + "delete": "Delete", + "connected-account": "Connected account", + "supported-actions": "Supported actions", + "supported-actions-count": "{{num}} supported actions", + "show-more": "Show more", + "show-less": "Show less", + "provider-website": "Provider website", + "built-in-title": "Local compatibility connector", + "built-in-desc": "This connector uses Eigent's local integration runtime. Connector Gateway is the primary marketplace for new hosted integrations.", + "configuration": "Configuration: {{vars}}", + "status": "Status", + "active": "Active", + "disabled": "Disabled", + "server-url": "Server URL", + "not-configured": "Not configured", + "command": "Command", + "arguments": "Arguments", + "requires": "Requires {{vars}}", + "notion-desc": "Connect a Notion workspace.", + "google-calendar-desc": "Manage Google Calendar events and schedules.", + "generic-desc": "Connect {{name}} to Eigent.", + "google-search": "Google Search", + "google-search-desc": "Connect Google Custom Search for browser and research tasks.", + "notion-install-failed": "Failed to install Notion", + "google-calendar-install-failed": "Failed to install Google Calendar", + "search-connectors": "Search connectors", + "auth-api-key": "API key", + "auth-credential": "Credential", + "auth-oauth": "OAuth", + "auth-none": "No authentication", + "unnamed-action": "Unnamed action", + "try-again": "Try again", + "no-gateway-found": "No connectors found.", + "no-built-in-found": "No Built-in connectors found.", + "loading": "Loading…", + "updating": "Updating…", + "loading-more": "Loading more…", + "new": "New", + "installed": "Installed", + "local-integration": "Local integration", + "authentication": "Authentication", + "oauth-title": "Connect with OAuth", + "oauth-desc": "Eigent will open the provider authorization page. This dialog will finish installation when authorization is complete.", + "oauth-scopes": "Requested scopes: {{scopes}}", + "no-auth-desc": "This connector does not require credentials.", + "waiting-authorization": "Waiting for authorization to complete…", + "authorization-started": "Authorization started", + "installed-toast": "{{name}} installed", + "no-authorization-url": "The connector did not return an authorization URL", + "authorization-pending": "Authorization is still pending. Complete it in the provider window, then try again.", + "install-failed": "Failed to install connector", + "detail-load-failed": "Failed to load connector details", + "authorization-incomplete": "Authorization has not completed yet. Finish it in the provider window and refresh again.", + "refresh-status-failed": "Failed to refresh connector status", + "field-required": "{{field}} is required", + "complete-authorization": "Complete authorization in the provider window.", + "refresh-status": "Refresh status", + "built-in-auth-desc": "Installation opens the provider authorization flow in a separate window.", + "built-in-generic-desc": "A local Eigent integration.", + "cancel": "Cancel", + "install": "Install", + "installing": "Installing…", + "save": "Save", + "enter-value": "Enter {{field}}", + "custom-title": "Add custom connector", + "custom-subtitle": "Install a local or remote MCP server you trust.", + "custom-warning": "Custom MCP servers can execute commands or access remote services. Only install configurations you trust.", + "local-json-desc": "Add a local MCP server by providing a valid JSON configuration.", + "learn-more": "Learn more", + "connector-name": "Connector name", + "remote-name-placeholder": "My remote MCP", + "remote-url": "Remote MCP URL", + "remote-url-note": "Use HTTPS for servers outside a trusted local network.", + "invalid-json": "Invalid JSON: {{message}}", + "json-format-error": "JSON format error: {{message}}", + "parse-failed": "Parsing failed", + "missing-mcp-servers": "Configuration must contain an mcpServers object", + "add-at-least-one": "Add at least one MCP server", + "already-exists": "{{name}} already exists", + "name-required": "Connector name is required", + "invalid-remote-url": "Enter a valid remote MCP URL", + "remote-url-protocol": "Remote MCP URLs must use HTTP or HTTPS", + "remote-missing-id": "Remote connector was created without an ID", + "custom-installed": "Custom connectors installed: {{num}}", + "install-custom-failed": "Failed to install custom connector", + "env-key-value": "Env (key-value)", + "value-placeholder": "Value", + "configuration-title": "Configuration", + "google-api-key": "Google API Key", + "google-api-key-placeholder": "Enter your Google API key from Google Cloud Console", + "google-api-key-note": "Learn how to get your Google API key → https://developers.google.com/custom-search/v1/overview", + "search-engine-id": "Search Engine ID", + "search-engine-id-placeholder": "Enter the Custom Search Engine ID associated with your API key", + "google-search-custom-desc": "Connect to Google Custom Search. Requires a Google API key and a Custom Search Engine (CSE) ID.", + "google-search-default-desc": "Google Search is enabled by default. No API key required." +} diff --git a/src/i18n/locales/en-us/index.ts b/src/i18n/locales/en-us/index.ts index efa753446..ae235fb7b 100644 --- a/src/i18n/locales/en-us/index.ts +++ b/src/i18n/locales/en-us/index.ts @@ -14,6 +14,7 @@ import agents from './agents.json'; import chat from './chat.json'; +import connectors from './connectors.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; import setting from './setting.json'; @@ -26,6 +27,7 @@ export default { dashboard, workforce, chat, + connectors, setting, update, triggers, diff --git a/src/i18n/locales/es/connectors.json b/src/i18n/locales/es/connectors.json new file mode 100644 index 000000000..2e694fb45 --- /dev/null +++ b/src/i18n/locales/es/connectors.json @@ -0,0 +1,132 @@ +{ + "title": "Conectores", + "search-placeholder": "Busca tus conectores", + "browse": "Explorar conectores", + "add-custom": "Añadir personalizado", + "retry": "Reintentar", + "your-connectors": "Tus conectores", + "no-matching": "No hay conectores coincidentes.", + "count-one": "conector", + "count-other": "conectores", + "recommended": "Conectores recomendados para ti.", + "no-recommended": "No hay conectores recomendados disponibles.", + "gateway-unavailable": "Los conectores no están disponibles.", + "gateway-unavailable-desc": "Esta implementación no tiene habilitado el Connector Gateway.", + "gateway-connectors": "Conectores", + "source-open": "Nube", + "source-built-in": "Integrado", + "source-local": "Local", + "source-remote": "Remoto", + "load-built-in-failed": "Error al cargar los conectores integrados", + "load-custom-failed": "Error al cargar los conectores personalizados", + "load-gateway-failed": "Error al cargar conectores", + "disconnected": "{{name}} desconectado", + "disconnect-failed": "Error al desconectar el conector", + "update-failed": "Error al actualizar el conector", + "status-save-failed-enabled": "Conector habilitado, pero no se pudo guardar su estado. Actualiza e inténtalo de nuevo.", + "status-save-failed-disabled": "Conector deshabilitado, pero no se pudo guardar su estado. Actualiza e inténtalo de nuevo.", + "updated": "Conector actualizado", + "save-failed": "Error al guardar el conector", + "deleted": "Conector eliminado", + "delete-failed": "Error al eliminar el conector", + "enable-connector": "Habilitar conector", + "disable-connector": "Deshabilitar conector", + "more-actions": "Más acciones", + "open": "Abrir", + "edit": "Editar", + "delete": "Eliminar", + "connected-account": "Cuenta conectada", + "supported-actions": "Acciones compatibles", + "supported-actions-count": "{{num}} acciones compatibles", + "show-more": "Mostrar más", + "show-less": "Mostrar menos", + "provider-website": "Sitio web del proveedor", + "built-in-title": "Conector de compatibilidad local", + "built-in-desc": "Este conector usa el entorno de integración local de Eigent. Connector Gateway es el mercado principal para nuevas integraciones alojadas.", + "configuration": "Configuración: {{vars}}", + "status": "Estado", + "active": "Activo", + "disabled": "Deshabilitado", + "server-url": "URL del servidor", + "not-configured": "Sin configurar", + "command": "Comando", + "arguments": "Argumentos", + "requires": "Requiere {{vars}}", + "notion-desc": "Conecta un espacio de trabajo de Notion.", + "google-calendar-desc": "Gestiona eventos y horarios de Google Calendar.", + "generic-desc": "Conecta {{name}} a Eigent.", + "google-search": "Búsqueda de Google", + "google-search-desc": "Conecta Google Custom Search para tareas de navegación e investigación.", + "notion-install-failed": "Error al instalar Notion", + "google-calendar-install-failed": "Error al instalar Google Calendar", + "search-connectors": "Buscar conectores", + "auth-api-key": "Clave API", + "auth-credential": "Credencial", + "auth-oauth": "OAuth", + "auth-none": "Sin autenticación", + "unnamed-action": "Acción sin nombre", + "try-again": "Intentar de nuevo", + "no-gateway-found": "No se encontraron conectores.", + "no-built-in-found": "No se encontraron conectores integrados.", + "loading": "Cargando…", + "updating": "Actualizando…", + "loading-more": "Cargando más…", + "new": "Nuevo", + "installed": "Instalado", + "local-integration": "Integración local", + "authentication": "Autenticación", + "oauth-title": "Conectar con OAuth", + "oauth-desc": "Eigent abrirá la página de autorización del proveedor. Este cuadro de diálogo finalizará la instalación cuando se complete la autorización.", + "oauth-scopes": "Permisos solicitados: {{scopes}}", + "no-auth-desc": "Este conector no requiere credenciales.", + "waiting-authorization": "Esperando a que se complete la autorización…", + "authorization-started": "Autorización iniciada", + "installed-toast": "{{name}} instalado", + "no-authorization-url": "El conector no devolvió una URL de autorización", + "authorization-pending": "La autorización sigue pendiente. Complétala en la ventana del proveedor e inténtalo de nuevo.", + "install-failed": "Error al instalar el conector", + "detail-load-failed": "Error al cargar los detalles del conector", + "authorization-incomplete": "La autorización aún no se ha completado. Termínala en la ventana del proveedor y actualiza de nuevo.", + "refresh-status-failed": "Error al actualizar el estado del conector", + "field-required": "{{field}} es obligatorio", + "complete-authorization": "Completa la autorización en la ventana del proveedor.", + "refresh-status": "Actualizar estado", + "built-in-auth-desc": "La instalación abre el flujo de autorización del proveedor en una ventana aparte.", + "built-in-generic-desc": "Una integración local de Eigent.", + "cancel": "Cancelar", + "install": "Instalar", + "installing": "Instalando…", + "save": "Guardar", + "enter-value": "Introduce {{field}}", + "custom-title": "Añadir conector personalizado", + "custom-subtitle": "Instala un servidor MCP local o remoto de confianza.", + "custom-warning": "Los servidores MCP personalizados pueden ejecutar comandos o acceder a servicios remotos. Instala solo configuraciones de confianza.", + "local-json-desc": "Añade un servidor MCP local proporcionando una configuración JSON válida.", + "learn-more": "Más información", + "connector-name": "Nombre del conector", + "remote-name-placeholder": "Mi MCP remoto", + "remote-url": "URL del MCP remoto", + "remote-url-note": "Usa HTTPS para servidores fuera de una red local de confianza.", + "invalid-json": "JSON no válido: {{message}}", + "json-format-error": "Error de formato JSON: {{message}}", + "parse-failed": "Error al analizar", + "missing-mcp-servers": "La configuración debe contener un objeto mcpServers", + "add-at-least-one": "Añade al menos un servidor MCP", + "already-exists": "{{name}} ya existe", + "name-required": "El nombre del conector es obligatorio", + "invalid-remote-url": "Introduce una URL de MCP remoto válida", + "remote-url-protocol": "Las URL de MCP remoto deben usar HTTP o HTTPS", + "remote-missing-id": "El conector remoto se creó sin un ID", + "custom-installed": "Conectores personalizados instalados: {{num}}", + "install-custom-failed": "Error al instalar el conector personalizado", + "env-key-value": "Variables de entorno (clave-valor)", + "value-placeholder": "Valor", + "configuration-title": "Configuración", + "google-api-key": "Clave de API de Google", + "google-api-key-placeholder": "Introduce tu clave de API de Google Cloud Console", + "google-api-key-note": "Cómo obtener tu clave de API de Google → https://developers.google.com/custom-search/v1/overview", + "search-engine-id": "ID del motor de búsqueda", + "search-engine-id-placeholder": "Introduce el ID del motor de búsqueda personalizado asociado a tu clave de API", + "google-search-custom-desc": "Conecta Google Custom Search. Requiere una clave de API de Google y un ID de motor de búsqueda personalizado (CSE).", + "google-search-default-desc": "Google Search está habilitado de forma predeterminada. No se requiere una clave de API." +} diff --git a/src/i18n/locales/es/index.ts b/src/i18n/locales/es/index.ts index efa753446..ae235fb7b 100644 --- a/src/i18n/locales/es/index.ts +++ b/src/i18n/locales/es/index.ts @@ -14,6 +14,7 @@ import agents from './agents.json'; import chat from './chat.json'; +import connectors from './connectors.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; import setting from './setting.json'; @@ -26,6 +27,7 @@ export default { dashboard, workforce, chat, + connectors, setting, update, triggers, diff --git a/src/i18n/locales/fr/connectors.json b/src/i18n/locales/fr/connectors.json new file mode 100644 index 000000000..69dff289f --- /dev/null +++ b/src/i18n/locales/fr/connectors.json @@ -0,0 +1,132 @@ +{ + "title": "Connecteurs", + "search-placeholder": "Rechercher dans vos connecteurs", + "browse": "Parcourir les connecteurs", + "add-custom": "Ajouter un connecteur personnalisé", + "retry": "Réessayer", + "your-connectors": "Vos connecteurs", + "no-matching": "Aucun connecteur correspondant.", + "count-one": "connecteur", + "count-other": "connecteurs", + "recommended": "Connecteurs recommandés pour vous.", + "no-recommended": "Aucun connecteur recommandé disponible.", + "gateway-unavailable": "Les connecteurs ne sont pas disponibles.", + "gateway-unavailable-desc": "Connector Gateway n’est pas activé pour ce déploiement.", + "gateway-connectors": "Connecteurs", + "source-open": "Cloud", + "source-built-in": "Intégré", + "source-local": "Local", + "source-remote": "Distant", + "load-built-in-failed": "Échec du chargement des connecteurs intégrés", + "load-custom-failed": "Échec du chargement des connecteurs personnalisés", + "load-gateway-failed": "Échec du chargement des connecteurs", + "disconnected": "{{name}} déconnecté", + "disconnect-failed": "Échec de la déconnexion du connecteur", + "update-failed": "Échec de la mise à jour du connecteur", + "status-save-failed-enabled": "Le connecteur a été activé, mais son état n’a pas pu être enregistré. Actualisez la page et réessayez.", + "status-save-failed-disabled": "Le connecteur a été désactivé, mais son état n’a pas pu être enregistré. Actualisez la page et réessayez.", + "updated": "Connecteur mis à jour", + "save-failed": "Échec de l’enregistrement du connecteur", + "deleted": "Connecteur supprimé", + "delete-failed": "Échec de la suppression du connecteur", + "enable-connector": "Activer le connecteur", + "disable-connector": "Désactiver le connecteur", + "more-actions": "Plus d’actions", + "open": "Ouvrir", + "edit": "Modifier", + "delete": "Supprimer", + "connected-account": "Compte connecté", + "supported-actions": "Actions prises en charge", + "supported-actions-count": "{{num}} actions prises en charge", + "show-more": "Afficher plus", + "show-less": "Afficher moins", + "provider-website": "Site web du fournisseur", + "built-in-title": "Connecteur de compatibilité locale", + "built-in-desc": "Ce connecteur utilise l’environnement d’intégration local d’Eigent. Connector Gateway est la place de marché principale pour les nouvelles intégrations hébergées.", + "configuration": "Configuration : {{vars}}", + "status": "État", + "active": "Actif", + "disabled": "Désactivé", + "server-url": "URL du serveur", + "not-configured": "Non configuré", + "command": "Commande", + "arguments": "Arguments", + "requires": "Nécessite {{vars}}", + "notion-desc": "Connecter un espace de travail Notion.", + "google-calendar-desc": "Gérer les événements et les calendriers Google Agenda.", + "generic-desc": "Connecter {{name}} à Eigent.", + "google-search": "Recherche Google", + "google-search-desc": "Connecter Google Custom Search pour les tâches de navigation et de recherche.", + "notion-install-failed": "Échec de l’installation de Notion", + "google-calendar-install-failed": "Échec de l’installation de Google Agenda", + "search-connectors": "Rechercher des connecteurs", + "auth-api-key": "Clé API", + "auth-credential": "Identifiants", + "auth-oauth": "OAuth", + "auth-none": "Aucune authentification", + "unnamed-action": "Action sans nom", + "try-again": "Réessayer", + "no-gateway-found": "Aucun connecteur trouvé.", + "no-built-in-found": "Aucun connecteur intégré trouvé.", + "loading": "Chargement…", + "updating": "Mise à jour…", + "loading-more": "Chargement d’autres éléments…", + "new": "Nouveau", + "installed": "Installé", + "local-integration": "Intégration locale", + "authentication": "Authentification", + "oauth-title": "Se connecter avec OAuth", + "oauth-desc": "Eigent ouvrira la page d’autorisation du fournisseur. Cette boîte de dialogue terminera l’installation une fois l’autorisation accordée.", + "oauth-scopes": "Autorisations demandées : {{scopes}}", + "no-auth-desc": "Ce connecteur ne nécessite aucun identifiant.", + "waiting-authorization": "En attente de l’autorisation…", + "authorization-started": "Autorisation démarrée", + "installed-toast": "{{name}} installé", + "no-authorization-url": "Le connecteur n’a pas renvoyé d’URL d’autorisation", + "authorization-pending": "L’autorisation est toujours en attente. Terminez-la dans la fenêtre du fournisseur, puis réessayez.", + "install-failed": "Échec de l’installation du connecteur", + "detail-load-failed": "Échec du chargement des détails du connecteur", + "authorization-incomplete": "L’autorisation n’est pas encore terminée. Terminez-la dans la fenêtre du fournisseur et actualisez à nouveau l’état.", + "refresh-status-failed": "Échec de l’actualisation de l’état du connecteur", + "field-required": "{{field}} est requis", + "complete-authorization": "Terminez l’autorisation dans la fenêtre du fournisseur.", + "refresh-status": "Actualiser l’état", + "built-in-auth-desc": "L’installation ouvre le processus d’autorisation du fournisseur dans une fenêtre séparée.", + "built-in-generic-desc": "Une intégration Eigent locale.", + "cancel": "Annuler", + "install": "Installer", + "installing": "Installation…", + "save": "Enregistrer", + "enter-value": "Saisir {{field}}", + "custom-title": "Ajouter un connecteur personnalisé", + "custom-subtitle": "Installez un serveur MCP local ou distant auquel vous faites confiance.", + "custom-warning": "Les serveurs MCP personnalisés peuvent exécuter des commandes ou accéder à des services distants. Installez uniquement des configurations auxquelles vous faites confiance.", + "local-json-desc": "Ajoutez un serveur MCP local en fournissant une configuration JSON valide.", + "learn-more": "En savoir plus", + "connector-name": "Nom du connecteur", + "remote-name-placeholder": "Mon MCP distant", + "remote-url": "URL du MCP distant", + "remote-url-note": "Utilisez HTTPS pour les serveurs situés hors d’un réseau local de confiance.", + "invalid-json": "JSON non valide : {{message}}", + "json-format-error": "Erreur de format JSON : {{message}}", + "parse-failed": "Échec de l’analyse", + "missing-mcp-servers": "La configuration doit contenir un objet mcpServers", + "add-at-least-one": "Ajoutez au moins un serveur MCP", + "already-exists": "{{name}} existe déjà", + "name-required": "Le nom du connecteur est requis", + "invalid-remote-url": "Saisissez une URL de MCP distant valide", + "remote-url-protocol": "Les URL de MCP distant doivent utiliser HTTP ou HTTPS", + "remote-missing-id": "Le connecteur distant a été créé sans identifiant", + "custom-installed": "Connecteurs personnalisés installés : {{num}}", + "install-custom-failed": "Échec de l’installation du connecteur personnalisé", + "env-key-value": "Variables d’environnement (clé-valeur)", + "value-placeholder": "Valeur", + "configuration-title": "Configuration", + "google-api-key": "Clé API Google", + "google-api-key-placeholder": "Saisissez votre clé API Google depuis Google Cloud Console", + "google-api-key-note": "Comment obtenir votre clé API Google → https://developers.google.com/custom-search/v1/overview", + "search-engine-id": "Identifiant du moteur de recherche", + "search-engine-id-placeholder": "Saisissez l’identifiant du moteur de recherche personnalisé associé à votre clé API", + "google-search-custom-desc": "Connectez Google Custom Search. Une clé API Google et un identifiant de moteur de recherche personnalisé (CSE) sont requis.", + "google-search-default-desc": "La recherche Google est activée par défaut. Aucune clé API n’est requise." +} diff --git a/src/i18n/locales/fr/index.ts b/src/i18n/locales/fr/index.ts index efa753446..ae235fb7b 100644 --- a/src/i18n/locales/fr/index.ts +++ b/src/i18n/locales/fr/index.ts @@ -14,6 +14,7 @@ import agents from './agents.json'; import chat from './chat.json'; +import connectors from './connectors.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; import setting from './setting.json'; @@ -26,6 +27,7 @@ export default { dashboard, workforce, chat, + connectors, setting, update, triggers, diff --git a/src/i18n/locales/it/connectors.json b/src/i18n/locales/it/connectors.json new file mode 100644 index 000000000..e15d90e0a --- /dev/null +++ b/src/i18n/locales/it/connectors.json @@ -0,0 +1,132 @@ +{ + "title": "Connettori", + "search-placeholder": "Cerca nei tuoi connettori", + "browse": "Sfoglia i connettori", + "add-custom": "Aggiungi personalizzato", + "retry": "Riprova", + "your-connectors": "I tuoi connettori", + "no-matching": "Nessun connettore corrispondente.", + "count-one": "connettore", + "count-other": "connettori", + "recommended": "Connettori consigliati per te.", + "no-recommended": "Nessun connettore consigliato disponibile.", + "gateway-unavailable": "Connectors non è disponibile.", + "gateway-unavailable-desc": "Connector Gateway non è abilitato per questa distribuzione.", + "gateway-connectors": "Connectors", + "source-open": "Cloud", + "source-built-in": "Integrato", + "source-local": "Locale", + "source-remote": "Remoto", + "load-built-in-failed": "Impossibile caricare i connettori integrati", + "load-custom-failed": "Impossibile caricare i connettori personalizzati", + "load-gateway-failed": "Impossibile caricare i connectors", + "disconnected": "{{name}} disconnesso", + "disconnect-failed": "Impossibile disconnettere il connettore", + "update-failed": "Impossibile aggiornare il connettore", + "status-save-failed-enabled": "Il connettore è stato abilitato, ma non è stato possibile salvarne lo stato. Aggiorna la pagina e riprova.", + "status-save-failed-disabled": "Il connettore è stato disabilitato, ma non è stato possibile salvarne lo stato. Aggiorna la pagina e riprova.", + "updated": "Connettore aggiornato", + "save-failed": "Impossibile salvare il connettore", + "deleted": "Connettore eliminato", + "delete-failed": "Impossibile eliminare il connettore", + "enable-connector": "Abilita connettore", + "disable-connector": "Disabilita connettore", + "more-actions": "Altre azioni", + "open": "Apri", + "edit": "Modifica", + "delete": "Elimina", + "connected-account": "Account connesso", + "supported-actions": "Azioni supportate", + "supported-actions-count": "{{num}} azioni supportate", + "show-more": "Mostra altro", + "show-less": "Mostra meno", + "provider-website": "Sito web del fornitore", + "built-in-title": "Connettore di compatibilità locale", + "built-in-desc": "Questo connettore utilizza l’ambiente di integrazione locale di Eigent. Connector Gateway è il marketplace principale per le nuove integrazioni ospitate.", + "configuration": "Configurazione: {{vars}}", + "status": "Stato", + "active": "Attivo", + "disabled": "Disabilitato", + "server-url": "URL del server", + "not-configured": "Non configurato", + "command": "Comando", + "arguments": "Argomenti", + "requires": "Richiede {{vars}}", + "notion-desc": "Connetti uno spazio di lavoro Notion.", + "google-calendar-desc": "Gestisci eventi e pianificazioni di Google Calendar.", + "generic-desc": "Connetti {{name}} a Eigent.", + "google-search": "Ricerca Google", + "google-search-desc": "Connetti Google Custom Search per attività di navigazione e ricerca.", + "notion-install-failed": "Impossibile installare Notion", + "google-calendar-install-failed": "Impossibile installare Google Calendar", + "search-connectors": "Cerca connettori", + "auth-api-key": "Chiave API", + "auth-credential": "Credenziale", + "auth-oauth": "OAuth", + "auth-none": "Nessuna autenticazione", + "unnamed-action": "Azione senza nome", + "try-again": "Riprova", + "no-gateway-found": "Nessun connector trovato.", + "no-built-in-found": "Nessun connettore integrato trovato.", + "loading": "Caricamento…", + "updating": "Aggiornamento…", + "loading-more": "Caricamento di altri elementi…", + "new": "Nuovo", + "installed": "Installato", + "local-integration": "Integrazione locale", + "authentication": "Autenticazione", + "oauth-title": "Connetti con OAuth", + "oauth-desc": "Eigent aprirà la pagina di autorizzazione del fornitore. Questa finestra completerà l’installazione al termine dell’autorizzazione.", + "oauth-scopes": "Autorizzazioni richieste: {{scopes}}", + "no-auth-desc": "Questo connettore non richiede credenziali.", + "waiting-authorization": "In attesa del completamento dell’autorizzazione…", + "authorization-started": "Autorizzazione avviata", + "installed-toast": "{{name}} installato", + "no-authorization-url": "Il connettore non ha restituito un URL di autorizzazione", + "authorization-pending": "L’autorizzazione è ancora in sospeso. Completala nella finestra del fornitore, quindi riprova.", + "install-failed": "Impossibile installare il connettore", + "detail-load-failed": "Impossibile caricare i dettagli del connettore", + "authorization-incomplete": "L’autorizzazione non è ancora completa. Completala nella finestra del fornitore e aggiorna nuovamente lo stato.", + "refresh-status-failed": "Impossibile aggiornare lo stato del connettore", + "field-required": "{{field}} è obbligatorio", + "complete-authorization": "Completa l’autorizzazione nella finestra del fornitore.", + "refresh-status": "Aggiorna stato", + "built-in-auth-desc": "L’installazione apre il flusso di autorizzazione del fornitore in una finestra separata.", + "built-in-generic-desc": "Un’integrazione locale di Eigent.", + "cancel": "Annulla", + "install": "Installa", + "installing": "Installazione…", + "save": "Salva", + "enter-value": "Inserisci {{field}}", + "custom-title": "Aggiungi connettore personalizzato", + "custom-subtitle": "Installa un server MCP locale o remoto attendibile.", + "custom-warning": "I server MCP personalizzati possono eseguire comandi o accedere a servizi remoti. Installa solo configurazioni attendibili.", + "local-json-desc": "Aggiungi un server MCP locale fornendo una configurazione JSON valida.", + "learn-more": "Scopri di più", + "connector-name": "Nome del connettore", + "remote-name-placeholder": "Il mio MCP remoto", + "remote-url": "URL MCP remoto", + "remote-url-note": "Usa HTTPS per i server esterni a una rete locale attendibile.", + "invalid-json": "JSON non valido: {{message}}", + "json-format-error": "Errore di formato JSON: {{message}}", + "parse-failed": "Analisi non riuscita", + "missing-mcp-servers": "La configurazione deve contenere un oggetto mcpServers", + "add-at-least-one": "Aggiungi almeno un server MCP", + "already-exists": "{{name}} esiste già", + "name-required": "Il nome del connettore è obbligatorio", + "invalid-remote-url": "Inserisci un URL MCP remoto valido", + "remote-url-protocol": "Gli URL MCP remoti devono utilizzare HTTP o HTTPS", + "remote-missing-id": "Il connettore remoto è stato creato senza ID", + "custom-installed": "Connettori personalizzati installati: {{num}}", + "install-custom-failed": "Impossibile installare il connettore personalizzato", + "env-key-value": "Variabili d’ambiente (chiave-valore)", + "value-placeholder": "Valore", + "configuration-title": "Configurazione", + "google-api-key": "Chiave API Google", + "google-api-key-placeholder": "Inserisci la chiave API Google da Google Cloud Console", + "google-api-key-note": "Come ottenere la chiave API Google → https://developers.google.com/custom-search/v1/overview", + "search-engine-id": "ID del motore di ricerca", + "search-engine-id-placeholder": "Inserisci l’ID del motore di ricerca personalizzato associato alla chiave API", + "google-search-custom-desc": "Connetti Google Custom Search. Sono richiesti una chiave API Google e un ID del motore di ricerca personalizzato (CSE).", + "google-search-default-desc": "La Ricerca Google è abilitata per impostazione predefinita. Non è richiesta alcuna chiave API." +} diff --git a/src/i18n/locales/it/index.ts b/src/i18n/locales/it/index.ts index efa753446..ae235fb7b 100644 --- a/src/i18n/locales/it/index.ts +++ b/src/i18n/locales/it/index.ts @@ -14,6 +14,7 @@ import agents from './agents.json'; import chat from './chat.json'; +import connectors from './connectors.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; import setting from './setting.json'; @@ -26,6 +27,7 @@ export default { dashboard, workforce, chat, + connectors, setting, update, triggers, diff --git a/src/i18n/locales/ja/connectors.json b/src/i18n/locales/ja/connectors.json new file mode 100644 index 000000000..839d73540 --- /dev/null +++ b/src/i18n/locales/ja/connectors.json @@ -0,0 +1,132 @@ +{ + "title": "コネクタ", + "search-placeholder": "コネクタを検索", + "browse": "コネクタを探す", + "add-custom": "カスタムを追加", + "retry": "再試行", + "your-connectors": "あなたのコネクタ", + "no-matching": "一致するコネクタがありません。", + "count-one": "件のコネクタ", + "count-other": "件のコネクタ", + "recommended": "あなたへのおすすめコネクタ。", + "no-recommended": "おすすめのコネクタはありません。", + "gateway-unavailable": "Connectors は利用できません。", + "gateway-unavailable-desc": "このデプロイでは Connector Gateway が有効になっていません。", + "gateway-connectors": "Connectors", + "source-open": "クラウド", + "source-built-in": "組み込み", + "source-local": "ローカル", + "source-remote": "リモート", + "load-built-in-failed": "組み込みコネクタの読み込みに失敗しました", + "load-custom-failed": "カスタムコネクタの読み込みに失敗しました", + "load-gateway-failed": "Connectors の読み込みに失敗しました", + "disconnected": "{{name}} の接続を解除しました", + "disconnect-failed": "コネクタの接続解除に失敗しました", + "update-failed": "コネクタの更新に失敗しました", + "status-save-failed-enabled": "コネクタは有効になりましたが、状態の保存に失敗しました。更新して再試行してください。", + "status-save-failed-disabled": "コネクタは無効になりましたが、状態の保存に失敗しました。更新して再試行してください。", + "updated": "コネクタを更新しました", + "save-failed": "コネクタの保存に失敗しました", + "deleted": "コネクタを削除しました", + "delete-failed": "コネクタの削除に失敗しました", + "enable-connector": "コネクタを有効化", + "disable-connector": "コネクタを無効化", + "more-actions": "その他の操作", + "open": "開く", + "edit": "編集", + "delete": "削除", + "connected-account": "接続中のアカウント", + "supported-actions": "対応アクション", + "supported-actions-count": "{{num}} 件の対応アクション", + "show-more": "もっと見る", + "show-less": "折りたたむ", + "provider-website": "プロバイダーのウェブサイト", + "built-in-title": "ローカル互換コネクタ", + "built-in-desc": "このコネクタは Eigent のローカル統合ランタイムを使用します。Connector Gateway は新しいホスト型統合の主要マーケットプレイスです。", + "configuration": "設定項目:{{vars}}", + "status": "ステータス", + "active": "有効", + "disabled": "無効", + "server-url": "サーバー URL", + "not-configured": "未設定", + "command": "コマンド", + "arguments": "引数", + "requires": "{{vars}} が必要です", + "notion-desc": "Notion ワークスペースを接続します。", + "google-calendar-desc": "Google カレンダーの予定とスケジュールを管理します。", + "generic-desc": "{{name}} を Eigent に接続します。", + "google-search": "Google 検索", + "google-search-desc": "ブラウザやリサーチタスク用に Google カスタム検索を接続します。", + "notion-install-failed": "Notion のインストールに失敗しました", + "google-calendar-install-failed": "Google カレンダーのインストールに失敗しました", + "search-connectors": "コネクタを検索", + "auth-api-key": "API キー", + "auth-credential": "認証情報", + "auth-oauth": "OAuth", + "auth-none": "認証不要", + "unnamed-action": "名称未設定のアクション", + "try-again": "再試行", + "no-gateway-found": "Connectors が見つかりません。", + "no-built-in-found": "組み込みコネクタが見つかりません。", + "loading": "読み込み中…", + "updating": "更新中…", + "loading-more": "さらに読み込み中…", + "new": "新着", + "installed": "インストール済み", + "local-integration": "ローカル統合", + "authentication": "認証", + "oauth-title": "OAuth で接続", + "oauth-desc": "Eigent がプロバイダーの認可ページを開きます。認可が完了すると、このダイアログでインストールが完了します。", + "oauth-scopes": "要求されるスコープ:{{scopes}}", + "no-auth-desc": "このコネクタに認証情報は不要です。", + "waiting-authorization": "認可の完了を待っています…", + "authorization-started": "認可を開始しました", + "installed-toast": "{{name}} をインストールしました", + "no-authorization-url": "コネクタが認可 URL を返しませんでした", + "authorization-pending": "認可はまだ完了していません。プロバイダーのウィンドウで完了してから再試行してください。", + "install-failed": "コネクタのインストールに失敗しました", + "detail-load-failed": "コネクタの詳細の読み込みに失敗しました", + "authorization-incomplete": "認可がまだ完了していません。プロバイダーのウィンドウで完了してから再度更新してください。", + "refresh-status-failed": "コネクタの状態の更新に失敗しました", + "field-required": "{{field}} は必須です", + "complete-authorization": "プロバイダーのウィンドウで認可を完了してください。", + "refresh-status": "状態を更新", + "built-in-auth-desc": "インストール時に別ウィンドウでプロバイダーの認可フローが開きます。", + "built-in-generic-desc": "Eigent のローカル統合です。", + "cancel": "キャンセル", + "install": "インストール", + "installing": "インストール中…", + "save": "保存", + "enter-value": "{{field}} を入力", + "custom-title": "カスタムコネクタを追加", + "custom-subtitle": "信頼できるローカルまたはリモートの MCP サーバーをインストールします。", + "custom-warning": "カスタム MCP サーバーはコマンドの実行やリモートサービスへのアクセスが可能です。信頼できる設定のみをインストールしてください。", + "local-json-desc": "有効な JSON 設定を指定してローカル MCP サーバーを追加します。", + "learn-more": "詳細", + "connector-name": "コネクタ名", + "remote-name-placeholder": "マイリモート MCP", + "remote-url": "リモート MCP URL", + "remote-url-note": "信頼できるローカルネットワーク外のサーバーには HTTPS を使用してください。", + "invalid-json": "無効な JSON:{{message}}", + "json-format-error": "JSON 形式エラー:{{message}}", + "parse-failed": "解析に失敗しました", + "missing-mcp-servers": "設定には mcpServers オブジェクトが必要です", + "add-at-least-one": "MCP サーバーを少なくとも 1 つ追加してください", + "already-exists": "{{name}} は既に存在します", + "name-required": "コネクタ名は必須です", + "invalid-remote-url": "有効なリモート MCP URL を入力してください", + "remote-url-protocol": "リモート MCP URL は HTTP または HTTPS を使用する必要があります", + "remote-missing-id": "リモートコネクタが ID なしで作成されました", + "custom-installed": "{{num}} 件のカスタムコネクタをインストールしました", + "install-custom-failed": "カスタムコネクタのインストールに失敗しました", + "env-key-value": "環境変数(キーと値)", + "value-placeholder": "値", + "configuration-title": "設定", + "google-api-key": "Google API キー", + "google-api-key-placeholder": "Google Cloud Console の API キーを入力してください", + "google-api-key-note": "Google API キーの取得方法 → https://developers.google.com/custom-search/v1/overview", + "search-engine-id": "検索エンジン ID", + "search-engine-id-placeholder": "API キーに関連付けられたカスタム検索エンジン ID を入力してください", + "google-search-custom-desc": "Google カスタム検索に接続します。Google API キーとカスタム検索エンジン(CSE)ID が必要です。", + "google-search-default-desc": "Google 検索はデフォルトで有効です。API キーは必要ありません。" +} diff --git a/src/i18n/locales/ja/index.ts b/src/i18n/locales/ja/index.ts index efa753446..ae235fb7b 100644 --- a/src/i18n/locales/ja/index.ts +++ b/src/i18n/locales/ja/index.ts @@ -14,6 +14,7 @@ import agents from './agents.json'; import chat from './chat.json'; +import connectors from './connectors.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; import setting from './setting.json'; @@ -26,6 +27,7 @@ export default { dashboard, workforce, chat, + connectors, setting, update, triggers, diff --git a/src/i18n/locales/ko/connectors.json b/src/i18n/locales/ko/connectors.json new file mode 100644 index 000000000..43f70f2b7 --- /dev/null +++ b/src/i18n/locales/ko/connectors.json @@ -0,0 +1,132 @@ +{ + "title": "커넥터", + "search-placeholder": "내 커넥터 검색", + "browse": "커넥터 찾아보기", + "add-custom": "사용자 지정 추가", + "retry": "다시 시도", + "your-connectors": "내 커넥터", + "no-matching": "일치하는 커넥터가 없습니다.", + "count-one": "개 커넥터", + "count-other": "개 커넥터", + "recommended": "추천 커넥터입니다.", + "no-recommended": "사용 가능한 추천 커넥터가 없습니다.", + "gateway-unavailable": "Connectors를 사용할 수 없습니다.", + "gateway-unavailable-desc": "이 배포에서는 Connector Gateway가 활성화되어 있지 않습니다.", + "gateway-connectors": "Connectors", + "source-open": "클라우드", + "source-built-in": "기본 제공", + "source-local": "로컬", + "source-remote": "원격", + "load-built-in-failed": "기본 제공 커넥터를 불러오지 못했습니다", + "load-custom-failed": "사용자 지정 커넥터를 불러오지 못했습니다", + "load-gateway-failed": "Connectors를 불러오지 못했습니다", + "disconnected": "{{name}} 연결이 해제되었습니다", + "disconnect-failed": "커넥터 연결을 해제하지 못했습니다", + "update-failed": "커넥터를 업데이트하지 못했습니다", + "status-save-failed-enabled": "커넥터가 활성화되었지만 상태를 저장하지 못했습니다. 새로 고친 후 다시 시도하세요.", + "status-save-failed-disabled": "커넥터가 비활성화되었지만 상태를 저장하지 못했습니다. 새로 고친 후 다시 시도하세요.", + "updated": "커넥터가 업데이트되었습니다", + "save-failed": "커넥터를 저장하지 못했습니다", + "deleted": "커넥터가 삭제되었습니다", + "delete-failed": "커넥터를 삭제하지 못했습니다", + "enable-connector": "커넥터 활성화", + "disable-connector": "커넥터 비활성화", + "more-actions": "추가 작업", + "open": "열기", + "edit": "편집", + "delete": "삭제", + "connected-account": "연결된 계정", + "supported-actions": "지원되는 작업", + "supported-actions-count": "지원되는 작업 {{num}}개", + "show-more": "더 보기", + "show-less": "간략히 보기", + "provider-website": "제공업체 웹사이트", + "built-in-title": "로컬 호환 커넥터", + "built-in-desc": "이 커넥터는 Eigent의 로컬 통합 런타임을 사용합니다. Connector Gateway는 새로운 호스팅 통합을 위한 기본 마켓플레이스입니다.", + "configuration": "구성: {{vars}}", + "status": "상태", + "active": "활성", + "disabled": "비활성", + "server-url": "서버 URL", + "not-configured": "구성되지 않음", + "command": "명령어", + "arguments": "인수", + "requires": "{{vars}} 필요", + "notion-desc": "Notion 워크스페이스를 연결합니다.", + "google-calendar-desc": "Google Calendar 일정과 스케줄을 관리합니다.", + "generic-desc": "{{name}}을(를) Eigent에 연결합니다.", + "google-search": "Google 검색", + "google-search-desc": "브라우저 및 리서치 작업에 Google Custom Search를 연결합니다.", + "notion-install-failed": "Notion을 설치하지 못했습니다", + "google-calendar-install-failed": "Google Calendar를 설치하지 못했습니다", + "search-connectors": "커넥터 검색", + "auth-api-key": "API 키", + "auth-credential": "자격 증명", + "auth-oauth": "OAuth", + "auth-none": "인증 없음", + "unnamed-action": "이름 없는 작업", + "try-again": "다시 시도", + "no-gateway-found": "Connectors를 찾을 수 없습니다.", + "no-built-in-found": "기본 제공 커넥터를 찾을 수 없습니다.", + "loading": "불러오는 중…", + "updating": "업데이트 중…", + "loading-more": "더 불러오는 중…", + "new": "신규", + "installed": "설치됨", + "local-integration": "로컬 통합", + "authentication": "인증", + "oauth-title": "OAuth로 연결", + "oauth-desc": "Eigent가 제공업체의 인증 페이지를 엽니다. 인증이 완료되면 이 대화 상자에서 설치가 완료됩니다.", + "oauth-scopes": "요청된 권한: {{scopes}}", + "no-auth-desc": "이 커넥터에는 자격 증명이 필요하지 않습니다.", + "waiting-authorization": "인증 완료 대기 중…", + "authorization-started": "인증이 시작되었습니다", + "installed-toast": "{{name}}이(가) 설치되었습니다", + "no-authorization-url": "커넥터가 인증 URL을 반환하지 않았습니다", + "authorization-pending": "인증이 아직 진행 중입니다. 제공업체 창에서 완료한 후 다시 시도하세요.", + "install-failed": "커넥터를 설치하지 못했습니다", + "detail-load-failed": "커넥터 세부 정보를 불러오지 못했습니다", + "authorization-incomplete": "인증이 아직 완료되지 않았습니다. 제공업체 창에서 완료한 후 상태를 다시 새로 고치세요.", + "refresh-status-failed": "커넥터 상태를 새로 고치지 못했습니다", + "field-required": "{{field}}은(는) 필수입니다", + "complete-authorization": "제공업체 창에서 인증을 완료하세요.", + "refresh-status": "상태 새로 고침", + "built-in-auth-desc": "설치하면 별도 창에서 제공업체 인증 절차가 열립니다.", + "built-in-generic-desc": "Eigent 로컬 통합입니다.", + "cancel": "취소", + "install": "설치", + "installing": "설치 중…", + "save": "저장", + "enter-value": "{{field}} 입력", + "custom-title": "사용자 지정 커넥터 추가", + "custom-subtitle": "신뢰할 수 있는 로컬 또는 원격 MCP 서버를 설치합니다.", + "custom-warning": "사용자 지정 MCP 서버는 명령을 실행하거나 원격 서비스에 접근할 수 있습니다. 신뢰할 수 있는 구성만 설치하세요.", + "local-json-desc": "유효한 JSON 구성을 입력하여 로컬 MCP 서버를 추가합니다.", + "learn-more": "자세히 알아보기", + "connector-name": "커넥터 이름", + "remote-name-placeholder": "내 원격 MCP", + "remote-url": "원격 MCP URL", + "remote-url-note": "신뢰할 수 있는 로컬 네트워크 외부의 서버에는 HTTPS를 사용하세요.", + "invalid-json": "잘못된 JSON: {{message}}", + "json-format-error": "JSON 형식 오류: {{message}}", + "parse-failed": "구문 분석 실패", + "missing-mcp-servers": "구성에 mcpServers 객체가 있어야 합니다", + "add-at-least-one": "MCP 서버를 하나 이상 추가하세요", + "already-exists": "{{name}}이(가) 이미 있습니다", + "name-required": "커넥터 이름은 필수입니다", + "invalid-remote-url": "유효한 원격 MCP URL을 입력하세요", + "remote-url-protocol": "원격 MCP URL은 HTTP 또는 HTTPS를 사용해야 합니다", + "remote-missing-id": "원격 커넥터가 ID 없이 생성되었습니다", + "custom-installed": "사용자 지정 커넥터 {{num}}개가 설치되었습니다", + "install-custom-failed": "사용자 지정 커넥터를 설치하지 못했습니다", + "env-key-value": "환경 변수(키-값)", + "value-placeholder": "값", + "configuration-title": "구성", + "google-api-key": "Google API 키", + "google-api-key-placeholder": "Google Cloud Console의 Google API 키를 입력하세요", + "google-api-key-note": "Google API 키를 발급받는 방법 → https://developers.google.com/custom-search/v1/overview", + "search-engine-id": "검색 엔진 ID", + "search-engine-id-placeholder": "API 키와 연결된 Custom Search Engine ID를 입력하세요", + "google-search-custom-desc": "Google Custom Search를 연결합니다. Google API 키와 Custom Search Engine(CSE) ID가 필요합니다.", + "google-search-default-desc": "Google 검색은 기본적으로 활성화되어 있습니다. API 키가 필요하지 않습니다." +} diff --git a/src/i18n/locales/ko/index.ts b/src/i18n/locales/ko/index.ts index efa753446..ae235fb7b 100644 --- a/src/i18n/locales/ko/index.ts +++ b/src/i18n/locales/ko/index.ts @@ -14,6 +14,7 @@ import agents from './agents.json'; import chat from './chat.json'; +import connectors from './connectors.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; import setting from './setting.json'; @@ -26,6 +27,7 @@ export default { dashboard, workforce, chat, + connectors, setting, update, triggers, diff --git a/src/i18n/locales/ru/connectors.json b/src/i18n/locales/ru/connectors.json new file mode 100644 index 000000000..5549504ee --- /dev/null +++ b/src/i18n/locales/ru/connectors.json @@ -0,0 +1,132 @@ +{ + "title": "Коннекторы", + "search-placeholder": "Поиск по вашим коннекторам", + "browse": "Обзор коннекторов", + "add-custom": "Добавить свой", + "retry": "Повторить", + "your-connectors": "Ваши коннекторы", + "no-matching": "Подходящие коннекторы не найдены.", + "count-one": "коннектор", + "count-other": "коннекторов", + "recommended": "Рекомендованные для вас коннекторы.", + "no-recommended": "Нет доступных рекомендованных коннекторов.", + "gateway-unavailable": "Connectors недоступны.", + "gateway-unavailable-desc": "Connector Gateway не включён для этого развёртывания.", + "gateway-connectors": "Connectors", + "source-open": "Облако", + "source-built-in": "Встроенный", + "source-local": "Локальный", + "source-remote": "Удалённый", + "load-built-in-failed": "Не удалось загрузить встроенные коннекторы", + "load-custom-failed": "Не удалось загрузить пользовательские коннекторы", + "load-gateway-failed": "Не удалось загрузить connectors", + "disconnected": "{{name}} отключён", + "disconnect-failed": "Не удалось отключить коннектор", + "update-failed": "Не удалось обновить коннектор", + "status-save-failed-enabled": "Коннектор включён, но сохранить его состояние не удалось. Обновите страницу и повторите попытку.", + "status-save-failed-disabled": "Коннектор отключён, но сохранить его состояние не удалось. Обновите страницу и повторите попытку.", + "updated": "Коннектор обновлён", + "save-failed": "Не удалось сохранить коннектор", + "deleted": "Коннектор удалён", + "delete-failed": "Не удалось удалить коннектор", + "enable-connector": "Включить коннектор", + "disable-connector": "Отключить коннектор", + "more-actions": "Другие действия", + "open": "Открыть", + "edit": "Изменить", + "delete": "Удалить", + "connected-account": "Подключённая учётная запись", + "supported-actions": "Поддерживаемые действия", + "supported-actions-count": "Поддерживаемых действий: {{num}}", + "show-more": "Показать больше", + "show-less": "Показать меньше", + "provider-website": "Сайт поставщика", + "built-in-title": "Локальный коннектор совместимости", + "built-in-desc": "Этот коннектор использует локальную среду интеграции Eigent. Connector Gateway — основной каталог новых облачных интеграций.", + "configuration": "Конфигурация: {{vars}}", + "status": "Состояние", + "active": "Активен", + "disabled": "Отключён", + "server-url": "URL сервера", + "not-configured": "Не настроено", + "command": "Команда", + "arguments": "Аргументы", + "requires": "Требуется: {{vars}}", + "notion-desc": "Подключить рабочее пространство Notion.", + "google-calendar-desc": "Управлять событиями и расписаниями Google Календаря.", + "generic-desc": "Подключить {{name}} к Eigent.", + "google-search": "Поиск Google", + "google-search-desc": "Подключить Google Custom Search для задач браузера и поиска информации.", + "notion-install-failed": "Не удалось установить Notion", + "google-calendar-install-failed": "Не удалось установить Google Календарь", + "search-connectors": "Поиск коннекторов", + "auth-api-key": "Ключ API", + "auth-credential": "Учётные данные", + "auth-oauth": "OAuth", + "auth-none": "Без аутентификации", + "unnamed-action": "Действие без названия", + "try-again": "Повторить", + "no-gateway-found": "Connectors не найдены.", + "no-built-in-found": "Встроенные коннекторы не найдены.", + "loading": "Загрузка…", + "updating": "Обновление…", + "loading-more": "Загрузка дополнительных элементов…", + "new": "Новый", + "installed": "Установлен", + "local-integration": "Локальная интеграция", + "authentication": "Аутентификация", + "oauth-title": "Подключение через OAuth", + "oauth-desc": "Eigent откроет страницу авторизации поставщика. После завершения авторизации установка будет завершена в этом диалоговом окне.", + "oauth-scopes": "Запрашиваемые разрешения: {{scopes}}", + "no-auth-desc": "Для этого коннектора не требуются учётные данные.", + "waiting-authorization": "Ожидание завершения авторизации…", + "authorization-started": "Авторизация начата", + "installed-toast": "{{name}} установлен", + "no-authorization-url": "Коннектор не вернул URL авторизации", + "authorization-pending": "Авторизация ещё не завершена. Завершите её в окне поставщика и повторите попытку.", + "install-failed": "Не удалось установить коннектор", + "detail-load-failed": "Не удалось загрузить сведения о коннекторе", + "authorization-incomplete": "Авторизация ещё не завершена. Завершите её в окне поставщика и снова обновите состояние.", + "refresh-status-failed": "Не удалось обновить состояние коннектора", + "field-required": "Поле {{field}} обязательно", + "complete-authorization": "Завершите авторизацию в окне поставщика.", + "refresh-status": "Обновить состояние", + "built-in-auth-desc": "При установке процесс авторизации поставщика откроется в отдельном окне.", + "built-in-generic-desc": "Локальная интеграция Eigent.", + "cancel": "Отмена", + "install": "Установить", + "installing": "Установка…", + "save": "Сохранить", + "enter-value": "Введите {{field}}", + "custom-title": "Добавить пользовательский коннектор", + "custom-subtitle": "Установите доверенный локальный или удалённый MCP-сервер.", + "custom-warning": "Пользовательские MCP-серверы могут выполнять команды или обращаться к удалённым службам. Устанавливайте только конфигурации, которым доверяете.", + "local-json-desc": "Добавьте локальный MCP-сервер, указав корректную конфигурацию JSON.", + "learn-more": "Подробнее", + "connector-name": "Название коннектора", + "remote-name-placeholder": "Мой удалённый MCP", + "remote-url": "URL удалённого MCP", + "remote-url-note": "Используйте HTTPS для серверов за пределами доверенной локальной сети.", + "invalid-json": "Недопустимый JSON: {{message}}", + "json-format-error": "Ошибка формата JSON: {{message}}", + "parse-failed": "Не удалось выполнить разбор", + "missing-mcp-servers": "Конфигурация должна содержать объект mcpServers", + "add-at-least-one": "Добавьте хотя бы один MCP-сервер", + "already-exists": "{{name}} уже существует", + "name-required": "Необходимо указать название коннектора", + "invalid-remote-url": "Введите корректный URL удалённого MCP", + "remote-url-protocol": "URL удалённого MCP должен использовать HTTP или HTTPS", + "remote-missing-id": "Удалённый коннектор создан без идентификатора", + "custom-installed": "Установленные пользовательские коннекторы: {{num}}", + "install-custom-failed": "Не удалось установить пользовательский коннектор", + "env-key-value": "Переменные среды (ключ-значение)", + "value-placeholder": "Значение", + "configuration-title": "Конфигурация", + "google-api-key": "Ключ API Google", + "google-api-key-placeholder": "Введите ключ API Google из Google Cloud Console", + "google-api-key-note": "Как получить ключ API Google → https://developers.google.com/custom-search/v1/overview", + "search-engine-id": "Идентификатор поисковой системы", + "search-engine-id-placeholder": "Введите идентификатор пользовательской поисковой системы, связанный с ключом API", + "google-search-custom-desc": "Подключите Google Custom Search. Требуются ключ API Google и идентификатор пользовательской поисковой системы (CSE).", + "google-search-default-desc": "Поиск Google включён по умолчанию. Ключ API не требуется." +} diff --git a/src/i18n/locales/ru/index.ts b/src/i18n/locales/ru/index.ts index efa753446..ae235fb7b 100644 --- a/src/i18n/locales/ru/index.ts +++ b/src/i18n/locales/ru/index.ts @@ -14,6 +14,7 @@ import agents from './agents.json'; import chat from './chat.json'; +import connectors from './connectors.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; import setting from './setting.json'; @@ -26,6 +27,7 @@ export default { dashboard, workforce, chat, + connectors, setting, update, triggers, diff --git a/src/i18n/locales/zh-Hans/connectors.json b/src/i18n/locales/zh-Hans/connectors.json new file mode 100644 index 000000000..c8bdb712b --- /dev/null +++ b/src/i18n/locales/zh-Hans/connectors.json @@ -0,0 +1,132 @@ +{ + "title": "连接器", + "search-placeholder": "搜索你的连接器", + "browse": "浏览连接器", + "add-custom": "添加自定义", + "retry": "重试", + "your-connectors": "你的连接器", + "no-matching": "没有匹配的连接器。", + "count-one": "个连接器", + "count-other": "个连接器", + "recommended": "为你推荐的连接器。", + "no-recommended": "暂无推荐的连接器。", + "gateway-unavailable": "连接器不可用。", + "gateway-unavailable-desc": "当前部署未启用 Connector Gateway。", + "gateway-connectors": "连接器", + "source-open": "云端", + "source-built-in": "内置", + "source-local": "本地", + "source-remote": "远程", + "load-built-in-failed": "加载内置连接器失败", + "load-custom-failed": "加载自定义连接器失败", + "load-gateway-failed": "加载连接器失败", + "disconnected": "{{name}} 已断开连接", + "disconnect-failed": "断开连接器失败", + "update-failed": "更新连接器失败", + "status-save-failed-enabled": "连接器已启用,但保存状态失败。请刷新后重试。", + "status-save-failed-disabled": "连接器已禁用,但保存状态失败。请刷新后重试。", + "updated": "连接器已更新", + "save-failed": "保存连接器失败", + "deleted": "连接器已删除", + "delete-failed": "删除连接器失败", + "enable-connector": "启用连接器", + "disable-connector": "禁用连接器", + "more-actions": "更多操作", + "open": "打开", + "edit": "编辑", + "delete": "删除", + "connected-account": "已连接账户", + "supported-actions": "支持的操作", + "supported-actions-count": "支持 {{num}} 项操作", + "show-more": "展开更多", + "show-less": "收起", + "provider-website": "服务商网站", + "built-in-title": "本地兼容连接器", + "built-in-desc": "此连接器使用 Eigent 的本地集成运行时。Connector Gateway 是新托管集成的主要市场。", + "configuration": "配置项:{{vars}}", + "status": "状态", + "active": "已启用", + "disabled": "已禁用", + "server-url": "服务器 URL", + "not-configured": "未配置", + "command": "命令", + "arguments": "参数", + "requires": "需要 {{vars}}", + "notion-desc": "连接 Notion 工作区。", + "google-calendar-desc": "管理 Google 日历的事件和日程。", + "generic-desc": "将 {{name}} 连接到 Eigent。", + "google-search": "Google 搜索", + "google-search-desc": "连接 Google 自定义搜索,用于浏览器和研究任务。", + "notion-install-failed": "安装 Notion 失败", + "google-calendar-install-failed": "安装 Google 日历失败", + "search-connectors": "搜索连接器", + "auth-api-key": "API 密钥", + "auth-credential": "凭证", + "auth-oauth": "OAuth", + "auth-none": "无需认证", + "unnamed-action": "未命名操作", + "try-again": "重试", + "no-gateway-found": "未找到连接器。", + "no-built-in-found": "未找到内置连接器。", + "loading": "加载中…", + "updating": "更新中…", + "loading-more": "加载更多…", + "new": "新", + "installed": "已安装", + "local-integration": "本地集成", + "authentication": "认证方式", + "oauth-title": "使用 OAuth 连接", + "oauth-desc": "Eigent 将打开服务商授权页面。授权完成后,此对话框将自动完成安装。", + "oauth-scopes": "请求的权限范围:{{scopes}}", + "no-auth-desc": "此连接器无需凭证。", + "waiting-authorization": "等待授权完成…", + "authorization-started": "授权已开始", + "installed-toast": "{{name}} 已安装", + "no-authorization-url": "连接器未返回授权 URL", + "authorization-pending": "授权仍在进行中。请在服务商窗口中完成授权后重试。", + "install-failed": "安装连接器失败", + "detail-load-failed": "加载连接器详情失败", + "authorization-incomplete": "授权尚未完成。请在服务商窗口中完成授权后再次刷新。", + "refresh-status-failed": "刷新连接器状态失败", + "field-required": "{{field}} 为必填项", + "complete-authorization": "请在服务商窗口中完成授权。", + "refresh-status": "刷新状态", + "built-in-auth-desc": "安装时将在单独窗口中打开服务商授权流程。", + "built-in-generic-desc": "一个 Eigent 本地集成。", + "cancel": "取消", + "install": "安装", + "installing": "安装中…", + "save": "保存", + "enter-value": "请输入 {{field}}", + "custom-title": "添加自定义连接器", + "custom-subtitle": "安装你信任的本地或远程 MCP 服务器。", + "custom-warning": "自定义 MCP 服务器可以执行命令或访问远程服务。请仅安装你信任的配置。", + "local-json-desc": "通过提供有效的 JSON 配置来添加本地 MCP 服务器。", + "learn-more": "了解更多", + "connector-name": "连接器名称", + "remote-name-placeholder": "我的远程 MCP", + "remote-url": "远程 MCP URL", + "remote-url-note": "对于可信本地网络之外的服务器,请使用 HTTPS。", + "invalid-json": "无效的 JSON:{{message}}", + "json-format-error": "JSON 格式错误:{{message}}", + "parse-failed": "解析失败", + "missing-mcp-servers": "配置必须包含 mcpServers 对象", + "add-at-least-one": "请至少添加一个 MCP 服务器", + "already-exists": "{{name}} 已存在", + "name-required": "连接器名称为必填项", + "invalid-remote-url": "请输入有效的远程 MCP URL", + "remote-url-protocol": "远程 MCP URL 必须使用 HTTP 或 HTTPS", + "remote-missing-id": "创建远程连接器时未返回 ID", + "custom-installed": "已安装 {{num}} 个自定义连接器", + "install-custom-failed": "安装自定义连接器失败", + "env-key-value": "环境变量(键值对)", + "value-placeholder": "值", + "configuration-title": "配置", + "google-api-key": "Google API 密钥", + "google-api-key-placeholder": "输入 Google Cloud Console 中的 Google API 密钥", + "google-api-key-note": "了解如何获取 Google API 密钥 → https://developers.google.com/custom-search/v1/overview", + "search-engine-id": "搜索引擎 ID", + "search-engine-id-placeholder": "输入与你的 API 密钥关联的自定义搜索引擎 ID", + "google-search-custom-desc": "连接 Google 自定义搜索。需要 Google API 密钥和自定义搜索引擎(CSE)ID。", + "google-search-default-desc": "Google 搜索默认已启用,无需 API 密钥。" +} diff --git a/src/i18n/locales/zh-Hans/index.ts b/src/i18n/locales/zh-Hans/index.ts index efa753446..ae235fb7b 100644 --- a/src/i18n/locales/zh-Hans/index.ts +++ b/src/i18n/locales/zh-Hans/index.ts @@ -14,6 +14,7 @@ import agents from './agents.json'; import chat from './chat.json'; +import connectors from './connectors.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; import setting from './setting.json'; @@ -26,6 +27,7 @@ export default { dashboard, workforce, chat, + connectors, setting, update, triggers, diff --git a/src/i18n/locales/zh-Hant/connectors.json b/src/i18n/locales/zh-Hant/connectors.json new file mode 100644 index 000000000..55a6a97fe --- /dev/null +++ b/src/i18n/locales/zh-Hant/connectors.json @@ -0,0 +1,132 @@ +{ + "title": "連接器", + "search-placeholder": "搜尋你的連接器", + "browse": "瀏覽連接器", + "add-custom": "新增自訂", + "retry": "重試", + "your-connectors": "你的連接器", + "no-matching": "沒有符合的連接器。", + "count-one": "個連接器", + "count-other": "個連接器", + "recommended": "為你推薦的連接器。", + "no-recommended": "暫無推薦的連接器。", + "gateway-unavailable": "連接器無法使用。", + "gateway-unavailable-desc": "目前部署未啟用 Connector Gateway。", + "gateway-connectors": "連接器", + "source-open": "雲端", + "source-built-in": "內建", + "source-local": "本機", + "source-remote": "遠端", + "load-built-in-failed": "載入內建連接器失敗", + "load-custom-failed": "載入自訂連接器失敗", + "load-gateway-failed": "載入連接器失敗", + "disconnected": "{{name}} 已中斷連接", + "disconnect-failed": "中斷連接器失敗", + "update-failed": "更新連接器失敗", + "status-save-failed-enabled": "連接器已啟用,但儲存狀態失敗。請重新整理後再試。", + "status-save-failed-disabled": "連接器已停用,但儲存狀態失敗。請重新整理後再試。", + "updated": "連接器已更新", + "save-failed": "儲存連接器失敗", + "deleted": "連接器已刪除", + "delete-failed": "刪除連接器失敗", + "enable-connector": "啟用連接器", + "disable-connector": "停用連接器", + "more-actions": "更多操作", + "open": "開啟", + "edit": "編輯", + "delete": "刪除", + "connected-account": "已連接帳戶", + "supported-actions": "支援的操作", + "supported-actions-count": "支援 {{num}} 項操作", + "show-more": "顯示更多", + "show-less": "收合", + "provider-website": "服務商網站", + "built-in-title": "本機相容連接器", + "built-in-desc": "此連接器使用 Eigent 的本機整合執行環境。Connector Gateway 是新託管整合的主要市集。", + "configuration": "設定項目:{{vars}}", + "status": "狀態", + "active": "已啟用", + "disabled": "已停用", + "server-url": "伺服器 URL", + "not-configured": "未設定", + "command": "指令", + "arguments": "參數", + "requires": "需要 {{vars}}", + "notion-desc": "連接 Notion 工作區。", + "google-calendar-desc": "管理 Google 日曆的活動與行程。", + "generic-desc": "將 {{name}} 連接到 Eigent。", + "google-search": "Google 搜尋", + "google-search-desc": "連接 Google 自訂搜尋,用於瀏覽器與研究任務。", + "notion-install-failed": "安裝 Notion 失敗", + "google-calendar-install-failed": "安裝 Google 日曆失敗", + "search-connectors": "搜尋連接器", + "auth-api-key": "API 金鑰", + "auth-credential": "憑證", + "auth-oauth": "OAuth", + "auth-none": "無需驗證", + "unnamed-action": "未命名操作", + "try-again": "再試一次", + "no-gateway-found": "找不到連接器。", + "no-built-in-found": "找不到內建連接器。", + "loading": "載入中…", + "updating": "更新中…", + "loading-more": "載入更多…", + "new": "新", + "installed": "已安裝", + "local-integration": "本機整合", + "authentication": "驗證方式", + "oauth-title": "使用 OAuth 連接", + "oauth-desc": "Eigent 將開啟服務商授權頁面。授權完成後,此對話框將自動完成安裝。", + "oauth-scopes": "要求的權限範圍:{{scopes}}", + "no-auth-desc": "此連接器無需憑證。", + "waiting-authorization": "等待授權完成…", + "authorization-started": "授權已開始", + "installed-toast": "{{name}} 已安裝", + "no-authorization-url": "連接器未回傳授權 URL", + "authorization-pending": "授權仍在進行中。請在服務商視窗中完成授權後再試。", + "install-failed": "安裝連接器失敗", + "detail-load-failed": "載入連接器詳細資訊失敗", + "authorization-incomplete": "授權尚未完成。請在服務商視窗中完成授權後再次重新整理。", + "refresh-status-failed": "重新整理連接器狀態失敗", + "field-required": "{{field}} 為必填欄位", + "complete-authorization": "請在服務商視窗中完成授權。", + "refresh-status": "重新整理狀態", + "built-in-auth-desc": "安裝時將在獨立視窗中開啟服務商授權流程。", + "built-in-generic-desc": "一個 Eigent 本機整合。", + "cancel": "取消", + "install": "安裝", + "installing": "安裝中…", + "save": "儲存", + "enter-value": "請輸入 {{field}}", + "custom-title": "新增自訂連接器", + "custom-subtitle": "安裝你信任的本機或遠端 MCP 伺服器。", + "custom-warning": "自訂 MCP 伺服器可以執行指令或存取遠端服務。請僅安裝你信任的設定。", + "local-json-desc": "透過提供有效的 JSON 設定來新增本機 MCP 伺服器。", + "learn-more": "瞭解更多", + "connector-name": "連接器名稱", + "remote-name-placeholder": "我的遠端 MCP", + "remote-url": "遠端 MCP URL", + "remote-url-note": "對於可信本機網路以外的伺服器,請使用 HTTPS。", + "invalid-json": "無效的 JSON:{{message}}", + "json-format-error": "JSON 格式錯誤:{{message}}", + "parse-failed": "解析失敗", + "missing-mcp-servers": "設定必須包含 mcpServers 物件", + "add-at-least-one": "請至少新增一個 MCP 伺服器", + "already-exists": "{{name}} 已存在", + "name-required": "連接器名稱為必填", + "invalid-remote-url": "請輸入有效的遠端 MCP URL", + "remote-url-protocol": "遠端 MCP URL 必須使用 HTTP 或 HTTPS", + "remote-missing-id": "建立遠端連接器時未回傳 ID", + "custom-installed": "已安裝 {{num}} 個自訂連接器", + "install-custom-failed": "安裝自訂連接器失敗", + "env-key-value": "環境變數(鍵值對)", + "value-placeholder": "值", + "configuration-title": "設定", + "google-api-key": "Google API 金鑰", + "google-api-key-placeholder": "輸入 Google Cloud Console 中的 Google API 金鑰", + "google-api-key-note": "瞭解如何取得 Google API 金鑰 → https://developers.google.com/custom-search/v1/overview", + "search-engine-id": "搜尋引擎 ID", + "search-engine-id-placeholder": "輸入與你的 API 金鑰相關聯的自訂搜尋引擎 ID", + "google-search-custom-desc": "連接 Google 自訂搜尋。需要 Google API 金鑰和自訂搜尋引擎(CSE)ID。", + "google-search-default-desc": "Google 搜尋預設已啟用,無需 API 金鑰。" +} diff --git a/src/i18n/locales/zh-Hant/index.ts b/src/i18n/locales/zh-Hant/index.ts index efa753446..ae235fb7b 100644 --- a/src/i18n/locales/zh-Hant/index.ts +++ b/src/i18n/locales/zh-Hant/index.ts @@ -14,6 +14,7 @@ import agents from './agents.json'; import chat from './chat.json'; +import connectors from './connectors.json'; import dashboard from './dashboard.json'; import layout from './layout.json'; import setting from './setting.json'; @@ -26,6 +27,7 @@ export default { dashboard, workforce, chat, + connectors, setting, update, triggers, diff --git a/src/pages/Connectors/ConnectorGateway.tsx b/src/pages/Connectors/ConnectorGateway.tsx new file mode 100644 index 000000000..9acdcc639 --- /dev/null +++ b/src/pages/Connectors/ConnectorGateway.tsx @@ -0,0 +1,1425 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { mcpInstall, mcpRemove, mcpUpdate } from '@/api/brain'; +import { + disconnectProvider, + fetchConnectedProviders, + fetchConnectorProvider, + fetchConnectorProviders, + invalidateConnectorProvidersCache, + prefetchConnectorProviders, + type ConnectorProvider, +} from '@/api/connectors'; +import { + fetchPost, + proxyFetchDelete, + proxyFetchGet, + proxyFetchPost, + proxyFetchPut, +} from '@/api/http'; +import SearchInput from '@/components/Dashboard/SearchInput'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Switch } from '@/components/ui/switch'; +import { + useIntegrationManagement, + type IntegrationItem, +} from '@/hooks/useIntegrationManagement'; +import { capitalizeFirstLetter, getProxyBaseURL } from '@/lib'; +import { integrationLeadingIconUrl } from '@/lib/connectorIcons'; +import { useAuthStore } from '@/store/authStore'; +import { useServerCapabilityStore } from '@/store/serverCapabilityStore'; +import type { TFunction } from 'i18next'; +import { + BadgeCheck, + ChevronDown, + Ellipsis, + ExternalLink, + Hammer, + Pencil, + Plus, + RefreshCw, + Server, + Settings, + Trash2, + Wrench, +} from 'lucide-react'; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom'; +import { toast } from 'sonner'; +import AddConnectorDialog, { + ProviderIcon, + actionLabel, + isConnectedProvider, + providerActionCount, + providerLabel, + type AddConnectorTarget, +} from './components/AddConnectorDialog'; +import AddCustomConnectorDialog from './components/AddCustomConnectorDialog'; +import { GoogleSearchPanel } from './components/GoogleSearchPanel'; +import MCPConfigDialog from './components/MCPConfigDialog'; +import MCPDeleteDialog from './components/MCPDeleteDialog'; +import type { + ConnectorInstallHint, + MCPConfigForm, + MCPUserItem, +} from './components/types'; +import { arrayToArgsJson, parseArgsToArray } from './components/utils'; + +const IS_LOCAL_MODE = import.meta.env.VITE_USE_LOCAL_PROXY === 'true'; +const OVERVIEW_ID = '__overview__'; +const HIDDEN_BUILT_INS = new Set([ + 'RAG', + 'X(Twitter)', + 'WhatsApp', + 'Reddit', + 'Github', +]); + +/** Preferred hosted connector service keys for the overview recommendations. */ +const RECOMMENDED_SERVICE_KEYS = [ + ['slack'], + ['notion'], + ['gmail', 'google_gmail'], + ['google_drive', 'googledrive', 'google-drive'], + ['github'], + ['google_calendar', 'googlecalendar', 'google-calendar'], + ['stripe'], + ['feishu', 'lark'], +] as const; + +type ConnectorListItem = + | { + id: string; + source: 'open'; + name: string; + active: true; + provider: ConnectorProvider; + } + | { + id: string; + source: 'builtin'; + name: string; + active: true; + item: IntegrationItem; + } + | { + id: string; + source: 'custom'; + name: string; + active: boolean; + subtype: 'local' | 'remote'; + item: MCPUserItem; + }; + +async function upsertConfigValue(group: string, name: string, value: string) { + const response = await proxyFetchGet('/api/v1/configs'); + const configs = Array.isArray(response) ? response : []; + const existing = configs.find((config: any) => config.config_name === name); + const payload = { + config_group: group, + config_name: name, + config_value: value, + }; + if (existing) { + await proxyFetchPut(`/api/v1/configs/${existing.id}`, payload); + } else { + await proxyFetchPost('/api/v1/configs', payload); + } +} + +function createBuiltInInstallAction( + key: string, + t: TFunction +): () => Promise | void { + if (key === 'Search') return () => undefined; + if (key === 'Notion') { + return async () => { + const response = await fetchPost('/install/tool/notion'); + if (!response?.success) { + throw new Error( + response?.error || t('connectors.notion-install-failed') + ); + } + await upsertConfigValue( + 'Notion', + 'MCP_REMOTE_CONFIG_DIR', + response.toolkit_name || 'NotionMCPToolkit' + ); + }; + } + if (key === 'Google Calendar') { + return async () => { + const response = await fetchPost('/install/tool/google_calendar'); + if (response?.success) { + await upsertConfigValue( + 'Google Calendar', + 'GOOGLE_REFRESH_TOKEN', + 'exists' + ); + return; + } + if (response?.status !== 'authorizing') { + throw new Error( + response?.error || + response?.message || + t('connectors.google-calendar-install-failed') + ); + } + }; + } + + return () => { + const baseUrl = getProxyBaseURL(); + window.open( + `${baseUrl}/api/v1/oauth/${key.toLowerCase()}/login`, + '_blank', + 'width=600,height=700' + ); + }; +} + +function buildBuiltInItems(response: unknown, t: TFunction): IntegrationItem[] { + const info = + response && typeof response === 'object' + ? (response as Record) + : {}; + const items = Object.entries(info) + .filter(([key]) => !HIDDEN_BUILT_INS.has(key)) + .map(([key, value]) => ({ + key, + name: key, + env_vars: Array.isArray(value?.env_vars) ? value.env_vars : [], + toolkit: value?.toolkit, + desc: + Array.isArray(value?.env_vars) && value.env_vars.length + ? t('connectors.requires', { vars: value.env_vars.join(', ') }) + : key === 'Notion' + ? t('connectors.notion-desc') + : key === 'Google Calendar' + ? t('connectors.google-calendar-desc') + : t('connectors.generic-desc', { name: key }), + onInstall: createBuiltInInstallAction(key, t), + })); + + if (!items.some((item) => item.key === 'Search')) { + items.unshift({ + key: 'Search', + name: t('connectors.google-search'), + env_vars: ['GOOGLE_API_KEY', 'SEARCH_ENGINE_ID'], + toolkit: undefined, + desc: t('connectors.google-search-desc'), + onInstall: createBuiltInInstallAction('Search', t), + }); + } + return items; +} + +function configuredSearch(configs: any[]): boolean { + const names = new Set( + configs + .filter((config) => String(config.config_value || '').trim()) + .map((config) => config.config_name) + ); + return names.has('GOOGLE_API_KEY') && names.has('SEARCH_ENGINE_ID'); +} + +function sourceLabel(item: ConnectorListItem, t: TFunction): string { + if (item.source === 'open') return t('connectors.source-open'); + if (item.source === 'builtin') return t('connectors.source-built-in'); + return item.subtype === 'remote' + ? t('connectors.source-remote') + : t('connectors.source-local'); +} + +function connectorListRank(item: ConnectorListItem): number { + if (item.source === 'custom') return 0; + if (item.source === 'open') return 1; + if (item.source === 'builtin' && item.item.key === 'Search') return 3; + return 2; +} + +function normalizedProviderKey(value: string): string { + return value.toLowerCase().replace(/[^a-z0-9]+/g, ''); +} + +function findOpenProviderByKeys( + providers: ConnectorProvider[], + serviceKeys: readonly string[] +): ConnectorProvider | null { + const keys = new Set(serviceKeys.map(normalizedProviderKey)); + return ( + providers.find((provider) => { + const service = normalizedProviderKey(provider.service); + const label = normalizedProviderKey(providerLabel(provider)); + return keys.has(service) || keys.has(label); + }) || null + ); +} + +async function resolveOpenProviderByName( + name: string +): Promise { + const normalized = name.toLowerCase().trim(); + const candidates = Array.from( + new Set([ + name, + normalized.replace(/\s+/g, '_'), + normalized.replace(/\s+/g, '-'), + normalized.replace(/\s+/g, ''), + normalized, + ]) + ); + + for (const query of candidates) { + try { + const search = await fetchConnectorProviders({ + page: 1, + pageSize: 24, + query, + }); + const exact = findOpenProviderByKeys(search.providers, candidates); + if (exact) return exact; + const fuzzy = + search.providers.find((provider) => { + const service = provider.service.toLowerCase(); + const label = providerLabel(provider).toLowerCase(); + return service.includes(normalized) || label.includes(normalized); + }) || null; + if (fuzzy) return fuzzy; + } catch { + return null; + } + } + return null; +} + +export default function ConnectorGateway() { + const { t } = useTranslation(); + const [searchParams, setSearchParams] = useSearchParams(); + const { checkAgentTool, modelType } = useAuthStore(); + const capabilityStatus = useServerCapabilityStore((state) => state.status); + const connectorGatewayEnabled = useServerCapabilityStore((state) => + state.isConnectorGatewayEnabled() + ); + const fetchCapabilities = useServerCapabilityStore( + (state) => state.fetchCapabilities + ); + + const [builtInItems, setBuiltInItems] = useState([]); + const [customMcps, setCustomMcps] = useState([]); + const [openConnections, setOpenConnections] = useState( + [] + ); + const [selectedId, setSelectedId] = useState(OVERVIEW_ID); + const [recommendedProviders, setRecommendedProviders] = useState< + ConnectorProvider[] + >([]); + const [recommendedLoading, setRecommendedLoading] = useState(false); + const [openDetail, setOpenDetail] = useState(null); + const [listQuery, setListQuery] = useState(''); + const [loadingOpen, setLoadingOpen] = useState(false); + const [loadingCustom, setLoadingCustom] = useState(false); + const [loadingBuiltIns, setLoadingBuiltIns] = useState(false); + const [detailLoading, setDetailLoading] = useState(false); + const [actionLoading, setActionLoading] = useState(false); + const [pageError, setPageError] = useState(null); + const [browseDialogOpen, setBrowseDialogOpen] = useState(false); + const [browseDialogTarget, setBrowseDialogTarget] = + useState(null); + const [customDialogOpen, setCustomDialogOpen] = useState(false); + const [showConfig, setShowConfig] = useState(null); + const [configForm, setConfigForm] = useState(null); + const [configError, setConfigError] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); + const [deleteLoading, setDeleteLoading] = useState(false); + const [actionsExpanded, setActionsExpanded] = useState(false); + const [actionsOverflow, setActionsOverflow] = useState(false); + const preferredSelectionRef = useRef(null); + const actionsListRef = useRef(null); + + const { + installed: rawBuiltInInstalled, + configs, + fetchInstalled: refreshBuiltIns, + saveEnvAndConfig, + handleUninstall, + } = useIntegrationManagement(builtInItems); + + // Google Search is enabled by default on managed models; custom-model users + // must provide their own Google Custom Search credentials. + const searchRequiresApiKey = modelType === 'custom'; + const builtInInstalled = useMemo(() => { + const next = { ...rawBuiltInInstalled }; + next.Search = searchRequiresApiKey ? configuredSearch(configs) : true; + return next; + }, [configs, rawBuiltInInstalled, searchRequiresApiKey]); + + // When Google Search is enabled by default there is nothing to install, so + // keep it out of the Add-connector dialog; it still shows in the sidebar. + const dialogBuiltInItems = useMemo( + () => + searchRequiresApiKey + ? builtInItems + : builtInItems.filter((item) => item.key !== 'Search'), + [builtInItems, searchRequiresApiKey] + ); + + const loadBuiltInCatalog = useCallback(async () => { + setLoadingBuiltIns(true); + try { + const response = await proxyFetchGet('/api/v1/config/info'); + setBuiltInItems(buildBuiltInItems(response, t)); + } catch (error: any) { + setPageError(error?.message || t('connectors.load-built-in-failed')); + setBuiltInItems(buildBuiltInItems({}, t)); + } finally { + setLoadingBuiltIns(false); + } + }, [t]); + + const loadCustomMcps = useCallback(async () => { + setLoadingCustom(true); + try { + const response = await proxyFetchGet('/api/v1/mcp/users'); + setCustomMcps( + Array.isArray(response) + ? response + : Array.isArray(response?.items) + ? response.items + : [] + ); + } catch (error: any) { + setPageError(error?.message || t('connectors.load-custom-failed')); + setCustomMcps([]); + } finally { + setLoadingCustom(false); + } + }, [t]); + + const loadOpenConnections = useCallback(async () => { + if (!connectorGatewayEnabled) { + setOpenConnections([]); + return; + } + setLoadingOpen(true); + try { + setOpenConnections(await fetchConnectedProviders()); + } catch (error: any) { + setPageError(error?.message || t('connectors.load-gateway-failed')); + setOpenConnections([]); + } finally { + setLoadingOpen(false); + } + }, [connectorGatewayEnabled, t]); + + const refreshAll = useCallback(async () => { + setPageError(null); + await Promise.all([ + loadOpenConnections(), + loadCustomMcps(), + refreshBuiltIns(), + ]); + }, [loadCustomMcps, loadOpenConnections, refreshBuiltIns]); + + useEffect(() => { + void fetchCapabilities(); + void loadBuiltInCatalog(); + void loadCustomMcps(); + }, [fetchCapabilities, loadBuiltInCatalog, loadCustomMcps]); + + useEffect(() => { + if (capabilityStatus !== 'ready' || !connectorGatewayEnabled) return; + // Warm the browse-dialog page-1 cache so Add Connector opens instantly. + void prefetchConnectorProviders({ page: 1, pageSize: 60 }); + void loadOpenConnections(); + }, [capabilityStatus, connectorGatewayEnabled, loadOpenConnections]); + + useEffect(() => { + if (capabilityStatus !== 'ready' || !connectorGatewayEnabled) { + setRecommendedProviders([]); + setRecommendedLoading(false); + return; + } + + let cancelled = false; + setRecommendedLoading(true); + void (async () => { + const catalog = await fetchConnectorProviders({ + page: 1, + pageSize: 60, + }); + if (cancelled) return; + const providers: ConnectorProvider[] = []; + const seen = new Set(); + for (const serviceKeys of RECOMMENDED_SERVICE_KEYS) { + const provider = findOpenProviderByKeys(catalog.providers, serviceKeys); + if ( + !provider || + seen.has(provider.service) || + isConnectedProvider(provider) + ) { + continue; + } + seen.add(provider.service); + providers.push(provider); + } + for (const provider of catalog.providers) { + if (providers.length >= RECOMMENDED_SERVICE_KEYS.length) break; + if (seen.has(provider.service) || isConnectedProvider(provider)) { + continue; + } + seen.add(provider.service); + providers.push(provider); + } + setRecommendedProviders(providers); + setRecommendedLoading(false); + })().catch(() => { + if (cancelled) return; + setRecommendedProviders([]); + setRecommendedLoading(false); + }); + + return () => { + cancelled = true; + }; + }, [capabilityStatus, connectorGatewayEnabled]); + + const connectorItems = useMemo(() => { + const openItems: ConnectorListItem[] = openConnections.map((provider) => ({ + id: `open:${provider.service}`, + source: 'open', + name: providerLabel(provider), + active: true, + provider, + })); + const builtIns: ConnectorListItem[] = builtInItems + .filter((item) => builtInInstalled[item.key]) + .map((item) => ({ + id: `builtin:${item.key}`, + source: 'builtin', + name: item.name, + active: true, + item, + })); + const custom: ConnectorListItem[] = customMcps.map((item) => ({ + id: `custom:${item.id}`, + source: 'custom', + name: capitalizeFirstLetter(item.mcp_name || item.mcp_key || ''), + active: Number(item.status) === 1, + subtype: Number(item.type) === 2 ? 'remote' : 'local', + item, + })); + return [...openItems, ...builtIns, ...custom].sort((left, right) => { + const rankDelta = connectorListRank(left) - connectorListRank(right); + if (rankDelta !== 0) return rankDelta; + if (left.active !== right.active) return left.active ? -1 : 1; + return left.name.localeCompare(right.name); + }); + }, [builtInInstalled, builtInItems, customMcps, openConnections]); + + useEffect(() => { + const preferred = preferredSelectionRef.current; + if (preferred) { + const match = connectorItems.find((item) => { + if (preferred.source === 'open') { + return item.id === `open:${preferred.key}`; + } + if (preferred.source === 'builtin') { + return item.id === `builtin:${preferred.key}`; + } + return ( + item.source === 'custom' && + (item.item.mcp_name === preferred.key || + item.item.mcp_key === preferred.key) + ); + }); + if (match) { + setSelectedId(match.id); + preferredSelectionRef.current = null; + return; + } + } + + if (selectedId === OVERVIEW_ID) return; + if (connectorItems.some((item) => item.id === selectedId)) return; + setSelectedId(OVERVIEW_ID); + }, [connectorItems, selectedId]); + + const selected = useMemo( + () => connectorItems.find((item) => item.id === selectedId) || null, + [connectorItems, selectedId] + ); + + const selectedOpenService = + selected?.source === 'open' ? selected.provider.service : null; + + useEffect(() => { + if (!selectedOpenService) { + setOpenDetail(null); + return; + } + let cancelled = false; + setDetailLoading(true); + setActionsExpanded(false); + void fetchConnectorProvider(selectedOpenService) + .then((response) => { + if (!cancelled) setOpenDetail(response.provider); + }) + .catch(() => { + // Fall back to the list-provider data via `openDetail || item.provider`. + if (!cancelled) setOpenDetail(null); + }) + .finally(() => { + if (!cancelled) setDetailLoading(false); + }); + return () => { + cancelled = true; + }; + }, [selectedOpenService]); + + const openDetailProvider = + selected?.source === 'open' ? openDetail || selected.provider : null; + + useLayoutEffect(() => { + const element = actionsListRef.current; + if (!element || !openDetailProvider?.actions?.length) { + setActionsOverflow(false); + return; + } + setActionsOverflow(element.scrollHeight > 200); + }, [openDetailProvider?.actions, openDetailProvider?.service, detailLoading]); + + useEffect(() => { + const action = searchParams.get('connectorAction'); + const section = searchParams.get('connectorSection'); + if (action !== 'add' && section !== 'mcp-tools' && section !== 'your-mcp') { + return; + } + if (section === 'your-mcp') { + setCustomDialogOpen(true); + } else { + setBrowseDialogTarget(null); + setBrowseDialogOpen(true); + } + const next = new URLSearchParams(searchParams); + next.delete('connectorAction'); + next.delete('connectorSection'); + setSearchParams(next, { replace: true }); + }, [searchParams, setSearchParams]); + + useEffect(() => { + if (!showConfig) { + setConfigForm(null); + setConfigError(null); + return; + } + setConfigForm({ + mcp_name: showConfig.mcp_name || '', + mcp_desc: showConfig.mcp_desc || '', + command: showConfig.command || '', + argsArr: showConfig.args ? parseArgsToArray(showConfig.args) : [], + env: showConfig.env ? { ...showConfig.env } : {}, + server_url: showConfig.server_url || '', + }); + }, [showConfig]); + + const visibleItems = useMemo(() => { + const query = listQuery.trim().toLowerCase(); + if (!query) return connectorItems; + return connectorItems.filter( + (item) => + item.name.toLowerCase().includes(query) || + sourceLabel(item, t).toLowerCase().includes(query) + ); + }, [connectorItems, listQuery, t]); + + const openBrowseDialog = (target: AddConnectorTarget = null) => { + // Non-local hosts always use Connector Gateway providers — never Built-in. + // Open the dialog immediately and resolve the matching hosted provider in the + // background so the button never feels dead. + if (!IS_LOCAL_MODE && target?.source === 'builtin') { + setBrowseDialogTarget(null); + setBrowseDialogOpen(true); + if (connectorGatewayEnabled) { + const builtInKey = target.item.key; + void resolveOpenProviderByName(builtInKey).then((provider) => { + if (provider) { + setBrowseDialogTarget({ source: 'open', provider }); + } + }); + } + return; + } + setBrowseDialogTarget(target); + setBrowseDialogOpen(true); + }; + + const openCustomDialog = () => { + setCustomDialogOpen(true); + }; + + const openRecommendedConnector = (provider: ConnectorProvider) => { + const existing = connectorItems.find( + (item) => + item.source === 'open' && item.provider.service === provider.service + ); + if (existing) { + setSelectedId(existing.id); + return; + } + openBrowseDialog({ source: 'open', provider }); + }; + + const handleInstalled = useCallback( + async (hint: ConnectorInstallHint) => { + preferredSelectionRef.current = hint; + invalidateConnectorProvidersCache(); + await refreshAll(); + }, + [refreshAll] + ); + + const handleDisconnectOpen = async (provider: ConnectorProvider) => { + setActionLoading(true); + try { + await disconnectProvider( + provider.service, + provider.connection?.connectionName + ); + invalidateConnectorProvidersCache(); + toast.success( + t('connectors.disconnected', { name: providerLabel(provider) }) + ); + await loadOpenConnections(); + } catch (error: any) { + toast.error(error?.message || t('connectors.disconnect-failed')); + } finally { + setActionLoading(false); + } + }; + + const handleDisconnectBuiltIn = async (item: IntegrationItem) => { + setActionLoading(true); + try { + await handleUninstall(item); + await refreshBuiltIns(); + toast.success(t('connectors.disconnected', { name: item.name })); + } catch (error: any) { + toast.error(error?.message || t('connectors.disconnect-failed')); + } finally { + setActionLoading(false); + } + }; + + const handleCustomSwitch = async (item: MCPUserItem, checked: boolean) => { + setActionLoading(true); + try { + const key = item.mcp_key || item.mcp_name; + if (checked) { + if (Number(item.type) === 2) { + await mcpInstall(key, { url: item.server_url || '' }); + } else { + await mcpInstall(key, { + description: item.mcp_desc || '', + command: item.command || '', + args: item.args ? parseArgsToArray(item.args) : [], + ...(item.env && Object.keys(item.env).length + ? { env: item.env } + : {}), + }); + } + } else { + await mcpRemove(key); + } + try { + await proxyFetchPut(`/api/v1/mcp/users/${item.id}`, { + status: checked ? 1 : 2, + }); + } catch { + // The runtime install/remove already succeeded; only the saved status + // is stale now, so surface that specifically. + toast.error( + t( + checked + ? 'connectors.status-save-failed-enabled' + : 'connectors.status-save-failed-disabled' + ) + ); + } + await loadCustomMcps(); + } catch (error: any) { + toast.error(error?.message || t('connectors.update-failed')); + } finally { + setActionLoading(false); + } + }; + + const handleConfigSave = async (event: React.FormEvent) => { + event.preventDefault(); + if (!configForm || !showConfig) return; + setActionLoading(true); + setConfigError(null); + try { + const isRemote = Number(showConfig.type) === 2; + const payload = isRemote + ? { + mcp_name: configForm.mcp_name, + mcp_desc: configForm.mcp_desc, + server_url: configForm.server_url, + } + : { + mcp_name: configForm.mcp_name, + mcp_desc: configForm.mcp_desc, + command: configForm.command, + args: arrayToArgsJson(configForm.argsArr), + env: configForm.env, + }; + await proxyFetchPut(`/api/v1/mcp/users/${showConfig.id}`, payload); + if (isRemote) { + await mcpUpdate(showConfig.mcp_key || showConfig.mcp_name, { + url: configForm.server_url, + }); + } else { + const brainPayload: Record = { + description: configForm.mcp_desc, + command: configForm.command, + args: arrayToArgsJson(configForm.argsArr), + }; + if (Object.keys(configForm.env).length) { + brainPayload.env = configForm.env; + } + await mcpUpdate( + showConfig.mcp_key || showConfig.mcp_name, + brainPayload + ); + } + setShowConfig(null); + await loadCustomMcps(); + toast.success(t('connectors.updated')); + } catch (error: any) { + setConfigError(error?.message || t('connectors.save-failed')); + } finally { + setActionLoading(false); + } + }; + + const handleDelete = async () => { + if (!deleteTarget) return; + setDeleteLoading(true); + try { + checkAgentTool(deleteTarget.mcp_name); + await proxyFetchDelete(`/api/v1/mcp/users/${deleteTarget.id}`); + const key = deleteTarget.mcp_key || deleteTarget.mcp_name; + if (key) await mcpRemove(key); + setDeleteTarget(null); + await loadCustomMcps(); + toast.success(t('connectors.deleted')); + } catch (error: any) { + toast.error(error?.message || t('connectors.delete-failed')); + } finally { + setDeleteLoading(false); + } + }; + + const pageLoading = loadingOpen || loadingCustom || loadingBuiltIns; + + const renderListIcon = (item: ConnectorListItem) => { + if (item.source === 'open') { + return ; + } + if (item.source === 'builtin') { + const iconUrl = integrationLeadingIconUrl(item.item.key); + return iconUrl ? ( + + ) : ( + + ); + } + return item.subtype === 'remote' ? ( + + ) : ( + + ); + }; + + const isDefaultEnabledSearch = (item: ConnectorListItem) => + item.source === 'builtin' && + item.item.key === 'Search' && + !searchRequiresApiKey; + + const renderDetailHeader = (item: ConnectorListItem) => ( +
+ {item.source === 'open' ? ( + + ) : ( + renderListIcon(item) + )} + + {item.name} + + {item.source !== 'open' ? ( + + {sourceLabel(item, t)} + + ) : null} +
+ {item.source === 'custom' ? ( + + void handleCustomSwitch(item.item, checked) + } + aria-label={ + item.active + ? t('connectors.disable-connector') + : t('connectors.enable-connector') + } + /> + ) : item.source === 'builtin' && item.item.key === 'Search' ? null : ( + + )} + {isDefaultEnabledSearch(item) ? null : ( + + + + + + {item.source === 'custom' ? ( + setShowConfig(item.item)}> + + {t('connectors.edit')} + + ) : null} + { + if (item.source === 'open') { + void handleDisconnectOpen(item.provider); + return; + } + if (item.source === 'builtin') { + void handleDisconnectBuiltIn(item.item); + return; + } + setDeleteTarget(item.item); + }} + > + + {t('connectors.delete')} + + + + )} +
+
+ ); + + const renderOpenDetailBody = ( + item: Extract + ) => { + const provider = openDetail || item.provider; + if (detailLoading) { + return ( +
+ ); + } + return ( + <> + {provider.connection?.profile?.displayName ? ( +
+ + {t('connectors.connected-account')} + + + {provider.connection.profile.displayName} + +
+ ) : null} + + {provider.actions?.length ? ( +
+
+ + {t('connectors.supported-actions')} + + + {provider.actions.length || providerActionCount(provider)} + +
+
+
+
+ {provider.actions.map((action, index) => ( + + {actionLabel(action, t)} + + ))} +
+ {!actionsExpanded && actionsOverflow ? ( +
+ ) : null} +
+ {actionsOverflow ? ( + + ) : null} +
+
+ ) : null} + + {provider.homepageUrl ? ( + + {t('connectors.provider-website')} + + + ) : null} + + ); + }; + + const renderBuiltInDetailBody = ( + item: Extract + ) => { + if (item.item.key === 'Search') { + return ( +
+ void refreshBuiltIns()} /> +
+ ); + } + return ( +
+ + {t('connectors.built-in-title')} + + + {t('connectors.built-in-desc')} + + {item.item.env_vars.length ? ( + + {t('connectors.configuration', { + vars: item.item.env_vars.join(', '), + })} + + ) : null} +
+ ); + }; + + const renderCustomDetailBody = ( + item: Extract + ) => { + const mcp = item.item; + return ( +
+
+ + {t('connectors.status')} + + + {item.active ? t('connectors.active') : t('connectors.disabled')} + +
+ {item.subtype === 'remote' ? ( +
+ + {t('connectors.server-url')} + + + {mcp.server_url || t('connectors.not-configured')} + +
+ ) : ( +
+
+ + {t('connectors.command')} + + + {mcp.command || t('connectors.not-configured')} + +
+ {mcp.args ? ( +
+ + {t('connectors.arguments')} + + + {parseArgsToArray(mcp.args).join(' ')} + +
+ ) : null} +
+ )} +
+ ); + }; + + const renderDetailPanel = (item: ConnectorListItem) => ( +
+ {renderDetailHeader(item)} +
+ {item.source === 'open' + ? renderOpenDetailBody(item) + : item.source === 'builtin' + ? renderBuiltInDetailBody(item) + : renderCustomDetailBody(item)} +
+
+ ); + + const renderOverviewPanel = () => { + const count = connectorItems.length; + return ( +
+
+ + {pageLoading && count === 0 ? '—' : count} + + + {count === 1 + ? t('connectors.count-one') + : t('connectors.count-other')} + +
+ +
+ + {t('connectors.recommended')} + + {recommendedLoading && recommendedProviders.length === 0 ? ( +
+ {Array.from({ length: 8 }).map((_, index) => ( +
+ ))} +
+ ) : recommendedProviders.length === 0 ? ( +
+ {connectorGatewayEnabled + ? t('connectors.no-recommended') + : t('connectors.gateway-unavailable')} +
+ ) : ( +
+ {recommendedProviders.map((provider) => { + const liveProvider = + openConnections.find( + (item) => item.service === provider.service + ) || provider; + const connected = isConnectedProvider(liveProvider); + const existing = connectorItems.find( + (item) => + item.source === 'open' && + item.provider.service === provider.service + ); + return ( + + ); + })} +
+ )} +
+
+ ); + }; + + return ( +
+
+

+ {t('connectors.title')} +

+
+ setListQuery(event.target.value)} + placeholder={t('connectors.search-placeholder')} + /> + + +
+
+ + {pageError ? ( +
+ {pageError} + +
+ ) : null} + +
+ + +
+ {selectedId === OVERVIEW_ID || !selected + ? renderOverviewPanel() + : renderDetailPanel(selected)} +
+
+ + { + setBrowseDialogOpen(next); + if (!next) setBrowseDialogTarget(null); + }} + onInstalled={handleInstalled} + saveBuiltInValue={saveEnvAndConfig} + refreshBuiltIns={refreshBuiltIns} + /> + + + + void} + onSave={handleConfigSave} + onClose={() => setShowConfig(null)} + loading={actionLoading} + errorMsg={configError} + onSwitchStatus={() => undefined} + /> + setDeleteTarget(null)} + onConfirm={handleDelete} + loading={deleteLoading} + /> +
+ ); +} diff --git a/src/pages/Connectors/MCP.tsx b/src/pages/Connectors/MCP.tsx deleted file mode 100644 index dfde0273f..000000000 --- a/src/pages/Connectors/MCP.tsx +++ /dev/null @@ -1,1351 +0,0 @@ -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= - -import { mcpInstall, mcpRemove, mcpUpdate } from '@/api/brain'; -import { - fetchGet, - fetchPost, - proxyFetchDelete, - proxyFetchGet, - proxyFetchPost, - proxyFetchPut, -} from '@/api/http'; -import googleIcon from '@/assets/icon/google.svg'; -import ellipseIcon from '@/assets/mcp/Ellipse-25.svg'; -import SearchInput from '@/components/Dashboard/SearchInput'; -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { Switch } from '@/components/ui/switch'; -import { TooltipSimple } from '@/components/ui/tooltip'; -import { - useIntegrationManagement, - type IntegrationItem, -} from '@/hooks/useIntegrationManagement'; -import { capitalizeFirstLetter, getProxyBaseURL } from '@/lib'; -import { integrationLeadingIconUrl } from '@/lib/connectorIcons'; -import { useAuthStore } from '@/store/authStore'; -import { motion } from 'framer-motion'; -import { - ChevronDown, - ChevronUp, - MoreHorizontal, - Pencil, - Plus, - Settings2, - Trash2, - Wrench, -} from 'lucide-react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useSearchParams } from 'react-router-dom'; -import { GoogleSearchPanel } from './components/GoogleSearchPanel'; -import MCPAddDialog from './components/MCPAddDialog'; -import MCPConfigDialog from './components/MCPConfigDialog'; -import MCPDeleteDialog from './components/MCPDeleteDialog'; -import { MCPEnvDialog } from './components/MCPEnvDialog'; -import type { MCPConfigForm, MCPUserItem } from './components/types'; -import { arrayToArgsJson, parseArgsToArray } from './components/utils'; - -import { ConfigFile } from 'electron/main/utils/mcpConfig'; -import { toast } from 'sonner'; - -// Filter out Search from integrations (it's now a hardcoded connector item below) -const EXCLUDED_FROM_MCP = ['Search', 'RAG']; - -const GOOGLE_SEARCH_ID = 'google-search' as const; - -const COMING_SOON_NAMES = [ - 'X(Twitter)', - 'WhatsApp', - 'Reddit', - 'Github', -] as const; - -export default function SettingMCP() { - const { checkAgentTool } = useAuthStore(); - const { t } = useTranslation(); - const [searchParams, setSearchParams] = useSearchParams(); - const [items, setItems] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [showConfig, setShowConfig] = useState(null); - const [configForm, setConfigForm] = useState(null); - const [saving, setSaving] = useState(false); - const [errorMsg, setErrorMsg] = useState(null); - const [showAdd, setShowAdd] = useState(false); - const [addType, setAddType] = useState<'local' | 'remote'>('local'); - const [localJson, setLocalJson] = useState( - `{ - "mcpServers": { - "sequential-thinking": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-sequential-thinking" - ] - } - } -}` - ); - const [remoteName, setRemoteName] = useState(''); - const [remoteUrl, setRemoteUrl] = useState(''); - const [installing, setInstalling] = useState(false); - const [deleteTarget, setDeleteTarget] = useState(null); - const [deleting, setDeleting] = useState(false); - const [switchLoading, setSwitchLoading] = useState>( - {} - ); - const [searchQuery, setSearchQuery] = useState(''); - const [webCollapsed, setWebCollapsed] = useState(false); - const [yourCollapsed, setYourCollapsed] = useState(false); - const [selected, setSelected] = useState< - | { type: 'web'; key: string } - | { type: 'your'; id: number } - | { type: typeof GOOGLE_SEARCH_ID } - | null - >(null); - const [showEnvConfig, setShowEnvConfig] = useState(false); - const [activeMcp, setActiveMcp] = useState(null); - const [folderHint, setFolderHint] = useState<'web' | 'your' | null>(null); - - const [integrations, setIntegrations] = useState([]); - const [isLoadingIntegrations, setIsLoadingIntegrations] = useState(true); - - const integrationItems = useMemo((): IntegrationItem[] => { - const searchItem: IntegrationItem = { - key: 'Search', - name: 'Search', - desc: '', - env_vars: [], - onInstall: async () => {}, - }; - return [searchItem, ...(integrations as IntegrationItem[])]; - }, [integrations]); - - const { - installed, - fetchInstalled, - saveEnvAndConfig, - handleUninstall, - createMcpFromItem, - } = useIntegrationManagement(integrationItems); - - const searchConnected = !!installed.Search; - - const refreshConnectorConfigs = useCallback(() => { - void fetchInstalled(); - }, [fetchInstalled]); - - useEffect(() => { - const action = searchParams.get('connectorAction'); - const section = searchParams.get('connectorSection'); - if (action !== 'add' && section !== 'mcp-tools' && section !== 'your-mcp') { - return; - } - const next = new URLSearchParams(searchParams); - if (action === 'add') { - setShowAdd(true); - next.delete('connectorAction'); - } - if (section === 'mcp-tools') { - setFolderHint('web'); - setWebCollapsed(false); - setYourCollapsed(true); - next.delete('connectorSection'); - } - if (section === 'your-mcp') { - setFolderHint('your'); - setYourCollapsed(false); - setWebCollapsed(true); - next.delete('connectorSection'); - } - setSearchParams(next, { replace: true }); - }, [searchParams, setSearchParams]); - - // Filter integrations (MCP & Tools) by search - const filteredIntegrations = useMemo(() => { - if (!searchQuery.trim()) return integrations; - const q = searchQuery.toLowerCase().trim(); - return integrations.filter( - (item) => - (item.key || '').toLowerCase().includes(q) || - (item.name || '').toLowerCase().includes(q) || - (item.desc || '').toLowerCase().includes(q) - ); - }, [integrations, searchQuery]); - - // Filter your MCPs by search - const filteredItems = useMemo(() => { - if (!searchQuery.trim()) return items; - const q = searchQuery.toLowerCase().trim(); - return items.filter( - (item) => - (item.mcp_name || '').toLowerCase().includes(q) || - (item.mcp_desc || '').toLowerCase().includes(q) || - (item.mcp_key || '').toLowerCase().includes(q) - ); - }, [items, searchQuery]); - - const webConnected = useMemo( - () => filteredIntegrations.filter((i) => installed[i.key]), - [filteredIntegrations, installed] - ); - - type SortedWebItem = - | { kind: 'google-search'; connected: boolean } - | { - kind: 'integration'; - item: IntegrationItem; - connected: boolean; - comingSoon: boolean; - }; - - const sortedWebItems = useMemo((): SortedWebItem[] => { - const all: SortedWebItem[] = [ - { kind: 'google-search', connected: searchConnected }, - ...filteredIntegrations.map((item) => ({ - kind: 'integration' as const, - item, - connected: !!installed[item.key], - comingSoon: (COMING_SOON_NAMES as readonly string[]).includes( - item.name - ), - })), - ]; - - const priority = (w: SortedWebItem) => { - if (w.kind === 'integration' && w.comingSoon) return 2; - return w.connected ? 0 : 1; - }; - const getName = (w: SortedWebItem) => - w.kind === 'google-search' ? 'Google Search' : w.item.name; - - return [...all].sort((a, b) => { - const diff = priority(a) - priority(b); - return diff !== 0 ? diff : getName(a).localeCompare(getName(b)); - }); - }, [filteredIntegrations, installed, searchConnected]); - - // get list - const fetchList = useCallback(() => { - setIsLoading(true); - setError(''); - proxyFetchGet('/api/v1/mcp/users') - .then((res) => { - if (Array.isArray(res)) { - setItems(res); - } else if (Array.isArray(res.items)) { - setItems(res.items); - } else { - setItems([]); - } - }) - .catch((err) => { - setError(err?.message || t('setting.load-failed')); - }) - .finally(() => { - setIsLoading(false); - }); - }, [t]); - - // get integrations - useEffect(() => { - setIsLoadingIntegrations(true); - proxyFetchGet('/api/v1/config/info') - .then((res) => { - if (res && typeof res === 'object') { - const baseURL = getProxyBaseURL(); - const list = Object.entries(res).map( - ([key, value]: [string, any]) => { - let onInstall = null; - - // Special handling for Notion MCP - if (key.toLowerCase() === 'notion') { - onInstall = async () => { - try { - const response = await fetchPost('/install/tool/notion'); - if (response.success) { - // Check if there's a warning (connection failed but installation marked as complete) - if (response.warning) { - toast.warning(response.warning, { duration: 5000 }); - } else { - toast.success( - t('setting.notion-mcp-installed-successfully') - ); - } - // Save to config to mark as installed - await proxyFetchPost('/api/v1/configs', { - config_group: 'Notion', - config_name: 'MCP_REMOTE_CONFIG_DIR', - config_value: - response.toolkit_name || 'NotionMCPToolkit', - }); - // Refresh the integrations list to show the installed state - fetchList(); - void fetchInstalled(); - } else { - toast.error( - response.error || - t('setting.failed-to-install-notion-mcp') - ); - } - } catch (error: any) { - toast.error( - error.message || t('setting.failed-to-install-notion-mcp') - ); - } - }; - } else if (key.toLowerCase() === 'google calendar') { - onInstall = async () => { - try { - const response = await fetchPost( - '/install/tool/google_calendar' - ); - if (response.success) { - // Check if there's a warning (connection failed but installation marked as complete) - if (response.warning) { - toast.warning(response.warning, { duration: 5000 }); - } else { - toast.success( - t('setting.google-calendar-installed-successfully') - ); - } - try { - // Ensure we persist a marker config to indicate installation - const existingConfigs = - await proxyFetchGet('/api/v1/configs'); - const existing = Array.isArray(existingConfigs) - ? existingConfigs.find( - (c: any) => - c.config_group?.toLowerCase() === - 'google calendar' && - c.config_name === 'GOOGLE_REFRESH_TOKEN' - ) - : null; - - const configPayload = { - config_group: 'Google Calendar', - config_name: 'GOOGLE_REFRESH_TOKEN', - config_value: 'exists', - }; - - if (existing) { - await proxyFetchPut( - `/api/v1/configs/${existing.id}`, - configPayload - ); - } else { - await proxyFetchPost( - '/api/v1/configs', - configPayload - ); - } - } catch (configError) { - console.warn( - 'Failed to persist Google Calendar config', - configError - ); - } - // Refresh the integrations list to show the installed state - fetchList(); - void fetchInstalled(); - } else if (response.status === 'authorizing') { - // Authorization in progress - start polling for completion - toast.info( - t('setting.please-complete-authorization-in-browser') - ); - - // Poll for authorization completion via oauth status endpoint - const pollInterval = setInterval(async () => { - try { - const statusResp = await fetchGet( - '/oauth/status/google_calendar' - ); - if (statusResp?.status === 'success') { - clearInterval(pollInterval); - // Now that auth succeeded, run install again to initialize toolkit - const finalize = await fetchPost( - '/install/tool/google_calendar' - ); - if (finalize?.success) { - const configs = - await proxyFetchGet('/api/v1/configs'); - const existing = Array.isArray(configs) - ? configs.find( - (c: any) => - c.config_group?.toLowerCase() === - 'google calendar' && - c.config_name === 'GOOGLE_REFRESH_TOKEN' - ) - : null; - - const payload = { - config_group: 'Google Calendar', - config_name: 'GOOGLE_REFRESH_TOKEN', - config_value: 'exists', - }; - - if (existing) { - await proxyFetchPut( - `/api/v1/configs/${existing.id}`, - payload - ); - } else { - await proxyFetchPost( - '/api/v1/configs', - payload - ); - } - - toast.success( - t( - 'setting.google-calendar-installed-successfully' - ) - ); - fetchList(); - void fetchInstalled(); - } - } else if ( - statusResp?.status === 'failed' || - statusResp?.status === 'cancelled' - ) { - clearInterval(pollInterval); - const msg = - statusResp?.error || - (statusResp?.status === 'cancelled' - ? t('setting.authorization-cancelled') - : t('setting.authorization-failed')); - toast.error(msg); - } - // if still authorizing, continue polling - } catch (err) { - console.error('Polling oauth status failed', err); - } - }, 2000); - - // Safety timeout - setTimeout( - () => clearInterval(pollInterval), - 5 * 60 * 1000 - ); - } else { - toast.error( - response.error || - response.message || - t('setting.failed-to-install-google-calendar') - ); - } - } catch (error: any) { - toast.error( - error.message || - t('setting.failed-to-install-google-calendar') - ); - } - }; - } else { - onInstall = () => { - const url = `${baseURL}/api/v1/oauth/${key.toLowerCase()}/login`; - // Open in a new window to avoid navigating the app/webview - window.open(url, '_blank'); - }; - } - - return { - key, - name: key, - env_vars: value.env_vars, - desc: - value.env_vars && value.env_vars.length > 0 - ? `${t( - 'setting.environmental-variables-required' - )}: ${value.env_vars.join(', ')}` - : key.toLowerCase() === 'notion' - ? t('setting.notion-workspace-integration') - : key.toLowerCase() === 'google calendar' - ? t('setting.google-calendar-integration') - : '', - onInstall, - }; - } - ); - setIntegrations( - list.filter((item) => !EXCLUDED_FROM_MCP.includes(item.key)) - ); - } - }) - .finally(() => { - setIsLoadingIntegrations(false); - void fetchInstalled(); - }); - }, [fetchList, t, fetchInstalled]); - - useEffect(() => { - fetchList(); - }, [fetchList]); - - // MCP list switch - const handleSwitch = async (id: number, checked: boolean) => { - setSwitchLoading((l) => ({ ...l, [id]: true })); - try { - await proxyFetchPut(`/api/v1/mcp/users/${id}`, { - status: checked ? 1 : 2, - }); - fetchList(); - } finally { - setSwitchLoading((l) => ({ ...l, [id]: false })); - } - }; - - const onCloseEnv = useCallback(() => { - setShowEnvConfig(false); - setActiveMcp(null); - }, []); - - const handleWebInstall = useCallback( - async (item: IntegrationItem) => { - if (item.key === 'Lark' || item.key === 'RAG') { - const mcp = createMcpFromItem(item, item.key === 'Lark' ? 15 : 16); - setActiveMcp(mcp); - setShowEnvConfig(true); - return; - } - - if (item.key === 'Google Calendar') { - const mcp = createMcpFromItem(item, 14); - setActiveMcp(mcp); - setShowEnvConfig(true); - return; - } - - if (item.key === 'LinkedIn') { - const baseUrl = getProxyBaseURL(); - window.open( - `${baseUrl}/api/v1/oauth/linkedin/login`, - '_blank', - 'width=600,height=700' - ); - return; - } - - if (installed[item.key]) return; - await item.onInstall(); - await fetchInstalled(); - fetchList(); - }, - [installed, createMcpFromItem, fetchInstalled, fetchList] - ); - - const onEnvConnect = useCallback( - async (mcp: any) => { - await fetchInstalled(); - await Promise.all( - Object.keys(mcp.install_command.env).map(async (k) => { - return saveEnvAndConfig(mcp.key, k, mcp.install_command.env[k]); - }) - ); - - if (mcp.key === 'Google Calendar') { - const calendarItem = integrations.find( - (it: IntegrationItem) => it.key === 'Google Calendar' - ); - try { - if (calendarItem?.onInstall) await calendarItem.onInstall(); - else await fetchPost('/install/tool/google_calendar'); - } catch (_) {} - } - - await fetchInstalled(); - fetchList(); - onCloseEnv(); - }, - [fetchInstalled, saveEnvAndConfig, integrations, fetchList, onCloseEnv] - ); - - const handleOpenWebConfig = useCallback( - (item: IntegrationItem) => { - if (item.env_vars?.length > 0) { - const mcp = createMcpFromItem(item, -1); - setActiveMcp(mcp); - setShowEnvConfig(true); - } - }, - [createMcpFromItem] - ); - - const renderSidebarRow = ( - tabId: string, - label: string, - kind: 'web' | 'your', - isActive: boolean, - onSelect: () => void, - webInstalled?: boolean, - yourEnabled?: boolean, - integrationKey?: string - ) => { - const assetUrl = - kind === 'web' && integrationKey - ? integrationLeadingIconUrl(integrationKey) - : undefined; - - return ( - - ); - }; - - useEffect(() => { - if (!folderHint) return; - if (folderHint === 'your') { - if (isLoading) return; - if (filteredItems.length > 0) { - setSelected({ type: 'your', id: filteredItems[0].id }); - } - setFolderHint(null); - return; - } - if (folderHint === 'web') { - if (isLoadingIntegrations) return; - const pick = filteredIntegrations[0]; - if (pick) setSelected({ type: 'web', key: pick.key }); - setFolderHint(null); - } - }, [ - folderHint, - filteredItems, - filteredIntegrations, - isLoading, - isLoadingIntegrations, - ]); - - useEffect(() => { - if (!selected) return; - if (selected.type === GOOGLE_SEARCH_ID) return; - if (selected.type === 'web') { - if (!integrations.some((i) => i.key === selected.key)) { - setSelected(null); - } - return; - } - if (!items.some((i) => i.id === selected.id)) { - setSelected(null); - } - }, [selected, integrations, items]); - - useEffect(() => { - if (selected || isLoadingIntegrations || isLoading || folderHint) return; - const pick = webConnected[0] || filteredItems[0] || filteredIntegrations[0]; - if (!pick) return; - if ('mcp_name' in pick) { - setSelected({ type: 'your', id: pick.id }); - } else { - setSelected({ type: 'web', key: pick.key }); - } - }, [ - selected, - webConnected, - filteredIntegrations, - filteredItems, - isLoadingIntegrations, - isLoading, - folderHint, - ]); - - const renderConnectionPanel = () => { - if (!selected) { - return ( -
- {t('setting.mcp-select-connection')} -
- ); - } - - if (selected.type === GOOGLE_SEARCH_ID) { - return ( -
-
-
-
- -
-
- Google Search -
-
- {searchConnected && ( - - {t('setting.configured', { defaultValue: 'Configured' })} - - )} -
- -
- ); - } - - if (selected.type === 'web') { - const item = integrations.find((i) => i.key === selected.key) as - | IntegrationItem - | undefined; - if (!item) return null; - const isConn = !!installed[item.key]; - const isComingSoon = (COMING_SOON_NAMES as readonly string[]).includes( - item.name - ); - const headerAssetUrl = integrationLeadingIconUrl(item.key); - - return ( -
-
-
- {headerAssetUrl ? ( -
- -
- ) : ( -
- -
- )} -
- {item.name} -
-
-
- - {item.env_vars?.length > 0 ? ( - - - - ) : null} -
-
- {item.desc ? ( -
- - {item.desc} - -
- ) : null} -
- ); - } - - const userItem = items.find((i) => i.id === selected.id); - if (!userItem) return null; - const enabled = userItem.status === 1; - - return ( -
-
-
- -
- {capitalizeFirstLetter(userItem.mcp_name || '')} -
-
-
- - void handleSwitch(userItem.id, checked) - } - aria-label={enabled ? t('setting.disable') : t('setting.enable')} - /> - - - - - - setShowConfig(userItem)} - > - - {t('setting.edit', { defaultValue: 'Edit' })} - - setDeleteTarget(userItem)} - > - - {t('setting.delete', { defaultValue: 'Delete' })} - - - -
-
-
- ); - }; - - // config dialog - useEffect(() => { - if (showConfig) { - setConfigForm({ - mcp_name: showConfig.mcp_name || '', - mcp_desc: showConfig.mcp_desc || '', - command: showConfig.command || '', - argsArr: showConfig.args ? parseArgsToArray(showConfig.args) : [], - env: showConfig.env ? { ...showConfig.env } : {}, - }); - setErrorMsg(null); - } else { - setConfigForm(null); - setErrorMsg(null); - } - }, [showConfig]); - - const handleConfigSave = async (e: React.FormEvent) => { - e.preventDefault(); - if (!configForm || !showConfig) return; - setSaving(true); - setErrorMsg(null); - try { - const mcpData = { - mcp_name: configForm.mcp_name, - mcp_desc: configForm.mcp_desc, - command: configForm.command, - args: arrayToArgsJson(configForm.argsArr), - env: configForm.env, - }; - await proxyFetchPut(`/api/v1/mcp/users/${showConfig.id}`, mcpData); - - const payload: Record = { - description: configForm.mcp_desc, - command: configForm.command, - args: arrayToArgsJson(configForm.argsArr), - }; - if (configForm.env && Object.keys(configForm.env).length > 0) { - payload.env = configForm.env; - } - await mcpUpdate(mcpData.mcp_name, payload); - - setShowConfig(null); - fetchList(); - } catch (err: any) { - setErrorMsg(err?.message || t('setting.save-failed')); - } finally { - setSaving(false); - } - }; - const handleConfigClose = () => { - setShowConfig(null); - setConfigForm(null); - setErrorMsg(null); - }; - const handleConfigSwitch = async (checked: boolean) => { - if (!showConfig) return; - setSaving(true); - try { - await proxyFetchPut(`/api/v1/mcp/users/${showConfig.id}`, { - status: checked ? 1 : 0, - }); - setShowConfig((prev) => - prev ? { ...prev, status: checked ? 1 : 0 } : prev - ); - fetchList(); - } finally { - setSaving(false); - } - }; - - // add MCP dialog - const handleInstall = async () => { - setInstalling(true); - try { - if (addType === 'local') { - let data: ConfigFile; - try { - data = JSON.parse(localJson); - - // validate mcpServers structure - if (!data.mcpServers || typeof data.mcpServers !== 'object') { - throw new Error('Invalid mcpServers'); - } - - // check for name conflicts with existing items - const serverNames = Object.keys(data.mcpServers); - const conflict = serverNames.find((name) => - items.some((d) => d.mcp_name === name) - ); - if (conflict) { - toast.error( - t('setting.mcp-server-already-exists', { name: conflict }), - { - closeButton: true, - } - ); - setInstalling(false); - return; - } - } catch (e) { - console.error('Invalid JSON:', e); - toast.error(t('setting.invalid-json'), { closeButton: true }); - setInstalling(false); - return; - } - let res = await proxyFetchPost('/api/v1/mcp/import/local', data); - if (res.detail) { - toast.error(t('setting.invalid-json'), { closeButton: true }); - setInstalling(false); - return; - } - const mcpServers = data['mcpServers']; - if (mcpServers && typeof mcpServers === 'object') { - for (const [key, value] of Object.entries(mcpServers)) { - await mcpInstall(key, value as Record); - } - } - } - setShowAdd(false); - setLocalJson(`{ - "mcpServers": {} - }`); - setRemoteName(''); - setRemoteUrl(''); - fetchList(); - } finally { - setInstalling(false); - } - }; - - // delete dialog - const handleDelete = async () => { - if (!deleteTarget) return; - setDeleting(true); - try { - checkAgentTool(deleteTarget.mcp_name); - await proxyFetchDelete(`/api/v1/mcp/users/${deleteTarget.id}`); - await mcpRemove(deleteTarget.mcp_key); - if (selected?.type === 'your' && selected.id === deleteTarget.id) { - setSelected(null); - } - setDeleteTarget(null); - fetchList(); - } finally { - setDeleting(false); - } - }; - - return ( -
-
-
- {t('setting.mcps-and-tools')} -
-
- -
-
-
-
- {t('setting.connectors')} -
-
- setSearchQuery(e.target.value)} - placeholder={t('setting.search-mcp')} - /> - -
-
- -
-
-
-
- -
- {isLoadingIntegrations ? ( -
- {[1, 2, 3].map((i) => ( -
- ))} -
- ) : ( - <> - {sortedWebItems.map((wi) => { - if (wi.kind === 'google-search') { - const isActive = - selected?.type === GOOGLE_SEARCH_ID; - return ( - - ); - } - - const { - item, - connected: isConn, - comingSoon: isComingSoon, - } = wi; - const assetUrl = integrationLeadingIconUrl(item.key); - const isActive = - selected?.type === 'web' && - selected.key === item.key; - return ( - - ); - })} - - )} -
-
- -
- -
- {isLoading ? ( -
- {t('setting.loading')} -
- ) : error ? ( -
- {error} -
- ) : filteredItems.length === 0 ? ( -
-

- {items.length === 0 - ? t('setting.no-mcp-servers', { - defaultValue: 'No MCPs', - }) - : t('dashboard.no-results')} -

-
- ) : ( - filteredItems.map((item) => - renderSidebarRow( - `your-${item.id}`, - capitalizeFirstLetter(item.mcp_name || ''), - 'your', - selected?.type === 'your' && selected.id === item.id, - () => setSelected({ type: 'your', id: item.id }), - undefined, - item.status === 1 - ) - ) - )} -
-
-
-
- -
- {isLoadingIntegrations && !selected ? ( -
- {[1, 2, 3, 4].map((i) => ( -
-
-
-
-
-
-
-
- -
- ))} -
- ) : ( - renderConnectionPanel() - )} -
-
-
-
- - - - - setShowAdd(false)} - onInstall={handleInstall} - /> - setDeleteTarget(null)} - onConfirm={handleDelete} - loading={deleting} - /> -
- ); -} diff --git a/src/pages/Connectors/components/AddConnectorDialog.tsx b/src/pages/Connectors/components/AddConnectorDialog.tsx new file mode 100644 index 000000000..95733e0fe --- /dev/null +++ b/src/pages/Connectors/components/AddConnectorDialog.tsx @@ -0,0 +1,1390 @@ +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= + +import { + connectProvider, + createConnectorOAuthAuthorization, + fetchConnectorProvider, + fetchConnectorProviders, + getCachedConnectorProviders, + isConnectedProvider, + providerLabel, + type ConnectorAction, + type ConnectorAuthDefinition, + type ConnectorCredentialField, + type ConnectorProvider, +} from '@/api/connectors'; +import { proxyFetchGet } from '@/api/http'; +import SearchInput from '@/components/Dashboard/SearchInput'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogContentSection, + DialogFooter, + DialogHeader, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Textarea } from '@/components/ui/textarea'; +import type { IntegrationItem } from '@/hooks/useIntegrationManagement'; +import { AnimatePresence, motion } from 'framer-motion'; +import type { TFunction } from 'i18next'; +import { + BadgeCheck, + ChevronDown, + ExternalLink, + KeyRound, + Loader2, + PlugZap, + Plus, + RefreshCw, + Server, + Settings, + ShieldCheck, +} from 'lucide-react'; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, + type UIEvent, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; +import type { ConnectorInstallHint } from './types'; + +export type { ConnectorInstallHint }; + +/** Large enough to fill the dialog viewport and avoid immediate load-more waterfalls. */ +const MARKET_PAGE_SIZE = 60; + +export type AddConnectorTarget = + | { source: 'open'; provider: ConnectorProvider } + | { source: 'builtin'; item: IntegrationItem } + | null; + +interface StoredConfig { + id: number; + config_group?: string; + config_name?: string; + config_value?: string; +} + +interface AddConnectorDialogProps { + open: boolean; + connectorGatewayEnabled: boolean; + localMode: boolean; + builtInItems: IntegrationItem[]; + builtInInstalled: Record; + configs: StoredConfig[]; + initialTarget?: AddConnectorTarget; + onOpenChange: (open: boolean) => void; + onInstalled: (hint: ConnectorInstallHint) => void | Promise; + saveBuiltInValue: ( + provider: string, + key: string, + value: string + ) => Promise; + refreshBuiltIns: () => Promise; +} + +export { isConnectedProvider, providerLabel }; + +export function providerActionCount(provider: ConnectorProvider): number { + return typeof provider.action_count === 'number' + ? provider.action_count + : Array.isArray(provider.actions) + ? provider.actions.length + : 0; +} + +export function actionLabel(action: ConnectorAction, t: TFunction): string { + return action.name || action.id || t('connectors.unnamed-action'); +} + +function authLabel(authType: string, t: TFunction): string { + if (authType === 'api_key') return t('connectors.auth-api-key'); + if (authType === 'custom_credential') return t('connectors.auth-credential'); + if (authType === 'oauth2') return t('connectors.auth-oauth'); + if (authType === 'no_auth') return t('connectors.auth-none'); + return authType; +} + +function authPriority(authType: string): number { + if (authType === 'api_key') return 0; + if (authType === 'custom_credential') return 1; + if (authType === 'no_auth') return 2; + if (authType === 'oauth2') return 3; + return 10; +} + +function authDefinitions( + provider: ConnectorProvider | null +): ConnectorAuthDefinition[] { + if (!provider) return []; + if (provider.auth?.length) { + return [...provider.auth].sort( + (left, right) => authPriority(left.type) - authPriority(right.type) + ); + } + return (provider.authTypes || []).map((type) => ({ type })); +} + +function preferredAuthType(provider: ConnectorProvider | null): string | null { + const definitions = authDefinitions(provider); + if (!definitions.length) return null; + const connectedAuth = provider?.connection?.authType; + if ( + connectedAuth && + definitions.some((auth) => auth.type === connectedAuth) + ) { + return connectedAuth; + } + return definitions[0].type; +} + +function credentialFieldsFor( + auth: ConnectorAuthDefinition | undefined, + t: TFunction +): ConnectorCredentialField[] { + if (!auth) return []; + if (auth.type === 'api_key') { + return [ + { + key: 'apiKey', + label: auth.label || t('connectors.auth-api-key'), + inputType: 'password', + required: true, + secret: true, + placeholder: auth.placeholder, + description: auth.description, + }, + ...(auth.extraFields || []), + ]; + } + if (auth.type === 'custom_credential') return auth.fields || []; + return []; +} + +function isBuiltInConfigured( + item: IntegrationItem, + configs: StoredConfig[] +): boolean { + if (item.key === 'Search') { + const names = new Set( + configs + .filter((config) => String(config.config_value || '').trim()) + .map((config) => config.config_name) + ); + return names.has('GOOGLE_API_KEY') && names.has('SEARCH_ENGINE_ID'); + } + return configs.some( + (config) => + config.config_group?.toLowerCase() === item.key.toLowerCase() && + String(config.config_value || '').trim().length > 0 + ); +} + +export function ProviderIcon({ + provider, + size = 'sm', +}: { + provider: ConnectorProvider | null | undefined; + size?: 'list' | 'sm' | 'detail' | 'lg'; +}) { + const iconUrl = provider?.iconUrl || ''; + const [iconFailed, setIconFailed] = useState(false); + + useEffect(() => setIconFailed(false), [iconUrl]); + + const shellClass = + size === 'list' + ? 'h-5 w-5' + : size === 'detail' + ? 'h-7 w-7 rounded-lg border border-solid border-ds-border-neutral-default-default bg-ds-bg-neutral-subtle-default' + : size === 'lg' + ? 'h-12 w-12 rounded-xl border border-solid border-ds-border-neutral-default-default bg-ds-bg-neutral-subtle-default' + : 'h-10 w-10 rounded-xl border border-solid border-ds-border-neutral-default-default bg-ds-bg-neutral-subtle-default'; + const imageClass = + size === 'list' || size === 'detail' + ? 'h-5 w-5' + : size === 'lg' + ? 'h-7 w-7 rounded-lg' + : 'h-6 w-6 rounded-lg'; + + return ( +
+ {iconUrl && !iconFailed ? ( + setIconFailed(true)} + /> + ) : ( + + )} +
+ ); +} + +function CatalogCardSkeleton() { + return ( +
+ +
+ + +
+ +
+ ); +} + +function CatalogLoadingGrid({ count = 8 }: { count?: number }) { + return ( +
+ {Array.from({ length: count }).map((_, index) => ( + + ))} +
+ ); +} + +function CatalogLoadingBanner({ label }: { label: string }) { + return ( + +
+ + {label} +
+
+ ); +} + +export default function AddConnectorDialog({ + open, + connectorGatewayEnabled, + localMode, + builtInItems, + builtInInstalled, + configs, + initialTarget = null, + onOpenChange, + onInstalled, + saveBuiltInValue, + refreshBuiltIns, +}: AddConnectorDialogProps) { + const { t } = useTranslation(); + const [browseSource, setBrowseSource] = useState<'open' | 'builtin'>( + connectorGatewayEnabled || !localMode ? 'open' : 'builtin' + ); + const [query, setQuery] = useState(''); + const [debouncedQuery, setDebouncedQuery] = useState(''); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [catalog, setCatalog] = useState([]); + const [catalogLoading, setCatalogLoading] = useState(false); + const [catalogRefreshing, setCatalogRefreshing] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); + const [catalogError, setCatalogError] = useState(null); + const [selectedProvider, setSelectedProvider] = + useState(null); + const [selectedBuiltIn, setSelectedBuiltIn] = + useState(null); + const [detailLoading, setDetailLoading] = useState(false); + const [selectedAuthType, setSelectedAuthType] = useState(null); + const [credentialValues, setCredentialValues] = useState< + Record + >({}); + const [builtInValues, setBuiltInValues] = useState>( + {} + ); + const [saving, setSaving] = useState(false); + const [formError, setFormError] = useState(null); + const [authorizationPending, setAuthorizationPending] = useState(false); + const [actionsExpanded, setActionsExpanded] = useState(false); + const [actionsOverflow, setActionsOverflow] = useState(false); + const browseScrollRef = useRef(null); + const loadMoreSentinelRef = useRef(null); + const actionsListRef = useRef(null); + const savedScrollTopRef = useRef(0); + const catalogRequestIdRef = useRef(0); + const oauthPollRef = useRef(null); + const oauthTimeoutRef = useRef(null); + // Poll responses can still be in flight after the interval is cleared, so + // guard against finishing the install more than once. + const oauthFinishedRef = useRef(false); + + const stopOAuthPolling = useCallback(() => { + if (oauthPollRef.current !== null) { + window.clearInterval(oauthPollRef.current); + oauthPollRef.current = null; + } + if (oauthTimeoutRef.current !== null) { + window.clearTimeout(oauthTimeoutRef.current); + oauthTimeoutRef.current = null; + } + }, []); + + useEffect(() => stopOAuthPolling, [stopOAuthPolling]); + + useEffect(() => { + if (!open) return; + // Built-in is only available in local mode. Hosted / non-local always uses + // Connector Gateway providers. + const allowBuiltin = localMode; + const targetSource = + allowBuiltin && initialTarget?.source === 'builtin' + ? 'builtin' + : connectorGatewayEnabled || !localMode + ? 'open' + : 'builtin'; + setBrowseSource(targetSource); + setSelectedProvider( + initialTarget?.source === 'open' ? initialTarget.provider : null + ); + setSelectedBuiltIn( + allowBuiltin && initialTarget?.source === 'builtin' + ? initialTarget.item + : null + ); + setSelectedAuthType( + initialTarget?.source === 'open' + ? preferredAuthType(initialTarget.provider) + : null + ); + setCredentialValues({}); + setFormError(null); + setAuthorizationPending(false); + setActionsExpanded(false); + }, [connectorGatewayEnabled, initialTarget, localMode, open]); + + useEffect(() => { + setActionsExpanded(false); + }, [selectedProvider?.service]); + + useLayoutEffect(() => { + const element = actionsListRef.current; + if (!element || !selectedProvider?.actions?.length) { + setActionsOverflow(false); + return; + } + setActionsOverflow(element.scrollHeight > 200); + }, [selectedProvider?.actions, selectedProvider?.service, detailLoading]); + + useEffect(() => { + const timer = window.setTimeout(() => setDebouncedQuery(query.trim()), 250); + return () => window.clearTimeout(timer); + }, [query]); + + const applyCatalogPage = useCallback( + ( + response: Awaited>, + pageToLoad: number, + append: boolean + ) => { + setCatalog((current) => { + if (!append) return response.providers; + const seen = new Set(current.map((provider) => provider.service)); + return [ + ...current, + ...response.providers.filter( + (provider) => !seen.has(provider.service) + ), + ]; + }); + setHasMore(pageToLoad < response.total_pages); + setPage(pageToLoad); + setCatalogError(null); + }, + [] + ); + + const loadCatalogPage = useCallback( + async ( + pageToLoad: number, + append: boolean, + options: { soft?: boolean; bypassCache?: boolean } = {} + ) => { + if (!open || !connectorGatewayEnabled || browseSource !== 'open') return; + const requestId = ++catalogRequestIdRef.current; + const soft = options.soft === true; + if (append) { + setLoadingMore(true); + } else if (soft) { + setCatalogRefreshing(true); + } else { + setCatalogLoading(true); + setCatalogError(null); + } + try { + const response = await fetchConnectorProviders( + { + page: pageToLoad, + pageSize: MARKET_PAGE_SIZE, + query: debouncedQuery, + }, + { bypassCache: options.bypassCache === true || soft } + ); + if (requestId !== catalogRequestIdRef.current) return; + applyCatalogPage(response, pageToLoad, append); + } catch (error: any) { + if (requestId !== catalogRequestIdRef.current) return; + if (!append && !soft) { + setCatalog([]); + setHasMore(false); + setCatalogError( + error?.message || t('connectors.load-gateway-failed') + ); + } + } finally { + if (requestId === catalogRequestIdRef.current) { + setCatalogLoading(false); + setCatalogRefreshing(false); + setLoadingMore(false); + } + } + }, + [ + applyCatalogPage, + browseSource, + connectorGatewayEnabled, + debouncedQuery, + open, + t, + ] + ); + + const loadNextCatalogPage = useCallback(() => { + if ( + browseSource !== 'open' || + selectedProvider || + selectedBuiltIn || + !hasMore || + catalogLoading || + loadingMore || + catalogError || + catalog.length === 0 + ) { + return; + } + void loadCatalogPage(page + 1, true); + }, [ + browseSource, + catalog.length, + catalogError, + catalogLoading, + hasMore, + loadCatalogPage, + loadingMore, + page, + selectedBuiltIn, + selectedProvider, + ]); + + const handleBrowseScroll = useCallback( + (event: UIEvent) => { + if (browseSource !== 'open') return; + const element = event.currentTarget; + const distanceToBottom = + element.scrollHeight - element.scrollTop - element.clientHeight; + if (distanceToBottom <= 280) { + loadNextCatalogPage(); + } + }, + [browseSource, loadNextCatalogPage] + ); + + // Hydrate from cache before paint to avoid empty-state / skeleton flash on open. + useLayoutEffect(() => { + if (!open || !connectorGatewayEnabled || browseSource !== 'open') return; + const cached = getCachedConnectorProviders({ + page: 1, + pageSize: MARKET_PAGE_SIZE, + query: debouncedQuery, + }); + if (cached) { + applyCatalogPage(cached, 1, false); + setCatalogLoading(false); + return; + } + setCatalog([]); + setHasMore(true); + setPage(1); + setCatalogError(null); + setCatalogLoading(true); + }, [ + applyCatalogPage, + browseSource, + connectorGatewayEnabled, + debouncedQuery, + open, + ]); + + useEffect(() => { + if (!open || !connectorGatewayEnabled || browseSource !== 'open') return; + const cached = getCachedConnectorProviders({ + page: 1, + pageSize: MARKET_PAGE_SIZE, + query: debouncedQuery, + }); + if (cached) { + void loadCatalogPage(1, false, { soft: true }); + return; + } + void loadCatalogPage(1, false); + }, [ + browseSource, + connectorGatewayEnabled, + debouncedQuery, + loadCatalogPage, + open, + ]); + + useEffect(() => { + if (browseSource !== 'open') return; + const root = browseScrollRef.current; + const sentinel = loadMoreSentinelRef.current; + if (!root || !sentinel) return; + + const observer = new IntersectionObserver( + (entries) => { + if (!entries[0]?.isIntersecting) return; + loadNextCatalogPage(); + }, + { root, rootMargin: '160px' } + ); + observer.observe(sentinel); + return () => observer.disconnect(); + }, [browseSource, loadNextCatalogPage]); + + const selectedProviderService = selectedProvider?.service; + + useEffect(() => { + if (!selectedProviderService) return; + let cancelled = false; + setDetailLoading(true); + void fetchConnectorProvider(selectedProviderService) + .then((response) => { + if (cancelled) return; + setSelectedProvider(response.provider); + setSelectedAuthType(preferredAuthType(response.provider)); + }) + .catch((error: any) => { + if (!cancelled) { + setFormError(error?.message || t('connectors.detail-load-failed')); + } + }) + .finally(() => { + if (!cancelled) setDetailLoading(false); + }); + return () => { + cancelled = true; + }; + }, [selectedProviderService, t]); + + useEffect(() => { + if (!selectedBuiltIn) { + setBuiltInValues({}); + return; + } + const next: Record = {}; + selectedBuiltIn.env_vars.forEach((key) => { + const match = configs.find((config) => config.config_name === key); + next[key] = String(match?.config_value || ''); + }); + setBuiltInValues(next); + }, [configs, selectedBuiltIn]); + + const selectedAuth = useMemo( + () => + authDefinitions(selectedProvider).find( + (auth) => auth.type === selectedAuthType + ), + [selectedAuthType, selectedProvider] + ); + const credentialFields = useMemo( + () => credentialFieldsFor(selectedAuth, t), + [selectedAuth, t] + ); + const canInstallProvider = + Boolean(selectedProvider && selectedAuth) && + (selectedAuth?.type === 'oauth2' || + selectedAuth?.type === 'no_auth' || + credentialFields.every( + (field) => !field.required || credentialValues[field.key]?.trim() + )); + + const filteredBuiltIns = useMemo(() => { + const normalized = debouncedQuery.toLowerCase(); + if (!normalized) return builtInItems; + return builtInItems.filter((item) => { + const description = + typeof item.desc === 'string' ? item.desc.toLowerCase() : ''; + return ( + item.name.toLowerCase().includes(normalized) || + item.key.toLowerCase().includes(normalized) || + description.includes(normalized) + ); + }); + }, [builtInItems, debouncedQuery]); + + const closeDialog = useCallback(() => { + stopOAuthPolling(); + onOpenChange(false); + }, [onOpenChange, stopOAuthPolling]); + + const finishInstall = useCallback( + async (hint: ConnectorInstallHint) => { + stopOAuthPolling(); + await onInstalled(hint); + closeDialog(); + }, + [closeDialog, onInstalled, stopOAuthPolling] + ); + + const startOAuthPolling = useCallback( + (service: string) => { + stopOAuthPolling(); + oauthFinishedRef.current = false; + setAuthorizationPending(true); + oauthPollRef.current = window.setInterval(() => { + void fetchConnectorProvider(service) + .then((response) => { + if (oauthFinishedRef.current) return; + setSelectedProvider(response.provider); + if (isConnectedProvider(response.provider)) { + oauthFinishedRef.current = true; + toast.success( + t('connectors.installed-toast', { + name: providerLabel(response.provider), + }) + ); + void finishInstall({ source: 'open', key: service }); + } + }) + .catch(() => undefined); + }, 1500); + oauthTimeoutRef.current = window.setTimeout( + () => { + stopOAuthPolling(); + setAuthorizationPending(false); + setFormError(t('connectors.authorization-pending')); + }, + 5 * 60 * 1000 + ); + }, + [finishInstall, stopOAuthPolling, t] + ); + + const installOpenProvider = useCallback(async () => { + if (!selectedProvider || !selectedAuth) return; + setSaving(true); + setFormError(null); + try { + if (selectedAuth.type === 'oauth2') { + const authorization = await createConnectorOAuthAuthorization( + selectedProvider.service, + selectedProvider.connection?.connectionName + ); + if (!authorization.authorizationUrl) { + throw new Error(t('connectors.no-authorization-url')); + } + window.open( + authorization.authorizationUrl, + 'eigent_connector_oauth', + 'popup=yes,width=720,height=760,menubar=no,toolbar=no,location=yes,status=no' + ); + startOAuthPolling(selectedProvider.service); + toast.success(t('connectors.authorization-started')); + return; + } + + await connectProvider(selectedProvider.service, { + auth_type: selectedAuth.type, + values: + selectedAuth.type === 'no_auth' + ? {} + : credentialFields.reduce>((acc, field) => { + acc[field.key] = credentialValues[field.key] || ''; + return acc; + }, {}), + }); + toast.success( + t('connectors.installed-toast', { + name: providerLabel(selectedProvider), + }) + ); + await finishInstall({ + source: 'open', + key: selectedProvider.service, + }); + } catch (error: any) { + setFormError(error?.message || t('connectors.install-failed')); + } finally { + setSaving(false); + } + }, [ + credentialFields, + credentialValues, + finishInstall, + selectedAuth, + selectedProvider, + startOAuthPolling, + t, + ]); + + const verifyBuiltInAuthorization = useCallback(async () => { + if (!selectedBuiltIn) return; + setSaving(true); + setFormError(null); + try { + const response = await proxyFetchGet('/api/v1/configs'); + const nextConfigs = Array.isArray(response) ? response : []; + await refreshBuiltIns(); + if (!isBuiltInConfigured(selectedBuiltIn, nextConfigs)) { + throw new Error(t('connectors.authorization-incomplete')); + } + await finishInstall({ source: 'builtin', key: selectedBuiltIn.key }); + } catch (error: any) { + setFormError(error?.message || t('connectors.refresh-status-failed')); + } finally { + setSaving(false); + } + }, [finishInstall, refreshBuiltIns, selectedBuiltIn, t]); + + const installBuiltIn = useCallback(async () => { + if (!selectedBuiltIn) return; + setSaving(true); + setFormError(null); + try { + if (selectedBuiltIn.env_vars.length > 0) { + for (const key of selectedBuiltIn.env_vars) { + const value = builtInValues[key]?.trim(); + if (!value) { + throw new Error(t('connectors.field-required', { field: key })); + } + await saveBuiltInValue(selectedBuiltIn.key, key, value); + } + } + + if ( + selectedBuiltIn.key === 'Google Calendar' || + selectedBuiltIn.env_vars.length === 0 + ) { + await selectedBuiltIn.onInstall(); + } + + await refreshBuiltIns(); + if ( + selectedBuiltIn.key === 'Google Calendar' || + (selectedBuiltIn.env_vars.length === 0 && + selectedBuiltIn.key !== 'Notion') + ) { + setAuthorizationPending(true); + toast.success(t('connectors.authorization-started')); + return; + } + + toast.success( + t('connectors.installed-toast', { name: selectedBuiltIn.name }) + ); + await finishInstall({ source: 'builtin', key: selectedBuiltIn.key }); + } catch (error: any) { + setFormError(error?.message || t('connectors.install-failed')); + } finally { + setSaving(false); + } + }, [ + builtInValues, + finishInstall, + refreshBuiltIns, + saveBuiltInValue, + selectedBuiltIn, + t, + ]); + + const openProvider = (provider: ConnectorProvider) => { + savedScrollTopRef.current = browseScrollRef.current?.scrollTop || 0; + setSelectedProvider(provider); + setSelectedAuthType(preferredAuthType(provider)); + setCredentialValues({}); + setFormError(null); + }; + + const goBackToBrowse = () => { + stopOAuthPolling(); + setSelectedProvider(null); + setSelectedBuiltIn(null); + setSelectedAuthType(null); + setCredentialValues({}); + setBuiltInValues({}); + setFormError(null); + setAuthorizationPending(false); + window.setTimeout(() => { + if (browseScrollRef.current) { + browseScrollRef.current.scrollTop = savedScrollTopRef.current; + } + }, 0); + }; + + const showingDetail = Boolean(selectedProvider || selectedBuiltIn); + + return ( + { + if (!next) closeDialog(); + }} + > + + + + {!showingDetail ? ( + +
+ setQuery(event.target.value)} + placeholder={t('connectors.search-connectors')} + /> +
+ + {localMode ? ( +
+ {connectorGatewayEnabled ? ( + + ) : null} + +
+ ) : null} + +
+ {browseSource === 'open' ? ( + !connectorGatewayEnabled ? ( +
+ + {t('connectors.gateway-unavailable')} + + + {t('connectors.gateway-unavailable-desc')} + +
+ ) : catalogError && catalog.length === 0 && !catalogLoading ? ( +
+ + {catalogError} + + +
+ ) : !catalogLoading && + !catalogRefreshing && + catalog.length === 0 ? ( +
+ {t('connectors.no-gateway-found')} +
+ ) : ( +
+ + {catalogLoading || catalogRefreshing ? ( + + ) : null} + + {catalogLoading && catalog.length === 0 ? ( + + ) : ( + <> +
+ {catalog.map((provider) => { + const installed = isConnectedProvider(provider); + return ( + + ); + })} +
+ {hasMore ? ( +
+ {loadingMore ? ( + <> +
+ + {t('connectors.loading-more')} +
+ + + ) : ( +
+ )} +
+ ) : null} + + )} +
+ ) + ) : filteredBuiltIns.length === 0 ? ( +
+ {t('connectors.no-built-in-found')} +
+ ) : ( +
+ {filteredBuiltIns.map((item) => { + const installed = Boolean(builtInInstalled[item.key]); + return ( + + ); + })} +
+ )} +
+ + ) : selectedProvider ? ( + <> + + {detailLoading ? ( +
+
+
+
+ ) : ( +
+
+ +
+ + {providerLabel(selectedProvider)} + + + {t('connectors.supported-actions-count', { + num: providerActionCount(selectedProvider), + })} + +
+ {selectedProvider.homepageUrl ? ( + + {t('connectors.provider-website')} + + + ) : null} +
+ + {selectedProvider.actions?.length ? ( +
+
+
+ {selectedProvider.actions.map((action, index) => ( + + {actionLabel(action, t)} + + ))} +
+ {!actionsExpanded && actionsOverflow ? ( +
+ ) : null} +
+ {actionsOverflow ? ( + + ) : null} +
+ ) : null} + + {authDefinitions(selectedProvider).length > 1 ? ( +
+
+ {t('connectors.authentication')} +
+ { + setSelectedAuthType(value); + setCredentialValues({}); + setFormError(null); + }} + > + + {authDefinitions(selectedProvider).map((auth) => ( + + {authLabel(auth.type, t)} + + ))} + + +
+ ) : null} + + {selectedAuth?.type === 'oauth2' ? ( +
+
+ {t('connectors.oauth-title')} +
+ {t('connectors.oauth-desc')} + {selectedAuth.scopes?.length ? ( + + {t('connectors.oauth-scopes', { + scopes: selectedAuth.scopes.join(', '), + })} + + ) : null} +
+ ) : selectedAuth?.type === 'no_auth' ? ( +
+ + {t('connectors.no-auth-desc')} +
+ ) : credentialFields.length ? ( +
+ {credentialFields.map((field) => + field.inputType === 'textarea' || + field.inputType === 'json' ? ( +