🏅 Sync 2026-10-10 12:03:15

This commit is contained in:
github-actions[bot] committed 2026-10-10 12:03:15 +08:00
1 parent ad68a3cd65
commit f080de4b89
109 files changed
+72311 -621

No files matched your search

@@ -143,6 +143,7 @@ const callOverview = rpc.declare({ object: 'luci.clashoo', method: 'ove
const callSmartFlushCache = rpc.declare({ object: 'luci.clashoo', method: 'smart_flush_cache', expect: {} });
const callListSingboxProfiles = rpc.declare({ object: 'luci.clashoo', method: 'list_singbox_profiles', expect: {} });
const callGetSingboxProfile = rpc.declare({ object: 'luci.clashoo', method: 'get_singbox_profile', params: ['name'], expect: {} });
const callGetSingboxProfileChunk = rpc.declare({ object: 'luci.clashoo', method: 'get_singbox_profile_chunk', params: ['name', 'index'], expect: {} });
const callSaveSingboxProfileChunk = rpc.declare({ object: 'luci.clashoo', method: 'save_singbox_profile_chunk', params: ['name', 'content', 'index', 'total'], expect: {} });
const callSetSingboxProfile = rpc.declare({ object: 'luci.clashoo', method: 'set_singbox_profile', params: ['name'], expect: {} });
const callDeleteSingboxProfile = rpc.declare({ object: 'luci.clashoo', method: 'delete_singbox_profile', params: ['name'], expect: {} });
@@ -234,14 +235,66 @@ return baseclass.extend({
smartFlushCache: function () { return L.resolveDefault(callSmartFlushCache(), { success: false }); },
listSingboxProfiles: function () { return L.resolveDefault(callListSingboxProfiles(), { profiles: [], active: '' }); },
getSingboxProfile: function (name) { return L.resolveDefault(callGetSingboxProfile(name), {}); },
getSingboxProfile: function (name) {
return L.resolveDefault(callGetSingboxProfile(name), {}).then(function (r) {
if (!r || r.error !== 'too large') return r || {};
var decoder = new TextDecoder('utf-8', { fatal: true });
var parts = [], total = 0, size = 0, mtime = '', received = 0;
function readNext(index) {
return L.resolveDefault(callGetSingboxProfileChunk(name, String(index)), {}).then(function (chunk) {
if (!chunk || chunk.error)
return { error: (chunk && chunk.error) || 'read_failed' };
if (!Number.isInteger(chunk.index) || !Number.isInteger(chunk.total) ||
!Number.isInteger(chunk.size) || chunk.index !== index ||
chunk.total < 1 || chunk.total > 683 || chunk.size < 1 ||
chunk.size > 16 * 1024 * 1024 || typeof chunk.content_b64 !== 'string')
return { error: 'invalid_chunk' };
if (index === 0) {
total = chunk.total;
size = chunk.size;
mtime = chunk.mtime;
} else if (chunk.total !== total || chunk.size !== size || chunk.mtime !== mtime) {
return { error: 'file_changed' };
}
try {
var raw = atob(chunk.content_b64);
var bytes = new Uint8Array(raw.length);
for (var i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
received += bytes.length;
parts.push(decoder.decode(bytes, { stream: true }));
} catch (e) {
return { error: 'invalid_chunk' };
}
if (index + 1 < total) return readNext(index + 1);
if (received !== size) return { error: 'file_changed' };
try { parts.push(decoder.decode()); }
catch (e) { return { error: 'invalid_chunk' }; }
return { content: parts.join(''), name: name };
});
}
return readNext(0);
});
},
saveSingboxProfile: function (name, content) {
var chunkSize = 24576;
var total = Math.max(1, Math.ceil((content || '').length / chunkSize));
var value = content || '';
var chunks = [];
for (var offset = 0; offset < value.length;) {
var end = Math.min(offset + chunkSize, value.length);
if (end < value.length && /[\uD800-\uDBFF]/.test(value.charAt(end - 1))) end--;
chunks.push(value.slice(offset, end));
offset = end;
}
if (!chunks.length) chunks.push('');
var total = chunks.length;
var index = 0;
function sendNext() {
var chunk = (content || '').slice(index * chunkSize, (index + 1) * chunkSize);
var chunk = chunks[index];
return L.resolveDefault(callSaveSingboxProfileChunk(name, chunk, String(index), String(total)), {}).then(function (r) {
if (!r || !r.success)
return { success: false, error: (r && r.error) || 'upload_failed', message: (r && (r.message || r.error)) || _('Upload failed') };
@@ -578,7 +578,7 @@ function createConfigEditor(mode) {
});
var host = E('div', { 'class': 'cl-cm-host', style: 'display:none' });
var wrap = E('div', { 'class': 'cl-cm-wrap' }, [host, ta]);
var cm = null, usingCM = false;
var cm = null, usingCM = false, valueSeq = 0;
function showTextarea(content) {
usingCM = false;
@@ -598,11 +598,14 @@ function createConfigEditor(mode) {
el: wrap,
textarea: ta,
setValue: function (content, readOnly) {
var seq = ++valueSeq;
content = (content == null) ? '' : String(content);
ta.readOnly = !!readOnly;
if (content.length > LARGE) { showTextarea(content); return; }
showTextarea(content);
if (content.length > LARGE) return;
if (cm) { cm.setOption('readOnly', !!readOnly); showCM(content); return; }
loadCodeMirror().then(function () {
if (seq !== valueSeq) return;
if (!cm && window.CodeMirror) {
cm = window.CodeMirror(host, {
value: '', mode: mode,
@@ -615,7 +618,7 @@ function createConfigEditor(mode) {
host.classList.add('cl-cm-dark');
}
if (cm) { cm.setOption('readOnly', !!readOnly); showCM(content); } else showTextarea(content);
}).catch(function () { showTextarea(content); });
}).catch(function () { if (seq === valueSeq) showTextarea(content); });
},
getValue: function () {
return (usingCM && cm) ? cm.getValue() : ta.value;
@@ -1707,8 +1710,9 @@ return view.extend({
var self = this;
var safeText = function (v) { return (v == null || v === 'null') ? '' : String(v); };
var formatJsonForEditor = function (content) {
if (!content) return '';
try {
return JSON.stringify(JSON.parse(content || '{}'), null, 2) + '\n';
return JSON.stringify(JSON.parse(content), null, 2) + '\n';
} catch (e) {
return content || '';
}
@@ -1741,7 +1745,7 @@ return view.extend({
var msg = r.changes && r.changes.length ? _("Fixed deprecated fields: ") + r.changes.join(', ') : _("Configuration is up to date; no fixes needed");
ui.addNotification(null, E('p', msg));
/* 重新加载编辑器内容 */
clashoo.getSingboxProfile(name).then(function (gr) { ed.setValue(formatJsonForEditor(gr.content || '')); });
loadEditor(name);
} else {
ui.addNotification(null, E('p', _("Fix failed: ") + ((r && r.message) || '')));
}
@@ -1759,14 +1763,26 @@ return view.extend({
])
]);
var loadSeq = 0;
function loadEditor(name) {
var seq = ++loadSeq;
editorTitle.textContent = _("Editing: ") + name;
saveBtn.removeAttribute('disabled');
migrateBtn.removeAttribute('disabled');
ed.textarea.dataset.name = name;
saveBtn.disabled = true;
migrateBtn.disabled = true;
ed.textarea.dataset.name = '';
ed.setValue(_("Loading…"));
clashoo.getSingboxProfile(name).then(function (r) {
ed.setValue(formatJsonForEditor(r.content || ''));
if (seq !== loadSeq) return;
if (!r || r.error || typeof r.content !== 'string' || !r.content)
throw new Error((r && (r.message || r.error)) || 'read_failed');
ed.setValue(formatJsonForEditor(r.content));
ed.textarea.dataset.name = name;
saveBtn.disabled = false;
migrateBtn.disabled = false;
}).catch(function (e) {
if (seq !== loadSeq) return;
ed.setValue('');
ui.addNotification(null, E('p', _("Load failed") + ': ' + (e.message || e)), 'error');
});
}
@@ -247,7 +247,7 @@ function clKernelUnlock() {
return view.extend({
_busy: false,
_op: null, /* 'start' | 'stop' | 'restart' | null */
_op: null, /* 'start' | 'stop' | 'restart' | 'reload' | null */
_opTimers: null, /* array of setTimeout IDs for cleanup */
_accessRefreshing: false,
_coreSwitchBusy: false,
@@ -313,6 +313,7 @@ return view.extend({
tcp_mode: uci.get('clashoo', 'config', 'tcp_mode') || 'tun',
udp_mode: uci.get('clashoo', 'config', 'udp_mode') || (uci.get('clashoo', 'config', 'tcp_mode') || 'tun'),
stack: uci.get('clashoo', 'config', 'stack') || 'system',
enhanced_mode: uci.get('clashoo', 'config', 'enhanced_mode') || 'fake-ip',
config: configName,
local_ip: location.hostname,
health_status: 'loading'
@@ -788,7 +789,29 @@ return view.extend({
if (disabled) return;
return running ? self._stop() : self._start();
}
}, [E('span', { 'class': 'cl-service-knob' })])
}, [E('span', { 'class': 'cl-service-knob' })]),
E('button', {
type: 'button',
'class': 'btn cbi-button-action cl-btn-sm',
title: _("Restart Service"),
disabled: (disabled || !running) ? '' : null,
click: function (ev) {
ev.preventDefault();
if (disabled || !running) return;
return self._restart();
}
}, _("Restart")),
E('button', {
type: 'button',
'class': 'btn cbi-button-action cl-btn-sm',
title: _("Reload Service"),
disabled: (disabled || !running) ? '' : null,
click: function (ev) {
ev.preventDefault();
if (disabled || !running) return;
return self._reload();
}
}, _("Reload"))
]);
},
@@ -1561,6 +1584,7 @@ return view.extend({
var tcpMode = String(st.tcp_mode || '').toLowerCase();
var udpMode = String(st.udp_mode || tcpMode).toLowerCase();
var stackMode = String(st.stack || '').toLowerCase();
var enhancedMode = String(st.enhanced_mode || 'fake-ip').toLowerCase();
var tpMode = 'custom';
if (tcpMode === 'tun' && udpMode === 'tun' && stackMode === 'mixed')
@@ -1570,7 +1594,7 @@ return view.extend({
else if (tcpMode === 'tun' && udpMode === 'tun')
tpMode = 'tun-system';
else if (tcpMode === 'redirect' && udpMode === 'tproxy')
tpMode = 'fake-ip';
tpMode = enhancedMode === 'redir-host' ? 'redir-host' : 'fake-ip';
var panelType = st.panel_type || 'zashboard';
var panelUrl = this._dashboardUrl(st);
var panels = ['metacubexd', 'yacd', 'zashboard', 'razord'];
@@ -1581,7 +1605,7 @@ return view.extend({
return E('option', { value: o[0], selected: o[0] === val ? '' : null, disabled: o[2] ? '' : null }, o[1]);
}));
};
var tpModeOptions = [['fake-ip','Fake-IP'],['tun-system','System'],['tun-gvisor','gVisor'],['tun-mixed','Mixed']];
var tpModeOptions = [['fake-ip','Fake-IP'],['redir-host','Redir-Host'],['tun-system','System'],['tun-gvisor','gVisor'],['tun-mixed','Mixed']];
if (tpMode === 'custom')
tpModeOptions.unshift(['custom',_("Custom")]);
@@ -1638,6 +1662,7 @@ return view.extend({
sel.disabled = true;
self._op = 'mode';
self._lastSt = self._lastSt || {};
self._lastSt.enhanced_mode = mode === 'redir-host' ? 'redir-host' : 'fake-ip';
if (mode === 'tun-mixed') {
self._lastSt.tcp_mode = 'tun';
self._lastSt.udp_mode = 'tun';
@@ -1787,10 +1812,12 @@ return view.extend({
尤其是切换 tab 回到 overview 时缓存数据与首次 poll 数据通常一致 */
var sig = JSON.stringify({
running: st.running,
pid: st.pid,
health: st.health_status,
proxy_mode: st.proxy_mode,
tcp_mode: st.tcp_mode,
udp_mode: st.udp_mode,
enhanced_mode: st.enhanced_mode,
stack: st.stack_type || st.stack,
config: st.config,
core_type: st.core_type,
@@ -1933,14 +1960,13 @@ return view.extend({
return _("Health check in progress…");
},
/* fn: fire-and-forget RPC + 秒速轮询直到状态到位 */
/* toggle switch instantly (optimistic), do not wait for RPC */
/* toggle switch instantly (optimistic), then poll until the operation takes effect */
_svc: function (fn, opKey) {
if (this._busy) return Promise.resolve();
// preflight: refuse start when no profile selected.
// backend select_config also defends but toast is slow, UI would flash.
if (opKey === 'start' || opKey === 'restart') {
if (opKey === 'start' || opKey === 'restart' || opKey === 'reload') {
var st0 = this._lastSt || {};
var hasConfig = !!(st0.config || st0.conf_path);
if (!hasConfig) {
@@ -1957,21 +1983,25 @@ return view.extend({
var btn = document.querySelector('.cl-service-switch');
if (btn) {
var running = btn.getAttribute('aria-pressed') === 'true';
var newRunning = opKey === 'start';
var newRunning = opKey !== 'stop';
if (newRunning !== running) {
btn.setAttribute('aria-pressed', newRunning ? 'true' : 'false');
btn.className = 'cl-service-switch' + (newRunning ? ' is-on' : ' is-off');
}
self._showOpMsg(opKey === 'stop' ? _("Stopping…") : _("Starting…"));
self._showOpMsg(opKey === 'stop' ? _("Stopping…") : opKey === 'reload' ? _("Reloading…") : _("Starting…"));
}
var maxWait = opKey === 'stop' ? 15000 : 35000;
fn().catch(function () {}); /* fire-and-forget */
var started = Date.now();
var pollTimer = null;
var initialPid = (this._lastSt && this._lastSt.pid) || 0;
var sawStopped = false;
var rpcDone = false;
var finished = false;
function finish(finalMsg) {
if (finished) return;
finished = true;
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
if (finalMsg) self._showOpMsg(finalMsg);
self._busy = false;
@@ -1979,19 +2009,34 @@ return view.extend({
self._pollOverview(true);
}
Promise.resolve().then(fn).then(function (r) {
if (!r || r.success !== true)
throw new Error((r && (r.message || r.error)) || 'RPC failed');
rpcDone = true;
}).catch(function (e) {
ui.addNotification(null, E('p', _("Operation failed: ") + (e.message || e)), 'error');
finish(null);
});
function pollOnce() {
if (finished) return;
L.resolveDefault(clashoo.status(), {}).then(function (st) {
if (finished) return;
st = st || {};
var elapsed = Date.now() - started;
if (opKey === 'stop') {
if (st.running === false) return finish(_("Stopped") + ' ⚪');
if (rpcDone && st.running === false) return finish(_("Stopped") + ' ⚪');
} else {
if (st.running === true) return finish(_("Running 🟢"));
if (st.health_status === 'fail') return finish(null);
if (st.running === false) sawStopped = true;
if (rpcDone && st.health_status === 'fail') return finish(null);
if (rpcDone && st.running === true &&
(opKey !== 'restart' || (initialPid && st.pid && st.pid !== initialPid) || sawStopped))
return finish(_("Running 🟢"));
}
if (elapsed >= maxWait) return finish(null);
pollTimer = setTimeout(pollOnce, 500);
}).catch(function () {
if (finished) return;
if (Date.now() - started < maxWait) pollTimer = setTimeout(pollOnce, 500);
else finish(null);
});
@@ -2002,6 +2047,7 @@ return view.extend({
_start: function () { return this._svc(function () { return clashoo.start(); }, 'start'); },
_stop: function () { return this._svc(function () { return clashoo.stop(); }, 'stop'); },
_restart: function () { return this._svc(function () { return clashoo.restart(); }, 'restart'); },
_reload: function () { return this._svc(function () { return clashoo.reload(); }, 'reload'); },
_updSubs: function (ev) {
var btn = ev && ev.target;