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,32 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
type Config struct {
|
||||
Port string
|
||||
Env string
|
||||
DataDir string
|
||||
IoTSvcURL string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
cfg := &Config{
|
||||
Port: "8094",
|
||||
Env: "development",
|
||||
DataDir: "./data",
|
||||
IoTSvcURL: "http://localhost:8093",
|
||||
}
|
||||
if v := os.Getenv("PORT"); v != "" {
|
||||
cfg.Port = v
|
||||
}
|
||||
if v := os.Getenv("ENV"); v != "" {
|
||||
cfg.Env = v
|
||||
}
|
||||
if v := os.Getenv("DATA_DIR"); v != "" {
|
||||
cfg.DataDir = v
|
||||
}
|
||||
if v := os.Getenv("IOT_SERVICE_URL"); v != "" {
|
||||
cfg.IoTSvcURL = v
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/yourname/cyrene-ai/plugin-manager/internal/manager"
|
||||
)
|
||||
|
||||
// PluginHandler exposes the Plugin Manager REST API via net/http.
|
||||
type PluginHandler struct {
|
||||
mgr *manager.PluginManager
|
||||
}
|
||||
|
||||
func NewPluginHandler(mgr *manager.PluginManager) *PluginHandler {
|
||||
return &PluginHandler{mgr: mgr}
|
||||
}
|
||||
|
||||
func (h *PluginHandler) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/plugins", h.listPlugins)
|
||||
mux.HandleFunc("/api/v1/plugins/", h.pluginRoute)
|
||||
mux.HandleFunc("/api/v1/tools", h.listTools)
|
||||
mux.HandleFunc("/api/v1/tools/", h.toolRoute)
|
||||
mux.HandleFunc("/health", h.health)
|
||||
}
|
||||
|
||||
func (h *PluginHandler) health(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"status": "ok", "service": "plugin-manager"})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) listPlugins(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
plugins := h.mgr.List()
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"plugins": plugins, "total": len(plugins)})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) pluginRoute(w http.ResponseWriter, r *http.Request) {
|
||||
// Path: /api/v1/plugins/{id}[/enable|/disable|/reload|/tools]
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/v1/plugins/")
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
pluginID := parts[0]
|
||||
|
||||
if pluginID == "" {
|
||||
// GET /api/v1/plugins (handled by listPlugins normally)
|
||||
h.listPlugins(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if len(parts) == 1 {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
h.getPlugin(w, pluginID)
|
||||
case "DELETE":
|
||||
h.uninstallPlugin(w, r, pluginID)
|
||||
default:
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
action := parts[1]
|
||||
switch action {
|
||||
case "enable":
|
||||
if r.Method != "POST" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
h.enablePlugin(w, r, pluginID)
|
||||
case "disable":
|
||||
if r.Method != "POST" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
h.disablePlugin(w, r, pluginID)
|
||||
case "reload":
|
||||
if r.Method != "POST" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
h.reloadPlugin(w, r, pluginID)
|
||||
case "tools":
|
||||
if r.Method != "GET" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
h.pluginTools(w, pluginID)
|
||||
default:
|
||||
writeJSON(w, http.StatusNotFound, errResp("not found"))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PluginHandler) getPlugin(w http.ResponseWriter, id string) {
|
||||
info, ok := h.mgr.Get(id)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, errResp("plugin not found"))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, info)
|
||||
}
|
||||
|
||||
func (h *PluginHandler) enablePlugin(w http.ResponseWriter, r *http.Request, id string) {
|
||||
if err := h.mgr.Enable(r.Context(), id); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "enabled"})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) disablePlugin(w http.ResponseWriter, r *http.Request, id string) {
|
||||
if err := h.mgr.Disable(r.Context(), id); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "disabled"})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) reloadPlugin(w http.ResponseWriter, r *http.Request, id string) {
|
||||
if err := h.mgr.Reload(r.Context(), id); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "reloaded"})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) uninstallPlugin(w http.ResponseWriter, r *http.Request, id string) {
|
||||
if err := h.mgr.Uninstall(r.Context(), id); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "uninstalled"})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) pluginTools(w http.ResponseWriter, id string) {
|
||||
info, ok := h.mgr.Get(id)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, errResp("plugin not found"))
|
||||
return
|
||||
}
|
||||
registry := h.mgr.Registry()
|
||||
tools := make([]interface{}, 0)
|
||||
for _, toolID := range info.Tools {
|
||||
if t, ok := registry.Get(toolID); ok {
|
||||
tools = append(tools, t.Definition())
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"tools": tools, "total": len(tools)})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) listTools(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
defs := h.mgr.Registry().Definitions()
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"tools": defs, "total": len(defs)})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) toolRoute(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/v1/tools/")
|
||||
toolID := path
|
||||
|
||||
// Check if this is an execute call
|
||||
if strings.HasSuffix(path, "/execute") {
|
||||
toolID = strings.TrimSuffix(path, "/execute")
|
||||
if r.Method != "POST" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
h.executeTool(w, r, toolID)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
tool, ok := h.mgr.Registry().Get(toolID)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, errResp("tool not found"))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, tool.Definition())
|
||||
}
|
||||
|
||||
func (h *PluginHandler) executeTool(w http.ResponseWriter, r *http.Request, toolID string) {
|
||||
var body struct {
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, errResp("invalid request body"))
|
||||
return
|
||||
}
|
||||
result, err := h.mgr.Registry().Execute(r.Context(), toolID, body.Arguments)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func errResp(msg string) map[string]string {
|
||||
return map[string]string{"error": msg}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yourname/cyrene-ai/plugin-manager/internal/sdk"
|
||||
)
|
||||
|
||||
// PluginManager manages the lifecycle of all plugins and their tools.
|
||||
type PluginManager struct {
|
||||
mu sync.RWMutex
|
||||
plugins map[string]*pluginEntry // plugin name -> entry
|
||||
registry *ToolRegistry
|
||||
host sdk.HostAPI
|
||||
}
|
||||
|
||||
type pluginEntry struct {
|
||||
instance sdk.Plugin
|
||||
info sdk.PluginInfo
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewPluginManager(registry *ToolRegistry, host sdk.HostAPI) *PluginManager {
|
||||
return &PluginManager{
|
||||
plugins: make(map[string]*pluginEntry),
|
||||
registry: registry,
|
||||
host: host,
|
||||
}
|
||||
}
|
||||
|
||||
// Install registers a plugin instance. For built-in plugins this is called at startup.
|
||||
func (m *PluginManager) Install(plugin sdk.Plugin) error {
|
||||
meta := plugin.Metadata()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if _, exists := m.plugins[meta.Name]; exists {
|
||||
return fmt.Errorf("plugin %q is already installed", meta.Name)
|
||||
}
|
||||
|
||||
m.plugins[meta.Name] = &pluginEntry{
|
||||
instance: plugin,
|
||||
info: sdk.PluginInfo{
|
||||
Metadata: meta,
|
||||
Status: sdk.StatusInstalled,
|
||||
InstalledAt: time.Now(),
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enable activates a plugin: Init → register tools → Start.
|
||||
func (m *PluginManager) Enable(ctx context.Context, pluginName string) error {
|
||||
m.mu.Lock()
|
||||
entry, ok := m.plugins[pluginName]
|
||||
m.mu.Unlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin %q not found", pluginName)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
entry.info.Status = sdk.StatusLoaded
|
||||
m.mu.Unlock()
|
||||
|
||||
meta := entry.instance.Metadata()
|
||||
if err := entry.instance.Init(ctx, nil); err != nil {
|
||||
m.mu.Lock()
|
||||
entry.info.Status = sdk.StatusError
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("plugin %q init failed: %w", meta.Name, err)
|
||||
}
|
||||
|
||||
pluginCtx, cancel := context.WithCancel(context.Background())
|
||||
if err := entry.instance.Start(pluginCtx, m.host); err != nil {
|
||||
cancel()
|
||||
m.mu.Lock()
|
||||
entry.info.Status = sdk.StatusError
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("plugin %q start failed: %w", meta.Name, err)
|
||||
}
|
||||
|
||||
// Register all tools from this plugin.
|
||||
tools := entry.instance.Tools()
|
||||
toolIDs := make([]string, 0, len(tools))
|
||||
for _, t := range tools {
|
||||
if err := m.registry.Register(t); err != nil {
|
||||
m.registry.UnregisterAll(toolIDs)
|
||||
cancel()
|
||||
m.mu.Lock()
|
||||
entry.info.Status = sdk.StatusError
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("plugin %q tool register failed: %w", meta.Name, err)
|
||||
}
|
||||
toolIDs = append(toolIDs, t.Definition().ID)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
entry.cancel = cancel
|
||||
entry.info.Status = sdk.StatusRunning
|
||||
entry.info.Enabled = true
|
||||
entry.info.Tools = toolIDs
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disable stops a plugin and unregisters its tools.
|
||||
func (m *PluginManager) Disable(ctx context.Context, pluginName string) error {
|
||||
m.mu.Lock()
|
||||
entry, ok := m.plugins[pluginName]
|
||||
m.mu.Unlock()
|
||||
if !ok {
|
||||
return fmt.Errorf("plugin %q not found", pluginName)
|
||||
}
|
||||
|
||||
if err := entry.instance.Stop(ctx); err != nil {
|
||||
return fmt.Errorf("plugin %q stop failed: %w", pluginName, err)
|
||||
}
|
||||
if entry.cancel != nil {
|
||||
entry.cancel()
|
||||
}
|
||||
|
||||
m.registry.UnregisterAll(entry.info.Tools)
|
||||
|
||||
m.mu.Lock()
|
||||
entry.info.Status = sdk.StatusDisabled
|
||||
entry.info.Enabled = false
|
||||
entry.info.Tools = nil
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uninstall removes a plugin completely.
|
||||
func (m *PluginManager) Uninstall(ctx context.Context, pluginName string) error {
|
||||
if err := m.Disable(ctx, pluginName); err != nil {
|
||||
// If already disabled, continue.
|
||||
if entry, ok := m.plugins[pluginName]; !ok || entry.info.Status != sdk.StatusRunning {
|
||||
// not running, skip stop
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.plugins, pluginName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns info for all installed plugins.
|
||||
func (m *PluginManager) List() []sdk.PluginInfo {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
result := make([]sdk.PluginInfo, 0, len(m.plugins))
|
||||
for _, entry := range m.plugins {
|
||||
result = append(result, entry.info)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Get returns info for a single plugin.
|
||||
func (m *PluginManager) Get(pluginName string) (*sdk.PluginInfo, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
entry, ok := m.plugins[pluginName]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
info := entry.info // copy
|
||||
return &info, true
|
||||
}
|
||||
|
||||
// Reload stops and re-starts a plugin.
|
||||
func (m *PluginManager) Reload(ctx context.Context, pluginName string) error {
|
||||
if err := m.Disable(ctx, pluginName); err != nil {
|
||||
return fmt.Errorf("reload disable: %w", err)
|
||||
}
|
||||
return m.Enable(ctx, pluginName)
|
||||
}
|
||||
|
||||
// EnableAll starts all installed plugins.
|
||||
func (m *PluginManager) EnableAll(ctx context.Context) []error {
|
||||
m.mu.RLock()
|
||||
names := make([]string, 0, len(m.plugins))
|
||||
for name := range m.plugins {
|
||||
names = append(names, name)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
var errs []error
|
||||
for _, name := range names {
|
||||
if err := m.Enable(ctx, name); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", name, err))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Shutdown stops all running plugins gracefully.
|
||||
func (m *PluginManager) Shutdown(ctx context.Context) []error {
|
||||
m.mu.RLock()
|
||||
names := make([]string, 0, len(m.plugins))
|
||||
for name, entry := range m.plugins {
|
||||
if entry.info.Status == sdk.StatusRunning {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
var errs []error
|
||||
for _, name := range names {
|
||||
if err := m.Disable(ctx, name); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// Registry returns the aggregated tool registry.
|
||||
func (m *PluginManager) Registry() *ToolRegistry {
|
||||
return m.registry
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/yourname/cyrene-ai/plugin-manager/internal/sdk"
|
||||
)
|
||||
|
||||
// ToolRegistry aggregates tool definitions from all running plugins and dispatches execution.
|
||||
type ToolRegistry struct {
|
||||
mu sync.RWMutex
|
||||
tools map[string]sdk.Tool // tool ID -> Tool
|
||||
}
|
||||
|
||||
func NewToolRegistry() *ToolRegistry {
|
||||
return &ToolRegistry{tools: make(map[string]sdk.Tool)}
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Register(tool sdk.Tool) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
id := tool.Definition().ID
|
||||
if _, exists := r.tools[id]; exists {
|
||||
return fmt.Errorf("tool %q already registered", id)
|
||||
}
|
||||
r.tools[id] = tool
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Unregister(toolID string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
delete(r.tools, toolID)
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Get(toolID string) (sdk.Tool, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
t, ok := r.tools[toolID]
|
||||
return t, ok
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) List() []sdk.Tool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
result := make([]sdk.Tool, 0, len(r.tools))
|
||||
for _, t := range r.tools {
|
||||
result = append(result, t)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Definitions() []sdk.ToolDefinition {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
defs := make([]sdk.ToolDefinition, 0, len(r.tools))
|
||||
for _, t := range r.tools {
|
||||
defs = append(defs, t.Definition())
|
||||
}
|
||||
return defs
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Execute(ctx context.Context, toolID string, args map[string]interface{}) (*sdk.ToolResult, error) {
|
||||
r.mu.RLock()
|
||||
tool, ok := r.tools[toolID]
|
||||
r.mu.RUnlock()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("tool %q not found", toolID)
|
||||
}
|
||||
if err := tool.Validate(args); err != nil {
|
||||
return &sdk.ToolResult{Success: false, Error: err.Error()}, nil
|
||||
}
|
||||
return tool.Execute(ctx, args)
|
||||
}
|
||||
|
||||
// UnregisterAll removes all tools matching a prefix (plugin's tools).
|
||||
func (r *ToolRegistry) UnregisterAll(toolIDs []string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for _, id := range toolIDs {
|
||||
delete(r.tools, id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// BasePlugin provides default implementations for optional Plugin methods.
|
||||
type BasePlugin struct{}
|
||||
|
||||
func (BasePlugin) Init(_ context.Context, _ PluginConfig) error { return nil }
|
||||
|
||||
func (BasePlugin) Start(_ context.Context, _ HostAPI) error { return nil }
|
||||
|
||||
func (BasePlugin) Stop(_ context.Context) error { return nil }
|
||||
|
||||
func (BasePlugin) Health(_ context.Context) error { return nil }
|
||||
|
||||
// BaseTool provides a Validate default that checks required parameters.
|
||||
type BaseTool struct {
|
||||
Def ToolDefinition
|
||||
Required []string
|
||||
}
|
||||
|
||||
func (b BaseTool) Definition() ToolDefinition { return b.Def }
|
||||
|
||||
func (b BaseTool) Complexity() ToolComplexity { return ComplexitySimple }
|
||||
|
||||
func (b BaseTool) Validate(args map[string]interface{}) error {
|
||||
for _, key := range b.Required {
|
||||
if _, ok := args[key]; !ok {
|
||||
return fmt.Errorf("missing required parameter: %s", key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b BaseTool) Execute(_ context.Context, _ map[string]interface{}) (*ToolResult, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package sdk
|
||||
|
||||
// PluginPermissions defines what a plugin is allowed to do.
|
||||
type PluginPermissions struct {
|
||||
NetworkAllowed bool `json:"networkAllowed"`
|
||||
AllowedHosts []string `json:"allowedHosts,omitempty"`
|
||||
IoTRead bool `json:"iotRead"`
|
||||
IoTWrite bool `json:"iotWrite"`
|
||||
MemoryRead bool `json:"memoryRead"`
|
||||
MemoryWrite bool `json:"memoryWrite"`
|
||||
FileRead bool `json:"fileRead"`
|
||||
FileWrite bool `json:"fileWrite"`
|
||||
AllowedPaths []string `json:"allowedPaths,omitempty"`
|
||||
ExecAllowed bool `json:"execAllowed"`
|
||||
MaxCPUPercent float64 `json:"maxCPUPercent"`
|
||||
MaxMemoryMB int `json:"maxMemoryMB"`
|
||||
}
|
||||
|
||||
// DefaultPermissions returns a safe default permission set.
|
||||
func DefaultPermissions() PluginPermissions {
|
||||
return PluginPermissions{
|
||||
NetworkAllowed: false,
|
||||
AllowedHosts: []string{},
|
||||
IoTRead: false,
|
||||
IoTWrite: false,
|
||||
MemoryRead: false,
|
||||
MemoryWrite: false,
|
||||
FileRead: false,
|
||||
FileWrite: false,
|
||||
AllowedPaths: []string{},
|
||||
ExecAllowed: false,
|
||||
MaxCPUPercent: 10.0,
|
||||
MaxMemoryMB: 128,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Plugin is the main interface every plugin must implement.
|
||||
type Plugin interface {
|
||||
Metadata() PluginMetadata
|
||||
Init(ctx context.Context, config PluginConfig) error
|
||||
Start(ctx context.Context, host HostAPI) error
|
||||
Stop(ctx context.Context) error
|
||||
Health(ctx context.Context) error
|
||||
Tools() []Tool
|
||||
}
|
||||
|
||||
// Tool is the interface every tool must implement.
|
||||
type Tool interface {
|
||||
Definition() ToolDefinition
|
||||
Execute(ctx context.Context, args map[string]interface{}) (*ToolResult, error)
|
||||
Validate(args map[string]interface{}) error
|
||||
Complexity() ToolComplexity
|
||||
}
|
||||
|
||||
// ComplexTool extends Tool for async multi-round execution.
|
||||
type ComplexTool interface {
|
||||
Tool
|
||||
ExecuteAsync(ctx context.Context, args map[string]interface{}) (<-chan ToolProgress, error)
|
||||
Cancel(ctx context.Context, executionID string) error
|
||||
}
|
||||
|
||||
// HostAPI gives plugins access to Cyrene core capabilities.
|
||||
type HostAPI interface {
|
||||
CallLLM(ctx context.Context, messages []LLMMessage) (*LLMResponse, error)
|
||||
SearchMemory(ctx context.Context, userID, query string, limit int) ([]MemoryEntry, error)
|
||||
StoreMemory(ctx context.Context, entry MemoryEntry) error
|
||||
Logger() Logger
|
||||
GetConfig(key string) (string, error)
|
||||
SetConfig(key, value string) error
|
||||
PublishEvent(ctx context.Context, event map[string]interface{}) error
|
||||
HTTPClient() *http.Client
|
||||
}
|
||||
|
||||
// Logger is a minimal logging interface for plugins.
|
||||
type Logger interface {
|
||||
Printf(format string, args ...interface{})
|
||||
Println(args ...interface{})
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package sdk
|
||||
|
||||
import "time"
|
||||
|
||||
// ToolComplexity grades tools into simple (single-call, <2s) and complex (multi-round, async).
|
||||
type ToolComplexity string
|
||||
|
||||
const (
|
||||
ComplexitySimple ToolComplexity = "simple"
|
||||
ComplexityComplex ToolComplexity = "complex"
|
||||
)
|
||||
|
||||
// PluginMetadata describes a plugin's identity and requirements.
|
||||
type PluginMetadata struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Version string `json:"version"`
|
||||
MinCyreneVersion string `json:"minCyreneVersion"`
|
||||
Author PluginAuthor `json:"author"`
|
||||
Description string `json:"description"`
|
||||
License string `json:"license"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Category string `json:"category"`
|
||||
Dependencies map[string]string `json:"dependencies,omitempty"` // plugin name -> version range
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
}
|
||||
|
||||
type PluginAuthor struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// PluginConfig holds runtime configuration for a plugin.
|
||||
type PluginConfig map[string]interface{}
|
||||
|
||||
// ToolDefinition describes a tool's interface for LLM function calling.
|
||||
type ToolDefinition struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
Complexity ToolComplexity `json:"complexity"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
Returns map[string]interface{} `json:"returns,omitempty"`
|
||||
TimeoutMs int `json:"timeout_ms,omitempty"`
|
||||
MaxRetries int `json:"max_retries,omitempty"`
|
||||
DangerLevel string `json:"danger_level,omitempty"` // low / medium / high
|
||||
}
|
||||
|
||||
// ToolResult is the standard tool execution result.
|
||||
type ToolResult struct {
|
||||
ToolName string `json:"tool_name"`
|
||||
Success bool `json:"success"`
|
||||
Output string `json:"output,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
DurationMs int64 `json:"duration_ms,omitempty"`
|
||||
}
|
||||
|
||||
// ToolProgress reports execution progress for complex (async) tools.
|
||||
type ToolProgress struct {
|
||||
ExecutionID string `json:"execution_id"`
|
||||
Status string `json:"status"` // started / running / completed / failed / cancelled
|
||||
Progress float64 `json:"progress"` // 0.0 - 1.0
|
||||
Message string `json:"message,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Result *ToolResult `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
// PluginStatus represents the current lifecycle state of a plugin.
|
||||
type PluginStatus string
|
||||
|
||||
const (
|
||||
StatusInstalled PluginStatus = "installed"
|
||||
StatusLoaded PluginStatus = "loaded"
|
||||
StatusRunning PluginStatus = "running"
|
||||
StatusPaused PluginStatus = "paused"
|
||||
StatusError PluginStatus = "error"
|
||||
StatusDisabled PluginStatus = "disabled"
|
||||
)
|
||||
|
||||
// PluginInfo is the runtime view of an installed plugin.
|
||||
type PluginInfo struct {
|
||||
Metadata PluginMetadata `json:"metadata"`
|
||||
Status PluginStatus `json:"status"`
|
||||
Tools []string `json:"tools"` // tool IDs provided by this plugin
|
||||
InstalledAt time.Time `json:"installed_at"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// LLMMessage is a message in an LLM conversation.
|
||||
type LLMMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// LLMResponse is the result of an LLM call.
|
||||
type LLMResponse struct {
|
||||
Content string `json:"content"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
// ToolCall represents a tool call requested by the LLM.
|
||||
type ToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
}
|
||||
|
||||
// IoTDeviceState is the shared device state across IoT plugins.
|
||||
type IoTDeviceState struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Brightness int `json:"brightness,omitempty"`
|
||||
Color string `json:"color,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
Position int `json:"position,omitempty"`
|
||||
Value float64 `json:"value,omitempty"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Battery int `json:"battery,omitempty"`
|
||||
}
|
||||
|
||||
// MemoryEntry is a memory record.
|
||||
type MemoryEntry struct {
|
||||
UserID string `json:"user_id"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
Meta map[string]interface{} `json:"meta,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user