基于Go语言的动态插件系统,支持热插拔和动态加载插件。
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.idea
|
||||
.vscode
|
||||
*.so
|
||||
*.log
|
||||
|
||||
dist
|
||||
logs
|
||||
example/storage
|
||||
web_admin
|
||||
280
README.md
Normal file
280
README.md
Normal file
@@ -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
|
||||
|
||||
## 许可证
|
||||
|
||||
[添加许可证信息]
|
||||
98
build_all.sh
Executable file
98
build_all.sh
Executable file
@@ -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
|
||||
856
example/demo_utils_client.html
Normal file
856
example/demo_utils_client.html
Normal file
@@ -0,0 +1,856 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DemoUtilsPlugin 测试客户端</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #005e8a;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 5px;
|
||||
border-left: 4px solid #005e8a;
|
||||
}
|
||||
.panel {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
background-color: white;
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
button {
|
||||
background-color: #005e8a;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 15px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #004a6e;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #cccccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.input-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
#operationForm {
|
||||
max-width: 600px;
|
||||
}
|
||||
#resultPanel, #eventPanel {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
background-color: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
.success {
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}
|
||||
.error {
|
||||
color: #dc3545;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tab-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.tab-header {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.tab-button {
|
||||
padding: 8px 15px;
|
||||
background-color: #f0f0f0;
|
||||
border: none;
|
||||
margin-right: 5px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tab-button.active {
|
||||
background-color: #005e8a;
|
||||
color: white;
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.event-log {
|
||||
padding: 5px;
|
||||
margin: 5px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.event-timestamp {
|
||||
color: #666;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.parameter-row {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.parameter-row .key {
|
||||
flex: 1;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.parameter-row .value {
|
||||
flex: 2;
|
||||
}
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.checkbox-group input {
|
||||
width: auto;
|
||||
margin-right: 10px;
|
||||
}
|
||||
#actionsList {
|
||||
margin-bottom: 20px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.action-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
background-color: #f0f0f0;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.action-item:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>DemoUtilsPlugin 测试客户端</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>插件状态</h2>
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span>插件信息</span>
|
||||
<button id="refreshStatus">刷新状态</button>
|
||||
</div>
|
||||
<div id="pluginStatus">正在加载插件信息...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>操作调用</h2>
|
||||
<div class="tab-container">
|
||||
<div class="tab-header">
|
||||
<button class="tab-button active" data-tab="operations">可用操作</button>
|
||||
<button class="tab-button" data-tab="execute">执行操作</button>
|
||||
<button class="tab-button" data-tab="events">事件监听</button>
|
||||
</div>
|
||||
|
||||
<!-- 可用操作列表 -->
|
||||
<div id="operations" class="tab-content active">
|
||||
<p>以下是 DemoUtilsPlugin 支持的所有操作:</p>
|
||||
<div id="actionsList">
|
||||
正在加载操作列表...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 执行操作表单 -->
|
||||
<div id="execute" class="tab-content">
|
||||
<div class="grid">
|
||||
<div class="panel">
|
||||
<div class="panel-header">执行操作</div>
|
||||
<form id="operationForm">
|
||||
<div class="input-group">
|
||||
<label for="operation">选择操作:</label>
|
||||
<select id="operation" name="operation" required>
|
||||
<option value="">-- 请选择操作 --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="parameterFields">
|
||||
<!-- 参数字段将在这里动态生成 -->
|
||||
</div>
|
||||
<button type="submit">执行</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header">执行结果</div>
|
||||
<div id="resultPanel">
|
||||
尚未执行任何操作。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 事件监听 -->
|
||||
<div id="events" class="tab-content">
|
||||
<div class="grid">
|
||||
<div class="panel">
|
||||
<div class="panel-header">事件订阅</div>
|
||||
<div class="input-group">
|
||||
<label>订阅事件类型:</label>
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="eventInitialized" value="initialized" checked>
|
||||
<label for="eventInitialized">初始化</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="eventStarted" value="started" checked>
|
||||
<label for="eventStarted">启动</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="eventStopped" value="stopped" checked>
|
||||
<label for="eventStopped">停止</label>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="eventCustom" value="custom" checked>
|
||||
<label for="eventCustom">自定义</label>
|
||||
</div>
|
||||
</div>
|
||||
<button id="subscribeEvents">订阅事件</button>
|
||||
<button id="unsubscribeEvents">取消订阅</button>
|
||||
<button id="triggerCustomEvent">触发自定义事件</button>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span>事件日志</span>
|
||||
<button id="clearEvents">清空</button>
|
||||
</div>
|
||||
<div id="eventPanel">
|
||||
尚未接收到任何事件。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>快速操作</h2>
|
||||
<button id="testCalculate">计算测试 (10*5)</button>
|
||||
<button id="testFormat">格式化测试 (大写转换)</button>
|
||||
<button id="testStore">数据存储测试</button>
|
||||
<button id="testRetrieve">数据检索测试</button>
|
||||
<button id="testCounter">计数器操作测试</button>
|
||||
<button id="testSort">排序测试</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 定义全局变量
|
||||
const PLUGIN_NAME = 'DemoUtilsPlugin';
|
||||
let operations = [];
|
||||
let isSubscribed = false;
|
||||
let eventSource = null;
|
||||
|
||||
// 当文档加载完成时执行
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化标签页切换
|
||||
initTabs();
|
||||
|
||||
// 加载插件状态
|
||||
loadPluginStatus();
|
||||
|
||||
// 加载操作列表
|
||||
loadOperations();
|
||||
|
||||
// 绑定表单提交事件
|
||||
document.getElementById('operationForm').addEventListener('submit', executeOperation);
|
||||
|
||||
// 绑定操作选择变化事件
|
||||
document.getElementById('operation').addEventListener('change', updateParameterFields);
|
||||
|
||||
// 绑定事件相关按钮
|
||||
document.getElementById('subscribeEvents').addEventListener('click', subscribeEvents);
|
||||
document.getElementById('unsubscribeEvents').addEventListener('click', unsubscribeEvents);
|
||||
document.getElementById('clearEvents').addEventListener('click', clearEventLog);
|
||||
document.getElementById('triggerCustomEvent').addEventListener('click', triggerCustomEvent);
|
||||
|
||||
// 绑定刷新状态按钮
|
||||
document.getElementById('refreshStatus').addEventListener('click', loadPluginStatus);
|
||||
|
||||
// 绑定快速操作按钮
|
||||
document.getElementById('testCalculate').addEventListener('click', () => quickTestOperation('calculate'));
|
||||
document.getElementById('testFormat').addEventListener('click', () => quickTestOperation('format'));
|
||||
document.getElementById('testStore').addEventListener('click', () => quickTestOperation('store'));
|
||||
document.getElementById('testRetrieve').addEventListener('click', () => quickTestOperation('retrieve'));
|
||||
document.getElementById('testCounter').addEventListener('click', () => quickTestOperation('counter'));
|
||||
document.getElementById('testSort').addEventListener('click', () => quickTestOperation('sort'));
|
||||
});
|
||||
|
||||
// 初始化标签页
|
||||
function initTabs() {
|
||||
const tabButtons = document.querySelectorAll('.tab-button');
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
// 移除所有活动类
|
||||
document.querySelectorAll('.tab-button').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// 添加活动类
|
||||
this.classList.add('active');
|
||||
const tabId = this.getAttribute('data-tab');
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 加载插件状态
|
||||
async function loadPluginStatus() {
|
||||
try {
|
||||
const statusElem = document.getElementById('pluginStatus');
|
||||
statusElem.innerHTML = '正在加载插件信息...';
|
||||
|
||||
const response = await fetch('/api/plugins');
|
||||
const plugins = await response.json();
|
||||
|
||||
const demoPlugin = plugins.find(p => p.name === PLUGIN_NAME);
|
||||
|
||||
if (demoPlugin) {
|
||||
let html = `
|
||||
<div>
|
||||
<p><strong>名称:</strong> ${demoPlugin.name}</p>
|
||||
<p><strong>版本:</strong> ${demoPlugin.version}</p>
|
||||
<p><strong>描述:</strong> ${demoPlugin.description}</p>
|
||||
<p><strong>作者:</strong> ${demoPlugin.author}</p>
|
||||
<p><strong>类型:</strong> ${demoPlugin.type}</p>
|
||||
<p><strong>状态:</strong> <span class="${demoPlugin.enabled ? 'success' : 'error'}">${demoPlugin.enabled ? '已启用' : '已禁用'}</span></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (demoPlugin.config && Object.keys(demoPlugin.config).length > 0) {
|
||||
html += '<p><strong>配置:</strong></p><ul>';
|
||||
for (const [key, value] of Object.entries(demoPlugin.config)) {
|
||||
html += `<li><strong>${key}:</strong> ${value}</li>`;
|
||||
}
|
||||
html += '</ul>';
|
||||
}
|
||||
|
||||
statusElem.innerHTML = html;
|
||||
} else {
|
||||
statusElem.innerHTML = '<p class="error">未找到 DemoUtilsPlugin 插件。请确保它已正确加载。</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('pluginStatus').innerHTML = `<p class="error">加载插件状态失败: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载操作列表
|
||||
async function loadOperations() {
|
||||
try {
|
||||
const actionsListElem = document.getElementById('actionsList');
|
||||
const operationSelectElem = document.getElementById('operation');
|
||||
|
||||
actionsListElem.innerHTML = '正在加载操作列表...';
|
||||
|
||||
const response = await fetch(`/api/operations/${PLUGIN_NAME}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.operations) {
|
||||
operations = data.operations;
|
||||
|
||||
let html = '';
|
||||
operationSelectElem.innerHTML = '<option value="">-- 请选择操作 --</option>';
|
||||
|
||||
operations.forEach(op => {
|
||||
// 更新可用操作列表
|
||||
html += `
|
||||
<div class="action-item">
|
||||
<span><strong>${op.name}</strong>: ${op.description || '无描述'}</span>
|
||||
<button onclick="selectOperation('${op.name}')">使用此操作</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 更新操作选择下拉框
|
||||
operationSelectElem.innerHTML += `<option value="${op.name}">${op.name} - ${op.description || '无描述'}</option>`;
|
||||
});
|
||||
|
||||
actionsListElem.innerHTML = html || '<p>没有可用的操作。</p>';
|
||||
} else {
|
||||
actionsListElem.innerHTML = '<p class="error">未找到任何操作。</p>';
|
||||
operationSelectElem.innerHTML = '<option value="">无可用操作</option>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('actionsList').innerHTML = `<p class="error">加载操作列表失败: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 选择操作
|
||||
function selectOperation(operationName) {
|
||||
// 切换到执行标签页
|
||||
document.querySelectorAll('.tab-button').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
document.querySelector('[data-tab="execute"]').classList.add('active');
|
||||
document.getElementById('execute').classList.add('active');
|
||||
|
||||
// 设置选定的操作
|
||||
document.getElementById('operation').value = operationName;
|
||||
|
||||
// 更新参数字段
|
||||
updateParameterFields();
|
||||
}
|
||||
|
||||
// 更新参数字段
|
||||
function updateParameterFields() {
|
||||
const operationName = document.getElementById('operation').value;
|
||||
const parameterFieldsElem = document.getElementById('parameterFields');
|
||||
|
||||
if (!operationName) {
|
||||
parameterFieldsElem.innerHTML = '<p>请先选择一个操作。</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const operation = operations.find(op => op.name === operationName);
|
||||
|
||||
if (operation && operation.params && operation.params.length > 0) {
|
||||
let html = '';
|
||||
|
||||
operation.params.forEach(param => {
|
||||
html += `
|
||||
<div class="input-group">
|
||||
<label for="param-${param.name}">${param.name}${param.required ? ' *' : ''}:</label>
|
||||
<div class="parameter-description">${param.description || ''}</div>
|
||||
`;
|
||||
|
||||
if (param.type === 'boolean' || param.type === 'bool') {
|
||||
html += `
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="param-${param.name}" name="${param.name}" ${param.default ? 'checked' : ''}>
|
||||
</div>
|
||||
`;
|
||||
} else if (param.type === 'integer' || param.type === 'int' || param.type === 'int64') {
|
||||
html += `
|
||||
<input type="number" id="param-${param.name}" name="${param.name}" value="${param.default || ''}" ${param.required ? 'required' : ''}>
|
||||
`;
|
||||
} else if (param.type === 'float' || param.type === 'float64') {
|
||||
html += `
|
||||
<input type="number" id="param-${param.name}" name="${param.name}" step="0.01" value="${param.default || ''}" ${param.required ? 'required' : ''}>
|
||||
`;
|
||||
} else {
|
||||
html += `
|
||||
<input type="text" id="param-${param.name}" name="${param.name}" value="${param.default || ''}" ${param.required ? 'required' : ''}>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
parameterFieldsElem.innerHTML = html;
|
||||
} else {
|
||||
parameterFieldsElem.innerHTML = '<p>此操作不需要参数。</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// 执行操作
|
||||
async function executeOperation(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const operationName = document.getElementById('operation').value;
|
||||
if (!operationName) {
|
||||
alert('请选择一个操作');
|
||||
return;
|
||||
}
|
||||
|
||||
const operation = operations.find(op => op.name === operationName);
|
||||
if (!operation) {
|
||||
alert('未找到所选操作');
|
||||
return;
|
||||
}
|
||||
|
||||
// 收集参数
|
||||
const parameters = {};
|
||||
|
||||
if (operation.params && operation.params.length > 0) {
|
||||
operation.params.forEach(param => {
|
||||
const element = document.getElementById(`param-${param.name}`);
|
||||
if (element) {
|
||||
if (param.type === 'boolean' || param.type === 'bool') {
|
||||
parameters[param.name] = element.checked;
|
||||
} else if (param.type === 'integer' || param.type === 'int' || param.type === 'int64') {
|
||||
parameters[param.name] = parseInt(element.value || '0');
|
||||
} else if (param.type === 'float' || param.type === 'float64') {
|
||||
parameters[param.name] = parseFloat(element.value || '0');
|
||||
} else {
|
||||
parameters[param.name] = element.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const resultElem = document.getElementById('resultPanel');
|
||||
resultElem.innerHTML = '正在执行操作...';
|
||||
|
||||
const response = await fetch('/api/execute', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
plugin: PLUGIN_NAME,
|
||||
operation: operationName,
|
||||
parameters: parameters
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
resultElem.innerHTML = `
|
||||
<div class="success">操作成功!</div>
|
||||
<h3>结果:</h3>
|
||||
<pre>${formatJSON(data.result)}</pre>
|
||||
`;
|
||||
} else {
|
||||
resultElem.innerHTML = `
|
||||
<div class="error">操作失败: ${data.error}</div>
|
||||
<h3>请求参数:</h3>
|
||||
<pre>${formatJSON(parameters)}</pre>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('resultPanel').innerHTML = `
|
||||
<div class="error">执行操作时发生错误: ${error.message}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅事件
|
||||
function subscribeEvents() {
|
||||
if (isSubscribed) {
|
||||
alert('已经订阅了事件');
|
||||
return;
|
||||
}
|
||||
|
||||
const eventTypes = [];
|
||||
if (document.getElementById('eventInitialized').checked) eventTypes.push('initialized');
|
||||
if (document.getElementById('eventStarted').checked) eventTypes.push('started');
|
||||
if (document.getElementById('eventStopped').checked) eventTypes.push('stopped');
|
||||
if (document.getElementById('eventCustom').checked) eventTypes.push('custom');
|
||||
|
||||
if (eventTypes.length === 0) {
|
||||
alert('请至少选择一种事件类型');
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用EventSource订阅服务器发送的事件
|
||||
const eventPanel = document.getElementById('eventPanel');
|
||||
eventPanel.innerHTML = '<div class="success">正在连接事件流...</div>';
|
||||
|
||||
// 创建EventSource连接
|
||||
eventSource = new EventSource(`/events/${PLUGIN_NAME}`);
|
||||
|
||||
// 连接建立时
|
||||
eventSource.addEventListener('connected', function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
eventPanel.innerHTML = `<div class="success">已成功连接到插件 ${data.pluginName} 的事件流</div>`;
|
||||
});
|
||||
|
||||
// 处理各种事件类型
|
||||
for (const eventType of ['initialized', 'started', 'stopped', 'custom', 'error', 'loaded']) {
|
||||
eventSource.addEventListener(eventType, function(e) {
|
||||
const event = JSON.parse(e.data);
|
||||
|
||||
// 根据选中的事件类型过滤
|
||||
if ((eventType === 'initialized' && !document.getElementById('eventInitialized').checked) ||
|
||||
(eventType === 'started' && !document.getElementById('eventStarted').checked) ||
|
||||
(eventType === 'stopped' && !document.getElementById('eventStopped').checked) ||
|
||||
(eventType === 'custom' && !document.getElementById('eventCustom').checked)) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventPanel.innerHTML += `
|
||||
<div class="event-log">
|
||||
<span class="event-timestamp">[${getCurrentTime()}]</span>
|
||||
<strong>事件类型: ${eventType}</strong><br>
|
||||
数据: ${formatJSON(event)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 滚动到底部
|
||||
eventPanel.scrollTop = eventPanel.scrollHeight;
|
||||
});
|
||||
}
|
||||
|
||||
// 处理通用消息
|
||||
eventSource.onmessage = function(e) {
|
||||
console.log('收到未处理的事件: ', e.data);
|
||||
};
|
||||
|
||||
// 处理连接打开
|
||||
eventSource.onopen = function() {
|
||||
console.log('连接已打开');
|
||||
isSubscribed = true;
|
||||
document.getElementById('subscribeEvents').disabled = true;
|
||||
document.getElementById('unsubscribeEvents').disabled = false;
|
||||
};
|
||||
|
||||
// 处理错误
|
||||
eventSource.onerror = function(e) {
|
||||
console.error('SSE错误:', e);
|
||||
eventPanel.innerHTML += `
|
||||
<div class="event-log error">
|
||||
<span class="event-timestamp">[${getCurrentTime()}]</span>
|
||||
<strong>连接错误</strong><br>
|
||||
尝试重新连接...
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
}
|
||||
|
||||
// 取消订阅事件
|
||||
function unsubscribeEvents() {
|
||||
if (!isSubscribed || !eventSource) {
|
||||
alert('尚未订阅事件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 关闭EventSource连接
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
isSubscribed = false;
|
||||
|
||||
document.getElementById('subscribeEvents').disabled = false;
|
||||
document.getElementById('unsubscribeEvents').disabled = true;
|
||||
|
||||
const eventPanel = document.getElementById('eventPanel');
|
||||
eventPanel.innerHTML += '<div class="event-log"><span class="event-timestamp">[' + getCurrentTime() + ']</span> 已取消订阅事件</div>';
|
||||
}
|
||||
|
||||
// 清空事件日志
|
||||
function clearEventLog() {
|
||||
const eventPanel = document.getElementById('eventPanel');
|
||||
eventPanel.innerHTML = isSubscribed
|
||||
? '<div class="success">已成功订阅事件,等待事件发生...</div>'
|
||||
: '尚未接收到任何事件。';
|
||||
}
|
||||
|
||||
// 触发自定义事件
|
||||
async function triggerCustomEvent() {
|
||||
try {
|
||||
const eventPanel = document.getElementById('eventPanel');
|
||||
|
||||
// 调用服务器触发自定义事件
|
||||
const response = await fetch('/api/trigger-event', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
plugin: PLUGIN_NAME,
|
||||
type: 'custom',
|
||||
data: {
|
||||
action: 'user_triggered',
|
||||
time: new Date().toISOString(),
|
||||
message: '用户触发的自定义事件'
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
eventPanel.innerHTML += `
|
||||
<div class="event-log">
|
||||
<span class="event-timestamp">[${getCurrentTime()}]</span>
|
||||
<span>已触发自定义事件,如果已订阅事件,应该会收到通知</span>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
eventPanel.innerHTML += `
|
||||
<div class="event-log error">
|
||||
<span class="event-timestamp">[${getCurrentTime()}]</span>
|
||||
<span>触发自定义事件失败: ${data.message || '未知错误'}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
const eventPanel = document.getElementById('eventPanel');
|
||||
eventPanel.innerHTML += `
|
||||
<div class="event-log error">
|
||||
<span class="event-timestamp">[${getCurrentTime()}]</span>
|
||||
<span>触发自定义事件时发生错误: ${error.message}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 快速测试操作
|
||||
async function quickTestOperation(operationType) {
|
||||
let operationName = operationType;
|
||||
let parameters = {};
|
||||
|
||||
// 根据操作类型设置默认参数
|
||||
switch (operationType) {
|
||||
case 'calculate':
|
||||
parameters = { expression: '10*5' };
|
||||
break;
|
||||
case 'format':
|
||||
parameters = { text: 'hello world', type: 'upper' };
|
||||
break;
|
||||
case 'store':
|
||||
parameters = { key: 'test_key', value: '测试数据 ' + new Date().toLocaleString() };
|
||||
break;
|
||||
case 'retrieve':
|
||||
parameters = { key: 'test_key' };
|
||||
break;
|
||||
case 'counter':
|
||||
parameters = { action: 'increment' };
|
||||
break;
|
||||
case 'sort':
|
||||
parameters = { items: '5,3,8,1,9,2', numeric: true };
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const resultElem = document.getElementById('resultPanel');
|
||||
resultElem.innerHTML = `正在执行快速测试: ${operationName}...`;
|
||||
|
||||
// 切换到执行标签页
|
||||
document.querySelectorAll('.tab-button').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
document.querySelector('[data-tab="execute"]').classList.add('active');
|
||||
document.getElementById('execute').classList.add('active');
|
||||
|
||||
const response = await fetch('/api/execute', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
plugin: PLUGIN_NAME,
|
||||
operation: operationName,
|
||||
parameters: parameters
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
resultElem.innerHTML = `
|
||||
<div class="success">快速测试成功!</div>
|
||||
<h3>操作: ${operationName}</h3>
|
||||
<h3>参数:</h3>
|
||||
<pre>${formatJSON(parameters)}</pre>
|
||||
<h3>结果:</h3>
|
||||
<pre>${formatJSON(data.result)}</pre>
|
||||
`;
|
||||
} else {
|
||||
resultElem.innerHTML = `
|
||||
<div class="error">快速测试失败: ${data.error}</div>
|
||||
<h3>操作: ${operationName}</h3>
|
||||
<h3>参数:</h3>
|
||||
<pre>${formatJSON(parameters)}</pre>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('resultPanel').innerHTML = `
|
||||
<div class="error">执行快速测试时发生错误: ${error.message}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:格式化JSON
|
||||
function formatJSON(data) {
|
||||
if (!data) return 'null';
|
||||
try {
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
return String(data);
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:获取当前时间
|
||||
function getCurrentTime() {
|
||||
const now = new Date();
|
||||
return now.toLocaleTimeString() + '.' + now.getMilliseconds().toString().padStart(3, '0');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1490
example/web_admin.go
Normal file
1490
example/web_admin.go
Normal file
File diff suppressed because it is too large
Load Diff
361
interface.go
Normal file
361
interface.go
Normal file
@@ -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
|
||||
}
|
||||
330
plugins/defaultlogger/default_logger_plugin.go
Normal file
330
plugins/defaultlogger/default_logger_plugin.go
Normal file
@@ -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() {
|
||||
// 不会被执行,仅用于编译插件
|
||||
}
|
||||
163
plugins/demoutils/README.md
Normal file
163
plugins/demoutils/README.md
Normal file
@@ -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: 要格式化的文本<br>type: 格式化类型 (upper, lower, title, trim) |
|
||||
| encode | 编码文本 | text: 要编码的文本<br>encoding: 编码类型 (base64, url) |
|
||||
| decode | 解码文本 | text: 要解码的文本<br>encoding: 解码类型 (base64, url) |
|
||||
|
||||
### 数学工具
|
||||
|
||||
| 操作 | 描述 | 参数 |
|
||||
|-----|------|------|
|
||||
| calculate | 执行基本数学计算 | expression: 数学表达式,例如 1+2 |
|
||||
| sort | 排序文本行或数字 | items: 要排序的项目,逗号分隔<br>order: 排序顺序 (asc, desc)<br>numeric: 是否按数字排序 |
|
||||
|
||||
### 数据存储
|
||||
|
||||
| 操作 | 描述 | 参数 |
|
||||
|-----|------|------|
|
||||
| store | 存储键值数据 | key: 数据键名<br>value: 要存储的值 |
|
||||
| retrieve | 根据键获取数据 | key: 数据键名 |
|
||||
| list | 列出存储的所有键 | 无 |
|
||||
| delete | 删除存储的键 | key: 要删除的数据键名 |
|
||||
| clear | 清空所有存储的数据 | 无 |
|
||||
|
||||
### 状态管理
|
||||
|
||||
| 操作 | 描述 | 参数 |
|
||||
|-----|------|------|
|
||||
| counter | 获取或设置计数器 | action: 操作 (get, set, increment, decrement, reset)<br>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. 使用事件系统通知状态变化
|
||||
39
plugins/demoutils/build.sh
Executable file
39
plugins/demoutils/build.sh
Executable file
@@ -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 插件构建完成!"
|
||||
1012
plugins/demoutils/demo_plugin.go
Normal file
1012
plugins/demoutils/demo_plugin.go
Normal file
File diff suppressed because it is too large
Load Diff
316
plugins/logger/logger_plugin.go
Normal file
316
plugins/logger/logger_plugin.go
Normal file
@@ -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() {
|
||||
// 不会被执行,仅用于编译插件
|
||||
}
|
||||
363
plugins/stats/stats_plugin.go
Normal file
363
plugins/stats/stats_plugin.go
Normal file
@@ -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() {
|
||||
// 不会被执行,仅用于编译插件
|
||||
}
|
||||
344
plugins/storage/storage_plugin.go
Normal file
344
plugins/storage/storage_plugin.go
Normal file
@@ -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() {
|
||||
// 不会被执行,仅用于编译插件
|
||||
}
|
||||
Reference in New Issue
Block a user