1491 lines
44 KiB
Go
1491 lines
44 KiB
Go
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="{"key": "value"}">{{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))
|
||
}
|