717ad65b05
- 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>
137 lines
3.6 KiB
Go
137 lines
3.6 KiB
Go
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))
|
|
}
|