update: 2023-03-15
1. 添加URL重写器 2. 添加插件系统
This commit is contained in:
460
examples/plugin/admin/web_admin.go
Normal file
460
examples/plugin/admin/web_admin.go
Normal file
@@ -0,0 +1,460 @@
|
||||
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))
|
||||
}
|
Reference in New Issue
Block a user