From 6b0c96545b3e685c94099d0e83abc75608d26a66 Mon Sep 17 00:00:00 2001 From: randy1568 Date: Mon, 24 Nov 2025 17:57:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(A2A):=20=E6=89=80=E6=9C=89=E8=B7=9FA2A?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E7=9A=84=E9=85=8D=E7=BD=AE=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E8=BD=AC=E5=88=B0A2APresenter,=E8=AE=BE?= =?UTF-8?q?=E5=AE=9A=E5=90=8C=E4=B8=80=E4=B8=AAURL=E7=9A=84a2a=E5=8F=AA?= =?UTF-8?q?=E8=83=BD=E9=85=8D=E7=BD=AE=E4=B8=80=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/presenter/A2APresenter/index.ts | 49 ++++--- .../presenter/A2APresenter/serverManager.ts | 80 ++++++++++- .../configPresenter/agentConfHelper.ts | 134 +++++------------- src/main/presenter/configPresenter/index.ts | 14 +- src/main/presenter/index.ts | 2 +- .../agent-config/AgentImportDialog.vue | 98 +++++-------- .../types/presenters/legacy.presenters.d.ts | 19 ++- 7 files changed, 200 insertions(+), 196 deletions(-) diff --git a/src/main/presenter/A2APresenter/index.ts b/src/main/presenter/A2APresenter/index.ts index cd86cbb..23ea264 100644 --- a/src/main/presenter/A2APresenter/index.ts +++ b/src/main/presenter/A2APresenter/index.ts @@ -1,14 +1,21 @@ import type { A2AClientData, + A2AerrorResponse, A2AMessageSendParams, A2AServerResponse, AgentCardData, - IA2APresenter + IA2APresenter, + IConfigPresenter } from '@shared/presenter' import { ServerManager } from './serverManager' export class A2APresenter implements IA2APresenter { - private readonly manager = new ServerManager() + private readonly manager: ServerManager + + constructor(configPresenter: IConfigPresenter) { + this.manager = new ServerManager(configPresenter) + } + async getA2AClient(serverURL: string): Promise { const a2aClientAction = await this.manager.getA2AClient(serverURL) if (!a2aClientAction) { @@ -29,31 +36,37 @@ export class A2APresenter implements IA2APresenter { url: agentCard.url, streamingSupported: agentCard.capabilities?.streaming === true ? true : false, skills: getAgentCardData(), + version: agentCard.version, + provider: { + organization: agentCard.provider?.organization || '', + url: agentCard.provider?.url || '' + }, iconUrl: agentCard.iconUrl } } } - async addA2AServer(serverURL: string): Promise { + async addA2AServer(serverURL: string): Promise { try { - const agentCard = await this.manager.addA2AServer(serverURL) - const getAgentCardData = () => { - return agentCard.skills.map((agentSkill) => ({ - name: agentSkill.name, - description: agentSkill.description - })) - } + return await this.manager.addA2AServer(serverURL) + } catch (error) { + console.error(`[A2A] Failed to add server ${serverURL}`) return { - name: agentCard.name, - description: agentCard.description, - url: agentCard.url, - streamingSupported: agentCard.capabilities?.streaming === true ? true : false, - skills: getAgentCardData(), - iconUrl: agentCard.iconUrl + errorCode: '-1', + errorMsg: (error as Error).message } + } + } + + async fetchAgentCard(serverURL: string): Promise { + try { + return await this.manager.fetchAgentCard(serverURL) } catch (error) { - console.error(`[A2A] Failed to add server ${serverURL}`) - return + console.error(`${(error as Error).message}`) + return { + errorCode: '-1', + errorMsg: (error as Error).message + } } } diff --git a/src/main/presenter/A2APresenter/serverManager.ts b/src/main/presenter/A2APresenter/serverManager.ts index 6224aae..2cd3cc4 100644 --- a/src/main/presenter/A2APresenter/serverManager.ts +++ b/src/main/presenter/A2APresenter/serverManager.ts @@ -1,20 +1,49 @@ import { AgentCard } from '@a2a-js/sdk' import { A2AClientAction } from './A2AClientAction' +import { AgentCardData, IConfigPresenter } from '@shared/presenter' export class ServerManager { //key:serverURL value:A2AClientAction private clientPool: Map + private configPresenter: IConfigPresenter - constructor() { + constructor(configPresenter: IConfigPresenter) { this.clientPool = new Map() + this.configPresenter = configPresenter + void this.loadExistingA2AAgents() } - async addA2AServer(serverURL: string): Promise { + /** + * 从配置中加载已有的 A2A agent 并对每个 agent 执行初始化处理(非阻塞) + */ + private async loadExistingA2AAgents(): Promise { + try { + const { agents } = await this.configPresenter.exportAgents('A2A') + // agents 可能为空 + if (!agents || agents.length === 0) return + + for (const agent of agents) { + try { + const normalizedURL = this.normalLizeServerURL(agent.a2aURL || '') + if (!normalizedURL) { + throw new Error(`[A2A] Invalid server URL: ${agent.a2aURL || ''}`) + } + const client = new A2AClientAction(normalizedURL) + this.clientPool.set(normalizedURL, client) + } catch (e) { + console.warn('[A2A] Failed to init client for agent', agent.name) + } + } + } catch (error) { + console.error('[A2A] Failed to load existing A2A agents:', error) + } + } + + async addA2AServer(serverURL: string): Promise { const normalizedURL = this.normalLizeServerURL(serverURL) if (!normalizedURL) { throw new Error(`[A2A] Invalid server URL: ${serverURL}`) } - if (this.clientPool.has(normalizedURL)) { throw new Error(`[A2A] ${serverURL} has been added`) } @@ -25,7 +54,25 @@ export class ServerManager { }) this.clientPool.set(normalizedURL, client) - return agentCard + const agentCardData = this.agentCardDataTransfor(agentCard) + await this.configPresenter.importAgentFromA2AData(agentCardData) + return agentCardData + } + + async fetchAgentCard(serverURL: string): Promise { + const normalizedURL = this.normalLizeServerURL(serverURL) + if (!normalizedURL) { + throw new Error(`[A2A] Invalid server URL: ${serverURL}`) + } + if (this.clientPool.has(normalizedURL)) { + throw new Error(`[A2A] ${serverURL} has been added`) + } + + const client = new A2AClientAction(normalizedURL) + const agentCard = await client.getAgentCard().catch((error: Error) => { + throw new Error(error.message) + }) + return this.agentCardDataTransfor(agentCard) } async removeA2AServer(serverURL: string): Promise { @@ -78,6 +125,29 @@ export class ServerManager { if (trimmed.endsWith(agentCardSuffix)) { trimmed = trimmed.slice(0, -agentCardSuffix.length) } - return trimmed + return trimmed.replace(/\/+$/, '') + } + + private agentCardDataTransfor(agentCard: AgentCard): AgentCardData { + const getAgentCardData = () => { + return agentCard.skills.map((agentSkill) => ({ + name: agentSkill.name, + description: agentSkill.description + })) + } + const agentCardData: AgentCardData = { + name: agentCard.name, + description: agentCard.description, + url: agentCard.url, + streamingSupported: agentCard.capabilities?.streaming === true ? true : false, + skills: getAgentCardData(), + version: agentCard.version, + provider: { + organization: agentCard.provider?.organization || '', + url: agentCard.provider?.url || '' + }, + iconUrl: agentCard.iconUrl + } + return agentCardData } } diff --git a/src/main/presenter/configPresenter/agentConfHelper.ts b/src/main/presenter/configPresenter/agentConfHelper.ts index e54f190..24c543a 100644 --- a/src/main/presenter/configPresenter/agentConfHelper.ts +++ b/src/main/presenter/configPresenter/agentConfHelper.ts @@ -1,5 +1,5 @@ import { eventBus, SendTarget } from '@/eventbus' -import { Agent } from '@shared/presenter' +import { Agent, AgentCardData } from '@shared/presenter' import { AGENT_EVENTS } from '@/events' import ElectronStore from 'electron-store' import fs from 'fs' @@ -82,6 +82,7 @@ export class AgentConfHelper { description: '通用AI助手', icon: 'lucide:bot', category: '', + type: 'local', installed: true, version: '1.0.0', provider: { @@ -98,6 +99,7 @@ export class AgentConfHelper { description: '智能代码生成和审查助手', icon: 'lucide:code', category: 'development', + type: 'local', installed: false, version: '1.0.0', provider: { @@ -455,7 +457,7 @@ export class AgentConfHelper { /** * 导出智能体数据 */ - async exportAgents(): Promise<{ + async exportAgents(typeFilterCondition?: string): Promise<{ agents: Agent[] installedAgents: string[] lastUpdateTime: number @@ -464,6 +466,14 @@ export class AgentConfHelper { const agents = await this.getAgents() const installedAgents = this.store.get('installedAgents') || [] const lastUpdateTime = this.store.get('lastUpdateTime') || Date.now() + if (typeFilterCondition) { + const filteredAgents = agents.filter((agent) => agent.type === typeFilterCondition) + return { + agents: filteredAgents, + installedAgents, + lastUpdateTime + } + } return { agents, @@ -485,56 +495,26 @@ export class AgentConfHelper { /** * 从URL导入智能体配置 */ - async importAgentFromUrl(url: string): Promise { + async importAgentFromA2AData(agentCardData: AgentCardData): Promise { try { - console.log('Importing agent from URL:', url) - - // 验证URL格式 - if (!url || !url.startsWith('http')) { - throw new Error('Invalid URL format') - } - - // 确保URL以 .well-known/agent-card.json 结尾 - let importUrl = url - if (!url.endsWith('.well-known/agent-card.json')) { - // 如果URL不以标准路径结尾,尝试添加标准路径 - const baseUrl = url.endsWith('/') ? url.slice(0, -1) : url - importUrl = `${baseUrl}/.well-known/agent-card.json` - } - - console.log('Fetching agent card from:', importUrl) - - // 使用fetch获取agent-card.json - const response = await fetch(importUrl) - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const agentCard = await response.json() - console.log('Received agent card:', agentCard) - - // 验证agent-card.json格式 - if (!agentCard.name || !agentCard.description) { - throw new Error('Invalid agent card format: missing required fields') - } - // 转换agent-card.json为Agent格式 const agent: Agent = { - id: this.generateAgentId(agentCard.name), - name: agentCard.name, - description: agentCard.description, - icon: agentCard.icon || 'lucide:bot', + id: this.generateAgentId(agentCardData.name), + name: agentCardData.name, + description: agentCardData.description, + icon: agentCardData.iconUrl ?? 'lucide:bot', category: 'my', // 导入的智能体分类为"我的" + type: 'A2A', installed: false, - version: agentCard.version || '1.0.0', - provider: agentCard.provider || { - organization: agentCard.author || 'Unknown', - url: agentCard.homepage || '' + version: agentCardData.version ?? '1.0.0', + provider: agentCardData.provider ?? { + organization: 'Unknown', + url: '' }, - skills: this.parseSkills(agentCard.skills || []), - mcpServers: agentCard.mcpServers || [], - config: agentCard.config || {} + skills: this.parseSkills(agentCardData.skills ?? []), + a2aURL: agentCardData.url, + mcpServers: [], + config: {} } // 直接保存智能体,名称重复检查由前端处理 @@ -550,56 +530,6 @@ export class AgentConfHelper { } } - /** - * 获取URL导入的智能体数据(不保存) - */ - async getImportAgentData( - url: string - ): Promise<{ name: string; description: string; agentCard: any }> { - try { - console.log('Getting import agent data for URL:', url) - - // 验证URL格式 - if (!url || !url.startsWith('http')) { - throw new Error('Invalid URL format') - } - - // 确保URL以 .well-known/agent-card.json 结尾 - let importUrl = url - if (!url.endsWith('.well-known/agent-card.json')) { - // 如果URL不以标准路径结尾,尝试添加标准路径 - const baseUrl = url.endsWith('/') ? url.slice(0, -1) : url - importUrl = `${baseUrl}/.well-known/agent-card.json` - } - - console.log('Fetching agent card from:', importUrl) - - // 使用fetch获取agent-card.json - const response = await fetch(importUrl) - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const agentCard = await response.json() - console.log('Received agent card:', agentCard) - - // 验证agent-card.json格式 - if (!agentCard.name || !agentCard.description) { - throw new Error('Invalid agent card format: missing required fields') - } - - return { - name: agentCard.name, - description: agentCard.description, - agentCard - } - } catch (error) { - console.error('Failed to get import agent data:', error) - throw error - } - } - /** * 生成智能体ID */ @@ -616,12 +546,12 @@ export class AgentConfHelper { private parseSkills(skills: any[]): any[] { return skills.map((skill, index) => ({ id: `skill-${index}`, - name: skill.name || 'Unknown Skill', - description: skill.description || '', - tags: skill.tags || [], - examples: skill.examples || [], - imputModes: skill.inputModes || ['text'], - ouputModes: skill.outputModes || ['text'] + name: skill.name ?? 'Unknown Skill', + description: skill.description ?? '', + tags: skill.tags ?? [], + examples: skill.examples ?? [], + imputModes: skill.inputModes ?? ['text'], + ouputModes: skill.outputModes ?? ['text'] })) } } diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index 0ea3890..0d06e82 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -10,7 +10,8 @@ import { SystemPrompt, IModelConfig, BuiltinKnowledgeConfig, - Agent + Agent, + AgentCardData } from '@shared/presenter' import { ProviderChange, @@ -1559,11 +1560,14 @@ export class ConfigPresenter implements IConfigPresenter { /** * 导出智能体数据 */ - async exportAgents(): Promise<{ + async exportAgents(typeFilterCondition?: string): Promise<{ agents: Agent[] installedAgents: string[] lastUpdateTime: number }> { + if (typeFilterCondition) { + return this.agentConfHelper.exportAgents(typeFilterCondition) + } return this.agentConfHelper.exportAgents() } @@ -1575,10 +1579,10 @@ export class ConfigPresenter implements IConfigPresenter { } /** - * 从URL导入智能体配置 + * 从A2A AgentCard Data导入智能体配置 */ - async importAgentFromUrl(url: string): Promise { - return this.agentConfHelper.importAgentFromUrl(url) + async importAgentFromA2AData(agentCardData: AgentCardData): Promise { + return this.agentConfHelper.importAgentFromA2AData(agentCardData) } /** diff --git a/src/main/presenter/index.ts b/src/main/presenter/index.ts index e04b81e..e75a183 100644 --- a/src/main/presenter/index.ts +++ b/src/main/presenter/index.ts @@ -105,7 +105,7 @@ export class Presenter implements IPresenter { this.llmproviderPresenter, this.configPresenter ) - this.a2aPresenter = new A2APresenter() + this.a2aPresenter = new A2APresenter(this.configPresenter) this.mcpPresenter = new McpPresenter(this.configPresenter) this.upgradePresenter = new UpgradePresenter(this.configPresenter) this.shortcutPresenter = new ShortcutPresenter(this.configPresenter) diff --git a/src/renderer/src/components/agent-config/AgentImportDialog.vue b/src/renderer/src/components/agent-config/AgentImportDialog.vue index d71693f..a373d84 100644 --- a/src/renderer/src/components/agent-config/AgentImportDialog.vue +++ b/src/renderer/src/components/agent-config/AgentImportDialog.vue @@ -33,7 +33,7 @@ v-model="importUrl" :placeholder="t('agents.import.dialog.a2aAgentCardPlaceholder')" class="w-full border border-border bg-background text-foreground" - @keyup.enter="handleImport" + @keyup.enter="canImport && handleImport()" />