diff --git a/db.go b/db.go index d6042db..4c78ba4 100644 --- a/db.go +++ b/db.go @@ -238,6 +238,12 @@ func GetProxies() ([]Proxy, error) { } proxies = append(proxies, p) } + + // 🟢 关键:检查遍历过程中是否有错误 + if err = rows.Err(); err != nil { + return nil, err + } + return proxies, nil } diff --git a/frpc-console.db b/frpc-console.db index ec717b8..3f16673 100644 Binary files a/frpc-console.db and b/frpc-console.db differ diff --git a/frpc.log b/frpc.log index d792ca2..25c9bd9 100644 --- a/frpc.log +++ b/frpc.log @@ -816,3 +816,9 @@ 2026-07-23 20:45:31.375 [W] [client/control.go:163] [926f05bb8cbbd6b8] [navidrome] start error: proxy [navidrome] already exists 2026-07-23 20:45:31.375 [W] [client/control.go:163] [926f05bb8cbbd6b8] [napcat-2] start error: proxy [napcat-2] already exists 2026-07-23 20:45:31.375 [W] [client/control.go:163] [926f05bb8cbbd6b8] [podux] start error: proxy [podux] already exists +2026-07-23 20:48:41.021 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration +2026-07-23 20:48:41.031 [I] [client/service.go:308] try to connect to server... +2026-07-23 20:48:41.209 [I] [client/service.go:328] [ddf463180e9c67e7] login to server success, get run id [ddf463180e9c67e7] +2026-07-23 20:56:09.600 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration +2026-07-23 20:56:09.609 [I] [client/service.go:308] try to connect to server... +2026-07-23 20:56:09.784 [I] [client/service.go:328] [41d8767c9cc81642] login to server success, get run id [41d8767c9cc81642] diff --git a/frpc.toml b/frpc.toml index 1d7c4a4..e2dfb58 100644 --- a/frpc.toml +++ b/frpc.toml @@ -19,94 +19,3 @@ poolCount = 8 [webServer] addr = "127.0.0.1:7400" - -[[proxies]] -name = "sun-panel" -type = "tcp" -localIP = "192.168.3.16" -localPort = 3002 -remotePort = 30005 - -[[proxies]] -name = "1panel-1" -type = "tcp" -localIP = "192.168.3.16" -localPort = 10086 -remotePort = 20003 - -[[proxies]] -name = "navidrome" -type = "tcp" -localIP = "192.168.3.16" -localPort = 4533 -remotePort = 20022 - -[[proxies]] -name = "openlist" -type = "tcp" -localIP = "192.168.3.16" -localPort = 5244 -remotePort = 20005 - -[[proxies]] -name = "SSH" -type = "tcp" -localIP = "192.168.3.16" -localPort = 22 -remotePort = 20001 - -[[proxies]] -name = "SMB" -type = "tcp" -localIP = "192.168.3.16" -localPort = 445 -remotePort = 20008 - -[[proxies]] -name = "napcat" -type = "tcp" -localIP = "192.168.3.16" -localPort = 6099 -remotePort = 20007 - -[[proxies]] -name = "Gitea" -type = "tcp" -localIP = "192.168.3.16" -localPort = 3800 -remotePort = 20017 - -[[proxies]] -name = "immich-APP" -type = "tcp" -localIP = "192.168.3.16" -localPort = 2283 -remotePort = 20020 - -[[proxies]] -name = "Halo" -type = "tcp" -localIP = "192.168.3.16" -localPort = 65535 -remotePort = 65535 - -[[proxies]] -name = "napcat-2" -type = "tcp" -localIP = "192.168.3.16" -localPort = 6100 -remotePort = 20012 - -[[proxies]] -name = "podux" -type = "tcp" -localIP = "192.168.3.16" -localPort = 8090 -remotePort = 20010 - -[[proxies]] -name = "Grafana" -type = "tcp" -localIP = "192.168.3.16" -localPort = 3030 -remotePort = 20014 diff --git a/static/app.js b/static/app.js index b3b81f3..012380e 100644 --- a/static/app.js +++ b/static/app.js @@ -1,19 +1,288 @@ -// app.js -import { createApp, ref, reactive, computed, onMounted } from 'vue'; +// app.js - 普通脚本(非模块) -// 导入各模块 -import { apiFetch } from './modules/api.js'; -import { login, loadAuthState, clearAuthState, saveAuthState } from './modules/auth.js'; -import { loadConfig, saveConfig as saveConfigApi } from './modules/config.js'; -import { loadProxies, createProxy, updateProxy, deleteProxy, filterProxies } from './modules/proxies.js'; -import { getFrpcStatus, reloadFrpc, startFrpc, stopFrpc } from './modules/frpc.js'; -import { - activeTab, switchTab, - dialogVisible, dialogMode, dialogForm, - openAddDialog, openEditDialog, closeDialog, - searchKeyword, -} from './modules/ui.js'; -import { importToml, exportToml } from './modules/import.js'; // 如果有的话 +const { createApp, ref, reactive, computed, onMounted } = Vue; + +// ============================================================ +// 1. 把 modules/*.js 的内容直接合并进来(因为是普通脚本,不能 import) +// ============================================================ + +// ---------- api ---------- +const API_BASE = '/api'; +function getToken() { + return localStorage.getItem('frpc_token') || ''; +} +function getHeaders() { + return { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + getToken(), + }; +} +async function apiFetch(endpoint, options = {}) { + const url = API_BASE + endpoint; + const headers = options.body instanceof FormData + ? { 'Authorization': 'Bearer ' + getToken() } + : getHeaders(); + const res = await fetch(url, { + ...options, + headers: { ...headers, ...(options.headers || {}) }, + }); + return res.json(); +} + +// ---------- auth ---------- +function parseJwtExpire(token) { + try { + const payload = JSON.parse(atob(token.split('.')[1])); + return payload.exp * 1000; + } catch { + return Date.now() + 7 * 24 * 60 * 60 * 1000; + } +} +function saveAuthState(token, username) { + const expire = parseJwtExpire(token); + localStorage.setItem('frpc_token', token); + localStorage.setItem('frpc_token_expire', String(expire)); + localStorage.setItem('frpc_username', username); + return expire; +} +function loadAuthState() { + const token = localStorage.getItem('frpc_token'); + const expire = parseInt(localStorage.getItem('frpc_token_expire') || '0', 10); + const username = localStorage.getItem('frpc_username') || ''; + if (token && expire > Date.now()) { + return { token, expire, username, valid: true }; + } + clearAuthState(); + return { token: '', expire: 0, username: '', valid: false }; +} +function clearAuthState() { + 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', + body: JSON.stringify({ username, password }), + }); + if (data.code === 0) { + const token = data.data.token; + const expire = saveAuthState(token, username); + return { success: true, token, expire }; + } + return { success: false, error: data.msg || '登录失败' }; +} + +// ---------- config ---------- +const defaultConfig = { + serverAddr: 'frp.whitetop.xyz', + serverPort: 9358, + token: '', + logLevel: 'info', + logMaxDays: 3, + tcpMux: true, + tcpMuxKeepalive: 30, + heartbeatInterval: 15, + heartbeatTimeout: 70, + poolCount: 8, +}; +async function loadConfig() { + try { + const data = await apiFetch('/config'); + if (data.code === 0) { + return { ...defaultConfig, ...data.data }; + } + } catch (e) { + console.warn('加载配置失败', e); + } + return { ...defaultConfig }; +} +async function saveConfigApi(config) { + const data = await apiFetch('/config', { + method: 'PUT', + body: JSON.stringify(config), + }); + return data; +} + +// ---------- proxies ---------- +async function loadProxies() { + try { + const data = await apiFetch('/proxies'); + if (data.code === 0) { + return data.data || []; + } + } catch (e) { + console.warn('加载隧道失败', e); + } + return []; +} +async function createProxy(proxy) { + 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', + body: JSON.stringify(proxy), + }); + return data; +} +async function deleteProxy(id) { + 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) + ); +} + +// ---------- frpc ---------- +async function getFrpcStatus() { + try { + const data = await apiFetch('/frpc/status'); + if (data.code === 0) { + return data.data.running || false; + } + } catch (e) { + console.warn('获取 frpc 状态失败', e); + } + return false; +} +async function reloadFrpc() { + const data = await apiFetch('/frpc/reload', { method: 'POST' }); + return data; +} +async function startFrpc() { + const data = await apiFetch('/frpc/start', { method: 'POST' }); + return data; +} +async function stopFrpc() { + const data = await apiFetch('/frpc/stop', { method: 'POST' }); + return data; +} + +// ---------- ui ---------- +// Tab 切换 +const activeTab = ref('proxies'); +function switchTab(tab) { + activeTab.value = tab; +} + +// 弹窗控制 +const dialogVisible = ref(false); +const dialogMode = ref('add'); +const dialogForm = ref({ + id: null, + 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 }; + dialogVisible.value = true; +} +function openEditDialog(proxy) { + dialogMode.value = 'edit'; + dialogForm.value = { ...proxy }; + dialogVisible.value = true; +} +function closeDialog() { + dialogVisible.value = false; +} + +const searchKeyword = ref(''); + +// ---------- import/export 功能 ---------- +async function triggerImport() { + 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); + try { + const res = await fetch('/api/import/toml', { + method: 'POST', + headers: { 'Authorization': 'Bearer ' + getToken() }, + body: formData, + }); + const data = await res.json(); + if (data.code === 0) { + alert(data.msg); + await loadAllData(); + } else { + alert('导入失败: ' + data.msg); + } + } catch (err) { + alert('网络错误: ' + err.message); + } + e.target.value = ''; +} +async function exportToml() { + try { + const configData = await apiFetch('/config'); + if (configData.code !== 0) { + alert('获取配置失败'); + 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 a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'frpc.toml'; + a.click(); + URL.revokeObjectURL(a.href); + } catch (e) { + alert('导出失败: ' + e.message); + } +} + +// ============================================================ +// 2. Vue 应用 +// ============================================================ const app = createApp({ setup() { @@ -32,6 +301,7 @@ const app = createApp({ const globalConfig = reactive({}); const proxies = ref([]); const frpcRunning = ref(false); + const showDetail = ref(false); // ===== 初始化 ===== onMounted(() => { @@ -175,6 +445,7 @@ const app = createApp({ activeTab, switchTab, globalConfig, + showDetail, saveConfig, proxies, filteredProxies, @@ -191,6 +462,9 @@ const app = createApp({ searchKeyword, transitioning, contentVisible, + triggerImport, + handleImport, + exportToml, }; }, }); diff --git a/static/index.html b/static/index.html index 3b8fe78..442dfd1 100644 --- a/static/index.html +++ b/static/index.html @@ -255,7 +255,7 @@ - + \ No newline at end of file diff --git a/static/modules/api.js b/static/modules/api.js deleted file mode 100644 index 5728c73..0000000 --- a/static/modules/api.js +++ /dev/null @@ -1,26 +0,0 @@ -// modules/api.js -const API_BASE = '/api'; - -export function getToken() { - return localStorage.getItem('frpc_token') || ''; -} - -export function getHeaders() { - return { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + getToken(), - }; -} - -export async function apiFetch(endpoint, options = {}) { - const url = API_BASE + endpoint; - const headers = options.body instanceof FormData - ? { 'Authorization': 'Bearer ' + getToken() } - : getHeaders(); - - const res = await fetch(url, { - ...options, - headers: { ...headers, ...(options.headers || {}) }, - }); - return res.json(); -} \ No newline at end of file diff --git a/static/modules/auth.js b/static/modules/auth.js deleted file mode 100644 index 75f0e2e..0000000 --- a/static/modules/auth.js +++ /dev/null @@ -1,49 +0,0 @@ -// modules/auth.js -import { apiFetch } from './api.js'; - -export function parseJwtExpire(token) { - try { - const payload = JSON.parse(atob(token.split('.')[1])); - return payload.exp * 1000; - } catch { - return Date.now() + 7 * 24 * 60 * 60 * 1000; - } -} - -export function saveAuthState(token, username) { - const expire = parseJwtExpire(token); - localStorage.setItem('frpc_token', token); - localStorage.setItem('frpc_token_expire', String(expire)); - localStorage.setItem('frpc_username', username); - return expire; -} - -export function loadAuthState() { - const token = localStorage.getItem('frpc_token'); - const expire = parseInt(localStorage.getItem('frpc_token_expire') || '0', 10); - const username = localStorage.getItem('frpc_username') || ''; - if (token && expire > Date.now()) { - return { token, expire, username, valid: true }; - } - clearAuthState(); - return { token: '', expire: 0, username: '', valid: false }; -} - -export function clearAuthState() { - localStorage.removeItem('frpc_token'); - localStorage.removeItem('frpc_token_expire'); - localStorage.removeItem('frpc_username'); -} - -export async function login(username, password) { - const data = await apiFetch('/login', { - method: 'POST', - body: JSON.stringify({ username, password }), - }); - if (data.code === 0) { - const token = data.data.token; - const expire = saveAuthState(token, username); - return { success: true, token, expire }; - } - return { success: false, error: data.msg || '登录失败' }; -} \ No newline at end of file diff --git a/static/modules/config.js b/static/modules/config.js deleted file mode 100644 index b8074f6..0000000 --- a/static/modules/config.js +++ /dev/null @@ -1,35 +0,0 @@ -// modules/config.js -import { apiFetch } from './api.js'; - -const defaultConfig = { - serverAddr: 'frp.whitetop.xyz', - serverPort: 9358, - token: '', - logLevel: 'info', - logMaxDays: 3, - tcpMux: true, - tcpMuxKeepalive: 30, - heartbeatInterval: 15, - heartbeatTimeout: 70, - poolCount: 8, -}; - -export async function loadConfig() { - try { - const data = await apiFetch('/config'); - if (data.code === 0) { - return { ...defaultConfig, ...data.data }; - } - } catch (e) { - console.warn('加载配置失败', e); - } - return { ...defaultConfig }; -} - -export async function saveConfig(config) { - const data = await apiFetch('/config', { - method: 'PUT', - body: JSON.stringify(config), - }); - return data; -} \ No newline at end of file diff --git a/static/modules/frpc.js b/static/modules/frpc.js deleted file mode 100644 index 792709b..0000000 --- a/static/modules/frpc.js +++ /dev/null @@ -1,29 +0,0 @@ -// modules/frpc.js -import { apiFetch } from './api.js'; - -export async function getFrpcStatus() { - try { - const data = await apiFetch('/frpc/status'); - if (data.code === 0) { - return data.data.running || false; - } - } catch (e) { - console.warn('获取 frpc 状态失败', e); - } - return false; -} - -export async function reloadFrpc() { - const data = await apiFetch('/frpc/reload', { method: 'POST' }); - return data; -} - -export async function startFrpc() { - const data = await apiFetch('/frpc/start', { method: 'POST' }); - return data; -} - -export async function stopFrpc() { - const data = await apiFetch('/frpc/stop', { method: 'POST' }); - return data; -} \ No newline at end of file diff --git a/static/modules/proxies.js b/static/modules/proxies.js deleted file mode 100644 index e902bd1..0000000 --- a/static/modules/proxies.js +++ /dev/null @@ -1,47 +0,0 @@ -// modules/proxies.js -import { apiFetch } from './api.js'; - -export async function loadProxies() { - try { - const data = await apiFetch('/proxies'); - if (data.code === 0) { - return data.data || []; - } - } catch (e) { - console.warn('加载隧道失败', e); - } - return []; -} - -export async function createProxy(proxy) { - const data = await apiFetch('/proxy', { - method: 'POST', - body: JSON.stringify(proxy), - }); - return data; -} - -export async function updateProxy(proxy) { - const data = await apiFetch('/proxy/' + proxy.id, { - method: 'PUT', - body: JSON.stringify(proxy), - }); - return data; -} - -export async function deleteProxy(id) { - const data = await apiFetch('/proxy/' + id, { - method: 'DELETE', - }); - return data; -} - -export 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) - ); -} \ No newline at end of file diff --git a/static/modules/ui.js b/static/modules/ui.js deleted file mode 100644 index 88d0540..0000000 --- a/static/modules/ui.js +++ /dev/null @@ -1,39 +0,0 @@ -// modules/ui.js -import { ref } from 'vue'; - -// Tab 切换 -export const activeTab = ref('proxies'); -export function switchTab(tab) { - activeTab.value = tab; -} - -// 弹窗控制 -export const dialogVisible = ref(false); -export const dialogMode = ref('add'); -export const dialogForm = ref({ - id: null, - name: '', - type: 'tcp', - localIP: '', - localPort: 0, - remotePort: 0, -}); - -export function openAddDialog() { - dialogMode.value = 'add'; - dialogForm.value = { id: null, name: '', type: 'tcp', localIP: '', localPort: 0, remotePort: 0 }; - dialogVisible.value = true; -} - -export function openEditDialog(proxy) { - dialogMode.value = 'edit'; - dialogForm.value = { ...proxy }; - dialogVisible.value = true; -} - -export function closeDialog() { - dialogVisible.value = false; -} - -// 搜索 -export const searchKeyword = ref(''); \ No newline at end of file