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,177 @@
package text
import (
"context"
"fmt"
"regexp"
"strings"
"unicode"
"github.com/yourname/cyrene-ai/plugin-manager/internal/sdk"
)
type TextPlugin struct{ sdk.BasePlugin }
func (p *TextPlugin) Metadata() sdk.PluginMetadata {
return sdk.PluginMetadata{
Name: "text", DisplayName: "Text Processing", Version: "1.0.0",
Description: "Text processing: count stats, summarize, regex extract",
Category: "utility", Author: sdk.PluginAuthor{Name: "Cyrene Team"},
}
}
func (p *TextPlugin) Tools() []sdk.Tool { return []sdk.Tool{&TextTool{}} }
type TextTool struct{ sdk.BaseTool }
func (t *TextTool) Definition() sdk.ToolDefinition {
return sdk.ToolDefinition{
ID: "text", Name: "text", DisplayName: "Text Processing",
Description: "Text processing. Count stats, summarize, translate, regex extract.",
Category: "utility", Complexity: sdk.ComplexitySimple,
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{"type": "string", "enum": []string{"count", "summarize", "translate", "extract"}},
"text": map[string]interface{}{"type": "string"},
"target_lang": map[string]interface{}{"type": "string", "enum": []string{"en", "zh", "ja", "ko", "fr", "de"}},
"pattern": map[string]interface{}{"type": "string"},
},
"required": []string{"action", "text"},
},
}
}
func (t *TextTool) Validate(args map[string]interface{}) error {
for _, k := range []string{"action", "text"} {
if _, ok := args[k]; !ok {
return fmt.Errorf("missing required parameter: %s", k)
}
}
return nil
}
func (t *TextTool) Execute(_ context.Context, args map[string]interface{}) (*sdk.ToolResult, error) {
action, _ := args["action"].(string)
txt, _ := args["text"].(string)
switch action {
case "count":
charsNoSpace := 0
chineseChars := 0
for _, r := range txt {
if !unicode.IsSpace(r) {
charsNoSpace++
}
if unicode.Is(unicode.Han, r) {
chineseChars++
}
}
words := strings.Fields(txt)
lines := strings.Split(txt, "\n")
paragraphs := regexp.MustCompile(`\n\s*\n`).Split(txt, -1)
return &sdk.ToolResult{ToolName: "text", Success: true, Output: fmt.Sprintf(
"Characters: %d (no spaces: %d, Chinese: %d)\nBytes: %d\nWords: %d\nLines: %d\nParagraphs: %d",
len([]rune(txt)), charsNoSpace, chineseChars, len(txt), len(words), len(lines), len(paragraphs))}, nil
case "summarize":
paragraphs := regexp.MustCompile(`\n\s*\n`).Split(txt, -1)
firstPara := ""
if len(paragraphs) > 0 {
runes := []rune(paragraphs[0])
if len(runes) > 300 {
runes = runes[:300]
}
firstPara = string(runes)
}
sentences := regexp.MustCompile(`[。!?.!?]+`).Split(txt, -1)
keywords := []string{"重要", "关键", "因此", "总结", "important", "key", "conclusion", "therefore"}
type scored struct {
text string
score int
}
var scoredSents []scored
for _, s := range sentences {
s = strings.TrimSpace(s)
if len([]rune(s)) < 10 {
continue
}
score := len([]rune(s))
for _, kw := range keywords {
if strings.Contains(strings.ToLower(s), strings.ToLower(kw)) {
score += 20
}
}
scoredSents = append(scoredSents, scored{s, score})
}
var out strings.Builder
out.WriteString(fmt.Sprintf("First paragraph: %s\n\nKey sentences:\n", firstPara))
count := 0
for i := 0; i < len(scoredSents) && count < 5; i++ {
out.WriteString(fmt.Sprintf("- %s\n", scoredSents[i].text))
count++
}
return &sdk.ToolResult{ToolName: "text", Success: true, Output: out.String()}, nil
case "translate":
targetLang, _ := args["target_lang"].(string)
if targetLang == "" {
targetLang = "en"
}
return &sdk.ToolResult{ToolName: "text", Success: true, Output: fmt.Sprintf(
"[Translation request] Please translate the following text to %s.\n\nOriginal text:\n%s", targetLang, txt)}, nil
case "extract":
pattern, _ := args["pattern"].(string)
var out strings.Builder
extracted := false
if pattern == "" || pattern == "email" {
re := regexp.MustCompile(`[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`)
if matches := re.FindAllString(txt, -1); len(matches) > 0 {
out.WriteString("Emails:\n")
for _, m := range matches {
out.WriteString(fmt.Sprintf("- %s\n", m))
}
extracted = true
}
}
if pattern == "" || pattern == "phone" {
re := regexp.MustCompile(`1[3-9]\d{9}`)
if matches := re.FindAllString(txt, -1); len(matches) > 0 {
out.WriteString("Phone numbers:\n")
for _, m := range matches {
out.WriteString(fmt.Sprintf("- %s\n", m))
}
extracted = true
}
}
if pattern == "" || pattern == "url" {
re := regexp.MustCompile(`https?://[^\s<>"{}|\\^` + "`" + `\[\]]+`)
if matches := re.FindAllString(txt, -1); len(matches) > 0 {
out.WriteString("URLs:\n")
for _, m := range matches {
out.WriteString(fmt.Sprintf("- %s\n", m))
}
extracted = true
}
}
if !extracted && pattern != "" && pattern != "email" && pattern != "phone" && pattern != "url" {
re, err := regexp.Compile(pattern)
if err != nil {
return &sdk.ToolResult{ToolName: "text", Success: false, Error: "Invalid regex: " + err.Error()}, nil
}
if matches := re.FindAllString(txt, -1); len(matches) > 0 {
out.WriteString(fmt.Sprintf("Pattern matches (%s):\n", pattern))
for _, m := range matches {
out.WriteString(fmt.Sprintf("- %s\n", m))
}
extracted = true
}
}
if !extracted {
return &sdk.ToolResult{ToolName: "text", Success: true, Output: "No matches found"}, nil
}
return &sdk.ToolResult{ToolName: "text", Success: true, Output: out.String()}, nil
}
return &sdk.ToolResult{ToolName: "text", Success: false, Error: "unknown action: " + action}, nil
}
@@ -0,0 +1,11 @@
{
"name": "text",
"displayName": "Text Processing",
"version": "1.0.0",
"minCyreneVersion": "1.0.0",
"author": { "name": "Cyrene Team" },
"description": "Text processing: count stats, summarize, regex extract",
"license": "MIT",
"keywords": ["text", "count", "summarize", "extract"],
"category": "utility"
}