第一份2.0-LTS代码上传
This commit is contained in:
@@ -4,15 +4,31 @@ import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
var DB *sql.DB
|
||||
|
||||
// ===== 全局配置表(A表)=====
|
||||
// ============================================================
|
||||
// 数据库版本常量
|
||||
// ============================================================
|
||||
|
||||
const (
|
||||
SchemaVersion = "2.0.0" // 当前数据库 Schema 版本,与项目版本同步
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 数据模型
|
||||
// ============================================================
|
||||
|
||||
// GlobalConfig 全局配置表
|
||||
type GlobalConfig struct {
|
||||
ID int `json:"id"`
|
||||
ServerAddr string `json:"serverAddr"`
|
||||
@@ -25,10 +41,10 @@ type GlobalConfig struct {
|
||||
HeartbeatInterval int `json:"heartbeatInterval"`
|
||||
HeartbeatTimeout int `json:"heartbeatTimeout"`
|
||||
PoolCount int `json:"poolCount"`
|
||||
WireProtocolV2 bool `json:"wireProtocolV2"` // frp v2 协议开关 (v2.0 启用)
|
||||
WireProtocolV2 bool `json:"wireProtocolV2"` // v2.0 正式启用
|
||||
}
|
||||
|
||||
// ===== 隧道表(B表)=====
|
||||
// Proxy 隧道表
|
||||
type Proxy struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -39,7 +55,7 @@ type Proxy struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// ===== 用户表(C表)=====
|
||||
// User 用户表
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
@@ -58,8 +74,32 @@ func InitDB() error {
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
@@ -72,7 +112,7 @@ func InitDB() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ---- 全局配置表 ----
|
||||
// 全局配置表
|
||||
_, err = DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS global_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
@@ -94,29 +134,7 @@ func InitDB() error {
|
||||
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(`
|
||||
CREATE TABLE IF NOT EXISTS proxies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -134,7 +152,7 @@ func InitDB() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ---- 应用配置表 ----
|
||||
// 应用配置表(存储 JWT 密钥、Schema 版本等)
|
||||
_, err = DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS app_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
@@ -146,66 +164,214 @@ func InitDB() error {
|
||||
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
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Schema 迁移:自动补全缺失的字段
|
||||
// 迁移引擎
|
||||
// ============================================================
|
||||
|
||||
// expectedColumns 定义所有期望的列,格式: 列名 => 完整定义
|
||||
// 新增字段时,在这里加一行即可
|
||||
var expectedColumns = map[string]string{
|
||||
"id": "id INTEGER PRIMARY KEY CHECK (id = 1)",
|
||||
"server_addr": "server_addr TEXT NOT NULL DEFAULT 'frp.example.com'",
|
||||
"server_port": "server_port INTEGER NOT NULL DEFAULT 7000",
|
||||
"token": "token TEXT NOT NULL DEFAULT 'CHANGE_ME'",
|
||||
"log_level": "log_level TEXT NOT NULL DEFAULT 'info'",
|
||||
"log_max_days": "log_max_days INTEGER NOT NULL DEFAULT 3",
|
||||
"tcp_mux": "tcp_mux INTEGER NOT NULL DEFAULT 1",
|
||||
"tcp_mux_keepalive": "tcp_mux_keepalive INTEGER NOT NULL DEFAULT 30",
|
||||
"heartbeat_interval": "heartbeat_interval INTEGER NOT NULL DEFAULT 15",
|
||||
"heartbeat_timeout": "heartbeat_timeout INTEGER NOT NULL DEFAULT 70",
|
||||
"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",
|
||||
// getSchemaVersion 读取当前数据库的 Schema 版本
|
||||
func getSchemaVersion() string {
|
||||
var version string
|
||||
err := DB.QueryRow("SELECT value FROM app_config WHERE key = 'schema_version'").Scan(&version)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// 没有版本记录 → 首次启动或 v1.x 升级
|
||||
// 检查是否已有数据(通过 users 表判断)
|
||||
var count int
|
||||
DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
|
||||
if count > 0 {
|
||||
// 有用户数据 → 这是 v1.x 升级,标记为 1.5
|
||||
return "1.5.0"
|
||||
}
|
||||
// 全新安装 → 直接标记为当前版本
|
||||
return SchemaVersion
|
||||
}
|
||||
log.Printf("⚠️ 读取 Schema 版本失败: %v", err)
|
||||
return "1.5.0" // 保守降级
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
func migrateGlobalConfigTable() error {
|
||||
// 1. 获取当前表的列名
|
||||
currentCols, err := getCurrentColumns("global_config")
|
||||
if err != nil {
|
||||
return err
|
||||
// setSchemaVersion 更新 Schema 版本
|
||||
func setSchemaVersion(version string) error {
|
||||
_, err := DB.Exec(`
|
||||
INSERT INTO app_config (key, value) VALUES ('schema_version', ?)
|
||||
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. 比对并补全缺失的列
|
||||
var added []string
|
||||
for colName, colDef := range expectedColumns {
|
||||
if !contains(currentCols, colName) {
|
||||
alterSQL := "ALTER TABLE global_config ADD COLUMN " + colDef
|
||||
if _, err := DB.Exec(alterSQL); err != nil {
|
||||
log.Printf("⚠️ 添加列 %s 失败: %v", colName, err)
|
||||
continue
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
dst := fmt.Sprintf("./frpc-console.db.pre-v%s.%s", SchemaVersion, timestamp)
|
||||
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("打开源数据库失败: %w", err)
|
||||
}
|
||||
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 {
|
||||
log.Printf("✅ 数据库迁移完成,新增字段: %v", added)
|
||||
} else {
|
||||
log.Println("✅ 数据库 Schema 已是最新")
|
||||
// ---- 3. 更新 Schema 版本 ----
|
||||
if err := setSchemaVersion(SchemaVersion); err != nil {
|
||||
return fmt.Errorf("更新 Schema 版本失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✅ 迁移完成,应用了 %d 个迁移", applied)
|
||||
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) {
|
||||
rows, err := DB.Query("PRAGMA table_info(" + tableName + ")")
|
||||
if err != nil {
|
||||
@@ -263,7 +429,7 @@ func ensureJwtSecret() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("✅ JWT 密钥已生成并保存到数据库")
|
||||
log.Printf("✅ JWT 密钥已生成")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@ var (
|
||||
frpcPathMutex sync.Mutex
|
||||
)
|
||||
|
||||
// getFrpcPath 获取 frpc 路径,优先级:
|
||||
// 1. 当前目录 ./bin/<platform> (便于用户手动管理 frpc 版本)
|
||||
// 2. embed 解压到 /tmp (便于开箱即用)
|
||||
// 3. 系统 PATH 中的 frpc (降级方案)
|
||||
// ============================================================
|
||||
// 获取 frpc 路径
|
||||
// ============================================================
|
||||
|
||||
func getFrpcPath() (string, error) {
|
||||
frpcPathMutex.Lock()
|
||||
defer frpcPathMutex.Unlock()
|
||||
@@ -68,7 +68,7 @@ func getFrpcPath() (string, error) {
|
||||
return localPath, nil
|
||||
}
|
||||
|
||||
// 优先级2:embed 解压到临时目录
|
||||
// 优先级2:embed 解压
|
||||
data, err := embeddedFrpc.ReadFile("bin/" + fileName)
|
||||
if err == nil {
|
||||
tmpPath := filepath.Join(os.TempDir(), "frpc")
|
||||
@@ -92,10 +92,13 @@ func getFrpcPath() (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("未找到 frpc 文件 (本地 bin/、embed、PATH 均无)")
|
||||
return "", fmt.Errorf("未找到 frpc 文件")
|
||||
}
|
||||
|
||||
// GenerateFrpcConfig 生成 frpc.toml
|
||||
// ============================================================
|
||||
// 生成 frpc.toml(v2.0 支持 wireProtocol)
|
||||
// ============================================================
|
||||
|
||||
func GenerateFrpcConfig() error {
|
||||
cfg, err := GetGlobalConfig()
|
||||
if err != nil {
|
||||
@@ -114,14 +117,24 @@ func GenerateFrpcConfig() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 构建模板数据
|
||||
data := struct {
|
||||
*GlobalConfig
|
||||
Proxies []Proxy
|
||||
Proxies []Proxy
|
||||
WireProtocolLine string // v2 协议配置行(为空则不输出)
|
||||
}{
|
||||
GlobalConfig: cfg,
|
||||
Proxies: activeProxies,
|
||||
}
|
||||
|
||||
// v2.0: 如果启用 v2 协议,生成配置行
|
||||
if cfg.WireProtocolV2 {
|
||||
data.WireProtocolLine = `wireProtocol = "v2"`
|
||||
} else {
|
||||
data.WireProtocolLine = "" // 不输出,使用默认 v1
|
||||
}
|
||||
|
||||
// 读取模板
|
||||
var tmplContent string
|
||||
if _, err := os.Stat("frpc.tmpl"); err == nil {
|
||||
content, readErr := os.ReadFile("frpc.tmpl")
|
||||
@@ -151,7 +164,10 @@ func GenerateFrpcConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// isFrpcRunning 通过 PID 文件检查 frpc 是否在运行
|
||||
// ============================================================
|
||||
// frpc 进程管理
|
||||
// ============================================================
|
||||
|
||||
func isFrpcRunning() bool {
|
||||
pidData, err := os.ReadFile("./frpc.pid")
|
||||
if err != nil {
|
||||
@@ -163,7 +179,6 @@ func isFrpcRunning() bool {
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows 用 tasklist 检查
|
||||
cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -172,7 +187,6 @@ func isFrpcRunning() bool {
|
||||
return strings.Contains(string(output), strconv.Itoa(pid))
|
||||
}
|
||||
|
||||
// Linux/Unix: 用 kill 0 检查进程是否存在
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -180,7 +194,6 @@ func isFrpcRunning() bool {
|
||||
return process.Signal(syscall.Signal(0)) == nil
|
||||
}
|
||||
|
||||
// StartFrpc 启动 frpc(独立进程,脱离父进程)
|
||||
func StartFrpc() error {
|
||||
frpcPath, err := getFrpcPath()
|
||||
if err != nil {
|
||||
@@ -197,16 +210,12 @@ func StartFrpc() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 清理旧的 PID 文件
|
||||
os.Remove("./frpc.pid")
|
||||
|
||||
cmd := exec.Command(frpcPath, "-c", "./frpc.toml")
|
||||
setWindowHide(cmd)
|
||||
|
||||
// ✅ 设置进程属性(Unix: Setsid 脱离父进程,Windows: 不处理)
|
||||
setSysProcAttr(cmd)
|
||||
|
||||
// 把输出重定向到日志文件
|
||||
logFile, err := os.OpenFile("./frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开日志文件失败: %w", err)
|
||||
@@ -218,7 +227,6 @@ func StartFrpc() error {
|
||||
return fmt.Errorf("启动 frpc 失败: %w", err)
|
||||
}
|
||||
|
||||
// ✅ 保存 PID 到文件
|
||||
if err := os.WriteFile("./frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil {
|
||||
return fmt.Errorf("保存 PID 失败: %w", err)
|
||||
}
|
||||
@@ -226,7 +234,6 @@ func StartFrpc() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopFrpc 停止 frpc(通过 PID 文件精准杀进程)
|
||||
func StopFrpc() error {
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe")
|
||||
@@ -237,10 +244,8 @@ func StopFrpc() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Linux: 读取 PID 文件精准杀进程
|
||||
pidData, err := os.ReadFile("./frpc.pid")
|
||||
if err != nil {
|
||||
// 降级到 pkill
|
||||
cmd := exec.Command("pkill", "-f", "frpc")
|
||||
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "no process") {
|
||||
return fmt.Errorf("停止 frpc 失败: %w", err)
|
||||
@@ -263,12 +268,10 @@ func StopFrpc() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFrpcStatus 获取 frpc 运行状态(通过 PID 文件)
|
||||
func GetFrpcStatus() (bool, error) {
|
||||
return isFrpcRunning(), nil
|
||||
}
|
||||
|
||||
// ReloadFrpc 热加载,失败时自动降级为重启
|
||||
func ReloadFrpc() error {
|
||||
running := isFrpcRunning()
|
||||
if !running {
|
||||
|
||||
@@ -15,6 +15,9 @@ tcpMuxKeepaliveInterval = {{.TcpMuxKeepalive}}
|
||||
heartbeatInterval = {{.HeartbeatInterval}}
|
||||
heartbeatTimeout = {{.HeartbeatTimeout}}
|
||||
poolCount = {{.PoolCount}}
|
||||
{{if .WireProtocolLine}}
|
||||
{{.WireProtocolLine}}
|
||||
{{end}}
|
||||
|
||||
[webServer]
|
||||
addr = "127.0.0.1:7400"
|
||||
|
||||
@@ -201,7 +201,7 @@ frpc-console 遵循 **“够用就好”** 的原则:
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### 1.0-Release (2026-07-24)
|
||||
### 1.0-Release (2026-07-24)(non-LTS)
|
||||
|
||||
· 🎉 首次发布</br>
|
||||
· ✨ 支持隧道增删改查</br>
|
||||
@@ -211,7 +211,7 @@ frpc-console 遵循 **“够用就好”** 的原则:
|
||||
· ✨ 多平台 frpc 自动适配</br>
|
||||
· 🐳 Docker 镜像支持
|
||||
|
||||
### 1.5-Release (2026-07-25)
|
||||
### 1.5-Release (2026-07-25)(non-LTS)
|
||||
|
||||
· ✨ 全局配置页面卡片式重构(服务器连接 / 传输配置 / 日志配置)</br>
|
||||
· ✨ 新增运行日志面板(实时查看 frpc 日志,8 秒自动刷新)</br>
|
||||
@@ -221,7 +221,16 @@ frpc-console 遵循 **“够用就好”** 的原则:
|
||||
· 🎨 样式拆分与优化</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
@@ -26,8 +26,8 @@ var platforms = []Platform{
|
||||
}
|
||||
|
||||
const (
|
||||
Version = "1.5.0"
|
||||
BuildTime = "2026-07-25"
|
||||
Version = "2.0.0"
|
||||
BuildTime = "2026-07-26"
|
||||
Binary = "frpc-console"
|
||||
)
|
||||
|
||||
|
||||
+1
-2
@@ -267,10 +267,9 @@
|
||||
<label>frp v2 隧道支持</label>
|
||||
<div class="v2-switch-wrapper">
|
||||
<label class="switch disabled">
|
||||
<input type="checkbox" v-model="globalConfig.wireProtocolV2" disabled />
|
||||
<input type="checkbox" v-model="globalConfig.wireProtocolV2" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="v2-badge">即将推出</span>
|
||||
</div>
|
||||
<span class="hint-text v2-hint">
|
||||
此功能为 frp 0.70.0 以上版本才能开启,若要打开必须搭配同版本 frps 使用,否则此功能不生效
|
||||
|
||||
+6
-15
@@ -155,21 +155,6 @@
|
||||
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 {
|
||||
@@ -183,6 +168,12 @@
|
||||
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 {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
|
||||
Reference in New Issue
Block a user