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 @@
-
+