Files
plugins/example/web_admin.go

1491 lines
44 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"context"
"encoding/json"
"fmt"
"html/template"
"log"
"net/http"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/darkit/plugins"
)
// 插件目录
var pluginsDir = "./dist"
// 全局插件管理器
var pm *plugins.PluginManager
// AdminHandler 处理管理界面主页
func AdminHandler(w http.ResponseWriter, r *http.Request) {
// HTML模板
tmpl := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>插件管理系统</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
.plugin-card {
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
background-color: #f9f9f9;
}
.plugin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.plugin-title { margin: 0; color: #333; }
.plugin-version { color: #888; font-size: 0.9em; }
.plugin-author { color: #888; font-size: 0.9em; }
.plugin-desc { margin: 10px 0; }
.plugin-status {
display: inline-block;
padding: 5px 10px;
border-radius: 3px;
font-size: 0.8em;
}
.status-enabled { background-color: #dff0d8; color: #3c763d; }
.status-disabled { background-color: #f2dede; color: #a94442; }
.plugin-actions { margin-top: 15px; }
button {
padding: 5px 10px;
margin-right: 5px;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 0.9em;
}
.btn-enable { background-color: #5cb85c; color: white; }
.btn-disable { background-color: #d9534f; color: white; }
.btn-config { background-color: #5bc0de; color: white; }
.btn-operations { background-color: #f0ad4e; color: white; }
.config-section {
margin-top: 10px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 3px;
}
.config-item {
margin: 5px 0;
display: flex;
}
.config-key {
font-weight: bold;
width: 150px;
}
.system-info {
background-color: #e9ecef;
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
}
#loading-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px;
border-radius: 5px;
color: white;
max-width: 300px;
z-index: 1001;
display: none;
}
.notification-success { background-color: #5cb85c; }
.notification-error { background-color: #d9534f; }
.notification-info { background-color: #5bc0de; }
</style>
</head>
<body>
<div id="loading-overlay">
<div class="spinner"></div>
</div>
<div id="notification" class="notification"></div>
<h1>插件管理系统</h1>
<div class="system-info">
<h3>系统信息</h3>
<p>操作系统: {{.OS}}</p>
<p>动态加载支持: {{if .DynamicLoadingSupported}}{{else}}{{end}}</p>
<p>插件目录: {{.PluginsDir}}</p>
<p>已加载插件数量: {{len .Plugins}}</p>
</div>
<h2>已安装插件</h2>
{{range .Plugins}}
<div class="plugin-card">
<div class="plugin-header">
<div>
<h3 class="plugin-title">{{.Name}}</h3>
<span class="plugin-version">版本: {{.Version}}</span>
<span class="plugin-author">作者: {{.Author}}</span>
</div>
<div>
{{if .Enabled}}
<span class="plugin-status status-enabled">已启用</span>
{{else}}
<span class="plugin-status status-disabled">已禁用</span>
{{end}}
</div>
</div>
<div class="plugin-desc">{{.Description}}</div>
{{if .Config}}
<div class="config-section">
<h4>配置:</h4>
{{range $key, $value := .Config}}
<div class="config-item">
<div class="config-key">{{$key}}:</div>
<div class="config-value">{{$value}}</div>
</div>
{{end}}
</div>
{{end}}
<div class="plugin-actions">
{{if .Enabled}}
<button class="btn-disable" onclick="disablePlugin('{{.Name}}')">禁用</button>
{{else}}
<button class="btn-enable" onclick="enablePlugin('{{.Name}}')">启用</button>
{{end}}
<button class="btn-config" onclick="window.location.href='/config/{{.Name}}'">配置</button>
<button class="btn-operations" onclick="window.location.href='/operations/{{.Name}}'">操作</button>
</div>
</div>
{{end}}
{{if not .Plugins}}
<p>没有已安装的插件。</p>
{{end}}
<h2>操作</h2>
<button onclick="reloadPlugins()">重新加载插件</button>
<script>
// 显示加载中遮罩
function showLoading() {
document.getElementById('loading-overlay').style.display = 'flex';
}
// 隐藏加载中遮罩
function hideLoading() {
document.getElementById('loading-overlay').style.display = 'none';
}
// 显示通知
function showNotification(message, type) {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = 'notification notification-' + type;
notification.style.display = 'block';
// 3秒后自动隐藏
setTimeout(function() {
notification.style.display = 'none';
}, 3000);
}
// 禁用插件
function disablePlugin(pluginName) {
showLoading();
fetch('/disable/' + pluginName)
.then(response => {
hideLoading();
if (response.ok) {
showNotification('插件已禁用,页面将刷新', 'success');
setTimeout(() => window.location.reload(), 1000);
} else {
response.text().then(text => {
showNotification('禁用插件失败: ' + text, 'error');
});
}
})
.catch(error => {
hideLoading();
showNotification('操作失败: ' + error, 'error');
});
}
// 启用插件
function enablePlugin(pluginName) {
showLoading();
fetch('/enable/' + pluginName)
.then(response => {
hideLoading();
if (response.ok) {
showNotification('插件已启用,页面将刷新', 'success');
setTimeout(() => window.location.reload(), 1000);
} else {
response.text().then(text => {
showNotification('启用插件失败: ' + text, 'error');
});
}
})
.catch(error => {
hideLoading();
showNotification('操作失败: ' + error, 'error');
});
}
// 重新加载插件
function reloadPlugins() {
showLoading();
fetch('/reload', {
method: 'POST'
})
.then(response => {
hideLoading();
if (response.ok) {
showNotification('插件已重新加载,页面将刷新', 'success');
setTimeout(() => window.location.reload(), 1000);
} else {
response.text().then(text => {
showNotification('重新加载插件失败: ' + text, 'error');
});
}
})
.catch(error => {
hideLoading();
showNotification('操作失败: ' + error, 'error');
});
}
</script>
</body>
</html>
`
// 解析模板
t, err := template.New("admin").Parse(tmpl)
if err != nil {
http.Error(w, fmt.Sprintf("模板解析错误: %v", err), http.StatusInternalServerError)
return
}
// 准备数据
data := struct {
Plugins []plugins.PluginInfo
OS string
DynamicLoadingSupported bool
PluginsDir string
}{
Plugins: pm.GetPluginInfos(),
OS: strings.Title(strings.ToLower(fmt.Sprintf("%s", strings.Split(runtime.GOOS, "/")[0]))),
DynamicLoadingSupported: pm.IsDynamicLoadingSupported(),
PluginsDir: pluginsDir,
}
// 渲染模板
if err := t.Execute(w, data); err != nil {
http.Error(w, fmt.Sprintf("模板渲染错误: %v", err), http.StatusInternalServerError)
}
}
// EnablePluginHandler 启用插件
func EnablePluginHandler(w http.ResponseWriter, r *http.Request) {
pluginName := r.URL.Path[len("/enable/"):]
log.Printf("尝试启用插件: %s", pluginName)
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 使用goroutine和通道处理可能的阻塞操作
enableErrChan := make(chan error, 1)
go func() {
enableErrChan <- pm.EnablePlugin(pluginName)
}()
// 等待操作完成或超时
select {
case err := <-enableErrChan:
if err != nil {
log.Printf("启用插件失败: %v", err)
http.Error(w, fmt.Sprintf("启用插件失败: %v", err), http.StatusInternalServerError)
return
}
case <-time.After(5 * time.Second):
log.Printf("启用插件操作超时")
http.Error(w, "启用插件操作超时", http.StatusRequestTimeout)
return
}
// 尝试初始化和启动插件
plugin, exists := pm.GetPlugin(pluginName)
if exists && plugin.IsEnabled() {
config, _ := pm.GetPluginConfig(pluginName)
log.Printf("初始化插件: %s", pluginName)
initErrChan := make(chan error, 1)
go func() {
initErrChan <- plugin.Init(ctx, config)
}()
// 等待初始化完成或超时
select {
case err := <-initErrChan:
if err != nil {
log.Printf("初始化插件失败: %v", err)
http.Error(w, fmt.Sprintf("初始化插件失败: %v", err), http.StatusInternalServerError)
return
}
case <-time.After(5 * time.Second):
log.Printf("初始化插件超时")
http.Error(w, "初始化插件超时", http.StatusRequestTimeout)
return
}
log.Printf("启动插件: %s", pluginName)
startErrChan := make(chan error, 1)
go func() {
startErrChan <- plugin.Start(ctx)
}()
// 等待启动完成或超时
select {
case err := <-startErrChan:
if err != nil {
log.Printf("启动插件失败: %v", err)
http.Error(w, fmt.Sprintf("启动插件失败: %v", err), http.StatusInternalServerError)
return
}
case <-time.After(5 * time.Second):
log.Printf("启动插件超时")
http.Error(w, "启动插件超时", http.StatusRequestTimeout)
return
}
}
log.Printf("插件 %s 启用成功", pluginName)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
// DisablePluginHandler 禁用插件
func DisablePluginHandler(w http.ResponseWriter, r *http.Request) {
pluginName := r.URL.Path[len("/disable/"):]
log.Printf("尝试禁用插件: %s", pluginName)
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 先检查插件是否存在和启用状态
plugin, exists := pm.GetPlugin(pluginName)
if !exists {
log.Printf("插件不存在: %s", pluginName)
http.Error(w, fmt.Sprintf("插件 %s 不存在", pluginName), http.StatusNotFound)
return
}
// 如果插件已经是禁用状态,直接返回成功
if !plugin.IsEnabled() {
log.Printf("插件 %s 已经是禁用状态", pluginName)
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
log.Printf("开始停止插件: %s", pluginName)
// 使用goroutine和通道处理可能的阻塞操作
stopErrChan := make(chan error, 1)
go func() {
if err := plugin.Stop(ctx); err != nil {
log.Printf("停止插件时发生错误: %v", err)
stopErrChan <- err
} else {
log.Printf("插件停止成功")
stopErrChan <- nil
}
}()
// 等待停止操作完成或超时
select {
case err := <-stopErrChan:
if err != nil {
log.Printf("停止插件失败: %v", err)
http.Error(w, fmt.Sprintf("停止插件失败: %v", err), http.StatusInternalServerError)
return
}
case <-time.After(5 * time.Second):
log.Printf("停止插件超时,继续执行禁用操作")
// 即使停止超时,我们也继续执行禁用操作
}
// 禁用插件 - 使用goroutine和通道处理可能的阻塞
log.Printf("开始禁用插件: %s", pluginName)
disableErrChan := make(chan error, 1)
disableDone := make(chan struct{})
go func() {
defer close(disableDone)
if err := pm.DisablePlugin(pluginName); err != nil {
log.Printf("禁用插件时发生错误: %v", err)
disableErrChan <- err
} else {
log.Printf("插件禁用成功")
disableErrChan <- nil
}
}()
// 等待禁用操作完成或超时
select {
case err := <-disableErrChan:
if err != nil {
log.Printf("禁用插件失败: %v", err)
http.Error(w, fmt.Sprintf("禁用插件失败: %v", err), http.StatusInternalServerError)
return
}
log.Printf("插件 %s 已成功禁用", pluginName)
case <-time.After(5 * time.Second):
log.Printf("禁用插件操作超时,尝试强制设置禁用状态")
// 超时后尝试直接设置禁用状态
plugin.SetEnabled(false)
// 保存配置
config, _ := pm.GetPluginConfig(pluginName)
if config == nil {
config = make(map[string]interface{})
}
config["enabled"] = false
if err := pm.SetPluginConfig(pluginName, config); err != nil {
log.Printf("保存禁用状态失败: %v", err)
http.Error(w, "禁用插件超时且无法保存状态", http.StatusRequestTimeout)
return
}
log.Printf("插件 %s 已强制设置为禁用状态", pluginName)
}
// 等待禁用操作的goroutine完成
go func() {
<-disableDone
log.Printf("禁用操作的goroutine已完成")
}()
// 返回成功响应
log.Printf("禁用操作处理完成,重定向到主页")
http.Redirect(w, r, "/", http.StatusSeeOther)
}
// ConfigPluginHandler 配置插件
func ConfigPluginHandler(w http.ResponseWriter, r *http.Request) {
pluginName := r.URL.Path[len("/config/"):]
log.Printf("访问插件配置页面: %s, 方法: %s", pluginName, r.Method)
if r.Method == http.MethodPost {
// 处理配置表单提交
if err := r.ParseForm(); err != nil {
log.Printf("解析表单失败: %v", err)
http.Error(w, fmt.Sprintf("解析表单失败: %v", err), http.StatusBadRequest)
return
}
// 收集配置项
config := make(map[string]interface{})
for key, values := range r.PostForm {
if key == "pluginName" {
continue // 跳过插件名称字段
}
if len(values) > 0 {
// 尝试将值转换为适当的类型
value := values[0]
log.Printf("配置项: %s = %s", key, value)
// 布尔值处理
if value == "true" {
config[key] = true
} else if value == "false" {
config[key] = false
} else if strings.Contains(value, ".") {
// 尝试解析为浮点数
if f, err := strconv.ParseFloat(value, 64); err == nil {
config[key] = f
} else {
config[key] = value
}
} else if i, err := strconv.Atoi(value); err == nil {
// 尝试解析为整数
config[key] = i
} else {
// 默认为字符串
config[key] = value
}
}
}
// 保存配置
log.Printf("保存插件配置: %s", pluginName)
if err := pm.SetPluginConfig(pluginName, config); err != nil {
log.Printf("保存配置失败: %v", err)
http.Error(w, fmt.Sprintf("保存配置失败: %v", err), http.StatusInternalServerError)
return
}
log.Printf("配置保存成功,重定向到主页")
// 重定向回主页
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
// 获取插件信息和配置
log.Printf("获取插件信息: %s", pluginName)
info, config := getPluginInfo(pluginName)
if info == nil {
log.Printf("插件不存在: %s", pluginName)
http.Error(w, "插件不存在", http.StatusNotFound)
return
}
// 配置表单模板
tmpl := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>配置 {{.Info.Name}}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"], input[type="number"] { width: 100%; padding: 8px; box-sizing: border-box; }
input[type="checkbox"] { margin-right: 5px; }
button { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; cursor: pointer; }
button:hover { background-color: #45a049; }
.back-link { margin-top: 20px; display: block; }
</style>
</head>
<body>
<h1>配置 {{.Info.Name}}</h1>
<form action="/config/{{.Info.Name}}" method="post">
<input type="hidden" name="pluginName" value="{{.Info.Name}}">
{{range $key, $value := .Config}}
<div class="form-group">
<label for="{{$key}}">{{$key}}:</label>
{{if eq (printf "%T" $value) "bool"}}
<input type="checkbox" id="{{$key}}" name="{{$key}}" {{if $value}}checked{{end}}>
{{else if eq (printf "%T" $value) "float64"}}
<input type="number" id="{{$key}}" name="{{$key}}" value="{{$value}}" step="0.01">
{{else if eq (printf "%T" $value) "int"}}
<input type="number" id="{{$key}}" name="{{$key}}" value="{{$value}}">
{{else}}
<input type="text" id="{{$key}}" name="{{$key}}" value="{{$value}}">
{{end}}
</div>
{{end}}
<button type="submit">保存配置</button>
</form>
<a href="/" class="back-link">返回插件列表</a>
</body>
</html>
`
// 解析模板
t, err := template.New("config").Parse(tmpl)
if err != nil {
http.Error(w, fmt.Sprintf("模板解析错误: %v", err), http.StatusInternalServerError)
return
}
// 准备数据
data := struct {
Info *plugins.PluginInfo
Config map[string]interface{}
}{
Info: info,
Config: config,
}
// 渲染模板
if err := t.Execute(w, data); err != nil {
http.Error(w, fmt.Sprintf("模板渲染错误: %v", err), http.StatusInternalServerError)
}
}
// ReloadPluginsHandler 重新加载插件
func ReloadPluginsHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
log.Printf("开始重新加载所有插件")
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 停止所有插件
log.Printf("停止所有插件")
// 使用goroutine和通道处理可能的阻塞操作
errChan := make(chan error, 1)
go func() {
errChan <- pm.StopPlugins(ctx)
}()
// 等待操作完成或超时
select {
case err := <-errChan:
if err != nil {
log.Printf("停止插件失败: %v", err)
http.Error(w, fmt.Sprintf("停止插件失败: %v", err), http.StatusInternalServerError)
return
}
case <-time.After(10 * time.Second):
log.Printf("停止插件超时")
http.Error(w, "停止插件超时", http.StatusRequestTimeout)
return
}
// 重新加载插件
log.Printf("加载插件")
if err := pm.LoadPlugins(); err != nil {
log.Printf("加载插件失败: %v", err)
http.Error(w, fmt.Sprintf("加载插件失败: %v", err), http.StatusInternalServerError)
return
}
// 初始化插件
log.Printf("初始化插件")
initErrChan := make(chan error, 1)
go func() {
initErrChan <- pm.InitPlugins(ctx)
}()
// 等待初始化完成或超时
select {
case err := <-initErrChan:
if err != nil {
log.Printf("初始化插件失败: %v", err)
http.Error(w, fmt.Sprintf("初始化插件失败: %v", err), http.StatusInternalServerError)
return
}
case <-time.After(10 * time.Second):
log.Printf("初始化插件超时")
http.Error(w, "初始化插件超时", http.StatusRequestTimeout)
return
}
// 启动插件
log.Printf("启动插件")
startErrChan := make(chan error, 1)
go func() {
startErrChan <- pm.StartPlugins(ctx)
}()
// 等待启动完成或超时
select {
case err := <-startErrChan:
if err != nil {
log.Printf("启动插件失败: %v", err)
http.Error(w, fmt.Sprintf("启动插件失败: %v", err), http.StatusInternalServerError)
return
}
case <-time.After(10 * time.Second):
log.Printf("启动插件超时")
http.Error(w, "启动插件超时", http.StatusRequestTimeout)
return
}
log.Printf("所有插件已重新加载")
http.Redirect(w, r, "/", http.StatusSeeOther)
}
// APIPluginsHandler API接口处理器 - 返回插件列表
func APIPluginsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
plugins := pm.GetPluginInfos()
if err := json.NewEncoder(w).Encode(plugins); err != nil {
http.Error(w, fmt.Sprintf("编码JSON失败: %v", err), http.StatusInternalServerError)
}
}
// OperationsHandler 处理插件操作信息页面
func OperationsHandler(w http.ResponseWriter, r *http.Request) {
pluginName := r.URL.Path[len("/operations/"):]
log.Printf("访问插件操作页面: %s", pluginName)
// 获取插件的所有操作信息
pluginOps, err := pm.GetPluginAllOperations(pluginName)
if err != nil {
log.Printf("获取插件操作失败: %v", err)
http.Error(w, fmt.Sprintf("获取插件操作失败: %v", err), http.StatusInternalServerError)
return
}
log.Printf("获取到 %d 个操作信息", len(pluginOps.Operations))
// 操作信息页面模板
tmpl := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{.PluginOps.PluginName}} 插件操作</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1, h2, h3 { color: #333; }
.operation-card {
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 15px;
border-radius: 5px;
background-color: #f9f9f9;
}
.operation-name {
font-weight: bold;
color: #333;
margin-bottom: 10px;
font-size: 1.2em;
}
.operation-desc {
margin-bottom: 10px;
color: #666;
}
.params-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
}
.params-table th, .params-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.params-table th {
background-color: #f2f2f2;
}
.execute-btn {
background-color: #5bc0de;
color: white;
padding: 8px 12px;
border: none;
border-radius: 3px;
cursor: pointer;
}
.back-link {
margin-top: 20px;
display: block;
}
</style>
</head>
<body>
<h1>{{.PluginOps.PluginName}} 插件操作</h1>
{{if .PluginOps.Operations}}
{{range .PluginOps.Operations}}
<div class="operation-card">
<div class="operation-name">{{.Name}}</div>
{{if .Description}}
<div class="operation-desc">{{.Description}}</div>
{{end}}
{{if .Params}}
<h3>参数列表:</h3>
<table class="params-table">
<tr>
<th>参数名</th>
<th>类型</th>
<th>必填</th>
<th>默认值</th>
<th>描述</th>
</tr>
{{range .Params}}
<tr>
<td>{{.Name}}</td>
<td>{{.Type}}</td>
<td>{{if .Required}}{{else}}{{end}}</td>
<td>{{if .Default}}{{.Default}}{{else}}-{{end}}</td>
<td>{{.Description}}</td>
</tr>
{{end}}
</table>
{{else}}
<p>此操作不需要参数</p>
{{end}}
<a href="/execute/{{$.PluginOps.PluginName}}/{{.Name}}">
<button class="execute-btn">执行此操作</button>
</a>
</div>
{{end}}
{{else}}
<p>该插件没有定义任何操作。</p>
{{end}}
<a href="/" class="back-link">返回插件列表</a>
</body>
</html>
`
// 解析模板
t, err := template.New("operations").Parse(tmpl)
if err != nil {
http.Error(w, fmt.Sprintf("模板解析错误: %v", err), http.StatusInternalServerError)
return
}
// 准备数据
data := struct {
PluginOps *plugins.PluginOperations
}{
PluginOps: pluginOps,
}
// 渲染模板
if err := t.Execute(w, data); err != nil {
http.Error(w, fmt.Sprintf("模板渲染错误: %v", err), http.StatusInternalServerError)
}
}
// ExecuteOperationHandler 处理操作执行页面
func ExecuteOperationHandler(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.URL.Path[len("/execute/"):], "/")
if len(parts) != 2 {
log.Printf("无效的URL格式: %s", r.URL.Path)
http.Error(w, "无效的URL格式", http.StatusBadRequest)
return
}
pluginName := parts[0]
operationName := parts[1]
log.Printf("执行操作: %s/%s, 方法: %s", pluginName, operationName, r.Method)
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 获取操作信息
opInfo, err := pm.GetPluginOperationInfo(pluginName, operationName)
if err != nil {
log.Printf("获取操作信息失败: %v", err)
http.Error(w, fmt.Sprintf("获取操作信息失败: %v", err), http.StatusInternalServerError)
return
}
// 处理表单提交
if r.Method == http.MethodPost {
log.Printf("处理操作执行表单")
if err := r.ParseForm(); err != nil {
log.Printf("解析表单失败: %v", err)
http.Error(w, fmt.Sprintf("解析表单失败: %v", err), http.StatusBadRequest)
return
}
// 构建参数
params := make(map[string]interface{})
for _, param := range opInfo.Params {
value := r.FormValue(param.Name)
log.Printf("参数: %s = %s (类型: %s)", param.Name, value, param.Type)
if value == "" && param.Required {
errMsg := fmt.Sprintf("缺少必填参数: %s", param.Name)
log.Panicln(errMsg)
http.Error(w, errMsg, http.StatusBadRequest)
return
}
// 根据参数类型转换值
switch param.Type {
case "integer", "int", "int64":
if value == "" {
params[param.Name] = 0
} else {
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
http.Error(w, fmt.Sprintf("参数 %s 格式错误: %v", param.Name, err), http.StatusBadRequest)
return
}
params[param.Name] = i
}
case "float", "float64":
if value == "" {
params[param.Name] = 0.0
} else {
f, err := strconv.ParseFloat(value, 64)
if err != nil {
http.Error(w, fmt.Sprintf("参数 %s 格式错误: %v", param.Name, err), http.StatusBadRequest)
return
}
params[param.Name] = f
}
case "boolean", "bool":
params[param.Name] = (value == "true" || value == "on")
default:
params[param.Name] = value
}
}
// 使用goroutine和channel执行操作避免阻塞
log.Printf("执行操作: %s", operationName)
resultChan := make(chan interface{}, 1)
errChan := make(chan error, 1)
go func() {
result, execErr := pm.ExecutePlugin(ctx, pluginName, operationName, params)
resultChan <- result
errChan <- execErr
}()
// 等待结果或超时
var result interface{}
var execErr error
select {
case result = <-resultChan:
execErr = <-errChan
log.Printf("操作执行完成: %v, 错误: %v", result, execErr)
case <-time.After(10 * time.Second):
log.Printf("操作执行超时")
http.Error(w, "操作执行超时", http.StatusRequestTimeout)
return
}
// 构建包含参数和结果的数据
data := struct {
PluginName string
OperationName string
Operation *plugins.OperationInfo
Params map[string]interface{}
Result interface{}
Error error
IsPost bool
}{
PluginName: pluginName,
OperationName: operationName,
Operation: opInfo,
Params: params,
Result: result,
Error: execErr,
IsPost: true,
}
// 使用相同的模板渲染结果
executeTemplate(w, data)
return
}
// 显示表单
log.Printf("显示操作执行表单")
data := struct {
PluginName string
OperationName string
Operation *plugins.OperationInfo
Params map[string]interface{}
Result interface{}
Error error
IsPost bool
}{
PluginName: pluginName,
OperationName: operationName,
Operation: opInfo,
Params: make(map[string]interface{}),
Result: nil,
Error: nil,
IsPost: false,
}
executeTemplate(w, data)
}
// executeTemplate 渲染操作执行页面模板
func executeTemplate(w http.ResponseWriter, data interface{}) {
tmpl := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>执行操作 - {{.PluginName}}/{{.OperationName}}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1, h2 { color: #333; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"], input[type="number"], textarea {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ddd;
border-radius: 3px;
}
input[type="checkbox"] { margin-right: 5px; }
button {
padding: 10px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
button:hover { background-color: #45a049; }
.back-link { margin-top: 20px; display: block; }
.result-section {
margin-top: 30px;
padding: 15px;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
}
.result-success {
color: #3c763d;
background-color: #dff0d8;
border-color: #d6e9c6;
}
.result-error {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
pre {
background-color: #f5f5f5;
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
overflow-x: auto;
}
</style>
</head>
<body>
<h1>执行操作: {{.OperationName}}</h1>
<p><strong>插件:</strong> {{.PluginName}}</p>
{{if .Operation.Description}}<p>{{.Operation.Description}}</p>{{end}}
{{if .IsPost}}
<div class="result-section {{if .Error}}result-error{{else}}result-success{{end}}">
<h2>执行结果</h2>
{{if .Error}}
<p><strong>错误:</strong> {{.Error}}</p>
{{else}}
<p><strong>操作成功!</strong></p>
<h3>返回结果:</h3>
<pre>{{printf "%+v" .Result}}</pre>
{{end}}
<h3>使用的参数:</h3>
<pre>{{printf "%+v" .Params}}</pre>
</div>
<a href="/execute/{{.PluginName}}/{{.OperationName}}">
<button>重新执行</button>
</a>
{{else}}
<form action="/execute/{{.PluginName}}/{{.OperationName}}" method="post">
<h2>参数</h2>
{{if .Operation.Params}}
{{range .Operation.Params}}
<div class="form-group">
<label for="{{.Name}}">
{{.Name}}{{if .Required}} *{{end}}:
{{if .Description}}<span style="font-weight:normal">({{.Description}})</span>{{end}}
</label>
{{if eq .Type "boolean" "bool"}}
<input type="checkbox" id="{{.Name}}" name="{{.Name}}"
{{if .Default}}checked{{end}}>
{{else if eq .Type "integer" "int" "int64"}}
<input type="number" id="{{.Name}}" name="{{.Name}}"
value="{{if .Default}}{{.Default}}{{end}}">
{{else if eq .Type "float" "float64"}}
<input type="number" id="{{.Name}}" name="{{.Name}}" step="0.01"
value="{{if .Default}}{{.Default}}{{end}}">
{{else if eq .Type "object" "map"}}
<textarea id="{{.Name}}" name="{{.Name}}" rows="5"
placeholder="{&quot;key&quot;: &quot;value&quot;}">{{if .Default}}{{.Default}}{{end}}</textarea>
{{else}}
<input type="text" id="{{.Name}}" name="{{.Name}}"
value="{{if .Default}}{{.Default}}{{end}}">
{{end}}
</div>
{{end}}
{{else}}
<p>此操作不需要参数</p>
{{end}}
<button type="submit">执行</button>
</form>
{{end}}
<a href="/operations/{{.PluginName}}" class="back-link">返回操作列表</a>
</body>
</html>
`
// 解析模板
t, err := template.New("execute").Parse(tmpl)
if err != nil {
http.Error(w, fmt.Sprintf("模板解析错误: %v", err), http.StatusInternalServerError)
return
}
// 渲染模板
if err := t.Execute(w, data); err != nil {
http.Error(w, fmt.Sprintf("模板渲染错误: %v", err), http.StatusInternalServerError)
}
}
// APIOperationsHandler API接口处理器 - 返回插件操作信息
func APIOperationsHandler(w http.ResponseWriter, r *http.Request) {
pluginName := r.URL.Path[len("/api/operations/"):]
if pluginName == "" {
// 返回所有插件的操作信息
allOps := pm.GetAllPluginsOperations()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(allOps); err != nil {
http.Error(w, fmt.Sprintf("编码JSON失败: %v", err), http.StatusInternalServerError)
}
return
}
// 返回特定插件的操作信息
ops, err := pm.GetPluginAllOperations(pluginName)
if err != nil {
http.Error(w, fmt.Sprintf("获取操作信息失败: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(ops); err != nil {
http.Error(w, fmt.Sprintf("编码JSON失败: %v", err), http.StatusInternalServerError)
}
}
// APIExecuteHandler API接口处理器 - 执行插件操作
func APIExecuteHandler(w http.ResponseWriter, r *http.Request) {
// 只接受POST请求
if r.Method != http.MethodPost {
http.Error(w, "只接受POST请求", http.StatusMethodNotAllowed)
return
}
log.Printf("收到API执行请求")
// 解析JSON请求
var request struct {
Plugin string `json:"plugin"`
Operation string `json:"operation"`
Parameters map[string]interface{} `json:"parameters"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
log.Printf("解析请求失败: %v", err)
http.Error(w, fmt.Sprintf("解析请求失败: %v", err), http.StatusBadRequest)
return
}
log.Printf("执行API请求: 插件=%s, 操作=%s", request.Plugin, request.Operation)
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 使用goroutine和通道执行操作避免阻塞
resultChan := make(chan interface{}, 1)
errChan := make(chan error, 1)
go func() {
result, err := pm.ExecutePlugin(ctx, request.Plugin, request.Operation, request.Parameters)
resultChan <- result
errChan <- err
}()
// 等待结果或超时
var result interface{}
var err error
select {
case result = <-resultChan:
err = <-errChan
log.Printf("API操作执行完成: %v, 错误: %v", result, err)
case <-time.After(10 * time.Second):
log.Printf("API操作执行超时")
http.Error(w, "操作执行超时", http.StatusRequestTimeout)
return
}
// 构建响应
response := struct {
Success bool `json:"success"`
Result interface{} `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}{
Success: err == nil,
Result: result,
}
if err != nil {
response.Error = err.Error()
}
// 返回响应
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("编码JSON失败: %v", err)
http.Error(w, fmt.Sprintf("编码JSON失败: %v", err), http.StatusInternalServerError)
}
}
// getPluginInfo 获取插件信息和配置
func getPluginInfo(name string) (*plugins.PluginInfo, map[string]interface{}) {
plugins := pm.GetPluginInfos()
for _, info := range plugins {
if info.Name == name {
config, _ := pm.GetPluginConfig(name)
return &info, config
}
}
return nil, nil
}
// EventStreamHandler 处理SSE事件流
func EventStreamHandler(w http.ResponseWriter, r *http.Request) {
// 设置SSE相关的响应头
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
// 获取请求参数
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 3 {
http.Error(w, "缺少插件名称", http.StatusBadRequest)
return
}
pluginName := parts[len(parts)-1]
if pluginName == "" {
http.Error(w, "缺少插件名称", http.StatusBadRequest)
return
}
// 检查插件是否存在
plugin, exists := pm.GetPlugin(pluginName)
if !exists {
http.Error(w, fmt.Sprintf("插件 %s 不存在", pluginName), http.StatusNotFound)
return
}
// 创建通知客户端的通道
eventChan := make(chan plugins.PluginEvent)
clientClosed := make(chan bool)
// 创建处理事件的函数
eventHandler := func(event plugins.PluginEvent) error {
// 仅处理指定插件的事件或全局事件
if event.PluginID == "" || event.PluginID == pluginName {
select {
case eventChan <- event:
// 成功发送事件
case <-clientClosed:
// 客户端已关闭
return fmt.Errorf("客户端已关闭连接")
}
}
return nil
}
// 订阅所有类型的事件
eventTypes := []plugins.PluginEventType{
plugins.PluginEventLoaded,
plugins.PluginEventInitialized,
plugins.PluginEventStarted,
plugins.PluginEventStopped,
plugins.PluginEventError,
plugins.PluginEventCustom,
}
// 注册事件处理器
for _, eventType := range eventTypes {
pm.SubscribeEvent(eventType, eventHandler)
plugin.SubscribeEvent(eventType, eventHandler)
}
// 在函数返回时取消订阅事件
defer func() {
for _, eventType := range eventTypes {
pm.UnsubscribeEvent(eventType, eventHandler)
plugin.UnsubscribeEvent(eventType, eventHandler)
}
close(clientClosed)
}()
// 发送一个初始事件,表示连接已建立
fmt.Fprintf(w, "event: connected\ndata: {\"pluginName\":\"%s\",\"time\":\"%s\"}\n\n",
pluginName, time.Now().Format(time.RFC3339))
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
// 监听客户端断开连接
notify := r.Context().Done()
// 事件循环
for {
select {
case <-notify:
// 客户端断开连接
log.Printf("客户端断开了SSE连接")
return
case event := <-eventChan:
// 序列化事件数据
eventData, err := json.Marshal(event)
if err != nil {
log.Printf("序列化事件失败: %v", err)
continue
}
// 发送事件
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, eventData)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
case <-time.After(30 * time.Second):
// 发送保持连接的注释
fmt.Fprintf(w, ": keepalive\n\n")
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
}
}
// TriggerEventHandler 触发自定义事件
func TriggerEventHandler(w http.ResponseWriter, r *http.Request) {
// 解析请求体
var request struct {
Plugin string `json:"plugin"`
Type string `json:"type"`
Data map[string]interface{} `json:"data"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, fmt.Sprintf("解析请求失败: %v", err), http.StatusBadRequest)
return
}
// 获取插件
plugin, exists := pm.GetPlugin(request.Plugin)
if !exists {
http.Error(w, fmt.Sprintf("插件 %s 不存在", request.Plugin), http.StatusNotFound)
return
}
// 创建事件
event := plugins.PluginEvent{
Type: plugins.PluginEventType(request.Type),
PluginID: request.Plugin,
Timestamp: time.Now(),
Data: request.Data,
}
// 触发事件
err := plugin.EmitEvent(event)
if err != nil {
http.Error(w, fmt.Sprintf("触发事件失败: %v", err), http.StatusInternalServerError)
return
}
// 返回成功响应
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"message": "事件已触发",
})
}
func main() {
// 创建插件管理器
pm = plugins.NewPluginManager(pluginsDir)
// 加载插件
if err := pm.LoadPlugins(); err != nil {
log.Fatalf("加载插件失败: %v", err)
}
// 初始化并启动插件
ctx := context.Background()
if err := pm.InitPlugins(ctx); err != nil {
log.Printf("初始化插件失败: %v", err)
}
if err := pm.StartPlugins(ctx); err != nil {
log.Printf("启动插件失败: %v", err)
}
// 添加静态文件服务
fs := http.FileServer(http.Dir(pluginsDir))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// 添加demo_utils_client.html的特殊路由
http.HandleFunc("/demo", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, filepath.Join("demo_utils_client.html"))
})
// 注册处理器
http.HandleFunc("/", AdminHandler)
http.HandleFunc("/enable/", EnablePluginHandler)
http.HandleFunc("/disable/", DisablePluginHandler)
http.HandleFunc("/config/", ConfigPluginHandler)
http.HandleFunc("/reload", ReloadPluginsHandler)
http.HandleFunc("/operations/", OperationsHandler)
http.HandleFunc("/execute/", ExecuteOperationHandler)
http.HandleFunc("/api/plugins", APIPluginsHandler)
http.HandleFunc("/api/operations/", APIOperationsHandler)
http.HandleFunc("/api/execute", APIExecuteHandler)
http.HandleFunc("/events/", EventStreamHandler)
http.HandleFunc("/api/trigger-event", TriggerEventHandler)
// 启动Web服务器
log.Println("管理界面启动在 :8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}