核心功能设计已完成,包含frpc热重启冷重启两种设计逻辑
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -9,13 +10,24 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// SetupRouter 配置所有路由
|
||||
func SetupRouter() *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
// 静态文件(前端)
|
||||
r.Static("/static", "./static")
|
||||
|
||||
// API 路由
|
||||
// 健康检查
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(200, "frpc-console 后端已启动 🎉")
|
||||
})
|
||||
|
||||
// 前端入口
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.File("./static/index.html")
|
||||
})
|
||||
|
||||
// API 路由组
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// 登录(无需鉴权)
|
||||
@@ -41,13 +53,11 @@ func SetupRouter() *gin.Engine {
|
||||
auth.POST("/frpc/start", startFrpcHandler)
|
||||
auth.POST("/frpc/stop", stopFrpcHandler)
|
||||
auth.GET("/frpc/status", getFrpcStatusHandler)
|
||||
}
|
||||
}
|
||||
|
||||
// 前端入口
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.File("./static/index.html")
|
||||
})
|
||||
// 🟢 导入 TOML(新增)
|
||||
auth.POST("/import/toml", importTomlHandler)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -69,8 +79,7 @@ func loginHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
|
||||
if err != nil {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"})
|
||||
return
|
||||
}
|
||||
@@ -81,7 +90,11 @@ func loginHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "登录成功", "data": gin.H{"token": token}})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": "登录成功",
|
||||
"data": gin.H{"token": token},
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 全局配置 ==========
|
||||
@@ -101,29 +114,23 @@ func updateConfigHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("收到配置更新: %+v", cfg)
|
||||
// 强制 tcpMux 为 true
|
||||
cfg.TcpMux = true
|
||||
|
||||
if err := UpdateGlobalConfig(&cfg); err != nil {
|
||||
log.Printf("❌ 更新数据库失败: %v", err) // 新增日志
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败"})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("✅ 数据库更新成功")
|
||||
|
||||
// 自动生成配置文件并热加载
|
||||
if err := GenerateFrpcConfig(); err != nil {
|
||||
log.Printf("❌ 生成配置文件失败: %v", err) // 新增日志
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置文件失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("✅ 配置文件生成成功")
|
||||
|
||||
if err := ReloadFrpc(); err != nil {
|
||||
log.Printf("❌ 热加载失败: %v", err) // 新增日志
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("✅ 热加载成功")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "配置更新成功"})
|
||||
}
|
||||
@@ -139,7 +146,11 @@ func getProxiesHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
func getProxyHandler(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||||
return
|
||||
}
|
||||
p, err := GetProxy(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"code": 1, "msg": "隧道不存在"})
|
||||
@@ -154,14 +165,13 @@ func createProxyHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||
return
|
||||
}
|
||||
p.Enabled = true // 默认启用
|
||||
p.Enabled = true
|
||||
|
||||
if err := CreateProxy(&p); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建隧道失败"})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建隧道失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 重新生成配置并热加载
|
||||
if err := generateAndReload(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
||||
return
|
||||
@@ -171,7 +181,12 @@ func createProxyHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
func updateProxyHandler(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var p Proxy
|
||||
if err := c.ShouldBindJSON(&p); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||||
@@ -180,11 +195,10 @@ func updateProxyHandler(c *gin.Context) {
|
||||
p.ID = id
|
||||
|
||||
if err := UpdateProxy(&p); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新隧道失败"})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新隧道失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 重新生成配置并热加载
|
||||
if err := generateAndReload(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
||||
return
|
||||
@@ -194,13 +208,17 @@ func updateProxyHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
func deleteProxyHandler(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := DeleteProxy(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "删除隧道失败"})
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := DeleteProxy(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "删除隧道失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 重新生成配置并热加载
|
||||
if err := generateAndReload(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
||||
return
|
||||
@@ -247,6 +265,80 @@ func getFrpcStatusHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"running": running}})
|
||||
}
|
||||
|
||||
// ========== 🟢 导入 TOML(新增) ==========
|
||||
func importTomlHandler(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请选择文件"})
|
||||
return
|
||||
}
|
||||
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取文件失败"})
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(f); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取文件失败"})
|
||||
return
|
||||
}
|
||||
|
||||
parsed, err := ParseToml(buf.String())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "解析 TOML 失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新全局配置(保持当前 tcpMux 值)
|
||||
cfg := parsed.ToGlobalConfig()
|
||||
currentCfg, _ := GetGlobalConfig()
|
||||
if currentCfg != nil {
|
||||
cfg.TcpMux = currentCfg.TcpMux // 保持当前值不变
|
||||
}
|
||||
cfg.TcpMux = true // 强制开启
|
||||
|
||||
if err := UpdateGlobalConfig(cfg); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 清空并导入隧道
|
||||
if _, err := DB.Exec("DELETE FROM proxies"); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "清空隧道失败"})
|
||||
return
|
||||
}
|
||||
|
||||
proxies := parsed.ToProxies()
|
||||
for _, p := range proxies {
|
||||
if err := CreateProxy(&p); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "导入隧道失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 生成配置并热加载
|
||||
if err := GenerateFrpcConfig(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if err := ReloadFrpc(); err != nil {
|
||||
// 导入成功但热加载失败,不影响整体结果
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": fmt.Sprintf("导入成功!共 %d 条隧道,但热加载失败: %s", len(proxies), err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"msg": fmt.Sprintf("导入成功!共 %d 条隧道", len(proxies)),
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 辅助函数 ==========
|
||||
func generateAndReload() error {
|
||||
if err := GenerateFrpcConfig(); err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
@@ -182,6 +183,7 @@ func StartFrpc() error {
|
||||
}
|
||||
|
||||
// ReloadFrpc 热加载 frpc 配置(仅发信号,不重复写入二进制)
|
||||
// ReloadFrpc 热加载 frpc 配置,失败时自动降级为重启
|
||||
func ReloadFrpc() error {
|
||||
running, err := GetFrpcStatus()
|
||||
if err != nil {
|
||||
@@ -191,7 +193,6 @@ func ReloadFrpc() error {
|
||||
return StartFrpc()
|
||||
}
|
||||
|
||||
// 确保缓存路径存在
|
||||
if cachedFrpcPath == "" {
|
||||
if _, err := getFrpcPath(); err != nil {
|
||||
return fmt.Errorf("获取 frpc 路径失败: %w", err)
|
||||
@@ -199,9 +200,16 @@ func ReloadFrpc() error {
|
||||
}
|
||||
|
||||
cmd := exec.Command(cachedFrpcPath, "reload", "-c", "./frpc.toml")
|
||||
output, err := cmd.CombinedOutput()
|
||||
_, err = cmd.CombinedOutput() // ← 这里改了
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload 失败: %s, %w", string(output), err)
|
||||
log.Printf("⚠️ 热加载失败 (%v),自动降级为重启 frpc", err)
|
||||
if stopErr := StopFrpc(); stopErr != nil {
|
||||
return fmt.Errorf("停止 frpc 失败: %w", stopErr)
|
||||
}
|
||||
if startErr := StartFrpc(); startErr != nil {
|
||||
return fmt.Errorf("启动 frpc 失败: %w", startErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -13,3 +13,53 @@
|
||||
2026-07-23 19:19:21.669 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 19:19:21.680 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 19:19:21.974 [I] [client/service.go:328] [5da2ae4457692b0e] login to server success, get run id [5da2ae4457692b0e]
|
||||
2026-07-23 19:23:33.949 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 19:23:33.958 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 19:23:34.135 [I] [client/service.go:328] [3d8ddea11ad24348] login to server success, get run id [3d8ddea11ad24348]
|
||||
2026-07-23 19:32:51.328 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 19:32:51.339 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 19:32:51.523 [I] [client/service.go:328] [03c218c64c950a94] login to server success, get run id [03c218c64c950a94]
|
||||
2026-07-23 19:51:09.560 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 19:51:09.568 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 19:51:09.748 [I] [client/service.go:328] [ad7003cd99cd5042] login to server success, get run id [ad7003cd99cd5042]
|
||||
2026-07-23 19:51:56.516 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 19:51:56.526 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 19:51:56.683 [I] [client/service.go:328] [74fb0125aa25f80a] login to server success, get run id [74fb0125aa25f80a]
|
||||
2026-07-23 20:13:36.963 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 20:13:36.972 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 20:13:37.156 [I] [client/service.go:328] [eccab666ed041f5d] login to server success, get run id [eccab666ed041f5d]
|
||||
2026-07-23 20:14:01.389 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 20:14:01.398 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 20:14:01.558 [I] [client/service.go:328] [ba1fe4056d350068] login to server success, get run id [ba1fe4056d350068]
|
||||
2026-07-23 20:14:01.558 [I] [proxy/proxy_manager.go:180] [ba1fe4056d350068] proxy added: [1panel-1 Gitea Grafana Halo SMB SSH immich immich-APP napcat napcat-2 navidrome openlist podux sun-panel]
|
||||
2026-07-23 20:14:01.626 [W] [client/control.go:163] [ba1fe4056d350068] [1panel-1] start error: proxy [1panel-1] already exists
|
||||
2026-07-23 20:14:01.626 [W] [client/control.go:163] [ba1fe4056d350068] [SMB] start error: proxy [SMB] already exists
|
||||
2026-07-23 20:14:01.626 [W] [client/control.go:163] [ba1fe4056d350068] [Grafana] start error: proxy [Grafana] already exists
|
||||
2026-07-23 20:14:01.626 [W] [client/control.go:163] [ba1fe4056d350068] [Halo] start error: proxy [Halo] already exists
|
||||
2026-07-23 20:14:01.626 [W] [client/control.go:163] [ba1fe4056d350068] [Gitea] start error: proxy [Gitea] already exists
|
||||
2026-07-23 20:14:01.626 [W] [client/control.go:163] [ba1fe4056d350068] [immich-APP] start error: proxy [immich-APP] already exists
|
||||
2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [immich] start error: proxy [immich] already exists
|
||||
2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [napcat] start error: proxy [napcat] already exists
|
||||
2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [napcat-2] start error: proxy [napcat-2] already exists
|
||||
2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [navidrome] start error: proxy [navidrome] already exists
|
||||
2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [SSH] start error: proxy [SSH] already exists
|
||||
2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [openlist] start error: proxy [openlist] already exists
|
||||
2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [podux] start error: proxy [podux] already exists
|
||||
2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [sun-panel] start error: proxy [sun-panel] already exists
|
||||
2026-07-23 20:14:23.803 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 20:14:23.811 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 20:14:23.983 [I] [client/service.go:328] [83ea7405ffa3e73d] login to server success, get run id [83ea7405ffa3e73d]
|
||||
2026-07-23 20:14:23.983 [I] [proxy/proxy_manager.go:180] [83ea7405ffa3e73d] proxy added: [1panel-1 Gitea Grafana Halo SMB SSH immich-APP napcat napcat-2 navidrome openlist podux sun-panel]
|
||||
2026-07-23 20:14:24.051 [W] [client/control.go:163] [83ea7405ffa3e73d] [Gitea] start error: proxy [Gitea] already exists
|
||||
2026-07-23 20:14:24.051 [W] [client/control.go:163] [83ea7405ffa3e73d] [SMB] start error: proxy [SMB] already exists
|
||||
2026-07-23 20:14:24.051 [W] [client/control.go:163] [83ea7405ffa3e73d] [Grafana] start error: proxy [Grafana] already exists
|
||||
2026-07-23 20:14:24.051 [W] [client/control.go:163] [83ea7405ffa3e73d] [SSH] start error: proxy [SSH] already exists
|
||||
2026-07-23 20:14:24.051 [W] [client/control.go:163] [83ea7405ffa3e73d] [napcat-2] start error: proxy [napcat-2] already exists
|
||||
2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [immich-APP] start error: proxy [immich-APP] already exists
|
||||
2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [napcat] start error: proxy [napcat] already exists
|
||||
2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [navidrome] start error: proxy [navidrome] already exists
|
||||
2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [openlist] start error: proxy [openlist] already exists
|
||||
2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [Halo] start error: proxy [Halo] already exists
|
||||
2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [podux] start error: proxy [podux] already exists
|
||||
2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [sun-panel] start error: proxy [sun-panel] already exists
|
||||
2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [1panel-1] start error: proxy [1panel-1] already exists
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
serverAddr = "frp.whitetop.xyz"
|
||||
serverPort = 9358
|
||||
|
||||
[auth]
|
||||
token = "Lxh10020328"
|
||||
|
||||
[log]
|
||||
to = "./frpc.log"
|
||||
level = "info"
|
||||
maxDays = 3
|
||||
|
||||
[transport]
|
||||
tcpMux = true
|
||||
tcpMuxKeepaliveInterval = 30
|
||||
heartbeatInterval = 15
|
||||
heartbeatTimeout = 70
|
||||
poolCount = 8
|
||||
|
||||
[webServer]
|
||||
addr = "127.0.0.1:7400"
|
||||
|
||||
|
||||
[[proxies]]
|
||||
name = "sun-panel"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 3002
|
||||
remotePort = 30005
|
||||
|
||||
[[proxies]]
|
||||
name = "1panel-1"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 10086
|
||||
remotePort = 20003
|
||||
|
||||
[[proxies]]
|
||||
name = "navidrome"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 4533
|
||||
remotePort = 20022
|
||||
|
||||
[[proxies]]
|
||||
name = "openlist"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 5244
|
||||
remotePort = 20005
|
||||
|
||||
[[proxies]]
|
||||
name = "SSH"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 22
|
||||
remotePort = 20001
|
||||
|
||||
[[proxies]]
|
||||
name = "SMB"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 445
|
||||
remotePort = 20008
|
||||
|
||||
[[proxies]]
|
||||
name = "napcat"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 6099
|
||||
remotePort = 20007
|
||||
|
||||
[[proxies]]
|
||||
name = "Gitea"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 3800
|
||||
remotePort = 20017
|
||||
|
||||
[[proxies]]
|
||||
name = "immich-APP"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 2283
|
||||
remotePort = 20020
|
||||
|
||||
[[proxies]]
|
||||
name = "Halo"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 65535
|
||||
remotePort = 65535
|
||||
|
||||
[[proxies]]
|
||||
name = "napcat-2"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 6100
|
||||
remotePort = 20012
|
||||
|
||||
[[proxies]]
|
||||
name = "podux"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 8090
|
||||
remotePort = 20010
|
||||
|
||||
[[proxies]]
|
||||
name = "Grafana"
|
||||
type = "tcp"
|
||||
localIP = "192.168.3.16"
|
||||
localPort = 3030
|
||||
remotePort = 20014
|
||||
+20
-41
@@ -17,7 +17,7 @@ const app = createApp({
|
||||
const token = ref("");
|
||||
|
||||
// ===== Tab =====
|
||||
const activeTab = ref("config");
|
||||
const activeTab = ref("proxies");
|
||||
|
||||
// ===== 全局配置 =====
|
||||
const globalConfig = reactive({
|
||||
@@ -296,51 +296,30 @@ const app = createApp({
|
||||
const handleImport = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const text = await file.text();
|
||||
const parsed = TOML.parse(text);
|
||||
if (!parsed.proxies || !Array.isArray(parsed.proxies)) {
|
||||
alert("无效的 TOML 文件:缺少 proxies 数组");
|
||||
return;
|
||||
}
|
||||
// 过滤出有效的隧道条目
|
||||
const list = parsed.proxies.filter(
|
||||
(p) => p.name && p.localIP && p.localPort && p.remotePort,
|
||||
);
|
||||
if (list.length === 0) {
|
||||
alert("未解析到有效的隧道条目");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!confirm(`解析到 ${list.length} 条隧道,将覆盖当前配置,确认导入?`)
|
||||
)
|
||||
return;
|
||||
|
||||
// 批量导入:先清空,再逐条插入(后端未提供批量接口,用循环)
|
||||
// 简单做法:先删除所有,再逐个创建
|
||||
for (const p of proxies.value) {
|
||||
await apiFetch("/api/proxy/" + p.id, { method: "DELETE" });
|
||||
}
|
||||
for (const p of list) {
|
||||
await apiFetch("/api/proxy", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: p.name,
|
||||
type: p.type || "tcp",
|
||||
localIP: p.localIP,
|
||||
localPort: p.localPort,
|
||||
remotePort: p.remotePort,
|
||||
enabled: p.enabled !== undefined ? p.enabled : true,
|
||||
}),
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/import/toml', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token.value,
|
||||
// 不要设置 Content-Type,浏览器会自动加上 boundary
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.code === 0) {
|
||||
alert(data.msg);
|
||||
await loadAllData();
|
||||
} else {
|
||||
alert('导入失败: ' + data.msg);
|
||||
}
|
||||
await loadProxies();
|
||||
await loadFrpcStatus();
|
||||
alert("导入成功!");
|
||||
} catch (err) {
|
||||
alert("解析 TOML 失败: " + err.message);
|
||||
alert('网络错误: ' + err.message);
|
||||
}
|
||||
e.target.value = "";
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
// ===== 导出 TOML =====
|
||||
|
||||
+3
-3
@@ -63,12 +63,12 @@
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="tab-bar">
|
||||
<span class="tab-item" :class="{ active: activeTab === 'config' }" @click="activeTab = 'config'">
|
||||
全局配置
|
||||
</span>
|
||||
<span class="tab-item" :class="{ active: activeTab === 'proxies' }" @click="activeTab = 'proxies'">
|
||||
隧道列表
|
||||
</span>
|
||||
<span class="tab-item" :class="{ active: activeTab === 'config' }" @click="activeTab = 'config'">
|
||||
全局配置信息
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 内容区 -->
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FrpcToml 对应 frpc.toml 的完整结构
|
||||
type FrpcToml struct {
|
||||
ServerAddr string `json:"serverAddr"`
|
||||
ServerPort int `json:"serverPort"`
|
||||
Auth struct {
|
||||
Token string `json:"token"`
|
||||
} `json:"auth"`
|
||||
Log struct {
|
||||
To string `json:"to"`
|
||||
Level string `json:"level"`
|
||||
MaxDays int `json:"maxDays"`
|
||||
} `json:"log"`
|
||||
Transport struct {
|
||||
TcpMux bool `json:"tcpMux"`
|
||||
TcpMuxKeepalive int `json:"tcpMuxKeepalive"`
|
||||
HeartbeatInterval int `json:"heartbeatInterval"`
|
||||
HeartbeatTimeout int `json:"heartbeatTimeout"`
|
||||
PoolCount int `json:"poolCount"`
|
||||
} `json:"transport"`
|
||||
Proxies []TomlProxy `json:"proxies"`
|
||||
}
|
||||
|
||||
// TomlProxy 对应 [[proxies]] 条目
|
||||
type TomlProxy struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
LocalIP string `json:"localIP"`
|
||||
LocalPort int `json:"localPort"`
|
||||
RemotePort int `json:"remotePort"`
|
||||
Enabled bool `json:"enabled"` // 导入时默认 true
|
||||
}
|
||||
|
||||
// ParseToml 解析 frpc.toml 内容
|
||||
func ParseToml(content string) (*FrpcToml, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
result := &FrpcToml{
|
||||
Proxies: []TomlProxy{},
|
||||
Transport: struct {
|
||||
TcpMux bool `json:"tcpMux"`
|
||||
TcpMuxKeepalive int `json:"tcpMuxKeepalive"`
|
||||
HeartbeatInterval int `json:"heartbeatInterval"`
|
||||
HeartbeatTimeout int `json:"heartbeatTimeout"`
|
||||
PoolCount int `json:"poolCount"`
|
||||
}{
|
||||
TcpMux: true, // 默认值
|
||||
TcpMuxKeepalive: 30,
|
||||
HeartbeatInterval: 15,
|
||||
HeartbeatTimeout: 70,
|
||||
PoolCount: 8,
|
||||
},
|
||||
}
|
||||
|
||||
// 简单状态机解析
|
||||
var currentProxy *TomlProxy
|
||||
inProxies := false
|
||||
|
||||
for _, rawLine := range lines {
|
||||
line := strings.TrimSpace(rawLine)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检测 [[proxies]] 段开始
|
||||
if strings.HasPrefix(line, "[[proxies]]") {
|
||||
inProxies = true
|
||||
currentProxy = &TomlProxy{
|
||||
Type: "tcp",
|
||||
Enabled: true,
|
||||
}
|
||||
result.Proxies = append(result.Proxies, *currentProxy)
|
||||
// 注意:由于值传递,需要取最后一个元素的指针
|
||||
currentProxy = &result.Proxies[len(result.Proxies)-1]
|
||||
continue
|
||||
}
|
||||
|
||||
// 检测其他段头(忽略,我们的解析器只关心具体键值对)
|
||||
if strings.HasPrefix(line, "[") {
|
||||
inProxies = false
|
||||
currentProxy = nil
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析键值对
|
||||
if strings.Contains(line, "=") {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
// 去掉引号
|
||||
value = strings.Trim(value, `"`)
|
||||
|
||||
if inProxies && currentProxy != nil {
|
||||
// 解析隧道字段
|
||||
switch key {
|
||||
case "name":
|
||||
currentProxy.Name = value
|
||||
case "type":
|
||||
currentProxy.Type = value
|
||||
case "localIP":
|
||||
currentProxy.LocalIP = value
|
||||
case "localPort":
|
||||
currentProxy.LocalPort, _ = strconv.Atoi(value)
|
||||
case "remotePort":
|
||||
currentProxy.RemotePort, _ = strconv.Atoi(value)
|
||||
}
|
||||
} else {
|
||||
// 解析全局字段
|
||||
switch key {
|
||||
case "serverAddr":
|
||||
result.ServerAddr = value
|
||||
case "serverPort":
|
||||
result.ServerPort, _ = strconv.Atoi(value)
|
||||
case "token":
|
||||
result.Auth.Token = value
|
||||
case "level":
|
||||
result.Log.Level = value
|
||||
case "maxDays":
|
||||
result.Log.MaxDays, _ = strconv.Atoi(value)
|
||||
case "tcpMux":
|
||||
result.Transport.TcpMux = value == "true"
|
||||
case "tcpMuxKeepaliveInterval":
|
||||
result.Transport.TcpMuxKeepalive, _ = strconv.Atoi(value)
|
||||
case "heartbeatInterval":
|
||||
result.Transport.HeartbeatInterval, _ = strconv.Atoi(value)
|
||||
case "heartbeatTimeout":
|
||||
result.Transport.HeartbeatTimeout, _ = strconv.Atoi(value)
|
||||
case "poolCount":
|
||||
result.Transport.PoolCount, _ = strconv.Atoi(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.ServerAddr == "" {
|
||||
return nil, fmt.Errorf("未找到 serverAddr 字段")
|
||||
}
|
||||
if len(result.Proxies) == 0 {
|
||||
return nil, fmt.Errorf("未找到任何隧道条目")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ToGlobalConfig 将解析结果转换为 GlobalConfig
|
||||
func (f *FrpcToml) ToGlobalConfig() *GlobalConfig {
|
||||
return &GlobalConfig{
|
||||
ServerAddr: f.ServerAddr,
|
||||
ServerPort: f.ServerPort,
|
||||
Token: f.Auth.Token,
|
||||
LogLevel: f.Log.Level,
|
||||
LogMaxDays: f.Log.MaxDays,
|
||||
TcpMux: f.Transport.TcpMux,
|
||||
TcpMuxKeepalive: f.Transport.TcpMuxKeepalive,
|
||||
HeartbeatInterval: f.Transport.HeartbeatInterval,
|
||||
HeartbeatTimeout: f.Transport.HeartbeatTimeout,
|
||||
PoolCount: f.Transport.PoolCount,
|
||||
}
|
||||
}
|
||||
|
||||
// ToProxies 将解析结果转换为 Proxy 列表
|
||||
func (f *FrpcToml) ToProxies() []Proxy {
|
||||
var proxies []Proxy
|
||||
for _, p := range f.Proxies {
|
||||
proxies = append(proxies, Proxy{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
LocalIP: p.LocalIP,
|
||||
LocalPort: p.LocalPort,
|
||||
RemotePort: p.RemotePort,
|
||||
Enabled: true, // 导入默认启用
|
||||
})
|
||||
}
|
||||
return proxies
|
||||
}
|
||||
Reference in New Issue
Block a user