461 lines
13 KiB
Go
461 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/darkit/goproxy/examples/plugin"
|
|
)
|
|
|
|
// 全局插件管理器
|
|
var pm *plugin.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; }
|
|
.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;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<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}}
|
|
<a href="/disable/{{.Name}}"><button class="btn-disable">禁用</button></a>
|
|
{{else}}
|
|
<a href="/enable/{{.Name}}"><button class="btn-enable">启用</button></a>
|
|
{{end}}
|
|
<a href="/config/{{.Name}}"><button class="btn-config">配置</button></a>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if not .Plugins}}
|
|
<p>没有已安装的插件。</p>
|
|
{{end}}
|
|
|
|
<h2>操作</h2>
|
|
<form action="/reload" method="post">
|
|
<button type="submit">重新加载插件</button>
|
|
</form>
|
|
</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 []plugin.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/"):]
|
|
|
|
if err := pm.EnablePlugin(pluginName); err != nil {
|
|
http.Error(w, fmt.Sprintf("启用插件失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// 尝试初始化和启动插件
|
|
ctx := context.Background()
|
|
plugin, exists := pm.GetPlugin(pluginName)
|
|
if exists && plugin.IsEnabled() {
|
|
config, _ := pm.GetPluginConfig(pluginName)
|
|
if err := plugin.Init(ctx, config); err != nil {
|
|
http.Error(w, fmt.Sprintf("初始化插件失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if err := plugin.Start(ctx); err != nil {
|
|
http.Error(w, fmt.Sprintf("启动插件失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
|
|
// DisablePluginHandler 禁用插件
|
|
func DisablePluginHandler(w http.ResponseWriter, r *http.Request) {
|
|
pluginName := r.URL.Path[len("/disable/"):]
|
|
|
|
// 先停止插件
|
|
ctx := context.Background()
|
|
plugin, exists := pm.GetPlugin(pluginName)
|
|
if exists && plugin.IsEnabled() {
|
|
if err := plugin.Stop(ctx); err != nil {
|
|
http.Error(w, fmt.Sprintf("停止插件失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// 禁用插件
|
|
if err := pm.DisablePlugin(pluginName); err != nil {
|
|
http.Error(w, fmt.Sprintf("禁用插件失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
|
|
// ConfigPluginHandler 配置插件
|
|
func ConfigPluginHandler(w http.ResponseWriter, r *http.Request) {
|
|
pluginName := r.URL.Path[len("/config/"):]
|
|
|
|
if r.Method == http.MethodPost {
|
|
// 处理配置表单提交
|
|
if err := r.ParseForm(); err != nil {
|
|
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]
|
|
|
|
// 布尔值处理
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
// 保存配置
|
|
if err := pm.SetPluginConfig(pluginName, config); err != nil {
|
|
http.Error(w, fmt.Sprintf("保存配置失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// 重定向回主页
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// 获取插件信息和配置
|
|
info, config := getPluginInfo(pluginName)
|
|
if info == nil {
|
|
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 *plugin.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
|
|
}
|
|
|
|
// 停止所有插件
|
|
ctx := context.Background()
|
|
if err := pm.StopPlugins(ctx); err != nil {
|
|
http.Error(w, fmt.Sprintf("停止插件失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// 重新加载插件
|
|
if err := pm.LoadPlugins(); err != nil {
|
|
http.Error(w, fmt.Sprintf("加载插件失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// 初始化和启动插件
|
|
if err := pm.InitPlugins(ctx); err != nil {
|
|
http.Error(w, fmt.Sprintf("初始化插件失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if err := pm.StartPlugins(ctx); err != nil {
|
|
http.Error(w, fmt.Sprintf("启动插件失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// getPluginInfo 获取插件信息和配置
|
|
func getPluginInfo(name string) (*plugin.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
|
|
}
|
|
|
|
// 插件目录
|
|
var pluginsDir = "./plugins"
|
|
|
|
func main3() {
|
|
// 创建插件管理器
|
|
pm = plugin.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)
|
|
}
|
|
|
|
// 注册处理器
|
|
http.HandleFunc("/", AdminHandler)
|
|
http.HandleFunc("/enable/", EnablePluginHandler)
|
|
http.HandleFunc("/disable/", DisablePluginHandler)
|
|
http.HandleFunc("/config/", ConfigPluginHandler)
|
|
http.HandleFunc("/reload", ReloadPluginsHandler)
|
|
http.HandleFunc("/api/plugins", APIPluginsHandler)
|
|
|
|
// 启动Web服务器
|
|
log.Println("管理界面启动在 :8080...")
|
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
|
}
|