mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 18:41:15 +08:00
1760 lines
53 KiB
Lua
1760 lines
53 KiB
Lua
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", "cli"}, form("openclawmgr/cli"), _("命令行"), 20).leaf = true
|
|
entry({"admin", "services", "openclawmgr", "logs"}, template("openclawmgr/logs"), _("日志"), 30).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", "check_update"}, call("action_check_update")).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", "security_data"}, call("action_security_data")).leaf = true
|
|
entry({"admin", "services", "openclawmgr", "security_add"}, call("action_security_add")).leaf = true
|
|
entry({"admin", "services", "openclawmgr", "security_remove"}, call("action_security_remove")).leaf = true
|
|
entry({"admin", "services", "openclawmgr", "security_recheck"}, call("action_security_recheck")).leaf = true
|
|
|
|
entry({"admin", "services", "openclawmgr", "logs_api"}, call("action_logs")).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_file(path)
|
|
local jsonc = require "luci.jsonc"
|
|
local f = io.open(path, "r")
|
|
if not f then return nil end
|
|
local raw = f:read("*a")
|
|
f:close()
|
|
if not raw or raw == "" then return nil end
|
|
return jsonc.parse(raw)
|
|
end
|
|
|
|
local function openclaw_config_path(base_dir)
|
|
base_dir = tostring(base_dir or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
|
if base_dir == "" then
|
|
return ""
|
|
end
|
|
return base_dir .. "/data/.openclaw/openclaw.json"
|
|
end
|
|
|
|
local function get_runtime_gateway_config(base_dir, fallback)
|
|
fallback = fallback or {}
|
|
local cfg = read_json_file(openclaw_config_path(base_dir)) or {}
|
|
local gateway = cfg.gateway or {}
|
|
local auth = gateway.auth or {}
|
|
local port = tostring(gateway.port or fallback.port or "18789")
|
|
if not port:match("^%d+$") then
|
|
port = tostring(fallback.port or "18789")
|
|
end
|
|
if not port:match("^%d+$") then
|
|
port = "18789"
|
|
end
|
|
local bind = tostring(gateway.bind or fallback.bind or "lan")
|
|
if bind ~= "loopback" and bind ~= "lan" and bind ~= "auto" and bind ~= "tailnet" and bind ~= "custom" then
|
|
bind = tostring(fallback.bind or "lan")
|
|
if bind ~= "loopback" and bind ~= "lan" and bind ~= "auto" and bind ~= "tailnet" and bind ~= "custom" then
|
|
bind = "lan"
|
|
end
|
|
end
|
|
local token = tostring(auth.token or fallback.token or "")
|
|
return {
|
|
port = port,
|
|
bind = bind,
|
|
token = token,
|
|
}
|
|
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 safe_int(v, def, minv, maxv)
|
|
v = tostring(v or "")
|
|
local n = tonumber(v)
|
|
if not n then
|
|
return def
|
|
end
|
|
n = math.floor(n)
|
|
if minv and n < minv then
|
|
return minv
|
|
end
|
|
if maxv and n > maxv then
|
|
return maxv
|
|
end
|
|
return n
|
|
end
|
|
|
|
local function trim(v)
|
|
return tostring(v or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
|
end
|
|
|
|
local function split_dots(v)
|
|
local parts = {}
|
|
for part in tostring(v or ""):gmatch("[^%.]+") do
|
|
parts[#parts + 1] = part
|
|
end
|
|
return parts
|
|
end
|
|
|
|
local function semver_compare(a, b)
|
|
a = trim(a):gsub("^v", "")
|
|
b = trim(b):gsub("^v", "")
|
|
if a == "" or b == "" then
|
|
return nil
|
|
end
|
|
|
|
local a_core, a_pre = a:match("^([^%-]+)%-?(.*)$")
|
|
local b_core, b_pre = b:match("^([^%-]+)%-?(.*)$")
|
|
local a_nums = split_dots(a_core)
|
|
local b_nums = split_dots(b_core)
|
|
local max_len = math.max(#a_nums, #b_nums)
|
|
|
|
for i = 1, max_len do
|
|
local av = tonumber(a_nums[i] or "0") or 0
|
|
local bv = tonumber(b_nums[i] or "0") or 0
|
|
if av ~= bv then
|
|
return av > bv and 1 or -1
|
|
end
|
|
end
|
|
|
|
a_pre = trim(a_pre)
|
|
b_pre = trim(b_pre)
|
|
if a_pre == "" and b_pre == "" then
|
|
return 0
|
|
end
|
|
if a_pre == "" then
|
|
return 1
|
|
end
|
|
if b_pre == "" then
|
|
return -1
|
|
end
|
|
|
|
local a_ids = split_dots(a_pre)
|
|
local b_ids = split_dots(b_pre)
|
|
max_len = math.max(#a_ids, #b_ids)
|
|
for i = 1, max_len do
|
|
local ai = a_ids[i]
|
|
local bi = b_ids[i]
|
|
if ai == nil then return -1 end
|
|
if bi == nil then return 1 end
|
|
|
|
local an = tonumber(ai)
|
|
local bn = tonumber(bi)
|
|
if an and bn then
|
|
if an ~= bn then
|
|
return an > bn and 1 or -1
|
|
end
|
|
elseif an and not bn then
|
|
return -1
|
|
elseif not an and bn then
|
|
return 1
|
|
elseif ai ~= bi then
|
|
return ai > bi and 1 or -1
|
|
end
|
|
end
|
|
|
|
return 0
|
|
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 security_config_path(base_dir)
|
|
base_dir = trim(base_dir)
|
|
if base_dir == "" then
|
|
return ""
|
|
end
|
|
return base_dir .. "/data/.openclawmgr/openclawmgr-security.json"
|
|
end
|
|
|
|
local function load_security_config(base_dir)
|
|
local data = read_json_file(security_config_path(base_dir))
|
|
if type(data) ~= "table" or type(data.items) ~= "table" then
|
|
return { items = {} }
|
|
end
|
|
return data
|
|
end
|
|
|
|
local function save_security_config(base_dir, data)
|
|
local jsonc = require "luci.jsonc"
|
|
local sys = require "luci.sys"
|
|
local util = require "luci.util"
|
|
local path = security_config_path(base_dir)
|
|
local dir = base_dir .. "/data/.openclawmgr"
|
|
local tmp = path .. ".tmp"
|
|
local f
|
|
|
|
if trim(path) == "" then
|
|
return false
|
|
end
|
|
sys.call("mkdir -p " .. util.shellquote(dir) .. " >/dev/null 2>&1")
|
|
f = io.open(tmp, "w")
|
|
if not f then
|
|
return false
|
|
end
|
|
f:write(jsonc.stringify(data) or "{\"items\":[]}")
|
|
f:close()
|
|
if not os.rename(tmp, path) then
|
|
os.remove(tmp)
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function security_stat(path)
|
|
local sys = require "luci.sys"
|
|
local util = require "luci.util"
|
|
local raw = sys.exec("stat -c '%u:%g:%a' " .. util.shellquote(path) .. " 2>/dev/null"):gsub("%s+$", "")
|
|
local uid, gid, mode = raw:match("^(%d+):(%d+):(%d+)$")
|
|
if not uid then
|
|
return nil
|
|
end
|
|
return {
|
|
uid = tonumber(uid),
|
|
gid = tonumber(gid),
|
|
mode = mode,
|
|
}
|
|
end
|
|
|
|
local function security_path_exists(path)
|
|
local sys = require "luci.sys"
|
|
local util = require "luci.util"
|
|
return sys.call("[ -e " .. util.shellquote(path) .. " ] >/dev/null 2>&1") == 0
|
|
end
|
|
|
|
local function security_path_is_dir(path)
|
|
local sys = require "luci.sys"
|
|
local util = require "luci.util"
|
|
return sys.call("[ -d " .. util.shellquote(path) .. " ] >/dev/null 2>&1") == 0
|
|
end
|
|
|
|
local function security_probe(path)
|
|
local sys = require "luci.sys"
|
|
local util = require "luci.util"
|
|
local raw = sys.exec("stat -Lc '%F|%u|%g|%a' " .. util.shellquote(path) .. " 2>/dev/null"):gsub("%s+$", "")
|
|
local kind, uid, gid, mode = raw:match("^(.-)|(%d+)|(%d+)|(%d+)$")
|
|
if kind then
|
|
return {
|
|
kind = kind,
|
|
uid = tonumber(uid),
|
|
gid = tonumber(gid),
|
|
mode = mode,
|
|
}
|
|
end
|
|
if security_path_is_dir(path) then
|
|
return { kind = "directory" }
|
|
end
|
|
if security_path_exists(path) then
|
|
return { kind = "other" }
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function security_apply(path)
|
|
local sys = require "luci.sys"
|
|
local util = require "luci.util"
|
|
local quoted = util.shellquote(path)
|
|
if sys.call("chown root:root " .. quoted .. " >/dev/null 2>&1") ~= 0 then
|
|
return false, "chown failed"
|
|
end
|
|
if sys.call("chmod 0750 " .. quoted .. " >/dev/null 2>&1") ~= 0 then
|
|
return false, "chmod failed"
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function security_restore(path, uid, gid, mode)
|
|
local sys = require "luci.sys"
|
|
local util = require "luci.util"
|
|
local quoted = util.shellquote(path)
|
|
if sys.call("chown " .. tostring(uid) .. ":" .. tostring(gid) .. " " .. quoted .. " >/dev/null 2>&1") ~= 0 then
|
|
return false, "restore chown failed"
|
|
end
|
|
if sys.call("chmod " .. tostring(mode) .. " " .. quoted .. " >/dev/null 2>&1") ~= 0 then
|
|
return false, "restore chmod failed"
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function security_protection_mode()
|
|
return "仅允许 root 和 root 组访问"
|
|
end
|
|
|
|
local function security_check_item(item)
|
|
if security_path_is_dir(item.path) then
|
|
local st = security_stat(item.path)
|
|
if not st then
|
|
return {
|
|
id = tostring(item.id or ""),
|
|
path = tostring(item.path or ""),
|
|
protectionMode = security_protection_mode(),
|
|
status = "check-failed",
|
|
checkResult = "检测失败",
|
|
}
|
|
end
|
|
if st.uid == 0 and st.gid == 0 and (st.mode == "750" or st.mode == "0750") then
|
|
return {
|
|
id = tostring(item.id or ""),
|
|
path = tostring(item.path or ""),
|
|
protectionMode = security_protection_mode(),
|
|
status = "active",
|
|
checkResult = "openclawmgr 无法进入该目录",
|
|
}
|
|
end
|
|
return {
|
|
id = tostring(item.id or ""),
|
|
path = tostring(item.path or ""),
|
|
protectionMode = security_protection_mode(),
|
|
status = "inactive",
|
|
checkResult = "目录权限与预期不一致",
|
|
}
|
|
end
|
|
if not security_path_exists(item.path) then
|
|
return {
|
|
id = tostring(item.id or ""),
|
|
path = tostring(item.path or ""),
|
|
protectionMode = security_protection_mode(),
|
|
status = "not-found",
|
|
checkResult = "目录不存在",
|
|
}
|
|
end
|
|
return {
|
|
id = tostring(item.id or ""),
|
|
path = tostring(item.path or ""),
|
|
protectionMode = security_protection_mode(),
|
|
status = "check-failed",
|
|
checkResult = "目标不是目录",
|
|
}
|
|
end
|
|
|
|
local function validate_security_path(path, base_dir, items)
|
|
path = trim(path)
|
|
base_dir = trim(base_dir)
|
|
if path == "" then
|
|
return false, "请输入目录路径"
|
|
end
|
|
if not path:match("^/") then
|
|
return false, "请输入绝对路径"
|
|
end
|
|
if path == "/" then
|
|
return false, "不能添加根目录 /"
|
|
end
|
|
for _, item in ipairs(items or {}) do
|
|
if tostring(item.path or "") == path then
|
|
return false, "该目录已在列表中"
|
|
end
|
|
end
|
|
if base_dir ~= "" then
|
|
local esc_base = base_dir:gsub("([^%w])", "%%%1")
|
|
local esc_path = path:gsub("([^%w])", "%%%1")
|
|
if path == base_dir or path:match("^" .. esc_base .. "/") then
|
|
return false, "不能添加 OpenClaw 的运行目录"
|
|
end
|
|
if base_dir:match("^" .. esc_path .. "/") then
|
|
return false, "不能添加 OpenClaw 运行目录的父目录"
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function find_security_item(items, id)
|
|
for index, item in ipairs(items or {}) do
|
|
if tostring(item.id or "") == tostring(id or "") then
|
|
return item, index
|
|
end
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
function action_logs()
|
|
local http = require "luci.http"
|
|
local sys = require "luci.sys"
|
|
local fs = require "nixio.fs"
|
|
local util = require "luci.util"
|
|
|
|
local limit = safe_int(http.formvalue("limit"), 200, 50, 2000)
|
|
local kind = (http.formvalue("kind") or "openclaw"):lower()
|
|
|
|
local cmd = ""
|
|
if kind == "tasks" then
|
|
local f = "/var/log/tasks/openclawmgr.log"
|
|
if fs.access(f) then
|
|
cmd = "tail -n " .. limit .. " " .. util.shellquote(f) .. " 2>/dev/null"
|
|
else
|
|
cmd = "echo '(task log not found: " .. f .. ")'"
|
|
end
|
|
elseif kind == "openclawmgr" then
|
|
cmd = "logread 2>/dev/null | grep -i openclawmgr | tail -n " .. limit
|
|
elseif kind == "all" then
|
|
cmd = "logread 2>/dev/null | tail -n " .. limit
|
|
else
|
|
-- default: openclaw gateway logs
|
|
cmd = "logread 2>/dev/null | grep -i openclaw | tail -n " .. limit
|
|
end
|
|
|
|
local log = sys.exec(cmd) or ""
|
|
write_json({ ok = true, kind = kind, limit = limit, server_time = os.time(), log = log })
|
|
end
|
|
|
|
local function configured_base_path(base_dir)
|
|
local jsonc = require "luci.jsonc"
|
|
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 "/"
|
|
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
|
|
|
|
local function probe_gateway_ready(port, base_dir, bind)
|
|
local sys = require "luci.sys"
|
|
local util = require "luci.util"
|
|
|
|
if not port or not tostring(port):match("^%d+$") then
|
|
return false, "", ""
|
|
end
|
|
if not base_dir or base_dir == "" then
|
|
return false, "", ""
|
|
end
|
|
|
|
local base_path = configured_base_path(base_dir)
|
|
local candidates = {}
|
|
|
|
if bind == "lan" or bind == "auto" then
|
|
local ip = lan_ipv4()
|
|
if ip ~= "" then
|
|
candidates[#candidates + 1] = ip
|
|
end
|
|
end
|
|
candidates[#candidates + 1] = "127.0.0.1"
|
|
|
|
local last_code, last_url = "", ""
|
|
for _, host in ipairs(candidates) do
|
|
local url = "http://" .. host .. ":" .. port .. base_path
|
|
last_url = url
|
|
local cmd = string.format(
|
|
"curl -fsS -o /dev/null --connect-timeout 1 --max-time 2 -w '%%{http_code}' %s 2>/dev/null",
|
|
util.shellquote(url)
|
|
)
|
|
local code = sys.exec(cmd):gsub("%s+$", "")
|
|
last_code = code
|
|
local n = tonumber(code) or 0
|
|
if n >= 200 and n < 400 then
|
|
return true, code, url
|
|
end
|
|
end
|
|
return false, last_code, last_url
|
|
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 base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
|
local runtime_gateway = get_runtime_gateway_config(base_dir, {
|
|
port = uci:get("openclawmgr", "main", "port") or "18789",
|
|
bind = uci:get("openclawmgr", "main", "bind") or "lan",
|
|
token = uci:get("openclawmgr", "main", "token") or "",
|
|
})
|
|
local port = runtime_gateway.port
|
|
local bind = runtime_gateway.bind
|
|
local token = runtime_gateway.token
|
|
|
|
if not port:match("^%d+$") then port = "18789" end
|
|
|
|
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 reachable, reachable_code, reachable_url = false, "", ""
|
|
if installed and not running then
|
|
reachable, reachable_code, reachable_url = probe_gateway_ready(port, base_dir, bind)
|
|
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 = ""
|
|
local oc_ver = ""
|
|
local target_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
|
|
target_oc_ver = sys.exec("/usr/libexec/istorec/openclawmgr.sh latest_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 = ""
|
|
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
|
|
end
|
|
|
|
write_json({
|
|
ok = true,
|
|
enabled = enabled,
|
|
installed = installed,
|
|
running = running,
|
|
reachable = reachable,
|
|
reachable_http_code = reachable_code,
|
|
reachable_url = reachable_url,
|
|
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,
|
|
target_openclaw_version = target_oc_ver,
|
|
pid = pid,
|
|
uptime_human = uptime_human,
|
|
base_url = base_url,
|
|
token_url = token_url,
|
|
url = token_url,
|
|
})
|
|
end
|
|
|
|
function action_ready()
|
|
local uci = require "luci.model.uci".cursor()
|
|
|
|
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
|
local runtime_gateway = get_runtime_gateway_config(base_dir, {
|
|
port = uci:get("openclawmgr", "main", "port") or "18789",
|
|
bind = uci:get("openclawmgr", "main", "bind") or "lan",
|
|
})
|
|
local port = runtime_gateway.port
|
|
local bind = runtime_gateway.bind
|
|
|
|
if base_dir == "" then
|
|
write_json({ ok = true, ready = false, reason = "base_dir missing" })
|
|
return
|
|
end
|
|
|
|
local ready, code, url = probe_gateway_ready(port, base_dir, bind)
|
|
|
|
write_json({
|
|
ok = true,
|
|
ready = ready,
|
|
http_code = code,
|
|
url = url,
|
|
})
|
|
end
|
|
|
|
function action_check_update()
|
|
local sys = require "luci.sys"
|
|
local http = require "luci.http"
|
|
local util = require "luci.util"
|
|
local uci = require "luci.model.uci".cursor()
|
|
local install_channel = http.formvalue("install_channel") or ""
|
|
if install_channel ~= "" and install_channel ~= "stable" and install_channel ~= "latest" then
|
|
write_json({ ok = false, error = "invalid install_channel" })
|
|
return
|
|
end
|
|
|
|
if not require_csrf() then
|
|
return
|
|
end
|
|
|
|
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
|
if trim(base_dir) == "" then
|
|
write_json({ ok = false, error = "请先配置数据目录" })
|
|
return
|
|
end
|
|
|
|
local st = trim(sys.exec("/usr/libexec/istorec/openclawmgr.sh status 2>/dev/null"))
|
|
local installed = (st == "running" or st == "stopped")
|
|
if not installed then
|
|
write_json({ ok = false, error = "OpenClaw 尚未安装", installed = false })
|
|
return
|
|
end
|
|
|
|
local local_ver = trim(sys.exec("/usr/libexec/istorec/openclawmgr.sh local_openclaw_version 2>/dev/null"))
|
|
if local_ver == "" then
|
|
local_ver = trim(sys.exec("/usr/libexec/istorec/openclawmgr.sh openclaw_version 2>/dev/null"))
|
|
end
|
|
if local_ver == "" then
|
|
write_json({ ok = false, error = "获取本地版本失败", installed = true })
|
|
return
|
|
end
|
|
|
|
local remote_channel = install_channel ~= "" and install_channel or "latest"
|
|
local remote_cmd = "INSTALL_CHANNEL=" .. util.shellquote(remote_channel) .. " /usr/libexec/istorec/openclawmgr.sh latest_openclaw_version 2>/dev/null"
|
|
local remote_ver = trim(sys.exec(remote_cmd))
|
|
if remote_ver == "" then
|
|
write_json({ ok = false, error = "获取远程版本失败", installed = true, local_version = local_ver })
|
|
return
|
|
end
|
|
|
|
local cmp = semver_compare(remote_ver, local_ver)
|
|
local has_update = false
|
|
if cmp == nil then
|
|
has_update = remote_ver ~= local_ver
|
|
else
|
|
has_update = cmp > 0
|
|
end
|
|
|
|
write_json({
|
|
ok = true,
|
|
installed = true,
|
|
local_version = local_ver,
|
|
remote_version = remote_ver,
|
|
has_update = has_update,
|
|
})
|
|
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 sys = require "luci.sys"
|
|
local util = require "luci.util"
|
|
local uci = require "luci.model.uci".cursor()
|
|
local model = require "luci.model.openclawmgr"
|
|
|
|
local function write_json_file(path, obj)
|
|
local dir = path:match("^(.+)/[^/]+$")
|
|
if dir and dir ~= "" then
|
|
sys.call("mkdir -p " .. util.shellquote(dir) .. " >/dev/null 2>&1")
|
|
end
|
|
local encoded = jsonc.stringify(obj, true) or "{}"
|
|
encoded = encoded:gsub("\\/", "/")
|
|
local f = io.open(path, "w")
|
|
if not f then
|
|
return false
|
|
end
|
|
f:write(encoded)
|
|
f:write("\n")
|
|
f:close()
|
|
return true
|
|
end
|
|
|
|
local function config_path(base_dir)
|
|
return openclaw_config_path(base_dir)
|
|
end
|
|
|
|
local function ensure_table(parent, key)
|
|
if type(parent[key]) ~= "table" then
|
|
parent[key] = {}
|
|
end
|
|
return parent[key]
|
|
end
|
|
|
|
local infer_custom_provider_model
|
|
local display_model_name
|
|
|
|
local function env_key_for_agent(agent)
|
|
if agent == "openai" then return "OPENAI_API_KEY" end
|
|
if agent == "anthropic" then return "ANTHROPIC_API_KEY" end
|
|
if agent == "minimax-cn" then return "MINIMAX_API_KEY" end
|
|
if agent == "moonshot" then return "MOONSHOT_API_KEY" end
|
|
return ""
|
|
end
|
|
|
|
local function default_model_for_agent(agent, current_custom_model)
|
|
if agent == "openai" then return "gpt-5.2" end
|
|
if agent == "anthropic" then return "claude-sonnet-4-6" end
|
|
if agent == "minimax-cn" then return "MiniMax-M2.5" end
|
|
if agent == "moonshot" then return "kimi-k2.5" end
|
|
if agent == "custom-provider" then return trim(current_custom_model) ~= "" and trim(current_custom_model) or "custom-model" end
|
|
return "claude-sonnet-4-6"
|
|
end
|
|
|
|
local function normalized_model_path(agent, value, current_custom_model)
|
|
local model_name = display_model_name(value)
|
|
if model_name == "" then
|
|
model_name = default_model_for_agent(agent, current_custom_model)
|
|
end
|
|
return agent .. "/" .. model_name
|
|
end
|
|
|
|
local function infer_agent_from_cfg(cfg)
|
|
local primary = cfg.agents and cfg.agents.defaults and cfg.agents.defaults.model and cfg.agents.defaults.model.primary
|
|
if type(primary) == "string" and primary:match("^[^/]+/.+") then
|
|
return primary:match("^([^/]+)/") or ""
|
|
end
|
|
local providers = cfg.models and cfg.models.providers
|
|
if type(providers) == "table" then
|
|
for _, agent in ipairs({ "openai", "anthropic", "minimax-cn", "moonshot", "custom-provider" }) do
|
|
if type(providers[agent]) == "table" then
|
|
return agent
|
|
end
|
|
end
|
|
end
|
|
return ""
|
|
end
|
|
|
|
local function get_runtime_config(base_dir)
|
|
local cfg = read_json_file(config_path(base_dir)) or {}
|
|
local gateway_cfg = get_runtime_gateway_config(base_dir, {
|
|
port = uci:get("openclawmgr", "main", "port") or "18789",
|
|
bind = uci:get("openclawmgr", "main", "bind") or "lan",
|
|
token = uci:get("openclawmgr", "main", "token") or "",
|
|
})
|
|
local gateway = cfg.gateway or {}
|
|
local control = gateway.controlUi or {}
|
|
local auth = gateway.auth or {}
|
|
local primary = cfg.agents and cfg.agents.defaults and cfg.agents.defaults.model and cfg.agents.defaults.model.primary
|
|
local agent = infer_agent_from_cfg(cfg)
|
|
if agent == "" then
|
|
agent = uci:get("openclawmgr", "main", "default_agent") or "anthropic"
|
|
end
|
|
|
|
local default_model = ""
|
|
if type(primary) == "string" and primary ~= "" then
|
|
default_model = display_model_name(primary)
|
|
elseif agent == "custom-provider" then
|
|
default_model = infer_custom_provider_model(base_dir)
|
|
else
|
|
default_model = display_model_name(uci:get("openclawmgr", "main", "default_model") or "")
|
|
end
|
|
|
|
local provider = cfg.models and cfg.models.providers and cfg.models.providers[agent] or {}
|
|
local env = cfg.env or {}
|
|
local provider_api_key = ""
|
|
local env_key = env_key_for_agent(agent)
|
|
if env_key ~= "" and type(env[env_key]) == "string" then
|
|
provider_api_key = env[env_key]
|
|
elseif type(provider.apiKey) == "string" then
|
|
provider_api_key = provider.apiKey
|
|
else
|
|
provider_api_key = uci:get("openclawmgr", "main", "provider_api_key") or ""
|
|
end
|
|
|
|
local allowed_origins = {}
|
|
if type(control.allowedOrigins) == "table" then
|
|
for _, item in ipairs(control.allowedOrigins) do
|
|
item = trim(item)
|
|
if item ~= "" then
|
|
allowed_origins[#allowed_origins + 1] = item
|
|
end
|
|
end
|
|
elseif #allowed_origins == 0 then
|
|
for _, item in ipairs(uci:get_list("openclawmgr", "main", "allowed_origins") or {}) do
|
|
item = trim(item)
|
|
if item ~= "" then
|
|
allowed_origins[#allowed_origins + 1] = item
|
|
end
|
|
end
|
|
end
|
|
|
|
local allow_insecure_auth = control.allowInsecureAuth
|
|
if type(allow_insecure_auth) ~= "boolean" then
|
|
allow_insecure_auth = (uci:get("openclawmgr", "main", "allow_insecure_auth") or "1") == "1"
|
|
end
|
|
|
|
local disable_device_auth = control.dangerouslyDisableDeviceAuth
|
|
if type(disable_device_auth) ~= "boolean" then
|
|
disable_device_auth = (uci:get("openclawmgr", "main", "disable_device_auth") or "1") == "1"
|
|
end
|
|
|
|
local provider_base_url = ""
|
|
if type(provider.baseUrl) == "string" then
|
|
provider_base_url = provider.baseUrl
|
|
else
|
|
provider_base_url = uci:get("openclawmgr", "main", "provider_base_url") or ""
|
|
end
|
|
|
|
return {
|
|
port = gateway_cfg.port,
|
|
bind = gateway_cfg.bind,
|
|
token = gateway_cfg.token,
|
|
allowed_origins = allowed_origins,
|
|
allow_insecure_auth = allow_insecure_auth,
|
|
disable_device_auth = disable_device_auth,
|
|
default_agent = agent,
|
|
default_model = default_model,
|
|
provider_api_key = provider_api_key,
|
|
provider_base_url = provider_base_url,
|
|
}
|
|
end
|
|
|
|
local function write_runtime_config(base_dir, service_cfg, runtime_cfg)
|
|
local path = config_path(base_dir)
|
|
if path == "" then
|
|
return false
|
|
end
|
|
local cfg = read_json_file(path) or {}
|
|
local gateway = ensure_table(cfg, "gateway")
|
|
gateway.mode = "local"
|
|
gateway.port = tonumber(service_cfg.port) or 18789
|
|
gateway.bind = service_cfg.bind or "lan"
|
|
local auth = ensure_table(gateway, "auth")
|
|
auth.mode = "token"
|
|
auth.token = runtime_cfg.token or ""
|
|
local control = ensure_table(gateway, "controlUi")
|
|
control.enabled = true
|
|
control.allowedOrigins = runtime_cfg.allowed_origins or {}
|
|
control.allowInsecureAuth = runtime_cfg.allow_insecure_auth == true
|
|
control.dangerouslyDisableDeviceAuth = runtime_cfg.disable_device_auth == true
|
|
control.dangerouslyAllowHostHeaderOriginFallback = service_cfg.bind ~= "loopback"
|
|
|
|
local env = type(cfg.env) == "table" and cfg.env or {}
|
|
for _, key in ipairs({ "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "MINIMAX_API_KEY", "MOONSHOT_API_KEY" }) do
|
|
env[key] = nil
|
|
end
|
|
local env_key = env_key_for_agent(runtime_cfg.default_agent)
|
|
if env_key ~= "" and trim(runtime_cfg.provider_api_key) ~= "" then
|
|
env[env_key] = runtime_cfg.provider_api_key
|
|
end
|
|
cfg.env = next(env) and env or nil
|
|
|
|
local models = ensure_table(cfg, "models")
|
|
if type(models.mode) ~= "string" or models.mode == "" then
|
|
models.mode = "merge"
|
|
end
|
|
local providers = {}
|
|
models.providers = providers
|
|
local provider = {}
|
|
providers[runtime_cfg.default_agent] = provider
|
|
local model_name = display_model_name(runtime_cfg.default_model)
|
|
if model_name == "" then
|
|
model_name = default_model_for_agent(runtime_cfg.default_agent, infer_custom_provider_model(base_dir))
|
|
end
|
|
if runtime_cfg.default_agent == "openai" then
|
|
provider.api = "openai-completions"
|
|
provider.baseUrl = trim(runtime_cfg.provider_base_url) ~= "" and runtime_cfg.provider_base_url or "https://api.openai.com/v1"
|
|
provider.apiKey = trim(runtime_cfg.provider_api_key) ~= "" and runtime_cfg.provider_api_key or nil
|
|
provider.authHeader = nil
|
|
elseif runtime_cfg.default_agent == "anthropic" then
|
|
provider.api = "anthropic-messages"
|
|
provider.baseUrl = trim(runtime_cfg.provider_base_url) ~= "" and runtime_cfg.provider_base_url or "https://api.anthropic.com"
|
|
provider.apiKey = trim(runtime_cfg.provider_api_key) ~= "" and runtime_cfg.provider_api_key or nil
|
|
provider.authHeader = nil
|
|
elseif runtime_cfg.default_agent == "minimax-cn" then
|
|
provider.api = "anthropic-messages"
|
|
provider.baseUrl = trim(runtime_cfg.provider_base_url) ~= "" and runtime_cfg.provider_base_url or "https://api.minimaxi.com/anthropic"
|
|
provider.apiKey = trim(runtime_cfg.provider_api_key) ~= "" and runtime_cfg.provider_api_key or nil
|
|
provider.authHeader = true
|
|
elseif runtime_cfg.default_agent == "moonshot" then
|
|
provider.api = "openai-completions"
|
|
provider.baseUrl = trim(runtime_cfg.provider_base_url) ~= "" and runtime_cfg.provider_base_url or "https://api.moonshot.cn/v1"
|
|
provider.apiKey = trim(runtime_cfg.provider_api_key) ~= "" and runtime_cfg.provider_api_key or nil
|
|
provider.authHeader = nil
|
|
else
|
|
provider.api = "openai-completions"
|
|
provider.baseUrl = runtime_cfg.provider_base_url or ""
|
|
provider.apiKey = trim(runtime_cfg.provider_api_key) ~= "" and runtime_cfg.provider_api_key or nil
|
|
provider.authHeader = nil
|
|
end
|
|
provider.models = {
|
|
{ id = model_name, name = model_name }
|
|
}
|
|
|
|
local agents = ensure_table(cfg, "agents")
|
|
local defaults = ensure_table(agents, "defaults")
|
|
local model_cfg = ensure_table(defaults, "model")
|
|
model_cfg.primary = normalized_model_path(runtime_cfg.default_agent, model_name, infer_custom_provider_model(base_dir))
|
|
|
|
return write_json_file(path, cfg)
|
|
end
|
|
|
|
infer_custom_provider_model = function(base_dir)
|
|
base_dir = tostring(base_dir or "")
|
|
if base_dir == "" then return "" end
|
|
local cfg = read_json_file(base_dir .. "/data/.openclaw/openclaw.json") or {}
|
|
local primary = cfg.agents and cfg.agents.defaults and cfg.agents.defaults.model and cfg.agents.defaults.model.primary
|
|
if type(primary) == "string" and primary:match("^custom%-provider/.+") then
|
|
return primary:gsub("^[^/]+/", "")
|
|
end
|
|
local providers = cfg.models and cfg.models.providers
|
|
local custom = providers and providers["custom-provider"]
|
|
local models = custom and custom.models
|
|
local first = type(models) == "table" and models[1] or nil
|
|
local id = first and first.id
|
|
if type(id) == "string" and id ~= "" then
|
|
return id
|
|
end
|
|
return ""
|
|
end
|
|
|
|
display_model_name = function(value)
|
|
value = tostring(value or "")
|
|
if value == "" then
|
|
return ""
|
|
end
|
|
return value:gsub("^[^/]+/", "")
|
|
end
|
|
|
|
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
|
|
|
|
local current = get_runtime_config(uci:get("openclawmgr", section, "base_dir") or "")
|
|
local requested_default_agent = tostring(has("default_agent") and body.default_agent or current.default_agent or "anthropic")
|
|
|
|
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 < 1025 or port_num > 65535 then
|
|
write_json({ ok = false, error = "invalid port (must be 1025-65535)" })
|
|
return
|
|
end
|
|
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
|
|
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" and agent ~= "custom-provider" then
|
|
write_json({ ok = false, error = "invalid default_agent" })
|
|
return
|
|
end
|
|
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_base_url") then
|
|
local value = tostring(body.provider_base_url or "")
|
|
if requested_default_agent == "custom-provider" and value == "" then
|
|
write_json({ ok = false, error = "provider_base_url required for custom-provider" })
|
|
return
|
|
end
|
|
if value ~= "" and not value:match("^https?://") then
|
|
write_json({ ok = false, error = "invalid provider_base_url" })
|
|
return
|
|
end
|
|
end
|
|
|
|
if has("token") then
|
|
body.token = tostring(body.token or "")
|
|
end
|
|
|
|
local effective_base_dir = tostring(has("base_dir") and body.base_dir or (uci:get("openclawmgr", section, "base_dir") or ""))
|
|
local effective_port = tostring(has("port") and body.port or current.port or "18789")
|
|
local effective_bind = tostring(has("bind") and body.bind or current.bind or "lan")
|
|
local runtime_cfg = {
|
|
token = tostring(has("token") and body.token or current.token or ""),
|
|
allowed_origins = current.allowed_origins,
|
|
allow_insecure_auth = current.allow_insecure_auth,
|
|
disable_device_auth = current.disable_device_auth,
|
|
default_agent = requested_default_agent,
|
|
default_model = tostring(has("default_model") and body.default_model or current.default_model or ""),
|
|
provider_api_key = tostring(has("provider_api_key") and body.provider_api_key or current.provider_api_key or ""),
|
|
provider_base_url = tostring(has("provider_base_url") and body.provider_base_url or current.provider_base_url or ""),
|
|
}
|
|
|
|
if has("allowed_origins") then
|
|
local origins = {}
|
|
if type(body.allowed_origins) == "table" then
|
|
for _, item in ipairs(body.allowed_origins) do
|
|
item = trim(item)
|
|
if item ~= "" then
|
|
origins[#origins + 1] = item
|
|
end
|
|
end
|
|
end
|
|
runtime_cfg.allowed_origins = origins
|
|
end
|
|
|
|
if has("allow_insecure_auth") then
|
|
runtime_cfg.allow_insecure_auth = (body.allow_insecure_auth == true or body.allow_insecure_auth == "1")
|
|
end
|
|
|
|
if has("disable_device_auth") then
|
|
runtime_cfg.disable_device_auth = (body.disable_device_auth == true or body.disable_device_auth == "1")
|
|
end
|
|
|
|
if runtime_cfg.default_agent == "custom-provider" and trim(runtime_cfg.provider_base_url) == "" then
|
|
write_json({ ok = false, error = "provider_base_url required for custom-provider" })
|
|
return
|
|
end
|
|
if trim(runtime_cfg.provider_base_url) ~= "" and not tostring(runtime_cfg.provider_base_url):match("^https?://") then
|
|
write_json({ ok = false, error = "invalid provider_base_url" })
|
|
return
|
|
end
|
|
|
|
uci:commit("openclawmgr")
|
|
if effective_base_dir == "" then
|
|
write_json({ ok = false, error = "base_dir required" })
|
|
return
|
|
end
|
|
local ok = write_runtime_config(effective_base_dir, {
|
|
port = effective_port,
|
|
bind = effective_bind,
|
|
}, runtime_cfg)
|
|
if not ok then
|
|
write_json({ ok = false, error = "write openclaw.json failed" })
|
|
return
|
|
end
|
|
for _, key in ipairs({ "port", "bind", "token", "allowed_origins", "allow_insecure_auth", "disable_device_auth", "default_agent", "default_model", "provider_api_key", "provider_base_url" }) do
|
|
uci:delete("openclawmgr", section, key)
|
|
end
|
|
uci:commit("openclawmgr")
|
|
write_json({ ok = true })
|
|
return
|
|
end
|
|
|
|
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
|
|
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
|
|
|
|
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 "/root/Configs/OpenClawMgr")
|
|
|
|
local runtime_cfg = get_runtime_config(base_dir)
|
|
|
|
write_json({
|
|
ok = true,
|
|
config = {
|
|
enabled = (uci:get("openclawmgr", "main", "enabled") or "0") == "1",
|
|
port = runtime_cfg.port,
|
|
bind = runtime_cfg.bind,
|
|
base_dir = base_dir,
|
|
token = runtime_cfg.token,
|
|
allowed_origins = runtime_cfg.allowed_origins,
|
|
allow_insecure_auth = runtime_cfg.allow_insecure_auth,
|
|
disable_device_auth = runtime_cfg.disable_device_auth,
|
|
default_agent = runtime_cfg.default_agent,
|
|
default_model = runtime_cfg.default_model,
|
|
install_accelerated = (uci:get("openclawmgr", "main", "install_accelerated") or "1") == "1",
|
|
provider_api_key = runtime_cfg.provider_api_key,
|
|
provider_base_url = runtime_cfg.provider_base_url,
|
|
},
|
|
options = {
|
|
base_dir_choices = choices,
|
|
suggested_base_dir = default_path or "",
|
|
default_origin = default_allowed_origin(runtime_cfg.port),
|
|
}
|
|
})
|
|
end
|
|
|
|
function action_security_data()
|
|
local uci = require "luci.model.uci".cursor()
|
|
local base_dir = trim(uci:get("openclawmgr", "main", "base_dir") or "")
|
|
local data, items = nil, {}
|
|
if base_dir == "" then
|
|
write_json({ ok = true, items = items })
|
|
return
|
|
end
|
|
data = load_security_config(base_dir)
|
|
for _, item in ipairs(data.items or {}) do
|
|
items[#items + 1] = security_check_item(item)
|
|
end
|
|
write_json({ ok = true, items = items })
|
|
end
|
|
|
|
function action_security_add()
|
|
local uci = require "luci.model.uci".cursor()
|
|
if not require_csrf() then
|
|
return
|
|
end
|
|
local base_dir = trim(uci:get("openclawmgr", "main", "base_dir") or "")
|
|
local body = read_json_body() or {}
|
|
local path = trim(body.path or "")
|
|
local data, ok, err, item, st, probe = nil, nil, nil, nil, nil, nil
|
|
if base_dir == "" then
|
|
write_json({ ok = false, error = "base_dir required" })
|
|
return
|
|
end
|
|
data = load_security_config(base_dir)
|
|
ok, err = validate_security_path(path, base_dir, data.items)
|
|
if not ok then
|
|
write_json({ ok = false, error = err })
|
|
return
|
|
end
|
|
item = {
|
|
id = "dir_" .. tostring(os.time()) .. tostring(math.random(1000, 9999)),
|
|
path = path,
|
|
orig_uid = 0,
|
|
orig_gid = 0,
|
|
orig_mode = "755",
|
|
}
|
|
probe = security_probe(path)
|
|
if not probe then
|
|
write_json({ ok = false, error = "目录不存在" })
|
|
return
|
|
end
|
|
if probe.kind ~= "directory" then
|
|
write_json({ ok = false, error = "目标不是目录" })
|
|
return
|
|
end
|
|
st = security_stat(path)
|
|
if not st then
|
|
write_json({ ok = false, error = "读取目录权限失败" })
|
|
return
|
|
end
|
|
item.orig_uid = st.uid
|
|
item.orig_gid = st.gid
|
|
item.orig_mode = st.mode
|
|
ok, err = security_apply(path)
|
|
if not ok then
|
|
write_json({ ok = false, error = err or "apply failed" })
|
|
return
|
|
end
|
|
data.items[#data.items + 1] = item
|
|
if not save_security_config(base_dir, data) then
|
|
write_json({ ok = false, error = "save security config failed" })
|
|
return
|
|
end
|
|
write_json({ ok = true, item = security_check_item(item) })
|
|
end
|
|
|
|
function action_security_remove()
|
|
local uci = require "luci.model.uci".cursor()
|
|
if not require_csrf() then
|
|
return
|
|
end
|
|
local base_dir = trim(uci:get("openclawmgr", "main", "base_dir") or "")
|
|
local body = read_json_body() or {}
|
|
local id = tostring(body.id or "")
|
|
local mode = tostring(body.mode or "direct")
|
|
local data, item, index = nil, nil, nil
|
|
if base_dir == "" then
|
|
write_json({ ok = false, error = "base_dir required" })
|
|
return
|
|
end
|
|
if mode ~= "direct" and mode ~= "restore" then
|
|
write_json({ ok = false, error = "invalid mode" })
|
|
return
|
|
end
|
|
data = load_security_config(base_dir)
|
|
item, index = find_security_item(data.items, id)
|
|
if not item then
|
|
write_json({ ok = false, error = "item not found" })
|
|
return
|
|
end
|
|
if mode == "restore" and security_path_exists(item.path) then
|
|
local ok, err = security_restore(item.path, item.orig_uid, item.orig_gid, item.orig_mode)
|
|
if not ok then
|
|
write_json({ ok = false, error = err or "restore failed" })
|
|
return
|
|
end
|
|
end
|
|
table.remove(data.items, index)
|
|
if not save_security_config(base_dir, data) then
|
|
write_json({ ok = false, error = "save security config failed" })
|
|
return
|
|
end
|
|
write_json({ ok = true })
|
|
end
|
|
|
|
function action_security_recheck()
|
|
local uci = require "luci.model.uci".cursor()
|
|
if not require_csrf() then
|
|
return
|
|
end
|
|
local base_dir = trim(uci:get("openclawmgr", "main", "base_dir") or "")
|
|
local body = read_json_body() or {}
|
|
local id = tostring(body.id or "")
|
|
local data, item = nil, nil
|
|
if base_dir == "" then
|
|
write_json({ ok = false, error = "base_dir required" })
|
|
return
|
|
end
|
|
data = load_security_config(base_dir)
|
|
item = find_security_item(data.items, id)
|
|
if not item then
|
|
write_json({ ok = false, error = "item not found" })
|
|
return
|
|
end
|
|
write_json({ ok = true, item = security_check_item(item) })
|
|
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 install_channel = http.formvalue("install_channel") 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
|
|
if install_channel ~= "" and install_channel ~= "stable" and install_channel ~= "latest" then
|
|
write_json({ ok = false, error = "invalid install_channel" })
|
|
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)
|
|
if install_channel ~= "" and (op == "install" or op == "upgrade") then
|
|
cmd = "INSTALL_CHANNEL=" .. util.shellquote(install_channel) .. " " .. cmd
|
|
end
|
|
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)
|
|
if install_channel ~= "" and (op == "install" or op == "upgrade") then
|
|
cmd = "INSTALL_CHANNEL=" .. util.shellquote(install_channel) .. " " .. cmd
|
|
end
|
|
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 ""
|
|
local enabled = uci:get("openclawmgr", "main", "enabled") or "0"
|
|
local runtime_gateway = get_runtime_gateway_config(base_dir, {
|
|
port = uci:get("openclawmgr", "main", "port") or "18789",
|
|
bind = uci:get("openclawmgr", "main", "bind") or "lan",
|
|
})
|
|
local port = runtime_gateway.port
|
|
local bind = runtime_gateway.bind
|
|
|
|
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 = ""
|
|
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 = 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+", "")
|
|
|
|
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 = (base_dir ~= "" and ("http://" .. get_host() .. ":" .. port .. configured_base_path(base_dir)) or ""),
|
|
})
|
|
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
|