From 704d06fee5ffa6679cad73315e8f4063a2d33921 Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Wed, 15 Jul 2026 14:06:14 +0800 Subject: [PATCH 1/5] feat(mcp): streamline market interactions --- src/main/presenter/mcpPresenter/index.ts | 14 ++ src/main/routes/index.ts | 9 + src/renderer/api/McpClient.ts | 10 ++ .../settings/components/McpBuiltinMarket.vue | 155 +++++++++--------- src/shared/contracts/routes.ts | 2 + src/shared/contracts/routes/mcp.routes.ts | 11 ++ .../types/presenters/core.presenter.d.ts | 1 + 7 files changed, 123 insertions(+), 79 deletions(-) diff --git a/src/main/presenter/mcpPresenter/index.ts b/src/main/presenter/mcpPresenter/index.ts index 5dd1b9e4f3..ab5b10f203 100644 --- a/src/main/presenter/mcpPresenter/index.ts +++ b/src/main/presenter/mcpPresenter/index.ts @@ -393,6 +393,20 @@ export class McpPresenter implements IMCPPresenter { return false } + async listInstalledServerIds(source: string, sourceIds: string[]): Promise { + const requestedIds = new Set(sourceIds) + if (requestedIds.size === 0) return [] + + const installedIds = new Set() + const servers = await this.configPresenter.getMcpServers() + for (const config of Object.values(servers)) { + if (config.source === source && config.sourceId && requestedIds.has(config.sourceId)) { + installedIds.add(config.sourceId) + } + } + return [...installedIds] + } + async updateMcpRouterServersAuth(apiKey: string): Promise { const servers = await this.configPresenter.getMcpServers() const updates: Array<{ name: string; config: Partial }> = [] diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts index 5bc832b6d4..449f6a66f2 100644 --- a/src/main/routes/index.ts +++ b/src/main/routes/index.ts @@ -167,6 +167,7 @@ import { mcpRouterGetApiKeyRoute, mcpRouterInstallServerRoute, mcpRouterIsServerInstalledRoute, + mcpRouterListInstalledServerIdsRoute, mcpRouterListServersRoute, mcpRouterSetApiKeyRoute, mcpRouterUpdateServersAuthRoute, @@ -3921,6 +3922,14 @@ export async function dispatchDeepchatRoute( }) } + case mcpRouterListInstalledServerIdsRoute.name: { + const input = mcpRouterListInstalledServerIdsRoute.input.parse(rawInput) + return mcpRouterListInstalledServerIdsRoute.output.parse({ + installedSourceIds: + (await runtime.mcpPresenter.listInstalledServerIds?.(input.source, input.sourceIds)) ?? [] + }) + } + case mcpRouterUpdateServersAuthRoute.name: { const input = mcpRouterUpdateServersAuthRoute.input.parse(rawInput) await runtime.mcpPresenter.updateMcpRouterServersAuth?.(input.apiKey) diff --git a/src/renderer/api/McpClient.ts b/src/renderer/api/McpClient.ts index b7ff08218f..fb04ddb0b4 100644 --- a/src/renderer/api/McpClient.ts +++ b/src/renderer/api/McpClient.ts @@ -33,6 +33,7 @@ import { mcpRouterGetApiKeyRoute, mcpRouterInstallServerRoute, mcpRouterIsServerInstalledRoute, + mcpRouterListInstalledServerIdsRoute, mcpRouterListServersRoute, mcpRouterSetApiKeyRoute, mcpRouterUpdateServersAuthRoute, @@ -217,6 +218,14 @@ export function createMcpClient(bridge: DeepchatBridge = getDeepchatBridge()) { return result.installed } + async function listInstalledServerIds(source: string, sourceIds: string[]) { + const result = await bridge.invoke(mcpRouterListInstalledServerIdsRoute.name, { + source, + sourceIds + }) + return result.installedSourceIds + } + async function updateMcpRouterServersAuth(apiKey: string) { await bridge.invoke(mcpRouterUpdateServersAuthRoute.name, { apiKey }) } @@ -317,6 +326,7 @@ export function createMcpClient(bridge: DeepchatBridge = getDeepchatBridge()) { getMcpRouterApiKey, setMcpRouterApiKey, isServerInstalled, + listInstalledServerIds, updateMcpRouterServersAuth, onServerStarted, onServerStopped, diff --git a/src/renderer/settings/components/McpBuiltinMarket.vue b/src/renderer/settings/components/McpBuiltinMarket.vue index aff8148a8b..87be12c972 100644 --- a/src/renderer/settings/components/McpBuiltinMarket.vue +++ b/src/renderer/settings/components/McpBuiltinMarket.vue @@ -31,7 +31,14 @@ :placeholder="t('mcp.market.apiKeyPlaceholder')" class="w-64" /> - + @@ -81,7 +88,9 @@ @@ -108,17 +128,25 @@ {{ t('common.loading') }} -
- {{ t('mcp.market.pullDownToLoad') }} +
+ {{ t('common.error.operationFailed') }} +
{{ t('mcp.market.noMore') }}
{{ t('mcp.market.empty') }} @@ -174,11 +202,12 @@ const limit = ref(20) const loading = ref(false) const hasMore = ref(true) const scrollContainer = ref(null) -const showPullToLoad = ref(false) -const canPullMore = ref(false) const installedServers = ref>(new Set()) +const installingServerKeys = ref>(new Set()) +const loadError = ref(null) const apiKeyInput = ref('') +const savingApiKey = ref(false) const loadApiKey = async () => { try { @@ -188,6 +217,8 @@ const loadApiKey = async () => { } const saveApiKey = async () => { + if (savingApiKey.value) return + savingApiKey.value = true try { const newKey = apiKeyInput.value.trim() await mcpClient.setMcpRouterApiKey(newKey) @@ -204,6 +235,8 @@ const saveApiKey = async () => { description: String(e), variant: 'destructive' }) + } finally { + savingApiKey.value = false } } @@ -211,56 +244,41 @@ const openHowToGetKey = () => { window.open('https://mcprouter.co/settings/keys', '_blank') } -const checkInstalledServers = async () => { - const installed = new Set() - for (const item of items.value) { - try { - // 使用 server_key 作为 sourceId 检查安装状态,因为这是我们在安装时保存的标识符 - const isInstalled = await mcpClient.isServerInstalled('mcprouter', item.server_key) - if (isInstalled) { - installed.add(item.server_key) - } - } catch (e) { - console.error('Failed to check installation status:', e) - } +const mergeInstalledServers = async (marketItems: MarketItem[]) => { + const sourceIds = [...new Set(marketItems.map((item) => item.server_key))] + if (sourceIds.length === 0) return + + try { + const installedIds = await mcpClient.listInstalledServerIds('mcprouter', sourceIds) + installedServers.value = new Set([...installedServers.value, ...installedIds]) + } catch (error) { + console.error('Failed to check MCP Router installation status:', error) } - installedServers.value = installed } -const fetchPage = async (forcePull = false) => { - if (loading.value || (!hasMore.value && !forcePull)) return +const fetchPage = async () => { + if (loading.value || !hasMore.value) return loading.value = true - showPullToLoad.value = false + loadError.value = null try { const data = await mcpClient.listMcpRouterServers(page.value, limit.value) const list = data?.servers || [] if (list.length === 0) { hasMore.value = false - canPullMore.value = false return } + await mergeInstalledServers(list) items.value.push(...list) page.value += 1 - - // 检查安装状态 - await checkInstalledServers() - - // 如果是强制拉取且成功获取到数据,重新启用拉取功能 - if (forcePull) { - hasMore.value = true - canPullMore.value = true - } + hasMore.value = list.length >= limit.value } catch (e) { + loadError.value = e toast({ title: t('settings.provider.operationFailed'), description: String(e), variant: 'destructive' }) - // 错误时重置状态 - if (forcePull) { - canPullMore.value = false - } } finally { loading.value = false } @@ -277,30 +295,18 @@ const onScroll = () => { // 正常滚动加载 if (hasMore.value && nearBottom) { - fetchPage() - return - } - - // 检测过度滚动(内容不足一屏或已滚动到底部且没有更多内容) - if (!hasMore.value) { - const atBottom = scrollTop + clientHeight >= scrollHeight - 50 - const overScroll = scrollTop + clientHeight > scrollHeight - const contentTooShort = scrollHeight <= clientHeight - - // 启用强制拉取模式 - if ((atBottom || overScroll || contentTooShort) && !canPullMore.value) { - canPullMore.value = true - showPullToLoad.value = true - } - - // 检测强制拉取触发条件 - if (canPullMore.value && (overScroll || (contentTooShort && scrollTop > 0))) { - fetchPage(true) - } + void fetchPage() } } const install = async (item: MarketItem) => { + if ( + installedServers.value.has(item.server_key) || + installingServerKeys.value.has(item.server_key) + ) { + return + } + try { if (!apiKeyInput.value.trim()) { toast({ @@ -310,35 +316,26 @@ const install = async (item: MarketItem) => { }) return } + installingServerKeys.value = new Set([...installingServerKeys.value, item.server_key]) await mcpClient.setMcpRouterApiKey(apiKeyInput.value.trim()) const ok = await mcpClient.installMcpRouterServer(item.server_key) if (ok) { toast({ title: t('mcp.market.installSuccess') }) - // 更新安装状态 - 使用 server_key 作为标识符 - installedServers.value.add(item.server_key) + installedServers.value = new Set([...installedServers.value, item.server_key]) } else { toast({ title: t('mcp.market.installFailed'), variant: 'destructive' }) } } catch (e) { toast({ title: t('mcp.market.installFailed'), description: String(e), variant: 'destructive' }) + } finally { + const nextInstalling = new Set(installingServerKeys.value) + nextInstalling.delete(item.server_key) + installingServerKeys.value = nextInstalling } } onMounted(async () => { - await loadApiKey() - await fetchPage() - - // 初始加载后检查是否需要启用强制拉取模式 - setTimeout(() => { - const el = scrollContainer.value - if (el && !hasMore.value) { - const contentTooShort = el.scrollHeight <= el.clientHeight - if (contentTooShort && items.value.length > 0) { - canPullMore.value = true - showPullToLoad.value = true - } - } - }, 100) + await Promise.all([loadApiKey(), fetchPage()]) }) diff --git a/src/shared/contracts/routes.ts b/src/shared/contracts/routes.ts index 2bdcd429af..7472ac1ec7 100644 --- a/src/shared/contracts/routes.ts +++ b/src/shared/contracts/routes.ts @@ -189,6 +189,7 @@ import { mcpRouterGetApiKeyRoute, mcpRouterInstallServerRoute, mcpRouterIsServerInstalledRoute, + mcpRouterListInstalledServerIdsRoute, mcpRouterListServersRoute, mcpRouterSetApiKeyRoute, mcpRouterUpdateServersAuthRoute, @@ -981,6 +982,7 @@ const DEEPCHAT_ROUTE_CATALOG_PART_5 = { [mcpRouterGetApiKeyRoute.name]: mcpRouterGetApiKeyRoute, [mcpRouterSetApiKeyRoute.name]: mcpRouterSetApiKeyRoute, [mcpRouterIsServerInstalledRoute.name]: mcpRouterIsServerInstalledRoute, + [mcpRouterListInstalledServerIdsRoute.name]: mcpRouterListInstalledServerIdsRoute, [mcpRouterUpdateServersAuthRoute.name]: mcpRouterUpdateServersAuthRoute, [syncGetBackupStatusRoute.name]: syncGetBackupStatusRoute, [syncListBackupsRoute.name]: syncListBackupsRoute, diff --git a/src/shared/contracts/routes/mcp.routes.ts b/src/shared/contracts/routes/mcp.routes.ts index e3739fd9a9..9da8152d35 100644 --- a/src/shared/contracts/routes/mcp.routes.ts +++ b/src/shared/contracts/routes/mcp.routes.ts @@ -372,6 +372,17 @@ export const mcpRouterIsServerInstalledRoute = defineRouteContract({ }) }) +export const mcpRouterListInstalledServerIdsRoute = defineRouteContract({ + name: 'mcp.router.listInstalledServerIds', + input: z.object({ + source: z.string().min(1), + sourceIds: z.array(z.string().min(1)).max(100) + }), + output: z.object({ + installedSourceIds: z.array(z.string()) + }) +}) + export const mcpRouterUpdateServersAuthRoute = defineRouteContract({ name: 'mcp.router.updateServersAuth', input: z.object({ diff --git a/src/shared/types/presenters/core.presenter.d.ts b/src/shared/types/presenters/core.presenter.d.ts index 2a61292e15..3d31a7cde7 100644 --- a/src/shared/types/presenters/core.presenter.d.ts +++ b/src/shared/types/presenters/core.presenter.d.ts @@ -1874,6 +1874,7 @@ export interface IMCPPresenter { getMcpRouterApiKey?(): Promise setMcpRouterApiKey?(key: string): Promise isServerInstalled?(source: string, sourceId: string): Promise + listInstalledServerIds?(source: string, sourceIds: string[]): Promise updateMcpRouterServersAuth?(apiKey: string): Promise } From 831811b5400b513977a347911139ecd6945c4a3e Mon Sep 17 00:00:00 2001 From: zhangmo8 Date: Wed, 15 Jul 2026 14:09:12 +0800 Subject: [PATCH 2/5] feat(ui): smooth panel and message actions --- src/renderer/settings/App.vue | 51 ++- src/renderer/settings/main.ts | 24 +- .../settings/settingsRouteComponents.ts | 27 ++ .../components/message/MessageBlockError.vue | 38 ++- .../message/MessageBlockToolCall.vue | 309 ++++++++++-------- .../src/components/message/MessageToolbar.vue | 49 ++- .../components/sidepanel/ChatSidePanel.vue | 11 - .../components/sidepanel/WorkspacePanel.vue | 19 -- src/renderer/src/views/ChatTabView.vue | 4 +- 9 files changed, 324 insertions(+), 208 deletions(-) create mode 100644 src/renderer/settings/settingsRouteComponents.ts diff --git a/src/renderer/settings/App.vue b/src/renderer/settings/App.vue index 3b4240201b..7fd8054a0a 100644 --- a/src/renderer/settings/App.vue +++ b/src/renderer/settings/App.vue @@ -7,7 +7,7 @@
@@ -15,6 +15,8 @@
@@ -116,6 +128,7 @@ import { } from '@shared/settingsNavigation' import type { SettingsNavigationPayload } from '@shared/settingsNavigation' import { useStartupWorkloadStore } from '@/stores/startupWorkloadStore' +import { preloadSettingsRoute } from './settingsRouteComponents' const DATABASE_REPAIR_SECTION = 'database-repair' const SETTINGS_SECTION_EVENT = 'deepchat:settings-section' @@ -173,6 +186,7 @@ const pendingProviderImportToken = computed(() => providerDeeplinkImportStore.pr const isProcessingProviderPreview = ref(false) const startupTimeOrigin = typeof performance !== 'undefined' ? performance.now() : Date.now() const hasLoggedFirstRouteResolved = ref(false) +const pendingRouteName = ref(null) const logSettingsStartup = (phase: string) => { const now = typeof performance !== 'undefined' ? performance.now() : Date.now() @@ -517,8 +531,33 @@ watch( { immediate: true } ) -const handleClick = (path: string) => { - router.push(path) +type SettingsNavigationItem = { + name: string + path: string +} + +const handleClick = async (setting: SettingsNavigationItem) => { + if (pendingRouteName.value || route.path === setting.path) return + + pendingRouteName.value = setting.name + try { + await router.push(setting.path) + } catch (error) { + console.error(`[Settings] Failed to navigate to ${setting.name}:`, error) + } finally { + if (pendingRouteName.value === setting.name) { + pendingRouteName.value = null + } + } +} + +const prefetchSetting = (routeName: string) => { + const preload = preloadSettingsRoute(routeName) + if (preload) { + void preload.catch((error) => { + console.debug(`[Settings] Failed to prefetch ${routeName}:`, error) + }) + } } const SETTINGS_TAB_TEST_IDS: Record = { diff --git a/src/renderer/settings/main.ts b/src/renderer/settings/main.ts index 791280d67b..65d37d29ac 100644 --- a/src/renderer/settings/main.ts +++ b/src/renderer/settings/main.ts @@ -10,34 +10,12 @@ import locales, { pluralRules } from '@/i18n' import { getSettingsRouteItems } from '@shared/settingsNavigation' import { preloadIcons } from '../src/lib/iconLoader' import { getRuntimeArch, getRuntimePlatform } from '@api/runtime' +import { settingsRouteComponents } from './settingsRouteComponents' const runtimePlatform = getRuntimePlatform() const runtimeArch = getRuntimeArch() const settingsRouteItems = getSettingsRouteItems(runtimePlatform, runtimeArch) -const settingsRouteComponents = { - 'settings-overview': () => import('./components/SettingsOverview.vue'), - 'settings-common': () => import('./components/CommonSettings.vue'), - 'settings-display': () => import('./components/DisplaySettings.vue'), - 'settings-environments': () => import('./components/EnvironmentsSettings.vue'), - 'settings-provider': () => import('./components/ModelProviderSettings.vue'), - 'settings-dashboard': () => import('./components/SettingsOverview.vue'), - 'settings-mcp': () => import('./components/McpSettings.vue'), - 'settings-deepchat-agents': () => import('./components/DeepChatAgentsSettings.vue'), - 'settings-acp': () => import('./components/AcpSettings.vue'), - 'settings-remote': () => import('./components/RemoteSettings.vue'), - 'settings-notifications-hooks': () => import('./components/NotificationsHooksSettings.vue'), - 'settings-scheduled-tasks': () => import('./components/CronJobsSettings.vue'), - 'settings-plugins': () => import('./components/PluginsSettings.vue'), - 'settings-skills': () => import('./components/skills/SkillsSettings.vue'), - 'settings-prompt': () => import('./components/PromptSetting.vue'), - 'settings-memory': () => import('./components/MemorySettings.vue'), - 'settings-knowledge-base': () => import('./components/KnowledgeBaseSettings.vue'), - 'settings-database': () => import('./components/DataSettings.vue'), - 'settings-shortcut': () => import('./components/ShortcutSettings.vue'), - 'settings-about': () => import('./components/AboutUsSettings.vue') -} as const - // Create i18n instance const i18n = createI18n({ locale: 'zh-CN', diff --git a/src/renderer/settings/settingsRouteComponents.ts b/src/renderer/settings/settingsRouteComponents.ts new file mode 100644 index 0000000000..01951357c9 --- /dev/null +++ b/src/renderer/settings/settingsRouteComponents.ts @@ -0,0 +1,27 @@ +export const settingsRouteComponents = { + 'settings-overview': () => import('./components/SettingsOverview.vue'), + 'settings-common': () => import('./components/CommonSettings.vue'), + 'settings-display': () => import('./components/DisplaySettings.vue'), + 'settings-environments': () => import('./components/EnvironmentsSettings.vue'), + 'settings-provider': () => import('./components/ModelProviderSettings.vue'), + 'settings-dashboard': () => import('./components/SettingsOverview.vue'), + 'settings-mcp': () => import('./components/McpSettings.vue'), + 'settings-deepchat-agents': () => import('./components/DeepChatAgentsSettings.vue'), + 'settings-acp': () => import('./components/AcpSettings.vue'), + 'settings-remote': () => import('./components/RemoteSettings.vue'), + 'settings-notifications-hooks': () => import('./components/NotificationsHooksSettings.vue'), + 'settings-scheduled-tasks': () => import('./components/CronJobsSettings.vue'), + 'settings-plugins': () => import('./components/PluginsSettings.vue'), + 'settings-skills': () => import('./components/skills/SkillsSettings.vue'), + 'settings-prompt': () => import('./components/PromptSetting.vue'), + 'settings-memory': () => import('./components/MemorySettings.vue'), + 'settings-knowledge-base': () => import('./components/KnowledgeBaseSettings.vue'), + 'settings-database': () => import('./components/DataSettings.vue'), + 'settings-shortcut': () => import('./components/ShortcutSettings.vue'), + 'settings-about': () => import('./components/AboutUsSettings.vue') +} as const + +export function preloadSettingsRoute(routeName: string): Promise | null { + const loader = settingsRouteComponents[routeName as keyof typeof settingsRouteComponents] + return loader?.() ?? null +} diff --git a/src/renderer/src/components/message/MessageBlockError.vue b/src/renderer/src/components/message/MessageBlockError.vue index 4480a02b06..6131759034 100644 --- a/src/renderer/src/components/message/MessageBlockError.vue +++ b/src/renderer/src/components/message/MessageBlockError.vue @@ -7,22 +7,35 @@ {{ t(block.content || '') }}
-
- {{ t('common.error.requestFailed') - }} -
+ aria-hidden="true" + /> +
- {{ t(block.content || '') }} +
+
+ {{ t(block.content || '') }} +
+
{{ t('common.error.causeOfError') }} {{ t(errorExplanation) }} @@ -33,7 +46,7 @@ diff --git a/src/renderer/src/components/sidepanel/ChatSidePanel.vue b/src/renderer/src/components/sidepanel/ChatSidePanel.vue index 1888e89c4f..20d5335c7b 100644 --- a/src/renderer/src/components/sidepanel/ChatSidePanel.vue +++ b/src/renderer/src/components/sidepanel/ChatSidePanel.vue @@ -325,9 +325,6 @@ onBeforeUnmount(() => { diff --git a/src/renderer/src/views/ChatTabView.vue b/src/renderer/src/views/ChatTabView.vue index 1f415c801a..ace6dc4c06 100644 --- a/src/renderer/src/views/ChatTabView.vue +++ b/src/renderer/src/views/ChatTabView.vue @@ -1,8 +1,6 @@