🍓 Sync 2026-09-15 02:13:06

This commit is contained in:
github-actions[bot] committed 2026-09-15 02:13:06 +08:00
1 parent 0e5393594c
commit 1dd96ac2bb
146 files changed
+26541 -508

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-frpc-next
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_LICENSE:=MIT
PKG_MAINTAINER:=Dante <dante@seckv.com>
LUCI_TITLE:=LuCI support for frp client (v0.68.x/v0.69.x+)
LUCI_DEPENDS:=+frpc-next +curl
LUCI_PKGARCH:=all
include $(TOPDIR)/feeds/luci/luci.mk
define Package/$(PKG_NAME)
$(call Package/luci/webtemplate)
TITLE:=$(LUCI_TITLE)
DEPENDS:=$(LUCI_DEPENDS)
PROVIDES:=luci-app-frpc
CONFLICTS:=luci-app-frpc
endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
chmod 0755 /etc/init.d/frpc /etc/hotplug.d/iface/99-frpc 2>/dev/null || true
}
exit 0
endef
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,44 @@
'use strict';
'require view';
'require rpc';
'require poll';
const callFrpcLog = rpc.declare({
object: 'luci.frpc',
method: 'get_log'
});
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
render: function() {
const area = E('textarea', {
id: 'frpc_log_content',
style: 'width:100%; height:500px; background:#f4f4f4; color:#333; padding:10px; border:1px solid #ccc; border-radius:3px; font-family:monospace; font-size:12px; overflow-y:auto; white-space:pre-wrap; word-break:break-all; margin-top:10px;',
readonly: 'readonly'
}, _('Loading logs...'));
poll.add(() => this.updateLogs(), 5);
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('Frp Client Title') + ' - ' + _('Log')),
E('div', { 'class': 'cbi-map-descr' }, _('Real-time background system logs of the active frpc process, helping you diagnose server handshake and protocol errors.')),
E('fieldset', { 'class': 'cbi-section' }, [
E('legend', {}, _('Logs')),
E('div', { 'class': 'cbi-section-node' }, area)
])
]);
},
updateLogs: function() {
return callFrpcLog().then(function(res) {
const area = document.getElementById('frpc_log_content');
if (area) {
area.value = res && res.log ? res.log : _('No logs available...');
area.scrollTop = area.scrollHeight;
}
});
}
});
@@ -0,0 +1,294 @@
'use strict';
'require view';
'require rpc';
'require poll';
'require ui';
const callFrpcStatus = rpc.declare({
object: 'luci.frpc',
method: 'get_status'
});
const callAdoptProcess = rpc.declare({
object: 'luci.frpc',
method: 'adopt_process'
});
const callServiceAction = rpc.declare({
object: 'luci.frpc',
method: 'service_action',
params: [ 'action' ]
});
const callRuntimeInfo = rpc.declare({
object: 'luci.frpc',
method: 'get_runtime_info'
});
function renderState(status) {
let text = _('Unknown');
let color = '#999';
if (status.state === 'managed') {
text = _('Running');
color = 'green';
} else if (status.state === 'unmanaged') {
text = _('Running (Unmanaged)');
color = 'orange';
} else if (status.state === 'stopped') {
text = _('Stopped');
color = 'red';
}
if (status.pid)
text += ' [PID: ' + status.pid + ']';
return { text, color };
}
function flattenProxies(statusObj) {
const rows = [];
if (!statusObj || typeof statusObj !== 'object')
return rows;
Object.keys(statusObj).forEach((ptype) => {
const arr = statusObj[ptype];
if (!Array.isArray(arr))
return;
arr.forEach((p) => {
if (!p || typeof p !== 'object')
return;
rows.push({
name: p.name || '-',
type: p.type || ptype || '-',
status: p.status || '-',
local: p.local_addr || '-',
remote: p.remote_addr || '-',
err: p.err || ''
});
});
});
return rows;
}
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return Promise.all([ callFrpcStatus(), callRuntimeInfo() ]);
},
render: function(data) {
const status = data && data[0] ? data[0] : {};
const runtime = data && data[1] ? data[1] : {};
const stateInfo = renderState(status || {});
const container = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('Frp Client Title') + ' - ' + _('Overview')),
E('div', { 'class': 'cbi-map-descr' }, _('Overview of the local frpc service state, active process PID, loaded config files, and detailed online proxy information.'))
]);
// Service Status Section (Aligned with lucky style)
const statusSection = E('fieldset', { 'class': 'cbi-section' }, [
E('legend', {}, _('Service Status')),
E('div', { 'class': 'cbi-section-node' }, [
E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, _('Current Status')),
E('td', { 'class': 'td left' }, [
E('span', { id: 'frpc-status-text', 'style': 'font-weight:bold; color:' + stateInfo.color + '; margin-right:10px;' }, stateInfo.text),
E('button', {
id: 'frpc-restart-btn',
'class': 'btn cbi-button cbi-button-apply',
'style': 'display:' + ((status.state === 'managed' || status.state === 'unmanaged') ? 'inline-block' : 'none') + ';',
click: ui.createHandlerFn(this, function(ev) {
if (!ev || !ev.currentTarget) return;
const btn = ev.currentTarget;
btn.disabled = true;
ui.addNotification(null, E('p', _('Restarting FRP Client, please wait...')), 'info');
return callServiceAction('restart').then(function(res) {
btn.disabled = false;
if (res && res.success) {
ui.addNotification(null, E('p', _('Service restarted successfully.')), 'info');
} else {
ui.addNotification(null, E('p', _('Failed to restart service.')), 'error');
}
}).catch(function(err) {
btn.disabled = false;
ui.addNotification(null, E('p', _('An error occurred: ') + err.message), 'error');
});
})
}, _('Restart'))
])
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, _('Generated Runtime Config')),
E('td', { 'class': 'td left', id: 'frpc-runtime-config' }, status.runtime_config || '-')
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, _('Configured Proxies')),
E('td', { 'class': 'td left', id: 'frpc-proxy-count' }, String(status.proxy_count || 0))
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, _('frpc Version')),
E('td', { 'class': 'td left', id: 'frpc-version' }, status.version || '-')
])
])
])
]);
container.appendChild(statusSection);
const proxyRows = flattenProxies(runtime.status);
const runtimeSection = E('fieldset', {
'class': 'cbi-section',
id: 'frpc-runtime-section',
style: (status.state === 'managed') ? '' : 'display:none'
}, [
E('legend', {}, _('Runtime Details')),
E('div', { 'class': 'cbi-section-node' }, [
E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, _('Web Panel')),
E('td', { 'class': 'td left' }, [
E('span', { id: 'frpc-web-api' }, [
runtime.web_url ? E('a', {
'class': 'btn cbi-button',
href: runtime.web_url,
target: '_blank',
rel: 'noopener noreferrer'
}, _('Open')) : '-'
])
])
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, _('Connected Server')),
E('td', { 'class': 'td left' }, [
E('span', { id: 'frpc-connected-server' }, (runtime.server_addr && runtime.server_port) ? (runtime.server_addr + ':' + runtime.server_port) : '-'),
E('div', { 'class': 'cbi-value-description', style: 'margin: 4px 0 0 0;' }, _('Connect to FRP server'))
])
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left' }, _('Proxy Runtime List')),
E('td', { 'class': 'td left' }, [
E('div', { 'class': 'cbi-value-description', style: 'margin: 0 0 6px 0;' }, _('Running proxy list')),
E('div', {
id: 'frpc-proxy-list-wrap',
style: 'max-height:220px; overflow:auto;'
}, proxyRows.length > 0 ? E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Name')),
E('th', { 'class': 'th' }, _('Type')),
E('th', { 'class': 'th' }, _('Status')),
E('th', { 'class': 'th' }, _('Local')),
E('th', { 'class': 'th' }, _('Remote')),
E('th', { 'class': 'th' }, _('Error'))
]),
...proxyRows.map((row) => E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, row.name),
E('td', { 'class': 'td' }, row.type),
E('td', { 'class': 'td' }, row.status),
E('td', { 'class': 'td' }, row.local),
E('td', { 'class': 'td' }, row.remote),
E('td', { 'class': 'td' }, row.err || '-')
]))
]) : E('div', {}, _('No running proxies')))
])
])
])
])
]);
container.appendChild(runtimeSection);
poll.add(() => Promise.all([callFrpcStatus(), callRuntimeInfo()]).then((res) => {
const s = res[0] || {};
const r = res[1] || {};
const next = renderState(s);
const statusNode = document.getElementById('frpc-status-text');
if (statusNode) {
statusNode.textContent = next.text;
statusNode.style.color = next.color;
}
const restartBtnNode = document.getElementById('frpc-restart-btn');
if (restartBtnNode) {
if (s.state === 'managed' || s.state === 'unmanaged') {
restartBtnNode.style.display = 'inline-block';
} else {
restartBtnNode.style.display = 'none';
}
}
const runtimeConfigNode = document.getElementById('frpc-runtime-config');
if (runtimeConfigNode)
runtimeConfigNode.textContent = s.runtime_config || '-';
const proxyCountNode = document.getElementById('frpc-proxy-count');
if (proxyCountNode)
proxyCountNode.textContent = String(s.proxy_count || 0);
const versionNode = document.getElementById('frpc-version');
if (versionNode)
versionNode.textContent = s.version || '-';
const runtimeSectionNode = document.getElementById('frpc-runtime-section');
if (runtimeSectionNode)
runtimeSectionNode.style.display = (s.state === 'managed') ? '' : 'none';
const webNode = document.getElementById('frpc-web-api');
if (webNode) {
if (r.web_url) {
webNode.innerHTML = '';
webNode.appendChild(E('a', {
'class': 'btn cbi-button',
href: r.web_url,
target: '_blank',
rel: 'noopener noreferrer'
}, _('Open')));
} else {
webNode.textContent = '-';
}
}
const serverNode = document.getElementById('frpc-connected-server');
if (serverNode)
serverNode.textContent = (r.server_addr && r.server_port) ? (r.server_addr + ':' + r.server_port) : '-';
const listWrap = document.getElementById('frpc-proxy-list-wrap');
if (listWrap) {
const rows = flattenProxies(r.status);
listWrap.innerHTML = '';
if (rows.length === 0) {
listWrap.appendChild(E('div', {}, _('No running proxies')));
} else {
const table = E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Name')),
E('th', { 'class': 'th' }, _('Type')),
E('th', { 'class': 'th' }, _('Status')),
E('th', { 'class': 'th' }, _('Local')),
E('th', { 'class': 'th' }, _('Remote')),
E('th', { 'class': 'th' }, _('Error'))
])
]);
rows.forEach((row) => {
table.appendChild(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, row.name),
E('td', { 'class': 'td' }, row.type),
E('td', { 'class': 'td' }, row.status),
E('td', { 'class': 'td' }, row.local),
E('td', { 'class': 'td' }, row.remote),
E('td', { 'class': 'td' }, row.err || '-')
]));
});
listWrap.appendChild(table);
}
}
}), 5);
return container;
}
});
@@ -0,0 +1,276 @@
'use strict';
'require view';
'require rpc';
'require ui';
const callGetConfig = rpc.declare({
object: 'luci.frpc',
method: 'get_config'
});
const callSaveConfig = rpc.declare({
object: 'luci.frpc',
method: 'save_config',
params: [ 'config' ]
});
const callValidateConfig = rpc.declare({
object: 'luci.frpc',
method: 'validate_config',
params: [ 'config' ]
});
const callStatus = rpc.declare({
object: 'luci.frpc',
method: 'get_status'
});
const callSetEnabled = rpc.declare({
object: 'luci.frpc',
method: 'set_enabled',
params: [ 'enabled' ]
});
const callServiceAction = rpc.declare({
object: 'luci.frpc',
method: 'service_action',
params: [ 'action' ]
});
function parseErrorPosition(errText) {
const s = String(errText || '');
let m = s.match(/line\s+(\d+)\s*[,;:]?\s*(?:col|column)\s+(\d+)/i);
if (m)
return { line: +m[1], col: +m[2] };
m = s.match(/at\s+line\s+(\d+)\s*[,;:]?\s*column\s+(\d+)/i);
if (m)
return { line: +m[1], col: +m[2] };
m = s.match(/line\s+(\d+)/i);
if (m)
return { line: +m[1], col: null };
return null;
}
return view.extend({
_validateTimer: null,
_errorLine: null,
injectStyle: function() {
if (document.getElementById('frpc_editor_style'))
return;
const css = [
'.frpc-editor-wrap{border:1px solid #ccc;border-radius:4px;overflow:hidden;background:#fff;max-width:900px;box-sizing:border-box;}',
'.frpc-editor-main{display:flex;min-height:400px;max-height:65vh;}',
'.frpc-editor-gutter{width:48px;background:#f7f7f7;color:#888;border-right:1px solid #e5e5e5;overflow:hidden;font:13px/1.5 monospace;padding:12px 6px;text-align:right;user-select:none;box-sizing:border-box;}',
'.frpc-editor-gutter-line{height:1.5em;line-height:1.5;}',
'.frpc-editor-gutter-line.err{background:#ffe8e8;color:#b00020;font-weight:700;}',
'.frpc-editor-input-wrap{flex:1;background:#fff;position:relative;}',
'.frpc-editor-input{width:100%;min-height:100%;box-sizing:border-box;border:0;outline:none;resize:vertical;overflow-wrap:anywhere;white-space:pre;word-break:keep-all;font:13px/1.5 monospace;padding:12px;margin:0;display:block;}'
].join('\n');
document.head.appendChild(E('style', { id: 'frpc_editor_style' }, css));
},
refreshEditor: function() {
const ta = document.getElementById('frpc_config');
const gutter = document.getElementById('frpc_editor_gutter');
if (!ta || !gutter)
return;
const text = ta.value || '';
const lineCount = Math.max(1, text.split('\n').length);
gutter.innerHTML = '';
for (let i = 1; i <= lineCount; i++) {
gutter.appendChild(E('div', {
'class': 'frpc-editor-gutter-line' + (this._errorLine === i ? ' err' : '')
}, String(i)));
}
},
renderEditor: function(configText) {
const gutter = E('div', { id: 'frpc_editor_gutter', 'class': 'frpc-editor-gutter' });
const ta = E('textarea', {
id: 'frpc_config',
class: 'frpc-editor-input',
spellcheck: 'false',
wrap: 'off'
}, configText || '');
ta.addEventListener('keydown', function(ev) {
if (ev.keyCode === 9) { // Tab key
ev.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
const val = this.value;
this.value = val.substring(0, start) + ' ' + val.substring(end);
this.selectionStart = this.selectionEnd = start + 2;
}
});
ta.addEventListener('input', L.bind(function() {
this.refreshEditor();
this.scheduleValidate();
}, this));
ta.addEventListener('scroll', function() {
gutter.scrollTop = ta.scrollTop;
});
window.setTimeout(L.bind(function() { this.refreshEditor(); }, this), 0);
const inputWrap = E('div', { 'class': 'frpc-editor-input-wrap' }, [ ta ]);
return E('div', { 'class': 'frpc-editor-wrap' }, [
E('div', { 'class': 'frpc-editor-main' }, [ gutter, inputWrap ])
]);
},
setValidateHint: function(ok, message, pos) {
const hint = document.getElementById('frpc_validate_hint');
if (!hint)
return;
hint.style.color = ok ? 'green' : 'red';
hint.textContent = message;
this._errorLine = (!ok && pos && pos.line) ? pos.line : null;
this.refreshEditor();
},
runValidate: function() {
const content = document.getElementById('frpc_config')?.value || '';
if (!content.trim()) {
this.setValidateHint(true, _('Configuration is empty.'));
return Promise.resolve();
}
return callValidateConfig(content).then(L.bind(function(res) {
if (res && res.success) {
this.setValidateHint(true, _('Configuration is valid.'));
return;
}
const err = (res && res.error) ? String(res.error).trim() : _('Unknown error');
const pos = parseErrorPosition(err);
let msg = _('Verification failed: ') + err;
if (pos && pos.line)
msg += ' (' + _('Line') + ' ' + pos.line + (pos.col ? ', ' + _('Column') + ' ' + pos.col : '') + ')';
this.setValidateHint(false, msg, pos);
}, this));
},
scheduleValidate: function() {
if (this._validateTimer)
window.clearTimeout(this._validateTimer);
this._validateTimer = window.setTimeout(L.bind(function() {
this.runValidate().catch(function() {});
}, this), 700);
},
load: function() {
return Promise.all([ callGetConfig(), callStatus() ]);
},
render: function(data) {
this.injectStyle();
const cfg = data && data[0] ? data[0] : {};
const st = data && data[1] ? data[1] : {};
const configText = cfg.config || '';
const enabled = (st.enabled === 1 || st.enabled === true);
window.setTimeout(L.bind(function() {
this.runValidate().catch(function() {});
}, this), 0);
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('Frp Client Title') + ' - ' + _('Settings')),
E('div', { 'class': 'cbi-map-descr' }, _('Configure the Frp client parameters. Complete the server link details and local port forwarding rules to build an intranet penetration channel.')),
E('div', { 'class': 'cbi-section' }, [
E('h3', { 'class': 'cbi-section-title' }, _('Service Control')),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Enabled')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
id: 'frpc_enabled',
type: 'checkbox',
checked: enabled ? 'checked' : null
})
])
])
]),
E('div', { 'class': 'cbi-section' }, [
E('h3', { 'class': 'cbi-section-title' }, _('Configuration Editor')),
this.renderEditor(configText)
]),
E('div', { 'class': 'cbi-section frpc-editor-buttons' }, [
E('button', {
'class': 'btn cbi-button cbi-button-apply',
click: ui.createHandlerFn(this, function() {
return this.runValidate();
})
}, _('Verify Configuration')),
E('span', { id: 'frpc_validate_hint', style: 'margin-left:12px;' }, '')
])
]);
},
validateAndSave: function() {
const configText = document.getElementById('frpc_config')?.value || '';
const enabledCheckbox = document.getElementById('frpc_enabled');
const on = enabledCheckbox && enabledCheckbox.checked ? 1 : 0;
ui.showModal(null, [ E('p', { 'class': 'spinning' }, [ _('Applying changes...') ]) ]);
return callValidateConfig(configText).then(L.bind(function(res) {
if (!res || !res.success) {
ui.hideModal();
const err = (res && res.error) ? String(res.error).trim() : _('Unknown error');
this.setValidateHint(false, _('Verification failed: ') + err, parseErrorPosition(err));
ui.addNotification(null, E('pre', { style: 'white-space:pre-wrap; margin:0;' }, _('frpc config verify failed:\n') + err), 'danger');
throw new Error('frpc verify failed');
}
this.setValidateHint(true, _('Configuration is valid.'));
return callSetEnabled(on).then(L.bind(function(setRes) {
if (!setRes || !setRes.success) {
ui.hideModal();
ui.addNotification(null, E('p', _('Failed to update service enable state.')), 'danger');
throw new Error('frpc set enabled failed');
}
return callSaveConfig(configText).then(L.bind(function(saveRes) {
ui.hideModal();
if (saveRes && saveRes.success) {
if (on === 0) {
return callServiceAction('stop');
} else {
if (saveRes.applied === false) {
ui.addNotification(null, E('p', _('Configuration saved, but service is not running or reload failed. Please force restart service.')), 'warning');
}
}
return;
}
ui.addNotification(null, E('p', _('Configuration save failed: ') + (saveRes && saveRes.error ? saveRes.error : _('Unknown error'))), 'danger');
throw new Error('frpc config save failed');
}, this));
}, this));
}, this)).catch(function(err) {
ui.hideModal();
throw err;
});
},
handleSave: function() {
return this.validateAndSave();
},
handleSaveApply: function() {
return this.validateAndSave();
}
});
+1
View File
@@ -0,0 +1 @@
zh_Hans
+342
View File
@@ -0,0 +1,342 @@
msgid ""
msgstr ""
"Project-Id-Version: luci-app-frpc\n"
"PO-Revision-Date: 2026-05-19 12:00+0800\n"
"Last-Translator: Dante <dante@seckv.com>\n"
"Language-Team: Chinese (Simplified)\n"
"Language: zh_Hans\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Transfer-Encoding: 8bit\n"
msgid "Service Status"
msgstr "服务运行状态"
msgid "Current Status"
msgstr "当前状态"
msgid "Adopting process, please wait..."
msgstr "正在接管进程,请稍候..."
msgid "Kill and Adopt Rogue Process"
msgstr "杀死并接管孤儿进程"
msgid "Proxy Health"
msgstr "穿透隧道健康度"
msgid "Configuration View (/var/etc/frpc.toml)"
msgstr "运行配置查看 (/var/etc/frpc.toml)"
msgid "Overview"
msgstr "概览"
msgid "Unknown"
msgstr "未知"
msgid "Running"
msgstr "运行中"
msgid "Running (Unmanaged)"
msgstr "运行中 (非托管)"
msgid "Stopped"
msgstr "已停止"
msgid "Name"
msgstr "名称"
msgid "Type"
msgstr "类型"
msgid "Status"
msgstr "状态"
msgid "Today Traffic (In/Out)"
msgstr "今日流量 (入/出)"
msgid "Error Message"
msgstr "报错信息"
msgid "No data (Service stopped or API not configured)"
msgstr "暂无数据 (服务未启动或 API 未配置)"
msgid "No proxies configured"
msgstr "没有配置任何隧道"
msgid "Frp Client"
msgstr "Frp客户端"
msgid "Open frp Console"
msgstr "打开 frp 控制台"
msgid "Global Settings"
msgstr "全局配置"
msgid "Basic Settings"
msgstr "基本设置"
msgid "Admin API (Required for Hot Reload)"
msgstr "管理 API (热重载必须)"
msgid "Recommended 0.0.0.0 for LAN access, 127.0.0.1 for local only."
msgstr "推荐设置 0.0.0.0 以便从 LAN 访问,127.0.0.1 仅限本地访问"
msgid "Log Settings"
msgstr "日志设置"
msgid "Enable"
msgstr "启用"
msgid "Server Address"
msgstr "服务器地址"
msgid "Server Port"
msgstr "服务器端口"
msgid "Default 7000"
msgstr "默认 7000"
msgid "Token"
msgstr "令牌"
msgid "Transport Protocol"
msgstr "传输协议"
msgid "Admin Address"
msgstr "管理地址"
msgid "Recommended 127.0.0.1"
msgstr "推荐 127.0.0.1"
msgid "Admin Port"
msgstr "管理端口"
msgid "Admin User"
msgstr "管理用户名"
msgid "Admin Password"
msgstr "管理密码"
msgid "Log File"
msgstr "日志路径"
msgid "Leave empty to use system log"
msgstr "留空则输出到系统日志"
msgid "Log Level"
msgstr "日志级别"
msgid "Max Days"
msgstr "保留天数"
msgid "Proxies"
msgstr "穿透列表"
msgid "Local IP"
msgstr "本地 IP"
msgid "Local Port"
msgstr "本地端口"
msgid "Remote Port"
msgstr "远程端口"
msgid "Custom Domains"
msgstr "自定义域名"
msgid "Use Encryption"
msgstr "开启加密"
msgid "Use Compression"
msgstr "开启压缩"
msgid "No logs available..."
msgstr "暂无日志..."
msgid "Real-time Logs"
msgstr "实时日志"
msgid "Log Viewer"
msgstr "日志查看"
msgid "Frp Client Overview"
msgstr "Frp客户端概览"
msgid "Overview of frpc runtime state, generated config, and proxy count."
msgstr "展示 frpc 运行状态、生成配置路径与代理数量。"
msgid "Generated Runtime Config"
msgstr "运行配置"
msgid "Configured Proxies"
msgstr "代理数量"
msgid "Frp Client Settings"
msgstr "Frp客户端设置"
msgid "Configuration saved successfully."
msgstr "配置保存成功。"
msgid "Configuration save failed: "
msgstr "配置保存失败:"
msgid "Unknown error"
msgstr "未知错误"
msgid "Log"
msgstr "日志"
msgid "Verify Configuration"
msgstr "校验客户端配置"
msgid "Configuration is empty."
msgstr "配置为空。"
msgid "Configuration is valid."
msgstr "配置校验通过。"
msgid "Verification failed: "
msgstr "配置校验失败:"
msgid "Service Status"
msgstr "服务状态"
msgid "Logs"
msgstr "日志"
msgid "Frp Client overview of runtime state, generated config, and proxy count."
msgstr "Frp Client 运行状态概览,包含生成配置路径和代理数量。"
msgid "Single TOML editor with syntax highlight, line numbers and error position hints."
msgstr "单一 TOML 编辑器,支持语法高亮、行号和错误位置提示。"
msgid "Line"
msgstr "行"
msgid "Column"
msgstr "列"
msgid "frpc Version"
msgstr "frpc 版本"
msgid "frpc config verify failed:\n"
msgstr "frpc 配置校验失败:\n"
msgid "Frp Client Log"
msgstr "Frp客户端日志"
msgid "Service Control"
msgstr "服务控制"
msgid "Enabled"
msgstr "启用"
msgid "Actions"
msgstr "操作"
msgid "Start"
msgstr "启动"
msgid "Stop"
msgstr "停止"
msgid "Restart"
msgstr "重启"
msgid "Service enable state updated."
msgstr "服务启用状态已更新。"
msgid "Failed to update service enable state."
msgstr "更新服务启用状态失败。"
msgid "Configuration Editor"
msgstr "配置修改"
msgid "Runtime Details"
msgstr "运行详情"
msgid "Web API"
msgstr "Web API"
msgid "Connected Server"
msgstr "连接服务器"
msgid "Web API Status JSON"
msgstr "Web API 状态 JSON"
msgid "Proxy Runtime List"
msgstr "代理运行列表"
msgid "Local"
msgstr "本地地址"
msgid "Remote"
msgstr "远程地址"
msgid "Error"
msgstr "错误"
msgid "No running proxies"
msgstr "暂无运行中的代理"
msgid "Connect to FRP server"
msgstr "连接FRP服务端"
msgid "Running proxy list"
msgstr "运行的代理列表"
msgid "Web Panel"
msgstr "Web面板"
msgid "Loading logs..."
msgstr "日志加载中..."
msgid "Showing the last 100 lines of frpc service logs."
msgstr "显示 frpc 服务最近 100 行日志。"
msgid "Open"
msgstr "打开"
msgid "Failed to apply service state."
msgstr "应用服务状态失败。"
msgid "Restarting service, please wait..."
msgstr "正在重启服务,请稍候..."
msgid "Restart and Take Over"
msgstr "重启并接管"
msgid "Restart Service"
msgstr "重启服务"
msgid "Force Restart"
msgstr "强制重启"
msgid "Force restarting service, please wait..."
msgstr "正在强制重启服务,请稍候..."
msgid "Hide Preview"
msgstr "关闭预览"
msgid "Configuration saved and applied successfully."
msgstr "配置已保存并生效。"
msgid "Configuration saved, but apply failed. Please force restart service."
msgstr "配置已保存,但生效失败,请强制重启服务。"
msgid "Configuration saved, but service is not running or reload failed. Please force restart service."
msgstr "配置已保存,但服务未运行或重载失败,请强制重启服务。"
msgid "Overview of the local frpc service state, active process PID, loaded config files, and detailed online proxy information."
msgstr "展示 frpc 客户端本地服务运行状态、活动进程 PID、当前加载的配置文件以及在线穿透隧道的实时统计与健康度参数。"
msgid "Configure the Frp client parameters. Complete the server link details and local port forwarding rules to build an intranet penetration channel."
msgstr "配置 Frp 客户端的核心参数。支持一键式服务状态启用控制,并提供具备语法高亮、实时语法校验、行号提示的专业级 TOML 配置文件编辑器。"
msgid "Real-time background system logs of the active frpc process, helping you diagnose server handshake and protocol errors."
msgstr "实时查询当前活跃 frpc 进程的后台系统日志,方便您快速诊断与 frp 服务端的握手协议及穿透连接异常。"
msgid "Frp Client Title"
msgstr "Frp 客户端"
+2
View File
@@ -0,0 +1,2 @@
config frpc 'main'
option enabled '0'
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
[ "$ACTION" = "ifup" ] || exit 0
[ "$INTERFACE" = "loopback" ] && exit 0
# Skip if frpc is not enabled
[ "$(uci -q get frpc.main.enabled)" = "1" ] || exit 0
# Skip if frpc process is already running
if pidof /usr/bin/frpc >/dev/null 2>&1 || pidof frpc >/dev/null 2>&1; then
exit 0
fi
LOCK=/tmp/frpc_hotplug.lock
# Debounce: avoid duplicate concurrent starts within 5 seconds
if [ -f "$LOCK" ]; then
ts=$(cat "$LOCK" 2>/dev/null)
[ -n "$ts" ] && [ $(( $(date +%s) - ts )) -lt 5 ] && exit 0
fi
date +%s > "$LOCK"
logger -t frpc-hotplug "${INTERFACE:-$DEVICE} ifup: frpc is enabled but not running, starting service"
/etc/init.d/frpc start >/dev/null 2>&1 &
+50
View File
@@ -0,0 +1,50 @@
#!/bin/sh /etc/rc.common
START=90
STOP=10
USE_PROCD=1
PROG=/usr/bin/frpc
CONF_FILE=/etc/config/frpc.toml
is_enabled() {
local enabled
config_load frpc
config_get_bool enabled "main" "enabled" 0
[ "$enabled" -eq 1 ]
}
start_service() {
is_enabled || return 0
if [ ! -s "$CONF_FILE" ]; then
logger -t frpc "Config file missing or empty: $CONF_FILE"
return 1
fi
procd_open_instance
procd_set_param command "$PROG" -c "$CONF_FILE"
procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
reload_service() {
if ! is_enabled; then
stop
return 0
fi
if [ ! -s "$CONF_FILE" ]; then
logger -t frpc "Config file missing or empty: $CONF_FILE"
stop
return 1
fi
restart
}
service_triggers() {
procd_add_reload_trigger "frpc"
}
@@ -0,0 +1,37 @@
{
"admin/services/frpc": {
"title": "Frp Client",
"order": 90,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-frpc" ],
"uci": { "frpc": true }
}
},
"admin/services/frpc/overview": {
"title": "Overview",
"order": 10,
"action": {
"type": "view",
"path": "frpc/overview"
}
},
"admin/services/frpc/settings": {
"title": "Settings",
"order": 20,
"action": {
"type": "view",
"path": "frpc/settings"
}
},
"admin/services/frpc/log": {
"title": "Log",
"order": 30,
"action": {
"type": "view",
"path": "frpc/log"
}
}
}
@@ -0,0 +1,37 @@
{
"luci-app-frpc": {
"description": "Grant access to frpc RPC methods",
"read": {
"ubus": {
"luci.frpc": [
"get_status",
"get_runtime_info",
"get_log",
"get_config"
]
},
"uci": [
"frpc"
]
},
"write": {
"ubus": {
"luci.frpc": [
"adopt_process",
"validate_config",
"set_enabled",
"service_action",
"save_config"
]
},
"uci": [
"frpc"
],
"file": {
"/etc/config/frpc.toml": [ "read", "write" ],
"/var/etc/frpc.toml": [ "read" ],
"/var/log/frpc.log": [ "read" ]
}
}
}
}
@@ -0,0 +1,283 @@
'use strict';
import { connect } from 'ubus';
import * as fs from 'fs';
const PROCD_SERVICE_NAME = 'frpc';
const CONFIG_FILE = '/etc/config/frpc.toml';
const RUNTIME_CONFIG_FILE = '/etc/config/frpc.toml';
function get_procd_status() {
let u = connect();
if (!u)
return { running: false };
let res = u.call('service', 'list', { name: PROCD_SERVICE_NAME });
if (res && res[PROCD_SERVICE_NAME] && res[PROCD_SERVICE_NAME].instances) {
let inst = res[PROCD_SERVICE_NAME].instances.instance1;
if (inst && inst.running)
return { running: true, pid: inst.pid };
}
return { running: false };
}
function get_rogue_pid(managed_pid) {
let p = fs.popen('pgrep -x frpc', 'r');
if (!p)
return null;
let pid_str = p.read('all');
p.close();
if (!pid_str)
return null;
let pids = pid_str.trim().split('\n');
for (let i = 0; i < length(pids); i++) {
let pid = pids[i];
if (pid != '' && (managed_pid == null || pid != (managed_pid + '')))
return pid;
}
return null;
}
function count_proxies(config) {
if (!config)
return 0;
let parts = split(config, '[[proxies]]');
return (parts && length(parts) > 0) ? (length(parts) - 1) : 0;
}
function verify_config(config) {
fs.writefile('/tmp/frpc_test.toml', config || '');
let p = fs.popen("/usr/bin/frpc verify -c /tmp/frpc_test.toml 2>&1", 'r');
if (!p) {
fs.unlink('/tmp/frpc_test.toml');
return { success: false, error: 'failed to validate config' };
}
let output = p.read('all') || '';
let code = p.close();
fs.unlink('/tmp/frpc_test.toml');
if (code !== 0)
return { success: false, error: output || 'verify failed' };
return { success: true };
}
function get_frpc_version() {
system("/bin/sh -c '/usr/bin/frpc --version >/tmp/frpc_version.out 2>&1 || /usr/bin/frpc -v >/tmp/frpc_version.out 2>&1'");
let out = fs.readfile('/tmp/frpc_version.out') || '';
fs.unlink('/tmp/frpc_version.out');
if (!out)
return '';
let line = split(out, '\n')[0] || '';
if (!line)
return '';
line = replace(line, /^[ \t\r\n]+/, '');
line = replace(line, /[ \t\r\n]+$/, '');
return line;
}
function get_enabled() {
let p = fs.popen("/bin/sh -c \"uci -q get frpc.main.enabled 2>/dev/null || echo 0\"", 'r');
if (!p)
return 0;
let out = p.read('all') || '0';
p.close();
out = replace(out, /^[ \t\r\n]+/, '');
out = replace(out, /[ \t\r\n]+$/, '');
return (out === '1') ? 1 : 0;
}
function toml_get(key, defv) {
let p = fs.popen("/bin/sh -c \"grep -F '" + key + "' " + CONFIG_FILE + " 2>/dev/null | head -n1 | cut -d= -f2-\"", 'r');
if (!p)
return defv;
let out = p.read('all') || '';
p.close();
out = replace(out, /^[ \t\r\n]+/, '');
out = replace(out, /[ \t\r\n]+$/, '');
out = replace(out, /^\"/, '');
out = replace(out, /\"$/, '');
return out ? out : defv;
}
function get_runtime_info() {
let status = get_procd_status();
if (!status.running)
return { running: false };
let addr = toml_get('webServer.addr', '127.0.0.1');
let port = toml_get('webServer.port', '7400');
let user = toml_get('webServer.user', '');
let pass = toml_get('webServer.password', '');
let auth = (user && pass) ? ("-u '" + user + ":" + pass + "' ") : '';
system("/bin/sh -c \"curl -s " + auth + "http://" + addr + ":" + port + "/api/status >/tmp/frpc_web_status.json 2>/tmp/frpc_web_err\"");
system("/bin/sh -c \"curl -s " + auth + "http://" + addr + ":" + port + "/api/config >/tmp/frpc_web_config.txt 2>>/tmp/frpc_web_err\"");
let raw_status = fs.readfile('/tmp/frpc_web_status.json') || '';
let raw_config = fs.readfile('/tmp/frpc_web_config.txt') || '';
let raw_err = fs.readfile('/tmp/frpc_web_err') || '';
fs.unlink('/tmp/frpc_web_status.json');
fs.unlink('/tmp/frpc_web_config.txt');
fs.unlink('/tmp/frpc_web_err');
let status_obj = {};
if (raw_status) {
try { status_obj = json(raw_status); } catch (e) {}
}
let server_addr = '';
let server_port = '';
if (raw_config) {
let m1 = match(raw_config, /serverAddr[ \t]*=[ \t]*\"([^\"]+)\"/);
let m2 = match(raw_config, /serverPort[ \t]*=[ \t]*([0-9]+)/);
if (m1 && length(m1) > 1) server_addr = m1[1];
if (m2 && length(m2) > 1) server_port = m2[1];
}
let lan_ip = '';
let u = connect();
if (u) {
let lan_st = u.call('network.interface.lan', 'status', {});
if (lan_st && lan_st['ipv4-address'] && length(lan_st['ipv4-address']) > 0)
lan_ip = lan_st['ipv4-address'][0].address || '';
}
let web_host = addr;
if (addr == '0.0.0.0' || addr == '127.0.0.1' || addr == '::')
web_host = lan_ip || '127.0.0.1';
let web_url = 'http://' + web_host + ':' + port;
return {
running: true,
web_addr: addr,
web_port: int(port),
web_url: web_url,
server_addr: server_addr,
server_port: server_port ? int(server_port) : 0,
raw_status: raw_status,
status: status_obj,
error: raw_err
};
}
const methods = {
get_status: {
call: function() {
let procd_st = get_procd_status();
let rogue_pid = get_rogue_pid(procd_st.pid);
let state = 'stopped';
if (procd_st.running)
state = 'managed';
else if (rogue_pid)
state = 'unmanaged';
let config = fs.readfile(CONFIG_FILE) || '';
return {
state: state,
pid: procd_st.pid || rogue_pid,
config_exists: !!fs.stat(CONFIG_FILE),
runtime_config: RUNTIME_CONFIG_FILE,
proxy_count: count_proxies(config),
version: get_frpc_version(),
enabled: get_enabled()
};
}
},
adopt_process: {
call: function() {
fs.popen('killall -9 frpc', 'r').close();
let rc = system('/etc/init.d/frpc start >/dev/null 2>&1');
return { success: (rc === 0) };
}
},
get_log: {
call: function() {
let p = fs.popen('logread -e frpc | tail -n 100', 'r');
if (!p)
return { log: '' };
let log = p.read('all');
p.close();
return { log: log || '' };
}
},
get_runtime_info: {
call: function() {
return get_runtime_info();
}
},
get_config: {
call: function() {
if (!fs.stat(CONFIG_FILE))
return { config: '' };
return { config: fs.readfile(CONFIG_FILE) || '' };
}
},
validate_config: {
args: { config: '' },
call: function(req) {
let config = req.args?.config ?? '';
return verify_config(config);
}
},
set_enabled: {
args: { enabled: 0 },
call: function(req) {
let enabled = (int(req.args?.enabled ?? 0) === 1) ? '1' : '0';
let cmd = "/bin/sh -c \"uci -q set frpc.main.enabled=" + enabled + " && uci -q commit frpc\"";
let rc = system(cmd);
return { success: (rc === 0), enabled: int(enabled) };
}
},
service_action: {
args: { action: '' },
call: function(req) {
let action = req.args?.action ?? '';
if (action != 'start' && action != 'stop' && action != 'restart' && action != 'reload')
return { success: false, error: 'invalid action' };
let rc = system('/etc/init.d/frpc ' + action + ' >/dev/null 2>&1');
return { success: (rc === 0), applied: (rc === 0) };
}
},
save_config: {
args: { config: '' },
call: function(req) {
let config = req.args?.config ?? '';
let vr = verify_config(config);
if (!vr.success)
return vr;
if (!fs.writefile(CONFIG_FILE, config))
return { success: false, error: 'unable to write config file (/etc/config/frpc.toml)' };
let rc = system('/etc/init.d/frpc reload >/dev/null 2>&1');
return {
success: true,
saved: true,
applied: (rc === 0)
};
}
}
};
return { 'luci.frpc': methods };