Initial commit: Cyrene Plugins SDK + Plugin Manager

Extracted from Cyrene main repo (backend/pkg/plugins + backend/plugin-manager).
Contains SDK interfaces (Plugin/Tool/HostAPI), 13 built-in plugins,
ToolRegistry with call log ring buffer, and Plugin Manager REST API service.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 09:49:12 +08:00
commit 5c807d76a0
27 changed files with 3609 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
package text
import (
"context"
"fmt"
"regexp"
"strings"
"unicode"
"git.yeij.top/AskaEth/Cyrene-Plugins/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
}