From f49bc07f425372d8dd91e48a8d0e1fb135144e19 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Sat, 25 Jul 2026 22:42:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD=EF=BC=9Afr?= =?UTF-8?q?p=20v2=20=E9=9A=A7=E9=81=93=E6=94=AF=E6=8C=81=EF=BC=881.5?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E4=B8=8B=E4=B8=BA=E7=81=B0=E6=A0=87=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E7=82=B9=E5=87=BB=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db.go | 154 +++++++++++++++++++++++++++++++++++++++------ static/app.js | 9 +++ static/index.html | 43 ++++++++----- static/style-3.css | 54 ++++++++++++++++ 4 files changed, 224 insertions(+), 36 deletions(-) diff --git a/db.go b/db.go index 2390914..ec458d1 100644 --- a/db.go +++ b/db.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/hex" "log" + "strings" _ "modernc.org/sqlite" ) @@ -24,6 +25,7 @@ type GlobalConfig struct { HeartbeatInterval int `json:"heartbeatInterval"` HeartbeatTimeout int `json:"heartbeatTimeout"` PoolCount int `json:"poolCount"` + WireProtocolV2 bool `json:"wireProtocolV2"` // frp v2 协议开关 (v2.0 启用) } // ===== 隧道表(B表)===== @@ -45,7 +47,10 @@ type User struct { CreatedAt string `json:"createdAt"` } -// ===== 初始化数据库 ===== +// ============================================================ +// 数据库初始化 +// ============================================================ + func InitDB() error { var err error DB, err = sql.Open("sqlite", "./frpc-console.db") @@ -53,7 +58,7 @@ func InitDB() error { return err } - // 用户表(C表) + // ---- 用户表 ---- _, err = DB.Exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -67,13 +72,13 @@ func InitDB() error { return err } - // 全局配置表(A表) + // ---- 全局配置表 ---- _, err = DB.Exec(` CREATE TABLE IF NOT EXISTS global_config ( id INTEGER PRIMARY KEY CHECK (id = 1), - server_addr TEXT NOT NULL DEFAULT 'frp.whitetop.xyz', - server_port INTEGER NOT NULL DEFAULT 9358, - token TEXT NOT NULL DEFAULT 'Lxh10020328', + server_addr TEXT NOT NULL DEFAULT 'frp.example.com', + server_port INTEGER NOT NULL DEFAULT 7000, + token TEXT NOT NULL DEFAULT 'CHANGE_ME', log_level TEXT NOT NULL DEFAULT 'info', log_max_days INTEGER NOT NULL DEFAULT 3, tcp_mux INTEGER NOT NULL DEFAULT 1, @@ -81,6 +86,7 @@ func InitDB() error { heartbeat_interval INTEGER NOT NULL DEFAULT 15, heartbeat_timeout INTEGER NOT NULL DEFAULT 70, pool_count INTEGER NOT NULL DEFAULT 8, + wire_protocol_v2 INTEGER NOT NULL DEFAULT 0, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `) @@ -88,23 +94,29 @@ 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 - ) VALUES (1, 'frp.example.com', 7000, 'CHANGE_ME', 'info', 3, 1, 30, 15, 70, 8) -`) + 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("✅ 全局配置初始化完成") } - // 隧道表(B表) + // ---- 隧道表 ---- _, err = DB.Exec(` CREATE TABLE IF NOT EXISTS proxies ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -122,7 +134,7 @@ func InitDB() error { return err } - // 应用配置表(JWT密钥等) + // ---- 应用配置表 ---- _, err = DB.Exec(` CREATE TABLE IF NOT EXISTS app_config ( key TEXT PRIMARY KEY, @@ -142,7 +154,96 @@ func InitDB() error { return nil } -// ===== JWT 密钥管理 ===== +// ============================================================ +// 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", +} + +func migrateGlobalConfigTable() error { + // 1. 获取当前表的列名 + currentCols, err := getCurrentColumns("global_config") + if err != nil { + return err + } + + // 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 + } + added = append(added, colName) + } + } + + if len(added) > 0 { + log.Printf("✅ 数据库迁移完成,新增字段: %v", added) + } else { + log.Println("✅ 数据库 Schema 已是最新") + } + + return nil +} + +// getCurrentColumns 查询指定表的所有列名 +func getCurrentColumns(tableName string) ([]string, error) { + rows, err := DB.Query("PRAGMA table_info(" + tableName + ")") + if err != nil { + return nil, err + } + defer rows.Close() + + var cols []string + for rows.Next() { + var ( + cid int + name string + typ string + notNull int + dfltVal sql.NullString + pk int + ) + if err := rows.Scan(&cid, &name, &typ, ¬Null, &dfltVal, &pk); err != nil { + return nil, err + } + cols = append(cols, name) + } + return cols, rows.Err() +} + +func contains(slice []string, item string) bool { + for _, s := range slice { + if strings.EqualFold(s, item) { + return true + } + } + return false +} + +// ============================================================ +// JWT 密钥管理 +// ============================================================ + func ensureJwtSecret() error { var value string err := DB.QueryRow("SELECT value FROM app_config WHERE key = 'jwt_secret'").Scan(&value) @@ -175,17 +276,22 @@ func GetJwtSecret() (string, error) { return secret, nil } -// ===== 全局配置 CRUD ===== +// ============================================================ +// 全局配置 CRUD +// ============================================================ + func GetGlobalConfig() (*GlobalConfig, error) { var cfg GlobalConfig err := DB.QueryRow(` SELECT id, server_addr, server_port, token, log_level, log_max_days, - tcp_mux, tcp_mux_keepalive, heartbeat_interval, heartbeat_timeout, pool_count + tcp_mux, tcp_mux_keepalive, heartbeat_interval, heartbeat_timeout, pool_count, + wire_protocol_v2 FROM global_config WHERE id = 1 `).Scan( &cfg.ID, &cfg.ServerAddr, &cfg.ServerPort, &cfg.Token, &cfg.LogLevel, &cfg.LogMaxDays, &cfg.TcpMux, &cfg.TcpMuxKeepalive, &cfg.HeartbeatInterval, &cfg.HeartbeatTimeout, &cfg.PoolCount, + &cfg.WireProtocolV2, ) if err != nil { return nil, err @@ -200,14 +306,19 @@ func UpdateGlobalConfig(cfg *GlobalConfig) error { server_addr = ?, server_port = ?, token = ?, log_level = ?, log_max_days = ?, tcp_mux = 1, tcp_mux_keepalive = ?, heartbeat_interval = ?, heartbeat_timeout = ?, pool_count = ?, + wire_protocol_v2 = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1 `, cfg.ServerAddr, cfg.ServerPort, cfg.Token, cfg.LogLevel, cfg.LogMaxDays, - cfg.TcpMuxKeepalive, cfg.HeartbeatInterval, cfg.HeartbeatTimeout, cfg.PoolCount) + cfg.TcpMuxKeepalive, cfg.HeartbeatInterval, cfg.HeartbeatTimeout, cfg.PoolCount, + cfg.WireProtocolV2) return err } -// ===== 隧道 CRUD ===== +// ============================================================ +// 隧道 CRUD +// ============================================================ + func GetProxies() ([]Proxy, error) { rows, err := DB.Query(` SELECT id, name, type, local_ip, local_port, remote_port, enabled @@ -273,7 +384,10 @@ func DeleteProxy(id int) error { return err } -// ===== 用户 CRUD ===== +// ============================================================ +// 用户 CRUD +// ============================================================ + func GetUserByUsername(username string) (*User, error) { var u User err := DB.QueryRow(` diff --git a/static/app.js b/static/app.js index be4e59e..a0e0c24 100644 --- a/static/app.js +++ b/static/app.js @@ -95,6 +95,7 @@ const defaultConfig = { heartbeatInterval: 15, heartbeatTimeout: 70, poolCount: 8, + wireProtocolV2: false, }; async function loadConfig() { @@ -639,6 +640,12 @@ const app = createApp({ filterProxies(proxies.value, searchKeyword.value) ); + const showDefaultTip = computed(() => { + // 如果 serverAddr 还是默认值,或者为空,显示警告 + const addr = globalConfig.serverAddr; + return !addr || addr === "frp.example.com" || addr.trim() === ""; + }); + // ---- 返回 ---- return { loggedIn, @@ -685,6 +692,8 @@ const app = createApp({ passwordChangeSuccess, changePassword, + showDefaultTip, + // 日志相关 logLines, logTotal, diff --git a/static/index.html b/static/index.html index fa0105b..b064008 100644 --- a/static/index.html +++ b/static/index.html @@ -108,11 +108,11 @@
📋 隧道列表 + @click="activeTab = 'proxies'">隧道列表 ⚙️ 全局配置信息 + @click="activeTab = 'config'">全局配置信息 📄 运行日志 + @click="activeTab = 'logs'">运行日志
@@ -162,19 +162,19 @@
-

⚙️ 全局配置信息

+

全局配置信息

管理 frpc 连接参数与传输设置

-
- ⚠️ 首次使用请修改「服务器地址」和「认证令牌」为您的真实 frps 配置 +
+ 首次使用请修改「服务器地址」和「认证令牌」为您的真实 frps 配置
- 👤 账户管理 + 账户管理 {{ loginForm.username }}
@@ -201,7 +201,6 @@
- 🌐 服务器连接
@@ -229,7 +228,6 @@
- 🔀 传输配置
@@ -261,13 +259,26 @@
+ +
+ +
+ + 即将推出 +
+ + 此功能为 frp 0.70.0 以上版本才能开启,若要打开必须搭配同版本 frps 使用,否则此功能不生效 + +
- 📋 日志配置
@@ -290,7 +301,7 @@
- +
@@ -298,15 +309,15 @@
- 📊 共 {{ logTotal }} 行 - ⚠️ {{ logError }} + 共 {{ logTotal }} 行 + {{ logError }}
- +
@@ -323,8 +334,8 @@
diff --git a/static/style-3.css b/static/style-3.css index 44cac22..92f72e4 100644 --- a/static/style-3.css +++ b/static/style-3.css @@ -1096,4 +1096,58 @@ select:disabled { .token-input-wrapper { flex-wrap: wrap; } +} + +/* ============================================================ + v2 协议开关(灰标禁用) + ============================================================ */ + +.v2-switch-row { + grid-column: 1 / -1 !important; + padding-top: 4px; + border-top: 1px solid var(--border-subtle); + margin-top: 4px; +} + +.v2-switch-wrapper { + display: flex; + align-items: center; + gap: 14px; + 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 { + font-size: 11px; + padding: 2px 12px; + background: rgba(255, 200, 50, 0.12); + border: 1px solid rgba(255, 200, 50, 0.15); + border-radius: 12px; + color: #f0c040; + letter-spacing: 0.5px; + font-weight: 500; +} + +.v2-hint { + font-size: 12px; + color: var(--text-dim); + line-height: 1.5; + padding: 2px 0 4px; + display: block; } \ No newline at end of file