🍓 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

@@ -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();
}
});