第一份2.0-LTS代码上传

This commit is contained in:
2026-07-26 21:55:42 +08:00
parent 542f265a35
commit e64a8687d2
8 changed files with 286 additions and 115 deletions
BIN
View File
Binary file not shown.
+237 -71
View File
@@ -4,15 +4,31 @@ import (
"crypto/rand" "crypto/rand"
"database/sql" "database/sql"
"encoding/hex" "encoding/hex"
"fmt"
"io"
"log" "log"
"os"
"strings" "strings"
"time"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
var DB *sql.DB var DB *sql.DB
// ===== 全局配置表(A表)===== // ============================================================
// 数据库版本常量
// ============================================================
const (
SchemaVersion = "2.0.0" // 当前数据库 Schema 版本,与项目版本同步
)
// ============================================================
// 数据模型
// ============================================================
// GlobalConfig 全局配置表
type GlobalConfig struct { type GlobalConfig struct {
ID int `json:"id"` ID int `json:"id"`
ServerAddr string `json:"serverAddr"` ServerAddr string `json:"serverAddr"`
@@ -25,10 +41,10 @@ type GlobalConfig struct {
HeartbeatInterval int `json:"heartbeatInterval"` HeartbeatInterval int `json:"heartbeatInterval"`
HeartbeatTimeout int `json:"heartbeatTimeout"` HeartbeatTimeout int `json:"heartbeatTimeout"`
PoolCount int `json:"poolCount"` PoolCount int `json:"poolCount"`
WireProtocolV2 bool `json:"wireProtocolV2"` // frp v2 协议开关 (v2.0 启用) WireProtocolV2 bool `json:"wireProtocolV2"` // v2.0 正式启用
} }
// ===== 隧道表(B表)===== // Proxy 隧道表
type Proxy struct { type Proxy struct {
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -39,7 +55,7 @@ type Proxy struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
} }
// ===== 用户表(C表)===== // User 用户表
type User struct { type User struct {
ID int `json:"id"` ID int `json:"id"`
Username string `json:"username"` Username string `json:"username"`
@@ -58,8 +74,32 @@ func InitDB() error {
return err return err
} }
// ---- 用户表 ---- // ---- 创建所有表 ----
_, err = DB.Exec(` if err := createTables(); err != nil {
return err
}
// ---- 执行版本迁移 ----
if err := runMigrations(); err != nil {
return err
}
// ---- 确保 JWT 密钥存在 ----
if err := ensureJwtSecret(); err != nil {
return err
}
log.Println("✅ 数据库初始化完成 (Schema v" + SchemaVersion + ")")
return nil
}
// ============================================================
// 建表
// ============================================================
func createTables() error {
// 用户表
_, err := DB.Exec(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL, username TEXT UNIQUE NOT NULL,
@@ -72,7 +112,7 @@ func InitDB() error {
return err return err
} }
// ---- 全局配置表 ---- // 全局配置表
_, err = DB.Exec(` _, err = DB.Exec(`
CREATE TABLE IF NOT EXISTS global_config ( CREATE TABLE IF NOT EXISTS global_config (
id INTEGER PRIMARY KEY CHECK (id = 1), id INTEGER PRIMARY KEY CHECK (id = 1),
@@ -94,29 +134,7 @@ func InitDB() error {
return err return err
} }
// ---- 迁移:自动补全缺失的字段 ---- // 隧道表
if err := migrateGlobalConfigTable(); err != nil {
log.Printf("⚠️ 迁移警告: %v", err)
}
// ---- 初始化默认配置(仅当表为空时) ----
var count int
DB.QueryRow("SELECT COUNT(*) FROM global_config").Scan(&count)
if count == 0 {
_, err = DB.Exec(`
INSERT INTO global_config (
id, server_addr, server_port, token, log_level, log_max_days,
tcp_mux, tcp_mux_keepalive, heartbeat_interval, heartbeat_timeout, pool_count,
wire_protocol_v2
) VALUES (1, 'frp.example.com', 7000, 'CHANGE_ME', 'info', 3, 1, 30, 15, 70, 8, 0)
`)
if err != nil {
return err
}
log.Println("✅ 全局配置初始化完成")
}
// ---- 隧道表 ----
_, err = DB.Exec(` _, err = DB.Exec(`
CREATE TABLE IF NOT EXISTS proxies ( CREATE TABLE IF NOT EXISTS proxies (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -134,7 +152,7 @@ func InitDB() error {
return err return err
} }
// ---- 应用配置表 ---- // 应用配置表(存储 JWT 密钥、Schema 版本等)
_, err = DB.Exec(` _, err = DB.Exec(`
CREATE TABLE IF NOT EXISTS app_config ( CREATE TABLE IF NOT EXISTS app_config (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
@@ -146,66 +164,214 @@ func InitDB() error {
return err return err
} }
if err := ensureJwtSecret(); err != nil { // 初始化默认配置(仅当表为空时)
return err var count int
DB.QueryRow("SELECT COUNT(*) FROM global_config").Scan(&count)
if count == 0 {
_, err = DB.Exec(`
INSERT INTO global_config (
id, server_addr, server_port, token, log_level, log_max_days,
tcp_mux, tcp_mux_keepalive, heartbeat_interval, heartbeat_timeout, pool_count,
wire_protocol_v2
) VALUES (1, 'frp.example.com', 7000, 'CHANGE_ME', 'info', 3, 1, 30, 15, 70, 8, 0)
`)
if err != nil {
return err
}
log.Println("✅ 全局配置初始化完成")
} }
log.Println("✅ 数据库初始化完成")
return nil return nil
} }
// ============================================================ // ============================================================
// Schema 迁移:自动补全缺失的字段 // 迁移引擎
// ============================================================ // ============================================================
// expectedColumns 定义所有期望的列,格式: 列名 => 完整定义 // getSchemaVersion 读取当前数据库的 Schema 版本
// 新增字段时,在这里加一行即可 func getSchemaVersion() string {
var expectedColumns = map[string]string{ var version string
"id": "id INTEGER PRIMARY KEY CHECK (id = 1)", err := DB.QueryRow("SELECT value FROM app_config WHERE key = 'schema_version'").Scan(&version)
"server_addr": "server_addr TEXT NOT NULL DEFAULT 'frp.example.com'", if err != nil {
"server_port": "server_port INTEGER NOT NULL DEFAULT 7000", if err == sql.ErrNoRows {
"token": "token TEXT NOT NULL DEFAULT 'CHANGE_ME'", // 没有版本记录 → 首次启动或 v1.x 升级
"log_level": "log_level TEXT NOT NULL DEFAULT 'info'", // 检查是否已有数据(通过 users 表判断)
"log_max_days": "log_max_days INTEGER NOT NULL DEFAULT 3", var count int
"tcp_mux": "tcp_mux INTEGER NOT NULL DEFAULT 1", DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
"tcp_mux_keepalive": "tcp_mux_keepalive INTEGER NOT NULL DEFAULT 30", if count > 0 {
"heartbeat_interval": "heartbeat_interval INTEGER NOT NULL DEFAULT 15", // 有用户数据 → 这是 v1.x 升级,标记为 1.5
"heartbeat_timeout": "heartbeat_timeout INTEGER NOT NULL DEFAULT 70", return "1.5.0"
"pool_count": "pool_count INTEGER NOT NULL DEFAULT 8", }
"wire_protocol_v2": "wire_protocol_v2 INTEGER NOT NULL DEFAULT 0", // v1.5 新增 // 全新安装 → 直接标记为当前版本
"updated_at": "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP", return SchemaVersion
}
log.Printf("⚠️ 读取 Schema 版本失败: %v", err)
return "1.5.0" // 保守降级
}
return version
} }
func migrateGlobalConfigTable() error { // setSchemaVersion 更新 Schema 版本
// 1. 获取当前表的列名 func setSchemaVersion(version string) error {
currentCols, err := getCurrentColumns("global_config") _, err := DB.Exec(`
if err != nil { INSERT INTO app_config (key, value) VALUES ('schema_version', ?)
return err ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
`, version)
return err
}
// backupDatabase 备份数据库文件
func backupDatabase() (string, error) {
src := "./frpc-console.db"
if _, err := os.Stat(src); os.IsNotExist(err) {
return "", nil // 数据库不存在,无需备份
} }
// 2. 比对并补全缺失的列 timestamp := time.Now().Format("20060102_150405")
var added []string dst := fmt.Sprintf("./frpc-console.db.pre-v%s.%s", SchemaVersion, timestamp)
for colName, colDef := range expectedColumns {
if !contains(currentCols, colName) { srcFile, err := os.Open(src)
alterSQL := "ALTER TABLE global_config ADD COLUMN " + colDef if err != nil {
if _, err := DB.Exec(alterSQL); err != nil { return "", fmt.Errorf("打开源数据库失败: %w", err)
log.Printf("⚠️ 添加列 %s 失败: %v", colName, err) }
continue defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return "", fmt.Errorf("创建备份文件失败: %w", err)
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return "", fmt.Errorf("复制数据库失败: %w", err)
}
log.Printf("✅ 数据库备份完成: %s", dst)
return dst, nil
}
// runMigrations 执行版本迁移
func runMigrations() error {
currentVersion := getSchemaVersion()
log.Printf("📌 当前数据库 Schema: %s, 目标版本: %s", currentVersion, SchemaVersion)
if currentVersion == SchemaVersion {
log.Println("✅ Schema 已是最新,无需迁移")
return nil
}
log.Printf("🔄 检测到版本变更 (%s → %s),开始迁移...", currentVersion, SchemaVersion)
// ---- 1. 备份数据库 ----
backupPath, err := backupDatabase()
if err != nil {
return fmt.Errorf("备份数据库失败: %w", err)
}
if backupPath != "" {
log.Printf("📦 备份文件: %s", backupPath)
} else {
log.Println("📦 数据库为空,跳过备份")
}
// ---- 2. 执行迁移 ----
// 按照版本号逐个升级
migrations := []struct {
from string
upgrade func() error
}{
{"1.5.0", migrateFrom1_5_0},
{"2.0.0", migrateFrom2_0_0}, // 预留,实际无操作
}
applied := 0
for _, m := range migrations {
if currentVersion == m.from {
log.Printf(" 执行迁移: %s → %s", m.from, SchemaVersion)
if err := m.upgrade(); err != nil {
// 迁移失败,尝试恢复备份
if backupPath != "" {
log.Printf("❌ 迁移失败,尝试恢复备份: %s", backupPath)
if restoreErr := restoreDatabase(backupPath); restoreErr != nil {
log.Printf("⚠️ 恢复备份失败: %v", restoreErr)
}
}
return fmt.Errorf("迁移失败: %w", err)
} }
added = append(added, colName) applied++
currentVersion = SchemaVersion
break
} }
} }
if len(added) > 0 { // ---- 3. 更新 Schema 版本 ----
log.Printf("✅ 数据库迁移完成,新增字段: %v", added) if err := setSchemaVersion(SchemaVersion); err != nil {
} else { return fmt.Errorf("更新 Schema 版本失败: %w", err)
log.Println("✅ 数据库 Schema 已是最新")
} }
log.Printf("✅ 迁移完成,应用了 %d 个迁移", applied)
return nil return nil
} }
// getCurrentColumns 查询指定表的所有列名 // restoreDatabase 从备份恢复数据库
func restoreDatabase(backupPath string) error {
srcFile, err := os.Open(backupPath)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.Create("./frpc-console.db")
if err != nil {
return err
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
log.Printf("✅ 数据库已从备份恢复: %s", backupPath)
return nil
}
// ============================================================
// 迁移函数(各版本)
// ============================================================
// migrateFrom1_5_0: v1.5 → v2.0
// v1.5 已经有 wire_protocol_v2 字段(灰标占位),v2.0 无需新增字段
// 但需要确保字段存在(兼容从 v1.0 直接升级的场景)
func migrateFrom1_5_0() error {
log.Println(" 迁移: v1.5.0 → v2.0.0")
// 检查并补全 wire_protocol_v2 字段(兼容从 v1.0 直接升级的场景)
cols, err := getCurrentColumns("global_config")
if err != nil {
return fmt.Errorf("获取列信息失败: %w", err)
}
if !contains(cols, "wire_protocol_v2") {
log.Println(" 添加字段: wire_protocol_v2")
_, err := DB.Exec("ALTER TABLE global_config ADD COLUMN wire_protocol_v2 INTEGER NOT NULL DEFAULT 0")
if err != nil {
return fmt.Errorf("添加 wire_protocol_v2 字段失败: %w", err)
}
}
log.Println(" ✅ v1.5.0 → v2.0.0 迁移完成")
return nil
}
// migrateFrom2_0_0: 预留,v2.0 → 未来版本
func migrateFrom2_0_0() error {
log.Println(" v2.0.0 已是当前版本,无需迁移")
return nil
}
// ============================================================
// 辅助函数
// ============================================================
func getCurrentColumns(tableName string) ([]string, error) { func getCurrentColumns(tableName string) ([]string, error) {
rows, err := DB.Query("PRAGMA table_info(" + tableName + ")") rows, err := DB.Query("PRAGMA table_info(" + tableName + ")")
if err != nil { if err != nil {
@@ -263,7 +429,7 @@ func ensureJwtSecret() error {
if err != nil { if err != nil {
return err return err
} }
log.Printf("✅ JWT 密钥已生成并保存到数据库") log.Printf("✅ JWT 密钥已生成")
return nil return nil
} }
+25 -22
View File
@@ -27,10 +27,10 @@ var (
frpcPathMutex sync.Mutex frpcPathMutex sync.Mutex
) )
// getFrpcPath 获取 frpc 路径,优先级: // ============================================================
// 1. 当前目录 ./bin/<platform> (便于用户手动管理 frpc 版本) // 获取 frpc 路径
// 2. embed 解压到 /tmp (便于开箱即用) // ============================================================
// 3. 系统 PATH 中的 frpc (降级方案)
func getFrpcPath() (string, error) { func getFrpcPath() (string, error) {
frpcPathMutex.Lock() frpcPathMutex.Lock()
defer frpcPathMutex.Unlock() defer frpcPathMutex.Unlock()
@@ -68,7 +68,7 @@ func getFrpcPath() (string, error) {
return localPath, nil return localPath, nil
} }
// 优先级2embed 解压到临时目录 // 优先级2embed 解压
data, err := embeddedFrpc.ReadFile("bin/" + fileName) data, err := embeddedFrpc.ReadFile("bin/" + fileName)
if err == nil { if err == nil {
tmpPath := filepath.Join(os.TempDir(), "frpc") tmpPath := filepath.Join(os.TempDir(), "frpc")
@@ -92,10 +92,13 @@ func getFrpcPath() (string, error) {
return path, nil return path, nil
} }
return "", fmt.Errorf("未找到 frpc 文件 (本地 bin/、embed、PATH 均无)") return "", fmt.Errorf("未找到 frpc 文件")
} }
// GenerateFrpcConfig 生成 frpc.toml // ============================================================
// 生成 frpc.tomlv2.0 支持 wireProtocol
// ============================================================
func GenerateFrpcConfig() error { func GenerateFrpcConfig() error {
cfg, err := GetGlobalConfig() cfg, err := GetGlobalConfig()
if err != nil { if err != nil {
@@ -114,14 +117,24 @@ func GenerateFrpcConfig() error {
} }
} }
// 构建模板数据
data := struct { data := struct {
*GlobalConfig *GlobalConfig
Proxies []Proxy Proxies []Proxy
WireProtocolLine string // v2 协议配置行(为空则不输出)
}{ }{
GlobalConfig: cfg, GlobalConfig: cfg,
Proxies: activeProxies, Proxies: activeProxies,
} }
// v2.0: 如果启用 v2 协议,生成配置行
if cfg.WireProtocolV2 {
data.WireProtocolLine = `wireProtocol = "v2"`
} else {
data.WireProtocolLine = "" // 不输出,使用默认 v1
}
// 读取模板
var tmplContent string var tmplContent string
if _, err := os.Stat("frpc.tmpl"); err == nil { if _, err := os.Stat("frpc.tmpl"); err == nil {
content, readErr := os.ReadFile("frpc.tmpl") content, readErr := os.ReadFile("frpc.tmpl")
@@ -151,7 +164,10 @@ func GenerateFrpcConfig() error {
return nil return nil
} }
// isFrpcRunning 通过 PID 文件检查 frpc 是否在运行 // ============================================================
// frpc 进程管理
// ============================================================
func isFrpcRunning() bool { func isFrpcRunning() bool {
pidData, err := os.ReadFile("./frpc.pid") pidData, err := os.ReadFile("./frpc.pid")
if err != nil { if err != nil {
@@ -163,7 +179,6 @@ func isFrpcRunning() bool {
} }
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
// Windows 用 tasklist 检查
cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid)) cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid))
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@@ -172,7 +187,6 @@ func isFrpcRunning() bool {
return strings.Contains(string(output), strconv.Itoa(pid)) return strings.Contains(string(output), strconv.Itoa(pid))
} }
// Linux/Unix: 用 kill 0 检查进程是否存在
process, err := os.FindProcess(pid) process, err := os.FindProcess(pid)
if err != nil { if err != nil {
return false return false
@@ -180,7 +194,6 @@ func isFrpcRunning() bool {
return process.Signal(syscall.Signal(0)) == nil return process.Signal(syscall.Signal(0)) == nil
} }
// StartFrpc 启动 frpc(独立进程,脱离父进程)
func StartFrpc() error { func StartFrpc() error {
frpcPath, err := getFrpcPath() frpcPath, err := getFrpcPath()
if err != nil { if err != nil {
@@ -197,16 +210,12 @@ func StartFrpc() error {
return nil return nil
} }
// 清理旧的 PID 文件
os.Remove("./frpc.pid") os.Remove("./frpc.pid")
cmd := exec.Command(frpcPath, "-c", "./frpc.toml") cmd := exec.Command(frpcPath, "-c", "./frpc.toml")
setWindowHide(cmd) setWindowHide(cmd)
// ✅ 设置进程属性(Unix: Setsid 脱离父进程,Windows: 不处理)
setSysProcAttr(cmd) setSysProcAttr(cmd)
// 把输出重定向到日志文件
logFile, err := os.OpenFile("./frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) logFile, err := os.OpenFile("./frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil { if err != nil {
return fmt.Errorf("打开日志文件失败: %w", err) return fmt.Errorf("打开日志文件失败: %w", err)
@@ -218,7 +227,6 @@ func StartFrpc() error {
return fmt.Errorf("启动 frpc 失败: %w", err) return fmt.Errorf("启动 frpc 失败: %w", err)
} }
// ✅ 保存 PID 到文件
if err := os.WriteFile("./frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil { if err := os.WriteFile("./frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil {
return fmt.Errorf("保存 PID 失败: %w", err) return fmt.Errorf("保存 PID 失败: %w", err)
} }
@@ -226,7 +234,6 @@ func StartFrpc() error {
return nil return nil
} }
// StopFrpc 停止 frpc(通过 PID 文件精准杀进程)
func StopFrpc() error { func StopFrpc() error {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe") cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe")
@@ -237,10 +244,8 @@ func StopFrpc() error {
return nil return nil
} }
// Linux: 读取 PID 文件精准杀进程
pidData, err := os.ReadFile("./frpc.pid") pidData, err := os.ReadFile("./frpc.pid")
if err != nil { if err != nil {
// 降级到 pkill
cmd := exec.Command("pkill", "-f", "frpc") cmd := exec.Command("pkill", "-f", "frpc")
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "no process") { if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "no process") {
return fmt.Errorf("停止 frpc 失败: %w", err) return fmt.Errorf("停止 frpc 失败: %w", err)
@@ -263,12 +268,10 @@ func StopFrpc() error {
return nil return nil
} }
// GetFrpcStatus 获取 frpc 运行状态(通过 PID 文件)
func GetFrpcStatus() (bool, error) { func GetFrpcStatus() (bool, error) {
return isFrpcRunning(), nil return isFrpcRunning(), nil
} }
// ReloadFrpc 热加载,失败时自动降级为重启
func ReloadFrpc() error { func ReloadFrpc() error {
running := isFrpcRunning() running := isFrpcRunning()
if !running { if !running {
+3
View File
@@ -15,6 +15,9 @@ tcpMuxKeepaliveInterval = {{.TcpMuxKeepalive}}
heartbeatInterval = {{.HeartbeatInterval}} heartbeatInterval = {{.HeartbeatInterval}}
heartbeatTimeout = {{.HeartbeatTimeout}} heartbeatTimeout = {{.HeartbeatTimeout}}
poolCount = {{.PoolCount}} poolCount = {{.PoolCount}}
{{if .WireProtocolLine}}
{{.WireProtocolLine}}
{{end}}
[webServer] [webServer]
addr = "127.0.0.1:7400" addr = "127.0.0.1:7400"
+12 -3
View File
@@ -201,7 +201,7 @@ frpc-console 遵循 **“够用就好”** 的原则:
## 📝 更新日志 ## 📝 更新日志
### 1.0-Release (2026-07-24) ### 1.0-Release (2026-07-24)non-LTS
· 🎉 首次发布</br> · 🎉 首次发布</br>
· ✨ 支持隧道增删改查</br> · ✨ 支持隧道增删改查</br>
@@ -211,7 +211,7 @@ frpc-console 遵循 **“够用就好”** 的原则:
· ✨ 多平台 frpc 自动适配</br> · ✨ 多平台 frpc 自动适配</br>
· 🐳 Docker 镜像支持 · 🐳 Docker 镜像支持
### 1.5-Release (2026-07-25) ### 1.5-Release (2026-07-25)non-LTS
· ✨ 全局配置页面卡片式重构(服务器连接 / 传输配置 / 日志配置)</br> · ✨ 全局配置页面卡片式重构(服务器连接 / 传输配置 / 日志配置)</br>
· ✨ 新增运行日志面板(实时查看 frpc 日志,8 秒自动刷新)</br> · ✨ 新增运行日志面板(实时查看 frpc 日志,8 秒自动刷新)</br>
@@ -221,7 +221,16 @@ frpc-console 遵循 **“够用就好”** 的原则:
· 🎨 样式拆分与优化</br> · 🎨 样式拆分与优化</br>
· 🐛 修复配置页面结构错乱、日志行泛光异常等问题</br> · 🐛 修复配置页面结构错乱、日志行泛光异常等问题</br>
### 2.0-Release (猫猫正在修整,等待后续开发) ### 2.0-Release LTS,跟随上游提供支持)
· 🔐 **首次启动引导** —— Web 端完成管理员注册,无需 CLI 交互
· 📋 **隧道全生命周期管理** —— 增删改查 + 一键启用/禁用
· 📦 **导入/导出 TOML** —— 无缝迁移现有 frpc 配置
· 🔄 **配置热加载** —— 修改即生效,无需重启 frpc
· 🖥️ **多平台支持** —— Windows / Linux / ARM 全平台兼容
· 🐳 **容器化就绪** —— 提供 Docker 镜像,开箱即用
· 🎨 **深色磨砂玻璃 UI** —— 现代化视觉体验,日夜皆宜
· 🚀 **frp v2 协议支持** —— 支持 frp 0.70.0 LTS 的 Wire Protocol v2
· 🔒 **数据库自动迁移** —— 升级时自动备份 + 迁移,数据零丢失
--- ---
+2 -2
View File
@@ -26,8 +26,8 @@ var platforms = []Platform{
} }
const ( const (
Version = "1.5.0" Version = "2.0.0"
BuildTime = "2026-07-25" BuildTime = "2026-07-26"
Binary = "frpc-console" Binary = "frpc-console"
) )
+1 -2
View File
@@ -267,10 +267,9 @@
<label>frp v2 隧道支持</label> <label>frp v2 隧道支持</label>
<div class="v2-switch-wrapper"> <div class="v2-switch-wrapper">
<label class="switch disabled"> <label class="switch disabled">
<input type="checkbox" v-model="globalConfig.wireProtocolV2" disabled /> <input type="checkbox" v-model="globalConfig.wireProtocolV2" />
<span class="slider"></span> <span class="slider"></span>
</label> </label>
<span class="v2-badge">即将推出</span>
</div> </div>
<span class="hint-text v2-hint"> <span class="hint-text v2-hint">
此功能为 frp 0.70.0 以上版本才能开启,若要打开必须搭配同版本 frps 使用,否则此功能不生效 此功能为 frp 0.70.0 以上版本才能开启,若要打开必须搭配同版本 frps 使用,否则此功能不生效
+6 -15
View File
@@ -155,21 +155,6 @@
padding: 6px 0; padding: 6px 0;
} }
/* 禁用状态开关 - 灰标 */
.switch.disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.switch.disabled .slider {
background: rgba(255, 255, 255, 0.08);
}
.switch.disabled input:checked + .slider {
background: rgba(74, 124, 255, 0.25);
}
.switch.disabled .slider::before {
background: #3a3a4a;
}
/* "即将推出" 徽章 */ /* "即将推出" 徽章 */
.v2-badge { .v2-badge {
@@ -183,6 +168,12 @@
font-weight: 500; font-weight: 500;
} }
.v2-badge.v2-enabled {
background: rgba(99, 226, 183, 0.12);
border-color: rgba(99, 226, 183, 0.2);
color: #63e2b7;
}
.v2-hint { .v2-hint {
font-size: 12px; font-size: 12px;
color: var(--text-dim); color: var(--text-dim);