diff --git a/api.go b/api.go index 809d438..2f746f4 100644 --- a/api.go +++ b/api.go @@ -6,11 +6,13 @@ import ( "fmt" "io" "io/fs" + "net" "net/http" "os" "strconv" "strings" "text/template" + "time" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" @@ -64,7 +66,7 @@ func SetupRouter() *gin.Engine { auth.POST("/frpc/stop", stopFrpcHandler) auth.GET("/frpc/status", getFrpcStatusHandler) auth.GET("/frpc/log", getFrpcLogHandler) - + auth.GET("/ping", pingHandler) auth.POST("/import/toml", importTomlHandler) auth.GET("/export/toml", ExportTomlHandler) @@ -143,6 +145,36 @@ func registerHandler(c *gin.Context) { }) } +// Ping 延迟检测 +func pingHandler(c *gin.Context) { + target := c.Query("target") + if target == "" { + c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "缺少 target 参数"}) + return + } + + // 获取全局配置(获取服务端端口) + cfg, err := GetGlobalConfig() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败"}) + return + } + + port := cfg.ServerPort + address := net.JoinHostPort(target, strconv.Itoa(port)) + + start := time.Now() + conn, err := net.DialTimeout("tcp", address, 5*time.Second) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "msg": "ping 失败", "latency": -1}) + return + } + conn.Close() + + latency := time.Since(start).Milliseconds() + c.JSON(http.StatusOK, gin.H{"code": 0, "latency": latency}) +} + func loginHandler(c *gin.Context) { var req struct { Username string `json:"username"` diff --git a/readme.md b/readme.md index 20c3a2f..97122b9 100644 --- a/readme.md +++ b/readme.md @@ -142,7 +142,14 @@ go build -ldflags="-s -w" -o frpc-console . #### Q: frpc 启动失败怎么办? -检查 frpc.toml 配置是否正确,或查看 ./frpc.log 日志文件。 +如果是首次启动,console 会自动生成一份符合官方规范的 `frps.toml` 配置文件,通常不需要额外操作。 + +如果在使用过程中遇到启动失败,可以按以下步骤排查: + +1. **检查配置是否正确** —— 在「全局配置」页面重新配置一次服务端参数,或导入已有的 `frpc.toml` 配置文件,console 会自动应用并尝试重启 +2. **查看日志定位问题** —— 若上述操作后仍然失败,请查看 `./frpc.log` 日志文件,定位具体报错原因 + +> 日志文件的位置:与 `frpc-console` 二进制同级目录下的 `frpc.log`。Docker 部署时,可通过 `docker logs frpc-console` 查看容器输出。 #### Q: 支持哪些 frp 版本? @@ -289,7 +296,7 @@ frpc-console 遵循 **“够用就好”** 的原则: ## 📄 许可证 -MIT License © 2026 Gitea:lxh2875931338/Github:XHLiang0 +MIT License © 2026 lxh2875931338(XHLiang0) --- diff --git a/static/app.js b/static/app.js index 7d3082d..d64c224 100644 --- a/static/app.js +++ b/static/app.js @@ -171,7 +171,7 @@ function filterProxies(list, keyword) { (p) => p.name.toLowerCase().includes(kw) || p.localIP.includes(kw) || - String(p.remotePort).includes(kw) + String(p.remotePort).includes(kw), ); } @@ -197,7 +197,11 @@ 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: data.data.lines || [], + total: data.data.total || 0, + error: data.data.error || "", + }; } return { lines: [], total: 0, error: "加载失败" }; } catch (e) { @@ -294,6 +298,80 @@ const app = createApp({ let logTimer = null; let logFetching = false; + // ---- Ping 延迟检测(移到 setup 内部) ---- + const pingLatency = ref(null); + let pingTimer = null; + const PING_INTERVAL_MS = 30000; + const PING_TIMEOUT_MS = 5000; + + const pingStatusClass = computed(() => { + if (pingLatency.value === null) return "ping-fail"; + if (pingLatency.value < 1000) return "ping-good"; + if (pingLatency.value < 5000) return "ping-slow"; + return "ping-fail"; + }); + + const pingIcon = computed(() => { + if (pingLatency.value === null) { + // 未接入/失败 —— 断开图标 + return ``; + } + if (pingLatency.value < 1000) { + // 延迟好 —— 实心信号图标(青色/绿色) + return ``; + } + // 延迟一般(1000-5000ms)—— 空心信号图标(黄色/橙色) + return ``; + }); + + async function doPing() { + // 如果 frpc 未运行,直接显示 --ms + if (!frpcRunning.value) { + pingLatency.value = null; + return; + } + const addr = globalConfig.serverAddr || "frp.example.com"; + const start = performance.now(); + try { + const res = await fetch( + `/api/ping?target=${encodeURIComponent(addr)}`, + { + signal: AbortSignal.timeout(PING_TIMEOUT_MS), + }, + ); + if (!res.ok) throw new Error("Ping failed"); + const end = performance.now(); + pingLatency.value = Math.round(end - start); + } catch { + pingLatency.value = null; + } + } + + function startPingPolling() { + if (pingTimer) return; + doPing(); + pingTimer = setInterval(doPing, PING_INTERVAL_MS); + } + + function stopPingPolling() { + if (pingTimer) { + clearInterval(pingTimer); + pingTimer = null; + } + } + // ---- 认证过期处理 ---- const handleAuthExpired = () => { doLogout(); @@ -301,7 +379,6 @@ const app = createApp({ // ---- 初始化 ---- onMounted(() => { - // 监听认证过期事件 window.addEventListener("auth:expired", handleAuthExpired); const saved = loadAuthState(); @@ -328,6 +405,15 @@ const app = createApp({ } }); + watch(frpcRunning, (running) => { + if (!running) { + stopPingPolling(); + pingLatency.value = null; // 显示 --ms + } else if (loggedIn.value) { + startPingPolling(); + } + }); + async function checkUsers() { try { const res = await fetch("/api/check/users"); @@ -367,7 +453,6 @@ const app = createApp({ logFetching = true; try { const result = await fetchLogsApi(); - // ---- 如果 API 返回 401,停止轮询(事件已触发登出) ---- if (result.code === 401) { logFetching = false; return; @@ -499,6 +584,7 @@ const app = createApp({ const doLogout = () => { if (expireTimer) clearInterval(expireTimer); stopLogPolling(); + stopPingPolling(); contentVisible.value = false; loggedIn.value = false; token.value = ""; @@ -658,7 +744,7 @@ const app = createApp({ // ---- 计算属性 ---- const filteredProxies = computed(() => - filterProxies(proxies.value, searchKeyword.value) + filterProxies(proxies.value, searchKeyword.value), ); const showDefaultTip = computed(() => { @@ -723,8 +809,13 @@ const app = createApp({ scrollLogToBottom, showToken, + + // ---- 新增 Ping 相关导出 ---- + pingLatency, + pingStatusClass, + pingIcon, }; }, }); -app.mount("#app"); \ No newline at end of file +app.mount("#app"); diff --git a/static/index.html b/static/index.html index bef17dc..5ced711 100644 --- a/static/index.html +++ b/static/index.html @@ -97,11 +97,15 @@ {{ frpcRunning ? 'frpc 运行中' : 'frpc 已停止' }} + + + + {{ pingLatency !== null ? pingLatency + 'ms' : '--ms' }} +
管理 frpc 连接参数与传输设置
{{ passwordChangeError }}
-{{ passwordChangeSuccess }}
-修改当前账户的登录密码
+