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:
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user