目前正在做A2A协议适配,由于在2026年3月才发布的1.0版本,导致很多AI模型都没有输入相关的知识,返回的很多知识都是旧版本(0.3)。从0.3->1.0这两个版本发生了很大的改动,1.0甚至不兼容0.3,官方给出的也是说建议大家逐步兼容,最后删除对0.3的支持。

Agent Card 1.0案例

官方案例地址:https://a2a-protocol.org/latest/specification/#843-signature-verification


{
  "name": "GeoSpatial Route Planner Agent",
  "description": "Provides advanced route planning, traffic analysis, and custom map generation services. This agent can calculate optimal routes, estimate travel times considering real-time traffic, and create personalized maps with points of interest.",
  "supportedInterfaces": [
    {"url": "https://georoute-agent.example.com/a2a/v1", "protocolBinding": "JSONRPC", "protocolVersion": "1.0"},
    {"url": "https://georoute-agent.example.com/a2a/grpc", "protocolBinding": "GRPC", "protocolVersion": "1.0"},
    {"url": "https://georoute-agent.example.com/a2a/json", "protocolBinding": "HTTP+JSON", "protocolVersion": "1.0"}
  ],
  "provider": {
    "organization": "Example Geo Services Inc.",
    "url": "https://www.examplegeoservices.com"
  },
  "iconUrl": "https://georoute-agent.example.com/icon.png",
  "version": "1.2.0",
  "documentationUrl": "https://docs.examplegeoservices.com/georoute-agent/api",
  "capabilities": {
    "streaming": true,
    "pushNotifications": true,
    "stateTransitionHistory": false,
    "extendedAgentCard": true
  },
  "securitySchemes": {
    "google": {
      "openIdConnectSecurityScheme": {
        "openIdConnectUrl": "https://accounts.google.com/.well-known/openid-configuration"
      }
    }
  },
  "security": [{ "google": ["openid", "profile", "email"] }],
  "defaultInputModes": ["application/json", "text/plain"],
  "defaultOutputModes": ["application/json", "image/png"],
  "skills": [
    {
      "id": "route-optimizer-traffic",
      "name": "Traffic-Aware Route Optimizer",
      "description": "Calculates the optimal driving route between two or more locations, taking into account real-time traffic conditions, road closures, and user preferences (e.g., avoid tolls, prefer highways).",
      "tags": ["maps", "routing", "navigation", "directions", "traffic"],
      "examples": [
        "Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls.",
        "{\"origin\": {\"lat\": 37.422, \"lng\": -122.084}, \"destination\": {\"lat\": 37.7749, \"lng\": -122.4194}, \"preferences\": [\"avoid_ferries\"]}"
      ],
      "inputModes": ["application/json", "text/plain"],
      "outputModes": [
        "application/json",
        "application/vnd.geo+json",
        "text/html"
      ]
    },
    {
      "id": "custom-map-generator",
      "name": "Personalized Map Generator",
      "description": "Creates custom map images or interactive map views based on user-defined points of interest, routes, and style preferences. Can overlay data layers.",
      "tags": ["maps", "customization", "visualization", "cartography"],
      "examples": [
        "Generate a map of my upcoming road trip with all planned stops highlighted.",
        "Show me a map visualizing all coffee shops within a 1-mile radius of my current location."
      ],
      "inputModes": ["application/json"],
      "outputModes": [
        "image/png",
        "image/jpeg",
        "application/json",
        "text/html"
      ]
    }
  ],
  "signatures": [
    {
      "protected": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpPU0UiLCJraWQiOiJrZXktMSIsImprdSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWdlbnQvandrcy5qc29uIn0",
      "signature": "QFdkNLNszlGj3z3u0YQGt_T9LixY3qtdQpZmsTdDHDe3fXV9y9-B3m2-XgCpzuhiLt8E0tV6HXoZKHv4GtHgKQ"
    }
  ]
}

AgentCard 字段说明

字段名 类型 必需 描述
name string ✅ 必需 代理的名称,用于标识和展示
description string ✅ 必需 代理的功能描述,帮助客户端理解代理的能力
supportedInterfaces AgentInterface[] ✅ 必需 支持的协议绑定列表,按优先级排序。第一个条目为首选接口
provider AgentProvider 可选 代理提供商信息,包括组织名称和URL
iconUrl string 可选 代理图标的URL地址
version string ✅ 必需 代理的版本号(非协议版本),用于版本管理
documentationUrl string 可选 代理API文档的URL链接
capabilities AgentCapabilities ✅ 必需 代理支持的能力声明(流式、推送通知等)
securitySchemes map<string, SecurityScheme> ✅ 必需 支持的认证方案映射,键为方案名称
security SecurityRequirement[] ✅ 必需 全局安全要求,指定需要应用的认证方案和作用域
defaultInputModes string[] ✅ 必需 代理默认支持的输入MIME类型列表(如"text/plain")
defaultOutputModes string[] ✅ 必需 代理默认支持的输出MIME类型列表
skills AgentSkill[] ✅ 必需 代理提供的技能列表,描述具体能力
signatures AgentCardSignature[] 可选 Agent Card的数字签名,用于验证真实性
extensions AgentExtension[] 可选 代理支持的扩展列表(已移至capabilities中,此为兼容性保留)

AgentCapabilities 字段说明

字段名 类型 必需 描述
streaming boolean ✅ 必需 是否支持流式响应(SendStreamingMessage和SubscribeToTask操作)
pushNotifications boolean ✅ 必需 是否支持推送通知(WebHook)机制
stateTransitionHistory boolean ✅ 必需 是否支持状态转换历史记录
extensions AgentExtension[] 可选 代理支持的扩展列表
extendedAgentCard boolean 可选 是否支持认证后获取扩展Agent Card(1.0版本新增)

AgentSkill 字段说明

字段名 类型 必需 描述
id string ✅ 必需 技能的唯一标识符
name string ✅ 必需 技能的显示名称
description string ✅ 必需 技能的详细描述
tags string[] 可选 技能标签,用于分类和搜索
examples string[] 可选 使用示例,帮助理解如何使用该技能
inputModes string[] 可选 该技能支持的输入MIME类型,覆盖全局defaultInputModes
outputModes string[] 可选 该技能支持的输出MIME类型,覆盖全局defaultOutputModes

AgentInterface 字段说明

字段名 类型 必需 描述
url string ✅ 必需 该协议绑定的访问端点URL
protocolBinding string ✅ 必需 协议绑定类型(JSONRPC、GRPC、HTTP+JSON等)
protocolVersion string ✅ 必需 使用的A2A协议版本号(如"1.0")

AgentProvider 字段说明

字段名 类型 必需 描述
organization string ✅ 必需 提供该代理的组织名称
url string 可选 组织或代理提供商的官方网站URL

本地启动A2A 1.0 Agent

使用Python,以及下述依赖,启动A2A 1.0 Agent:

先执行npm install, 然后再使用npm run dev:agent_v1或npm urn dev:agent_streaming

{
  "name": "a2a-agent-demo",
  "version": "1.0.0",
  "description": "A2A Agent 示例 - 使用 @a2a-relay/server",
  "type": "module",
  "scripts": {
    "dev:agent_v1": "tsx watch src/agent_v1.ts",
    "dev:agent_streaming": "tsx watch src/agent_streaming.ts",
    "dev:client": "tsx watch src/client.ts",
    "build": "tsc",
    "start": "node dist/agent.js",
    "start:agent3": "node dist/agent3.js",
    "start:agent_v1": "node dist/agent_v1.js",
    "start:client": "node dist/client.js"
  },
  "dependencies": {
    "@a2a-relay/client": "^0.1.0",
    "@a2a-relay/server": "^0.2.0",
    "express": "^4.22.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.25",
    "@types/node": "^22.0.0",
    "tsx": "^4.19.0",
    "typescript": "^5.7.0"
  }
}

非流式 A2A 1.0 Agent

/**
 * A2A Protocol v1.0 Agent 实现
 * 
 * 主要变化 (v0.3 → v1.0):
 * 1. AgentCard 使用 supportedInterfaces[] 结构
 * 2. Message Role: "user" → "ROLE_USER"
 * 3. Part 结构: 移除 kind 字段
 * 4. JSON-RPC 方法: message/send → SendMessage
 */

import express from 'express';
import crypto from 'crypto';

// ============= A2A v1.0 类型定义 =============

interface AgentCardV1 {
  name: string;
  description: string;
  version: string;
  supportedInterfaces: SupportedInterface[];
  capabilities: {
    streaming: boolean;
    pushNotifications: boolean;
    extendedAgentCard: boolean;
    stateTransitionHistory: boolean;
  };
  defaultInputModes: string[];
  defaultOutputModes: string[];
  skills: Skill[];
  provider?: {
    organization: string;
    url?: string;
  };
  documentationUrl?: string;
  securitySchemes?: { [key: string]: any };
  securityRequirements?: Array<{ [key: string]: any }>;
  signatures?: Signature[];
  iconUrl?: string;
}

interface SupportedInterface {
  protocolBinding: string;
  url: string;
  protocolVersion: string;
}

interface Signature {
  signature: string;
  publicKey: {
    kid: string;
    jwk: {
      kty: string;
      n: string;
      e: string;
    };
  };
}

interface Skill {
  id: string;
  name: string;
  description: string;
  tags?: string[];
  examples?: string[];
  inputModes?: string[];
  outputModes?: string[];
}

interface Message {
  role: string;
  parts: Part[];
  extensions?: string[];
}

interface Part {
  text?: string;
  url?: string;
  raw?: string;
  filename?: string;
  mediaType?: string;
  data?: any;
}

interface Task {
  id: string;
  status: {
    state: string;
    timestamp: string;
  };
  createdAt: string;
  lastModified?: string;
  messages?: Message[];
  artifacts?: Artifact[];
  extensions?: string[];
}

interface Artifact {
  name?: string;
  description?: string;
  parts: Part[];
  index?: number;
  extensions?: string[];
}

// ============= 工具函数 =============

function getTimestamp(): string {
  return new Date().toISOString().replace(/\.(\d{3})Z$/, '.$1Z');
}

function generateId(): string {
  return crypto.randomUUID();
}

// ============= 天气数据 =============

const weatherDatabase: Record<string, { temp: string; condition: string; humidity: string }> = {
  '北京': { temp: '15°C', condition: '晴', humidity: '45%' },
  '上海': { temp: '18°C', condition: '多云', humidity: '60%' },
  '深圳': { temp: '25°C', condition: '晴', humidity: '55%' },
  '广州': { temp: '26°C', condition: '晴', humidity: '58%' },
  '杭州': { temp: '17°C', condition: '小雨', humidity: '75%' },
  '成都': { temp: '14°C', condition: '阴', humidity: '65%' },
  '武汉': { temp: '16°C', condition: '多云', humidity: '55%' },
  '西安': { temp: '12°C', condition: '晴', humidity: '40%' },
};

// ============= 待办事项存储 =============

interface TodoItem {
  id: number;
  text: string;
  completed: boolean;
  createdAt: string;
}

const todoList: TodoItem[] = [
  { id: 1, text: '学习 A2A 协议', completed: false, createdAt: getTimestamp() },
  { id: 2, text: '实现天气查询功能', completed: true, createdAt: getTimestamp() },
];

let todoIdCounter = 3;

// ============= 笑话数据 =============

const jokes = [
  '为什么程序员总是分不清万圣节和圣诞节?因为 Oct 31 = Dec 25',
  '程序员的两大谎言:1. 我明天就写注释 2. 这个bug很简单',
  '一个程序员在婚礼上迟到,大家问他原因。他说:「我在优化路径。」',
  '为什么程序员喜欢暗色主题?因为 light 主题会让代码产生热量(bug)。',
  '程序员最讨厌的事:1. 写文档 2. 别人不写文档',
  '程序员相亲:女:你有房吗?男:GitHub上有。',
  '代码写得好的人,删代码的时候也会很优雅。',
  '世界上最远的距离是:你有 bug 我有 bug',
];

// ============= 计算器数据 =============

const exchangeRates: Record<string, number> = {
  '美元': 7.24,
  '欧元': 7.85,
  '英镑': 8.95,
  '日元': 0.048,
  '港币': 0.93,
};

const lengthConversions: Record<string, Record<string, number>> = {
  '公里': { '英里': 0.621371, '米': 1000, '英尺': 3280.84 },
  '英里': { '公里': 1.60934, '米': 1609.34, '英尺': 5280 },
  '米': { '英尺': 3.28084, '英寸': 39.3701, '厘米': 100 },
};

// ============= 工具函数实现 =============

function handleWeather(city: string): string {
  const weather = weatherDatabase[city];
  if (weather) {
    return `${city}今天天气:\n🌡️ 温度:${weather.temp}\n☁️ 天气状况:${weather.condition}\n💧 湿度:${weather.humidity}`;
  }
  return `抱歉,我没有 ${city} 的天气数据。支持的城市有:北京、上海、深圳、广州、杭州、成都、武汉、西安`;
}

function handleTodo(input: string): string {
  // 添加待办
  if (input.includes('添加') || input.includes('新增')) {
    const match = input.match(/[::]\s*(.+)/);
    if (match) {
      const todo: TodoItem = {
        id: todoIdCounter++,
        text: match[1].trim(),
        completed: false,
        createdAt: getTimestamp(),
      };
      todoList.push(todo);
      return `✅ 已添加待办事项 #${todo.id}:${todo.text}`;
    }
  }

  // 查看待办
  if (input.includes('查看') || input.includes('列表')) {
    if (todoList.length === 0) {
      return '📋 待办列表为空';
    }
    const lines = ['📋 待办事项列表:'];
    for (const item of todoList) {
      const status = item.completed ? '✅' : '⬜';
      const strike = item.completed ? '~~' : '';
      lines.push(`${status} #${item.id} ${strike}${item.text}${strike}`);
    }
    return lines.join('\n');
  }

  // 完成待办
  const completeMatch = input.match(/完成[^\d]*(\d+)|(\d+)[^\d]*完成/);
  if (completeMatch) {
    const id = parseInt(completeMatch[1] || completeMatch[2]);
    const item = todoList.find(t => t.id === id);
    if (item) {
      item.completed = true;
      return `✅ 已完成:${item.text}`;
    }
    return `未找到待办事项 #${id}`;
  }

  // 删除待办
  const deleteMatch = input.match(/删除[^\d]*(\d+)|(\d+)[^\d]*删除/);
  if (deleteMatch) {
    const id = parseInt(deleteMatch[1] || deleteMatch[2]);
    const index = todoList.findIndex(t => t.id === id);
    if (index !== -1) {
      const removed = todoList.splice(index, 1)[0];
      return `🗑️ 已删除:${removed.text}`;
    }
    return `未找到待办事项 #${id}`;
  }

  return '待办事项支持:添加、查看、完成、删除。例如:"添加待办:买牛奶"、"查看待办"、"完成#1"、"删除#2"';
}

function handleJoke(): string {
  const joke = jokes[Math.floor(Math.random() * jokes.length)];
  return `😄 ${joke}`;
}

function handleCalc(input: string): string {
  // 货币换算
  const currencyMatch = input.match(/(\d+(?:\.\d+)?)\s*(美元|欧元|英镑|日元|港币)\s*(等于|兑换|转换成)\s*(人民币|美元|欧元|英镑|日元)?/i);
  if (currencyMatch) {
    const amount = parseFloat(currencyMatch[1]);
    const fromCurrency = currencyMatch[2];
    const toCurrency = currencyMatch[4] || '人民币';

    if (fromCurrency === '人民币' && toCurrency !== '人民币') {
      const rate = exchangeRates[toCurrency];
      if (rate) {
        return `${amount} 人民币 = ${(amount / rate).toFixed(2)} ${toCurrency}`;
      }
    } else if (toCurrency === '人民币') {
      const rate = exchangeRates[fromCurrency];
      if (rate) {
        return `${amount} ${fromCurrency} = ${(amount * rate).toFixed(2)} 人民币`;
      }
    }
  }

  // 次方运算
  const powerMatch = input.match(/(\d+)\s*(的|的|的)\s*(\d+)\s*(次方|次|幂)/);
  if (powerMatch) {
    const base = parseInt(powerMatch[1]);
    const exp = parseInt(powerMatch[3]);
    const result = Math.pow(base, exp);
    return `计算结果:${base}^${exp} = ${result.toLocaleString()}`;
  }

  // 长度换算
  const lengthMatch = input.match(/(\d+(?:\.\d+)?)\s*(公里|千米|km|英里|mile|米|m|英尺|feet|cm|厘米)\s*(等于|换算成|转换成)\s*(公里|千米|km|英里|mile|米|m|英尺|feet|厘米|cm)?/i);
  if (lengthMatch) {
    const amount = parseFloat(lengthMatch[1]);
    const fromUnit = lengthMatch[2].toLowerCase();
    const toUnit = (lengthMatch[4] || '米').toLowerCase();

    const unitMap: Record<string, string> = {
      '公里': '公里', '千米': '公里', 'km': '公里',
      '英里': '英里', 'mile': '英里',
      '米': '米', 'm': '米',
      '英尺': '英尺', 'feet': '英尺',
      '厘米': '厘米', 'cm': '厘米',
    };

    const from = unitMap[fromUnit] || fromUnit;
    const to = unitMap[toUnit] || toUnit;

    if (from === to) {
      return `${amount} ${from} = ${amount} ${to}`;
    }

    if (lengthConversions[from] && lengthConversions[from][to]) {
      const result = amount * lengthConversions[from][to];
      return `${amount} ${from} = ${result.toFixed(4)} ${to}`;
    }
  }

  // 基本表达式计算
  const evalMatch = input.match(/(\d+\.?\d*)\s*([+\-*/])\s*(\d+\.?\d*)/);
  if (evalMatch) {
    const a = parseFloat(evalMatch[1]);
    const op = evalMatch[2];
    const b = parseFloat(evalMatch[3]);
    let result: number;
    switch (op) {
      case '+': result = a + b; break;
      case '-': result = a - b; break;
      case '*': result = a * b; break;
      case '/': result = b !== 0 ? a / b : Infinity; break;
      default: return '不支持的操作符';
    }
    return `计算结果:${a} ${op} ${b} = ${result}`;
  }

  return '支持:\n• 货币换算:100美元等于多少人民币\n• 次方运算:2的10次方\n• 长度换算:1英里等于多少公里\n• 基本计算:100+50';
}

// ============= 消息处理 =============

function processMessage(input: string): string {
  // 移除命令前缀
  const text = input.replace(/^(讲个?|来[个条]?|random\s*joke)/i, '').trim() || input;

  if (text.includes('天气') || weatherDatabase[text]) {
    const city = Object.keys(weatherDatabase).find(c => text.includes(c));
    return handleWeather(city || text.replace(/天气[^天]*$/, '').trim() || '北京');
  }

  if (text.includes('待办') || text.includes('todo') || text.match(/完成|删除|#\d/)) {
    return handleTodo(text);
  }

  if (text.includes('笑话') || text.includes('幽默') || text.match(/^(讲个?|来[个条]?|joke)/i)) {
    return handleJoke();
  }

  if (text.includes('计算') || text.includes('换算') || text.includes('等于') || text.includes('次方')) {
    return handleCalc(text);
  }

  // 默认响应
  return `收到消息:${text}\n\n我是一个工具助手,支持以下功能:\n• 天气查询 - 如"北京天气"\n• 待办事项 - 如"添加待办:买牛奶"\n• 讲笑话 - 如"讲个笑话"\n• 快捷计算 - 如"100美元等于多少人民币"`;
}

// ============= A2A v1.0 Agent Card =============

const agentCard: AgentCardV1 = {
  name: 'tool_assistant_v1',
  description: 'A2A v1.0 工具助手 - 支持天气查询、待办事项、笑话和快捷计算',
  version: '1.0.0',
  capabilities: {
    streaming: false,
    pushNotifications: false,
    extendedAgentCard: false,
    stateTransitionHistory: false,
  },
  defaultInputModes: ['text/plain', 'application/json'],
  defaultOutputModes: ['text/plain', 'application/json'],
  skills: [
    {
      id: 'weather',
      name: '天气查询',
      description: '查询指定城市的天气信息',
      tags: ['weather', 'temperature', 'climate'],
      examples: ['北京天气', '上海今天怎么样', '查询深圳天气'],
    },
    {
      id: 'todo',
      name: '待办事项',
      description: '管理待办事项列表,支持添加、查看、标记完成和删除',
      tags: ['todo', 'task', 'list'],
      examples: ['添加待办:买牛奶', '查看待办', '完成#1', '删除#2'],
    },
    {
      id: 'joke',
      name: '讲笑话',
      description: '随机讲一个程序员笑话',
      tags: ['joke', 'humor', 'funny'],
      examples: ['讲个笑话', '来个笑话', 'joke'],
    },
    {
      id: 'quickcalc',
      name: '快捷计算',
      description: '支持货币换算、单位换算、次方运算',
      tags: ['calculator', 'conversion', 'math'],
      examples: ['100美元等于多少人民币', '1英里等于多少公里', '2的10次方'],
    },
  ],
  supportedInterfaces: [
    {
      protocolBinding: 'JSONRPC',
      url: 'http://localhost:5001/a2a/v1',
      protocolVersion: '1.0',
    },
    {
      protocolBinding: 'HTTP+JSON',
      url: 'http://localhost:5001/a2a/json',
      protocolVersion: '1.0',
    },
    {
      protocolBinding: 'GRPC',
      url: 'http://localhost:5003/a2a/grpc',
      protocolVersion: '1.0',
    },
  ],
};

// ============= Express 服务器 =============

const app = express();
app.use(express.json());

// 请求日志中间件
app.use((req, res, next) => {
  const timestamp = getTimestamp();
  console.log('\n' + '='.repeat(60));
  console.log(`[${timestamp}] 收到请求`);
  console.log(`📡 ${req.method} ${req.path}`);
  console.log('📦 Body:', JSON.stringify(req.body, null, 2));
  console.log('='.repeat(60));
  next();
});

// ============= A2A v1.0 端点 =============

// 1. Agent Card (v1.0 格式)
app.get('/.well-known/agent-card.json', (req, res) => {
  res.json(agentCard);
});

// 2. JSON-RPC 端点 (v1.0)
app.post('/a2a/v1', (req, res) => {
  const { jsonrpc, id, method, params } = req.body;

  console.log('\n📨 JSON-RPC 请求 (v1.0):');
  console.log(`   Method: ${method}`);

  if (method === 'SendMessage') {
    // v1.0: SendMessage (不是 message/send)
    const message = params?.message;
    const text = message?.parts?.[0]?.text || '';

    console.log(`   Role: ${message?.role} (v1.0: ROLE_USER/ROLE_AGENT)`);
    console.log(`   Text: ${text}`);

    const responseText = processMessage(text);

    // v1.0 响应格式
    const task: Task = {
      id: generateId(),
      status: {
        state: 'TASK_STATE_COMPLETED',
        timestamp: getTimestamp(),
      },
      createdAt: getTimestamp(),
      lastModified: getTimestamp(),
      messages: [
        {
          role: 'ROLE_USER',
          parts: [{ text }],
        },
        {
          role: 'ROLE_AGENT',
          parts: [{ text: responseText }],
        },
      ],
    };

    res.json({
      jsonrpc: '2.0',
      id,
      result: task,
    });
  } else {
    res.status(404).json({
      jsonrpc: '2.0',
      id,
      error: {
        code: -32601,
        message: `Method not found: ${method}`,
      },
    });
  }
});

// 3. REST 端点 (v1.0)
app.post('/a2a/json', (req, res) => {
  const { message } = req.body;

  console.log('\n📨 REST 请求 (v1.0):');
  console.log(`   Role: ${message?.role}`);
  console.log(`   Text: ${message?.parts?.[0]?.text}`);

  const text = message?.parts?.[0]?.text || '';
  const responseText = processMessage(text);

  // v1.0 REST 响应
  const task: Task = {
    id: generateId(),
    status: {
      state: 'TASK_STATE_COMPLETED',
      timestamp: getTimestamp(),
    },
    createdAt: getTimestamp(),
    lastModified: getTimestamp(),
    messages: [
      {
        role: message?.role || 'ROLE_USER',
        parts: [{ text }],
      },
      {
        role: 'ROLE_AGENT',
        parts: [{ text: responseText }],
      },
    ],
  };

  res.json(task);
});

// 4. 健康检查
app.get('/health', (req, res) => {
  res.json({
    status: 'healthy',
    timestamp: getTimestamp(),
    agent: 'tool_assistant_v1',
    protocol: 'A2A v1.0',
  });
});

// ============= 启动服务器 =============

const PORT = 5001;

app.listen(PORT, () => {
  console.log('\n' + '='.repeat(60));
  console.log('🤖 A2A Protocol v1.0 Agent 已启动');
  console.log('='.repeat(60));
  console.log('\n📍 Agent Card (v1.0):');
  console.log(`   curl http://localhost:${PORT}/.well-known/agent-card.json`);
  console.log('\n📍 支持两种协议绑定:');
  console.log('   1. JSON-RPC (A2A/JSONRPC):');
  console.log(`      curl -X POST http://localhost:${PORT}/a2a/v1 \\`);
  console.log(`        -H "Content-Type: application/json" \\`);
  console.log(`        -d '{`);
  console.log(`          "jsonrpc": "2.0",`);
  console.log(`          "id": 1,`);
  console.log(`          "method": "SendMessage",`);
  console.log(`          "params": {`);
  console.log(`            "message": {`);
  console.log(`              "role": "ROLE_USER",`);
  console.log(`              "parts": [{"text": "讲个笑话"}]`);
  console.log(`            }`);
  console.log(`          }`);
  console.log(`        }'`);
  console.log('\n   2. HTTP+JSON (A2A/HTTP+JSON):');
  console.log(`      curl -X POST http://localhost:${PORT}/a2a/json \\`);
  console.log(`        -H "Content-Type: application/json" \\`);
  console.log(`        -d '{`);
  console.log(`          "message": {`);
  console.log(`            "role": "ROLE_USER",`);
  console.log(`            "parts": [{"text": "北京天气"}]`);
  console.log(`          }`);
  console.log(`        }'`);
  console.log('\n📍 健康检查:');
  console.log(`   curl http://localhost:${PORT}/health`);
  console.log('\n' + '='.repeat(60));
  console.log('\n功能列表:');
  console.log('  • 天气查询 - 查询城市天气');
  console.log('  • 待办事项 - 管理待办列表');
  console.log('  • 讲笑话 - 程序员笑话');
  console.log('  • 快捷计算 - 货币换算、单位换算、次方运算');
  console.log('\n按 Ctrl+C 停止服务器\n');
});

流式 A2A 1.0 Agent

/**
 * A2A Protocol v1.0 Streaming Agent 实现
 * 
 * 与 v1 的主要区别:
 * 1. capabilities.streaming = true (支持流式回复)
 * 2. 功能简化:专注于流式文本回复
 */

import express from 'express';
import crypto from 'crypto';

// ============= A2A v1.0 类型定义 =============

interface AgentCardV1 {
  name: string;
  description: string;
  version: string;
  supportedInterfaces: SupportedInterface[];
  capabilities: {
    streaming: boolean;
    pushNotifications: boolean;
    extendedAgentCard: boolean;
    stateTransitionHistory: boolean;
  };
  defaultInputModes: string[];
  defaultOutputModes: string[];
  skills: Skill[];
  provider?: {
    organization: string;
    url?: string;
  };
  documentationUrl?: string;
  securitySchemes?: { [key: string]: any };
  securityRequirements?: Array<{ [key: string]: any }>;
  signatures?: Signature[];
  iconUrl?: string;
}

interface SupportedInterface {
  protocolBinding: string;
  url: string;
  protocolVersion: string;
}

interface Signature {
  signature: string;
  publicKey: {
    kid: string;
    jwk: {
      kty: string;
      n: string;
      e: string;
    };
  };
}

interface Skill {
  id: string;
  name: string;
  description: string;
  tags?: string[];
  examples?: string[];
  inputModes?: string[];
  outputModes?: string[];
}

interface Message {
  role: string;
  parts: Part[];
  extensions?: string[];
}

interface Part {
  text?: string;
  url?: string;
  raw?: string;
  filename?: string;
  mediaType?: string;
  data?: any;
}

interface Task {
  id: string;
  status: {
    state: string;
    timestamp: string;
  };
  createdAt: string;
  lastModified?: string;
  messages?: Message[];
  artifacts?: Artifact[];
  extensions?: string[];
}

interface Artifact {
  name?: string;
  description?: string;
  parts: Part[];
  index?: number;
  extensions?: string[];
}

// ============= 工具函数 =============

function getTimestamp(): string {
  return new Date().toISOString().replace(/\.(\d{3})Z$/, '.$1Z');
}

function generateId(): string {
  return crypto.randomUUID();
}

// ============= 流式文本生成 =============

interface StreamChunk {
  type: 'chunk' | 'done' | 'error';
  content?: string;
  taskId?: string;
  timestamp?: string;
}

async function* generateStreamingText(text: string, taskId: string): AsyncGenerator<StreamChunk> {
  // 根据输入生成响应内容
  const response = generateResponse(text);
  
  // 模拟流式输出:将文本逐字符/逐词发送
  const chars = response.split('');
  
  for (let i = 0; i < chars.length; i++) {
    // 添加小延迟模拟真实流式效果
    await new Promise(resolve => setTimeout(resolve, 20 + Math.random() * 30));
    
    yield {
      type: 'chunk',
      content: chars[i],
      taskId,
      timestamp: getTimestamp(),
    };
  }
  
  // 发送完成信号
  yield {
    type: 'done',
    taskId,
    timestamp: getTimestamp(),
  };
}

function generateResponse(input: string): string {
  const lowerInput = input.toLowerCase();
  
  if (lowerInput.includes('你好') || lowerInput.includes('hi') || lowerInput.includes('hello')) {
    return '你好!很高兴见到你!我是流式助手,可以实时回复你的消息。你有什么想聊的吗?';
  }
  
  if (lowerInput.includes('天气')) {
    return '今天天气很棒!阳光明媚,温度适宜,非常适合外出活动。不过具体天气还要看您所在的城市哦!';
  }
  
  if (lowerInput.includes('时间') || lowerInput.includes('几点了')) {
    const now = new Date();
    return `现在是 ${now.toLocaleString('zh-CN')},愿你今天过得愉快!`;
  }
  
  if (lowerInput.includes('叫什么') || lowerInput.includes('名字')) {
    return '我叫流式助手,是一个支持实时流式回复的 A2A Agent。我可以帮你回答问题、聊天互动!';
  }
  
  if (lowerInput.includes('能做什么') || lowerInput.includes('功能')) {
    return '我可以帮你:1) 回答各种问题 2) 陪你聊天解闷 3) 提供信息查询 4) 流式输出长文本。我的特点是回复会一个字一个字地显示出来!';
  }
  
  if (lowerInput.includes('笑话')) {
    return '好的,给你讲个笑话:一个程序员问他的电脑:"你用过百度吗?" 电脑回答:"用过,但我更喜欢 Google。" 程序员笑了:"你也会开玩笑!" 😂';
  }
  
  if (lowerInput.includes('谢谢') || lowerInput.includes('感谢')) {
    return '不客气!很高兴能帮到你。如果还有其他问题,随时问我哦!';
  }
  
  if (lowerInput.includes('bye') || lowerInput.includes('再见')) {
    return '再见!很高兴和你聊天,下次再见!';
  }
  
  // 默认回复
  return `收到你的消息:"${input}"\n\n我是一个流式助手,支持实时回复。你可以问我问题、聊天、查询信息等。我的回复会一个字一个字地显示出来,这就是流式输出的效果!`;
}

// ============= A2A v1.0 Agent Card (streaming = true) =============

const agentCard: AgentCardV1 = {
  name: 'streaming_assistant',
  description: 'A2A v1.0 流式助手 - 支持实时流式回复的智能助手',
  version: '1.0.0',
  capabilities: {
    streaming: true,
    pushNotifications: false,
    extendedAgentCard: false,
    stateTransitionHistory: false,
  },
  defaultInputModes: ['text/plain', 'application/json'],
  defaultOutputModes: ['text/plain', 'application/json'],
  skills: [
    {
      id: 'chat',
      name: '智能对话',
      description: '支持实时流式回复的智能对话功能',
      tags: ['chat', 'conversation', 'streaming'],
      examples: ['你好', '讲个笑话', '今天天气怎么样'],
    },
    {
      id: 'info',
      name: '信息查询',
      description: '提供各类信息的流式回复',
      tags: ['info', 'query', 'answer'],
      examples: ['现在几点了', '你叫什么名字', '你能做什么'],
    },
  ],
  supportedInterfaces: [
    {
      protocolBinding: 'JSONRPC',
      url: 'http://localhost:5002/a2a/v1',
      protocolVersion: '1.0',
    },
    {
      protocolBinding: 'HTTP+JSON',
      url: 'http://localhost:5002/a2a/json',
      protocolVersion: '1.0',
    },
    {
      protocolBinding: 'GRPC',
      url: 'http://localhost:5003/a2a/grpc',
      protocolVersion: '1.0',
    },
  ],
};

// ============= Express 服务器 =============

const app = express();
app.use(express.json());
app.set('json spaces', 2);

// 请求日志中间件
app.use((req, res, next) => {
  const timestamp = getTimestamp();
  console.log('\n' + '='.repeat(60));
  console.log(`[${timestamp}] 收到请求`);
  console.log(`📡 ${req.method} ${req.path}`);
  console.log('📦 Body:', JSON.stringify(req.body, null, 2));
  console.log('='.repeat(60));
  next();
});

// ============= A2A v1.0 端点 =============

// 1. Agent Card (v1.0 格式)
app.get('/.well-known/agent-card.json', (req, res) => {
  res.json(agentCard);
});

// 2. JSON-RPC 端点 (v1.0) - 非流式
app.post('/a2a/v1', (req, res) => {
  const { jsonrpc, id, method, params } = req.body;

  console.log('\n📨 JSON-RPC 请求 (v1.0):');
  console.log(`   Method: ${method}`);

  if (method === 'SendMessage') {
    const message = params?.message;
    const text = message?.parts?.[0]?.text || '';

    console.log(`   Role: ${message?.role}`);
    console.log(`   Text: ${text}`);

    const responseText = generateResponse(text);

    const task: Task = {
      id: generateId(),
      status: {
        state: 'TASK_STATE_COMPLETED',
        timestamp: getTimestamp(),
      },
      createdAt: getTimestamp(),
      lastModified: getTimestamp(),
      messages: [
        {
          role: 'ROLE_USER',
          parts: [{ text }],
        },
        {
          role: 'ROLE_AGENT',
          parts: [{ text: responseText }],
        },
      ],
    };

    res.json({
      jsonrpc: '2.0',
      id,
      result: task,
    });
  } else if (method === 'tasks/sendSubscribe') {
    // 流式消息处理
    const message = params?.message;
    const taskId = params?.taskId || generateId();
    const text = message?.parts?.[0]?.text || '';

    console.log(`   [Streaming] Task ID: ${taskId}`);
    console.log(`   [Streaming] Text: ${text}`);

    // 设置 SSE headers
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.setHeader('X-Accel-Buffering', 'no');

    // 发送任务开始事件
    res.write(`event: task/start\n`);
    res.write(`data: ${JSON.stringify({ taskId, status: 'started' })}\n\n`);

    // 流式发送内容
    (async () => {
      try {
        for await (const chunk of generateStreamingText(text, taskId)) {
          if (chunk.type === 'chunk') {
            res.write(`event: task/chunk\n`);
            res.write(`data: ${JSON.stringify({ content: chunk.content, taskId })}\n\n`);
          } else if (chunk.type === 'done') {
            res.write(`event: task/completed\n`);
            res.write(`data: ${JSON.stringify({ taskId, status: 'completed' })}\n\n`);
            res.end();
            break;
          }
        }
      } catch (error) {
        res.write(`event: task/error\n`);
        res.write(`data: ${JSON.stringify({ taskId, error: 'Stream error' })}\n\n`);
        res.end();
      }
    })();
  } else {
    res.status(404).json({
      jsonrpc: '2.0',
      id,
      error: {
        code: -32601,
        message: `Method not found: ${method}`,
      },
    });
  }
});

// 3. HTTP+JSON 端点 (支持流式和非流式)
app.post('/a2a/json', (req, res) => {
  // 判断是否为 SSE 流式请求 (通过 Accept header 或 taskId 参数判断)
  const acceptHeader = req.headers.accept || '';
  const isStreaming = acceptHeader.includes('text/event-stream') || req.body.taskId;

  const { message, taskId } = req.body;
  const id = taskId || generateId();
  const text = message?.parts?.[0]?.text || '';

  if (isStreaming) {
    // SSE 流式响应
    console.log('\n📨 流式请求 (SSE via HTTP+JSON):');
    console.log(`   Task ID: ${id}`);
    console.log(`   Text: ${text}`);

    // 设置 SSE headers
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.setHeader('X-Accel-Buffering', 'no');

    // 发送初始任务状态
    res.write(`event: task/start\n`);
    res.write(`data: ${JSON.stringify({ taskId: id, status: 'started' })}\n\n`);

    // 流式发送内容
    (async () => {
      try {
        for await (const chunk of generateStreamingText(text, id)) {
          if (chunk.type === 'chunk') {
            res.write(`event: task/chunk\n`);
            res.write(`data: ${JSON.stringify({ content: chunk.content, taskId: id })}\n\n`);
          } else if (chunk.type === 'done') {
            res.write(`event: task/completed\n`);
            res.write(`data: ${JSON.stringify({ taskId: id, status: 'completed' })}\n\n`);
            res.end();
            break;
          }
        }
      } catch (error) {
        res.write(`event: task/error\n`);
        res.write(`data: ${JSON.stringify({ taskId: id, error: 'Stream processing failed' })}\n\n`);
        res.end();
      }
    })();
  } else {
    // 非流式同步响应
    console.log('\n📨 HTTP+JSON 请求 (v1.0):');
    console.log(`   Role: ${message?.role}`);
    console.log(`   Text: ${text}`);

    const responseText = generateResponse(text);

    const task: Task = {
      id,
      status: {
        state: 'TASK_STATE_COMPLETED',
        timestamp: getTimestamp(),
      },
      createdAt: getTimestamp(),
      lastModified: getTimestamp(),
      messages: [
        {
          role: message?.role || 'ROLE_USER',
          parts: [{ text }],
        },
        {
          role: 'ROLE_AGENT',
          parts: [{ text: responseText }],
        },
      ],
    };

    res.json(task);
  }
});

// 5. HTML 测试页面
app.get('/', (req, res) => {
  res.send(`
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>A2A 流式助手</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 20px;
    }
    .container {
      background: white;
      border-radius: 20px;
      box-shadow: 0 20px 60px rgba(0,0,0,0.3);
      width: 100%;
      max-width: 600px;
      overflow: hidden;
    }
    .header {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 25px;
      text-align: center;
    }
    .header h1 { font-size: 1.5rem; margin-bottom: 5px; }
    .header p { opacity: 0.9; font-size: 0.9rem; }
    .chat-area {
      padding: 20px;
      height: 400px;
      overflow-y: auto;
      background: #f5f5f5;
    }
    .message {
      margin-bottom: 15px;
      animation: fadeIn 0.3s ease;
    }
    @keyframes fadeIn {
      from { opacity: 0; transform: translateY(10px); }
      to { opacity: 1; transform: translateY(0); }
    }
    .message.user { text-align: right; }
    .message .bubble {
      display: inline-block;
      padding: 12px 18px;
      border-radius: 18px;
      max-width: 80%;
      line-height: 1.5;
      word-break: break-word;
    }
    .message.user .bubble {
      background: #667eea;
      color: white;
      border-bottom-right-radius: 4px;
    }
    .message.assistant .bubble {
      background: white;
      color: #333;
      border-bottom-left-radius: 4px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }
    .typing {
      color: #999;
      font-style: italic;
      padding: 10px;
    }
    .input-area {
      padding: 20px;
      background: white;
      border-top: 1px solid #eee;
      display: flex;
      gap: 10px;
    }
    input {
      flex: 1;
      padding: 15px;
      border: 2px solid #eee;
      border-radius: 25px;
      font-size: 1rem;
      outline: none;
      transition: border-color 0.3s;
    }
    input:focus { border-color: #667eea; }
    button {
      padding: 15px 25px;
      background: #667eea;
      color: white;
      border: none;
      border-radius: 25px;
      font-size: 1rem;
      cursor: pointer;
      transition: transform 0.2s, background 0.2s;
    }
    button:hover { background: #5a6fd6; transform: scale(1.05); }
    button:disabled { background: #ccc; cursor: not-allowed; transform: none; }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <h1>🤖 A2A 流式助手</h1>
      <p>支持实时流式回复</p>
    </div>
    <div class="chat-area" id="chatArea">
      <div class="message assistant">
        <div class="bubble">你好!我是流式助手,可以实时回复你的消息。试试发送一条消息吧!</div>
      </div>
    </div>
    <div class="input-area">
      <input type="text" id="input" placeholder="输入消息..." onkeypress="if(event.key==='Enter')send()">
      <button onclick="send()" id="sendBtn">发送</button>
    </div>
  </div>

  <script>
    async function send() {
      const input = document.getElementById('input');
      const chatArea = document.getElementById('chatArea');
      const sendBtn = document.getElementById('sendBtn');
      const text = input.value.trim();
      
      if (!text) return;
      
      // 添加用户消息
      const userMsg = document.createElement('div');
      userMsg.className = 'message user';
      userMsg.innerHTML = '<div class="bubble">' + escapeHtml(text) + '</div>';
      chatArea.appendChild(userMsg);
      
      input.value = '';
      sendBtn.disabled = true;
      
      // 添加助手消息占位
      const assistantMsg = document.createElement('div');
      assistantMsg.className = 'message assistant';
      const bubble = document.createElement('div');
      bubble.className = 'bubble';
      assistantMsg.appendChild(bubble);
      chatArea.appendChild(assistantMsg);
      
// 使用 SSE 流式获取响应
        try {
          const response = await fetch('/a2a/json', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            message: { role: 'ROLE_USER', parts: [{ text }] },
            taskId: 'web-' + Date.now()
          })
        });
        
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          
          const chunk = decoder.decode(value);
          const lines = chunk.split('\\n');
          
          for (const line of lines) {
            if (line.startsWith('data: ')) {
              try {
                const data = JSON.parse(line.slice(6));
                if (data.content) {
                  bubble.textContent += data.content;
                  chatArea.scrollTop = chatArea.scrollHeight;
                }
              } catch (e) {}
            }
          }
        }
      } catch (error) {
        bubble.textContent = '抱歉,发生了错误。';
      }
      
      sendBtn.disabled = false;
      chatArea.scrollTop = chatArea.scrollHeight;
    }
    
    function escapeHtml(text) {
      const div = document.createElement('div');
      div.textContent = text;
      return div.innerHTML;
    }
  </script>
</body>
</html>
  `);
});

// 6. 健康检查
app.get('/health', (req, res) => {
  res.json({
    status: 'healthy',
    timestamp: getTimestamp(),
    agent: 'streaming_assistant',
    protocol: 'A2A v1.0',
    streaming: true,
  });
});

// ============= 启动服务器 =============

const PORT = 5002;

app.listen(PORT, () => {
  console.log('\n' + '='.repeat(60));
  console.log('🤖 A2A Protocol v1.0 Streaming Agent 已启动');
  console.log('='.repeat(60));
  console.log('\n📍 关键特性:');
  console.log('   • capabilities.streaming = true ✓');
  console.log('   • 支持 SSE 流式响应 ✓');
  console.log('\n📍 访问地址:');
  console.log(`   • Web 测试页面: http://localhost:${PORT}/`);
  console.log(`   • Agent Card: http://localhost:${PORT}/.well-known/agent-card.json`);
  console.log('\n📍 API 端点:');
  console.log(`   • JSON-RPC: http://localhost:${PORT}/a2a/v1`);
  console.log(`   • HTTP+JSON: http://localhost:${PORT}/a2a/json`);
  console.log(`   • HTTP+JSON: http://localhost:${PORT}/a2a/json (支持 SSE 流式)`);
  console.log('\n📍 测试命令:');
  console.log(`   curl -X POST http://localhost:${PORT}/a2a/json \\`);
  console.log(`     -H "Content-Type: application/json" \\`);
  console.log(`     -d '{"message":{"role":"ROLE_USER","parts":[{"text":"你好"}]}}'`);
  console.log('\n' + '='.repeat(60));
  console.log('\n功能说明:');
  console.log('  • 流式助手专注于流式文本回复');
  console.log('  • 支持智能对话、笑话、时间查询等功能');
  console.log('  • 回复会一个字一个字地实时显示');
  console.log('\n按 Ctrl+C 停止服务器\n');
});

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐