mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 23:53:56 +08:00
🏅 Sync 2026-10-10 12:03:15
This commit is contained in:
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;
|
||||
|
||||
Reference in new issue
Block a user