From 892fc98be28ae1d35c1698ab1499e2c93246e156 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Thu, 23 Jul 2026 21:26:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96frpc.toml=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BB=8E=E5=89=8D=E7=AB=AF=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=BA=93=E6=94=B9=E4=B8=BA=E8=87=AA=E5=BB=BA=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.go | 50 +++++ frp.go | 6 +- frpc-console.db | Bin 36864 -> 36864 bytes frpc.log | 501 ++++++++++++++++++++++++++++++++++++++++++++++ frpc.toml | 21 -- static/app.js | 228 +++++++++++---------- static/index.html | 77 +++---- 7 files changed, 705 insertions(+), 178 deletions(-) diff --git a/api.go b/api.go index 44b6cdc..2792da7 100644 --- a/api.go +++ b/api.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strconv" + "text/template" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" @@ -56,6 +57,7 @@ func SetupRouter() *gin.Engine { // 🟢 导入 TOML(新增) auth.POST("/import/toml", importTomlHandler) + auth.GET("/export/toml", ExportTomlHandler) } } @@ -339,6 +341,54 @@ func importTomlHandler(c *gin.Context) { }) } +// ExportTomlHandler 导出 TOML 配置文件 +func ExportTomlHandler(c *gin.Context) { + cfg, err := GetGlobalConfig() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败: " + err.Error()}) + return + } + + proxies, err := GetProxies() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取隧道失败: " + err.Error()}) + return + } + + var activeProxies []Proxy + for _, p := range proxies { + if p.Enabled { + activeProxies = append(activeProxies, p) + } + } + + data := struct { + *GlobalConfig + Proxies []Proxy + }{ + GlobalConfig: cfg, + Proxies: activeProxies, + } + + // 解析模板(直接使用 frp.go 中的 frpcTemplateContent) + tmpl, err := template.New("frpc").Parse(FrpcTemplateContent) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "解析模板失败: " + err.Error()}) + return + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "渲染模板失败: " + err.Error()}) + return + } + + // 返回文件下载 + c.Header("Content-Type", "text/plain; charset=utf-8") + c.Header("Content-Disposition", "attachment; filename=frpc.toml") + c.String(http.StatusOK, buf.String()) +} + // ========== 辅助函数 ========== func generateAndReload() error { if err := GenerateFrpcConfig(); err != nil { diff --git a/frp.go b/frp.go index 68fdd57..badeab8 100644 --- a/frp.go +++ b/frp.go @@ -18,7 +18,7 @@ import ( var embeddedFrpc embed.FS //go:embed frpc.tmpl -var frpcTemplateContent string +var FrpcTemplateContent string var ( cachedFrpcPath string @@ -127,11 +127,11 @@ func GenerateFrpcConfig() error { tmplContent = string(content) } else { // 读取失败时回退到 embed - tmplContent = frpcTemplateContent + tmplContent = FrpcTemplateContent } } else { // 外部文件不存在,使用 embed 中的模板 - tmplContent = frpcTemplateContent + tmplContent = FrpcTemplateContent } // ===== 核心改动结束 ===== diff --git a/frpc-console.db b/frpc-console.db index 5afffff41c97b3ad6f75fcd032c5698f70e960f5..312e52dbbaae041820e40b3e3478c67c3104fc3e 100644 GIT binary patch delta 173 zcmZozz|^pSX@WGP{6raNM){2iv;8#<4XjK}czJmkxOqw#_&4#V@{9AG;%nx!=Ka9C zj5nO;AWsQT3HLqjdz%FXc5zR>7wyBsz`)=&Suf@uh~d3iAXbf$)zHMs)NrzO+;vt% zLn{NL$>Q;^l==G@xcSQ%_@D7FcgXJRqaE RCKu$(ff$^dC*|iU004VxF`)ne delta 168 zcmZozz|^pSX@WGP#6%fqMv09Hv;8OM$FQpKGB7akao=O$-^8EFFV1(0ubI!9_XF=T z-f&(Xo`XCkJX+lMHVX>u;+}jj+K0u1my2PtUd%lfPcANo%>uD%jFS`MStnb^T?f Date.now()) { return { token, expire, username, valid: true }; } clearAuthState(); - return { token: '', expire: 0, username: '', valid: false }; + return { token: "", expire: 0, username: "", valid: false }; } function clearAuthState() { - localStorage.removeItem('frpc_token'); - localStorage.removeItem('frpc_token_expire'); - localStorage.removeItem('frpc_username'); + localStorage.removeItem("frpc_token"); + localStorage.removeItem("frpc_token_expire"); + localStorage.removeItem("frpc_username"); } async function login(username, password) { - const data = await apiFetch('/login', { - method: 'POST', + const data = await apiFetch("/login", { + method: "POST", body: JSON.stringify({ username, password }), }); if (data.code === 0) { @@ -70,15 +71,15 @@ async function login(username, password) { const expire = saveAuthState(token, username); return { success: true, token, expire }; } - return { success: false, error: data.msg || '登录失败' }; + return { success: false, error: data.msg || "登录失败" }; } // ---------- config ---------- const defaultConfig = { - serverAddr: 'frp.whitetop.xyz', + serverAddr: "frp.whitetop.xyz", serverPort: 9358, - token: '', - logLevel: 'info', + token: "", + logLevel: "info", logMaxDays: 3, tcpMux: true, tcpMuxKeepalive: 30, @@ -88,18 +89,18 @@ const defaultConfig = { }; async function loadConfig() { try { - const data = await apiFetch('/config'); + const data = await apiFetch("/config"); if (data.code === 0) { return { ...defaultConfig, ...data.data }; } } catch (e) { - console.warn('加载配置失败', e); + console.warn("加载配置失败", e); } return { ...defaultConfig }; } async function saveConfigApi(config) { - const data = await apiFetch('/config', { - method: 'PUT', + const data = await apiFetch("/config", { + method: "PUT", body: JSON.stringify(config), }); return data; @@ -108,96 +109,104 @@ async function saveConfigApi(config) { // ---------- proxies ---------- async function loadProxies() { try { - const data = await apiFetch('/proxies'); + const data = await apiFetch("/proxies"); if (data.code === 0) { return data.data || []; } } catch (e) { - console.warn('加载隧道失败', e); + console.warn("加载隧道失败", e); } return []; } async function createProxy(proxy) { - const data = await apiFetch('/proxy', { - method: 'POST', + const data = await apiFetch("/proxy", { + method: "POST", body: JSON.stringify(proxy), }); return data; } async function updateProxy(proxy) { - const data = await apiFetch('/proxy/' + proxy.id, { - method: 'PUT', + const data = await apiFetch("/proxy/" + proxy.id, { + method: "PUT", body: JSON.stringify(proxy), }); return data; } async function deleteProxy(id) { - const data = await apiFetch('/proxy/' + id, { - method: 'DELETE', + const data = await apiFetch("/proxy/" + id, { + method: "DELETE", }); return data; } function filterProxies(list, keyword) { if (!keyword) return list; const kw = keyword.toLowerCase(); - return list.filter(p => - p.name.toLowerCase().includes(kw) || - p.localIP.includes(kw) || - String(p.remotePort).includes(kw) + return list.filter( + (p) => + p.name.toLowerCase().includes(kw) || + p.localIP.includes(kw) || + String(p.remotePort).includes(kw), ); } // ---------- frpc ---------- async function getFrpcStatus() { try { - const data = await apiFetch('/frpc/status'); + const data = await apiFetch("/frpc/status"); if (data.code === 0) { return data.data.running || false; } } catch (e) { - console.warn('获取 frpc 状态失败', e); + console.warn("获取 frpc 状态失败", e); } return false; } async function reloadFrpc() { - const data = await apiFetch('/frpc/reload', { method: 'POST' }); + const data = await apiFetch("/frpc/reload", { method: "POST" }); return data; } async function startFrpc() { - const data = await apiFetch('/frpc/start', { method: 'POST' }); + const data = await apiFetch("/frpc/start", { method: "POST" }); return data; } async function stopFrpc() { - const data = await apiFetch('/frpc/stop', { method: 'POST' }); + const data = await apiFetch("/frpc/stop", { method: "POST" }); return data; } // ---------- ui ---------- // Tab 切换 -const activeTab = ref('proxies'); +const activeTab = ref("proxies"); function switchTab(tab) { activeTab.value = tab; } // 弹窗控制 const dialogVisible = ref(false); -const dialogMode = ref('add'); +const dialogMode = ref("add"); const dialogForm = ref({ id: null, - name: '', - type: 'tcp', - localIP: '', + name: "", + type: "tcp", + localIP: "", localPort: 0, remotePort: 0, }); function openAddDialog() { - dialogMode.value = 'add'; - dialogForm.value = { id: null, name: '', type: 'tcp', localIP: '', localPort: 0, remotePort: 0 }; + dialogMode.value = "add"; + dialogForm.value = { + id: null, + name: "", + type: "tcp", + localIP: "", + localPort: 0, + remotePort: 0, + }; dialogVisible.value = true; } function openEditDialog(proxy) { - dialogMode.value = 'edit'; + dialogMode.value = "edit"; dialogForm.value = { ...proxy }; dialogVisible.value = true; } @@ -205,21 +214,21 @@ function closeDialog() { dialogVisible.value = false; } -const searchKeyword = ref(''); +const searchKeyword = ref(""); // ---------- import/export 功能 ---------- async function triggerImport() { - document.getElementById('tomlFileInput').click(); + document.getElementById("tomlFileInput").click(); } async function handleImport(e) { const file = e.target.files[0]; if (!file) return; const formData = new FormData(); - formData.append('file', file); + formData.append("file", file); try { - const res = await fetch('/api/import/toml', { - method: 'POST', - headers: { 'Authorization': 'Bearer ' + getToken() }, + const res = await fetch("/api/import/toml", { + method: "POST", + headers: { Authorization: "Bearer " + getToken() }, body: formData, }); const data = await res.json(); @@ -227,55 +236,39 @@ async function handleImport(e) { alert(data.msg); await loadAllData(); } else { - alert('导入失败: ' + data.msg); + alert("导入失败: " + data.msg); } } catch (err) { - alert('网络错误: ' + err.message); + alert("网络错误: " + err.message); } - e.target.value = ''; + e.target.value = ""; } async function exportToml() { + console.log('🔍 exportToml 被调用了'); try { - const configData = await apiFetch('/config'); - if (configData.code !== 0) { - alert('获取配置失败'); + const res = await fetch('/api/export/toml', { + method: 'GET', + headers: { 'Authorization': 'Bearer ' + getToken() }, + }); + + if (!res.ok) { + const data = await res.json(); + alert('导出失败: ' + (data.msg || '未知错误')); return; } - const proxyData = await apiFetch('/proxies'); - if (proxyData.code !== 0) { - alert('获取隧道列表失败'); - return; - } - const cfg = configData.data; - const proxyList = proxyData.data || []; - const tomlObj = { - serverAddr: cfg.serverAddr, - serverPort: cfg.serverPort, - auth: { token: cfg.token }, - log: { to: './frpc.log', level: cfg.logLevel, maxDays: cfg.logMaxDays }, - transport: { - tcpMux: cfg.tcpMux, - tcpMuxKeepaliveInterval: cfg.tcpMuxKeepalive, - heartbeatInterval: cfg.heartbeatInterval, - heartbeatTimeout: cfg.heartbeatTimeout, - poolCount: cfg.poolCount, - }, - proxies: proxyList.map(p => ({ - name: p.name, - type: p.type, - localIP: p.localIP, - localPort: p.localPort, - remotePort: p.remotePort, - })), - }; - const tomlStr = TOML.stringify(tomlObj); - const blob = new Blob([tomlStr], { type: 'text/plain' }); + + const blob = await res.blob(); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'frpc.toml'; + document.body.appendChild(a); a.click(); + document.body.removeChild(a); URL.revokeObjectURL(a.href); + + console.log('✅ 导出成功'); } catch (e) { + console.error('导出失败:', e); alert('导出失败: ' + e.message); } } @@ -289,9 +282,9 @@ const app = createApp({ // ===== 登录态 ===== const loggedIn = ref(false); const loading = ref(false); - const loginError = ref(''); - const loginForm = reactive({ username: '', password: '' }); - const token = ref(''); + const loginError = ref(""); + const loginForm = reactive({ username: "", password: "" }); + const token = ref(""); const tokenExpireTime = ref(0); const transitioning = ref(false); const contentVisible = ref(false); @@ -323,7 +316,7 @@ const app = createApp({ expireTimer = setInterval(() => { if (Date.now() >= tokenExpireTime.value) { doLogout(); - alert('登录已超时,请重新登录'); + alert("登录已超时,请重新登录"); } }, 5000); }; @@ -342,11 +335,11 @@ const app = createApp({ // ===== 登录/退出 ===== const doLogin = async () => { if (!loginForm.username || !loginForm.password) { - loginError.value = '请输入用户名和密码'; + loginError.value = "请输入用户名和密码"; return; } loading.value = true; - loginError.value = ''; + loginError.value = ""; const result = await login(loginForm.username, loginForm.password); if (result.success) { token.value = result.token; @@ -372,10 +365,10 @@ const app = createApp({ if (expireTimer) clearInterval(expireTimer); contentVisible.value = false; loggedIn.value = false; - token.value = ''; + token.value = ""; tokenExpireTime.value = 0; - loginForm.username = ''; - loginForm.password = ''; + loginForm.username = ""; + loginForm.password = ""; loading.value = false; transitioning.value = false; clearAuthState(); @@ -386,9 +379,9 @@ const app = createApp({ const result = await saveConfigApi(globalConfig); if (result.code === 0) { frpcRunning.value = await getFrpcStatus(); - alert('配置已更新并热加载'); + alert("配置已更新并热加载"); } else { - alert('保存失败: ' + result.msg); + alert("保存失败: " + result.msg); } }; @@ -396,7 +389,7 @@ const app = createApp({ const toggleProxy = async (p) => { const result = await updateProxy(p); if (result.code !== 0) { - alert('更新失败: ' + result.msg); + alert("更新失败: " + result.msg); proxies.value = await loadProxies(); } else { frpcRunning.value = await getFrpcStatus(); @@ -404,37 +397,40 @@ const app = createApp({ }; const removeProxy = async (id) => { - console.log('🔍 removeProxy 被调用, id:', id); // ← 加这行 - if (!confirm('确定要删除这条隧道吗?')) return; + console.log("🔍 removeProxy 被调用, id:", id); // ← 加这行 + if (!confirm("确定要删除这条隧道吗?")) return; const result = await deleteProxy(id); if (result.code === 0) { proxies.value = await loadProxies(); frpcRunning.value = await getFrpcStatus(); } else { - alert('删除失败: ' + result.msg); + alert("删除失败: " + result.msg); } }; const confirmDialog = async () => { const form = dialogForm.value; if (!form.name || !form.localIP || !form.localPort || !form.remotePort) { - alert('请填写完整信息'); + alert("请填写完整信息"); return; } - const result = dialogMode.value === 'add' - ? await createProxy(form) - : await updateProxy(form); + const result = + dialogMode.value === "add" + ? await createProxy(form) + : await updateProxy(form); if (result.code === 0) { closeDialog(); proxies.value = await loadProxies(); frpcRunning.value = await getFrpcStatus(); } else { - alert('操作失败: ' + result.msg); + alert("操作失败: " + result.msg); } }; // ===== 计算属性 ===== - const filteredProxies = computed(() => filterProxies(proxies.value, searchKeyword.value)); + const filteredProxies = computed(() => + filterProxies(proxies.value, searchKeyword.value), + ); return { loggedIn, @@ -470,4 +466,4 @@ const app = createApp({ }, }); -app.mount('#app'); \ No newline at end of file +app.mount("#app"); diff --git a/static/index.html b/static/index.html index 5dae20f..27708f7 100644 --- a/static/index.html +++ b/static/index.html @@ -10,8 +10,8 @@ - - + + @@ -188,7 +188,8 @@ - @@ -211,44 +212,44 @@ - -
-
-

{{ dialogMode === 'add' ? '新增隧道' : '编辑隧道' }}

-
-
- - + +
+
+

{{ dialogMode === 'add' ? '新增隧道' : '编辑隧道' }}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
-
- - -
-
- - -
-
- - -
-
- - +
+ +
-
- - -
-