mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-20 08:58:03 +00:00
docs: 修正全部文档使其与源码实现一致
- Plugin.Start(sdk *PluginSDK) 接口签名改为指针 - 方法表重写: 移除 CallLLM/QueryKnowledge/SetMemory 等不存在方法 - IOInjector 参数顺序修正为 (source, channel, text) - 删除虚构 SDKConfig, 替换为实际 New() 构造函数签名 - .hmap 内容描述一致化 (plugin.so + plugin.dll + main.lua) - 添加 meta/ 包元数据文件
This commit is contained in:
7
example/a2a/go.mod
Normal file
7
example/a2a/go.mod
Normal file
@ -0,0 +1,7 @@
|
||||
module a2a
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0-20260708004841-e9bdcf9304b0
|
||||
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => ../..
|
||||
11
example/a2a/plg.json
Normal file
11
example/a2a/plg.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "a2a",
|
||||
"name_zh": "A2A 代理通信",
|
||||
"name_en": "A2A Agent Communication",
|
||||
"version": "1.0.0",
|
||||
"description": "Agent-to-Agent 协议通信插件,支持双向 A2A 通信:可查询其他 Agent 并回复其请求。提供 HTTP 服务端暴露本 Agent 能力。",
|
||||
"author": "HomeAgent",
|
||||
"entry": "plugin.so",
|
||||
"tags": ["a2a", "agent", "interop"],
|
||||
"targets": "linux/amd64"
|
||||
}
|
||||
370
example/a2a/plugin.go
Normal file
370
example/a2a/plugin.go
Normal file
@ -0,0 +1,370 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
sdk *sdk.PluginSDK
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
s.SetAutoRestart(true)
|
||||
p.sdk = s
|
||||
tp := p.name + "_"
|
||||
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "plugin." + p.name + ".listen", Default: "127.0.0.1:12000",
|
||||
Type: "string", DisplayName: "监听地址",
|
||||
Description: "A2A 服务端监听地址,设为空可禁用 HTTP 服务",
|
||||
Category: p.name,
|
||||
})
|
||||
|
||||
// Outbound: query + discover
|
||||
s.RegisterTool(tp+"a2a_query", sdk.ToolDef{
|
||||
Name: tp + "a2a_query", Description: "向另一个 A2A Agent 发送查询并获取回复",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"agent_url": map[string]interface{}{"type": "string", "description": "目标 Agent 的 A2A 端点 URL"},
|
||||
"query": map[string]interface{}{"type": "string", "description": "发送给目标 Agent 的文本查询"},
|
||||
"timeout": map[string]interface{}{"type": "integer", "description": "超时时间(秒),默认 60"},
|
||||
},
|
||||
"required": []string{"agent_url", "query"},
|
||||
},
|
||||
}, p.handleA2AQuery)
|
||||
|
||||
s.RegisterTool(tp+"a2a_discover", sdk.ToolDef{
|
||||
Name: tp + "a2a_discover", Description: "获取另一个 A2A Agent 的能力描述(Agent Card)",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"agent_url": map[string]interface{}{"type": "string", "description": "目标 Agent 的 A2A 端点 URL"},
|
||||
},
|
||||
"required": []string{"agent_url"},
|
||||
},
|
||||
}, p.handleA2ADiscover)
|
||||
|
||||
// Inbound HTTP server
|
||||
if addr, _ := s.Settings().Get("plugin." + p.name + ".listen"); addr != nil {
|
||||
if addrStr, ok := addr.(string); ok && addrStr != "" {
|
||||
p.startServer(addrStr)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[%s] started", p.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error {
|
||||
if p.server != nil {
|
||||
p.server.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- Inbound HTTP Server ----
|
||||
|
||||
func (p *Plugin) startServer(addr string) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/agent-card", p.handleAgentCard)
|
||||
mux.HandleFunc("/task", p.handleIncomingTask)
|
||||
mux.HandleFunc("/a2a", p.handleIncomingA2A)
|
||||
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Printf("[%s] listen %s: %v", p.name, addr, err)
|
||||
return
|
||||
}
|
||||
|
||||
p.server = &http.Server{Handler: mux}
|
||||
go func() {
|
||||
log.Printf("[%s] A2A server on %s", p.name, listener.Addr())
|
||||
if err := p.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("[%s] serve: %v", p.name, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Plugin) handleAgentCard(w http.ResponseWriter, r *http.Request) {
|
||||
card := map[string]interface{}{
|
||||
"name": p.name,
|
||||
"description": "HomeAgent A2A Agent - 支持多工具调用与记忆管理",
|
||||
"url": r.Host,
|
||||
"version": "1.0.0",
|
||||
"capabilities": []map[string]string{
|
||||
{"id": "a2a_query", "name": "查询", "description": "接收并处理文本查询"},
|
||||
{"id": "a2a_stream", "name": "流式响应", "description": "支持 SSE 流式回复"},
|
||||
},
|
||||
"skills": []map[string]string{
|
||||
{"id": "chat", "name": "对话", "description": "通用对话与问题回答"},
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(card)
|
||||
}
|
||||
|
||||
func (p *Plugin) handleIncomingA2A(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
p.handleAgentCard(w, r)
|
||||
return
|
||||
}
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var req struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID string `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params struct {
|
||||
Query string `json:"query,omitempty"`
|
||||
Message *struct {
|
||||
Role string `json:"role"`
|
||||
Parts []struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
} `json:"parts"`
|
||||
} `json:"message,omitempty"`
|
||||
} `json:"params,omitempty"`
|
||||
}
|
||||
json.Unmarshal(body, &req)
|
||||
|
||||
switch req.Method {
|
||||
case "tasks.send":
|
||||
// Extract query text
|
||||
queryText := req.Params.Query
|
||||
if queryText == "" && req.Params.Message != nil {
|
||||
for _, part := range req.Params.Message.Parts {
|
||||
if part.Text != "" {
|
||||
queryText += part.Text + "\n"
|
||||
}
|
||||
}
|
||||
queryText = strings.TrimSpace(queryText)
|
||||
}
|
||||
|
||||
// Inject into agent pipeline via interrupt (preempt current processing) or direct input
|
||||
if queryText != "" {
|
||||
p.sdk.InjectInterruptText("a2a", "webui", fmt.Sprintf("[来自A2A Agent的查询]\n%s", queryText))
|
||||
}
|
||||
|
||||
// Respond with task accepted
|
||||
resp := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"id": req.ID,
|
||||
"result": map[string]interface{}{
|
||||
"id": fmt.Sprintf("task_%d", time.Now().UnixNano()),
|
||||
"status": "submitted",
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
|
||||
case "tasks.get":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"jsonrpc": "2.0", "id": req.ID,
|
||||
"result": map[string]interface{}{"id": req.Params.Query, "status": "unknown"},
|
||||
})
|
||||
|
||||
default:
|
||||
http.Error(w, "unknown method", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) handleIncomingTask(w http.ResponseWriter, r *http.Request) {
|
||||
p.handleIncomingA2A(w, r)
|
||||
}
|
||||
|
||||
// ---- A2A Protocol Types ----
|
||||
|
||||
type A2AAgentCard struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
URL string `json:"url"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Capabilities []A2ACapability `json:"capabilities,omitempty"`
|
||||
Skills []A2ASkill `json:"skills,omitempty"`
|
||||
}
|
||||
|
||||
type A2ACapability struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type A2ASkill struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
InputSchema string `json:"input_schema,omitempty"`
|
||||
}
|
||||
|
||||
type A2ARequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID string `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params A2AParams `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type A2AParams struct {
|
||||
Query string `json:"query,omitempty"`
|
||||
Message *A2AMessage `json:"message,omitempty"`
|
||||
TaskID string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
type A2AResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID string `json:"id"`
|
||||
Result *A2AResult `json:"result,omitempty"`
|
||||
Error *A2AError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type A2AResult struct {
|
||||
TaskID string `json:"id,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Message *A2AMessage `json:"message,omitempty"`
|
||||
AgentCard *A2AAgentCard `json:"agent_card,omitempty"`
|
||||
}
|
||||
|
||||
type A2AMessage struct {
|
||||
Role string `json:"role"`
|
||||
Parts []A2APart `json:"parts"`
|
||||
}
|
||||
|
||||
type A2APart struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
type A2AError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ---- Outbound Handlers ----
|
||||
|
||||
func (p *Plugin) handleA2ADiscover(args map[string]interface{}) (interface{}, error) {
|
||||
agentURL, _ := args["agent_url"].(string)
|
||||
agentURL = strings.TrimRight(agentURL, "/")
|
||||
if !strings.HasPrefix(agentURL, "http://") && !strings.HasPrefix(agentURL, "https://") {
|
||||
agentURL = "http://" + agentURL
|
||||
}
|
||||
|
||||
cardURL := agentURL
|
||||
if !strings.HasSuffix(cardURL, "/agent-card") {
|
||||
cardURL = agentURL + "/agent-card"
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Get(cardURL)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("连接失败: %v", err)}, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("状态码 %d", resp.StatusCode), "raw_body": string(body)}, nil
|
||||
}
|
||||
|
||||
var card A2AAgentCard
|
||||
if err := json.Unmarshal(body, &card); err != nil {
|
||||
var fallback map[string]interface{}
|
||||
if err2 := json.Unmarshal(body, &fallback); err2 == nil {
|
||||
return map[string]interface{}{"agent_info": fallback, "format": "非标准格式"}, nil
|
||||
}
|
||||
return map[string]interface{}{"error": fmt.Sprintf("解析失败: %v", err), "raw_body": string(body)}, nil
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"name": card.Name, "description": card.Description,
|
||||
"version": card.Version, "url": card.URL,
|
||||
"capabilities": card.Capabilities, "skills": card.Skills,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleA2AQuery(args map[string]interface{}) (interface{}, error) {
|
||||
agentURL, _ := args["agent_url"].(string)
|
||||
query, _ := args["query"].(string)
|
||||
timeoutSec := 60
|
||||
if v, ok := args["timeout"].(float64); ok && v > 0 {
|
||||
timeoutSec = int(v)
|
||||
}
|
||||
|
||||
agentURL = strings.TrimRight(agentURL, "/")
|
||||
if !strings.HasPrefix(agentURL, "http://") && !strings.HasPrefix(agentURL, "https://") {
|
||||
agentURL = "http://" + agentURL
|
||||
}
|
||||
|
||||
taskURL := agentURL
|
||||
if strings.HasSuffix(agentURL, "/agent-card") {
|
||||
taskURL = strings.TrimSuffix(agentURL, "/agent-card")
|
||||
}
|
||||
taskURL = strings.TrimRight(taskURL, "/") + "/task"
|
||||
|
||||
reqBody := A2ARequest{
|
||||
JSONRPC: "2.0",
|
||||
ID: fmt.Sprintf("a2a_%d", time.Now().UnixNano()),
|
||||
Method: "tasks.send",
|
||||
Params: A2AParams{
|
||||
Message: &A2AMessage{Role: "user", Parts: []A2APart{{Text: query, Type: "text"}}},
|
||||
},
|
||||
}
|
||||
|
||||
bodyData, _ := json.Marshal(reqBody)
|
||||
client := &http.Client{Timeout: time.Duration(timeoutSec) * time.Second}
|
||||
resp, err := client.Post(taskURL, "application/json", bytes.NewReader(bodyData))
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("请求失败(超时%d秒): %v", timeoutSec, err)}, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("状态码 %d", resp.StatusCode), "raw_body": string(body)}, nil
|
||||
}
|
||||
|
||||
var a2aResp A2AResponse
|
||||
if err := json.Unmarshal(body, &a2aResp); err != nil {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("解析响应失败: %v", err), "raw_body": string(body)}, nil
|
||||
}
|
||||
|
||||
if a2aResp.Error != nil {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("Agent错误 [%d]: %s", a2aResp.Error.Code, a2aResp.Error.Message)}, nil
|
||||
}
|
||||
if a2aResp.Result == nil {
|
||||
return map[string]interface{}{"error": "空结果", "raw_body": string(body)}, nil
|
||||
}
|
||||
|
||||
var replyText string
|
||||
if a2aResp.Result.Message != nil {
|
||||
for _, part := range a2aResp.Result.Message.Parts {
|
||||
if part.Text != "" {
|
||||
replyText += part.Text + "\n"
|
||||
}
|
||||
}
|
||||
replyText = strings.TrimSpace(replyText)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"task_id": a2aResp.Result.TaskID, "status": a2aResp.Result.Status,
|
||||
"response": replyText,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return &Plugin{name: name}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user