mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 05:12:51 +08:00
🎉 Sync 2026-09-21 03:58:45
This commit is contained in:
1 parent
da26c8c705
commit
b8539f4af6
8 files changed
+443
-74
No files matched your search
+3
-3
@@ -10,12 +10,12 @@ AGENTFLOW_ARCH_x86_64:=amd64
|
||||
AGENTFLOW_ARCH_aarch64:=arm64
|
||||
AGENTFLOW_ARCH:=$(AGENTFLOW_ARCH_$(ARCH))
|
||||
|
||||
AGENTFLOW_HASH_x86_64:=95cb43ba4e2ce5f060e3609d050caf25170dc151ba882a3a626494b7750d5fbc
|
||||
AGENTFLOW_HASH_aarch64:=93e92628b26087b87d8571400e83046197167e4795593e757d4dcf7dac5f8231
|
||||
AGENTFLOW_HASH_x86_64:=478fdb17cfb2b1664e7bc01315186e71f228871c0d20c816d8b3b8fe789bdb4d
|
||||
AGENTFLOW_HASH_aarch64:=257faeaad2445dd14f87c1e1d9ded869fd301c5df2c2c9af817e70303eab2512
|
||||
|
||||
PKG_NAME:=agentflow
|
||||
PKG_VERSION:=0.3.3
|
||||
PKG_RELEASE:=11
|
||||
PKG_RELEASE:=12
|
||||
|
||||
AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH)
|
||||
AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH))
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=AgentFlow
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=5
|
||||
LUCI_DEPENDS:=+agentflow +luci-compat
|
||||
PKG_VERSION:=1.0.1
|
||||
PKG_RELEASE:=6
|
||||
LUCI_DEPENDS:=+agentflow +luci-compat +luci-lib-taskd
|
||||
LUCI_MINIFY_CSS:=0
|
||||
LUCI_MINIFY_JS:=0
|
||||
|
||||
|
||||
@@ -17,10 +17,38 @@ function index()
|
||||
return
|
||||
end
|
||||
|
||||
local install = entry({"admin", "services", "agentflow", "agent_install"}, call("agentflow_agent_install"))
|
||||
install.leaf = true
|
||||
|
||||
local page = entry({"admin", "services", "agentflow"}, cbi("agentflow"), _("AgentFlow"), 100)
|
||||
page.dependent = true
|
||||
end
|
||||
|
||||
local function write_json(obj)
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(obj)
|
||||
end
|
||||
|
||||
local function require_post_csrf()
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
local context = dispatcher.context or {}
|
||||
local expected = context.authtoken or context.token
|
||||
|
||||
if (http.getenv("REQUEST_METHOD") or "") ~= "POST" then
|
||||
http.status(405, "Method Not Allowed")
|
||||
write_json({ ok = false, error = "method not allowed" })
|
||||
return false
|
||||
end
|
||||
|
||||
if not context.authsession or not expected or http.formvalue("token") ~= expected then
|
||||
http.status(403, "Forbidden")
|
||||
write_json({ ok = false, error = "invalid csrf token" })
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function uhttpd_has_apps_proxy_prefix()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local mappings = uci:get_list("uhttpd", "main", "proxy_prefix") or {}
|
||||
@@ -119,8 +147,97 @@ function agentflow_status()
|
||||
proxy_prefix_enabled = uhttpd_apps_proxy_available(),
|
||||
linkeasefull_running = linkeasefull_running()
|
||||
}
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(status)
|
||||
write_json(status)
|
||||
end
|
||||
|
||||
function agentflow_agent_install()
|
||||
local fs = require "nixio.fs"
|
||||
local jsonc = require "luci.jsonc"
|
||||
local sys = require "luci.sys"
|
||||
local util = require "luci.util"
|
||||
local task_id = "agentflow-agent-install"
|
||||
local task_script = "/tmp/agentflow-agent-install.sh"
|
||||
local installer_url = "https://fw.koolcenter.com/binary/geili/agentflow/releases/installapp/installapp-mise.sh"
|
||||
local agents = {
|
||||
codexcli = true,
|
||||
["claude-code"] = true,
|
||||
opencode = true,
|
||||
kimi = true,
|
||||
reasonix = true
|
||||
}
|
||||
|
||||
if not require_post_csrf() then
|
||||
return
|
||||
end
|
||||
|
||||
local agent = http.formvalue("agent") or ""
|
||||
if not agents[agent] then
|
||||
write_json({ ok = false, error = "unsupported agent" })
|
||||
return
|
||||
end
|
||||
if not fs.access("/etc/init.d/tasks") then
|
||||
write_json({ ok = false, error = "taskd is not available" })
|
||||
return
|
||||
end
|
||||
if not fs.access("/lib/functions/mise.sh") then
|
||||
write_json({ ok = false, error = "mise environment helper is not available" })
|
||||
return
|
||||
end
|
||||
|
||||
local task_status = sys.exec("/etc/init.d/tasks task_status " .. task_id .. " 2>/dev/null")
|
||||
local status = jsonc.parse(task_status) or {}
|
||||
local running = type(status) == "table" and status.running
|
||||
if running == true or running == 1 or running == "1" or running == "true" then
|
||||
write_json({ ok = true, busy = true, task_id = task_id })
|
||||
return
|
||||
end
|
||||
|
||||
local install_script = table.concat({
|
||||
"set -e",
|
||||
"agent=" .. util.shellquote(agent),
|
||||
"installer_url=" .. util.shellquote(installer_url),
|
||||
'installer="/tmp/agentflow-installapp-mise.$$"',
|
||||
'cleanup() { rm -f "$installer" "$0"; }',
|
||||
"trap cleanup 0 HUP INT TERM",
|
||||
'. /lib/functions/mise.sh',
|
||||
'if ! istore_runtime_env; then echo "[agentflow] Failed to initialize the shared runtime environment" >&2; exit 1; fi',
|
||||
"set -u",
|
||||
"export MISE_YES=1",
|
||||
'echo "[agentflow] Downloading installer: $installer_url"',
|
||||
'if command -v wget >/dev/null 2>&1; then',
|
||||
'\twget -O "$installer" "$installer_url"',
|
||||
'elif command -v curl >/dev/null 2>&1; then',
|
||||
'\tcurl -fL -o "$installer" "$installer_url"',
|
||||
'elif command -v uclient-fetch >/dev/null 2>&1; then',
|
||||
'\tuclient-fetch -O "$installer" "$installer_url"',
|
||||
"else",
|
||||
'\techo "[agentflow] No HTTPS download tool is available" >&2',
|
||||
"\texit 1",
|
||||
"fi",
|
||||
'if [ ! -s "$installer" ]; then echo "[agentflow] Failed to download the agent installer" >&2; exit 1; fi',
|
||||
'chmod 0700 "$installer"',
|
||||
'echo "[agentflow] Running installapp-mise.sh for $agent in $HOME"',
|
||||
'/bin/sh "$installer" "$agent"'
|
||||
}, "\n")
|
||||
if not fs.writefile(task_script, install_script .. "\n") then
|
||||
write_json({ ok = false, error = "failed to create install task", task_id = task_id })
|
||||
return
|
||||
end
|
||||
if sys.call("chmod 0600 " .. util.shellquote(task_script)) ~= 0 then
|
||||
fs.unlink(task_script)
|
||||
write_json({ ok = false, error = "failed to secure install task", task_id = task_id })
|
||||
return
|
||||
end
|
||||
|
||||
local command = "/bin/sh " .. util.shellquote(task_script)
|
||||
local rc = sys.call("/etc/init.d/tasks task_add " .. task_id .. " " .. util.shellquote(command) .. " >/dev/null 2>&1")
|
||||
if rc ~= 0 then
|
||||
fs.unlink(task_script)
|
||||
write_json({ ok = false, error = "failed to start install task", task_id = task_id })
|
||||
return
|
||||
end
|
||||
|
||||
write_json({ ok = true, task_id = task_id })
|
||||
end
|
||||
|
||||
function agentflow_open()
|
||||
|
||||
@@ -1,11 +1,80 @@
|
||||
<%+tasks/embed%>
|
||||
|
||||
<style type="text/css">
|
||||
.agentflow-actions { display:flex; gap:10px; flex-wrap:wrap; margin:12px; }
|
||||
.agentflow-actions .btn, .agentflow-agent-dialog-actions .btn { min-height:40px; }
|
||||
.agentflow-agent-modal { position:fixed; inset:0; z-index:10000; display:none; align-items:center; justify-content:center; padding:20px; box-sizing:border-box; background:rgba(15,23,42,.45); }
|
||||
.agentflow-agent-modal.open { display:flex; }
|
||||
.agentflow-agent-dialog, body[theme="light"] .agentflow-agent-dialog { width:min(520px, 100%); padding:20px; box-sizing:border-box; border:1px solid #d0d7de; border-radius:12px; background:#fff; color:inherit; box-shadow:0 18px 48px rgba(15,23,42,.24); }
|
||||
.agentflow-agent-dialog h3 { margin:0 0 8px; text-wrap:balance; }
|
||||
.agentflow-agent-dialog > p, body[theme="light"] .agentflow-agent-dialog > p { margin:0 0 16px; color:#666; text-wrap:pretty; }
|
||||
.agentflow-agent-field label { display:block; margin-bottom:6px; font-weight:600; }
|
||||
.agentflow-agent-field select { width:100%; min-height:40px; box-sizing:border-box; }
|
||||
.agentflow-agent-dialog-actions { display:flex; justify-content:flex-end; gap:10px; margin-top:18px; }
|
||||
body[theme="dark"] .agentflow-agent-dialog { border-color:#465166; background:#1f2937; color:#e5e7eb; }
|
||||
body[theme="dark"] .agentflow-agent-dialog > p { color:#aeb8c7; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body:not([theme]) .agentflow-agent-dialog { border-color:#465166; background:#1f2937; color:#e5e7eb; }
|
||||
body:not([theme]) .agentflow-agent-dialog > p { color:#aeb8c7; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var agentflowOpenUrl = '<%=url("admin/services/agentflow/open")%>';
|
||||
var agentflowAgentInstallUrl = '<%=url("admin/services/agentflow/agent_install")%>';
|
||||
var agentflowCsrfToken = (window.taskd && window.taskd.csrfToken) || '';
|
||||
|
||||
function agentflowAgentDialog(open) {
|
||||
var modal = document.getElementById('agentflow-agent-modal');
|
||||
var select = document.getElementById('agentflow-agent-select');
|
||||
var trigger = document.getElementById('agentflow-agent-open');
|
||||
if (!modal) return;
|
||||
if (open) {
|
||||
modal.classList.add('open');
|
||||
if (select) select.focus();
|
||||
} else {
|
||||
modal.classList.remove('open');
|
||||
if (trigger) trigger.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function agentflowInstallAgent() {
|
||||
var selected = document.getElementById('agentflow-agent-select');
|
||||
var button = document.getElementById('agentflow-agent-install-now');
|
||||
if (!selected || !button) return;
|
||||
if (!window.taskd || !window.taskd.show_log) {
|
||||
alert('<%:Task log window is not available.%>');
|
||||
return;
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
(new XHR()).post(agentflowAgentInstallUrl, {
|
||||
agent: selected.value,
|
||||
token: agentflowCsrfToken
|
||||
}, function(x, data) {
|
||||
button.disabled = false;
|
||||
if (!data) {
|
||||
try { data = JSON.parse(x.responseText || '{}'); }
|
||||
catch (e) { data = {}; }
|
||||
}
|
||||
if (!data.ok) {
|
||||
alert(data.error || '<%:Failed to start agent installation.%>');
|
||||
return;
|
||||
}
|
||||
agentflowAgentDialog(false);
|
||||
window.taskd.show_log(data.task_id || 'agentflow-agent-install');
|
||||
});
|
||||
}
|
||||
|
||||
XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st) {
|
||||
var el = document.getElementById('agentflow_status');
|
||||
var openButton = document.getElementById('agentflow-open');
|
||||
if (st && el) {
|
||||
if (!st.running) {
|
||||
if (openButton) openButton.style.display = 'none';
|
||||
el.innerHTML = '<br/><em style="color:red"><%:The AgentFlow service is not running.%></em>';
|
||||
} else {
|
||||
if (openButton) openButton.style.display = '';
|
||||
var accessMode = '';
|
||||
if (st.proxy_prefix_enabled && st.linkeasefull_running) {
|
||||
accessMode = '<br/><em><%:Access mode: LinkEase Desktop /apps proxy.%></em>';
|
||||
@@ -13,11 +82,14 @@ XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st)
|
||||
accessMode = '<br/><em><%:Access mode: external port.%></em>';
|
||||
}
|
||||
el.innerHTML = '<br/><em style="color:green"><%:The AgentFlow service is running.%></em>'
|
||||
+ accessMode
|
||||
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open AgentFlow%> \" onclick=\"window.open('" + agentflowOpenUrl + "')\"/>";
|
||||
+ accessMode;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(ev) {
|
||||
if (ev.key === 'Escape') agentflowAgentDialog(false);
|
||||
});
|
||||
//]]></script>
|
||||
|
||||
<fieldset class="cbi-section">
|
||||
@@ -25,4 +97,29 @@ XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st)
|
||||
<p id="agentflow_status">
|
||||
<em><%:Collecting data...%></em>
|
||||
</p>
|
||||
<div class="agentflow-actions">
|
||||
<input id="agentflow-open" class="btn cbi-button cbi-button-apply" type="button" value="<%:Click to open AgentFlow%>" onclick="window.open(agentflowOpenUrl)" style="display:none">
|
||||
<input id="agentflow-agent-open" class="btn cbi-button cbi-button-apply" type="button" value="<%:Install agent%>" onclick="agentflowAgentDialog(true)">
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div id="agentflow-agent-modal" class="agentflow-agent-modal" role="dialog" aria-modal="true" aria-labelledby="agentflow-agent-title" onclick="if (event.target === this) agentflowAgentDialog(false)">
|
||||
<div class="agentflow-agent-dialog">
|
||||
<h3 id="agentflow-agent-title"><%:Install agent%></h3>
|
||||
<p><%:Select a coding agent to install into the shared AgentFlow runtime.%></p>
|
||||
<div class="agentflow-agent-field">
|
||||
<label for="agentflow-agent-select"><%:Agent%></label>
|
||||
<select id="agentflow-agent-select" class="cbi-input-select">
|
||||
<option value="codexcli">Codex</option>
|
||||
<option value="claude-code">Claude Code</option>
|
||||
<option value="opencode">OpenCode</option>
|
||||
<option value="kimi">Kimi</option>
|
||||
<option value="reasonix">Reasonix</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="agentflow-agent-dialog-actions">
|
||||
<input class="btn cbi-button" type="button" value="<%:Cancel%>" onclick="agentflowAgentDialog(false)">
|
||||
<input id="agentflow-agent-install-now" class="btn cbi-button cbi-button-apply" type="button" value="<%:Install now%>" onclick="agentflowInstallAgent()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,3 +51,24 @@ msgstr "AgentFlow 服务未运行。"
|
||||
|
||||
msgid "The AgentFlow service is running."
|
||||
msgstr "AgentFlow 服务运行中。"
|
||||
|
||||
msgid "Install agent"
|
||||
msgstr "安装智能体"
|
||||
|
||||
msgid "Agent"
|
||||
msgstr "智能体"
|
||||
|
||||
msgid "Select a coding agent to install into the shared AgentFlow runtime."
|
||||
msgstr "选择要安装到 AgentFlow 共享运行环境中的编码智能体。"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
msgid "Install now"
|
||||
msgstr "立即安装"
|
||||
|
||||
msgid "Task log window is not available."
|
||||
msgstr "任务日志窗口不可用。"
|
||||
|
||||
msgid "Failed to start agent installation."
|
||||
msgstr "启动智能体安装失败。"
|
||||
@@ -0,0 +1,69 @@
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
APP_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class AgentInstallContractTest(unittest.TestCase):
|
||||
def read(self, relative):
|
||||
return (APP_DIR / relative).read_text(encoding="utf-8")
|
||||
|
||||
def test_luci_loads_task_window_and_starts_whitelisted_installer(self):
|
||||
makefile = self.read("Makefile")
|
||||
controller = self.read("luasrc/controller/agentflow.lua")
|
||||
status = self.read("luasrc/view/agentflow/status.htm")
|
||||
|
||||
self.assertIn("+luci-lib-taskd", makefile)
|
||||
self.assertIn('<%+tasks/embed%>', status)
|
||||
self.assertIn('<select id="agentflow-agent-select"', status)
|
||||
for agent in ("codexcli", "claude-code", "opencode", "kimi", "reasonix"):
|
||||
self.assertIn(f'<option value="{agent}">', status)
|
||||
self.assertNotIn('type="radio"', status)
|
||||
self.assertIn('id="agentflow-open"', status)
|
||||
self.assertIn('id="agentflow-agent-open"', status)
|
||||
self.assertGreater(status.index('id="agentflow-agent-open"'), status.index('id="agentflow_status"'))
|
||||
self.assertIn('margin:12px;', status)
|
||||
self.assertIn("var selected = document.getElementById('agentflow-agent-select')", status)
|
||||
self.assertIn("window.taskd.show_log(data.task_id", status)
|
||||
self.assertNotIn('local token =', status)
|
||||
self.assertIn("window.taskd.csrfToken", status)
|
||||
self.assertIn("token: agentflowCsrfToken", status)
|
||||
self.assertIn('http.formvalue("agent")', controller)
|
||||
self.assertIn('codexcli = true', controller)
|
||||
self.assertIn('["claude-code"] = true', controller)
|
||||
self.assertIn('opencode = true', controller)
|
||||
self.assertIn('kimi = true', controller)
|
||||
self.assertIn('reasonix = true', controller)
|
||||
self.assertIn('context.authtoken or context.token', controller)
|
||||
self.assertIn('http.formvalue("token") ~= expected', controller)
|
||||
self.assertIn('/etc/init.d/tasks task_add ', controller)
|
||||
self.assertIn('task_id = "agentflow-agent-install"', controller)
|
||||
|
||||
def test_modal_theme_prefers_body_attribute_then_system_theme(self):
|
||||
status = self.read("luasrc/view/agentflow/status.htm")
|
||||
|
||||
self.assertIn('body[theme="light"] .agentflow-agent-dialog', status)
|
||||
self.assertIn('body[theme="dark"] .agentflow-agent-dialog', status)
|
||||
self.assertIn('@media (prefers-color-scheme: dark)', status)
|
||||
self.assertIn('body:not([theme]) .agentflow-agent-dialog', status)
|
||||
|
||||
def test_controller_runs_remote_installer_inside_taskd(self):
|
||||
controller = self.read("luasrc/controller/agentflow.lua")
|
||||
|
||||
self.assertFalse((APP_DIR / "root/usr/libexec/istorec/agentflow-agent.sh").exists())
|
||||
self.assertNotIn("/usr/libexec/istorec/agentflow-agent.sh", controller)
|
||||
self.assertIn("installapp/installapp-mise.sh", controller)
|
||||
self.assertIn("istore_runtime_env", controller)
|
||||
self.assertIn('wget -O "$installer" "$installer_url"', controller)
|
||||
self.assertIn('/bin/sh "$installer" "$agent"', controller)
|
||||
self.assertIn('task_script = "/tmp/agentflow-agent-install.sh"', controller)
|
||||
self.assertIn("fs.writefile(task_script", controller)
|
||||
self.assertIn('rm -f "$installer" "$0"', controller)
|
||||
self.assertIn('local command = "/bin/sh "', controller)
|
||||
self.assertNotIn('local command = "/bin/sh -c "', controller)
|
||||
self.assertLess(controller.index("istore_runtime_env"), controller.index('"set -u"'))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -71,6 +71,7 @@ function getRadioBand(devName) {
|
||||
return view.extend({
|
||||
initialValues: {},
|
||||
initialShortcuts: [],
|
||||
originalLanIp: null, // 持久快照,防止被 handleSave 覆盖
|
||||
hasWireless: false,
|
||||
hasNginx: false,
|
||||
map: null,
|
||||
@@ -119,6 +120,7 @@ return view.extend({
|
||||
}
|
||||
}
|
||||
|
||||
// 剥离现有频段后缀,确保输入框展示纯净的基准名称
|
||||
var rawSsid = (ap && ap.ssid) || (this.hasWireless ? 'Kwrt' : '');
|
||||
var baseSsid = rawSsid.replace(/_(2\.4G|5G|6G)$/i, '').trim();
|
||||
|
||||
@@ -147,6 +149,7 @@ return view.extend({
|
||||
|
||||
var sys = this.getSystemConfig();
|
||||
this.initialValues = Object.assign({}, sys);
|
||||
this.originalLanIp = sys.lan_ipaddr; // 记录不可变初始 IP
|
||||
|
||||
// 缓存初始 shortcuts 配置快照,用于保存时比对差异
|
||||
var shortcuts = uci.sections('wizard', 'shortcuts') || [];
|
||||
@@ -502,32 +505,59 @@ return view.extend({
|
||||
});
|
||||
},
|
||||
|
||||
// 5. 重写 View 级别的 handleSaveApply:应用生效并实现 LAN IP 变更平滑迁移与自动跳转
|
||||
// 5. 重写 View 级别的 handleSaveApply:前置检测 LAN IP 变动,强制非回滚提交并精准弹出倒计时
|
||||
handleSaveApply: function(ev, mode) {
|
||||
var self = this;
|
||||
|
||||
// 关键修复:在 handleSave 覆盖 initialValues 之前,立即计算并锁定是否修改了 LAN IP
|
||||
var oldIp = (self.originalLanIp || self.initialValues.lan_ipaddr || '').split('/')[0].trim();
|
||||
var newIp = (self.optMap['lan_ipaddr'] ? self.optMap['lan_ipaddr'].formvalue('default') : '') || '';
|
||||
newIp = newIp.split('/')[0].trim();
|
||||
var ipChanged = !!(newIp && oldIp && (newIp !== oldIp));
|
||||
|
||||
return this.handleSave(ev).then(function(hasChanges) {
|
||||
if (!hasChanges) return false;
|
||||
|
||||
var oldIp = self.initialValues.lan_ipaddr;
|
||||
var newIp = (self.optMap['lan_ipaddr'].formvalue('default') || '').split('/')[0].trim();
|
||||
var ipChanged = newIp && (newIp !== oldIp);
|
||||
if (ipChanged) {
|
||||
var sec = 15;
|
||||
var targetUrl = window.location.protocol + '//' + newIp + (window.location.pathname || '/cgi-bin/luci/');
|
||||
var countSpan = E('strong', {}, String(sec));
|
||||
|
||||
return ui.changes.apply(mode == '0').then(function() {
|
||||
if (ipChanged) {
|
||||
var sec = 15;
|
||||
ui.showModal(_('LAN IP Address Changed'), [
|
||||
E('p', _('LAN IP changed to %s. Redirecting in %d seconds...').format(newIp, sec)),
|
||||
E('div', { 'class': 'spinning', 'style': 'margin: 1em auto;' })
|
||||
]);
|
||||
var timer = window.setInterval(function() {
|
||||
sec--;
|
||||
if (sec <= 0) {
|
||||
window.clearInterval(timer);
|
||||
window.location.href = window.location.protocol + '//' + newIp + window.location.pathname;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
// 1. 核心操作:调用 apply_unchecked,进行强制永久提交,彻底绕过 90 秒回滚保护机制!
|
||||
L.post(L.url('admin', 'uci', 'apply_unchecked')).catch(function() {});
|
||||
|
||||
// 2. 立即在前端呼出弹窗遮罩层
|
||||
ui.showModal(_('LAN IP Address Changed'), [
|
||||
E('p', {}, [_('LAN IP has been changed to '), E('strong', {}, newIp), '.']),
|
||||
E('p', {}, [_('Applying changes without rollback. Redirecting to the new address in '), countSpan, _(' seconds...')]),
|
||||
E('p', { 'class': 'alert-message notice', 'style': 'margin-top: 1em;' },
|
||||
_('提示:更改网段后,若未能自动加载新页面,请尝试重新插拔网线或断开重连 Wi-Fi,以获取新网段的 IP 地址。')),
|
||||
E('div', { 'class': 'spinning', 'style': 'margin: 1.5em auto;' }),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': function() {
|
||||
window.location.href = targetUrl;
|
||||
}
|
||||
}, _('Redirect Now'))
|
||||
])
|
||||
]);
|
||||
|
||||
// 3. 启动倒计时并自动跳转
|
||||
var timer = window.setInterval(function() {
|
||||
sec--;
|
||||
countSpan.textContent = String(sec);
|
||||
if (sec <= 0) {
|
||||
window.clearInterval(timer);
|
||||
window.location.href = targetUrl;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// 未改动 LAN IP 的常规操作,保留原生的回滚自愈保护
|
||||
return ui.changes.apply(mode == '0');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -1,92 +1,106 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8\n"
|
||||
msgstr ""
|
||||
"Project-Id-Version: luci-app-wizard\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-20 20:00+0800\n"
|
||||
"PO-Revision-Date: 2026-09-20 20:00+0800\n"
|
||||
"Last-Translator: Eric\n"
|
||||
"Language-Team: Chinese (Simplified) <zh_Hans>\n"
|
||||
"Language: zh_Hans\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
msgid "Setup Wizard"
|
||||
msgstr "设置向导"
|
||||
msgstr "快速设置向导"
|
||||
|
||||
msgid "Quickly configure common network, wireless, and system settings."
|
||||
msgstr "快速配置常用网络、无线与系统设置。"
|
||||
msgstr "快速配置常用网络、无线与系统运行设置。"
|
||||
|
||||
msgid "Net Settings"
|
||||
msgstr "网络设置"
|
||||
|
||||
msgid "Firmware Settings"
|
||||
msgstr "固件与系统"
|
||||
msgstr "固件设置"
|
||||
|
||||
msgid "Wireless Settings"
|
||||
msgstr "无线设置"
|
||||
|
||||
msgid "Shortcuts"
|
||||
msgstr "快捷方式"
|
||||
msgstr "域名快捷跳转"
|
||||
|
||||
msgid "比如设置google.com的快捷方式为字母g,则在此路由器网络的任何浏览器中输入g/即可访问google.com"
|
||||
msgstr "比如设置 google.com 的快捷方式为字母 g,则在此路由器网络的任何浏览器中输入 g/ 即可访问 google.com"
|
||||
|
||||
msgid "WAN Protocol / Mode"
|
||||
msgstr "WAN 协议 / 工作模式"
|
||||
|
||||
msgid "DHCP Client (Default Router)"
|
||||
msgstr "DHCP 客户端 (标准主路由模式)"
|
||||
msgstr "DHCP 客户端(标准主路由模式)"
|
||||
|
||||
msgid "PPPoE Dial-up"
|
||||
msgstr "PPPoE 拨号上网"
|
||||
msgstr "PPPoE 拨号"
|
||||
|
||||
msgid "Side-Router Mode (Bypass Gateway)"
|
||||
msgstr "旁路由模式 (网关旁路)"
|
||||
msgstr "旁路由模式(网关旁路)"
|
||||
|
||||
msgid "PPPoE Username"
|
||||
msgstr "宽带拨号账号"
|
||||
msgstr "PPPoE 用户名"
|
||||
|
||||
msgid "PPPoE Password"
|
||||
msgstr "宽带拨号密码"
|
||||
msgstr "PPPoE 密码"
|
||||
|
||||
msgid "LAN IPv4 Address"
|
||||
msgstr "LAN 口 IPv4 地址"
|
||||
msgstr "LAN IPv4 地址"
|
||||
|
||||
msgid "Custom DNS Server(s)"
|
||||
msgstr "自定义 DNS 服务器,留空默认运营商DNS"
|
||||
msgstr "自定义 DNS 服务器"
|
||||
|
||||
msgid "Gateway Address"
|
||||
msgstr "网关地址"
|
||||
|
||||
msgid "Primary router IP address used when operating in side-router mode."
|
||||
msgstr "处于旁路由模式时的主路由 IP 地址。"
|
||||
msgstr "在旁路由模式下工作时填写的上级主路由 IP 地址。"
|
||||
|
||||
msgid "Enable DHCP Server"
|
||||
msgstr "启用 DHCP 服务"
|
||||
msgstr "启用 DHCP 服务器"
|
||||
|
||||
msgid "Usually disabled in side-router mode to avoid IP address assignment conflicts with the main router."
|
||||
msgstr "在旁路由模式下通常关闭此项,避免与主路由的 IP 分配冲突。"
|
||||
msgid "Enable or disable LAN DHCP server (usually disabled in side-router mode to avoid IP address conflicts)."
|
||||
msgstr "开启或关闭局域网 DHCP 服务(旁路由模式下通常建议关闭,避免局域网 IP 分配冲突)。"
|
||||
|
||||
msgid "IPv6 Support"
|
||||
msgstr "IPv6 支持"
|
||||
|
||||
msgid "Enable or disable IPv6 router advertisements and DHCPv6."
|
||||
msgstr "启用或关闭 IPv6 路由通告与 DHCPv6 服务。"
|
||||
msgstr "开启或关闭 IPv6 路由通告 (RA) 与 DHCPv6 服务。"
|
||||
|
||||
msgid "Firmware Upgrade Notice"
|
||||
msgstr "固件升级提示"
|
||||
msgstr "固件更新检测提示"
|
||||
|
||||
msgid "Check and display notices for newer firmware versions."
|
||||
msgstr "检查并显示新版本固件更新提示。"
|
||||
msgstr "开机或访问时检测并提示可用的新版本固件。"
|
||||
|
||||
msgid "Run CoreMark on Boot"
|
||||
msgstr "开机 CoreMark 跑分"
|
||||
msgstr "开机自动运行 CoreMark 跑分"
|
||||
|
||||
msgid "Run CPU benchmark asynchronously upon router initialization."
|
||||
msgstr "在路由器初始化启动时运行 CPU 基准测试。"
|
||||
msgstr "路由器启动完成后在后台异步运行 CPU 性能基准测试。"
|
||||
|
||||
msgid "Persistent Cookie Session"
|
||||
msgstr "持久 Cookie 登录"
|
||||
msgstr "保持登录会话 (Persistent Cookie)"
|
||||
|
||||
msgid "Maintain persistent login sessions in the web browser."
|
||||
msgstr "在浏览器中保持登录会话状态,避免频繁重复登录。"
|
||||
msgstr "关闭浏览器后仍保持管理后台登录状态。"
|
||||
|
||||
msgid "Enforce HTTPS Access"
|
||||
msgstr "强制 HTTPS 访问"
|
||||
|
||||
msgid "Automatically redirect HTTP requests to secure HTTPS."
|
||||
msgstr "将所有 HTTP 请求自动重定向至安全加密的 HTTPS。"
|
||||
msgstr "自动将普通 HTTP 请求重定向到加密的 HTTPS 连接。"
|
||||
|
||||
msgid "Landing Dashboard Mode"
|
||||
msgstr "主页展示模式"
|
||||
msgstr "管理首页展示模式"
|
||||
|
||||
msgid "Default"
|
||||
msgstr "默认"
|
||||
@@ -95,46 +109,67 @@ msgid "RouterDog"
|
||||
msgstr "路由狗"
|
||||
|
||||
msgid "NAS"
|
||||
msgstr "NAS 模式"
|
||||
msgstr "NAS"
|
||||
|
||||
msgid "Next-NAS"
|
||||
msgstr "Next-NAS 模式"
|
||||
msgstr "Next-NAS"
|
||||
|
||||
msgid "Router"
|
||||
msgstr "路由器模式"
|
||||
msgstr "Router"
|
||||
|
||||
msgid "Wireless Network Name (SSID)"
|
||||
msgstr "无线网络名称 (SSID)"
|
||||
|
||||
msgid "无线名称基准前缀,系统将自动识别并为 2.4G 追加 _2.4G、5G 追加 _5G、6G 追加 _6G。"
|
||||
msgstr "无线名称基准前缀,系统将自动识别并为 2.4G 追加 _2.4G、5G 追加 _5G、6G 追加 _6G。"
|
||||
|
||||
msgid "Wireless Password (Key)"
|
||||
msgstr "无线网络密码"
|
||||
msgstr "无线密码"
|
||||
|
||||
msgid "Leave empty for open network or 8+ characters"
|
||||
msgstr "留空为开放无密码网络,或输入 8 位以上密码"
|
||||
msgstr "留空表示无密码开放网络,或输入至少 8 位密码"
|
||||
|
||||
msgid "URL Shortcuts"
|
||||
msgstr "快捷方式列表"
|
||||
msgid "Wireless key must be between 8 and 64 characters long"
|
||||
msgstr "无线密码长度必须在 8 到 64 个字符之间"
|
||||
|
||||
msgid "Applying Changes"
|
||||
msgstr "正在应用修改"
|
||||
msgid "Shortcut"
|
||||
msgstr "快捷短语"
|
||||
|
||||
msgid "Applying modified settings: %s..."
|
||||
msgstr "正在应用已修改的设置项: %s..."
|
||||
msgid "Shortcut phrase cannot be empty"
|
||||
msgstr "快捷短语不能为空"
|
||||
|
||||
msgid "No settings were modified. Services remain uninterrupted."
|
||||
msgstr "未检测到任何修改,无需保存或重启服务。"
|
||||
msgid "Only alphanumeric characters, dashes and underscores are allowed"
|
||||
msgstr "仅允许输入字母、数字、破折号 (-) 和下划线 (_)"
|
||||
|
||||
msgid "Settings successfully applied: %s"
|
||||
msgstr "已成功应用并重载修改项: %s"
|
||||
msgid "Target URL"
|
||||
msgstr "目标网址 (URL)"
|
||||
|
||||
msgid "Failed to apply settings: %s"
|
||||
msgstr "应用设置失败: %s"
|
||||
msgid "Please enter a valid URL starting with http:// or https://"
|
||||
msgstr "请输入以 http:// 或 https:// 开头的有效网址"
|
||||
|
||||
msgid "Comments"
|
||||
msgstr "备注说明"
|
||||
|
||||
msgid "Optional"
|
||||
msgstr "可选"
|
||||
|
||||
msgid "There are no changes to apply"
|
||||
msgstr "没有需要应用的更改"
|
||||
|
||||
msgid "LAN IP Address Changed"
|
||||
msgstr "LAN 口 IP 地址已变更"
|
||||
|
||||
msgid "LAN IP changed to %s. Redirecting to the new address in %d seconds..."
|
||||
msgstr "LAN 口 IP 已变更为 %s。将在 %d 秒后自动跳转至新地址..."
|
||||
msgid "LAN IP has been changed to "
|
||||
msgstr "LAN 口 IP 地址已更改为 "
|
||||
|
||||
msgid "Applying changes without rollback. Redirecting to the new address in "
|
||||
msgstr "正在永久应用更改(已禁用回滚保护)。将在 "
|
||||
|
||||
msgid " seconds..."
|
||||
msgstr " 秒后自动跳转至新地址..."
|
||||
|
||||
msgid "提示:更改网段后,若未能自动加载新页面,请尝试重新插拔网线或断开重连 Wi-Fi,以获取新网段的 IP 地址。"
|
||||
msgstr "提示:更改网段后,若未能自动加载新页面,请尝试重新插拔网线或断开重连 Wi-Fi,以获取新网段的 IP 地址。"
|
||||
|
||||
msgid "Redirect Now"
|
||||
msgstr "立即跳转"
|
||||
Reference in new issue
Block a user