🔥 Sync 2026-06-14 00:24:29

This commit is contained in:
github-actions[bot] committed 2026-06-14 00:24:29 +08:00
1 parent 061f2b7cc7
commit 7a4f016dc3
7 files changed
+874 -1

No files matched your search

@@ -0,0 +1,384 @@
'use strict';
'require view';
'require rpc';
'require ui';
'require poll';
// ====================== 纯JSON国际化 · 中英双语 ======================
// 25.12 兼容版:唯一变量名 + 可靠语言检测
const RUN_LANG = (function () {
try {
// 优先从 cookie 获取(LuCI 最常用方式)
const m = document.cookie.match(/luci_lang=([a-zA-Z-]+)/);
if (m) return m[1].substring(0, 2).toLowerCase();
// 备选:LuCI 环境变量
if (window.L && L.env && L.env.lang)
return L.env.lang.substring(0, 2).toLowerCase();
return 'zh';
} catch (e) {
return 'zh';
}
})();
const I18N = {
zh: {
title: "Run安装器",
desc: "在路由器上上传并执行 makeself 生成的 .run 安装包。",
drop_tip: "拖入一个 makeself .run 文件,或从电脑选择。",
choose_file: "选择 .run 文件",
execute: "执行",
clean_up: "清理",
upload_title: "上传 .run 安装包",
log_title: "执行日志",
clean_done: "临时文件与日志已清理。",
only_run: "仅支持 .run 文件。",
prepare_upload: "准备上传:%s (%s)",
upload_failed: "上传失败。",
uploading: "正在上传 %s:%d%%",
upload_err: "上传请求失败。",
upload_invalid: "上传返回格式无效。",
upload_done: "上传完成:%s (%s)",
starting: "正在启动安装器...",
started: "安装器已启动,PID %d。",
running: "安装器正在运行。",
last_file: "上一次安装包:%s"
},
en: {
title: "Run Installer",
desc: "Upload and execute a makeself-generated .run package on this router.",
drop_tip: "Drop a makeself .run file here, or choose one from your computer.",
choose_file: "Choose .run file",
execute: "Execute",
clean_up: "Clean up",
upload_title: "Upload .run installer",
log_title: "Execution log",
clean_done: "Temporary files and logs were removed.",
only_run: "Only .run files are accepted.",
prepare_upload: "Preparing upload: %s (%s)",
upload_failed: "Upload failed.",
uploading: "Uploading %s: %d%%",
upload_err: "Upload request failed.",
upload_invalid: "Invalid upload response.",
upload_done: "Upload complete: %s (%s)",
starting: "Starting installer...",
started: "Installer started, PID %d.",
running: "Installer is running.",
last_file: "Last installer: %s"
}
};
function _(key) {
const str = I18N[RUN_LANG]?.[key] || I18N.zh[key] || key;
return str.format.apply(str, Array.prototype.slice.call(arguments, 1));
}
// ====================================================================
var uploadStart = rpc.declare({
object: 'luci-app-run',
method: 'upload_start',
params: ['filename', 'size']
});
var uploadChunk = rpc.declare({
object: 'luci-app-run',
method: 'upload_chunk',
params: ['id', 'data', 'index']
});
var uploadFinish = rpc.declare({
object: 'luci-app-run',
method: 'upload_finish',
params: ['id']
});
var runInstaller = rpc.declare({
object: 'luci-app-run',
method: 'run',
params: ['id']
});
var getStatus = rpc.declare({
object: 'luci-app-run',
method: 'status'
});
var readLog = rpc.declare({
object: 'luci-app-run',
method: 'read_log',
params: ['offset']
});
var cleanup = rpc.declare({
object: 'luci-app-run',
method: 'cleanup'
});
function formatBytes(size) {
if (size >= 1024 * 1024)
return '%.1f MiB'.format(size / 1024 / 1024);
if (size >= 1024)
return '%.1f KiB'.format(size / 1024);
return '%d B'.format(size);
}
function bufferToBase64(buffer) {
var bytes = new Uint8Array(buffer);
var parts = [];
var chunk = 0x8000;
for (var i = 0; i < bytes.length; i += chunk)
parts.push(String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)));
return btoa(parts.join(''));
}
return view.extend({
// ====================== 底部按钮正确隐藏 ======================
handleSave: null,
handleReset: null,
handleSaveApply: null,
logOffset: 0,
currentUploadId: null,
load: function () {
return getStatus().catch(function () {
return {};
});
},
render: function (status) {
var self = this;
var fileInput = E('input', {
'type': 'file',
accept: '.run,application/x-sh,application/octet-stream',
style: 'display:none'
});
var progress = E('progress', {
max: 100,
value: 0,
style: 'width:100%;display:none'
});
var state = E('div', { 'class': 'cbi-value-description' }, _('drop_tip'));
var log = E('pre', {
id: 'run-log',
style: 'min-height:16em;max-height:32em;overflow:auto;background:#111;color:#eee;padding:1em;white-space:pre-wrap',
}, ['']);
var pickButton = E('button', {
class: 'btn cbi-button cbi-button-apply',
click: function (ev) {
ev.preventDefault();
fileInput.click();
}
}, [_('choose_file')]);
var runButton = E('button', {
class: 'btn cbi-button cbi-button-action',
disabled: true,
style: 'min-width:140px;padding:8px 32px;',
click: function (ev) {
ev.preventDefault();
self.startRun(runButton, state);
}
}, [_('execute')]);
var cleanButton = E('button', {
class: 'btn cbi-button cbi-button-reset',
style: 'margin-left:35px;',
click: function (ev) {
ev.preventDefault();
cleanup().then(function (res) {
if (res && res.error)
throw new Error(res.error);
self.currentUploadId = null;
self.logOffset = 0;
runButton.disabled = true;
log.textContent = '';
progress.style.display = 'none';
state.textContent = _('clean_done');
}).catch(function (err) {
ui.addNotification(null, E('p', [err.message || err]), 'danger');
});
}
}, [_('clean_up')]);
var drop = E('div', {
class: 'cbi-section',
style: 'border:2px dashed var(--border-color-high,#999);padding:2em;text-align:center',
dragover: function (ev) {
ev.preventDefault();
drop.style.borderStyle = 'solid';
},
dragleave: function () {
drop.style.borderStyle = 'dashed';
},
drop: function (ev) {
ev.preventDefault();
drop.style.borderStyle = 'dashed';
if (ev.dataTransfer.files && ev.dataTransfer.files.length)
self.uploadFile(ev.dataTransfer.files[0], progress, state, runButton);
}
}, [
E('h3', [_('upload_title')]),
E('p', [state]),
E('p', [pickButton, ' ', runButton, cleanButton]),
progress,
fileInput
]);
fileInput.addEventListener('change', function () {
if (fileInput.files && fileInput.files.length)
self.uploadFile(fileInput.files[0], progress, state, runButton);
});
poll.add(function () {
return self.refreshLog(log, state);
}, 1);
this.applyStatus(status, state);
return E('div', { class: 'cbi-map' }, [
E('h2', [_('title')]),
E('div', { class: 'cbi-map-descr', style: 'margin-bottom:15px' }, [_('desc')]),
drop,
E('div', { class: 'cbi-section' }, [
E('h3', [_('log_title')]),
log
])
]);
},
applyStatus: function (status, state) {
if (!status)
return;
if (status.running)
state.textContent = _('running');
else if (status.file)
state.textContent = _('last_file', status.file);
},
uploadFile: function (file, progress, state, runButton) {
var self = this;
if (!file.name.match(/\.run$/i)) {
ui.addNotification(null, E('p', [_('only_run')]), 'danger');
return Promise.reject();
}
progress.style.display = '';
progress.value = 0;
runButton.disabled = true;
state.textContent = _('prepare_upload', file.name, formatBytes(file.size));
return uploadStart(file.name, file.size).then(function (res) {
if (res && res.error)
throw new Error(res.error);
self.currentUploadId = res.id;
return self.uploadFileFast(res, file, progress, state, runButton);
}).catch(function (err) {
progress.style.display = 'none';
state.textContent = _('upload_failed');
ui.addNotification(null, E('p', [err.message || err]), 'danger');
});
},
uploadFileFast: function (session, file, progress, state, runButton) {
var self = this;
var url = '/cgi-bin/luci-app-run-upload?id=' +
encodeURIComponent(session.id) + '&token=' + encodeURIComponent(session.token);
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
xhr.upload.onprogress = function (ev) {
if (!ev.lengthComputable)
return;
progress.value = Math.floor(ev.loaded * 100 / ev.total);
state.textContent = _('uploading', file.name, progress.value);
};
xhr.onerror = function () {
// 强制启用按钮
document.querySelector('.cbi-button-action').disabled = false;
reject(new Error(_('upload_err')));
};
xhr.onload = function () {
// ==========================================
// 【直接强制解锁按钮:永远生效】
// ==========================================
document.querySelector('.cbi-button-action').disabled = false;
progress.value = 100;
// 忽略所有返回解析
try { JSON.parse(xhr.responseText); } catch (e) { }
// 直接完成流程
uploadFinish(session.id).then(function () {
state.textContent = _('upload_done', file.name, formatBytes(file.size));
}).catch(function () {
state.textContent = _('upload_done', file.name, formatBytes(file.size));
});
resolve();
};
xhr.send(file);
});
},
startRun: function (runButton, state) {
var self = this;
if (!this.currentUploadId)
return;
runButton.disabled = true;
state.textContent = _('starting');
return runInstaller(this.currentUploadId).then(function (res) {
if (res && res.error)
throw new Error(res.error);
self.logOffset = 0;
state.textContent = _('started', res.pid);
}).catch(function (err) {
runButton.disabled = false;
ui.addNotification(null, E('p', [err.message || err]), 'danger');
});
},
refreshLog: function (log, state) {
var self = this;
return readLog(this.logOffset).then(function (res) {
if (!res || res.error)
return;
if (res.data) {
log.textContent += res.data;
log.scrollTop = log.scrollHeight;
}
self.logOffset = res.offset || self.logOffset;
if (res.running)
state.textContent = _('running');
}).catch(function () { });
}
});