mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 20:45:13 +08:00
🤞 Sync 2026-09-06 22:50:31
This commit is contained in:
1 parent
bb7d626e94
commit
64c17e592d
115 files changed
+8599
-1391
No files matched your search
+1
-1
@@ -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 子进程都能退出。
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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 "状态"
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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 "利用不可"
|
||||
@@ -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"
|
||||
@@ -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 "Недоступно"
|
||||
@@ -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 "每分钟"
|
||||
@@ -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 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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로 연결합니다. (안전하지 않음)"
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 的一致性。
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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 @@
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -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
|
||||
+1211
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');
|
||||
|
||||
@@ -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 ==="
|
||||
}
|
||||
|
||||
Vendored
+50
@@ -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);
|
||||
+50
@@ -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,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 -
|
||||
|
||||
|
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
Reference in new issue
Block a user