feat: Phase 3 插件与工具系统 — Plugin SDK + Plugin Manager + 13内置插件 (40文件, 3293行)

- Plugin SDK: Plugin/Tool/ComplexTool/HostAPI 标准化接口
- Plugin Manager: 插件生命周期管理 (Install/Enable/Disable/Uninstall/Reload)
- Tool Registry: 聚合工具注册表 (Register/Execute/Dispatch)
- 13 个内置插件: 将原有硬编码工具迁移为标准插件格式
- REST API: 11 个端点 (net/http, 零外部依赖)
- ai-core 集成: PluginManagerClient 替代本地工具调用
- plugin.json 元数据: 每个插件含完整 author/version/category/permissions

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 15:50:19 +08:00
parent 87214b9441
commit 717ad65b05
42 changed files with 3797 additions and 0 deletions
@@ -0,0 +1,136 @@
package websearch
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/yourname/cyrene-ai/plugin-manager/internal/sdk"
)
type WebSearchPlugin struct {
sdk.BasePlugin
client *http.Client
}
func NewWebSearchPlugin() *WebSearchPlugin {
return &WebSearchPlugin{client: &http.Client{Timeout: 10 * time.Second}}
}
func (p *WebSearchPlugin) Metadata() sdk.PluginMetadata {
return sdk.PluginMetadata{
Name: "web_search", DisplayName: "Web Search", Version: "1.0.0",
Description: "Search the internet via DuckDuckGo Instant Answer API",
Category: "network", Author: sdk.PluginAuthor{Name: "Cyrene Team"},
}
}
func (p *WebSearchPlugin) Tools() []sdk.Tool { return []sdk.Tool{&WebSearchTool{client: p.client}} }
type WebSearchTool struct {
sdk.BaseTool
client *http.Client
}
type ddgResponse struct {
Abstract string `json:"Abstract"`
AbstractText string `json:"AbstractText"`
Answer string `json:"Answer"`
Heading string `json:"Heading"`
Results []ddgTopic `json:"Results"`
RelatedTopics []ddgTopic `json:"RelatedTopics"`
}
type ddgTopic struct {
FirstURL string `json:"FirstURL"`
Text string `json:"Text"`
}
func (t *WebSearchTool) Definition() sdk.ToolDefinition {
return sdk.ToolDefinition{
ID: "web_search", Name: "web_search", DisplayName: "Web Search",
Description: "Search the internet using DuckDuckGo Instant Answer API. Returns up to 5 results.",
Category: "network", Complexity: sdk.ComplexitySimple,
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{"query": map[string]interface{}{"type": "string"}},
"required": []string{"query"},
},
}
}
func (t *WebSearchTool) Validate(args map[string]interface{}) error {
if _, ok := args["query"]; !ok {
return fmt.Errorf("missing required parameter: query")
}
return nil
}
func (t *WebSearchTool) Execute(_ context.Context, args map[string]interface{}) (*sdk.ToolResult, error) {
query, _ := args["query"].(string)
apiURL := fmt.Sprintf("https://api.duckduckgo.com/?q=%s&format=json&no_html=1", url.QueryEscape(query))
resp, err := t.client.Get(apiURL)
if err != nil {
return &sdk.ToolResult{ToolName: "web_search", Success: false, Error: err.Error()}, nil
}
defer resp.Body.Close()
var result ddgResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return &sdk.ToolResult{ToolName: "web_search", Success: false, Error: err.Error()}, nil
}
var out strings.Builder
if result.Answer != "" {
out.WriteString(fmt.Sprintf("Answer: %s\n\n", result.Answer))
}
if result.AbstractText != "" {
text := result.AbstractText
if len([]rune(text)) > 500 {
text = string([]rune(text)[:500]) + "..."
}
out.WriteString(fmt.Sprintf("Abstract: %s\n\n", stripHTML(text)))
}
topics := result.Results
if len(topics) == 0 {
topics = result.RelatedTopics
}
count := 0
for _, topic := range topics {
if count >= 5 {
break
}
if topic.Text == "" {
continue
}
out.WriteString(fmt.Sprintf("%d. %s (%s)\n", count+1, stripHTML(topic.Text), topic.FirstURL))
count++
}
if out.Len() == 0 {
return &sdk.ToolResult{ToolName: "web_search", Success: true, Output: "No results found for: " + query}, nil
}
return &sdk.ToolResult{ToolName: "web_search", Success: true, Output: out.String()}, nil
}
func stripHTML(s string) string {
result := make([]rune, 0, len([]rune(s)))
inTag := false
for _, r := range s {
if r == '<' {
inTag = true
continue
}
if r == '>' {
inTag = false
continue
}
if !inTag {
result = append(result, r)
}
}
return strings.TrimSpace(string(result))
}
@@ -0,0 +1,12 @@
{
"name": "web_search",
"displayName": "Web Search",
"version": "1.0.0",
"minCyreneVersion": "1.0.0",
"author": { "name": "Cyrene Team" },
"description": "Search the internet via DuckDuckGo Instant Answer API",
"license": "MIT",
"keywords": ["search", "web", "duckduckgo"],
"category": "network",
"permissions": ["network:outbound"]
}