🚀 Sync 2026-03-18 20:29:42

This commit is contained in:
github-actions[bot] committed 2026-03-18 20:29:42 +08:00
1 parent c62c96aa04
commit 79996b4e00
212 files changed
+6047 -30308

No files matched your search

+3 -3
View File
@@ -1,12 +1,12 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=0.1.0
PKG_RELEASE:=2
PKG_VERSION:=0.1.1
PKG_RELEASE:=3
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
LUCI_DEPENDS:=+luci-base +luci-compat +luci-lib-taskd +curl +ca-bundle +tar +xz-utils +jsonfilter +git +git-http
define Package/luci-app-openclawmgr/conffiles
/etc/config/openclawmgr
@@ -34,11 +34,12 @@
activeTab: "basic",
savingSection: "",
lastAppliedAt: "",
showLogModal: false,
logText: "",
logTimer: null,
statusTimer: null,
installWatchTimer: null
installWatchTimer: null,
lastTaskRunning: false,
taskLogOpenTs: 0,
consoleReady: false,
consoleCheckTimer: null
};
var styleText = "";
@@ -247,45 +248,22 @@
'</svg>';
}
function openLogModal() {
state.showLogModal = true;
if (state.logTimer) {
window.clearTimeout(state.logTimer);
state.logTimer = null;
}
pollLogs();
render();
function taskWindowAvailable() {
return !!(window.taskd && window.taskd.show_log);
}
function closeLogModal() {
state.showLogModal = false;
if (state.logTimer) {
window.clearTimeout(state.logTimer);
state.logTimer = null;
}
refreshStatus();
}
function pollLogs() {
if (!state.showLogModal) {
function showTaskLog(taskId) {
taskId = taskId || "openclawmgr";
if (!taskWindowAvailable()) {
window.alert("任务窗口不可用(未加载 taskd/xterm)。请安装 luci-lib-taskd 和 luci-lib-xterm,然后强制刷新页面。");
return;
}
if (state.logTimer) {
window.clearTimeout(state.logTimer);
state.logTimer = null;
var now = Date.now ? Date.now() : (+new Date());
if (state.taskLogOpenTs && now - state.taskLogOpenTs < 1500) {
return;
}
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);
}
});
state.taskLogOpenTs = now;
window.taskd.show_log(taskId);
}
function stopStatusPolling() {
@@ -302,6 +280,41 @@
}
}
function stopConsoleCheck() {
if (state.consoleCheckTimer) {
window.clearTimeout(state.consoleCheckTimer);
state.consoleCheckTimer = null;
}
}
function pollConsoleReady(rounds) {
rounds = typeof rounds === "number" ? rounds : 20;
stopConsoleCheck();
state.consoleReady = false;
function tick(remaining) {
request(config.readyUrl).then(function(rv) {
state.consoleReady = !!(rv && rv.ok && rv.ready);
render();
if (!state.consoleReady && remaining > 1) {
state.consoleCheckTimer = window.setTimeout(function() { tick(remaining - 1); }, 1000);
} else {
state.consoleCheckTimer = null;
}
}).catch(function() {
state.consoleReady = false;
render();
if (remaining > 1) {
state.consoleCheckTimer = window.setTimeout(function() { tick(remaining - 1); }, 1500);
} else {
state.consoleCheckTimer = null;
}
});
}
tick(rounds);
}
function backgroundStatusDelay(status) {
if (status && status.installing) {
return 3000;
@@ -317,9 +330,6 @@
function ensureInstallWatch() {
stopInstallWatch();
if (state.status && state.status.installing && state.showLogModal) {
return;
}
state.installWatchTimer = window.setTimeout(function() {
refreshStatus(function() {
ensureInstallWatch();
@@ -347,16 +357,6 @@
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 || {};
@@ -381,6 +381,12 @@
var savingBasic = state.savingSection === "basic";
var savingAccess = state.savingSection === "access";
var serviceButtons = showServiceActions ? (
(!status.running ? '<button class="oclm-button" type="button" data-op="start">启动服务</button>' : '') +
(status.running ? '<button class="oclm-button" type="button" data-op="stop">停止服务</button>' : '') +
'<button class="oclm-button" type="button" data-op="restart">重启服务</button>'
) : '';
root.innerHTML =
'<style>' + styleText + '</style>' +
'<div class="oclm-app"' + (detectDarkMode() ? ' data-darkmode="true"' : '') + '>' +
@@ -403,10 +409,7 @@
'</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.running ? '<button class="oclm-button oclm-button-primary" type="button" data-open-console="1"' + (state.consoleReady ? '' : ' disabled') + '>' + openclawIcon("oclm-button-icon") + (state.consoleReady ? '打开控制台' : '控制台准备中…') + '</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>' : '') +
@@ -421,7 +424,6 @@
'<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", "所有地址"],
@@ -438,7 +440,7 @@
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 class="oclm-section-submit"><button class="oclm-button oclm-button-primary" type="button" id="oclm-save-basic"' + (savingBasic ? ' disabled' : '') + '>' + (savingBasic ? '应用中…' : '保存并应用') + '</button>' + serviceButtons + (state.lastAppliedAt ? '<span class="oclm-applied-hint">已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置</span>' : '') + '</div>' +
'</div></div>' +
'<div class="' + (activeTab === "access" ? '' : 'oclm-hidden') + '">' +
@@ -463,27 +465,9 @@
'</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) {
@@ -547,18 +531,45 @@
if (op === "purge" && !window.confirm("彻底清理会删除运行时和数据目录。确认继续?")) return;
postForm(config.opUrl, { op: op }).then(function(rv) {
if (!rv || !rv.ok) {
if (rv && rv.busy && rv.running_task_id) {
showTaskLog(rv.running_task_id);
return;
}
window.alert((rv && rv.error) || "操作失败");
return;
}
state.lastTaskRunning = true;
showTaskLog((rv && (rv.running_task_id || rv.task_id)) || "openclawmgr");
scheduleStatusRefresh(op === "restart" ? 8 : 6, 1000);
});
};
});
Array.prototype.forEach.call(root.querySelectorAll("[data-open-console]"), function(el) {
el.onclick = function() {
if (!state.consoleReady) {
window.alert("控制台正在启动,请稍候再试。");
return;
}
var url = (state.status && state.status.token_url) || "";
if (!url) {
window.alert("控制台地址不可用");
return;
}
window.open(url, "_blank", "noreferrer");
};
});
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);
var baseDirEl = root.getElementById("oclm-base-dir");
var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : "";
if (!baseDir) {
window.alert("请先选择数据目录并保存应用,再执行安装。");
if (baseDirEl) baseDirEl.focus();
return;
}
if (!state.status || state.status.installing) {
return;
}
@@ -566,7 +577,7 @@
state.status.task_running = true;
state.status.task_op = "install";
render();
postJson(config.configUrl, { install_accelerated: accelerated }).then(function(cfgRv) {
postJson(config.configUrl, { base_dir: baseDir, install_accelerated: accelerated }).then(function(cfgRv) {
if (!cfgRv || !cfgRv.ok) {
state.status.installing = false;
render();
@@ -574,15 +585,22 @@
return;
}
state.form.install_accelerated = accelerated;
state.form.base_dir = baseDir;
return postForm(config.opUrl, { op: "install" });
}).then(function(rv) {
if (!rv) return;
if (!rv || !rv.ok) {
if (rv && rv.busy && rv.running_task_id) {
showTaskLog(rv.running_task_id);
return;
}
state.status.installing = false;
render();
window.alert((rv && rv.error) || "启动安装失败");
return;
}
state.lastTaskRunning = true;
showTaskLog((rv && (rv.running_task_id || rv.task_id)) || "openclawmgr");
scheduleStatusRefresh(10, 1000);
}).catch(function() {
state.status.installing = false;
@@ -592,12 +610,6 @@
};
});
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");
@@ -616,18 +628,6 @@
};
}
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)) || "";
@@ -687,7 +687,6 @@
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,
@@ -759,6 +758,18 @@
function refreshStatus(done) {
request(config.statusUrl).then(function(data) {
state.status = data || {};
if (state.status && state.status.running) {
if (!state.consoleReady) {
pollConsoleReady(20);
}
} else {
stopConsoleCheck();
state.consoleReady = false;
}
if (state.status && state.status.task_running && !state.lastTaskRunning) {
showTaskLog("openclawmgr");
}
state.lastTaskRunning = !!(state.status && state.status.task_running);
render();
ensureInstallWatch();
if (typeof done === "function") {
@@ -782,6 +793,12 @@
state.form = data.config || {};
state.form.default_model = resolveModelValue(state.form);
state.options = data.options || {};
if (!state.form.base_dir) {
var suggested = state.options && state.options.suggested_base_dir ? String(state.options.suggested_base_dir) : "";
if (suggested) {
state.form.base_dir = suggested;
}
}
render();
refreshStatus();
}).catch(function() {
@@ -11,10 +11,10 @@ function index()
page.leaf = true
entry({"admin", "services", "openclawmgr", "status"}, call("action_status")).leaf = true
entry({"admin", "services", "openclawmgr", "ready"}, call("action_ready")).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
@@ -181,7 +181,10 @@ end
local function configured_base_path(base_dir)
local jsonc = require "luci.jsonc"
local path = (base_dir or "/opt/openclawmgr") .. "/data/.openclaw/openclaw.json"
if type(base_dir) ~= "string" or base_dir == "" then
return "/"
end
local path = base_dir .. "/data/.openclaw/openclaw.json"
local f = io.open(path, "r")
if not f then
return "/"
@@ -307,29 +310,40 @@ function action_status()
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 base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
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 running = false
local installed = false
if base_dir ~= "" then
local st = sys.exec("/usr/libexec/istorec/openclawmgr.sh status 2>/dev/null"):gsub("%s+$", "")
running = (st == "running")
installed = (st == "running" or st == "stopped")
end
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 node_ver = ""
local oc_ver = ""
if base_dir ~= "" then
node_ver = sys.exec("/usr/libexec/istorec/openclawmgr.sh node_version 2>/dev/null"):gsub("%s+$", "")
oc_ver = sys.exec("/usr/libexec/istorec/openclawmgr.sh openclaw_version 2>/dev/null"):gsub("%s+$", "")
end
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 base_url = ""
if base_dir ~= "" then
base_url = "http://" .. get_host() .. ":" .. port .. configured_base_path(base_dir)
end
local token_url = base_url
if token ~= "" then
token_url = token_url .. "#token=" .. token
@@ -357,19 +371,36 @@ function action_status()
})
end
function action_log()
function action_ready()
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 port = uci:get("openclawmgr", "main", "port") or "18789"
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
local content = sys.exec("tail -n " .. n .. " " .. util.shellquote(log_file) .. " 2>/dev/null") or ""
write_json({ ok = true, log = content })
if not port:match("^%d+$") then port = "18789" end
if base_dir == "" then
write_json({ ok = true, ready = false, reason = "base_dir missing" })
return
end
local base_url = "http://127.0.0.1:" .. port .. configured_base_path(base_dir)
local cmd = string.format(
"curl -fsS -o /dev/null --connect-timeout 1 --max-time 2 -w '%%{http_code}' %s 2>/dev/null",
util.shellquote(base_url)
)
local code = sys.exec(cmd):gsub("%s+$", "")
local n = tonumber(code) or 0
local ready = (n >= 200 and n < 400)
write_json({
ok = true,
ready = ready,
http_code = code,
url = base_url,
})
end
function action_apply_config()
@@ -402,8 +433,8 @@ function action_config_data()
return
end
local body = read_json_body() or {}
local section = "main"
local body = read_json_body() or {}
local section = "main"
local function bool_to_uci(value)
return value and "1" or "0"
@@ -513,7 +544,7 @@ function action_config_data()
return
end
local base_dir = uci:get("openclawmgr", "main", "base_dir") or "/opt/openclawmgr"
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
local port = uci:get("openclawmgr", "main", "port") or "18789"
local blocks = model.blocks()
local home = model.home()
@@ -528,11 +559,13 @@ function action_config_data()
end
end
add_choice(base_dir)
if base_dir ~= "" then
add_choice(base_dir)
end
for _, path in ipairs(paths or {}) do
add_choice(path)
end
add_choice(default_path or "/opt/openclawmgr")
add_choice(default_path or "/root/Configs/OpenClawMgr")
local allowed_origins = {}
for _, item in ipairs(uci:get_list("openclawmgr", "main", "allowed_origins") or {}) do
@@ -558,6 +591,7 @@ function action_config_data()
},
options = {
base_dir_choices = choices,
suggested_base_dir = default_path or "",
default_origin = default_allowed_origin(port),
}
})
@@ -665,22 +699,34 @@ function action_diag_info()
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 base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
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 avail_mb = 0
if base_dir ~= "" then
local df_kb = sys.exec("df -kP " .. util.shellquote(base_dir) .. " 2>/dev/null | awk 'NR==2{print $4}'"):gsub("%s+", "")
avail_mb = math.floor((tonumber(df_kb) or 0) / 1024)
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 node_ver = ""
local oc_ver = ""
if base_dir ~= "" then
node_ver = sys.exec("/usr/libexec/istorec/openclawmgr.sh node_version 2>/dev/null"):gsub("%s+$", "")
oc_ver = sys.exec("/usr/libexec/istorec/openclawmgr.sh openclaw_version 2>/dev/null"):gsub("%s+$", "")
end
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 has_node = false
local has_openclaw = false
local has_config = false
if base_dir ~= "" then
has_node = sys.call("[ -x " .. util.shellquote(base_dir .. "/node/bin/node") .. " ] >/dev/null 2>&1") == 0
has_openclaw = sys.call("[ -x " .. util.shellquote(base_dir .. "/global/bin/openclaw") .. " ] >/dev/null 2>&1") == 0
has_config = sys.call("[ -f " .. util.shellquote(base_dir .. "/data/.openclaw/openclaw.json") .. " ] >/dev/null 2>&1") == 0
end
local default_gw = sys.exec("ip route 2>/dev/null | awk '/^default/{print $3; exit}' 2>/dev/null"):gsub("%s+", "")
@@ -709,9 +755,9 @@ function action_diag_info()
has_config = has_config,
procd_pid = pid,
running = running,
url = "http://" .. get_host() .. ":" .. port .. configured_base_path(base_dir),
})
end
url = (base_dir ~= "" and ("http://" .. get_host() .. ":" .. port .. configured_base_path(base_dir)) or ""),
})
end
function action_diag_run()
local http = require "luci.http"
@@ -1,13 +1,15 @@
<%+header%>
<%+tasks/embed%>
<script type="text/javascript">
(function() {
window.openclawmgrConfig = {
token: "<%=token or ''%>",
statusUrl: "<%=url('admin/services/openclawmgr/status')%>",
readyUrl: "<%=url('admin/services/openclawmgr/ready')%>",
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"
};
})();
@@ -5,7 +5,6 @@ local info_url = luci.dispatcher.build_url("admin", "services", "openclawmgr", "
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 ""
%>
@@ -60,7 +59,6 @@ local token = (luci.dispatcher.context and luci.dispatcher.context.token) or ""
<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">
@@ -101,7 +99,6 @@ local token = (luci.dispatcher.context and luci.dispatcher.context.token) or ""
var runUrl = '<%=run_url%>';
var pollUrl = '<%=poll_url%>';
var opUrl = '<%=op_url%>';
var logUrl = '<%=log_url%>';
var csrfToken = '<%=token%>';
var currentOp = '';
@@ -238,17 +235,6 @@ local token = (luci.dispatcher.context and luci.dispatcher.context.token) or ""
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);
})();
@@ -2,10 +2,9 @@ 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 allow_insecure_auth '1'
option disable_device_auth '1'
option install_accelerated '1'
option default_agent 'anthropic'
option default_model ''
@@ -19,10 +19,11 @@ start_service() {
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 base_dir main base_dir ""
config_get token main token ""
[ "$enabled" -eq 1 ] || return 0
[ -n "$base_dir" ] || return 1
local node_bin="${base_dir}/node/bin/node"
local global_dir="${base_dir}/global"
@@ -38,7 +39,7 @@ start_service() {
[ -n "$entry" ] || return 1
[ -n "$token" ] || return 1
mkdir -p "${data_dir}/.openclaw" 2>/dev/null
mkdir -p "${data_dir}/.openclaw/workspace" 2>/dev/null
if [ -f "$env_file" ]; then
# shellcheck disable=SC1090
. "$env_file"
@@ -59,6 +60,10 @@ start_service() {
if id openclawmgr >/dev/null 2>&1; then
user_opt="openclawmgr"
fi
if [ -n "$user_opt" ]; then
chown -R "$user_opt:$user_opt" "$data_dir" 2>/dev/null || true
chmod -R u+rwX "$data_dir" 2>/dev/null || true
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
@@ -1,8 +1,5 @@
#!/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
@@ -14,23 +11,13 @@ fi
[ -n "$(uci -q get openclawmgr.main.bind 2>/dev/null)" ] || uci -q set openclawmgr.main.bind='lan'
[ -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.allow_insecure_auth 2>/dev/null)" ] || uci -q set openclawmgr.main.allow_insecure_auth='1'
[ -n "$(uci -q get openclawmgr.main.disable_device_auth 2>/dev/null)" ] || uci -q set openclawmgr.main.disable_device_auth='1'
[ -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
@@ -119,7 +119,17 @@ default_gateway() {
}
ensure_dirs() {
mkdir -p "$BASE_DIR" "$BASE_DIR/log" "$NODE_DIR" "$GLOBAL_DIR" "$DATA_DIR/.openclaw" 2>/dev/null || true
mkdir -p "$BASE_DIR" "$NODE_DIR" "$GLOBAL_DIR" "$DATA_DIR/.openclaw/workspace" "$BASE_DIR/npm-cache" 2>/dev/null || true
fix_data_permissions || true
}
fix_data_permissions() {
# When running under procd as user "openclawmgr", OpenClaw needs write access to data/.openclaw/*
if ! id openclawmgr >/dev/null 2>&1; then
return 0
fi
chown -R openclawmgr:openclawmgr "$DATA_DIR" 2>/dev/null || true
chmod -R u+rwX "$DATA_DIR" 2>/dev/null || true
}
check_space_mb() {
@@ -308,11 +318,9 @@ download() {
write_installer_log() {
local msg="$1"
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
local line
line="$(printf "%s [%s] %s" "$(log_ts)" "$APP" "$msg")"
printf "%s\n" "$line"
printf "%s\n" "$line" >> "$LOG_FILE"
}
openclaw_bin() {
@@ -689,6 +697,73 @@ install_openclaw() {
local flags="" npm_registry="" effective_registry=""
[ -x "$NPM_BIN" ] || return 1
ensure_git() {
ensure_remote_https() {
command -v git >/dev/null 2>&1 || return 1
local execp
execp="$(git --exec-path 2>/dev/null || true)"
[ -n "$execp" ] || return 1
[ -x "$execp/git-remote-https" ] && return 0
# Some builds ship helpers under libexec paths; exec-path should cover it,
# but keep a fallback in case of unusual layouts.
[ -x /usr/lib/git-core/git-remote-https ] && return 0
[ -x /usr/libexec/git-core/git-remote-https ] && return 0
return 1
}
command -v git >/dev/null 2>&1 || {
write_installer_log "git not found; some npm dependencies require git. Trying to install git via opkg."
local pm=""
if command -v is-opkg >/dev/null 2>&1; then
pm="is-opkg"
elif command -v opkg >/dev/null 2>&1; then
pm="opkg"
fi
if [ -z "$pm" ]; then
write_installer_log "git is required but opkg/is-opkg is not available; please install git and retry."
return 1
fi
if "$pm" install git >/dev/null 2>&1; then
command -v git >/dev/null 2>&1 || true
fi
}
command -v git >/dev/null 2>&1 || {
write_installer_log "git is required but could not be installed automatically; please opkg install git and retry."
return 1
}
# Ensure HTTPS transport is available (git-remote-https), required for git URL rewrites.
if ensure_remote_https; then
return 0
fi
write_installer_log "git HTTPS transport helper not found (git-remote-https); trying to install git-http/git-https."
local pm=""
if command -v is-opkg >/dev/null 2>&1; then
pm="is-opkg"
elif command -v opkg >/dev/null 2>&1; then
pm="opkg"
fi
if [ -z "$pm" ]; then
write_installer_log "git-remote-https is required but opkg/is-opkg is not available; please install git-http (or equivalent) and retry."
return 1
fi
"$pm" install git-http >/dev/null 2>&1 || true
"$pm" install git-https >/dev/null 2>&1 || true
if ensure_remote_https; then
return 0
fi
write_installer_log "git-remote-https is required but could not be installed automatically; please opkg install git-http (or git-https) and retry."
return 1
}
ensure_git || return 1
if is_musl; then
flags="--ignore-scripts"
fi
@@ -698,7 +773,7 @@ install_openclaw() {
fi
write_installer_log "Installing OpenClaw from npm"
write_installer_log "Node.js: $($NODE_BIN --version 2>/dev/null || echo unknown), npm: $($NPM_BIN --version 2>/dev/null || echo unknown)"
write_installer_log "Node.js: $(PATH=\"${NODE_DIR}/bin:/usr/sbin:/usr/bin:/sbin:/bin\" \"$NODE_BIN\" --version 2>/dev/null || echo unknown), npm: $(PATH=\"${NODE_DIR}/bin:/usr/sbin:/usr/bin:/sbin:/bin\" \"$NPM_BIN\" --version 2>/dev/null || echo unknown)"
write_installer_log "npm prefix: $GLOBAL_DIR, cache: ${BASE_DIR}/npm-cache, musl: $(is_musl && echo yes || echo no) ${flags:+($flags)}"
write_installer_log "npm registry: ${npm_registry:-default}"
if [ -n "$npm_registry" ]; then
@@ -722,6 +797,7 @@ install_openclaw() {
mkdir -p "$DATA_DIR" 2>/dev/null || true
# Use --add because url.*.insteadOf is multi-valued (git config without --add overwrites).
git config --file "$DATA_DIR/.gitconfig" --add url."https://github.com/".insteadOf "ssh://git@github.com/" 2>/dev/null || true
git config --file "$DATA_DIR/.gitconfig" --add url."https://github.com/".insteadOf "ssh://git@github.com" 2>/dev/null || true
git config --file "$DATA_DIR/.gitconfig" --add url."https://github.com/".insteadOf "git@github.com:" 2>/dev/null || true
fi
@@ -786,28 +862,26 @@ install_openclaw() {
local npm_rc=0
wait "$npmpid" || npm_rc=$?
if [ "$npm_rc" -ne 0 ]; then
write_installer_log "npm install failed (rc=$npm_rc); dumping recent output"
kill "$tailpid" 2>/dev/null || true
wait "$tailpid" 2>/dev/null || true
tail -n 200 "$tmp" >> "$LOG_FILE" 2>/dev/null || true
rm -f "$tmp" 2>/dev/null || true
return 1
fi
if [ "$npm_rc" -ne 0 ]; then
write_installer_log "npm install failed (rc=$npm_rc); dumping recent output"
kill "$tailpid" 2>/dev/null || true
wait "$tailpid" 2>/dev/null || true
rm -f "$tmp" 2>/dev/null || true
return 1
fi
local end_ts elapsed_s elapsed_h
end_ts="$(date +%s 2>/dev/null || echo 0)"
elapsed_s=$((end_ts - start_ts))
elapsed_h="$(fmt_elapsed "$elapsed_s")"
write_installer_log "npm install finished (${elapsed_h} elapsed)"
kill "$tailpid" 2>/dev/null || true
wait "$tailpid" 2>/dev/null || true
tail -n 200 "$tmp" >> "$LOG_FILE" 2>/dev/null || true
rm -f "$tmp" 2>/dev/null || true
find_entry >/dev/null 2>&1 || return 1
write_installer_log "OpenClaw installed: $(openclaw_version || echo unknown)"
return 0
}
write_installer_log "npm install finished (${elapsed_h} elapsed)"
kill "$tailpid" 2>/dev/null || true
wait "$tailpid" 2>/dev/null || true
rm -f "$tmp" 2>/dev/null || true
find_entry >/dev/null 2>&1 || return 1
write_installer_log "OpenClaw installed: $(openclaw_version || echo unknown)"
return 0
}
do_install() {
acquire_lock
@@ -834,16 +908,18 @@ install_openclaw() {
uci -q set "${UCI_NS}.main.token=$TOKEN" && uci -q commit "$UCI_NS" || true
fi
install_node
install_openclaw
ensure_gateway_config || true
install_node
install_openclaw
ensure_gateway_config || true
fix_data_permissions || true
# Do not auto-enable/start on first install; let user Save & Apply after choosing base_dir and settings.
if [ "${ENABLED:-0}" = "1" ]; then
/etc/init.d/openclawmgr enable >/dev/null 2>&1 || true
/etc/init.d/openclawmgr restart >/dev/null 2>&1 || true
fi
/etc/init.d/openclawmgr enable >/dev/null 2>&1 || true
uci -q set "${UCI_NS}.main.enabled=1" && uci -q commit "$UCI_NS" || true
/etc/init.d/openclawmgr restart >/dev/null 2>&1 || true
write_installer_log "== install done =="
}
write_installer_log "== install done =="
}
do_uninstall() {
acquire_lock
@@ -885,6 +961,7 @@ do_start() {
acquire_lock
write_installer_log "== start begin =="
ensure_dirs
fix_data_permissions || true
/etc/init.d/openclawmgr enable >/dev/null 2>&1 || true
uci -q set "${UCI_NS}.main.enabled=1" && uci -q commit "$UCI_NS" || true
ensure_gateway_config || true
@@ -905,28 +982,32 @@ do_restart() {
acquire_lock
write_installer_log "== restart begin =="
ensure_dirs
fix_data_permissions || true
ensure_gateway_config || true
/etc/init.d/openclawmgr restart >/dev/null 2>&1 || true
write_installer_log "== restart done =="
}
do_apply_config() {
acquire_lock
write_installer_log "== apply_config begin =="
ensure_dirs
ensure_gateway_config || true
do_apply_config() {
acquire_lock
write_installer_log "== apply_config begin =="
ensure_dirs
fix_data_permissions || true
ensure_gateway_config || true
local pid=""
pid="$(ubus call service list "{\"name\":\"openclawmgr\"}" 2>/dev/null | jsonfilter -e '$.openclawmgr.instances.gateway.pid' 2>/dev/null || true)"
if [ "${ENABLED:-0}" = "1" ]; then
/etc/init.d/openclawmgr enable >/dev/null 2>&1 || true
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
/etc/init.d/openclawmgr restart >/dev/null 2>&1 || true
local pid=""
pid="$(ubus call service list "{\"name\":\"openclawmgr\"}" 2>/dev/null | jsonfilter -e '$.openclawmgr.instances.gateway.pid' 2>/dev/null || true)"
if [ "${ENABLED:-0}" = "1" ]; then
/etc/init.d/openclawmgr enable >/dev/null 2>&1 || true
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
/etc/init.d/openclawmgr restart >/dev/null 2>&1 || true
else
/etc/init.d/openclawmgr start >/dev/null 2>&1 || true
fi
else
/etc/init.d/openclawmgr stop >/dev/null 2>&1 || true
/etc/init.d/openclawmgr disable >/dev/null 2>&1 || true
fi
else
/etc/init.d/openclawmgr stop >/dev/null 2>&1 || true
/etc/init.d/openclawmgr disable >/dev/null 2>&1 || true
fi
write_installer_log "== apply_config done =="
}
@@ -1052,10 +1133,10 @@ diag_poll() {
ACTION="${1:-help}"
shift 1 || true
BASE_DIR="$(uci_get base_dir)"
PORT="$(uci_get port)"
BIND="$(uci_get bind)"
TOKEN="$(uci_get token)"
BASE_DIR="$(uci_get base_dir)"
PORT="$(uci_get port)"
BIND="$(uci_get bind)"
TOKEN="$(uci_get token)"
ALLOW_INSECURE_AUTH="$(uci_get allow_insecure_auth)"
DISABLE_DEVICE_AUTH="$(uci_get disable_device_auth)"
ALLOWED_ORIGINS="$(uci_get_list allowed_origins)"
@@ -1065,12 +1146,11 @@ INSTALL_ACCELERATED="$(uci_get install_accelerated)"
PROVIDER_API_KEY="$(uci_get provider_api_key)"
PROVIDER_BASE_URL="$(uci_get provider_base_url)"
[ -n "$BASE_DIR" ] || BASE_DIR="/opt/openclawmgr"
[ -n "$PORT" ] || PORT="18789"
[ -n "$BIND" ] || BIND="lan"
NODE_VERSION="24.14.0"
[ -n "$ALLOW_INSECURE_AUTH" ] || ALLOW_INSECURE_AUTH="0"
[ -n "$DISABLE_DEVICE_AUTH" ] || DISABLE_DEVICE_AUTH="0"
[ -n "$PORT" ] || PORT="18789"
[ -n "$BIND" ] || BIND="lan"
NODE_VERSION="24.14.0"
[ -n "$ALLOW_INSECURE_AUTH" ] || ALLOW_INSECURE_AUTH="1"
[ -n "$DISABLE_DEVICE_AUTH" ] || DISABLE_DEVICE_AUTH="1"
[ -n "$DEFAULT_AGENT" ] || DEFAULT_AGENT="anthropic"
[ -n "$DEFAULT_MODEL" ] || DEFAULT_MODEL=""
[ -n "$INSTALL_ACCELERATED" ] || INSTALL_ACCELERATED="1"
@@ -1101,43 +1181,59 @@ case "$INSTALL_ACCELERATED" in
*) INSTALL_ACCELERATED="0" ;;
esac
require_base_dir() {
if [ -z "$BASE_DIR" ]; then
write_installer_log "base_dir is not configured; please choose a data directory in LuCI and Save & Apply, then retry."
exit 2
fi
}
NODE_DIR="${BASE_DIR}/node"
GLOBAL_DIR="${BASE_DIR}/global"
DATA_DIR="${BASE_DIR}/data"
LOG_FILE="${BASE_DIR}/log/installer.log"
NODE_BIN="${NODE_DIR}/bin/node"
NPM_BIN="${NODE_DIR}/bin/npm"
NODE_BIN="${NODE_DIR}/bin/node"
NPM_BIN="${NODE_DIR}/bin/npm"
case "$ACTION" in
install|upgrade)
require_base_dir
do_install
;;
uninstall)
require_base_dir
do_uninstall
;;
uninstall_openclaw)
require_base_dir
do_uninstall_openclaw
;;
purge)
require_base_dir
do_purge
;;
rm)
require_base_dir
do_uninstall
;;
start)
require_base_dir
do_start
;;
stop)
require_base_dir
do_stop
;;
restart)
require_base_dir
do_restart
;;
apply_config)
require_base_dir
do_apply_config
;;
status)
[ -n "$BASE_DIR" ] || exit 0
do_status
;;
port)