From a2a756e724d5dadaaab4d2876917bcfc62ce0a95 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Mon, 11 Aug 2025 18:52:57 +0800 Subject: [PATCH 1/7] wip: add modelscope provider --- src/main/presenter/configPresenter/index.ts | 28 +++ .../configPresenter/mcpConfHelper.ts | 169 +++++++++++++ .../presenter/configPresenter/providers.ts | 15 ++ .../presenter/llmProviderPresenter/index.ts | 39 +++ .../providers/modelscopeProvider.ts | 218 ++++++++++++++++ .../src/components/icons/ModelIcon.vue | 2 + .../settings/ModelProviderSettingsDetail.vue | 7 + .../components/settings/ModelScopeMcpSync.vue | 237 ++++++++++++++++++ src/renderer/src/i18n/en-US/settings.json | 26 ++ src/renderer/src/i18n/zh-CN/settings.json | 26 ++ 10 files changed, 767 insertions(+) create mode 100644 src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts create mode 100644 src/renderer/src/components/settings/ModelScopeMcpSync.vue diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index 6dada2d34..4d248ce53 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -1166,6 +1166,34 @@ export class ConfigPresenter implements IConfigPresenter { newConfigs ) } + + // 批量导入MCP服务器 + async batchImportMcpServers( + servers: Array<{ + name: string + description: string + package: string + version?: string + type?: any + args?: string[] + env?: Record + enabled?: boolean + source?: string + [key: string]: unknown + }>, + options: { + skipExisting?: boolean + enableByDefault?: boolean + overwriteExisting?: boolean + } = {} + ): Promise<{ imported: number; skipped: number; errors: string[] }> { + return this.mcpConfHelper.batchImportMcpServers(servers, options) + } + + // 根据包名查找服务器 + async findMcpServerByPackage(packageName: string): Promise { + return this.mcpConfHelper.findServerByPackage(packageName) + } } export { defaultShortcutKey } from './shortcutKeySettings' diff --git a/src/main/presenter/configPresenter/mcpConfHelper.ts b/src/main/presenter/configPresenter/mcpConfHelper.ts index 593eb211a..a9ba84654 100644 --- a/src/main/presenter/configPresenter/mcpConfHelper.ts +++ b/src/main/presenter/configPresenter/mcpConfHelper.ts @@ -26,6 +26,23 @@ interface IMcpSettings { } export type MCPServerType = 'stdio' | 'sse' | 'inmemory' | 'http' +// Extended MCP server config with additional properties for ModelScope sync +export interface ExtendedMCPServerConfig { + name: string + description: string + args: string[] + env: Record + enabled: boolean + type: MCPServerType + package?: string + version?: string + source?: string + logo_url?: string + publisher?: string + tags?: string[] + view_count?: number +} + // 检查当前系统平台 function isMacOS(): boolean { return process.platform === 'darwin' @@ -624,6 +641,158 @@ export class McpConfHelper { }) } + /** + * Batch import MCP servers from external source (like ModelScope) + * @param servers - Array of MCP server configs to import + * @param options - Import options + * @returns Promise<{ imported: number; skipped: number; errors: string[] }> + */ + async batchImportMcpServers( + servers: Array<{ + name: string + description: string + package: string + version?: string + type?: MCPServerType + args?: string[] + env?: Record + enabled?: boolean + source?: string + [key: string]: unknown + }>, + options: { + skipExisting?: boolean + enableByDefault?: boolean + overwriteExisting?: boolean + } = {} + ): Promise<{ imported: number; skipped: number; errors: string[] }> { + const { skipExisting = true, enableByDefault = false, overwriteExisting = false } = options + const result = { + imported: 0, + skipped: 0, + errors: [] as string[] + } + + const existingServers = await this.getMcpServers() + + for (const serverConfig of servers) { + try { + // Generate unique server name based on package name + const serverName = this.generateUniqueServerName(serverConfig.package, existingServers) + const existingServer = existingServers[serverName] + + // Check if server already exists + if (existingServer && !overwriteExisting) { + if (skipExisting) { + console.log(`Skipping existing MCP server: ${serverName}`) + result.skipped++ + continue + } else { + result.errors.push(`Server ${serverName} already exists`) + continue + } + } + + // Create MCP server config + const mcpConfig: ExtendedMCPServerConfig = { + name: serverConfig.name, + description: serverConfig.description, + args: serverConfig.args || [], + env: serverConfig.env || {}, + enabled: serverConfig.enabled ?? enableByDefault, + type: (serverConfig.type as MCPServerType) || 'stdio', + package: serverConfig.package, + version: serverConfig.version || 'latest', + source: serverConfig.source as string | undefined, + logo_url: serverConfig.logo_url as string | undefined, + publisher: serverConfig.publisher as string | undefined, + tags: serverConfig.tags as string[] | undefined, + view_count: serverConfig.view_count as number | undefined + } + + // Add or update the server + const success = await this.addMcpServer(serverName, mcpConfig as unknown as MCPServerConfig) + if (success || overwriteExisting) { + if (existingServer && overwriteExisting) { + await this.updateMcpServer(serverName, mcpConfig as unknown as Partial) + console.log(`Updated MCP server: ${serverName}`) + } else { + console.log(`Imported MCP server: ${serverName}`) + } + result.imported++ + } else { + result.errors.push(`Failed to import server: ${serverName}`) + } + } catch (error) { + const errorMsg = `Error importing server ${serverConfig.name}: ${error instanceof Error ? error.message : String(error)}` + console.error(errorMsg) + result.errors.push(errorMsg) + } + } + + console.log( + `MCP batch import completed. Imported: ${result.imported}, Skipped: ${result.skipped}, Errors: ${result.errors.length}` + ) + + // Emit event to notify about the import + eventBus.sendToRenderer(MCP_EVENTS.CONFIG_CHANGED, SendTarget.ALL_WINDOWS, { + action: 'batch_import', + result + }) + + return result + } + + /** + * Generate a unique server name based on package name + * @param packageName - The package name to base the server name on + * @param existingServers - Existing servers to check against + * @returns Unique server name + */ + private generateUniqueServerName( + packageName: string, + existingServers: Record + ): string { + // Clean up package name to create a suitable server name + let baseName = packageName + .replace(/[@\/]/g, '-') + .replace(/[^a-zA-Z0-9-_]/g, '') + .toLowerCase() + + // If the base name doesn't exist, use it directly + if (!existingServers[baseName]) { + return baseName + } + + // If it exists, append a number suffix + let counter = 1 + let uniqueName = `${baseName}-${counter}` + while (existingServers[uniqueName]) { + counter++ + uniqueName = `${baseName}-${counter}` + } + + return uniqueName + } + + /** + * Check if a server with given package already exists + * @param packageName - Package name to check + * @returns Promise - Returns server name if exists, null otherwise + */ + async findServerByPackage(packageName: string): Promise { + const servers = await this.getMcpServers() + + for (const [serverName, config] of Object.entries(servers)) { + const extendedConfig = config as unknown as ExtendedMCPServerConfig + if (extendedConfig.package === packageName) { + return serverName + } + } + + return null + } + public onUpgrade(oldVersion: string | undefined): void { console.log('onUpgrade', oldVersion) if (oldVersion && compare(oldVersion, '0.0.12', '<=')) { diff --git a/src/main/presenter/configPresenter/providers.ts b/src/main/presenter/configPresenter/providers.ts index 32c2df736..b3485ff15 100644 --- a/src/main/presenter/configPresenter/providers.ts +++ b/src/main/presenter/configPresenter/providers.ts @@ -562,5 +562,20 @@ export const DEFAULT_PROVIDERS: LLM_PROVIDER_BASE[] = [ defaultBaseUrl: 'https://your-resource-name.openai.azure.com/openai/deployments/your-deployment-name' } + }, + { + id: 'modelscope', + name: 'ModelScope', + apiType: 'openai', + apiKey: '', + baseUrl: 'https://api-inference.modelscope.cn/v1/', + enable: false, + websites: { + official: 'https://modelscope.cn/', + apiKey: 'https://modelscope.cn/my/myaccesstoken', + docs: 'https://modelscope.cn/docs/modelscope_agent/api_service', + models: 'https://modelscope.cn/models', + defaultBaseUrl: 'https://api-inference.modelscope.cn/v1/' + } } ] diff --git a/src/main/presenter/llmProviderPresenter/index.ts b/src/main/presenter/llmProviderPresenter/index.ts index 0cc2a5b46..3004e4229 100644 --- a/src/main/presenter/llmProviderPresenter/index.ts +++ b/src/main/presenter/llmProviderPresenter/index.ts @@ -38,6 +38,7 @@ import { OpenRouterProvider } from './providers/openRouterProvider' import { MinimaxProvider } from './providers/minimaxProvider' import { AihubmixProvider } from './providers/aihubmixProvider' import { _302AIProvider } from './providers/_302AIProvider' +import { ModelscopeProvider } from './providers/modelscopeProvider' // 速率限制配置接口 interface RateLimitConfig { @@ -167,6 +168,9 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { if (provider.id === 'aihubmix') { return new AihubmixProvider(provider, this.configPresenter) } + if (provider.id === 'modelscope') { + return new ModelscopeProvider(provider, this.configPresenter) + } switch (provider.apiType) { case 'minimax': return new OpenAIProvider(provider, this.configPresenter) @@ -1599,4 +1603,39 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { } } } + + /** + * Sync MCP servers from ModelScope + * @param providerId - Provider ID (should be 'modelscope') + * @param syncOptions - Sync options including filters + * @returns Promise with MCP servers response + */ + async syncModelScopeMcpServers( + providerId: string, + syncOptions?: { + filter?: { + category?: string + is_hosted?: boolean + tag?: string + } + page_number?: number + page_size?: number + search?: string + } + ) { + if (providerId !== 'modelscope') { + throw new Error('MCP sync is only supported for ModelScope provider') + } + + const provider = this.getProviderInstance(providerId) + + // 类型断言确保是 ModelscopeProvider + if (provider.constructor.name !== 'ModelscopeProvider') { + throw new Error('Provider is not a ModelScope provider instance') + } + + // 调用 ModelscopeProvider 的 syncMcpServers 方法 + const modelscopeProvider = provider as any // 使用 any 因为 ModelscopeProvider 类型在此处不可直接引用 + return await modelscopeProvider.syncMcpServers(syncOptions) + } } diff --git a/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts b/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts new file mode 100644 index 000000000..4a011736a --- /dev/null +++ b/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts @@ -0,0 +1,218 @@ +import { LLM_PROVIDER, LLMResponse, ChatMessage, KeyStatus } from '@shared/presenter' +import { OpenAICompatibleProvider } from './openAICompatibleProvider' +import { ConfigPresenter } from '../../configPresenter' + +// Define interface for ModelScope MCP API response +interface ModelScopeMcpServerResponse { + code: number + data: { + mcp_server_list: ModelScopeMcpServer[] + total_count: number + } + message: string + request_id: string + success: boolean +} + +// Define interface for ModelScope MCP server +interface ModelScopeMcpServer { + name: string + description: string + id: string + logo_url: string + publisher: string + tags: string[] + view_count: number + locales: { + zh: { + name: string + description: string + } + en: { + name: string + description: string + } + } +} + +export class ModelscopeProvider extends OpenAICompatibleProvider { + constructor(provider: LLM_PROVIDER, configPresenter: ConfigPresenter) { + super(provider, configPresenter) + } + + async completions( + messages: ChatMessage[], + modelId: string, + temperature?: number, + maxTokens?: number + ): Promise { + return this.openAICompletion(messages, modelId, temperature, maxTokens) + } + + async summaries( + text: string, + modelId: string, + temperature?: number, + maxTokens?: number + ): Promise { + return this.openAICompletion( + [ + { + role: 'user', + content: `You need to summarize the user's conversation into a title of no more than 10 words, with the title language matching the user's primary language, without using punctuation or other special symbols:\n${text}` + } + ], + modelId, + temperature, + maxTokens + ) + } + + async generateText( + prompt: string, + modelId: string, + temperature?: number, + maxTokens?: number + ): Promise { + return this.openAICompletion( + [ + { + role: 'user', + content: prompt + } + ], + modelId, + temperature, + maxTokens + ) + } + + /** + * Get current API key status from ModelScope + * @returns Promise API key status information + */ + public async getKeyStatus(): Promise { + if (!this.provider.apiKey) { + throw new Error('API key is required') + } + + try { + // Use models endpoint to check API key validity + const response = await this.openai.models.list({ timeout: 10000 }) + + return { + limit_remaining: 'Available', + remainNum: response.data?.length || 0 + } + } catch (error) { + console.error('ModelScope API key check failed:', error) + throw new Error( + `ModelScope API key check failed: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + + /** + * Override check method to use ModelScope's API validation + * @returns Promise<{ isOk: boolean; errorMsg: string | null }> + */ + public async check(): Promise<{ isOk: boolean; errorMsg: string | null }> { + try { + await this.getKeyStatus() + return { isOk: true, errorMsg: null } + } catch (error: unknown) { + let errorMessage = 'An unknown error occurred during ModelScope API key check.' + if (error instanceof Error) { + errorMessage = error.message + } else if (typeof error === 'string') { + errorMessage = error + } + + console.error('ModelScope API key check failed:', error) + return { isOk: false, errorMsg: errorMessage } + } + } + + /** + * Sync MCP servers from ModelScope API + * @param options - Sync options including filters + * @returns Promise MCP servers response + */ + public async syncMcpServers(options?: { + filter?: { + category?: string + is_hosted?: boolean + tag?: string + } + page_number?: number + page_size?: number + search?: string + }): Promise { + if (!this.provider.apiKey) { + throw new Error('API key is required for MCP sync') + } + + const defaultOptions = { + filter: {}, + page_number: 1, + page_size: 50, + search: '', + ...options + } + + try { + const response = await fetch('https://www.modelscope.cn/openapi/v1/mcp/servers', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.provider.apiKey}` + }, + body: JSON.stringify(defaultOptions) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error( + `ModelScope MCP sync failed: ${response.status} ${response.statusText} - ${errorText}` + ) + } + + const data: ModelScopeMcpServerResponse = await response.json() + + if (!data.success) { + throw new Error(`ModelScope MCP sync failed: ${data.message}`) + } + + console.log( + `Successfully synced ${data.data.mcp_server_list.length} MCP servers from ModelScope` + ) + return data + } catch (error) { + console.error('ModelScope MCP sync error:', error) + throw error + } + } + + /** + * Convert ModelScope MCP server to internal MCP server config format + * @param mcpServer - ModelScope MCP server data + * @returns Internal MCP server config + */ + public convertMcpServerToConfig(mcpServer: ModelScopeMcpServer) { + return { + name: mcpServer.locales.zh.name || mcpServer.name, + description: mcpServer.locales.zh.description || mcpServer.description, + package: mcpServer.id, + version: 'latest', + type: 'npm' as const, + args: [], + env: {}, + enabled: false, + source: 'modelscope' as const, + logo_url: mcpServer.logo_url, + publisher: mcpServer.publisher, + tags: mcpServer.tags, + view_count: mcpServer.view_count + } + } +} diff --git a/src/renderer/src/components/icons/ModelIcon.vue b/src/renderer/src/components/icons/ModelIcon.vue index 2d00f3c58..2ce4b3d31 100644 --- a/src/renderer/src/components/icons/ModelIcon.vue +++ b/src/renderer/src/components/icons/ModelIcon.vue @@ -57,8 +57,10 @@ import defaultIcon from '@/assets/logo.png?url' import metaColorIcon from '@/assets/llm-icons/meta.svg?url' import lmstudioColorIcon from '@/assets/llm-icons/lmstudio.svg?url' import _302aiIcon from '@/assets/llm-icons/302ai.svg?url' +import modelscopeColorIcon from '@/assets/llm-icons/modelscope-color.svg?url' // 导入所有图标 const icons = { + 'modelscope': modelscopeColorIcon, '302ai': _302aiIcon, aihubmix: aihubmixColorIcon, dashscope: dashscopeColorIcon, diff --git a/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue b/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue index 103bb0b23..b55611161 100644 --- a/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue +++ b/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue @@ -33,6 +33,12 @@ + + + +
+

+ + {{ $t('settings.providers.modelscope.mcpSync.title') }} +

+ +
+

+ {{ $t('settings.providers.modelscope.mcpSync.description') }} +

+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ + +
+ + +
+ + + +
+ + +
+ + +
+ + {{ $t('settings.providers.modelscope.mcpSync.imported', { count: syncResult.imported }) }} + + + {{ $t('settings.providers.modelscope.mcpSync.skipped', { count: syncResult.skipped }) }} + + + {{ $t('settings.providers.modelscope.mcpSync.errors', { count: syncResult.errors.length }) }} + +
+
+ + +
+

{{ errorMessage }}

+
+ + +
+

+ {{ $t('settings.providers.modelscope.mcpSync.errorDetails') }} +

+
+
+ {{ error }} +
+
+
+
+
+ + + diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index f3884ff1f..b79840dc8 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -351,6 +351,32 @@ "modelRunning": "The model is running", "modelRunningDesc": "Please stop the model {model} first and then delete it." }, + "modelscope": { + "mcpSync": { + "title": "Sync MCP Services", + "description": "Sync MCP servers from ModelScope to local configuration, allowing quick addition of common MCP tools.", + "sync": "Start Sync", + "syncing": "Syncing...", + "pageSize": "Page Size", + "category": "Category", + "search": "Search", + "searchPlaceholder": "Search MCP services...", + "enableByDefault": "Enable by default when importing", + "overwriteExisting": "Overwrite existing services", + "imported": "Imported {count} services", + "skipped": "Skipped {count} services", + "errors": "{count} errors", + "errorDetails": "Error Details", + "noApiKey": "Please configure ModelScope API Key first", + "noServersFound": "No available MCP services found", + "categories": { + "communication": "Communication", + "productivity": "Productivity", + "development": "Development", + "data": "Data" + } + } + }, "anthropicApiKeyTip": "Please go to Anthropic Console to get your API Key", "anthropicConnected": "Anthropic connected", "anthropicNotConnected": "Anthropic not connected", diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index 30e0b5783..d40810574 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -251,6 +251,32 @@ "modelRunning": "模型正在运行", "modelRunningDesc": "请先停止模型 {model},然后再删除。" }, + "modelscope": { + "mcpSync": { + "title": "同步 MCP 服务", + "description": "从 ModelScope 同步 MCP 服务器到本地配置,可以快速添加常用的 MCP 工具。", + "sync": "开始同步", + "syncing": "同步中...", + "pageSize": "每页数量", + "category": "分类", + "search": "搜索", + "searchPlaceholder": "搜索 MCP 服务...", + "enableByDefault": "导入时默认启用", + "overwriteExisting": "覆盖已存在的服务", + "imported": "已导入 {count} 个服务", + "skipped": "跳过 {count} 个服务", + "errors": "错误 {count} 个", + "errorDetails": "错误详情", + "noApiKey": "请先配置 ModelScope API Key", + "noServersFound": "未找到可用的 MCP 服务", + "categories": { + "communication": "通讯", + "productivity": "生产力", + "development": "开发", + "data": "数据" + } + } + }, "dialog": { "disableModel": { "title": "确认禁用模型", From 1024a140f24550805ce9a681ba74da1655652553 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Mon, 11 Aug 2025 19:40:36 +0800 Subject: [PATCH 2/7] feat: add mcp sync to modelscope --- .../presenter/llmProviderPresenter/index.ts | 144 +++++++++++-- .../providers/modelscopeProvider.ts | 192 ++++++++++++++---- .../components/settings/ModelScopeMcpSync.vue | 120 +++-------- src/renderer/src/i18n/en-US/settings.json | 63 ++++-- src/renderer/src/i18n/fa-IR/settings.json | 66 +++++- src/renderer/src/i18n/fr-FR/settings.json | 66 +++++- src/renderer/src/i18n/ja-JP/settings.json | 66 +++++- src/renderer/src/i18n/ko-KR/settings.json | 66 +++++- src/renderer/src/i18n/ru-RU/settings.json | 66 +++++- src/renderer/src/i18n/zh-CN/settings.json | 62 ++++-- src/renderer/src/i18n/zh-HK/settings.json | 66 +++++- src/shared/presenter.d.ts | 20 ++ 12 files changed, 809 insertions(+), 188 deletions(-) diff --git a/src/main/presenter/llmProviderPresenter/index.ts b/src/main/presenter/llmProviderPresenter/index.ts index 3004e4229..33b76b575 100644 --- a/src/main/presenter/llmProviderPresenter/index.ts +++ b/src/main/presenter/llmProviderPresenter/index.ts @@ -8,7 +8,9 @@ import { ChatMessage, LLMAgentEvent, KeyStatus, - LLM_EMBEDDING_ATTRS + LLM_EMBEDDING_ATTRS, + ModelScopeMcpSyncOptions, + ModelScopeMcpSyncResult } from '@shared/presenter' import { BaseLLMProvider } from './baseProvider' import { OpenAIProvider } from './providers/openAIProvider' @@ -1605,37 +1607,137 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { } /** - * Sync MCP servers from ModelScope + * Sync MCP servers from ModelScope and import them to local configuration * @param providerId - Provider ID (should be 'modelscope') - * @param syncOptions - Sync options including filters - * @returns Promise with MCP servers response + * @param syncOptions - Simplified sync options + * @returns Promise with sync result statistics */ async syncModelScopeMcpServers( providerId: string, - syncOptions?: { - filter?: { - category?: string - is_hosted?: boolean - tag?: string - } - page_number?: number - page_size?: number - search?: string - } - ) { + syncOptions?: ModelScopeMcpSyncOptions + ): Promise { + console.log(`[ModelScope MCP Sync] Starting sync for provider: ${providerId}`) + console.log(`[ModelScope MCP Sync] Sync options:`, syncOptions) + if (providerId !== 'modelscope') { - throw new Error('MCP sync is only supported for ModelScope provider') + const error = 'MCP sync is only supported for ModelScope provider' + console.error(`[ModelScope MCP Sync] Error: ${error}`) + throw new Error(error) } const provider = this.getProviderInstance(providerId) - // 类型断言确保是 ModelscopeProvider + // Type check for ModelscopeProvider if (provider.constructor.name !== 'ModelscopeProvider') { - throw new Error('Provider is not a ModelScope provider instance') + const error = 'Provider is not a ModelScope provider instance' + console.error(`[ModelScope MCP Sync] Error: ${error}`) + throw new Error(error) } - // 调用 ModelscopeProvider 的 syncMcpServers 方法 - const modelscopeProvider = provider as any // 使用 any 因为 ModelscopeProvider 类型在此处不可直接引用 - return await modelscopeProvider.syncMcpServers(syncOptions) + const result: ModelScopeMcpSyncResult = { + imported: 0, + skipped: 0, + errors: [] + } + + try { + // Create async task to prevent blocking main thread + const syncTask = async () => { + console.log(`[ModelScope MCP Sync] Fetching MCP servers from ModelScope API...`) + + // Call ModelscopeProvider to fetch MCP servers + const modelscopeProvider = provider as any + const mcpResponse = await modelscopeProvider.syncMcpServers(syncOptions) + + if (!mcpResponse || !mcpResponse.success || !mcpResponse.data?.mcp_server_list) { + const errorMsg = 'Invalid response from ModelScope MCP API' + console.error(`[ModelScope MCP Sync] ${errorMsg}`, mcpResponse) + result.errors.push(errorMsg) + return result + } + + const mcpServers = mcpResponse.data.mcp_server_list + console.log(`[ModelScope MCP Sync] Fetched ${mcpServers.length} MCP servers from API`) + + // Convert ModelScope operational MCP servers to internal format + const convertedServers = mcpServers + .map((server: any) => { + try { + // Check if operational URLs are available + if (!server.operational_urls || server.operational_urls.length === 0) { + const errorMsg = `No operational URLs found for server ${server.id}` + console.warn(`[ModelScope MCP Sync] ${errorMsg}`) + result.errors.push(errorMsg) + return null + } + + // Use ModelScope provider's conversion method for consistency + const modelscopeProvider = provider as any + const converted = modelscopeProvider.convertMcpServerToConfig(server) + + console.log( + `[ModelScope MCP Sync] Converted operational server: ${converted.displayName} (${converted.name})` + ) + return converted + } catch (conversionError) { + const errorMsg = `Failed to convert server ${server.name || server.id}: ${conversionError instanceof Error ? conversionError.message : String(conversionError)}` + console.error(`[ModelScope MCP Sync] ${errorMsg}`) + result.errors.push(errorMsg) + return null + } + }) + .filter((server: any) => server !== null) + + console.log( + `[ModelScope MCP Sync] Successfully converted ${convertedServers.length} servers` + ) + + // Import servers to configuration using configPresenter + for (const serverConfig of convertedServers) { + try { + const existingServers = await this.configPresenter.getMcpServers() + + // Check if server already exists + if (existingServers[serverConfig.name]) { + console.log( + `[ModelScope MCP Sync] Server ${serverConfig.name} already exists, skipping` + ) + result.skipped++ + continue + } + + // Add server to configuration + const success = await this.configPresenter.addMcpServer(serverConfig.name, serverConfig) + if (success) { + console.log( + `[ModelScope MCP Sync] Successfully imported server: ${serverConfig.name}` + ) + result.imported++ + } else { + const errorMsg = `Failed to add server ${serverConfig.name} to configuration` + console.error(`[ModelScope MCP Sync] ${errorMsg}`) + result.errors.push(errorMsg) + } + } catch (importError) { + const errorMsg = `Failed to import server ${serverConfig.name}: ${importError instanceof Error ? importError.message : String(importError)}` + console.error(`[ModelScope MCP Sync] ${errorMsg}`) + result.errors.push(errorMsg) + } + } + + console.log( + `[ModelScope MCP Sync] Sync completed. Imported: ${result.imported}, Skipped: ${result.skipped}, Errors: ${result.errors.length}` + ) + return result + } + + // Execute async without blocking + return await syncTask() + } catch (error) { + const errorMsg = `ModelScope MCP sync failed: ${error instanceof Error ? error.message : String(error)}` + console.error(`[ModelScope MCP Sync] ${errorMsg}`) + result.errors.push(errorMsg) + return result + } } } diff --git a/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts b/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts index 4a011736a..1a4468cd7 100644 --- a/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts @@ -14,15 +14,18 @@ interface ModelScopeMcpServerResponse { success: boolean } -// Define interface for ModelScope MCP server +// Define interface for ModelScope MCP server (updated for operational API) interface ModelScopeMcpServer { name: string description: string id: string + chinese_name?: string // Chinese name field logo_url: string - publisher: string + operational_urls: Array<{ + id: string + url: string + }> tags: string[] - view_count: number locales: { zh: { name: string @@ -134,43 +137,38 @@ export class ModelscopeProvider extends OpenAICompatibleProvider { } /** - * Sync MCP servers from ModelScope API - * @param options - Sync options including filters + * Sync operational MCP servers from ModelScope API + * @param _options - Sync options including filters (currently not used by operational API) * @returns Promise MCP servers response */ - public async syncMcpServers(options?: { + public async syncMcpServers(_options?: { filter?: { - category?: string is_hosted?: boolean - tag?: string } page_number?: number page_size?: number - search?: string }): Promise { if (!this.provider.apiKey) { throw new Error('API key is required for MCP sync') } - const defaultOptions = { - filter: {}, - page_number: 1, - page_size: 50, - search: '', - ...options - } - try { - const response = await fetch('https://www.modelscope.cn/openapi/v1/mcp/servers', { - method: 'PUT', + // Use the operational API endpoint - GET request, no body needed + const response = await fetch('https://www.modelscope.cn/openapi/v1/mcp/servers/operational', { + method: 'GET', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.provider.apiKey}` - }, - body: JSON.stringify(defaultOptions) + } }) - if (!response.ok) { + // Handle authentication errors + if (response.status === 401 || response.status === 403) { + throw new Error('ModelScope MCP sync unauthorized: Invalid or expired API key') + } + + // Handle server errors + if (response.status === 500 || !response.ok) { const errorText = await response.text() throw new Error( `ModelScope MCP sync failed: ${response.status} ${response.statusText} - ${errorText}` @@ -184,7 +182,7 @@ export class ModelscopeProvider extends OpenAICompatibleProvider { } console.log( - `Successfully synced ${data.data.mcp_server_list.length} MCP servers from ModelScope` + `Successfully fetched ${data.data.mcp_server_list.length} operational MCP servers from ModelScope` ) return data } catch (error) { @@ -194,25 +192,149 @@ export class ModelscopeProvider extends OpenAICompatibleProvider { } /** - * Convert ModelScope MCP server to internal MCP server config format + * Convert ModelScope operational MCP server to internal MCP server config format * @param mcpServer - ModelScope MCP server data * @returns Internal MCP server config */ public convertMcpServerToConfig(mcpServer: ModelScopeMcpServer) { + // Check if operational URLs are available + if (!mcpServer.operational_urls || mcpServer.operational_urls.length === 0) { + throw new Error(`No operational URLs found for server ${mcpServer.id}`) + } + + // Use the first operational URL + const baseUrl = mcpServer.operational_urls[0].url + + // Generate random emoji for icon + const emojis = [ + '🔧', + '⚡', + '🚀', + '🔨', + '⚙️', + '🛠️', + '🔥', + '💡', + '⭐', + '🎯', + '🎨', + '🔮', + '💎', + '🎪', + '🎭', + '🎨', + '🔬', + '📱', + '💻', + '🖥️', + '⌨️', + '🖱️', + '📡', + '🔊', + '📢', + '📣', + '📯', + '🔔', + '🔕', + '📻', + '📺', + '📷', + '📹', + '🎥', + '📽️', + '🔍', + '🔎', + '💰', + '💳', + '💸', + '💵', + '🎲', + '🃏', + '🎮', + '🕹️', + '🎯', + '🎳', + '🎨', + '🖌️', + '🖍️', + '📝', + '✏️', + '📏', + '📐', + '📌', + '📍', + '🗂️', + '📂', + '📁', + '📰', + '📄', + '📃', + '📜', + '📋', + '📊', + '📈', + '📉', + '📦', + '📫', + '📪', + '📬', + '📭', + '📮', + '🗳️', + '✉️', + '📧', + '📨', + '📩', + '📤', + '📥', + '📬', + '📭', + '📮', + '🗂️', + '📂', + '📁', + '🗄️', + '🗃️', + '📋', + '📑', + '📄', + '📃', + '📰', + '🗞️', + '📜', + '🔖' + ] + const randomEmoji = emojis[Math.floor(Math.random() * emojis.length)] + + // Get display name: chinese_name first, then id + const displayName = mcpServer.chinese_name || mcpServer.id + return { - name: mcpServer.locales.zh.name || mcpServer.name, - description: mcpServer.locales.zh.description || mcpServer.description, - package: mcpServer.id, - version: 'latest', - type: 'npm' as const, - args: [], + name: `@modelscope/${mcpServer.id}`, // Use ModelScope ID format + description: + mcpServer.locales?.zh?.description || + mcpServer.description || + `ModelScope MCP Server: ${displayName}`, + command: '', // Not needed for SSE type + args: [], // Not needed for SSE type env: {}, - enabled: false, + type: 'sse' as const, // SSE type for operational servers + baseUrl: baseUrl, // Use operational URL + enabled: false, // Default to disabled for safety source: 'modelscope' as const, - logo_url: mcpServer.logo_url, - publisher: mcpServer.publisher, - tags: mcpServer.tags, - view_count: mcpServer.view_count + // Additional metadata + descriptions: + mcpServer.locales?.zh?.description || + mcpServer.description || + `ModelScope MCP Server: ${displayName}`, + icons: randomEmoji, // Random emoji instead of URL + provider: 'ModelScope', + providerUrl: `https://www.modelscope.cn/mcp/servers/@${mcpServer.id}`, + logoUrl: '', // No longer using logo URL + tags: mcpServer.tags || [], + // Store original data for reference + originalId: mcpServer.id, + displayName: displayName } } } diff --git a/src/renderer/src/components/settings/ModelScopeMcpSync.vue b/src/renderer/src/components/settings/ModelScopeMcpSync.vue index 3dba45df9..d194ca9a6 100644 --- a/src/renderer/src/components/settings/ModelScopeMcpSync.vue +++ b/src/renderer/src/components/settings/ModelScopeMcpSync.vue @@ -2,19 +2,19 @@

- {{ $t('settings.providers.modelscope.mcpSync.title') }} + {{ t('settings.provider.modelscope.mcpSync.title') }}

- {{ $t('settings.providers.modelscope.mcpSync.description') }} + {{ t('settings.provider.modelscope.mcpSync.description') }}

- - - - - - + :placeholder="t('settings.provider.modelscope.mcpSync.pageNumberPlaceholder')" + />
- -
- - -
- - +
- -
@@ -95,18 +70,18 @@ icon="lucide:download" class="h-4 w-4" /> - {{ isSyncing ? $t('settings.providers.modelscope.mcpSync.syncing') : $t('settings.providers.modelscope.mcpSync.sync') }} + {{ isSyncing ? t('settings.provider.modelscope.mcpSync.syncing') : t('settings.provider.modelscope.mcpSync.sync') }}
- {{ $t('settings.providers.modelscope.mcpSync.imported', { count: syncResult.imported }) }} + {{ t('settings.provider.modelscope.mcpSync.imported', { count: syncResult.imported }) }} - {{ $t('settings.providers.modelscope.mcpSync.skipped', { count: syncResult.skipped }) }} + {{ t('settings.provider.modelscope.mcpSync.skipped', { count: syncResult.skipped }) }} - {{ $t('settings.providers.modelscope.mcpSync.errors', { count: syncResult.errors.length }) }} + {{ t('settings.provider.modelscope.mcpSync.errors', { count: syncResult.errors.length }) }}
@@ -119,7 +94,7 @@

- {{ $t('settings.providers.modelscope.mcpSync.errorDetails') }} + {{ t('settings.provider.modelscope.mcpSync.errorDetails') }}

@@ -146,7 +121,6 @@ const props = defineProps<{ }>() const llmP = usePresenter('llmproviderPresenter') -const configP = usePresenter('configPresenter') const isSyncing = ref(false) const errorMessage = ref('') @@ -159,25 +133,15 @@ const syncResult = ref<{ // 同步选项 const syncOptions = reactive({ filter: { - category: '', - is_hosted: true, - tag: '' + is_hosted: true }, page_number: 1, - page_size: 50, - search: '' -}) - -// 导入选项 -const importOptions = reactive({ - enableByDefault: false, - overwriteExisting: false, - skipExisting: true + page_size: 50 }) const handleSync = async () => { if (!props.provider.apiKey) { - errorMessage.value = t('settings.providers.modelscope.mcpSync.noApiKey') + errorMessage.value = t('settings.provider.modelscope.mcpSync.noApiKey') return } @@ -186,46 +150,16 @@ const handleSync = async () => { syncResult.value = null try { - // 通过 usePresenter 调用主进程的同步功能 - const mcpServers = await (llmP as any).syncModelScopeMcpServers( + // 调用简化的同步API,所有的格式转换和导入都在服务端处理 + const result = await llmP.syncModelScopeMcpServers( props.provider.id, syncOptions ) - if (mcpServers && mcpServers.data && mcpServers.data.mcp_server_list) { - // 将 ModelScope MCP 服务器转换为内部格式 - const serversToImport = mcpServers.data.mcp_server_list.map((server: any) => ({ - name: server.locales?.zh?.name || server.name, - description: server.locales?.zh?.description || server.description, - package: server.id, - version: 'latest', - type: 'npm', - args: [], - env: {}, - enabled: importOptions.enableByDefault, - source: 'modelscope', - logo_url: server.logo_url, - publisher: server.publisher, - tags: server.tags || [], - view_count: server.view_count || 0 - })) - - // 批量导入 MCP 服务器 - const result = await (configP as any).batchImportMcpServers(serversToImport, { - skipExisting: !importOptions.overwriteExisting, - enableByDefault: importOptions.enableByDefault, - overwriteExisting: importOptions.overwriteExisting - }) - - syncResult.value = result - - if (result.imported > 0) { - // 触发 MCP 服务器列表更新 - 这里可能需要通过事件或其他方式通知 - // 可以考虑触发一个事件或调用相关的刷新方法 - console.log('MCP servers imported successfully') - } - } else { - errorMessage.value = t('settings.providers.modelscope.mcpSync.noServersFound') + syncResult.value = result + + if (result.imported > 0) { + console.log('MCP servers imported successfully:', result) } } catch (error) { console.error('MCP sync error:', error) diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index b79840dc8..0906fdb97 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -266,7 +266,10 @@ "failed": "Verification failed", "success": "Verification successful", "failedDesc": "API key or configuration verification failed, please check your configuration", - "successDesc": "API key and configuration verified successfully, ready to use" + "successDesc": "API key and configuration verified successfully, ready to use", + "connectionError": "Connection error, please check the network connection and API address", + "serverError": "Server error, please try again later", + "unauthorized": "Authentication failed, API Key is invalid or expired" }, "addCustomProvider": { "title": "Add Custom Provider", @@ -358,24 +361,50 @@ "sync": "Start Sync", "syncing": "Syncing...", "pageSize": "Page Size", - "category": "Category", - "search": "Search", - "searchPlaceholder": "Search MCP services...", - "enableByDefault": "Enable by default when importing", - "overwriteExisting": "Overwrite existing services", "imported": "Imported {count} services", "skipped": "Skipped {count} services", "errors": "{count} errors", "errorDetails": "Error Details", "noApiKey": "Please configure ModelScope API Key first", "noServersFound": "No available MCP services found", - "categories": { - "communication": "Communication", - "productivity": "Productivity", - "development": "Development", - "data": "Data" - } - } + "authenticationFailed": "Authentication failed, please check API Key", + "convertingServers": "Converting server configuration...", + "fetchingServers": "Getting the list of MCP servers...", + "importingServers": "Importing server configuration...", + "noOperationalUrls": "No available operating address found", + "onlyHosted": "Hosting services only", + "pageNumber": "page number", + "pageNumberPlaceholder": "Please enter the page number", + "serverAlreadyExists": "The server already exists, skip import", + "syncComplete": "Synchronous completion" + }, + "apiKey": "API Key", + "apiKeyHelper": "Get your API Key in the ModelScope console", + "apiKeyPlaceholder": "Please enter ModelScope API Key", + "baseUrl": "API Address", + "baseUrlHelper": "ModelScope API service address", + "connected": "Connected", + "connecting": "Connecting...", + "description": "ModelScope is a model-as-a-service sharing platform launched by Alibaba Damo Academy", + "details": { + "apiConfig": "API Configuration", + "mcpSync": "MCP Synchronization", + "modelManagement": "Model Management", + "operationalDescription": "Synchronize MCP servers that can be used directly on the ModelScope platform", + "operationalServers": "Operating a server", + "rateLimitConfig": "Rate limit configuration", + "safetySettings": "Security settings", + "specialConfig": "Special configuration", + "syncFromModelScope": "Sync from ModelScope", + "title": "Provider settings details" + }, + "invalidKey": "Invalid API Key", + "keyRequired": "Please enter API Key", + "name": "ModelScope", + "networkError": "Network connection error", + "notConnected": "Not connected", + "verifyFailed": "Verification failed", + "verifySuccess": "Verification is successful" }, "anthropicApiKeyTip": "Please go to Anthropic Console to get your API Key", "anthropicConnected": "Anthropic connected", @@ -400,7 +429,13 @@ "manageModels": "Manage Models", "anthropicOAuthActiveTip": "OAuth authentication is enabled, you can use Anthropic services directly", "oauthVerifySuccess": "OAuth connection verified successfully", - "oauthVerifyFailed": "OAuth connection verification failed" + "oauthVerifyFailed": "OAuth connection verification failed", + "configurationSaved": "Configuration saved", + "configurationUpdated": "Configuration updated", + "dataRefreshed": "Data has been refreshed", + "operationFailed": "Operation failed", + "operationSuccess": "Operation is successful", + "settingsApplied": "Settings applied" }, "knowledgeBase": { "title": "Knowledge Base Settings", diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json index db8c2cf2c..cbd63f906 100644 --- a/src/renderer/src/i18n/fa-IR/settings.json +++ b/src/renderer/src/i18n/fa-IR/settings.json @@ -266,7 +266,10 @@ "failed": "راستی‌آزمایی ناموفق", "success": "راستی‌آزمایی موفق", "failedDesc": "راستی‌آزمایی کلید API یا پیکربندی ناموفق بود، لطفاً تنظیمات را بررسی کنید", - "successDesc": "کلید API و پیکربندی با موفقیت راستی‌آزمایی شد، آماده استفاده است" + "successDesc": "کلید API و پیکربندی با موفقیت راستی‌آزمایی شد، آماده استفاده است", + "connectionError": "خطای اتصال ، لطفا اتصال شبکه و آدرس API را بررسی کنید", + "serverError": "خطای سرور ، لطفاً بعداً دوباره امتحان کنید", + "unauthorized": "احراز هویت انجام نشد ، کلید API نامعتبر یا منقضی شده است" }, "addCustomProvider": { "title": "افزودن فراهم‌کننده دلخواه", @@ -374,7 +377,66 @@ "manageModels": "مدیریت مدل‌ها", "anthropicOAuthActiveTip": "احراز هویت OAuth فعال است، می‌توانید مستقیماً از سرویس‌های Anthropic استفاده کنید", "oauthVerifySuccess": "اتصال OAuth با موفقیت تأیید شد", - "oauthVerifyFailed": "تأیید اتصال OAuth ناموفق بود" + "oauthVerifyFailed": "تأیید اتصال OAuth ناموفق بود", + "configurationSaved": "پیکربندی ذخیره شده", + "configurationUpdated": "پیکربندی به روز شد", + "dataRefreshed": "داده ها تازه شده است", + "modelscope": { + "apiKey": "کلید API", + "apiKeyHelper": "کلید API خود را در کنسول ModelCope دریافت کنید", + "apiKeyPlaceholder": "لطفاً کلید API Modelcope را وارد کنید", + "baseUrl": "آدرس API", + "baseUrlHelper": "آدرس خدمات API Modelcope", + "connected": "متصل", + "connecting": "اتصال ...", + "description": "Modelcope یک بستر اشتراک گذاری مدل به عنوان یک سرویس است که توسط آکادمی Alibaba Damo راه اندازی شده است", + "details": { + "apiConfig": "پیکربندی API", + "mcpSync": "همگام سازی MCP", + "modelManagement": "مدیریت مدل", + "operationalDescription": "سرورهای MCP را همگام سازی کنید که می توانند مستقیماً در سیستم عامل ModelCope استفاده شوند", + "operationalServers": "کار کردن یک سرور", + "rateLimitConfig": "پیکربندی حد نرخ", + "safetySettings": "تنظیمات امنیتی", + "specialConfig": "پیکربندی خاص", + "syncFromModelScope": "همگام سازی از Modelcope", + "title": "جزئیات تنظیمات ارائه دهنده" + }, + "invalidKey": "کلید API نامعتبر", + "keyRequired": "لطفا کلید API را وارد کنید", + "mcpSync": { + "authenticationFailed": "احراز هویت انجام نشد ، لطفاً کلید API را بررسی کنید", + "convertingServers": "تبدیل پیکربندی سرور ...", + "description": "همگام سازی یک سرور MCP از ModelScope به تنظیمات محلی به شما امکان می دهد تا به سرعت ابزارهای MCP متداول را اضافه کنید. کلیه خدمات به طور پیش فرض غیرفعال می شوند و پس از وارد کردن می توانند به صورت دستی فعال شوند.", + "errorDetails": "جزئیات خطا", + "errors": "خطا {تعداد}", + "fetchingServers": "دریافت لیست سرورهای MCP ...", + "imported": "{تعداد خدمات وارد شده است", + "importingServers": "وارد کردن پیکربندی سرور ...", + "invalidServerData": "داده های سرور نامعتبر", + "noApiKey": "لطفاً ابتدا کلید API Modelcope را پیکربندی کنید", + "noOperationalUrls": "هیچ آدرس عملیاتی موجود یافت نمی شود", + "noServersFound": "هیچ سرویس MCP در دسترس یافت نشد", + "onlyHosted": "فقط خدمات میزبانی", + "pageNumber": "شماره صفحه", + "pageNumberPlaceholder": "لطفا شماره صفحه را وارد کنید", + "pageSize": "مقدار در هر صفحه", + "serverAlreadyExists": "سرور در حال حاضر وجود دارد ، واردات را پرش کنید", + "skipped": "خدمات SKIP {COUNT", + "sync": "همگام سازی را شروع کنید", + "syncComplete": "اتمام همزمان", + "syncing": "هماهنگ سازی ...", + "title": "همگام سازی خدمات MCP" + }, + "name": "مدلهای مدل", + "networkError": "خطای اتصال شبکه", + "notConnected": "متصل نیست", + "verifyFailed": "تأیید انجام نشد", + "verifySuccess": "تأیید موفقیت آمیز است" + }, + "operationFailed": "عملیات شکست خورد", + "operationSuccess": "عملیات موفقیت آمیز است", + "settingsApplied": "تنظیمات اعمال شده" }, "knowledgeBase": { "title": "تنظیمات پایگاه دانش", diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json index ddd01ab00..3f6289aa6 100644 --- a/src/renderer/src/i18n/fr-FR/settings.json +++ b/src/renderer/src/i18n/fr-FR/settings.json @@ -266,7 +266,10 @@ "failed": "Échec de la vérification", "success": "Vérification réussie", "failedDesc": "La vérification de la clé API ou de la configuration a échoué, veuillez vérifier vos paramètres", - "successDesc": "La clé API et la configuration ont été vérifiées avec succès, prêt à utiliser" + "successDesc": "La clé API et la configuration ont été vérifiées avec succès, prêt à utiliser", + "connectionError": "Erreur de connexion, veuillez vérifier la connexion réseau et l'adresse de l'API", + "serverError": "Erreur du serveur, veuillez réessayer plus tard", + "unauthorized": "Échec de l'authentification, la clé API n'est pas valide ou expirée" }, "addCustomProvider": { "title": "Ajouter un fournisseur personnalisé", @@ -374,7 +377,66 @@ "manageModels": "Gérer les modèles", "anthropicOAuthActiveTip": "L'authentification OAuth est activée, vous pouvez utiliser les services Anthropic directement", "oauthVerifySuccess": "Connexion OAuth vérifiée avec succès", - "oauthVerifyFailed": "Échec de la vérification de la connexion OAuth" + "oauthVerifyFailed": "Échec de la vérification de la connexion OAuth", + "configurationSaved": "Configuration enregistrée", + "configurationUpdated": "Configuration mise à jour", + "dataRefreshed": "Les données ont été rafraîchies", + "modelscope": { + "apiKey": "Clé API", + "apiKeyHelper": "Obtenez votre clé API dans la console Modelcope", + "apiKeyPlaceholder": "Veuillez saisir la clé de l'API Modelscope", + "baseUrl": "Adresse API", + "baseUrlHelper": "Adresse du service API Modelcope", + "connected": "Connecté", + "connecting": "De liaison...", + "description": "Modelcope est une plate-forme de partage modèle en tant que service lancée par Alibaba Damo Academy", + "details": { + "apiConfig": "Configuration de l'API", + "mcpSync": "Synchronisation MCP", + "modelManagement": "Gestion des modèles", + "operationalDescription": "Synchroniser les serveurs MCP qui peuvent être utilisés directement sur la plate-forme Modelcope", + "operationalServers": "Exploitation d'un serveur", + "rateLimitConfig": "Configuration de la limite de taux", + "safetySettings": "Paramètres de sécurité", + "specialConfig": "Configuration spéciale", + "syncFromModelScope": "Synchronisation de Modelscope", + "title": "Détails des paramètres du fournisseur" + }, + "invalidKey": "Clé API non valide", + "keyRequired": "Veuillez saisir la clé de l'API", + "mcpSync": { + "authenticationFailed": "Échec de l'authentification, veuillez vérifier la clé de l'API", + "convertingServers": "Conversion de la configuration du serveur ...", + "description": "La synchronisation d'un serveur MCP de Modelcope aux configurations locales vous permet d'ajouter rapidement des outils MCP couramment utilisés. Tous les services sont désactivés par défaut et peuvent être activés manuellement après l'importation.", + "errorDetails": "Détails d'erreur", + "errors": "Erreur {count}", + "fetchingServers": "Obtenir la liste des serveurs MCP ...", + "imported": "{comte} Les services ont été importés", + "importingServers": "Configuration d'importation de serveur ...", + "invalidServerData": "Données de serveur non valides", + "noApiKey": "Veuillez d'abord configurer la clé de l'API Modelcope", + "noOperationalUrls": "Aucune adresse de fonctionnement disponible trouvée", + "noServersFound": "Aucun service MCP disponible trouvé", + "onlyHosted": "Services d'hébergement uniquement", + "pageNumber": "numéro de page", + "pageNumberPlaceholder": "Veuillez saisir le numéro de page", + "pageSize": "Quantité par page", + "serverAlreadyExists": "Le serveur existe déjà, sautez l'importation", + "skipped": "Skip {count} Services", + "sync": "Commencer à se synchroniser", + "syncComplete": "Achèvement synchrone", + "syncing": "Synchronisation...", + "title": "Synchroniser les services MCP" + }, + "name": "Modelcope", + "networkError": "Erreur de connexion réseau", + "notConnected": "Non connecté", + "verifyFailed": "Échec de la vérification", + "verifySuccess": "La vérification est réussie" + }, + "operationFailed": "L'opération a échoué", + "operationSuccess": "L'opération est réussie", + "settingsApplied": "Paramètres appliqués" }, "knowledgeBase": { "title": "Paramètres de la base de connaissances", diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json index 948c3f2eb..26e465769 100644 --- a/src/renderer/src/i18n/ja-JP/settings.json +++ b/src/renderer/src/i18n/ja-JP/settings.json @@ -266,7 +266,10 @@ "failed": "検証に失敗しました", "success": "検証に成功しました", "failedDesc": "APIキーまたは設定の検証に失敗しました。設定を確認してください", - "successDesc": "APIキーと設定の検証に成功しました。使用可能です" + "successDesc": "APIキーと設定の検証に成功しました。使用可能です", + "connectionError": "接続エラー、ネットワーク接続とAPIアドレスを確認してください", + "serverError": "サーバーエラー、後でもう一度やり直してください", + "unauthorized": "認証に失敗し、APIキーが無効または期限切れです" }, "addCustomProvider": { "title": "カスタムプロバイダーを追加", @@ -374,7 +377,66 @@ "manageModels": "モデル管理", "anthropicOAuthActiveTip": "OAuth認証が有効になっており、Anthropicサービスを直接使用できます", "oauthVerifySuccess": "OAuth接続の確認が成功しました", - "oauthVerifyFailed": "OAuth接続の確認に失敗しました" + "oauthVerifyFailed": "OAuth接続の確認に失敗しました", + "configurationSaved": "構成が保存されました", + "configurationUpdated": "設定が更新されました", + "dataRefreshed": "データは更新されています", + "modelscope": { + "apiKey": "APIキー", + "apiKeyHelper": "ModelScopeコンソールでAPIキーを取得します", + "apiKeyPlaceholder": "ModelScope APIキーを入力してください", + "baseUrl": "APIアドレス", + "baseUrlHelper": "ModelScope APIサービスアドレス", + "connected": "接続", + "connecting": "接続...", + "description": "ModelScopeは、Alibaba Damo Academyによって開始されたサービスとしてのモデル共有プラットフォームです", + "details": { + "apiConfig": "API構成", + "mcpSync": "MCP同期", + "modelManagement": "モデル管理", + "operationalDescription": "ModelScopeプラットフォームで直接使用できるMCPサーバーを同期する", + "operationalServers": "サーバーの操作", + "rateLimitConfig": "レート制限構成", + "safetySettings": "セキュリティ設定", + "specialConfig": "特別な構成", + "syncFromModelScope": "ModelScopeから同期します", + "title": "プロバイダーの設定の詳細" + }, + "invalidKey": "無効なAPIキー", + "keyRequired": "APIキーを入力してください", + "mcpSync": { + "authenticationFailed": "認証に失敗しました。APIキーを確認してください", + "convertingServers": "サーバー構成の変換...", + "description": "MODESCOPEからローカル構成までMCPサーバーを同期すると、一般的に使用されるMCPツールをすばやく追加できます。すべてのサービスはデフォルトで無効になり、インポート後に手動で有効にすることができます。", + "errorDetails": "エラーの詳細", + "errors": "エラー{count}", + "fetchingServers": "MCPサーバーのリストを取得...", + "imported": "{count}サービスがインポートされています", + "importingServers": "サーバー構成のインポート...", + "invalidServerData": "無効なサーバーデータ", + "noApiKey": "ModelScope APIキーを最初に構成してください", + "noOperationalUrls": "利用可能な操作アドレスは見つかりません", + "noServersFound": "利用可能なMCPサービスは見つかりません", + "onlyHosted": "ホスティングサービスのみ", + "pageNumber": "ページ番号", + "pageNumberPlaceholder": "ページ番号を入力してください", + "pageSize": "ページごとの数量", + "serverAlreadyExists": "サーバーはすでに存在し、インポートをスキップします", + "skipped": "Skip {count}サービス", + "sync": "同期を開始します", + "syncComplete": "同期完了", + "syncing": "同期...", + "title": "MCPサービスを同期します" + }, + "name": "ModelScope", + "networkError": "ネットワーク接続エラー", + "notConnected": "接続されていません", + "verifyFailed": "検証に失敗しました", + "verifySuccess": "検証は成功しました" + }, + "operationFailed": "操作に失敗しました", + "operationSuccess": "操作は成功しました", + "settingsApplied": "適用された設定" }, "knowledgeBase": { "title": "ナレッジベース設定", diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json index e0f8b8028..28fa5d6cb 100644 --- a/src/renderer/src/i18n/ko-KR/settings.json +++ b/src/renderer/src/i18n/ko-KR/settings.json @@ -265,7 +265,10 @@ "failed": "확인 실패", "success": "확인 성공", "failedDesc": "API 키 또는 설정 확인에 실패했습니다. 설정을 확인해주세요", - "successDesc": "API 키와 설정이 성공적으로 확인되었습니다. 사용 가능합니다" + "successDesc": "API 키와 설정이 성공적으로 확인되었습니다. 사용 가능합니다", + "connectionError": "연결 오류, 네트워크 연결 및 API 주소를 확인하십시오.", + "serverError": "서버 오류, 나중에 다시 시도하십시오", + "unauthorized": "인증 실패, API 키가 유효하지 않거나 만료되었습니다" }, "addCustomProvider": { "title": "커스텀 제공자 추가", @@ -374,7 +377,66 @@ "manageModels": "모델 관리", "anthropicOAuthActiveTip": "OAuth 인증이 활성화되어 있어 Anthropic 서비스를 직접 사용할 수 있습니다", "oauthVerifySuccess": "OAuth 연결 확인 성공", - "oauthVerifyFailed": "OAuth 연결 확인 실패" + "oauthVerifyFailed": "OAuth 연결 확인 실패", + "configurationSaved": "구성 저장", + "configurationUpdated": "구성 업데이트", + "dataRefreshed": "데이터가 새로 고쳐졌습니다", + "modelscope": { + "apiKey": "API 키", + "apiKeyHelper": "ModelScope 콘솔에서 API 키를 얻으십시오", + "apiKeyPlaceholder": "ModelScope API 키를 입력하십시오", + "baseUrl": "API 주소", + "baseUrlHelper": "ModelScope API 서비스 주소", + "connected": "연결", + "connecting": "연결 ...", + "description": "ModelScope는 Alibaba Damo Academy가 시작한 Model-as-A-Service 공유 플랫폼입니다.", + "details": { + "apiConfig": "API 구성", + "mcpSync": "MCP 동기화", + "modelManagement": "모델 관리", + "operationalDescription": "ModelScope 플랫폼에서 직접 사용할 수있는 MCP 서버 동기화", + "operationalServers": "서버 작동", + "rateLimitConfig": "요율 제한 구성", + "safetySettings": "보안 설정", + "specialConfig": "특별 구성", + "syncFromModelScope": "ModelScope에서 동기화됩니다", + "title": "제공자 설정 세부 정보" + }, + "invalidKey": "잘못된 API 키", + "keyRequired": "API 키를 입력하십시오", + "mcpSync": { + "authenticationFailed": "인증 실패, API 키를 확인하십시오", + "convertingServers": "서버 구성 변환 ...", + "description": "Modescope에서 로컬 구성으로 MCP 서버를 동기화하면 일반적으로 사용되는 MCP 도구를 빠르게 추가 할 수 있습니다. 모든 서비스는 기본적으로 비활성화되며 가져온 후 수동으로 활성화 할 수 있습니다.", + "errorDetails": "오류 세부 사항", + "errors": "오류 {count}", + "fetchingServers": "MCP 서버 목록 얻기 ...", + "imported": "{count} 서비스가 가져 왔습니다", + "importingServers": "서버 구성 가져 오기 ...", + "invalidServerData": "잘못된 서버 데이터", + "noApiKey": "먼저 ModelScope API 키를 구성하십시오", + "noOperationalUrls": "사용 가능한 운영 주소가 없습니다", + "noServersFound": "사용 가능한 MCP 서비스가 없습니다", + "onlyHosted": "호스팅 서비스 만", + "pageNumber": "페이지 번호", + "pageNumberPlaceholder": "페이지 번호를 입력하십시오", + "pageSize": "페이지 당 수량", + "serverAlreadyExists": "서버가 이미 존재하고 가져 오기를 건너 뛰십시오", + "skipped": "{count} 서비스를 건너 뛰십시오", + "sync": "동기화를 시작하십시오", + "syncComplete": "동기 완성", + "syncing": "동기화...", + "title": "MCP 서비스 동기화" + }, + "name": "ModelsCope", + "networkError": "네트워크 연결 오류", + "notConnected": "연결되지 않았습니다", + "verifyFailed": "확인이 실패했습니다", + "verifySuccess": "확인이 성공적입니다" + }, + "operationFailed": "작동 실패", + "operationSuccess": "운영이 성공적입니다", + "settingsApplied": "설정이 적용됩니다" }, "knowledgeBase": { "title": "지식 베이스 설정", diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json index ee8b305da..0ce38b8e1 100644 --- a/src/renderer/src/i18n/ru-RU/settings.json +++ b/src/renderer/src/i18n/ru-RU/settings.json @@ -265,7 +265,10 @@ "failed": "Проверка не удалась", "success": "Проверка успешна", "failedDesc": "Проверка API ключа или конфигурации не удалась, проверьте настройки", - "successDesc": "API ключ и конфигурация успешно проверены, готовы к использованию" + "successDesc": "API ключ и конфигурация успешно проверены, готовы к использованию", + "connectionError": "Ошибка соединения, пожалуйста, проверьте сетевое соединение и адрес API", + "serverError": "Ошибка сервера, попробуйте еще раз позже", + "unauthorized": "Аутентификация не удалась, ключ API является недействительным или истекшим" }, "addCustomProvider": { "title": "Добавить пользовательский провайдер", @@ -374,7 +377,66 @@ "manageModels": "Управление моделями", "anthropicOAuthActiveTip": "Аутентификация OAuth включена, вы можете использовать сервисы Anthropic напрямую", "oauthVerifySuccess": "Соединение OAuth успешно проверено", - "oauthVerifyFailed": "Ошибка проверки соединения OAuth" + "oauthVerifyFailed": "Ошибка проверки соединения OAuth", + "configurationSaved": "Конфигурация сохранена", + "configurationUpdated": "Конфигурация обновлена", + "dataRefreshed": "Данные были обновлены", + "modelscope": { + "apiKey": "API -ключ", + "apiKeyHelper": "Получите ключ API в консоли моделей", + "apiKeyPlaceholder": "Пожалуйста, введите ключ API моделей моделей", + "baseUrl": "Адрес API", + "baseUrlHelper": "ModelsCope API -адрес службы службы", + "connected": "Подключенный", + "connecting": "Соединение ...", + "description": "ModelCope-это платформа для обмена моделями, запущенная Alibaba Damo Academy", + "details": { + "apiConfig": "Конфигурация API", + "mcpSync": "Синхронизация MCP", + "modelManagement": "Управление моделями", + "operationalDescription": "Синхронизировать серверы MCP, которые можно использовать непосредственно на платформе моделей", + "operationalServers": "Эксплуатация сервера", + "rateLimitConfig": "Конфигурация ограничения скорости", + "safetySettings": "Настройки безопасности", + "specialConfig": "Специальная конфигурация", + "syncFromModelScope": "Синхронизация от моделей", + "title": "Детали настроек поставщика" + }, + "invalidKey": "Неверный ключ API", + "keyRequired": "Пожалуйста, введите ключ API", + "mcpSync": { + "authenticationFailed": "Аутентификация не удалась, пожалуйста, проверьте ключ API", + "convertingServers": "Преобразование конфигурации сервера ...", + "description": "Синхронизация сервера MCP от моделей к локальным конфигурациям позволяет быстро добавлять обычно используемые инструменты MCP. Все службы отключены по умолчанию и могут быть включены вручную после импорта.", + "errorDetails": "Детали ошибки", + "errors": "Ошибка {count}", + "fetchingServers": "Получение списка серверов MCP ...", + "imported": "{count} Услуги были импортированы", + "importingServers": "Конфигурация импорта сервера ...", + "invalidServerData": "Неверные данные сервера", + "noApiKey": "Сначала настройте ключ API моделей ModelsCope", + "noOperationalUrls": "Не найдено доступного операционного адреса", + "noServersFound": "Не найдена доступной услуги MCP", + "onlyHosted": "Только услуги хостинга", + "pageNumber": "номер страницы", + "pageNumberPlaceholder": "Пожалуйста, введите номер страницы", + "pageSize": "Количество на страницу", + "serverAlreadyExists": "Сервер уже существует, пропустите импорт", + "skipped": "Skip {count} Сервисы", + "sync": "Начните синхронизировать", + "syncComplete": "Синхронное завершение", + "syncing": "Синхронизация ...", + "title": "Синхронизировать услуги MCP" + }, + "name": "Моделикоп", + "networkError": "Ошибка сетевого соединения", + "notConnected": "Не подключен", + "verifyFailed": "Проверка не удалась", + "verifySuccess": "Проверка успешна" + }, + "operationFailed": "Операция не удалась", + "operationSuccess": "Операция успешна", + "settingsApplied": "Применяются настройки" }, "knowledgeBase": { "title": "Настройки базы знаний", diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index d40810574..3906cae6e 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -247,34 +247,67 @@ "stopModel": "停止模型", "pulling": "拉取中...", "runModel": "运行模型", + "configurationUpdated": "配置已更新", + "configurationSaved": "配置已保存", + "operationSuccess": "操作成功", + "operationFailed": "操作失败", + "dataRefreshed": "数据已刷新", + "settingsApplied": "设置已应用", "toast": { "modelRunning": "模型正在运行", "modelRunningDesc": "请先停止模型 {model},然后再删除。" }, "modelscope": { + "name": "ModelScope", + "description": "ModelScope 是阿里巴巴达摩院推出的模型即服务共享平台", + "apiKey": "API 密钥", + "apiKeyPlaceholder": "请输入 ModelScope API Key", + "apiKeyHelper": "在 ModelScope 控制台获取您的 API Key", + "baseUrl": "API 地址", + "baseUrlHelper": "ModelScope API 服务地址", + "connected": "已连接", + "notConnected": "未连接", + "connecting": "连接中...", + "verifySuccess": "验证成功", + "verifyFailed": "验证失败", + "keyRequired": "请输入 API Key", + "invalidKey": "无效的 API Key", + "networkError": "网络连接错误", "mcpSync": { "title": "同步 MCP 服务", - "description": "从 ModelScope 同步 MCP 服务器到本地配置,可以快速添加常用的 MCP 工具。", + "description": "从 ModelScope 同步 MCP 服务器到本地配置,可以快速添加常用的 MCP 工具。所有服务默认禁用,导入后可手动启用。", "sync": "开始同步", "syncing": "同步中...", "pageSize": "每页数量", - "category": "分类", - "search": "搜索", - "searchPlaceholder": "搜索 MCP 服务...", - "enableByDefault": "导入时默认启用", - "overwriteExisting": "覆盖已存在的服务", + "pageNumber": "页码", + "pageNumberPlaceholder": "请输入页码", + "onlyHosted": "仅托管服务", "imported": "已导入 {count} 个服务", "skipped": "跳过 {count} 个服务", "errors": "错误 {count} 个", "errorDetails": "错误详情", "noApiKey": "请先配置 ModelScope API Key", "noServersFound": "未找到可用的 MCP 服务", - "categories": { - "communication": "通讯", - "productivity": "生产力", - "development": "开发", - "data": "数据" - } + "fetchingServers": "正在获取 MCP 服务器列表...", + "convertingServers": "正在转换服务器配置...", + "importingServers": "正在导入服务器配置...", + "syncComplete": "同步完成", + "serverAlreadyExists": "服务器已存在,跳过导入", + "noOperationalUrls": "未找到可用的运营地址", + "invalidServerData": "无效的服务器数据", + "authenticationFailed": "认证失败,请检查 API Key" + }, + "details": { + "title": "提供商设置详情", + "apiConfig": "API 配置", + "rateLimitConfig": "速率限制配置", + "modelManagement": "模型管理", + "safetySettings": "安全设置", + "specialConfig": "特殊配置", + "mcpSync": "MCP 同步", + "operationalServers": "运营服务器", + "syncFromModelScope": "从 ModelScope 同步", + "operationalDescription": "同步 ModelScope 平台上可直接使用的 MCP 服务器" } }, "dialog": { @@ -297,7 +330,10 @@ "failed": "验证失败", "success": "验证成功", "failedDesc": "API 密钥或配置验证失败,请检查配置信息", - "successDesc": "API 密钥和配置验证成功,可以正常使用" + "successDesc": "API 密钥和配置验证成功,可以正常使用", + "unauthorized": "认证失败,API Key 无效或已过期", + "serverError": "服务器错误,请稍后重试", + "connectionError": "连接错误,请检查网络连接和 API 地址" }, "addCustomProvider": { "title": "添加自定义服务商", diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json index e73cc8fbb..29c6b8d65 100644 --- a/src/renderer/src/i18n/zh-HK/settings.json +++ b/src/renderer/src/i18n/zh-HK/settings.json @@ -265,7 +265,10 @@ "failed": "驗證失敗", "success": "驗證成功", "failedDesc": "API 密鑰或配置驗證失敗,請檢查配置信息", - "successDesc": "API 密鑰和配置驗證成功,可以正常使用" + "successDesc": "API 密鑰和配置驗證成功,可以正常使用", + "connectionError": "連接錯誤,請檢查網絡連接和 API 地址", + "serverError": "服務器錯誤,請稍後重試", + "unauthorized": "認證失敗,API Key 無效或已過期" }, "addCustomProvider": { "title": "添加自定義服務商", @@ -374,7 +377,66 @@ "manageModels": "管理模型", "anthropicOAuthActiveTip": "OAuth 認證已啟用,您可以直接使用 Anthropic 服務", "oauthVerifySuccess": "OAuth 連接驗證成功", - "oauthVerifyFailed": "OAuth 連接驗證失敗" + "oauthVerifyFailed": "OAuth 連接驗證失敗", + "configurationSaved": "配置已保存", + "configurationUpdated": "配置已更新", + "dataRefreshed": "數據已刷新", + "modelscope": { + "apiKey": "API 密鑰", + "apiKeyHelper": "在 ModelScope 控制台獲取您的 API Key", + "apiKeyPlaceholder": "請輸入 ModelScope API Key", + "baseUrl": "API 地址", + "baseUrlHelper": "ModelScope API 服務地址", + "connected": "已連接", + "connecting": "連接中...", + "description": "ModelScope 是阿里巴巴達摩院推出的模型即服務共享平台", + "details": { + "apiConfig": "API 配置", + "mcpSync": "MCP 同步", + "modelManagement": "模型管理", + "operationalDescription": "同步 ModelScope 平台上可直接使用的 MCP 服務器", + "operationalServers": "運營服務器", + "rateLimitConfig": "速率限製配置", + "safetySettings": "安全設置", + "specialConfig": "特殊配置", + "syncFromModelScope": "從 ModelScope 同步", + "title": "提供商設置詳情" + }, + "invalidKey": "無效的 API Key", + "keyRequired": "請輸入 API Key", + "mcpSync": { + "authenticationFailed": "認證失敗,請檢查 API Key", + "convertingServers": "正在轉換服務器配置...", + "description": "從 ModelScope 同步 MCP 服務器到本地配置,可以快速添加常用的 MCP 工具。\n所有服務默認禁用,導入後可手動啟用。", + "errorDetails": "錯誤詳情", + "errors": "錯誤 {count} 個", + "fetchingServers": "正在獲取 MCP 服務器列表...", + "imported": "已導入 {count} 個服務", + "importingServers": "正在導入服務器配置...", + "invalidServerData": "無效的服務器數據", + "noApiKey": "請先配置 ModelScope API Key", + "noOperationalUrls": "未找到可用的運營地址", + "noServersFound": "未找到可用的 MCP 服務", + "onlyHosted": "僅託管服務", + "pageNumber": "頁碼", + "pageNumberPlaceholder": "請輸入頁碼", + "pageSize": "每頁數量", + "serverAlreadyExists": "服務器已存在,跳過導入", + "skipped": "跳過 {count} 個服務", + "sync": "開始同步", + "syncComplete": "同步完成", + "syncing": "同步中...", + "title": "同步 MCP 服務" + }, + "name": "ModelScope", + "networkError": "網絡連接錯誤", + "notConnected": "未連接", + "verifyFailed": "驗證失敗", + "verifySuccess": "驗證成功" + }, + "operationFailed": "操作失敗", + "operationSuccess": "操作成功", + "settingsApplied": "設置已應用" }, "knowledgeBase": { "fastgptTitle": "FastGPT知識庫", diff --git a/src/shared/presenter.d.ts b/src/shared/presenter.d.ts index e74ee929e..2696186d6 100644 --- a/src/shared/presenter.d.ts +++ b/src/shared/presenter.d.ts @@ -525,6 +525,22 @@ export type LLM_EMBEDDING_ATTRS = { normalized: boolean } +// Simplified ModelScope MCP sync options +export interface ModelScopeMcpSyncOptions { + filter?: { + is_hosted?: boolean + } + page_number?: number + page_size?: number +} + +// ModelScope MCP sync result interface +export interface ModelScopeMcpSyncResult { + imported: number + skipped: number + errors: string[] +} + export interface ILlmProviderPresenter { setProviders(provider: LLM_PROVIDER[]): void getProviders(): LLM_PROVIDER[] @@ -594,6 +610,10 @@ export interface ILlmProviderPresenter { lastRequestTime: number } > + syncModelScopeMcpServers( + providerId: string, + syncOptions?: ModelScopeMcpSyncOptions + ): Promise } export type CONVERSATION_SETTINGS = { systemPrompt: string From 3b2dc24342d0ed8df98a2b219f9b4c83c9a773cc Mon Sep 17 00:00:00 2001 From: yyhhyyyyyy Date: Mon, 11 Aug 2025 20:56:16 +0800 Subject: [PATCH 3/7] fix: add scrollable support to PopoverContent to prevent overflow (#720) --- src/renderer/src/components/NewThread.vue | 39 ++++++++++-------- .../src/components/ScrollablePopover.vue | 34 +++++++++++++++ src/renderer/src/components/TitleView.vue | 41 +++++++++---------- .../components/ui/popover/PopoverContent.vue | 4 +- 4 files changed, 79 insertions(+), 39 deletions(-) create mode 100644 src/renderer/src/components/ScrollablePopover.vue diff --git a/src/renderer/src/components/NewThread.vue b/src/renderer/src/components/NewThread.vue index 5121074a8..c1e55dab5 100644 --- a/src/renderer/src/components/NewThread.vue +++ b/src/renderer/src/components/NewThread.vue @@ -64,8 +64,14 @@ /> - - + + + +
@@ -104,6 +108,7 @@ import { useI18n } from 'vue-i18n' import ChatInput from './ChatInput.vue' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import ScrollablePopover from './ScrollablePopover.vue' import { Button } from '@/components/ui/button' import ModelIcon from './icons/ModelIcon.vue' import { Badge } from '@/components/ui/badge' diff --git a/src/renderer/src/components/ScrollablePopover.vue b/src/renderer/src/components/ScrollablePopover.vue new file mode 100644 index 000000000..7afa43c45 --- /dev/null +++ b/src/renderer/src/components/ScrollablePopover.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/renderer/src/components/TitleView.vue b/src/renderer/src/components/TitleView.vue index ba27aba6f..7618af8b0 100644 --- a/src/renderer/src/components/TitleView.vue +++ b/src/renderer/src/components/TitleView.vue @@ -43,30 +43,28 @@
- - + + + +
@@ -77,6 +75,7 @@ import { Icon } from '@iconify/vue' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import ScrollablePopover from './ScrollablePopover.vue' import ChatConfig from './ChatConfig.vue' import ModelSelect from './ModelSelect.vue' import ModelIcon from './icons/ModelIcon.vue' diff --git a/src/renderer/src/components/ui/popover/PopoverContent.vue b/src/renderer/src/components/ui/popover/PopoverContent.vue index 10ae0adde..6744b583b 100644 --- a/src/renderer/src/components/ui/popover/PopoverContent.vue +++ b/src/renderer/src/components/ui/popover/PopoverContent.vue @@ -10,7 +10,9 @@ defineOptions({ }) const props = withDefaults( - defineProps(), + defineProps(), { align: 'center', sideOffset: 4 From cfe0b0daa77fc8327ba44c20e01f08d2c8926309 Mon Sep 17 00:00:00 2001 From: yyhhyyyyyy Date: Mon, 11 Aug 2025 20:59:19 +0800 Subject: [PATCH 4/7] feat: implement floating chat window system with performance optimization (#724) --- src/main/events.ts | 1 + .../FloatingButtonWindow.ts | 4 + .../floatingButtonPresenter/index.ts | 122 +++++- src/main/presenter/tabPresenter.ts | 36 ++ .../windowPresenter/FloatingChatWindow.ts | 398 ++++++++++++++++++ src/main/presenter/windowPresenter/index.ts | 108 ++++- src/preload/floating-preload.ts | 11 +- src/renderer/floating/FloatingButton.vue | 23 + src/renderer/floating/env.d.ts | 1 + src/shared/presenter.d.ts | 3 + 10 files changed, 696 insertions(+), 11 deletions(-) create mode 100644 src/main/presenter/windowPresenter/FloatingChatWindow.ts diff --git a/src/main/events.ts b/src/main/events.ts index dae18ecaa..996df620e 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -177,6 +177,7 @@ export const MEETING_EVENTS = { // 悬浮按钮相关事件 export const FLOATING_BUTTON_EVENTS = { CLICKED: 'floating-button:clicked', // 悬浮按钮被点击 + RIGHT_CLICKED: 'floating-button:right-clicked', // 悬浮按钮被右键点击 VISIBILITY_CHANGED: 'floating-button:visibility-changed', // 悬浮按钮显示状态改变 POSITION_CHANGED: 'floating-button:position-changed', // 悬浮按钮位置改变 ENABLED_CHANGED: 'floating-button:enabled-changed' // 悬浮按钮启用状态改变 diff --git a/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts index 41b7d4d5b..f9cc1caac 100644 --- a/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts +++ b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts @@ -170,6 +170,10 @@ export class FloatingButtonWindow { return this.window !== null && !this.window.isDestroyed() } + public getWindow(): BrowserWindow | null { + return this.window + } + /** * 计算悬浮按钮位置 */ diff --git a/src/main/presenter/floatingButtonPresenter/index.ts b/src/main/presenter/floatingButtonPresenter/index.ts index a7b2833dd..5f67bb09f 100644 --- a/src/main/presenter/floatingButtonPresenter/index.ts +++ b/src/main/presenter/floatingButtonPresenter/index.ts @@ -1,9 +1,9 @@ import { FloatingButtonWindow } from './FloatingButtonWindow' import { FloatingButtonConfig, FloatingButtonState, DEFAULT_FLOATING_BUTTON_CONFIG } from './types' import { ConfigPresenter } from '../configPresenter' -import { ipcMain } from 'electron' +import { ipcMain, Menu, app } from 'electron' import { FLOATING_BUTTON_EVENTS } from '@/events' -import { handleShowHiddenWindow } from '@/utils' +import { presenter } from '../index' export class FloatingButtonPresenter { private floatingWindow: FloatingButtonWindow | null = null @@ -48,6 +48,7 @@ export class FloatingButtonPresenter { this.config.enabled = false ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED) + ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED) if (this.floatingWindow) { this.floatingWindow.destroy() this.floatingWindow = null @@ -107,12 +108,40 @@ export class FloatingButtonPresenter { */ private async createFloatingWindow(): Promise { ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED) + ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED) - ipcMain.on(FLOATING_BUTTON_EVENTS.CLICKED, () => { + ipcMain.on(FLOATING_BUTTON_EVENTS.CLICKED, async () => { try { - // 触发内置事件处理器 - handleShowHiddenWindow(true) - } catch {} + let floatingButtonPosition: { x: number; y: number; width: number; height: number } | null = + null + if (this.floatingWindow && this.floatingWindow.exists()) { + const buttonWindow = this.floatingWindow.getWindow() + if (buttonWindow && !buttonWindow.isDestroyed()) { + const bounds = buttonWindow.getBounds() + floatingButtonPosition = { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height + } + } + } + if (floatingButtonPosition) { + await presenter.windowPresenter.toggleFloatingChatWindow(floatingButtonPosition) + } else { + await presenter.windowPresenter.toggleFloatingChatWindow() + } + } catch (error) { + console.error('Failed to handle floating button click:', error) + } + }) + + ipcMain.on(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED, () => { + try { + this.showContextMenu() + } catch (error) { + console.error('Failed to handle floating button right click:', error) + } }) if (!this.floatingWindow) { @@ -122,5 +151,86 @@ export class FloatingButtonPresenter { // 悬浮按钮创建后立即显示 this.floatingWindow.show() + + this.preCreateFloatingChatWindow() + } + + private preCreateFloatingChatWindow(): void { + try { + presenter.windowPresenter.createFloatingChatWindow().catch((error) => { + console.error('Failed to pre-create floating chat window:', error) + }) + console.log('Started pre-creating floating chat window in background') + } catch (error) { + console.error('Error starting pre-creation of floating chat window:', error) + } + } + + private showContextMenu(): void { + const template = [ + { + label: '打开主窗口', + click: () => { + this.openMainWindow() + } + }, + { + type: 'separator' as const + }, + { + label: '退出应用', + click: () => { + this.exitApplication() + } + } + ] + + const contextMenu = Menu.buildFromTemplate(template) + + if (this.floatingWindow && this.floatingWindow.exists()) { + const buttonWindow = this.floatingWindow.getWindow() + if (buttonWindow && !buttonWindow.isDestroyed()) { + contextMenu.popup({ window: buttonWindow }) + return + } + } + + const mainWindow = presenter.windowPresenter.mainWindow + if (mainWindow) { + contextMenu.popup({ window: mainWindow }) + } else { + contextMenu.popup() + } + } + + private openMainWindow(): void { + try { + const windowPresenter = presenter.windowPresenter + if (windowPresenter) { + const mainWindow = windowPresenter.mainWindow + if (mainWindow && !mainWindow.isDestroyed()) { + if (mainWindow.isMinimized()) { + mainWindow.restore() + } + mainWindow.show() + mainWindow.focus() + console.log('Main window opened from floating button context menu') + } else { + windowPresenter.createShellWindow({ initialTab: { url: 'local://chat' } }) + console.log('Created new main window from floating button context menu') + } + } + } catch (error) { + console.error('Failed to open main window from floating button:', error) + } + } + + private exitApplication(): void { + try { + console.log('Exiting application from floating button context menu') + app.quit() + } catch (error) { + console.error('Failed to exit application from floating button:', error) + } } } diff --git a/src/main/presenter/tabPresenter.ts b/src/main/presenter/tabPresenter.ts index 27053a0e7..037b56379 100644 --- a/src/main/presenter/tabPresenter.ts +++ b/src/main/presenter/tabPresenter.ts @@ -942,4 +942,40 @@ export class TabPresenter implements ITabPresenter { } } } + + registerFloatingWindow(webContentsId: number, webContents: Electron.WebContents): void { + try { + console.log(`TabPresenter: Registering floating window as virtual tab, ID: ${webContentsId}`) + if (this.tabs.has(webContentsId)) { + console.warn(`TabPresenter: Tab ${webContentsId} already exists, skipping registration`) + return + } + const virtualView = { + webContents: webContents, + setVisible: () => {}, + setBounds: () => {}, + getBounds: () => ({ x: 0, y: 0, width: 400, height: 600 }) + } as any + this.webContentsToTabId.set(webContentsId, webContentsId) + this.tabs.set(webContentsId, virtualView) + console.log( + `TabPresenter: Virtual tab registered successfully for floating window ${webContentsId}` + ) + } catch (error) { + console.error('TabPresenter: Failed to register floating window:', error) + } + } + + unregisterFloatingWindow(webContentsId: number): void { + try { + console.log(`TabPresenter: Unregistering floating window virtual tab, ID: ${webContentsId}`) + this.webContentsToTabId.delete(webContentsId) + this.tabs.delete(webContentsId) + console.log( + `TabPresenter: Virtual tab unregistered successfully for floating window ${webContentsId}` + ) + } catch (error) { + console.error('TabPresenter: Failed to unregister floating window:', error) + } + } } diff --git a/src/main/presenter/windowPresenter/FloatingChatWindow.ts b/src/main/presenter/windowPresenter/FloatingChatWindow.ts new file mode 100644 index 000000000..b97233600 --- /dev/null +++ b/src/main/presenter/windowPresenter/FloatingChatWindow.ts @@ -0,0 +1,398 @@ +import { BrowserWindow, screen, nativeImage } from 'electron' +import path from 'path' +import logger from '../../../shared/logger' +import { platform, is } from '@electron-toolkit/utils' +import icon from '../../../../resources/icon.png?asset' +import iconWin from '../../../../resources/icon.ico?asset' +import { eventBus } from '../../eventbus' +import { TAB_EVENTS } from '../../events' +import { presenter } from '../' + +interface FloatingChatConfig { + size: { + width: number + height: number + } + minSize: { + width: number + height: number + } + opacity: number + alwaysOnTop: boolean +} + +interface FloatingButtonPosition { + x: number + y: number + width: number + height: number +} + +const DEFAULT_FLOATING_CHAT_CONFIG: FloatingChatConfig = { + size: { + width: 400, + height: 600 + }, + minSize: { + width: 350, + height: 450 + }, + opacity: 0.95, + alwaysOnTop: true +} + +export class FloatingChatWindow { + private window: BrowserWindow | null = null + private config: FloatingChatConfig + private isVisible: boolean = false + private shouldShowWhenReady: boolean = false + + constructor(config?: Partial) { + this.config = { + ...DEFAULT_FLOATING_CHAT_CONFIG, + ...config + } + } + + public async create(floatingButtonPosition?: FloatingButtonPosition): Promise { + if (this.window) { + return + } + + try { + const position = this.calculatePosition(floatingButtonPosition) + const iconFile = nativeImage.createFromPath(process.platform === 'win32' ? iconWin : icon) + const isDev = is.dev + + this.window = new BrowserWindow({ + width: this.config.size.width, + height: this.config.size.height, + minWidth: this.config.minSize.width, + minHeight: this.config.minSize.height, + x: position.x, + y: position.y, + frame: false, + transparent: true, + alwaysOnTop: this.config.alwaysOnTop, + skipTaskbar: true, + resizable: true, + minimizable: false, + maximizable: false, + closable: true, + show: false, + movable: true, + autoHideMenuBar: true, + icon: iconFile, + vibrancy: platform.isMacOS ? 'under-window' : undefined, + visualEffectState: platform.isMacOS ? 'followWindow' : undefined, + backgroundMaterial: platform.isWindows ? 'mica' : undefined, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, '../preload/index.mjs'), + webSecurity: false, + devTools: isDev, + sandbox: false + } + }) + + this.window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }) + this.window.setAlwaysOnTop(true, 'floating') + this.window.setOpacity(this.config.opacity) + this.setupWindowEvents() + this.registerVirtualTab() + + logger.info('FloatingChatWindow created successfully') + + this.loadPageContent() + .then(() => logger.info('FloatingChatWindow page content loaded')) + .catch((error) => logger.error('Failed to load FloatingChatWindow page content:', error)) + } catch (error) { + logger.error('Failed to create FloatingChatWindow:', error) + throw error + } + } + + public show(floatingButtonPosition?: FloatingButtonPosition): void { + if (!this.window) { + return + } + + if (floatingButtonPosition) { + const position = this.calculatePosition(floatingButtonPosition) + this.window.setPosition(position.x, position.y) + } + if (!this.window.isVisible()) { + if (this.window.webContents.isLoading() === false) { + this.window.show() + this.window.focus() + this.refreshWindowData() + } else { + this.window.show() + this.window.focus() + this.shouldShowWhenReady = true + this.window.webContents.once('did-finish-load', () => { + if (this.shouldShowWhenReady) { + this.refreshWindowData() + this.shouldShowWhenReady = false + } + }) + } + } else { + this.window.show() + this.window.focus() + this.refreshWindowData() + } + this.isVisible = true + logger.debug('FloatingChatWindow shown') + } + + public hide(): void { + if (!this.window) { + return + } + + this.window.hide() + this.isVisible = false + logger.debug('FloatingChatWindow hidden') + } + + public toggle(floatingButtonPosition?: FloatingButtonPosition): void { + if (this.isVisible) { + this.hide() + } else { + this.show(floatingButtonPosition) + } + } + + public destroy(): void { + if (this.window) { + this.unregisterVirtualTab() + try { + if (!this.window.isDestroyed()) { + this.window.destroy() + } + } catch (error) { + logger.error('Error destroying FloatingChatWindow:', error) + } + this.window = null + this.isVisible = false + logger.debug('FloatingChatWindow destroyed') + } + } + + public isShowing(): boolean { + return this.window !== null && !this.window.isDestroyed() && this.isVisible + } + + public getWindow(): BrowserWindow | null { + return this.window + } + + private refreshWindowData(): void { + if (this.window && !this.window.isDestroyed()) { + logger.debug('Refreshing floating window data') + setTimeout(() => { + if (this.window && !this.window.isDestroyed()) { + eventBus.sendToMain(TAB_EVENTS.RENDERER_TAB_READY, this.window.webContents.id) + } + }, 100) + } + } + + private registerVirtualTab(): void { + if (!this.window || this.window.isDestroyed()) { + return + } + + try { + const tabPresenter = presenter.tabPresenter + if (tabPresenter) { + const webContentsId = this.window.webContents.id + logger.info(`Registering virtual tab for floating window, WebContents ID: ${webContentsId}`) + tabPresenter.registerFloatingWindow(webContentsId, this.window.webContents) + } + } catch (error) { + logger.error('Failed to register virtual tab for floating window:', error) + } + } + + private unregisterVirtualTab(): void { + if (!this.window) { + return + } + + try { + const tabPresenter = presenter.tabPresenter + if (tabPresenter) { + const webContentsId = this.window.webContents.id + logger.info( + `Unregistering virtual tab for floating window, WebContents ID: ${webContentsId}` + ) + tabPresenter.unregisterFloatingWindow(webContentsId) + } + } catch (error) { + logger.error('Failed to unregister virtual tab for floating window:', error) + } + } + private calculatePosition(floatingButtonPosition?: FloatingButtonPosition): { + x: number + y: number + } { + const primaryDisplay = screen.getPrimaryDisplay() + const { workArea } = primaryDisplay + let x: number, y: number + + if (!floatingButtonPosition) { + x = workArea.x + workArea.width - this.config.size.width - 20 + y = workArea.y + workArea.height - this.config.size.height - 20 + return { x, y } + } + + const buttonX = floatingButtonPosition.x + const buttonY = floatingButtonPosition.y + const buttonWidth = floatingButtonPosition.width + const buttonHeight = floatingButtonPosition.height + const windowWidth = this.config.size.width + const windowHeight = this.config.size.height + const gap = 15 + const buttonCenterX = buttonX + buttonWidth / 2 + const buttonCenterY = buttonY + buttonHeight / 2 + const screenCenterX = workArea.x + workArea.width / 2 + const screenCenterY = workArea.y + workArea.height / 2 + + let positions: Array<{ x: number; y: number; priority: number }> = [] + if (buttonX + buttonWidth + gap + windowWidth <= workArea.x + workArea.width) { + positions.push({ + x: buttonX + buttonWidth + gap, + y: Math.max( + workArea.y, + Math.min( + buttonY + (buttonHeight - windowHeight) / 2, + workArea.y + workArea.height - windowHeight + ) + ), + priority: buttonCenterX < screenCenterX ? 1 : 3 + }) + } + + if (buttonX - gap - windowWidth >= workArea.x) { + positions.push({ + x: buttonX - gap - windowWidth, + y: Math.max( + workArea.y, + Math.min( + buttonY + (buttonHeight - windowHeight) / 2, + workArea.y + workArea.height - windowHeight + ) + ), + priority: buttonCenterX >= screenCenterX ? 1 : 3 + }) + } + + if (buttonY + buttonHeight + gap + windowHeight <= workArea.y + workArea.height) { + positions.push({ + x: Math.max( + workArea.x, + Math.min( + buttonX + (buttonWidth - windowWidth) / 2, + workArea.x + workArea.width - windowWidth + ) + ), + y: buttonY + buttonHeight + gap, + priority: buttonCenterY < screenCenterY ? 2 : 4 + }) + } + + if (buttonY - gap - windowHeight >= workArea.y) { + positions.push({ + x: Math.max( + workArea.x, + Math.min( + buttonX + (buttonWidth - windowWidth) / 2, + workArea.x + workArea.width - windowWidth + ) + ), + y: buttonY - gap - windowHeight, + priority: buttonCenterY >= screenCenterY ? 2 : 4 + }) + } + + if (positions.length === 0) { + x = workArea.x + workArea.width - windowWidth - 20 + y = workArea.y + workArea.height - windowHeight - 20 + } else { + positions.sort((a, b) => a.priority - b.priority) + x = positions[0].x + y = positions[0].y + } + x = Math.max(workArea.x + 10, Math.min(x, workArea.x + workArea.width - windowWidth - 10)) + y = Math.max(workArea.y + 10, Math.min(y, workArea.y + workArea.height - windowHeight - 10)) + return { x, y } + } + + private async loadPageContent(): Promise { + if (!this.window || this.window.isDestroyed()) { + throw new Error('Window is not available for page loading') + } + + const isDev = is.dev + if (isDev) { + await this.window.loadURL('http://localhost:5173/') + } else { + await this.window.loadFile(path.join(__dirname, '../renderer/index.html')) + } + + this.window.webContents.once('did-finish-load', () => { + logger.info('FloatingChatWindow did-finish-load, requesting fresh data') + setTimeout(async () => { + if (this.window && !this.window.isDestroyed()) { + logger.info(`Broadcasting thread list update for floating window`) + eventBus.sendToMain(TAB_EVENTS.RENDERER_TAB_READY, this.window.webContents.id) + } + }, 300) + }) + } + + private setupWindowEvents(): void { + if (!this.window) { + return + } + + this.window.on('ready-to-show', () => { + if (this.window && !this.window.isDestroyed()) { + if (this.shouldShowWhenReady) { + this.window.show() + this.window.focus() + this.shouldShowWhenReady = false + this.refreshWindowData() + } + } + }) + + this.window.on('close', (event) => { + const windowPresenter = presenter.windowPresenter + const isAppQuitting = windowPresenter?.isApplicationQuitting() || false + if (isAppQuitting) { + logger.info('App is quitting, allowing FloatingChatWindow to close normally') + return + } + event.preventDefault() + this.hide() + logger.debug('FloatingChatWindow close prevented, window hidden instead') + }) + + this.window.on('closed', () => { + this.window = null + this.isVisible = false + }) + + this.window.on('show', () => { + this.isVisible = true + }) + + this.window.on('hide', () => { + this.isVisible = false + }) + } +} diff --git a/src/main/presenter/windowPresenter/index.ts b/src/main/presenter/windowPresenter/index.ts index c35c25262..a05201bf7 100644 --- a/src/main/presenter/windowPresenter/index.ts +++ b/src/main/presenter/windowPresenter/index.ts @@ -13,6 +13,7 @@ import windowStateManager from 'electron-window-state' // 窗口状态管理器 import { SHORTCUT_EVENTS } from '@/events' // 快捷键事件常量 // TrayPresenter 在 main/index.ts 中全局管理,本 Presenter 不负责其生命周期 import { TabPresenter } from '../tabPresenter' // TabPresenter 类型 +import { FloatingChatWindow } from './FloatingChatWindow' // 悬浮对话窗口 /** * 窗口 Presenter,负责管理所有 BrowserWindow 实例及其生命周期。 @@ -38,6 +39,7 @@ export class WindowPresenter implements IWindowPresenter { hasInitialFocus: boolean } >() + private floatingChatWindow: FloatingChatWindow | null = null constructor(configPresenter: ConfigPresenter) { this.windows = new Map() @@ -57,6 +59,7 @@ export class WindowPresenter implements IWindowPresenter { app.on('before-quit', () => { console.log('App is quitting, setting isQuitting flag.') this.isQuitting = true + this.destroyFloatingChatWindow() }) // 监听快捷键事件:创建新窗口 @@ -171,16 +174,26 @@ export class WindowPresenter implements IWindowPresenter { * @param filePath 文件路径。 */ previewFile(filePath: string): void { - const window = this.mainWindow - if (window) { + let targetWindow = this.getFocusedWindow() + if (!targetWindow && this.floatingChatWindow && this.floatingChatWindow.isShowing()) { + const floatingWindow = this.floatingChatWindow.getWindow() + if (floatingWindow) { + targetWindow = floatingWindow + } + } + if (!targetWindow) { + targetWindow = this.mainWindow + } + + if (targetWindow && !targetWindow.isDestroyed()) { console.log(`Previewing file: ${filePath}`) if (process.platform === 'darwin') { - window.previewFile(filePath) + targetWindow.previewFile(filePath) } else { shell.openPath(filePath) // 使用系统默认应用打开 } } else { - console.warn('Cannot preview file, no valid main window found.') + console.warn('Cannot preview file, no valid window found.') } } @@ -487,6 +500,17 @@ export class WindowPresenter implements IWindowPresenter { console.warn(`Skipping sending message "${channel}" to destroyed window ${window.id}.`) } } + + if (this.floatingChatWindow && this.floatingChatWindow.isShowing()) { + const floatingWindow = this.floatingChatWindow.getWindow() + if (floatingWindow && !floatingWindow.isDestroyed()) { + try { + floatingWindow.webContents.send(channel, ...args) + } catch (error) { + console.error(`Error sending message "${channel}" to floating chat window:`, error) + } + } + } } /** @@ -1078,4 +1102,80 @@ export class WindowPresenter implements IWindowPresenter { return false // 过程中发生错误 } } + + public async createFloatingChatWindow(): Promise { + if (this.floatingChatWindow) { + console.log('FloatingChatWindow already exists') + return + } + + try { + this.floatingChatWindow = new FloatingChatWindow() + await this.floatingChatWindow.create() + console.log('FloatingChatWindow created successfully') + } catch (error) { + console.error('Failed to create FloatingChatWindow:', error) + this.floatingChatWindow = null + throw error + } + } + + public async showFloatingChatWindow(floatingButtonPosition?: { + x: number + y: number + width: number + height: number + }): Promise { + if (!this.floatingChatWindow) { + await this.createFloatingChatWindow() + } + + if (this.floatingChatWindow) { + this.floatingChatWindow.show(floatingButtonPosition) + console.log('FloatingChatWindow shown') + } + } + + public hideFloatingChatWindow(): void { + if (this.floatingChatWindow) { + this.floatingChatWindow.hide() + console.log('FloatingChatWindow hidden') + } + } + + public async toggleFloatingChatWindow(floatingButtonPosition?: { + x: number + y: number + width: number + height: number + }): Promise { + if (!this.floatingChatWindow) { + await this.createFloatingChatWindow() + } + + if (this.floatingChatWindow) { + this.floatingChatWindow.toggle(floatingButtonPosition) + console.log('FloatingChatWindow toggled') + } + } + + public destroyFloatingChatWindow(): void { + if (this.floatingChatWindow) { + this.floatingChatWindow.destroy() + this.floatingChatWindow = null + console.log('FloatingChatWindow destroyed') + } + } + + public isFloatingChatWindowVisible(): boolean { + return this.floatingChatWindow?.isShowing() || false + } + + public getFloatingChatWindow(): FloatingChatWindow | null { + return this.floatingChatWindow + } + + public isApplicationQuitting(): boolean { + return this.isQuitting + } } diff --git a/src/preload/floating-preload.ts b/src/preload/floating-preload.ts index 4639912b8..2839f9a9b 100644 --- a/src/preload/floating-preload.ts +++ b/src/preload/floating-preload.ts @@ -2,7 +2,8 @@ import { contextBridge, ipcRenderer } from 'electron' // 直接定义事件常量,避免路径解析问题 const FLOATING_BUTTON_EVENTS = { - CLICKED: 'floating-button:clicked' + CLICKED: 'floating-button:clicked', + RIGHT_CLICKED: 'floating-button:right-clicked' } as const // 定义悬浮按钮的 API @@ -16,6 +17,14 @@ const floatingButtonAPI = { } }, + onRightClick: () => { + try { + ipcRenderer.send(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED) + } catch (error) { + console.error('FloatingPreload: Error sending right click IPC message:', error) + } + }, + // 监听来自主进程的事件 onConfigUpdate: (callback: (config: any) => void) => { ipcRenderer.on('floating-button-config-update', (_event, config) => { diff --git a/src/renderer/floating/FloatingButton.vue b/src/renderer/floating/FloatingButton.vue index 0806e973c..db4b0f9f7 100644 --- a/src/renderer/floating/FloatingButton.vue +++ b/src/renderer/floating/FloatingButton.vue @@ -7,6 +7,7 @@ class="w-15 h-15 rounded-full border-2 border-white/30 flex items-center justify-center cursor-pointer transition-all duration-300 relative overflow-hidden select-none floating-button no-drag" :class="{ 'floating-button-pulse': isPulsing }" @click="handleClick" + @contextmenu="handleRightClick" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave" > @@ -48,6 +49,28 @@ const handleClick = () => { } } +const handleRightClick = (event: MouseEvent) => { + event.preventDefault() + if (floatingButton.value) { + floatingButton.value.style.transform = 'scale(0.9)' + setTimeout(() => { + if (floatingButton.value) { + floatingButton.value.style.transform = '' + } + }, 150) + } + + if (window.floatingButtonAPI) { + try { + window.floatingButtonAPI.onRightClick() + } catch (error) { + console.error('=== FloatingButton: Error calling onRightClick API ===:', error) + } + } else { + console.error('=== FloatingButton: floatingButtonAPI not available ===') + } +} + // 鼠标事件处理 const handleMouseEnter = () => { isPulsing.value = false diff --git a/src/renderer/floating/env.d.ts b/src/renderer/floating/env.d.ts index 6f624ddac..58ffc5d98 100644 --- a/src/renderer/floating/env.d.ts +++ b/src/renderer/floating/env.d.ts @@ -11,6 +11,7 @@ declare global { interface Window { floatingButtonAPI: { onClick: () => void + onRightClick: () => void onConfigUpdate: (callback: (config: any) => void) => void removeAllListeners: () => void } diff --git a/src/shared/presenter.d.ts b/src/shared/presenter.d.ts index 2696186d6..33edd0b75 100644 --- a/src/shared/presenter.d.ts +++ b/src/shared/presenter.d.ts @@ -178,6 +178,7 @@ export interface IWindowPresenter { sendToWindow(windowId: number, channel: string, ...args: unknown[]): boolean sendToDefaultTab(channel: string, switchToTarget?: boolean, ...args: unknown[]): Promise closeWindow(windowId: number, forceClose?: boolean): Promise + isApplicationQuitting(): boolean } export interface ITabPresenter { @@ -214,6 +215,8 @@ export interface ITabPresenter { onRendererTabReady(tabId: number): Promise onRendererTabActivated(threadId: string): Promise isLastTabInWindow(tabId: number): Promise + registerFloatingWindow(webContentsId: number, webContents: Electron.WebContents): void + unregisterFloatingWindow(webContentsId: number): void resetTabToBlank(tabId: number): Promise } From efa1c4f9d1423313cd0562044478b7165526c4f2 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Mon, 11 Aug 2025 21:16:21 +0800 Subject: [PATCH 5/7] chore: i18n and format --- .../configPresenter/mcpConfHelper.ts | 2 +- .../providers/modelscopeProvider.ts | 3 +- .../src/components/icons/ModelIcon.vue | 2 +- .../settings/ModelProviderSettingsDetail.vue | 5 +- .../components/settings/ModelScopeMcpSync.vue | 50 +++++++------- src/renderer/src/i18n/en-US/settings.json | 3 +- src/renderer/src/i18n/zh-TW/settings.json | 66 ++++++++++++++++++- 7 files changed, 93 insertions(+), 38 deletions(-) diff --git a/src/main/presenter/configPresenter/mcpConfHelper.ts b/src/main/presenter/configPresenter/mcpConfHelper.ts index a9ba84654..06399b9ae 100644 --- a/src/main/presenter/configPresenter/mcpConfHelper.ts +++ b/src/main/presenter/configPresenter/mcpConfHelper.ts @@ -755,7 +755,7 @@ export class McpConfHelper { ): string { // Clean up package name to create a suitable server name let baseName = packageName - .replace(/[@\/]/g, '-') + .replace(/[@/]/g, '-') .replace(/[^a-zA-Z0-9-_]/g, '') .toLowerCase() diff --git a/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts b/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts index 1a4468cd7..bf2d78c1d 100644 --- a/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/modelscopeProvider.ts @@ -159,7 +159,8 @@ export class ModelscopeProvider extends OpenAICompatibleProvider { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.provider.apiKey}` - } + }, + signal: AbortSignal.timeout(30000) // 30 second timeout }) // Handle authentication errors diff --git a/src/renderer/src/components/icons/ModelIcon.vue b/src/renderer/src/components/icons/ModelIcon.vue index 2ce4b3d31..2ef7c4db1 100644 --- a/src/renderer/src/components/icons/ModelIcon.vue +++ b/src/renderer/src/components/icons/ModelIcon.vue @@ -60,7 +60,7 @@ import _302aiIcon from '@/assets/llm-icons/302ai.svg?url' import modelscopeColorIcon from '@/assets/llm-icons/modelscope-color.svg?url' // 导入所有图标 const icons = { - 'modelscope': modelscopeColorIcon, + modelscope: modelscopeColorIcon, '302ai': _302aiIcon, aihubmix: aihubmixColorIcon, dashscope: dashscopeColorIcon, diff --git a/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue b/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue index b55611161..4d38c2751 100644 --- a/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue +++ b/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue @@ -34,10 +34,7 @@ - +
-
@@ -81,13 +69,18 @@ {{ t('settings.provider.modelscope.mcpSync.skipped', { count: syncResult.skipped }) }} - {{ t('settings.provider.modelscope.mcpSync.errors', { count: syncResult.errors.length }) }} + {{ + t('settings.provider.modelscope.mcpSync.errors', { count: syncResult.errors.length }) + }}
-
+

{{ errorMessage }}

@@ -97,7 +90,11 @@ {{ t('settings.provider.modelscope.mcpSync.errorDetails') }}
-
+
{{ error }}
@@ -151,10 +148,7 @@ const handleSync = async () => { try { // 调用简化的同步API,所有的格式转换和导入都在服务端处理 - const result = await llmP.syncModelScopeMcpServers( - props.provider.id, - syncOptions - ) + const result = await llmP.syncModelScopeMcpServers(props.provider.id, syncOptions) syncResult.value = result diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index 0906fdb97..791b9f7a3 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -376,7 +376,8 @@ "pageNumber": "page number", "pageNumberPlaceholder": "Please enter the page number", "serverAlreadyExists": "The server already exists, skip import", - "syncComplete": "Synchronous completion" + "syncComplete": "Synchronous completion", + "invalidServerData": "Invalid server data" }, "apiKey": "API Key", "apiKeyHelper": "Get your API Key in the ModelScope console", diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json index 728529296..1ed1f5cde 100644 --- a/src/renderer/src/i18n/zh-TW/settings.json +++ b/src/renderer/src/i18n/zh-TW/settings.json @@ -266,7 +266,10 @@ "failed": "驗證失敗", "success": "驗證成功", "failedDesc": "API 金鑰或設定驗證失敗,請檢查設定資訊", - "successDesc": "API 金鑰和設定驗證成功,可以正常使用" + "successDesc": "API 金鑰和設定驗證成功,可以正常使用", + "connectionError": "連接錯誤,請檢查網絡連接和 API 地址", + "serverError": "服務器錯誤,請稍後重試", + "unauthorized": "認證失敗,API Key 無效或已過期" }, "addCustomProvider": { "title": "新增自訂服務提供者", @@ -374,7 +377,66 @@ "manageModels": "管理模型", "anthropicOAuthActiveTip": "OAuth 認證已啟用,您可以直接使用 Anthropic 服務", "oauthVerifySuccess": "OAuth 連接驗證成功", - "oauthVerifyFailed": "OAuth 連接驗證失敗" + "oauthVerifyFailed": "OAuth 連接驗證失敗", + "modelscope": { + "mcpSync": { + "invalidServerData": "無效的服務器數據", + "authenticationFailed": "認證失敗,請檢查 API Key", + "convertingServers": "正在轉換服務器配置...", + "description": "從 ModelScope 同步 MCP 服務器到本地配置,可以快速添加常用的 MCP 工具。\n所有服務默認禁用,導入後可手動啟用。", + "errorDetails": "錯誤詳情", + "errors": "錯誤 {count} 個", + "fetchingServers": "正在獲取 MCP 服務器列表...", + "imported": "已導入 {count} 個服務", + "importingServers": "正在導入服務器配置...", + "noApiKey": "請先配置 ModelScope API Key", + "noOperationalUrls": "未找到可用的運營地址", + "noServersFound": "未找到可用的 MCP 服務", + "onlyHosted": "僅託管服務", + "pageNumber": "頁碼", + "pageNumberPlaceholder": "請輸入頁碼", + "pageSize": "每頁數量", + "serverAlreadyExists": "服務器已存在,跳過導入", + "skipped": "跳過 {count} 個服務", + "sync": "開始同步", + "syncComplete": "同步完成", + "syncing": "同步中...", + "title": "同步 MCP 服務" + }, + "apiKey": "API 密鑰", + "apiKeyHelper": "在 ModelScope 控制台獲取您的 API Key", + "apiKeyPlaceholder": "請輸入 ModelScope API Key", + "baseUrl": "API 地址", + "baseUrlHelper": "ModelScope API 服務地址", + "connected": "已連接", + "connecting": "連接中...", + "description": "ModelScope 是阿里巴巴達摩院推出的模型即服務共享平台", + "details": { + "apiConfig": "API 配置", + "mcpSync": "MCP 同步", + "modelManagement": "模型管理", + "operationalDescription": "同步 ModelScope 平台上可直接使用的 MCP 服務器", + "operationalServers": "運營服務器", + "rateLimitConfig": "速率限製配置", + "safetySettings": "安全設置", + "specialConfig": "特殊配置", + "syncFromModelScope": "從 ModelScope 同步", + "title": "提供商設置詳情" + }, + "invalidKey": "無效的 API Key", + "keyRequired": "請輸入 API Key", + "name": "ModelScope", + "networkError": "網絡連接錯誤", + "notConnected": "未連接", + "verifyFailed": "驗證失敗", + "verifySuccess": "驗證成功" + }, + "configurationSaved": "配置已保存", + "configurationUpdated": "配置已更新", + "dataRefreshed": "數據已刷新", + "operationFailed": "操作失敗", + "operationSuccess": "操作成功", + "settingsApplied": "設置已應用" }, "knowledgeBase": { "title": "知識庫設置", From 5d349d5f8d6dea7ee8644faf288b6935b8976d8b Mon Sep 17 00:00:00 2001 From: zerob13 Date: Mon, 11 Aug 2025 21:30:42 +0800 Subject: [PATCH 6/7] feat: better style --- .../components/settings/ModelScopeMcpSync.vue | 133 +++++++++++------- 1 file changed, 81 insertions(+), 52 deletions(-) diff --git a/src/renderer/src/components/settings/ModelScopeMcpSync.vue b/src/renderer/src/components/settings/ModelScopeMcpSync.vue index 46a3ce33d..33cf626db 100644 --- a/src/renderer/src/components/settings/ModelScopeMcpSync.vue +++ b/src/renderer/src/components/settings/ModelScopeMcpSync.vue @@ -1,24 +1,30 @@