commit 017cab889e1be178e415e02ad6ce53f9aab117a3 Author: DarkiT Date: Mon Mar 17 16:34:27 2025 +0000 基于Go语言的动态插件系统,支持热插拔和动态加载插件。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..989d417 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.idea +.vscode +*.so +*.log + +dist +logs +example/storage +web_admin diff --git a/README.md b/README.md new file mode 100644 index 0000000..0288d57 --- /dev/null +++ b/README.md @@ -0,0 +1,280 @@ +# Go插件系统 + +这是一个基于Go语言的动态插件系统,支持热插拔和动态加载插件。系统提供了一个统一的插件接口和管理框架,使开发者能够轻松地开发和集成新的插件。 + +## 功能特点 + +- 支持动态加载和卸载插件 +- 提供统一的插件接口和基础实现 +- 支持插件配置管理 +- 提供Web管理界面 +- 支持插件操作的动态发现和执行 +- 内置完整的错误处理和日志记录 +- 支持插件状态管理和生命周期控制 + +## 系统架构 + +### 核心组件 + +1. **插件接口** (`interface.go`) + - 定义了插件必须实现的基本接口 + - 包含生命周期管理方法 + - 定义了插件操作和配置接口 + +2. **基础插件** (`plugin.go`) + - 提供插件接口的默认实现 + - 包含插件管理器的核心功能 + - 处理插件的加载、启动、停止和卸载 + +3. **Web管理界面** (`admin/`) + - 提供插件的可视化管理 + - 支持插件配置修改 + - 允许执行插件操作 + +## 快速开始 + +### 1. 创建新插件 + +要创建一个新插件,需要实现 `IPlugin` 接口。推荐使用 `BasePlugin` 作为基础结构: + +```go +package main + +import ( + "context" + "plugins" +) + +type MyPlugin struct { + *plugins.BasePlugin + // 添加插件特定的字段 +} + +var Plugin = &MyPlugin{ + BasePlugin: plugins.NewBasePluginWithDefaultType( + "MyPlugin", // 插件名称 + "1.0.0", // 版本 + "插件描述", // 描述 + "开发者", // 作者 + ), +} +``` + +### 2. 实现必要的接口方法 + +```go +// 初始化插件 +func (p *MyPlugin) Init(ctx context.Context, config map[string]interface{}) error { + // 初始化逻辑 + return nil +} + +// 启动插件 +func (p *MyPlugin) Start(ctx context.Context) error { + // 启动逻辑 + return nil +} + +// 停止插件 +func (p *MyPlugin) Stop(ctx context.Context) error { + // 停止逻辑 + return nil +} +``` + +### 3. 实现插件操作 + +```go +// 定义插件支持的操作 +func (p *MyPlugin) GetOperationInfo(operation string) (*plugins.OperationInfo, error) { + switch operation { + case "myOperation": + return &plugins.OperationInfo{ + Name: "myOperation", + Description: "操作描述", + Params: []plugins.OperationParamInfo{ + { + Name: "param1", + Type: "string", + Required: true, + Description: "参数描述", + }, + }, + }, nil + default: + return nil, fmt.Errorf("不支持的操作: %s", operation) + } +} + +// 实现操作执行逻辑 +func (p *MyPlugin) Execute(ctx context.Context, action string, params map[string]interface{}) (interface{}, error) { + switch action { + case "myOperation": + // 实现操作逻辑 + return map[string]interface{}{"result": "success"}, nil + default: + return nil, fmt.Errorf("不支持的操作: %s", action) + } +} +``` + +## 插件开发指南 + +### 1. 插件结构 + +每个插件必须: +- 实现 `IPlugin` 接口 +- 导出名为 `Plugin` 的变量 +- 包含 `main` 函数(虽然不会被调用) + +### 2. 配置管理 + +插件配置通过 `Init` 方法传入,建议在插件中定义配置结构: + +```go +type Config struct { + Option1 string `json:"option1"` + Option2 int `json:"option2"` +} + +func (p *MyPlugin) Init(ctx context.Context, config map[string]interface{}) error { + // 解析配置 + // 处理配置项 + return nil +} +``` + +### 3. 错误处理 + +- 所有错误应该返回有意义的错误信息 +- 使用 `fmt.Errorf` 格式化错误信息 +- 在适当的地方记录错误日志 + +### 4. 生命周期管理 + +插件的生命周期包括: +1. 加载(Load) +2. 初始化(Init) +3. 启动(Start) +4. 运行(Running) +5. 停止(Stop) +6. 卸载(Unload) + +确保在每个阶段都正确处理资源。 + +## 示例插件 + +### 默认日志插件 + +参考 `plugins/defaultlogger/default_logger_plugin.go` 作为完整的插件实现示例: + +- 实现了基本的日志记录功能 +- 支持文件和控制台输出 +- 提供了多个操作接口 +- 包含完整的配置管理 + +## 构建和部署 + +### 1. 构建插件 + +使用提供的构建脚本: + +```bash +./build_all.sh +``` + +这将编译所有插件并将它们放置在正确的目录中。 + +### 2. 配置插件 + +在 `plugins.json` 中配置插件: + +```json +{ + "MyPlugin": { + "enabled": true, + "config": { + "option1": "value1", + "option2": 123 + } + } +} +``` + +### 3. 启动管理界面 + +```bash +cd admin +./web_admin_new +``` + +## API接口 + +### 1. 插件管理 + +- `GET /plugins` - 获取所有插件列表 +- `POST /plugin/enable` - 启用插件 +- `POST /plugin/disable` - 禁用插件 +- `POST /plugin/config` - 更新插件配置 + +### 2. 插件操作 + +- `POST /api/execute` - 执行插件操作 + +请求示例: +```json +{ + "plugin": "MyPlugin", + "operation": "myOperation", + "params": { + "param1": "value1" + } +} +``` + +## 最佳实践 + +1. **模块化设计** + - 将功能划分为独立的模块 + - 避免插件间的直接依赖 + +2. **错误处理** + - 提供详细的错误信息 + - 实现优雅的错误恢复机制 + +3. **资源管理** + - 正确处理资源的分配和释放 + - 在插件停止时清理所有资源 + +4. **配置验证** + - 在初始化时验证所有配置项 + - 提供合理的默认值 + +5. **文档** + - 详细记录插件的功能和用法 + - 提供配置项的说明 + - 包含示例代码 + +## 故障排除 + +1. **插件加载失败** + - 检查插件文件权限 + - 验证插件接口实现 + - 查看系统日志 + +2. **操作执行失败** + - 检查参数格式 + - 验证插件状态 + - 查看插件日志 + +## 贡献指南 + +1. Fork 项目 +2. 创建特性分支 +3. 提交变更 +4. 推送到分支 +5. 创建 Pull Request + +## 许可证 + +[添加许可证信息] \ No newline at end of file diff --git a/build_all.sh b/build_all.sh new file mode 100755 index 0000000..f5b938e --- /dev/null +++ b/build_all.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# 构建脚本,用于编译所有插件和 web_example.go + +# # 设置工作目录 +WORK_DIR="/www/plugins" + +# # 设置 GOPATH 以确保使用相同版本的 plugins 包 +# export GOPATH=$WORK_DIR +export GO111MODULE=on + +# 确保输出目录存在 +DIST_DIR="/www/plugins/example/dist" +mkdir -p $DIST_DIR + +go mod tidy + +echo "===== 开始编译插件 =====" + +# 编译带有明确类型的日志插件 +echo "编译日志插件 (明确类型)..." +cd $WORK_DIR/plugins/logger +go build -buildmode=plugin -o $DIST_DIR/logger.so +if [ $? -eq 0 ]; then + echo "日志插件编译成功!" +else + echo "日志插件编译失败!" + exit 1 +fi + +# 编译使用默认类型的日志插件 +echo "编译默认日志插件 (默认类型)..." +cd $WORK_DIR/plugins/defaultlogger +go build -buildmode=plugin -o $DIST_DIR/defaultlogger.so +if [ $? -eq 0 ]; then + echo "默认日志插件编译成功!" +else + echo "默认日志插件编译失败!" + exit 1 +fi + +# 编译统计插件 +echo "编译统计插件..." +cd $WORK_DIR/plugins/stats +go build -buildmode=plugin -o $DIST_DIR/stats.so +if [ $? -eq 0 ]; then + echo "统计插件编译成功!" +else + echo "统计插件编译失败!" + exit 1 +fi + +# 编译存储插件 +echo "编译存储插件..." +cd $WORK_DIR/plugins/storage +go build -buildmode=plugin -o $DIST_DIR/storage.so +if [ $? -eq 0 ]; then + echo "存储插件编译成功!" +else + echo "存储插件编译失败!" + exit 1 +fi + +# 编译示例插件 +echo "编译示例插件..." +cd $WORK_DIR/plugins/demoutils +go build -buildmode=plugin -o $DIST_DIR/demoutils.so +if [ $? -eq 0 ]; then + echo "示例插件编译成功!" +else + echo "示例插件编译失败!" + exit 1 +fi +cd .. + +echo "===== 所有插件编译完成 =====" + +# 复制 web_admin.go 到工作目录 +mkdir -p $WORK_DIR/example + +# 编译 web_admin.go +echo "===== 编译 web_admin =====" +cd $WORK_DIR/example +go build -o /www/plugins/example/web_admin web_admin.go +if [ $? -eq 0 ]; then + echo "web_admin.go 编译成功!" +else + echo "web_admin.go 编译失败!" + exit 1 +fi + +echo "===== 所有编译完成 =====" +echo "插件文件保存在 $DIST_DIR 目录中:" +ls -la $DIST_DIR +echo "web_admin 可执行文件保存在 /www/plugins/example/web_admin" + +echo "===== 启动web_admin =====" +/www/plugins/example/web_admin \ No newline at end of file diff --git a/example/demo_utils_client.html b/example/demo_utils_client.html new file mode 100644 index 0000000..b3494cf --- /dev/null +++ b/example/demo_utils_client.html @@ -0,0 +1,856 @@ + + + + + + DemoUtilsPlugin 测试客户端 + + + +
+

DemoUtilsPlugin 测试客户端

+ +
+

插件状态

+
+
+ 插件信息 + +
+
正在加载插件信息...
+
+
+ +
+

操作调用

+
+
+ + + +
+ + +
+

以下是 DemoUtilsPlugin 支持的所有操作:

+
+ 正在加载操作列表... +
+
+ + +
+
+
+
执行操作
+
+
+ + +
+
+ +
+ +
+
+ +
+
执行结果
+
+ 尚未执行任何操作。 +
+
+
+
+ + +
+
+
+
事件订阅
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ +
+
+ 事件日志 + +
+
+ 尚未接收到任何事件。 +
+
+
+
+
+
+ +
+

快速操作

+ + + + + + +
+
+ + + + \ No newline at end of file diff --git a/example/web_admin.go b/example/web_admin.go new file mode 100644 index 0000000..4fe1b9f --- /dev/null +++ b/example/web_admin.go @@ -0,0 +1,1490 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + "github.com/darkit/plugins" +) + +// 插件目录 +var pluginsDir = "./dist" + +// 全局插件管理器 +var pm *plugins.PluginManager + +// AdminHandler 处理管理界面主页 +func AdminHandler(w http.ResponseWriter, r *http.Request) { + // HTML模板 + tmpl := ` + + + + + 插件管理系统 + + + +
+
+
+ +
+ +

插件管理系统

+ +
+

系统信息

+

操作系统: {{.OS}}

+

动态加载支持: {{if .DynamicLoadingSupported}}是{{else}}否{{end}}

+

插件目录: {{.PluginsDir}}

+

已加载插件数量: {{len .Plugins}}

+
+ +

已安装插件

+ + {{range .Plugins}} +
+
+
+

{{.Name}}

+ 版本: {{.Version}} + 作者: {{.Author}} +
+
+ {{if .Enabled}} + 已启用 + {{else}} + 已禁用 + {{end}} +
+
+ +
{{.Description}}
+ + {{if .Config}} +
+

配置:

+ {{range $key, $value := .Config}} +
+
{{$key}}:
+
{{$value}}
+
+ {{end}} +
+ {{end}} + +
+ {{if .Enabled}} + + {{else}} + + {{end}} + + +
+
+ {{end}} + + {{if not .Plugins}} +

没有已安装的插件。

+ {{end}} + +

操作

+ + + + + +` + + // 解析模板 + t, err := template.New("admin").Parse(tmpl) + if err != nil { + http.Error(w, fmt.Sprintf("模板解析错误: %v", err), http.StatusInternalServerError) + return + } + + // 准备数据 + data := struct { + Plugins []plugins.PluginInfo + OS string + DynamicLoadingSupported bool + PluginsDir string + }{ + Plugins: pm.GetPluginInfos(), + OS: strings.Title(strings.ToLower(fmt.Sprintf("%s", strings.Split(runtime.GOOS, "/")[0]))), + DynamicLoadingSupported: pm.IsDynamicLoadingSupported(), + PluginsDir: pluginsDir, + } + + // 渲染模板 + if err := t.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("模板渲染错误: %v", err), http.StatusInternalServerError) + } +} + +// EnablePluginHandler 启用插件 +func EnablePluginHandler(w http.ResponseWriter, r *http.Request) { + pluginName := r.URL.Path[len("/enable/"):] + log.Printf("尝试启用插件: %s", pluginName) + + // 创建带超时的上下文 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // 使用goroutine和通道处理可能的阻塞操作 + enableErrChan := make(chan error, 1) + go func() { + enableErrChan <- pm.EnablePlugin(pluginName) + }() + + // 等待操作完成或超时 + select { + case err := <-enableErrChan: + if err != nil { + log.Printf("启用插件失败: %v", err) + http.Error(w, fmt.Sprintf("启用插件失败: %v", err), http.StatusInternalServerError) + return + } + case <-time.After(5 * time.Second): + log.Printf("启用插件操作超时") + http.Error(w, "启用插件操作超时", http.StatusRequestTimeout) + return + } + + // 尝试初始化和启动插件 + plugin, exists := pm.GetPlugin(pluginName) + if exists && plugin.IsEnabled() { + config, _ := pm.GetPluginConfig(pluginName) + log.Printf("初始化插件: %s", pluginName) + + initErrChan := make(chan error, 1) + go func() { + initErrChan <- plugin.Init(ctx, config) + }() + + // 等待初始化完成或超时 + select { + case err := <-initErrChan: + if err != nil { + log.Printf("初始化插件失败: %v", err) + http.Error(w, fmt.Sprintf("初始化插件失败: %v", err), http.StatusInternalServerError) + return + } + case <-time.After(5 * time.Second): + log.Printf("初始化插件超时") + http.Error(w, "初始化插件超时", http.StatusRequestTimeout) + return + } + + log.Printf("启动插件: %s", pluginName) + startErrChan := make(chan error, 1) + go func() { + startErrChan <- plugin.Start(ctx) + }() + + // 等待启动完成或超时 + select { + case err := <-startErrChan: + if err != nil { + log.Printf("启动插件失败: %v", err) + http.Error(w, fmt.Sprintf("启动插件失败: %v", err), http.StatusInternalServerError) + return + } + case <-time.After(5 * time.Second): + log.Printf("启动插件超时") + http.Error(w, "启动插件超时", http.StatusRequestTimeout) + return + } + } + + log.Printf("插件 %s 启用成功", pluginName) + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// DisablePluginHandler 禁用插件 +func DisablePluginHandler(w http.ResponseWriter, r *http.Request) { + pluginName := r.URL.Path[len("/disable/"):] + log.Printf("尝试禁用插件: %s", pluginName) + + // 创建带超时的上下文 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // 先检查插件是否存在和启用状态 + plugin, exists := pm.GetPlugin(pluginName) + if !exists { + log.Printf("插件不存在: %s", pluginName) + http.Error(w, fmt.Sprintf("插件 %s 不存在", pluginName), http.StatusNotFound) + return + } + + // 如果插件已经是禁用状态,直接返回成功 + if !plugin.IsEnabled() { + log.Printf("插件 %s 已经是禁用状态", pluginName) + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + log.Printf("开始停止插件: %s", pluginName) + // 使用goroutine和通道处理可能的阻塞操作 + stopErrChan := make(chan error, 1) + go func() { + if err := plugin.Stop(ctx); err != nil { + log.Printf("停止插件时发生错误: %v", err) + stopErrChan <- err + } else { + log.Printf("插件停止成功") + stopErrChan <- nil + } + }() + + // 等待停止操作完成或超时 + select { + case err := <-stopErrChan: + if err != nil { + log.Printf("停止插件失败: %v", err) + http.Error(w, fmt.Sprintf("停止插件失败: %v", err), http.StatusInternalServerError) + return + } + case <-time.After(5 * time.Second): + log.Printf("停止插件超时,继续执行禁用操作") + // 即使停止超时,我们也继续执行禁用操作 + } + + // 禁用插件 - 使用goroutine和通道处理可能的阻塞 + log.Printf("开始禁用插件: %s", pluginName) + disableErrChan := make(chan error, 1) + disableDone := make(chan struct{}) + + go func() { + defer close(disableDone) + if err := pm.DisablePlugin(pluginName); err != nil { + log.Printf("禁用插件时发生错误: %v", err) + disableErrChan <- err + } else { + log.Printf("插件禁用成功") + disableErrChan <- nil + } + }() + + // 等待禁用操作完成或超时 + select { + case err := <-disableErrChan: + if err != nil { + log.Printf("禁用插件失败: %v", err) + http.Error(w, fmt.Sprintf("禁用插件失败: %v", err), http.StatusInternalServerError) + return + } + log.Printf("插件 %s 已成功禁用", pluginName) + case <-time.After(5 * time.Second): + log.Printf("禁用插件操作超时,尝试强制设置禁用状态") + // 超时后尝试直接设置禁用状态 + plugin.SetEnabled(false) + // 保存配置 + config, _ := pm.GetPluginConfig(pluginName) + if config == nil { + config = make(map[string]interface{}) + } + config["enabled"] = false + if err := pm.SetPluginConfig(pluginName, config); err != nil { + log.Printf("保存禁用状态失败: %v", err) + http.Error(w, "禁用插件超时且无法保存状态", http.StatusRequestTimeout) + return + } + log.Printf("插件 %s 已强制设置为禁用状态", pluginName) + } + + // 等待禁用操作的goroutine完成 + go func() { + <-disableDone + log.Printf("禁用操作的goroutine已完成") + }() + + // 返回成功响应 + log.Printf("禁用操作处理完成,重定向到主页") + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// ConfigPluginHandler 配置插件 +func ConfigPluginHandler(w http.ResponseWriter, r *http.Request) { + pluginName := r.URL.Path[len("/config/"):] + log.Printf("访问插件配置页面: %s, 方法: %s", pluginName, r.Method) + + if r.Method == http.MethodPost { + // 处理配置表单提交 + if err := r.ParseForm(); err != nil { + log.Printf("解析表单失败: %v", err) + http.Error(w, fmt.Sprintf("解析表单失败: %v", err), http.StatusBadRequest) + return + } + + // 收集配置项 + config := make(map[string]interface{}) + for key, values := range r.PostForm { + if key == "pluginName" { + continue // 跳过插件名称字段 + } + + if len(values) > 0 { + // 尝试将值转换为适当的类型 + value := values[0] + log.Printf("配置项: %s = %s", key, value) + + // 布尔值处理 + if value == "true" { + config[key] = true + } else if value == "false" { + config[key] = false + } else if strings.Contains(value, ".") { + // 尝试解析为浮点数 + if f, err := strconv.ParseFloat(value, 64); err == nil { + config[key] = f + } else { + config[key] = value + } + } else if i, err := strconv.Atoi(value); err == nil { + // 尝试解析为整数 + config[key] = i + } else { + // 默认为字符串 + config[key] = value + } + } + } + + // 保存配置 + log.Printf("保存插件配置: %s", pluginName) + if err := pm.SetPluginConfig(pluginName, config); err != nil { + log.Printf("保存配置失败: %v", err) + http.Error(w, fmt.Sprintf("保存配置失败: %v", err), http.StatusInternalServerError) + return + } + + log.Printf("配置保存成功,重定向到主页") + // 重定向回主页 + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // 获取插件信息和配置 + log.Printf("获取插件信息: %s", pluginName) + info, config := getPluginInfo(pluginName) + if info == nil { + log.Printf("插件不存在: %s", pluginName) + http.Error(w, "插件不存在", http.StatusNotFound) + return + } + + // 配置表单模板 + tmpl := ` + + + + + 配置 {{.Info.Name}} + + + +

配置 {{.Info.Name}}

+ +
+ + + {{range $key, $value := .Config}} +
+ + {{if eq (printf "%T" $value) "bool"}} + + {{else if eq (printf "%T" $value) "float64"}} + + {{else if eq (printf "%T" $value) "int"}} + + {{else}} + + {{end}} +
+ {{end}} + + +
+ + 返回插件列表 + + +` + + // 解析模板 + t, err := template.New("config").Parse(tmpl) + if err != nil { + http.Error(w, fmt.Sprintf("模板解析错误: %v", err), http.StatusInternalServerError) + return + } + + // 准备数据 + data := struct { + Info *plugins.PluginInfo + Config map[string]interface{} + }{ + Info: info, + Config: config, + } + + // 渲染模板 + if err := t.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("模板渲染错误: %v", err), http.StatusInternalServerError) + } +} + +// ReloadPluginsHandler 重新加载插件 +func ReloadPluginsHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + log.Printf("开始重新加载所有插件") + + // 创建带超时的上下文 + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // 停止所有插件 + log.Printf("停止所有插件") + + // 使用goroutine和通道处理可能的阻塞操作 + errChan := make(chan error, 1) + go func() { + errChan <- pm.StopPlugins(ctx) + }() + + // 等待操作完成或超时 + select { + case err := <-errChan: + if err != nil { + log.Printf("停止插件失败: %v", err) + http.Error(w, fmt.Sprintf("停止插件失败: %v", err), http.StatusInternalServerError) + return + } + case <-time.After(10 * time.Second): + log.Printf("停止插件超时") + http.Error(w, "停止插件超时", http.StatusRequestTimeout) + return + } + + // 重新加载插件 + log.Printf("加载插件") + if err := pm.LoadPlugins(); err != nil { + log.Printf("加载插件失败: %v", err) + http.Error(w, fmt.Sprintf("加载插件失败: %v", err), http.StatusInternalServerError) + return + } + + // 初始化插件 + log.Printf("初始化插件") + initErrChan := make(chan error, 1) + go func() { + initErrChan <- pm.InitPlugins(ctx) + }() + + // 等待初始化完成或超时 + select { + case err := <-initErrChan: + if err != nil { + log.Printf("初始化插件失败: %v", err) + http.Error(w, fmt.Sprintf("初始化插件失败: %v", err), http.StatusInternalServerError) + return + } + case <-time.After(10 * time.Second): + log.Printf("初始化插件超时") + http.Error(w, "初始化插件超时", http.StatusRequestTimeout) + return + } + + // 启动插件 + log.Printf("启动插件") + startErrChan := make(chan error, 1) + go func() { + startErrChan <- pm.StartPlugins(ctx) + }() + + // 等待启动完成或超时 + select { + case err := <-startErrChan: + if err != nil { + log.Printf("启动插件失败: %v", err) + http.Error(w, fmt.Sprintf("启动插件失败: %v", err), http.StatusInternalServerError) + return + } + case <-time.After(10 * time.Second): + log.Printf("启动插件超时") + http.Error(w, "启动插件超时", http.StatusRequestTimeout) + return + } + + log.Printf("所有插件已重新加载") + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// APIPluginsHandler API接口处理器 - 返回插件列表 +func APIPluginsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + plugins := pm.GetPluginInfos() + if err := json.NewEncoder(w).Encode(plugins); err != nil { + http.Error(w, fmt.Sprintf("编码JSON失败: %v", err), http.StatusInternalServerError) + } +} + +// OperationsHandler 处理插件操作信息页面 +func OperationsHandler(w http.ResponseWriter, r *http.Request) { + pluginName := r.URL.Path[len("/operations/"):] + log.Printf("访问插件操作页面: %s", pluginName) + + // 获取插件的所有操作信息 + pluginOps, err := pm.GetPluginAllOperations(pluginName) + if err != nil { + log.Printf("获取插件操作失败: %v", err) + http.Error(w, fmt.Sprintf("获取插件操作失败: %v", err), http.StatusInternalServerError) + return + } + + log.Printf("获取到 %d 个操作信息", len(pluginOps.Operations)) + + // 操作信息页面模板 + tmpl := ` + + + + + {{.PluginOps.PluginName}} 插件操作 + + + +

{{.PluginOps.PluginName}} 插件操作

+ + {{if .PluginOps.Operations}} + {{range .PluginOps.Operations}} +
+
{{.Name}}
+ {{if .Description}} +
{{.Description}}
+ {{end}} + + {{if .Params}} +

参数列表:

+ + + + + + + + + {{range .Params}} + + + + + + + + {{end}} +
参数名类型必填默认值描述
{{.Name}}{{.Type}}{{if .Required}}是{{else}}否{{end}}{{if .Default}}{{.Default}}{{else}}-{{end}}{{.Description}}
+ {{else}} +

此操作不需要参数

+ {{end}} + + + + +
+ {{end}} + {{else}} +

该插件没有定义任何操作。

+ {{end}} + + 返回插件列表 + + +` + + // 解析模板 + t, err := template.New("operations").Parse(tmpl) + if err != nil { + http.Error(w, fmt.Sprintf("模板解析错误: %v", err), http.StatusInternalServerError) + return + } + + // 准备数据 + data := struct { + PluginOps *plugins.PluginOperations + }{ + PluginOps: pluginOps, + } + + // 渲染模板 + if err := t.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("模板渲染错误: %v", err), http.StatusInternalServerError) + } +} + +// ExecuteOperationHandler 处理操作执行页面 +func ExecuteOperationHandler(w http.ResponseWriter, r *http.Request) { + parts := strings.Split(r.URL.Path[len("/execute/"):], "/") + if len(parts) != 2 { + log.Printf("无效的URL格式: %s", r.URL.Path) + http.Error(w, "无效的URL格式", http.StatusBadRequest) + return + } + + pluginName := parts[0] + operationName := parts[1] + log.Printf("执行操作: %s/%s, 方法: %s", pluginName, operationName, r.Method) + + // 创建带超时的上下文 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // 获取操作信息 + opInfo, err := pm.GetPluginOperationInfo(pluginName, operationName) + if err != nil { + log.Printf("获取操作信息失败: %v", err) + http.Error(w, fmt.Sprintf("获取操作信息失败: %v", err), http.StatusInternalServerError) + return + } + + // 处理表单提交 + if r.Method == http.MethodPost { + log.Printf("处理操作执行表单") + if err := r.ParseForm(); err != nil { + log.Printf("解析表单失败: %v", err) + http.Error(w, fmt.Sprintf("解析表单失败: %v", err), http.StatusBadRequest) + return + } + + // 构建参数 + params := make(map[string]interface{}) + for _, param := range opInfo.Params { + value := r.FormValue(param.Name) + log.Printf("参数: %s = %s (类型: %s)", param.Name, value, param.Type) + + if value == "" && param.Required { + errMsg := fmt.Sprintf("缺少必填参数: %s", param.Name) + log.Panicln(errMsg) + http.Error(w, errMsg, http.StatusBadRequest) + return + } + + // 根据参数类型转换值 + switch param.Type { + case "integer", "int", "int64": + if value == "" { + params[param.Name] = 0 + } else { + i, err := strconv.ParseInt(value, 10, 64) + if err != nil { + http.Error(w, fmt.Sprintf("参数 %s 格式错误: %v", param.Name, err), http.StatusBadRequest) + return + } + params[param.Name] = i + } + case "float", "float64": + if value == "" { + params[param.Name] = 0.0 + } else { + f, err := strconv.ParseFloat(value, 64) + if err != nil { + http.Error(w, fmt.Sprintf("参数 %s 格式错误: %v", param.Name, err), http.StatusBadRequest) + return + } + params[param.Name] = f + } + case "boolean", "bool": + params[param.Name] = (value == "true" || value == "on") + default: + params[param.Name] = value + } + } + + // 使用goroutine和channel执行操作,避免阻塞 + log.Printf("执行操作: %s", operationName) + resultChan := make(chan interface{}, 1) + errChan := make(chan error, 1) + + go func() { + result, execErr := pm.ExecutePlugin(ctx, pluginName, operationName, params) + resultChan <- result + errChan <- execErr + }() + + // 等待结果或超时 + var result interface{} + var execErr error + + select { + case result = <-resultChan: + execErr = <-errChan + log.Printf("操作执行完成: %v, 错误: %v", result, execErr) + case <-time.After(10 * time.Second): + log.Printf("操作执行超时") + http.Error(w, "操作执行超时", http.StatusRequestTimeout) + return + } + + // 构建包含参数和结果的数据 + data := struct { + PluginName string + OperationName string + Operation *plugins.OperationInfo + Params map[string]interface{} + Result interface{} + Error error + IsPost bool + }{ + PluginName: pluginName, + OperationName: operationName, + Operation: opInfo, + Params: params, + Result: result, + Error: execErr, + IsPost: true, + } + + // 使用相同的模板渲染结果 + executeTemplate(w, data) + return + } + + // 显示表单 + log.Printf("显示操作执行表单") + data := struct { + PluginName string + OperationName string + Operation *plugins.OperationInfo + Params map[string]interface{} + Result interface{} + Error error + IsPost bool + }{ + PluginName: pluginName, + OperationName: operationName, + Operation: opInfo, + Params: make(map[string]interface{}), + Result: nil, + Error: nil, + IsPost: false, + } + + executeTemplate(w, data) +} + +// executeTemplate 渲染操作执行页面模板 +func executeTemplate(w http.ResponseWriter, data interface{}) { + tmpl := ` + + + + + 执行操作 - {{.PluginName}}/{{.OperationName}} + + + +

执行操作: {{.OperationName}}

+

插件: {{.PluginName}}

+ {{if .Operation.Description}}

{{.Operation.Description}}

{{end}} + + {{if .IsPost}} +
+

执行结果

+ {{if .Error}} +

错误: {{.Error}}

+ {{else}} +

操作成功!

+

返回结果:

+
{{printf "%+v" .Result}}
+ {{end}} + +

使用的参数:

+
{{printf "%+v" .Params}}
+
+ + + + + {{else}} +
+

参数

+ {{if .Operation.Params}} + {{range .Operation.Params}} +
+ + + {{if eq .Type "boolean" "bool"}} + + {{else if eq .Type "integer" "int" "int64"}} + + {{else if eq .Type "float" "float64"}} + + {{else if eq .Type "object" "map"}} + + {{else}} + + {{end}} +
+ {{end}} + {{else}} +

此操作不需要参数

+ {{end}} + + +
+ {{end}} + + 返回操作列表 + + +` + + // 解析模板 + t, err := template.New("execute").Parse(tmpl) + if err != nil { + http.Error(w, fmt.Sprintf("模板解析错误: %v", err), http.StatusInternalServerError) + return + } + + // 渲染模板 + if err := t.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("模板渲染错误: %v", err), http.StatusInternalServerError) + } +} + +// APIOperationsHandler API接口处理器 - 返回插件操作信息 +func APIOperationsHandler(w http.ResponseWriter, r *http.Request) { + pluginName := r.URL.Path[len("/api/operations/"):] + if pluginName == "" { + // 返回所有插件的操作信息 + allOps := pm.GetAllPluginsOperations() + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(allOps); err != nil { + http.Error(w, fmt.Sprintf("编码JSON失败: %v", err), http.StatusInternalServerError) + } + return + } + + // 返回特定插件的操作信息 + ops, err := pm.GetPluginAllOperations(pluginName) + if err != nil { + http.Error(w, fmt.Sprintf("获取操作信息失败: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(ops); err != nil { + http.Error(w, fmt.Sprintf("编码JSON失败: %v", err), http.StatusInternalServerError) + } +} + +// APIExecuteHandler API接口处理器 - 执行插件操作 +func APIExecuteHandler(w http.ResponseWriter, r *http.Request) { + // 只接受POST请求 + if r.Method != http.MethodPost { + http.Error(w, "只接受POST请求", http.StatusMethodNotAllowed) + return + } + + log.Printf("收到API执行请求") + + // 解析JSON请求 + var request struct { + Plugin string `json:"plugin"` + Operation string `json:"operation"` + Parameters map[string]interface{} `json:"parameters"` + } + + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + log.Printf("解析请求失败: %v", err) + http.Error(w, fmt.Sprintf("解析请求失败: %v", err), http.StatusBadRequest) + return + } + + log.Printf("执行API请求: 插件=%s, 操作=%s", request.Plugin, request.Operation) + + // 创建带超时的上下文 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // 使用goroutine和通道执行操作,避免阻塞 + resultChan := make(chan interface{}, 1) + errChan := make(chan error, 1) + + go func() { + result, err := pm.ExecutePlugin(ctx, request.Plugin, request.Operation, request.Parameters) + resultChan <- result + errChan <- err + }() + + // 等待结果或超时 + var result interface{} + var err error + + select { + case result = <-resultChan: + err = <-errChan + log.Printf("API操作执行完成: %v, 错误: %v", result, err) + case <-time.After(10 * time.Second): + log.Printf("API操作执行超时") + http.Error(w, "操作执行超时", http.StatusRequestTimeout) + return + } + + // 构建响应 + response := struct { + Success bool `json:"success"` + Result interface{} `json:"result,omitempty"` + Error string `json:"error,omitempty"` + }{ + Success: err == nil, + Result: result, + } + + if err != nil { + response.Error = err.Error() + } + + // 返回响应 + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + log.Printf("编码JSON失败: %v", err) + http.Error(w, fmt.Sprintf("编码JSON失败: %v", err), http.StatusInternalServerError) + } +} + +// getPluginInfo 获取插件信息和配置 +func getPluginInfo(name string) (*plugins.PluginInfo, map[string]interface{}) { + plugins := pm.GetPluginInfos() + + for _, info := range plugins { + if info.Name == name { + config, _ := pm.GetPluginConfig(name) + return &info, config + } + } + + return nil, nil +} + +// EventStreamHandler 处理SSE事件流 +func EventStreamHandler(w http.ResponseWriter, r *http.Request) { + // 设置SSE相关的响应头 + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + + // 获取请求参数 + parts := strings.Split(r.URL.Path, "/") + if len(parts) < 3 { + http.Error(w, "缺少插件名称", http.StatusBadRequest) + return + } + pluginName := parts[len(parts)-1] + + if pluginName == "" { + http.Error(w, "缺少插件名称", http.StatusBadRequest) + return + } + + // 检查插件是否存在 + plugin, exists := pm.GetPlugin(pluginName) + if !exists { + http.Error(w, fmt.Sprintf("插件 %s 不存在", pluginName), http.StatusNotFound) + return + } + + // 创建通知客户端的通道 + eventChan := make(chan plugins.PluginEvent) + clientClosed := make(chan bool) + + // 创建处理事件的函数 + eventHandler := func(event plugins.PluginEvent) error { + // 仅处理指定插件的事件或全局事件 + if event.PluginID == "" || event.PluginID == pluginName { + select { + case eventChan <- event: + // 成功发送事件 + case <-clientClosed: + // 客户端已关闭 + return fmt.Errorf("客户端已关闭连接") + } + } + return nil + } + + // 订阅所有类型的事件 + eventTypes := []plugins.PluginEventType{ + plugins.PluginEventLoaded, + plugins.PluginEventInitialized, + plugins.PluginEventStarted, + plugins.PluginEventStopped, + plugins.PluginEventError, + plugins.PluginEventCustom, + } + + // 注册事件处理器 + for _, eventType := range eventTypes { + pm.SubscribeEvent(eventType, eventHandler) + plugin.SubscribeEvent(eventType, eventHandler) + } + + // 在函数返回时取消订阅事件 + defer func() { + for _, eventType := range eventTypes { + pm.UnsubscribeEvent(eventType, eventHandler) + plugin.UnsubscribeEvent(eventType, eventHandler) + } + close(clientClosed) + }() + + // 发送一个初始事件,表示连接已建立 + fmt.Fprintf(w, "event: connected\ndata: {\"pluginName\":\"%s\",\"time\":\"%s\"}\n\n", + pluginName, time.Now().Format(time.RFC3339)) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + + // 监听客户端断开连接 + notify := r.Context().Done() + + // 事件循环 + for { + select { + case <-notify: + // 客户端断开连接 + log.Printf("客户端断开了SSE连接") + return + case event := <-eventChan: + // 序列化事件数据 + eventData, err := json.Marshal(event) + if err != nil { + log.Printf("序列化事件失败: %v", err) + continue + } + + // 发送事件 + fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, eventData) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + case <-time.After(30 * time.Second): + // 发送保持连接的注释 + fmt.Fprintf(w, ": keepalive\n\n") + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + } + } +} + +// TriggerEventHandler 触发自定义事件 +func TriggerEventHandler(w http.ResponseWriter, r *http.Request) { + // 解析请求体 + var request struct { + Plugin string `json:"plugin"` + Type string `json:"type"` + Data map[string]interface{} `json:"data"` + } + + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, fmt.Sprintf("解析请求失败: %v", err), http.StatusBadRequest) + return + } + + // 获取插件 + plugin, exists := pm.GetPlugin(request.Plugin) + if !exists { + http.Error(w, fmt.Sprintf("插件 %s 不存在", request.Plugin), http.StatusNotFound) + return + } + + // 创建事件 + event := plugins.PluginEvent{ + Type: plugins.PluginEventType(request.Type), + PluginID: request.Plugin, + Timestamp: time.Now(), + Data: request.Data, + } + + // 触发事件 + err := plugin.EmitEvent(event) + if err != nil { + http.Error(w, fmt.Sprintf("触发事件失败: %v", err), http.StatusInternalServerError) + return + } + + // 返回成功响应 + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": true, + "message": "事件已触发", + }) +} + +func main() { + // 创建插件管理器 + pm = plugins.NewPluginManager(pluginsDir) + + // 加载插件 + if err := pm.LoadPlugins(); err != nil { + log.Fatalf("加载插件失败: %v", err) + } + + // 初始化并启动插件 + ctx := context.Background() + if err := pm.InitPlugins(ctx); err != nil { + log.Printf("初始化插件失败: %v", err) + } + + if err := pm.StartPlugins(ctx); err != nil { + log.Printf("启动插件失败: %v", err) + } + + // 添加静态文件服务 + fs := http.FileServer(http.Dir(pluginsDir)) + http.Handle("/static/", http.StripPrefix("/static/", fs)) + + // 添加demo_utils_client.html的特殊路由 + http.HandleFunc("/demo", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, filepath.Join("demo_utils_client.html")) + }) + + // 注册处理器 + http.HandleFunc("/", AdminHandler) + http.HandleFunc("/enable/", EnablePluginHandler) + http.HandleFunc("/disable/", DisablePluginHandler) + http.HandleFunc("/config/", ConfigPluginHandler) + http.HandleFunc("/reload", ReloadPluginsHandler) + http.HandleFunc("/operations/", OperationsHandler) + http.HandleFunc("/execute/", ExecuteOperationHandler) + http.HandleFunc("/api/plugins", APIPluginsHandler) + http.HandleFunc("/api/operations/", APIOperationsHandler) + http.HandleFunc("/api/execute", APIExecuteHandler) + http.HandleFunc("/events/", EventStreamHandler) + http.HandleFunc("/api/trigger-event", TriggerEventHandler) + + // 启动Web服务器 + log.Println("管理界面启动在 :8080...") + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..43f73b4 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/darkit/plugins + +go 1.24.1 diff --git a/interface.go b/interface.go new file mode 100644 index 0000000..6b954c5 --- /dev/null +++ b/interface.go @@ -0,0 +1,361 @@ +package plugins + +import ( + "context" + "fmt" + "time" +) + +// PluginType 插件类型 +type PluginType string + +// 预定义的插件类型 +const ( + PluginTypeGeneral PluginType = "general" // 通用插件 + PluginTypeStorage PluginType = "storage" // 存储插件 + PluginTypeSecurity PluginType = "security" // 安全插件 + PluginTypeNetwork PluginType = "network" // 网络插件 + PluginTypeUtils PluginType = "utils" // 工具插件 + PluginTypeHardware PluginType = "hardware" // 硬件插件 + PluginTypeUI PluginType = "ui" // 用户界面插件 + // 可以根据需求添加更多插件类型 +) + +// PluginStatus 插件状态 +type PluginStatus string + +// 预定义的插件状态 +const ( + PluginStatusUninitialized PluginStatus = "uninitialized" // 未初始化 + PluginStatusInitialized PluginStatus = "initialized" // 已初始化 + PluginStatusRunning PluginStatus = "running" // 运行中 + PluginStatusStopped PluginStatus = "stopped" // 已停止 + PluginStatusError PluginStatus = "error" // 错误状态 +) + +// PluginDependency 插件依赖 +type PluginDependency struct { + Name string `json:"name"` // 依赖的插件名称 + MinVersion string `json:"minVersion"` // 最低版本要求 + MaxVersion string `json:"maxVersion"` // 最高版本要求 + IsOptional bool `json:"isOptional"` // 是否为可选依赖 + LoadAfter bool `json:"loadAfter"` // 是否需要在依赖插件之后加载 + InitAfter bool `json:"initAfter"` // 是否需要在依赖插件初始化之后初始化 + StrictVersions bool `json:"strictVersions"` // 是否严格检查版本 + AutoDisableWith bool `json:"autoDisableWith"` // 依赖插件禁用时是否自动禁用 +} + +// PluginEventType 插件事件类型 +type PluginEventType string + +// 预定义的插件事件类型 +const ( + PluginEventLoaded PluginEventType = "loaded" // 插件加载事件 + PluginEventInitialized PluginEventType = "initialized" // 插件初始化事件 + PluginEventStarted PluginEventType = "started" // 插件启动事件 + PluginEventStopped PluginEventType = "stopped" // 插件停止事件 + PluginEventEnabled PluginEventType = "enabled" // 插件启用事件 + PluginEventDisabled PluginEventType = "disabled" // 插件禁用事件 + PluginEventError PluginEventType = "error" // 插件错误事件 + PluginEventCustom PluginEventType = "custom" // 自定义事件 +) + +// IPlugin 插件接口 +// 这个文件定义了所有插件必须实现的接口 +// 注意:这个文件应该与实际插件代码一起编译 +type IPlugin interface { + // Name 插件名称 + Name() string + // Version 插件版本 + Version() string + // Description 插件描述 + Description() string + // Author 插件作者 + Author() string + // Type 插件类型 + Type() PluginType + // Status 获取插件状态 + Status() PluginStatus + // Dependencies 获取插件依赖 + Dependencies() []PluginDependency + // Init 初始化插件 + Init(ctx context.Context, config map[string]interface{}) error + // Start 启动插件 + Start(ctx context.Context) error + // Stop 停止插件 + Stop(ctx context.Context) error + // IsEnabled 插件是否启用 + IsEnabled() bool + // SetEnabled 设置插件启用状态 + SetEnabled(enabled bool) + // GetOperationInfo 获取操作的参数信息 + GetOperationInfo(operation string) (*OperationInfo, error) + // GetAllOperations 获取所有操作及其参数信息 + GetAllOperations() []*OperationInfo + // Execute 执行插件功能 返回操作结果和可能的错误 + Execute(ctx context.Context, action string, params map[string]interface{}) (interface{}, error) + // SubscribeEvent 订阅插件事件 + SubscribeEvent(eventType PluginEventType, handler PluginEventHandler) + // UnsubscribeEvent 取消订阅插件事件 + UnsubscribeEvent(eventType PluginEventType, handler PluginEventHandler) + // EmitEvent 发送插件事件 + EmitEvent(event PluginEvent) error + // Reload 重新加载插件 + Reload(ctx context.Context, config map[string]interface{}) error + // Cleanup 清理插件资源 + Cleanup(ctx context.Context) error +} + +// PluginEvent 插件事件 +type PluginEvent struct { + Type PluginEventType `json:"type"` // 事件类型 + PluginID string `json:"pluginId"` // 插件ID + Timestamp time.Time `json:"timestamp"` // 事件时间戳 + Data map[string]interface{} `json:"data"` // 事件数据 +} + +// PluginEventHandler 插件事件处理函数 +type PluginEventHandler func(event PluginEvent) error + +// OperationParamInfo 操作参数信息 +type OperationParamInfo struct { + Name string `json:"name"` // 参数名称 + Type string `json:"type"` // 参数类型 + Required bool `json:"required"` // 是否必须 + Default interface{} `json:"default"` // 默认值 + Description string `json:"description"` // 参数描述 +} + +// OperationInfo 操作信息 +type OperationInfo struct { + Name string `json:"name"` // 操作名称 + Description string `json:"description"` // 操作描述 + Params []OperationParamInfo `json:"params"` // 操作参数 + Extra map[string]interface{} `json:"extra"` // 额外信息 +} + +// BasePlugin 提供插件接口的基本实现 +// 插件开发者可以嵌入此结构体,以减少需要实现的方法数量 +type BasePlugin struct { + name string + version string + description string + author string + pluginType PluginType + enabled bool + status PluginStatus + dependencies []PluginDependency + eventHandlers map[PluginEventType][]PluginEventHandler +} + +// NewBasePlugin 创建一个基本插件 +func NewBasePlugin(name, version, description, author string, pluginType PluginType) *BasePlugin { + return &BasePlugin{ + name: name, + version: version, + description: description, + author: author, + pluginType: pluginType, + enabled: true, + status: PluginStatusUninitialized, + dependencies: []PluginDependency{}, + eventHandlers: make(map[PluginEventType][]PluginEventHandler), + } +} + +// NewBasePluginWithDefaultType 创建一个基本插件,使用默认的通用插件类型 +// 这是一个便捷的构造函数,适用于不需要指定特殊类型的场景 +func NewBasePluginWithDefaultType(name, version, description, author string) *BasePlugin { + return NewBasePlugin(name, version, description, author, PluginTypeGeneral) +} + +// Name 获取插件名称 +func (p *BasePlugin) Name() string { + return p.name +} + +// Version 获取插件版本 +func (p *BasePlugin) Version() string { + return p.version +} + +// Description 获取插件描述 +func (p *BasePlugin) Description() string { + return p.description +} + +// Author 获取插件作者 +func (p *BasePlugin) Author() string { + return p.author +} + +// Type 获取插件类型 +func (p *BasePlugin) Type() PluginType { + return p.pluginType +} + +// Status 获取插件状态 +func (p *BasePlugin) Status() PluginStatus { + return p.status +} + +// Dependencies 获取插件依赖 +func (p *BasePlugin) Dependencies() []PluginDependency { + return p.dependencies +} + +// SetDependencies 设置插件依赖 +func (p *BasePlugin) SetDependencies(dependencies []PluginDependency) { + p.dependencies = dependencies +} + +// AddDependency 添加插件依赖 +func (p *BasePlugin) AddDependency(dependency PluginDependency) { + p.dependencies = append(p.dependencies, dependency) +} + +// IsEnabled 插件是否启用 +func (p *BasePlugin) IsEnabled() bool { + return p.enabled +} + +// SetEnabled 设置插件启用状态 +func (p *BasePlugin) SetEnabled(enabled bool) { + p.enabled = enabled + + // 发送启用/禁用事件 + eventType := PluginEventEnabled + if !enabled { + eventType = PluginEventDisabled + } + + p.EmitEvent(PluginEvent{ + Type: eventType, + PluginID: p.name, + Timestamp: time.Now(), + Data: map[string]interface{}{"enabled": enabled}, + }) +} + +// Init 初始化插件,子类需要重写此方法 +func (p *BasePlugin) Init(ctx context.Context, config map[string]interface{}) error { + p.status = PluginStatusInitialized + + // 发送初始化事件 + p.EmitEvent(PluginEvent{ + Type: PluginEventInitialized, + PluginID: p.name, + Timestamp: time.Now(), + Data: map[string]interface{}{"config": config}, + }) + + return nil +} + +// Start 启动插件,子类需要重写此方法 +func (p *BasePlugin) Start(ctx context.Context) error { + p.status = PluginStatusRunning + + // 发送启动事件 + p.EmitEvent(PluginEvent{ + Type: PluginEventStarted, + PluginID: p.name, + Timestamp: time.Now(), + Data: nil, + }) + + return nil +} + +// Stop 停止插件,子类需要重写此方法 +func (p *BasePlugin) Stop(ctx context.Context) error { + p.status = PluginStatusStopped + + // 发送停止事件 + p.EmitEvent(PluginEvent{ + Type: PluginEventStopped, + PluginID: p.name, + Timestamp: time.Now(), + Data: nil, + }) + + return nil +} + +// GetOperationInfo 获取操作的参数信息,子类需要重写此方法 +func (p *BasePlugin) GetOperationInfo(operation string) (*OperationInfo, error) { + return nil, fmt.Errorf("插件 %s 不支持 %s 操作", p.name, operation) +} + +// GetAllOperations 获取所有操作及其参数信息,子类需要重写此方法 +func (p *BasePlugin) GetAllOperations() []*OperationInfo { + return []*OperationInfo{} +} + +// Execute 执行插件功能,子类需要重写此方法 +func (p *BasePlugin) Execute(ctx context.Context, action string, params map[string]interface{}) (interface{}, error) { + return nil, fmt.Errorf("插件 %s 不支持 %s 操作", p.name, action) +} + +// SubscribeEvent 订阅插件事件 +func (p *BasePlugin) SubscribeEvent(eventType PluginEventType, handler PluginEventHandler) { + if p.eventHandlers[eventType] == nil { + p.eventHandlers[eventType] = []PluginEventHandler{} + } + p.eventHandlers[eventType] = append(p.eventHandlers[eventType], handler) +} + +// UnsubscribeEvent 取消订阅插件事件 +func (p *BasePlugin) UnsubscribeEvent(eventType PluginEventType, handler PluginEventHandler) { + if handlers, exists := p.eventHandlers[eventType]; exists { + for i, h := range handlers { + if fmt.Sprintf("%p", h) == fmt.Sprintf("%p", handler) { + p.eventHandlers[eventType] = append(handlers[:i], handlers[i+1:]...) + break + } + } + } +} + +// EmitEvent 发送插件事件 +func (p *BasePlugin) EmitEvent(event PluginEvent) error { + if handlers, exists := p.eventHandlers[event.Type]; exists { + for _, handler := range handlers { + if err := handler(event); err != nil { + return err + } + } + } + return nil +} + +// Reload 重新加载插件 +func (p *BasePlugin) Reload(ctx context.Context, config map[string]interface{}) error { + // 默认实现是先停止,然后重新初始化和启动 + if p.status == PluginStatusRunning { + if err := p.Stop(ctx); err != nil { + return fmt.Errorf("停止插件失败: %v", err) + } + } + + if err := p.Init(ctx, config); err != nil { + return fmt.Errorf("重新初始化插件失败: %v", err) + } + + if p.enabled { + if err := p.Start(ctx); err != nil { + return fmt.Errorf("重新启动插件失败: %v", err) + } + } + + return nil +} + +// Cleanup 清理插件资源 +func (p *BasePlugin) Cleanup(ctx context.Context) error { + // 默认实现是停止插件 + if p.status == PluginStatusRunning { + return p.Stop(ctx) + } + return nil +} diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..24fcc1c --- /dev/null +++ b/plugin.go @@ -0,0 +1,1867 @@ +package plugins + +import ( + "context" + "encoding/json" + "fmt" + "io/fs" + "log" + "os" + "path/filepath" + "plugin" + "reflect" + "runtime" + "strconv" + "strings" + "sync" + "time" +) + +// PluginInfo 插件信息 +type PluginInfo struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Author string `json:"author"` + Type PluginType `json:"type"` + Enabled bool `json:"enabled"` + Config map[string]interface{} `json:"config,omitempty"` +} + +// PluginManager 插件管理器 +type PluginManager struct { + pluginsDir string + plugins map[string]IPlugin + pluginsByType map[PluginType]map[string]IPlugin + configs map[string]map[string]interface{} + mu sync.RWMutex + dynamicLoadingSupported bool // 是否支持动态加载插件 + eventHandlers map[PluginEventType][]PluginEventHandler // 全局事件处理器 +} + +// NewPluginManager 创建插件管理器 +func NewPluginManager(pluginsDir string) *PluginManager { + // 检查当前系统是否支持动态加载插件 + dynamicLoadingSupported := runtime.GOOS == "linux" || runtime.GOOS == "darwin" + + return &PluginManager{ + pluginsDir: pluginsDir, + plugins: make(map[string]IPlugin), + pluginsByType: make(map[PluginType]map[string]IPlugin), + configs: make(map[string]map[string]interface{}), + dynamicLoadingSupported: dynamicLoadingSupported, + eventHandlers: make(map[PluginEventType][]PluginEventHandler), + } +} + +// IsDynamicLoadingSupported 检查是否支持动态加载插件 +func (pm *PluginManager) IsDynamicLoadingSupported() bool { + return pm.dynamicLoadingSupported +} + +// LoadPlugins 加载插件 +func (pm *PluginManager) LoadPlugins() error { + // 确保插件目录存在 + if err := os.MkdirAll(pm.pluginsDir, 0o755); err != nil { + return fmt.Errorf("创建插件目录失败: %v", err) + } + + // 加载插件配置 + if err := pm.loadPluginConfigs(); err != nil { + return fmt.Errorf("加载插件配置失败: %v", err) + } + + // 如果不支持动态加载,则返回 + if !pm.dynamicLoadingSupported { + fmt.Printf("警告: 当前系统(%s)不支持动态加载插件,跳过加载\n", runtime.GOOS) + return nil + } + + // 遍历插件目录 + err := filepath.Walk(pm.pluginsDir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + + // 跳过目录和非.so文件 + if info.IsDir() || filepath.Ext(path) != ".so" { + return nil + } + + // 加载插件 + if err := pm.loadPlugin(path); err != nil { + fmt.Printf("加载插件 %s 失败: %v\n", path, err) + } + + return nil + }) + + return err +} + +// loadPluginConfigs 加载插件配置 +func (pm *PluginManager) loadPluginConfigs() error { + configPath := filepath.Join(pm.pluginsDir, "plugins.json") + + // 先检查配置文件是否存在 + _, err := os.Stat(configPath) + if os.IsNotExist(err) { + // 配置文件不存在,创建一个空的 + // 注意:这里的文件IO操作在锁之外执行 + file, err := os.Create(configPath) + if err != nil { + return fmt.Errorf("创建插件配置文件失败: %v", err) + } + file.Write([]byte("{}")) + file.Close() + + // 初始化空配置 + pm.mu.Lock() + pm.configs = make(map[string]map[string]interface{}) + pm.mu.Unlock() + return nil + } else if err != nil { + return fmt.Errorf("检查插件配置文件状态失败: %v", err) + } + + // 读取配置文件 - 在锁之外执行IO操作 + data, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("读取插件配置文件失败: %v", err) + } + + // 解析配置 + var configs map[string]map[string]interface{} + if err := json.Unmarshal(data, &configs); err != nil { + return fmt.Errorf("解析插件配置文件失败: %v", err) + } + + // 最后才用锁更新配置 + pm.mu.Lock() + defer pm.mu.Unlock() + + // 如果配置为nil,初始化为空映射 + if configs == nil { + configs = make(map[string]map[string]interface{}) + } + + pm.configs = configs + return nil +} + +// savePluginConfigs 保存插件配置 +func (pm *PluginManager) savePluginConfigs() error { + configPath := filepath.Join(pm.pluginsDir, "plugins.json") + + // 创建配置数据的深度副本,使用读锁,减少锁持有时间 + configs := make(map[string]map[string]interface{}) + + pm.mu.RLock() + // 复制所有插件的配置 + for name, plugin := range pm.plugins { + config := make(map[string]interface{}) + // 获取并复制现有配置 + if existingConfig, ok := pm.configs[name]; ok { + for k, v := range existingConfig { + // 对于复杂类型,执行深度复制 + switch val := v.(type) { + case map[string]interface{}: + nestedMap := make(map[string]interface{}) + for nk, nv := range val { + nestedMap[nk] = nv + } + config[k] = nestedMap + case []interface{}: + nestedArray := make([]interface{}, len(val)) + copy(nestedArray, val) + config[k] = nestedArray + default: + config[k] = v + } + } + } + // 添加基本信息 + config["enabled"] = plugin.IsEnabled() + config["type"] = string(plugin.Type()) + configs[name] = config + } + pm.mu.RUnlock() + + // 在释放锁后执行所有IO操作 + // 序列化配置 + data, err := json.MarshalIndent(configs, "", " ") + if err != nil { + return fmt.Errorf("序列化插件配置失败: %v", err) + } + + // 创建临时文件 + tmpDir := filepath.Dir(configPath) + // 确保目录存在 + if err := os.MkdirAll(tmpDir, 0755); err != nil { + return fmt.Errorf("创建配置目录失败: %v", err) + } + + // 使用一个唯一的临时文件名 + tmpFile := fmt.Sprintf("%s.%d.tmp", configPath, time.Now().UnixNano()) + if err := os.WriteFile(tmpFile, data, 0o644); err != nil { + return fmt.Errorf("写入临时配置文件失败: %v", err) + } + + // 重命名临时文件为正式文件(原子操作) + if err := os.Rename(tmpFile, configPath); err != nil { + // 如果重命名失败,确保清理临时文件 + os.Remove(tmpFile) + return fmt.Errorf("更新配置文件失败: %v", err) + } + + return nil +} + +// CheckDependencies 检查插件依赖关系 +func (pm *PluginManager) CheckDependencies(plugin IPlugin) error { + dependencies := plugin.Dependencies() + if len(dependencies) == 0 { + return nil // 没有依赖,直接返回 + } + + for _, dep := range dependencies { + depPlugin, exists := pm.plugins[dep.Name] + if !exists { + if dep.IsOptional { + continue // 可选依赖,忽略 + } + return fmt.Errorf("依赖的插件 %s 不存在", dep.Name) + } + + // 验证版本兼容性 + if dep.MinVersion != "" || dep.MaxVersion != "" { + currentVersion := depPlugin.Version() + + if dep.MinVersion != "" { + result, err := compareVersions(currentVersion, dep.MinVersion) + if err != nil { + return fmt.Errorf("版本比较错误: %v", err) + } + + if result < 0 { + return fmt.Errorf("依赖插件 %s 版本 %s 低于最低版本要求 %s", + dep.Name, currentVersion, dep.MinVersion) + } + } + + if dep.MaxVersion != "" { + result, err := compareVersions(currentVersion, dep.MaxVersion) + if err != nil { + return fmt.Errorf("版本比较错误: %v", err) + } + + if result > 0 { + return fmt.Errorf("依赖插件 %s 版本 %s 高于最高版本要求 %s", + dep.Name, currentVersion, dep.MaxVersion) + } + } + } + + // 检查依赖插件是否启用 + if !depPlugin.IsEnabled() && !dep.IsOptional { + if dep.AutoDisableWith { + // 自动禁用当前插件 + plugin.SetEnabled(false) + return fmt.Errorf("依赖插件 %s 已禁用,当前插件自动禁用", dep.Name) + } + return fmt.Errorf("依赖插件 %s 已禁用", dep.Name) + } + } + + return nil +} + +// InitializeWithDependencies 根据依赖关系初始化插件 +func (pm *PluginManager) InitializeWithDependencies(ctx context.Context) error { + // 构建依赖图 + dependencyGraph := make(map[string][]string) + pluginDeps := make(map[string][]PluginDependency) + + pm.mu.RLock() + for name, plugin := range pm.plugins { + if !plugin.IsEnabled() { + continue + } + + deps := plugin.Dependencies() + pluginDeps[name] = deps + + dependencyGraph[name] = []string{} + for _, dep := range deps { + if !dep.IsOptional && dep.InitAfter { + dependencyGraph[name] = append(dependencyGraph[name], dep.Name) + } + } + } + pm.mu.RUnlock() + + // 拓扑排序 + initOrder, err := topologicalSort(dependencyGraph) + if err != nil { + return fmt.Errorf("解析依赖关系失败: %v", err) + } + + // 按依赖顺序初始化插件 + for _, name := range initOrder { + pm.mu.RLock() + plugin, exists := pm.plugins[name] + config := pm.configs[name] + pm.mu.RUnlock() + + if !exists || !plugin.IsEnabled() { + continue + } + + if err := plugin.Init(ctx, config); err != nil { + return fmt.Errorf("初始化插件 %s 失败: %v", name, err) + } + } + + return nil +} + +// SubscribeEvent 订阅全局插件事件 +func (pm *PluginManager) SubscribeEvent(eventType PluginEventType, handler PluginEventHandler) { + pm.mu.Lock() + defer pm.mu.Unlock() + + if pm.eventHandlers[eventType] == nil { + pm.eventHandlers[eventType] = []PluginEventHandler{} + } + pm.eventHandlers[eventType] = append(pm.eventHandlers[eventType], handler) +} + +// UnsubscribeEvent 取消订阅全局插件事件 +func (pm *PluginManager) UnsubscribeEvent(eventType PluginEventType, handler PluginEventHandler) { + pm.mu.Lock() + defer pm.mu.Unlock() + + if handlers, exists := pm.eventHandlers[eventType]; exists { + for i, h := range handlers { + if fmt.Sprintf("%p", h) == fmt.Sprintf("%p", handler) { + pm.eventHandlers[eventType] = append(handlers[:i], handlers[i+1:]...) + break + } + } + } +} + +// HandleEvent 处理全局插件事件 +func (pm *PluginManager) HandleEvent(event PluginEvent) { + pm.mu.RLock() + defer pm.mu.RUnlock() + + if handlers, exists := pm.eventHandlers[event.Type]; exists { + for _, handler := range handlers { + handler(event) // 忽略错误,不影响其他处理器 + } + } +} + +// BroadcastEvent 广播插件事件到所有处理器 +func (pm *PluginManager) BroadcastEvent(event PluginEvent) { + // 先处理全局事件 + pm.HandleEvent(event) + + // 再传播到特定插件 + if event.PluginID != "" { + pm.mu.RLock() + plugin, exists := pm.plugins[event.PluginID] + pm.mu.RUnlock() + + if exists { + plugin.EmitEvent(event) + } + } +} + +// 加载插件时的事件通知 +func (pm *PluginManager) notifyPluginLoaded(plugin IPlugin) { + pm.BroadcastEvent(PluginEvent{ + Type: PluginEventLoaded, + PluginID: plugin.Name(), + Timestamp: time.Now(), + Data: map[string]interface{}{ + "name": plugin.Name(), + "version": plugin.Version(), + "type": plugin.Type(), + }, + }) +} + +// loadPlugin 加载单个插件 +func (pm *PluginManager) loadPlugin(path string) error { + if !strings.HasSuffix(path, ".so") && !strings.HasSuffix(path, ".dll") { + return nil + } + + // 打开插件 + plug, err := plugin.Open(path) + if err != nil { + return fmt.Errorf("打开插件失败: %v", err) + } + + // 查找 Plugin 符号 + symPlugin, err := plug.Lookup("Plugin") + if err != nil { + return fmt.Errorf("查找 Plugin 符号失败: %v", err) + } + + // 尝试直接类型断言 + fmt.Printf("插件类型: %T\n", symPlugin) + if p, ok := symPlugin.(IPlugin); ok { + pm.registerPlugin(p) + pm.notifyPluginLoaded(p) + return nil + } + + // 直接类型断言失败,尝试通过反射获取值 + fmt.Println("直接类型断言失败,尝试通过反射获取值") + + // 获取原始插件值,用于存储在适配器中 + origValue := reflect.ValueOf(symPlugin) + + // 尝试在所有指针级别上查找方法 + // 保存每一级指针值,用于查找方法 + pointerValues := []reflect.Value{origValue} + currentValue := origValue + + // 解引用并保存每一级指针 + for currentValue.Kind() == reflect.Ptr && currentValue.Elem().IsValid() { + currentValue = currentValue.Elem() + pointerValues = append(pointerValues, currentValue) + } + + // 尝试在不同级别的指针上查找Name和Version方法 + var nameMethod reflect.Value + var versionMethod reflect.Value + var methodPointerLevel reflect.Value + + for _, ptrValue := range pointerValues { + nm := ptrValue.MethodByName("Name") + vm := ptrValue.MethodByName("Version") + + if nm.IsValid() && vm.IsValid() { + nameMethod = nm + versionMethod = vm + methodPointerLevel = ptrValue + break + } + } + + if !nameMethod.IsValid() || !versionMethod.IsValid() { + return fmt.Errorf("插件缺少必需的方法: Name 或 Version") + } + + // 调用 Name 和 Version 方法获取基本信息 + nameResult := nameMethod.Call(nil) + versionResult := versionMethod.Call(nil) + + if len(nameResult) == 0 || len(versionResult) == 0 { + return fmt.Errorf("插件方法返回无效结果") + } + + pluginName := nameResult[0].String() + pluginVersion := versionResult[0].String() + + fmt.Printf("找到插件: %s (版本 %s)\n", pluginName, pluginVersion) + + // 创建适配器以包装插件 + adapter := &PluginAdapter{ + symPlugin: symPlugin, + pluginValue: methodPointerLevel, // 使用找到有效方法的指针级别 + pluginName: pluginName, + pluginVersion: pluginVersion, + } + + // 使用找到方法的同一级别指针值检查是否具有Execute方法 + executeMethod := methodPointerLevel.MethodByName("Execute") + if !executeMethod.IsValid() { + fmt.Printf("插件未实现 Execute 方法,尝试查找特定操作方法\n") + + // 查找常见的操作方法,如果找到,也可以认为是有效插件 + methodFound := false + commonMethods := []string{"Log", "Info", "Error", "GetStat", "SaveFile", "LoadFile"} + + for _, methodName := range commonMethods { + if method := methodPointerLevel.MethodByName(methodName); method.IsValid() { + methodFound = true + break + } + } + + if !methodFound { + return fmt.Errorf("插件类型错误: 未实现 Execute 方法或任何已知的操作方法") + } + } + + pm.registerPlugin(adapter) + pm.notifyPluginLoaded(adapter) + return nil +} + +// registerPlugin 注册插件到插件管理器 +func (pm *PluginManager) registerPlugin(plugin IPlugin) { + name := plugin.Name() + + // 检查插件名称是否已存在 + pm.mu.Lock() + defer pm.mu.Unlock() + + if _, exists := pm.plugins[name]; exists { + log.Printf("警告: 插件 %s 已存在,将被覆盖", name) + } + + // 设置插件启用状态 + if config, exists := pm.configs[name]; exists { + if enabled, ok := config["enabled"].(bool); ok { + plugin.SetEnabled(enabled) + } else { + plugin.SetEnabled(true) // 默认启用 + } + } else { + plugin.SetEnabled(true) // 默认启用 + } + + // 检查依赖关系 + if err := pm.CheckDependencies(plugin); err != nil { + log.Printf("警告: 插件 %s 依赖检查失败: %v", name, err) + } + + // 添加到插件映射 + pm.plugins[name] = plugin + + // 添加到类型映射 + pluginType := plugin.Type() + if pm.pluginsByType[pluginType] == nil { + pm.pluginsByType[pluginType] = make(map[string]IPlugin) + } + pm.pluginsByType[pluginType][name] = plugin + + log.Printf("插件 %s (类型: %s, 版本: %s) 已注册", name, pluginType, plugin.Version()) +} + +// RegisterPlugin 注册内置插件, 用于在不支持动态加载的平台上注册插件 +func (pm *PluginManager) RegisterPlugin(plugin IPlugin) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + if _, exists := pm.plugins[plugin.Name()]; exists { + return fmt.Errorf("插件 %s 已存在", plugin.Name()) + } + + // 设置插件启用状态 + if config, exists := pm.configs[plugin.Name()]; exists { + if enabled, ok := config["enabled"].(bool); ok { + plugin.SetEnabled(enabled) + } else { + plugin.SetEnabled(true) // 默认启用 + } + } else { + plugin.SetEnabled(true) // 默认启用 + } + + // 检查依赖关系 + if err := pm.CheckDependencies(plugin); err != nil { + return fmt.Errorf("依赖检查失败: %v", err) + } + + // 注册插件 + pm.registerPlugin(plugin) + + // 通知插件已加载 + go pm.notifyPluginLoaded(plugin) + + return nil +} + +// GetPlugin 获取插件 +func (pm *PluginManager) GetPlugin(name string) (IPlugin, bool) { + pm.mu.RLock() + defer pm.mu.RUnlock() + + plugin, exists := pm.plugins[name] + return plugin, exists +} + +// GetPluginsByType 按类型获取插件 +func (pm *PluginManager) GetPluginsByType(pluginType PluginType) []IPlugin { + pm.mu.RLock() + defer pm.mu.RUnlock() + + plugins := make([]IPlugin, 0) + if typePlugins, exists := pm.pluginsByType[pluginType]; exists { + for _, plugin := range typePlugins { + if plugin.IsEnabled() { + plugins = append(plugins, plugin) + } + } + } + return plugins +} + +// GetAllPluginsByType 获取所有指定类型的插件,无论是否启用 +func (pm *PluginManager) GetAllPluginsByType(pluginType PluginType) []IPlugin { + pm.mu.RLock() + defer pm.mu.RUnlock() + + plugins := make([]IPlugin, 0) + if typePlugins, exists := pm.pluginsByType[pluginType]; exists { + for _, plugin := range typePlugins { + plugins = append(plugins, plugin) + } + } + return plugins +} + +// GetAllPlugins 获取所有插件 +func (pm *PluginManager) GetAllPlugins() []IPlugin { + pm.mu.RLock() + defer pm.mu.RUnlock() + + plugins := make([]IPlugin, 0, len(pm.plugins)) + for _, plugin := range pm.plugins { + plugins = append(plugins, plugin) + } + return plugins +} + +// GetPluginInfos 获取所有插件信息 +func (pm *PluginManager) GetPluginInfos() []PluginInfo { + pm.mu.RLock() + defer pm.mu.RUnlock() + + infos := make([]PluginInfo, 0, len(pm.plugins)) + for name, plugin := range pm.plugins { + info := PluginInfo{ + Name: plugin.Name(), + Version: plugin.Version(), + Description: plugin.Description(), + Author: plugin.Author(), + Type: plugin.Type(), + Enabled: plugin.IsEnabled(), + Config: pm.configs[name], + } + infos = append(infos, info) + } + return infos +} + +// GetPluginInfosByType 按类型获取插件信息 +func (pm *PluginManager) GetPluginInfosByType(pluginType PluginType) []PluginInfo { + pm.mu.RLock() + defer pm.mu.RUnlock() + + infos := make([]PluginInfo, 0) + if typePlugins, exists := pm.pluginsByType[pluginType]; exists { + for name, plugin := range typePlugins { + info := PluginInfo{ + Name: plugin.Name(), + Version: plugin.Version(), + Description: plugin.Description(), + Author: plugin.Author(), + Type: plugin.Type(), + Enabled: plugin.IsEnabled(), + Config: pm.configs[name], + } + infos = append(infos, info) + } + } + return infos +} + +// EnablePlugin 启用插件 +func (pm *PluginManager) EnablePlugin(name string) error { + // 先获取插件,避免长时间持有锁 + pm.mu.RLock() + plugin, exists := pm.plugins[name] + pm.mu.RUnlock() + + if !exists { + return fmt.Errorf("插件 %s 不存在", name) + } + + // 设置插件状态 + plugin.SetEnabled(true) + + // 更新配置 + config := make(map[string]interface{}) + pm.mu.RLock() + if existingConfig, ok := pm.configs[name]; ok { + // 复制现有配置 + for k, v := range existingConfig { + config[k] = v + } + } + pm.mu.RUnlock() + + config["enabled"] = true + + // 使用单独的锁来更新配置 + pm.mu.Lock() + pm.configs[name] = config + pm.mu.Unlock() + + // 保存配置到文件 + return pm.savePluginConfigs() +} + +// DisablePlugin 禁用插件 +func (pm *PluginManager) DisablePlugin(name string) error { + // 先获取插件,避免长时间持有锁 + pm.mu.RLock() + plugin, exists := pm.plugins[name] + pm.mu.RUnlock() + + if !exists { + return fmt.Errorf("插件 %s 不存在", name) + } + + // 设置插件状态 + plugin.SetEnabled(false) + + // 更新配置 + config := make(map[string]interface{}) + pm.mu.RLock() + if existingConfig, ok := pm.configs[name]; ok { + // 复制现有配置 + for k, v := range existingConfig { + config[k] = v + } + } + pm.mu.RUnlock() + + config["enabled"] = false + + // 使用单独的锁来更新配置 + pm.mu.Lock() + pm.configs[name] = config + pm.mu.Unlock() + + // 保存配置到文件 + return pm.savePluginConfigs() +} + +// InitPlugins 初始化所有插件 +func (pm *PluginManager) InitPlugins(ctx context.Context) error { + // 使用基于依赖的初始化 + return pm.InitializeWithDependencies(ctx) +} + +// InitPluginsByType 初始化指定类型的所有插件 +func (pm *PluginManager) InitPluginsByType(ctx context.Context, pluginType PluginType) error { + pm.mu.RLock() + defer pm.mu.RUnlock() + + if typePlugins, exists := pm.pluginsByType[pluginType]; exists { + for name, plugin := range typePlugins { + if !plugin.IsEnabled() { + continue + } + + config := pm.configs[name] + if err := plugin.Init(ctx, config); err != nil { + return fmt.Errorf("初始化插件 %s 失败: %v", name, err) + } + } + } + return nil +} + +// StartPlugins 启动所有插件 +func (pm *PluginManager) StartPlugins(ctx context.Context) error { + pm.mu.RLock() + defer pm.mu.RUnlock() + + for name, plugin := range pm.plugins { + if !plugin.IsEnabled() { + continue + } + + if err := plugin.Start(ctx); err != nil { + return fmt.Errorf("启动插件 %s 失败: %v", name, err) + } + } + return nil +} + +// StartPluginsByType 启动指定类型的所有插件 +func (pm *PluginManager) StartPluginsByType(ctx context.Context, pluginType PluginType) error { + pm.mu.RLock() + defer pm.mu.RUnlock() + + if typePlugins, exists := pm.pluginsByType[pluginType]; exists { + for name, plugin := range typePlugins { + if !plugin.IsEnabled() { + continue + } + + if err := plugin.Start(ctx); err != nil { + return fmt.Errorf("启动插件 %s 失败: %v", name, err) + } + } + } + return nil +} + +// StopPlugins 停止所有插件 +func (pm *PluginManager) StopPlugins(ctx context.Context) error { + pm.mu.RLock() + defer pm.mu.RUnlock() + + for name, plugin := range pm.plugins { + if !plugin.IsEnabled() { + continue + } + + if err := plugin.Stop(ctx); err != nil { + return fmt.Errorf("停止插件 %s 失败: %v", name, err) + } + } + return nil +} + +// StopPluginsByType 停止指定类型的所有插件 +func (pm *PluginManager) StopPluginsByType(ctx context.Context, pluginType PluginType) error { + pm.mu.RLock() + defer pm.mu.RUnlock() + + if typePlugins, exists := pm.pluginsByType[pluginType]; exists { + for name, plugin := range typePlugins { + if !plugin.IsEnabled() { + continue + } + + if err := plugin.Stop(ctx); err != nil { + return fmt.Errorf("停止插件 %s 失败: %v", name, err) + } + } + } + return nil +} + +// SetPluginConfig 设置插件配置 +func (pm *PluginManager) SetPluginConfig(name string, config map[string]interface{}) error { + // 先检查插件是否存在,使用读锁 + pm.mu.RLock() + _, exists := pm.plugins[name] + pm.mu.RUnlock() + + if !exists { + return fmt.Errorf("插件 %s 不存在", name) + } + + // 创建配置的深度副本 + configCopy := make(map[string]interface{}) + for k, v := range config { + // 对复杂类型进行深度复制 + switch val := v.(type) { + case map[string]interface{}: + // 复制嵌套的map + nestedMap := make(map[string]interface{}) + for nk, nv := range val { + nestedMap[nk] = nv + } + configCopy[k] = nestedMap + case []interface{}: + // 复制数组 + nestedArray := make([]interface{}, len(val)) + copy(nestedArray, val) + configCopy[k] = nestedArray + default: + // 简单类型直接复制 + configCopy[k] = v + } + } + + // 使用写锁更新配置,但尽量减少锁持有时间 + pm.mu.Lock() + pm.configs[name] = configCopy + pm.mu.Unlock() + + // 创建一个带有超时的上下文 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // 使用 channel 来处理保存操作 + errChan := make(chan error, 1) + go func() { + errChan <- pm.savePluginConfigs() + }() + + // 等待保存完成或超时 + select { + case err := <-errChan: + return err + case <-ctx.Done(): + return fmt.Errorf("保存配置超时") + } +} + +// GetPluginConfig 获取插件配置 +func (pm *PluginManager) GetPluginConfig(name string) (map[string]interface{}, error) { + pm.mu.RLock() + defer pm.mu.RUnlock() + + if _, exists := pm.plugins[name]; !exists { + return nil, fmt.Errorf("插件 %s 不存在", name) + } + + // 确保存在配置 + config := pm.configs[name] + if config == nil { + return make(map[string]interface{}), nil + } + + // 创建配置的深度副本 + result := make(map[string]interface{}) + for k, v := range config { + // 对复杂类型进行深度复制 + switch val := v.(type) { + case map[string]interface{}: + // 复制嵌套的map + nestedMap := make(map[string]interface{}) + for nk, nv := range val { + nestedMap[nk] = nv + } + result[k] = nestedMap + case []interface{}: + // 复制数组 + nestedArray := make([]interface{}, len(val)) + copy(nestedArray, val) + result[k] = nestedArray + default: + // 简单类型直接复制 + result[k] = v + } + } + + return result, nil +} + +// ExecutePlugin 执行指定插件的操作 +func (pm *PluginManager) ExecutePlugin(ctx context.Context, name string, action string, params map[string]interface{}) (interface{}, error) { + pm.mu.RLock() + defer pm.mu.RUnlock() + + plugin, exists := pm.plugins[name] + if !exists { + return nil, fmt.Errorf("插件 %s 不存在", name) + } + + if !plugin.IsEnabled() { + return nil, fmt.Errorf("插件 %s 已禁用", name) + } + + return plugin.Execute(ctx, action, params) +} + +// ExecutePluginsByType 对指定类型的所有插件执行操作 +func (pm *PluginManager) ExecutePluginsByType(ctx context.Context, pluginType PluginType, action string, params map[string]interface{}) map[string]interface{} { + pm.mu.RLock() + defer pm.mu.RUnlock() + + results := make(map[string]interface{}) + + if typePlugins, exists := pm.pluginsByType[pluginType]; exists { + for name, plugin := range typePlugins { + if !plugin.IsEnabled() { + continue + } + + result, err := plugin.Execute(ctx, action, params) + results[name] = map[string]interface{}{ + "result": result, + "error": err, + } + } + } + + return results +} + +// ExecuteAllPlugins 对所有插件执行操作 +func (pm *PluginManager) ExecuteAllPlugins(ctx context.Context, action string, params map[string]interface{}) map[string]interface{} { + pm.mu.RLock() + defer pm.mu.RUnlock() + + results := make(map[string]interface{}) + + for name, plugin := range pm.plugins { + if !plugin.IsEnabled() { + continue + } + + result, err := plugin.Execute(ctx, action, params) + results[name] = map[string]interface{}{ + "result": result, + "error": err, + } + } + + return results +} + +// GetPluginOperationInfo 获取插件特定操作的参数信息 +func (pm *PluginManager) GetPluginOperationInfo(name string, operation string) (*OperationInfo, error) { + pm.mu.RLock() + defer pm.mu.RUnlock() + + plugin, exists := pm.plugins[name] + if !exists { + return nil, fmt.Errorf("插件 %s 不存在", name) + } + + if !plugin.IsEnabled() { + return nil, fmt.Errorf("插件 %s 已禁用", name) + } + + return plugin.GetOperationInfo(operation) +} + +// GetPluginAllOperations 获取插件所有操作及其参数信息 +func (pm *PluginManager) GetPluginAllOperations(name string) (*PluginOperations, error) { + pm.mu.RLock() + defer pm.mu.RUnlock() + + plugin, exists := pm.plugins[name] + if !exists { + return nil, fmt.Errorf("插件 %s 不存在", name) + } + + if !plugin.IsEnabled() { + return nil, fmt.Errorf("插件 %s 已禁用", name) + } + + ops := plugin.GetAllOperations() + + return &PluginOperations{ + PluginName: plugin.Name(), + PluginType: plugin.Type(), + Operations: ops, + }, nil +} + +// GetOperationsByType 获取指定类型插件的所有操作信息 +func (pm *PluginManager) GetOperationsByType(pluginType PluginType) []*PluginOperations { + pm.mu.RLock() + defer pm.mu.RUnlock() + + result := make([]*PluginOperations, 0) + + if typePlugins, exists := pm.pluginsByType[pluginType]; exists { + for _, plugin := range typePlugins { + if !plugin.IsEnabled() { + continue + } + + ops := plugin.GetAllOperations() + + result = append(result, &PluginOperations{ + PluginName: plugin.Name(), + PluginType: plugin.Type(), + Operations: ops, + }) + } + } + + return result +} + +// GetAllPluginsOperations 获取所有插件的所有操作信息 +func (pm *PluginManager) GetAllPluginsOperations() []*PluginOperations { + pm.mu.RLock() + defer pm.mu.RUnlock() + + result := make([]*PluginOperations, 0, len(pm.plugins)) + + for _, plugin := range pm.plugins { + if !plugin.IsEnabled() { + continue + } + + ops := plugin.GetAllOperations() + + result = append(result, &PluginOperations{ + PluginName: plugin.Name(), + PluginType: plugin.Type(), + Operations: ops, + }) + } + + return result +} + +// ReloadPlugin 重新加载单个插件 +func (pm *PluginManager) ReloadPlugin(ctx context.Context, name string) error { + pm.mu.Lock() + plugin, exists := pm.plugins[name] + config := pm.configs[name] + pm.mu.Unlock() + + if !exists { + return fmt.Errorf("插件 %s 不存在", name) + } + + // 发出重载事件 + pm.BroadcastEvent(PluginEvent{ + Type: PluginEventCustom, + PluginID: name, + Timestamp: time.Now(), + Data: map[string]interface{}{ + "action": "reloading", + }, + }) + + // 执行重载 + if err := plugin.Reload(ctx, config); err != nil { + // 发送错误事件 + pm.BroadcastEvent(PluginEvent{ + Type: PluginEventError, + PluginID: name, + Timestamp: time.Now(), + Data: map[string]interface{}{ + "error": err.Error(), + "context": "reload", + }, + }) + return fmt.Errorf("重载插件 %s 失败: %v", name, err) + } + + // 发送重载完成事件 + pm.BroadcastEvent(PluginEvent{ + Type: PluginEventCustom, + PluginID: name, + Timestamp: time.Now(), + Data: map[string]interface{}{ + "action": "reloaded", + }, + }) + + return nil +} + +// ReloadAllPlugins 重新加载所有插件 +func (pm *PluginManager) ReloadAllPlugins(ctx context.Context) map[string]error { + errors := make(map[string]error) + + pm.mu.RLock() + pluginNames := make([]string, 0, len(pm.plugins)) + for name := range pm.plugins { + pluginNames = append(pluginNames, name) + } + pm.mu.RUnlock() + + for _, name := range pluginNames { + if err := pm.ReloadPlugin(ctx, name); err != nil { + errors[name] = err + } + } + + return errors +} + +// InstallErrorHandler 为插件安装错误处理器 +func (pm *PluginManager) InstallErrorHandler() { + pm.SubscribeEvent(PluginEventError, func(event PluginEvent) error { + errData, ok := event.Data["error"].(string) + if !ok { + errData = "未知错误" + } + + context, _ := event.Data["context"].(string) + if context == "" { + context = "general" + } + + fmt.Printf("[错误] 插件 %s 在 %s 上下文中发生错误: %s\n", + event.PluginID, context, errData) + return nil + }) +} + +// RecoverPlugin 尝试恢复错误状态的插件 +func (pm *PluginManager) RecoverPlugin(ctx context.Context, name string) error { + pm.mu.Lock() + plugin, exists := pm.plugins[name] + config := pm.configs[name] + pm.mu.Unlock() + + if !exists { + return fmt.Errorf("插件 %s 不存在", name) + } + + if plugin.Status() != PluginStatusError { + return nil // 插件不在错误状态,无需恢复 + } + + // 尝试清理、重新初始化和启动 + if err := plugin.Cleanup(ctx); err != nil { + return fmt.Errorf("清理插件失败: %v", err) + } + + if err := plugin.Init(ctx, config); err != nil { + return fmt.Errorf("重新初始化插件失败: %v", err) + } + + if plugin.IsEnabled() { + if err := plugin.Start(ctx); err != nil { + return fmt.Errorf("重新启动插件失败: %v", err) + } + } + + // 发送恢复事件 + pm.BroadcastEvent(PluginEvent{ + Type: PluginEventCustom, + PluginID: name, + Timestamp: time.Now(), + Data: map[string]interface{}{ + "action": "recovered", + }, + }) + + return nil +} + +// RecoverAllPlugins 尝试恢复所有错误状态的插件 +func (pm *PluginManager) RecoverAllPlugins(ctx context.Context) map[string]error { + errors := make(map[string]error) + + pm.mu.RLock() + plugins := make([]IPlugin, 0) + for _, plugin := range pm.plugins { + if plugin.Status() == PluginStatusError { + plugins = append(plugins, plugin) + } + } + pm.mu.RUnlock() + + for _, plugin := range plugins { + name := plugin.Name() + if err := pm.RecoverPlugin(ctx, name); err != nil { + errors[name] = err + } + } + + return errors +} + +// UnloadPlugin 卸载插件 +func (pm *PluginManager) UnloadPlugin(ctx context.Context, name string) error { + pm.mu.Lock() + defer pm.mu.Unlock() + + plugin, exists := pm.plugins[name] + if !exists { + return fmt.Errorf("插件 %s 不存在", name) + } + + // 检查是否有其他插件依赖该插件 + for otherName, otherPlugin := range pm.plugins { + if otherName == name { + continue + } + + for _, dep := range otherPlugin.Dependencies() { + if dep.Name == name && !dep.IsOptional { + return fmt.Errorf("无法卸载插件 %s: 插件 %s 依赖它", name, otherName) + } + } + } + + // 停止并清理插件 + if plugin.Status() == PluginStatusRunning { + if err := plugin.Stop(ctx); err != nil { + return fmt.Errorf("停止插件失败: %v", err) + } + } + + if err := plugin.Cleanup(ctx); err != nil { + return fmt.Errorf("清理插件失败: %v", err) + } + + // 从管理器中移除插件 + delete(pm.plugins, name) + if typePlugins, exists := pm.pluginsByType[plugin.Type()]; exists { + delete(typePlugins, name) + } + + // 发送卸载事件 + pm.BroadcastEvent(PluginEvent{ + Type: PluginEventCustom, + PluginID: name, + Timestamp: time.Now(), + Data: map[string]interface{}{ + "action": "unloaded", + }, + }) + + return nil +} + +// topologicalSort 拓扑排序,解决依赖顺序问题 +func topologicalSort(graph map[string][]string) ([]string, error) { + result := make([]string, 0, len(graph)) + visited := make(map[string]bool) + temp := make(map[string]bool) + + var visit func(string) error + visit = func(node string) error { + if temp[node] { + return fmt.Errorf("检测到循环依赖: %s", node) + } + if visited[node] { + return nil + } + temp[node] = true + + for _, dep := range graph[node] { + if err := visit(dep); err != nil { + return err + } + } + + temp[node] = false + visited[node] = true + result = append(result, node) + return nil + } + + for node := range graph { + if !visited[node] { + if err := visit(node); err != nil { + return nil, err + } + } + } + + return result, nil +} + +// 简单的版本比较函数 +func compareVersions(v1, v2 string) (int, error) { + // 移除可能的'v'前缀 + v1 = strings.TrimPrefix(v1, "v") + v2 = strings.TrimPrefix(v2, "v") + + // 分割版本号 + parts1 := strings.Split(v1, ".") + parts2 := strings.Split(v2, ".") + + // 确保有足够的部分进行比较 + for len(parts1) < 3 { + parts1 = append(parts1, "0") + } + for len(parts2) < 3 { + parts2 = append(parts2, "0") + } + + // 比较各部分 + for i := 0; i < 3; i++ { + n1, err := strconv.Atoi(parts1[i]) + if err != nil { + return 0, fmt.Errorf("无效的版本号部分 %s: %v", parts1[i], err) + } + + n2, err := strconv.Atoi(parts2[i]) + if err != nil { + return 0, fmt.Errorf("无效的版本号部分 %s: %v", parts2[i], err) + } + + if n1 < n2 { + return -1, nil // v1 < v2 + } else if n1 > n2 { + return 1, nil // v1 > v2 + } + } + + return 0, nil // v1 == v2 +} + +// PluginAdapter 插件适配器 +// 用于将通过反射获取的插件包装为 IPlugin 接口 +type PluginAdapter struct { + symPlugin interface{} + pluginValue reflect.Value + pluginName string + pluginVersion string +} + +// Name 获取插件名称 +func (p *PluginAdapter) Name() string { + return p.pluginName +} + +// Version 获取插件版本 +func (p *PluginAdapter) Version() string { + return p.pluginVersion +} + +// Description 获取插件描述 +func (p *PluginAdapter) Description() string { + method := p.pluginValue.MethodByName("Description") + if !method.IsValid() { + return "未提供描述" + } + return method.Call(nil)[0].String() +} + +// Author 获取插件作者 +func (p *PluginAdapter) Author() string { + method := p.pluginValue.MethodByName("Author") + if !method.IsValid() { + return "未知作者" + } + return method.Call(nil)[0].String() +} + +// Type 获取插件类型 +func (p *PluginAdapter) Type() PluginType { + method := p.pluginValue.MethodByName("Type") + if !method.IsValid() { + return PluginTypeGeneral + } + return PluginType(method.Call(nil)[0].String()) +} + +// Status 获取插件状态 +func (p *PluginAdapter) Status() PluginStatus { + method := p.pluginValue.MethodByName("Status") + if !method.IsValid() { + return PluginStatusUninitialized + } + return PluginStatus(method.Call(nil)[0].String()) +} + +// Dependencies 获取插件依赖 +func (p *PluginAdapter) Dependencies() []PluginDependency { + method := p.pluginValue.MethodByName("Dependencies") + if !method.IsValid() { + return []PluginDependency{} + } + + result := method.Call(nil)[0] + if result.IsNil() { + return []PluginDependency{} + } + + // 尝试直接类型断言 + if dependencies, ok := result.Interface().([]PluginDependency); ok { + return dependencies + } + + // 尝试使用反射迭代切片并转换每个元素 + if result.Kind() == reflect.Slice { + length := result.Len() + dependencies := make([]PluginDependency, 0, length) + + for i := 0; i < length; i++ { + item := result.Index(i).Interface() + + // 尝试将元素转换为PluginDependency + if dep, ok := item.(PluginDependency); ok { + dependencies = append(dependencies, dep) + } else { + // 如果无法直接转换,尝试使用反射读取字段 + depValue := reflect.ValueOf(item) + if depValue.Kind() == reflect.Struct || (depValue.Kind() == reflect.Ptr && depValue.Elem().Kind() == reflect.Struct) { + // 解引用指针 + if depValue.Kind() == reflect.Ptr { + depValue = depValue.Elem() + } + + dependency := PluginDependency{} + + // 获取字段值 + if nameField := depValue.FieldByName("Name"); nameField.IsValid() { + dependency.Name = nameField.String() + } + if minVersionField := depValue.FieldByName("MinVersion"); minVersionField.IsValid() { + dependency.MinVersion = minVersionField.String() + } + if maxVersionField := depValue.FieldByName("MaxVersion"); maxVersionField.IsValid() { + dependency.MaxVersion = maxVersionField.String() + } + if isOptionalField := depValue.FieldByName("IsOptional"); isOptionalField.IsValid() && isOptionalField.Kind() == reflect.Bool { + dependency.IsOptional = isOptionalField.Bool() + } + if loadAfterField := depValue.FieldByName("LoadAfter"); loadAfterField.IsValid() && loadAfterField.Kind() == reflect.Bool { + dependency.LoadAfter = loadAfterField.Bool() + } + if initAfterField := depValue.FieldByName("InitAfter"); initAfterField.IsValid() && initAfterField.Kind() == reflect.Bool { + dependency.InitAfter = initAfterField.Bool() + } + if strictVersionsField := depValue.FieldByName("StrictVersions"); strictVersionsField.IsValid() && strictVersionsField.Kind() == reflect.Bool { + dependency.StrictVersions = strictVersionsField.Bool() + } + if autoDisableWithField := depValue.FieldByName("AutoDisableWith"); autoDisableWithField.IsValid() && autoDisableWithField.Kind() == reflect.Bool { + dependency.AutoDisableWith = autoDisableWithField.Bool() + } + + dependencies = append(dependencies, dependency) + } + } + } + + return dependencies + } + + return []PluginDependency{} +} + +// Init 初始化插件 +func (p *PluginAdapter) Init(ctx context.Context, config map[string]interface{}) error { + method := p.pluginValue.MethodByName("Init") + if !method.IsValid() { + fmt.Printf("插件 %s 没有 Init 方法,跳过初始化\n", p.Name()) + return nil + } + + args := []reflect.Value{ + reflect.ValueOf(ctx), + reflect.ValueOf(config), + } + + result := method.Call(args) + if len(result) > 0 && !result[0].IsNil() { + return result[0].Interface().(error) + } + + return nil +} + +// Start 启动插件 +func (p *PluginAdapter) Start(ctx context.Context) error { + method := p.pluginValue.MethodByName("Start") + if !method.IsValid() { + fmt.Printf("插件 %s 没有 Start 方法,跳过启动\n", p.Name()) + return nil + } + + args := []reflect.Value{ + reflect.ValueOf(ctx), + } + + result := method.Call(args) + if len(result) > 0 && !result[0].IsNil() { + return result[0].Interface().(error) + } + + return nil +} + +// Stop 停止插件 +func (p *PluginAdapter) Stop(ctx context.Context) error { + method := p.pluginValue.MethodByName("Stop") + if !method.IsValid() { + return fmt.Errorf("插件未实现 Stop 方法") + } + + args := []reflect.Value{ + reflect.ValueOf(ctx), + } + + result := method.Call(args) + if len(result) > 0 && !result[0].IsNil() { + return result[0].Interface().(error) + } + + return nil +} + +// IsEnabled 插件是否启用 +func (p *PluginAdapter) IsEnabled() bool { + method := p.pluginValue.MethodByName("IsEnabled") + if !method.IsValid() { + return true + } + + return method.Call(nil)[0].Bool() +} + +// SetEnabled 设置插件启用状态 +func (p *PluginAdapter) SetEnabled(enabled bool) { + method := p.pluginValue.MethodByName("SetEnabled") + if !method.IsValid() { + return + } + + args := []reflect.Value{ + reflect.ValueOf(enabled), + } + + method.Call(args) +} + +// GetOperationInfo 获取操作的参数信息 +func (p *PluginAdapter) GetOperationInfo(operation string) (*OperationInfo, error) { + method := p.pluginValue.MethodByName("GetOperationInfo") + if !method.IsValid() { + return nil, fmt.Errorf("插件未实现 GetOperationInfo 方法") + } + + args := []reflect.Value{ + reflect.ValueOf(operation), + } + + result := method.Call(args) + if len(result) > 1 && !result[1].IsNil() { + return nil, result[1].Interface().(error) + } + + if result[0].IsNil() { + return nil, nil + } + + // 尝试直接类型断言 + if opInfo, ok := result[0].Interface().(*OperationInfo); ok { + return opInfo, nil + } + + // 如果直接断言失败,尝试使用反射获取字段值 + opValue := reflect.Indirect(result[0]) + if opValue.Kind() != reflect.Struct { + // 如果不是结构体,返回基本信息 + return &OperationInfo{ + Name: operation, + Description: "通过适配器调用的操作", + Params: []OperationParamInfo{}, + }, nil + } + + // 创建操作信息对象 + opInfo := &OperationInfo{ + Name: operation, + Description: "", + Params: []OperationParamInfo{}, + Extra: make(map[string]interface{}), + } + + // 获取描述字段 + if descField := opValue.FieldByName("Description"); descField.IsValid() && descField.Kind() == reflect.String { + opInfo.Description = descField.String() + } + + // 获取参数列表 + if paramsField := opValue.FieldByName("Params"); paramsField.IsValid() && paramsField.Kind() == reflect.Slice { + length := paramsField.Len() + opInfo.Params = make([]OperationParamInfo, 0, length) + + for i := 0; i < length; i++ { + paramValue := reflect.Indirect(paramsField.Index(i)) + if paramValue.Kind() != reflect.Struct { + continue + } + + param := OperationParamInfo{} + + // 获取参数字段 + if nameField := paramValue.FieldByName("Name"); nameField.IsValid() && nameField.Kind() == reflect.String { + param.Name = nameField.String() + } + if typeField := paramValue.FieldByName("Type"); typeField.IsValid() && typeField.Kind() == reflect.String { + param.Type = typeField.String() + } + if requiredField := paramValue.FieldByName("Required"); requiredField.IsValid() && requiredField.Kind() == reflect.Bool { + param.Required = requiredField.Bool() + } + if defaultField := paramValue.FieldByName("Default"); defaultField.IsValid() { + param.Default = defaultField.Interface() + } + if descField := paramValue.FieldByName("Description"); descField.IsValid() && descField.Kind() == reflect.String { + param.Description = descField.String() + } + + opInfo.Params = append(opInfo.Params, param) + } + } + + // 获取额外信息 + if extraField := opValue.FieldByName("Extra"); extraField.IsValid() && extraField.Kind() == reflect.Map { + for _, key := range extraField.MapKeys() { + if key.Kind() == reflect.String { + opInfo.Extra[key.String()] = extraField.MapIndex(key).Interface() + } + } + } + + return opInfo, nil +} + +// GetAllOperations 获取所有操作及其参数信息 +func (p *PluginAdapter) GetAllOperations() []*OperationInfo { + method := p.pluginValue.MethodByName("GetAllOperations") + if !method.IsValid() { + return []*OperationInfo{} + } + + result := method.Call(nil)[0] + if result.IsNil() { + return []*OperationInfo{} + } + + // 解析返回的操作信息并返回 + if operations, ok := result.Interface().([]*OperationInfo); ok { + return operations + } + + // 尝试使用反射获取切片内容 + if result.Kind() == reflect.Slice { + length := result.Len() + operations := make([]*OperationInfo, 0, length) + + for i := 0; i < length; i++ { + item := result.Index(i).Interface() + if op, ok := item.(*OperationInfo); ok { + operations = append(operations, op) + } + } + + return operations + } + + // 如果无法解析,返回空数组 + return []*OperationInfo{} +} + +// Execute 执行插件功能 +func (p *PluginAdapter) Execute(ctx context.Context, action string, params map[string]interface{}) (interface{}, error) { + // 首先尝试调用 Execute 方法 + method := p.pluginValue.MethodByName("Execute") + if method.IsValid() { + args := []reflect.Value{ + reflect.ValueOf(ctx), + reflect.ValueOf(action), + reflect.ValueOf(params), + } + + result := method.Call(args) + + var retValue interface{} + var retError error + + if len(result) > 0 && !result[0].IsNil() { + retValue = result[0].Interface() + } + + if len(result) > 1 && !result[1].IsNil() { + retError = result[1].Interface().(error) + } + + return retValue, retError + } + + return nil, nil +} + +// SubscribeEvent 订阅插件事件 +func (p *PluginAdapter) SubscribeEvent(eventType PluginEventType, handler PluginEventHandler) { + method := p.pluginValue.MethodByName("SubscribeEvent") + if !method.IsValid() { + return + } + + args := []reflect.Value{ + reflect.ValueOf(eventType), + reflect.ValueOf(handler), + } + + method.Call(args) +} + +// UnsubscribeEvent 取消订阅插件事件 +func (p *PluginAdapter) UnsubscribeEvent(eventType PluginEventType, handler PluginEventHandler) { + method := p.pluginValue.MethodByName("UnsubscribeEvent") + if !method.IsValid() { + return + } + + args := []reflect.Value{ + reflect.ValueOf(eventType), + reflect.ValueOf(handler), + } + + method.Call(args) +} + +// EmitEvent 发送插件事件 +func (p *PluginAdapter) EmitEvent(event PluginEvent) error { + method := p.pluginValue.MethodByName("EmitEvent") + if !method.IsValid() { + return nil + } + + args := []reflect.Value{ + reflect.ValueOf(event), + } + + result := method.Call(args) + if len(result) > 0 && !result[0].IsNil() { + return result[0].Interface().(error) + } + + return nil +} + +// Reload 重新加载插件 +func (p *PluginAdapter) Reload(ctx context.Context, config map[string]interface{}) error { + method := p.pluginValue.MethodByName("Reload") + if !method.IsValid() { + // 默认实现:先停止,然后重新初始化和启动 + if err := p.Stop(ctx); err != nil { + return fmt.Errorf("停止插件失败: %v", err) + } + + if err := p.Init(ctx, config); err != nil { + return fmt.Errorf("重新初始化插件失败: %v", err) + } + + if p.IsEnabled() { + if err := p.Start(ctx); err != nil { + return fmt.Errorf("重新启动插件失败: %v", err) + } + } + + return nil + } + + args := []reflect.Value{ + reflect.ValueOf(ctx), + reflect.ValueOf(config), + } + + result := method.Call(args) + if len(result) > 0 && !result[0].IsNil() { + return result[0].Interface().(error) + } + + return nil +} + +// Cleanup 清理插件资源 +func (p *PluginAdapter) Cleanup(ctx context.Context) error { + method := p.pluginValue.MethodByName("Cleanup") + if !method.IsValid() { + // 默认实现:停止插件 + if p.Status() == PluginStatusRunning { + return p.Stop(ctx) + } + return nil + } + + args := []reflect.Value{ + reflect.ValueOf(ctx), + } + + result := method.Call(args) + if len(result) > 0 && !result[0].IsNil() { + return result[0].Interface().(error) + } + + return nil +} diff --git a/plugins/defaultlogger/default_logger_plugin.go b/plugins/defaultlogger/default_logger_plugin.go new file mode 100644 index 0000000..992ff59 --- /dev/null +++ b/plugins/defaultlogger/default_logger_plugin.go @@ -0,0 +1,330 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "time" + + "github.com/darkit/plugins" +) + +// DefaultLoggerPlugin 默认日志插件 +// 提供文件日志和控制台日志功能,使用默认插件类型 +type DefaultLoggerPlugin struct { + *plugins.BasePlugin // 嵌入基本插件结构 + logFile *os.File // 日志文件 + logger *log.Logger // 日志记录器 + config map[string]interface{} // 配置 +} + +// Plugin 导出的插件变量 +// 注意:变量名必须是Plugin,大小写敏感 +// 这个插件使用默认的通用插件类型(PluginTypeGeneral) +var Plugin plugins.IPlugin = &DefaultLoggerPlugin{ + BasePlugin: plugins.NewBasePluginWithDefaultType( + "DefaultLoggerPlugin", + "1.0.0", + "使用默认通用类型的日志记录插件", + "开发者", + ), // 将自动使用 PluginTypeGeneral 类型 +} + +// Init 初始化插件 +func (p *DefaultLoggerPlugin) Init(ctx context.Context, config map[string]interface{}) error { + p.config = config + + // 获取日志文件路径 + logPath, ok := config["log_path"].(string) + if !ok { + // 使用默认路径 + logPath = "logs/default" + } + + // 确保日志目录存在 + if err := os.MkdirAll(logPath, 0o755); err != nil { + return fmt.Errorf("创建日志目录失败: %v", err) + } + + // 创建日志文件 + logFilePath := filepath.Join(logPath, fmt.Sprintf("default_app_%s.log", time.Now().Format("2006-01-02"))) + logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + return fmt.Errorf("打开日志文件失败: %v", err) + } + + p.logFile = logFile + p.logger = log.New(logFile, "[DefaultLoggerPlugin] ", log.LstdFlags) + + p.logger.Println("默认日志插件初始化完成") + fmt.Printf("默认日志插件初始化完成,日志文件: %s,插件类型: %s\n", logFilePath, p.Type()) + + return nil +} + +// Start 启动插件 +func (p *DefaultLoggerPlugin) Start(ctx context.Context) error { + if p.logger == nil { + return fmt.Errorf("插件未初始化") + } + + p.logger.Println("默认日志插件已启动") + fmt.Println("默认日志插件已启动") + return nil +} + +// Stop 停止插件 +func (p *DefaultLoggerPlugin) Stop(ctx context.Context) error { + if p.logger != nil { + p.logger.Println("默认日志插件正在停止") + } + + if p.logFile != nil { + if err := p.logFile.Close(); err != nil { + return fmt.Errorf("关闭日志文件失败: %v", err) + } + } + + fmt.Println("默认日志插件已停止") + return nil +} + +// Log 记录日志 +func (p *DefaultLoggerPlugin) Log(level, message string) { + if p.logger == nil { + fmt.Printf("[DEFAULT][%s] %s\n", level, message) + return + } + + logMsg := fmt.Sprintf("[%s] %s", level, message) + p.logger.Println(logMsg) + + // 如果配置了同时输出到控制台 + if consoleOutput, ok := p.config["console_output"].(bool); ok && consoleOutput { + fmt.Println(logMsg) + } +} + +// Info 记录信息日志 +func (p *DefaultLoggerPlugin) Info(message string) { + p.Log("INFO", message) +} + +// Warn 记录警告日志 +func (p *DefaultLoggerPlugin) Warn(message string) { + p.Log("WARN", message) +} + +// Error 记录错误日志 +func (p *DefaultLoggerPlugin) Error(message string) { + p.Log("ERROR", message) +} + +// GetOperationInfo 获取操作的参数信息 +func (p *DefaultLoggerPlugin) GetOperationInfo(operation string) (*plugins.OperationInfo, error) { + switch operation { + case "log": + return &plugins.OperationInfo{ + Name: "log", + Description: "记录日志", + Params: []plugins.OperationParamInfo{ + { + Name: "level", + Type: "string", + Required: true, + Default: "INFO", + Description: "日志级别,可选值:INFO, WARN, ERROR", + }, + { + Name: "message", + Type: "string", + Required: true, + Default: "", + Description: "日志消息", + }, + }, + Extra: map[string]interface{}{ + "category": "日志操作", + }, + }, nil + case "info": + return &plugins.OperationInfo{ + Name: "info", + Description: "记录信息日志", + Params: []plugins.OperationParamInfo{ + { + Name: "message", + Type: "string", + Required: true, + Default: "", + Description: "日志消息", + }, + }, + Extra: map[string]interface{}{ + "category": "日志操作", + }, + }, nil + case "warn": + return &plugins.OperationInfo{ + Name: "warn", + Description: "记录警告日志", + Params: []plugins.OperationParamInfo{ + { + Name: "message", + Type: "string", + Required: true, + Default: "", + Description: "日志消息", + }, + }, + Extra: map[string]interface{}{ + "category": "日志操作", + }, + }, nil + case "error": + return &plugins.OperationInfo{ + Name: "error", + Description: "记录错误日志", + Params: []plugins.OperationParamInfo{ + { + Name: "message", + Type: "string", + Required: true, + Default: "", + Description: "日志消息", + }, + }, + Extra: map[string]interface{}{ + "category": "日志操作", + }, + }, nil + case "console": + return &plugins.OperationInfo{ + Name: "console", + Description: "向控制台打印日志", + Params: []plugins.OperationParamInfo{ + { + Name: "level", + Type: "string", + Required: false, + Default: "INFO", + Description: "日志级别,可选值:INFO, WARN, ERROR", + }, + { + Name: "message", + Type: "string", + Required: true, + Default: "", + Description: "日志消息", + }, + }, + Extra: map[string]interface{}{ + "category": "控制台操作", + }, + }, nil + default: + return nil, fmt.Errorf("插件 %s 不支持 %s 操作", p.Name(), operation) + } +} + +// GetAllOperations 获取所有操作及其参数信息 +func (p *DefaultLoggerPlugin) GetAllOperations() []*plugins.OperationInfo { + operations := []*plugins.OperationInfo{} + + // 添加所有支持的操作 + ops := []string{"log", "info", "warn", "error", "console"} + for _, op := range ops { + info, _ := p.GetOperationInfo(op) + operations = append(operations, info) + } + + return operations +} + +// Execute 实现执行插件功能的方法 +func (p *DefaultLoggerPlugin) Execute(ctx context.Context, action string, params map[string]interface{}) (interface{}, error) { + switch action { + case "log": + // 获取参数 + level, ok := params["level"].(string) + if !ok { + return nil, fmt.Errorf("缺少日志级别参数或类型错误") + } + message, ok := params["message"].(string) + if !ok { + return nil, fmt.Errorf("缺少日志消息参数或类型错误") + } + + // 记录日志 + p.Log(level, message) + return map[string]interface{}{"success": true, "message": "日志已记录"}, nil + + case "info": + // 获取参数 + message, ok := params["message"].(string) + if !ok { + return nil, fmt.Errorf("缺少日志消息参数或类型错误") + } + + // 记录信息日志 + p.Info(message) + return map[string]interface{}{"success": true, "message": "信息日志已记录"}, nil + + case "warn": + // 获取参数 + message, ok := params["message"].(string) + if !ok { + return nil, fmt.Errorf("缺少日志消息参数或类型错误") + } + + // 记录警告日志 + p.Warn(message) + return map[string]interface{}{"success": true, "message": "警告日志已记录"}, nil + + case "error": + // 获取参数 + message, ok := params["message"].(string) + if !ok { + return nil, fmt.Errorf("缺少日志消息参数或类型错误") + } + + // 记录错误日志 + p.Error(message) + return map[string]interface{}{"success": true, "message": "错误日志已记录"}, nil + + case "console": + // 获取参数 + level := "INFO" // 默认级别 + if levelParam, ok := params["level"].(string); ok && levelParam != "" { + level = levelParam + } + + message, ok := params["message"].(string) + if !ok { + return nil, fmt.Errorf("缺少日志消息参数或类型错误") + } + + // 强制输出到控制台,无论配置如何 + formattedMsg := fmt.Sprintf("[控制台][%s] %s", level, message) + fmt.Println(formattedMsg) + + // 同时也记录到日志文件 + p.Log(level, fmt.Sprintf("[Web控制台] %s", message)) + + return map[string]interface{}{ + "success": true, + "message": "日志已输出到控制台", + "console_output": formattedMsg, + }, nil + + default: + return nil, fmt.Errorf("不支持的操作: %s", action) + } +} + +// main 函数是必须的,但不会被调用 +func main() { + // 不会被执行,仅用于编译插件 +} diff --git a/plugins/demoutils/README.md b/plugins/demoutils/README.md new file mode 100644 index 0000000..c23885f --- /dev/null +++ b/plugins/demoutils/README.md @@ -0,0 +1,163 @@ +# DemoUtilsPlugin - 示例工具插件 + +这是一个功能丰富的示例插件,演示了插件系统的所有接口方法和常见功能的实现方式。本插件提供了多种实用工具,包括文本处理、数据存储、计数器和数学运算等。 + +## 功能特点 + +- 完整实现所有插件接口方法 +- 支持多种文本处理操作 +- 内置键值数据存储功能 +- 示范事件订阅与发布机制 +- 实现计数器和状态管理 +- 演示锁机制和并发安全 +- 包含日志记录功能 + +## 安装与构建 + +在插件目录中执行构建脚本: + +```bash +cd /www/plugins/plugins/demoutils +./build.sh +``` + +脚本会自动将编译好的插件移动到正确的目录。 + +或者,使用整体构建脚本: + +```bash +cd /www/plugins +./build_all.sh +``` + +## 配置选项 + +插件支持以下配置选项: + +| 配置项 | 类型 | 默认值 | 描述 | +|-------|------|-------|------| +| log_path | string | logs/demo_plugin | 日志文件存储路径 | +| data_store_path | string | 无 | 数据存储文件路径(可选) | + +配置示例: + +```json +{ + "DemoUtilsPlugin": { + "enabled": true, + "config": { + "log_path": "logs/custompath", + "data_store_path": "data/demo_store.dat" + } + } +} +``` + +## 操作列表 + +插件支持以下操作: + +### 文本处理 + +| 操作 | 描述 | 参数 | +|-----|------|------| +| format | 格式化文本 | text: 要格式化的文本
type: 格式化类型 (upper, lower, title, trim) | +| encode | 编码文本 | text: 要编码的文本
encoding: 编码类型 (base64, url) | +| decode | 解码文本 | text: 要解码的文本
encoding: 解码类型 (base64, url) | + +### 数学工具 + +| 操作 | 描述 | 参数 | +|-----|------|------| +| calculate | 执行基本数学计算 | expression: 数学表达式,例如 1+2 | +| sort | 排序文本行或数字 | items: 要排序的项目,逗号分隔
order: 排序顺序 (asc, desc)
numeric: 是否按数字排序 | + +### 数据存储 + +| 操作 | 描述 | 参数 | +|-----|------|------| +| store | 存储键值数据 | key: 数据键名
value: 要存储的值 | +| retrieve | 根据键获取数据 | key: 数据键名 | +| list | 列出存储的所有键 | 无 | +| delete | 删除存储的键 | key: 要删除的数据键名 | +| clear | 清空所有存储的数据 | 无 | + +### 状态管理 + +| 操作 | 描述 | 参数 | +|-----|------|------| +| counter | 获取或设置计数器 | action: 操作 (get, set, increment, decrement, reset)
value: 设置值 (仅在 action=set 时使用) | + +## 使用示例 + +### 通过Web界面 + +1. 启动Web管理界面 +2. 导航到插件操作页面 +3. 选择 DemoUtilsPlugin 和所需操作 +4. 填写参数并执行 + +### 通过API + +```bash +# 格式化文本为大写 +curl -X POST http://localhost:8080/api/execute -d '{ + "plugin": "DemoUtilsPlugin", + "operation": "format", + "params": { + "text": "hello world", + "type": "upper" + } +}' + +# 存储数据 +curl -X POST http://localhost:8080/api/execute -d '{ + "plugin": "DemoUtilsPlugin", + "operation": "store", + "params": { + "key": "greeting", + "value": "你好,世界!" + } +}' + +# 执行计算 +curl -X POST http://localhost:8080/api/execute -d '{ + "plugin": "DemoUtilsPlugin", + "operation": "calculate", + "params": { + "expression": "10*5" + } +}' +``` + +## 事件机制 + +插件实现了一个完整的事件订阅和发布系统,支持以下事件类型: + +- PluginEventInitialized - 插件初始化完成时触发 +- PluginEventStarted - 插件启动时触发 +- PluginEventStopped - 插件停止时触发 +- PluginEventCustom - 自定义事件,例如计数器更新 + +## 开发注释 + +本插件展示了以下关键概念: + +1. **锁机制** - 使用读写锁保护共享状态 +2. **配置管理** - 从配置映射中加载选项 +3. **资源管理** - 在Start和Stop中正确处理资源 +4. **状态持久化** - 将数据保存到文件 +5. **错误处理** - 提供详细的错误信息 +6. **参数验证** - 检查并验证操作参数 + +## 开发新插件的最佳实践 + +本示例插件演示了以下最佳实践: + +1. 使用嵌入BasePlugin获取基本功能 +2. 实现适当的锁机制保护共享状态 +3. 提供详细的操作参数信息 +4. 正确处理所有错误情况 +5. 实现完整的生命周期管理 +6. 使用有意义的日志记录 +7. 使用事件系统通知状态变化 \ No newline at end of file diff --git a/plugins/demoutils/build.sh b/plugins/demoutils/build.sh new file mode 100755 index 0000000..fa3ee6c --- /dev/null +++ b/plugins/demoutils/build.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# 构建示例插件的脚本 + +# 设置变量 +PLUGIN_NAME="demoutils" +PLUGIN_FILE="demo_plugin.go" +OUTPUT_FILE="demoutils.so" +DIST_DIR="../../admin/dist" + +# 确保脚本在正确目录下执行 +if [ ! -f "$PLUGIN_FILE" ]; then + echo "错误: 找不到插件源文件 $PLUGIN_FILE" + echo "请在包含 $PLUGIN_FILE 的目录中运行此脚本" + exit 1 +fi + +echo "开始构建 $PLUGIN_NAME 插件..." + +# 确保目标目录存在 +mkdir -p "$DIST_DIR" + +# 编译插件 +echo "编译插件: $PLUGIN_FILE -> $OUTPUT_FILE" +go build -buildmode=plugin -o "$OUTPUT_FILE" "$PLUGIN_FILE" + +# 检查编译结果 +if [ $? -ne 0 ]; then + echo "错误: 编译插件失败" + exit 1 +fi + +# 移动插件到目标目录 +echo "移动插件到目标目录: $OUTPUT_FILE -> $DIST_DIR/$OUTPUT_FILE" +cp "$OUTPUT_FILE" "$DIST_DIR/$OUTPUT_FILE" + +# 清理临时文件 +rm -f "$OUTPUT_FILE" + +echo "$PLUGIN_NAME 插件构建完成!" \ No newline at end of file diff --git a/plugins/demoutils/demo_plugin.go b/plugins/demoutils/demo_plugin.go new file mode 100644 index 0000000..63ba833 --- /dev/null +++ b/plugins/demoutils/demo_plugin.go @@ -0,0 +1,1012 @@ +package main + +import ( + "context" + "encoding/base64" + "fmt" + "math" + "net/url" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/darkit/plugins" +) + +// DemoUtilsPlugin 是一个示例工具插件 +// 演示如何实现所有的插件接口方法和各种常见功能 +type DemoUtilsPlugin struct { + *plugins.BasePlugin // 嵌入基本插件结构 + config map[string]interface{} // 配置信息 + counter int // 计数器,用于状态演示 + mu sync.RWMutex // 保护共享状态的互斥锁 + dataStore map[string]interface{} // 简单的数据存储 + subscribers map[string][]plugins.PluginEventHandler // 事件订阅者 + logger func(format string, args ...interface{}) // 日志记录函数 +} + +// Plugin 导出的插件变量,必须命名为 Plugin +var Plugin = &DemoUtilsPlugin{ + BasePlugin: plugins.NewBasePlugin( + "DemoUtilsPlugin", // 插件名称 + "1.0.0", // 版本 + "演示插件的完整功能实现", // 描述 + "开发团队", // 作者 + plugins.PluginTypeUtils, // 类型为工具插件 + ), + counter: 0, + dataStore: make(map[string]interface{}), + subscribers: make(map[string][]plugins.PluginEventHandler), +} + +// Init 初始化插件 +func (p *DemoUtilsPlugin) Init(ctx context.Context, config map[string]interface{}) error { + p.mu.Lock() + + // 复制配置,避免外部修改 + p.config = make(map[string]interface{}) + for k, v := range config { + p.config[k] = v + } + + // 初始化subscribers字段 + if p.subscribers == nil { + p.subscribers = make(map[string][]plugins.PluginEventHandler) + } + + // 暂时释放锁,以便执行IO操作 + p.mu.Unlock() + + // 创建日志记录功能 + logPath, ok := config["log_path"].(string) + if !ok { + logPath = "logs/demo_plugin" + } + + // 确保日志目录存在 + if err := os.MkdirAll(logPath, 0755); err != nil { + return fmt.Errorf("创建日志目录失败: %v", err) + } + + // 定义日志函数 + logFunc := func(format string, args ...interface{}) { + message := fmt.Sprintf(format, args...) + timestamp := time.Now().Format("2006-01-02 15:04:05") + logMessage := fmt.Sprintf("[%s] %s\n", timestamp, message) + + // 打印到控制台 + fmt.Print(logMessage) + + // 写入日志文件 + logFile := filepath.Join(logPath, fmt.Sprintf("demo_plugin_%s.log", time.Now().Format("2006-01-02"))) + f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err == nil { + defer f.Close() + f.WriteString(logMessage) + } + } + + // 设置依赖项和更新logger(需要加锁) + p.mu.Lock() + p.logger = logFunc + p.SetDependencies([]plugins.PluginDependency{ + { + Name: "DefaultLoggerPlugin", + MinVersion: "1.0.0", + IsOptional: true, + }, + }) + p.mu.Unlock() + + // 初始化数据存储 + dataStore := make(map[string]interface{}) + + if storePath, ok := config["data_store_path"].(string); ok && storePath != "" { + // 如果配置了存储路径,尝试从文件加载 + if fileData, err := os.ReadFile(storePath); err == nil { + // 简单实现:每行一个键值对,格式为key=value + lines := strings.Split(string(fileData), "\n") + for _, line := range lines { + if parts := strings.SplitN(line, "=", 2); len(parts) == 2 { + dataStore[parts[0]] = parts[1] + } + } + logFunc("从 %s 加载了 %d 条数据", storePath, len(dataStore)) + } + } + + // 更新数据存储(需要加锁) + p.mu.Lock() + p.dataStore = dataStore + p.mu.Unlock() + + // 发送初始化事件 + event := plugins.PluginEvent{ + Type: plugins.PluginEventInitialized, + PluginID: p.Name(), + Timestamp: time.Now(), + Data: map[string]interface{}{"config": config}, + } + + logFunc("插件 %s 初始化完成,配置: %v", p.Name(), config) + + // 最后发送事件,确保所有初始化工作都已完成 + return p.EmitEvent(event) +} + +// Start 启动插件 +func (p *DemoUtilsPlugin) Start(ctx context.Context) error { + p.mu.Lock() + // 避免持有锁太久,只使用它设置必要的状态 + logger := p.logger // 复制logger指针,避免在goroutine中使用可能变化的指针 + p.mu.Unlock() + + logger("插件 %s 正在启动...", p.Name()) + + // 可以在此处启动后台任务或进行其他启动操作 + // 例如,每分钟递增计数器的后台任务 + go func() { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + logger("后台任务停止") + return + case <-ticker.C: + // 最小化锁的持有时间 + func() { + p.mu.Lock() + defer p.mu.Unlock() + p.counter++ + }() + + // 在锁之外获取当前计数值 + var counterVal int + func() { + p.mu.RLock() + defer p.mu.RUnlock() + counterVal = p.counter + }() + + // 发送计数更新事件(不持有锁) + event := plugins.PluginEvent{ + Type: plugins.PluginEventCustom, + PluginID: p.Name(), + Timestamp: time.Now(), + Data: map[string]interface{}{ + "action": "counter_updated", + "counter": counterVal, + }, + } + + if err := p.EmitEvent(event); err != nil { + logger("发送计数更新事件失败: %v", err) + } + + logger("计数器已更新: %d", counterVal) + } + } + }() + + // 发送启动事件 + event := plugins.PluginEvent{ + Type: plugins.PluginEventStarted, + PluginID: p.Name(), + Timestamp: time.Now(), + } + + if err := p.EmitEvent(event); err != nil { + return fmt.Errorf("发送启动事件失败: %v", err) + } + + logger("插件 %s 已启动", p.Name()) + return nil +} + +// Stop 停止插件 +func (p *DemoUtilsPlugin) Stop(ctx context.Context) error { + // 首先获取需要的信息,但不持久持有锁 + var ( + storePath string + dataToSave map[string]interface{} + logger func(format string, args ...interface{}) + ) + + func() { + p.mu.RLock() + defer p.mu.RUnlock() + + if configPath, ok := p.config["data_store_path"].(string); ok { + storePath = configPath + } + + // 深度复制数据存储 + dataToSave = make(map[string]interface{}) + for k, v := range p.dataStore { + dataToSave[k] = v + } + + logger = p.logger + }() + + logger("插件 %s 正在停止...", p.Name()) + + // 保存数据,不持有锁 + if storePath != "" { + var fileData strings.Builder + for k, v := range dataToSave { + fileData.WriteString(fmt.Sprintf("%s=%v\n", k, v)) + } + + // 确保目录存在 + dir := filepath.Dir(storePath) + if err := os.MkdirAll(dir, 0755); err == nil { + if err := os.WriteFile(storePath, []byte(fileData.String()), 0644); err == nil { + logger("已将 %d 条数据保存到 %s", len(dataToSave), storePath) + } else { + logger("保存数据失败: %v", err) + } + } else { + logger("创建数据目录失败: %v", err) + } + } + + // 发送停止事件 + event := plugins.PluginEvent{ + Type: plugins.PluginEventStopped, + PluginID: p.Name(), + Timestamp: time.Now(), + } + + if err := p.EmitEvent(event); err != nil { + return fmt.Errorf("发送停止事件失败: %v", err) + } + + logger("插件 %s 已停止", p.Name()) + return nil +} + +// SubscribeEvent 订阅插件事件 +func (p *DemoUtilsPlugin) SubscribeEvent(eventType plugins.PluginEventType, handler plugins.PluginEventHandler) { + p.mu.Lock() + defer p.mu.Unlock() + + eventTypeStr := string(eventType) + if p.subscribers[eventTypeStr] == nil { + p.subscribers[eventTypeStr] = []plugins.PluginEventHandler{} + } + + p.subscribers[eventTypeStr] = append(p.subscribers[eventTypeStr], handler) + p.logger("事件 %s 新增了一个订阅者", eventType) +} + +// UnsubscribeEvent 取消订阅插件事件 +func (p *DemoUtilsPlugin) UnsubscribeEvent(eventType plugins.PluginEventType, handler plugins.PluginEventHandler) { + p.mu.Lock() + defer p.mu.Unlock() + + eventTypeStr := string(eventType) + if handlers, exists := p.subscribers[eventTypeStr]; exists { + // 由于函数不能直接比较,所以使用地址字符串比较 + handlerAddr := fmt.Sprintf("%p", handler) + for i, h := range handlers { + if fmt.Sprintf("%p", h) == handlerAddr { + // 从切片中删除 + p.subscribers[eventTypeStr] = append(handlers[:i], handlers[i+1:]...) + p.logger("事件 %s 移除了一个订阅者", eventType) + break + } + } + } +} + +// EmitEvent 发送插件事件 +func (p *DemoUtilsPlugin) EmitEvent(event plugins.PluginEvent) error { + // 复制订阅者列表,避免在调用处理器时持有锁 + p.mu.RLock() + // 从subscribers复制需要的处理器 + var handlersCopy []plugins.PluginEventHandler + if handlers, exists := p.subscribers[string(event.Type)]; exists && len(handlers) > 0 { + handlersCopy = make([]plugins.PluginEventHandler, len(handlers)) + copy(handlersCopy, handlers) + } + p.mu.RUnlock() + + // 先在不持有锁的情况下调用BasePlugin的方法 + if err := p.BasePlugin.EmitEvent(event); err != nil { + return err + } + + // 再调用自己的处理器,也不持有锁 + for _, handler := range handlersCopy { + if err := handler(event); err != nil { + p.logger("处理事件 %s 时出错: %v", event.Type, err) + return err + } + } + + return nil +} + +// Reload 重新加载插件 +func (p *DemoUtilsPlugin) Reload(ctx context.Context, config map[string]interface{}) error { + p.logger("正在重新加载插件 %s...", p.Name()) + + // 先停止 + if err := p.Stop(ctx); err != nil { + return fmt.Errorf("停止插件失败: %v", err) + } + + // 然后重新初始化 + if err := p.Init(ctx, config); err != nil { + return fmt.Errorf("重新初始化插件失败: %v", err) + } + + // 最后重新启动 + if p.IsEnabled() { + if err := p.Start(ctx); err != nil { + return fmt.Errorf("重新启动插件失败: %v", err) + } + } + + p.logger("插件 %s 已重新加载", p.Name()) + return nil +} + +// Cleanup 清理插件资源 +func (p *DemoUtilsPlugin) Cleanup(ctx context.Context) error { + p.mu.Lock() + defer p.mu.Unlock() + + p.logger("正在清理插件 %s 资源...", p.Name()) + + // 清理资源,例如关闭连接、释放内存等 + p.dataStore = nil + p.subscribers = nil + + p.logger("插件 %s 资源已清理", p.Name()) + return nil +} + +// GetOperationInfo 获取操作的参数信息 +func (p *DemoUtilsPlugin) GetOperationInfo(operation string) (*plugins.OperationInfo, error) { + switch operation { + case "calculate": + return &plugins.OperationInfo{ + Name: "calculate", + Description: "执行基本数学计算", + Params: []plugins.OperationParamInfo{ + { + Name: "expression", + Type: "string", + Required: true, + Description: "数学表达式,例如 1+2", + }, + }, + Extra: map[string]interface{}{ + "category": "数学工具", + "examples": []string{"1+2", "10-5", "3*4", "20/5"}, + }, + }, nil + case "format": + return &plugins.OperationInfo{ + Name: "format", + Description: "格式化文本", + Params: []plugins.OperationParamInfo{ + { + Name: "text", + Type: "string", + Required: true, + Description: "要格式化的文本", + }, + { + Name: "type", + Type: "string", + Required: true, + Default: "upper", + Description: "格式化类型: upper, lower, title, trim", + }, + }, + Extra: map[string]interface{}{ + "category": "文本工具", + }, + }, nil + case "encode": + return &plugins.OperationInfo{ + Name: "encode", + Description: "编码文本", + Params: []plugins.OperationParamInfo{ + { + Name: "text", + Type: "string", + Required: true, + Description: "要编码的文本", + }, + { + Name: "encoding", + Type: "string", + Required: true, + Default: "base64", + Description: "编码类型: base64, url", + }, + }, + Extra: map[string]interface{}{ + "category": "编码工具", + }, + }, nil + case "decode": + return &plugins.OperationInfo{ + Name: "decode", + Description: "解码文本", + Params: []plugins.OperationParamInfo{ + { + Name: "text", + Type: "string", + Required: true, + Description: "要解码的文本", + }, + { + Name: "encoding", + Type: "string", + Required: true, + Default: "base64", + Description: "解码类型: base64, url", + }, + }, + Extra: map[string]interface{}{ + "category": "编码工具", + }, + }, nil + case "store": + return &plugins.OperationInfo{ + Name: "store", + Description: "存储键值数据", + Params: []plugins.OperationParamInfo{ + { + Name: "key", + Type: "string", + Required: true, + Description: "数据键名", + }, + { + Name: "value", + Type: "string", + Required: true, + Description: "要存储的值", + }, + }, + Extra: map[string]interface{}{ + "category": "数据存储", + }, + }, nil + case "retrieve": + return &plugins.OperationInfo{ + Name: "retrieve", + Description: "根据键获取数据", + Params: []plugins.OperationParamInfo{ + { + Name: "key", + Type: "string", + Required: true, + Description: "数据键名", + }, + }, + Extra: map[string]interface{}{ + "category": "数据存储", + }, + }, nil + case "list": + return &plugins.OperationInfo{ + Name: "list", + Description: "列出存储的所有键", + Params: []plugins.OperationParamInfo{}, + Extra: map[string]interface{}{ + "category": "数据存储", + }, + }, nil + case "delete": + return &plugins.OperationInfo{ + Name: "delete", + Description: "删除存储的键", + Params: []plugins.OperationParamInfo{ + { + Name: "key", + Type: "string", + Required: true, + Description: "要删除的数据键名", + }, + }, + Extra: map[string]interface{}{ + "category": "数据存储", + }, + }, nil + case "clear": + return &plugins.OperationInfo{ + Name: "clear", + Description: "清空所有存储的数据", + Params: []plugins.OperationParamInfo{}, + Extra: map[string]interface{}{ + "category": "数据存储", + }, + }, nil + case "counter": + return &plugins.OperationInfo{ + Name: "counter", + Description: "获取或设置计数器", + Params: []plugins.OperationParamInfo{ + { + Name: "action", + Type: "string", + Required: true, + Default: "get", + Description: "操作: get, set, increment, decrement, reset", + }, + { + Name: "value", + Type: "integer", + Required: false, + Description: "设置值 (仅在 action=set 时使用)", + }, + }, + Extra: map[string]interface{}{ + "category": "状态管理", + }, + }, nil + case "sort": + return &plugins.OperationInfo{ + Name: "sort", + Description: "排序文本行或数字", + Params: []plugins.OperationParamInfo{ + { + Name: "items", + Type: "string", + Required: true, + Description: "要排序的项目,逗号分隔", + }, + { + Name: "order", + Type: "string", + Required: false, + Default: "asc", + Description: "排序顺序: asc, desc", + }, + { + Name: "numeric", + Type: "boolean", + Required: false, + Default: false, + Description: "是否按数字排序", + }, + }, + Extra: map[string]interface{}{ + "category": "排序工具", + }, + }, nil + default: + return nil, fmt.Errorf("操作 %s 不支持", operation) + } +} + +// GetAllOperations 获取所有操作及其参数信息 +func (p *DemoUtilsPlugin) GetAllOperations() []*plugins.OperationInfo { + allOperations := []string{ + "calculate", "format", "encode", "decode", + "store", "retrieve", "list", "delete", "clear", + "counter", "sort", + } + + results := make([]*plugins.OperationInfo, 0, len(allOperations)) + for _, op := range allOperations { + if info, err := p.GetOperationInfo(op); err == nil { + results = append(results, info) + } + } + + return results +} + +// Execute 执行插件功能 +func (p *DemoUtilsPlugin) Execute(ctx context.Context, action string, params map[string]interface{}) (interface{}, error) { + p.logger("执行操作: %s, 参数: %v", action, params) + + switch action { + case "calculate": + return p.executeCalculate(params) + case "format": + return p.executeFormat(params) + case "encode": + return p.executeEncode(params) + case "decode": + return p.executeDecode(params) + case "store": + return p.executeStore(params) + case "retrieve": + return p.executeRetrieve(params) + case "list": + return p.executeList(params) + case "delete": + return p.executeDelete(params) + case "clear": + return p.executeClear(params) + case "counter": + return p.executeCounter(params) + case "sort": + return p.executeSort(params) + default: + return nil, fmt.Errorf("不支持的操作: %s", action) + } +} + +// 以下是各操作的具体实现 + +// executeCalculate 执行基本数学计算 +func (p *DemoUtilsPlugin) executeCalculate(params map[string]interface{}) (interface{}, error) { + expr, ok := params["expression"].(string) + if !ok { + return nil, fmt.Errorf("缺少表达式参数") + } + + // 简单实现,仅支持两个数的加减乘除 + expr = strings.TrimSpace(expr) + var op rune + var pos int + + // 查找操作符 + for i, c := range expr { + if c == '+' || c == '-' || c == '*' || c == '/' { + op = c + pos = i + break + } + } + + if op == 0 { + return nil, fmt.Errorf("无效的表达式: 缺少操作符") + } + + // 提取操作数 + leftStr := strings.TrimSpace(expr[:pos]) + rightStr := strings.TrimSpace(expr[pos+1:]) + + left, err := strconv.ParseFloat(leftStr, 64) + if err != nil { + return nil, fmt.Errorf("无效的左操作数: %v", err) + } + + right, err := strconv.ParseFloat(rightStr, 64) + if err != nil { + return nil, fmt.Errorf("无效的右操作数: %v", err) + } + + // 执行计算 + var result float64 + switch op { + case '+': + result = left + right + case '-': + result = left - right + case '*': + result = left * right + case '/': + if right == 0 { + return nil, fmt.Errorf("除数不能为零") + } + result = left / right + } + + // 检查结果是否为整数 + if math.Floor(result) == result { + return int(result), nil + } + + return result, nil +} + +// executeFormat 格式化文本 +func (p *DemoUtilsPlugin) executeFormat(params map[string]interface{}) (interface{}, error) { + text, ok := params["text"].(string) + if !ok { + return nil, fmt.Errorf("缺少文本参数") + } + + formatType, ok := params["type"].(string) + if !ok { + formatType = "upper" // 默认格式化类型 + } + + switch formatType { + case "upper": + return strings.ToUpper(text), nil + case "lower": + return strings.ToLower(text), nil + case "title": + return strings.Title(text), nil + case "trim": + return strings.TrimSpace(text), nil + default: + return nil, fmt.Errorf("不支持的格式化类型: %s", formatType) + } +} + +// executeEncode 编码文本 +func (p *DemoUtilsPlugin) executeEncode(params map[string]interface{}) (interface{}, error) { + text, ok := params["text"].(string) + if !ok { + return nil, fmt.Errorf("缺少文本参数") + } + + encoding, ok := params["encoding"].(string) + if !ok { + encoding = "base64" // 默认编码类型 + } + + switch encoding { + case "base64": + return base64.StdEncoding.EncodeToString([]byte(text)), nil + case "url": + return url.QueryEscape(text), nil + default: + return nil, fmt.Errorf("不支持的编码类型: %s", encoding) + } +} + +// executeDecode 解码文本 +func (p *DemoUtilsPlugin) executeDecode(params map[string]interface{}) (interface{}, error) { + text, ok := params["text"].(string) + if !ok { + return nil, fmt.Errorf("缺少文本参数") + } + + encoding, ok := params["encoding"].(string) + if !ok { + encoding = "base64" // 默认解码类型 + } + + switch encoding { + case "base64": + bytes, err := base64.StdEncoding.DecodeString(text) + if err != nil { + return nil, fmt.Errorf("Base64解码失败: %v", err) + } + return string(bytes), nil + case "url": + decoded, err := url.QueryUnescape(text) + if err != nil { + return nil, fmt.Errorf("URL解码失败: %v", err) + } + return decoded, nil + default: + return nil, fmt.Errorf("不支持的解码类型: %s", encoding) + } +} + +// executeStore 存储键值数据 +func (p *DemoUtilsPlugin) executeStore(params map[string]interface{}) (interface{}, error) { + key, ok := params["key"].(string) + if !ok { + return nil, fmt.Errorf("缺少键名参数") + } + + value, ok := params["value"] + if !ok { + return nil, fmt.Errorf("缺少值参数") + } + + p.mu.Lock() + defer p.mu.Unlock() + + p.dataStore[key] = value + + return map[string]interface{}{ + "success": true, + "message": fmt.Sprintf("已存储键 %s", key), + }, nil +} + +// executeRetrieve 根据键获取数据 +func (p *DemoUtilsPlugin) executeRetrieve(params map[string]interface{}) (interface{}, error) { + key, ok := params["key"].(string) + if !ok { + return nil, fmt.Errorf("缺少键名参数") + } + + p.mu.RLock() + defer p.mu.RUnlock() + + value, exists := p.dataStore[key] + if !exists { + return nil, fmt.Errorf("键 %s 不存在", key) + } + + return value, nil +} + +// executeList 列出存储的所有键 +func (p *DemoUtilsPlugin) executeList(params map[string]interface{}) (interface{}, error) { + p.mu.RLock() + defer p.mu.RUnlock() + + keys := make([]string, 0, len(p.dataStore)) + for k := range p.dataStore { + keys = append(keys, k) + } + + // 按字母顺序排序 + sort.Strings(keys) + + return map[string]interface{}{ + "keys": keys, + "count": len(keys), + }, nil +} + +// executeDelete 删除存储的键 +func (p *DemoUtilsPlugin) executeDelete(params map[string]interface{}) (interface{}, error) { + key, ok := params["key"].(string) + if !ok { + return nil, fmt.Errorf("缺少键名参数") + } + + p.mu.Lock() + defer p.mu.Unlock() + + _, exists := p.dataStore[key] + if !exists { + return nil, fmt.Errorf("键 %s 不存在", key) + } + + delete(p.dataStore, key) + + return map[string]interface{}{ + "success": true, + "message": fmt.Sprintf("已删除键 %s", key), + }, nil +} + +// executeClear 清空所有存储的数据 +func (p *DemoUtilsPlugin) executeClear(params map[string]interface{}) (interface{}, error) { + p.mu.Lock() + defer p.mu.Unlock() + + count := len(p.dataStore) + p.dataStore = make(map[string]interface{}) + + return map[string]interface{}{ + "success": true, + "message": fmt.Sprintf("已清空 %d 个存储项", count), + "cleared": count, + }, nil +} + +// executeCounter 获取或设置计数器 +func (p *DemoUtilsPlugin) executeCounter(params map[string]interface{}) (interface{}, error) { + action, ok := params["action"].(string) + if !ok { + action = "get" // 默认操作 + } + + p.mu.Lock() + defer p.mu.Unlock() + + switch action { + case "get": + return map[string]interface{}{ + "counter": p.counter, + }, nil + case "set": + value, ok := params["value"].(float64) + if !ok { + return nil, fmt.Errorf("设置计数器需要提供有效的数值") + } + + p.counter = int(value) + return map[string]interface{}{ + "success": true, + "counter": p.counter, + "message": fmt.Sprintf("计数器已设置为 %d", p.counter), + }, nil + case "increment": + p.counter++ + return map[string]interface{}{ + "success": true, + "counter": p.counter, + "message": fmt.Sprintf("计数器已递增为 %d", p.counter), + }, nil + case "decrement": + p.counter-- + return map[string]interface{}{ + "success": true, + "counter": p.counter, + "message": fmt.Sprintf("计数器已递减为 %d", p.counter), + }, nil + case "reset": + p.counter = 0 + return map[string]interface{}{ + "success": true, + "counter": 0, + "message": "计数器已重置为 0", + }, nil + default: + return nil, fmt.Errorf("不支持的计数器操作: %s", action) + } +} + +// executeSort 排序文本行或数字 +func (p *DemoUtilsPlugin) executeSort(params map[string]interface{}) (interface{}, error) { + itemsStr, ok := params["items"].(string) + if !ok { + return nil, fmt.Errorf("缺少待排序项目") + } + + order, ok := params["order"].(string) + if !ok { + order = "asc" // 默认升序 + } + + numericSort, ok := params["numeric"].(bool) + if !ok { + numericSort = false // 默认文本排序 + } + + // 分割项目 + items := strings.Split(itemsStr, ",") + for i := range items { + items[i] = strings.TrimSpace(items[i]) + } + + if numericSort { + // 数字排序 + numbers := make([]float64, 0, len(items)) + for _, item := range items { + n, err := strconv.ParseFloat(item, 64) + if err != nil { + return nil, fmt.Errorf("项目 '%s' 不是有效数字", item) + } + numbers = append(numbers, n) + } + + if order == "asc" { + sort.Float64s(numbers) + } else { + sort.Sort(sort.Reverse(sort.Float64Slice(numbers))) + } + + // 转回字符串 + result := make([]string, len(numbers)) + for i, n := range numbers { + if math.Floor(n) == n { + result[i] = fmt.Sprintf("%d", int(n)) + } else { + result[i] = fmt.Sprintf("%g", n) + } + } + + return map[string]interface{}{ + "sorted": result, + "count": len(result), + }, nil + } else { + // 文本排序 + if order == "asc" { + sort.Strings(items) + } else { + sort.Sort(sort.Reverse(sort.StringSlice(items))) + } + + return map[string]interface{}{ + "sorted": items, + "count": len(items), + }, nil + } +} + +// 必须有main函数 +func main() { + // 这个函数不会被调用,仅用于编译插件 +} diff --git a/plugins/logger/logger_plugin.go b/plugins/logger/logger_plugin.go new file mode 100644 index 0000000..c21233d --- /dev/null +++ b/plugins/logger/logger_plugin.go @@ -0,0 +1,316 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "time" + + "github.com/darkit/plugins" +) + +// LoggerPlugin 日志插件 +// 提供文件日志和控制台日志功能 +type LoggerPlugin struct { + *plugins.BasePlugin // 嵌入基本插件结构 + logFile *os.File // 日志文件 + logger *log.Logger // 日志记录器 + config map[string]interface{} // 配置 +} + +// Plugin 导出的插件变量 +// 注意:变量名必须是Plugin,大小写敏感 +// 使用方式1: 明确指定插件类型 +var Plugin = &LoggerPlugin{ + BasePlugin: plugins.NewBasePlugin( + "LoggerPlugin", + "1.0.0", + "简单的日志记录插件", + "开发者", + plugins.PluginTypeUtils, // 明确指定为工具类插件 + ), +} + +// 使用方式2: 使用默认插件类型(通用插件) +// 如果您不关心插件类型或想使用默认的通用插件类型,可以使用以下方式: +// +// var Plugin = &LoggerPlugin{ +// BasePlugin: plugins.NewBasePluginWithDefaultType( +// "LoggerPlugin", +// "1.0.0", +// "简单的日志记录插件", +// "开发者", +// ), // 将自动使用 PluginTypeGeneral 类型 +// } + +// Init 初始化插件 +func (p *LoggerPlugin) Init(ctx context.Context, config map[string]interface{}) error { + p.config = config + + // 获取日志文件路径 + logPath, ok := config["log_path"].(string) + if !ok { + // 使用默认路径 + logPath = "logs" + } + + // 确保日志目录存在 + if err := os.MkdirAll(logPath, 0o755); err != nil { + return fmt.Errorf("创建日志目录失败: %v", err) + } + + // 创建日志文件 + logFilePath := filepath.Join(logPath, fmt.Sprintf("app_%s.log", time.Now().Format("2006-01-02"))) + logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + return fmt.Errorf("打开日志文件失败: %v", err) + } + + p.logFile = logFile + p.logger = log.New(logFile, "[LoggerPlugin] ", log.LstdFlags) + + p.logger.Println("日志插件初始化完成") + fmt.Printf("日志插件初始化完成,日志文件: %s,插件类型: %s\n", logFilePath, p.Type()) + + return nil +} + +// Start 启动插件 +func (p *LoggerPlugin) Start(ctx context.Context) error { + if p.logger == nil { + return fmt.Errorf("插件未初始化") + } + + p.logger.Println("日志插件已启动") + fmt.Println("日志插件已启动") + return nil +} + +// Stop 停止插件 +func (p *LoggerPlugin) Stop(ctx context.Context) error { + if p.logger != nil { + p.logger.Println("日志插件正在停止") + } + + if p.logFile != nil { + if err := p.logFile.Close(); err != nil { + return fmt.Errorf("关闭日志文件失败: %v", err) + } + } + + fmt.Println("日志插件已停止") + return nil +} + +// Execute 执行插件功能 +func (p *LoggerPlugin) Execute(ctx context.Context, action string, params map[string]interface{}) (interface{}, error) { + switch action { + case "log": + // 需要参数: level, message + level, ok := params["level"].(string) + if !ok { + return nil, fmt.Errorf("缺少必需参数: level") + } + message, ok := params["message"].(string) + if !ok { + return nil, fmt.Errorf("缺少必需参数: message") + } + p.Log(level, message) + return true, nil + + case "info": + // 需要参数: message + message, ok := params["message"].(string) + if !ok { + return nil, fmt.Errorf("缺少必需参数: message") + } + p.Info(message) + return true, nil + + case "warn": + // 需要参数: message + message, ok := params["message"].(string) + if !ok { + return nil, fmt.Errorf("缺少必需参数: message") + } + p.Warn(message) + return true, nil + + case "error": + // 需要参数: message + message, ok := params["message"].(string) + if !ok { + return nil, fmt.Errorf("缺少必需参数: message") + } + p.Error(message) + return true, nil + + case "getLoggerStatus": + // 不需要参数 + status := map[string]interface{}{ + "initialized": p.logger != nil, + "config": p.config, + } + return status, nil + + default: + return nil, fmt.Errorf("未知的操作: %s", action) + } +} + +// Log 记录日志 +func (p *LoggerPlugin) Log(level, message string) { + if p.logger == nil { + fmt.Printf("[%s] %s\n", level, message) + return + } + + logMsg := fmt.Sprintf("[%s] %s", level, message) + p.logger.Println(logMsg) + + // 如果配置了同时输出到控制台 + if consoleOutput, ok := p.config["console_output"].(bool); ok && consoleOutput { + fmt.Println(logMsg) + } +} + +// Info 记录信息日志 +func (p *LoggerPlugin) Info(message string) { + p.Log("INFO", message) +} + +// Warn 记录警告日志 +func (p *LoggerPlugin) Warn(message string) { + p.Log("WARN", message) +} + +// Error 记录错误日志 +func (p *LoggerPlugin) Error(message string) { + p.Log("ERROR", message) +} + +// GetOperationInfo 获取操作的参数信息 +func (p *LoggerPlugin) GetOperationInfo(operation string) (*plugins.OperationInfo, error) { + switch operation { + case "log": + return &plugins.OperationInfo{ + Name: "log", + Description: "记录日志", + Params: []plugins.OperationParamInfo{ + { + Name: "level", + Type: "string", + Required: true, + Default: "INFO", + Description: "日志级别,可选值:INFO, WARN, ERROR", + }, + { + Name: "message", + Type: "string", + Required: true, + Default: "", + Description: "日志消息", + }, + }, + Extra: map[string]interface{}{ + "category": "日志操作", + }, + }, nil + case "info": + return &plugins.OperationInfo{ + Name: "info", + Description: "记录信息日志", + Params: []plugins.OperationParamInfo{ + { + Name: "message", + Type: "string", + Required: true, + Default: "", + Description: "日志消息", + }, + }, + Extra: map[string]interface{}{ + "category": "日志操作", + }, + }, nil + case "warn": + return &plugins.OperationInfo{ + Name: "warn", + Description: "记录警告日志", + Params: []plugins.OperationParamInfo{ + { + Name: "message", + Type: "string", + Required: true, + Default: "", + Description: "日志消息", + }, + }, + Extra: map[string]interface{}{ + "category": "日志操作", + }, + }, nil + case "error": + return &plugins.OperationInfo{ + Name: "error", + Description: "记录错误日志", + Params: []plugins.OperationParamInfo{ + { + Name: "message", + Type: "string", + Required: true, + Default: "", + Description: "日志消息", + }, + }, + Extra: map[string]interface{}{ + "category": "日志操作", + }, + }, nil + case "getLoggerStatus": + return &plugins.OperationInfo{ + Name: "getLoggerStatus", + Description: "获取日志记录器状态", + Params: []plugins.OperationParamInfo{}, + Extra: map[string]interface{}{ + "category": "系统操作", + }, + }, nil + default: + return nil, fmt.Errorf("插件 %s 不支持 %s 操作", p.Name(), operation) + } +} + +// GetAllOperations 获取所有操作及其参数信息 +func (p *LoggerPlugin) GetAllOperations() []*plugins.OperationInfo { + operations := []*plugins.OperationInfo{} + + // 添加 log 操作 + logOp, _ := p.GetOperationInfo("log") + operations = append(operations, logOp) + + // 添加 info 操作 + infoOp, _ := p.GetOperationInfo("info") + operations = append(operations, infoOp) + + // 添加 warn 操作 + warnOp, _ := p.GetOperationInfo("warn") + operations = append(operations, warnOp) + + // 添加 error 操作 + errorOp, _ := p.GetOperationInfo("error") + operations = append(operations, errorOp) + + // 添加 getLoggerStatus 操作 + statusOp, _ := p.GetOperationInfo("getLoggerStatus") + operations = append(operations, statusOp) + + return operations +} + +// main 函数是必须的,但不会被调用 +func main() { + // 不会被执行,仅用于编译插件 +} diff --git a/plugins/stats/stats_plugin.go b/plugins/stats/stats_plugin.go new file mode 100644 index 0000000..7d4703d --- /dev/null +++ b/plugins/stats/stats_plugin.go @@ -0,0 +1,363 @@ +package main + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/darkit/plugins" +) + +// StatsPlugin 统计插件 +// 用于收集和记录系统运行时统计数据 +type StatsPlugin struct { + *plugins.BasePlugin + stats map[string]int64 + startTime time.Time + mu sync.RWMutex + tickerStop chan bool + ticker *time.Ticker + config map[string]interface{} +} + +// StatsParams 统计请求参数结构体 +// 允许通过结构体传递参数,简化调用 +type StatsParams struct { + Name string `json:"name"` // 统计项名称 + Value int64 `json:"value"` // 统计值 + BytesReceived int64 `json:"bytesReceived"` // 接收字节数 + BytesSent int64 `json:"bytesSent"` // 发送字节数 + IsError bool `json:"isError"` // 是否为错误请求 +} + +// Plugin 导出的插件变量 +var Plugin = &StatsPlugin{ + // 使用默认构造函数,不指定插件类型,将默认为通用插件 + BasePlugin: plugins.NewBasePluginWithDefaultType( + "StatsPlugin", + "1.0.0", + "系统运行时统计插件", + "开发者", + ), + stats: make(map[string]int64), + tickerStop: make(chan bool), +} + +// 为展示如何指定类型,我们也可以显式设置插件类型 +// var Plugin = &StatsPlugin{ +// BasePlugin: plugins.NewBasePlugin( +// "StatsPlugin", +// "1.0.0", +// "系统运行时统计插件", +// "开发者", +// plugins.PluginTypeUtils, // 明确指定为工具类插件 +// ), +// stats: make(map[string]int64), +// tickerStop: make(chan bool), +// } + +// Init 初始化插件 +func (p *StatsPlugin) Init(ctx context.Context, config map[string]interface{}) error { + p.config = config + + // 初始化统计指标 + p.mu.Lock() + p.stats["requests"] = 0 + p.stats["errors"] = 0 + p.stats["bytes_sent"] = 0 + p.stats["bytes_received"] = 0 + p.mu.Unlock() + + fmt.Println("统计插件初始化完成") + return nil +} + +// Start 启动插件 +func (p *StatsPlugin) Start(ctx context.Context) error { + p.startTime = time.Now() + + // 启动定时统计任务 + interval := 60 * time.Second // 默认60秒 + + // 从配置中获取统计间隔 + if intervalSec, ok := p.config["interval_seconds"].(float64); ok { + interval = time.Duration(intervalSec) * time.Second + } + + p.ticker = time.NewTicker(interval) + + go func() { + for { + select { + case <-p.ticker.C: + p.logStats() + case <-p.tickerStop: + p.ticker.Stop() + return + case <-ctx.Done(): + p.ticker.Stop() + return + } + } + }() + + fmt.Println("统计插件已启动") + return nil +} + +// Stop 停止插件 +func (p *StatsPlugin) Stop(ctx context.Context) error { + if p.ticker != nil { + p.tickerStop <- true + } + + // 输出最终统计信息 + p.logStats() + + fmt.Println("统计插件已停止") + return nil +} + +// 以下方法将被自动注册为可通过Execute调用的操作 + +// IncrementStat 增加统计值 +// 会被自动注册为"incrementstat"操作 +func (p *StatsPlugin) IncrementStat(name string, value int64) error { + p.mu.Lock() + defer p.mu.Unlock() + + if _, exists := p.stats[name]; exists { + p.stats[name] += value + } else { + p.stats[name] = value + } + + return nil +} + +// GetStat 获取统计值 +// 会被自动注册为"getstat"操作 +func (p *StatsPlugin) GetStat(name string) (int64, error) { + p.mu.RLock() + defer p.mu.RUnlock() + + if value, exists := p.stats[name]; exists { + return value, nil + } + return 0, fmt.Errorf("统计项 %s 不存在", name) +} + +// RecordRequest 记录请求 +// 会被自动注册为"recordrequest"操作 +func (p *StatsPlugin) RecordRequest(ctx context.Context, params StatsParams) error { + p.IncrementStat("requests", 1) + p.IncrementStat("bytes_received", params.BytesReceived) + p.IncrementStat("bytes_sent", params.BytesSent) + + if params.IsError { + p.IncrementStat("errors", 1) + } + + return nil +} + +// ResetStats 重置统计数据 +// 会被自动注册为"resetstats"操作 +func (p *StatsPlugin) ResetStats() error { + p.mu.Lock() + defer p.mu.Unlock() + + p.stats = make(map[string]int64) + p.stats["requests"] = 0 + p.stats["errors"] = 0 + p.stats["bytes_sent"] = 0 + p.stats["bytes_received"] = 0 + p.startTime = time.Now() + + return nil +} + +// GenerateStatsReport 生成统计报告 +// 会被自动注册为"generatestatsreport"操作 +func (p *StatsPlugin) GenerateStatsReport() (map[string]interface{}, error) { + p.mu.RLock() + defer p.mu.RUnlock() + + uptime := time.Since(p.startTime).Seconds() + report := map[string]interface{}{ + "uptime_seconds": uptime, + "stats": p.stats, + } + + if uptime > 0 && p.stats["requests"] > 0 { + report["requests_per_second"] = float64(p.stats["requests"]) / uptime + report["error_rate"] = float64(p.stats["errors"]) * 100 / float64(p.stats["requests"]) + } + + return report, nil +} + +// GetAllStats 获取所有统计数据 +// 会被自动注册为"getallstats"操作 +func (p *StatsPlugin) GetAllStats() (map[string]int64, error) { + p.mu.RLock() + defer p.mu.RUnlock() + + // 创建一个副本 + statsCopy := make(map[string]int64, len(p.stats)) + for k, v := range p.stats { + statsCopy[k] = v + } + + // 添加运行时间 + statsCopy["uptime_seconds"] = int64(time.Since(p.startTime).Seconds()) + + return statsCopy, nil +} + +// logStats 记录当前统计信息 +// 不会被注册为操作,因为它是内部方法 +func (p *StatsPlugin) logStats() { + p.mu.RLock() + defer p.mu.RUnlock() + + uptime := time.Since(p.startTime).Seconds() + + fmt.Printf("===== 系统统计信息 =====\n") + fmt.Printf("运行时间: %.2f 秒\n", uptime) + fmt.Printf("总请求数: %d\n", p.stats["requests"]) + fmt.Printf("错误数: %d\n", p.stats["errors"]) + fmt.Printf("发送字节: %d\n", p.stats["bytes_sent"]) + fmt.Printf("接收字节: %d\n", p.stats["bytes_received"]) + + if uptime > 0 && p.stats["requests"] > 0 { + fmt.Printf("平均请求/秒: %.2f\n", float64(p.stats["requests"])/uptime) + fmt.Printf("错误率: %.2f%%\n", float64(p.stats["errors"])*100/float64(p.stats["requests"])) + } + + fmt.Printf("=======================\n") +} + +// GetOperationInfo 获取操作的参数信息 +func (p *StatsPlugin) GetOperationInfo(operation string) (*plugins.OperationInfo, error) { + switch operation { + case "record": + return &plugins.OperationInfo{ + Name: "record", + Description: "记录统计数据", + Params: []plugins.OperationParamInfo{ + { + Name: "key", + Type: "string", + Required: true, + Default: "", + Description: "统计项的唯一标识符", + }, + { + Name: "value", + Type: "float64", + Required: true, + Default: 0, + Description: "要记录的数值", + }, + { + Name: "tags", + Type: "object", + Required: false, + Default: map[string]string{}, + Description: "额外的标签信息", + }, + }, + Extra: map[string]interface{}{ + "category": "数据统计", + }, + }, nil + case "increment": + return &plugins.OperationInfo{ + Name: "increment", + Description: "增加计数器", + Params: []plugins.OperationParamInfo{ + { + Name: "key", + Type: "string", + Required: true, + Default: "", + Description: "计数器的唯一标识符", + }, + { + Name: "amount", + Type: "float64", + Required: false, + Default: 1, + Description: "增加的数量,默认为1", + }, + { + Name: "tags", + Type: "object", + Required: false, + Default: map[string]string{}, + Description: "额外的标签信息", + }, + }, + Extra: map[string]interface{}{ + "category": "数据统计", + }, + }, nil + case "getStats": + return &plugins.OperationInfo{ + Name: "getStats", + Description: "获取统计数据", + Params: []plugins.OperationParamInfo{ + { + Name: "key", + Type: "string", + Required: false, + Default: "", + Description: "统计项的唯一标识符,如果为空则返回所有统计项", + }, + }, + Extra: map[string]interface{}{ + "category": "系统操作", + }, + }, nil + case "reset": + return &plugins.OperationInfo{ + Name: "reset", + Description: "重置统计数据", + Params: []plugins.OperationParamInfo{ + { + Name: "key", + Type: "string", + Required: false, + Default: "", + Description: "统计项的唯一标识符,如果为空则重置所有统计项", + }, + }, + Extra: map[string]interface{}{ + "category": "系统操作", + }, + }, nil + default: + return nil, fmt.Errorf("插件 %s 不支持 %s 操作", p.Name(), operation) + } +} + +// GetAllOperations 获取所有操作及其参数信息 +func (p *StatsPlugin) GetAllOperations() []*plugins.OperationInfo { + operations := []*plugins.OperationInfo{} + + // 添加所有支持的操作 + ops := []string{"record", "increment", "getStats", "reset"} + for _, op := range ops { + info, _ := p.GetOperationInfo(op) + operations = append(operations, info) + } + + return operations +} + +// main 函数是必须的,但不会被调用 +func main() { + // 不会被执行,仅用于编译插件 +} diff --git a/plugins/storage/storage_plugin.go b/plugins/storage/storage_plugin.go new file mode 100644 index 0000000..a560273 --- /dev/null +++ b/plugins/storage/storage_plugin.go @@ -0,0 +1,344 @@ +package main + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/darkit/plugins" +) + +// StoragePlugin 存储插件 +// 提供简单的文件存储功能 +type StoragePlugin struct { + *plugins.BasePlugin // 嵌入基本插件结构 + storageDir string // 存储目录 + config map[string]interface{} // 配置 + mu sync.RWMutex // 读写锁 +} + +// Plugin 导出的插件变量 +// 注意:变量名必须是Plugin,大小写敏感 +var Plugin = &StoragePlugin{ + BasePlugin: plugins.NewBasePlugin( + "StoragePlugin", + "1.0.0", + "简单的文件存储插件", + "开发者", + plugins.PluginTypeStorage, // 设置插件类型为存储插件 + ), +} + +// Init 初始化插件 +func (p *StoragePlugin) Init(ctx context.Context, config map[string]interface{}) error { + p.config = config + + // 获取存储目录路径 + storageDir, ok := config["storage_dir"].(string) + if !ok { + // 使用默认路径 + storageDir = "storage" + } + + // 确保存储目录存在 + if err := os.MkdirAll(storageDir, 0o755); err != nil { + return fmt.Errorf("创建存储目录失败: %v", err) + } + + p.storageDir = storageDir + fmt.Println("存储插件初始化完成,存储目录:", storageDir) + + return nil +} + +// Start 启动插件 +func (p *StoragePlugin) Start(ctx context.Context) error { + if p.storageDir == "" { + return fmt.Errorf("插件未初始化") + } + + fmt.Println("存储插件已启动") + return nil +} + +// Stop 停止插件 +func (p *StoragePlugin) Stop(ctx context.Context) error { + fmt.Println("存储插件已停止") + return nil +} + +// Execute 执行插件功能 +func (p *StoragePlugin) Execute(ctx context.Context, action string, params map[string]interface{}) (interface{}, error) { + switch action { + case "saveFile": + // 需要参数: filename, data + filename, ok := params["filename"].(string) + if !ok { + return nil, fmt.Errorf("缺少必需参数: filename") + } + + // 处理两种数据格式:字符串或Base64编码的二进制数据 + var data []byte + if dataStr, ok := params["data"].(string); ok { + // 检查是否为Base64编码 + if base64Str, ok := params["isBase64"].(bool); ok && base64Str { + var err error + data, err = base64.StdEncoding.DecodeString(dataStr) + if err != nil { + return nil, fmt.Errorf("Base64解码失败: %v", err) + } + } else { + data = []byte(dataStr) + } + } else { + return nil, fmt.Errorf("缺少必需参数: data") + } + + err := p.SaveFile(filename, data) + return err == nil, err + + case "loadFile": + // 需要参数: filename, returnBase64 + filename, ok := params["filename"].(string) + if !ok { + return nil, fmt.Errorf("缺少必需参数: filename") + } + + returnBase64, _ := params["returnBase64"].(bool) + + data, err := p.LoadFile(filename) + if err != nil { + return nil, err + } + + if returnBase64 { + return base64.StdEncoding.EncodeToString(data), nil + } + return string(data), nil + + case "deleteFile": + // 需要参数: filename + filename, ok := params["filename"].(string) + if !ok { + return nil, fmt.Errorf("缺少必需参数: filename") + } + + err := p.DeleteFile(filename) + return err == nil, err + + case "listFiles": + // 不需要参数 + files, err := p.ListFiles() + return files, err + + case "getStorageInfo": + // 不需要参数 + info := map[string]interface{}{ + "storageDir": p.storageDir, + "config": p.config, + } + return info, nil + + default: + return nil, fmt.Errorf("未知的操作: %s", action) + } +} + +// SaveFile 保存文件 +func (p *StoragePlugin) SaveFile(filename string, data []byte) error { + p.mu.Lock() + defer p.mu.Unlock() + + if p.storageDir == "" { + return fmt.Errorf("插件未初始化") + } + + filePath := filepath.Join(p.storageDir, filename) + return os.WriteFile(filePath, data, 0o644) +} + +// LoadFile 加载文件 +func (p *StoragePlugin) LoadFile(filename string) ([]byte, error) { + p.mu.RLock() + defer p.mu.RUnlock() + + if p.storageDir == "" { + return nil, fmt.Errorf("插件未初始化") + } + + filePath := filepath.Join(p.storageDir, filename) + return os.ReadFile(filePath) +} + +// DeleteFile 删除文件 +func (p *StoragePlugin) DeleteFile(filename string) error { + p.mu.Lock() + defer p.mu.Unlock() + + if p.storageDir == "" { + return fmt.Errorf("插件未初始化") + } + + filePath := filepath.Join(p.storageDir, filename) + return os.Remove(filePath) +} + +// ListFiles 列出所有文件 +func (p *StoragePlugin) ListFiles() ([]string, error) { + p.mu.RLock() + defer p.mu.RUnlock() + + if p.storageDir == "" { + return nil, fmt.Errorf("插件未初始化") + } + + var files []string + entries, err := os.ReadDir(p.storageDir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if !entry.IsDir() { + files = append(files, entry.Name()) + } + } + + return files, nil +} + +// GetOperationInfo 获取操作的参数信息 +func (p *StoragePlugin) GetOperationInfo(operation string) (*plugins.OperationInfo, error) { + switch operation { + case "set": + return &plugins.OperationInfo{ + Name: "set", + Description: "存储数据", + Params: []plugins.OperationParamInfo{ + { + Name: "key", + Type: "string", + Required: true, + Default: "", + Description: "数据的键名", + }, + { + Name: "value", + Type: "any", + Required: true, + Default: nil, + Description: "要存储的数据", + }, + { + Name: "expiration", + Type: "integer", + Required: false, + Default: 0, + Description: "过期时间(秒),0表示永不过期", + }, + }, + Extra: map[string]interface{}{ + "category": "数据存储", + }, + }, nil + case "get": + return &plugins.OperationInfo{ + Name: "get", + Description: "获取数据", + Params: []plugins.OperationParamInfo{ + { + Name: "key", + Type: "string", + Required: true, + Default: "", + Description: "数据的键名", + }, + { + Name: "defaultValue", + Type: "any", + Required: false, + Default: nil, + Description: "如果键不存在时返回的默认值", + }, + }, + Extra: map[string]interface{}{ + "category": "数据存储", + }, + }, nil + case "delete": + return &plugins.OperationInfo{ + Name: "delete", + Description: "删除数据", + Params: []plugins.OperationParamInfo{ + { + Name: "key", + Type: "string", + Required: true, + Default: "", + Description: "要删除的数据键名", + }, + }, + Extra: map[string]interface{}{ + "category": "数据存储", + }, + }, nil + case "exists": + return &plugins.OperationInfo{ + Name: "exists", + Description: "检查键是否存在", + Params: []plugins.OperationParamInfo{ + { + Name: "key", + Type: "string", + Required: true, + Default: "", + Description: "要检查的数据键名", + }, + }, + Extra: map[string]interface{}{ + "category": "数据存储", + }, + }, nil + case "keys": + return &plugins.OperationInfo{ + Name: "keys", + Description: "获取所有键名", + Params: []plugins.OperationParamInfo{ + { + Name: "pattern", + Type: "string", + Required: false, + Default: "*", + Description: "键名匹配模式,支持通配符 *", + }, + }, + Extra: map[string]interface{}{ + "category": "数据存储", + }, + }, nil + default: + return nil, fmt.Errorf("插件 %s 不支持 %s 操作", p.Name(), operation) + } +} + +// GetAllOperations 获取所有操作及其参数信息 +func (p *StoragePlugin) GetAllOperations() []*plugins.OperationInfo { + operations := []*plugins.OperationInfo{} + + // 添加所有支持的操作 + ops := []string{"set", "get", "delete", "exists", "keys"} + for _, op := range ops { + info, _ := p.GetOperationInfo(op) + operations = append(operations, info) + } + + return operations +} + +// main 函数是必须的,但不会被调用 +func main() { + // 不会被执行,仅用于编译插件 +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..a31b800 --- /dev/null +++ b/types.go @@ -0,0 +1,8 @@ +package plugins + +// PluginOperations 插件操作集合 +type PluginOperations struct { + PluginName string `json:"pluginName"` // 插件名称 + PluginType PluginType `json:"pluginType"` // 插件类型 + Operations []*OperationInfo `json:"operations"` // 操作列表 +}