🎉 Sync 2026-09-21 03:58:45

This commit is contained in:
github-actions[bot] committed 2026-09-21 03:58:45 +08:00
1 parent da26c8c705
commit b8539f4af6
8 files changed
+443 -74

No files matched your search

+3 -3
View File
@@ -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))
+3 -3
View File
@@ -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>
+21
View File
@@ -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');
}
});
},
+79 -44
View File
@@ -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 "立即跳转"