Sync 2026-03-17 20:27:41

This commit is contained in:
github-actions[bot] 2026-03-17 20:27:41 +08:00
parent 738f2b9cd8
commit cb0fc3c571
22 changed files with 4371 additions and 82 deletions

View File

@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-openclaw
PKG_VERSION:=$(strip $(shell cat $(CURDIR)/VERSION 2>/dev/null || echo "1.0.0"))
PKG_RELEASE:=2
PKG_RELEASE:=3
PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com>
PKG_LICENSE:=GPL-3.0

View File

@ -41,6 +41,9 @@ function index()
-- 配置备份 API (v2026.3.8+: openclaw backup create/verify)
entry({"admin", "services", "openclaw", "backup"}, call("action_backup"), nil).leaf = true
-- 系统配置检测 API (安装前检测)
entry({"admin", "services", "openclaw", "check_system"}, call("action_check_system"), nil).leaf = true
end
-- ═══════════════════════════════════════════
@ -372,19 +375,21 @@ function action_uninstall()
sys.exec("/etc/init.d/openclaw disable 2>/dev/null")
-- 设置 UCI enabled=0
sys.exec("uci set openclaw.main.enabled=0; uci commit openclaw 2>/dev/null")
-- 删除 Node.js + OpenClaw 运行环境
-- 删除 Node.js + OpenClaw 运行环境 (包含所有插件: qqbot, 飞书等)
sys.exec("rm -rf /opt/openclaw")
-- 清理旧数据迁移后可能残留的目录
sys.exec("rm -rf /root/.openclaw 2>/dev/null")
-- 清理临时文件
sys.exec("rm -f /tmp/openclaw-setup.* /tmp/openclaw-update.log /var/run/openclaw*.pid")
sys.exec("rm -f /tmp/openclaw-setup.* /tmp/openclaw-update.log /tmp/openclaw-plugin-upgrade.* /var/run/openclaw*.pid")
-- 清理 LuCI 缓存
sys.exec("rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null")
-- 删除 openclaw 系统用户
sys.exec("sed -i '/^openclaw:/d' /etc/passwd /etc/shadow /etc/group 2>/dev/null")
http.prepare_content("application/json")
http.write_json({
status = "ok",
message = "运行环境已卸载。已清理: Node.js 运行环境 (/opt/openclaw)、旧数据目录 (/root/.openclaw)、临时文件。"
message = "运行环境已卸载。已清理: Node.js 运行环境 (/opt/openclaw)、所有插件 (qqbot/飞书等)、旧数据目录 (/root/.openclaw)、临时文件、LuCI 缓存"
})
end
@ -801,3 +806,74 @@ function action_backup()
http.write_json({ status = "error", message = "未知备份操作: " .. action })
end
end
-- ═══════════════════════════════════════════
-- 系统配置检测 API (安装前检测)
-- 检测内存和磁盘空间是否满足最低要求
-- 要求: 内存 > 1GB, 磁盘可用空间 > 1.5GB
-- ═══════════════════════════════════════════
function action_check_system()
local http = require "luci.http"
local sys = require "luci.sys"
-- 最低要求配置
local MIN_MEMORY_MB = 1024 -- 1GB
local MIN_DISK_MB = 1536 -- 1.5GB
local result = {
memory_mb = 0,
memory_ok = false,
disk_mb = 0,
disk_ok = false,
disk_path = "",
pass = false,
message = ""
}
-- 检测总内存 (从 /proc/meminfo 读取 MemTotal)
local meminfo = io.open("/proc/meminfo", "r")
if meminfo then
for line in meminfo:lines() do
local mem_total = line:match("MemTotal:%s+(%d+)%s+kB")
if mem_total then
result.memory_mb = math.floor(tonumber(mem_total) / 1024)
break
end
end
meminfo:close()
end
result.memory_ok = result.memory_mb >= MIN_MEMORY_MB
-- 检测磁盘可用空间
-- 优先检测 /opt 所在分区,如果 /opt 不存在则检测 /overlay 或 /
local disk_paths = {"/opt", "/overlay", "/"}
for _, path in ipairs(disk_paths) do
local df_output = sys.exec("df -m " .. path .. " 2>/dev/null | tail -1 | awk '{print $4}'"):gsub("%s+", "")
if df_output and df_output ~= "" and tonumber(df_output) then
result.disk_mb = tonumber(df_output)
result.disk_path = path
break
end
end
result.disk_ok = result.disk_mb >= MIN_DISK_MB
-- 综合判断
result.pass = result.memory_ok and result.disk_ok
-- 生成提示信息
if result.pass then
result.message = "系统配置检测通过"
else
local issues = {}
if not result.memory_ok then
table.insert(issues, string.format("内存不足: 当前 %d MB需要至少 %d MB", result.memory_mb, MIN_MEMORY_MB))
end
if not result.disk_ok then
table.insert(issues, string.format("磁盘空间不足: 当前 %d MB 可用,需要至少 %d MB", result.disk_mb, MIN_DISK_MB))
end
result.message = table.concat(issues, "")
end
http.prepare_content("application/json")
http.write_json(result)
end

View File

@ -27,6 +27,7 @@ act.cfgvalue = function(self, section)
local uninstall_url = luci.dispatcher.build_url("admin", "services", "openclaw", "uninstall")
local plugin_upgrade_url = luci.dispatcher.build_url("admin", "services", "openclaw", "plugin_upgrade")
local plugin_upgrade_log_url = luci.dispatcher.build_url("admin", "services", "openclaw", "plugin_upgrade_log")
local check_system_url = luci.dispatcher.build_url("admin", "services", "openclaw", "check_system")
local html = {}
-- 按钮区域
@ -91,30 +92,64 @@ act.cfgvalue = function(self, section)
html[#html+1] = 'document.getElementById("oc-setup-dialog").style.display="none";'
html[#html+1] = '}'
html[#html+1] = 'function ocConfirmSetup(){'
html[#html+1] = 'ocCloseSetupDialog();'
html[#html+1] = 'var radios=document.getElementsByName("oc-ver-choice");'
html[#html+1] = 'var choice="stable";'
html[#html+1] = 'for(var i=0;i<radios.length;i++){if(radios[i].checked){choice=radios[i].value;break;}}'
html[#html+1] = 'var verParam=(choice==="stable")?"stable":"latest";'
html[#html+1] = 'ocSetup(verParam);'
html[#html+1] = '}'
-- 安装运行环境 (带实时日志)
html[#html+1] = 'function ocSetup(version){'
html[#html+1] = 'var btn=document.getElementById("btn-setup");'
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 检测系统配置...";'
html[#html+1] = '(new XHR()).get("' .. check_system_url .. '",null,function(x){'
html[#html+1] = 'try{'
html[#html+1] = 'var r=JSON.parse(x.responseText);'
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] = 'var actionEl=document.getElementById("action-result");'
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 安装中...";'
html[#html+1] = 'actionEl.textContent="";'
html[#html+1] = 'panel.style.display="block";'
html[#html+1] = 'logEl.textContent="正在启动安装 ("+((version==="stable")?"稳定版":"最新版")+")...\\n";'
html[#html+1] = 'titleEl.textContent="📋 安装日志";'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 安装进行中...</span>";'
html[#html+1] = 'resultEl.style.display="none";'
html[#html+1] = 'titleEl.textContent="📋 安装日志";'
html[#html+1] = 'logEl.textContent="";'
html[#html+1] = 'logEl.textContent+="════════════════════════════════════════\\n";'
html[#html+1] = 'logEl.textContent+="🔍 系统配置检测\\n";'
html[#html+1] = 'logEl.textContent+="════════════════════════════════════════\\n";'
html[#html+1] = 'logEl.textContent+="内存: "+r.memory_mb+" MB (需要 ≥ 1024 MB) — "+(r.memory_ok?"✅ 通过":"❌ 不达标")+"\\n";'
html[#html+1] = 'logEl.textContent+="磁盘: "+r.disk_mb+" MB 可用 (需要 ≥ 1536 MB) — "+(r.disk_ok?"✅ 通过":"❌ 不达标")+"\\n";'
html[#html+1] = 'logEl.textContent+="\\n";'
html[#html+1] = 'if(!r.pass){'
html[#html+1] = 'ocCloseSetupDialog();'
html[#html+1] = 'btn.disabled=false;btn.textContent="📦 安装运行环境";'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#cf222e;\\">❌ 系统配置不满足要求</span>";'
html[#html+1] = 'logEl.textContent+="❌ 系统配置不满足要求,安装已终止\\n";'
html[#html+1] = 'logEl.textContent+="💡 请升级硬件配置或清理磁盘空间后重试\\n";'
html[#html+1] = 'resultEl.style.display="block";'
html[#html+1] = 'resultEl.innerHTML="<div style=\\"border:1px solid #f5c6cb;background:#ffeef0;padding:12px 16px;border-radius:6px;\\">"+'
html[#html+1] = '"<strong style=\\"color:#cf222e;font-size:14px;\\">❌ 系统配置不满足要求</strong><br/>"+'
html[#html+1] = '"<div style=\\"margin-top:8px;font-size:12px;color:#666;\\">💡 请升级硬件配置或清理磁盘空间后重试。</div></div>";'
html[#html+1] = 'return;'
html[#html+1] = '}'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 安装进行中...</span>";'
html[#html+1] = 'logEl.textContent+="✅ 系统配置检测通过,开始安装...\\n\\n";'
html[#html+1] = 'ocCloseSetupDialog();'
html[#html+1] = 'var radios=document.getElementsByName("oc-ver-choice");'
html[#html+1] = 'var choice="stable";'
html[#html+1] = 'for(var i=0;i<radios.length;i++){if(radios[i].checked){choice=radios[i].value;break;}}'
html[#html+1] = 'var verParam=(choice==="stable")?"stable":"latest";'
html[#html+1] = 'ocSetup(verParam,r.memory_mb,r.disk_mb);'
html[#html+1] = '}catch(e){'
html[#html+1] = 'ocCloseSetupDialog();'
html[#html+1] = 'btn.disabled=false;btn.textContent="📦 安装运行环境";'
html[#html+1] = 'alert("系统检测失败,请重试");'
html[#html+1] = '}});'
html[#html+1] = '}'
-- 安装运行环境 (带实时日志)
html[#html+1] = 'function ocSetup(version,mem_mb,disk_mb){'
html[#html+1] = 'var btn=document.getElementById("btn-setup");'
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 安装中...";'
html[#html+1] = 'logEl.textContent+="════════════════════════════════════════\\n";'
html[#html+1] = 'logEl.textContent+="📦 安装运行环境 ("+((version==="stable")?"稳定版":"最新版")+")\\n";'
html[#html+1] = 'logEl.textContent+="════════════════════════════════════════\\n";'
html[#html+1] = 'logEl.textContent+="正在启动安装...\\n";'
html[#html+1] = '(new XHR()).get("' .. ctl_url .. '?action=setup&version="+encodeURIComponent(version),null,function(x){'
html[#html+1] = 'try{JSON.parse(x.responseText);}catch(e){}'
html[#html+1] = 'ocPollSetupLog();'
@ -122,24 +157,30 @@ act.cfgvalue = function(self, section)
html[#html+1] = '}'
-- 轮询安装日志
html[#html+1] = 'var _lastLogLen=0;'
html[#html+1] = 'function ocPollSetupLog(){'
html[#html+1] = 'if(_setupTimer)clearInterval(_setupTimer);'
html[#html+1] = '_lastLogLen=0;'
html[#html+1] = '_setupTimer=setInterval(function(){'
html[#html+1] = '(new XHR()).get("' .. log_url .. '",null,function(x){'
html[#html+1] = 'try{'
html[#html+1] = 'var r=JSON.parse(x.responseText);'
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
html[#html+1] = 'if(r.log)logEl.textContent=r.log;'
html[#html+1] = 'if(r.log&&r.log.length>_lastLogLen){'
html[#html+1] = 'var newLog=r.log.substring(_lastLogLen);'
html[#html+1] = 'logEl.textContent+=newLog;'
html[#html+1] = '_lastLogLen=r.log.length;'
html[#html+1] = '}'
html[#html+1] = 'logEl.scrollTop=logEl.scrollHeight;'
html[#html+1] = 'if(r.state==="running"){'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 安装进行中...</span>";'
html[#html+1] = '}else if(r.state==="success"){'
html[#html+1] = 'clearInterval(_setupTimer);_setupTimer=null;'
html[#html+1] = 'ocSetupDone(true,r.log);'
html[#html+1] = 'ocSetupDone(true,logEl.textContent);'
html[#html+1] = '}else if(r.state==="failed"){'
html[#html+1] = 'clearInterval(_setupTimer);_setupTimer=null;'
html[#html+1] = 'ocSetupDone(false,r.log);'
html[#html+1] = 'ocSetupDone(false,logEl.textContent);'
html[#html+1] = '}'
html[#html+1] = '}catch(e){}'
html[#html+1] = '});'

View File

@ -258,24 +258,33 @@ all) gw_bind="custom" ;; # custom = 0.0.0.0
esac
# 确保网关端口未被残留进程占用 (防止 restart 时 crash loop)
# v2026.3.14 优化: 快速轮询 + 批量 kill
_ensure_port_free() {
local p="$1" max_wait="${2:-5}" i=0
local p="$1"
local i=0 max_wait=10 # 10 * 0.2 = 2 秒
# 先检查端口是否已被占用
if command -v ss >/dev/null 2>&1; then
ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0
else
netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0
fi
# 端口被占用,尝试清理
for occ_pid in $(pgrep -f "openclaw-gateway" 2>/dev/null); do
kill "$occ_pid" 2>/dev/null
done
while [ $i -lt $max_wait ]; do
if command -v ss >/dev/null 2>&1; then
ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0
else
netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0
fi
# 尝试杀掉占用端口的 gateway 进程
if [ $i -eq 0 ]; then
local occ_pid
for occ_pid in $(pgrep -f "openclaw-gateway" 2>/dev/null); do
kill "$occ_pid" 2>/dev/null
done
fi
sleep 1
usleep 200000 2>/dev/null || sleep 0.2
i=$((i + 1))
done
# 最后手段: SIGKILL
local port_pid=""
if command -v ss >/dev/null 2>&1; then
@ -283,7 +292,7 @@ _ensure_port_free() {
else
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${p} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1)
fi
[ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null && sleep 1
[ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null && usleep 300000 2>/dev/null
return 0
}
_ensure_port_free "$port"
@ -350,36 +359,43 @@ stop_service() {
local port
port=$(uci -q get openclaw.main.port || echo "18789")
# 杀掉所有 openclaw / openclaw-gateway 残留进程
# (排除 web-pty.js 和 oc-config.sh它们由 pty 实例管理)
local pid
for pid in $(pgrep -f "openclaw-gateway" 2>/dev/null) \
$(pgrep -f "openclaw.*gateway.*run" 2>/dev/null); do
kill "$pid" 2>/dev/null
# v2026.3.14 优化: 快速终止进程树,减少等待时间
# 1) 先获取所有相关 PID
local gw_pids=""
gw_pids=$(pgrep -f "openclaw-gateway" 2>/dev/null)
gw_pids="$gw_pids $(pgrep -f "openclaw.*gateway.*run" 2>/dev/null)"
# 2) 同时发送 SIGTERM 给所有进程
for pid in $gw_pids; do
[ -n "$pid" ] && kill "$pid" 2>/dev/null
done
# 等待端口真正释放 (最多 8 秒)
# 3) 快速轮询等待端口释放 (200ms 间隔,最多 3 秒)
local wait_count=0
while [ $wait_count -lt 8 ]; do
local max_wait=15 # 15 * 0.2 = 3 秒
while [ $wait_count -lt $max_wait ]; do
if command -v ss >/dev/null 2>&1; then
ss -tulnp 2>/dev/null | grep -q ":${port} " || break
else
netstat -tulnp 2>/dev/null | grep -q ":${port} " || break
fi
sleep 1
usleep 200000 2>/dev/null || sleep 0.2
wait_count=$((wait_count + 1))
done
# 如果端口仍被占用,强制杀掉占用者
if [ $wait_count -ge 8 ]; then
# 4) 如果端口仍被占用,强制 SIGKILL
if [ $wait_count -ge $max_wait ]; then
local port_pid=""
if command -v ss >/dev/null 2>&1; then
port_pid=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
else
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${port} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1)
fi
[ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null
sleep 1
if [ -n "$port_pid" ]; then
kill -9 "$port_pid" 2>/dev/null
# 等待内核回收 (缩短到 0.5 秒)
usleep 500000 2>/dev/null || sleep 0.5
fi
fi
}
@ -390,8 +406,8 @@ procd_add_reload_trigger "openclaw"
reload_service() {
stop
# stop_service 已确保端口释放,但额外等待 1 秒让内核回收
sleep 1
# v2026.3.14: stop_service 已优化等待逻辑,缩短额外等待时间
usleep 300000 2>/dev/null || sleep 0.3
start
}
@ -407,47 +423,45 @@ restart_gateway() {
local port
port=$(uci -q get openclaw.main.port || echo "18789")
# ── 第一步: kill 监听端口的 gateway 子进程 (openclaw-gateway) ──
# openclaw 启动后会 fork 出 openclaw-gateway 子进程实际监听端口
# 必须先杀子进程释放端口,否则 procd respawn 的新实例会因端口冲突而崩溃
local port_pid=""
# v2026.3.14 优化: 快速终止并重启
# 1) 同时获取端口 PID 和 procd 管理的 PID
local port_pid="" gw_pid=""
if command -v ss >/dev/null 2>&1; then
port_pid=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
else
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${port} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1)
fi
[ -n "$port_pid" ] && kill "$port_pid" 2>/dev/null
# ── 第二步: kill procd 管理的 gateway 主进程 (openclaw) ──
# procd 追踪的是主进程 PIDkill 它才能触发 respawn
local gw_pid=""
gw_pid=$(ubus call service list '{"name":"openclaw"}' 2>/dev/null | \
jsonfilter -e '$.openclaw.instances.gateway.pid' 2>/dev/null) || true
if [ -n "$gw_pid" ] && kill -0 "$gw_pid" 2>/dev/null; then
kill "$gw_pid" 2>/dev/null
fi
# 2) 同时发送 SIGTERM 给所有相关进程
[ -n "$port_pid" ] && kill "$port_pid" 2>/dev/null
[ -n "$gw_pid" ] && kill -0 "$gw_pid" 2>/dev/null && kill "$gw_pid" 2>/dev/null
# ── 第三步: 兜底 — kill 所有 openclaw gateway 相关残留进程 ──
# 避免任何残留进程占据端口
sleep 1
# 3) 兜底: kill 所有残留进程
for pid in $(pgrep -f "openclaw-gateway" 2>/dev/null) $(pgrep -f "openclaw.*gateway.*run" 2>/dev/null); do
kill "$pid" 2>/dev/null
done
# ── 第四步: 等待端口真正释放 (最多 5 秒) ──
# 4) 快速轮询等待端口释放 (200ms 间隔,最多 2 秒)
local wait_count=0
while [ $wait_count -lt 5 ]; do
while [ $wait_count -lt 10 ]; do
if command -v ss >/dev/null 2>&1; then
ss -tulnp 2>/dev/null | grep -q ":${port} " || break
else
netstat -tulnp 2>/dev/null | grep -q ":${port} " || break
fi
sleep 1
usleep 200000 2>/dev/null || sleep 0.2
wait_count=$((wait_count + 1))
done
# ── 第五步: 如果 procd 中没有 gateway 服务注册 (首次/崩溃),调用 start ──
# 5) 如果端口仍被占用,强制 SIGKILL
if [ $wait_count -ge 10 ]; then
[ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null
usleep 300000 2>/dev/null || sleep 0.3
fi
# 6) 如果 procd 中没有 gateway 服务注册,调用 start
if [ -z "$gw_pid" ]; then
/etc/init.d/openclaw start >/dev/null 2>&1
fi

View File

@ -452,13 +452,8 @@ do_setup() {
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ ✅ 安装完成! ║"
echo "║ ║"
echo "║ 下一步: ║"
echo "║ uci set openclaw.main.enabled=1 ║"
echo "║ uci commit openclaw ║"
echo "║ /etc/init.d/openclaw enable ║"
echo "║ /etc/init.d/openclaw start ║"
echo "║ ║"
echo "║ 或在 LuCI → 服务 → OpenClaw 中启用 ║"
echo "║ 如通过 LuCI 安装,服务已自动启用并启动。 ║"
echo "║ 如通过命令行安装,请在 LuCI → 服务 → OpenClaw 中启用。 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
}
@ -724,13 +719,7 @@ do_setup_offline() {
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ ✅ 离线安装完成! ║"
echo "║ ║"
echo "║ 下一步: ║"
echo "║ uci set openclaw.main.enabled=1 ║"
echo "║ uci commit openclaw ║"
echo "║ /etc/init.d/openclaw enable ║"
echo "║ /etc/init.d/openclaw start ║"
echo "║ ║"
echo "║ 或在 LuCI → 服务 → OpenClaw 中启用 ║"
echo "║ 请在 LuCI → 服务 → OpenClaw 中启用服务。 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
}

View File

@ -2059,7 +2059,69 @@ reset_to_defaults() {
if [ "$confirm" = "yes" ]; then
echo ""
echo -e " ${CYAN}正在清除渠道配置...${NC}"
# 清除 openclaw.json 中的 channels 配置
oc_cmd config unset channels >/dev/null 2>&1 || true
# v2026.3.14: 同时清除 plugins 中与渠道相关的配置
# 防止重置后插件配置残留导致状态不一致
if [ -f "$CONFIG_FILE" ] && [ -x "$NODE_BIN" ]; then
"$NODE_BIN" -e "
const fs=require('fs');
try{
const d=JSON.parse(fs.readFileSync('${CONFIG_FILE}','utf8'));
let modified=false;
// 清除 plugins.entries 中与消息渠道相关的插件
if(d.plugins && d.plugins.entries){
const channelPlugins=['openclaw-qqbot','@tencent-connect/openclaw-qqbot','openclaw-lark','@larksuite/openclaw-lark'];
channelPlugins.forEach(p=>{
if(d.plugins.entries[p]){
delete d.plugins.entries[p];
modified=true;
}
});
}
// 清除 plugins.allow 中的渠道插件
if(Array.isArray(d.plugins && d.plugins.allow)){
const beforeLen=d.plugins.allow.length;
d.plugins.allow=d.plugins.allow.filter(p=>
!p.includes('qqbot') &&
!p.includes('lark') &&
!p.includes('telegram') &&
!p.includes('discord') &&
!p.includes('slack') &&
!p.includes('whatsapp')
);
if(d.plugins.allow.length!==beforeLen)modified=true;
}
if(modified){
fs.writeFileSync('${CONFIG_FILE}',JSON.stringify(d,null,2));
console.log('CLEANED');
}
}catch(e){}
" 2>/dev/null
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
fi
# 清除飞书扩展目录中的敏感数据 (保留插件本体)
local feishu_ext_dir="${OC_STATE_DIR}/extensions/openclaw-lark"
if [ -d "$feishu_ext_dir" ]; then
# 只清除配置文件,保留插件代码
rm -f "${feishu_ext_dir}/.credentials"* 2>/dev/null
rm -f "${feishu_ext_dir}/config.json" 2>/dev/null
rm -rf "${feishu_ext_dir}/.cache" 2>/dev/null
echo -e " ${CYAN}已清理飞书插件缓存数据${NC}"
fi
# 清除 QQ 机器人扩展目录中的敏感数据
local qqbot_ext_dir="${OC_STATE_DIR}/extensions/openclaw-qqbot"
if [ -d "$qqbot_ext_dir" ]; then
rm -f "${qqbot_ext_dir}/credentials"* 2>/dev/null
rm -f "${qqbot_ext_dir}/config.json" 2>/dev/null
fi
echo -e " ${GREEN}✅ 渠道配置已清除${NC}"
echo -e " ${YELLOW}请通过菜单 [4] 重新配置消息渠道${NC}"
ask_restart

View File

@ -0,0 +1,29 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=0.1.0
PKG_RELEASE:=1
LUCI_TITLE:=LuCI support for OpenClaw Launcher
LUCI_PKGARCH:=all
LUCI_DEPENDS:=+luci-base +luci-compat +luci-lib-taskd +curl +ca-bundle +tar +xz-utils +jsonfilter
define Package/luci-app-openclawmgr/conffiles
/etc/config/openclawmgr
endef
define Package/luci-app-openclawmgr/prerm
#!/bin/sh
/etc/init.d/openclawmgr stop >/dev/null 2>&1 || true
exit 0
endef
define Package/luci-app-openclawmgr/postrm
#!/bin/sh
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null
exit 0
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,583 @@
.oclm-app {
--oclm-bg: #f5f7fb;
--oclm-card: #ffffff;
--oclm-border: #e7ebf3;
--oclm-text: #1f2937;
--oclm-subtle: #6b7280;
--oclm-line: #d9e1ee;
--oclm-primary: #2f67f6;
--oclm-primary-strong: #1f4fd4;
--oclm-danger: #ef4444;
--oclm-success: #12b981;
--oclm-warning: #f59e0b;
--oclm-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
color: var(--oclm-text);
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.oclm-app[data-darkmode="true"] {
--oclm-bg: #101722;
--oclm-card: #182130;
--oclm-border: #2a3446;
--oclm-text: #edf2f7;
--oclm-subtle: #aeb8c8;
--oclm-line: #314056;
--oclm-primary: #5b86ff;
--oclm-primary-strong: #7ea0ff;
--oclm-danger: #ff7070;
--oclm-success: #34d399;
--oclm-warning: #fbbf24;
--oclm-shadow: 0 10px 28px rgba(0, 0, 0, 0.32);
}
.oclm-root {
min-height: 100%;
background: linear-gradient(180deg, rgba(47, 103, 246, 0.06), transparent 180px), var(--oclm-bg);
padding: 20px;
}
.oclm-shell {
max-width: 920px;
}
.oclm-header {
margin-bottom: 18px;
}
.oclm-header h1 {
margin: 0 0 6px;
font-size: 30px;
line-height: 1.15;
font-weight: 800;
display: inline-flex;
align-items: center;
gap: 12px;
}
.oclm-header p {
margin: 0;
color: var(--oclm-subtle);
}
.oclm-card {
background: var(--oclm-card);
border: 1px solid var(--oclm-border);
border-radius: 18px;
box-shadow: var(--oclm-shadow);
padding: 18px 20px;
margin-bottom: 18px;
}
.oclm-card h2 {
margin: 0 0 14px;
font-size: 16px;
font-weight: 700;
}
.oclm-tabs {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 18px;
}
.oclm-tab {
appearance: none;
border: 1px solid var(--oclm-border);
background: transparent;
color: var(--oclm-subtle);
border-radius: 999px;
height: 38px;
padding: 0 16px;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: .16s ease;
}
.oclm-tab.is-active {
background: rgba(47, 103, 246, 0.10);
color: var(--oclm-primary);
border-color: rgba(47, 103, 246, 0.22);
}
.oclm-status-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px 20px;
margin-bottom: 14px;
}
.oclm-status-row {
display: flex;
gap: 10px;
align-items: center;
}
.oclm-status-label,
.oclm-meta-label {
color: var(--oclm-subtle);
font-size: 12px;
min-width: 36px;
}
.oclm-status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-weight: 700;
min-height: 22px;
}
.oclm-version-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.oclm-tag {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 30px;
padding: 0 12px;
border-radius: 999px;
background: rgba(47, 103, 246, 0.08);
color: var(--oclm-primary);
font-size: 12px;
font-weight: 700;
}
.oclm-tag-link {
text-decoration: none;
}
.oclm-tag strong {
color: var(--oclm-text);
font-weight: 700;
}
.oclm-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--oclm-warning);
}
.oclm-status-spinner {
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid rgba(47, 103, 246, 0.18);
border-top-color: var(--oclm-primary);
animation: oclm-status-spin var(--oclm-spin-duration, 15s) linear infinite;
flex: 0 0 14px;
}
.oclm-dot.is-success { background: var(--oclm-success); }
.oclm-dot.is-danger { background: var(--oclm-danger); }
@keyframes oclm-status-spin {
to { transform: rotate(360deg); }
}
.oclm-status-actions,
.oclm-danger-actions,
.oclm-origin-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.oclm-status-note {
margin-top: 10px;
color: var(--oclm-subtle);
font-size: 12px;
}
.oclm-install-inline {
display: inline-flex;
align-items: center;
gap: 18px;
flex-wrap: wrap;
}
.oclm-check {
display: inline-flex;
align-items: center;
gap: 8px;
margin-left: 10px;
color: var(--oclm-subtle);
font-size: 12px;
}
.oclm-button {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid var(--oclm-border);
background: #fff;
color: var(--oclm-text);
border-radius: 10px;
height: 36px;
padding: 0 14px;
font-size: 13px;
font-weight: 600;
line-height: 1;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: .16s ease;
}
.oclm-button-icon {
width: 16px;
height: 16px;
flex: 0 0 16px;
}
.oclm-title-icon {
width: 28px;
height: 28px;
flex: 0 0 28px;
}
.oclm-app[data-darkmode="true"] .oclm-button {
background: #162132;
}
.oclm-button:hover {
border-color: var(--oclm-primary);
color: var(--oclm-primary);
}
.oclm-button:disabled {
opacity: 0.65;
cursor: wait;
pointer-events: none;
}
.oclm-button-primary {
background: linear-gradient(180deg, var(--oclm-primary), var(--oclm-primary-strong));
color: #fff;
border-color: transparent;
}
.oclm-button-primary:hover {
color: #fff;
filter: brightness(1.03);
}
.oclm-button-danger {
color: #fff;
background: linear-gradient(180deg, #ff5d6d, #ef4444);
border-color: transparent;
}
.oclm-form-grid {
display: grid;
gap: 14px;
}
.oclm-field {
display: grid;
grid-template-columns: 150px minmax(0, 1fr);
gap: 14px;
align-items: start;
}
.oclm-label {
padding-top: 10px;
font-weight: 600;
font-size: 13px;
}
.oclm-control,
.oclm-select {
appearance: none;
width: 100%;
min-height: 40px;
border: 1px solid var(--oclm-line);
border-radius: 12px;
background: var(--oclm-card);
color: var(--oclm-text);
box-sizing: border-box;
padding: 10px 12px;
font-size: 14px;
outline: none;
}
.oclm-control::placeholder {
color: #9ca3af;
}
.oclm-select {
padding-right: 36px;
background-image:
linear-gradient(45deg, transparent 50%, #8ea0bf 50%),
linear-gradient(135deg, #8ea0bf 50%, transparent 50%);
background-position:
calc(100% - 18px) calc(50% - 2px),
calc(100% - 12px) calc(50% - 2px);
background-size: 6px 6px, 6px 6px;
background-repeat: no-repeat;
}
.oclm-control:focus,
.oclm-select:focus {
border-color: var(--oclm-primary);
box-shadow: 0 0 0 3px rgba(47, 103, 246, 0.14);
}
.oclm-password-wrap {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
}
.oclm-eye-button {
min-width: 52px;
padding: 0 12px;
}
.oclm-eye-button svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 2;
}
.oclm-hint {
margin-top: 6px;
color: var(--oclm-subtle);
font-size: 12px;
line-height: 1.45;
}
.oclm-toggle {
position: relative;
width: 48px;
height: 28px;
display: inline-flex;
align-items: center;
}
.oclm-toggle input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.oclm-toggle-track {
width: 48px;
height: 28px;
border-radius: 999px;
background: #d5dbe8;
box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.08);
position: relative;
transition: background-color .16s ease;
}
.oclm-app[data-darkmode="true"] .oclm-toggle-track {
background: #334155;
}
.oclm-toggle-track::after {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 22px;
height: 22px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,.22);
transition: transform .16s ease;
}
.oclm-toggle input:checked + .oclm-toggle-track {
background: var(--oclm-primary);
}
.oclm-toggle input:checked + .oclm-toggle-track::after {
transform: translateX(20px);
}
.oclm-inline-row {
display: flex;
align-items: center;
gap: 10px;
}
.oclm-address-row {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.oclm-address-text {
min-width: 0;
word-break: break-all;
}
.oclm-icon-button {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
border: 1px solid var(--oclm-border);
border-radius: 8px;
background: transparent;
color: var(--oclm-subtle);
cursor: pointer;
flex: 0 0 26px;
}
.oclm-icon-button:hover {
color: var(--oclm-primary);
border-color: rgba(47, 103, 246, 0.24);
}
.oclm-icon-button.is-copied {
color: var(--oclm-success);
border-color: rgba(18, 185, 129, 0.35);
background: rgba(18, 185, 129, 0.08);
}
.oclm-copy-icon {
width: 14px;
height: 14px;
}
.oclm-origin-list {
display: grid;
gap: 10px;
}
.oclm-origin-item,
.oclm-origin-new {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
}
.oclm-origin-item button,
.oclm-origin-new button {
min-width: 64px;
}
.oclm-section-submit {
margin-top: 8px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.oclm-applied-hint {
color: var(--oclm-subtle);
font-size: 12px;
}
.oclm-banner {
color: var(--oclm-danger);
margin-bottom: 12px;
}
.oclm-hidden {
display: none !important;
}
.oclm-log-modal {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(15, 23, 42, 0.42);
}
.oclm-log-dialog {
width: min(860px, 100%);
max-height: 82vh;
display: flex;
flex-direction: column;
border-radius: 18px;
background: var(--oclm-card);
border: 1px solid var(--oclm-border);
box-shadow: var(--oclm-shadow);
overflow: hidden;
}
.oclm-log-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 18px;
border-bottom: 1px solid var(--oclm-border);
}
.oclm-log-head h3 {
margin: 0;
font-size: 15px;
}
.oclm-log-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.oclm-log-body {
padding: 0;
overflow: auto;
background: #0f172a;
}
.oclm-log-body pre {
margin: 0;
padding: 16px 18px;
color: #e2e8f0;
font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
white-space: pre-wrap;
word-break: break-word;
}
@media (max-width: 720px) {
.oclm-root {
padding: 14px;
}
.oclm-header h1 {
font-size: 24px;
}
.oclm-status-grid,
.oclm-field {
grid-template-columns: 1fr;
gap: 8px;
}
.oclm-log-modal {
padding: 12px;
}
.oclm-label {
padding-top: 0;
}
}

View File

@ -0,0 +1,798 @@
(function() {
var config = window.openclawmgrConfig || {};
var container = document.getElementById("openclawmgr-app");
if (!container) {
return;
}
function parseRgb(value) {
var m = value && value.match(/\d+/g);
if (m && m.length >= 3) {
return { r: parseInt(m[0], 10), g: parseInt(m[1], 10), b: parseInt(m[2], 10) };
}
return null;
}
function detectDarkMode() {
try {
var bg = window.getComputedStyle(document.body).backgroundColor;
var rgb = parseRgb(bg);
if (rgb) {
var luminance = 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b;
return luminance < 128;
}
} catch (e) {}
return false;
}
var root = container.attachShadow ? (container.shadowRoot || container.attachShadow({ mode: "open" })) : container;
var state = {
status: null,
form: null,
options: null,
newOrigin: "",
activeTab: "basic",
savingSection: "",
lastAppliedAt: "",
showLogModal: false,
logText: "",
logTimer: null,
statusTimer: null,
installWatchTimer: null
};
var styleText = "";
function request(url, options) {
options = options || {};
options.credentials = "same-origin";
options.headers = options.headers || {};
if (config.token) {
options.headers["X-LuCI-Token"] = config.token;
}
return fetch(url, options).then(function(r) { return r.json(); });
}
function postJson(url, payload) {
payload = payload || {};
if (config.token && payload.token == null) {
payload.token = config.token;
}
return request(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
}
function postForm(url, payload) {
var body = new URLSearchParams();
payload = payload || {};
if (config.token && payload.token == null) {
payload.token = config.token;
}
Object.keys(payload || {}).forEach(function(key) {
body.append(key, payload[key]);
});
return request(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
body: body.toString()
});
}
function escapeHtml(value) {
return String(value == null ? "" : value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function modelForAgent(agent) {
return {
openai: "openai/gpt-5.2",
anthropic: "anthropic/claude-sonnet-4-6",
"minimax-cn": "minimax-cn/MiniMax-M2.5",
moonshot: "moonshot/kimi-k2.5"
}[agent] || "anthropic/claude-sonnet-4-6";
}
function modelMatchesAgent(agent, model) {
model = String(model || "");
return {
openai: /^openai\//,
anthropic: /^anthropic\//,
"minimax-cn": /^minimax-cn\//,
moonshot: /^moonshot\//
}[agent] ? ({
openai: /^openai\//,
anthropic: /^anthropic\//,
"minimax-cn": /^minimax-cn\//,
moonshot: /^moonshot\//
}[agent]).test(model) : false;
}
function resolveModelValue(form) {
var agent = form && form.default_agent ? form.default_agent : "anthropic";
var value = form && form.default_model ? String(form.default_model) : "";
if (!value || !modelMatchesAgent(agent, value)) {
return modelForAgent(agent);
}
return value;
}
function statusText(status) {
if (status.installing) return "安装中";
if (!status || !status.installed) return "未安装";
if (status.running) return "运行中";
return "已停止";
}
function statusDotClass(status) {
if (status && status.installing) return "";
if (status && status.running) return "is-success";
return "is-danger";
}
function statusSpinSeconds(status) {
return (backgroundStatusDelay(status) / 1000).toFixed(1);
}
function escapeAttr(value) {
return escapeHtml(value).replace(/'/g, "&#39;");
}
function maskedTokenUrl(url) {
var value = String(url || "");
if (!value) {
return "-";
}
return value.replace(/(#token=)([^&#]+)/, function(_, prefix, token) {
if (!token) {
return prefix;
}
var head = token.slice(0, 6);
var tail = token.length > 10 ? token.slice(-4) : "";
return prefix + head + "****" + tail;
});
}
function copyIcon(className) {
return '' +
'<svg class="' + escapeAttr(className || "") + '" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
'<rect x="9" y="9" width="10" height="10" rx="2" stroke="currentColor" stroke-width="1.8"></rect>' +
'<path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path>' +
'</svg>';
}
function copiedIcon(className) {
return '' +
'<svg class="' + escapeAttr(className || "") + '" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
'<path d="M5 12.5l4 4L19 7" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"></path>' +
'</svg>';
}
function fallbackCopyText(value) {
var el = document.createElement("textarea");
el.value = String(value || "");
el.setAttribute("readonly", "readonly");
el.style.position = "fixed";
el.style.top = "-9999px";
el.style.left = "-9999px";
document.body.appendChild(el);
el.focus();
el.select();
var ok = false;
try {
ok = document.execCommand("copy");
} catch (e) {}
document.body.removeChild(el);
return ok;
}
function copyText(value) {
value = String(value || "");
if (!value) {
return Promise.resolve(false);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(value).then(function() {
return true;
}).catch(function() {
return fallbackCopyText(value);
});
}
return Promise.resolve(fallbackCopyText(value));
}
function flashCopied(el) {
if (!el) {
return;
}
if (el._oclmCopyTimer) {
window.clearTimeout(el._oclmCopyTimer);
el._oclmCopyTimer = null;
}
el.classList.add("is-copied");
el.innerHTML = copiedIcon("oclm-copy-icon");
el._oclmCopyTimer = window.setTimeout(function() {
el.classList.remove("is-copied");
el.innerHTML = copyIcon("oclm-copy-icon");
el._oclmCopyTimer = null;
}, 1100);
}
function openclawIcon(className) {
return '' +
'<svg class="' + escapeAttr(className || "") + '" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
'<defs>' +
'<linearGradient id="oclm-lobster-gradient" x1="0%" y1="0%" x2="100%" y2="100%">' +
'<stop offset="0%" stop-color="#ff4d4d"/>' +
'<stop offset="100%" stop-color="#991b1b"/>' +
'</linearGradient>' +
'</defs>' +
'<path d="M60 10 C30 10 15 35 15 55 C15 75 30 95 45 100 L45 110 L55 110 L55 100 C55 100 60 102 65 100 L65 110 L75 110 L75 100 C90 95 105 75 105 55 C105 35 90 10 60 10Z" fill="url(#oclm-lobster-gradient)"/>' +
'<path d="M20 45 C5 40 0 50 5 60 C10 70 20 65 25 55 C28 48 25 45 20 45Z" fill="url(#oclm-lobster-gradient)"/>' +
'<path d="M100 45 C115 40 120 50 115 60 C110 70 100 65 95 55 C92 48 95 45 100 45Z" fill="url(#oclm-lobster-gradient)"/>' +
'<path d="M45 15 Q35 5 30 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>' +
'<path d="M75 15 Q85 5 90 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>' +
'<circle cx="45" cy="35" r="6" fill="#050810"/>' +
'<circle cx="75" cy="35" r="6" fill="#050810"/>' +
'<circle cx="46" cy="34" r="2.5" fill="#00e5cc"/>' +
'<circle cx="76" cy="34" r="2.5" fill="#00e5cc"/>' +
'</svg>';
}
function openLogModal() {
state.showLogModal = true;
if (state.logTimer) {
window.clearTimeout(state.logTimer);
state.logTimer = null;
}
pollLogs();
render();
}
function closeLogModal() {
state.showLogModal = false;
if (state.logTimer) {
window.clearTimeout(state.logTimer);
state.logTimer = null;
}
refreshStatus();
}
function pollLogs() {
if (!state.showLogModal) {
return;
}
if (state.logTimer) {
window.clearTimeout(state.logTimer);
state.logTimer = null;
}
request(config.logUrl + "?n=200").then(function(data) {
state.logText = (data && data.log) || "";
refreshStatus(function() {
if (state.showLogModal) {
state.logTimer = window.setTimeout(pollLogs, 3000);
}
});
}).catch(function() {
if (state.showLogModal) {
state.logTimer = window.setTimeout(pollLogs, 5000);
}
});
}
function stopStatusPolling() {
if (state.statusTimer) {
window.clearTimeout(state.statusTimer);
state.statusTimer = null;
}
}
function stopInstallWatch() {
if (state.installWatchTimer) {
window.clearTimeout(state.installWatchTimer);
state.installWatchTimer = null;
}
}
function backgroundStatusDelay(status) {
if (status && status.installing) {
return 3000;
}
if (!status || !status.installed) {
return 30000;
}
if (status.running) {
return 15000;
}
return 20000;
}
function ensureInstallWatch() {
stopInstallWatch();
if (state.status && state.status.installing && state.showLogModal) {
return;
}
state.installWatchTimer = window.setTimeout(function() {
refreshStatus(function() {
ensureInstallWatch();
});
}, backgroundStatusDelay(state.status));
}
function scheduleStatusRefresh(rounds, delay) {
rounds = typeof rounds === "number" ? rounds : 6;
delay = typeof delay === "number" ? delay : 1000;
stopStatusPolling();
function tick(remaining) {
refreshStatus(function() {
if (remaining > 1) {
state.statusTimer = window.setTimeout(function() {
tick(remaining - 1);
}, delay);
} else {
state.statusTimer = null;
}
});
}
tick(rounds);
}
function scrollLogToBottom() {
if (!state.showLogModal) {
return;
}
var body = root.querySelector(".oclm-log-body");
if (body) {
body.scrollTop = body.scrollHeight;
}
}
function render() {
var status = state.status || {};
var form = state.form || {};
var options = state.options || { base_dir_choices: [] };
var allowedOrigins = Array.isArray(form.allowed_origins) ? form.allowed_origins : [];
var baseDirOptions = (options.base_dir_choices || []).map(function(path) {
return '<option value="' + escapeHtml(path) + '"></option>';
}).join("");
var origins = allowedOrigins.map(function(item, index) {
return '' +
'<div class="oclm-origin-item">' +
'<input class="oclm-control" type="text" value="' + escapeHtml(item) + '" data-origin-index="' + index + '" />' +
'<button class="oclm-button oclm-button-danger" type="button" data-remove-origin="' + index + '">删除</button>' +
'</div>';
}).join("");
var installLabel = status.installing ? "安装中" : "立即安装";
var installAcceleratedChecked = form.install_accelerated == null ? true : form.install_accelerated === true;
var showInstallAction = !status.installed || status.installing;
var showServiceActions = status.installed && !status.installing;
var activeTab = state.activeTab || "basic";
var savingBasic = state.savingSection === "basic";
var savingAccess = state.savingSection === "access";
root.innerHTML =
'<style>' + styleText + '</style>' +
'<div class="oclm-app"' + (detectDarkMode() ? ' data-darkmode="true"' : '') + '>' +
'<div class="oclm-root">' +
'<div class="oclm-shell">' +
'<div class="oclm-header">' +
'<h1>' + openclawIcon("oclm-title-icon") + 'OpenClaw 启动器</h1>' +
'<p>在 OpenWrt 上原生安装、启动并管理官方 OpenClaw</p>' +
'</div>' +
'<section class="oclm-card">' +
'<h2>服务状态</h2>' +
'<div class="oclm-status-grid">' +
'<div class="oclm-status-row"><span class="oclm-status-label">状态</span><span class="oclm-status-pill" style="--oclm-spin-duration:' + escapeAttr(statusSpinSeconds(status)) + 's"><span class="oclm-dot ' + statusDotClass(status) + '"></span><span class="oclm-status-spinner" aria-hidden="true"></span>' + escapeHtml(statusText(status)) + '</span>' + (status.running && status.uptime_human ? ('<span class="oclm-tag">运行时间 <strong>' + escapeHtml(status.uptime_human) + '</strong></span>') : '') + '</div>' +
'<div>' + ((status.running && status.pid) ? ('<div class="oclm-inline-row"><span class="oclm-tag">PID <strong>' + escapeHtml(status.pid) + '</strong></span></div>') : '') + '</div>' +
'<div><div class="oclm-meta-label">地址</div><div class="oclm-address-row"><span class="oclm-address-text">' + escapeHtml(maskedTokenUrl(status.token_url || status.base_url || "")) + '</span>' + ((status.token_url || status.base_url) ? ('<button class="oclm-icon-button" type="button" data-copy-token-url="1" aria-label="复制地址">' + copyIcon("oclm-copy-icon") + '</button>') : '') + '</div></div>' +
'<div><div class="oclm-meta-label">目录</div><div>' + escapeHtml(status.base_dir || "-") + '</div></div>' +
'<div><div class="oclm-meta-label">版本</div><div class="oclm-version-tags"><a class="oclm-tag oclm-tag-link" href="https://github.com/openclaw/openclaw/releases" target="_blank" rel="noreferrer">OpenClaw <strong>' + escapeHtml(status.openclaw_version || "-") + '</strong></a><span class="oclm-tag">Node <strong>' + escapeHtml(status.node_version || "-") + '</strong></span></div></div>' +
'<div></div>' +
'</div>' +
'<div class="oclm-status-actions">' +
(showInstallAction ? '<div class="oclm-install-inline"><button class="oclm-button oclm-button-primary" type="button" data-install-action="1">' + installLabel + '</button><label class="oclm-check"><input type="checkbox" id="oclm-install-accelerated"' + (installAcceleratedChecked ? ' checked' : '') + ' />Kspeeder 加速安装</label></div>' : '') +
(status.running ? '<a class="oclm-button oclm-button-primary" href="' + escapeAttr(status.token_url || "#") + '" target="_blank" rel="noreferrer">' + openclawIcon("oclm-button-icon") + '打开控制台</a>' : '') +
(!status.running && showServiceActions ? '<button class="oclm-button" type="button" data-op="start">启动服务</button>' : '') +
(status.running && showServiceActions ? '<button class="oclm-button" type="button" data-op="stop">停止服务</button>' : '') +
(showServiceActions ? '<button class="oclm-button" type="button" data-op="restart">重启服务</button>' : '') +
(status.installing ? '<button class="oclm-button oclm-button-danger" type="button" data-op="cancel_install">停止安装</button>' : '') +
'</div>' +
(status.installing ? '<div class="oclm-status-note">安装任务正在后台运行,点击“安装中”可继续查看日志。</div>' : '') +
'</section>' +
'<section class="oclm-card">' +
'<div class="oclm-tabs">' +
'<button class="oclm-tab' + (activeTab === "basic" ? ' is-active' : '') + '" type="button" data-tab="basic">基础配置</button>' +
'<button class="oclm-tab' + (activeTab === "access" ? ' is-active' : '') + '" type="button" data-tab="access">访问控制</button>' +
'<button class="oclm-tab' + (activeTab === "cleanup" ? ' is-active' : '') + '" type="button" data-tab="cleanup">卸载清理</button>' +
'</div>' +
'<div class="' + (activeTab === "basic" ? '' : 'oclm-hidden') + '">' +
'<h2>基础配置</h2>' +
'<div class="oclm-form-grid">' +
fieldToggle("启用服务", "enabled", form.enabled) +
fieldInput("监听端口", '<input class="oclm-control" type="number" min="1" max="65535" id="oclm-port" value="' + escapeHtml(form.port || "18789") + '" />') +
fieldInput("监听范围", selectHtml("oclm-bind", form.bind, [
["lan", "所有地址"],
["loopback", "仅本机"],
["auto", "自动"]
])) +
fieldInput("数据目录", '<input class="oclm-control" type="text" id="oclm-base-dir" list="oclm-base-dir-options" value="' + escapeAttr(form.base_dir || "") + '" /><datalist id="oclm-base-dir-options">' + baseDirOptions + '</datalist>') +
fieldInput("默认服务提供商", selectHtml("oclm-agent", form.default_agent, [
["openai", "OpenAI"],
["anthropic", "Anthropic"],
["minimax-cn", "MiniMax CN"],
["moonshot", "Moonshot CN"]
])) +
fieldInput("API 密钥", passwordHtml("oclm-api-key", form.provider_api_key || "", "sk-...")) +
fieldInput("中转地址(可选)", '<input class="oclm-control" type="text" id="oclm-base-url" value="' + escapeHtml(form.provider_base_url || "") + '" placeholder="https://api.example.com" />') +
fieldInput("默认模型", '<input class="oclm-control" type="text" id="oclm-model" value="' + escapeAttr(resolveModelValue(form)) + '" placeholder="请按照&lt;provider&gt;/&lt;model-id&gt;格式填写" />') +
'<div class="oclm-section-submit"><button class="oclm-button oclm-button-primary" type="button" id="oclm-save-basic"' + (savingBasic ? ' disabled' : '') + '>' + (savingBasic ? '应用中…' : '保存并应用') + '</button>' + (state.lastAppliedAt ? '<span class="oclm-applied-hint">已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置</span>' : '') + '</div>' +
'</div></div>' +
'<div class="' + (activeTab === "access" ? '' : 'oclm-hidden') + '">' +
'<h2>访问控制</h2>' +
'<div class="oclm-form-grid">' +
fieldInput("访问令牌", passwordHtml("oclm-token", form.token || "", "")) +
fieldInput("允许访问来源", '<div class="oclm-origin-list">' + origins +
'<div class="oclm-origin-new"><input class="oclm-control" type="text" id="oclm-new-origin" value="' + escapeHtml(state.newOrigin || "") + '" placeholder="' + escapeHtml((options.default_origin || "http://192.168.1.1:18789")) + '" /><button class="oclm-button" type="button" id="oclm-add-origin">添加</button></div>' +
'<div class="oclm-hint">展示:仅允许来源于该地址访问控制台的控制功能</div></div>') +
fieldToggle("允许通过 HTTP 访问时认证", "allow_insecure_auth", form.allow_insecure_auth, "仅控制 HTTP 下的控制台认证,不影响端口监听") +
fieldToggle("关闭设备身份校验", "disable_device_auth", form.disable_device_auth, "警告:仅建议在可信环境中开启,在内网生效") +
'<div class="oclm-section-submit"><button class="oclm-button oclm-button-primary" type="button" id="oclm-save-access"' + (savingAccess ? ' disabled' : '') + '>' + (savingAccess ? '应用中…' : '保存访问控制设置') + '</button>' + (state.lastAppliedAt ? '<span class="oclm-applied-hint">已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置</span>' : '') + '</div>' +
'</div></div>' +
'<div class="' + (activeTab === "cleanup" ? '' : 'oclm-hidden') + '">' +
'<h2>卸载清理</h2>' +
'<div class="oclm-banner">以下操作会影响当前安装环境,请在确认后执行。</div>' +
'<div class="oclm-danger-actions">' +
'<button class="oclm-button" type="button" data-op="uninstall_openclaw">卸载 OpenClaw保留 Node</button>' +
'<button class="oclm-button oclm-button-danger" type="button" data-op="uninstall">卸载</button>' +
'<button class="oclm-button oclm-button-danger" type="button" data-op="purge">彻底清理</button>' +
'</div>' +
'</div>' +
'</section>' +
(state.showLogModal ? (
'<div class="oclm-log-modal" data-close-log="1">' +
'<div class="oclm-log-dialog" onclick="event.stopPropagation()">' +
'<div class="oclm-log-head">' +
'<h3>安装日志</h3>' +
'<div class="oclm-log-actions">' +
'<button class="oclm-button" type="button" data-refresh-log="1">刷新</button>' +
'<button class="oclm-button" type="button" data-copy-log="1">复制日志</button>' +
'<button class="oclm-button" type="button" data-close-log-btn="1">关闭</button>' +
'</div>' +
'</div>' +
'<div class="oclm-log-body"><pre>' + escapeHtml(state.logText || "日志为空") + '</pre></div>' +
'</div>' +
'</div>'
) : '') +
'</div></div></div>';
bindEvents();
scrollLogToBottom();
}
function fieldInput(label, control) {
return '<div class="oclm-field"><div class="oclm-label">' + label + '</div><div>' + control + '</div></div>';
}
function fieldToggle(label, key, checked, hint) {
return '' +
'<div class="oclm-field">' +
'<div class="oclm-label">' + label + '</div>' +
'<div><label class="oclm-toggle"><input type="checkbox" id="oclm-' + key + '"' + (checked ? ' checked' : '') + ' /><span class="oclm-toggle-track"></span></label>' +
(hint ? '<div class="oclm-hint">' + hint + '</div>' : '') +
'</div></div>';
}
function selectHtml(id, value, items) {
return '<select class="oclm-select" id="' + id + '">' + items.map(function(item) {
return '<option value="' + escapeHtml(item[0]) + '"' + (value === item[0] ? ' selected' : '') + '>' + escapeHtml(item[1]) + '</option>';
}).join("") + '</select>';
}
function passwordHtml(id, value, placeholder) {
return '' +
'<div class="oclm-password-wrap">' +
'<input class="oclm-control" type="password" id="' + id + '" value="' + escapeAttr(value || "") + '" placeholder="' + escapeAttr(placeholder || "") + '" />' +
'<button class="oclm-button oclm-eye-button" type="button" data-toggle-password="' + id + '" aria-label="显示密钥">' + eyeIcon(false) + '</button>' +
'</div>';
}
function eyeIcon(off) {
if (off) {
return '' +
'<svg viewBox="0 0 24 24" aria-hidden="true">' +
'<path d="M3 3l18 18"></path>' +
'<path d="M10.6 10.7a3 3 0 0 0 4.2 4.2"></path>' +
'<path d="M9.9 5.1A10.9 10.9 0 0 1 12 5c5.2 0 9.3 4.1 10 7-0.3 1.1-1.1 2.6-2.4 3.9"></path>' +
'<path d="M6.2 6.2C4.3 7.6 3.2 9.6 2 12c0.7 2.9 4.8 7 10 7 1 0 2-.2 2.9-.5"></path>' +
'</svg>';
}
return '' +
'<svg viewBox="0 0 24 24" aria-hidden="true">' +
'<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z"></path>' +
'<circle cx="12" cy="12" r="3"></circle>' +
'</svg>';
}
function bindEvents() {
Array.prototype.forEach.call(root.querySelectorAll("[data-tab]"), function(el) {
el.onclick = function() {
state.activeTab = el.getAttribute("data-tab") || "basic";
render();
};
});
Array.prototype.forEach.call(root.querySelectorAll("[data-op]"), function(el) {
el.onclick = function() {
var op = el.getAttribute("data-op");
if (!op) return;
if (op === "uninstall_openclaw" && !window.confirm("卸载 OpenClaw 运行时但保留 Node.js确认继续")) return;
if (op === "uninstall" && !window.confirm("卸载将删除运行时,但保留数据目录。确认继续?")) return;
if (op === "purge" && !window.confirm("彻底清理会删除运行时和数据目录。确认继续?")) return;
postForm(config.opUrl, { op: op }).then(function(rv) {
if (!rv || !rv.ok) {
window.alert((rv && rv.error) || "操作失败");
return;
}
scheduleStatusRefresh(op === "restart" ? 8 : 6, 1000);
});
};
});
Array.prototype.forEach.call(root.querySelectorAll("[data-install-action]"), function(el) {
el.onclick = function() {
openLogModal();
var accelerated = !!(root.getElementById("oclm-install-accelerated") && root.getElementById("oclm-install-accelerated").checked);
if (!state.status || state.status.installing) {
return;
}
state.status.installing = true;
state.status.task_running = true;
state.status.task_op = "install";
render();
postJson(config.configUrl, { install_accelerated: accelerated }).then(function(cfgRv) {
if (!cfgRv || !cfgRv.ok) {
state.status.installing = false;
render();
window.alert((cfgRv && cfgRv.error) || "保存安装选项失败");
return;
}
state.form.install_accelerated = accelerated;
return postForm(config.opUrl, { op: "install" });
}).then(function(rv) {
if (!rv) return;
if (!rv || !rv.ok) {
state.status.installing = false;
render();
window.alert((rv && rv.error) || "启动安装失败");
return;
}
scheduleStatusRefresh(10, 1000);
}).catch(function() {
state.status.installing = false;
render();
window.alert("启动安装失败");
});
};
});
Array.prototype.forEach.call(root.querySelectorAll("[data-close-log], [data-close-log-btn]"), function(el) {
el.onclick = function() {
closeLogModal();
};
});
Array.prototype.forEach.call(root.querySelectorAll("[data-toggle-password]"), function(el) {
el.onclick = function() {
var id = el.getAttribute("data-toggle-password");
var input = id ? root.getElementById(id) : null;
if (!input) return;
input.type = input.type === "password" ? "text" : "password";
el.innerHTML = eyeIcon(input.type !== "password");
el.setAttribute("aria-label", input.type === "password" ? "显示密钥" : "隐藏密钥");
};
});
var installAccelerated = root.getElementById("oclm-install-accelerated");
if (installAccelerated) {
installAccelerated.onchange = function() {
state.form.install_accelerated = !!installAccelerated.checked;
};
}
Array.prototype.forEach.call(root.querySelectorAll("[data-refresh-log]"), function(el) {
el.onclick = function() {
pollLogs();
};
});
Array.prototype.forEach.call(root.querySelectorAll("[data-copy-log]"), function(el) {
el.onclick = function() {
copyText(state.logText || "");
};
});
Array.prototype.forEach.call(root.querySelectorAll("[data-copy-token-url]"), function(el) {
el.onclick = function() {
var value = (state.status && (state.status.token_url || state.status.base_url)) || "";
copyText(value).then(function(ok) {
if (ok) {
flashCopied(el);
}
});
};
});
var agent = root.getElementById("oclm-agent");
if (agent) {
agent.onchange = function() {
state.form.default_agent = agent.value;
var model = root.getElementById("oclm-model");
if (model) {
var nextModel = modelForAgent(agent.value);
model.value = nextModel;
state.form.default_model = nextModel;
}
};
}
var addOrigin = root.getElementById("oclm-add-origin");
if (addOrigin) {
addOrigin.onclick = function() {
var input = root.getElementById("oclm-new-origin");
var value = input && input.value ? input.value.trim() : "";
if (!value) return;
state.form.allowed_origins = Array.isArray(state.form.allowed_origins) ? state.form.allowed_origins : [];
state.form.allowed_origins.push(value);
state.newOrigin = "";
render();
};
}
Array.prototype.forEach.call(root.querySelectorAll("[data-remove-origin]"), function(el) {
el.onclick = function() {
var index = parseInt(el.getAttribute("data-remove-origin"), 10);
if (isNaN(index)) return;
state.form.allowed_origins.splice(index, 1);
render();
};
});
Array.prototype.forEach.call(root.querySelectorAll("[data-origin-index]"), function(el) {
el.oninput = function() {
var index = parseInt(el.getAttribute("data-origin-index"), 10);
if (!isNaN(index)) {
state.form.allowed_origins[index] = el.value;
}
};
});
var saveBasic = root.getElementById("oclm-save-basic");
if (saveBasic) {
saveBasic.onclick = function() {
var payload = {
enabled: !!root.getElementById("oclm-enabled").checked,
port: root.getElementById("oclm-port").value,
bind: root.getElementById("oclm-bind").value,
base_dir: root.getElementById("oclm-base-dir").value,
default_agent: root.getElementById("oclm-agent").value,
default_model: root.getElementById("oclm-model").value,
install_accelerated: !!(root.getElementById("oclm-install-accelerated") && root.getElementById("oclm-install-accelerated").checked),
provider_api_key: root.getElementById("oclm-api-key").value,
provider_base_url: root.getElementById("oclm-base-url").value
};
state.savingSection = "basic";
render();
postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "basic"); }).catch(function() {
state.savingSection = "";
render();
window.alert("保存失败");
});
};
}
var saveAccess = root.getElementById("oclm-save-access");
if (saveAccess) {
saveAccess.onclick = function() {
var payload = {
token: root.getElementById("oclm-token").value,
allowed_origins: (Array.isArray(state.form.allowed_origins) ? state.form.allowed_origins : []).map(function(item) { return item.trim(); }).filter(Boolean),
allow_insecure_auth: !!root.getElementById("oclm-allow_insecure_auth").checked,
disable_device_auth: !!root.getElementById("oclm-disable_device_auth").checked
};
state.savingSection = "access";
render();
postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "access"); }).catch(function() {
state.savingSection = "";
render();
window.alert("保存失败");
});
};
}
}
function handleSaveResult(rv, section) {
if (!rv || !rv.ok) {
state.savingSection = "";
render();
window.alert((rv && rv.error) || "保存失败");
return;
}
postForm(config.applyUrl, {}).then(function(applyRv) {
state.savingSection = "";
if (!applyRv || !applyRv.ok) {
render();
window.alert((applyRv && applyRv.error) || "应用配置失败");
loadConfig();
scheduleStatusRefresh(3, 600);
return;
}
state.lastAppliedAt = applyRv.applied_at || "";
render();
loadConfig();
scheduleStatusRefresh(6, 800);
}).catch(function() {
state.savingSection = "";
render();
window.alert("应用配置失败");
loadConfig();
scheduleStatusRefresh(3, 600);
});
}
function refreshStatus(done) {
request(config.statusUrl).then(function(data) {
state.status = data || {};
render();
ensureInstallWatch();
if (typeof done === "function") {
done();
}
}).catch(function() {
state.status = state.status || {};
render();
ensureInstallWatch();
if (typeof done === "function") {
done();
}
});
}
function loadConfig() {
request(config.configUrl).then(function(data) {
if (!data || !data.ok) {
return;
}
state.form = data.config || {};
state.form.default_model = resolveModelValue(state.form);
state.options = data.options || {};
render();
refreshStatus();
}).catch(function() {
state.form = state.form || {};
state.options = state.options || { base_dir_choices: [] };
render();
});
}
fetch(config.staticBase + "/app.css").then(function(r) { return r.text(); }).then(function(css) {
styleText = css || "";
loadConfig();
});
})();

View File

@ -0,0 +1,17 @@
<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="lobster-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ff4d4d"/>
<stop offset="100%" stop-color="#991b1b"/>
</linearGradient>
</defs>
<path d="M60 10 C30 10 15 35 15 55 C15 75 30 95 45 100 L45 110 L55 110 L55 100 C55 100 60 102 65 100 L65 110 L75 110 L75 100 C90 95 105 75 105 55 C105 35 90 10 60 10Z" fill="url(#lobster-gradient)"/>
<path d="M20 45 C5 40 0 50 5 60 C10 70 20 65 25 55 C28 48 25 45 20 45Z" fill="url(#lobster-gradient)"/>
<path d="M100 45 C115 40 120 50 115 60 C110 70 100 65 95 55 C92 48 95 45 100 45Z" fill="url(#lobster-gradient)"/>
<path d="M45 15 Q35 5 30 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>
<path d="M75 15 Q85 5 90 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>
<circle cx="45" cy="35" r="6" fill="#050810"/>
<circle cx="75" cy="35" r="6" fill="#050810"/>
<circle cx="46" cy="34" r="2.5" fill="#00e5cc"/>
<circle cx="76" cy="34" r="2.5" fill="#00e5cc"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,784 @@
module("luci.controller.openclawmgr", package.seeall)
function index()
local fs = require "nixio.fs"
if not fs.access("/etc/config/openclawmgr") then
return
end
entry({"admin", "services", "openclawmgr"}, alias("admin", "services", "openclawmgr", "config"), _("OpenClaw 启动器"), 90).dependent = true
local page = entry({"admin", "services", "openclawmgr", "config"}, call("action_app"), _("Config"), 10)
page.leaf = true
entry({"admin", "services", "openclawmgr", "status"}, call("action_status")).leaf = true
entry({"admin", "services", "openclawmgr", "op"}, call("action_op")).leaf = true
entry({"admin", "services", "openclawmgr", "config_data"}, call("action_config_data")).leaf = true
entry({"admin", "services", "openclawmgr", "apply_config"}, call("action_apply_config")).leaf = true
entry({"admin", "services", "openclawmgr", "log"}, call("action_log")).leaf = true
entry({"admin", "services", "openclawmgr", "diag_info"}, call("action_diag_info")).leaf = true
entry({"admin", "services", "openclawmgr", "diag_run"}, call("action_diag_run")).leaf = true
entry({"admin", "services", "openclawmgr", "diag_poll"}, call("action_diag_poll")).leaf = true
end
function action_app()
local http = require "luci.http"
local tmpl = require "luci.template"
local disp = require "luci.dispatcher"
local ctx = disp.context or {}
tmpl.render("openclawmgr/app", {
token = ctx.token or "",
})
http.close()
end
local function lan_ipv4()
local sys = require "luci.sys"
local jsonc = require "luci.jsonc"
local raw = sys.exec("ubus call network.interface.lan status 2>/dev/null")
local obj = jsonc.parse(raw)
if obj and obj["ipv4-address"] then
for _, addr in ipairs(obj["ipv4-address"]) do
if addr.address and addr.address ~= "" then
return addr.address
end
end
end
return ""
end
local function default_allowed_origin(port_val)
local ip = lan_ipv4()
if ip ~= "" then
return "http://" .. ip .. ":" .. port_val
end
return ""
end
local function write_json(obj)
local http = require "luci.http"
http.prepare_content("application/json")
http.write_json(obj)
end
local function read_json_body()
local http = require "luci.http"
local jsonc = require "luci.jsonc"
local ctype = http.getenv("CONTENT_TYPE") or ""
if not ctype:match("^application/json") then
return nil
end
local raw = http.content() or ""
if #raw == 0 then
return nil
end
local obj = jsonc.parse(raw)
if type(obj) ~= "table" then
return nil
end
return obj
end
local function get_task_state(task_id)
local fs = require "nixio.fs"
local jsonc = require "luci.jsonc"
local sys = require "luci.sys"
if not fs.access("/etc/init.d/tasks") then
return {
running = false,
op = "",
command = "",
}
end
local raw = sys.exec("/etc/init.d/tasks task_status " .. task_id .. " 2>/dev/null")
local obj = jsonc.parse(raw) or {}
local cmd = ""
local cmd_parts = {}
local function as_bool(v)
return v == true or v == "true" or v == "1" or v == 1
end
if type(obj.command) == "table" then
cmd_parts = obj.command
cmd = table.concat(obj.command, " ")
elseif type(obj.command) == "string" then
cmd = obj.command
elseif type(obj.data) == "table" and type(obj.data.command) == "string" then
cmd = obj.data.command
end
local op = ""
for _, candidate in ipairs({ "install", "upgrade", "uninstall_openclaw", "uninstall", "purge", "restart", "start", "stop" }) do
if cmd:match("(^|[^%w_])" .. candidate .. "([^%w_]|$)") then
op = candidate
break
end
for _, part in ipairs(cmd_parts) do
if type(part) == "string" and part:match("(^|[^%w_])" .. candidate .. "([^%w_]|$)") then
op = candidate
break
end
end
if op ~= "" then
break
end
end
return {
running = as_bool(obj.running),
op = op,
command = cmd,
pid = obj.pid and tostring(obj.pid) or "",
}
end
local function require_csrf()
local http = require "luci.http"
local disp = require "luci.dispatcher"
local method = http.getenv("REQUEST_METHOD") or ""
if method ~= "POST" then
return true
end
local ctx = disp.context
if not (ctx and ctx.authsession) then
write_json({ ok = false, error = "auth session missing" })
return false
end
local expected = ctx.token
local header_token = http.getenv("HTTP_X_LUCI_TOKEN")
local form_token = http.formvalue("token")
local body = read_json_body()
local body_token = (type(body) == "table") and body["token"] or nil
local provided = header_token or form_token or body_token
if expected and provided ~= expected then
write_json({ ok = false, error = "bad csrf token" })
return false
end
if not expected and (not provided or #provided == 0) then
write_json({ ok = false, error = "csrf token missing" })
return false
end
return true
end
local function get_host()
local http = require "luci.http"
local host = http.getenv("HTTP_HOST") or http.getenv("SERVER_NAME") or ""
host = host:gsub(":%d+$", "")
if host == "_redirect2ssl" or host == "redirect2ssl" or host == "" then
host = http.getenv("SERVER_ADDR") or "localhost"
end
return host
end
local function configured_base_path(base_dir)
local jsonc = require "luci.jsonc"
local path = (base_dir or "/opt/openclawmgr") .. "/data/.openclaw/openclaw.json"
local f = io.open(path, "r")
if not f then
return "/"
end
local raw = f:read("*a") or ""
f:close()
local obj = jsonc.parse(raw)
local base_path = obj
and obj.gateway
and obj.gateway.controlUi
and obj.gateway.controlUi.basePath
if type(base_path) ~= "string" or base_path == "" or base_path == "/" then
return "/"
end
if base_path:sub(1, 1) ~= "/" then
base_path = "/" .. base_path
end
return base_path:gsub("/+$", "") .. "/"
end
local function fmt_elapsed(seconds)
seconds = tonumber(seconds) or 0
if seconds < 60 then
return string.format("%ds", seconds)
elseif seconds < 3600 then
return string.format("%dm%02ds", math.floor(seconds / 60), seconds % 60)
elseif seconds < 86400 then
return string.format("%dh%02dm", math.floor(seconds / 3600), math.floor((seconds % 3600) / 60))
end
return string.format("%dd%02dh", math.floor(seconds / 86400), math.floor((seconds % 86400) / 3600))
end
local function get_running_pid()
local sys = require "luci.sys"
local jsonc = require "luci.jsonc"
local raw = sys.exec("ubus call service list '{\"name\":\"openclawmgr\"}' 2>/dev/null")
local obj = jsonc.parse(raw) or {}
local svc = obj.openclawmgr
if type(svc) ~= "table" or type(svc.instances) ~= "table" then
return ""
end
local inst = svc.instances.gateway
if type(inst) == "table" and inst.pid then
return tostring(inst.pid)
end
return ""
end
local function installer_lock_pid()
local f = io.open("/tmp/openclawmgr-installer.lock/pid", "r")
if not f then
return ""
end
local pid = (f:read("*l") or ""):gsub("%s+$", "")
f:close()
return pid
end
local function installer_lock_running()
local sys = require "luci.sys"
local pid = installer_lock_pid()
if pid ~= "" and sys.call("kill -0 " .. pid .. " >/dev/null 2>&1") == 0 then
local f = io.open("/proc/" .. pid .. "/cmdline", "r")
local cmdline = ""
if f then
cmdline = (f:read("*a") or ""):gsub("%z", " ")
f:close()
end
if cmdline:match("openclawmgr%.sh") and (cmdline:match(" install([%s]|$)") or cmdline:match(" upgrade([%s]|$)")) then
return true, pid
end
end
return false, pid
end
local function get_pid_uptime_human(pid)
if not pid or pid == "" then
return ""
end
local f = io.open("/proc/" .. pid .. "/stat", "r")
if not f then
return ""
end
local stat_line = f:read("*l") or ""
f:close()
local uf = io.open("/proc/uptime", "r")
if not uf then
return ""
end
local uptime_line = uf:read("*l") or ""
uf:close()
local after = stat_line:match("%) (.+)$")
if not after then
return ""
end
local fields = {}
for part in after:gmatch("%S+") do
fields[#fields + 1] = part
end
local start_ticks = tonumber(fields[20] or "")
local system_uptime = tonumber((uptime_line:match("^(%S+)"))) or 0
if not start_ticks or system_uptime <= 0 then
return ""
end
local ticks_per_sec = 100
local elapsed = math.floor(system_uptime - (start_ticks / ticks_per_sec))
if elapsed < 0 then
elapsed = 0
end
return fmt_elapsed(elapsed)
end
function action_status()
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
local task = get_task_state("openclawmgr")
local enabled = uci:get("openclawmgr", "main", "enabled") or "0"
local port = uci:get("openclawmgr", "main", "port") or "18789"
local bind = uci:get("openclawmgr", "main", "bind") or "lan"
local base_dir = uci:get("openclawmgr", "main", "base_dir") or "/opt/openclawmgr"
local token = uci:get("openclawmgr", "main", "token") or ""
if not port:match("^%d+$") then port = "18789" end
local st = sys.exec("/usr/libexec/istorec/openclawmgr.sh status 2>/dev/null"):gsub("%s+$", "")
local running = (st == "running")
local installed = (st == "running" or st == "stopped")
local lock_running, lock_pid = installer_lock_running()
local installing = (task.running and (task.op == "install" or task.op == "upgrade")) or lock_running
if not installing and task.running and not installed and not running then
installing = true
end
local node_ver = sys.exec("/usr/libexec/istorec/openclawmgr.sh node_version 2>/dev/null"):gsub("%s+$", "")
local oc_ver = sys.exec("/usr/libexec/istorec/openclawmgr.sh openclaw_version 2>/dev/null"):gsub("%s+$", "")
local pid = running and get_running_pid() or ""
if pid == "" and installing and lock_pid ~= "" then
pid = lock_pid
end
local uptime_human = running and get_pid_uptime_human(pid) or ""
local base_url = "http://" .. get_host() .. ":" .. port .. configured_base_path(base_dir)
local token_url = base_url
if token ~= "" then
token_url = token_url .. "#token=" .. token
end
write_json({
ok = true,
enabled = enabled,
installed = installed,
running = running,
task_running = task.running,
task_op = task.op,
installing = installing,
port = port,
bind = bind,
base_dir = base_dir,
token = token,
node_version = node_ver,
openclaw_version = oc_ver,
pid = pid,
uptime_human = uptime_human,
base_url = base_url,
token_url = token_url,
url = token_url,
})
end
function action_log()
local sys = require "luci.sys"
local util = require "luci.util"
local uci = require "luci.model.uci".cursor()
local base_dir = uci:get("openclawmgr", "main", "base_dir") or "/opt/openclawmgr"
local log_file = base_dir .. "/log/installer.log"
local n = tonumber((require "luci.http").formvalue("n") or "") or 200
if n < 10 then n = 10 end
if n > 2000 then n = 2000 end
local content = sys.exec("tail -n " .. n .. " " .. util.shellquote(log_file) .. " 2>/dev/null") or ""
write_json({ ok = true, log = content })
end
function action_apply_config()
local sys = require "luci.sys"
if not require_csrf() then
return
end
local rc = sys.call("/usr/libexec/istorec/openclawmgr.sh apply_config >/dev/null 2>&1")
if rc ~= 0 then
write_json({ ok = false, error = "apply config failed" })
return
end
write_json({
ok = true,
applied_at = os.date("%Y-%m-%d %H:%M:%S"),
})
end
function action_config_data()
local http = require "luci.http"
local jsonc = require "luci.jsonc"
local uci = require "luci.model.uci".cursor()
local model = require "luci.model.openclawmgr"
if (http.getenv("REQUEST_METHOD") or "GET") == "POST" then
if not require_csrf() then
return
end
local body = read_json_body() or {}
local section = "main"
local function bool_to_uci(value)
return value and "1" or "0"
end
local function has(key)
return body[key] ~= nil
end
if has("enabled") then
uci:set("openclawmgr", section, "enabled", bool_to_uci(body.enabled == true or body.enabled == "1"))
end
if has("port") then
local port = tostring(body.port or "")
if not port:match("^%d+$") then
write_json({ ok = false, error = "invalid port" })
return
end
local port_num = tonumber(port) or 0
if port_num < 1 or port_num > 65535 then
write_json({ ok = false, error = "invalid port" })
return
end
uci:set("openclawmgr", section, "port", port)
end
if has("bind") then
local bind = tostring(body.bind or "")
if bind ~= "loopback" and bind ~= "lan" and bind ~= "auto" and bind ~= "tailnet" and bind ~= "custom" then
write_json({ ok = false, error = "invalid bind" })
return
end
uci:set("openclawmgr", section, "bind", bind)
end
if has("base_dir") then
local base_dir = tostring(body.base_dir or "")
if base_dir == "" then
write_json({ ok = false, error = "base_dir required" })
return
end
uci:set("openclawmgr", section, "base_dir", base_dir)
end
if has("default_agent") then
local agent = tostring(body.default_agent or "")
if agent ~= "openai" and agent ~= "anthropic" and agent ~= "minimax-cn" and agent ~= "moonshot" then
write_json({ ok = false, error = "invalid default_agent" })
return
end
uci:set("openclawmgr", section, "default_agent", agent)
end
if has("default_model") then
uci:set("openclawmgr", section, "default_model", tostring(body.default_model or ""))
end
if has("install_accelerated") then
uci:set("openclawmgr", section, "install_accelerated", bool_to_uci(body.install_accelerated == true or body.install_accelerated == "1"))
end
if has("provider_api_key") then
uci:set("openclawmgr", section, "provider_api_key", tostring(body.provider_api_key or ""))
end
if has("provider_base_url") then
local value = tostring(body.provider_base_url or "")
if value ~= "" and not value:match("^https?://") then
write_json({ ok = false, error = "invalid provider_base_url" })
return
end
uci:set("openclawmgr", section, "provider_base_url", value)
end
if has("token") then
uci:set("openclawmgr", section, "token", tostring(body.token or ""))
end
if has("allowed_origins") then
local origins = {}
if type(body.allowed_origins) == "table" then
for _, item in ipairs(body.allowed_origins) do
item = tostring(item or ""):match("^%s*(.-)%s*$")
if item and item ~= "" then
table.insert(origins, item)
end
end
end
if #origins > 0 then
uci:set_list("openclawmgr", section, "allowed_origins", origins)
else
uci:delete("openclawmgr", section, "allowed_origins")
end
end
if has("allow_insecure_auth") then
uci:set("openclawmgr", section, "allow_insecure_auth", bool_to_uci(body.allow_insecure_auth == true or body.allow_insecure_auth == "1"))
end
if has("disable_device_auth") then
uci:set("openclawmgr", section, "disable_device_auth", bool_to_uci(body.disable_device_auth == true or body.disable_device_auth == "1"))
end
uci:commit("openclawmgr")
write_json({ ok = true })
return
end
local base_dir = uci:get("openclawmgr", "main", "base_dir") or "/opt/openclawmgr"
local port = uci:get("openclawmgr", "main", "port") or "18789"
local blocks = model.blocks()
local home = model.home()
local paths, default_path = model.find_paths(blocks, home, "Configs")
local choices = {}
local seen = {}
local function add_choice(path)
if path and path ~= "" and not seen[path] then
table.insert(choices, path)
seen[path] = true
end
end
add_choice(base_dir)
for _, path in ipairs(paths or {}) do
add_choice(path)
end
add_choice(default_path or "/opt/openclawmgr")
local allowed_origins = {}
for _, item in ipairs(uci:get_list("openclawmgr", "main", "allowed_origins") or {}) do
allowed_origins[#allowed_origins + 1] = item
end
write_json({
ok = true,
config = {
enabled = (uci:get("openclawmgr", "main", "enabled") or "0") == "1",
port = port,
bind = uci:get("openclawmgr", "main", "bind") or "lan",
base_dir = base_dir,
token = uci:get("openclawmgr", "main", "token") or "",
allowed_origins = allowed_origins,
allow_insecure_auth = (uci:get("openclawmgr", "main", "allow_insecure_auth") or "0") == "1",
disable_device_auth = (uci:get("openclawmgr", "main", "disable_device_auth") or "0") == "1",
default_agent = uci:get("openclawmgr", "main", "default_agent") or "anthropic",
default_model = uci:get("openclawmgr", "main", "default_model") or "",
install_accelerated = (uci:get("openclawmgr", "main", "install_accelerated") or "1") == "1",
provider_api_key = uci:get("openclawmgr", "main", "provider_api_key") or "",
provider_base_url = uci:get("openclawmgr", "main", "provider_base_url") or "",
},
options = {
base_dir_choices = choices,
default_origin = default_allowed_origin(port),
}
})
end
function action_op()
local http = require "luci.http"
local i18n = require "luci.i18n"
local sys = require "luci.sys"
local util = require "luci.util"
local fs = require "nixio.fs"
local tasks = nil
do
local ok, mod = pcall(require, "luci.model.tasks")
if ok then tasks = mod end
end
if not require_csrf() then
return
end
local op = http.formvalue("op") or ""
local ok_ops = { install = true, upgrade = true, start = true, stop = true, restart = true, apply_config = true, uninstall = true, uninstall_openclaw = true, purge = true, cancel_install = true }
if not ok_ops[op] then
write_json({ ok = false, error = "unknown op" })
return
end
local task_id = "openclawmgr"
local script_path = "/usr/libexec/istorec/openclawmgr.sh"
if op == "cancel_install" then
if not fs.access("/etc/init.d/tasks") then
write_json({ ok = false, error = i18n.translate("taskd is not available") })
return
end
local task = get_task_state(task_id)
local st = sys.exec("/usr/libexec/istorec/openclawmgr.sh status 2>/dev/null"):gsub("%s+$", "")
local installed = (st == "running" or st == "stopped")
local install_like = task.running and (task.op == "install" or task.op == "upgrade" or not installed)
if not install_like then
write_json({ ok = false, error = i18n.translate("No install task is running") })
return
end
local rc = sys.call("/etc/init.d/tasks task_del " .. task_id .. " >/dev/null 2>&1")
if rc ~= 0 and task.pid ~= "" then
sys.call("kill -TERM " .. util.shellquote(task.pid) .. " >/dev/null 2>&1")
sys.call("sleep 1")
sys.call("kill -KILL " .. util.shellquote(task.pid) .. " >/dev/null 2>&1")
rc = sys.call("/etc/init.d/tasks task_del " .. task_id .. " >/dev/null 2>&1")
end
if rc == 0 or not get_task_state(task_id).running then
sys.call("rm -rf /tmp/openclawmgr-installer.lock >/dev/null 2>&1")
write_json({ ok = true, canceled = true, task_id = task_id })
return
end
write_json({ ok = false, error = i18n.translate("Failed to stop installation"), task_id = task_id, pid = task.pid })
return
end
if not fs.access("/etc/init.d/tasks") then
-- fallback (shouldn't happen on iStoreOS with luci-lib-taskd installed)
local cmd = script_path .. " " .. util.shellquote(op)
sys.exec("( " .. cmd .. " ) >/dev/null 2>&1 &")
write_json({ ok = true, queued = true, task_id = task_id, warning = "taskd missing; fallback to background exec" })
return
end
local cmd = string.format("\"%s\" %s", script_path, op)
local rc = sys.call("/etc/init.d/tasks task_add " .. task_id .. " " .. util.shellquote(cmd) .. " >/dev/null 2>&1")
if rc == 0 then
write_json({ ok = true, task_id = task_id })
return
end
-- busy: try to report which task is running
local running_task = ""
if tasks and tasks.status then
local all = tasks.status("") or {}
for id, st in pairs(all) do
if type(st) == "table" and st.running then
running_task = id
break
end
end
end
write_json({ ok = false, busy = true, task_id = task_id, running_task_id = running_task })
end
local function file_read(path)
local f = io.open(path, "r")
if not f then return nil end
local c = f:read("*a")
f:close()
return c
end
function action_diag_info()
local sys = require "luci.sys"
local util = require "luci.util"
local uci = require "luci.model.uci".cursor()
local base_dir = uci:get("openclawmgr", "main", "base_dir") or "/opt/openclawmgr"
local port = uci:get("openclawmgr", "main", "port") or "18789"
local bind = uci:get("openclawmgr", "main", "bind") or "lan"
local enabled = uci:get("openclawmgr", "main", "enabled") or "0"
if not port:match("^%d+$") then port = "18789" end
local df_kb = sys.exec("df -kP " .. util.shellquote(base_dir) .. " 2>/dev/null | awk 'NR==2{print $4}'"):gsub("%s+", "")
local avail_mb = math.floor((tonumber(df_kb) or 0) / 1024)
local node_ver = sys.exec("/usr/libexec/istorec/openclawmgr.sh node_version 2>/dev/null"):gsub("%s+$", "")
local oc_ver = sys.exec("/usr/libexec/istorec/openclawmgr.sh openclaw_version 2>/dev/null"):gsub("%s+$", "")
local has_node = sys.call("[ -x " .. util.shellquote(base_dir .. "/node/bin/node") .. " ] >/dev/null 2>&1") == 0
local has_openclaw = sys.call("[ -x " .. util.shellquote(base_dir .. "/global/bin/openclaw") .. " ] >/dev/null 2>&1") == 0
local has_config = sys.call("[ -f " .. util.shellquote(base_dir .. "/data/.openclaw/openclaw.json") .. " ] >/dev/null 2>&1") == 0
local default_gw = sys.exec("ip route 2>/dev/null | awk '/^default/{print $3; exit}' 2>/dev/null"):gsub("%s+", "")
local svc = util.ubus("service", "list", { name = "openclawmgr" })
local pid = ""
if type(svc) == "table" and type(svc.openclawmgr) == "table" and type(svc.openclawmgr.instances) == "table" then
local inst = svc.openclawmgr.instances.gateway
if type(inst) == "table" and inst.pid then
pid = tostring(inst.pid)
end
end
local running = pid ~= ""
write_json({
ok = true,
base_dir = base_dir,
available_mb = avail_mb,
enabled = enabled,
port = port,
bind = bind,
default_gw = default_gw,
node_version = node_ver,
openclaw_version = oc_ver,
has_node = has_node,
has_openclaw = has_openclaw,
has_config = has_config,
procd_pid = pid,
running = running,
url = "http://" .. get_host() .. ":" .. port .. configured_base_path(base_dir),
})
end
function action_diag_run()
local http = require "luci.http"
local sys = require "luci.sys"
local util = require "luci.util"
if not require_csrf() then
return
end
local op = http.formvalue("op") or ""
local limit = http.formvalue("limit") or ""
local ok_ops = {
doctor = true,
gateway_status = true,
gateway_health = true,
logs = true,
channels_status = true,
}
if not ok_ops[op] then
write_json({ ok = false, error = "unknown op" })
return
end
local cmd = "/usr/libexec/istorec/openclawmgr.sh diag " .. util.shellquote(op)
if op == "logs" and limit:match("^%d+$") then
cmd = cmd .. " " .. util.shellquote(limit)
end
sys.exec("( " .. cmd .. " ) >/dev/null 2>&1 &")
write_json({ ok = true, queued = true })
end
function action_diag_poll()
local http = require "luci.http"
local sys = require "luci.sys"
local util = require "luci.util"
local op = http.formvalue("op") or ""
local ok_ops = {
doctor = true,
gateway_status = true,
gateway_health = true,
logs = true,
channels_status = true,
}
if not ok_ops[op] then
write_json({ ok = false, error = "unknown op" })
return
end
local st = sys.exec("/usr/libexec/istorec/openclawmgr.sh diag_poll " .. util.shellquote(op) .. " 2>/dev/null"):gsub("%s+$", "")
local log = file_read("/tmp/openclawmgr-diag-" .. op .. ".log") or ""
local exit_code = nil
local state = "idle"
if st == "running" then
state = "running"
elseif st:match("^done:") then
state = "done"
exit_code = tonumber(st:gsub("^done:", "")) or -1
end
write_json({
ok = true,
state = state,
exit_code = exit_code,
log = log,
})
end

View File

@ -0,0 +1,54 @@
local jsonc = require "luci.jsonc"
local openclawmgr = {}
openclawmgr.blocks = function()
local f = io.popen("lsblk -s -f -b -o NAME,FSSIZE,MOUNTPOINT --json", "r")
local vals = {}
if f then
local ret = f:read("*all")
f:close()
local obj = jsonc.parse(ret)
for _, val in pairs(obj and obj["blockdevices"] or {}) do
local fsize = val["fssize"]
if fsize ~= nil and string.len(fsize) > 10 and val["mountpoint"] then
vals[#vals + 1] = val["mountpoint"]
end
end
end
return vals
end
openclawmgr.home = function()
local uci = require "luci.model.uci".cursor()
local home_dirs = {}
home_dirs["main_dir"] = uci:get_first("quickstart", "main", "main_dir", "/root")
home_dirs["Configs"] = uci:get_first("quickstart", "main", "conf_dir", home_dirs["main_dir"] .. "/Configs")
home_dirs["Public"] = uci:get_first("quickstart", "main", "pub_dir", home_dirs["main_dir"] .. "/Public")
home_dirs["Downloads"] = uci:get_first("quickstart", "main", "dl_dir", home_dirs["Public"] .. "/Downloads")
home_dirs["Caches"] = uci:get_first("quickstart", "main", "tmp_dir", home_dirs["main_dir"] .. "/Caches")
return home_dirs
end
openclawmgr.find_paths = function(blocks, home_dirs, path_name)
local default_path = ""
local paths = {}
default_path = home_dirs[path_name] .. "/OpenClawMgr"
if #blocks == 0 then
table.insert(paths, default_path)
else
for _, val in pairs(blocks) do
table.insert(paths, val .. "/" .. path_name .. "/OpenClawMgr")
end
local without_conf_dir = "/root/" .. path_name .. "/OpenClawMgr"
if default_path == without_conf_dir then
default_path = paths[1]
end
end
return paths, default_path
end
return openclawmgr

View File

@ -0,0 +1,19 @@
<%+header%>
<script type="text/javascript">
(function() {
window.openclawmgrConfig = {
token: "<%=token or ''%>",
statusUrl: "<%=url('admin/services/openclawmgr/status')%>",
opUrl: "<%=url('admin/services/openclawmgr/op')%>",
configUrl: "<%=url('admin/services/openclawmgr/config_data')%>",
applyUrl: "<%=url('admin/services/openclawmgr/apply_config')%>",
logUrl: "<%=url('admin/services/openclawmgr/log')%>",
staticBase: "<%=resource%>/openclawmgr"
};
})();
</script>
<div class="openclawmgr-host">
<div id="openclawmgr-app" data-openclawmgr-shadow></div>
</div>
<script type="text/javascript" src="<%=resource%>/openclawmgr/app.js?v=<%=os.time()%>"></script>
<%+footer%>

View File

@ -0,0 +1,258 @@
<%+header%>
<%
local info_url = luci.dispatcher.build_url("admin", "services", "openclawmgr", "diag_info")
local run_url = luci.dispatcher.build_url("admin", "services", "openclawmgr", "diag_run")
local poll_url = luci.dispatcher.build_url("admin", "services", "openclawmgr", "diag_poll")
local op_url = luci.dispatcher.build_url("admin", "services", "openclawmgr", "op")
local log_url = luci.dispatcher.build_url("admin", "services", "openclawmgr", "log")
local token = (luci.dispatcher.context and luci.dispatcher.context.token) or ""
%>
<%+tasks/embed%>
<style type="text/css">
.oc-box{border:1px solid #e5e5e5;border-radius:8px;background:#fff;margin:0 0 16px 0;overflow:hidden}
.oc-box .hd{padding:10px 14px;background:#f6f8fa;border-bottom:1px solid #eee;font-weight:600}
.oc-box .bd{padding:12px 14px}
.oc-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.oc-kv{font-size:13px;line-height:1.8}
.oc-kv code{background:#f6f8fa;padding:2px 6px;border-radius:4px}
.oc-tag{display:inline-block;padding:2px 10px;border-radius:999px;font-size:12px;border:1px solid #d0d7de;background:#fff}
.ok{color:#1a7f37;border-color:#b7dfc3;background:#e6f7e9}
.bad{color:#cf222e;border-color:#f0b8bf;background:#ffeef0}
.warn{color:#9a6700;border-color:#f2d084;background:#fff8c5}
.oc-row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.oc-row .btn{display:inline-block;padding:6px 12px;border:1px solid #d0d7de;border-radius:6px;background:#fff;cursor:pointer}
.oc-row .btn.primary{background:#0969da;border-color:#0969da;color:#fff}
.oc-row .btn.danger{background:#cf222e;border-color:#cf222e;color:#fff}
.oc-row .btn:disabled{opacity:.6;cursor:not-allowed}
.oc-out{width:100%;height:300px;white-space:pre-wrap;background:#0b1020;color:#dbe2ff;border-radius:8px;padding:10px;overflow:auto;font-family:monospace;font-size:12px}
@media (max-width: 1000px){.oc-grid{grid-template-columns:1fr}}
</style>
<div class="oc-box">
<div class="hd"><%:Quick Checks%></div>
<div class="bd">
<div class="oc-grid">
<div class="oc-kv">
<div><%:Base Dir%>: <code id="oc-base">-</code></div>
<div><%:Disk Available%>: <code id="oc-disk">-</code></div>
<div><%:Enable%>: <span id="oc-enabled" class="oc-tag">-</span></div>
<div><%:Running%>: <span id="oc-running" class="oc-tag">-</span> <span id="oc-pid"></span></div>
</div>
<div class="oc-kv">
<div><%:Node%>: <code id="oc-node">-</code> <span id="oc-has-node" class="oc-tag">-</span></div>
<div><%:OpenClaw%>: <code id="oc-ver">-</code> <span id="oc-has-oc" class="oc-tag">-</span></div>
<div><%:Config%>: <span id="oc-has-cfg" class="oc-tag">-</span></div>
<div><%:URL%>: <a id="oc-url" href="#" target="_blank" rel="noopener">-</a></div>
</div>
</div>
<div id="oc-hints" style="margin-top:10px;color:#666;font-size:12px;line-height:1.6"></div>
</div>
</div>
<div class="oc-box">
<div class="hd"><%:Service Actions%></div>
<div class="bd">
<div class="oc-row">
<button class="btn" onclick="ocSvc('restart')"><%:Restart Service%></button>
<button class="btn" onclick="ocSvc('upgrade')"><%:Upgrade%></button>
<button class="btn" onclick="ocConfirmUninstallOpenclaw()"><%:Uninstall OpenClaw (Keep Node)%></button>
<button class="btn" onclick="ocConfirmUninstall()"><%:Uninstall%></button>
<button class="btn" onclick="ocShowInstallerLog()"><%:Installer Log%></button>
<button class="btn danger" onclick="ocConfirmPurge()"><%:Purge%></button>
</div>
<div style="margin-top:10px;color:#666;font-size:12px;line-height:1.6">
<%:Purge is destructive and will remove data under Base Dir. Use it only for troubleshooting.%>
</div>
</div>
</div>
<div class="oc-box">
<div class="hd"><%:Diagnostics%></div>
<div class="bd">
<div class="oc-row">
<button id="btn-doctor" class="btn primary" onclick="ocRun('doctor')"><%:Run Doctor%></button>
<button id="btn-gwst" class="btn" onclick="ocRun('gateway_status')"><%:Gateway Status%></button>
<button id="btn-gwh" class="btn" onclick="ocRun('gateway_health')"><%:Gateway Health%></button>
<button id="btn-chst" class="btn" onclick="ocRun('channels_status')"><%:Channels Status%></button>
<button id="btn-logs" class="btn" onclick="ocRunLogs()"><%:Gateway Logs%></button>
<select id="oc-log-limit" style="padding:6px 8px;border:1px solid #d0d7de;border-radius:6px">
<option value="200">200</option>
<option value="500">500</option>
<option value="1000">1000</option>
<option value="2000">2000</option>
</select>
</div>
<div style="margin-top:10px;color:#666;font-size:12px">
<%:All actions run in background; output will appear below.%>
</div>
<div style="margin-top:10px">
<div class="oc-out" id="oc-out">-</div>
</div>
</div>
</div>
<script type="text/javascript">
//<![CDATA[
(function() {
var infoUrl = '<%=info_url%>';
var runUrl = '<%=run_url%>';
var pollUrl = '<%=poll_url%>';
var opUrl = '<%=op_url%>';
var logUrl = '<%=log_url%>';
var csrfToken = '<%=token%>';
var currentOp = '';
var pollTimer = null;
function tag(el, ok, text) {
if (!el) return;
el.className = 'oc-tag ' + (ok === true ? 'ok' : ok === false ? 'bad' : 'warn');
el.textContent = text;
}
function setText(id, v) {
var el = document.getElementById(id);
if (el) el.textContent = (v === undefined || v === null || v === '') ? '-' : v;
}
function setLink(id, href) {
var el = document.getElementById(id);
if (!el) return;
if (!href || href === '-') {
el.textContent = '-';
el.href = '#';
return;
}
el.textContent = href;
el.href = href;
}
function refreshInfo() {
(new XHR()).get(infoUrl, null, function(x) {
try {
var d = JSON.parse(x.responseText);
setText('oc-base', d.base_dir);
setText('oc-disk', (d.available_mb || 0) + ' MB');
tag(document.getElementById('oc-enabled'), d.enabled === '1', d.enabled === '1' ? 'yes' : 'no');
tag(document.getElementById('oc-running'), d.running === true, d.running === true ? 'yes' : 'no');
setText('oc-pid', d.procd_pid ? ('(pid ' + d.procd_pid + ')') : '');
setText('oc-node', d.node_version || '-');
setText('oc-ver', d.openclaw_version || '-');
tag(document.getElementById('oc-has-node'), d.has_node === true, d.has_node ? 'node ok' : 'node missing');
tag(document.getElementById('oc-has-oc'), d.has_openclaw === true, d.has_openclaw ? 'openclaw ok' : 'openclaw missing');
tag(document.getElementById('oc-has-cfg'), d.has_config === true, d.has_config ? 'config ok' : 'config missing');
setLink('oc-url', d.url || '-');
var hints = [];
if (!d.default_gw) hints.push('No default gateway: internet unreachable (install/upgrade will fail).');
if (!d.has_node) hints.push('Node.js missing: click Install in the action bar.');
if (d.has_node && !d.has_openclaw) hints.push('OpenClaw not installed: click Install in the action bar.');
if (d.has_openclaw && !d.has_config) hints.push('Config missing: run Doctor or re-run Install to generate minimal config.');
if (d.available_mb && d.available_mb < 2048) hints.push('Disk space low (< 2GB): installation may fail.');
if (d.enabled === '1' && d.has_openclaw && !d.running) hints.push('Service enabled but not running: run Gateway Status or Doctor for details.');
document.getElementById('oc-hints').textContent = hints.length ? ('Hints: ' + hints.join(' ')) : 'Hints: -';
} catch (e) {}
});
}
function pollOnce() {
if (!currentOp) return;
(new XHR()).get(pollUrl, { op: currentOp }, function(x) {
try {
var d = JSON.parse(x.responseText);
document.getElementById('oc-out').textContent = d.log || '-';
if (d.state === 'done') {
stopPoll();
}
} catch (e) {}
});
}
function startPoll(op) {
currentOp = op;
if (pollTimer) window.clearInterval(pollTimer);
pollOnce();
pollTimer = window.setInterval(pollOnce, 1200);
}
function stopPoll() {
if (pollTimer) window.clearInterval(pollTimer);
pollTimer = null;
}
function disableButtons(disabled) {
var ids = ['btn-doctor','btn-gwst','btn-gwh','btn-chst','btn-logs'];
for (var i=0;i<ids.length;i++) {
var el = document.getElementById(ids[i]);
if (el) el.disabled = disabled;
}
}
window.ocRun = function(op) {
disableButtons(true);
(new XHR()).post(runUrl, { op: op, token: csrfToken }, function() {
disableButtons(false);
startPoll(op);
});
};
window.ocRunLogs = function() {
var limit = document.getElementById('oc-log-limit').value || '200';
disableButtons(true);
(new XHR()).post(runUrl, { op: 'logs', limit: limit, token: csrfToken }, function() {
disableButtons(false);
startPoll('logs');
});
};
window.ocSvc = function(op) {
(new XHR()).post(opUrl, { op: op, token: csrfToken }, function(x) {
try {
var d = JSON.parse(x.responseText || '{}');
if (window.taskd && window.taskd.show_log) {
if (d.running_task_id && d.running_task_id !== '') {
window.taskd.show_log(d.running_task_id);
} else if (d.task_id) {
window.taskd.show_log(d.task_id);
}
}
} catch (e) {}
});
};
window.ocConfirmUninstall = function() {
if (!confirm('<%:Uninstall will remove runtime but keep data under Base Dir. Continue?%>')) return;
window.ocSvc('uninstall');
};
window.ocConfirmUninstallOpenclaw = function() {
if (!confirm('<%:Uninstall OpenClaw runtime but keep Node.js. Continue?%>')) return;
window.ocSvc('uninstall_openclaw');
};
window.ocConfirmPurge = function() {
if (!confirm('<%:Purge will remove runtime and data under Base Dir. Continue?%>')) return;
window.ocSvc('purge');
};
window.ocShowInstallerLog = function() {
(new XHR()).get(logUrl, { n: 400 }, function(x) {
try {
var d = JSON.parse(x.responseText || '{}');
if (window.taskd && window.taskd.show_log_txt) {
window.taskd.show_log_txt('installer.log', d.log || '-');
}
} catch (e) {}
});
};
refreshInfo();
window.setInterval(refreshInfo, 5000);
})();
//]]>
</script>
<%+footer%>

View File

@ -0,0 +1,245 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8\n"
msgid "OpenClaw"
msgstr "OpenClaw"
msgid "OpenClaw Launcher"
msgstr "OpenClaw 启动器"
msgid "Native installer & service manager for OpenClaw (procd + Node/npm)."
msgstr "OpenClaw 原生安装器与服务管理procd + Node/npm。"
msgid "Status"
msgstr "状态"
msgid "Open Web UI"
msgstr "打开 Web 界面"
msgid "Unavailable"
msgstr "不可用"
msgid "Service Status"
msgstr "服务状态"
msgid "Installed"
msgstr "已安装"
msgid "Running"
msgstr "运行中"
msgid "Not running"
msgstr "未运行"
msgid "Not installed"
msgstr "未安装"
msgid "Node"
msgstr "Node.js"
msgid "URL"
msgstr "访问地址"
msgid "Actions"
msgstr "操作"
msgid "Install"
msgstr "安装"
msgid "Apply Settings"
msgstr "应用设置"
msgid "Upgrade"
msgstr "升级"
msgid "Start"
msgstr "启动"
msgid "Stop"
msgstr "停止"
msgid "Restart"
msgstr "重启"
msgid "Purge"
msgstr "清理(含数据)"
msgid "Installer Log"
msgstr "安装日志"
msgid "Service Actions"
msgstr "服务操作"
msgid "Restart Service"
msgstr "重启服务"
msgid "Purge will remove runtime and data under Base Dir. Continue?"
msgstr "清理将删除运行时与安装目录下的数据。确定继续?"
msgid "Purge is destructive and will remove data under Base Dir. Use it only for troubleshooting."
msgstr "清理操作具有破坏性,会删除安装目录下的数据;仅建议用于排障。"
msgid "Install/upgrade runs in background; check logs in the popup task window."
msgstr "安装/升级在后台执行;请在弹出的任务日志窗口查看。"
msgid "Task log will pop up after clicking an action."
msgstr "点击操作后会弹出任务日志窗口。"
msgid "Task window is not available (taskd/xterm not loaded). Please install luci-lib-taskd and luci-lib-xterm, then hard refresh the page."
msgstr "任务窗口不可用(未加载 taskd/xterm。请安装 luci-lib-taskd 和 luci-lib-xterm然后强制刷新页面。"
msgid "Config saved, but apply failed"
msgstr "配置已保存,但应用失败"
msgid "Another task running, try again later."
msgstr "有任务正在运行,请稍后再试。"
msgid "Click here to check running task"
msgstr "点击这里查看正在运行的任务"
msgid "Task Running"
msgstr "任务运行中"
msgid "Installing"
msgstr "安装中"
msgid "Stop Install"
msgstr "停止安装"
msgid "Failed to stop installation"
msgstr "停止安装失败"
msgid "No install task is running"
msgstr "当前没有正在执行的安装任务"
msgid "taskd is not available"
msgstr "taskd 不可用"
msgid "Diagnostics"
msgstr "诊断"
msgid "Quick Checks"
msgstr "快速检查"
msgid "Disk Available"
msgstr "可用空间"
msgid "Config"
msgstr "配置文件"
msgid "Run Doctor"
msgstr "运行 Doctor"
msgid "Gateway Status"
msgstr "网关状态"
msgid "Gateway Health"
msgstr "网关健康检查"
msgid "Gateway Logs"
msgstr "网关日志"
msgid "Channels Status"
msgstr "渠道状态"
msgid "All actions run in background; output will appear below."
msgstr "所有操作在后台执行;输出会显示在下方。"
msgid "Settings"
msgstr "设置"
msgid "Enable"
msgstr "启用"
msgid "Port"
msgstr "端口"
msgid "Bind"
msgstr "监听地址"
msgid "Loopback"
msgstr "仅本机"
msgid "LAN"
msgstr "LAN"
msgid "Auto"
msgstr "自动"
msgid "Base Dir"
msgstr "安装目录"
msgid "Recommended. OpenClaw Launcher will install Node.js/OpenClaw runtime and store data under this directory."
msgstr "推荐设置。OpenClaw 启动器会在该目录下安装 Node.js/OpenClaw 运行时并存放数据。"
msgid "Gateway Token"
msgstr "网关 Token"
msgid "Allowed Origins"
msgstr "允许的访问地址"
msgid "Origins allowed to access Control UI over HTTP. Example: http://lan-ip:port"
msgstr "允许通过 HTTP 访问控制台的来源域名,例如 http://LAN-IP:端口"
msgid "Allow Insecure HTTP Control UI"
msgstr "允许 HTTP 访问"
msgid "Enable token-only Control UI access over HTTP (no device identity). Recommended only on trusted LAN; prefer HTTPS when possible."
msgstr "允许通过 HTTP 使用仅令牌访问控制台(不校验设备身份)。仅建议在受信任的局域网使用,尽量使用 HTTPS。"
msgid "Disable Device Auth"
msgstr "关闭设备校验"
msgid "Disable Control UI device identity checks (dangerous). Use only on trusted LAN."
msgstr "关闭控制台设备身份校验(危险)。仅建议在受信任的局域网使用。"
msgid "Default Agent"
msgstr "默认 AGENT"
msgid "Select the provider used by agents.defaults.model.primary."
msgstr "选择 agents.defaults.model.primary 使用的提供商。"
msgid "API Key"
msgstr "API Key"
msgid "Only updates the API key for the selected Default Agent."
msgstr "只更新当前所选默认 AGENT 对应的 API Key。"
msgid "Relay URL"
msgstr "中转地址"
msgid "Optional. Must start with http:// or https://. Leave empty to restore the selected provider's default baseUrl."
msgstr "可选。必须以 http:// 或 https:// 开头。留空会恢复当前所选 provider 的默认 baseUrl。"
msgid "Relay URL must start with http:// or https://"
msgstr "中转地址必须以 http:// 或 https:// 开头"
msgid "Default Model"
msgstr "默认模型"
msgid "OpenAI"
msgstr "OpenAI"
msgid "Anthropic"
msgstr "Anthropic"
msgid "MiniMax CN"
msgstr "MiniMax CN"
msgid "Moonshot CN"
msgstr "Moonshot CN"
msgid "Uninstall"
msgstr "卸载(保留数据)"
msgid "Uninstall OpenClaw (Keep Node)"
msgstr "卸载 OpenClaw保留 Node"
msgid "Uninstall OpenClaw runtime but keep Node.js. Continue?"
msgstr "卸载 OpenClaw 运行时但保留 Node.js。确定继续"
msgid "Uninstall will remove runtime but keep data under Base Dir. Continue?"
msgstr "卸载将删除运行时,但保留安装目录下的数据。确定继续?"
msgid "Network Diagnostics"
msgstr "网络诊断"

View File

@ -0,0 +1 @@
zh-cn

View File

@ -0,0 +1,14 @@
config openclawmgr 'main'
option enabled '0'
option port '18789'
option bind 'lan'
option base_dir '/opt/openclawmgr'
option token ''
option allow_insecure_auth '0'
option disable_device_auth '0'
option node_version '22.16.0'
option install_accelerated '1'
option default_agent 'anthropic'
option default_model ''
option provider_api_key ''
option provider_base_url ''

View File

@ -0,0 +1,87 @@
#!/bin/sh /etc/rc.common
USE_PROCD=1
START=99
STOP=10
SERVICE_NAME="openclawmgr"
_find_entry() {
local global_dir="$1"
local d="${global_dir}/lib/node_modules/openclaw"
[ -f "${d}/openclaw.mjs" ] && { echo "${d}/openclaw.mjs"; return 0; }
[ -f "${d}/dist/cli.js" ] && { echo "${d}/dist/cli.js"; return 0; }
return 1
}
start_service() {
config_load openclawmgr
config_get_bool enabled main enabled 0
config_get port main port 18789
config_get bind main bind lan
config_get base_dir main base_dir /opt/openclawmgr
config_get token main token ""
[ "$enabled" -eq 1 ] || return 0
local node_bin="${base_dir}/node/bin/node"
local global_dir="${base_dir}/global"
local data_dir="${base_dir}/data"
local env_file="${data_dir}/.openclaw/openclaw.env"
local entry="$(_find_entry "$global_dir")"
local openai_api_key=""
local anthropic_api_key=""
local minimax_api_key=""
local moonshot_api_key=""
[ -x "$node_bin" ] || return 1
[ -n "$entry" ] || return 1
[ -n "$token" ] || return 1
mkdir -p "${data_dir}/.openclaw" 2>/dev/null
if [ -f "$env_file" ]; then
# shellcheck disable=SC1090
. "$env_file"
openai_api_key="${OPENAI_API_KEY:-}"
anthropic_api_key="${ANTHROPIC_API_KEY:-}"
minimax_api_key="${MINIMAX_API_KEY:-}"
moonshot_api_key="${MOONSHOT_API_KEY:-}"
fi
# OpenClaw uses --force to kill conflicting processes, but requires either `lsof`
# or `fuser`. iStoreOS images may not include them by default.
local force_opt=""
if command -v lsof >/dev/null 2>&1 || command -v fuser >/dev/null 2>&1; then
force_opt="--force"
fi
local user_opt=""
if id openclawmgr >/dev/null 2>&1; then
user_opt="openclawmgr"
fi
procd_open_instance "gateway"
procd_set_param command "$node_bin" "$entry" gateway run --port "$port" --bind "$bind" --auth token --token "$token" --allow-unconfigured $force_opt
procd_set_param env \
HOME="$data_dir" \
OPENCLAW_HOME="$data_dir" \
OPENCLAW_STATE_DIR="${data_dir}/.openclaw" \
OPENCLAW_CONFIG_PATH="${data_dir}/.openclaw/openclaw.json" \
OPENCLAW_GATEWAY_PORT="$port" \
OPENCLAW_GATEWAY_BIND="$bind" \
OPENCLAW_GATEWAY_TOKEN="$token" \
OPENAI_API_KEY="$openai_api_key" \
ANTHROPIC_API_KEY="$anthropic_api_key" \
MINIMAX_API_KEY="$minimax_api_key" \
MOONSHOT_API_KEY="$moonshot_api_key" \
PATH="${base_dir}/node/bin:${global_dir}/bin:/usr/sbin:/usr/bin:/sbin:/bin"
[ -n "$user_opt" ] && procd_set_param user "$user_opt"
procd_set_param respawn 3600 10 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger "openclawmgr"
}

View File

@ -0,0 +1,46 @@
#!/bin/sh
conf_dir="$(uci -q get quickstart.@main[0].conf_dir 2>/dev/null || true)"
[ -n "$conf_dir" ] && suggested_base_dir="$conf_dir/OpenClawMgr" || suggested_base_dir=""
# Ensure section exists
if ! uci -q get openclawmgr.main >/dev/null 2>&1; then
uci -q set openclawmgr.main=openclawmgr >/dev/null 2>&1 || true
fi
# Fill defaults (only if missing)
[ -n "$(uci -q get openclawmgr.main.enabled 2>/dev/null)" ] || uci -q set openclawmgr.main.enabled='0'
[ -n "$(uci -q get openclawmgr.main.port 2>/dev/null)" ] || uci -q set openclawmgr.main.port='18789'
[ -n "$(uci -q get openclawmgr.main.bind 2>/dev/null)" ] || uci -q set openclawmgr.main.bind='lan'
[ -n "$(uci -q get openclawmgr.main.node_version 2>/dev/null)" ] || uci -q set openclawmgr.main.node_version='22.16.0'
[ -n "$(uci -q get openclawmgr.main.install_accelerated 2>/dev/null)" ] || uci -q set openclawmgr.main.install_accelerated='1'
[ -n "$(uci -q get openclawmgr.main.token 2>/dev/null)" ] || uci -q set openclawmgr.main.token=''
[ -n "$(uci -q get openclawmgr.main.allow_insecure_auth 2>/dev/null)" ] || uci -q set openclawmgr.main.allow_insecure_auth='0'
[ -n "$(uci -q get openclawmgr.main.disable_device_auth 2>/dev/null)" ] || uci -q set openclawmgr.main.disable_device_auth='0'
[ -n "$(uci -q get openclawmgr.main.default_agent 2>/dev/null)" ] || uci -q set openclawmgr.main.default_agent='anthropic'
[ -n "$(uci -q get openclawmgr.main.default_model 2>/dev/null)" ] || uci -q set openclawmgr.main.default_model=''
[ -n "$(uci -q get openclawmgr.main.provider_api_key 2>/dev/null)" ] || uci -q set openclawmgr.main.provider_api_key=''
[ -n "$(uci -q get openclawmgr.main.provider_base_url 2>/dev/null)" ] || uci -q set openclawmgr.main.provider_base_url=''
# Improve default base_dir on iStoreOS: if still unset or using legacy /opt, prefer quickstart conf_dir.
current_base_dir="$(uci -q get openclawmgr.main.base_dir 2>/dev/null || true)"
if [ -z "$current_base_dir" ] || [ "$current_base_dir" = "/opt/openclawmgr" ]; then
if [ -n "$suggested_base_dir" ]; then
uci -q set openclawmgr.main.base_dir="$suggested_base_dir"
else
uci -q set openclawmgr.main.base_dir='/opt/openclawmgr'
fi
fi
uci -q commit openclawmgr >/dev/null 2>&1 || true
# best-effort: create an unprivileged user for procd
if ! id openclawmgr >/dev/null 2>&1; then
if command -v adduser >/dev/null 2>&1; then
adduser -D -H -s /bin/false openclawmgr >/dev/null 2>&1 || true
elif command -v useradd >/dev/null 2>&1; then
useradd -r -M -s /bin/false openclawmgr >/dev/null 2>&1 || true
fi
fi
exit 0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
{
"luci-app-openclawmgr": {
"description": "Grant UCI access for luci-app-openclawmgr",
"read": {
"uci": [ "openclawmgr" ]
},
"write": {
"uci": [ "openclawmgr" ]
}
}
}

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=mtproxy
PKG_VERSION:=3.0.10
PKG_RELEASE:=1
PKG_VERSION:=3.0.11
PKG_RELEASE:=2
PKG_MAINTAINER:=Kosntantine Shevlakov <shevlakov@132lan.ru>
PKG_LICENSE:=GPLv2
@ -10,7 +10,7 @@ PKG_LICENSE_FILES:=LICENSE
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/GetPageSpeed/MTProxy.git
PKG_SOURCE_VERSION:=4333cafad15acaf6f627b23c852ef3a4e4b68ae0
PKG_SOURCE_VERSION:=eab5096fb539bb63d323e81c03130d408591cf41
PKG_SOURCE_SUBDIR:=$(PKG_NAME)
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz