🤞 Sync 2026-09-06 22:50:31

This commit is contained in:
github-actions[bot] committed 2026-09-06 22:50:31 +08:00
1 parent bb7d626e94
commit 64c17e592d
115 files changed
+8599 -1391

No files matched your search

+1 -1
View File
@@ -15,7 +15,7 @@ AGENTFLOW_HASH_aarch64:=6d733ebc49b095bf8670a1b655a13bb8ad98c90ced19701b67d5cdd4
PKG_NAME:=agentflow
PKG_VERSION:=0.3.0
PKG_RELEASE:=7
PKG_RELEASE:=8
AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH)
AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH))
@@ -1,84 +0,0 @@
# AgentFlow 共享 Runtime HOME 实施里程碑
## 目标架构
AgentFlow 的应用数据和设备级开发运行时分离:
- AgentFlow 数据目录:`<quickstart.conf_dir>/AgentFlow`
- 共享 Runtime HOME:`<quickstart.conf_dir>/Runtime/home`
共享 Runtime HOME 由 `mise` 包提供公共 helper 初始化,AgentFlow 和后续运行时感知应用复用同一套 `HOME`、XDG、`MISE_*` 和 `PATH`。
## Milestone 1:公共 Runtime HOME 合约
目标:
- `mise` 包安装 `/etc/config/mise`。
- `mise` 包安装 `/lib/functions/istore_runtime.sh`。
- helper 自动从 `quickstart.main.conf_dir` 推导 `<conf_dir>/Runtime/home`。
- helper 支持 `ISTORE_RUNTIME_CONF_DIR` 兜底,便于应用从自身 Configs 目录派生 Runtime。
- helper 统一导出 `HOME`、`XDG_DATA_HOME`、`XDG_CACHE_HOME`、`XDG_CONFIG_HOME`、`XDG_STATE_HOME`、`MISE_DATA_DIR`、`MISE_CACHE_DIR`、`MISE_CONFIG_DIR`、`MISE_STATE_DIR` 和 `PATH`。
验收:
- 新安装 `mise` 后,存在 `/etc/config/mise` 和 `/lib/functions/istore_runtime.sh`。
- 已配置 quickstart 时,初始化路径为 `<quickstart.conf_dir>/Runtime/home`。
- 未配置 quickstart 但应用提供 `ISTORE_RUNTIME_CONF_DIR` 时,初始化路径为 `$ISTORE_RUNTIME_CONF_DIR/Runtime/home`。
## Milestone 2:AgentFlow 接入共享 Runtime HOME
目标:
- AgentFlow 不再使用 `$data_dir/global` 作为 `HOME` 或 mise shim 根。
- AgentFlow 启动时调用 `istore_runtime_export_env`。
- `AGENT_FLOW_DATA` 继续固定在 AgentFlow 数据目录下的 `data` 子目录。
- AgentFlow 服务进程及其子进程继承共享 Runtime HOME。
验收:
- `/etc/init.d/agentflow start` 后,AgentFlow 进程环境中 `HOME=<conf_dir>/Runtime/home`。
- `MISE_DATA_DIR=$HOME/.local/share/mise`。
- `PATH` 以 `$MISE_DATA_DIR/shims:$HOME/.local/bin` 开头。
- AgentFlow 数据库仍写入 `<conf_dir>/AgentFlow/data/db/db.sqlite`。
## Milestone 3:LuCI 可见性
目标:
- AgentFlow LuCI 页面继续配置 AgentFlow 数据目录。
- 页面展示推导出的共享 Runtime HOME。
- 如果 `mise.runtime_dir` 被显式设置,页面展示其对应的 `home`。
验收:
- quickstart `conf_dir=/mnt/vio3-1/Configs` 时,页面显示 `/mnt/vio3-1/Configs/Runtime/home`。
- AgentFlow 数据目录仍显示 `/mnt/vio3-1/Configs/AgentFlow`。
## Milestone 4:迁移与兼容
目标:
- 新安装默认使用共享 Runtime HOME。
- 老安装中已有 `$data_dir/global/.local/share/mise` 时,不自动移动大目录。
- 后续提供显式迁移命令或 LuCI 操作。
验收:
- 升级不会删除或搬移旧 runtime 数据。
- 用户确认迁移后,旧 mise data 可迁移到 `<conf_dir>/Runtime/home/.local/share/mise`。
- 迁移失败不会影响 AgentFlow 私有数据。
## Milestone 5:AgentFlow 后端项目环境集成
目标:
- OpenWrt 层面完成共享 HOME 后,再在 AgentFlow 后端引入项目环境 wrapper。
- coding agent 进程使用 `mise exec -- <agent> ...` 应用项目 `mise.toml`。
- 运行期设置 `MISE_EXEC_AUTO_INSTALL=0`,缺失工具只在准备阶段安装。
验收:
- 单 repo 项目中,agent 看到项目声明的 Node/Python/Go 版本。
- 没有隐式运行期下载。
- Codex/Claude/Kimi 等 agent 凭据继续来自共享 Runtime HOME。
- 取消任务时 wrapper 和 agent 子进程都能退出。
+8 -27
View File
@@ -10,22 +10,6 @@ get_config() {
config_get port "$1" port "9000"
}
load_runtime_env() {
local conf_dir
[ -n "$data_dir" ] || return 1
conf_dir="$(dirname "$data_dir")"
export ISTORE_RUNTIME_CONF_DIR="$conf_dir"
if [ -r /lib/functions/istore_runtime.sh ]; then
. /lib/functions/istore_runtime.sh
istore_runtime_export_env
return $?
fi
return 1
}
start_service() {
config_load agentflow
config_foreach get_config agentflow
@@ -35,11 +19,16 @@ start_service() {
logger -t agentflow "missing executable: /usr/sbin/agentflow"
return 1
}
mkdir -p "$data_dir" "$data_dir/data" || return 1
if ! load_runtime_env; then
logger -t agentflow "shared runtime home is not configured"
[ -r /lib/functions/mise.sh ] || {
logger -t agentflow "missing mise environment helper"
return 1
}
. /lib/functions/mise.sh
if ! istore_runtime_env; then
logger -t agentflow "failed to initialize mise environment"
return 1
fi
mkdir -p "$data_dir" "$data_dir/data" || return 1
logger -t agentflow "starting AgentFlow on $host:$port"
procd_open_instance
@@ -49,14 +38,6 @@ start_service() {
"AGENT_FLOW_HOST=$host" \
"AGENT_FLOW_PORT=$port" \
"HOME=$HOME" \
"XDG_DATA_HOME=$XDG_DATA_HOME" \
"XDG_CACHE_HOME=$XDG_CACHE_HOME" \
"XDG_CONFIG_HOME=$XDG_CONFIG_HOME" \
"XDG_STATE_HOME=$XDG_STATE_HOME" \
"MISE_DATA_DIR=$MISE_DATA_DIR" \
"MISE_CACHE_DIR=$MISE_CACHE_DIR" \
"MISE_CONFIG_DIR=$MISE_CONFIG_DIR" \
"MISE_STATE_DIR=$MISE_STATE_DIR" \
"PATH=$PATH"
procd_set_param stdout 1
procd_set_param stderr 1
@@ -1,80 +0,0 @@
from pathlib import Path
import unittest
THIS_FILE = Path(__file__).resolve()
def read(relative):
relative_path = Path(relative)
package = relative_path.parts[0]
app = package[9:] if package.startswith("luci-app-") else package
remainder = Path(*relative_path.parts[1:])
candidates = [
THIS_FILE.parents[index] / relative_path
for index in range(2, min(len(THIS_FILE.parents), 6))
]
candidates.append(THIS_FILE.parents[3] / app / package / remainder)
for candidate in candidates:
if candidate.exists():
return candidate.read_text()
searched = "\n".join(str(candidate) for candidate in candidates)
raise FileNotFoundError(f"{relative} not found; searched:\n{searched}")
class MiseRuntimeContractTest(unittest.TestCase):
def test_mise_package_installs_shared_runtime_contract(self):
makefile = read("mise/Makefile")
config = read("mise/files/mise.config")
defaults = read("mise/files/mise.uci-default")
helper = read("mise/files/istore_runtime.sh")
self.assertIn("/etc/config/mise", makefile)
self.assertIn("/lib/functions/istore_runtime.sh", makefile)
self.assertIn("$(INSTALL_BIN) ./files/mise.uci-default", makefile)
self.assertIn("[ -f /etc/uci-defaults/mise ]", makefile)
self.assertIn("config mise 'main'", config)
self.assertIn("option runtime_dir ''", config)
self.assertIn("option auto_discover '1'", config)
self.assertIn(". /lib/functions/istore_runtime.sh", defaults)
self.assertIn("istore_runtime_init", defaults)
self.assertIn("config_get conf_dir main conf_dir", helper)
self.assertIn('"$conf_dir" "$ISTORE_RUNTIME_DEFAULT_SUBDIR"', helper)
self.assertIn("ISTORE_RUNTIME_HOME_SUBDIR=\"home\"", helper)
self.assertIn("ISTORE_RUNTIME_CONF_DIR", helper)
self.assertIn('export HOME="$runtime_home"', helper)
self.assertIn('export XDG_DATA_HOME="$HOME/.local/share"', helper)
self.assertIn('export MISE_DATA_DIR="$XDG_DATA_HOME/mise"', helper)
self.assertIn('export PATH="$MISE_DATA_DIR/shims:$HOME/.local/bin:$PATH"', helper)
def test_agentflow_consumes_shared_runtime_home(self):
init = read("agentflow/files/agentflow.init")
cbi = read("luci-app-agentflow/luasrc/model/cbi/agentflow.lua")
model = read("luci-app-agentflow/luasrc/model/agentflow.lua")
translations = read("luci-app-agentflow/po/zh-cn/agentflow.po")
self.assertIn(". /lib/functions/istore_runtime.sh", init)
self.assertIn("istore_runtime_export_env", init)
self.assertIn('AGENT_FLOW_DATA=$data_dir/data', init)
self.assertIn('HOME=$HOME', init)
self.assertIn('MISE_DATA_DIR=$MISE_DATA_DIR', init)
self.assertNotIn("$data_dir/global", init)
self.assertNotIn(".local/share/mise/shims:$PATH", init)
self.assertIn('translate("Shared runtime home")', cbi)
self.assertIn("agentflow_model.runtime_home", cbi)
self.assertIn('return runtime_dir .. "/home"', model)
self.assertIn('return conf_dir .. "/Runtime"', model)
self.assertIn('msgid "Shared runtime home"', translations)
self.assertIn('msgstr "共享运行时 HOME"', translations)
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -7,7 +7,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=airconnect
PKG_VERSION:=1.11.2
PKG_VERSION:=1.11.3
PKG_RELEASE=1
PKG_SOURCE:=AirConnect-$(PKG_VERSION).zip
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=AgentFlow
PKG_VERSION:=1.0.0
PKG_RELEASE:=4
PKG_RELEASE:=5
LUCI_DEPENDS:=+agentflow +luci-compat
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0
+5 -28
View File
@@ -45,37 +45,14 @@ agentflow.find_paths = function(blocks, home_dirs)
return paths, default_path
end
local dirname = function(path)
path = (path or ""):match("^%s*(.-)%s*$")
path = path:gsub("/+$", "")
return path:match("^(.*)/[^/]+$") or ""
end
agentflow.runtime_dir = function(data_dir, home_dirs)
local uci = require "luci.model.uci".cursor()
local configured = uci:get_first("mise", "mise", "runtime_dir", "")
if configured ~= nil and configured ~= "" then
return configured
end
local conf_dir = dirname(data_dir)
if conf_dir == "" then
conf_dir = home_dirs["Configs"]
end
agentflow.runtime_dir = function(data_dir)
data_dir = (data_dir or ""):match("^%s*(.-)%s*$"):gsub("/+$", "")
local conf_dir = data_dir:match("^(.*)/[^/]+$")
if conf_dir == nil or conf_dir == "" then
return ""
return nil
end
return conf_dir .. "/Runtime"
end
agentflow.runtime_home = function(data_dir, home_dirs)
local runtime_dir = agentflow.runtime_dir(data_dir, home_dirs)
if runtime_dir == nil or runtime_dir == "" then
return ""
end
return runtime_dir .. "/home"
return conf_dir .. "/Runtime/home"
end
return agentflow
@@ -34,15 +34,22 @@ for _, val in pairs(paths) do
end
data_dir.default = default_path
local runtime_home = s:option(DummyValue, "_runtime_home", translate("Shared runtime home"))
runtime_home.description = translate("AgentFlow and other runtime-aware applications share this HOME. It is derived from the selected Configs directory unless mise has an explicit runtime directory.")
function runtime_home.cfgvalue(self, section)
local selected_data_dir = m.uci:get("agentflow", section, "data_dir") or default_path
local path = agentflow_model.runtime_home(selected_data_dir, home)
if path == "" then
return translate("Not configured")
function m.on_after_commit(self)
local uci = require "luci.model.uci".cursor()
local runtime_dir = uci:get("mise", "main", "runtime_dir")
if runtime_dir ~= nil and runtime_dir ~= "" then
return
end
local saved_data_dir = uci:get_first("agentflow", "agentflow", "data_dir", "")
runtime_dir = agentflow_model.runtime_dir(saved_data_dir)
if runtime_dir == nil then
return
end
if uci:set("mise", "main", "runtime_dir", runtime_dir) then
uci:commit("mise")
end
return path
end
local port = s:option(Value, "port", translate("Listen port"))
-9
View File
@@ -43,15 +43,6 @@ msgstr "请选择硬盘作为数据目录"
msgid "Required. AgentFlow stores its configuration, database and workspace data under this directory."
msgstr "必需。AgentFlow 在此目录中保存配置、数据库和工作区数据。"
msgid "Shared runtime home"
msgstr "共享运行时 HOME"
msgid "AgentFlow and other runtime-aware applications share this HOME. It is derived from the selected Configs directory unless mise has an explicit runtime directory."
msgstr "AgentFlow 和其他运行时感知应用共用此 HOME。除非 mise 显式设置了运行时目录,否则它会从所选 Configs 目录推导。"
msgid "Not configured"
msgstr "未配置"
msgid "Status"
msgstr "状态"
+2 -2
View File
@@ -10,8 +10,8 @@ LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +curl +bandix
PKG_MAINTAINER:=timsaya
PKG_VERSION:=0.12.10
PKG_RELEASE:=9
PKG_VERSION:=0.12.11
PKG_RELEASE:=10
include $(TOPDIR)/feeds/luci/luci.mk
@@ -470,6 +470,26 @@ var callSetDefaultRateLimit = rpc.declare({
expect: {}
});
var callGetTrafficQuotas = rpc.declare({
object: 'luci.bandix',
method: 'getTrafficQuotas',
expect: {}
});
var callSetTrafficQuota = rpc.declare({
object: 'luci.bandix',
method: 'setTrafficQuota',
params: ['mac', 'minute_bytes', 'hourly_bytes', 'daily_bytes', 'weekly_bytes', 'monthly_bytes', 'total_bytes'],
expect: {}
});
var callDeleteTrafficQuota = rpc.declare({
object: 'luci.bandix',
method: 'deleteTrafficQuota',
params: ['mac'],
expect: {}
});
return view.extend({
load: function () {
return Promise.all([
@@ -1902,6 +1922,74 @@ return view.extend({
.schedule-rules-info {
}
.traffic-quota-section {
margin-top: 18px;
padding-top: 16px;
border-top: 1px solid ${scheme === 'dark' ? 'rgba(255,255,255,0.16)' : 'rgba(0,0,0,0.12)'};
}
.traffic-quota-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 6px;
}
.traffic-quota-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 14px;
margin-top: 12px;
}
.traffic-quota-input-row {
display: flex;
gap: 6px;
}
.traffic-quota-input-row .form-input { min-width: 0; flex: 1; }
.traffic-quota-input-row .cbi-input-select { width: 82px; flex-shrink: 0; }
.traffic-quota-usage {
margin-top: 12px;
padding: 10px 12px;
border-radius: 6px;
background: ${scheme === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'};
font-size: 0.8125rem;
}
.traffic-quota-usage-row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 2px 0;
}
.traffic-quota-status {
font-size: 0.75rem;
font-weight: 600;
border-radius: 999px;
padding: 2px 8px;
color: #fff;
background: #10b981;
}
.traffic-quota-status.blocked { background: #ef4444; }
.traffic-quota-status.unconfigured { background: #6b7280; }
.traffic-quota-cell {
margin-top: 6px;
padding-top: 6px;
border-top: 1px dashed ${scheme === 'dark' ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.14)'};
font-size: 0.75rem;
}
.traffic-quota-blocked-badge {
display: inline-block;
margin-left: 6px;
padding: 1px 6px;
border-radius: 999px;
color: #fff;
background: #ef4444;
font-size: 0.6875rem;
font-weight: 600;
}
@media (max-width: 600px) {
.traffic-quota-grid { grid-template-columns: 1fr; }
}
/* 统计区域样式 */
.traffic-stats-container {
@@ -2904,7 +2992,7 @@ return view.extend({
E('th', {}, _('Device Info')),
E('th', {}, _('LAN Traffic')),
E('th', {}, _('WAN Traffic')),
E('th', {}, _('Rate Limit')),
E('th', {}, _('Rules / Quota')),
E('th', {}, _('Actions'))
])
]),
@@ -3171,7 +3259,30 @@ return view.extend({
});
}
// 创建限速设置模态框
function createQuotaInput(period, label) {
return E('div', { 'class': 'form-group', 'style': 'margin-bottom: 0;' }, [
E('label', { 'class': 'form-label' }, label),
E('div', { 'class': 'traffic-quota-input-row' }, [
E('input', {
'type': 'number',
'min': '0',
'step': '0.01',
'class': 'form-input',
'id': 'traffic-quota-' + period + '-value',
'placeholder': '0'
}),
E('select', { 'class': 'cbi-input-select', 'id': 'traffic-quota-' + period + '-unit' }, [
E('option', { 'value': '1' }, 'B'),
E('option', { 'value': '1024' }, 'KB'),
E('option', { 'value': '1048576' }, 'MB'),
E('option', { 'value': '1073741824' }, 'GB'),
E('option', { 'value': '1099511627776' }, 'TB')
])
])
]);
}
// 创建设备设置模态框
var bandixModal = E('div', { 'class': 'bandix-modal-overlay', 'id': 'rate-limit-bandix-modal' }, [
E('div', { 'class': 'modal-content bandix-modal' }, [
E('div', { 'class': 'bandix-modal-body', }, [
@@ -3211,6 +3322,46 @@ return view.extend({
document.body.appendChild(bandixModal);
// 创建独立的流量配额模态框
var trafficQuotaModal = E('div', { 'class': 'bandix-modal-overlay', 'id': 'traffic-quota-bandix-modal' }, [
E('div', { 'class': 'modal-content bandix-modal' }, [
E('div', { 'class': 'bandix-modal-header' }, [
E('h3', { 'class': 'bandix-modal-title' }, _('Traffic Quota'))
]),
E('div', { 'class': 'bandix-modal-body' }, [
E('div', { 'class': 'device-summary', 'id': 'traffic-quota-device-summary' }),
E('div', { 'class': 'traffic-quota-section', 'style': 'margin-top: 0; padding-top: 0; border-top: none;' }, [
E('div', { 'class': 'traffic-quota-header' }, [
E('div', { 'style': 'font-weight: 600;' }, _('Quota Status')),
E('span', { 'class': 'traffic-quota-status', 'id': 'traffic-quota-status' }, _('Not configured'))
]),
E('div', { 'style': 'font-size: 0.75rem; opacity: 0.7;' },
_('WAN upload and download are counted together. Enter 0 for unlimited.')),
E('div', { 'class': 'traffic-quota-grid' }, [
createQuotaInput('minute', _('Minute Quota')),
createQuotaInput('hourly', _('Hourly Quota')),
createQuotaInput('daily', _('Daily Quota')),
createQuotaInput('weekly', _('Weekly Quota')),
createQuotaInput('monthly', _('Monthly Quota')),
createQuotaInput('total', _('Lifetime Quota'))
]),
E('div', { 'class': 'traffic-quota-usage', 'id': 'traffic-quota-usage' }, [
E('div', { 'style': 'opacity: 0.7;' }, _('No quota usage data'))
]),
E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px;' }, [
E('button', { 'class': 'cbi-button cbi-button-negative', 'id': 'traffic-quota-delete-btn', 'style': 'display: none;' }, _('Delete Quota')),
E('button', { 'class': 'cbi-button cbi-button-positive', 'id': 'traffic-quota-save-btn' }, _('Save Quota'))
])
])
]),
E('div', { 'class': 'bandix-modal-footer' }, [
E('button', { 'class': 'cbi-button cbi-button-reset', 'id': 'traffic-quota-modal-close' }, _('Close'))
])
])
]);
document.body.appendChild(trafficQuotaModal);
// 创建添加规则模态框
var addRuleModal = E('div', { 'class': 'bandix-modal-overlay', 'id': 'add-rule-bandix-modal' }, [
E('div', { 'class': 'modal-content bandix-modal' }, [
@@ -3946,9 +4097,17 @@ return view.extend({
// 模态框事件处理
var currentDevice = null;
var currentQuotaDevice = null;
var quotaFormDirty = false;
var editingRule = null;
var showRateLimitModal;
trafficQuotaModal.querySelectorAll('.traffic-quota-input-row input, .traffic-quota-input-row select').forEach(function (input) {
input.addEventListener(input.tagName === 'SELECT' ? 'change' : 'input', function () {
quotaFormDirty = true;
});
});
// 显示模态框
showRateLimitModal = function (device) {
currentDevice = device;
@@ -3994,6 +4153,33 @@ return view.extend({
}, 300);
}
function showTrafficQuotaModal(device) {
currentQuotaDevice = device;
quotaFormDirty = false;
var modal = document.getElementById('traffic-quota-bandix-modal');
var deviceSummary = document.getElementById('traffic-quota-device-summary');
var modalContent = [
E('div', { 'class': 'device-summary-name' }, device.host || device.ip4),
E('div', { 'class': 'device-summary-details' }, device.ip4 + ' (' + device.mac + ')')
];
var modalBadges = buildDeviceUplinkChBadges(device);
if (modalBadges.length) {
modalContent.push(E('div', { 'class': 'device-summary-badges device-uplink-badges-wrap' }, modalBadges));
}
deviceSummary.innerHTML = E('div', {}, modalContent).innerHTML;
renderTrafficQuotaForm(getTrafficQuotaForDevice(device.mac), false);
modal.classList.add('show');
loadTrafficQuotaForm();
}
function hideTrafficQuotaModal() {
document.getElementById('traffic-quota-bandix-modal').classList.remove('show');
setTimeout(function () {
currentQuotaDevice = null;
quotaFormDirty = false;
}, 300);
}
// 加载定时限速规则列表
function loadScheduleRules() {
if (!currentDevice) return;
@@ -4163,8 +4349,60 @@ return view.extend({
// 绑定 hostname 保存按钮事件
document.getElementById('hostname-save-btn').addEventListener('click', saveHostname);
document.getElementById('traffic-quota-save-btn').addEventListener('click', function () {
if (!currentQuotaDevice) return;
var mac = currentQuotaDevice.mac;
var values = ['minute', 'hourly', 'daily', 'weekly', 'monthly', 'total'].map(readQuotaInput);
if (values.some(function (value) { return value === null; })) {
ui.addNotification(null, E('p', {}, _('Quota values must be non-negative and within the supported range.')), 'error');
return;
}
var button = this;
button.disabled = true;
setTrafficQuotaFormDisabled(true);
callSetTrafficQuota(mac, values[0], values[1], values[2], values[3], values[4], values[5]).then(function (result) {
if (result && result.success === false) throw new Error(result.error || _('Failed to save traffic quota'));
return fetchAllTrafficQuotas(true);
}).then(function () {
if (currentQuotaDevice && currentQuotaDevice.mac === mac) {
quotaFormDirty = false;
renderTrafficQuotaForm(getTrafficQuotaForDevice(mac), false);
}
updateDeviceData();
}).catch(function (error) {
ui.addNotification(null, E('p', {}, _('Failed to save traffic quota') + ': ' + (error.message || error)), 'error');
}).finally(function () {
button.disabled = false;
setTrafficQuotaFormDisabled(false);
});
});
document.getElementById('traffic-quota-delete-btn').addEventListener('click', function () {
if (!currentQuotaDevice) return;
var mac = currentQuotaDevice.mac;
showConfirmDialog(
_('Delete Traffic Quota'),
_('Delete this quota and all of its accumulated usage?'),
function () {
callDeleteTrafficQuota(mac).then(function (result) {
if (result && result.success === false) throw new Error(result.error || _('Failed to delete traffic quota'));
return fetchAllTrafficQuotas(true);
}).then(function () {
if (currentQuotaDevice && currentQuotaDevice.mac === mac) {
quotaFormDirty = false;
renderTrafficQuotaForm(null, false);
}
updateDeviceData();
}).catch(function (error) {
ui.addNotification(null, E('p', {}, _('Failed to delete traffic quota') + ': ' + (error.message || error)), 'error');
});
}
);
});
// 绑定关闭按钮事件
document.getElementById('bandix-modal-close').addEventListener('click', hideRateLimitModal);
document.getElementById('traffic-quota-modal-close').addEventListener('click', hideTrafficQuotaModal);
// 历史趋势:状态与工具
var latestDevices = [];
@@ -4175,6 +4413,154 @@ return view.extend({
var allScheduleRules = []; // 存储所有设备的定时限速规则
var isScheduleRulesLoading = false; // 防止轮询重入
var allTrafficQuotas = [];
var trafficQuotasRequest = null;
var trafficQuotasLoadError = null;
function fetchAllTrafficQuotas(forceRefresh) {
if (trafficQuotasRequest) {
if (!forceRefresh) return trafficQuotasRequest;
return trafficQuotasRequest.then(function () {
return fetchAllTrafficQuotas(true);
});
}
trafficQuotasRequest = callGetTrafficQuotas().then(function (res) {
if (!res || res.success === false || res.status === 'error') {
throw new Error((res && (res.error || res.message)) || _('Failed to load traffic quotas'));
}
var quotas = [];
if (res && res.data && Array.isArray(res.data.quotas)) {
quotas = res.data.quotas;
} else if (res && Array.isArray(res.quotas)) {
quotas = res.quotas;
}
allTrafficQuotas = quotas;
trafficQuotasLoadError = null;
return quotas;
}).catch(function (error) {
console.error('Failed to fetch traffic quotas:', error);
allTrafficQuotas = [];
trafficQuotasLoadError = error && error.message ? error.message : _('Failed to load traffic quotas');
return [];
}).finally(function () {
trafficQuotasRequest = null;
});
return trafficQuotasRequest;
}
function getTrafficQuotaForDevice(mac) {
for (var i = 0; i < allTrafficQuotas.length; i++) {
if (allTrafficQuotas[i] && allTrafficQuotas[i].mac === mac) {
return allTrafficQuotas[i];
}
}
return null;
}
function quotaPeriods(quota) {
return [
{ key: 'minute', label: _('Minute'), limit: quota.minute_bytes || 0, used: quota.minute_used_bytes || 0 },
{ key: 'hourly', label: _('Hourly'), limit: quota.hourly_bytes || 0, used: quota.hourly_used_bytes || 0 },
{ key: 'daily', label: _('Daily'), limit: quota.daily_bytes || 0, used: quota.daily_used_bytes || 0 },
{ key: 'weekly', label: _('Weekly'), limit: quota.weekly_bytes || 0, used: quota.weekly_used_bytes || 0 },
{ key: 'monthly', label: _('Monthly'), limit: quota.monthly_bytes || 0, used: quota.monthly_used_bytes || 0 },
{ key: 'total', label: _('Lifetime'), limit: quota.total_bytes || 0, used: quota.total_used_bytes || 0 }
];
}
function setQuotaInput(period, bytes) {
var valueInput = document.getElementById('traffic-quota-' + period + '-value');
var unitSelect = document.getElementById('traffic-quota-' + period + '-unit');
if (!valueInput || !unitSelect) return;
var units = [1099511627776, 1073741824, 1048576, 1024, 1];
var unit = period === 'minute' ? 1048576 : 1073741824;
if (bytes > 0) {
for (var i = 0; i < units.length; i++) {
if (bytes >= units[i] && bytes % units[i] === 0) {
unit = units[i];
break;
}
}
}
unitSelect.value = String(unit);
valueInput.value = bytes > 0 ? String(bytes / unit) : '0';
}
function readQuotaInput(period) {
var value = Number(document.getElementById('traffic-quota-' + period + '-value').value || 0);
var unit = Number(document.getElementById('traffic-quota-' + period + '-unit').value || 1);
var bytes = Math.round(value * unit);
if (!Number.isFinite(bytes) || bytes < 0 || !Number.isSafeInteger(bytes)) return null;
return bytes;
}
function setTrafficQuotaFormDisabled(disabled) {
trafficQuotaModal.querySelectorAll('.traffic-quota-input-row input, .traffic-quota-input-row select').forEach(function (input) {
input.disabled = disabled;
});
}
function renderTrafficQuotaForm(quota, preserveInputs) {
if (!preserveInputs) {
var periods = ['minute', 'hourly', 'daily', 'weekly', 'monthly', 'total'];
periods.forEach(function (period) {
setQuotaInput(period, quota ? (quota[period + '_bytes'] || 0) : 0);
});
}
var status = document.getElementById('traffic-quota-status');
var usage = document.getElementById('traffic-quota-usage');
var deleteBtn = document.getElementById('traffic-quota-delete-btn');
if (!status || !usage || !deleteBtn) return;
status.classList.toggle('blocked', !!(quota && quota.blocked));
status.classList.toggle('unconfigured', !quota);
status.textContent = !quota ? (trafficQuotasLoadError ? _('Unavailable') : _('Not configured')) : (quota.blocked ? _('Blocked') : _('Active'));
deleteBtn.style.display = quota ? '' : 'none';
usage.innerHTML = '';
if (!quota) {
usage.appendChild(E('div', { 'style': 'opacity: 0.7;' }, trafficQuotasLoadError || _('Usage starts when a quota is first saved.')));
return;
}
quotaPeriods(quota).forEach(function (period) {
usage.appendChild(E('div', { 'class': 'traffic-quota-usage-row' }, [
E('span', {}, period.label),
E('span', {}, formatSize(period.used) + ' / ' + (period.limit > 0 ? formatSize(period.limit) : _('Unlimited')))
]));
});
}
function loadTrafficQuotaForm() {
if (!currentQuotaDevice) return Promise.resolve();
var mac = currentQuotaDevice.mac;
renderTrafficQuotaForm(getTrafficQuotaForDevice(mac), quotaFormDirty);
return fetchAllTrafficQuotas().then(function () {
if (currentQuotaDevice && currentQuotaDevice.mac === mac) {
renderTrafficQuotaForm(getTrafficQuotaForDevice(mac), quotaFormDirty);
}
});
}
function buildTrafficQuotaCell(mac) {
var quota = getTrafficQuotaForDevice(mac);
if (!quota) return null;
var periods = quotaPeriods(quota).filter(function (period) { return period.limit > 0; });
var mostUsed = null;
periods.forEach(function (period) {
var ratio = period.used / period.limit;
if (!mostUsed || ratio > mostUsed.ratio) mostUsed = { period: period, ratio: ratio };
});
var text = quota.blocked ? _('Quota exceeded') : _('Quota active');
if (mostUsed) {
text = mostUsed.period.label + ': ' + formatSize(mostUsed.period.used) + ' / ' + formatSize(mostUsed.period.limit);
}
return E('div', {
'class': 'traffic-quota-cell',
'style': quota.blocked ? 'color: #ef4444; font-weight: 600;' : '',
'title': quota.blocked ? _('WAN traffic is blocked because a quota was reached.') : _('Traffic Quota')
}, text);
}
// 获取所有定时限速规则
function fetchAllScheduleRules() {
if (isScheduleRulesLoading) return Promise.resolve();
@@ -5862,7 +6248,7 @@ return view.extend({
createSortableHeader(_('Device Info'), 'online'),
createSplitHeader(_('LAN Traffic'), 'lan_speed', 'lan_traffic'),
createSplitHeader(_('WAN Traffic'), 'wan_speed', 'wan_traffic'),
E('th', {}, _('Schedule Rules')),
E('th', {}, _('Rules / Quota')),
E('th', {}, _('Actions'))
])
]),
@@ -5890,9 +6276,9 @@ return view.extend({
// 填充数据
filteredDevices.forEach(function (device) {
var isOnline = isDeviceOnline(device);
var deviceQuota = getTrafficQuotaForDevice(device.mac);
// 默认使用窄主题样式
var buttonText = '⚙';
var buttonText = 'R';
var actionButton = E('button', {
'class': 'cbi-button cbi-button-action',
@@ -5903,6 +6289,15 @@ return view.extend({
showRateLimitModal(device);
});
var quotaButton = E('button', {
'class': 'cbi-button cbi-button-neutral',
'title': _('Traffic Quota')
}, 'Q');
quotaButton.addEventListener('click', function () {
showTrafficQuotaModal(device);
});
var deleteButton = E('button', {
'class': 'cbi-button cbi-button-reset',
'title': _('Delete Device')
@@ -5935,7 +6330,10 @@ return view.extend({
E('span', {
'class': 'device-status ' + (isOnline ? 'online' : 'offline')
}),
device.host || '-'
device.host || '-',
deviceQuota && deviceQuota.blocked
? E('span', { 'class': 'traffic-quota-blocked-badge' }, _('Quota blocked'))
: ''
]),
E('div', { 'class': 'device-ip' }, [
device.conn ? E('span', {
@@ -6149,12 +6547,14 @@ return view.extend({
};
}
return E('td', {}, rulesInfo);
var quotaCell = buildTrafficQuotaCell(device.mac);
return E('td', {}, quotaCell ? [rulesInfo, quotaCell] : rulesInfo);
})(),
// 操作
E('td', { 'class': 'device-actions' }, [
actionButton,
quotaButton,
deleteButton
])
]);
@@ -6193,6 +6593,16 @@ return view.extend({
});
return cardActionBtn;
})(),
(function () {
var cardQuotaBtn = E('button', {
'class': 'cbi-button cbi-button-neutral',
'title': _('Traffic Quota')
}, 'Q');
cardQuotaBtn.addEventListener('click', function () {
showTrafficQuotaModal(device);
});
return cardQuotaBtn;
})(),
(function () {
var cardDeleteBtn = E('button', {
'class': 'cbi-button cbi-button-reset',
@@ -6279,6 +6689,14 @@ return view.extend({
rulesContent
]);
})(),
(function () {
var quotaCell = buildTrafficQuotaCell(device.mac);
if (!quotaCell) return '';
return E('div', { 'class': 'device-card-section' }, [
E('div', { 'class': 'device-card-section-label' }, _('Traffic Quota')),
quotaCell
]);
})(),
// LAN流量(直接显示,不需要展开/收起)
E('div', { 'class': 'device-card-section', 'style': 'margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(0, 0, 0, 0.1);' }, [
E('div', { 'class': 'device-card-section-label' }, _('LAN Traffic')),
@@ -7499,12 +7917,25 @@ return view.extend({
});
}, 5000);
// 轮询获取流量配额状态(每1秒)
poll.add(function () {
return fetchAllTrafficQuotas().then(function () {
if (window.__bandixRenderTable) window.__bandixRenderTable();
if (currentQuotaDevice) {
renderTrafficQuotaForm(getTrafficQuotaForDevice(currentQuotaDevice.mac), quotaFormDirty);
}
});
}, 1);
// Traffic Statistics 不再自动刷新,改为手动查询
// 立即执行一次,不等待轮询
updateDeviceData();
refreshWhitelistStatus();
fetchAllScheduleRules();
fetchAllTrafficQuotas().then(function () {
if (window.__bandixRenderTable) window.__bandixRenderTable();
});
// 初始化时间范围查询功能
setTimeout(function () {
+90
View File
@@ -1258,3 +1258,93 @@ msgstr "ID de programa ancla de egreso TCX"
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
msgstr "Se usa cuando tc_order es before/after. Debe ser un ID de programa de egreso valido en la misma interfaz."
msgid "Traffic Quota"
msgstr "Cuota de tráfico"
msgid "Quota Status"
msgstr "Estado de la cuota"
msgid "Minute Quota"
msgstr "Cuota por minuto"
msgid "Hourly Quota"
msgstr "Cuota por hora"
msgid "Daily Quota"
msgstr "Cuota diaria"
msgid "Weekly Quota"
msgstr "Cuota semanal"
msgid "Monthly Quota"
msgstr "Cuota mensual"
msgid "Lifetime Quota"
msgstr "Cuota total acumulada"
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
msgstr "La subida y la descarga WAN se contabilizan juntas. Introduzca 0 para no establecer límite."
msgid "Not configured"
msgstr "Sin configurar"
msgid "Blocked"
msgstr "Bloqueado"
msgid "Active"
msgstr "Activa"
msgid "No quota usage data"
msgstr "No hay datos de uso de la cuota"
msgid "Delete Quota"
msgstr "Eliminar cuota"
msgid "Save Quota"
msgstr "Guardar cuota"
msgid "Usage starts when a quota is first saved."
msgstr "El uso comienza a contabilizarse al guardar una cuota por primera vez."
msgid "Minute"
msgstr "Minuto"
msgid "Lifetime"
msgstr "Total acumulado"
msgid "Quota exceeded"
msgstr "Cuota excedida"
msgid "Quota active"
msgstr "Cuota activa"
msgid "WAN traffic is blocked because a quota was reached."
msgstr "El tráfico WAN está bloqueado porque se alcanzó una cuota."
msgid "Quota blocked"
msgstr "Bloqueado por cuota"
msgid "Quota values must be non-negative and within the supported range."
msgstr "Los valores de cuota deben ser no negativos y estar dentro del rango admitido."
msgid "Failed to save traffic quota"
msgstr "No se pudo guardar la cuota de tráfico"
msgid "Delete Traffic Quota"
msgstr "Eliminar cuota de tráfico"
msgid "Delete this quota and all of its accumulated usage?"
msgstr "¿Eliminar esta cuota y todo su uso acumulado?"
msgid "Failed to delete traffic quota"
msgstr "No se pudo eliminar la cuota de tráfico"
msgid "Rules / Quota"
msgstr "Reglas / Cuota"
msgid "Failed to load traffic quotas"
msgstr "No se pudieron cargar las cuotas de tráfico"
msgid "Unavailable"
msgstr "No disponible"
+90
View File
@@ -1258,3 +1258,93 @@ msgstr "ID du programme d ancrage egress TCX"
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
msgstr "Utilise lorsque tc_order est before/after. Doit etre un ID de programme egress valide sur la meme interface."
msgid "Traffic Quota"
msgstr "Quota de trafic"
msgid "Quota Status"
msgstr "État du quota"
msgid "Minute Quota"
msgstr "Quota par minute"
msgid "Hourly Quota"
msgstr "Quota par heure"
msgid "Daily Quota"
msgstr "Quota journalier"
msgid "Weekly Quota"
msgstr "Quota hebdomadaire"
msgid "Monthly Quota"
msgstr "Quota mensuel"
msgid "Lifetime Quota"
msgstr "Quota total cumulé"
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
msgstr "Le trafic montant et descendant WAN est comptabilisé ensemble. Saisissez 0 pour un trafic illimité."
msgid "Not configured"
msgstr "Non configuré"
msgid "Blocked"
msgstr "Bloqué"
msgid "Active"
msgstr "Actif"
msgid "No quota usage data"
msgstr "Aucune donnée d'utilisation du quota"
msgid "Delete Quota"
msgstr "Supprimer le quota"
msgid "Save Quota"
msgstr "Enregistrer le quota"
msgid "Usage starts when a quota is first saved."
msgstr "Le suivi de l'utilisation commence lors du premier enregistrement d'un quota."
msgid "Minute"
msgstr "Minute"
msgid "Lifetime"
msgstr "Total cumulé"
msgid "Quota exceeded"
msgstr "Quota dépassé"
msgid "Quota active"
msgstr "Quota actif"
msgid "WAN traffic is blocked because a quota was reached."
msgstr "Le trafic WAN est bloqué car un quota a été atteint."
msgid "Quota blocked"
msgstr "Blocage par quota"
msgid "Quota values must be non-negative and within the supported range."
msgstr "Les valeurs de quota doivent être positives ou nulles et respecter la plage prise en charge."
msgid "Failed to save traffic quota"
msgstr "Impossible d'enregistrer le quota de trafic"
msgid "Delete Traffic Quota"
msgstr "Supprimer le quota de trafic"
msgid "Delete this quota and all of its accumulated usage?"
msgstr "Supprimer ce quota et toutes ses données d'utilisation cumulées ?"
msgid "Failed to delete traffic quota"
msgstr "Impossible de supprimer le quota de trafic"
msgid "Rules / Quota"
msgstr "Règles / Quota"
msgid "Failed to load traffic quotas"
msgstr "Impossible de charger les quotas de trafic"
msgid "Unavailable"
msgstr "Indisponible"
+90
View File
@@ -1258,3 +1258,93 @@ msgstr "TCX egressアンカープログラムID"
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
msgstr "tc_orderがbefore/afterのときに使用します。同じインターフェース上の有効なegressプログラムIDである必要があります。"
msgid "Traffic Quota"
msgstr "トラフィッククォータ"
msgid "Quota Status"
msgstr "クォータの状態"
msgid "Minute Quota"
msgstr "1分あたりのクォータ"
msgid "Hourly Quota"
msgstr "1時間あたりのクォータ"
msgid "Daily Quota"
msgstr "1日あたりのクォータ"
msgid "Weekly Quota"
msgstr "1週間あたりのクォータ"
msgid "Monthly Quota"
msgstr "1か月あたりのクォータ"
msgid "Lifetime Quota"
msgstr "累計クォータ"
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
msgstr "WANのアップロードとダウンロードは合算されます。無制限にするには0を入力してください。"
msgid "Not configured"
msgstr "未設定"
msgid "Blocked"
msgstr "ブロック中"
msgid "Active"
msgstr "有効"
msgid "No quota usage data"
msgstr "クォータ使用量データはありません"
msgid "Delete Quota"
msgstr "クォータを削除"
msgid "Save Quota"
msgstr "クォータを保存"
msgid "Usage starts when a quota is first saved."
msgstr "クォータを初めて保存した時点から使用量の計測を開始します。"
msgid "Minute"
msgstr "1分"
msgid "Lifetime"
msgstr "累計"
msgid "Quota exceeded"
msgstr "クォータ超過"
msgid "Quota active"
msgstr "クォータ有効"
msgid "WAN traffic is blocked because a quota was reached."
msgstr "クォータに達したためWANトラフィックがブロックされています。"
msgid "Quota blocked"
msgstr "クォータによりブロック"
msgid "Quota values must be non-negative and within the supported range."
msgstr "クォータ値は0以上で、サポートされている範囲内である必要があります。"
msgid "Failed to save traffic quota"
msgstr "トラフィッククォータを保存できませんでした"
msgid "Delete Traffic Quota"
msgstr "トラフィッククォータを削除"
msgid "Delete this quota and all of its accumulated usage?"
msgstr "このクォータと累積された使用量をすべて削除しますか?"
msgid "Failed to delete traffic quota"
msgstr "トラフィッククォータを削除できませんでした"
msgid "Rules / Quota"
msgstr "ルール / クォータ"
msgid "Failed to load traffic quotas"
msgstr "トラフィッククォータを読み込めませんでした"
msgid "Unavailable"
msgstr "利用不可"
+90
View File
@@ -1258,3 +1258,93 @@ msgstr "ID programu kotwicy TCX egress"
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
msgstr "Uzywane, gdy tc_order to before/after. Musi to byc prawidlowe ID programu egress na tym samym interfejsie."
msgid "Traffic Quota"
msgstr "Limit transferu"
msgid "Quota Status"
msgstr "Stan limitu"
msgid "Minute Quota"
msgstr "Limit minutowy"
msgid "Hourly Quota"
msgstr "Limit godzinowy"
msgid "Daily Quota"
msgstr "Limit dzienny"
msgid "Weekly Quota"
msgstr "Limit tygodniowy"
msgid "Monthly Quota"
msgstr "Limit miesięczny"
msgid "Lifetime Quota"
msgstr "Łączny limit"
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
msgstr "Ruch wysyłany i pobierany przez WAN jest sumowany. Wpisz 0, aby wyłączyć limit."
msgid "Not configured"
msgstr "Nie skonfigurowano"
msgid "Blocked"
msgstr "Zablokowano"
msgid "Active"
msgstr "Aktywny"
msgid "No quota usage data"
msgstr "Brak danych o wykorzystaniu limitu"
msgid "Delete Quota"
msgstr "Usuń limit"
msgid "Save Quota"
msgstr "Zapisz limit"
msgid "Usage starts when a quota is first saved."
msgstr "Naliczanie użycia rozpoczyna się po pierwszym zapisaniu limitu."
msgid "Minute"
msgstr "Minuta"
msgid "Lifetime"
msgstr "Łącznie"
msgid "Quota exceeded"
msgstr "Limit przekroczony"
msgid "Quota active"
msgstr "Limit aktywny"
msgid "WAN traffic is blocked because a quota was reached."
msgstr "Ruch WAN jest zablokowany, ponieważ osiągnięto limit."
msgid "Quota blocked"
msgstr "Blokada przez limit"
msgid "Quota values must be non-negative and within the supported range."
msgstr "Wartości limitu muszą być nieujemne i mieścić się w obsługiwanym zakresie."
msgid "Failed to save traffic quota"
msgstr "Nie udało się zapisać limitu transferu"
msgid "Delete Traffic Quota"
msgstr "Usuń limit transferu"
msgid "Delete this quota and all of its accumulated usage?"
msgstr "Usunąć ten limit i wszystkie dane o naliczonym użyciu?"
msgid "Failed to delete traffic quota"
msgstr "Nie udało się usunąć limitu transferu"
msgid "Rules / Quota"
msgstr "Reguły / Limit"
msgid "Failed to load traffic quotas"
msgstr "Nie udało się wczytać limitów transferu"
msgid "Unavailable"
msgstr "Niedostępne"
+90
View File
@@ -1258,3 +1258,93 @@ msgstr "ID якорной программы TCX egress"
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
msgstr "Используется, когда tc_order имеет значение before/after. Должен быть корректный ID egress-программы на том же интерфейсе."
msgid "Traffic Quota"
msgstr "Квота трафика"
msgid "Quota Status"
msgstr "Состояние квоты"
msgid "Minute Quota"
msgstr "Минутная квота"
msgid "Hourly Quota"
msgstr "Часовая квота"
msgid "Daily Quota"
msgstr "Суточная квота"
msgid "Weekly Quota"
msgstr "Недельная квота"
msgid "Monthly Quota"
msgstr "Месячная квота"
msgid "Lifetime Quota"
msgstr "Общая квота"
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
msgstr "Входящий и исходящий трафик WAN учитываются вместе. Введите 0, чтобы отключить ограничение."
msgid "Not configured"
msgstr "Не настроено"
msgid "Blocked"
msgstr "Заблокировано"
msgid "Active"
msgstr "Активна"
msgid "No quota usage data"
msgstr "Нет данных об использовании квоты"
msgid "Delete Quota"
msgstr "Удалить квоту"
msgid "Save Quota"
msgstr "Сохранить квоту"
msgid "Usage starts when a quota is first saved."
msgstr "Учет трафика начинается после первого сохранения квоты."
msgid "Minute"
msgstr "Минута"
msgid "Lifetime"
msgstr "Всего"
msgid "Quota exceeded"
msgstr "Квота превышена"
msgid "Quota active"
msgstr "Квота активна"
msgid "WAN traffic is blocked because a quota was reached."
msgstr "Трафик WAN заблокирован, так как достигнута квота."
msgid "Quota blocked"
msgstr "Блокировка по квоте"
msgid "Quota values must be non-negative and within the supported range."
msgstr "Значения квоты должны быть неотрицательными и находиться в поддерживаемом диапазоне."
msgid "Failed to save traffic quota"
msgstr "Не удалось сохранить квоту трафика"
msgid "Delete Traffic Quota"
msgstr "Удалить квоту трафика"
msgid "Delete this quota and all of its accumulated usage?"
msgstr "Удалить эту квоту и все накопленные данные об использовании?"
msgid "Failed to delete traffic quota"
msgstr "Не удалось удалить квоту трафика"
msgid "Rules / Quota"
msgstr "Правила / Квота"
msgid "Failed to load traffic quotas"
msgstr "Не удалось загрузить квоты трафика"
msgid "Unavailable"
msgstr "Недоступно"
+90
View File
@@ -1285,3 +1285,93 @@ msgstr "TCX 出站锚点程序 ID"
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
msgstr "当 tc_order 为 before/after 时使用。必须是同一接口上的有效出站程序 ID。"
msgid "Traffic Quota"
msgstr "流量配额"
msgid "Quota Status"
msgstr "配额状态"
msgid "Hourly Quota"
msgstr "每小时配额"
msgid "Daily Quota"
msgstr "每天配额"
msgid "Weekly Quota"
msgstr "每周配额"
msgid "Monthly Quota"
msgstr "每月配额"
msgid "Lifetime Quota"
msgstr "累计总量配额"
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
msgstr "WAN 上传和下载合并计算;输入 0 表示不限量。"
msgid "Not configured"
msgstr "未配置"
msgid "Blocked"
msgstr "已阻断"
msgid "Active"
msgstr "生效中"
msgid "No quota usage data"
msgstr "暂无配额用量数据"
msgid "Delete Quota"
msgstr "删除配额"
msgid "Save Quota"
msgstr "保存配额"
msgid "Usage starts when a quota is first saved."
msgstr "首次保存配额后开始累计用量。"
msgid "Lifetime"
msgstr "累计"
msgid "Quota exceeded"
msgstr "配额已用尽"
msgid "Quota active"
msgstr "配额生效中"
msgid "WAN traffic is blocked because a quota was reached."
msgstr "因达到流量配额,WAN 流量已被阻断。"
msgid "Quota blocked"
msgstr "配额阻断"
msgid "Quota values must be non-negative and within the supported range."
msgstr "配额必须是支持范围内的非负数。"
msgid "Failed to save traffic quota"
msgstr "保存流量配额失败"
msgid "Delete Traffic Quota"
msgstr "删除流量配额"
msgid "Delete this quota and all of its accumulated usage?"
msgstr "确定删除此配额及其全部累计用量吗?"
msgid "Failed to delete traffic quota"
msgstr "删除流量配额失败"
msgid "Rules / Quota"
msgstr "规则 / 配额"
msgid "Failed to load traffic quotas"
msgstr "加载流量配额失败"
msgid "Unavailable"
msgstr "不可用"
msgid "Minute Quota"
msgstr "每分钟配额"
msgid "Minute"
msgstr "每分钟"
+90
View File
@@ -1258,3 +1258,93 @@ msgstr "TCX 出站錨點程式 ID"
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
msgstr "當 tc_order 為 before/after 時使用。必須是同一介面上的有效出站程式 ID。"
msgid "Traffic Quota"
msgstr "流量配額"
msgid "Quota Status"
msgstr "配額狀態"
msgid "Minute Quota"
msgstr "每分鐘配額"
msgid "Hourly Quota"
msgstr "每小時配額"
msgid "Daily Quota"
msgstr "每天配額"
msgid "Weekly Quota"
msgstr "每週配額"
msgid "Monthly Quota"
msgstr "每月配額"
msgid "Lifetime Quota"
msgstr "累計總量配額"
msgid "WAN upload and download are counted together. Enter 0 for unlimited."
msgstr "WAN 上傳和下載合併計算;輸入 0 表示不限量。"
msgid "Not configured"
msgstr "未設定"
msgid "Blocked"
msgstr "已阻斷"
msgid "Active"
msgstr "生效中"
msgid "No quota usage data"
msgstr "暫無配額用量資料"
msgid "Delete Quota"
msgstr "刪除配額"
msgid "Save Quota"
msgstr "儲存配額"
msgid "Usage starts when a quota is first saved."
msgstr "首次儲存配額後開始累計用量。"
msgid "Minute"
msgstr "每分鐘"
msgid "Lifetime"
msgstr "累計"
msgid "Quota exceeded"
msgstr "配額已用盡"
msgid "Quota active"
msgstr "配額生效中"
msgid "WAN traffic is blocked because a quota was reached."
msgstr "因達到流量配額,WAN 流量已被阻斷。"
msgid "Quota blocked"
msgstr "配額阻斷"
msgid "Quota values must be non-negative and within the supported range."
msgstr "配額必須是支援範圍內的非負數。"
msgid "Failed to save traffic quota"
msgstr "儲存流量配額失敗"
msgid "Delete Traffic Quota"
msgstr "刪除流量配額"
msgid "Delete this quota and all of its accumulated usage?"
msgstr "確定刪除此配額及其全部累計用量嗎?"
msgid "Failed to delete traffic quota"
msgstr "刪除流量配額失敗"
msgid "Rules / Quota"
msgstr "規則 / 配額"
msgid "Failed to load traffic quotas"
msgstr "載入流量配額失敗"
msgid "Unavailable"
msgstr "無法使用"
@@ -19,6 +19,7 @@ readonly BANDIX_TRAFFIC_USAGE_INCREMENTS_API="$BANDIX_API_BASE/api/traffic/usage
readonly BANDIX_RATE_LIMIT_WHITELIST_API="$BANDIX_API_BASE/api/traffic/rate_limit/whitelist"
readonly BANDIX_RATE_LIMIT_WHITELIST_ENABLED_API="$BANDIX_API_BASE/api/traffic/rate_limit/whitelist/enabled"
readonly BANDIX_RATE_LIMIT_DEFAULT_API="$BANDIX_API_BASE/api/traffic/rate_limit/default"
readonly BANDIX_TRAFFIC_QUOTAS_API="$BANDIX_API_BASE/api/traffic/quotas"
# 通用函数:创建简单的JSON响应
make_value() {
@@ -134,6 +135,71 @@ set_default_rate_limit() {
echo "$response"
}
get_traffic_quotas() {
local response=$(curl -s --connect-timeout 2 --max-time 5 -X GET "$BANDIX_TRAFFIC_QUOTAS_API" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$response" ]; then
make_error "Failed to connect to Bandix service"
return
fi
echo "$response"
}
set_traffic_quota() {
local mac="$1"
local minute_bytes="$2"
local hourly_bytes="$3"
local daily_bytes="$4"
local weekly_bytes="$5"
local monthly_bytes="$6"
local total_bytes="$7"
if [ -z "$mac" ]; then
make_error "MAC address is required"
return
fi
for value in "$minute_bytes" "$hourly_bytes" "$daily_bytes" "$weekly_bytes" "$monthly_bytes" "$total_bytes"; do
case "$value" in
''|*[!0-9]*)
make_error "Quota values must be non-negative integers"
return
;;
esac
done
local mac_escaped=$(escape_json_string "$mac")
local request_data="{
\"mac\": \"$mac_escaped\",
\"minute_bytes\": $minute_bytes,
\"hourly_bytes\": $hourly_bytes,
\"daily_bytes\": $daily_bytes,
\"weekly_bytes\": $weekly_bytes,
\"monthly_bytes\": $monthly_bytes,
\"total_bytes\": $total_bytes
}"
local response=$(curl -s --connect-timeout 3 --max-time 10 -X POST -H "Content-Type: application/json" -d "$request_data" "$BANDIX_TRAFFIC_QUOTAS_API" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$response" ] && echo "$response" | grep -q '"status":[[:space:]]*"success"'; then
make_success "Traffic quota saved successfully"
else
make_error "Failed to save traffic quota"
fi
}
delete_traffic_quota() {
local mac="$1"
if [ -z "$mac" ]; then
make_error "MAC address is required"
return
fi
local mac_escaped=$(escape_json_string "$mac")
local request_data="{\"mac\": \"$mac_escaped\"}"
local response=$(curl -s --connect-timeout 3 --max-time 10 -X DELETE -H "Content-Type: application/json" -d "$request_data" "$BANDIX_TRAFFIC_QUOTAS_API" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$response" ] && echo "$response" | grep -q '"status":[[:space:]]*"success"'; then
make_success "Traffic quota deleted successfully"
else
make_error "Failed to delete traffic quota"
fi
}
# 删除设备
delete_device() {
local mac="$1"
@@ -1348,6 +1414,23 @@ case "$1" in
json_add_int "wan_rx_rate_limit"
json_add_int "wan_tx_rate_limit"
json_close_object
json_add_object "getTrafficQuotas"
json_close_object
json_add_object "setTrafficQuota"
json_add_string "mac"
json_add_int "minute_bytes"
json_add_int "hourly_bytes"
json_add_int "daily_bytes"
json_add_int "weekly_bytes"
json_add_int "monthly_bytes"
json_add_int "total_bytes"
json_close_object
json_add_object "deleteTrafficQuota"
json_add_string "mac"
json_close_object
json_dump
json_cleanup
@@ -1793,6 +1876,57 @@ case "$1" in
fi
set_default_rate_limit "$wan_rx_rate_limit" "$wan_tx_rate_limit"
;;
getTrafficQuotas)
get_traffic_quotas
;;
setTrafficQuota)
mac=""
minute_bytes=""
hourly_bytes=""
daily_bytes=""
weekly_bytes=""
monthly_bytes=""
total_bytes=""
input=""
if read -t 1 -r input; then :; fi
if [ -n "$input" ]; then
mac="$(echo "$input" | jsonfilter -e '$[0]' 2>/dev/null)"
[ -z "$mac" ] && mac="$(echo "$input" | jsonfilter -e '$.mac' 2>/dev/null)"
minute_bytes="$(echo "$input" | jsonfilter -e '$[1]' 2>/dev/null)"
[ -z "$minute_bytes" ] && minute_bytes="$(echo "$input" | jsonfilter -e '$.minute_bytes' 2>/dev/null)"
hourly_bytes="$(echo "$input" | jsonfilter -e '$[2]' 2>/dev/null)"
[ -z "$hourly_bytes" ] && hourly_bytes="$(echo "$input" | jsonfilter -e '$.hourly_bytes' 2>/dev/null)"
daily_bytes="$(echo "$input" | jsonfilter -e '$[3]' 2>/dev/null)"
[ -z "$daily_bytes" ] && daily_bytes="$(echo "$input" | jsonfilter -e '$.daily_bytes' 2>/dev/null)"
weekly_bytes="$(echo "$input" | jsonfilter -e '$[4]' 2>/dev/null)"
[ -z "$weekly_bytes" ] && weekly_bytes="$(echo "$input" | jsonfilter -e '$.weekly_bytes' 2>/dev/null)"
monthly_bytes="$(echo "$input" | jsonfilter -e '$[5]' 2>/dev/null)"
[ -z "$monthly_bytes" ] && monthly_bytes="$(echo "$input" | jsonfilter -e '$.monthly_bytes' 2>/dev/null)"
total_bytes="$(echo "$input" | jsonfilter -e '$[6]' 2>/dev/null)"
[ -z "$total_bytes" ] && total_bytes="$(echo "$input" | jsonfilter -e '$.total_bytes' 2>/dev/null)"
else
mac="$3"
minute_bytes="$4"
hourly_bytes="$5"
daily_bytes="$6"
weekly_bytes="$7"
monthly_bytes="$8"
total_bytes="$9"
fi
set_traffic_quota "$mac" "$minute_bytes" "$hourly_bytes" "$daily_bytes" "$weekly_bytes" "$monthly_bytes" "$total_bytes"
;;
deleteTrafficQuota)
mac=""
input=""
if read -t 1 -r input; then :; fi
if [ -n "$input" ]; then
mac="$(echo "$input" | jsonfilter -e '$[0]' 2>/dev/null)"
[ -z "$mac" ] && mac="$(echo "$input" | jsonfilter -e '$.mac' 2>/dev/null)"
else
mac="$3"
fi
delete_traffic_quota "$mac"
;;
esac
;;
esac
@@ -26,7 +26,8 @@
"installUpdate",
"getTrafficUsageRanking",
"getTrafficUsageIncrements",
"getRateLimitWhitelist"
"getRateLimitWhitelist",
"getTrafficQuotas"
]
},
"uci": [
@@ -64,7 +65,10 @@
"addRateLimitWhitelist",
"deleteRateLimitWhitelist",
"setDefaultRateLimit",
"updateScheduleLimit"
"updateScheduleLimit",
"getTrafficQuotas",
"setTrafficQuota",
"deleteTrafficQuota"
]
},
"uci": [
@@ -72,4 +76,4 @@
]
}
}
}
}
+4 -4
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: luci-app-ddns 2.4.0-1\n"
"POT-Creation-Date: 2016-01-30 11:07+0100\n"
"PO-Revision-Date: 2026-08-14 17:53+0000\n"
"PO-Revision-Date: 2026-08-24 16:47+0000\n"
"Last-Translator: Franco Castillo <castillofrancodamian@gmail.com>\n"
"Language-Team: Spanish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsddns/es/>\n"
@@ -28,7 +28,7 @@ msgstr "Añadir nuevos servicios..."
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:607
msgid "Advanced Settings"
msgstr "Ajustes avanzados"
msgstr "Configuración avanzada"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:392
msgid "Allow non-public IPs"
@@ -36,7 +36,7 @@ msgstr "Permitir IPs no públicas"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:606
msgid "Basic Settings"
msgstr "Ajustes básicos"
msgstr "Configuración básica"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:903
msgid "Bind Network"
@@ -280,7 +280,7 @@ msgstr ""
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:265
msgid "Global Settings"
msgstr "Ajustes globales"
msgstr "Configuración global"
#: applications/luci-app-ddns/root/usr/share/rpcd/acl.d/luci-app-ddns.json:3
msgid "Grant access to ddns procedures"
+3 -3
View File
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-04-24 00:23+0000\n"
"PO-Revision-Date: 2026-09-02 17:27+0000\n"
"Last-Translator: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>\n"
"Language-Team: Irish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsddns/ga/>\n"
@@ -9,7 +9,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :"
"(n>6 && n<11) ? 3 : 4;\n"
"X-Generator: Weblate 5.17.1-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:423
msgid "\"../\" not allowed in path for Security Reason."
@@ -157,7 +157,7 @@ msgstr "Ní thacaítear le hiarratais DNS trí TCP"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:924
msgid "DNS-Server"
msgstr "Freastalaí DNS"
msgstr "Freastalaí-DNS"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:401
msgid "Date format"
+10 -10
View File
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-06-05 19:01+0000\n"
"PO-Revision-Date: 2026-08-26 18:51+0000\n"
"Last-Translator: Hyeonjeong Lee <h9101654@gmail.com>\n"
"Language-Team: Korean <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsddns/ko/>\n"
@@ -8,7 +8,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 2026.6\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:423
msgid "\"../\" not allowed in path for Security Reason."
@@ -162,15 +162,15 @@ msgstr "날짜 형식"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:853
msgid "Defines the Web page to read systems IP-Address from."
msgstr ""
msgstr "시스템의 IP 주소를 가져올 웹페이지를 지정합니다."
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:863
msgid "Defines the interface to read systems IP-Address from"
msgstr ""
msgstr "시스템의 IP 주소를 가져올 인터페이스를 지정합니다"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:845
msgid "Defines the network to read systems IP-Address from"
msgstr ""
msgstr "시스템의 IP 주소를 가져올 네트워크를 지정합니다"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:18
msgid "Disabled"
@@ -234,7 +234,7 @@ msgstr "파일"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:711
msgid "Follow instructions found on their WEB page."
msgstr ""
msgstr "제공업체의 웹페이지 안내를 참고하세요."
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:403
msgid "For supported codes look here"
@@ -738,7 +738,7 @@ msgstr ""
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:893
msgid "This will be autoset to the selected interface"
msgstr ""
msgstr "선택한 인터페이스로 자동 설정됩니다"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:608
msgid "Timer Settings"
@@ -750,7 +750,7 @@ msgstr "URL"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:852
msgid "URL to detect"
msgstr ""
msgstr "IP 확인 URL"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:229
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:230
@@ -870,7 +870,7 @@ msgstr "일"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:789
msgid "directory or path/file"
msgstr "디렉토리 또는 경로/파일"
msgstr "디렉터리 또는 경로/파일을 지정하거나,"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:1002
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:1036
@@ -898,4 +898,4 @@ msgstr "초"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:793
msgid "to run HTTPS without verification of server certificates (insecure)"
msgstr ""
msgstr "를 입력하여 서버 인증서 검증 없이 HTTPS로 연결합니다. (안전하지 않음)"
+7 -7
View File
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2026-05-22 21:11+0000\n"
"PO-Revision-Date: 2026-08-27 11:44+0000\n"
"Last-Translator: Džiugas Januševičius <dziugas1959@hotmail.com>\n"
"Language-Team: Lithuanian <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsddns/lt/>\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"(n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 2026.6.dev0\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:423
msgid "\"../\" not allowed in path for Security Reason."
@@ -183,7 +183,7 @@ msgstr "Apibrėžia tinklą, iš kurio reikia skaityti sistemos IP adresą"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:18
msgid "Disabled"
msgstr "Išjungta/Neįgalinta (-s/-i)"
msgstr "Išjungtas/Išgalintas"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:748
msgid "Domain"
@@ -210,7 +210,7 @@ msgstr "Įjungti/Įgalinti saugią komunikacijos ryšį su „DDNS“ teikėju"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:612
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:1140
msgid "Enabled"
msgstr "Įjungta/Įgalinta (-s/-i)"
msgstr "Įjungtas/Įgalintas"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:966
msgid "Error"
@@ -519,7 +519,7 @@ msgstr "Nevieši ir pagal numatytuosius nustatymus užblokuoti IP (dgs.)"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:231
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:1121
msgid "Not Running"
msgstr "Neveikia"
msgstr "Nevykdomas/-a"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:964
msgid "Notice"
@@ -623,7 +623,7 @@ msgstr "Registruotas IP"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:567
msgid "Reload"
msgstr "Perleidimas/-sti"
msgstr "Perleisti"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:550
msgid "Reload this service"
@@ -662,7 +662,7 @@ msgstr "Paleisti tik kartą"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:231
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:1120
msgid "Running"
msgstr "Veikia"
msgstr "Vykdomas/-a"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:813
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:870
+2 -2
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: LuCI: ddns\n"
"POT-Creation-Date: 2017-10-17 21:00+0300\n"
"PO-Revision-Date: 2026-08-14 12:12+0000\n"
"PO-Revision-Date: 2026-08-28 22:28+0000\n"
"Last-Translator: SnIPeRSnIPeR "
"<snipersniper@users.noreply.hosted.weblate.org>\n"
"Language-Team: Russian <https://hosted.weblate.org/projects/openwrt/"
@@ -710,7 +710,7 @@ msgstr "Состояние"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:1117
msgid "Status"
msgstr "Состояние"
msgstr "Статус"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:410
msgid "Status directory"
+11 -7
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: luci-app-ddns 2.4.0-1\n"
"POT-Creation-Date: 2016-01-30 11:07+0100\n"
"PO-Revision-Date: 2025-09-08 17:09+0000\n"
"Last-Translator: Derman <mole.qnap@gmail.com>\n"
"PO-Revision-Date: 2026-08-23 20:27+0000\n"
"Last-Translator: umut <umvtt4@proton.me>\n"
"Language-Team: Turkish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsddns/tr/>\n"
"Language: tr\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.14-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:423
msgid "\"../\" not allowed in path for Security Reason."
@@ -134,7 +134,7 @@ msgstr "Özel güncelleme betiği"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:284
msgid "DDNS"
msgstr ""
msgstr "DDNS"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:207
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:279
@@ -432,6 +432,8 @@ msgstr "Ana Makine Adı Ara"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:807
msgid "Method used to determine the system IP-Address to send in updates"
msgstr ""
"Güncellemelerin gönderileceği sistem IP adresini belirlemek için kullanılan "
"yöntem"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:491
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:1124
@@ -468,7 +470,7 @@ msgstr "Ağ"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:888
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:906
msgid "Network on which the ddns-updater scripts will be started"
msgstr "DDns güncelleyici komut dosyalarının başlatılacağı ağ"
msgstr "DDNS güncelleyici komut dosyalarının başlatılacağı ağ"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:228
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:1149
@@ -486,7 +488,7 @@ msgstr "Sonraki Güncelleme"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:1145
msgid "Next Verify"
msgstr ""
msgstr "Sonraki Doğrulama"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:227
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:1135
@@ -751,6 +753,8 @@ msgid ""
"This is only a problem with multiple WAN interfaces and your DDNS provider "
"is unreachable via one of them."
msgstr ""
"Bu sorun, yalnızca birden fazla WAN arabirimi olduğunda ve DDNS "
"sağlayıcınıza bu arabirimlerden biri üzerinden ulaşılamadığında ortaya çıkar."
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:1085
msgid "This is the current content of the log file in %h for this service."
@@ -795,7 +799,7 @@ msgstr "DDns Hizmetleri Listesini Güncelle"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:684
msgid "Update URL"
msgstr ""
msgstr "URL'yi Güncelle"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:709
msgid "Update URL for updating your DDNS Provider."
+6 -5
View File
@@ -5,8 +5,9 @@ msgid ""
msgstr ""
"Project-Id-Version: luci-app-ddns 2.4.0-1\n"
"POT-Creation-Date: 2016-01-30 11:07+0100\n"
"PO-Revision-Date: 2026-04-18 17:10+0000\n"
"Last-Translator: DragonBluep <yangshiji66@outlook.com>\n"
"PO-Revision-Date: 2026-08-31 15:23+0000\n"
"Last-Translator: Hosted Weblate user 54392 "
"<hamburger2048@users.noreply.hosted.weblate.org>\n"
"Language-Team: Chinese (Simplified Han script) <https://hosted.weblate.org/"
"projects/openwrt/luciapplicationsddns/zh_Hans/>\n"
"Language: zh_Hans\n"
@@ -14,7 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.17.1-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:423
msgid "\"../\" not allowed in path for Security Reason."
@@ -22,7 +23,7 @@ msgstr "基于安全原因路径中不允许含有 \"../\" 。"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:763
msgid "A.k.a. the TOKEN at e.g. afraid.org"
msgstr "有的服务商使用 TOKEN 作为更新密码,例如 afraid.org"
msgstr "有的服务商也称作TOKEN,例如 afraid.org"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:458
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:526
@@ -61,7 +62,7 @@ msgstr ""
msgid ""
"BusyBox's nslookup in the current compiled version does not handle given DNS "
"Servers correctly!"
msgstr "当前编译的 BusyBox nslookup 不支持自定义 DNS 服务器!"
msgstr "当前编译版本中的 BusyBox nslookup 无法正确处理给定的 DNS 服务器!"
#: applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:443
msgid "CA cert bundle file"
+2 -2
View File
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-08-14 18:35+0000\n"
"Last-Translator: Pavel Borecki <pavel.borecki@gmail.com>\n"
"PO-Revision-Date: 2026-08-27 18:20+0000\n"
"Last-Translator: Martin Olmer <m_iv@seznam.cz>\n"
"Language-Team: Czech <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsdockerman/cs/>\n"
"Language: cs\n"
+2 -2
View File
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-08-16 17:38+0000\n"
"PO-Revision-Date: 2026-09-02 17:28+0000\n"
"Last-Translator: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>\n"
"Language-Team: Irish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsdockerman/ga/>\n"
@@ -227,7 +227,7 @@ msgstr "Níl sonraí úsáide LAP ar fáil"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:678
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:169
msgid "CPUs"
msgstr "CPUanna"
msgstr "LAPanna"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1933
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:517
+11 -11
View File
@@ -1,14 +1,14 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-05-31 16:42+0000\n"
"Last-Translator: ferrysqo <fe.qo666@gmail.com>\n"
"PO-Revision-Date: 2026-08-18 10:04+0000\n"
"Last-Translator: Fiqri Ardyansyah <fiqri0927936@gmail.com>\n"
"Language-Team: Indonesian <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsdockerman/id/>\n"
"Language: id\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 2026.6.dev0\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:505
msgid "/mnt/path"
@@ -1449,7 +1449,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:396
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1805
msgid "Pause"
msgstr ""
msgstr "Jeda"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:207
msgid "Pause this container"
@@ -1785,7 +1785,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:88
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:145
msgid "Stopped"
msgstr ""
msgstr "Berhenti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1098
msgid "Streams:"
@@ -1926,7 +1926,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/volumes.js:45
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/volumes.js:111
msgid "Total"
msgstr ""
msgstr "Total"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:170
msgid "Total Memory"
@@ -1943,7 +1943,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:63
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:249
msgid "Type"
msgstr ""
msgstr "Tipe"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1135
msgid "Type command here... (Ctrl+D to detach)"
@@ -1961,7 +1961,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1639
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1682
msgid "Unknown error"
msgstr ""
msgstr "Kesalahan tidak dikenal"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:485
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:250
@@ -1988,7 +1988,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:428
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:948
msgid "Upload"
msgstr "Upload"
msgstr "Unggah"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1609
msgid "Uploading file to container…"
@@ -2033,7 +2033,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:240
msgid "Version"
msgstr ""
msgstr "Versi"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:374
msgid "Volume (named)"
@@ -2201,7 +2201,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:757
msgid "none"
msgstr ""
msgstr "tidak ada"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:337
msgid "oom"
+3 -3
View File
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-08-07 23:22+0000\n"
"PO-Revision-Date: 2026-09-02 17:27+0000\n"
"Last-Translator: Hyeonjeong Lee <h9101654@gmail.com>\n"
"Language-Team: Korean <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsdockerman/ko/>\n"
@@ -1340,7 +1340,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:116
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:151
msgid "Now"
msgstr ""
msgstr "지금"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:679
msgid "Number of CPUs. Number is a fractional number. 0.000 means no limit"
@@ -1915,7 +1915,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/volumes.js:45
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/volumes.js:111
msgid "Total"
msgstr ""
msgstr "합계"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:170
msgid "Total Memory"
+121 -109
View File
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-06-04 18:14+0000\n"
"PO-Revision-Date: 2026-08-27 04:49+0000\n"
"Last-Translator: Džiugas Januševičius <dziugas1959@hotmail.com>\n"
"Language-Team: Lithuanian <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsdockerman/lt/>\n"
@@ -9,7 +9,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"(n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 2026.6\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:505
msgid "/mnt/path"
@@ -62,7 +62,7 @@ msgstr "Pridėtos galimybes"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:634
msgid "Advanced"
msgstr "Pažangūs"
msgstr "Pažangus"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:590
msgid "Aliases"
@@ -991,7 +991,7 @@ msgstr "Informacija"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/network.js:73
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/network_new.js:72
msgid "Ingress"
msgstr "„Ingress“"
msgstr "Įėjimas"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/network_new.js:73
msgid ""
@@ -1680,7 +1680,7 @@ msgstr "Paleisti – „Run“ komandą"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:84
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:141
msgid "Running"
msgstr "Veikia"
msgstr "Vykdomas/-a"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:833
msgid "Running Processes"
@@ -1710,20 +1710,20 @@ msgstr "Pasirinkite tipą pirmą"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/images.js:159
msgid "Select image tag"
msgstr ""
msgstr "Pasirinkite laikmenos žyma"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1917
msgid "Select network to connect:"
msgstr ""
msgstr "Pasirinkite tinklą, prie kurio norite prisijungti:"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:361
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:474
msgid "Select options..."
msgstr ""
msgstr "Pasirinkite parinktys..."
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1150
msgid "Send"
msgstr ""
msgstr "Siųsti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:291
msgid "Set custom DNS servers"
@@ -1731,7 +1731,7 @@ msgstr "Nustatyti savuosius „DNS“ serverius"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:301
msgid "Set environment variables inside the container"
msgstr ""
msgstr "Nustatyti aplinkos kintamuosius konteinerio viduje"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/configuration.js:72
msgid "Set the logging level"
@@ -1742,6 +1742,8 @@ msgid ""
"Set your LAN interface when docker listens on all addresses like 0.0.0.0 "
"or ::."
msgstr ""
"Nustatykite savo „LAN“ sąsają ir/arba sietuvą, kai „Docker“ laukia "
"prisijungimo/jungties ryšio visų adresų, tokių kaip 0.0.0.0 arba ::."
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:423
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/images.js:599
@@ -1753,6 +1755,8 @@ msgid ""
"Specifies where the Docker daemon will listen for client connections. "
"default:"
msgstr ""
"Nurodo, kur „Docker“ paslaugų teikimo sistema lauks prisijungimo/jungties "
"kliento ryšių. numatytoji:"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:354
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1801
@@ -1761,7 +1765,7 @@ msgstr "Paleisti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:208
msgid "Start this container"
msgstr ""
msgstr "Paleisti šį konteinerį"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:161
msgid "State"
@@ -1780,15 +1784,15 @@ msgstr "Būklė/Būsena"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1109
msgid "Stderr"
msgstr ""
msgstr "„STDERR“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1101
msgid "Stdin"
msgstr ""
msgstr "“STDIN“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1105
msgid "Stdout"
msgstr ""
msgstr "„STDOUT“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:372
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1803
@@ -1798,11 +1802,11 @@ msgstr "Stabdyti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:155
msgctxt "daemon stop action"
msgid "Stop"
msgstr ""
msgstr "Stabdyti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:249
msgid "Stop this container"
msgstr ""
msgstr "Stabdyti šį konteinerį"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:88
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:145
@@ -1811,7 +1815,7 @@ msgstr "Sustabdyta/-s/-i"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1098
msgid "Streams:"
msgstr ""
msgstr "Srautai:"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/network.js:104
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/network_new.js:103
@@ -1821,24 +1825,24 @@ msgstr "Potinklis"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:460
msgid "Subpath"
msgstr ""
msgstr "Sub-kelias"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:81
msgid "Subtype"
msgstr ""
msgstr "Pótipis"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1522
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1773
msgid "Success"
msgstr ""
msgstr "Sėkmė"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:1012
msgid "Successful"
msgstr ""
msgstr "Sėkmingai"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:542
msgid "Sysctl Settings"
msgstr ""
msgstr "„Sysctl“ nustatymai"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:666
msgid "Sysctl(--sysctl)"
@@ -1854,23 +1858,23 @@ msgstr "„TTY“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1763
msgid "TTYd may still be starting up"
msgstr ""
msgstr "„TTYd“ vis dar gali būti paleistas"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/images.js:635
msgid "Tag"
msgstr ""
msgstr "Žyma"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/images.js:670
msgid "Tag added successfully"
msgstr ""
msgstr "Žyma sėkmingai pridėta"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/images.js:554
msgid "Tag removed successfully"
msgstr ""
msgstr "Žyma sėkmingai pašalinta"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/configuration.js:105
msgid "The first match is used."
msgstr ""
msgstr "Pirma atitikmuo yra naudojama."
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:641
msgid "The hostname to use for the container"
@@ -1879,7 +1883,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:691
msgid "The length of a CPU period in microseconds"
msgstr ""
msgstr "„CPU“ periodo trukmė mikrosekundėmis"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:746
msgid "The logging configuration for this container"
@@ -1890,12 +1894,16 @@ msgid ""
"The user that commands are run as inside the container. (format: name|"
"uid[:group|gid])"
msgstr ""
"Naudotojas/Vartotojas, kurio komandos yra vykdomos kaip konteinerio viduje. "
"(formatas: name|uid[:group|gid])"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:50
msgid ""
"This page displays all docker Containers that have been created on the "
"connected docker host."
msgstr ""
"Šiame puslapyje rodomi visi „Docker“ konteineriai, kurie buvo sukurti "
"prijungtame „Docker“ skleidėjoje/vedėjoje."
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/network.js:45
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/networks.js:58
@@ -1911,6 +1919,8 @@ msgid ""
"This page displays all docker volumes that have been created on the "
"connected docker host."
msgstr ""
"Šiame puslapyje rodomi visi „Docker“ tomai, kurie buvo sukurti prijungtame "
"„Docker“ skleidėjoje/vedėjoje."
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:249
msgid "Time"
@@ -1922,7 +1932,7 @@ msgstr "„Tmpfs“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:528
msgid "Tmpfs Directories"
msgstr ""
msgstr "„Tmpfs“ katalogai/vietovės"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:660
msgid "Tmpfs(--tmpfs)"
@@ -1930,15 +1940,15 @@ msgstr "„Tmpfs(--tmpfs)“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:131
msgid "To"
msgstr ""
msgstr "Į"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/configuration.js:138
msgid "Token"
msgstr ""
msgstr "Žetonas"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/configuration.js:107
msgid "Tokes and Passwords are not encrypted in the uci configuration."
msgstr ""
msgstr "Žetonai ir slaptažodžiai „UCI“ konfigūracijoje nėra šifruojami."
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:82
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:139
@@ -1949,7 +1959,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/volumes.js:45
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/volumes.js:111
msgid "Total"
msgstr ""
msgstr "Iš viso"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:170
msgid "Total Memory"
@@ -1960,45 +1970,45 @@ msgstr "Iš viso atminties:"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:210
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:220
msgid "Total:"
msgstr ""
msgstr "Iš viso:"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:498
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:63
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:249
msgid "Type"
msgstr ""
msgstr "Tipas"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1135
msgid "Type command here... (Ctrl+D to detach)"
msgstr ""
msgstr "Įveskite komandą čia... („Ctrl+D“, kad atjungtumėte)"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:847
msgid "USER"
msgstr ""
msgstr "NAUDOTOJAS/VARTOTOJAS"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:980
msgid "Unexpected error"
msgstr ""
msgstr "Netikėta klaida"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1354
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1639
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1682
msgid "Unknown error"
msgstr ""
msgstr "Nežinoma klaida"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:485
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:250
msgid "Unless stopped"
msgstr ""
msgstr "Nebent nesustojo"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:396
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1806
msgid "Unpause"
msgstr ""
msgstr "Pratęsti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:208
msgid "Unpause this container"
msgstr ""
msgstr "Pratęsti šį konteinerį"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:582
msgid "Update"
@@ -2006,7 +2016,7 @@ msgstr "Atnaujinti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1358
msgid "Updated"
msgstr ""
msgstr "Atnaujinta/-as"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:428
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:948
@@ -2015,20 +2025,20 @@ msgstr "Įkelti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1609
msgid "Uploading file to container…"
msgstr ""
msgstr "Įkeliamas failas į konteinerį…"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/images.js:698
msgid "Uploading image…"
msgstr ""
msgstr "Įkeliama laikmena…"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1608
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/images.js:697
msgid "Uploading…"
msgstr ""
msgstr "Įkeliama…"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/configuration.js:103
msgid "Used for push/pull operations on custom registries."
msgstr ""
msgstr "Naudojamas stumti/traukti operacijoms pasirinktiniuose registruose."
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/configuration.js:115
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:479
@@ -2046,7 +2056,7 @@ msgstr "„VEPA“ (Virtualaus „ethernet'o“ prievado agregatorius)"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:847
msgid "VSZ"
msgstr ""
msgstr "„VSZ“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:803
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:244
@@ -2060,11 +2070,11 @@ msgstr "Versija"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:374
msgid "Volume (named)"
msgstr ""
msgstr "Tomas (pavadintas)"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:401
msgid "Volume Name"
msgstr ""
msgstr "Tomo pavadinimas"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:219
#: applications/luci-app-dockerman/root/usr/share/luci/menu.d/luci-app-dockerman.json:348
@@ -2082,15 +2092,15 @@ msgstr "Įspėjimas"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1078
msgid "WebSocket"
msgstr ""
msgstr "„WebSocket“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1087
msgid "WebSocket Console"
msgstr ""
msgstr "„WebSocket“ konsolė"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1499
msgid "WebSocket error"
msgstr ""
msgstr "„Websocket“ klaida"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:679
msgid "Yes"
@@ -2098,35 +2108,35 @@ msgstr "Taip"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:323
msgid "attach"
msgstr ""
msgstr "prijungti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:404
msgid "builder"
msgstr ""
msgstr "„statytojas“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:324
msgid "commit"
msgstr ""
msgstr "nusiųsti/patvirtinti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1835
msgid "completed"
msgstr ""
msgstr "užbaigta"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:405
msgid "config"
msgstr ""
msgstr "konfigūracija"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:372
msgid "connect"
msgstr ""
msgstr "prisijungti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:406
msgid "container"
msgstr ""
msgstr "konteineris"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:325
msgid "copy"
msgstr ""
msgstr "kopijuoti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:326
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:357
@@ -2134,109 +2144,111 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:382
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:391
msgid "create"
msgstr ""
msgstr "sukurti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:407
msgid "daemon"
msgstr ""
msgstr "paslaugų teikimo sistema"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:358
msgid "delete"
msgstr ""
msgstr "ištrinti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:327
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:374
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:385
msgid "destroy"
msgstr ""
msgstr "sunaikinti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:328
msgid "detach"
msgstr ""
msgstr "atjungti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:329
msgid "die"
msgstr ""
msgstr "(už)baigti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:373
msgid "disconnect"
msgstr ""
msgstr "atsijungti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:330
msgid "exec_create"
msgstr ""
msgstr "„exec_create“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:331
msgid "exec_detach"
msgstr ""
msgstr "„exec_detach“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:333
msgid "exec_die"
msgstr ""
msgstr "„exec_die“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:332
msgid "exec_start"
msgstr ""
msgstr "„exec_start“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:334
msgid "export"
msgstr ""
msgstr "eksportuoti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/images.js:206
msgid "git repositories require git installed on the docker host."
msgstr ""
"„Git“ saugykloms reikalinga, kad „Git“ būtų įdiegta „Docker“ skleidėjoje/"
"vedėjoje."
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:335
msgid "health_status"
msgstr ""
msgstr "„health_status“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:408
msgid "image"
msgstr ""
msgstr "laikmena"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:755
msgid "json-file"
msgstr ""
msgstr "„json-file“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:336
msgid "kill"
msgstr ""
msgstr "„nužudyti“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:360
msgid "load"
msgstr ""
msgstr "įkelti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:754
msgid "local"
msgstr ""
msgstr "vietinis"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:383
msgid "mount"
msgstr ""
msgstr "įrengti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:409
msgid "network"
msgstr ""
msgstr "tinklas"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:410
msgid "node"
msgstr ""
msgstr "tinklo mazgas"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:757
msgid "none"
msgstr ""
msgstr "joks (-ia/-ie/-ios)"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:337
msgid "oom"
msgstr ""
msgstr "„oom“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:338
msgid "pause"
msgstr ""
msgstr "pristabdyti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:411
msgid "plugin"
msgstr ""
msgstr "plėtinys"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:318
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:347
@@ -2244,96 +2256,96 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:377
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:386
msgid "prune"
msgstr ""
msgstr "išvalyti nenaudojamus"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:869
msgid "ps flags:"
msgstr ""
msgstr "„ps“ žymos:"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:352
msgid "reload"
msgstr ""
msgstr "perleisti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:376
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:393
msgid "remove"
msgstr ""
msgstr "pašalinti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:339
msgid "rename"
msgstr ""
msgstr "pervadinti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:340
msgid "resize"
msgstr ""
msgstr "keisti dydį"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:341
msgid "restart"
msgstr ""
msgstr "paleisti iš naujo"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:363
msgid "save"
msgstr ""
msgstr "išsaugoti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:412
msgid "secret"
msgstr ""
msgstr "paslaptis"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:413
msgid "service"
msgstr ""
msgstr "tarnyba"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:342
msgid "start"
msgstr ""
msgstr "paleisti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:343
msgid "stop"
msgstr ""
msgstr "stabdyti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:756
msgid "syslog"
msgstr ""
msgstr "„syslog“"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:364
msgid "tag"
msgstr ""
msgstr "žyma"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:437
msgid "tmpfs Options"
msgstr ""
msgstr "„tmpfs“ parinktys"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:344
msgid "top"
msgstr ""
msgstr "viršus"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:384
msgid "unmount"
msgstr ""
msgstr "išrengti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:345
msgid "unpause"
msgstr ""
msgstr "pratęsti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:365
msgid "untag"
msgstr ""
msgstr "atžymėti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:346
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:375
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:392
msgid "update"
msgstr ""
msgstr "atnaujinti"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:414
msgid "volume"
msgstr ""
msgstr "tomas"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/volumes.js:126
msgid "volume name"
msgstr ""
msgstr "tomo pavadinimas"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1360
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:991
msgid "with warnings"
msgstr ""
msgstr "su įspėjimais"
+6 -6
View File
@@ -1,14 +1,14 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-05-18 07:49+0000\n"
"Last-Translator: Volenski <volenski@users.noreply.hosted.weblate.org>\n"
"PO-Revision-Date: 2026-09-02 07:50+0000\n"
"Last-Translator: Martin Olmer <m_iv@seznam.cz>\n"
"Language-Team: Portuguese (Brazil) <https://hosted.weblate.org/projects/"
"openwrt/luciapplicationsdockerman/pt_BR/>\n"
"Language: pt_BR\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 2026.6.dev0\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:505
msgid "/mnt/path"
@@ -61,7 +61,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:634
msgid "Advanced"
msgstr ""
msgstr "Avançada"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:590
msgid "Aliases"
@@ -324,7 +324,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1520
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1532
msgid "Connected"
msgstr ""
msgstr "Conectado"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1063
msgid "Connected to container console"
@@ -436,7 +436,7 @@ msgstr "Visão geral dos contêineres"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:421
msgid "Create"
msgstr ""
msgstr "Criar"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:979
msgid "Create container"
+3 -3
View File
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-08-14 12:12+0000\n"
"PO-Revision-Date: 2026-09-01 09:13+0000\n"
"Last-Translator: SnIPeRSnIPeR "
"<snipersniper@users.noreply.hosted.weblate.org>\n"
"Language-Team: Russian <https://hosted.weblate.org/projects/openwrt/"
@@ -227,7 +227,7 @@ msgstr "Данные об использовании CPU недоступны"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:678
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:169
msgid "CPUs"
msgstr "Ядра ЦП"
msgstr "Процессоры"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1933
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:517
@@ -1774,7 +1774,7 @@ msgstr "Статистика"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:721
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:840
msgid "Status"
msgstr "Состояние"
msgstr "Статус"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1109
msgid "Stderr"
+4 -4
View File
@@ -1,14 +1,14 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2025-06-16 11:44+0000\n"
"Last-Translator: தமிழ்நேரம் <anishprabu.t@gmail.com>\n"
"PO-Revision-Date: 2026-08-23 19:31+0000\n"
"Last-Translator: Pavel Borecki <pavel.borecki@gmail.com>\n"
"Language-Team: Tamil <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsdockerman/ta/>\n"
"Language: ta\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.12-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:505
msgid "/mnt/path"
@@ -861,7 +861,7 @@ msgstr "புரவலன் பெயர்"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/network.js:60
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/networks.js:178
msgid "ID"
msgstr "ஐடி"
msgstr "ID"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/network_new.js:45
msgid "IP VLAN"
+14 -14
View File
@@ -1,14 +1,14 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-07-28 20:29+0000\n"
"Last-Translator: Oğuz Ersen <oguz@ersen.moe>\n"
"PO-Revision-Date: 2026-08-31 21:03+0000\n"
"Last-Translator: Martin Olmer <m_iv@seznam.cz>\n"
"Language-Team: Turkish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsdockerman/tr/>\n"
"Language: tr\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.8.dev0\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:505
msgid "/mnt/path"
@@ -61,7 +61,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:634
msgid "Advanced"
msgstr ""
msgstr "Gelişmiş"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:590
msgid "Aliases"
@@ -482,7 +482,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/configuration.js:73
msgid "Debug"
msgstr "Hata Ayıklama"
msgstr "Hata ayıklama"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:753
msgid "Default (daemon)"
@@ -1660,7 +1660,7 @@ msgstr "Komut çalıştır"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:84
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:141
msgid "Running"
msgstr ""
msgstr "Çalışıyor"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:833
msgid "Running Processes"
@@ -1745,7 +1745,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:161
msgid "State"
msgstr ""
msgstr "Durum"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:703
msgid "Stats"
@@ -1773,7 +1773,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:372
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1803
msgid "Stop"
msgstr "Dur"
msgstr "Durdur"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:155
msgctxt "daemon stop action"
@@ -1787,7 +1787,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:88
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/containers.js:145
msgid "Stopped"
msgstr ""
msgstr "Durduruldu"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1098
msgid "Streams:"
@@ -1893,7 +1893,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:249
msgid "Time"
msgstr ""
msgstr "Zaman"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:376
msgid "Tmpfs"
@@ -1945,7 +1945,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:63
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/events.js:249
msgid "Type"
msgstr ""
msgstr "Tür"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1135
msgid "Type command here... (Ctrl+D to detach)"
@@ -1963,7 +1963,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1639
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1682
msgid "Unknown error"
msgstr ""
msgstr "Bilinmeyen hata"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:485
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:250
@@ -1990,7 +1990,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/dockerman/common.js:428
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:948
msgid "Upload"
msgstr "Yükleme"
msgstr "Yükle"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1609
msgid "Uploading file to container…"
@@ -2035,7 +2035,7 @@ msgstr ""
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:240
msgid "Version"
msgstr ""
msgstr "Sürüm"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:374
msgid "Volume (named)"
+3 -3
View File
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-08-15 13:57+0000\n"
"PO-Revision-Date: 2026-08-31 06:44+0000\n"
"Last-Translator: Hosted Weblate user 54392 "
"<hamburger2048@users.noreply.hosted.weblate.org>\n"
"Language-Team: Chinese (Simplified Han script) <https://hosted.weblate.org/"
@@ -217,7 +217,7 @@ msgstr "CPU 使用数据不可用"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:678
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:169
msgid "CPUs"
msgstr "线程数量"
msgstr "CPUs"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container.js:1933
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:517
@@ -1913,7 +1913,7 @@ msgstr "令牌和密码在 uci 配置中是非加密的。"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/volumes.js:45
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/volumes.js:111
msgid "Total"
msgstr "总计"
msgstr "总数"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/overview.js:170
msgid "Total Memory"
+4 -3
View File
@@ -1,14 +1,15 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-06-05 19:01+0000\n"
"Last-Translator: ZW <roc_fe@users.noreply.hosted.weblate.org>\n"
"PO-Revision-Date: 2026-08-17 17:58+0000\n"
"Last-Translator: 加尼瑪空格把變數都破壞了! "
"<roc_fe@users.noreply.hosted.weblate.org>\n"
"Language-Team: Chinese (Traditional Han script) <https://hosted.weblate.org/"
"projects/openwrt/luciapplicationsdockerman/zh_Hant/>\n"
"Language: zh_Hant\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 2026.6\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-dockerman/htdocs/luci-static/resources/view/dockerman/container_new.js:505
msgid "/mnt/path"
+3 -3
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2019-10-16 16:58-0300\n"
"PO-Revision-Date: 2026-02-27 01:01+0000\n"
"PO-Revision-Date: 2026-08-24 16:48+0000\n"
"Last-Translator: Franco Castillo <castillofrancodamian@gmail.com>\n"
"Language-Team: Spanish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsksmbd/es/>\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.1-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-ksmbd/htdocs/luci-static/resources/view/ksmbd.js:58
msgid "Add optional guest access to IPC$ share, disabled by default"
@@ -73,7 +73,7 @@ msgstr "Forzar Root"
#: applications/luci-app-ksmbd/htdocs/luci-static/resources/view/ksmbd.js:33
msgid "General Settings"
msgstr "Ajustes generales"
msgstr "Configuración general"
#: applications/luci-app-ksmbd/root/usr/share/rpcd/acl.d/luci-app-ksmbd.json:3
msgid "Grant access to LuCI app ksmbd"
+2 -2
View File
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2025-12-30 17:00+0000\n"
"PO-Revision-Date: 2026-08-27 11:44+0000\n"
"Last-Translator: Džiugas Januševičius <dziugas1959@hotmail.com>\n"
"Language-Team: Lithuanian <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsksmbd/lt/>\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"(n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 5.15.1\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-ksmbd/htdocs/luci-static/resources/view/ksmbd.js:58
msgid "Add optional guest access to IPC$ share, disabled by default"
+4 -3
View File
@@ -1,7 +1,8 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-07-03 15:29+0000\n"
"Last-Translator: chigelo <chigelo@gmail.com>\n"
"PO-Revision-Date: 2026-08-28 06:20+0000\n"
"Last-Translator: SnIPeRSnIPeR "
"<snipersniper@users.noreply.hosted.weblate.org>\n"
"Language-Team: Russian <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationsksmbd/ru/>\n"
"Language: ru\n"
@@ -9,7 +10,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 2026.7.1.dev0\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-ksmbd/htdocs/luci-static/resources/view/ksmbd.js:58
msgid "Add optional guest access to IPC$ share, disabled by default"
+5 -4
View File
@@ -2,8 +2,9 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 2026-04-04 14:23+0000\n"
"Last-Translator: evan-yang <yws112358@vip.qq.com>\n"
"PO-Revision-Date: 2026-08-31 06:44+0000\n"
"Last-Translator: Hosted Weblate user 54392 "
"<hamburger2048@users.noreply.hosted.weblate.org>\n"
"Language-Team: Chinese (Simplified Han script) <https://hosted.weblate.org/"
"projects/openwrt/luciapplicationsksmbd/zh_Hans/>\n"
"Language: zh_Hans\n"
@@ -11,7 +12,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.17-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-ksmbd/htdocs/luci-static/resources/view/ksmbd.js:58
msgid "Add optional guest access to IPC$ share, disabled by default"
@@ -69,7 +70,7 @@ msgstr "强制 Root"
#: applications/luci-app-ksmbd/htdocs/luci-static/resources/view/ksmbd.js:33
msgid "General Settings"
msgstr "常规设置"
msgstr "通用设置"
#: applications/luci-app-ksmbd/root/usr/share/rpcd/acl.d/luci-app-ksmbd.json:3
msgid "Grant access to LuCI app ksmbd"
+32
View File
@@ -4,6 +4,38 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
## [2.2.0] - 2026-09-06
本次发布全面适配 OpenClaw 2026.9.1 最新稳定版基线,完成安全升级事务、外部 Supervisor 治理、微信插件 2.4.8 与废弃特性清理。
### 新增与优化
- **OpenClaw 2026.9.1 基线适配**:默认 OpenClaw 升级至 `2026.9.1`,Node.js 运行时基线升级至 `22.23.2`,微信插件升级至 `@tencent-weixin/openclaw-weixin@2.4.8`(兼容 openclaw >=2026.5.12)。
- **安全升级事务机(Staging & Explicit Rollback)**:
- 杜绝原地破坏性 npm 覆写,升级采用独立 Staging 暂存目录提取与包契约校验(`openclaw-package-contract.js`),支持 npm lifecycle 白名单过滤。
- 新增 `openclaw-upgrade-state.sh`,在 `.luci-openclaw-upgrade/status.json` 中完整记录升级事务状态(`phase`、`target_version`、`backup_verified`、`migration_started`、`rollback_mode`、`error_code`)。
- 确立安全红线:迁移一旦开始,任何失败均进入 `recovery` 状态并保留 2026.9.1 候选与迁移状态,严禁旧代码自动覆写已迁移数据库,必须通过 `rollback-explicit` 进行受控的显式回滚。
- **外部 Supervisor 治理**:
- 在 procd 守护进程、LuCI 运行时以及 CLI 执行环境中全面注入 `OPENCLAW_SUPERVISOR_MODE="external"` 与 `OPENCLAW_SERVICE_REPAIR_POLICY="external"`,禁止上游内部 supervisor 干预 OpenWrt procd 的进程与自愈生命周期。
- **废弃补丁与弃用配置清理**:
- 彻底移除对旧版本 upstream dist 的历史猴子补丁(`patch_iframe_headers`、`patch_webchat_session_conflict`)。
- 彻底移除上游已废弃的配置项 `gateway.controlUi.dangerouslyDisableDeviceAuth` 及 `d.plugins.installs` 写入,全量契约测试均已锁定。
- **微信插件运行时合约验证**:
- 微信插件升级至 `2.4.8`,废弃脆弱的纯文本正则匹配,改用官方 JSON 合约 `plugins inspect openclaw-weixin --runtime --json` 与 `plugins registry --refresh --json` 验证渠道健康度。
- **前端体验与低负载轮询优化**:
- LuCI 状态页面轮询增加 `document.hidden` 检查与 `visibilitychange` 事件监听,后台标签页自动挂起轮询,切换回前台时即时触发刷新,降低 OpenWrt 路由器 CPU 负载。
- 安装弹窗明确标注 `v2026.9.1 (已验证版)` 与 `最新版 (未经验证)`,并在 openclaw-env 未就绪时优雅兜底。
- **Web 控制台原生会话内嵌与自动探活**:
- Web 控制台页面原生内嵌 OpenClaw 官方 WebChat / Control UI 会话 iframe,携带 Gateway 鉴权令牌自动免密登录,支持一键全屏切换与会话就地刷新;
- 增加状态探活轮询机制,网关启动中时自动平滑重试并完成内嵌载入,无需手动 F5 刷新;增加 HTTPS 混杂内容安全指引。
- **一键设备配对管理(Web 端与终端配置)**:
- 全面支持 OpenClaw 2026.9.1 新增的设备配对安全机制,在 Web 控制台与终端配置页增加待配对请求悬浮检测卡片与一键批准功能;
- 交互式终端与 Shell 菜单新增 `[p] 设备配对管理 / 一键批准设备配对`,支持批量或单独批准,免除手动复制复杂 ID 的门槛;
- 在 Web 界面与终端各处醒目标注安全风险提示,且控制器使用 `post()` + CSRF 保护,对客户端元数据做 HTML 转义杜绝存储型 XSS。
- **配置管理模型菜单优化与终端重连加固**:
- 移除模型提供商顶级菜单中硬编码的具体过时模型名称,具体型号选择移交二级向导动态发现与精选预设;
- 修复终端配置点击重启后因连接代际竞争导致的“等待服务就绪... 5秒后重试”无限循环问题,引入序列锁与 Cookie 凭据注入,实现秒级就地重连。
## [2.1.1] - 2026-08-27
本次维护以 OpenClaw 2026.7.1-2 为基准,逐项核对配置写入与上游 schema 的一致性。
+3 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-openclaw
PKG_VERSION:=$(strip $(shell cat $(CURDIR)/VERSION 2>/dev/null || echo "1.0.0"))
PKG_RELEASE:=26
PKG_RELEASE:=27
PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com>
PKG_LICENSE:=GPL-3.0
@@ -67,6 +67,7 @@ define Package/$(PKG_NAME)/install
$(INSTALL_BIN) ./root/usr/libexec/openclaw-paths.sh $(1)/usr/libexec/openclaw-paths.sh
$(INSTALL_BIN) ./root/usr/libexec/openclaw-node.sh $(1)/usr/libexec/openclaw-node.sh
$(INSTALL_BIN) ./root/usr/libexec/openclaw-permissions.sh $(1)/usr/libexec/openclaw-permissions.sh
$(INSTALL_BIN) ./root/usr/libexec/openclaw-upgrade-state.sh $(1)/usr/libexec/openclaw-upgrade-state.sh
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller
$(INSTALL_DATA) ./luasrc/controller/openclaw.lua $(1)/usr/lib/lua/luci/controller/openclaw.lua
$(INSTALL_DIR) $(1)/usr/lib/lua/openclaw
@@ -86,6 +87,7 @@ define Package/$(PKG_NAME)/install
$(INSTALL_DATA) ./root/usr/share/openclaw/web-pty.js $(1)/usr/share/openclaw/web-pty.js
$(INSTALL_BIN) ./root/usr/share/openclaw/oc-config-interactive.js $(1)/usr/share/openclaw/oc-config-interactive.js
$(INSTALL_DATA) ./root/usr/share/openclaw/oc-menu-engine.js $(1)/usr/share/openclaw/oc-menu-engine.js
$(INSTALL_BIN) ./root/usr/share/openclaw/openclaw-package-contract.js $(1)/usr/share/openclaw/openclaw-package-contract.js
$(INSTALL_DATA) ./root/usr/share/openclaw/model-presets.json $(1)/usr/share/openclaw/model-presets.json
$(INSTALL_DIR) $(1)/usr/share/openclaw/ui
$(CP) ./root/usr/share/openclaw/ui/* $(1)/usr/share/openclaw/ui/
+8 -3
View File
@@ -27,9 +27,14 @@
| 组件 | 默认版本 | 说明 |
|------|----------|------|
| OpenClaw | `2026.7.1-2` | npm `latest` 稳定标签 |
| Node.js | `22.23.0` | Alpine musl LTS 包;OpenClaw 2026.7.x 要求 `>=22.22.3`,安装后会按 `engines.node` 做强校验 |
| 微信插件 | `@tencent-weixin/openclaw-weixin@2.4.6` | CLI 使用 `@tencent-weixin/openclaw-weixin-cli@2.1.4` |
| OpenClaw | `2026.9.1` | npm `latest` 稳定标签(Phase 0 锁定已测试版本;安装时阻断未知脚本,仅运行已校验 lifecycle) |
| Node.js | `22.23.2` | x86_64 与 aarch64 均优先从 unofficial-builds 及镜像源获取,自托管资产作为兜底;严格验证 `engines.node`,失败关闭 |
| 微信插件 | `@tencent-weixin/openclaw-weixin@2.4.8` | CLI 使用 `@tencent-weixin/openclaw-weixin-cli@2.1.4` |
### 运行时与架构说明
- **x86_64 / aarch64 (musl)**:Node.js v22.23.2 预编译运行时优先通过上游 [unofficial-builds](https://unofficial-builds.nodejs.org/) 及 npmmirror 镜像源获取,安装时自动下载并强校验 `SHASUMS256.txt`。同时在 `.github/workflows/build-node-musl.yml` 中保留基于 Alpine LTS 的自托管构建打包流程作为兜底源。
## 📦 安装
+1 -1
View File
@@ -1 +1 @@
2.1.1
2.2.0
@@ -209,7 +209,7 @@ local function openclaw_user_runner_cmd()
"_oc_raise_openclaw_limits; " ..
"if command -v su >/dev/null 2>&1; then su -s /bin/sh openclaw -c \"$1\"; " ..
"elif command -v runuser >/dev/null 2>&1; then runuser -u openclaw -- sh -c \"$1\"; " ..
"elif command -v start-stop-daemon >/dev/null 2>&1; then _oc_pid=/tmp/openclaw-user-$$.pid; _oc_cwd=$(pwd); rm -f \"$_oc_pid\"; start-stop-daemon -S -m -p \"$_oc_pid\" -c openclaw:openclaw -d \"$_oc_cwd\" -x /bin/sh -- -c \"$1\"; _oc_rc=$?; rm -f \"$_oc_pid\"; return $_oc_rc; " ..
"elif command -v start-stop-daemon >/dev/null 2>&1; then _oc_pid=/tmp/openclaw-user-$$.pid; _oc_cwd=$(pwd); rm -f \"$_oc_pid\"; (cd \"$_oc_cwd\" 2>/dev/null || cd /tmp; start-stop-daemon -S -m -p \"$_oc_pid\" -c openclaw:openclaw -x /bin/sh -- -c \"$1\"); _oc_rc=$?; rm -f \"$_oc_pid\"; return $_oc_rc; " ..
"else echo '❌ 缺少 su/runuser/start-stop-daemon,无法以 openclaw 用户运行命令' >&2; return 127; fi; " ..
"}; "
end
@@ -219,9 +219,10 @@ local function wechat_openclaw_plugin_install_cmd(install_path, oc_entry, log_fi
"OC_WECHAT_ENTRY=" .. shellquote(oc_entry) .. "; export OC_WECHAT_NODE OC_WECHAT_ENTRY; " ..
"echo '使用 OpenClaw 官方插件安装器写入 SQLite 插件索引...' >> " .. shellquote(log_file) .. "; " ..
"_oc_as_openclaw 'HOME=$OC_WECHAT_DATA OPENCLAW_HOME=$OC_WECHAT_DATA OPENCLAW_STATE_DIR=$OC_WECHAT_DATA/.openclaw OPENCLAW_CONFIG_PATH=$OC_WECHAT_DATA/.openclaw/openclaw.json " ..
"OPENCLAW_SUPERVISOR_MODE=external OPENCLAW_SERVICE_REPAIR_POLICY=external " ..
"NODE_ICU_DATA=" .. install_path .. "/node/share/icu NPM_CONFIG_CACHE=$OC_WECHAT_DATA/.npm npm_config_cache=$OC_WECHAT_DATA/.npm TMPDIR=$OC_WECHAT_DATA/.tmp " ..
"PATH=" .. install_path .. "/node/bin:" .. install_path .. "/global/bin:$PATH " ..
"\"$OC_WECHAT_NODE\" \"$OC_WECHAT_ENTRY\" plugins install --force --pin @tencent-weixin/openclaw-weixin@2.4.6' >> " .. shellquote(log_file) .. " 2>&1; " ..
"\"$OC_WECHAT_NODE\" \"$OC_WECHAT_ENTRY\" plugins install --force --pin @tencent-weixin/openclaw-weixin@2.4.8' >> " .. shellquote(log_file) .. " 2>&1; " ..
"RC=$?; echo $RC > " .. shellquote(exit_file) .. "; " ..
"if [ $RC -eq 0 ]; then echo '✅ OpenClaw 插件索引注册完成' >> " .. shellquote(log_file) .. "; else echo '❌ OpenClaw 插件安装失败 (exit: '$RC')' >> " .. shellquote(log_file) .. "; fi; "
end
@@ -233,6 +234,7 @@ local function wechat_finalize_plugin_registry_cmd(install_path, oc_entry, log_f
local cli_env = "HOME=" .. oc_data .. " OPENCLAW_HOME=" .. oc_data ..
" OPENCLAW_STATE_DIR=" .. oc_data .. "/.openclaw" ..
" OPENCLAW_CONFIG_PATH=" .. oc_data .. "/.openclaw/openclaw.json" ..
" OPENCLAW_SUPERVISOR_MODE=external OPENCLAW_SERVICE_REPAIR_POLICY=external" ..
" NODE_ICU_DATA=" .. install_path .. "/node/share/icu" ..
" NPM_CONFIG_CACHE=" .. oc_data .. "/.npm TMPDIR=" .. oc_data .. "/.tmp" ..
" PATH=" .. install_path .. "/node/bin:" .. install_path .. "/global/bin:$PATH "
@@ -244,13 +246,14 @@ local function wechat_finalize_plugin_registry_cmd(install_path, oc_entry, log_f
"FINAL_RC=$?; " ..
"if [ $FINAL_RC -eq 0 ]; then " ..
"echo '正在刷新 SQLite 插件注册表...' >> " .. shellquote(log_file) .. "; " ..
"_oc_as_openclaw '" .. cli .. " plugins registry --refresh' >> " .. shellquote(log_file) .. " 2>&1; " ..
"_oc_as_openclaw '" .. cli .. " plugins registry --refresh --json' >> " .. shellquote(log_file) .. " 2>&1; " ..
"FINAL_RC=$?; fi; " ..
"if [ $FINAL_RC -eq 0 ]; then " ..
"echo '正在验证微信插件运行时状态 (plugins inspect openclaw-weixin --runtime --json)...' >> " .. shellquote(log_file) .. "; " ..
"_OC_WECHAT_INSPECT=/tmp/openclaw-wechat-inspect-$$.log; " ..
"_oc_as_openclaw '" .. cli .. " plugins inspect openclaw-weixin' > \"$_OC_WECHAT_INSPECT\" 2>&1; " ..
"_oc_as_openclaw '" .. cli .. " plugins inspect openclaw-weixin --runtime --json' > \"$_OC_WECHAT_INSPECT\" 2>&1; " ..
"VERIFY_RC=$?; cat \"$_OC_WECHAT_INSPECT\" >> " .. shellquote(log_file) .. "; " ..
"if [ $VERIFY_RC -ne 0 ] || ! grep -q '^Status: loaded$' \"$_OC_WECHAT_INSPECT\" || ! grep -q '^channel: openclaw-weixin$' \"$_OC_WECHAT_INSPECT\"; then FINAL_RC=1; fi; " ..
"if [ $VERIFY_RC -ne 0 ] || ! " .. node_bin .. " -e 'const fs=require(\"fs\"); const raw=fs.readFileSync(process.argv[1],\"utf8\"); try { const j=JSON.parse(raw); const ok=(j && (j.status===\"loaded\" || j.loaded===true || j.enabled===true || j.channel===\"openclaw-weixin\" || (j.channels && j.channels.includes(\"openclaw-weixin\")) || (j.capabilities && j.capabilities.includes(\"channel\")))); process.exit(ok ? 0 : 1); } catch(e) { process.exit(1); }' \"$_OC_WECHAT_INSPECT\" >/dev/null 2>&1; then FINAL_RC=1; fi; " ..
"rm -f \"$_OC_WECHAT_INSPECT\"; fi; " ..
"if [ $FINAL_RC -eq 0 ]; then " ..
"echo '✅ 微信插件已启用,SQLite 注册表已刷新并通过加载验证' >> " .. shellquote(log_file) .. "; " ..
@@ -382,6 +385,10 @@ function index()
-- 微信退出/删除账号 API
entry({"admin", "services", "openclaw", "wechat_logout"}, post("action_wechat_logout"), nil).leaf = true
-- 设备配对管理 API (v2026.9.1+ 浏览器设备配对审批)
entry({"admin", "services", "openclaw", "devices_list"}, call("action_devices_list"), nil).leaf = true
entry({"admin", "services", "openclaw", "devices_approve"}, post("action_devices_approve"), nil).leaf = true
end-- ═══════════════════════════════════════════
-- 获取安装路径 (唯一权威来源: UCI 配置)
-- ═══════════════════════════════════════════
@@ -448,6 +455,49 @@ local function ensure_port_free(port)
end
end
local function read_upgrade_transaction_status(install_path)
local status = {
phase = "idle",
target_version = "",
backup_verified = false,
migration_started = false,
rollback_mode = "none",
error_code = 0
}
install_path = install_path or get_install_path()
local candidates = {
install_path .. "/.luci-openclaw-upgrade/status.json",
install_path .. "/data/.luci-openclaw-upgrade/status.json",
install_path .. "/data/.openclaw/.luci-openclaw-upgrade/status.json",
"/tmp/.luci-openclaw-upgrade/status.json"
}
for _, fpath in ipairs(candidates) do
local f = io.open(fpath, "r")
if f then
local content = f:read("*a")
f:close()
if content and content ~= "" then
local phase = content:match('"phase"%s*:%s*"([^"]+)"')
local target_ver = content:match('"target_version"%s*:%s*"([^"]*)"')
local bk_ver = content:match('"backup_verified"%s*:%s*([%a]+)')
local mig_st = content:match('"migration_started"%s*:%s*([%a]+)')
local rb_mode = content:match('"rollback_mode"%s*:%s*"([^"]+)"')
local err_code = content:match('"error_code"%s*:%s*"?(-?%d+)"?')
if phase then status.phase = phase end
if target_ver then status.target_version = target_ver end
if bk_ver == "true" then status.backup_verified = true
elseif bk_ver == "false" then status.backup_verified = false end
if mig_st == "true" then status.migration_started = true
elseif mig_st == "false" then status.migration_started = false end
if rb_mode then status.rollback_mode = rb_mode end
if err_code then status.error_code = tonumber(err_code) or 0 end
break
end
end
end
return status
end
-- ═══════════════════════════════════════════
-- 状态查询 API: 返回 JSON
-- ═══════════════════════════════════════════
@@ -484,6 +534,12 @@ function action_status()
oc_version = "",
plugin_version = "",
disk_free = "",
phase = "idle",
target_version = "",
backup_verified = false,
migration_started = false,
rollback_mode = "none",
error_code = 0,
}
-- 插件版本
@@ -636,6 +692,14 @@ function action_status()
result.disk_free = df_output
end
local up_status = read_upgrade_transaction_status(install_path)
result.phase = up_status.phase
result.target_version = up_status.target_version
result.backup_verified = up_status.backup_verified
result.migration_started = up_status.migration_started
result.rollback_mode = up_status.rollback_mode
result.error_code = up_status.error_code
http.prepare_content("application/json")
http.write_json(result)
end
@@ -708,6 +772,23 @@ function action_service_ctl()
http.prepare_content("application/json")
http.write_json({ status = "ok", message = "安装已启动,请查看安装日志..." })
return
elseif action == "upgrade" then
-- 先清理旧日志和状态
sys.exec("rm -f /tmp/openclaw-setup.log /tmp/openclaw-setup.pid /tmp/openclaw-setup.exit")
local target_ver = http.formvalue("version") or ""
local env_prefix = ""
if target_ver == "latest" then
env_prefix = "OC_VERSION='latest' "
elseif target_ver ~= "" and target_ver ~= "stable" then
if target_ver:match("^[%d%.%-a-zA-Z]+$") then
env_prefix = "OC_VERSION=" .. shellquote(target_ver) .. " "
end
end
-- 异步执行 openclaw-env upgrade 事务状态机
sys.exec("( " .. env_prefix .. "/usr/bin/openclaw-env upgrade > /tmp/openclaw-setup.log 2>&1; echo $? > /tmp/openclaw-setup.exit ) & echo $! > /tmp/openclaw-setup.pid")
http.prepare_content("application/json")
http.write_json({ status = "ok", message = "OpenClaw 核心升级已启动,正在执行安全事务..." })
return
else
http.prepare_content("application/json")
http.write_json({ status = "error", message = "未知操作: " .. action })
@@ -766,11 +847,18 @@ function action_setup_log()
state = "failed"
end
local up_status = read_upgrade_transaction_status()
http.prepare_content("application/json")
http.write_json({
state = state,
exit_code = exit_code,
log = log
log = log,
phase = up_status.phase,
target_version = up_status.target_version,
backup_verified = up_status.backup_verified,
migration_started = up_status.migration_started,
rollback_mode = up_status.rollback_mode,
error_code = up_status.error_code
})
end
@@ -816,13 +904,47 @@ function action_check_update()
plugin_has_update = true
end
-- 2. OpenClaw 核心版本检查
local openclaw_current = ""
local openclaw_latest = ""
local openclaw_has_update = false
local oc_check_str = sys.exec("/usr/bin/openclaw-env check 2>/dev/null | grep -i 'OpenClaw:' | head -1"):gsub("%s+", "")
if oc_check_str ~= "" then
openclaw_current = oc_check_str:match("[vV]?([%d%.%-]+)") or ""
end
if openclaw_current == "" or openclaw_current == "未安装" then
-- 备用路径读取: 从 package.json 探测
local candidate_pkg = "/opt/openclaw/global/lib/node_modules/openclaw/package.json"
local f_pkg = io.open(candidate_pkg, "r")
if f_pkg then
local content = f_pkg:read("*a")
f_pkg:close()
openclaw_current = content:match('"version"%s*:%s*"([^"]+)"') or ""
end
end
-- 推荐/已验证的最新稳定版本
local tested_ver = sys.exec("sed -n 's/^OC_TESTED_VERSION=\"\\(.*\\)\"/\\1/p' /usr/bin/openclaw-env 2>/dev/null"):gsub("%s+", "")
if tested_ver == "" then tested_ver = "2026.9.1" end
openclaw_latest = tested_ver
if openclaw_current ~= "" and openclaw_current ~= "未安装" and openclaw_latest ~= "" then
if is_newer_version(openclaw_latest, openclaw_current) then
openclaw_has_update = true
end
end
http.prepare_content("application/json")
http.write_json({
status = "ok",
plugin_current = plugin_current,
plugin_latest = plugin_latest,
plugin_has_update = plugin_has_update,
release_notes = release_notes
release_notes = release_notes,
openclaw_current = openclaw_current,
openclaw_latest = openclaw_latest,
openclaw_has_update = openclaw_has_update
})
end
@@ -900,6 +1022,16 @@ function action_get_token()
local uci = require "luci.model.uci".cursor()
local token = uci:get("openclaw", "main", "token") or ""
local pty_token = uci:get("openclaw", "main", "pty_token") or ""
if token == "" then
local install_path = normalize_install_path(uci:get("openclaw", "main", "install_path"))
local config_file = install_path .. "/data/.openclaw/openclaw.json"
local cf = io.open(config_file, "r")
if cf then
local content = cf:read("*a")
cf:close()
token = content:match('"token"%s*:%s*"([^"]+)"') or ""
end
end
http.prepare_content("application/json")
http.write_json({ token = token, pty_token = pty_token })
end
@@ -1599,7 +1731,7 @@ function action_wechat_install()
wechat_python3_bootstrap_cmd("/tmp/openclaw-wechat-install.log") ..
"OC_WECHAT_DATA=%s; export OC_WECHAT_DATA; " ..
"if [ -x /usr/libexec/openclaw-permissions.sh ]; then /usr/libexec/openclaw-permissions.sh prepare-workdirs \"$OC_WECHAT_DATA\" >/dev/null 2>&1; " ..
"else mkdir -p \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" \"$OC_WECHAT_DATA/.openclaw/extensions\"; chown -R openclaw:openclaw \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" 2>/dev/null; chown openclaw:openclaw \"$OC_WECHAT_DATA/.openclaw\" 2>/dev/null; fi; " ..
"else mkdir -p \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" \"$OC_WECHAT_DATA/.openclaw/extensions\"; chown -R openclaw:openclaw \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" 2>/dev/null; chown openclaw:openclaw \"$OC_WECHAT_DATA/.openclaw\" 2>/dev/null; chown -R openclaw:openclaw \"$OC_WECHAT_DATA/.openclaw/extensions\" 2>/dev/null; chmod -R 755 \"$OC_WECHAT_DATA/.openclaw/extensions\" 2>/dev/null; fi; " ..
"if [ ! -w %s/.npm ] || [ ! -w %s/.tmp ]; then echo '❌ npm cache/tmp 目录不可写' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; fi; " ..
"_oc_as_openclaw 'test -w %s/.npm && test -w %s/.tmp && test -w %s/.openclaw' || { echo '❌ openclaw 用户无法写入 npm cache/tmp/data 目录' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; }; " ..
wechat_openclaw_plugin_install_cmd(install_path, oc_entry, "/tmp/openclaw-wechat-install.log", "/tmp/openclaw-wechat-install.exit") ..
@@ -2114,7 +2246,7 @@ function action_wechat_upgrade_plugin()
wechat_python3_bootstrap_cmd("/tmp/openclaw-wechat-install.log") ..
"OC_WECHAT_DATA=%s; export OC_WECHAT_DATA; " ..
"if [ -x /usr/libexec/openclaw-permissions.sh ]; then /usr/libexec/openclaw-permissions.sh prepare-workdirs \"$OC_WECHAT_DATA\" >/dev/null 2>&1; " ..
"else mkdir -p \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" \"$OC_WECHAT_DATA/.openclaw/extensions\"; chown -R openclaw:openclaw \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" 2>/dev/null; chown openclaw:openclaw \"$OC_WECHAT_DATA/.openclaw\" 2>/dev/null; fi; " ..
"else mkdir -p \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" \"$OC_WECHAT_DATA/.openclaw/extensions\"; chown -R openclaw:openclaw \"$OC_WECHAT_DATA/.npm\" \"$OC_WECHAT_DATA/.tmp\" 2>/dev/null; chown openclaw:openclaw \"$OC_WECHAT_DATA/.openclaw\" 2>/dev/null; chown -R openclaw:openclaw \"$OC_WECHAT_DATA/.openclaw/extensions\" 2>/dev/null; chmod -R 755 \"$OC_WECHAT_DATA/.openclaw/extensions\" 2>/dev/null; fi; " ..
"if [ ! -w %s/.npm ] || [ ! -w %s/.tmp ]; then echo '❌ npm cache/tmp 目录不可写' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; fi; " ..
"_oc_as_openclaw 'test -w %s/.npm && test -w %s/.tmp && test -w %s/.openclaw' || { echo '❌ openclaw 用户无法写入 npm cache/tmp/data 目录' >> /tmp/openclaw-wechat-install.log; echo 1 > /tmp/openclaw-wechat-install.exit; exit 0; }; " ..
wechat_openclaw_plugin_install_cmd(install_path, oc_entry, "/tmp/openclaw-wechat-install.log", "/tmp/openclaw-wechat-install.exit") ..
@@ -2205,3 +2337,165 @@ function action_wechat_logout()
http.prepare_content("application/json")
http.write_json({ status = "ok", message = "已下线账号: " .. account_id })
end
-- ═══════════════════════════════════════════
-- 设备配对管理 API (v2026.9.1+ Control UI 设备配对审批)
-- ═══════════════════════════════════════════
local function run_openclaw_devices_cli(args)
local sys = require "luci.sys"
local install_path = get_install_path()
local node_bin = install_path .. "/node/bin/node"
local oc_entry = ""
local search_dirs = {
install_path .. "/global/lib/node_modules/openclaw",
install_path .. "/global/node_modules/openclaw",
install_path .. "/node/lib/node_modules/openclaw",
}
for _, d in ipairs(search_dirs) do
if nixio.fs.stat(d .. "/openclaw.mjs", "type") then
oc_entry = d .. "/openclaw.mjs"
break
elseif nixio.fs.stat(d .. "/dist/cli.js", "type") then
oc_entry = d .. "/dist/cli.js"
break
end
end
if oc_entry == "" or not nixio.fs.stat(node_bin, "type") then
return nil, "OpenClaw 未安装或 Node.js 不可用"
end
local oc_data = install_path .. "/data"
local cmd = string.format(
"ulimit -v unlimited 2>/dev/null || true; " ..
"HOME=%s OPENCLAW_HOME=%s " ..
"OPENCLAW_STATE_DIR=%s/.openclaw " ..
"OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json " ..
"NODE_ICU_DATA=%s/node/share/icu " ..
"PATH=%s/node/bin:%s/global/bin:$PATH " ..
"%s %s %s 2>&1; printf '\\n__EXIT:%%d__' $?",
shellquote(oc_data), shellquote(oc_data),
shellquote(oc_data), shellquote(oc_data),
shellquote(install_path),
shellquote(install_path), shellquote(install_path),
shellquote(node_bin), shellquote(oc_entry), args
)
local output = sys.exec(cmd)
local exit_code = 0
if output then
local code_str = output:match("__EXIT:(%d+)__")
if code_str then
exit_code = tonumber(code_str) or 0
output = output:gsub("%s*__EXIT:%d+__%s*$", "")
end
end
return output, exit_code
end
function action_devices_list()
local http = require "luci.http"
local json = require "luci.jsonc"
local output, exit_code = run_openclaw_devices_cli("devices list --json")
http.prepare_content("application/json")
if not output then
http.write_json({ status = "error", message = "执行失败", pending = {}, paired = {} })
return
end
local json_str = output:match("({%s*\"pending\".*})") or output:match("({.*})")
local data = json_str and json.parse(json_str) or nil
if data and (data.pending or data.paired) then
http.write_json({
status = "ok",
pending = data.pending or {},
paired = data.paired or {}
})
else
http.write_json({
status = "ok",
pending = {},
paired = {},
raw = output:sub(1, 300)
})
end
end
function action_devices_approve()
local http = require "luci.http"
local json = require "luci.jsonc"
local request_id = http.formvalue("request_id")
local approve_all = http.formvalue("all") == "1" or http.formvalue("all") == "true"
local results = {}
local success_count = 0
local fail_count = 0
if approve_all then
local list_out, list_code = run_openclaw_devices_cli("devices list --json")
local json_str = list_out and (list_out:match("({%s*\"pending\".*})") or list_out:match("({.*})"))
local data = json_str and json.parse(json_str) or nil
local pending = (data and data.pending) or {}
if #pending == 0 then
http.prepare_content("application/json")
http.write_json({ status = "ok", success_count = 0, fail_count = 0, count = 0, message = "当前没有待配对的设备请求" })
return
end
for _, item in ipairs(pending) do
local rid = item.requestId
if rid and rid ~= "" and rid:match("^[a-zA-Z0-9%-_]+$") then
local out, exit_code = run_openclaw_devices_cli("devices approve " .. shellquote(rid))
local is_ok = (exit_code == 0) and (out and not out:match("No pending device") and not out:match("Error") and not out:match("ERROR"))
if is_ok or (out and out:lower():match("approved")) then
success_count = success_count + 1
table.insert(results, { requestId = rid, ok = true, message = out })
else
fail_count = fail_count + 1
table.insert(results, { requestId = rid, ok = false, message = out or "批准失败" })
end
end
end
elseif request_id and request_id ~= "" then
if not request_id:match("^[a-zA-Z0-9%-_]+$") then
http.prepare_content("application/json")
http.write_json({ status = "error", message = "无效的请求 ID 格式" })
return
end
local out, exit_code = run_openclaw_devices_cli("devices approve " .. shellquote(request_id))
local is_ok = (exit_code == 0) and (out and not out:match("No pending device") and not out:match("Error") and not out:match("ERROR"))
if is_ok or (out and out:lower():match("approved")) then
success_count = 1
table.insert(results, { requestId = request_id, ok = true, message = out })
else
fail_count = 1
table.insert(results, { requestId = request_id, ok = false, message = out or "批准失败" })
end
else
http.prepare_content("application/json")
http.write_json({ status = "error", message = "缺少 request_id 或 all 参数" })
return
end
local install_path = get_install_path()
fix_openclaw_state_permissions(install_path .. "/data")
http.prepare_content("application/json")
if success_count > 0 then
http.write_json({
status = "ok",
success_count = success_count,
fail_count = fail_count,
results = results,
message = string.format("成功批准 %d 个设备配对请求%s", success_count, fail_count > 0 and string.format(",%d 个失败", fail_count) or "")
})
else
http.write_json({
status = "error",
success_count = 0,
fail_count = fail_count,
results = results,
message = results[1] and results[1].message or "批准设备配对失败"
})
end
end
@@ -54,16 +54,18 @@ act.cfgvalue = function(self, section)
html[#html+1] = '<h3 style="margin:0 0 16px 0;font-size:16px;color:#333;">📦 选择安装版本</h3>'
html[#html+1] = '<div style="display:flex;flex-direction:column;gap:12px;">'
-- 稳定版选项
local default_tested_ver = luci.sys.exec("sed -n 's/^OC_TESTED_VERSION=\"\\(.*\\)\"/\\1/p' /usr/bin/openclaw-env 2>/dev/null"):gsub("%s+", "")
if default_tested_ver == "" then default_tested_ver = "2026.9.1" end
html[#html+1] = '<label style="display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border:2px solid #4a90d9;border-radius:8px;cursor:pointer;background:#f0f7ff;" id="oc-opt-stable">'
html[#html+1] = '<input type="radio" name="oc-ver-choice" value="stable" checked style="margin-top:2px;">'
html[#html+1] = '<div><strong style="color:#333;">✅ 稳定版 (推荐)</strong>'
html[#html+1] = '<div style="font-size:12px;color:#666;margin-top:4px;">版本 v' .. luci.sys.exec("sed -n 's/^OC_TESTED_VERSION=\"\\(.*\\)\"/\\1/p' /usr/bin/openclaw-env 2>/dev/null"):gsub("%s+", "") .. ',已经过完整测试,兼容性良好。</div>'
html[#html+1] = '<div style="font-size:12px;color:#666;margin-top:4px;">版本 v' .. default_tested_ver .. ' (已验证版),已经过完整测试,兼容性良好。</div>'
html[#html+1] = '</div></label>'
-- 最新版选项
html[#html+1] = '<label style="display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border:2px solid #e0e0e0;border-radius:8px;cursor:pointer;background:#fff;" id="oc-opt-latest">'
html[#html+1] = '<input type="radio" name="oc-ver-choice" value="latest" style="margin-top:2px;">'
html[#html+1] = '<div><strong style="color:#333;">🆕 最新版</strong>'
html[#html+1] = '<div style="font-size:12px;color:#e36209;margin-top:4px;">⚠️ 安装 npm 上的最新发布版本,可能存在未经验证的兼容性问题。</div>'
html[#html+1] = '<div><strong style="color:#333;">🆕 最新版 (未经验证)</strong>'
html[#html+1] = '<div style="font-size:12px;color:#e36209;margin-top:4px;">⚠️ 安装 npm 上的最新发布版本 (未经验证),可能存在未经验证的兼容性问题。</div>'
html[#html+1] = '</div></label>'
html[#html+1] = '</div>'
-- 自定义安装路径
@@ -372,27 +374,57 @@ act.cfgvalue = function(self, section)
html[#html+1] = 'var verBadge="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-family:SF Mono,Consolas,Menlo,monospace;background:#e1e4e8;color:#24292f;margin-left:4px;";'
-- 插件版本检查 (带渐变徽章)
html[#html+1] = 'if(r.plugin_current){'
html[#html+1] = 'if(r.plugin_has_update){msgs.push("<span style=\\""+badgeNew+"\\">🔌 有新版本</span> v"+r.plugin_current+" → <span style=\\""+verBadge+"\\">v"+r.plugin_latest+"</span>");}'
html[#html+1] = 'else if(r.plugin_latest){msgs.push("<span style=\\""+badgeLatest+"\\">✅ 已是最新</span> v"+r.plugin_current);}'
html[#html+1] = 'else{msgs.push("<span style=\\""+badgeUnknown+"\\">🔌 无法检查</span> v"+r.plugin_current);}'
html[#html+1] = 'if(r.plugin_has_update){msgs.push("<span style=\\""+badgeNew+"\\">🔌 插件有新版</span> v"+r.plugin_current+" → <span style=\\""+verBadge+"\\">v"+r.plugin_latest+"</span>");}'
html[#html+1] = 'else if(r.plugin_latest){msgs.push("<span style=\\""+badgeLatest+"\\">🔌 插件已是最新</span> v"+r.plugin_current);}'
html[#html+1] = 'else{msgs.push("<span style=\\""+badgeUnknown+"\\">🔌 插件无法检查</span> v"+r.plugin_current);}'
html[#html+1] = '}'
-- OpenClaw 核心版本检查
html[#html+1] = 'if(r.openclaw_current){'
html[#html+1] = 'if(r.openclaw_has_update){msgs.push("<span style=\\""+badgeNew+"\\">🦞 核心有新版</span> v"+r.openclaw_current+" → <span style=\\""+verBadge+"\\">v"+r.openclaw_latest+"</span>");}'
html[#html+1] = 'else if(r.openclaw_latest){msgs.push("<span style=\\""+badgeLatest+"\\">🦞 核心已是最新</span> v"+r.openclaw_current);}'
html[#html+1] = 'else{msgs.push("<span style=\\""+badgeUnknown+"\\">🦞 核心无法检查</span> v"+r.openclaw_current);}'
html[#html+1] = '}'
html[#html+1] = 'if(msgs.length===0)msgs.push("<span style=\\""+badgeUnknown+"\\">无法获取版本信息</span>");'
html[#html+1] = 'el.innerHTML=msgs.join("<br/>");'
-- 插件有更新时: 卡片式更新日志 + 操作按钮
html[#html+1] = 'if(r.plugin_has_update){'
-- 有任何更新时显示操作面板
html[#html+1] = 'if(r.plugin_has_update || r.openclaw_has_update){'
html[#html+1] = 'act.style.display="block";'
html[#html+1] = 'window._pluginLatestVer=r.plugin_latest;'
html[#html+1] = 'window._coreLatestVer=r.openclaw_latest;'
html[#html+1] = 'var notesHtml="";'
html[#html+1] = 'if(r.release_notes){'
html[#html+1] = 'if(r.plugin_has_update && r.release_notes){'
html[#html+1] = 'var rendered=ocMarkdownToHtml(r.release_notes);'
-- 卡片式容器: 圆角边框 + 微阴影 + 版本标题栏
html[#html+1] = 'notesHtml="<div style=\\"margin:12px 0;border:1px solid #d0d7de;border-radius:10px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06);\\"><div style=\\"background:linear-gradient(135deg,#f6f8fa 0%,#ffffff 100%);padding:12px 16px;border-bottom:1px solid #d0d7de;display:flex;align-items:center;justify-content:space-between;\\"><span style=\\"font-size:14px;font-weight:600;color:#24292f;\\">📋 更新日志</span><span style=\\"display:inline-flex;align-items:center;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600;background:linear-gradient(135deg,#e3f2fd 0%,#bbdefb 100%);color:#1565c0;border:1px solid #64b5f6;\\">v"+r.plugin_latest+"</span></div><div style=\\"padding:16px;max-height:450px;overflow-y:auto;background:#fff;\\">"+rendered+"</div></div>";'
html[#html+1] = 'notesHtml="<div style=\\"margin:12px 0;border:1px solid #d0d7de;border-radius:10px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06);\\"><div style=\\"background:linear-gradient(135deg,#f6f8fa 0%,#ffffff 100%);padding:12px 16px;border-bottom:1px solid #d0d7de;display:flex;align-items:center;justify-content:space-between;\\"><span style=\\"font-size:14px;font-weight:600;color:#24292f;\\">📋 插件更新日志</span><span style=\\"display:inline-flex;align-items:center;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600;background:linear-gradient(135deg,#e3f2fd 0%,#bbdefb 100%);color:#1565c0;border:1px solid #64b5f6;\\">v"+r.plugin_latest+"</span></div><div style=\\"padding:16px;max-height:450px;overflow-y:auto;background:#fff;\\">"+rendered+"</div></div>";'
html[#html+1] = '}'
-- 操作按钮区: 分组设计
html[#html+1] = 'act.innerHTML=notesHtml+"<div style=\\"margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;\\"><button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"ocPluginUpgrade()\\" id=\\"btn-plugin-upgrade\\" style=\\"box-shadow:0 2px 4px rgba(0,0,0,0.1);\\">⬆️ 一键升级 v"+r.plugin_latest+"</button><a href=\\"https://github.com/10000ge10000/luci-app-openclaw/releases/latest\\" target=\\"_blank\\" rel=\\"noopener\\" class=\\"btn cbi-button cbi-button-action\\" style=\\"text-decoration:none;\\">📥 GitHub 下载</a></div>";'
-- 操作按钮区: 支持一键升级插件与一键升级核心
html[#html+1] = 'var actBtns="<div style=\\"margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;\\">";'
html[#html+1] = 'if(r.plugin_has_update){actBtns+="<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"ocPluginUpgrade()\\" id=\\"btn-plugin-upgrade\\" style=\\"box-shadow:0 2px 4px rgba(0,0,0,0.1);\\">⬆️ 一键升级插件 (v"+r.plugin_latest+")</button><a href=\\"https://github.com/10000ge10000/luci-app-openclaw/releases/latest\\" target=\\"_blank\\" rel=\\"noopener\\" class=\\"btn cbi-button cbi-button-action\\" style=\\"text-decoration:none;\\">📥 GitHub 下载</a>";}'
html[#html+1] = 'if(r.openclaw_has_update){actBtns+="<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"ocOpenclawCoreUpgrade()\\" id=\\"btn-core-upgrade\\" style=\\"background:#d97706;border-color:#b45309;color:#fff;box-shadow:0 2px 4px rgba(0,0,0,0.1);\\">⬆️ 一键升级 OpenClaw 核心 (v"+r.openclaw_latest+")</button>";}'
html[#html+1] = 'actBtns+="</div>";'
html[#html+1] = 'act.innerHTML=notesHtml+actBtns;'
html[#html+1] = '}'
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 检测失败</span>";}'
html[#html+1] = '});}'
html[#html+1] = 'function ocOpenclawCoreUpgrade(){'
html[#html+1] = 'var ver=window._coreLatestVer||"2026.9.1";'
html[#html+1] = 'if(!confirm("确定要将 OpenClaw 核心升级到 v"+ver+"?\\n\\n升级将执行安全事务:自动全量备份状态、SQLite 数据库预检与配置平滑迁移。"))return;'
html[#html+1] = 'var btn=document.getElementById("btn-core-upgrade");'
html[#html+1] = 'var panel=document.getElementById("setup-log-panel");'
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
html[#html+1] = 'var titleEl=document.getElementById("setup-log-title");'
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
html[#html+1] = 'var resultEl=document.getElementById("setup-log-result");'
html[#html+1] = 'if(btn){btn.disabled=true;btn.textContent="⏳ 正在启动核心升级...";}'
html[#html+1] = 'panel.style.display="block";'
html[#html+1] = 'logEl.textContent="正在启动 OpenClaw 核心升级事务...\\n";'
html[#html+1] = 'titleEl.textContent="📋 OpenClaw 核心升级日志";'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 核心升级中 (执行状态机事务)...</span>";'
html[#html+1] = 'resultEl.style.display="none";'
html[#html+1] = '(new XHR()).post("' .. service_ctl_url .. '",{action:"upgrade",version:ver,token:ocCsrfToken},function(x){'
html[#html+1] = 'ocPollSetupLog();'
html[#html+1] = '});'
html[#html+1] = '}'
-- ═══ 插件一键升级 ═══
html[#html+1] = 'var _pluginUpgradeTimer=null;'
@@ -6,6 +6,7 @@
<%
local uci = require "luci.model.uci".cursor()
local pty_port = uci:get("openclaw", "main", "pty_port") or "18793"
local pty_token = uci:get("openclaw", "main", "pty_token") or ""
%>
<style type="text/css">
@@ -13,6 +14,39 @@ local pty_port = uci:get("openclaw", "main", "pty_port") or "18793"
.oc-page-header h2 { font-size: 18px; font-weight: 600; color: #333; margin: 0 0 6px 0; }
.oc-page-header p { font-size: 13px; color: #666; margin: 0; line-height: 1.6; }
.oc-console-info {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
margin-bottom: 16px;
background: #f6f8fa;
border: 1px solid #e1e4e8;
border-radius: 6px;
font-size: 13px;
color: #555;
flex-wrap: wrap;
}
.oc-console-info .label { color: #888; }
.oc-console-info .value { font-family: monospace; color: #333; font-weight: 500; }
.oc-console-info .sep { color: #ddd; }
.oc-console-info .btn-open {
margin-left: auto;
padding: 5px 14px;
background: #4a90d9;
color: #fff;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
font-weight: 500;
}
.oc-console-info .btn-open:hover { background: #357abd; }
.oc-terminal-wrap {
border: 2px solid #2d333b;
border-radius: 8px;
@@ -20,17 +54,55 @@ local pty_port = uci:get("openclaw", "main", "pty_port") or "18793"
background: #1a1b26;
}
#oc-terminal-iframe { width: 100%; height: 600px; border: none; display: block; }
#oc-terminal-iframe { width: 100%; height: 650px; border: none; display: block; }
.oc-terminal-loading {
display: flex; align-items: center; justify-content: center;
height: 200px; color: #7aa2f7; font-size: 14px; background: #1a1b26;
min-height: 250px; color: #7aa2f7; font-size: 14px; background: #1a1b26;
}
</style>
<div class="oc-page-header">
<h2>⚙️ 终端配置</h2>
<p>通过内嵌的交互式终端 (oc-config) 进行 OpenClaw 的完整配置管理。支持 AI 模型配置、消息渠道设置(QQ、Telegram、Discord 等)、健康检查等。</p>
<h2>⚙️ 终端配置 (配置管理)</h2>
<p>通过交互式终端 (oc-config) 进行 OpenClaw 的完整配置管理。支持 AI 模型配置、消息渠道设置(QQ、Telegram、Discord 等)、健康检查等。</p>
</div>
<div class="oc-console-info">
<span class="label">终端服务:</span>
<span class="value" id="oc-term-addr">-</span>
<span class="sep">|</span>
<span class="label">状态:</span>
<span id="oc-term-status-text">检测中...</span>
<a id="oc-term-open-btn" class="btn-open" href="#" target="_blank" rel="noopener">
↗ 新窗口打开终端
</a>
</div>
<div id="oc-adv-pairing-banner" style="display:none; padding:12px 16px; margin-bottom:14px; background:#fffbe6; border:1px solid #ffe58f; border-left:4px solid #faad14; border-radius:6px;">
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; flex-wrap:wrap;">
<div style="flex:1; min-width:280px;">
<div style="font-weight:600; color:#d48806; font-size:14px; margin-bottom:4px; display:flex; align-items:center; gap:8px;">
<span>🔔 检测到待配对设备请求</span>
<span id="oc-adv-pairing-badge" style="background:#faad14; color:#fff; padding:1px 7px; border-radius:10px; font-size:11px;">0</span>
</div>
<div style="font-size:13px; color:#555; line-height:1.5; margin-bottom:6px;">
Control UI 浏览器连接时触发了安全配对策略,您可直接在此一键批准,无需在下方终端中手动查找与输入 Request ID。
</div>
<div style="font-size:12px; color:#cf1322; background:#fff1f0; border:1px solid #ffa39e; padding:6px 10px; border-radius:4px; line-height:1.4; margin-bottom:6px;">
⚠️ <strong>风险提示</strong>:批准设备配对后,该浏览器将获得 OpenClaw 网关的完全控制权限。请确保在受信任的局域网环境,并仅在您本人正在连接时批准。
</div>
<div id="oc-adv-pairing-items" style="font-size:12px; font-family:monospace; color:#333; line-height:1.6;"></div>
</div>
<div style="display:flex; gap:8px; align-items:center; margin-top:4px;">
<button type="button" id="oc-adv-pairing-approve-all-btn" style="padding:6px 14px; background:#52c41a; color:#fff; border:none; border-radius:4px; font-weight:600; font-size:12px; cursor:pointer;">
⚡ 一键批准配对
</button>
<button type="button" id="oc-adv-pairing-refresh-btn" style="padding:6px 10px; background:#f0f2f5; color:#333; border:1px solid #d0d7de; border-radius:4px; font-size:12px; cursor:pointer;">
🔄 刷新
</button>
</div>
</div>
<div id="oc-adv-pairing-msg" style="display:none; margin-top:8px; font-size:13px; font-weight:500;"></div>
</div>
<div class="oc-terminal-wrap">
@@ -45,49 +117,165 @@ local pty_port = uci:get("openclaw", "main", "pty_port") or "18793"
//<![CDATA[
(function() {
var ptyPort = '<%=pty_port%>';
var ptyToken = '<%=pty_token%>';
var statusUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "status_api")%>';
var tokenUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "get_token")%>';
// get_token 返回凭据,已改为 post(),需带 CSRF token
var ocCsrfToken = '<%=token%>';
var container = document.getElementById('oc-terminal-container');
var loading = document.getElementById('oc-terminal-loading');
var ptyToken = '';
var addrEl = document.getElementById('oc-term-addr');
var statusTextEl = document.getElementById('oc-term-status-text');
var openBtn = document.getElementById('oc-term-open-btn');
var host = window.location.hostname;
if (addrEl) addrEl.textContent = host + ':' + ptyPort;
var targetUrl = 'http://' + host + ':' + ptyPort + '/';
if (ptyToken) {
targetUrl += '?pty_token=' + encodeURIComponent(ptyToken);
}
if (openBtn) openBtn.href = targetUrl;
var devicesListUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "devices_list")%>';
var devicesApproveUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "devices_approve")%>';
var advPairingBanner = document.getElementById('oc-adv-pairing-banner');
var advPairingBadge = document.getElementById('oc-adv-pairing-badge');
var advPairingItems = document.getElementById('oc-adv-pairing-items');
var advPairingMsg = document.getElementById('oc-adv-pairing-msg');
var advPairingApproveAllBtn = document.getElementById('oc-adv-pairing-approve-all-btn');
var advPairingRefreshBtn = document.getElementById('oc-adv-pairing-refresh-btn');
function escapeHtml(str) {
return String(str || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function checkAdvDevices() {
(new XHR()).get(devicesListUrl, null, function(x) {
try {
var data = JSON.parse(x.responseText);
var pending = (data && data.pending) || [];
if (advPairingBadge) advPairingBadge.textContent = pending.length;
if (pending.length > 0) {
var html = '';
for (var i = 0; i < pending.length; i++) {
var item = pending[i];
var ip = escapeHtml(item.remoteIp || item.ip || '未知IP');
var client = escapeHtml(item.clientId || item.clientMode || 'webchat');
var plat = escapeHtml(item.platform || '');
var rid = escapeHtml(item.requestId || '');
html += '<div style="background:#fff; border:1px solid #e1e4e8; border-radius:4px; padding:6px 10px; margin-bottom:6px; display:flex; justify-content:space-between; align-items:center; gap:8px; flex-wrap:wrap;">' +
'<div><strong>IP:</strong> ' + ip + ' | <strong>客户端:</strong> ' + client + (plat ? ' (' + plat + ')' : '') + ' <br/><span style="color:#888; font-size:11px;">ID: ' + rid + '</span></div>' +
'<button type="button" class="btn-adv-approve-single" data-rid="' + rid + '" style="font-size:12px; padding:3px 10px; background:#f6ffed; border:1px solid #b7eb8f; color:#389e0d; border-radius:4px; cursor:pointer;">批准配对</button>' +
'</div>';
}
advPairingItems.innerHTML = html;
advPairingBanner.style.display = 'block';
var singleBtns = advPairingItems.querySelectorAll('.btn-adv-approve-single');
for (var j = 0; j < singleBtns.length; j++) {
singleBtns[j].addEventListener('click', function(ev) {
var rid = this.getAttribute('data-rid');
approveAdvDevice(rid);
});
}
} else {
advPairingBanner.style.display = 'none';
}
} catch(e) {}
});
}
function approveAdvDevice(rid) {
advPairingMsg.style.display = 'block';
advPairingMsg.style.color = '#1890ff';
advPairingMsg.textContent = '⏳ 正在批准设备配对...';
var postData = { token: ocCsrfToken };
if (rid) {
postData.request_id = rid;
} else {
postData.all = '1';
}
(new XHR()).post(devicesApproveUrl, postData, function(x) {
try {
var res = JSON.parse(x.responseText);
if (res.status === 'ok') {
advPairingMsg.style.color = '#52c41a';
advPairingMsg.innerHTML = '✅ ' + (res.message || '批准成功!');
setTimeout(function() {
checkAdvDevices();
}, 1500);
} else {
advPairingMsg.style.color = '#cf222e';
advPairingMsg.innerHTML = '❌ ' + (res.message || '批准失败');
}
} catch(e) {
advPairingMsg.style.color = '#cf222e';
advPairingMsg.textContent = '❌ 批准失败:请求异常';
}
});
}
if (advPairingApproveAllBtn) {
advPairingApproveAllBtn.addEventListener('click', function() {
approveAdvDevice(null);
});
}
if (advPairingRefreshBtn) {
advPairingRefreshBtn.addEventListener('click', function() {
checkAdvDevices();
});
}
function checkAndLoadTerminal() {
// 先获取 PTY token
(new XHR()).post(tokenUrl, { token: ocCsrfToken }, function(tx) {
checkAdvDevices();
setTimeout(checkAdvDevices, 3000);
(new XHR()).get(statusUrl, null, function(x) {
try {
var td = JSON.parse(tx.responseText);
ptyToken = td.pty_token || '';
} catch(e) {}
(new XHR()).get(statusUrl, null, function(x) {
try {
var d = JSON.parse(x.responseText);
if (d.pty_running) {
showIframe();
} else {
loading.innerHTML = '❌ 配置终端未运行<br/>' +
'<span style="font-size:12px;color:#999;">请先在「基本设置」中启用并启动服务。</span>';
}
} catch(e) {
loading.textContent = '检查终端状态失败';
var d = JSON.parse(x.responseText);
if (d.pty_running) {
if (statusTextEl) statusTextEl.innerHTML = '<span style="color:#1a7f37;">● 运行中</span>';
showIframe();
} else {
if (statusTextEl) statusTextEl.innerHTML = '<span style="color:#cf222e;">● 未运行</span>';
loading.innerHTML = '<div style="text-align:center;padding:24px;color:#cf222e;">❌ 配置终端未运行<br/>' +
'<span style="font-size:12px;color:#999;">请先在「基本设置」中启用并启动服务。</span></div>';
}
});
} catch(e) {
if (statusTextEl) statusTextEl.textContent = '检测完成';
showIframe();
}
});
}
function showIframe() {
var proto = window.location.protocol;
var host = window.location.hostname;
var url = proto + '//' + host + ':' + ptyPort + '/';
if (ptyToken) url += '?pty_token=' + encodeURIComponent(ptyToken);
var isHttps = (window.location.protocol === 'https:');
if (isHttps) {
loading.innerHTML = '<div style="text-align:center;padding:40px 20px;color:#abb2bf;">' +
'<div style="font-size:36px;margin-bottom:12px;">↗</div>' +
'<div style="font-size:15px;margin-bottom:8px;color:#e5c07b;font-weight:600;">由于浏览器安全策略限制,无法在 HTTPS 页面中直接内嵌 HTTP 终端</div>' +
'<div style="font-size:13px;color:#828997;max-width:560px;margin:0 auto 20px;line-height:1.6;">' +
'终端服务已在端口 ' + ptyPort + ' 正常就绪。请点击下方按钮直接在新窗口打开交互式终端,已自动绑定认证 Token。' +
'</div>' +
'<a href="' + targetUrl + '" target="_blank" rel="noopener" style="display:inline-block;padding:9px 24px;background:#4a90d9;color:#fff;border-radius:6px;text-decoration:none;font-weight:500;font-size:13px;box-shadow:0 2px 6px rgba(0,0,0,0.3);">' +
'立即在新窗口打开终端' +
'</a>' +
'<div style="margin-top:16px;font-size:12px;color:#5c6370;">' +
'小贴士:您也可以通过 HTTP (http://' + host + ') 登录路由器后台直接在此内嵌操作。' +
'</div>' +
'</div>';
return;
}
loading.style.display = 'none';
var iframe = document.createElement('iframe');
iframe.id = 'oc-terminal-iframe';
iframe.src = url;
iframe.src = targetUrl;
iframe.setAttribute('allowfullscreen', 'true');
iframe.setAttribute('allow', 'clipboard-read; clipboard-write');
// 防止 LuCI 父页面键盘事件干扰 iframe 内的终端输入
@@ -116,6 +304,28 @@ local pty_port = uci:get("openclaw", "main", "pty_port") or "18793"
}
}
// 监听 iframe 终端组件的 token 请求,支持重载后自动补充认证凭据
window.addEventListener('message', function(ev) {
if (ev.data && ev.data.type === 'openclaw_request_token') {
var srcWin = ev.source;
if (ptyToken) {
if (srcWin) {
try { srcWin.postMessage({ type: 'openclaw_token', pty_token: ptyToken }, '*'); } catch(e) {}
}
} else {
(new XHR()).post(tokenUrl, { token: ocCsrfToken }, function(tx) {
try {
var td = JSON.parse(tx.responseText);
ptyToken = td.pty_token || '';
if (ptyToken && srcWin) {
srcWin.postMessage({ type: 'openclaw_token', pty_token: ptyToken }, '*');
}
} catch(e) {}
});
}
}
});
checkAndLoadTerminal();
})();
//]]>
@@ -30,8 +30,28 @@ local port = uci:get("openclaw", "main", "port") or "18789"
.oc-console-info .label { color: #888; }
.oc-console-info .value { font-family: monospace; color: #333; font-weight: 500; }
.oc-console-info .sep { color: #ddd; }
.oc-console-info .btn-open {
.oc-console-actions {
margin-left: auto;
display: flex;
gap: 8px;
align-items: center;
}
.oc-console-actions .btn-action {
padding: 4px 12px;
background: #f0f2f5;
color: #333;
border: 1px solid #d0d7de;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
text-decoration: none;
transition: background 0.2s;
}
.oc-console-actions .btn-action:hover {
background: #e1e4e8;
text-decoration: none;
}
.oc-console-actions .btn-open {
padding: 4px 14px;
background: #4a90d9;
color: #fff;
@@ -41,7 +61,10 @@ local port = uci:get("openclaw", "main", "port") or "18789"
cursor: pointer;
text-decoration: none;
}
.oc-console-info .btn-open:hover { background: #357abd; }
.oc-console-actions .btn-open:hover {
background: #357abd;
text-decoration: none;
}
.oc-console-wrap {
border: 2px solid #e0e0e0;
@@ -49,11 +72,36 @@ local port = uci:get("openclaw", "main", "port") or "18789"
overflow: hidden;
background: #fff;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
position: relative;
min-height: 720px;
}
.oc-console-wrap:fullscreen {
border: none;
border-radius: 0;
height: 100vh;
width: 100vw;
}
.oc-console-wrap:-webkit-full-screen {
border: none;
border-radius: 0;
height: 100vh;
width: 100vw;
}
#oc-console-iframe {
width: 100%;
height: 760px;
border: none;
display: block;
}
.oc-console-wrap:fullscreen #oc-console-iframe,
.oc-console-wrap:-webkit-full-screen #oc-console-iframe {
height: 100vh;
}
.oc-console-loading {
display: flex; flex-direction: column; align-items: center; justify-content: center;
height: 300px; color: #666; font-size: 14px; background: #fafafa;
height: 360px; color: #666; font-size: 14px; background: #fafafa;
}
.oc-console-loading .spinner {
width: 32px; height: 32px; border: 3px solid #e0e0e0; border-top: 3px solid #4a90d9;
@@ -76,12 +124,48 @@ local port = uci:get("openclaw", "main", "port") or "18789"
<span class="sep">|</span>
<span class="label">状态:</span>
<span id="oc-console-status-text">检查中...</span>
<a id="oc-console-open-btn" class="btn-open" href="#" target="_blank" rel="noopener" style="display:none;">
↗ 新窗口打开
</a>
<div class="oc-console-actions" id="oc-console-actions" style="display:none;">
<button type="button" id="oc-console-pairing-toggle-btn" class="btn-action" title="管理/批准设备配对请求">📱 设备配对 <span id="oc-console-pairing-nav-badge" style="display:none; background:#ff4d4f; color:#fff; padding:0 6px; border-radius:10px; font-size:11px; font-weight:600; margin-left:2px;">0</span></button>
<button type="button" id="oc-console-refresh-btn" class="btn-action" title="重新载入当前会话">🔄 刷新</button>
<button type="button" id="oc-console-fullscreen-btn" class="btn-action" title="全屏查看">⛶ 全屏</button>
<a id="oc-console-open-btn" class="btn-open" href="#" target="_blank" rel="noopener">
↗ 新窗口打开
</a>
</div>
</div>
<div class="oc-console-wrap">
<div id="oc-pairing-banner" style="display:none; padding:12px 16px; margin-bottom:14px; background:#fffbe6; border:1px solid #ffe58f; border-left:4px solid #faad14; border-radius:6px;">
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; flex-wrap:wrap;">
<div style="flex:1; min-width:280px;">
<div style="font-weight:600; color:#d48806; font-size:14px; margin-bottom:4px; display:flex; align-items:center; gap:8px;">
<span>🔔 检测到待配对设备请求</span>
<span id="oc-pairing-badge" style="background:#faad14; color:#fff; padding:1px 7px; border-radius:10px; font-size:11px;">0</span>
</div>
<div style="font-size:13px; color:#555; line-height:1.5; margin-bottom:6px;">
首次访问 Control UI 时网关需要主机批准该浏览器。点击「一键批准配对」即可完成授权并进入控制台。
</div>
<div style="font-size:12px; color:#cf1322; background:#fff1f0; border:1px solid #ffa39e; padding:6px 10px; border-radius:4px; line-height:1.4; margin-bottom:6px;">
⚠️ <strong>风险提示</strong>:批准设备配对后,该浏览器将获得 OpenClaw 网关的完全控制权限。请确保在受信任的局域网环境,并仅在您本人正在连接时批准。
</div>
<div id="oc-pairing-items" style="font-size:12px; font-family:monospace; color:#333; line-height:1.6;"></div>
</div>
<div style="display:flex; gap:8px; align-items:center; margin-top:4px;">
<button type="button" id="oc-pairing-approve-all-btn" class="btn-open" style="background:#52c41a; font-weight:600; padding:6px 14px;">
⚡ 一键批准配对
</button>
<button type="button" id="oc-pairing-refresh-btn" class="btn-action" style="padding:6px 10px;">
🔄 刷新
</button>
</div>
</div>
<div id="oc-pairing-msg" style="display:none; margin-top:8px; font-size:13px; font-weight:500;"></div>
</div>
<div id="oc-console-https-hint" style="display:none; padding:10px 14px; margin-bottom:14px; background:#fffbe6; border:1px solid #ffe58f; border-radius:6px; font-size:13px; color:#d48806; line-height:1.6;">
⚠️ <strong>浏览器安全提示</strong>:当前正在通过 HTTPS 访问 LuCI。由于 OpenClaw 网关为 HTTP 服务,浏览器可能会拦截跨协议内嵌页面(Mixed Content)。若下方界面显示空白,请点击右上角「<strong>↗ 新窗口打开</strong>」直接访问会话,或改用 HTTP 协议访问 LuCI。
</div>
<div class="oc-console-wrap" id="oc-console-wrap">
<div id="oc-console-container">
<div class="oc-console-loading" id="oc-console-loading">
<div class="spinner"></div>
@@ -97,27 +181,54 @@ local port = uci:get("openclaw", "main", "port") or "18789"
var gwToken = '';
var statusUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "status_api")%>';
var tokenUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "get_token")%>';
// get_token 返回凭据,已改为 post(),需带 CSRF token
var ocCsrfToken = '<%=token%>';
var container = document.getElementById('oc-console-container');
var loading = document.getElementById('oc-console-loading');
var addrEl = document.getElementById('oc-console-addr');
var modelEl = document.getElementById('oc-console-model');
var statusTextEl = document.getElementById('oc-console-status-text');
var actionsEl = document.getElementById('oc-console-actions');
var openBtn = document.getElementById('oc-console-open-btn');
var refreshBtn = document.getElementById('oc-console-refresh-btn');
var fullscreenBtn = document.getElementById('oc-console-fullscreen-btn');
var wrapEl = document.getElementById('oc-console-wrap');
var iframeEl = null;
function getConsoleUrl() {
var host = window.location.hostname;
// OpenClaw Gateway 当前只监听 HTTP;LuCI 使用 HTTPS 时也必须强制走 http,避免浏览器误连 HTTPS 端口。
var url = 'http://' + host + ':' + gwPort + '/';
if (gwToken) url += '#token=' + encodeURIComponent(gwToken);
return url;
}
function toggleFullScreen() {
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
if (wrapEl.requestFullscreen) {
wrapEl.requestFullscreen();
} else if (wrapEl.webkitRequestFullscreen) {
wrapEl.webkitRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
}
var retryTimer = null;
function scheduleCheck(ms) {
if (retryTimer) clearTimeout(retryTimer);
retryTimer = setTimeout(function() {
retryTimer = null;
checkAndLoad();
}, ms || 3000);
}
function checkAndLoad() {
addrEl.textContent = window.location.hostname + ':' + gwPort;
// 先获取 token,再检查状态
(new XHR()).post(tokenUrl, { token: ocCsrfToken }, function(tx) {
try {
var td = JSON.parse(tx.responseText);
@@ -132,13 +243,22 @@ local port = uci:get("openclaw", "main", "port") or "18789"
modelEl.textContent = d.active_model;
}
if (d.gateway_running) {
if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
statusTextEl.innerHTML = '<span style="color:#1a7f37;">● 网关运行中</span>';
openBtn.href = url;
openBtn.style.display = '';
showLaunch(url);
actionsEl.style.display = 'flex';
if (location.protocol === 'https:') {
var hintEl = document.getElementById('oc-console-https-hint');
if (hintEl) hintEl.style.display = 'block';
}
showIframe(url);
checkDevices(false);
setTimeout(function() { checkDevices(false); }, 2500);
setTimeout(function() { checkDevices(false); }, 6000);
} else if (d.gateway_failed) {
if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
statusTextEl.innerHTML = '<span style="color:#cf222e;">● 网关启动失败</span>';
openBtn.style.display = 'none';
actionsEl.style.display = 'none';
loading.style.display = '';
loading.innerHTML = '<div style="text-align:center;color:#666;">' +
'<div style="font-size:40px;margin-bottom:12px;">×</div>' +
@@ -147,37 +267,183 @@ local port = uci:get("openclaw", "main", "port") or "18789"
'</div>';
} else if (d.gateway_starting) {
statusTextEl.innerHTML = '<span style="color:#9a6700;">⏳ 网关正在启动</span>';
openBtn.style.display = 'none';
actionsEl.style.display = 'none';
loading.style.display = '';
loading.innerHTML = '<div style="text-align:center;color:#666;">' +
'<div style="font-size:40px;margin-bottom:12px;">⏳</div>' +
'<div style="font-size:15px;margin-bottom:6px;">OpenClaw 网关正在启动中...</div>' +
'<div style="font-size:12px;color:#999;">通常 20~40 秒;首次安装或路由器负载较高时会更久,页面会自动刷新。</div>' +
'</div>';
scheduleCheck(3000);
} else {
statusTextEl.innerHTML = '<span style="color:#cf222e;">● 网关未运行</span>';
openBtn.style.display = 'none';
actionsEl.style.display = 'none';
loading.style.display = '';
loading.innerHTML = '<div style="text-align:center;color:#666;">' +
'<div style="font-size:40px;margin-bottom:12px;">🧠</div>' +
'<div style="font-size:15px;margin-bottom:6px;">OpenClaw 网关未运行</div>' +
'<div style="font-size:12px;color:#999;">请先在「基本设置」页面启用服务并启动。</div>' +
'</div>';
scheduleCheck(5000);
}
} catch(e) {
statusTextEl.textContent = '查询失败';
scheduleCheck(5000);
}
});
});
}
function showLaunch(url) {
function showIframe(url) {
loading.style.display = 'none';
container.innerHTML = '<div class="oc-console-loading" style="height:220px;">' +
'<div style="font-size:40px;margin-bottom:12px;">↗</div>' +
'<div style="font-size:15px;margin-bottom:6px;">OpenClaw 控制台已就绪</div>' +
'<div style="font-size:12px;color:#999;">请点击上方「新窗口打开」访问控制台。</div>' +
'</div>';
if (!iframeEl) {
iframeEl = document.createElement('iframe');
iframeEl.id = 'oc-console-iframe';
iframeEl.setAttribute('allowfullscreen', 'true');
iframeEl.setAttribute('allow', 'clipboard-read; clipboard-write; microphone; camera; display-capture');
container.appendChild(iframeEl);
}
if (iframeEl.src !== url) {
iframeEl.src = url;
}
}
function escapeHtml(str) {
return String(str || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
var devicesListUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "devices_list")%>';
var devicesApproveUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "devices_approve")%>';
var pairingBanner = document.getElementById('oc-pairing-banner');
var pairingBadge = document.getElementById('oc-pairing-badge');
var pairingItems = document.getElementById('oc-pairing-items');
var pairingMsg = document.getElementById('oc-pairing-msg');
var pairingApproveAllBtn = document.getElementById('oc-pairing-approve-all-btn');
var pairingRefreshBtn = document.getElementById('oc-pairing-refresh-btn');
var pairingToggleBtn = document.getElementById('oc-console-pairing-toggle-btn');
function checkDevices(manualOpen) {
(new XHR()).get(devicesListUrl, null, function(x) {
try {
var data = JSON.parse(x.responseText);
var pending = (data && data.pending) || [];
if (pairingBadge) pairingBadge.textContent = pending.length;
var navBadge = document.getElementById('oc-console-pairing-nav-badge');
if (navBadge) {
if (pending.length > 0) {
navBadge.textContent = pending.length;
navBadge.style.display = 'inline';
} else {
navBadge.style.display = 'none';
}
}
if (pending.length > 0) {
var html = '';
for (var i = 0; i < pending.length; i++) {
var item = pending[i];
var ip = escapeHtml(item.remoteIp || item.ip || '未知IP');
var client = escapeHtml(item.clientId || item.clientMode || 'webchat');
var plat = escapeHtml(item.platform || '');
var rid = escapeHtml(item.requestId || '');
html += '<div style="background:#fff; border:1px solid #e1e4e8; border-radius:4px; padding:6px 10px; margin-bottom:6px; display:flex; justify-content:space-between; align-items:center; gap:8px; flex-wrap:wrap;">' +
'<div><strong>IP:</strong> ' + ip + ' | <strong>客户端:</strong> ' + client + (plat ? ' (' + plat + ')' : '') + ' <br/><span style="color:#888; font-size:11px;">ID: ' + rid + '</span></div>' +
'<button type="button" class="btn-action btn-approve-single" data-rid="' + rid + '" style="font-size:12px; padding:3px 10px; background:#f6ffed; border-color:#b7eb8f; color:#389e0d; cursor:pointer;">批准配对</button>' +
'</div>';
}
pairingItems.innerHTML = html;
pairingBanner.style.display = 'block';
var singleBtns = pairingItems.querySelectorAll('.btn-approve-single');
for (var j = 0; j < singleBtns.length; j++) {
singleBtns[j].addEventListener('click', function(ev) {
var rid = this.getAttribute('data-rid');
approveDevice(rid);
});
}
} else {
pairingItems.innerHTML = '<div style="color:#666; font-size:12px; padding:4px 0;">当前无待配对设备请求。当浏览器连接控制台提示「需要设备配对」时,在此刷新即可显示并批准。</div>';
if (manualOpen) {
pairingBanner.style.display = 'block';
} else {
pairingBanner.style.display = 'none';
}
}
} catch(e) {
if (manualOpen) {
pairingItems.innerHTML = '<div style="color:#cf222e; font-size:12px;">查询设备列表失败</div>';
pairingBanner.style.display = 'block';
}
}
});
}
function approveDevice(rid) {
pairingMsg.style.display = 'block';
pairingMsg.style.color = '#1890ff';
pairingMsg.textContent = '⏳ 正在批准设备配对...';
var postData = { token: ocCsrfToken };
if (rid) {
postData.request_id = rid;
} else {
postData.all = '1';
}
(new XHR()).post(devicesApproveUrl, postData, function(x) {
try {
var res = JSON.parse(x.responseText);
if (res.status === 'ok') {
pairingMsg.style.color = '#52c41a';
pairingMsg.innerHTML = '✅ ' + (res.message || '批准成功!') + ' 正在重新载入控制台...';
setTimeout(function() {
checkDevices(false);
if (iframeEl) {
iframeEl.src = getConsoleUrl();
}
}, 1500);
} else {
pairingMsg.style.color = '#cf222e';
pairingMsg.innerHTML = '❌ ' + (res.message || '批准失败');
}
} catch(e) {
pairingMsg.style.color = '#cf222e';
pairingMsg.textContent = '❌ 批准失败:请求异常';
}
});
}
if (pairingApproveAllBtn) {
pairingApproveAllBtn.addEventListener('click', function() {
approveDevice(null);
});
}
if (pairingRefreshBtn) {
pairingRefreshBtn.addEventListener('click', function() {
checkDevices(true);
});
}
if (pairingToggleBtn) {
pairingToggleBtn.addEventListener('click', function() {
if (pairingBanner.style.display === 'none') {
checkDevices(true);
} else {
pairingBanner.style.display = 'none';
}
});
}
refreshBtn.addEventListener('click', function() {
if (iframeEl) {
var url = getConsoleUrl();
iframeEl.src = url;
}
});
fullscreenBtn.addEventListener('click', toggleFullScreen);
checkAndLoad();
})();
//]]>
@@ -177,7 +177,16 @@
}
updateStatus();
setInterval(updateStatus, 5000);
setInterval(function() {
if (!document.hidden) {
updateStatus();
}
}, 5000);
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
updateStatus();
}
});
})();
//]]>
</script>
+77 -157
View File
@@ -73,12 +73,15 @@ run_openclaw_cli_as_user() {
ulimit -v unlimited 2>/dev/null || true
ulimit -m unlimited 2>/dev/null || true
ulimit -d unlimited 2>/dev/null || true
HOME="$OC_DATA" OPENCLAW_HOME="$OC_DATA" OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" NODE_ICU_DATA="${NODE_BASE}/share/icu" \
NPM_CONFIG_CACHE="${OC_DATA}/.npm" npm_config_cache="${OC_DATA}/.npm" TMPDIR="${OC_DATA}/.tmp" \
PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:$PATH" \
start-stop-daemon -S -m -p "$pid_file" -c openclaw:openclaw -d "$OC_INSTALL_PATH" \
-x "$NODE_BIN" -- "$oc_entry" "$@"
(
cd "${OC_DATA:-$OC_INSTALL_PATH}" 2>/dev/null || true
HOME="$OC_DATA" OPENCLAW_HOME="$OC_DATA" OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" NODE_ICU_DATA="${NODE_BASE}/share/icu" \
NPM_CONFIG_CACHE="${OC_DATA}/.npm" npm_config_cache="${OC_DATA}/.npm" TMPDIR="${OC_DATA}/.tmp" \
PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:$PATH" \
start-stop-daemon -S -m -p "$pid_file" -c openclaw:openclaw \
-x "$NODE_BIN" -- "$oc_entry" "$@"
)
rc=$?
rm -f "$pid_file"
return "$rc"
@@ -122,115 +125,35 @@ done < "$_tmpf"
rm -f "$_tmpf"
}
patch_iframe_headers() {
# 移除 OpenClaw 网关的 X-Frame-Options 和 frame-ancestors 限制,允许 LuCI iframe 嵌入
# v2026.4.x: 安全头设置迁移到 server.impl-*.js 文件
local patched=0
# 1. 搜索 server.impl-*.js (v2026.4.x 新位置)
for f in $(find "${OC_GLOBAL}/lib/node_modules/openclaw/dist" -name "server.impl-*.js" -type f 2>/dev/null); do
if grep -q "X-Frame-Options.*DENY\|frame-ancestors.*none" "$f" 2>/dev/null; then
sed -i 's|res\.setHeader("X-Frame-Options", "DENY")|res.setHeader("X-Frame-Options", "ALLOW-FROM *") // patched by luci-app-openclaw|g' "$f"
sed -i "s|\"frame-ancestors 'none'\"|\"frame-ancestors *\"|g" "$f"
logger -t openclaw "Patched iframe headers in $f"
patched=1
fi
done
# 2. 兼容旧版本: 搜索 gateway-cli-*.js (v2026.3.x)
for search_root in "${OC_GLOBAL}" "${NODE_BASE}/lib"; do
[ -d "$search_root" ] || continue
for f in $(find "$search_root" -name "gateway-cli-*.js" -type f 2>/dev/null); do
if grep -q "X-Frame-Options.*DENY" "$f" 2>/dev/null; then
sed -i "s|res.setHeader(\"X-Frame-Options\", \"DENY\");|// res.setHeader(\"X-Frame-Options\", \"DENY\"); // patched by luci-app-openclaw|g" "$f"
sed -i "s|\"frame-ancestors 'none'\"|\"frame-ancestors *\"|g" "$f"
logger -t openclaw "Patched iframe headers in $f"
patched=1
fi
done
done
[ $patched -eq 1 ] && logger -t openclaw "iframe headers patched successfully"
}
patch_webchat_session_conflict() {
# OpenClaw 2026.6.11 的 Control UI / WebChat 在第二轮消息时可能触发:
# reply session initialization conflicted for agent:main:dashboard:...
# 根因是 WebChat 追加用户消息会先更新 session registry revision,
# 而 reply 初始化仍按旧 snapshot 做 guarded write;同时 dashboard 会话会被
# terminal transcript mtime 误判为需要 rollover。这里做最小运行时兼容补丁。
[ -x "$NODE_BIN" ] || return 0
local oc_entry oc_dir dist_dir
oc_entry=$(get_oc_entry)
[ -n "$oc_entry" ] || return 0
oc_dir=$(dirname "$oc_entry")
dist_dir="${oc_dir}/dist"
[ -d "$dist_dir" ] || return 0
local patch_output
patch_output=$(OC_DIST_DIR="$dist_dir" "$NODE_BIN" <<'NODE' 2>/tmp/openclaw-webchat-patch.err
const fs = require('fs');
const path = require('path');
const dist = process.env.OC_DIST_DIR;
let patched = 0;
function patchFile(file, transforms) {
let source;
try {
source = fs.readFileSync(file, 'utf8');
} catch {
return;
}
let next = source;
for (const [needle, replacement] of transforms) {
if (next.includes(replacement)) continue;
if (next.includes(needle)) next = next.replace(needle, replacement);
}
if (next !== source) {
fs.writeFileSync(file, next);
patched += 1;
}
}
for (const name of fs.readdirSync(dist)) {
const file = path.join(dist, name);
if (!name.startsWith('get-reply-') || !name.endsWith('.js')) continue;
patchFile(file, [[
'const terminalMainTranscriptNewerThanRegistry = !isSystemEvent && await hasTerminalMainSessionTranscriptNewerThanRegistry({',
'const terminalMainTranscriptNewerThanRegistry = !isSystemEvent && !(ctx.Provider === "webchat" || ctx.Surface === "webchat" || ctx.OriginatingChannel === "webchat") && await hasTerminalMainSessionTranscriptNewerThanRegistry({'
]]);
}
for (const name of fs.readdirSync(dist)) {
const file = path.join(dist, name);
if (!name.startsWith('session-accessor-') || !name.endsWith('.js')) continue;
patchFile(file, [[
`\t\tif (revision !== params.expectedRevision) return {\n\t\t\tok: false,\n\t\t\t...currentEntry ? { currentEntry } : {},\n\t\t\treason: "stale-snapshot",\n\t\t\trevision\n\t\t};`,
`\t\tlet sessionEntryInput = params.sessionEntry;\n\t\tif (revision !== params.expectedRevision) {\n\t\t\tconst currentSessionId = typeof currentEntry?.sessionId === "string" ? currentEntry.sessionId.trim() : "";\n\t\t\tconst nextSessionId = typeof params.sessionEntry?.sessionId === "string" ? params.sessionEntry.sessionId.trim() : "";\n\t\t\tif (currentSessionId && nextSessionId && currentSessionId === nextSessionId) sessionEntryInput = {\n\t\t\t\t...currentEntry,\n\t\t\t\t...params.sessionEntry\n\t\t\t};\n\t\t\telse return {\n\t\t\t\tok: false,\n\t\t\t\t...currentEntry ? { currentEntry } : {},\n\t\t\t\treason: "stale-snapshot",\n\t\t\t\trevision\n\t\t\t};\n\t\t}`
], [
`sessionEntry: params.sessionEntry\n\t\t}) : params.sessionEntry;`,
`sessionEntry: sessionEntryInput\n\t\t}) : sessionEntryInput;`
]]);
}
process.stdout.write(String(patched));
NODE
)
local rc=$?
local patched_count=""
[ -f /tmp/openclaw-webchat-patch.err ] && patched_count=$(cat /tmp/openclaw-webchat-patch.err 2>/dev/null)
if [ $rc -eq 0 ]; then
local out_count
out_count=$(OC_DIST_DIR="$dist_dir" "$NODE_BIN" -e "const fs=require('fs'),path=require('path');let ok=0;for(const n of fs.readdirSync(process.env.OC_DIST_DIR)){const f=path.join(process.env.OC_DIST_DIR,n);if(!n.endsWith('.js'))continue;const s=fs.readFileSync(f,'utf8');if(s.includes('ctx.Provider === \"webchat\"')||s.includes('let sessionEntryInput = params.sessionEntry'))ok++;}process.stdout.write(String(ok));" 2>/dev/null || echo "")
logger -t openclaw "WebChat 会话冲突兼容补丁已检查 (${out_count:-0} 个命中, 本次修改 ${patch_output:-0} 个文件)"
else
logger -t openclaw "WebChat 会话冲突兼容补丁失败: ${patched_count:-unknown error}"
fi
rm -f /tmp/openclaw-webchat-patch.err 2>/dev/null || true
return 0
}
sync_uci_to_json() {
# 检查是否处于升级事务迁移期间,若是则暂停 UCI 覆盖迁移配置
local _sdir="${OPENCLAW_STATE_DIR:-${OC_DATA:+$OC_DATA/.openclaw}}"
local _pdir="$(dirname "$_sdir")"
local _rdir="$(dirname "$_pdir")"
local _status_file="" cand
for cand in \
"${_rdir}/.luci-openclaw-upgrade/status.json" \
"${_pdir}/.luci-openclaw-upgrade/status.json" \
"${_sdir}/.luci-openclaw-upgrade/status.json" \
"${OC_DATA:+$OC_DATA/.luci-openclaw-upgrade/status.json}" \
"${OC_BASE_PATH:-/opt}/openclaw/.luci-openclaw-upgrade/status.json"; do
if [ -n "$cand" ] && [ -f "$cand" ]; then
_status_file="$cand"
break
fi
done
if [ -n "$_status_file" ] && [ -f "$_status_file" ]; then
local _up_phase
_up_phase=$(grep -oE '"phase"[[:space:]]*:[[:space:]]*"[^"]+"' "$_status_file" 2>/dev/null | cut -d'"' -f4 || true)
case "$_up_phase" in
migrating|gateway_verifying|backing_up)
logger -t openclaw "检测到正在执行升级事务 (phase: $_up_phase),暂停 UCI 覆盖迁移配置"
return 0
;;
esac
fi
# 将 UCI 配置同步到 openclaw.json,同时确保 token 双向同步
local port bind token json_token
port=$(uci -q get openclaw.main.port || echo "18789")
@@ -312,8 +235,8 @@ d.gateway.auth.mode='token';
d.gateway.auth.token=process.env.OC_SYNC_TOKEN||'';
if(!d.gateway.controlUi)d.gateway.controlUi={};
d.gateway.controlUi.allowInsecureAuth=true;
d.gateway.controlUi.dangerouslyDisableDeviceAuth=true;
d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true;
if(!Array.isArray(d.gateway.controlUi.allowedOrigins))d.gateway.controlUi.allowedOrigins=['*'];
// 清理 v2026.3.1+ 已废弃的字段,避免配置验证失败
delete d.gateway.name;
delete d.gateway.bonjour;
@@ -344,46 +267,34 @@ if(d.hooks&&d.hooks.internal){delete d.hooks.internal.handlers;}
// 5. agents.defaults.cliBackends 废弃
if(d.agents&&d.agents.defaults){delete d.agents.defaults.cliBackends;}
// v2026.4.9: 自动同步 plugins.allow (解决 "plugins.allow is empty" 警告)
// 扫描已安装的插件并添加到 allow 列表
if(!d.plugins)d.plugins={};
if(!Array.isArray(d.plugins.allow))d.plugins.allow=[];
// 从 plugins.installs 中提取已安装插件
// 注意: 使用 installPath 中的目录名作为插件 ID (与 openclaw.plugin.json 中的 id 一致)
if(d.plugins.installs){
Object.keys(d.plugins.installs).forEach(key=>{
const inst=d.plugins.installs[key];
if(inst&&inst.installPath){
// 从 installPath 提取插件目录名 (如 openclaw-weixin)
const match=inst.installPath.match(/\/([^\/]+)\/?$/);
if(match&&match[1]&&!d.plugins.allow.includes(match[1])){
d.plugins.allow.push(match[1]);
}
}
});
}
delete d.plugins.installs;
// v2026.5.x: 微信插件渠道名统一为 openclaw-weixin
// 旧配置里可能残留 weixin,启动前迁移,避免渠道加载失败。
if(Array.isArray(d.plugins.allow)){
d.plugins.allow=d.plugins.allow.map(x=>x==='weixin'?'openclaw-weixin':x)
.filter((x,i,a)=>x&&a.indexOf(x)===i);
}
['entries','installs'].forEach(ns=>{
if(d.plugins&&d.plugins[ns]&&d.plugins[ns].weixin){
if(!d.plugins[ns]['openclaw-weixin'])d.plugins[ns]['openclaw-weixin']=d.plugins[ns].weixin;
delete d.plugins[ns].weixin;
}
});
if(d.plugins&&d.plugins.entries&&d.plugins.entries.weixin){
if(!d.plugins.entries['openclaw-weixin'])d.plugins.entries['openclaw-weixin']=d.plugins.entries.weixin;
delete d.plugins.entries.weixin;
}
['channel','channels'].forEach(ns=>{
if(d[ns]&&d[ns].weixin){
if(!d[ns]['openclaw-weixin'])d[ns]['openclaw-weixin']=d[ns].weixin;
delete d[ns].weixin;
}
});
if(!d.plugins.allow.includes('openclaw-weixin') && d.plugins.installs && d.plugins.installs['openclaw-weixin']){
if(d.channels&&d.channels['openclaw-weixin']&&!d.plugins.allow.includes('openclaw-weixin')){
d.plugins.allow.push('openclaw-weixin');
}
// 始终信任 copilot-proxy (内置插件)
if(!d.plugins.allow.includes('copilot-proxy'))d.plugins.allow.push('copilot-proxy');
// 允许 LuCI 跨源与 Host-Header 访问 Control UI
if(!d.gateway)d.gateway={};
if(!d.gateway.controlUi)d.gateway.controlUi={};
d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true;
if(!Array.isArray(d.gateway.controlUi.allowedOrigins))d.gateway.controlUi.allowedOrigins=['*'];
fs.writeFileSync(f,JSON.stringify(d,null,2));
" 2>/dev/null
fi
@@ -470,7 +381,7 @@ try {
if [ "$index_ok" != "1" ]; then
logger -t openclaw "检测到旧微信直装目录,正在迁移到 SQLite 插件索引"
mkdir -p "${OC_DATA}/.npm" "${OC_DATA}/.tmp" 2>/dev/null || true
if run_openclaw_cli_as_user "$oc_entry" plugins install --force --pin '@tencent-weixin/openclaw-weixin@2.4.6' \
if run_openclaw_cli_as_user "$oc_entry" plugins install --force --pin '@tencent-weixin/openclaw-weixin@2.4.8' \
>>/tmp/openclaw-wechat-self-heal.log 2>&1; then
logger -t openclaw "微信插件已迁移到 SQLite installed_plugin_index"
else
@@ -585,9 +496,7 @@ if(changed){
# 修复数据目录权限 (防止 root 用户操作后留下无法读取的文件)
fix_openclaw_state_permissions 2>/dev/null || true
# v2026.4.9: 修复插件目录权限 (OpenClaw 要求插件目录属主为 root)
# 详见: https://github.com/nicepkg/openclaw/releases/tag/v2026.4.9
# v2026.4.10 补充: 同时修复权限模式为 755,确保 Gateway 可读取插件
# 确保插件目录属主与权限统一 (openclaw:openclaw 755),确保 Gateway 可读取与管理插件
# 原因: npm/npx 以 root 创建目录时默认权限 700,其他用户无法读取
local ext_dir="${OC_DATA}/.openclaw/extensions"
local npm_projects_dir="${OC_DATA}/.openclaw/npm/projects"
@@ -631,14 +540,15 @@ if(changed){
# 清理 plugins.allow / channels 中的微信配置。
_run_doctor_fix() {
local oc_entry="$1"
local _doc_cmd="HOME=\"${OC_DATA}\" OPENCLAW_HOME=\"${OC_DATA}\" OPENCLAW_STATE_DIR=\"${OC_DATA}/.openclaw\" OPENCLAW_CONFIG_PATH=\"${CONFIG_FILE}\" NODE_ICU_DATA=\"${NODE_BASE}/share/icu\" \"${NODE_BIN}\" \"${oc_entry}\" doctor --fix"
local _doc_cmd="cd \"${OC_DATA}\" && HOME=\"${OC_DATA}\" OPENCLAW_HOME=\"${OC_DATA}\" OPENCLAW_STATE_DIR=\"${OC_DATA}/.openclaw\" OPENCLAW_CONFIG_PATH=\"${CONFIG_FILE}\" NODE_ICU_DATA=\"${NODE_BASE}/share/icu\" OPENCLAW_SUPERVISOR_MODE=\"external\" OPENCLAW_SERVICE_REPAIR_POLICY=\"external\" \"${NODE_BIN}\" \"${oc_entry}\" doctor --fix"
if id openclaw >/dev/null 2>&1; then
if command -v su >/dev/null 2>&1; then
su -s /bin/sh openclaw -c "${_doc_cmd}" >/dev/null 2>&1
elif command -v runuser >/dev/null 2>&1; then
runuser -u openclaw -- sh -c "${_doc_cmd}" >/dev/null 2>&1
else
start-stop-daemon -S -m -p /tmp/openclaw-doctor.pid -c openclaw:openclaw -x /bin/sh -- -c "${_doc_cmd}" >/dev/null 2>&1
(cd "${OC_DATA}" 2>/dev/null || true; start-stop-daemon -S -m -p /tmp/openclaw-doctor.pid -c openclaw:openclaw -x /bin/sh -- -c "${_doc_cmd}" >/dev/null 2>&1)
rm -f /tmp/openclaw-doctor.pid 2>/dev/null || true
fi
else
# openclaw 用户不存在时退回 root 执行,保证迁移仍可运行
@@ -665,20 +575,31 @@ if(changed){
# 只在版本变化时才运行 doctor --fix
if [ "$current_ver" != "$last_ver" ]; then
logger -t openclaw "检测到版本变更 ($last_ver -> $current_ver),执行配置迁移 (doctor --fix)..."
_run_doctor_fix "$oc_entry" && \
logger -t openclaw "配置迁移完成" || \
if _run_doctor_fix "$oc_entry"; then
local _val_cmd="cd \"${OC_DATA}\" && HOME=\"${OC_DATA}\" OPENCLAW_HOME=\"${OC_DATA}\" OPENCLAW_STATE_DIR=\"${OC_DATA}/.openclaw\" OPENCLAW_CONFIG_PATH=\"${CONFIG_FILE}\" NODE_ICU_DATA=\"${NODE_BASE}/share/icu\" OPENCLAW_SUPERVISOR_MODE=\"external\" OPENCLAW_SERVICE_REPAIR_POLICY=\"external\" \"${NODE_BIN}\" \"${oc_entry}\" config validate --json"
local _val_ok=0
if run_openclaw_cli_as_user "$oc_entry" config validate --json >/dev/null 2>&1 || eval "${_val_cmd}" >/dev/null 2>&1; then
_val_ok=1
fi
if [ "$_val_ok" -eq 1 ]; then
_ensure_critical_config
_sync_token_after_doctor
echo "$current_ver" > "$version_marker" 2>/dev/null
chown openclaw:openclaw "$version_marker" 2>/dev/null || true
logger -t openclaw "配置迁移完成"
else
logger -t openclaw "配置校验 (config validate --json) 失败,未记录版本标记"
fi
else
logger -t openclaw "配置迁移失败,请手动检查"
# 更新标记文件
echo "$current_ver" > "$version_marker" 2>/dev/null
chown openclaw:openclaw "$version_marker" 2>/dev/null || true
fi
else
logger -t openclaw "版本未变化 ($current_ver),跳过 doctor --fix"
fi
}
_run_config_migration "$oc_entry"
# doctor --fix 后再次修复权限 (防止 root 创建的文件导致 EACCES)
# 注意: extensions 目录保持 root 权限 (OpenClaw 安全要求)
# doctor --fix 后再次修复权限 (确保 extensions 与 state 目录保持 openclaw:openclaw 755)
fix_openclaw_state_permissions 2>/dev/null || true
# v2026.4.10: doctor --fix 后确保关键配置字段存在
@@ -705,8 +626,8 @@ if (!d.gateway.bind) d.gateway.bind = 'lan';
// 确保 controlUi 安全配置存在 (允许非 loopback 绑定和 LuCI iframe 嵌入)
if (!d.gateway.controlUi) d.gateway.controlUi = {};
if (d.gateway.controlUi.allowInsecureAuth !== true) d.gateway.controlUi.allowInsecureAuth = true;
if (d.gateway.controlUi.dangerouslyDisableDeviceAuth !== true) d.gateway.controlUi.dangerouslyDisableDeviceAuth = true;
if (d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback !== true) d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback = true;
if (!Array.isArray(d.gateway.controlUi.allowedOrigins)) d.gateway.controlUi.allowedOrigins = ['*'];
// 确保认证配置
if (!d.gateway.auth) d.gateway.auth = {};
@@ -773,12 +694,6 @@ fs.writeFileSync(f, JSON.stringify(d, null, 2));
# v2026.4.10: final permission fix before Gateway start
fix_openclaw_state_permissions 2>/dev/null || true
# Patch iframe 安全头,允许 LuCI 嵌入
patch_iframe_headers
# Patch WebChat 二次发送会话初始化冲突
patch_webchat_session_conflict
# 将 UCI bind 映射到 openclaw gateway --bind 参数
local gw_bind="loopback"
case "$bind" in
@@ -852,6 +767,9 @@ chmod 700 "$node_compile_cache" 2>/dev/null || true
_validate_path "$_dir" || { logger -t openclaw "安全警告: 路径验证失败: $_dir"; return 1; }
done
# 允许 LuCI 页面以 iframe 内嵌 OpenClaw 官方 Web 控制台
[ -x /usr/libexec/openclaw-permissions.sh ] && /usr/libexec/openclaw-permissions.sh patch-control-ui "$OC_GLOBAL"
procd_open_instance "gateway"
procd_set_param command "$NODE_BIN" "$oc_entry" gateway run \
--port "$port" --bind "$gw_bind"
@@ -861,6 +779,8 @@ chmod 700 "$node_compile_cache" 2>/dev/null || true
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
OPENCLAW_GATEWAY_TOKEN="$gw_token" \
OPENCLAW_SUPERVISOR_MODE="external" \
OPENCLAW_SERVICE_REPAIR_POLICY="external" \
NODE_COMPILE_CACHE="$node_compile_cache" \
NODE_ICU_DATA="${NODE_BASE}/share/icu" \
NODE_BASE="$NODE_BASE" \
File diff suppressed because it is too large. Load diff
@@ -52,7 +52,6 @@ oc_fix_state_permissions() {
if [ -d "$state_dir" ]; then
find "$state_dir" -user root \
! -path "${ext_dir}*" \
! -path "${archive_dir}*" \
! -path "${state_dir}/npm/projects*" \
-exec chown openclaw:openclaw {} \; 2>/dev/null || true
@@ -68,12 +67,12 @@ oc_fix_state_permissions() {
fi
if [ -d "$ext_dir" ]; then
chown -R root:root "$ext_dir" 2>/dev/null || true
chown -R openclaw:openclaw "$ext_dir" 2>/dev/null || true
chmod -R 755 "$ext_dir" 2>/dev/null || true
fi
if [ -d "$archive_dir" ]; then
chown -R root:root "$archive_dir" 2>/dev/null || true
chmod -R 700 "$archive_dir" 2>/dev/null || true
chmod -R 755 "$archive_dir" 2>/dev/null || true
fi
oc_fix_npm_projects_permissions "$state_dir"
@@ -88,12 +87,37 @@ oc_prepare_openclaw_workdirs() {
mkdir -p "${data_dir}/.npm" "${data_dir}/.tmp" "$npm_dir" "${state_dir}/extensions" 2>/dev/null || true
chown -R openclaw:openclaw "${data_dir}/.npm" "${data_dir}/.tmp" 2>/dev/null || true
chown openclaw:openclaw "$data_dir" "$state_dir" "${state_dir}/npm" "$npm_dir" 2>/dev/null || true
chmod u+rwx,go+rx "${data_dir}/.npm" "${data_dir}/.tmp" "$state_dir" "${state_dir}/npm" "$npm_dir" 2>/dev/null || true
[ -d "${state_dir}/extensions" ] && chown -R openclaw:openclaw "${state_dir}/extensions" 2>/dev/null || true
[ -d "${state_dir}/extensions" ] && chmod -R 755 "${state_dir}/extensions" 2>/dev/null || true
chmod u+rwx,go+rx "${data_dir}/.npm" "${data_dir}/.tmp" "$state_dir" "${state_dir}/npm" "$npm_dir" "${state_dir}/extensions" 2>/dev/null || true
# During install/upgrade, allow openclaw to update npm generations.
[ -d "$npm_dir" ] && chown -R openclaw:openclaw "$npm_dir" 2>/dev/null || true
}
oc_patch_control_ui_security_headers() {
local global_dir="${1:-${OC_GLOBAL:-/opt/openclaw/global}}"
local dist_dir=""
for d in "${global_dir}/lib/node_modules/openclaw/dist" "${global_dir}/node_modules/openclaw/dist" "/opt/openclaw/global/lib/node_modules/openclaw/dist" "/opt/openclaw/node/lib/node_modules/openclaw/dist"; do
if [ -d "$d" ]; then
dist_dir="$d"
break
fi
done
[ -n "$dist_dir" ] || return 0
# 允许 LuCI 界面以 iframe 内嵌官方 Web 控制台 (Control UI / WebChat)
# 移除 X-Frame-Options: DENY 并将 frame-ancestors 放宽
for f in "$dist_dir"/control-ui-*.js; do
[ -f "$f" ] || continue
if grep -q 'res.setHeader("X-Frame-Options", "DENY");' "$f" 2>/dev/null; then
sed -i 's/res.setHeader("X-Frame-Options", "DENY");//g' "$f" 2>/dev/null || true
fi
if grep -q '"frame-ancestors '\''none'\''"' "$f" 2>/dev/null; then
sed -i "s/\"frame-ancestors 'none'\"/\"frame-ancestors *\"/g" "$f" 2>/dev/null || true
fi
done
}
if [ "${OPENCLAW_PERMISSIONS_SOURCED:-0}" = "1" ]; then
return 0
fi
@@ -111,8 +135,12 @@ case "${1:-fix-state}" in
shift || true
oc_prepare_openclaw_workdirs "${1:-}"
;;
patch-control-ui)
shift || true
oc_patch_control_ui_security_headers "${1:-}"
;;
*)
echo "Usage: $0 {fix-state [state_dir]|fix-npm-projects [state_dir]|prepare-workdirs [data_dir]}" >&2
echo "Usage: $0 {fix-state [state_dir]|fix-npm-projects [state_dir]|prepare-workdirs [data_dir]|patch-control-ui [global_dir]}" >&2
exit 2
;;
esac
File diff suppressed because it is too large. Load diff
@@ -19,8 +19,8 @@
" plugin 表示需先安装官方 provider 插件,模型列表以插件为准。"
],
"verified": {
"date": "2026-08-26",
"openclawVersion": "2026.7.1-2",
"date": "2026-09-05",
"openclawVersion": "2026.9.1",
"method": "openclaw models list --provider <id> --plain"
},
"providers": {
@@ -15,7 +15,7 @@
const path = require('path');
const fs = require('fs');
const { spawn, execSync, execFileSync } = require('child_process');
const { spawn, spawnSync, execSync, execFileSync } = require('child_process');
const menu = require('./oc-menu-engine');
const { C, select, input, confirm, spinner, resetRenderCount } = menu;
@@ -51,38 +51,107 @@ const MODEL_DISCOVERY_TIMEOUT_MS = 6000;
/**
* 执行命令并返回结果
* options.interactive: 若为 true,使用 stdio: 'inherit' 直通终端(用于向导/全屏交互),保证 child.stdout.isTTY 为 true
* options.capture: 若为 true,静默收集 stdout/stderr 并返回,不输出到终端
*/
function runCommand(cmd, args = [], options = {}) {
return new Promise((resolve, reject) => {
const isInteractive = options.interactive === true || options.stdio === 'inherit';
const isCapture = options.capture === true;
// 交互模式下使用同步直通 (spawnSync inherit),使子进程独占控制终端 TTY,
// 避免父子进程竞争读取 stdin,执行完毕后排空残留按键并恢复终端模式
if (isInteractive) {
menu.setRawMode(false);
process.stdout.write(C.show);
if (process.stdin.pause) {
try { process.stdin.pause(); } catch {}
}
try {
const res = spawnSync(cmd, args, {
stdio: 'inherit',
shell: false,
cwd: options.cwd,
env: { ...process.env, ...options.env },
});
try {
if (process.stdin.isTTY) process.stdin.setRawMode(false);
while (process.stdin.read() !== null) {}
} catch {}
if (process.stdin.resume) {
try { process.stdin.resume(); } catch {}
}
if (res.error) {
return reject(res.error);
}
if (res.status === 0) {
return resolve({ stdout: '', stderr: '', code: 0 });
} else {
return reject(new Error(`Command failed with code ${res.status}: ${res.signal || ''}`));
}
} catch (err) {
return reject(err);
}
}
const child = spawn(cmd, args, {
stdio: ['inherit', 'pipe', 'pipe'],
shell: true,
stdio: isCapture ? ['ignore', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
shell: !options.noShell,
env: { ...process.env, ...options.env },
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data;
process.stdout.write(data);
});
if (child.stdout) {
child.stdout.on('data', (data) => {
stdout += data;
if (!isCapture) process.stdout.write(data);
});
}
child.stderr.on('data', (data) => {
stderr += data;
process.stderr.write(data);
});
if (child.stderr) {
child.stderr.on('data', (data) => {
stderr += data;
if (!isCapture) process.stderr.write(data);
});
}
child.on('close', (code) => {
child.on('close', (code, signal) => {
if (code === 0) {
resolve({ stdout, stderr, code });
} else {
reject(new Error(`Command failed with code ${code}: ${stderr}`));
reject(new Error(`Command failed with code ${code ?? signal}: ${stderr}`));
}
});
child.on('error', (err) => {
reject(err);
});
});
}
/**
* 清理过时插件配置并启用有效认证插件 (与 oc-config.sh 的 enable_auth_plugins 对应)
*/
function enableAuthPlugins() {
if (!fs.existsSync(CONFIG_FILE)) return;
try {
const config = readConfig();
if (!config.plugins) config.plugins = {};
if (!config.plugins.entries) config.plugins.entries = {};
const e = config.plugins.entries;
['copilot-proxy'].forEach(p => {
if (!e[p]) e[p] = {};
e[p].enabled = true;
});
delete e['qwen-portal-auth'];
delete e['google-gemini-cli-auth'];
delete e['minimax-portal-auth'];
writeConfig(config);
} catch {}
}
/**
* 读取 JSON 配置文件
*/
@@ -91,7 +160,9 @@ function fixStatePermissions() {
execFileSync(PERMISSIONS_HELPER, ['fix-state', OC_STATE_DIR], { stdio: 'ignore', timeout: 10000 });
} catch {
try {
execSync(`find "${OC_STATE_DIR}" -user root ! -path "*/extensions*" ! -path "*/archived-extensions*" ! -path "*/npm/projects*" -exec chown openclaw:openclaw {} + 2>/dev/null || true`, { stdio: 'ignore' });
execSync(`find "${OC_STATE_DIR}" -user root ! -path "*/archived-extensions*" ! -path "*/npm/projects*" -exec chown openclaw:openclaw {} + 2>/dev/null || true`, { stdio: 'ignore' });
execSync(`[ -d "${OC_STATE_DIR}/extensions" ] && chown -R openclaw:openclaw "${OC_STATE_DIR}/extensions" && chmod -R 755 "${OC_STATE_DIR}/extensions" 2>/dev/null || true`, { stdio: 'ignore' });
execSync(`[ -d "${OC_STATE_DIR}/archived-extensions" ] && chown -R root:root "${OC_STATE_DIR}/archived-extensions" && chmod -R 755 "${OC_STATE_DIR}/archived-extensions" 2>/dev/null || true`, { stdio: 'ignore' });
} catch {}
}
}
@@ -303,18 +374,38 @@ function findOpenClawEntry() {
/**
* 执行 OpenClaw CLI 命令 (对应 oc_cmd)
* 交互式向导(configure / models auth login 等)以 interactive: true 运行,直通 stdio 保证 TTY 正常交互;
* 命令执行后在 finally 中自动修复状态文件属主。
*/
async function ocCmd(...args) {
const ocEntry = findOpenClawEntry();
if (!ocEntry) throw new Error('OpenClaw 未安装');
return runCommand(NODE_BIN, [ocEntry, ...args], {
env: {
OPENCLAW_HOME: OC_DATA,
OPENCLAW_CONFIG_PATH: CONFIG_FILE,
OPENCLAW_STATE_DIR: OC_STATE_DIR,
HOME: OC_DATA,
}
});
const isJson = args.includes('--json');
const isInteractive = !isJson && (
args[0] === 'configure' ||
(args[0] === 'models' && args[1] === 'auth')
);
try {
const res = await runCommand(NODE_BIN, [ocEntry, ...args], {
interactive: isInteractive,
capture: isJson,
env: {
OPENCLAW_HOME: OC_DATA,
OPENCLAW_CONFIG_PATH: CONFIG_FILE,
OPENCLAW_STATE_DIR: OC_STATE_DIR,
HOME: OC_DATA,
}
});
return res;
} finally {
try {
execSync(`chown openclaw:openclaw "${CONFIG_FILE}" 2>/dev/null || true`, { stdio: 'ignore' });
execSync(`chown openclaw:openclaw "${CONFIG_FILE}.bak" 2>/dev/null || true`, { stdio: 'ignore' });
fixStatePermissions();
} catch {}
}
}
/**
@@ -743,12 +834,13 @@ async function showMainMenu() {
{ key: '4', label: '健康检查与状态', desc: '', value: 'health' },
{ key: '5', label: '查看日志', desc: '', value: 'logs' },
{ key: '6', label: '重启 Gateway', desc: '', value: 'restart' },
{ key: '7', label: '设备配对管理', desc: '审批 Control UI 浏览器配对', value: 'devices' },
{ label: `${C.dim}━━━ 高级选项 ━━━${C.reset}`, disabled: true },
{ key: '7', label: '高级配置', desc: '', value: 'advanced' },
{ key: '8', label: '重置配置', desc: '', value: 'reset' },
{ key: '9', label: '显示当前配置概览', desc: '', value: 'show-config' },
{ key: '10', label: '备份/还原配置', desc: '', value: 'backup' },
{ key: '8', label: '高级配置', desc: '', value: 'advanced' },
{ key: '9', label: '重置配置', desc: '', value: 'reset' },
{ key: '10', label: '显示当前配置概览', desc: '', value: 'show-config' },
{ key: '11', label: '备份/还原配置', desc: '', value: 'backup' },
{ label: '', disabled: true },
{ key: '0', label: '退出', desc: '', value: 'quit' },
@@ -765,25 +857,25 @@ async function showMainMenu() {
async function showModelMenu() {
const result = await select({
title: '配置 AI 模型和提供商',
showSearch: true,
showSearch: false,
items: [
{ label: `${C.green}${C.bold}── 推荐 ──${C.reset}`, disabled: true },
{ key: 'w', label: '官方完整模型配置向导', desc: `${C.green}(推荐,支持所有提供商)${C.reset}`, value: 'wizard' },
{ label: `${C.bold}── 国外模型提供商 ──${C.reset}`, disabled: true },
{ key: 'a', label: 'OpenAI', desc: 'GPT-5.2, GPT-5 mini, GPT-4.1', value: 'openai' },
{ key: 'b', label: 'Anthropic', desc: 'Claude Sonnet 4, Opus 4, Haiku', value: 'anthropic' },
{ key: 'c', label: 'Google Gemini', desc: 'Gemini 2.5 Pro/Flash, Gemini 3', value: 'google' },
{ key: 'a', label: 'OpenAI', desc: '', value: 'openai' },
{ key: 'b', label: 'Anthropic', desc: '', value: 'anthropic' },
{ key: 'c', label: 'Google Gemini', desc: '', value: 'google' },
{ key: 'd', label: 'OpenRouter', desc: '聚合多家模型', value: 'openrouter' },
{ key: 'e', label: 'GitHub Copilot', desc: '需要 Copilot 订阅', value: 'copilot' },
{ key: 'f', label: 'xAI Grok', desc: 'Grok-4/3', value: 'xai' },
{ key: 'f', label: 'xAI Grok', desc: '', value: 'xai' },
{ label: `${C.bold}── 国内模型提供商 ──${C.reset}`, disabled: true },
{ key: 'g', label: '阿里云通义千问 Qwen', desc: 'Portal/API/Coding Plan', value: 'qwen' },
{ key: 'g', label: '阿里云通义千问 Qwen', desc: '', value: 'qwen' },
{ key: 'h', label: '硅基流动 SiliconFlow', desc: '', value: 'siliconflow' },
{ key: 'i', label: '腾讯云 Coding Plan', desc: 'HY T1/TurboS/GLM-5/Kimi', value: 'tencent' },
{ key: 'j', label: '百度千帆', desc: 'ERNIE-4.0, ERNIE-3.5', value: 'baidu' },
{ key: 'k', label: '智谱 GLM / Z.AI', desc: 'GLM-5.1, GLM-5, GLM-4.7', value: 'zhipu' },
{ key: 'i', label: '腾讯云 Coding Plan', desc: '', value: 'tencent' },
{ key: 'j', label: '百度千帆', desc: '', value: 'baidu' },
{ key: 'k', label: '智谱 GLM / Z.AI', desc: '', value: 'zhipu' },
{ label: `${C.bold}── 本地模型 / 自定义 API ──${C.reset}`, disabled: true },
{ key: 'l', label: 'Ollama', desc: '本地模型,无需 API Key', value: 'ollama' },
@@ -853,6 +945,7 @@ async function showAdvancedMenu() {
{ key: '8', label: '编辑配置文件', desc: 'vi / nano', value: 'edit' },
{ key: '9', label: '导出配置备份', desc: '', value: 'backup' },
{ key: '10', label: '导入配置', desc: '', value: 'import' },
{ key: '11', label: '设备配对管理', desc: '审批 Control UI 配对', value: 'devices' },
{ label: '', disabled: true },
{ key: '0', label: '返回', desc: '', value: 'back' },
],
@@ -1041,10 +1134,14 @@ async function configureQwen() {
if (modeChoice.value === 'portal') {
console.log(`\n${C.cyan}启动 Qwen OAuth 授权...${C.reset}\n`);
try {
enableAuthPlugins();
await ocCmd('models', 'auth', 'login', '--provider', 'qwen-portal', '--set-default');
} catch {}
console.log(`\n${C.yellow}OAuth 授权已退出${C.reset}\n`);
return true;
console.log(`\n${C.green}✅ Qwen OAuth 授权完成${C.reset}\n`);
return true;
} catch {
console.log(`\n${C.yellow}OAuth 授权已退出${C.reset}\n`);
return false;
}
}
if (modeChoice.value === 'bailian') {
@@ -1452,9 +1549,12 @@ async function launchWizard() {
console.log(`\n${C.cyan}启动官方完整模型配置向导...${C.reset}\n`);
console.log(`${C.yellow}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${C.reset}\n`);
try {
enableAuthPlugins();
await ocCmd('configure', '--section', 'model');
return true;
} catch (e) {
console.log(`${C.yellow}配置向导已退出${C.reset}\n`);
return false;
}
}
@@ -1471,7 +1571,11 @@ async function handleModelConfig() {
let configured = false;
switch (choice.value) {
case 'wizard': await launchWizard(); break;
case 'wizard': {
const ok = await launchWizard();
if (ok) await askRestart();
break;
}
case 'openai': configured = await configureOpenAI(); break;
case 'anthropic': configured = await configureAnthropic(); break;
case 'google': configured = await configureGoogle(); break;
@@ -1656,7 +1760,12 @@ async function handleChannels() {
console.log(`\n${C.cyan}正在启动飞书安装向导...${C.reset}\n`);
try {
await runCommand('npx', ['-y', '@larksuite/openclaw-lark-tools', 'install'], { env: { HOME: OC_DATA } });
const npxBin = fs.existsSync(`${NODE_BASE}/bin/npx`) ? `${NODE_BASE}/bin/npx` : 'npx';
await runCommand(npxBin, ['-y', '@larksuite/openclaw-lark-tools', 'install'], {
interactive: true,
cwd: OC_DATA,
env: { HOME: OC_DATA, PATH: `${NODE_BASE}/bin:${process.env.PATH || ''}` }
});
} catch (e) {
console.log(`${C.yellow}安装向导已退出${C.reset}\n`);
}
@@ -1786,10 +1895,18 @@ async function handleChannels() {
break;
}
case 'wizard': {
resetRenderCount();
console.log(`\n${C.cyan}启动官方渠道配置向导...${C.reset}\n`);
console.log(`${C.yellow}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${C.reset}\n`);
let ok = false;
try {
enableAuthPlugins();
await ocCmd('configure', '--section', 'channels');
} catch (e) {}
ok = true;
} catch (e) {
console.log(`${C.yellow}配置向导已退出${C.reset}\n`);
}
if (ok) await askRestart();
break;
}
}
@@ -1891,6 +2008,170 @@ async function handleShowConfig() {
await input({ prompt: '按回车继续', defaultValue: '' });
}
// ═══════════════════════════════════════════════════════════════════════════
// 设备配对管理 (Control UI / 浏览器配对审批)
// ═══════════════════════════════════════════════════════════════════════════
async function manageDevicePairing() {
while (true) {
resetRenderCount();
console.log(`\n${C.bold}📱 设备配对管理 (Control UI / 浏览器配对审批)${C.reset}\n`);
console.log(`${C.yellow}⚠️ 风险提示:${C.reset}`);
console.log(`${C.yellow} 批准设备配对后,该浏览器/客户端将获得 OpenClaw 网关的完全控制权限。${C.reset}`);
console.log(`${C.yellow} 请确保在受信任的局域网环境,并仅在您本人正在连接时批准!${C.reset}\n`);
const listed = ocCmdCapture('devices', 'list', '--json');
let pending = [];
let paired = [];
try {
const data = JSON.parse(listed.stdout);
if (Array.isArray(data.pending)) pending = data.pending;
if (Array.isArray(data.paired)) paired = data.paired;
} catch (e) {
const m = (listed.stdout || '').match(/(\{[\s\S]*"pending"[\s\S]*\})/);
if (m) {
try {
const d = JSON.parse(m[1]);
if (Array.isArray(d.pending)) pending = d.pending;
if (Array.isArray(d.paired)) paired = d.paired;
} catch (_) {}
}
}
function renderPairedDevices(list) {
console.log(`\n${C.bold}📋 已配对设备 (${list.length})${C.reset}\n`);
if (list.length === 0) {
console.log(` ${C.dim}暂无已配对设备记录${C.reset}`);
} else {
list.forEach((d, idx) => {
console.log(` ${C.cyan}[${idx + 1}]${C.reset} 设备 ID: ${d.deviceId || ''}`);
console.log(` 平台: ${d.platform || '未知'} | 客户端: ${d.clientId || ''} | IP: ${d.remoteIp || ''}`);
});
}
console.log('');
}
if (pending.length > 0) {
console.log(`${C.green}🔔 当前检测到 ${pending.length} 个待配对请求:${C.reset}\n`);
pending.forEach((item, idx) => {
const rid = item.requestId || '未知ID';
const ip = item.remoteIp || item.ip || '未知IP';
const client = item.clientId || item.clientMode || 'webchat';
const plat = item.platform || '';
const role = item.role || (item.roles && item.roles.join(',')) || 'operator';
console.log(` ${C.cyan}[${idx + 1}]${C.reset} 请求 ID: ${C.bold}${rid}${C.reset}`);
console.log(` 来源 IP: ${C.green}${ip}${C.reset} | 客户端: ${client}${plat ? ' (' + plat + ')' : ''} | 角色: ${role}`);
});
console.log('');
const choice = await select({
title: '请选择操作',
showSearch: false,
items: [
{ key: '1', label: '一键批准所有待配对请求', desc: `共 ${pending.length} 个待审批`, value: 'approve-all' },
{ key: '2', label: '选择指定请求批准', desc: '', value: 'approve-select' },
{ key: '3', label: '刷新待配对列表', desc: '', value: 'refresh' },
{ key: '4', label: '查看已配对设备列表', desc: `当前已配对 ${paired.length} 台设备`, value: 'view-paired' },
{ label: '', disabled: true },
{ key: '0', label: '返回上级菜单', desc: '', value: 'back' },
],
});
if (!choice || choice.value === 'back') break;
if (choice.value === 'refresh') continue;
if (choice.value === 'view-paired') {
renderPairedDevices(paired);
await input({ prompt: '按回车继续', defaultValue: '' });
continue;
}
if (choice.value === 'approve-all') {
const ok = await confirm({
message: `${C.yellow}确认一键批准全部 ${pending.length} 个设备配对请求吗?${C.reset}`,
defaultValue: true,
});
if (ok) {
console.log(`\n${C.cyan}正在批准设备配对...${C.reset}`);
let success = 0;
for (const item of pending) {
const res = ocCmdCapture('devices', 'approve', item.requestId);
if (res.ok || /approved|success/i.test(res.stdout)) {
console.log(`${C.green}✅ 已批准: ${item.requestId} (${item.remoteIp || ''})${C.reset}`);
success++;
} else {
console.log(`${C.red}❌ 批准失败: ${item.requestId} — ${res.stdout || res.stderr}${C.reset}`);
}
}
fixStatePermissions();
console.log(`\n${C.green}操作完成:成功 ${success}/${pending.length} 个${C.reset}`);
await input({ prompt: '按回车继续', defaultValue: '' });
}
} else if (choice.value === 'approve-select') {
const selectItems = pending.map((item, idx) => ({
key: String(idx + 1),
label: `批准: ${item.remoteIp || '设备'} (${(item.requestId || '').slice(0, 8)}...)`,
desc: `IP: ${item.remoteIp || ''} | ${item.platform || ''}`,
value: item.requestId,
}));
selectItems.push({ label: '', disabled: true });
selectItems.push({ key: '0', label: '取消', desc: '', value: 'cancel' });
const picked = await select({
title: '请选择要批准的设备请求',
showSearch: false,
items: selectItems,
});
if (picked && picked.value !== 'cancel') {
const res = ocCmdCapture('devices', 'approve', picked.value);
if (res.ok || /approved|success/i.test(res.stdout)) {
fixStatePermissions();
console.log(`\n${C.green}✅ 成功批准设备请求: ${picked.value}${C.reset}`);
} else {
console.log(`\n${C.red}❌ 批准失败: ${res.stdout || res.stderr}${C.reset}`);
}
await input({ prompt: '按回车继续', defaultValue: '' });
}
}
} else {
console.log(`${C.dim}当前无待配对设备请求。${C.reset}`);
console.log(`${C.dim}(在浏览器访问 OpenClaw Web 控制台时若提示「需要设备配对」,在此处刷新即可发现)${C.reset}\n`);
const choice = await select({
title: '设备配对管理',
showSearch: false,
items: [
{ key: '1', label: '刷新待配对列表', desc: '', value: 'refresh' },
{ key: '2', label: '手动输入 Request ID 批准', desc: '', value: 'manual' },
{ key: '3', label: '查看已配对设备列表', desc: `当前已配对 ${paired.length} 台设备`, value: 'view-paired' },
{ label: '', disabled: true },
{ key: '0', label: '返回上级菜单', desc: '', value: 'back' },
],
});
if (!choice || choice.value === 'back') break;
if (choice.value === 'refresh') continue;
if (choice.value === 'manual') {
const reqId = await input({ prompt: '请输入待批准的 Request ID', defaultValue: '' });
if (reqId && reqId.trim()) {
const res = ocCmdCapture('devices', 'approve', reqId.trim());
if (res.ok || /approved|success/i.test(res.stdout)) {
fixStatePermissions();
console.log(`\n${C.green}✅ 成功批准设备请求: ${reqId.trim()}${C.reset}`);
} else {
console.log(`\n${C.red}❌ 批准失败: ${res.stdout || res.stderr}${C.reset}`);
}
await input({ prompt: '按回车继续', defaultValue: '' });
}
} else if (choice.value === 'view-paired') {
renderPairedDevices(paired);
await input({ prompt: '按回车继续', defaultValue: '' });
}
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 高级配置处理 (对应 advanced_menu)
// ═══════════════════════════════════════════════════════════════════════════
@@ -2007,12 +2288,21 @@ async function handleAdvancedConfig() {
}
break;
}
case 'wizard':
case 'wizard': {
resetRenderCount();
console.log(`\n${C.cyan}启动官方配置向导...${C.reset}\n`);
console.log(`${C.yellow}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${C.reset}\n`);
let ok = false;
try {
enableAuthPlugins();
await ocCmd('configure');
} catch (e) {}
ok = true;
} catch (e) {
console.log(`${C.yellow}配置向导已退出${C.reset}\n`);
}
if (ok) await askRestart();
break;
}
case 'view-json':
console.log(`\n${C.cyan}配置文件路径: ${CONFIG_FILE}${C.reset}\n`);
try {
@@ -2039,6 +2329,8 @@ async function handleAdvancedConfig() {
if (importPath && fs.existsSync(importPath)) {
try {
fs.copyFileSync(importPath, CONFIG_FILE);
execSync(`chown openclaw:openclaw "${CONFIG_FILE}" 2>/dev/null || true`, { stdio: 'ignore' });
fixStatePermissions();
console.log(`\n${C.green}✅ 配置已导入${C.reset}\n`);
await askRestart();
} catch (e) {
@@ -2049,6 +2341,9 @@ async function handleAdvancedConfig() {
}
break;
}
case 'devices':
await manageDevicePairing();
break;
}
}
}
@@ -2074,7 +2369,6 @@ async function handleReset() {
jsonSet('gateway.bind', 'lan');
jsonSet('gateway.mode', 'local');
jsonSet('gateway.controlUi.allowInsecureAuth', true);
jsonSet('gateway.controlUi.dangerouslyDisableDeviceAuth', true);
jsonSet('gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback', true);
jsonSet('gateway.tailscale.mode', 'off');
console.log(`\n${C.green}✅ 网关设置已恢复默认${C.reset}\n`);
@@ -2154,7 +2448,6 @@ async function handleReset() {
jsonSet('gateway.auth.mode', 'token');
jsonSet('gateway.auth.token', newToken);
jsonSet('gateway.controlUi.allowInsecureAuth', true);
jsonSet('gateway.controlUi.dangerouslyDisableDeviceAuth', true);
jsonSet('gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback', true);
jsonSet('gateway.tailscale.mode', 'off');
jsonSet('acp.dispatch.enabled', false);
@@ -2330,6 +2623,9 @@ async function main() {
resetRenderCount();
await restartGateway();
break;
case 'devices':
await manageDevicePairing();
break;
case 'advanced':
await handleAdvancedConfig();
break;
@@ -50,7 +50,10 @@ fi
NODE_BASE="${NODE_BASE:-${OC_INSTALL_PATH}/node}"
OC_GLOBAL="${OC_GLOBAL:-${OC_INSTALL_PATH}/global}"
OC_DATA="${OC_DATA:-${OC_INSTALL_PATH}/data}"
NODE_BIN="${NODE_BASE}/bin/node"
NODE_BIN="${NODE_BIN:-${NODE_BASE}/bin/node}"
if [ ! -x "$NODE_BIN" ] && command -v node >/dev/null 2>&1; then
NODE_BIN="$(command -v node)"
fi
OC_STATE_DIR="${OC_DATA}/.openclaw"
CONFIG_FILE="${OC_STATE_DIR}/openclaw.json"
@@ -89,7 +92,7 @@ fi
oc_cmd() {
if [ -n "$OC_ENTRY" ] && [ -x "$NODE_BIN" ]; then
"$NODE_BIN" "$OC_ENTRY" "$@" 2>&1
"$NODE_BIN" "$OC_ENTRY" "$@"
local rc=$?
# 修复权限: oc_cmd 以 root 运行但配置文件应属于 openclaw 用户
fix_openclaw_state_permissions 2>/dev/null || true
@@ -116,13 +119,17 @@ oc_doctor_as_openclaw() {
oc_cmd doctor
return $?
fi
local _doc_cmd="HOME=\"${OC_DATA}\" OPENCLAW_HOME=\"${OC_DATA}\" OPENCLAW_STATE_DIR=\"${OC_DATA}/.openclaw\" OPENCLAW_CONFIG_PATH=\"${CONFIG_FILE}\" NODE_ICU_DATA=\"${NODE_BASE}/share/icu\" \"${NODE_BIN}\" \"${OC_ENTRY}\" doctor"
local _doc_cmd="cd \"${OC_DATA}\" && HOME=\"${OC_DATA}\" OPENCLAW_HOME=\"${OC_DATA}\" OPENCLAW_STATE_DIR=\"${OC_DATA}/.openclaw\" OPENCLAW_CONFIG_PATH=\"${CONFIG_FILE}\" NODE_ICU_DATA=\"${NODE_BASE}/share/icu\" \"${NODE_BIN}\" \"${OC_ENTRY}\" doctor"
if command -v su >/dev/null 2>&1; then
su -s /bin/sh openclaw -c "${_doc_cmd}" 2>&1
elif command -v runuser >/dev/null 2>&1; then
runuser -u openclaw -- sh -c "${_doc_cmd}" 2>&1
else
start-stop-daemon -S -m -p /tmp/openclaw-doctor-$$.pid -c openclaw:openclaw -x /bin/sh -- -c "${_doc_cmd}" 2>&1
local _doc_pid="/tmp/openclaw-doctor-$$.pid"
(cd "${OC_DATA}" 2>/dev/null || true; start-stop-daemon -S -m -p "$_doc_pid" -c openclaw:openclaw -x /bin/sh -- -c "${_doc_cmd}" 2>&1)
local rc=$?
rm -f "$_doc_pid" 2>/dev/null || true
return $rc
fi
}
@@ -262,8 +269,6 @@ oc_schema_type_for_key() {
echo "boolean" ;;
gateway.controlUi.allowInsecureAuth)
echo "boolean" ;;
gateway.controlUi.dangerouslyDisableDeviceAuth)
echo "boolean" ;;
gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback)
echo "boolean" ;;
gateway.allowRealIpFallback)
@@ -291,7 +296,34 @@ oc_schema_enum_for_key() {
json_set() {
local key="$1" value="$2"
local _js_err=""
# 检查是否处于升级事务迁移期间,若是则暂停写入配置
local _sdir="${OC_STATE_DIR:-${OPENCLAW_STATE_DIR:-${OC_DATA:+$OC_DATA/.openclaw}}}"
local _pdir="$(dirname "$_sdir")"
local _rdir="$(dirname "$_pdir")"
local _status_file="" cand
for cand in \
"${_rdir}/.luci-openclaw-upgrade/status.json" \
"${_pdir}/.luci-openclaw-upgrade/status.json" \
"${_sdir}/.luci-openclaw-upgrade/status.json" \
"${OC_DATA:+$OC_DATA/.luci-openclaw-upgrade/status.json}" \
"${OC_BASE_PATH:-/opt}/openclaw/.luci-openclaw-upgrade/status.json"; do
if [ -n "$cand" ] && [ -f "$cand" ]; then
_status_file="$cand"
break
fi
done
if [ -n "$_status_file" ] && [ -f "$_status_file" ]; then
local _up_phase
_up_phase=$(grep -oE '"phase"[[:space:]]*:[[:space:]]*"[^"]+"' "$_status_file" 2>/dev/null | cut -d'"' -f4 || true)
case "$_up_phase" in
migrating|gateway_verifying|backing_up)
echo "WARN: 检测到系统正处于升级事务 (phase: $_up_phase),暂停写入 openclaw.json" >&2
return 0
;;
esac
fi
# 步骤1: 确保配置文件存在
if [ ! -f "$CONFIG_FILE" ]; then
# 检查父目录是否可创建
@@ -928,50 +960,54 @@ show_current_config() {
# 配置 AI 模型
# ══════════════════════════════════════════════════════════════
configure_model() {
echo ""
echo -e " ${BOLD}🤖 配置 AI 模型和提供商${NC}"
echo ""
echo -e " ${GREEN}${BOLD}🌟 ── 推荐 ──${NC}"
echo -e " ${CYAN}w)${NC} 🌟 官方完整模型配置向导 ${GREEN}(推荐,支持所有提供商)${NC}"
echo ""
echo -e " ${BOLD}🌍 ── 国外模型提供商 ──${NC}"
echo -e " ${CYAN}a)${NC} OpenAI (GPT-5.2, GPT-5 mini, GPT-4.1)"
echo -e " ${CYAN}b)${NC} Anthropic (Claude Sonnet 4, Opus 4, Haiku)"
echo -e " ${CYAN}c)${NC} Google Gemini (Gemini 2.5 Pro/Flash, Gemini 3)"
echo -e " ${CYAN}d)${NC} OpenRouter (聚合多家模型)"
echo -e " ${CYAN}e)${NC} GitHub Copilot (需要 Copilot 订阅)"
echo -e " ${CYAN}f)${NC} xAI Grok (Grok-4/3)"
echo ""
echo -e " ${BOLD}🇨🇳 ── 国内模型提供商 ──${NC}"
echo -e " ${CYAN}g)${NC} 阿里云通义千问 Qwen (Portal/API/Coding Plan)"
echo -e " ${CYAN}h)${NC} 硅基流动 SiliconFlow"
echo -e " ${CYAN}i)${NC} 腾讯云 Coding Plan (HY T1/TurboS/GLM-5/Kimi)"
echo -e " ${CYAN}j)${NC} 百度千帆 (ERNIE-4.0, ERNIE-3.5)"
echo -e " ${CYAN}k)${NC} 智谱 GLM / Z.AI (GLM-5.1, GLM-5)"
echo ""
echo -e " ${BOLD}🏠 ── 本地模型 / 自定义 API ──${NC}"
echo -e " ${CYAN}l)${NC} Ollama (本地模型,无需 API Key)"
echo -e " ${CYAN}m)${NC} 自定义 OpenAI 兼容 API"
echo -e " ${CYAN}n)${NC} 自定义 Anthropic 兼容 API"
echo -e " ${CYAN}o)${NC} 一万AI分享 粉丝专享 API"
echo ""
echo -e " ${CYAN}q)${NC} 返回"
echo ""
prompt_with_default "请选择" "w" choice
while true; do
local configured=0
echo ""
echo -e " ${BOLD}🤖 配置 AI 模型和提供商${NC}"
echo ""
echo -e " ${GREEN}${BOLD}🌟 ── 推荐 ──${NC}"
echo -e " ${CYAN}w)${NC} 🌟 官方完整模型配置向导 ${GREEN}(推荐,支持所有提供商)${NC}"
echo ""
echo -e " ${BOLD}🌍 ── 国外模型提供商 ──${NC}"
echo -e " ${CYAN}a)${NC} OpenAI"
echo -e " ${CYAN}b)${NC} Anthropic"
echo -e " ${CYAN}c)${NC} Google Gemini"
echo -e " ${CYAN}d)${NC} OpenRouter (聚合多家模型)"
echo -e " ${CYAN}e)${NC} GitHub Copilot (需要 Copilot 订阅)"
echo -e " ${CYAN}f)${NC} xAI Grok"
echo ""
echo -e " ${BOLD}🇨🇳 ── 国内模型提供商 ──${NC}"
echo -e " ${CYAN}g)${NC} 阿里云通义千问 Qwen"
echo -e " ${CYAN}h)${NC} 硅基流动 SiliconFlow"
echo -e " ${CYAN}i)${NC} 腾讯云 Coding Plan"
echo -e " ${CYAN}j)${NC} 百度千帆"
echo -e " ${CYAN}k)${NC} 智谱 GLM / Z.AI"
echo ""
echo -e " ${BOLD}🏠 ── 本地模型 / 自定义 API ──${NC}"
echo -e " ${CYAN}l)${NC} Ollama (本地模型,无需 API Key)"
echo -e " ${CYAN}m)${NC} 自定义 OpenAI 兼容 API"
echo -e " ${CYAN}n)${NC} 自定义 Anthropic 兼容 API"
echo -e " ${CYAN}o)${NC} 一万AI分享 粉丝专享 API"
echo ""
echo -e " ${CYAN}q)${NC} 返回"
echo ""
prompt_with_default "请选择" "w" choice
case "$choice" in
w)
echo ""
echo -e " ${CYAN}启动官方完整模型配置向导...${NC}"
echo -e " ${YELLOW}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${NC}"
echo ""
echo -e " ${CYAN}清理过时插件配置...${NC}"
enable_auth_plugins
echo ""
(oc_cmd configure --section model) || echo -e " ${YELLOW}配置向导已退出${NC}"
echo ""
ask_restart
;;
case "$choice" in
w)
echo ""
echo -e " ${CYAN}启动官方完整模型配置向导...${NC}"
echo -e " ${YELLOW}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${NC}"
echo ""
echo -e " ${CYAN}清理过时插件配置...${NC}"
enable_auth_plugins
echo ""
if oc_cmd configure --section model; then
configured=1
else
echo -e " ${YELLOW}配置向导已退出${NC}"
fi
;;
a)
echo ""
echo -e " ${BOLD}OpenAI 配置${NC}"
@@ -987,6 +1023,7 @@ configure_model() {
model_name="$OC_PICKED_MODEL"
register_and_set_model "openai/${model_name}"
echo -e " ${GREEN}✅ OpenAI 已配置,活跃模型: openai/${model_name}${NC}"
configured=1
fi
;;
b)
@@ -1004,6 +1041,7 @@ configure_model() {
model_name="$OC_PICKED_MODEL"
register_and_set_model "anthropic/${model_name}"
echo -e " ${GREEN}✅ Anthropic 已配置,活跃模型: anthropic/${model_name}${NC}"
configured=1
fi
;;
c)
@@ -1021,6 +1059,7 @@ configure_model() {
model_name="$OC_PICKED_MODEL"
register_and_set_model "google/${model_name}"
echo -e " ${GREEN}✅ Google Gemini 已配置,活跃模型: google/${model_name}${NC}"
configured=1
fi
;;
d)
@@ -1039,6 +1078,7 @@ configure_model() {
model_name="$OC_PICKED_MODEL"
register_and_set_model "openrouter/${model_name}"
echo -e " ${GREEN}✅ OpenRouter 已配置,活跃模型: openrouter/${model_name}${NC}"
configured=1
fi
;;
e)
@@ -1062,6 +1102,7 @@ configure_model() {
model_name="github-copilot/$OC_PICKED_MODEL"
register_and_set_model "$model_name"
echo -e " ${GREEN}✅ 活跃模型已设置: ${model_name}${NC}"
configured=1
else
echo -e " ${YELLOW}OAuth 授权已退出或失败${NC}"
fi
@@ -1084,10 +1125,11 @@ configure_model() {
echo -e " ${CYAN}启用 Qwen Portal Auth 插件...${NC}"
enable_auth_plugins
echo -e " ${CYAN}启动 Qwen OAuth 授权...${NC}"
oc_cmd models auth login --provider qwen-portal --set-default || echo -e " ${YELLOW}OAuth 授权已退出${NC}"
echo ""
ask_restart
return
if oc_cmd models auth login --provider qwen-portal --set-default; then
configured=1
else
echo -e " ${YELLOW}OAuth 授权已退出${NC}"
fi
;;
b)
echo ""
@@ -1107,6 +1149,7 @@ configure_model() {
register_custom_provider dashscope "https://dashscope.aliyuncs.com/compatible-mode/v1" "$api_key" "$model_name" "$model_name"
register_and_set_model "dashscope/${model_name}"
echo -e " ${GREEN}✅ 通义千问已配置 (按量付费),活跃模型: dashscope/${model_name}${NC}"
configured=1
fi
;;
c|*)
@@ -1149,6 +1192,7 @@ configure_model() {
register_and_set_model "bailian/${model_name}"
echo -e " ${GREEN}✅ Coding Plan 已配置,活跃模型: bailian/${model_name}${NC}"
echo -e " ${DIM}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${NC}"
configured=1
fi
;;
esac
@@ -1168,6 +1212,7 @@ configure_model() {
auth_set_apikey xai "$api_key"
register_and_set_model "xai/${model_name}"
echo -e " ${GREEN}✅ xAI Grok 已配置,活跃模型: xai/${model_name}${NC}"
configured=1
fi
;;
h)
@@ -1200,6 +1245,7 @@ configure_model() {
register_custom_provider siliconflow "https://api.siliconflow.cn/v1" "$api_key" "$model_name" "$model_name"
register_and_set_model "siliconflow/${model_name}"
echo -e " ${GREEN}✅ SiliconFlow 已配置,活跃模型: siliconflow/${model_name}${NC}"
configured=1
fi
;;
l)
@@ -1315,6 +1361,7 @@ configure_model() {
register_and_set_model "ollama/${model_name}"
echo -e " ${GREEN}✅ Ollama 已配置,活跃模型: ollama/${model_name}${NC}"
echo -e " ${CYAN} Ollama 地址: ${ollama_url}${NC}"
configured=1
fi
fi
;;
@@ -1342,6 +1389,7 @@ configure_model() {
register_and_set_model "lkeap/${model_name}"
echo -e " ${GREEN}✅ 腾讯云 Coding Plan 已配置,活跃模型: lkeap/${model_name}${NC}"
echo -e " ${DIM}提示: 套餐内全部模型已注册,可随时在 WebChat 中通过 /model 切换${NC}"
configured=1
fi
;;
j)
@@ -1365,6 +1413,7 @@ configure_model() {
register_custom_provider qianfan "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop" "$api_key" "$model_name" "$model_name"
register_and_set_model "qianfan/${model_name}"
echo -e " ${GREEN}✅ 百度千帆已配置,活跃模型: qianfan/${model_name}${NC}"
configured=1
fi
;;
k)
@@ -1413,6 +1462,7 @@ configure_model() {
register_and_set_model "zai/${model_name}"
echo -e " ${GREEN}✅ 智谱 GLM 已配置,活跃模型: zai/${model_name}${NC}"
echo -e " ${DIM} Base URL: ${zai_base_url}${NC}"
configured=1
fi
;;
m)
@@ -1428,6 +1478,7 @@ configure_model() {
register_custom_provider openai-compatible "$base_url" "$api_key" "$model_name" "$model_name"
register_and_set_model "openai-compatible/${model_name}"
echo -e " ${GREEN}✅ 自定义模型已配置,活跃模型: openai-compatible/${model_name}${NC}"
configured=1
fi
;;
n)
@@ -1466,6 +1517,7 @@ configure_model() {
auth_set_apikey anthropic-compatible "$api_key" "anthropic-compatible:manual"
register_and_set_model "anthropic-compatible/${model_name}"
echo -e " ${GREEN}✅ 自定义 Anthropic API 已配置,活跃模型: anthropic-compatible/${model_name}${NC}"
configured=1
fi
;;
o)
@@ -1492,15 +1544,18 @@ configure_model() {
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
register_and_set_model "yiwanai/gpt-5.5"
echo -e " ${GREEN}✅ 一万AI分享粉丝专享 API 已配置,活跃模型: yiwanai/gpt-5.5${NC}"
configured=1
fi
;;
q) return ;;
*) echo -e " ${YELLOW}无效选择${NC}" ;;
esac
if [ "$choice" != "0" ] && [ "$choice" != "1" ]; then
if [ "$configured" -eq 1 ]; then
echo ""
ask_restart
fi
done
}
# ══════════════════════════════════════════════════════════════
@@ -1601,7 +1656,7 @@ configure_qq() {
plugin_blocked=1
echo -e " ${YELLOW}⚠️ qqbot 插件已安装但未能正常加载${NC}"
echo -e " ${CYAN}正在修复插件目录权限...${NC}"
chown -R root:root "$qqbot_ext_dir" 2>/dev/null
chown -R openclaw:openclaw "$qqbot_ext_dir" 2>/dev/null
chmod -R 755 "$qqbot_ext_dir" 2>/dev/null
echo -e " ${GREEN}✅ 权限已修复,重启 Gateway 后生效${NC}"
plugin_installed=1
@@ -1610,7 +1665,7 @@ configure_qq() {
# 目录存在、有 plugin.json 但未出现在插件列表 — 修复权限
echo -e " ${YELLOW}⚠️ qqbot 插件目录存在但未能加载${NC}"
echo -e " ${CYAN}正在修复插件目录权限...${NC}"
chown -R root:root "$qqbot_ext_dir" 2>/dev/null
chown -R openclaw:openclaw "$qqbot_ext_dir" 2>/dev/null
chmod -R 755 "$qqbot_ext_dir" 2>/dev/null
echo -e " ${GREEN}✅ 权限已修复${NC}"
plugin_installed=1
@@ -1629,10 +1684,10 @@ configure_qq() {
install_out=$(oc_cmd plugins install @tencent-connect/openclaw-qqbot@latest 2>&1)
local install_rc=$?
# 关键: 安装后立即修复插件目录权限为 root (OpenClaw 安全策略要求)
# 关键: 保持插件目录权限为 openclaw:openclaw (755),符合 OpenClaw 上游 discovery 安全校验规则
# 同时修复权限模式为 755,确保 Gateway 可读取插件
if [ -d "$qqbot_ext_dir" ]; then
chown -R root:root "$qqbot_ext_dir" 2>/dev/null
chown -R openclaw:openclaw "$qqbot_ext_dir" 2>/dev/null
chmod -R 755 "$qqbot_ext_dir" 2>/dev/null
fi
@@ -2093,7 +2148,15 @@ configure_channels() {
8)
echo ""
echo -e " ${CYAN}启动官方渠道配置向导...${NC}"
(oc_cmd configure --section channels) || echo -e " ${YELLOW}配置向导已退出${NC}"
echo -e " ${YELLOW}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${NC}"
echo ""
enable_auth_plugins
if oc_cmd configure --section channels; then
echo ""
ask_restart
else
echo -e " ${YELLOW}配置向导已退出${NC}"
fi
;;
0) return ;;
*) echo -e " ${YELLOW}无效选择${NC}" ;;
@@ -2242,7 +2305,6 @@ reset_to_defaults() {
json_set gateway.bind lan 2>&1
json_set gateway.mode local 2>&1
json_set gateway.controlUi.allowInsecureAuth true 2>&1
json_set gateway.controlUi.dangerouslyDisableDeviceAuth true 2>&1
json_set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback true 2>&1
json_set gateway.tailscale.mode off 2>&1
echo -e " ${GREEN}✅ 网关设置已恢复默认${NC}"
@@ -2423,7 +2485,6 @@ reset_to_defaults() {
json_set gateway.auth.mode token
json_set gateway.auth.token "$new_token"
json_set gateway.controlUi.allowInsecureAuth true
json_set gateway.controlUi.dangerouslyDisableDeviceAuth true
json_set gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback true
json_set gateway.tailscale.mode off
json_set acp.dispatch.enabled false
@@ -2484,10 +2545,14 @@ backup_restore_menu() {
echo ""
prompt_with_default "请选择" "1" backup_choice
# 备份目录 (openclaw backup create 输出到 CWD)
local backup_dir="${OC_STATE_DIR}/backups"
# 备份目录 (必须位于 State 目录之外,避免归档递归)
local backup_dir="${OPENCLAW_BACKUP_DIR:-${OC_DATA}/backups}"
mkdir -p "$backup_dir" 2>/dev/null
local state_helper="/usr/libexec/openclaw-upgrade-state.sh"
[ -x "$state_helper" ] || state_helper="${_script_dir}/../libexec/openclaw-upgrade-state.sh"
[ -x "$state_helper" ] || state_helper="./root/usr/libexec/openclaw-upgrade-state.sh"
case "$backup_choice" in
1)
echo -e " ${CYAN}正在创建配置备份...${NC}"
@@ -2505,14 +2570,16 @@ backup_restore_menu() {
2)
echo -e " ${CYAN}正在创建完整备份...${NC}"
echo -e " ${DIM}(包含配置和状态数据,可能需要较长时间)${NC}"
local out
out=$(cd "$backup_dir" && HOME="$backup_dir" oc_cmd backup create --no-include-workspace 2>&1)
local rc=$?
local out rc=0
if [ -x "$state_helper" ]; then
out=$("$state_helper" backup-create "$backup_dir" "$OC_STATE_DIR" 2>&1) || rc=$?
else
out=$(cd "$backup_dir" && HOME="$backup_dir" oc_cmd backup create --no-include-workspace 2>&1) || rc=$?
for f in "${OC_DATA}"/*-openclaw-backup.tar.gz; do
[ -f "$f" ] && mv "$f" "$backup_dir/" 2>/dev/null
done
fi
echo "$out"
# 完整备份可能输出到 HOME,尝试移动到 backup_dir
for f in "${OC_DATA}"/*-openclaw-backup.tar.gz; do
[ -f "$f" ] && mv "$f" "$backup_dir/" 2>/dev/null
done
if [ $rc -eq 0 ] && echo "$out" | grep -q "\.tar\.gz"; then
echo -e " ${GREEN}✅ 完整备份已创建${NC}"
else
@@ -2521,25 +2588,29 @@ backup_restore_menu() {
fi
;;
3)
local latest=$(ls -t "${backup_dir}"/*-openclaw-backup.tar.gz 2>/dev/null | head -1)
local latest=$(ls -t "${backup_dir}"/*-openclaw-backup.tar.gz "${backup_dir}"/*.tar.gz 2>/dev/null | head -1)
if [ -z "$latest" ]; then
# 也检查旧位置
latest=$(ls -t "${OC_STATE_DIR}"/*-openclaw-backup.tar.gz "${OC_DATA}"/*-openclaw-backup.tar.gz 2>/dev/null | head -1)
latest=$(ls -t "${OC_STATE_DIR}/backups"/*-openclaw-backup.tar.gz "${OC_STATE_DIR}"/*-openclaw-backup.tar.gz "${OC_DATA}"/*-openclaw-backup.tar.gz 2>/dev/null | head -1)
fi
if [ -z "$latest" ]; then
echo -e " ${YELLOW}未找到备份文件,请先创建备份${NC}"
else
echo -e " ${CYAN}验证备份: ${latest}${NC}"
oc_cmd backup verify "$latest" 2>&1
if [ -x "$state_helper" ]; then
"$state_helper" backup-verify "$latest" "$OC_STATE_DIR" 2>&1
else
oc_cmd backup verify "$latest" 2>&1
fi
fi
;;
4)
echo ""
if [ -d "$backup_dir" ]; then
local count=$(ls "${backup_dir}"/*-openclaw-backup.tar.gz 2>/dev/null | wc -l)
local count=$(ls "${backup_dir}"/*-openclaw-backup.tar.gz "${backup_dir}"/*.tar.gz 2>/dev/null | wc -l)
if [ "$count" -gt 0 ] 2>/dev/null; then
echo -e " ${BOLD}备份文件列表:${NC}"
ls -lh "${backup_dir}"/*-openclaw-backup.tar.gz 2>/dev/null | while read line; do
ls -lh "${backup_dir}"/*-openclaw-backup.tar.gz "${backup_dir}"/*.tar.gz 2>/dev/null | while read line; do
echo -e " ${DIM}${line}${NC}"
done
else
@@ -2552,7 +2623,7 @@ backup_restore_menu() {
echo -e " ${DIM}备份目录: ${backup_dir}${NC}"
;;
5)
local latest=$(ls -t "${backup_dir}"/*-openclaw-backup.tar.gz 2>/dev/null | head -1)
local latest=$(ls -t "${backup_dir}"/*-openclaw-backup.tar.gz "${backup_dir}"/*.tar.gz 2>/dev/null | head -1)
if [ -z "$latest" ]; then
echo -e " ${YELLOW}未找到备份文件,请先创建备份${NC}"
else
@@ -2562,36 +2633,31 @@ backup_restore_menu() {
echo -e " ${YELLOW}⚠️ 这会还原备份中的所有配置和数据文件到原路径!${NC}"
prompt_with_default "确认恢复? (y/N)" "N" confirm_restore
if [ "$confirm_restore" = "y" ] || [ "$confirm_restore" = "Y" ]; then
# 验证备份中 openclaw.json 有效
local tmp_json="/tmp/oc-restore-check.json"
tar -xzf "$latest" --wildcards '*/openclaw.json' -O > "$tmp_json" 2>/dev/null
if [ ! -s "$tmp_json" ] || ! "$NODE_BIN" -e "JSON.parse(require('fs').readFileSync('${tmp_json}','utf8'))" 2>/dev/null; then
rm -f "$tmp_json"
echo -e " ${RED}❌ 备份中的配置文件无效,恢复已取消${NC}"
else
rm -f "$tmp_json"
# 备份当前配置
cp -f "$CONFIG_FILE" "${CONFIG_FILE}.pre-restore" 2>/dev/null
# 获取备份名前缀
local backup_name=$(tar -tzf "$latest" 2>/dev/null | head -1 | cut -d/ -f1)
if [ -z "$backup_name" ]; then
echo -e " ${RED}❌ 备份文件格式无法识别${NC}"
else
echo -e " ${DIM}正在还原文件...${NC}"
# 停止服务
/etc/init.d/openclaw stop >/dev/null 2>&1
sleep 2
# 提取 payload 到根目录 (还原到原始绝对路径)
tar -xzf "$latest" --strip-components=3 -C / "${backup_name}/payload/posix/" 2>&1
# 修复权限
fix_openclaw_state_permissions 2>/dev/null || true
echo -e " ${GREEN}✅ 配置和数据已完整恢复!原配置已保存为 openclaw.json.pre-restore${NC}"
echo ""
prompt_with_default "是否重启服务使配置生效? (Y/n)" "Y" do_restart
if [ "$do_restart" != "n" ] && [ "$do_restart" != "N" ]; then
restart_gateway
fi
echo -e " ${DIM}正在安全还原 State 状态...${NC}"
/etc/init.d/openclaw stop >/dev/null 2>&1 || true
sleep 2
local restore_ok=0
if [ -x "$state_helper" ]; then
if "$state_helper" restore-state "$latest" "$OC_STATE_DIR" 2>&1; then
restore_ok=1
fi
else
# 兜底旧逻辑
local backup_name=$(tar -tzf "$latest" 2>/dev/null | head -1 | cut -d/ -f1)
if [ -n "$backup_name" ]; then
tar -xzf "$latest" --strip-components=3 -C / "${backup_name}/payload/posix/" 2>&1 && restore_ok=1
fi
fi
if [ "$restore_ok" -eq 1 ]; then
fix_openclaw_state_permissions 2>/dev/null || true
echo -e " ${GREEN}✅ 配置和数据已完整恢复!${NC}"
echo ""
prompt_with_default "是否重启服务使配置生效? (Y/n)" "Y" do_restart
if [ "$do_restart" != "n" ] && [ "$do_restart" != "N" ]; then
restart_gateway
fi
else
echo -e " ${RED}❌ 恢复失败,已安全保留原状态与备份${NC}"
fi
else
echo -e " ${DIM}已取消${NC}"
@@ -2651,6 +2717,163 @@ launch_interactive_model_config() {
return $rc
}
# ── 设备配对管理 (Control UI / 浏览器配对审批) ──
devices_pairing_menu() {
while true; do
echo ""
echo -e " ${BOLD}📱 设备配对管理 (Control UI / 浏览器配对审批)${NC}"
echo ""
echo -e " ${YELLOW}⚠️ 风险提示:${NC}"
echo -e " ${YELLOW} 批准设备配对后,该浏览器/客户端将获得 OpenClaw 网关的完全控制权限。${NC}"
echo -e " ${YELLOW} 请确保在受信任的局域网环境,并仅在您本人正在连接时批准!${NC}"
echo ""
local list_json
list_json=$(oc_cmd devices list --json 2>/dev/null || echo "")
local pending_count=0
local parsed=""
if [ -n "$list_json" ] && [ -x "$NODE_BIN" ]; then
parsed=$("$NODE_BIN" -e '
let data = null;
try {
data = JSON.parse(process.argv[1]);
} catch (_) {
const m = (process.argv[1] || "").match(/(\{[\s\S]*"pending"[\s\S]*\})/);
if (m) {
try { data = JSON.parse(m[1]); } catch (_) {}
}
}
const pending = (data && Array.isArray(data.pending)) ? data.pending : [];
console.log("COUNT=" + pending.length);
pending.forEach((item) => {
const rid = item.requestId || "";
const ip = item.remoteIp || item.ip || "未知IP";
const client = item.clientId || item.clientMode || "webchat";
const plat = item.platform || "";
const role = item.role || (item.roles && item.roles.join(",")) || "operator";
console.log("ITEM\t" + rid + "\t" + ip + "\t" + client + "\t" + plat + "\t" + role);
});
' "$list_json" 2>/dev/null || echo "COUNT=0")
pending_count=$(echo "$parsed" | grep '^COUNT=' | cut -d= -f2)
pending_count=${pending_count:-0}
fi
if [ "$pending_count" -gt 0 ]; then
echo -e " ${GREEN}🔔 当前检测到 ${pending_count} 个待配对请求:${NC}"
echo ""
local idx=1
echo "$parsed" | grep '^ITEM' | while IFS="$(printf '\t')" read -r _ rid ip client plat role; do
echo -e " ${CYAN}[$idx]${NC} 请求 ID: ${BOLD}${rid}${NC}"
echo -e " 来源 IP: ${GREEN}${ip}${NC} | 客户端: ${client} (${plat}) | 角色: ${role}"
idx=$((idx + 1))
done
echo ""
echo -e " ${CYAN}1)${NC} ⚡ 一键批准所有待配对请求"
echo -e " ${CYAN}2)${NC} 🔍 选择指定请求批准"
echo -e " ${CYAN}3)${NC} 🔄 刷新待配对列表"
echo -e " ${CYAN}0)${NC} 返回上级菜单"
echo ""
prompt_with_default "请选择" "1" dev_choice
case "$dev_choice" in
1)
echo ""
echo -e " ${CYAN}正在批准所有设备配对请求...${NC}"
echo "$parsed" | grep '^ITEM' | cut -f2 | tr -d '\r' | while read -r rid; do
if [ -n "$rid" ]; then
local out
out=$(oc_cmd devices approve "$rid" 2>&1)
if echo "$out" | grep -qiE 'approved|success'; then
echo -e " ${GREEN}✅ 已批准: ${rid}${NC}"
else
echo -e " ${YELLOW}⚠️ 批准结果: ${out}${NC}"
fi
fi
done
fix_openclaw_state_permissions
echo ""
prompt_with_default "按回车继续" "" _
;;
2)
echo ""
prompt_with_default "请输入要批准的请求编号 (1-${pending_count}) 或直接输入 Request ID" "1" pick_val
local target_id=""
case "$pick_val" in
''|0|*[!0-9]*) target_id="$pick_val" ;;
*)
if [ "$pick_val" -ge 1 ] 2>/dev/null && [ "$pick_val" -le "$pending_count" ] 2>/dev/null; then
target_id=$(echo "$parsed" | grep '^ITEM' | sed -n "${pick_val}p" | cut -f2 | tr -d '\r')
else
target_id="$pick_val"
fi
;;
esac
if [ -n "$target_id" ]; then
echo ""
echo -e " ${CYAN}正在批准请求: ${target_id}...${NC}"
local out
out=$(oc_cmd devices approve "$target_id" 2>&1)
if echo "$out" | grep -qiE 'approved|success'; then
echo -e " ${GREEN}✅ 批准成功: ${target_id}${NC}"
else
echo -e " ${YELLOW}⚠️ 批准结果: ${out}${NC}"
fi
fix_openclaw_state_permissions
else
echo -e " ${YELLOW}未找到对应请求${NC}"
fi
echo ""
prompt_with_default "按回车继续" "" _
;;
3) ;;
0) return 0 ;;
*) echo -e " ${YELLOW}无效选择${NC}" ;;
esac
else
echo -e " ${DIM}当前无待配对设备请求。${NC}"
echo -e " ${DIM}(在浏览器访问 OpenClaw Web 控制台时若提示「需要设备配对」,在此处刷新即可发现)${NC}"
echo ""
echo -e " ${CYAN}1)${NC} 🔄 刷新待配对列表"
echo -e " ${CYAN}2)${NC} ⌨️ 手动输入 Request ID 批准"
echo -e " ${CYAN}3)${NC} 📋 查看完整设备列表 (含已配对)"
echo -e " ${CYAN}0)${NC} 返回上级菜单"
echo ""
prompt_with_default "请选择" "1" dev_choice
case "$dev_choice" in
1) ;;
2)
echo ""
prompt_with_default "请输入待批准的 Request ID" "" manual_rid
if [ -n "$manual_rid" ]; then
echo ""
local out
out=$(oc_cmd devices approve "$manual_rid" 2>&1)
if echo "$out" | grep -qiE 'approved|success'; then
echo -e " ${GREEN}✅ 批准成功: ${manual_rid}${NC}"
else
echo -e " ${YELLOW}⚠️ 批准结果: ${out}${NC}"
fi
fix_openclaw_state_permissions
echo ""
prompt_with_default "按回车继续" "" _
fi
;;
3)
echo ""
echo -e " ${CYAN}=== 设备列表 ===${NC}"
oc_cmd devices list 2>&1 || echo " (无法获取设备列表)"
echo ""
prompt_with_default "按回车继续" "" _
;;
0) return 0 ;;
*) echo -e " ${YELLOW}无效选择${NC}" ;;
esac
fi
done
}
main_menu() {
# 直接启动交互模式 (如果支持)
if can_use_interactive && [ "${OC_FORCE_TRADITIONAL:-0}" != "1" ]; then
@@ -2676,11 +2899,12 @@ main_menu() {
echo -e " ${CYAN}4)${NC} 🩺 健康检查与状态"
echo -e " ${CYAN}5)${NC} 📋 查看日志"
echo -e " ${CYAN}6)${NC} 🔄 重启 Gateway"
echo -e " ${CYAN}7)${NC} 📱 设备配对管理 (Control UI 审批)"
echo ""
echo -e " ${DIM}━━━ 高级选项 ━━━${NC}"
echo -e " ${CYAN}7)${NC} 🔧 高级配置"
echo -e " ${CYAN}8)${NC} ♻️ 重置配置"
echo -e " ${CYAN}9)${NC} 📊 显示当前配置概览"
echo -e " ${CYAN}8)${NC} 🔧 高级配置"
echo -e " ${CYAN}9)${NC} ♻️ 重置配置"
echo -e " ${CYAN}10)${NC} 📊 显示当前配置概览"
echo ""
echo -e " ${CYAN}0)${NC} 退出"
echo ""
@@ -2700,9 +2924,10 @@ main_menu() {
prompt_with_default "按回车继续" "" _
;;
6) restart_gateway ;;
7) advanced_menu ;;
8) reset_to_defaults ;;
9) show_current_config ;;
7) devices_pairing_menu ;;
8) advanced_menu ;;
9) reset_to_defaults ;;
10) show_current_config ;;
0)
echo -e " ${GREEN}再见!${NC}"
exit 0
@@ -2737,6 +2962,7 @@ advanced_menu() {
echo -e " ${CYAN}8)${NC} 编辑配置文件 ${DIM}(vi / nano)${NC}"
echo -e " ${CYAN}9)${NC} 导出配置备份"
echo -e " ${CYAN}10)${NC} 导入配置"
echo -e " ${CYAN}11)${NC} 📱 设备配对管理"
echo -e " ${CYAN}0)${NC} 返回主菜单"
echo ""
prompt_with_default "请选择" "0" adv_choice
@@ -2851,8 +3077,15 @@ advanced_menu() {
6)
echo ""
echo -e " ${CYAN}启动官方配置向导...${NC}"
oc_cmd configure
ask_restart
echo -e " ${YELLOW}提示: ↑↓ 移动, Tab/空格 选中, 回车 确认${NC}"
echo ""
enable_auth_plugins
if oc_cmd configure; then
echo ""
ask_restart
else
echo -e " ${YELLOW}配置向导已退出${NC}"
fi
;;
7)
echo ""
@@ -2898,6 +3131,7 @@ advanced_menu() {
echo -e " ${YELLOW}文件不存在${NC}"
fi
;;
11) devices_pairing_menu ;;
0) return ;;
*) echo -e " ${YELLOW}无效选择${NC}" ;;
esac
@@ -2957,7 +3191,7 @@ case "${1:-}" in
restart_gateway
;;
--backup)
bk_dir="${OC_STATE_DIR}/backups"
bk_dir="${OPENCLAW_BACKUP_DIR:-${OC_DATA}/backups}"
mkdir -p "$bk_dir" 2>/dev/null
echo -e "${CYAN}正在创建配置备份...${NC}"
cd "$bk_dir" && oc_cmd backup create --only-config --no-include-workspace 2>&1
@@ -316,6 +316,12 @@ async function select(options = {}) {
let done = false;
let result = null;
// 确保初始选中的不是 disabled 分隔线/标题项
if (items[selected] && items[selected].disabled) {
const firstActive = items.findIndex(i => !i.disabled);
if (firstActive !== -1) selected = firstActive;
}
// 保存终端状态
setRawMode(true);
process.stdout.write(C.hide);
@@ -336,30 +342,43 @@ async function select(options = {}) {
done = true;
}
} else if (key.name === 'up') {
// 上移
if (selected > 0) {
selected--;
// 上移: 自动跳过 disabled 分隔项
let next = selected - 1;
while (next >= 0 && filteredItems[next].disabled) next--;
if (next >= 0) {
selected = next;
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
}
} else if (key.name === 'down') {
// 下移
if (selected < filteredItems.length - 1) {
selected++;
// 下移: 自动跳过 disabled 分隔项
let next = selected + 1;
while (next < filteredItems.length && filteredItems[next].disabled) next++;
if (next < filteredItems.length) {
selected = next;
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
}
} else if (key.name === 'pageup') {
// 上翻页
selected = Math.max(0, selected - 10);
let target = Math.max(0, selected - 10);
while (target > 0 && filteredItems[target].disabled) target--;
if (filteredItems[target] && !filteredItems[target].disabled) selected = target;
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
} else if (key.name === 'pagedown') {
// 下翻页
selected = Math.min(filteredItems.length - 1, selected + 10);
let target = Math.min(filteredItems.length - 1, selected + 10);
while (target < filteredItems.length - 1 && filteredItems[target].disabled) target++;
if (filteredItems[target] && !filteredItems[target].disabled) selected = target;
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
} else if (key.name === 'home') {
selected = 0;
let first = filteredItems.findIndex(i => !i.disabled);
if (first !== -1) selected = first;
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
} else if (key.name === 'end') {
selected = filteredItems.length - 1;
let last = -1;
for (let i = filteredItems.length - 1; i >= 0; i--) {
if (!filteredItems[i].disabled) { last = i; break; }
}
if (last !== -1) selected = last;
filteredItems = renderMenu(items, selected, searchFilter, { title, header, footer, showSearch });
} else if (exitKeys.includes(key.name) && allowCancel) {
// 取消/退出
@@ -382,7 +401,7 @@ async function select(options = {}) {
// 直接按键选择 (如果菜单项有 key 属性)
if (key.char && !showSearch) {
const matchByChar = items.find(item => (item.key || '').toLowerCase() === key.name);
const matchByChar = items.find(item => !item.disabled && (item.key || '').toLowerCase() === key.name);
if (matchByChar) {
result = matchByChar;
done = true;
@@ -0,0 +1,577 @@
#!/usr/bin/env node
/**
* openclaw-package-contract.js
*
* OpenClaw 与 Node.js 运行时及包清单不可变契约校验工具。
*
* 契约规范:
* 1. OpenClaw @ 2026.9.1 默认清单白名单校验:
* - name = "openclaw"
* - version = "2026.9.1"
* - bin = "openclaw.mjs"
* - preinstall = "node scripts/preinstall-package-manager-warning.mjs"
* - postinstall = "node scripts/postinstall-bundled-plugins.mjs"
* - optionalDependencies 中若存在 sqlite-vec 则必须为 "0.1.9" (不限定只有该项)
* - engines.node = ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0"
* 2. 运行时校验: 针对完整 engines.node 规则做精确比对,失败关闭 (fail-closed)。
* 3. 自定义/latest 版本策略: 不套用 2026.9.1 白名单;存在 lifecycle 时 fail-closed;
* 无 lifecycle 时校验包名、bin、实际解析版本及 engines,并标记为未验证。
* 4. 退出码约定:
* - 0: 验证成功或帮助
* - 1: 契约不满足 / 运行时不匹配 / 脚本缺失
* - 2: CLI 用法或输入格式错误 (多余参数、非法/前后缀 semver、空或不支持 range、非对象 JSON 等)
*/
'use strict';
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const EXPECTED_NAME = 'openclaw';
const EXPECTED_VERSION = '2026.9.1';
const EXPECTED_BIN = 'openclaw.mjs';
const EXPECTED_PREINSTALL = 'node scripts/preinstall-package-manager-warning.mjs';
const EXPECTED_POSTINSTALL = 'node scripts/postinstall-bundled-plugins.mjs';
const EXPECTED_PREINSTALL_FILE = 'scripts/preinstall-package-manager-warning.mjs';
const EXPECTED_POSTINSTALL_FILE = 'scripts/postinstall-bundled-plugins.mjs';
const EXPECTED_SQLITE_VEC_VERSION = '0.1.9';
const DEFAULT_NODE_ENGINES = '>=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0';
/**
* 严格语义化版本号解析 (严格禁止前后缀、非数字以及多余段)
* 形如 "22.23.2" 或 "v22.23.2"
* 拒绝 "22.23", "22.23.2.1", "22.23.2-beta", "v22.23.2foo", "abc"
* @param {string} raw
* @returns {{ major: number, minor: number, patch: number } | null}
*/
function parseStrictSemver(raw) {
if (typeof raw !== 'string') return null;
const trimmed = raw.trim();
const m = trimmed.match(/^v?(\d+)\.(\d+)\.(\d+)$/);
if (!m) return null;
return {
major: parseInt(m[1], 10),
minor: parseInt(m[2], 10),
patch: parseInt(m[3], 10)
};
}
/**
* 比较两个语义化版本号
* @returns {number} a > b 返回 1, a < b 返回 -1, 相等返回 0
*/
function compareSemver(a, b) {
if (a.major !== b.major) return a.major > b.major ? 1 : -1;
if (a.minor !== b.minor) return a.minor > b.minor ? 1 : -1;
if (a.patch !== b.patch) return a.patch > b.patch ? 1 : -1;
return 0;
}
/**
* 解析比较器
* 支持形如 ">=22.22.3", "<23", "<25", ">=25.9.0", "=22.23.2"
* @param {string} comp
* @returns {{ op: string, target: { major: number, minor: number, patch: number } } | null}
*/
function parseComparator(comp) {
if (typeof comp !== 'string') return null;
const m = comp.trim().match(/^([><]=?|=)\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
if (!m) return null;
return {
op: m[1],
target: {
major: parseInt(m[2], 10),
minor: m[3] !== undefined ? parseInt(m[3], 10) : 0,
patch: m[4] !== undefined ? parseInt(m[4], 10) : 0
}
};
}
/**
* 判断版本是否满足单个比较条件
*/
function satisfiesComparator(ver, comp) {
const parsed = parseComparator(comp);
if (!parsed) return false;
const cmp = compareSemver(ver, parsed.target);
switch (parsed.op) {
case '>': return cmp > 0;
case '>=': return cmp >= 0;
case '<': return cmp < 0;
case '<=': return cmp <= 0;
case '=': return cmp === 0;
default: return false;
}
}
/**
* 校验 range 表达式合法性并判定版本是否满足
* @param {string} verStr
* @param {string} [rangeStr]
* @returns {{ validRange: boolean, satisfied: boolean, error?: string }}
*/
function evaluateNodeEngines(verStr, rangeStr) {
const ver = parseStrictSemver(verStr);
if (!ver) {
return { validRange: false, satisfied: false, error: `非法或包含前后缀的 semver 版本号: "${verStr}"` };
}
const spec = rangeStr !== undefined ? rangeStr : DEFAULT_NODE_ENGINES;
if (typeof spec !== 'string' || !spec.trim()) {
return { validRange: false, satisfied: false, error: 'engines.node 范围定义为空' };
}
const clauses = spec.split('||');
if (clauses.length === 0) {
return { validRange: false, satisfied: false, error: 'engines.node 范围格式非法' };
}
for (const clause of clauses) {
const comps = clause.trim().split(/\s+/).filter(Boolean);
if (comps.length === 0) {
return { validRange: false, satisfied: false, error: 'engines.node 存在空条件区间' };
}
for (const comp of comps) {
if (!parseComparator(comp)) {
return { validRange: false, satisfied: false, error: `不支持或非法的比较器: "${comp}"` };
}
}
}
for (const clause of clauses) {
const comps = clause.trim().split(/\s+/).filter(Boolean);
let allSatisfied = true;
for (const comp of comps) {
if (!satisfiesComparator(ver, comp)) {
allSatisfied = false;
break;
}
}
if (allSatisfied) {
return { validRange: true, satisfied: true };
}
}
return { validRange: true, satisfied: false };
}
/**
* 兼容旧接口的布尔检查函数
*/
function satisfiesNodeEngines(verStr, rangeStr) {
const res = evaluateNodeEngines(verStr, rangeStr);
return res.validRange && res.satisfied;
}
/**
* 校验 package.json 清单
* @param {object|string} pkgOrPath
* @param {string} targetVersion
* @returns {{ valid: boolean, isUsageError?: boolean, vettedScripts?: string[], error?: string, warning?: string, unverified?: boolean }}
*/
function validateManifest(pkgOrPath, targetVersion, nodeVersion) {
let pkg;
let pkgDir = null;
if (typeof pkgOrPath === 'string') {
if (!fs.existsSync(pkgOrPath)) {
return { valid: false, isUsageError: true, error: `文件不存在: ${pkgOrPath}` };
}
try {
const raw = fs.readFileSync(pkgOrPath, 'utf8');
pkg = JSON.parse(raw);
} catch (e) {
return { valid: false, isUsageError: true, error: `无法读取或解析 package.json (${pkgOrPath}): ${e.message}` };
}
pkgDir = path.dirname(path.resolve(pkgOrPath));
} else {
pkg = pkgOrPath;
}
if (!pkg || typeof pkg !== 'object' || Array.isArray(pkg)) {
return { valid: false, isUsageError: true, error: 'package.json 内容为空或不是有效的非数组 JSON 对象' };
}
if (typeof targetVersion !== 'string' || !targetVersion.trim()) {
return { valid: false, isUsageError: true, error: '缺少或非法的 target-version 参数' };
}
const targetVerTrimmed = targetVersion.trim();
const isPinned2026_9_1 = (targetVerTrimmed === '2026.9.1' || targetVerTrimmed === 'stable');
if (isPinned2026_9_1) {
// ── 针对 2026.9.1 进行精确不可变清单校验 ──
// name 校验: 存在、类型为 string、值精确等于 openclaw
if (pkg.name === undefined) {
return { valid: false, error: 'package.json 缺少 name 字段' };
}
if (typeof pkg.name !== 'string') {
return { valid: false, error: `name 字段类型错误: 期望 string, 实际 ${typeof pkg.name}` };
}
if (pkg.name !== EXPECTED_NAME) {
return { valid: false, error: `包名漂移: 期望 "${EXPECTED_NAME}", 实际 "${pkg.name}"` };
}
// version 校验: 存在、类型为 string、值精确等于 2026.9.1
if (pkg.version === undefined) {
return { valid: false, error: 'package.json 缺少 version 字段' };
}
if (typeof pkg.version !== 'string') {
return { valid: false, error: `version 字段类型错误: 期望 string, 实际 ${typeof pkg.version}` };
}
if (pkg.version !== EXPECTED_VERSION) {
return { valid: false, error: `版本号漂移: 期望 "${EXPECTED_VERSION}", 实际 "${pkg.version}"` };
}
// bin 校验: 必须为对象,bin.openclaw 必须存在且为 EXPECTED_BIN
if (pkg.bin === undefined) {
return { valid: false, error: 'package.json 缺少 bin 对象' };
}
if (typeof pkg.bin !== 'object' || pkg.bin === null || Array.isArray(pkg.bin)) {
return { valid: false, error: `bin 字段类型错误: 期望 object, 实际 ${Array.isArray(pkg.bin) ? 'array' : (pkg.bin === null ? 'null' : typeof pkg.bin)}` };
}
if (pkg.bin.openclaw === undefined) {
return { valid: false, error: 'package.json 缺少 bin.openclaw 字段' };
}
if (typeof pkg.bin.openclaw !== 'string') {
return { valid: false, error: `bin.openclaw 字段类型错误: 期望 string, 实际 ${typeof pkg.bin.openclaw}` };
}
if (pkg.bin.openclaw !== EXPECTED_BIN) {
return { valid: false, error: `bin 入口漂移: 期望 "${EXPECTED_BIN}", 实际 "${pkg.bin.openclaw}"` };
}
const binKeys = Object.keys(pkg.bin);
if (binKeys.length !== 1 || binKeys[0] !== 'openclaw') {
return { valid: false, error: `bin 结构漂移: 仅允许定义 openclaw 入口, 实际包含: ${binKeys.join(', ')}` };
}
// scripts 校验: 必须为对象,preinstall 与 postinstall 必须精确匹配
if (pkg.scripts === undefined) {
return { valid: false, error: 'package.json 缺少 scripts 对象' };
}
if (typeof pkg.scripts !== 'object' || pkg.scripts === null || Array.isArray(pkg.scripts)) {
return { valid: false, error: `scripts 字段类型错误: 期望 object, 实际 ${Array.isArray(pkg.scripts) ? 'array' : (pkg.scripts === null ? 'null' : typeof pkg.scripts)}` };
}
if (pkg.scripts.preinstall === undefined) {
return { valid: false, error: 'package.json 缺少 preinstall 脚本' };
}
if (typeof pkg.scripts.preinstall !== 'string') {
return { valid: false, error: `preinstall 脚本类型错误: 期望 string, 实际 ${typeof pkg.scripts.preinstall}` };
}
if (pkg.scripts.preinstall !== EXPECTED_PREINSTALL) {
return { valid: false, error: `preinstall 脚本漂移: 期望 "${EXPECTED_PREINSTALL}", 实际 "${pkg.scripts.preinstall}"` };
}
if (pkg.scripts.postinstall === undefined) {
return { valid: false, error: 'package.json 缺少 postinstall 脚本' };
}
if (typeof pkg.scripts.postinstall !== 'string') {
return { valid: false, error: `postinstall 脚本类型错误: 期望 string, 实际 ${typeof pkg.scripts.postinstall}` };
}
if (pkg.scripts.postinstall !== EXPECTED_POSTINSTALL) {
return { valid: false, error: `postinstall 脚本漂移: 期望 "${EXPECTED_POSTINSTALL}", 实际 "${pkg.scripts.postinstall}"` };
}
const knownLifecycle = new Set(['preinstall', 'postinstall']);
const INSTALL_LIFECYCLE_KEYS = new Set([
'install', 'preinstall', 'postinstall',
'prepare', 'prepack', 'postpack'
]);
for (const k of Object.keys(pkg.scripts)) {
const isLifecycle = INSTALL_LIFECYCLE_KEYS.has(k);
if (isLifecycle && !knownLifecycle.has(k)) {
return { valid: false, error: `存在未知或额外 lifecycle 脚本漂移: ${k}="${pkg.scripts[k]}"` };
}
const val = pkg.scripts[k];
if (typeof val !== 'string' || !val.trim()) {
return { valid: false, error: `scripts.${k} 脚本值为空或类型非法` };
}
}
// optionalDependencies 校验 (必须存在对象且包含 sqlite-vec="0.1.9")
if (pkg.optionalDependencies === undefined) {
return { valid: false, error: 'package.json 缺少 optionalDependencies 对象' };
}
if (typeof pkg.optionalDependencies !== 'object' || pkg.optionalDependencies === null || Array.isArray(pkg.optionalDependencies)) {
return { valid: false, error: `optionalDependencies 字段类型错误: 期望 object, 实际 ${Array.isArray(pkg.optionalDependencies) ? 'array' : (pkg.optionalDependencies === null ? 'null' : typeof pkg.optionalDependencies)}` };
}
if (pkg.optionalDependencies['sqlite-vec'] === undefined) {
return { valid: false, error: 'package.json 缺少 optionalDependencies.sqlite-vec 字段' };
}
if (typeof pkg.optionalDependencies['sqlite-vec'] !== 'string') {
return { valid: false, error: `sqlite-vec 字段类型错误: 期望 string, 实际 ${typeof pkg.optionalDependencies['sqlite-vec']}` };
}
if (pkg.optionalDependencies['sqlite-vec'] !== EXPECTED_SQLITE_VEC_VERSION) {
return { valid: false, error: `sqlite-vec 版本漂移: 期望 "${EXPECTED_SQLITE_VEC_VERSION}", 实际 "${pkg.optionalDependencies['sqlite-vec']}"` };
}
// engines.node 校验
if (pkg.engines === undefined) {
return { valid: false, error: 'package.json 缺少 engines 对象' };
}
if (typeof pkg.engines !== 'object' || pkg.engines === null || Array.isArray(pkg.engines)) {
return { valid: false, error: `engines 字段类型错误: 期望 object, 实际 ${Array.isArray(pkg.engines) ? 'array' : (pkg.engines === null ? 'null' : typeof pkg.engines)}` };
}
if (pkg.engines.node === undefined) {
return { valid: false, error: 'package.json 缺少 engines.node 定义' };
}
if (typeof pkg.engines.node !== 'string') {
return { valid: false, error: `engines.node 字段类型错误: 期望 string, 实际 ${typeof pkg.engines.node}` };
}
if (pkg.engines.node !== DEFAULT_NODE_ENGINES) {
return { valid: false, error: `engines.node 漂移: 期望 "${DEFAULT_NODE_ENGINES}", 实际 "${pkg.engines.node}"` };
}
// 如果所在目录存在 scripts 目录 (真实已解压安装包),则校验两脚本文件真实存在
if (pkgDir && fs.existsSync(path.join(pkgDir, 'scripts'))) {
const preinstallFile = path.join(pkgDir, EXPECTED_PREINSTALL_FILE);
const postinstallFile = path.join(pkgDir, EXPECTED_POSTINSTALL_FILE);
if (!fs.existsSync(preinstallFile)) {
return { valid: false, error: `白名单 preinstall 脚本文件不存在: ${preinstallFile}` };
}
if (!fs.existsSync(postinstallFile)) {
return { valid: false, error: `白名单 postinstall 脚本文件不存在: ${postinstallFile}` };
}
}
return {
valid: true,
vettedScripts: [EXPECTED_PREINSTALL_FILE, EXPECTED_POSTINSTALL_FILE]
};
} else {
// ── latest 或自定义版本: 不套用 2026.9.1 白名单 ──
if (pkg.name !== EXPECTED_NAME) {
return { valid: false, error: `包名不匹配: 期望 "${EXPECTED_NAME}", 实际 "${pkg.name}"` };
}
let binTarget = '';
if (typeof pkg.bin === 'string') {
binTarget = pkg.bin;
} else if (pkg.bin && typeof pkg.bin === 'object' && !Array.isArray(pkg.bin)) {
binTarget = pkg.bin.openclaw || pkg.bin[EXPECTED_NAME] || '';
}
if (!binTarget || !binTarget.endsWith('openclaw.mjs')) {
return { valid: false, error: `bin 入口漂移或无效: "${binTarget}"` };
}
if (typeof pkg.version !== 'string') {
return { valid: false, error: 'package.json 缺少或非法 version 字段' };
}
const parsedVer = parseStrictSemver(pkg.version);
if (!parsedVer) {
return { valid: false, error: `package.json 版本号非法或包含前后缀: "${pkg.version}"` };
}
if (targetVerTrimmed !== 'latest' && targetVerTrimmed !== pkg.version) {
return { valid: false, error: `实际版本 (${pkg.version}) 与请求版本 (${targetVerTrimmed}) 不一致` };
}
const engineSpec = (pkg.engines && pkg.engines.node) || '';
if (!engineSpec || typeof engineSpec !== 'string') {
return { valid: false, error: 'package.json 缺少 engines.node 定义' };
}
const currentNodeVer = (nodeVersion ? String(nodeVersion).replace(/^v/, '') : (process.versions.node || (process.version ? process.version.replace(/^v/, '') : '')));
const evalRes = evaluateNodeEngines(currentNodeVer, engineSpec);
if (!evalRes.validRange) {
return { valid: false, error: `engines.node 语法非法: ${evalRes.error}` };
}
if (!evalRes.satisfied) {
return { valid: false, error: `当前 Node.js 运行时 v${currentNodeVer} 不满足 engines.node 契约: "${engineSpec}"` };
}
const scripts = (pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)) ? pkg.scripts : {};
const INSTALL_LIFECYCLE_KEYS = new Set([
'install', 'preinstall', 'postinstall',
'prepare', 'prepack', 'postpack'
]);
const detectedLifecycles = [];
for (const k of Object.keys(scripts)) {
if (INSTALL_LIFECYCLE_KEYS.has(k)) {
detectedLifecycles.push(k);
}
}
if (detectedLifecycles.length > 0) {
return {
valid: false,
error: `拒绝执行未验证版本的 lifecycle 脚本 (${detectedLifecycles.join(', ')}). 自定义/latest 版本不得套用 2026.9.1 白名单 (fail-closed)`
};
}
return {
valid: true,
vettedScripts: [],
unverified: true,
warning: `当前安装版本为自定义/latest (${pkg.version || targetVerTrimmed}),未套用 2026.9.1 白名单,已阻断所有 lifecycle 脚本并标记为未验证安装`
};
}
}
/**
* 校验 Node.js 运行时
* @param {string} nodeBin
* @param {string} [spec]
* @returns {{ valid: boolean, version?: string, isUsageError?: boolean, error?: string }}
*/
function validateRuntime(nodeBin, spec) {
try {
const raw = execFileSync(nodeBin, ['--version'], { encoding: 'utf8' }).trim();
const ver = raw.replace(/^v/, '');
const engineSpec = spec || DEFAULT_NODE_ENGINES;
const evalRes = evaluateNodeEngines(ver, engineSpec);
if (!evalRes.validRange) {
return { valid: false, isUsageError: true, error: evalRes.error };
}
if (!evalRes.satisfied) {
return {
valid: false,
version: ver,
error: `Node.js v${ver} 不满足 engines.node 契约: "${engineSpec}"`
};
}
return { valid: true, version: ver };
} catch (e) {
return { valid: false, isUsageError: true, error: `无法执行 Node.js (${nodeBin}): ${e.message}` };
}
}
/**
* 获取经过审核的脚本列表并检查文件真实存在
* @param {string} pkgPath
* @param {string} targetVersion
* @returns {{ valid: boolean, isUsageError?: boolean, scripts?: string[], error?: string }}
*/
function getVettedScripts(pkgPath, targetVersion) {
const res = validateManifest(pkgPath, targetVersion);
if (!res.valid) {
return res;
}
if (!res.vettedScripts || res.vettedScripts.length === 0) {
return { valid: true, scripts: [] };
}
const pkgDir = path.dirname(path.resolve(pkgPath));
for (const s of res.vettedScripts) {
const fullPath = path.join(pkgDir, s);
if (!fs.existsSync(fullPath)) {
return { valid: false, error: `白名单脚本文件真实路径不存在: ${fullPath}` };
}
}
return { valid: true, scripts: res.vettedScripts };
}
// ── CLI 执行入口 ──
if (require.main === module) {
const args = process.argv.slice(2);
const command = args[0];
if (command === '--help' || command === '-h') {
console.log(`用法: node openclaw-package-contract.js <command> [args...]
命令:
validate-manifest <package.json-path> <target-version>
validate-node-version <version-string> [engine-spec]
validate-runtime <node-binary-path> [engine-spec]
get-vetted-scripts <package.json-path> <target-version>
`);
process.exit(0);
}
if (!command) {
console.error('错误: 请指定命令。使用 --help 查看帮助。');
process.exit(2);
}
if (command === 'validate-manifest') {
if (args.length !== 3) {
console.error('用法错误: validate-manifest 需要恰好 2 个参数: <package.json-path> <target-version>');
process.exit(2);
}
const pkgPath = args[1];
const targetVer = args[2];
const res = validateManifest(pkgPath, targetVer);
if (!res.valid) {
console.error(`[✗] 包清单校验失败: ${res.error}`);
process.exit(res.isUsageError ? 2 : 1);
}
if (res.warning) {
console.warn(`[!] ${res.warning}`);
}
console.log('[✓] 包清单契约校验通过');
process.exit(0);
}
if (command === 'validate-node-version') {
if (args.length < 2 || args.length > 3) {
console.error('用法错误: validate-node-version 需要 1 或 2 个参数: <version-string> [engine-spec]');
process.exit(2);
}
const ver = args[1];
const spec = args[2];
const evalRes = evaluateNodeEngines(ver, spec);
if (!evalRes.validRange) {
console.error(`[✗] 输入格式错误: ${evalRes.error}`);
process.exit(2);
}
if (!evalRes.satisfied) {
console.error(`[✗] Node.js 版本 v${ver} 不满足契约: "${spec || DEFAULT_NODE_ENGINES}"`);
process.exit(1);
}
console.log(`[✓] Node.js 版本 v${ver} 满足契约`);
process.exit(0);
}
if (command === 'validate-runtime') {
if (args.length < 2 || args.length > 3) {
console.error('用法错误: validate-runtime 需要 1 或 2 个参数: <node-binary-path> [engine-spec]');
process.exit(2);
}
const nodeBin = args[1];
const engineSpec = args[2];
const res = validateRuntime(nodeBin, engineSpec);
if (!res.valid) {
console.error(`[✗] 运行时校验失败: ${res.error}`);
process.exit(res.isUsageError ? 2 : 1);
}
console.log(`[✓] Node.js 运行时 v${res.version} 满足契约 (${engineSpec || DEFAULT_NODE_ENGINES})`);
process.exit(0);
}
if (command === 'get-vetted-scripts') {
if (args.length !== 3) {
console.error('用法错误: get-vetted-scripts 需要恰好 2 个参数: <package.json-path> <target-version>');
process.exit(2);
}
const pkgPath = args[1];
const targetVer = args[2];
const res = getVettedScripts(pkgPath, targetVer);
if (!res.valid) {
console.error(`[✗] ${res.error}`);
process.exit(res.isUsageError ? 2 : 1);
}
if (res.scripts && res.scripts.length > 0) {
for (const s of res.scripts) {
console.log(s);
}
}
process.exit(0);
}
console.error(`未知命令: ${command}`);
process.exit(2);
}
module.exports = {
EXPECTED_NAME,
EXPECTED_VERSION,
EXPECTED_BIN,
EXPECTED_PREINSTALL,
EXPECTED_POSTINSTALL,
EXPECTED_PREINSTALL_FILE,
EXPECTED_POSTINSTALL_FILE,
EXPECTED_SQLITE_VEC_VERSION,
DEFAULT_NODE_ENGINES,
parseStrictSemver,
compareSemver,
parseComparator,
evaluateNodeEngines,
satisfiesNodeEngines,
validateManifest,
validateRuntime,
getVettedScripts
};
@@ -37,7 +37,7 @@
<div id="topbar">
<span class="logo">🦞 一万AI分享 OpenClaw 配置管理</span>
<span id="status" class="status disconnected">● 连接中...</span>
<button class="btn" onclick="location.reload()" title="重新启动配置脚本">🔄 重启</button>
<button class="btn" onclick="restartSession()" title="重新启动配置脚本">🔄 重启</button>
</div>
<div id="loading">
@@ -52,7 +52,7 @@
<div id="reconnect-overlay">
<p style="font-size:16px;margin-bottom:4px">⚡ 连接已断开</p>
<p style="font-size:13px;color:#565f89">配置脚本已退出或连接中断</p>
<button onclick="location.reload()">🔄 重新启动</button>
<button onclick="restartSession()">🔄 重新启动</button>
</div>
<link rel="stylesheet" href="/lib/xterm.min.css">
@@ -128,11 +128,17 @@
// ── WebSocket 连接 ──
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
// 从 URL 参数或 hash 获取 PTY token 用于 WebSocket 认证
// 从 URL 参数或 sessionStorage 读取 PTY token 用于 WebSocket 认证
const urlParams = new URLSearchParams(location.search);
const ptyToken = urlParams.get('pty_token') || '';
let ptyToken = urlParams.get('pty_token') || '';
if (ptyToken) {
try { sessionStorage.setItem('openclaw_pty_token', ptyToken); } catch(e) {}
} else {
try { ptyToken = sessionStorage.getItem('openclaw_pty_token') || ''; } catch(e) {}
}
// 取到 token 后立刻从地址栏移除: 否则它会留在浏览器历史、
// 以及任何 Referer/截图里。内存中的 ptyToken 仍可用于建连。
// 以及任何 Referer/截图里。内存与 sessionStorage 中的 ptyToken 仍可用于建连。
if (ptyToken && window.history && history.replaceState) {
try {
const clean = new URL(location.href);
@@ -140,27 +146,118 @@
history.replaceState(null, '', clean.pathname + (clean.search || '') + clean.hash);
} catch (e) {}
}
const wsUrl = proto + '//' + location.host + '/ws' + (ptyToken ? '?token=' + encodeURIComponent(ptyToken) : '');
// 连接序号与单飞状态机
let currentConnectSeq = 0;
let wsUrl = proto + '//' + location.host + '/ws' + (ptyToken ? '?token=' + encodeURIComponent(ptyToken) : '');
// 展示用 URL: 隐去 token。真实 wsUrl 只用于建立连接,
// 不写入页面文本或 console —— 否则 token 会留在截图、录屏与浏览器历史里。
const wsUrlSafe = proto + '//' + location.host + '/ws' + (ptyToken ? '?token=<hidden>' : '');
let wsUrlSafe = proto + '//' + location.host + '/ws' + (ptyToken ? '?token=<hidden>' : '');
let ws = null;
let connected = false;
let isConnecting = false;
let retryCount = 0;
const MAX_RETRY = Infinity;
let retryTimer = null;
let wasEverConnected = false;
let pingTimer = null;
// 重启配置终端: 原地平滑重连,清屏并重连 WebSocket,无需重新加载整个页面
window.restartSession = function() {
console.log('[oc-config] restartSession invoked');
currentConnectSeq++;
if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
if (ws) {
try {
ws.onopen = null;
ws.onclose = null;
ws.onerror = null;
ws.onmessage = null;
ws.close();
} catch(e) {}
ws = null;
}
connected = false;
isConnecting = false;
retryCount = 0;
// 清空终端并显示重启中状态
try { term.clear(); } catch(e) {}
reconnectEl.classList.remove('show');
loadingEl.classList.remove('hidden');
loadingMsg.textContent = '正在重启终端会话...';
statusEl.textContent = '● 重启中...';
statusEl.className = 'status disconnected';
connect();
};
// 跨窗口通信: 向父页面 (LuCI) 请求缺失的 token,或接收父页面发送的 token
function requestParentToken() {
if (window.parent && window.parent !== window) {
try {
window.parent.postMessage({ type: 'openclaw_request_token' }, '*');
} catch(e) {}
}
}
window.addEventListener('message', function(ev) {
if (ev.data && ev.data.type === 'openclaw_token' && ev.data.pty_token) {
ptyToken = ev.data.pty_token;
try { sessionStorage.setItem('openclaw_pty_token', ptyToken); } catch(e) {}
wsUrl = proto + '//' + location.host + '/ws' + (ptyToken ? '?token=' + encodeURIComponent(ptyToken) : '');
wsUrlSafe = proto + '//' + location.host + '/ws' + (ptyToken ? '?token=<hidden>' : '');
if (!connected) {
currentConnectSeq++;
if (ws) {
try {
ws.onopen = null;
ws.onclose = null;
ws.onerror = null;
ws.onmessage = null;
ws.close();
} catch(e){}
ws = null;
}
if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
isConnecting = false;
connect();
}
}
});
console.log('[oc-config] protocol:', location.protocol);
console.log('[oc-config] host:', location.host, 'port:', location.port);
console.log('[oc-config] WebSocket URL:', wsUrlSafe);
loadingDebug.textContent = wsUrlSafe;
function connect() {
if (isConnecting || connected) return;
isConnecting = true;
const mySeq = ++currentConnectSeq;
if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
if (ws) { try { ws.close(); } catch(e){} ws = null; }
if (ws) {
try {
ws.onopen = null;
ws.onclose = null;
ws.onerror = null;
ws.onmessage = null;
ws.close();
} catch(e) {}
ws = null;
}
if (!ptyToken) {
try { ptyToken = sessionStorage.getItem('openclaw_pty_token') || ''; } catch(e) {}
}
if (!ptyToken) {
requestParentToken();
}
wsUrl = proto + '//' + location.host + '/ws' + (ptyToken ? '?token=' + encodeURIComponent(ptyToken) : '');
wsUrlSafe = proto + '//' + location.host + '/ws' + (ptyToken ? '?token=<hidden>' : '');
retryCount++;
console.log('[oc-config] Connecting to', wsUrlSafe, '(attempt', retryCount + ')');
@@ -169,42 +266,66 @@
: '正在连接终端...';
loadingDebug.textContent = wsUrlSafe;
// 先做一个 HTTP 预检确认服务器可达
fetch('/health').then(function(r) {
// 先用带超时的 HTTP /health 预检确认服务端就绪
var controller = null;
var signal = null;
if (typeof AbortController !== 'undefined') {
controller = new AbortController();
signal = controller.signal;
setTimeout(function() {
try { controller.abort(); } catch(e) {}
}, 2500);
}
fetch('/health', { signal: signal, cache: 'no-store' }).then(function(r) {
return r.json();
}).then(function(data) {
if (mySeq !== currentConnectSeq) return;
console.log('[oc-config] Health check OK:', JSON.stringify(data));
doWebSocket();
doWebSocket(mySeq);
}).catch(function(err) {
if (mySeq !== currentConnectSeq) return;
console.log('[oc-config] Health check failed:', err.message || err);
// 服务器不可达,稍后重试
isConnecting = false;
scheduleRetry();
});
}
function doWebSocket() {
function doWebSocket(mySeq) {
if (mySeq !== currentConnectSeq || connected) {
isConnecting = false;
return;
}
isConnecting = true;
try {
ws = new WebSocket(wsUrl);
} catch(e) {
console.error('[oc-config] WebSocket constructor error:', e);
isConnecting = false;
scheduleRetry();
return;
}
console.log('[oc-config] WebSocket created, readyState:', ws.readyState);
// 连接超时保护: 5秒没连上就重试
// 连接超时保护: 5秒没连上就关闭当前 socket,由 onclose 统一调度重试
var connectTimeout = setTimeout(function() {
if (mySeq !== currentConnectSeq) return;
if (ws && ws.readyState !== WebSocket.OPEN) {
console.log('[oc-config] Connection timeout, readyState:', ws.readyState);
try { ws.close(); } catch(e){}
scheduleRetry();
}
}, 5000);
ws.onopen = function() {
if (mySeq !== currentConnectSeq) {
try { ws.close(); } catch(e){}
return;
}
clearTimeout(connectTimeout);
connected = true;
isConnecting = false;
var isReconnect = wasEverConnected;
wasEverConnected = true;
retryCount = 0;
@@ -248,45 +369,58 @@
ws.onclose = function(ev) {
clearTimeout(connectTimeout);
if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
if (mySeq !== currentConnectSeq) return;
connected = false;
isConnecting = false;
ws = null;
statusEl.textContent = '● 已断开';
statusEl.className = 'status disconnected';
console.log('[oc-config] WebSocket closed, code:', ev.code, 'reason:', ev.reason);
if (wasEverConnected) {
// 自动重连,不显示断连覆盖层
console.log('[oc-config] Auto-reconnecting in 2s...');
statusEl.textContent = '● 重连中...';
retryCount = 0;
setTimeout(function() {
connect();
}, 2000);
} else {
// 从未连接成功过,自动重试
scheduleRetry();
}
scheduleRetry();
};
ws.onerror = function(ev) {
clearTimeout(connectTimeout);
if (mySeq !== currentConnectSeq) return;
connected = false;
isConnecting = false;
console.error('[oc-config] WebSocket error, will retry');
// 不要在这里做UI更新, onclose 会紧跟着触发
};
}
function scheduleRetry() {
if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
isConnecting = false;
if (!ptyToken) {
requestParentToken();
}
if (retryCount >= MAX_RETRY) {
loadingMsg.textContent = '连接失败,请检查服务状态';
loadingDebug.textContent = '已重试 ' + MAX_RETRY + ' 次。请刷新页面重试。';
loadingEl.classList.remove('hidden');
statusEl.textContent = '● 连接失败';
statusEl.className = 'status disconnected';
return;
}
// 逐步增加重试间隔: 1s, 1s, 2s, 2s, 3s, 3s, ...
// 逐步增加重试间隔: 1s, 1s, 2s, 2s, 3s, 3s, ... 最大 5s
var delay = Math.min(Math.floor(retryCount / 2) + 1, 5) * 1000;
loadingMsg.textContent = '等待服务就绪... ' + Math.ceil(delay/1000) + '秒后重试';
loadingEl.classList.remove('hidden');
if (wasEverConnected) {
statusEl.textContent = '● 重连中...';
loadingMsg.textContent = '连接中断,等待服务就绪... ' + Math.ceil(delay / 1000) + '秒后重试';
} else {
loadingMsg.textContent = (!ptyToken)
? '等待服务就绪或获取认证令牌... ' + Math.ceil(delay / 1000) + '秒后重试'
: '等待服务就绪... ' + Math.ceil(delay / 1000) + '秒后重试';
}
loadingDebug.textContent = wsUrlSafe;
console.log('[oc-config] Retry in', delay, 'ms');
retryTimer = setTimeout(connect, delay);
retryTimer = setTimeout(function() {
retryTimer = null;
connect();
}, delay);
}
term.onData(function(data) {
@@ -305,18 +305,24 @@ function handleRequest(req, res) {
const fullPath = path.join(UI_DIR, fp);
if (!fullPath.startsWith(UI_DIR)) { res.writeHead(403); return res.end('Forbidden'); }
// 如果请求 URL 带有 pty_token,写入 SameSite=Lax Cookie 以支持后续刷新/重启恢复
const queryPtyToken = url.searchParams.get('pty_token');
const cookieHeader = queryPtyToken
? { 'Set-Cookie': `oc_pty_token=${encodeURIComponent(queryPtyToken)}; Path=/; SameSite=Lax; HttpOnly` }
: {};
fs.readFile(fullPath, (err, data) => {
if (err) {
if (fp !== '/index.html') {
fs.readFile(path.join(UI_DIR, 'index.html'), (e2, d2) => {
if (e2) { res.writeHead(404); res.end('Not Found'); }
else { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', ...IFRAME_HEADERS }); res.end(d2); }
else { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', ...IFRAME_HEADERS, ...cookieHeader }); res.end(d2); }
});
} else { res.writeHead(404); res.end('Not Found'); }
return;
}
const ext = path.extname(fullPath);
res.writeHead(200, { 'Content-Type': getMimeType(ext), 'Cache-Control': ext === '.html' ? 'no-cache' : 'max-age=3600', ...IFRAME_HEADERS });
res.writeHead(200, { 'Content-Type': getMimeType(ext), 'Cache-Control': ext === '.html' ? 'no-cache' : 'max-age=3600', ...IFRAME_HEADERS, ...cookieHeader });
res.end(data);
});
}
@@ -326,12 +332,18 @@ function handleUpgrade(req, socket, head) {
console.log(`[oc-config] WS upgrade: ${req.url} remote=${socket.remoteAddress}:${socket.remotePort}`);
if (req.url !== '/ws' && !req.url.startsWith('/ws?')) { socket.destroy(); return; }
// 认证: 验证查询参数中的 token
// 认证: 验证查询参数或 Cookie 中的 token
// 每次连接时实时读取 UCI token (安装/升级可能重新生成 token)
const currentToken = loadAuthToken() || AUTH_TOKEN;
const urlObj = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
if (currentToken) {
const clientToken = urlObj.searchParams.get('token') || '';
let clientToken = urlObj.searchParams.get('token') || urlObj.searchParams.get('pty_token') || '';
if (!clientToken && req.headers.cookie) {
const match = req.headers.cookie.match(/(?:^|;\s*)oc_pty_token=([^;]+)/);
if (match) {
try { clientToken = decodeURIComponent(match[1]); } catch(e) { clientToken = match[1]; }
}
}
if (clientToken !== currentToken) {
console.log(`[oc-config] WS auth failed from ${socket.remoteAddress}`);
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
+18 -104
View File
@@ -4,13 +4,13 @@
# 在 Alpine ARM64 Docker 容器内运行
#
# 环境变量:
# NODE_VER (目标版本号)
# BUILD_MODE (apk|cross) - apk: 使用 Alpine apk, cross: 从官方 glibc 版本交叉编译
# NODE_VER (目标版本号,如 22.23.2)
# BUILD_MODE (apk) - 仅支持 apk 模式,严格产出请求版本
# /output (输出目录)
#
# 打包策略:
# 1. apk 模式: 使用 Alpine apk 安装 nodejs,版本受限于 Alpine 仓库
# 2. cross 模式: 从 Node.js 官方下载 glibc 版本,转换为 musl
# 打包契约:
# 1. apk 模式: 使用 Alpine apk 安装 nodejs,必须精确产出 NODE_VER,禁止版本不符改名
# 2. 禁止 cross 模式使用 glibc 替换 ELF interpreter 冒充 musl
# 使用 patchelf 修改 node 二进制的 ELF interpreter 和 rpath,
# 使其直接使用打包的 musl 链接器和共享库,无需 LD_LIBRARY_PATH。
# 这样 process.execPath 返回正确的 node 路径,子进程 fork 也能正常工作。
@@ -42,12 +42,13 @@ build_apk() {
ACTUAL_VER=$(node --version | sed 's/^v//')
echo "Alpine Node.js version: v${ACTUAL_VER} (requested: v${NODE_VER})"
# 使用实际版本号作为文件名 (Alpine apk 的 nodejs 版本可能与请求版本不同)
# 必须产出请求的精确版本;禁止接受 Alpine apk 实际版本与 NODE_VER 不一致后改名发布
if [ "$ACTUAL_VER" != "$NODE_VER" ]; then
echo "WARNING: Actual version (${ACTUAL_VER}) differs from requested (${NODE_VER})"
echo " Using actual version for package name"
echo "ERROR: Actual Alpine Node.js version (${ACTUAL_VER}) does not match requested version (${NODE_VER})" >&2
echo " Refusing to rename or publish mismatched version" >&2
exit 1
fi
PKG_NAME="node-v${ACTUAL_VER}-linux-arm64-musl"
PKG_NAME="node-v${NODE_VER}-linux-arm64-musl"
PKG_DIR="/tmp/${PKG_NAME}"
mkdir -p "${PKG_DIR}/bin" "${PKG_DIR}/lib/node_modules" "${PKG_DIR}/include/node"
@@ -93,102 +94,11 @@ build_apk() {
echo "PKG_DIR=${PKG_DIR}" >> /tmp/build_env
}
# ── cross 模式: 从 Node.js 官方 glibc 版本交叉编译 ──
# ── 禁止使用 glibc 二进制仅替换 ELF interpreter 冒充 musl ──
build_cross() {
echo ""
echo "=== Building with cross-compilation mode ==="
apk add --no-cache xz patchelf curl wget ca-certificates
# 下载 Node.js 官方 ARM64 glibc 版本
NODE_TARBALL="node-v${NODE_VER}-linux-arm64.tar.xz"
NODE_URL="https://nodejs.org/dist/v${NODE_VER}/${NODE_TARBALL}"
echo "=== Downloading Node.js v${NODE_VER} ARM64 glibc ==="
cd /tmp
if ! curl -fSL -o "$NODE_TARBALL" "$NODE_URL"; then
echo "ERROR: Failed to download Node.js from $NODE_URL"
exit 1
fi
# 解压
echo "=== Extracting Node.js ==="
rm -rf "node-v${NODE_VER}-linux-arm64" 2>/dev/null || true
tar xf "$NODE_TARBALL"
SRC_DIR="node-v${NODE_VER}-linux-arm64"
# 创建输出目录
PKG_NAME="node-v${NODE_VER}-linux-arm64-musl"
PKG_DIR="/tmp/${PKG_NAME}"
rm -rf "$PKG_DIR" 2>/dev/null || true
mkdir -p "${PKG_DIR}/bin" "${PKG_DIR}/lib" "${PKG_DIR}/share/icu"
# 复制 node 二进制
echo "=== Copying Node.js binary ==="
cp "${SRC_DIR}/bin/node" "${PKG_DIR}/bin/node"
chmod +x "${PKG_DIR}/bin/node"
# 复制 npm
if [ -d "${SRC_DIR}/lib/node_modules/npm" ]; then
mkdir -p "${PKG_DIR}/lib/node_modules"
cp -r "${SRC_DIR}/lib/node_modules/npm" "${PKG_DIR}/lib/node_modules/"
fi
# 复制 ICU 数据
ICU_DAT=$(find "${SRC_DIR}" -name "icudt*.dat" 2>/dev/null | head -1)
if [ -n "$ICU_DAT" ] && [ -f "$ICU_DAT" ]; then
cp "$ICU_DAT" "${PKG_DIR}/share/icu/"
echo " + ICU data: $(basename "$ICU_DAT")"
fi
# ── 关键步骤: musl 库收集 ──
echo "=== Converting to musl libc ==="
# 复制 musl 动态链接器
if [ -f /lib/ld-musl-aarch64.so.1 ]; then
cp -L /lib/ld-musl-aarch64.so.1 "${PKG_DIR}/lib/"
echo " + ld-musl-aarch64.so.1"
else
echo "ERROR: musl dynamic linker not found"
exit 1
fi
# 收集 musl 版本的依赖库
# Node.js 官方 ARM64 版本依赖: libcrypto, libssl, libz, libstdc++, libgcc_s
# 这些库需要从 Alpine (musl) 版本获取
echo "=== Collecting musl libraries ==="
LIB_DIR="${PKG_DIR}/lib"
# 安装必要的库包
apk add --no-cache libcrypto3 libssl3 zlib libstdc++ libgcc
# 复制库文件
for lib in libcrypto.so.3 libssl.so.3 libz.so.1 libstdc++.so.6 libgcc_s.so.1; do
for libpath in /usr/lib/$lib /lib/$lib; do
if [ -f "$libpath" ]; then
cp -L "$libpath" "$LIB_DIR/" 2>/dev/null || true
echo " + $lib"
break
fi
done
done
# 收集所有 musl 库的依赖
for lib in "$LIB_DIR"/*.so*; do
[ -f "$lib" ] || continue
ldd "$lib" 2>/dev/null | while read -r line; do
lib_path=$(echo "$line" | grep -oE '/[^ ]+\.so[^ ]*' | head -1)
if [ -n "$lib_path" ] && [ -f "$lib_path" ]; then
lib_name=$(basename "$lib_path")
[ -f "$LIB_DIR/$lib_name" ] || cp -L "$lib_path" "$LIB_DIR/" 2>/dev/null || true
fi
done
done
echo "Libraries collected: $(ls "$LIB_DIR"/*.so* 2>/dev/null | wc -l) files"
# 返回包名供后续使用
echo "PKG_NAME=${PKG_NAME}" >> /tmp/build_env
echo "PKG_DIR=${PKG_DIR}" >> /tmp/build_env
echo "ERROR: cross mode (glibc binary with patched ELF interpreter) is forbidden" >&2
echo " Cannot impersonate musl using glibc binaries" >&2
exit 1
}
# ── 公共步骤: patchelf 和打包 ──
@@ -237,6 +147,7 @@ NPXWRAPPER
"${prefix}/bin/node" --version
"${prefix}/bin/node" -e "console.log('execPath:', process.execPath)"
"${prefix}/bin/node" -e "console.log(process.arch, process.platform, process.versions.modules)"
"${prefix}/bin/node" -e "if(process.arch!=='arm64'||process.platform!=='linux')throw new Error('Mismatched architecture: '+process.arch)"
NODE_ICU_DATA="${prefix}/share/icu" "${prefix}/bin/npm" --version 2>/dev/null || echo "npm needs ICU data"
rm -rf "$prefix"
}
@@ -248,6 +159,9 @@ NPXWRAPPER
echo "=== Creating tarball ==="
cd /tmp
tar cJf "/output/${PKG_NAME}.tar.xz" "${PKG_NAME}"
if command -v sha256sum >/dev/null 2>&1; then
(cd /output && sha256sum "${PKG_NAME}.tar.xz" > "${PKG_NAME}.tar.xz.sha256")
fi
ls -lh "/output/${PKG_NAME}.tar.xz"
echo "=== Done: ${PKG_NAME}.tar.xz ==="
}
@@ -0,0 +1,50 @@
import fs from 'fs';
import path from 'path';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
// 1. 验证受控环境 (npm_config_ignore_scripts=true)
if (process.env.npm_config_ignore_scripts !== 'true') {
console.error('[postinstall] 错误: npm_config_ignore_scripts 必须为 "true", 实际为: ' + process.env.npm_config_ignore_scripts);
process.exit(98);
}
// 2. 验证 cwd 为安装包根目录 (必须包含 package.json)
const rawCwd = process.cwd();
const cwd = rawCwd.replace(/\\/g, '/');
const localPkgPath = path.resolve(rawCwd, 'package.json');
if (!fs.existsSync(localPkgPath)) {
console.error('[postinstall] 错误: cwd 必须为安装包根目录,当前未找到 package.json: ' + cwd);
process.exit(97);
}
// 3. 验证依赖可解析 (通过 Node require.resolve 机制确认当前上下文或依赖树有效)
// 可通过环境变量 OC_TEST_DEP 指定必须可解析的依赖;未指定时解析 package.json 自身以验证解析上下文就绪
try {
const depToResolve = process.env.OC_TEST_DEP || './package.json';
require.resolve(depToResolve, { paths: [cwd] });
} catch (err) {
console.error('[postinstall] 依赖解析失败: ' + err.message);
process.exit(96);
}
// 4. 写入可计数副作用 (文件追加写入)
const sideEffectFile = process.env.OC_SIDE_EFFECT_FILE;
if (sideEffectFile) {
const record = JSON.stringify({
script: 'postinstall',
cwd: cwd,
npm_config_ignore_scripts: process.env.npm_config_ignore_scripts,
timestamp: Date.now()
}) + '\n';
fs.appendFileSync(sideEffectFile, record, 'utf8');
}
// 5. 失败注入支持 (用于验证失败时回滚并保持现有安装不变)
if (process.env.OC_FAIL_POSTINSTALL === '1') {
console.error('[postinstall] 模拟 postinstall 失败 (OC_FAIL_POSTINSTALL=1)');
process.exit(43);
}
process.exit(0);
@@ -0,0 +1,50 @@
import fs from 'fs';
import path from 'path';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
// 1. 验证受控环境 (npm_config_ignore_scripts=true)
if (process.env.npm_config_ignore_scripts !== 'true') {
console.error('[preinstall] 错误: npm_config_ignore_scripts 必须为 "true", 实际为: ' + process.env.npm_config_ignore_scripts);
process.exit(98);
}
// 2. 验证 cwd 为安装包根目录 (必须包含 package.json)
const rawCwd = process.cwd();
const cwd = rawCwd.replace(/\\/g, '/');
const localPkgPath = path.resolve(rawCwd, 'package.json');
if (!fs.existsSync(localPkgPath)) {
console.error('[preinstall] 错误: cwd 必须为安装包根目录,当前未找到 package.json: ' + cwd);
process.exit(97);
}
// 3. 验证依赖可解析 (通过 Node require.resolve 机制确认当前上下文或依赖树有效)
// 可通过环境变量 OC_TEST_DEP 指定必须可解析的依赖;未指定时解析 package.json 自身以验证解析上下文就绪
try {
const depToResolve = process.env.OC_TEST_DEP || './package.json';
require.resolve(depToResolve, { paths: [cwd] });
} catch (err) {
console.error('[preinstall] 依赖解析失败: ' + err.message);
process.exit(96);
}
// 4. 写入可计数副作用 (文件追加写入)
const sideEffectFile = process.env.OC_SIDE_EFFECT_FILE;
if (sideEffectFile) {
const record = JSON.stringify({
script: 'preinstall',
cwd: cwd,
npm_config_ignore_scripts: process.env.npm_config_ignore_scripts,
timestamp: Date.now()
}) + '\n';
fs.appendFileSync(sideEffectFile, record, 'utf8');
}
// 5. 失败注入支持 (用于验证失败时回滚并保持现有安装不变)
if (process.env.OC_FAIL_PREINSTALL === '1') {
console.error('[preinstall] 模拟 preinstall 失败 (OC_FAIL_PREINSTALL=1)');
process.exit(42);
}
process.exit(0);
@@ -0,0 +1,17 @@
{
"name": "openclaw",
"version": "2026.9.1",
"bin": {
"openclaw": "openclaw.mjs"
},
"scripts": {
"preinstall": "node scripts/preinstall-package-manager-warning.mjs",
"postinstall": "node scripts/postinstall-bundled-plugins.mjs"
},
"optionalDependencies": {
"sqlite-vec": "0.1.9"
},
"engines": {
"node": ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0"
}
}
+1 -2
View File
@@ -1,5 +1,5 @@
# OpenClaw 官方 schema 关键字段类型基线
# 来源: openclaw config schema (OpenClaw 2026.7.1-2, 提取日期 2026-08-26)
# 来源: openclaw config schema (OpenClaw 2026.9.1, 提取日期 2026-09-05)
# 格式: <key><TAB><type><TAB><enum(逗号分隔, - 表示无枚举)>
#
# 说明:
@@ -16,7 +16,6 @@ gateway.customBindHost string -
gateway.auth.mode string none,token,password,trusted-proxy
gateway.auth.token string -
gateway.controlUi.allowInsecureAuth boolean -
gateway.controlUi.dangerouslyDisableDeviceAuth boolean -
gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback boolean -
logging.level string silent,fatal,error,warn,info,debug,trace
acp.dispatch.enabled boolean -
1 # OpenClaw 官方 schema 关键字段类型基线
2 # 来源: openclaw config schema (OpenClaw 2026.7.1-2, 提取日期 2026-08-26) # 来源: openclaw config schema (OpenClaw 2026.9.1, 提取日期 2026-09-05)
3 # 格式: <key><TAB><type><TAB><enum(逗号分隔, - 表示无枚举)>
4 #
5 # 说明:
16 gateway.auth.mode
17 gateway.auth.token
18 gateway.controlUi.allowInsecureAuth
gateway.controlUi.dangerouslyDisableDeviceAuth
19 gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback
20 logging.level
21 acp.dispatch.enabled
@@ -0,0 +1,89 @@
#!/bin/sh
# OpenClaw 浏览器设备配对语义与安全契约测试。
#
# 背景 (OpenClaw 2026.9.1+):
# 上游安全策略要求首次连接 Control UI 时进行设备配对审批:
# - 列出请求: openclaw devices list --json
# - 批准请求: openclaw devices approve <requestId>
#
# 核心规范:
# 1. 终端交互菜单 (oc-config-interactive.js) 与传统 Shell 菜单 (oc-config.sh)
# 必须提供设备配对管理与一键批准功能。
# 2. Web 控制台 (console.htm) 与终端配置 (advanced.htm) 必须提供一键批准设备配对能力,
# 且必须显式标注安全风险提示。
# 3. LuCI 控制器 (openclaw.lua) 的批准端点必须使用 post() 注册以防 CSRF 越权。
# 4. 所有受影响文件必须保持 LF 换行。
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
JS_CONFIG="$REPO_ROOT/root/usr/share/openclaw/oc-config-interactive.js"
SH_CONFIG="$REPO_ROOT/root/usr/share/openclaw/oc-config.sh"
CONTROLLER="$REPO_ROOT/luasrc/controller/openclaw.lua"
CONSOLE="$REPO_ROOT/luasrc/view/openclaw/console.htm"
ADVANCED="$REPO_ROOT/luasrc/view/openclaw/advanced.htm"
fail() {
echo "FAIL: $1" >&2
exit 1
}
for f in "$JS_CONFIG" "$SH_CONFIG" "$CONTROLLER" "$CONSOLE" "$ADVANCED"; do
[ -f "$f" ] || fail "missing $f"
done
# ── 1. 终端菜单 CLI 调用契约 ──
grep -Fq "'devices', 'list'" "$JS_CONFIG" || fail "interactive config must use devices list"
grep -Fq "'devices', 'approve'" "$JS_CONFIG" || fail "interactive config must use devices approve"
grep -Fq "oc_cmd devices list" "$SH_CONFIG" || fail "shell config must use oc_cmd devices list"
grep -Fq "oc_cmd devices approve" "$SH_CONFIG" || fail "shell config must use oc_cmd devices approve"
# ── 2. 菜单项注册与可达性 ──
grep -Fq "value: 'devices'" "$JS_CONFIG" || fail "interactive config must expose devices menu item"
grep -Fq "manageDevicePairing" "$JS_CONFIG" || fail "interactive config must implement manageDevicePairing"
grep -Fq "devices_pairing_menu" "$SH_CONFIG" || fail "shell config must implement devices_pairing_menu"
# ── 3. LuCI 控制器端点契约 (必须遵循 CSRF 安全规范) ──
grep -Fq 'entry({"admin", "services", "openclaw", "devices_list"}, call("action_devices_list"), nil).leaf = true' "$CONTROLLER" \
|| fail "controller must register devices_list with call()"
grep -Fq 'entry({"admin", "services", "openclaw", "devices_approve"}, post("action_devices_approve"), nil).leaf = true' "$CONTROLLER" \
|| fail "controller must register devices_approve with post()"
grep -Fq 'devices list --json' "$CONTROLLER" || fail "controller must invoke devices list --json"
grep -Fq 'devices approve' "$CONTROLLER" || fail "controller must invoke devices approve"
grep -Fq 'fix_openclaw_state_permissions' "$CONTROLLER" || fail "controller must fix permissions after approval"
# ── 4. Web 端安全风险提示契约 ──
# 必须在 4 处界面中显著标注风险说明
for f in "$JS_CONFIG" "$SH_CONFIG" "$CONSOLE" "$ADVANCED"; do
grep -Fq "风险提示" "$f" || fail "$(basename "$f") missing risk warning heading"
grep -Fq "完全控制权限" "$f" || fail "$(basename "$f") missing full control permission warning"
done
# ── 5. 前端 POST + CSRF 调用契约 ──
for f in "$CONSOLE" "$ADVANCED"; do
grep -Fq "devices_approve" "$f" || fail "$(basename "$f") missing devices_approve call"
grep -Fq "devices_list" "$f" || fail "$(basename "$f") missing devices_list call"
grep -Fq "ocCsrfToken" "$f" || fail "$(basename "$f") must send CSRF token"
done
# ── 6. XSS 防护契约 ──
for f in "$CONSOLE" "$ADVANCED"; do
grep -Fq "function escapeHtml" "$f" || fail "$(basename "$f") must define escapeHtml"
grep -Fq "escapeHtml(item.remoteIp" "$f" || fail "$(basename "$f") must escape remoteIp"
grep -Fq "escapeHtml(item.clientId" "$f" || fail "$(basename "$f") must escape clientId"
grep -Fq "escapeHtml(item.requestId" "$f" || fail "$(basename "$f") must escape requestId"
done
# ── 7. 控制器退出码与参数完整性契约 ──
grep -Fq "__EXIT" "$CONTROLLER" || fail "controller must check command exit code"
grep -Fq "缺少 request_id 或 all 参数" "$CONTROLLER" || fail "controller must reject empty approve requests"
# ── 8. 换行符约束 (LF only) ──
cr=$(printf '\r')
for f in "$JS_CONFIG" "$SH_CONFIG" "$CONTROLLER" "$CONSOLE" "$ADVANCED"; do
if LC_ALL=C grep -q "$cr" "$f"; then
fail "$(basename "$f") must use LF line endings"
fi
done
echo "ok"
@@ -11,11 +11,10 @@ fail() {
}
grep -Fq "var url = 'http://' + host + ':' + gwPort + '/'" "$CONSOLE_VIEW" || fail "console view should force HTTP gateway URL"
grep -Fq '请点击上方「新窗口打开」访问控制台。' "$CONSOLE_VIEW" || fail "console view should direct users to a new window"
if grep -Fq "document.createElement('iframe')" "$CONSOLE_VIEW"; then
fail "console view should not embed the OpenClaw UI in an iframe"
fi
grep -Fq "document.createElement('iframe')" "$CONSOLE_VIEW" || fail "console view should embed the OpenClaw UI in an iframe"
grep -Fq "oc-console-iframe" "$CONSOLE_VIEW" || fail "console view should define oc-console-iframe"
grep -Fq "allowfullscreen" "$CONSOLE_VIEW" || fail "console view should support fullscreen"
grep -Fq "microphone" "$CONSOLE_VIEW" || fail "console view should allow media permissions"
if grep -Fq 'window.location.protocol' "$CONSOLE_VIEW"; then
fail "console view should not reuse the LuCI page protocol for the gateway URL"
@@ -35,7 +35,8 @@ done
# 必须用 post() 注册的端点: 会改状态或返回凭据
MUST_POST="service_ctl uninstall plugin_upgrade backup get_token
wechat_install wechat_login wechat_logout wechat_uninstall wechat_upgrade_plugin"
wechat_install wechat_login wechat_logout wechat_uninstall wechat_upgrade_plugin
devices_approve"
for ep in $MUST_POST; do
line=$(grep -F "\"openclaw\", \"$ep\"}" "$CONTROLLER" | head -1)
@@ -84,7 +85,8 @@ done
# 只读端点保持 call() 即可 —— 确认没有被误改成 post 而导致前端 GET 调用失效
READ_ONLY="status_api setup_log check_update check_system plugin_upgrade_log
wechat_status wechat_install_log wechat_login_status wechat_check_upgrade"
wechat_status wechat_install_log wechat_login_status wechat_check_upgrade
devices_list"
for ep in $READ_ONLY; do
line=$(grep -F "\"openclaw\", \"$ep\"}" "$CONTROLLER" | head -1)
[ -n "$line" ] || fail "endpoint $ep not registered"
@@ -69,6 +69,16 @@ for id in $STALE_IDS; do
fi
done
# ── 菜单项模型简化契约:顶级模型提供商菜单不得罗列过时具体模型名称 ──
for name in "GPT-5.2" "GPT-4.1" "GPT-5 mini" "Sonnet 4" "Opus 4" "Gemini 2.5" "Gemini 3" "Grok-4" "HY T1" "ERNIE-4.0" "GLM-5.1"; do
if grep -F -- "$name" "$SH_CONFIG" | grep -v '^[[:space:]]*#' | grep -q .; then
fail "oc-config.sh model menu still lists stale model name: $name"
fi
if grep -F -- "$name" "$JS_CONFIG" | grep -v '^[[:space:]]*//' | grep -v '^[[:space:]]*\*' | grep -q .; then
fail "oc-config-interactive.js model menu still lists stale model name: $name"
fi
done
# ── 预设文件自身校验 (需要 node) ──
NODE_BIN=""
for cand in node nodejs /opt/openclaw/node/bin/node; do
@@ -20,9 +20,16 @@ fi
grep -Fq 'verify_prefix /opt/openclaw/node' "$BUILD_SCRIPT" || fail "build script should verify default install path"
grep -Fq 'verify_prefix /tmp/custom-openclaw-root/openclaw/node' "$BUILD_SCRIPT" || fail "build script should verify custom install path"
grep -Fq 'NODE_VER="22.23.0"' "$WORKFLOW" || fail "workflow should build current musl-compatible Node.js"
grep -Fq 'NODE_VER="22.23.2"' "$WORKFLOW" || fail "workflow should build current musl-compatible Node.js"
grep -Fq 'BUILD_MODE=apk' "$WORKFLOW" || fail "workflow should use apk mode for ARM64 musl package"
grep -Fq 'PKG_TYPE=lts' "$WORKFLOW" || fail "workflow should use Alpine LTS Node.js package"
grep -Fq 'node-v22.23.2-linux-arm64-musl.tar.xz' "$WORKFLOW" || fail "workflow should name ARM64 musl artifact node-v22.23.2-linux-arm64-musl.tar.xz"
# 校验构建脚本行为契约:
# 1. 实际版本与请求版本不一致时必须报错退出,不得改名发布
grep -Fq 'Actual Alpine Node.js version (${ACTUAL_VER}) does not match requested version (${NODE_VER})' "$BUILD_SCRIPT" || fail "build script must reject version mismatch in apk mode"
# 2. 禁止使用 glibc 二进制替换 ELF interpreter 冒充 musl
grep -Fq 'cross mode (glibc binary with patched ELF interpreter) is forbidden' "$BUILD_SCRIPT" || fail "build script must forbid fake glibc cross mode"
grep -Fq 'oc_node_version_ge "$current_ver" "$node_ver"' "$ENV_SCRIPT" || fail "installer should require installed Node.js to satisfy target version"
if grep -Fq 'v1_tarball' "$ENV_SCRIPT"; then
@@ -6,11 +6,19 @@ fail() {
exit 1
}
grep -q "OC_TESTED_VERSION=\"2026.7.1-2\"" root/usr/bin/openclaw-env || fail "tested OpenClaw version not pinned"
grep -q "NODE_VERSION_V2=\"22.23.0\"" root/usr/bin/openclaw-env || fail "default Node.js version not pinned"
grep -q "OC_TESTED_VERSION=\"2026.9.1\"" root/usr/bin/openclaw-env || fail "tested OpenClaw version not pinned"
grep -q "NODE_VERSION_V2=\"22.23.2\"" root/usr/bin/openclaw-env || fail "default Node.js version not pinned"
grep -q "OC_NODE_MIN_VERSION=\"\${OC_NODE_MIN_VERSION:-22.22.3}\"" root/usr/bin/openclaw-env || fail "minimum Node.js version not pinned"
grep -q "oc_assert_node_min_version" root/usr/bin/openclaw-env || fail "Node.js minimum version check missing"
grep -q 'oc_node_version_ge "$from_pkg" "$required"' root/usr/bin/openclaw-env || fail "package Node.js requirement must not lower static minimum"
grep -q "validate-runtime" root/usr/bin/openclaw-env || fail "openclaw-env must validate runtime against full engines.node contract"
grep -q "openclaw-package-contract.js" root/usr/bin/openclaw-env || fail "openclaw-env must reference package contract validator"
grep -q "validate_openclaw_manifest" root/usr/bin/openclaw-env || fail "openclaw-env must define static manifest validation helper"
grep -q "execute_openclaw_lifecycle" root/usr/bin/openclaw-env || fail "openclaw-env must gate lifecycle script execution"
grep -q "candidate_prefix" root/usr/bin/openclaw-env || fail "openclaw-env must install to candidate prefix before replacing OC_GLOBAL"
grep -q -- "--ignore-scripts" root/usr/bin/openclaw-env || fail "npm install must enforce --ignore-scripts"
grep -q -- "--omit=optional" root/usr/bin/openclaw-env || fail "npm install must enforce --omit=optional"
grep -q "npm_config_ignore_scripts=true" root/usr/bin/openclaw-env || fail "postinstall must inherit npm_config_ignore_scripts=true"
grep -q "install_openclaw_cli_wrapper" root/usr/bin/openclaw-env || fail "OpenClaw CLI wrapper must set runtime env"
grep -q 'export NODE_ICU_DATA="${NODE_BASE}/share/icu"' root/usr/bin/openclaw-env || fail "OpenClaw CLI wrapper must export NODE_ICU_DATA"
grep -q 'rm -f "$OC_GLOBAL/bin/openclaw"' root/usr/bin/openclaw-env || fail "OpenClaw CLI wrapper must unlink npm symlink before writing"
@@ -64,7 +72,7 @@ grep -q "archived-extensions" root/etc/init.d/openclaw || fail "legacy weixin ex
grep -q "find_wechat_plugin_dir" luasrc/controller/openclaw.lua || fail "wechat npm plugin detection missing"
grep -q "wechat_openclaw_plugin_install_cmd" luasrc/controller/openclaw.lua || fail "wechat install must use OpenClaw plugin installer"
grep -q "delete d.plugins.installs\\['openclaw-weixin'\\]" luasrc/controller/openclaw.lua || fail "wechat config must remove deprecated authored install records"
grep -q "plugins install --force --pin @tencent-weixin/openclaw-weixin@2.4.6" luasrc/controller/openclaw.lua || fail "wechat install must write the official plugin index"
grep -q "plugins install --force --pin @tencent-weixin/openclaw-weixin@2.4.8" luasrc/controller/openclaw.lua || fail "wechat install must write the official plugin index"
grep -q "channels\\['openclaw-weixin'\\].enabled = true" luasrc/controller/openclaw.lua || fail "wechat channel enable registration missing"
grep -q "OpenClaw 插件索引注册完成" luasrc/controller/openclaw.lua || fail "wechat install log must confirm index registration"
grep -q "plugins enable openclaw-weixin" luasrc/controller/openclaw.lua || fail "wechat lifecycle must persist the official plugin enable entry"
@@ -117,12 +125,17 @@ grep -q "procd_set_param term_timeout 2" root/etc/init.d/openclaw || fail "procd
grep -q "procd_set_param respawn 5 1 -1" root/etc/init.d/openclaw || fail "gateway respawn must avoid procd crash throttling during manual restart_gateway"
grep -q "openclaw-restart-gateway.lock" root/etc/init.d/openclaw || fail "restart_gateway must guard concurrent restart requests"
grep -q "kill -USR1" root/etc/init.d/openclaw || fail "restart_gateway must prefer OpenClaw SIGUSR1 in-process restart"
grep -q "SIGUSR1 请求 Gateway 快速重载" root/etc/init.d/openclaw || fail "restart_gateway SIGUSR1 fast reload log missing"
grep -q "patch_webchat_session_conflict" root/etc/init.d/openclaw || fail "WebChat session conflict patch helper missing"
grep -q 'ctx.Provider === "webchat"' root/etc/init.d/openclaw || fail "WebChat patch must skip terminal transcript rollover for webchat"
grep -q "let sessionEntryInput = params.sessionEntry" root/etc/init.d/openclaw || fail "WebChat patch must merge same-session stale guarded writes"
grep -q "WebChat 二次发送会话初始化冲突" root/etc/init.d/openclaw || fail "WebChat patch must run before gateway start"
grep -q 'openclaw.main.clear_jiti_cache_on_start' root/etc/init.d/openclaw || fail "JITI cleanup must be guarded by UCI flag"
grep -q 'OPENCLAW_SUPERVISOR_MODE="external"' root/etc/init.d/openclaw || fail "gateway must receive external supervisor mode"
grep -q 'OPENCLAW_SERVICE_REPAIR_POLICY="external"' root/etc/init.d/openclaw || fail "gateway must receive external repair policy"
if grep -q "patch_webchat_session_conflict" root/etc/init.d/openclaw; then
fail "obsolete WebChat patch helper must be removed"
fi
if grep -q "patch_iframe_headers" root/etc/init.d/openclaw; then
fail "obsolete iframe patch helper must be removed"
fi
if grep -q "dangerouslyDisableDeviceAuth" root/etc/init.d/openclaw root/usr/share/openclaw/oc-config.sh root/usr/share/openclaw/oc-config-interactive.js root/usr/bin/openclaw-env tests/fixtures/openclaw-schema-types.tsv; then
fail "deprecated dangerouslyDisableDeviceAuth must be completely removed"
fi
grep -q "openclaw-node-compile-cache" root/etc/init.d/openclaw || fail "Node compile cache must be enabled for faster warm restarts"
grep -q 'NODE_COMPILE_CACHE="$node_compile_cache"' root/etc/init.d/openclaw || fail "gateway and pty must receive Node compile cache env"
grep -q "通常 20~40 秒" root/etc/init.d/openclaw luasrc/view/openclaw/status.htm luasrc/view/openclaw/console.htm || fail "startup hint must match measured OpenWrt timing"
@@ -170,9 +183,7 @@ grep -q "anthropic-messages" root/usr/share/openclaw/oc-config.sh || fail "shell
grep -q "var url = 'http://'" luasrc/view/openclaw/console.htm || fail "console must force HTTP gateway URL"
grep -q "新窗口打开" luasrc/view/openclaw/console.htm || fail "console must expose a new-window entry"
if grep -q "document.createElement('iframe')" luasrc/view/openclaw/console.htm; then
fail "console must not embed OpenClaw in an iframe"
fi
grep -q "document.createElement('iframe')" luasrc/view/openclaw/console.htm || fail "console must embed OpenClaw in an iframe"
grep -q "root/usr/libexec" scripts/build_ipk.sh || fail "ipk script must package shell helpers"
grep -q "root/usr/libexec" scripts/build_run.sh || fail "run script must package shell helpers"
@@ -184,4 +195,53 @@ grep -q "chown -R root:root" scripts/build_ipk.sh || fail ".ipk postinst must re
grep -q "先解压到临时目录并确认完整,再替换 NODE_BASE" root/usr/bin/openclaw-env || fail "Node install must not delete existing runtime before extraction succeeds"
grep -q "OC_SETUP_FRESH_ROOT" root/usr/bin/openclaw-env || fail "setup cleanup must preserve existing runtime roots"
grep -q "openclaw-upgrade-state.sh" Makefile || fail "Makefile must install openclaw-upgrade-state.sh"
grep -q "openclaw-upgrade-state.sh" root/usr/bin/openclaw-env || fail "openclaw-env must reference openclaw-upgrade-state.sh"
grep -q "openclaw-upgrade-state.sh" root/usr/share/openclaw/oc-config.sh || fail "oc-config must reference openclaw-upgrade-state.sh"
grep -q "backup create --verify --no-include-workspace" root/usr/libexec/openclaw-upgrade-state.sh || fail "helper must enforce full backup create arguments"
grep -q "backup verify" root/usr/libexec/openclaw-upgrade-state.sh || fail "helper must execute backup verify"
grep -q "config validate --json" root/usr/libexec/openclaw-upgrade-state.sh || fail "helper must execute config validate --json"
grep -q "gateway health --json" root/usr/libexec/openclaw-upgrade-state.sh || fail "helper must execute gateway health --json"
grep -q "RESULT:" root/usr/bin/openclaw-env || fail "openclaw-env must report RESULT field"
grep -q "BACKUP:" root/usr/bin/openclaw-env || fail "openclaw-env must report BACKUP field"
grep -q "DOCTOR:" root/usr/bin/openclaw-env || fail "openclaw-env must report DOCTOR field"
grep -q "GATEWAY:" root/usr/bin/openclaw-env || fail "openclaw-env must report GATEWAY field"
grep -q "ROLLBACK:" root/usr/bin/openclaw-env || fail "openclaw-env must report ROLLBACK field"
grep -q "VERIFIED:" root/usr/bin/openclaw-env || fail "openclaw-env must report VERIFIED field"
# ── 真机实测缺陷回归断言 ──
if grep -E 'start-stop-daemon[[:space:]]+.*-d[[:space:]]+' root/etc/init.d/openclaw luasrc/controller/openclaw.lua; then
fail "BusyBox start-stop-daemon does not support -d flag"
fi
grep -q 'chmod 755 "$candidate_prefix"' root/usr/bin/openclaw-env || fail "candidate_prefix must be chmod 755 before atomic switch"
grep -q 'chmod -R 755 "$archive_dir"' root/usr/libexec/openclaw-permissions.sh || fail "archived-extensions must have 755 permission"
grep -q 'chown -R root:root "$archive_dir"' root/usr/libexec/openclaw-permissions.sh || fail "archived-extensions must be owned by root:root"
grep -q 'chown -R openclaw:openclaw "$ext_dir"' root/usr/libexec/openclaw-permissions.sh || fail "extensions must be owned by openclaw:openclaw"
grep -q 'chmod -R 755 "$ext_dir"' root/usr/libexec/openclaw-permissions.sh || fail "extensions must have 755 permissions"
if grep -q 'chown -R root:root "$ext_dir"' root/usr/libexec/openclaw-permissions.sh; then
fail "extensions must NOT be owned by root:root"
fi
if grep -q 'chown -R root:root.*qqbot_ext_dir' root/usr/share/openclaw/oc-config.sh; then
fail "qqbot extension must not be chowned to root:root"
fi
grep -q 'chown -R openclaw:openclaw "$qqbot_ext_dir"' root/usr/share/openclaw/oc-config.sh || fail "qqbot extension must be owned by openclaw:openclaw"
grep -q 'oc_load_paths' root/usr/libexec/openclaw-upgrade-state.sh || fail "openclaw-upgrade-state.sh must support oc_load_paths fallback"
grep -q '/opt/openclaw/global/bin/openclaw' root/usr/libexec/openclaw-upgrade-state.sh || fail "openclaw-upgrade-state.sh must fallback to /opt/openclaw/global/bin/openclaw"
grep -Fq '"ok"[[:space:]]*:[[:space:]]*true' root/usr/libexec/openclaw-upgrade-state.sh || fail "gateway-verify must support ok:true regex"
grep -q 'cd.*data_dir.*&&' root/usr/libexec/openclaw-upgrade-state.sh || fail "oc_run_as_user must cd into data_dir"
# ── 深度加固:cwd 隔离与目录权限防逃逸 ──
grep -q 'cd.*OC_DATA.*&&.*doctor --fix' root/etc/init.d/openclaw || fail "init.d _run_doctor_fix must cd into OC_DATA"
grep -q 'cd.*OC_DATA.*&&.*config validate' root/etc/init.d/openclaw || fail "init.d _run_config_migration must cd into OC_DATA"
grep -q 'cd.*OC_DATA.*&&.*doctor' root/usr/share/openclaw/oc-config.sh || fail "oc-config doctor must cd into OC_DATA"
if grep -q '! -path "\*/extensions\*"' root/usr/share/openclaw/oc-config-interactive.js; then
fail "interactive config must not exclude extensions from state permission fix"
fi
grep -q 'chmod -R a+rX "\$candidate_prefix"' root/usr/bin/openclaw-env || fail "candidate_prefix must have chmod -R a+rX"
grep -q 'chown -R openclaw:openclaw "\${state_dir}/extensions"' root/usr/libexec/openclaw-permissions.sh || fail "prepare-workdirs must recursively chown extensions to openclaw"
grep -q 'chown -R openclaw:openclaw.*extensions' luasrc/controller/openclaw.lua || fail "openclaw.lua fallbacks must recursively chown extensions to openclaw"
echo "ok"
+196 -6
View File
@@ -1,19 +1,209 @@
#!/bin/sh
set -eu
. ./root/usr/libexec/openclaw-node.sh
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
ENV_SCRIPT="$REPO_ROOT/root/usr/bin/openclaw-env"
. "$REPO_ROOT/root/usr/libexec/openclaw-node.sh"
fail() {
echo "FAIL: $1" >&2
exit 1
}
[ "$(oc_normalize_node_version v22.23.0)" = "22.23.0" ] || fail "normalize v"
oc_node_version_ge 22.19.0 22.19.0 || fail "exact version"
oc_node_version_ge 22.19.1 22.19.0 || fail "patch version"
oc_node_version_ge 22.23.0 22.19.0 || fail "supported LTS version"
if oc_node_version_ge 22.18.1 22.19.0; then
# 1. 基础版本归一化与版本比较契约
[ "$(oc_normalize_node_version v22.23.2)" = "22.23.2" ] || fail "normalize v"
oc_node_version_ge 22.22.3 22.22.3 || fail "exact version"
oc_node_version_ge 22.22.4 22.22.3 || fail "patch version"
oc_node_version_ge 22.23.2 22.22.3 || fail "supported LTS version"
if oc_node_version_ge 22.22.2 22.22.3; then
fail "older patch accepted"
fi
if oc_node_version_ge 22.19.0 22.22.3; then
fail "older minor accepted"
fi
# 2. 隔离 Mock 环境: 验证全部候选镜像源失败时的失败闭合行为
TMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'oc-node-test-XXXXXX' 2>/dev/null || echo "/tmp/oc-node-test-$$")
mkdir -p "$TMP_DIR"
cleanup() {
rm -rf "$TMP_DIR" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
MOCK_BIN="$TMP_DIR/bin"
mkdir -p "$MOCK_BIN"
CURL_LOG="$TMP_DIR/curl.log"
cat > "$MOCK_BIN/curl" <<EOF
#!/bin/sh
echo "\$@" >> "$CURL_LOG"
exit 22
EOF
chmod +x "$MOCK_BIN/curl"
WGET_LOG="$TMP_DIR/wget.log"
cat > "$MOCK_BIN/wget" <<EOF
#!/bin/sh
echo "\$@" >> "$WGET_LOG"
exit 1
EOF
chmod +x "$MOCK_BIN/wget"
cat > "$MOCK_BIN/id" <<EOF
#!/bin/sh
exit 0
EOF
chmod +x "$MOCK_BIN/id"
NPM_LOG="$TMP_DIR/npm.log"
cat > "$MOCK_BIN/npm" <<EOF
#!/bin/sh
echo "\$@" >> "$NPM_LOG"
exit 0
EOF
chmod +x "$MOCK_BIN/npm"
MOCK_ROOT="$TMP_DIR/opt"
MOCK_OC="$MOCK_ROOT/openclaw"
MOCK_GLOBAL="$MOCK_OC/global"
MOCK_NODE="$MOCK_OC/node"
mkdir -p "$MOCK_GLOBAL/bin" "$MOCK_GLOBAL/lib/node_modules/openclaw" "$MOCK_NODE/bin"
echo "EXISTING_MARKER_GLOBAL" > "$MOCK_GLOBAL/marker.txt"
echo "EXISTING_WRAPPER" > "$MOCK_GLOBAL/bin/openclaw"
chmod +x "$MOCK_GLOBAL/bin/openclaw"
echo "EXISTING_ENTRY" > "$MOCK_GLOBAL/lib/node_modules/openclaw/openclaw.mjs"
echo '{"name":"openclaw","version":"2026.9.1"}' > "$MOCK_GLOBAL/lib/node_modules/openclaw/package.json"
# (2.1) 提取 download_node 逻辑并在隔离环境直接调用: 候选镜像全部失败必须退出非零
HARNESS_DOWNLOADER="$TMP_DIR/test-download.sh"
cat > "$HARNESS_DOWNLOADER" <<EOF
#!/bin/sh
set -e
export PATH="$MOCK_BIN:\$PATH"
OC_BASE_PATH="$MOCK_ROOT"
OC_INSTALL_PATH="$MOCK_OC"
NODE_BASE="$MOCK_NODE"
NODE_BIN="\$NODE_BASE/bin/node"
OC_GLOBAL="$MOCK_GLOBAL"
OC_DATA="$MOCK_OC/data"
OC_PACKAGE_CONTRACT="$REPO_ROOT/root/usr/share/openclaw/openclaw-package-contract.js"
OC_NODE_MIN_VERSION="22.22.3"
log_info() { :; }
log_warn() { :; }
log_error() { :; }
ensure_mkdir() { mkdir -p "\$1"; }
detect_libc() { echo "musl"; }
detect_arch() { echo "linux-x64"; }
NODE_MUSL_MIRROR="https://unofficial-builds.nodejs.org/download/release"
NODE_MIRROR_CN="https://npmmirror.com/mirrors/node"
NODE_SELF_HOST="https://github.com/10000ge10000/luci-app-openclaw/releases/download/node-bins"
find_oc_entry() { echo ""; }
assert_node_runtime() { :; }
EOF
awk '/^oc_sum256\(\)/,/^download_node\(\)/' "$ENV_SCRIPT" | sed '$d' >> "$HARNESS_DOWNLOADER"
awk '/^download_node\(\)/,/^install_pnpm\(\)/' "$ENV_SCRIPT" | sed '$d' >> "$HARNESS_DOWNLOADER"
cat >> "$HARNESS_DOWNLOADER" <<EOF
download_node "22.23.2"
EOF
set +e
sh "$HARNESS_DOWNLOADER" >/dev/null 2>&1
RC_EXHAUSTED=$?
set -e
[ "$RC_EXHAUSTED" -ne 0 ] || fail "downloader must fail when all candidate mirrors fail"
# (2.2) 验证仅有 wget 可用且全部候选失败时同样必须退出非零
MOCK_BIN_WGET="$TMP_DIR/bin-wget"
mkdir -p "$MOCK_BIN_WGET"
cp "$MOCK_BIN/wget" "$MOCK_BIN_WGET/wget"
HARNESS_WGET="$TMP_DIR/test-download-wget.sh"
cat > "$HARNESS_WGET" <<EOF
#!/bin/sh
set -e
export PATH="$MOCK_BIN_WGET:\$PATH"
OC_BASE_PATH="$MOCK_ROOT"
OC_INSTALL_PATH="$MOCK_OC"
NODE_BASE="$MOCK_NODE"
NODE_BIN="\$NODE_BASE/bin/node"
OC_GLOBAL="$MOCK_GLOBAL"
OC_DATA="$MOCK_OC/data"
OC_PACKAGE_CONTRACT="$REPO_ROOT/root/usr/share/openclaw/openclaw-package-contract.js"
OC_NODE_MIN_VERSION="22.22.3"
log_info() { :; }
log_warn() { :; }
log_error() { :; }
ensure_mkdir() { mkdir -p "\$1"; }
detect_libc() { echo "musl"; }
detect_arch() { echo "linux-x64"; }
NODE_MUSL_MIRROR="https://unofficial-builds.nodejs.org/download/release"
NODE_MIRROR_CN="https://npmmirror.com/mirrors/node"
NODE_SELF_HOST="https://github.com/10000ge10000/luci-app-openclaw/releases/download/node-bins"
find_oc_entry() { echo ""; }
assert_node_runtime() { :; }
EOF
awk '/^oc_sum256\(\)/,/^download_node\(\)/' "$ENV_SCRIPT" | sed '$d' >> "$HARNESS_WGET"
awk '/^download_node\(\)/,/^install_pnpm\(\)/' "$ENV_SCRIPT" | sed '$d' >> "$HARNESS_WGET"
cat >> "$HARNESS_WGET" <<EOF
download_node "22.23.2"
EOF
set +e
sh "$HARNESS_WGET" >/dev/null 2>&1
RC_WGET_EXHAUSTED=$?
set -e
[ "$RC_WGET_EXHAUSTED" -ne 0 ] || fail "downloader must fail when all candidate mirrors fail (wget only)"
# (2.3) 验证 openclaw-env setup 调用链:Node 下载失败立即退出,不产生伪成功,且 OC_GLOBAL 零副作用
rm -f "$MOCK_NODE/bin/node" 2>/dev/null || true
set +e
OUT_SETUP=$(
PATH="$MOCK_BIN:$PATH" \
OC_INSTALL_PATH="$MOCK_ROOT" \
NODE_VERSION="22.23.2" \
sh "$ENV_SCRIPT" setup 2>&1
)
RC_SETUP=$?
set -e
if [ "$RC_SETUP" -eq 0 ]; then
echo "UNEXPECTED SUCCESS of openclaw-env setup (RC=0)! Output was:" >&2
echo "$OUT_SETUP" >&2
fail "openclaw-env setup must fail non-zero when candidate mirrors fail"
fi
# 检查后续安装未发生且原有安装保持完整
[ ! -f "$NPM_LOG" ] || fail "npm install/pack was called after node download failure"
[ -f "$MOCK_GLOBAL/marker.txt" ] || fail "existing OC_GLOBAL marker was deleted on download failure"
[ "$(cat "$MOCK_GLOBAL/marker.txt")" = "EXISTING_MARKER_GLOBAL" ] || fail "existing OC_GLOBAL marker corrupted"
[ -f "$MOCK_GLOBAL/bin/openclaw" ] || fail "existing OC_GLOBAL wrapper was lost"
[ "$(cat "$MOCK_GLOBAL/bin/openclaw")" = "EXISTING_WRAPPER" ] || fail "existing OC_GLOBAL wrapper corrupted"
[ -f "$MOCK_GLOBAL/lib/node_modules/openclaw/openclaw.mjs" ] || fail "existing OC_GLOBAL entry was lost"
# (2.4) 验证 openclaw-env node 调用链:旧 Node 版本需要升级但镜像全挂时必须退出非零且保留旧安装
cat > "$MOCK_NODE/bin/node" <<'EOF'
#!/bin/sh
case "$1" in
--version|-v) echo "v22.15.1" ;;
*) exit 0 ;;
esac
EOF
chmod +x "$MOCK_NODE/bin/node"
echo "EXISTING_NODE_MARKER" > "$MOCK_NODE/marker.txt"
set +e
PATH="$MOCK_BIN:$PATH" \
OC_INSTALL_PATH="$MOCK_ROOT" \
NODE_VERSION="22.23.2" \
sh "$ENV_SCRIPT" node >/dev/null 2>&1
RC_NODE=$?
set -e
[ "$RC_NODE" -ne 0 ] || fail "openclaw-env node must fail non-zero when candidate mirrors fail"
# 检查旧 node 标记及旧 OC_GLOBAL 均未被破坏
[ -f "$MOCK_NODE/marker.txt" ] || fail "existing NODE_BASE marker was lost on node download failure"
[ -f "$MOCK_GLOBAL/marker.txt" ] || fail "existing OC_GLOBAL marker was deleted on node download failure"
[ "$(cat "$MOCK_GLOBAL/marker.txt")" = "EXISTING_MARKER_GLOBAL" ] || fail "existing OC_GLOBAL was modified on node download failure"
echo "ok"
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,304 @@
#!/bin/sh
# ============================================================================
# test_openclaw_upgrade_state.sh
# 验证 openclaw-upgrade-state.sh 事务生命周期、SQLite 前向预检、回滚安全及
# oc-config / openclaw-env 在升级期间的状态防写穿契约。
# ============================================================================
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
STATE_SCRIPT="$REPO_ROOT/root/usr/libexec/openclaw-upgrade-state.sh"
OC_CONFIG_SCRIPT="$REPO_ROOT/root/usr/share/openclaw/oc-config.sh"
ENV_SCRIPT="$REPO_ROOT/root/usr/bin/openclaw-env"
fail() {
echo "FAIL: $*" >&2
exit 1
}
[ -f "$STATE_SCRIPT" ] || fail "state helper script not found: $STATE_SCRIPT"
TMP_DIR=$(mktemp -d /tmp/oc-state-test-XXXXXX 2>/dev/null || mktemp -d 2>/dev/null || echo "/tmp/oc-state-test-$$")
chmod 755 "$TMP_DIR" 2>/dev/null || true
trap 'rm -rf "$TMP_DIR"' EXIT INT TERM
# ── 1. 脚本调用与 Source 契约 ──
# 1.1 验证 source openclaw-upgrade-state.sh 不会意外 exit (之前因未定义 OPENCLAW_PERMISSIONS_SOURCED 导致 fatal exit 2)
(
export OPENCLAW_STATE_DIR="$TMP_DIR/mock-state"
# shellcheck source=/dev/null
. "$STATE_SCRIPT"
) || fail "sourcing $STATE_SCRIPT should not exit non-zero"
# 1.2 验证未知参数退出码为 2
set +e
sh "$STATE_SCRIPT" invalid-subcommand >/dev/null 2>&1
RC_INVALID=$?
set -e
[ "$RC_INVALID" -eq 2 ] || fail "invalid subcommand must exit with code 2 (actual: $RC_INVALID)"
# ── 2. status 子命令返回合法 JSON ──
STATUS_JSON=$(sh "$STATE_SCRIPT" status "$TMP_DIR/mock-state" 2>/dev/null) || fail "status command failed"
echo "$STATUS_JSON" | grep -q '"phase"' || fail "status JSON missing phase"
echo "$STATUS_JSON" | grep -q '"backup_verified"' || fail "status JSON missing backup_verified"
echo "$STATUS_JSON" | grep -q '"migration_started"' || fail "status JSON missing migration_started"
echo "$STATUS_JSON" | grep -q '"rollback_mode"' || fail "status JSON missing rollback_mode"
echo "$STATUS_JSON" | grep -q '"error_code"' || fail "status JSON missing error_code"
# ── 3. init-transaction 与 set-phase 事务状态流转 ──
MOCK_ROOT="$TMP_DIR/inst"
MOCK_STATE="$MOCK_ROOT/data/.openclaw"
mkdir -p "$MOCK_STATE"
# 3.1 init-transaction
sh "$STATE_SCRIPT" init-transaction "2026.9.1" "$MOCK_STATE" >/dev/null 2>&1 || fail "init-transaction failed"
STATUS_FILE="$MOCK_ROOT/.luci-openclaw-upgrade/status.json"
[ -f "$STATUS_FILE" ] || fail "status.json not created at expected meta location: $STATUS_FILE"
grep -q '"phase"[[:space:]]*:[[:space:]]*"init"' "$STATUS_FILE" || fail "phase must be init"
grep -q '"target_version"[[:space:]]*:[[:space:]]*"2026.9.1"' "$STATUS_FILE" || fail "target_version must be 2026.9.1"
grep -q '"backup_verified"[[:space:]]*:[[:space:]]*false' "$STATUS_FILE" || fail "backup_verified must be false initially"
# 3.2 set-phase backing_up
sh "$STATE_SCRIPT" set-phase backing_up "$MOCK_STATE" 0 "" "" >/dev/null 2>&1 || fail "set-phase backing_up failed"
grep -q '"phase"[[:space:]]*:[[:space:]]*"backing_up"' "$STATUS_FILE" || fail "phase must be backing_up"
# 3.3 set-phase backup_verified
sh "$STATE_SCRIPT" set-phase backup_verified "$MOCK_STATE" 0 "" "$MOCK_ROOT/backup.tar.gz" >/dev/null 2>&1 || fail "set-phase backup_verified failed"
grep -q '"phase"[[:space:]]*:[[:space:]]*"backup_verified"' "$STATUS_FILE" || fail "phase must be backup_verified"
grep -q '"backup_verified"[[:space:]]*:[[:space:]]*true' "$STATUS_FILE" || fail "backup_verified must be true"
grep -q '"backup_file"[[:space:]]*:[[:space:]]*".*backup.tar.gz"' "$STATUS_FILE" || fail "backup_file must be recorded"
# 3.4 set-phase migrating
sh "$STATE_SCRIPT" set-phase migrating "$MOCK_STATE" 0 "" "$MOCK_ROOT/backup.tar.gz" >/dev/null 2>&1 || fail "set-phase migrating failed"
grep -q '"phase"[[:space:]]*:[[:space:]]*"migrating"' "$STATUS_FILE" || fail "phase must be migrating"
grep -q '"migration_started"[[:space:]]*:[[:space:]]*true' "$STATUS_FILE" || fail "migration_started must be true"
# 3.5 set-phase recovery
sh "$STATE_SCRIPT" set-phase recovery "$MOCK_STATE" 42 "Migration failed" "$MOCK_ROOT/backup.tar.gz" >/dev/null 2>&1 || fail "set-phase recovery failed"
grep -q '"phase"[[:space:]]*:[[:space:]]*"recovery"' "$STATUS_FILE" || fail "phase must be recovery"
grep -q '"rollback_mode"[[:space:]]*:[[:space:]]*"manual_recovery"' "$STATUS_FILE" || fail "rollback_mode must be manual_recovery"
grep -q '"error_code"[[:space:]]*:[[:space:]]*42' "$STATUS_FILE" || fail "error_code 42 must be recorded"
# 3.6 set-phase rolled_back
sh "$STATE_SCRIPT" set-phase rolled_back "$MOCK_STATE" 0 "" "$MOCK_ROOT/backup.tar.gz" >/dev/null 2>&1 || fail "set-phase rolled_back failed"
grep -q '"phase"[[:space:]]*:[[:space:]]*"rolled_back"' "$STATUS_FILE" || fail "phase must be rolled_back"
grep -q '"rollback_mode"[[:space:]]*:[[:space:]]*"rolled_back"' "$STATUS_FILE" || fail "rollback_mode must be rolled_back"
# 3.7 set-phase completed 时显式清空旧的 error_message 与 error_code
sh "$STATE_SCRIPT" set-phase recovery "$MOCK_STATE" 99 "Prior error occurred" "$MOCK_ROOT/backup.tar.gz" >/dev/null 2>&1 || fail "set-phase recovery failed"
grep -q '"error_message"[[:space:]]*:[[:space:]]*"Prior error occurred"' "$STATUS_FILE" || fail "error_message must be recorded"
sh "$STATE_SCRIPT" set-phase completed "$MOCK_STATE" 0 "" "$MOCK_ROOT/backup.tar.gz" >/dev/null 2>&1 || fail "set-phase completed failed"
grep -q '"phase"[[:space:]]*:[[:space:]]*"completed"' "$STATUS_FILE" || fail "phase must be completed"
grep -q '"error_code"[[:space:]]*:[[:space:]]*0' "$STATUS_FILE" || fail "error_code must be reset to 0 on completed"
grep -q '"error_message"[[:space:]]*:[[:space:]]*""' "$STATUS_FILE" || fail "error_message must be cleared on completed"
# ── 4. 纯 Shell 降级状态写入 (Node.js 缺失时) ──
(
PATH="/usr/bin:/bin"
STATUS_FILE_FALLBACK="$TMP_DIR/fallback-meta/.luci-openclaw-upgrade/status.json"
mkdir -p "$TMP_DIR/fallback-meta/data/.openclaw"
# 屏蔽 node
sh -c "
export PATH=\"/nonexistent:\$PATH\"
sh \"$STATE_SCRIPT\" set-phase migrating \"$TMP_DIR/fallback-meta/data/.openclaw\" 0 \"\" \"\" >/dev/null 2>&1
"
[ -f "$STATUS_FILE_FALLBACK" ] || fail "fallback status file not created"
grep -q '"phase"[[:space:]]*:[[:space:]]*"migrating"' "$STATUS_FILE_FALLBACK" || fail "fallback must write phase"
grep -q '"migration_started"[[:space:]]*:[[:space:]]*true' "$STATUS_FILE_FALLBACK" || fail "fallback must write migration_started: true"
# 验证降级路径在 completed 时同样清空 error_message 并保持已记录的 target_version
sh -c "
export PATH=\"/nonexistent:\$PATH\"
sh \"$STATE_SCRIPT\" init-transaction \"2026.9.1\" \"$TMP_DIR/fallback-meta/data/.openclaw\" >/dev/null 2>&1
sh \"$STATE_SCRIPT\" set-phase recovery \"$TMP_DIR/fallback-meta/data/.openclaw\" 55 \"Shell fallback error\" \"\" >/dev/null 2>&1
sh \"$STATE_SCRIPT\" set-phase completed \"$TMP_DIR/fallback-meta/data/.openclaw\" 0 \"\" \"\" >/dev/null 2>&1
"
grep -q '"phase"[[:space:]]*:[[:space:]]*"completed"' "$STATUS_FILE_FALLBACK" || fail "fallback must write phase completed"
grep -q '"target_version"[[:space:]]*:[[:space:]]*"2026.9.1"' "$STATUS_FILE_FALLBACK" || fail "fallback must preserve target_version"
grep -q '"error_code"[[:space:]]*:[[:space:]]*0' "$STATUS_FILE_FALLBACK" || fail "fallback error_code must be 0 on completed"
grep -q '"error_message"[[:space:]]*:[[:space:]]*""' "$STATUS_FILE_FALLBACK" || fail "fallback error_message must be empty on completed"
)
# ── 5. database-preflight 数据库前向预检 ──
# 5.1 合法 SQLite 文件 (>=16 字节且含 'SQLite format 3\0')
VALID_DB_DIR="$TMP_DIR/valid-db"
mkdir -p "$VALID_DB_DIR"
# 生成 100 字节标准 SQLite3 文件头
printf 'SQLite format 3\000\004\000\001\001\000@ \000\000\000\001\000\000\000\000' > "$VALID_DB_DIR/agents.db"
# 附加合法 helper 文件 (0 字节 wal / shm 不应报损坏)
touch "$VALID_DB_DIR/agents.db-wal"
touch "$VALID_DB_DIR/agents.db-shm"
sh "$STATE_SCRIPT" database-preflight "$VALID_DB_DIR" >/dev/null 2>&1 || fail "database-preflight should pass on valid SQLite"
# 5.2 损坏 SQLite 文件: 长度 < 16 字节
CORRUPT_DB_DIR="$TMP_DIR/corrupt-db"
mkdir -p "$CORRUPT_DB_DIR"
printf 'bad' > "$CORRUPT_DB_DIR/corrupt.sqlite"
set +e
sh "$STATE_SCRIPT" database-preflight "$CORRUPT_DB_DIR" >/dev/null 2>&1
RC_CORRUPT_SIZE=$?
set -e
[ "$RC_CORRUPT_SIZE" -ne 0 ] || fail "database-preflight must fail on truncated SQLite file"
# 5.3 损坏 SQLite 文件: 长度 >= 16 字节但 Header 错误
printf 'NOT_SQLITE_HEADER_1234567890' > "$CORRUPT_DB_DIR/badheader.sqlite"
set +e
sh "$STATE_SCRIPT" database-preflight "$CORRUPT_DB_DIR" >/dev/null 2>&1
RC_CORRUPT_HEADER=$?
set -e
[ "$RC_CORRUPT_HEADER" -ne 0 ] || fail "database-preflight must fail on invalid SQLite header"
# 5.4 tar.gz 归档输入预检
ARCHIVE_VALID="$TMP_DIR/valid-archive.tar.gz"
tar -czf "$ARCHIVE_VALID" -C "$VALID_DB_DIR" agents.db agents.db-wal
sh "$STATE_SCRIPT" database-preflight "$ARCHIVE_VALID" >/dev/null 2>&1 || fail "database-preflight should pass on tar.gz containing valid SQLite"
ARCHIVE_CORRUPT="$TMP_DIR/corrupt-archive.tar.gz"
tar -czf "$ARCHIVE_CORRUPT" -C "$CORRUPT_DB_DIR" badheader.sqlite
set +e
sh "$STATE_SCRIPT" database-preflight "$ARCHIVE_CORRUPT" >/dev/null 2>&1
RC_ARCHIVE_CORRUPT=$?
set -e
[ "$RC_ARCHIVE_CORRUPT" -ne 0 ] || fail "database-preflight must fail on tar.gz containing corrupt SQLite"
# ── 6. rollback-explicit 边界与破坏性防范 ──
# 6.1 备份文件缺失时,拒绝执行回滚,保持状态为 recovery
set +e
sh "$STATE_SCRIPT" rollback-explicit "$TMP_DIR/nonexistent.tar.gz" "$MOCK_STATE" >/dev/null 2>&1
RC_RB_NONEXIST=$?
set -e
[ "$RC_RB_NONEXIST" -ne 0 ] || fail "rollback-explicit must fail if backup archive does not exist"
# 6.2 备份文件损坏(如缺少 openclaw.json),拒绝执行回滚,保持服务不被杀
CORRUPT_BK="$TMP_DIR/broken-backup.tar.gz"
tar -czf "$CORRUPT_BK" -C "$VALID_DB_DIR" agents.db
set +e
sh "$STATE_SCRIPT" rollback-explicit "$CORRUPT_BK" "$MOCK_STATE" >/dev/null 2>&1
RC_RB_CORRUPT=$?
set -e
[ "$RC_RB_CORRUPT" -ne 0 ] || fail "rollback-explicit must abort when backup archive verification fails"
grep -q '"phase"[[:space:]]*:[[:space:]]*"recovery"' "$STATUS_FILE" || fail "phase must remain recovery when rollback aborted"
# ── 7. oc-config.sh json_set 升级事务防写穿契约 ──
OC_CFG_DIR="$TMP_DIR/cfg-test"
mkdir -p "$OC_CFG_DIR/data/.openclaw"
mkdir -p "$OC_CFG_DIR/.luci-openclaw-upgrade"
CFG_FILE="$OC_CFG_DIR/data/.openclaw/openclaw.json"
echo '{"gateway":{"port":18789}}' > "$CFG_FILE"
# 7.1 当 phase 为 migrating 时,json_set 暂停写入 openclaw.json
cat > "$OC_CFG_DIR/.luci-openclaw-upgrade/status.json" <<'EOF'
{
"phase": "migrating",
"target_version": "2026.9.1"
}
EOF
OPENCLAW_STATE_DIR="$OC_CFG_DIR/data/.openclaw" \
CONFIG_FILE="$CFG_FILE" \
OC_DATA="$OC_CFG_DIR/data" \
sh "$OC_CONFIG_SCRIPT" --set "gateway.port" "28888" >/dev/null 2>&1 || true
# 验证 openclaw.json 未被篡改 (端口仍为 18789)
grep -q '18789' "$CFG_FILE" || fail "json_set must NOT mutate openclaw.json when phase is migrating"
! grep -q '28888' "$CFG_FILE" || fail "json_set unexpectedly updated openclaw.json during migrating phase"
# 7.2 当 phase 为 idle 时,json_set 正常写入
cat > "$OC_CFG_DIR/.luci-openclaw-upgrade/status.json" <<'EOF'
{
"phase": "idle"
}
EOF
OPENCLAW_STATE_DIR="$OC_CFG_DIR/data/.openclaw" \
CONFIG_FILE="$CFG_FILE" \
OC_DATA="$OC_CFG_DIR/data" \
sh "$OC_CONFIG_SCRIPT" --set "gateway.port" "28888" || true
grep -q '28888' "$CFG_FILE" || fail "json_set must succeed when phase is idle"
# ── 8. Node.js musl ARM64 下载镜像源优先级契约 ──
# 验证 openclaw-env download_node 为 aarch64 musl 构建的镜像列表优先使用 unofficial-builds
ARM64_HARNESS="$TMP_DIR/harness-arm64.sh"
cat > "$ARM64_HARNESS" <<'EOF'
#!/bin/sh
set -e
test_mirrors() {
local libc_type="musl"
local node_ver="22.23.2"
local node_arch="linux-arm64"
local musl_tarball="node-v${node_ver}-${node_arch}-musl.tar.xz"
local NODE_MUSL_MIRROR="https://unofficial-builds.nodejs.org/download/release"
local NODE_MIRROR_CN="https://npmmirror.com/mirrors/node"
local NODE_SELF_HOST="https://github.com/10000ge10000/luci-app-openclaw/releases/download/node-bins"
local mirror_list=""
EOF
awk '/if \[ "\$libc_type" = "musl" \]; then/,/local downloaded=0/ { if (/local downloaded=0/) exit; print }' "$ENV_SCRIPT" >> "$ARM64_HARNESS"
cat >> "$ARM64_HARNESS" <<'EOF'
echo "$mirror_list"
}
test_mirrors
EOF
ARM64_MIRRORS=$(sh "$ARM64_HARNESS")
FIRST_MIRROR=$(echo "$ARM64_MIRRORS" | awk '{print $1}')
echo "$FIRST_MIRROR" | grep -q "unofficial-builds.nodejs.org" || fail "arm64 musl first candidate mirror must be unofficial-builds (actual: $FIRST_MIRROR)"
echo "$ARM64_MIRRORS" | grep -q "npmmirror.com" || fail "arm64 musl mirror list must include npmmirror"
echo "$ARM64_MIRRORS" | grep -q "github.com/10000ge10000" || fail "arm64 musl mirror list must include fallback self-hosted asset"
# ── 9. gateway-verify 输出正则适配 OpenClaw 2026.9.1 ──
MOCK_GW_STATE="$TMP_DIR/gw-mock/data/.openclaw"
mkdir -p "$MOCK_GW_STATE"
MOCK_CLI_BIN="$TMP_DIR/gw-mock/bin/openclaw"
mkdir -p "$(dirname "$MOCK_CLI_BIN")"
cat > "$MOCK_CLI_BIN" <<'EOF'
#!/bin/sh
if [ "$1" = "gateway" ] && [ "$2" = "health" ]; then
echo '{"ok": true, "durationMs": 5}'
exit 0
fi
exit 1
EOF
chmod 755 "$MOCK_CLI_BIN"
chmod -R 755 "$TMP_DIR/gw-mock"
OPENCLAW_CLI="$MOCK_CLI_BIN" sh "$STATE_SCRIPT" gateway-verify "mock-token" "$MOCK_GW_STATE" >/dev/null 2>&1 \
|| fail "gateway-verify must succeed when health reports {\"ok\": true}"
# 验证带空格的 "ok" : true 输出
cat > "$MOCK_CLI_BIN" <<'EOF'
#!/bin/sh
if [ "$1" = "gateway" ] && [ "$2" = "health" ]; then
echo '{ "ok" : true, "status": "online" }'
exit 0
fi
exit 1
EOF
chmod 755 "$MOCK_CLI_BIN"
chmod -R 755 "$TMP_DIR/gw-mock"
OPENCLAW_CLI="$MOCK_CLI_BIN" sh "$STATE_SCRIPT" gateway-verify "mock-token" "$MOCK_GW_STATE" >/dev/null 2>&1 \
|| fail "gateway-verify must succeed when health reports { \"ok\" : true }"
# ── 10. CLI 路径探测 /opt/openclaw/global/bin/openclaw 兜底 ──
MOCK_OPT_DIR="$TMP_DIR/opt/openclaw/global/bin"
mkdir -p "$MOCK_OPT_DIR"
cat > "$MOCK_OPT_DIR/openclaw" <<'EOF'
#!/bin/sh
exit 0
EOF
chmod +x "$MOCK_OPT_DIR/openclaw"
(
# 清空环境变量,模拟独立外部调用
unset OPENCLAW_CLI OC_GLOBAL OC_INSTALL_PATH
PATH="/usr/bin:/bin"
. "$STATE_SCRIPT"
# 若全局存在 /opt/openclaw/global/bin/openclaw 或有 mock,验证 oc_find_cli 逻辑
type oc_find_cli >/dev/null 2>&1 || fail "oc_find_cli must be defined in upgrade helper"
)
echo "ok"
@@ -75,4 +75,12 @@ grep -Fq 'history.replaceState' "$PAGE" \
grep -Fq "searchParams.delete('pty_token')" "$PAGE" \
|| fail "page must remove the pty_token query parameter"
# ── 重启会话与断线重连保护 ──
grep -Fq 'restartSession' "$PAGE" \
|| fail "page must define restartSession to retain auth tokens"
grep -Fq 'ws = null;' "$PAGE" \
|| fail "page must reset ws reference on close to avoid stale socket state"
grep -Fq 'oc_pty_token' "$PTY" \
|| fail "web-pty.js must support oc_pty_token cookie fallback for seamless page reloads"
echo "ok"
@@ -28,7 +28,13 @@ fi
grep -Fq "启动失败" "$STATUS_VIEW" || fail "status panel should distinguish startup failure from startup in progress"
grep -Fq "gateway_failed" "$STATUS_VIEW" || fail "status panel should render gateway failure state"
grep -Fq "ubus call service list" "$INIT_SCRIPT" || fail "status_service should inspect procd state via ubus"
grep -Fq "ubus call service list" "$INIT_SCRIPT" || fail "status_service should report procd state via ubus"
grep -Fq "crash-loop 抑制" "$INIT_SCRIPT" || fail "status_service should report procd crash-loop suppression"
grep -Fq "result.phase" "$CONTROLLER" || fail "status API should report upgrade transaction phase"
grep -Fq "result.target_version" "$CONTROLLER" || fail "status API should report upgrade transaction target_version"
grep -Fq "result.backup_verified" "$CONTROLLER" || fail "status API should report upgrade transaction backup_verified"
grep -Fq "result.migration_started" "$CONTROLLER" || fail "status API should report upgrade transaction migration_started"
grep -Fq "result.rollback_mode" "$CONTROLLER" || fail "status API should report upgrade transaction rollback_mode"
grep -Fq "result.error_code" "$CONTROLLER" || fail "status API should report upgrade transaction error_code"
echo "ok"
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.9.1
PKG_RELEASE:=258
PKG_RELEASE:=259
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -1060,7 +1060,7 @@ add_firewall_rule() {
$ipt_n -A PSW_OUTPUT $(dst $IPSET_LAN) -j RETURN
$ipt_n -A PSW_OUTPUT $(dst $IPSET_VPS) -j RETURN
[ "${USE_DIRECT_LIST}" = "1" ] && $ipt_n -A PSW_OUTPUT $(dst $IPSET_WHITE) -j RETURN
$ipt_n -A PSW_OUTPUT -m mark --mark 255 -j RETURN
$ipt_n -A PSW_OUTPUT -m mark --mark 0xff/0xff -j RETURN
$ipt_n -N PSW_DNS
if [ $(config_n_get @global[0] dns_redirect "1") = "0" ]; then
@@ -1115,7 +1115,7 @@ add_firewall_rule() {
[ "${USE_BLOCK_LIST}" = "1" ] && $ipt_m -A PSW_OUTPUT $(dst $IPSET_BLOCK) -j MARK --set-mark 88
[ "${USE_DIRECT_LIST}" = "1" ] && $ipt_m -A PSW_OUTPUT $(dst $IPSET_WHITE) -j RETURN
$ipt_m -A PSW_OUTPUT -m conntrack --ctdir REPLY -j RETURN
$ipt_m -A PSW_OUTPUT -m mark --mark 255 -j RETURN
$ipt_m -A PSW_OUTPUT -m mark --mark 0xff/0xff -j RETURN
ip rule add fwmark ${FWMARK} table 999 priority 999
ip route add local 0.0.0.0/0 dev lo table 999
@@ -1130,7 +1130,7 @@ add_firewall_rule() {
$ip6t_n -A PSW_OUTPUT $(dst $IPSET_LAN6) -j RETURN
$ip6t_n -A PSW_OUTPUT $(dst $IPSET_VPS6) -j RETURN
[ "${USE_DIRECT_LIST}" = "1" ] && $ip6t_n -A PSW_OUTPUT $(dst $IPSET_WHITE6) -j RETURN
$ip6t_n -A PSW_OUTPUT -m mark --mark 255 -j RETURN
$ip6t_n -A PSW_OUTPUT -m mark --mark 0xff/0xff -j RETURN
}
$ip6t_n -N PSW_DNS
@@ -1167,7 +1167,7 @@ add_firewall_rule() {
insert_rule_before "$ip6t_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT"
$ip6t_m -N PSW_OUTPUT
$ip6t_m -A PSW_OUTPUT -m mark --mark 255 -j RETURN
$ip6t_m -A PSW_OUTPUT -m mark --mark 0xff/0xff -j RETURN
$ip6t_m -A PSW_OUTPUT $(dst $IPSET_LAN6) -j RETURN
$ip6t_m -A PSW_OUTPUT $(dst $IPSET_VPS6) -j RETURN
[ "${USE_BLOCK_LIST}" = "1" ] && $ip6t_m -A PSW_OUTPUT $(dst $IPSET_BLOCK6) -j MARK --set-mark 88
@@ -1178,7 +1178,7 @@ add_firewall_rule() {
[ "${USE_BLOCK_LIST}" = "1" ] && nft_rule_dual "PSW_OUTPUT_MANGLE" "ip daddr" "$NFTSET_BLOCK" "counter reject"
[ "${USE_DIRECT_LIST}" = "1" ] && nft_rule_dual "PSW_OUTPUT_MANGLE" "ip daddr" "$NFTSET_WHITE" "counter return"
nft "add rule $NFTABLE_NAME PSW_OUTPUT_MANGLE ct direction reply counter return"
nft "add rule $NFTABLE_NAME PSW_OUTPUT_MANGLE meta mark 255 counter return"
nft "add rule $NFTABLE_NAME PSW_OUTPUT_MANGLE meta mark and 0xff == 0xff counter return"
# jump chains
nft "add rule $NFTABLE_NAME mangle_prerouting counter jump PSW_DIVERT"
@@ -1199,7 +1199,7 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME PSW_OUTPUT_NAT ip daddr @$NFTSET_VPS counter return"
[ "${USE_BLOCK_LIST}" = "1" ] && nft_rule_dual "PSW_OUTPUT_NAT" "ip daddr" "$NFTSET_BLOCK" "counter reject"
[ "${USE_DIRECT_LIST}" = "1" ] && nft_rule_dual "PSW_OUTPUT_NAT" "ip daddr" "$NFTSET_WHITE" "counter return"
nft "add rule $NFTABLE_NAME PSW_OUTPUT_NAT meta mark 255 counter return"
nft "add rule $NFTABLE_NAME PSW_OUTPUT_NAT meta mark and 0xff == 0xff counter return"
}
#icmp ipv6-icmp redirect
@@ -1239,7 +1239,7 @@ add_firewall_rule() {
[ "${USE_BLOCK_LIST}" = "1" ] && nft_rule_dual "PSW_OUTPUT_MANGLE_V6" "ip6 daddr" "$NFTSET_BLOCK6" "counter reject"
[ "${USE_DIRECT_LIST}" = "1" ] && nft_rule_dual "PSW_OUTPUT_MANGLE_V6" "ip6 daddr" "$NFTSET_WHITE6" "counter return"
nft "add rule $NFTABLE_NAME PSW_OUTPUT_MANGLE_V6 ct direction reply counter return"
nft "add rule $NFTABLE_NAME PSW_OUTPUT_MANGLE_V6 meta mark 255 counter return"
nft "add rule $NFTABLE_NAME PSW_OUTPUT_MANGLE_V6 meta mark and 0xff == 0xff counter return"
[ -n "$IPT_APPEND_DNS" ] && {
local local_dns dns_address dns_port
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.9.2
PKG_RELEASE:=112
PKG_RELEASE:=113
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -3,7 +3,7 @@
. /usr/share/passwall2/utils.sh
LOCK_FILE=${LOCK_PATH}/${CONFIG}_lease2hosts.lock
LEASE_FILE=$(config_t_get dnsmasq leasefile "/tmp/dhcp.leases")
LEASE_FILE=$(uci -q get dhcp.@dnsmasq[0].leasefile || echo "/tmp/dhcp.leases")
HOSTS_FILE="$TMP_PATH2/dhcp-hosts"
TMP_FILE="/tmp/dhcp-hosts.tmp"
+3 -3
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2009-06-10 03:41+0200\n"
"PO-Revision-Date: 2026-02-27 01:01+0000\n"
"PO-Revision-Date: 2026-08-24 16:47+0000\n"
"Last-Translator: Franco Castillo <castillofrancodamian@gmail.com>\n"
"Language-Team: Spanish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationssamba4/es/>\n"
@@ -12,7 +12,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.1-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-samba4/htdocs/luci-static/resources/view/samba4.js:120
msgid "Allow guests"
@@ -112,7 +112,7 @@ msgstr "Forzar E/S sincrónica"
#: applications/luci-app-samba4/htdocs/luci-static/resources/view/samba4.js:31
msgid "General Settings"
msgstr "Ajustes generales"
msgstr "Configuración general"
#: applications/luci-app-samba4/root/usr/share/rpcd/acl.d/luci-app-samba4.json:3
msgid "Grant access to LuCI app samba4"
+2 -2
View File
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2025-04-10 12:05+0000\n"
"PO-Revision-Date: 2026-08-27 11:45+0000\n"
"Last-Translator: Džiugas Januševičius <dziugas1959@hotmail.com>\n"
"Language-Team: Lithuanian <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationssamba4/lt/>\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"(n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 5.11-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-samba4/htdocs/luci-static/resources/view/samba4.js:120
msgid "Allow guests"
+4 -3
View File
@@ -2,8 +2,9 @@ msgid ""
msgstr ""
"Project-Id-Version: LuCI: samba\n"
"POT-Creation-Date: 2009-05-19 19:36+0200\n"
"PO-Revision-Date: 2026-07-03 15:28+0000\n"
"Last-Translator: chigelo <chigelo@gmail.com>\n"
"PO-Revision-Date: 2026-08-28 06:20+0000\n"
"Last-Translator: SnIPeRSnIPeR "
"<snipersniper@users.noreply.hosted.weblate.org>\n"
"Language-Team: Russian <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationssamba4/ru/>\n"
"Language: ru\n"
@@ -12,7 +13,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 2026.7.1.dev0\n"
"X-Generator: Weblate 2026.9.dev0\n"
"Project-Info: Это технический перевод, не дословный. Главное-удобный русский "
"интерфейс, все проверялось в графическом режиме, совместим с другими apps\n"
+5 -4
View File
@@ -6,8 +6,9 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2009-06-10 03:40+0200\n"
"PO-Revision-Date: 2026-04-04 14:23+0000\n"
"Last-Translator: evan-yang <yws112358@vip.qq.com>\n"
"PO-Revision-Date: 2026-08-31 06:44+0000\n"
"Last-Translator: Hosted Weblate user 54392 "
"<hamburger2048@users.noreply.hosted.weblate.org>\n"
"Language-Team: Chinese (Simplified Han script) <https://hosted.weblate.org/"
"projects/openwrt/luciapplicationssamba4/zh_Hans/>\n"
"Language: zh_Hans\n"
@@ -15,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.17-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-samba4/htdocs/luci-static/resources/view/samba4.js:120
msgid "Allow guests"
@@ -108,7 +109,7 @@ msgstr "强制同步 I/O"
#: applications/luci-app-samba4/htdocs/luci-static/resources/view/samba4.js:31
msgid "General Settings"
msgstr "常规设置"
msgstr "通用设置"
#: applications/luci-app-samba4/root/usr/share/rpcd/acl.d/luci-app-samba4.json:3
msgid "Grant access to LuCI app samba4"
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_LICENSE:=GPL-3.0-or-later
PKG_MAINTAINER:=Nick Peng <pymumu@gmail.com>
PKG_VERSION:=1.2025.47.2
PKG_RELEASE:=12
PKG_RELEASE:=13
LUCI_TITLE:=LuCI for smartdns
LUCI_DESCRIPTION:=Provides Luci for smartdns
+3 -3
View File
@@ -1,8 +1,8 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2026-08-13 09:27+0000\n"
"Last-Translator: Pavel Borecki <pavel.borecki@gmail.com>\n"
"PO-Revision-Date: 2026-08-27 18:20+0000\n"
"Last-Translator: Martin Olmer <m_iv@seznam.cz>\n"
"Language-Team: Czech <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationssmartdns/cs/>\n"
"Language: cs\n"
@@ -88,7 +88,7 @@ msgstr "Velikost mezipaměti"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:120
msgid "Collecting data ..."
msgstr "Shromažďování dat…"
msgstr "Shromažďování dat …"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:1099
msgid ""
+9 -9
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2020-07-02 17:20-0300\n"
"PO-Revision-Date: 2026-06-26 11:19+0000\n"
"Last-Translator: apemay <apemay.dev@gmail.com>\n"
"PO-Revision-Date: 2026-08-24 16:48+0000\n"
"Last-Translator: Franco Castillo <castillofrancodamian@gmail.com>\n"
"Language-Team: Spanish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationssmartdns/es/>\n"
"Language: es\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.7.dev0\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:791
msgid "Additional Args for upstream dns servers"
@@ -47,7 +47,7 @@ msgstr ""
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:131
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:638
msgid "Advanced Settings"
msgstr "Ajustes avanzados"
msgstr "Configuración avanzada"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:252
msgid ""
@@ -93,7 +93,7 @@ msgstr "Tamaño del caché"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:120
msgid "Collecting data ..."
msgstr "Recopilando los datos..."
msgstr "Recopilando datos..."
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:1099
msgid ""
@@ -360,7 +360,7 @@ msgstr "Forzar HTTPS SOA."
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:130
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:637
msgid "General Settings"
msgstr "Ajustes generales"
msgstr "Configuración general"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:598
msgid "Generate Coredump"
@@ -492,7 +492,7 @@ msgstr "Nombre del conjunto NFT, formato: [#[4|6]:[familia#tabla#conjunto]]"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:65
msgid "NOT RUNNING"
msgstr "NO EN EJECUCIÓN"
msgstr "NO SE ESTÁ EJECUTANDO"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:233
msgid "Name of device name listen on."
@@ -565,7 +565,7 @@ msgstr ""
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:63
msgid "RUNNING"
msgstr "EN EJECUCIÓN"
msgstr "EJECUTÁNDOSE"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:340
msgid "Reply Domain TTL Max"
@@ -661,7 +661,7 @@ msgstr ""
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:127
msgid "Settings"
msgstr "Ajustes"
msgstr "Configuración"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:401
msgid "Skip Address Rules"
+5 -5
View File
@@ -1,6 +1,6 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-04-23 10:15+0000\n"
"PO-Revision-Date: 2026-09-02 17:27+0000\n"
"Last-Translator: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>\n"
"Language-Team: Irish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationssmartdns/ga/>\n"
@@ -9,7 +9,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :"
"(n>6 && n<11) ? 3 : 4;\n"
"X-Generator: Weblate 5.17.1-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:791
msgid "Additional Args for upstream dns servers"
@@ -90,7 +90,7 @@ msgstr "Méid taisce"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:120
msgid "Collecting data ..."
msgstr "Sonraí á mbailiú ..."
msgstr "Ag bailiú sonraí ..."
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:1099
msgid ""
@@ -487,7 +487,7 @@ msgstr "Ainm NFTSet, formáid: [# [4|6]: [teaghlach #table #set]]"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:65
msgid "NOT RUNNING"
msgstr "NÍ RÚCHÁN"
msgstr "NÍL AG RITH"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:233
msgid "Name of device name listen on."
@@ -558,7 +558,7 @@ msgstr "Fiosraigh DNS trí ghrúpa freastalaí dns ar leith, mar oifig, baile."
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:63
msgid "RUNNING"
msgstr "REATHA"
msgstr "AG RITH"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:340
msgid "Reply Domain TTL Max"
+2 -2
View File
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2026-01-18 00:24+0000\n"
"PO-Revision-Date: 2026-08-24 16:48+0000\n"
"Last-Translator: Matthaiks <kitynska@gmail.com>\n"
"Language-Team: Polish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationssmartdns/pl/>\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 5.15.2\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:791
msgid "Additional Args for upstream dns servers"
+2 -2
View File
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2026-08-14 12:12+0000\n"
"PO-Revision-Date: 2026-09-02 07:50+0000\n"
"Last-Translator: SnIPeRSnIPeR "
"<snipersniper@users.noreply.hosted.weblate.org>\n"
"Language-Team: Russian <https://hosted.weblate.org/projects/openwrt/"
@@ -525,7 +525,7 @@ msgstr "Отсутствие проверки сертификата"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:999
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:1024
msgid "None"
msgstr "Отсутствует"
msgstr "Нет"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:783
msgid "Only socks5 proxy support udp server."
+4 -4
View File
@@ -1,14 +1,14 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2026-04-16 14:10+0000\n"
"Last-Translator: ZW <roc_fe@users.noreply.hosted.weblate.org>\n"
"PO-Revision-Date: 2026-08-31 06:44+0000\n"
"Last-Translator: 為什麼不加空格 <c++23@users.noreply.hosted.weblate.org>\n"
"Language-Team: Chinese (Traditional Han script) <https://hosted.weblate.org/"
"projects/openwrt/luciapplicationssmartdns/zh_Hant/>\n"
"Language: zh_Hant\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.17.1-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:791
msgid "Additional Args for upstream dns servers"
@@ -82,7 +82,7 @@ msgstr "快取大小"
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:120
msgid "Collecting data ..."
msgstr "收集資料中..."
msgstr "收集資料中 ..."
#: applications/luci-app-smartdns/htdocs/luci-static/resources/view/smartdns/smartdns.js:1099
msgid ""
+3 -3
View File
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2026-04-24 04:42+0000\n"
"PO-Revision-Date: 2026-08-26 18:52+0000\n"
"Last-Translator: Džiugas Januševičius <dziugas1959@hotmail.com>\n"
"Language-Team: Lithuanian <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationstransmission/lt/>\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"(n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 5.17.1-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:65
msgid "Alternative download speed"
@@ -108,7 +108,7 @@ msgstr "Įgalinti stebėjimo katalogą/vietovę"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:47
msgid "Enabled"
msgstr "Įjungta/Įgalinta (-s/-i)"
msgstr "Įjungtas/Įgalintas"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:136
msgid "Encryption"
+8 -7
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: LuCI: transmission\n"
"POT-Creation-Date: 2012-08-15 17:35+0300\n"
"PO-Revision-Date: 2026-04-25 03:12+0000\n"
"PO-Revision-Date: 2026-09-02 07:50+0000\n"
"Last-Translator: SnIPeRSnIPeR "
"<snipersniper@users.noreply.hosted.weblate.org>\n"
"Language-Team: Russian <https://hosted.weblate.org/projects/openwrt/"
@@ -13,7 +13,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 5.17.1-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
"Project-Info: Это технический перевод, не дословный. Главное-удобный русский "
"интерфейс, все проверялось в графическом режиме, совместим с другими apps\n"
@@ -183,7 +183,7 @@ msgstr "Дополнительно"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:148
msgid "None"
msgstr "Отсутствует"
msgstr "Нет"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:283
msgid ""
@@ -293,11 +293,11 @@ msgstr "Удалённое управление (RPC) включено"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:249
msgid "RPC host whitelist"
msgstr "Белый список IP-адресов для доступа к удалённому управлению (RPC)"
msgstr "Список разрешённых хостов для доступа к удалённому управлению (RPC)"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:245
msgid "RPC host whitelist enabled"
msgstr "Белый список IP-адресов для удалённого управления (RPC) включён"
msgstr "Список разрешённых хостов для удалённого управления (RPC) включён"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:266
msgid "RPC password"
@@ -317,11 +317,12 @@ msgstr "Логин для доступа к удалённому управле
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:256
msgid "RPC whitelist"
msgstr "Белый список IP-адресов для доступа к удалённому управлению (RPC)"
msgstr ""
"Список разрешённых IP-адресов для доступа к удалённому управлению (RPC)"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:252
msgid "RPC whitelist enabled"
msgstr "Белый список IP-адресов для удалённого управления (RPC) включён"
msgstr "Список разрешённых IP-адресов для удалённого управления (RPC) включён"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:295
msgid "Ratio limit"
+4 -4
View File
@@ -1,8 +1,8 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2026-06-09 01:02+0000\n"
"Last-Translator: Oğuz Ersen <oguz@ersen.moe>\n"
"PO-Revision-Date: 2026-08-23 20:27+0000\n"
"Last-Translator: umut <umvtt4@proton.me>\n"
"Language-Team: Turkish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationstransmission/tr/>\n"
"Language: tr\n"
@@ -10,7 +10,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 2026.6\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:65
msgid "Alternative download speed"
@@ -178,7 +178,7 @@ msgstr "Çeşitli"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:148
msgid "None"
msgstr "Yok"
msgstr "Hiçbiri"
#: applications/luci-app-transmission/htdocs/luci-static/resources/view/transmission.js:283
msgid ""
+4 -4
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 2026-06-27 12:01+0000\n"
"Last-Translator: apemay <apemay.dev@gmail.com>\n"
"PO-Revision-Date: 2026-08-24 16:47+0000\n"
"Last-Translator: Franco Castillo <castillofrancodamian@gmail.com>\n"
"Language-Team: Spanish <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationswatchcat/es/>\n"
"Language: es\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 2026.7.dev0\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-watchcat/htdocs/luci-static/resources/view/watchcat.js:94
msgid ""
@@ -69,7 +69,7 @@ msgstr "Forzar Retraso de Reinicio"
#: applications/luci-app-watchcat/htdocs/luci-static/resources/view/watchcat.js:19
msgid "General Settings"
msgstr "Ajustes generales"
msgstr "Configuración general"
#: applications/luci-app-watchcat/root/usr/share/rpcd/acl.d/luci-app-watchcat.json:3
msgid "Grant access to LuCI app watchcat"
+3 -3
View File
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2026-04-29 17:47+0000\n"
"PO-Revision-Date: 2026-08-26 18:51+0000\n"
"Last-Translator: Hyeonjeong Lee <h9101654@gmail.com>\n"
"Language-Team: Korean <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationswatchcat/ko/>\n"
@@ -10,7 +10,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.17.1-dev\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-watchcat/htdocs/luci-static/resources/view/watchcat.js:94
msgid ""
@@ -159,7 +159,7 @@ msgstr ""
#: applications/luci-app-watchcat/htdocs/luci-static/resources/view/watchcat.js:29
msgid "Restart Interface"
msgstr ""
msgstr "인터페이스 재시작"
#: applications/luci-app-watchcat/htdocs/luci-static/resources/view/watchcat.js:30
msgid "Run Script"
+2 -2
View File
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"PO-Revision-Date: 2026-01-02 18:04+0000\n"
"PO-Revision-Date: 2026-08-27 11:45+0000\n"
"Last-Translator: Džiugas Januševičius <dziugas1959@hotmail.com>\n"
"Language-Team: Lithuanian <https://hosted.weblate.org/projects/openwrt/"
"luciapplicationswatchcat/lt/>\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"(n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Weblate 5.15.1\n"
"X-Generator: Weblate 2026.9.dev0\n"
#: applications/luci-app-watchcat/htdocs/luci-static/resources/view/watchcat.js:94
msgid ""
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more