diff --git a/api.go b/api.go index 8acaf59..809d438 100644 --- a/api.go +++ b/api.go @@ -4,9 +4,12 @@ import ( "bytes" "embed" "fmt" + "io" "io/fs" "net/http" + "os" "strconv" + "strings" "text/template" "github.com/gin-gonic/gin" @@ -60,6 +63,7 @@ func SetupRouter() *gin.Engine { auth.POST("/frpc/start", startFrpcHandler) auth.POST("/frpc/stop", stopFrpcHandler) auth.GET("/frpc/status", getFrpcStatusHandler) + auth.GET("/frpc/log", getFrpcLogHandler) auth.POST("/import/toml", importTomlHandler) auth.GET("/export/toml", ExportTomlHandler) @@ -491,6 +495,114 @@ func ExportTomlHandler(c *gin.Context) { c.String(http.StatusOK, buf.String()) } +// ========== 日志读取 ========== + +func getFrpcLogHandler(c *gin.Context) { + // 读取 ./frpc.log,最多返回 200 行(最新的 200 行) + lines, err := readTailLog("./frpc.log", 200) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "data": gin.H{ + "lines": []string{}, + "total": 0, + "error": err.Error(), + }, + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "data": gin.H{ + "lines": lines, + "total": len(lines), + }, + }) +} + +// readTailLog 读取文件末尾 n 行(倒序读取,返回正序) +func readTailLog(filePath string, n int) ([]string, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + // 获取文件大小 + info, err := file.Stat() + if err != nil { + return nil, err + } + fileSize := info.Size() + if fileSize == 0 { + return []string{}, nil + } + + // 从末尾开始读 + const chunkSize = 4096 + var lines []string + var leftover []byte + offset := fileSize + + for len(lines) < n && offset > 0 { + // 计算读取起点 + readSize := chunkSize + if offset < int64(chunkSize) { + readSize = int(offset) + } + offset -= int64(readSize) + + // 读取这一块 + buf := make([]byte, readSize) + _, err := file.ReadAt(buf, offset) + if err != nil && err != io.EOF { + return nil, err + } + + // 将当前块与之前剩下的拼接 + data := append(buf, leftover...) + leftover = nil + + // 按行分割 + start := 0 + for i := len(data) - 1; i >= 0; i-- { + if data[i] == '\n' { + if i+1 < len(data) { + line := string(data[i+1:]) + if line != "" { + lines = append([]string{line}, lines...) + if len(lines) >= n { + break + } + } + } + start = i + } + } + + // 如果还没凑够,把未处理的部分留到下一轮 + if len(lines) < n && start > 0 { + leftover = data[:start] + } + } + + // 处理最后剩余部分(文件开头) + if len(lines) < n && len(leftover) > 0 { + // 从 leftover 中提取行 + parts := strings.Split(string(leftover), "\n") + for i := len(parts) - 1; i >= 0; i-- { + if parts[i] != "" { + lines = append([]string{parts[i]}, lines...) + if len(lines) >= n { + break + } + } + } + } + + return lines, nil +} + func generateAndReload() error { if err := GenerateFrpcConfig(); err != nil { return err diff --git a/bin/frpc_linux_arm_hf b/bin/frpc_linux_arm_hf new file mode 100644 index 0000000..8f992e8 Binary files /dev/null and b/bin/frpc_linux_arm_hf differ diff --git a/db.go b/db.go index c1bd674..2390914 100644 --- a/db.go +++ b/db.go @@ -91,12 +91,13 @@ func InitDB() error { 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.whitetop.xyz', 9358, 'Lxh10020328', '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 + ) VALUES (1, 'frp.example.com', 7000, 'CHANGE_ME', 'info', 3, 1, 30, 15, 70, 8) +`) if err != nil { return err } diff --git a/readme.md b/readme.md index ca02821..ac88765 100644 --- a/readme.md +++ b/readme.md @@ -150,9 +150,10 @@ go build -ldflags="-s -w" -o frpc-console . |---|---|---| | Linux | AMD64/x86-64 | 0.70.0 | | Linux | ARM64/Aarch64 | 0.70.0 | -| Windows | AMD64/x86-64 | 0.69.0 | +| Linux | ARM_hf/ARMv7l | 0.70.0 | +| Windows | AMD64/x86-64 | 0.70.0 | -> 不是Windows不配高版本,是直到现在frp设计者那边0.70.0都没发Windows版本
(除非我单独下载下来make一个0.70.0的Windows,但是作者没放出来,肯定是有原因的) +> 行了行了,Windows 版和 Linux 版版本号已同步。不用再等了。 #### Q: 为什么不用 / 不推荐 Podux? diff --git a/script/build.go b/script/build.go index 41de78d..d4ee56f 100644 --- a/script/build.go +++ b/script/build.go @@ -23,7 +23,7 @@ var platforms = []Platform{ {"windows", "386", "Windows x86", ".exe"}, {"linux", "amd64", "Linux x86-64", ""}, {"linux", "arm64", "Linux ARM64", ""}, - {"linux", "armv7", "Linux ARMv7", ""}, + {"linux", "armv7", "Linux ARMv7l", ""}, } const ( diff --git a/static/app.js b/static/app.js index cf3ab0a..be4e59e 100644 --- a/static/app.js +++ b/static/app.js @@ -1,13 +1,12 @@ // app.js - 普通脚本(非模块) -const { createApp, ref, reactive, computed, onMounted } = Vue; +const { createApp, ref, reactive, computed, onMounted, nextTick, watch } = Vue; // ============================================================ // 1. 基础 API // ============================================================ const API_BASE = "/api"; - function getToken() { return localStorage.getItem("frpc_token") || ""; } @@ -86,9 +85,9 @@ async function login(username, password) { // 3. Config // ============================================================ const defaultConfig = { - serverAddr: "frp.whitetop.xyz", - serverPort: 9358, - token: "", + serverAddr: "frp.example.com", + serverPort: 7000, + token: "CHANGE_ME", logLevel: "info", logMaxDays: 3, tcpMux: true, @@ -183,7 +182,22 @@ async function getFrpcStatus() { } // ============================================================ -// 6. UI State +// 6. Logs +// ============================================================ +async function fetchLogsApi() { + try { + const data = await apiFetch("/frpc/log"); + if (data.code === 0) { + return { lines: data.data.lines || [], total: data.data.total || 0, error: data.data.error || "" }; + } + return { lines: [], total: 0, error: "加载失败" }; + } catch (e) { + return { lines: [], total: 0, error: "网络错误" }; + } +} + +// ============================================================ +// 7. UI State // ============================================================ const activeTab = ref("proxies"); const dialogVisible = ref(false); @@ -197,6 +211,7 @@ const dialogForm = ref({ remotePort: 0, }); const searchKeyword = ref(""); +const showToken = ref(false); function openAddDialog() { dialogMode.value = "add"; @@ -222,7 +237,7 @@ function closeDialog() { } // ============================================================ -// 7. Vue App +// 8. Vue App // ============================================================ const app = createApp({ setup() { @@ -251,7 +266,6 @@ const app = createApp({ const globalConfig = reactive({}); const proxies = ref([]); const frpcRunning = ref(false); - const showDetail = ref(false); // ---- 修改密码 ---- const passwordChange = reactive({ @@ -262,6 +276,15 @@ const app = createApp({ const passwordChangeError = ref(""); const passwordChangeSuccess = ref(""); + // ---- 日志 ---- + const logLines = ref([]); + const logTotal = ref(0); + const logError = ref(""); + const logLastUpdate = ref(""); + const logAutoScroll = ref(true); + let logTimer = null; + let logFetching = false; + // ---- 初始化 ---- onMounted(() => { const saved = loadAuthState(); @@ -279,6 +302,15 @@ const app = createApp({ } }); + // ---- Tab 切换监听日志轮询 ---- + watch(activeTab, (newTab) => { + if (newTab === "logs") { + startLogPolling(); + } else { + stopLogPolling(); + } + }); + async function checkUsers() { try { const res = await fetch("/api/check/users"); @@ -312,6 +344,54 @@ const app = createApp({ frpcRunning.value = running; } + // ---- 日志轮询 ---- + async function fetchLogs() { + if (logFetching) return; + logFetching = true; + try { + const result = await fetchLogsApi(); + logLines.value = result.lines; + logTotal.value = result.total; + logError.value = result.error; + logLastUpdate.value = new Date().toLocaleTimeString(); + if (logAutoScroll.value) { + await nextTick(); + const container = document.getElementById("logContainer"); + if (container) { + container.scrollTop = container.scrollHeight; + } + } + } catch (e) { + logError.value = "网络错误"; + } finally { + logFetching = false; + } + } + + function startLogPolling() { + if (logTimer) return; + fetchLogs(); + logTimer = setInterval(fetchLogs, 8000); + } + + function stopLogPolling() { + if (logTimer) { + clearInterval(logTimer); + logTimer = null; + } + } + + function refreshLogs() { + fetchLogs(); + } + + function scrollLogToBottom() { + const container = document.getElementById("logContainer"); + if (container) { + container.scrollTop = container.scrollHeight; + } + } + // ---- 登录 ---- const doLogin = async () => { if (!loginForm.username || !loginForm.password) { @@ -368,9 +448,7 @@ const app = createApp({ }); const data = await res.json(); if (data.code === 0) { - // ✅ 注册成功,不再是首次启动 isFirstRun.value = false; - const token = data.data.token; const expire = saveAuthState(token, registerForm.username); token.value = token; @@ -398,6 +476,7 @@ const app = createApp({ // ---- 退出 ---- const doLogout = () => { if (expireTimer) clearInterval(expireTimer); + stopLogPolling(); contentVisible.value = false; loggedIn.value = false; token.value = ""; @@ -407,8 +486,6 @@ const app = createApp({ loading.value = false; transitioning.value = false; clearAuthState(); - - // ✅ 重置注册相关状态 isFirstRun.value = false; registerForm.username = ""; registerForm.password = ""; @@ -504,7 +581,7 @@ const app = createApp({ } }; - // ---- 导入导出(已移入 setup,可访问 loadAllData) ---- + // ---- 导入导出 ---- const triggerImport = () => { document.getElementById("tomlFileInput").click(); }; @@ -579,7 +656,6 @@ const app = createApp({ activeTab, globalConfig, - showDetail, saveConfig, proxies, @@ -608,6 +684,18 @@ const app = createApp({ passwordChangeError, passwordChangeSuccess, changePassword, + + // 日志相关 + logLines, + logTotal, + logError, + logLastUpdate, + logAutoScroll, + refreshLogs, + scrollLogToBottom, + + // Token 显示切换 + showToken, }; }, }); diff --git a/static/index.html b/static/index.html index e3e4c84..fa0105b 100644 --- a/static/index.html +++ b/static/index.html @@ -26,7 +26,7 @@
- +