基于Go语言的动态插件系统,支持热插拔和动态加载插件。

This commit is contained in:
2025-03-17 16:34:27 +00:00
commit 017cab889e
16 changed files with 7539 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.idea
.vscode
*.so
*.log
dist
logs
example/storage
web_admin

280
README.md Normal file
View 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
View 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

View 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

File diff suppressed because it is too large Load Diff

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/darkit/plugins
go 1.24.1

361
interface.go Normal file
View 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
}

1867
plugin.go Normal file

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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 插件构建完成!"

File diff suppressed because it is too large Load Diff

View 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() {
// 不会被执行,仅用于编译插件
}

View 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() {
// 不会被执行,仅用于编译插件
}

View 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() {
// 不会被执行,仅用于编译插件
}

8
types.go Normal file
View File

@@ -0,0 +1,8 @@
package plugins
// PluginOperations 插件操作集合
type PluginOperations struct {
PluginName string `json:"pluginName"` // 插件名称
PluginType PluginType `json:"pluginType"` // 插件类型
Operations []*OperationInfo `json:"operations"` // 操作列表
}