From 12134840d21395c50a7702e7a0167b4640c66153 Mon Sep 17 00:00:00 2001 From: randy1568 Date: Fri, 21 Nov 2025 16:18:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(A2A=20Server):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=AF=BC=E5=85=A5=E9=9B=86=E6=88=90=E4=B8=89?= =?UTF-8?q?=E6=96=B9A2A=20Server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presenter/A2APresenter/A2AClientAction.ts | 120 +++---- src/main/presenter/A2APresenter/index.ts | 157 ++++----- .../presenter/A2APresenter/serverManager.ts | 96 ++++-- src/main/presenter/A2APresenter/types.ts | 113 +------ src/main/presenter/index.ts | 6 +- .../agent-config/AgentImportDialog.vue | 306 ++++++++++++++++-- src/renderer/src/i18n/en-US/agents.json | 29 +- src/renderer/src/i18n/zh-CN/agents.json | 29 +- .../types/presenters/legacy.presenters.d.ts | 130 +++++++- 9 files changed, 670 insertions(+), 316 deletions(-) diff --git a/src/main/presenter/A2APresenter/A2AClientAction.ts b/src/main/presenter/A2APresenter/A2AClientAction.ts index 7d5ffbf..ef5fe59 100644 --- a/src/main/presenter/A2APresenter/A2AClientAction.ts +++ b/src/main/presenter/A2APresenter/A2AClientAction.ts @@ -14,81 +14,65 @@ import type { FilePart, Artifact } from '@a2a-js/sdk' - -import { A2AResponseData, A2APart, A2AArtifact, A2A_INNER_ERROR_CODE } from './types' +import { A2AMessageSendParams, A2AServerResponse } from '@shared/presenter' +import { A2A_INNER_ERROR_CODE, A2AArtifact, A2APart } from './types' export type { Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent, AgentCard } export class A2AClientAction { - sdkClient: A2AClient - agentCardUrl: string = '' - agentCard: AgentCard | undefined = undefined + private sdkClient: A2AClient + private agentCardUrl: string + constructor( serverUrl: string, private timeout: number = 120000 ) { this.sdkClient = new A2AClient(serverUrl) - if (serverUrl.endsWith('/.well-known/agent-card.json')) { this.agentCardUrl = serverUrl } else { this.agentCardUrl = serverUrl + '/.well-known/agent-card.json' } - - this.initialize(serverUrl).catch((error) => { - console.error(`[A2A] Failed to initialize client:`, error.message) - throw error - }) - } - - async initialize(serverUrl: string): Promise { - await this.fetchAgentCard(serverUrl) - } - /** - * get agentCard - */ - async fetchAgentCard(serverUrl: string): Promise { - try { - console.log(`[A2A] Fetching agent card ${serverUrl}`) - - // Get agent card from client - this.agentCard = await this.sdkClient.getAgentCard() - } catch (error) { - console.error(`[A2A] Failed to connect to server ${serverUrl}:`, error) - throw error - } } - /** * Check if client is connected */ async isConnected(): Promise { const response = await fetch(this.agentCardUrl) if (response.ok) { - console.log('[A2A] JS Server is connected') + console.log('[A2A] A2A Server is connected') return true } else { - console.error('[A2A] JS Server response error:', response.status) + console.error('[A2A] A2A Server response error:', response.status) } return false } async getAgentCard(): Promise { - if (this.agentCard == undefined) { - await this.fetchAgentCard(this.agentCardUrl) + try { + console.log(`[A2A] Fetching agent card ${this.agentCardUrl}`) + // Get agent card from client + return await this.sdkClient.getAgentCard() + } catch (error) { + console.error(`[A2A] Failed to connect to server ${this.agentCardUrl}:`, error) + throw error } - return this.agentCard as AgentCard } /** * Send a message and create a task (non-streaming) */ - async sendMessage(params: MessageSendParams): Promise { + async sendMessage(params: A2AMessageSendParams): Promise { if (!(await this.isConnected())) { throw new Error('Client not connected') } try { const sendParams: MessageSendParams = { - message: params.message, + message: { + messageId: params.messageId, + kind: params.kind, + role: params.role, + parts: this.partDataTransfer(params.parts) + }, configuration: { blocking: true // Non-streaming mode } @@ -116,7 +100,6 @@ export class A2AClientAction { console.error(`[A2A] Error: ${errorResponse.error.message}`) return { kind: 'error', - serverName: this?.agentCard?.name || '', error: { code: errorResponse.error.code, message: errorResponse.error.message @@ -127,9 +110,8 @@ export class A2AClientAction { let successResponse = response as SendMessageSuccessResponse return this.formatEventToResponse(successResponse.result) } catch (error) { - const errorResponse: A2AResponseData = { + const errorResponse: A2AServerResponse = { kind: 'error', - serverName: this?.agentCard?.name || '', error: { code: A2A_INNER_ERROR_CODE.STREAMING_MESSAGE_ERROR, message: `send message error: ${error instanceof Error ? error.message : String(error)}` @@ -142,13 +124,21 @@ export class A2AClientAction { /** * Send a streaming message */ - async *sendStreamingMessage(params: MessageSendParams): AsyncGenerator { + async *sendStreamingMessage(params: A2AMessageSendParams): AsyncGenerator { if (!(await this.isConnected())) { throw new Error('Client not connected') } try { console.log(`[A2A] Starting streaming message`) - const streamIterator = this.sdkClient.sendMessageStream(params) + const sendParams: MessageSendParams = { + message: { + messageId: params.messageId, + kind: params.kind, + role: params.role, + parts: this.partDataTransfer(params.parts) + } + } + const streamIterator = this.sdkClient.sendMessageStream(sendParams) // Helper function to race event with timeout async function nextWithTimeout( iterator: AsyncIterator, @@ -176,15 +166,14 @@ export class A2AClientAction { break } const event = result.value - // 将事件转换为统一的 A2AResponseData 格式 + // 将事件转换为统一的 A2AServerResponse 格式 const formatted = this.formatEventToResponse(event) yield formatted } streamIterator.return() } catch (error) { - const errorResponse: A2AResponseData = { + const errorResponse: A2AServerResponse = { kind: 'error', - serverName: this?.agentCard?.name || '', error: { code: A2A_INNER_ERROR_CODE.STREAMING_MESSAGE_ERROR, message: `Streaming message error: ${error instanceof Error ? error.message : String(error)}` @@ -203,7 +192,6 @@ export class A2AClientAction { } try { await this.sdkClient.cancelTask({ id: taskId }) - console.log(`[A2A] Task cancelled: ${taskId}`) } catch (error) { console.error('[A2A] Failed to cancel task:', error) @@ -211,9 +199,35 @@ export class A2AClientAction { } } + private partDataTransfer(a2aPartData: A2APart[]): Part[] { + return a2aPartData.map((part) => { + if (part.type === 'text') { + return { + kind: 'text', + text: part.text + } as TextPart + } else if (part.type === 'data') { + return { + kind: 'data', + data: part.data + } as DataPart + } else { + return { + kind: 'file', + file: { + name: part?.file?.name, + mimeType: part?.file?.mimeType, + uri: part?.file?.uri, + bytes: part?.file?.bytes + } + } as FilePart + } + }) + } + private formatEventToResponse( event: Message | Task | TaskStatusUpdateEvent | TaskArtifactUpdateEvent - ): A2AResponseData { + ): A2AServerResponse { if (event.kind === 'message') { return this.resolveMessage(event as Message) } else if (event.kind === 'task') { @@ -226,10 +240,9 @@ export class A2AClientAction { throw new Error('Unknown event type') } - private resolveMessage(message: Message): A2AResponseData { + private resolveMessage(message: Message): A2AServerResponse { return { kind: 'message', - serverName: this?.agentCard?.name || '', contextId: message.contextId, message: { parts: this.convertParts(message.parts) @@ -237,10 +250,9 @@ export class A2AClientAction { } } - private resolveTask(task: Task): A2AResponseData { + private resolveTask(task: Task): A2AServerResponse { return { kind: 'task', - serverName: this?.agentCard?.name || '', contextId: task.contextId, taskId: task.id, task: { @@ -253,10 +265,9 @@ export class A2AClientAction { } } - private resolveTaskStatusUpdate(event: TaskStatusUpdateEvent): A2AResponseData { + private resolveTaskStatusUpdate(event: TaskStatusUpdateEvent): A2AServerResponse { return { kind: 'status-update', - serverName: this?.agentCard?.name || '', contextId: event.contextId, taskId: event.taskId, statusUpdate: { @@ -269,10 +280,9 @@ export class A2AClientAction { } } - private resolveTaskArtifactUpdate(event: TaskArtifactUpdateEvent): A2AResponseData { + private resolveTaskArtifactUpdate(event: TaskArtifactUpdateEvent): A2AServerResponse { return { kind: 'artifact-update', - serverName: this?.agentCard?.name || '', contextId: event.contextId, taskId: event.taskId, artifactUpdate: { diff --git a/src/main/presenter/A2APresenter/index.ts b/src/main/presenter/A2APresenter/index.ts index 9875117..cd86cbb 100644 --- a/src/main/presenter/A2APresenter/index.ts +++ b/src/main/presenter/A2APresenter/index.ts @@ -1,116 +1,89 @@ -/** - * A2A Presenter Implementation - * Main interface exposing all A2A operations and integrating with the existing system - */ - -import { serverManager } from './serverManager' -import type { MessageSendParams, AgentCard } from '@a2a-js/sdk' - -// Local interfaces for presenter API -export interface TaskQueryParams { - taskId: string - contextId?: string -} - -export interface TaskIdParams { - taskId: string -} -import { IA2APresenter } from '@shared/presenter' -import { A2AClientAction } from './A2AClientAction' -import { A2AResponseData } from './types' +import type { + A2AClientData, + A2AMessageSendParams, + A2AServerResponse, + AgentCardData, + IA2APresenter +} from '@shared/presenter' +import { ServerManager } from './serverManager' export class A2APresenter implements IA2APresenter { - private manager: serverManager - - constructor() { - this.manager = new serverManager() - } - - /** - * Get all A2A server configurations - */ - async getA2AServers(): Promise> { - return this.manager.getA2AServers() + private readonly manager = new ServerManager() + async getA2AClient(serverURL: string): Promise { + const a2aClientAction = await this.manager.getA2AClient(serverURL) + if (!a2aClientAction) { + return + } + const agentCard = await a2aClientAction.getAgentCard() + const getAgentCardData = () => { + return agentCard?.skills.map((agentSkill) => ({ + name: agentSkill.name, + description: agentSkill.description + })) + } + return { + isRunning: await this.manager.isA2AServerRunning(serverURL), + agentCard: { + name: agentCard.name, + description: agentCard.description, + url: agentCard.url, + streamingSupported: agentCard.capabilities?.streaming === true ? true : false, + skills: getAgentCardData(), + iconUrl: agentCard.iconUrl + } + } } - /** - * Add a new A2A server - */ - async addA2AServer(serverID: string): Promise { + async addA2AServer(serverURL: string): Promise { try { - // Check if server already exists - const existingServers = await this.getA2AServers() - if (existingServers[serverID]) { - console.error(`[A2A] Failed to add A2A server: Server "${serverID}" already exists.`) - return false + const agentCard = await this.manager.addA2AServer(serverURL) + const getAgentCardData = () => { + return agentCard.skills.map((agentSkill) => ({ + name: agentSkill.name, + description: agentSkill.description + })) + } + return { + name: agentCard.name, + description: agentCard.description, + url: agentCard.url, + streamingSupported: agentCard.capabilities?.streaming === true ? true : false, + skills: getAgentCardData(), + iconUrl: agentCard.iconUrl } - - this.manager.addA2AServer(serverID) - console.log(`[A2A] Added server: ${serverID}`) - return true } catch (error) { - console.error(`[A2A] Failed to add server ${serverID}:`, error) - throw error + console.error(`[A2A] Failed to add server ${serverURL}`) + return } } - /** - * Remove an A2A server - */ - async removeA2AServer(serverID: string): Promise { + + async removeA2AServer(serverURL: string): Promise { try { - this.manager.removeA2AServer(serverID) - console.log(`[A2A] Removed server: ${serverID}`) + return await this.manager.removeA2AServer(serverURL) } catch (error) { - console.error(`[A2A] Failed to remove server ${serverID}:`, error) - throw error + console.error(`[A2A] Failed to remove server ${serverURL}:`, error) + return false } } - /** - * Check if a server is running - */ - async isServerRunning(serverID: string): Promise { - return await this.manager.isA2AServerRunning(serverID) - } - - /** - * Send a message to an A2A server - */ async sendMessage( - serverID: string, - params: MessageSendParams - ): Promise> { - const client = this.manager.getA2AClient(serverID) - if (!client) { - throw new Error(`A2A server '${serverID}' is not running`) + serverURL: string, + params: A2AMessageSendParams + ): Promise> { + const a2aClientAction = await this.manager.getA2AClient(serverURL) + if (!a2aClientAction) { + throw new Error(`A2A server '${serverURL}' is not running`) } - const agentCard = await client.getAgentCard() + const agentCard = await a2aClientAction.getAgentCard() const isStreaming = agentCard.capabilities?.streaming === true if (isStreaming) { - return client.sendStreamingMessage(params) + return a2aClientAction.sendStreamingMessage(params) } else { - return client.sendMessage(params) + return a2aClientAction.sendMessage(params) } } - /** - * Cancel a task - */ - async cancelTask(serverID: string, taskID: string): Promise { - const client = this.manager.getA2AClient(serverID) - if (!client) { - throw new Error(`A2A server '${serverID}' is not running`) - } - await client.cancelTask(taskID) - } - /** - * Get agent card for a server - */ - async getAgentCard(serverName: string): Promise { - const client = this.manager.getA2AClient(serverName) - if (!client) { - throw new Error(`A2A server '${serverName}' is not running`) - } - return await client.getAgentCard() + async isServerRunning(serverURL: string): Promise { + return this.manager.isA2AServerRunning(serverURL) } } diff --git a/src/main/presenter/A2APresenter/serverManager.ts b/src/main/presenter/A2APresenter/serverManager.ts index 3a31697..6224aae 100644 --- a/src/main/presenter/A2APresenter/serverManager.ts +++ b/src/main/presenter/A2APresenter/serverManager.ts @@ -1,43 +1,83 @@ +import { AgentCard } from '@a2a-js/sdk' import { A2AClientAction } from './A2AClientAction' -export class serverManager { - private clients: Map = new Map() +export class ServerManager { + //key:serverURL value:A2AClientAction + private clientPool: Map - /** - * Add a new A2A server - */ - addA2AServer(serverID: string) { - if (this.clients.has(serverID)) { - throw new Error(`A2A server ${serverID} already exists`) + constructor() { + this.clientPool = new Map() + } + + 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`) } - this.clients.set(serverID, new A2AClientAction(serverID)) + + const client = new A2AClientAction(normalizedURL) + const agentCard = await client.getAgentCard().catch((error: Error) => { + throw new Error(error.message) + }) + + this.clientPool.set(normalizedURL, client) + return agentCard } - /** - * Remove an A2A server - */ - removeA2AServer(serverID: string) { - this.clients.delete(serverID) + async removeA2AServer(serverURL: string): Promise { + const normalizedURL = this.normalLizeServerURL(serverURL) + if (!normalizedURL) { + throw new Error(`[A2A] Invalid server URL: ${serverURL}`) + } + if (!this.clientPool.has(normalizedURL)) { + console.log(`[A2A] ${serverURL} doesn't exist`) + return false + } + return this.clientPool.delete(normalizedURL) } - getA2AServers(): Record { - return Object.fromEntries(this.clients.entries()) + async getA2AClient(serverURL: string): Promise { + const normalizedURL = this.normalLizeServerURL(serverURL) + if (!normalizedURL) { + console.log(`[A2A] Invalid server URL: ${serverURL}`) + return + } + if (!this.clientPool.has(normalizedURL)) { + console.log(`[A2A] ${serverURL} doesn't exist`) + return + } + return this.clientPool.get(normalizedURL) } - /** - * Check if a server is running - */ - async isA2AServerRunning(serverID: string): Promise { - if (!this.clients.has(serverID)) { + + async isA2AServerRunning(serverURL: string): Promise { + const a2aClientAction = await this.getA2AClient(serverURL) + if (!a2aClientAction) { return false + } else { + return await a2aClientAction.isConnected() } - const client = this.clients.get(serverID) - return client ? await client.isConnected() : false } - /** - * Get a running client by name - */ - getA2AClient(serverID: string): A2AClientAction | undefined { - return this.clients.get(serverID) + private normalLizeServerURL(serverURL: string): string | undefined { + if (!serverURL || !serverURL.trim()) { + console.log('[A2A] server URL is null') + return undefined + } + let trimmed = serverURL.trim() + try { + new URL(trimmed) + } catch (e) { + console.log('[A2A] server URL is invalid:', serverURL) + return undefined + } + const agentCardSuffix = '/.well-known/agent-card.json' + if (trimmed.endsWith(agentCardSuffix)) { + trimmed = trimmed.slice(0, -agentCardSuffix.length) + } + return trimmed } } diff --git a/src/main/presenter/A2APresenter/types.ts b/src/main/presenter/A2APresenter/types.ts index 3832006..0623f84 100644 --- a/src/main/presenter/A2APresenter/types.ts +++ b/src/main/presenter/A2APresenter/types.ts @@ -1,113 +1,20 @@ /** * A2A Presenter Type Definitions * - * This file contains all custom types and interfaces used by the A2A Presenter. - * It also re-exports necessary types from the @a2a-js/sdk for convenience. + * This file centralizes all custom types that the presenter relies on so that + * the rest of the module can import from a single place. */ - -// Re-export SDK types +// Re-export shared contract types that the presenter must comply with export type { - AgentCard, - Task, - TaskState, - TaskStatusUpdateEvent, - TaskArtifactUpdateEvent, - Message, - MessageSendParams, - Part, - TextPart, - FilePart, - DataPart, - JSONRPCErrorResponse -} from '@a2a-js/sdk' + A2AArtifact, + A2APart, + A2AServerResponse, + A2AMessageSendParams, + A2AClientData, + AgentCardData +} from '@shared/presenter' export enum A2A_INNER_ERROR_CODE { MESSAGE_ERROR = -1, STREAMING_MESSAGE_ERROR = -2 } - -/** - * Unified response data format for A2A interactions - * Used for frontend display and event propagation - */ -export interface A2AResponseData { - /** Response type discriminator */ - kind: 'message' | 'task' | 'status-update' | 'artifact-update' | 'error' - /** Timestamp when this response was created */ - /** Name of the A2A server that generated this response */ - serverName: string - contextId?: string - taskId?: string - - /** Message data (when type is 'message') */ - message?: { - parts: A2APart[] - } - - /** Task data (when type is 'task') */ - task?: { - status: { - state: string // TaskState - parts?: A2APart[] - } - artifacts?: A2AArtifact[] - } - - /** Status update data (when type is 'task-status-update') */ - statusUpdate?: { - status: { - state: string // TaskState - parts?: A2APart[] - } - final: boolean - } - - /** Artifact update data (when type is 'task-artifact-update') */ - artifactUpdate?: { - artifact: A2AArtifact - /** If true, the content of this artifact should be appended to a previously sent artifact with the same ID. */ - append?: boolean - /** If true, this is the final chunk of the artifact. */ - lastChunk?: boolean - } - - /** Error data (when type is 'error') */ - error?: { - code: number - message: string - data?: unknown - } -} - -/** - * Simplified part representation for frontend consumption - */ -export interface A2APart { - /** Part type */ - type: 'text' | 'data' | 'file' - /** Text content (for text parts) */ - text?: string - /** Structured data (for data parts) */ - data?: unknown - /** File information (for file parts) */ - file?: { - name?: string - mimeType?: string - uri?: string - bytes?: string // base64 encoded - } -} - -/** - * Artifact representation for frontend consumption - */ -export interface A2AArtifact { - /** - * An optional, human-readable name for the artifact. - */ - name?: string - /** - * An array of content parts that make up the artifact. - */ - parts: A2APart[] -} diff --git a/src/main/presenter/index.ts b/src/main/presenter/index.ts index 4f6df5b..e04b81e 100644 --- a/src/main/presenter/index.ts +++ b/src/main/presenter/index.ts @@ -23,7 +23,8 @@ import { IThreadPresenter, IUpgradePresenter, IWindowPresenter, - IBuiltInToolsPresenter + IBuiltInToolsPresenter, + IA2APresenter } from '@shared/presenter' import { eventBus } from '@/eventbus' import { LLMProviderPresenter } from './llmProviderPresenter' @@ -42,6 +43,7 @@ import { FloatingButtonPresenter } from './floatingButtonPresenter' import { CONFIG_EVENTS, WINDOW_EVENTS } from '@/events' import { KnowledgePresenter } from './knowledgePresenter' import { BuiltInToolsPresenter } from './builtInToolsPresenter' +import { A2APresenter } from './A2APresenter' // IPC调用上下文接口 interface IPCCallContext { @@ -83,6 +85,7 @@ export class Presenter implements IPresenter { // llamaCppPresenter: LlamaCppPresenter // 保留原始注释 dialogPresenter: IDialogPresenter lifecycleManager: ILifecycleManager + a2aPresenter: IA2APresenter private constructor(lifecycleManager: ILifecycleManager) { // Store lifecycle manager reference for component access @@ -102,6 +105,7 @@ export class Presenter implements IPresenter { this.llmproviderPresenter, this.configPresenter ) + this.a2aPresenter = new A2APresenter() 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 dc33738..d71693f 100644 --- a/src/renderer/src/components/agent-config/AgentImportDialog.vue +++ b/src/renderer/src/components/agent-config/AgentImportDialog.vue @@ -1,6 +1,6 @@