Files
op-packages/luci-app-openclaw/luasrc/model/cbi/openclaw/basic.lua
T

745 lines
57 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- luci-app-openclaw — 基本设置 CBI Model
local sys = require "luci.sys"
m = Map("openclaw", "OpenClaw AI 网关",
"OpenClaw 是一个 AI 编程代理网关,支持 GitHub Copilot、Claude、GPT、Gemini 等大模型以及 QQ、Telegram、Discord 等多种消息渠道。")
-- 隐藏底部的「保存并应用」「保存」「复位」按钮 (本页无可编辑的 UCI 选项)
m.pageaction = false
-- ═══════════════════════════════════════════
-- 状态面板
-- ═══════════════════════════════════════════
m:section(SimpleSection).template = "openclaw/status"
-- ═══════════════════════════════════════════
-- 快捷操作
-- ═══════════════════════════════════════════
s3 = m:section(SimpleSection, nil, "快捷操作")
s3.template = "cbi/nullsection"
act = s3:option(DummyValue, "_actions")
act.rawhtml = true
act.cfgvalue = function(self, section)
local ctl_url = luci.dispatcher.build_url("admin", "services", "openclaw", "service_ctl")
local service_ctl_url = ctl_url
local log_url = luci.dispatcher.build_url("admin", "services", "openclaw", "setup_log")
local check_url = luci.dispatcher.build_url("admin", "services", "openclaw", "check_update")
local uninstall_url = luci.dispatcher.build_url("admin", "services", "openclaw", "uninstall")
local plugin_upgrade_url = luci.dispatcher.build_url("admin", "services", "openclaw", "plugin_upgrade")
local plugin_upgrade_log_url = luci.dispatcher.build_url("admin", "services", "openclaw", "plugin_upgrade_log")
local check_system_url = luci.dispatcher.build_url("admin", "services", "openclaw", "check_system")
-- CSRF token: 会改状态或返回凭据的端点已改为 post(),
-- LuCI 的 test_post_security() 要求表单里带上与会话匹配的 token。
local csrf_token = luci.dispatcher.context.authtoken or ""
local html = {}
html[#html+1] = '<script type="text/javascript">var ocCsrfToken=' ..
string.format('%q', csrf_token) .. ';</script>'
-- 按钮区域
html[#html+1] = '<div style="display:flex;gap:10px;flex-wrap:wrap;margin:10px 0;">'
html[#html+1] = '<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocShowSetupDialog()" id="btn-setup" title="下载 Node.js 并安装 OpenClaw">📦 安装运行环境</button>'
html[#html+1] = '<button class="btn cbi-button cbi-button-action" type="button" onclick="ocServiceCtl(\'restart\')">🔄 重启服务</button>'
html[#html+1] = '<button class="btn cbi-button cbi-button-action" type="button" onclick="ocServiceCtl(\'stop\')">⏹️ 停止服务</button>'
html[#html+1] = '<span style="position:relative;display:inline-block;" id="btn-check-update-wrap"><button class="btn cbi-button cbi-button-action" type="button" onclick="ocCheckUpdate()" id="btn-check-update">🔍 检测升级</button><span id="update-dot" style="display:none;position:absolute;top:-2px;right:-2px;width:10px;height:10px;background:#e36209;border-radius:50%;border:2px solid #fff;box-shadow:0 0 0 1px #e36209;"></span></span>'
html[#html+1] = '<button class="btn cbi-button cbi-button-action" type="button" onclick="ocBackupRestore()" id="btn-backup" title="备份或恢复 OpenClaw 配置">💾 备份/恢复</button>'
html[#html+1] = '<button class="btn cbi-button cbi-button-remove" type="button" onclick="ocUninstall()" id="btn-uninstall" title="删除 Node.js、OpenClaw 运行环境及相关数据">🗑️ 卸载环境</button>'
html[#html+1] = '</div>'
html[#html+1] = '<div id="action-result" style="margin-top:8px;"></div>'
html[#html+1] = '<div id="oc-update-action" style="margin-top:8px;display:none;"></div>'
-- 版本选择对话框 (默认隐藏) - 支持自定义安装路径
html[#html+1] = '<div id="oc-setup-dialog" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;align-items:center;justify-content:center;">'
html[#html+1] = '<div style="background:#fff;border-radius:12px;padding:24px 28px;max-width:520px;width:92%;box-shadow:0 8px 32px rgba(0,0,0,0.2);">'
html[#html+1] = '<h3 style="margin:0 0 16px 0;font-size:16px;color:#333;">📦 选择安装版本</h3>'
html[#html+1] = '<div style="display:flex;flex-direction:column;gap:12px;">'
-- 稳定版选项
local default_tested_ver = luci.sys.exec("sed -n 's/^OC_TESTED_VERSION=\"\\(.*\\)\"/\\1/p' /usr/bin/openclaw-env 2>/dev/null"):gsub("%s+", "")
if default_tested_ver == "" then default_tested_ver = "2026.9.1" end
html[#html+1] = '<label style="display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border:2px solid #4a90d9;border-radius:8px;cursor:pointer;background:#f0f7ff;" id="oc-opt-stable">'
html[#html+1] = '<input type="radio" name="oc-ver-choice" value="stable" checked style="margin-top:2px;">'
html[#html+1] = '<div><strong style="color:#333;">✅ 稳定版 (推荐)</strong>'
html[#html+1] = '<div style="font-size:12px;color:#666;margin-top:4px;">版本 v' .. default_tested_ver .. ' (已验证版),已经过完整测试,兼容性良好。</div>'
html[#html+1] = '</div></label>'
-- 最新版选项
html[#html+1] = '<label style="display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border:2px solid #e0e0e0;border-radius:8px;cursor:pointer;background:#fff;" id="oc-opt-latest">'
html[#html+1] = '<input type="radio" name="oc-ver-choice" value="latest" style="margin-top:2px;">'
html[#html+1] = '<div><strong style="color:#333;">🆕 最新版 (未经验证)</strong>'
html[#html+1] = '<div style="font-size:12px;color:#e36209;margin-top:4px;">⚠️ 安装 npm 上的最新发布版本 (未经验证),可能存在未经验证的兼容性问题。</div>'
html[#html+1] = '</div></label>'
html[#html+1] = '</div>'
-- 自定义安装路径
html[#html+1] = '<div style="margin-top:16px;padding-top:14px;border-top:1px solid #eee;">'
html[#html+1] = '<div style="font-weight:600;font-size:13px;color:#333;margin-bottom:8px;">📂 安装路径</div>'
html[#html+1] = '<div style="display:flex;gap:8px;align-items:center;">'
html[#html+1] = '<input type="text" id="oc-install-path" value="/opt" style="flex:1;padding:8px 12px;border:1px solid #d0d7de;border-radius:6px;font-size:13px;" placeholder="/opt">'
html[#html+1] = '<button class="btn cbi-button" type="button" onclick="ocCheckInstallPath()" style="font-size:12px;padding:4px 10px;">检测空间</button>'
html[#html+1] = '</div>'
html[#html+1] = '<div id="oc-path-info" style="font-size:11px;color:#666;margin-top:6px;">💡 程序将在此路径下创建 openclaw 目录进行安装。最小需要 2GB 可用空间。如安装在第二块硬盘,请确保硬盘已挂载。</div>'
html[#html+1] = '</div>'
-- 按钮区
html[#html+1] = '<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:20px;">'
html[#html+1] = '<button class="btn cbi-button" type="button" onclick="ocCloseSetupDialog()" style="min-width:80px;">取消</button>'
html[#html+1] = '<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocConfirmSetup()" style="min-width:80px;">开始安装</button>'
html[#html+1] = '</div>'
html[#html+1] = '</div></div>'
-- 安装日志面板 (默认隐藏)
html[#html+1] = '<div id="setup-log-panel" style="display:none;margin-top:12px;">'
html[#html+1] = '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">'
html[#html+1] = '<span id="setup-log-title" style="font-weight:600;font-size:14px;">📋 安装日志</span>'
html[#html+1] = '<span id="setup-log-status" style="font-size:12px;color:#999;"></span>'
html[#html+1] = '</div>'
html[#html+1] = '<pre id="setup-log-content" style="background:#1a1b26;color:#a9b1d6;padding:14px 16px;border-radius:6px;font-size:12px;line-height:1.6;max-height:400px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;border:1px solid #2d333b;margin:0;"></pre>'
html[#html+1] = '<div id="setup-log-result" style="margin-top:10px;display:none;"></div>'
html[#html+1] = '</div>'
-- JavaScript
html[#html+1] = '<script type="text/javascript">'
html[#html+1] = 'console.log("OpenClaw JS loading...");'
-- 版本选择对话框逻辑
html[#html+1] = 'var _setupTimer=null;'
html[#html+1] = 'var _setupOperation="setup";'
html[#html+1] = 'var _setupPollErrors=0;'
html[#html+1] = 'function ocAcceptedResponse(x){'
html[#html+1] = 'if(!x||typeof x.status!=="number"||x.status<200||x.status>=300)return{ok:false,message:"服务器拒绝请求 (HTTP "+(x&&x.status||0)+")"};'
html[#html+1] = 'try{var r=JSON.parse(x.responseText);return r&&r.status==="ok"?{ok:true,data:r}:{ok:false,message:(r&&r.message)||"服务器未接受请求"};}catch(e){return{ok:false,message:"服务器返回了无效响应"};}'
html[#html+1] = '}'
html[#html+1] = 'function ocSetupRequestRejected(btn,label,message){'
html[#html+1] = 'if(_setupTimer){clearInterval(_setupTimer);_setupTimer=null;}'
html[#html+1] = 'if(btn){btn.disabled=false;btn.textContent=label;}'
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");var logEl=document.getElementById("setup-log-content");'
html[#html+1] = 'if(statusEl)statusEl.textContent="❌ "+message;if(logEl)logEl.textContent+=message+"\\n";'
html[#html+1] = '}'
html[#html+1] = 'function ocSetupPollingUnknown(){'
html[#html+1] = 'if(_setupTimer){clearInterval(_setupTimer);_setupTimer=null;}'
html[#html+1] = 'var btn=document.getElementById(_setupOperation==="core"?"btn-core-upgrade":"btn-setup");'
html[#html+1] = 'if(btn){btn.disabled=false;btn.textContent=_setupOperation==="core"?"⬆️ 重试核心升级":"📦 安装运行环境";}'
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");if(statusEl)statusEl.textContent="⚠️ 状态查询连续失败,请手动刷新确认;后台任务可能仍在运行。";'
html[#html+1] = '}'
html[#html+1] = 'function ocShowSetupDialog(){'
html[#html+1] = 'var dlg=document.getElementById("oc-setup-dialog");'
html[#html+1] = 'dlg.style.display="flex";'
html[#html+1] = 'var radios=document.getElementsByName("oc-ver-choice");'
html[#html+1] = 'for(var i=0;i<radios.length;i++){if(radios[i].value==="stable")radios[i].checked=true;}'
html[#html+1] = '}'
html[#html+1] = 'function ocCloseSetupDialog(){'
html[#html+1] = 'document.getElementById("oc-setup-dialog").style.display="none";'
html[#html+1] = '}'
-- 检测自定义安装路径的磁盘空间
html[#html+1] = 'function ocCheckInstallPath(){'
html[#html+1] = 'var pathEl=document.getElementById("oc-install-path");'
html[#html+1] = 'var infoEl=document.getElementById("oc-path-info");'
html[#html+1] = 'var path=pathEl.value.trim();'
html[#html+1] = 'if(!path){path="/opt";pathEl.value=path;}'
html[#html+1] = 'infoEl.innerHTML="⏳ 正在检测空间...";'
html[#html+1] = '(new XHR()).get("' .. check_system_url .. '?install_path="+encodeURIComponent(path),null,function(x){'
html[#html+1] = 'try{'
html[#html+1] = 'var r=JSON.parse(x.responseText);'
html[#html+1] = 'if(r.disk_ok){'
html[#html+1] = 'infoEl.innerHTML="<span style=\\"color:#1a7f37;\\">✅ 可用空间: "+r.disk_free_str+" (检测路径: "+r.disk_path+")</span>";'
html[#html+1] = '}else{'
html[#html+1] = 'infoEl.innerHTML="<span style=\\"color:#cf222e;\\">❌ 空间不足: "+r.disk_mb+" MB 可用,需要 ≥ 2048 MB (检测路径: "+r.disk_path+")</span>";'
html[#html+1] = '}'
html[#html+1] = '}catch(e){infoEl.innerHTML="<span style=\\"color:#e36209;\\">⚠️ 检测失败</span>";}'
html[#html+1] = '});'
html[#html+1] = '}'
html[#html+1] = 'function ocConfirmSetup(){'
html[#html+1] = 'var btn=document.getElementById("btn-setup");'
html[#html+1] = 'var pathEl=document.getElementById("oc-install-path");'
html[#html+1] = 'var installPath=pathEl.value.trim()||"/opt";'
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 检测系统配置...";'
html[#html+1] = '(new XHR()).get("' .. check_system_url .. '?install_path="+encodeURIComponent(installPath),null,function(x){'
html[#html+1] = 'try{'
html[#html+1] = 'var r=JSON.parse(x.responseText);'
html[#html+1] = 'var panel=document.getElementById("setup-log-panel");'
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
html[#html+1] = 'var titleEl=document.getElementById("setup-log-title");'
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
html[#html+1] = 'var resultEl=document.getElementById("setup-log-result");'
html[#html+1] = 'var actionEl=document.getElementById("action-result");'
html[#html+1] = 'actionEl.textContent="";'
html[#html+1] = 'panel.style.display="block";'
html[#html+1] = 'resultEl.style.display="none";'
html[#html+1] = 'titleEl.textContent="📋 安装日志";'
html[#html+1] = 'logEl.textContent="";'
html[#html+1] = 'logEl.textContent+="════════════════════════════════════════\\n";'
html[#html+1] = 'logEl.textContent+="🔍 系统配置检测\\n";'
html[#html+1] = 'logEl.textContent+="════════════════════════════════════════\\n";'
html[#html+1] = 'logEl.textContent+="安装路径: "+r.install_path+"\\n";'
html[#html+1] = 'logEl.textContent+="内存: "+r.memory_mb+" MB (需要 ≥ 1024 MB) — "+(r.memory_ok?"✅ 通过":"❌ 不达标")+"\\n";'
html[#html+1] = 'logEl.textContent+="磁盘: "+r.disk_mb+" MB 可用 (需要 ≥ 2048 MB) — "+(r.disk_ok?"✅ 通过":"❌ 不达标")+"\\n";'
html[#html+1] = 'logEl.textContent+="\\n";'
html[#html+1] = 'if(!r.pass){'
html[#html+1] = 'ocCloseSetupDialog();'
html[#html+1] = 'btn.disabled=false;btn.textContent="📦 安装运行环境";'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#cf222e;\\">❌ 系统配置不满足要求</span>";'
html[#html+1] = 'logEl.textContent+="❌ 系统配置不满足要求,安装已终止\\n";'
html[#html+1] = 'logEl.textContent+="💡 请升级硬件配置或清理磁盘空间后重试\\n";'
html[#html+1] = 'resultEl.style.display="block";'
html[#html+1] = 'resultEl.innerHTML="<div style=\\"border:1px solid #f5c6cb;background:#ffeef0;padding:12px 16px;border-radius:6px;\\">"+'
html[#html+1] = '"<strong style=\\"color:#cf222e;font-size:14px;\\">❌ 系统配置不满足要求</strong><br/>"+'
html[#html+1] = '"<div style=\\"margin-top:8px;font-size:12px;color:#666;\\">💡 请升级硬件配置或清理磁盘空间后重试。</div></div>";'
html[#html+1] = 'return;'
html[#html+1] = '}'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 安装进行中...</span>";'
html[#html+1] = 'logEl.textContent+="✅ 系统配置检测通过,开始安装...\\n\\n";'
html[#html+1] = 'ocCloseSetupDialog();'
html[#html+1] = 'var radios=document.getElementsByName("oc-ver-choice");'
html[#html+1] = 'var choice="stable";'
html[#html+1] = 'for(var i=0;i<radios.length;i++){if(radios[i].checked){choice=radios[i].value;break;}}'
html[#html+1] = 'var verParam=(choice==="stable")?"stable":"latest";'
html[#html+1] = 'ocSetup(verParam,r.memory_mb,r.disk_mb,r.install_path);'
html[#html+1] = '}catch(e){'
html[#html+1] = 'ocCloseSetupDialog();'
html[#html+1] = 'btn.disabled=false;btn.textContent="📦 安装运行环境";'
html[#html+1] = 'alert("系统检测失败,请重试");'
html[#html+1] = '}});'
html[#html+1] = '}'
-- 安装运行环境 (带实时日志,支持自定义路径)
html[#html+1] = 'function ocSetup(version,mem_mb,disk_mb,install_path){'
html[#html+1] = '_setupOperation="setup";'
html[#html+1] = 'var btn=document.getElementById("btn-setup");'
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 安装中...";'
html[#html+1] = 'logEl.textContent+="════════════════════════════════════════\\n";'
html[#html+1] = 'logEl.textContent+="📦 安装运行环境 ("+((version==="stable")?"稳定版":"最新版")+")\\n";'
html[#html+1] = 'logEl.textContent+="════════════════════════════════════════\\n";'
html[#html+1] = 'logEl.textContent+="安装路径: "+install_path+"\\n";'
html[#html+1] = 'logEl.textContent+="正在启动安装...\\n";'
html[#html+1] = '(new XHR()).post("' .. ctl_url .. '?action=setup&version="+encodeURIComponent(version)+"&install_path="+encodeURIComponent(install_path),{token:ocCsrfToken},function(x){'
html[#html+1] = 'var accepted=ocAcceptedResponse(x);'
html[#html+1] = 'if(accepted.ok){ocPollSetupLog();}else{ocSetupRequestRejected(btn,"📦 安装运行环境",accepted.message);}'
html[#html+1] = '});'
html[#html+1] = '}'
-- 轮询安装日志 (智能滚动: 用户向上滚动时暂停自动滚动,滚动到底部时恢复)
html[#html+1] = 'var _lastLogLen=0;'
html[#html+1] = 'var _autoScrollEnabled=true;' -- 智能滚动状态标志
html[#html+1] = 'function ocPollSetupLog(){'
html[#html+1] = 'if(_setupTimer)clearInterval(_setupTimer);'
html[#html+1] = '_lastLogLen=0;'
html[#html+1] = '_setupPollErrors=0;'
html[#html+1] = '_autoScrollEnabled=true;' -- 初始状态: 启用自动滚动
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
-- 绑定滚动事件监听器 (只绑定一次)
html[#html+1] = 'if(!logEl._scrollListenerAttached){'
html[#html+1] = 'logEl.addEventListener("scroll",function(){'
html[#html+1] = 'var el=this;'
html[#html+1] = 'var atBottom=el.scrollHeight-el.scrollTop-el.clientHeight<5;'
html[#html+1] = 'if(atBottom){'
html[#html+1] = '_autoScrollEnabled=true;' -- 滚动到底部: 恢复自动滚动
html[#html+1] = '}else{'
html[#html+1] = '_autoScrollEnabled=false;' -- 用户向上滚动: 暂停自动滚动
html[#html+1] = '}'
html[#html+1] = '});'
html[#html+1] = 'logEl._scrollListenerAttached=true;'
html[#html+1] = '}'
html[#html+1] = '_setupTimer=setInterval(function(){'
html[#html+1] = '(new XHR()).get("' .. log_url .. '",null,function(x){'
html[#html+1] = 'try{'
html[#html+1] = 'if(!x||x.status<200||x.status>=300)throw new Error("HTTP "+(x&&x.status||0));'
html[#html+1] = 'var r=JSON.parse(x.responseText);'
html[#html+1] = '_setupPollErrors=0;'
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
html[#html+1] = 'if(r.log&&r.log.length>_lastLogLen){'
html[#html+1] = 'var newLog=r.log.substring(_lastLogLen);'
html[#html+1] = 'logEl.textContent+=newLog;'
html[#html+1] = '_lastLogLen=r.log.length;'
html[#html+1] = '}'
-- 智能滚动: 仅在自动滚动启用时滚动到底部
html[#html+1] = 'if(_autoScrollEnabled){'
html[#html+1] = 'logEl.scrollTop=logEl.scrollHeight;'
html[#html+1] = '}'
html[#html+1] = 'if(r.state==="running"){'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 安装进行中...</span>";'
html[#html+1] = '}else if(r.state==="success"){'
html[#html+1] = 'clearInterval(_setupTimer);_setupTimer=null;'
html[#html+1] = 'ocSetupDone(true,logEl.textContent);'
html[#html+1] = '}else if(r.state==="failed"){'
html[#html+1] = 'clearInterval(_setupTimer);_setupTimer=null;'
html[#html+1] = 'ocSetupDone(false,logEl.textContent);'
html[#html+1] = '}'
html[#html+1] = '}catch(e){_setupPollErrors++;if(_setupPollErrors>=3)ocSetupPollingUnknown();}'
html[#html+1] = '});'
html[#html+1] = '},1500);'
html[#html+1] = '}'
-- 安装完成处理
html[#html+1] = 'function ocSetupDone(ok,log){'
html[#html+1] = 'var btn=document.getElementById(_setupOperation==="core"?"btn-core-upgrade":"btn-setup");'
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
html[#html+1] = 'var resultEl=document.getElementById("setup-log-result");'
html[#html+1] = 'if(btn){btn.disabled=false;btn.textContent=_setupOperation==="core"?(ok?"⬆️ 核心升级完成":"⬆️ 重试核心升级"):"📦 安装运行环境";}'
html[#html+1] = 'resultEl.style.display="block";'
html[#html+1] = 'if(ok){'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#1a7f37;\\">✅ 安装完成</span>";'
html[#html+1] = 'resultEl.innerHTML="<div style=\\"border:1px solid #c6e9c9;background:#e6f7e9;padding:12px 16px;border-radius:6px;\\">"+'
html[#html+1] = '"<strong style=\\"color:#1a7f37;font-size:14px;\\">🎉 恭喜!OpenClaw 运行环境安装成功!</strong><br/>"+'
html[#html+1] = '"<span style=\\"color:#555;font-size:13px;line-height:1.8;\\">服务已自动启用并启动,点击下方按钮刷新页面查看运行状态。</span><br/>"+'
html[#html+1] = '"<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"location.reload()\\" style=\\"margin-top:10px;\\">🔄 刷新页面</button></div>";'
html[#html+1] = '}else{'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#cf222e;\\">❌ 安装失败</span>";'
-- 分析失败原因
html[#html+1] = 'var reasons=ocAnalyzeFailure(log);'
html[#html+1] = 'resultEl.innerHTML="<div style=\\"border:1px solid #f5c6cb;background:#ffeef0;padding:12px 16px;border-radius:6px;\\">"+'
html[#html+1] = '"<strong style=\\"color:#cf222e;font-size:14px;\\">❌ 安装失败</strong><br/>"+'
html[#html+1] = '"<div style=\\"margin:8px 0;padding:10px 14px;background:#fff5f5;border-radius:4px;font-size:13px;line-height:1.8;\\">"+'
html[#html+1] = '"<strong>🔍 可能的失败原因:</strong><br/>"+reasons+"</div>"+'
html[#html+1] = '"<div style=\\"margin-top:8px;font-size:12px;color:#666;\\">💡 完整日志见上方终端输出,也可在终端查看:<code>cat /tmp/openclaw-setup.log</code></div></div>";'
html[#html+1] = '}'
html[#html+1] = '}'
-- 分析失败原因
html[#html+1] = 'function ocAnalyzeFailure(log){'
html[#html+1] = 'var reasons=[];'
html[#html+1] = 'if(!log)return"未知错误,请检查日志。";'
html[#html+1] = 'var ll=log.toLowerCase();'
-- 网络问题
html[#html+1] = 'if(ll.indexOf("could not resolve")>=0||ll.indexOf("connection timed out")>=0||ll.indexOf("curl")>=0&&ll.indexOf("fail")>=0||ll.indexOf("wget")>=0&&ll.indexOf("fail")>=0||ll.indexOf("所有镜像均下载失败")>=0){'
html[#html+1] = 'reasons.push("🌐 <b>网络连接失败</b> — 无法下载 Node.js。请检查路由器是否能访问外网。<br/>&nbsp;&nbsp;💡 解决: 检查 DNS 设置和网络连接,或手动指定镜像: <code>NODE_MIRROR=https://npmmirror.com/mirrors/node openclaw-env setup</code>");'
html[#html+1] = '}'
-- 磁盘空间
html[#html+1] = 'if(ll.indexOf("no space")>=0||ll.indexOf("disk full")>=0||ll.indexOf("enospc")>=0){'
html[#html+1] = 'reasons.push("💾 <b>磁盘空间不足</b> — Node.js + OpenClaw 需要约 200MB 空间。<br/>&nbsp;&nbsp;💡 解决: 运行 <code>df -h</code> 检查可用空间,清理不需要的文件或使用外部存储。");'
html[#html+1] = '}'
-- 架构不支持
html[#html+1] = 'if(ll.indexOf("不支持的 cpu 架构")>=0||ll.indexOf("不支持的架构")>=0){'
html[#html+1] = 'reasons.push("🔧 <b>CPU 架构不支持</b> — 仅支持 x86_64 和 aarch64 (ARM64)。<br/>&nbsp;&nbsp;💡 当前设备架构可能是 32 位 ARM 或 MIPS,无法运行 Node.js 22。");'
html[#html+1] = '}'
-- npm 安装失败
html[#html+1] = 'if(ll.indexOf("npm err")>=0||ll.indexOf("npm warn")>=0&&ll.indexOf("openclaw 安装验证失败")>=0){'
html[#html+1] = 'reasons.push("📦 <b>npm 安装 OpenClaw 失败</b> — npm 包下载或安装出错。<br/>&nbsp;&nbsp;💡 解决: 尝试手动安装 <code>openclaw-env setup</code> 或检查网络连接。");'
html[#html+1] = '}'
-- 权限问题
html[#html+1] = 'if(ll.indexOf("permission denied")>=0||ll.indexOf("eacces")>=0){'
html[#html+1] = 'reasons.push("🔒 <b>权限不足</b> — 文件或目录权限问题。<br/>&nbsp;&nbsp;💡 解决: 运行 <code>openclaw-env setup</code> 或以 root 用户重试。");'
html[#html+1] = '}'
-- tar 解压失败
html[#html+1] = 'if(ll.indexOf("tar")>=0&&(ll.indexOf("error")>=0||ll.indexOf("fail")>=0)){'
html[#html+1] = 'reasons.push("📂 <b>解压失败</b> — Node.js 安装包可能下载不完整。<br/>&nbsp;&nbsp;💡 解决: 删除缓存重试 <code>openclaw-env setup</code>");'
html[#html+1] = '}'
-- 验证失败
html[#html+1] = 'if(ll.indexOf("安装验证失败")>=0){'
html[#html+1] = 'reasons.push("⚠️ <b>安装验证失败</b> — 程序已下载但无法正常运行。<br/>&nbsp;&nbsp;💡 可能是 glibc/musl 不兼容,请确认系统 C 库类型: <code>ldd --version 2>&1 | head -1</code>");'
html[#html+1] = '}'
-- 兜底
html[#html+1] = 'if(reasons.length===0){'
html[#html+1] = 'reasons.push("⚠️ <b>未识别的错误</b> — 请查看上方完整日志分析具体原因。<br/>&nbsp;&nbsp;💡 您也可以尝试手动执行: <code>openclaw-env setup</code> 查看详细输出。");'
html[#html+1] = '}'
html[#html+1] = 'return reasons.join("<br/><br/>");'
html[#html+1] = '}'
-- 普通服务操作 (restart/stop)
html[#html+1] = 'function ocServiceCtl(action){'
html[#html+1] = 'var el=document.getElementById("action-result");'
html[#html+1] = 'el.innerHTML="<span style=\\"color:#999\\">⏳ 正在执行...</span>";'
html[#html+1] = '(new XHR()).post("' .. ctl_url .. '?action="+action,{token:ocCsrfToken},function(x){'
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
html[#html+1] = 'if(r.status==="ok"){el.innerHTML="<span style=\\"color:green\\">✅ "+action+" 已完成</span>";}'
html[#html+1] = 'else{el.innerHTML="<span style=\\"color:red\\">❌ "+(r.message||"失败")+"</span>";}'
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 错误</span>";}'
html[#html+1] = '});}'
-- Markdown 转 HTML 函数 (优化版 - 用于渲染 GitHub Release Notes)
-- 特性:中英文字体栈、代码块样式、链接悬停效果、列表美化
html[#html+1] = 'function ocMarkdownToHtml(md){'
html[#html+1] = 'if(!md)return "";'
-- 转义 HTML 特殊字符
html[#html+1] = 'var html=md.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");'
-- 代码块 (```code```) - 圆角边框 + 等宽字体栈
html[#html+1] = 'html=html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g,function(m,lang,code){return"<pre style=\\"background:#f6f8fa;padding:12px 16px;border-radius:8px;overflow-x:auto;font-size:13px;line-height:1.6;font-family:SF Mono,Consolas,Menlo,monospace;border:1px solid #d0d7de;\\"><code>"+code.trim()+"</code></pre>";});'
-- 行内代码 (`code`)
html[#html+1] = 'html=html.replace(/`([^`]+)`/g,"<code style=\\"background:#f6f8fa;padding:2px 6px;border-radius:4px;font-size:13px;font-family:SF Mono,Consolas,Menlo,monospace;border:1px solid #e1e4e8;\\">$1</code>");'
-- 标题层级 (优化比例: h1=20px, h2=17px, h3=15px, h4=14px)
html[#html+1] = 'html=html.replace(/^#### (.+)$/gm,"<h5 style=\\"margin:12px 0 6px;font-size:14px;font-weight:600;color:#1f2328;letter-spacing:-0.02em;\\">$1</h5>");'
html[#html+1] = 'html=html.replace(/^### (.+)$/gm,"<h4 style=\\"margin:14px 0 8px;font-size:15px;font-weight:600;color:#1f2328;letter-spacing:-0.02em;\\">$1</h4>");'
html[#html+1] = 'html=html.replace(/^## (.+)$/gm,"<h3 style=\\"margin:16px 0 10px;font-size:17px;font-weight:600;color:#1f2328;letter-spacing:-0.02em;\\">$1</h3>");'
html[#html+1] = 'html=html.replace(/^# (.+)$/gm,"<h2 style=\\"margin:18px 0 12px;font-size:20px;font-weight:600;color:#1f2328;border-bottom:1px solid #d0d7de;padding-bottom:8px;letter-spacing:-0.02em;\\">$1</h2>");'
-- 粗体和斜体
html[#html+1] = 'html=html.replace(/\\*\\*([^*]+)\\*\\*/g,"<strong style=\\"font-weight:600;\\">$1</strong>");'
html[#html+1] = 'html=html.replace(/\\*([^*]+)\\*/g,"<em>$1</em>");'
-- 链接 [text](url) - 悬停效果
html[#html+1] = 'html=html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,"<a href=\\"$2\\" target=\\"_blank\\" rel=\\"noopener\\" style=\\"color:#0969da;text-decoration:none;border-bottom:1px solid transparent;transition:border-color 0.2s;\\">$1</a>");'
-- 无序列表 (- 或 *)
html[#html+1] = 'html=html.replace(/^[*-] (.+)$/gm,"<li style=\\"margin:4px 0 4px 0;padding-left:4px;line-height:1.75;list-style-position:inside;color:#32383f;\\">$1</li>");'
-- 有序列表 (1. 2. 等)
html[#html+1] = 'html=html.replace(/^(\\d+)\\. (.+)$/gm,"<li style=\\"margin:4px 0 4px 0;padding-left:4px;line-height:1.75;list-style-position:inside;color:#32383f;\\"><span style=\\"color:#656d76;margin-right:4px;\\">$1.</span>$2</li>");'
-- 水平线 (--- 或 ***)
html[#html+1] = 'html=html.replace(/^(---|\\*\\*\\*)$/gm,"<hr style=\\"border:none;border-top:1px solid #d0d7de;margin:16px 0;\\"/>");'
-- 段落: 连续空行合并为段落分隔 (简化处理,避免生成未闭合标签)
html[#html+1] = 'html=html.replace(/\\n\\n+/g,"<br/><br/>");'
-- 换行
html[#html+1] = 'html=html.replace(/\\n/g,"<br/>");'
-- 外层容器 - 中英文字体栈
html[#html+1] = 'return"<div style=\\"font-size:14px;line-height:1.75;color:#32383f;font-family:PingFang SC,Microsoft YaHei,Noto Sans SC,sans-serif;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;\\">"+html+"</div>";'
html[#html+1] = '}'
-- 检测升级 (只检查插件版本,有新版本时显示更新内容)
html[#html+1] = 'function ocCheckUpdate(){'
html[#html+1] = 'var btn=document.getElementById("btn-check-update");'
html[#html+1] = 'var el=document.getElementById("action-result");'
html[#html+1] = 'var act=document.getElementById("oc-update-action");'
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 正在检测...";el.textContent="";act.style.display="none";'
html[#html+1] = '(new XHR()).get("' .. check_url .. '",null,function(x){'
html[#html+1] = 'btn.disabled=false;btn.textContent="🔍 检测升级";'
html[#html+1] = 'var dot=document.getElementById("update-dot");if(dot)dot.style.display="none";'
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
html[#html+1] = 'var msgs=[];'
-- 版本状态徽章样式
html[#html+1] = 'var badgeNew="display:inline-flex;align-items:center;padding:3px 10px;border-radius:16px;font-size:12px;font-weight:600;background:linear-gradient(135deg,#fff7e6 0%,#ffe7ba 100%);color:#9a6700;border:1px solid #f5a623;";'
html[#html+1] = 'var badgeLatest="display:inline-flex;align-items:center;padding:3px 10px;border-radius:16px;font-size:12px;font-weight:600;background:linear-gradient(135deg,#e6f7e6 0%,#c8f7c8 100%);color:#1a7f1a;border:1px solid #28a745;";'
html[#html+1] = 'var badgeUnknown="display:inline-flex;align-items:center;padding:3px 10px;border-radius:16px;font-size:12px;font-weight:600;background:#f6f8fa;color:#656d76;border:1px solid #d0d7de;";'
html[#html+1] = 'var verBadge="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-family:SF Mono,Consolas,Menlo,monospace;background:#e1e4e8;color:#24292f;margin-left:4px;";'
-- 插件版本检查 (带渐变徽章)
html[#html+1] = 'if(r.plugin_current){'
html[#html+1] = 'if(r.plugin_has_update){msgs.push("<span style=\\""+badgeNew+"\\">🔌 插件有新版</span> v"+r.plugin_current+" → <span style=\\""+verBadge+"\\">v"+r.plugin_latest+"</span>");}'
html[#html+1] = 'else if(r.plugin_latest){msgs.push("<span style=\\""+badgeLatest+"\\">🔌 插件已是最新</span> v"+r.plugin_current);}'
html[#html+1] = 'else{msgs.push("<span style=\\""+badgeUnknown+"\\">🔌 插件无法检查</span> v"+r.plugin_current);}'
html[#html+1] = '}'
-- OpenClaw 核心版本检查
html[#html+1] = 'if(r.openclaw_current){'
html[#html+1] = 'if(r.openclaw_has_update){msgs.push("<span style=\\""+badgeNew+"\\">🦞 核心有新版</span> v"+r.openclaw_current+" → <span style=\\""+verBadge+"\\">v"+r.openclaw_latest+"</span>");}'
html[#html+1] = 'else if(r.openclaw_latest){msgs.push("<span style=\\""+badgeLatest+"\\">🦞 核心已是最新</span> v"+r.openclaw_current);}'
html[#html+1] = 'else{msgs.push("<span style=\\""+badgeUnknown+"\\">🦞 核心无法检查</span> v"+r.openclaw_current);}'
html[#html+1] = '}'
html[#html+1] = 'if(msgs.length===0)msgs.push("<span style=\\""+badgeUnknown+"\\">无法获取版本信息</span>");'
html[#html+1] = 'el.innerHTML=msgs.join("<br/>");'
-- 有任何更新时显示操作面板
html[#html+1] = 'if(r.plugin_has_update || r.openclaw_has_update){'
html[#html+1] = 'act.style.display="block";'
html[#html+1] = 'window._pluginLatestVer=r.plugin_latest;'
html[#html+1] = 'window._coreLatestVer=r.openclaw_latest;'
html[#html+1] = 'var notesHtml="";'
html[#html+1] = 'if(r.plugin_has_update && r.release_notes){'
html[#html+1] = 'var rendered=ocMarkdownToHtml(r.release_notes);'
-- 卡片式容器: 圆角边框 + 微阴影 + 版本标题栏
html[#html+1] = 'notesHtml="<div style=\\"margin:12px 0;border:1px solid #d0d7de;border-radius:10px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06);\\"><div style=\\"background:linear-gradient(135deg,#f6f8fa 0%,#ffffff 100%);padding:12px 16px;border-bottom:1px solid #d0d7de;display:flex;align-items:center;justify-content:space-between;\\"><span style=\\"font-size:14px;font-weight:600;color:#24292f;\\">📋 插件更新日志</span><span style=\\"display:inline-flex;align-items:center;padding:2px 10px;border-radius:12px;font-size:12px;font-weight:600;background:linear-gradient(135deg,#e3f2fd 0%,#bbdefb 100%);color:#1565c0;border:1px solid #64b5f6;\\">v"+r.plugin_latest+"</span></div><div style=\\"padding:16px;max-height:450px;overflow-y:auto;background:#fff;\\">"+rendered+"</div></div>";'
html[#html+1] = '}'
-- 操作按钮区: 支持一键升级插件与一键升级核心
html[#html+1] = 'var actBtns="<div style=\\"margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;\\">";'
html[#html+1] = 'if(r.plugin_has_update){actBtns+="<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"ocPluginUpgrade()\\" id=\\"btn-plugin-upgrade\\" style=\\"box-shadow:0 2px 4px rgba(0,0,0,0.1);\\">⬆️ 一键升级插件 (v"+r.plugin_latest+")</button><a href=\\"https://github.com/10000ge10000/luci-app-openclaw/releases/latest\\" target=\\"_blank\\" rel=\\"noopener\\" class=\\"btn cbi-button cbi-button-action\\" style=\\"text-decoration:none;\\">📥 GitHub 下载</a>";}'
html[#html+1] = 'if(r.openclaw_has_update){actBtns+="<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"ocOpenclawCoreUpgrade()\\" id=\\"btn-core-upgrade\\" style=\\"background:#d97706;border-color:#b45309;color:#fff;box-shadow:0 2px 4px rgba(0,0,0,0.1);\\">⬆️ 一键升级 OpenClaw 核心 (v"+r.openclaw_latest+")</button>";}'
html[#html+1] = 'actBtns+="</div>";'
html[#html+1] = 'act.innerHTML=notesHtml+actBtns;'
html[#html+1] = '}'
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 检测失败</span>";}'
html[#html+1] = '});}'
html[#html+1] = 'function ocOpenclawCoreUpgrade(){'
html[#html+1] = '_setupOperation="core";'
html[#html+1] = 'var ver=window._coreLatestVer||"2026.9.1";'
html[#html+1] = 'if(!confirm("确定要将 OpenClaw 核心升级到 v"+ver+"?\\n\\n升级将执行安全事务:自动全量备份状态、SQLite 数据库预检与配置平滑迁移。"))return;'
html[#html+1] = 'var btn=document.getElementById("btn-core-upgrade");'
html[#html+1] = 'var panel=document.getElementById("setup-log-panel");'
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
html[#html+1] = 'var titleEl=document.getElementById("setup-log-title");'
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
html[#html+1] = 'var resultEl=document.getElementById("setup-log-result");'
html[#html+1] = 'if(btn){btn.disabled=true;btn.textContent="⏳ 正在启动核心升级...";}'
html[#html+1] = 'panel.style.display="block";'
html[#html+1] = 'logEl.textContent="正在启动 OpenClaw 核心升级事务...\\n";'
html[#html+1] = 'titleEl.textContent="📋 OpenClaw 核心升级日志";'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 核心升级中 (执行状态机事务)...</span>";'
html[#html+1] = 'resultEl.style.display="none";'
html[#html+1] = '(new XHR()).post("' .. service_ctl_url .. '",{action:"upgrade",version:ver,token:ocCsrfToken},function(x){'
html[#html+1] = 'var accepted=ocAcceptedResponse(x);'
html[#html+1] = 'if(accepted.ok){ocPollSetupLog();}else{ocSetupRequestRejected(btn,"⬆️ 重试核心升级",accepted.message);}'
html[#html+1] = '});'
html[#html+1] = '}'
-- ═══ 插件一键升级 ═══
html[#html+1] = 'var _pluginUpgradeTimer=null;'
html[#html+1] = 'function ocPluginUpgrade(){'
html[#html+1] = 'var ver=window._pluginLatestVer;'
html[#html+1] = 'if(!ver){alert("无法获取最新版本号");return;}'
html[#html+1] = 'if(!confirm("确定要升级插件到 v"+ver+"?\\n\\n升级会替换插件文件并清除 LuCI 缓存,不会影响正在运行的 OpenClaw 服务。"))return;'
html[#html+1] = 'var btn=document.getElementById("btn-plugin-upgrade");'
html[#html+1] = 'var panel=document.getElementById("setup-log-panel");'
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
html[#html+1] = 'var titleEl=document.getElementById("setup-log-title");'
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
html[#html+1] = 'var resultEl=document.getElementById("setup-log-result");'
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 正在升级插件...";'
html[#html+1] = 'panel.style.display="block";'
html[#html+1] = 'logEl.textContent="正在启动插件升级...\\n";'
html[#html+1] = 'titleEl.textContent="📋 插件升级日志";'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 插件升级中...</span>";'
html[#html+1] = 'resultEl.style.display="none";'
html[#html+1] = '(new XHR()).post("' .. plugin_upgrade_url .. '?version="+encodeURIComponent(ver),{token:ocCsrfToken},function(x){'
html[#html+1] = 'var accepted=ocAcceptedResponse(x);'
html[#html+1] = 'if(accepted.ok){ocPollPluginUpgradeLog();}else{if(btn){btn.disabled=false;btn.textContent="⬆️ 重试插件升级";}statusEl.textContent="❌ "+accepted.message;logEl.textContent+=accepted.message+"\\n";}'
html[#html+1] = '});'
html[#html+1] = '}'
-- 轮询插件升级日志 (带容错: 安装时文件被替换可能导致API暂时不可用)
-- 复用安装日志的智能滚动机制
html[#html+1] = 'var _pluginPollErrors=0;'
html[#html+1] = 'function ocPollPluginUpgradeLog(){'
html[#html+1] = 'if(_pluginUpgradeTimer)clearInterval(_pluginUpgradeTimer);'
html[#html+1] = '_pluginPollErrors=0;'
html[#html+1] = '_autoScrollEnabled=true;' -- 重置: 启用自动滚动
html[#html+1] = '_pluginUpgradeTimer=setInterval(function(){'
html[#html+1] = '(new XHR()).get("' .. plugin_upgrade_log_url .. '",null,function(x){'
html[#html+1] = 'try{'
html[#html+1] = 'var r=JSON.parse(x.responseText);'
html[#html+1] = '_pluginPollErrors=0;'
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
html[#html+1] = 'if(r.log)logEl.textContent=r.log;'
-- 智能滚动: 仅在自动滚动启用时滚动到底部
html[#html+1] = 'if(_autoScrollEnabled){'
html[#html+1] = 'logEl.scrollTop=logEl.scrollHeight;'
html[#html+1] = '}'
html[#html+1] = 'if(r.state==="running"){'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 插件升级中...</span>";'
html[#html+1] = '}else if(r.state==="success"){'
html[#html+1] = 'clearInterval(_pluginUpgradeTimer);_pluginUpgradeTimer=null;'
html[#html+1] = 'ocPluginUpgradeDone(true);'
html[#html+1] = '}else if(r.state==="failed"){'
html[#html+1] = 'clearInterval(_pluginUpgradeTimer);_pluginUpgradeTimer=null;'
html[#html+1] = 'ocPluginUpgradeDone(false);'
html[#html+1] = '}'
html[#html+1] = '}catch(e){'
html[#html+1] = '_pluginPollErrors++;'
html[#html+1] = 'if(_pluginPollErrors>=3){'
html[#html+1] = 'clearInterval(_pluginUpgradeTimer);_pluginUpgradeTimer=null;'
html[#html+1] = 'var btn=document.getElementById("btn-plugin-upgrade");if(btn){btn.disabled=false;btn.textContent="⬆️ 重试插件升级";}'
html[#html+1] = 'statusEl.textContent="⚠️ 状态查询连续失败,请手动刷新确认;后台任务可能仍在运行。";'
html[#html+1] = '}'
html[#html+1] = '}'
html[#html+1] = '});'
html[#html+1] = '},2000);'
html[#html+1] = '}'
-- 插件升级完成处理
html[#html+1] = 'function ocPluginUpgradeDone(ok){'
html[#html+1] = 'var btn=document.getElementById("btn-plugin-upgrade");'
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
html[#html+1] = 'var resultEl=document.getElementById("setup-log-result");'
html[#html+1] = 'if(btn){btn.disabled=false;btn.textContent="⬆️ 升级插件";}'
html[#html+1] = 'resultEl.style.display="block";'
html[#html+1] = 'if(ok){'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#1a7f37;\\">✅ 插件升级完成</span>";'
html[#html+1] = 'resultEl.innerHTML="<div style=\\"border:1px solid #c6e9c9;background:#e6f7e9;padding:12px 16px;border-radius:6px;\\">"+'
html[#html+1] = '"<strong style=\\"color:#1a7f37;font-size:14px;\\">🎉 插件升级成功!</strong><br/>"+'
html[#html+1] = '"<span style=\\"color:#555;font-size:13px;line-height:1.8;\\">插件文件已更新,OpenClaw 服务不受影响。请刷新页面加载新版界面。</span><br/>"+'
html[#html+1] = '"<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"location.reload()\\" style=\\"margin-top:10px;\\">🔄 刷新页面</button></div>";'
html[#html+1] = '}else{'
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#cf222e;\\">❌ 插件升级失败</span>";'
html[#html+1] = 'resultEl.innerHTML="<div style=\\"border:1px solid #f5c6cb;background:#ffeef0;padding:12px 16px;border-radius:6px;\\">"+'
html[#html+1] = '"<strong style=\\"color:#cf222e;font-size:14px;\\">❌ 插件升级失败</strong><br/>"+'
html[#html+1] = '"<span style=\\"color:#555;font-size:13px;\\">请查看上方日志了解详情。也可手动执行:<code>cat /tmp/openclaw-plugin-upgrade.log</code></span><br/>"+'
html[#html+1] = '"<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"location.reload()\\" style=\\"margin-top:10px;\\">🔄 刷新页面</button></div>";'
html[#html+1] = '}'
html[#html+1] = '}'
-- 卸载运行环境
html[#html+1] = 'function ocUninstall(){'
html[#html+1] = 'if(!confirm("确定要卸载 OpenClaw 运行环境?\\n\\n将删除 Node.js、OpenClaw 程序及配置数据,服务将停止运行。\\n\\n插件本身不会被删除,之后可重新安装运行环境。"))return;'
html[#html+1] = 'var btn=document.getElementById("btn-uninstall");'
html[#html+1] = 'var el=document.getElementById("action-result");'
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 正在卸载...";'
html[#html+1] = 'el.innerHTML="<span style=\\"color:#999\\">正在停止服务并清理文件...</span>";'
html[#html+1] = '(new XHR()).post("' .. uninstall_url .. '",{token:ocCsrfToken},function(x){'
html[#html+1] = 'btn.disabled=false;btn.textContent="🗑️ 卸载环境";'
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
html[#html+1] = 'if(r.status==="ok"){'
html[#html+1] = 'el.innerHTML="<div style=\\"border:1px solid #d0d7de;background:#f6f8fa;padding:12px 16px;border-radius:6px;\\">"+'
html[#html+1] = '"<strong style=\\"color:#1a7f37;\\">✅ 卸载完成</strong><br/>"+'
html[#html+1] = '"<span style=\\"color:#555;font-size:13px;\\">"+r.message+"</span><br/>"+'
html[#html+1] = '"<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"location.reload()\\" style=\\"margin-top:8px;\\">🔄 刷新页面</button></div>";'
html[#html+1] = '}else{el.innerHTML="<span style=\\"color:red\\">❌ "+(r.message||"卸载失败")+"</span>";}'
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 请求失败</span>";}'
html[#html+1] = '});}'
html[#html+1] = 'console.log("OpenClaw JS first block loaded");'
-- ═══ 备份/恢复 对话框 + 功能 (v2026.3.8+ openclaw backup) ═══
local backup_url = luci.dispatcher.build_url("admin", "services", "openclaw", "backup")
-- 先关闭 script,插入对话框 HTML,再重新打开 script
html[#html+1] = '</script>'
-- 对话框 HTML (附加到按钮区域后面)
html[#html+1] = '<div id="oc-backup-dialog" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;align-items:center;justify-content:center;">'
html[#html+1] = '<div style="background:#fff;border-radius:12px;padding:24px 28px;max-width:520px;width:92%;box-shadow:0 8px 32px rgba(0,0,0,0.2);">'
html[#html+1] = '<h3 style="margin:0 0 16px 0;font-size:16px;color:#333;">💾 备份 / 恢复配置</h3>'
-- 备份操作区
html[#html+1] = '<div style="margin-bottom:16px;">'
html[#html+1] = '<div style="font-weight:600;font-size:13px;color:#555;margin-bottom:8px;">📤 创建备份</div>'
html[#html+1] = '<div style="display:flex;gap:10px;">'
html[#html+1] = '<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocDoBackup(1)" id="btn-bk-config" style="font-size:12px;">📄 仅配置文件</button>'
html[#html+1] = '<button class="btn cbi-button cbi-button-action" type="button" onclick="ocDoBackup(0)" id="btn-bk-full" style="font-size:12px;">📦 配置 + 状态数据</button>'
html[#html+1] = '</div>'
html[#html+1] = '<div style="font-size:11px;color:#888;margin-top:6px;">仅配置文件 (~2KB) 包含模型、渠道、插件设置;完整备份含会话历史等状态数据(可能较大)</div>'
html[#html+1] = '</div>'
-- 备份列表区(恢复/删除在这里动态渲染)
html[#html+1] = '<div style="border-top:1px solid #eee;padding-top:14px;margin-bottom:16px;">'
html[#html+1] = '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">'
html[#html+1] = '<div style="font-weight:600;font-size:13px;color:#555;">📥 现有备份</div>'
html[#html+1] = '<button class="btn cbi-button" type="button" onclick="ocLoadBackupList()" style="font-size:11px;padding:2px 10px;">🔄 刷新</button>'
html[#html+1] = '</div>'
html[#html+1] = '<div id="oc-backup-list" style="max-height:260px;overflow-y:auto;"></div>'
html[#html+1] = '</div>'
-- 操作结果提示区
html[#html+1] = '<div id="oc-backup-result" style="margin-bottom:14px;display:none;"></div>'
-- 关闭按钮
html[#html+1] = '<div style="display:flex;justify-content:flex-end;">'
html[#html+1] = '<button class="btn cbi-button" type="button" onclick="document.getElementById(\'oc-backup-dialog\').style.display=\'none\';" style="min-width:80px;">关闭</button>'
html[#html+1] = '</div>'
html[#html+1] = '</div></div>'
-- 重新打开 script 继续 JS 函数
html[#html+1] = '<script type="text/javascript">'
-- 打开备份/恢复对话框并加载列表
html[#html+1] = 'function ocBackupRestore(){'
html[#html+1] = 'var dlg=document.getElementById("oc-backup-dialog");'
html[#html+1] = 'dlg.style.display="flex";'
html[#html+1] = 'document.getElementById("oc-backup-result").style.display="none";'
html[#html+1] = 'ocLoadBackupList();'
html[#html+1] = '}'
-- 加载备份文件列表
html[#html+1] = 'function ocLoadBackupList(){'
html[#html+1] = 'var el=document.getElementById("oc-backup-list");'
html[#html+1] = 'el.innerHTML="<div style=\\"color:#7aa2f7;font-size:12px;padding:8px;\\">⏳ 加载备份列表...</div>";'
html[#html+1] = '(new XHR()).post("' .. backup_url .. '?action=list",{token:ocCsrfToken},function(x){'
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
html[#html+1] = 'if(r.status==="ok"&&r.backups&&r.backups.length>0){'
html[#html+1] = 'var h="<table style=\\"width:100%;border-collapse:collapse;font-size:12px;\\">";'
html[#html+1] = 'h+="<tr style=\\"background:#f6f8fa;border-bottom:2px solid #d0d7de;\\">"+'
html[#html+1] = '"<th style=\\"padding:6px 8px;text-align:left;\\">类型</th>"+'
html[#html+1] = '"<th style=\\"padding:6px 8px;text-align:left;\\">备份时间</th>"+'
html[#html+1] = '"<th style=\\"padding:6px 8px;text-align:right;\\">大小</th>"+'
html[#html+1] = '"<th style=\\"padding:6px 8px;text-align:center;\\">操作</th></tr>";'
html[#html+1] = 'for(var i=0;i<r.backups.length;i++){'
html[#html+1] = 'var b=r.backups[i];'
html[#html+1] = 'var typeBadge=b.backup_type==="config"?'
html[#html+1] = '"<span style=\\"background:#ddf4ff;color:#0969da;padding:2px 6px;border-radius:3px;font-size:11px;white-space:nowrap;\\">📄 仅配置</span>":'
html[#html+1] = '"<span style=\\"background:#fff8c5;color:#9a6700;padding:2px 6px;border-radius:3px;font-size:11px;white-space:nowrap;\\">📦 完整备份</span>";'
html[#html+1] = 'var rowBg=i%2===0?"#fff":"#f6f8fa";'
html[#html+1] = 'h+="<tr style=\\"border-bottom:1px solid #eee;background:"+rowBg+";\\">"+' -- 行开始
html[#html+1] = '"<td style=\\"padding:7px 8px;\\">"+typeBadge+"</td>"+' -- 类型
html[#html+1] = '"<td style=\\"padding:7px 8px;color:#555;white-space:nowrap;\\">"+b.time+"</td>"+' -- 时间
html[#html+1] = '"<td style=\\"padding:7px 8px;text-align:right;color:#666;white-space:nowrap;\\">"+b.size_str+"</td>"+' -- 大小
html[#html+1] = '"<td style=\\"padding:5px 8px;text-align:center;white-space:nowrap;\\">"+'
html[#html+1] = '"<button class=\\"btn cbi-button cbi-button-action\\" style=\\"font-size:11px;padding:1px 8px;margin-right:4px;\\" onclick=\\"ocRestoreBackup(\\x27"+b.filename+"\\x27)\\">恢复</button>"+'
html[#html+1] = '"<button class=\\"btn cbi-button cbi-button-remove\\" style=\\"font-size:11px;padding:1px 8px;\\" onclick=\\"ocDeleteBackup(\\x27"+b.filename+"\\x27)\\">删除</button>"+'
html[#html+1] = '"</td></tr>";'
html[#html+1] = '}'
html[#html+1] = 'h+="</table>";'
html[#html+1] = 'el.innerHTML=h;'
html[#html+1] = '}else if(r.status==="ok"){'
html[#html+1] = 'el.innerHTML="<div style=\\"color:#888;font-size:12px;padding:8px;text-align:center;\\">暂无备份,请先创建备份</div>";'
html[#html+1] = '}else{'
html[#html+1] = 'el.innerHTML="<div style=\\"color:#e36209;font-size:12px;padding:8px;\\">⚠️ "+(r.message||"获取列表失败")+"</div>";'
html[#html+1] = '}'
html[#html+1] = '}catch(e){el.innerHTML="<div style=\\"color:#e36209;font-size:12px;padding:8px;\\">⚠️ 无法加载列表</div>";}'
html[#html+1] = '});}'
-- 创建备份(创建完成后刷新列表)
html[#html+1] = 'function ocDoBackup(onlyConfig){'
html[#html+1] = 'var resEl=document.getElementById("oc-backup-result");'
html[#html+1] = 'var btnC=document.getElementById("btn-bk-config");'
html[#html+1] = 'var btnF=document.getElementById("btn-bk-full");'
html[#html+1] = 'btnC.disabled=true;btnF.disabled=true;'
html[#html+1] = 'resEl.style.display="block";'
html[#html+1] = 'resEl.innerHTML="<div style=\\"color:#7aa2f7;font-size:12px;padding:8px;\\">⏳ 正在创建备份..."+(onlyConfig?"(仅配置)":"(完整备份,可能需要较长时间)")+"</div>";'
html[#html+1] = '(new XHR()).post("' .. backup_url .. '?action=create&only_config="+onlyConfig,{token:ocCsrfToken},function(x){'
html[#html+1] = 'btnC.disabled=false;btnF.disabled=false;'
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
html[#html+1] = 'if(r.status==="ok"){'
html[#html+1] = 'resEl.innerHTML="<div style=\\"border:1px solid #c6e9c9;background:#e6f7e9;padding:10px 14px;border-radius:6px;font-size:12px;\\">"+'
html[#html+1] = '"<strong style=\\"color:#1a7f37;\\">✅ 备份完成</strong></div>";'
html[#html+1] = 'ocLoadBackupList();'
html[#html+1] = '}else{'
html[#html+1] = 'resEl.innerHTML="<div style=\\"color:#e36209;font-size:12px;padding:8px;\\">⚠️ "+(r.message||"备份失败")+"</div>";'
html[#html+1] = '}'
html[#html+1] = '}catch(e){resEl.innerHTML="<div style=\\"color:#e36209;font-size:12px;padding:8px;\\">⚠️ 备份功能需要 OpenClaw v2026.3.8+</div>";}'
html[#html+1] = '});}'
-- 恢复指定备份
html[#html+1] = 'function ocRestoreBackup(filename){'
html[#html+1] = 'if(!confirm("确定要从此备份恢复配置?\\n\\n"+filename+"\\n\\n当前 openclaw.json 将被备份中的版本覆盖,服务将自动重启。"))return;'
html[#html+1] = 'var resEl=document.getElementById("oc-backup-result");'
html[#html+1] = 'resEl.style.display="block";'
html[#html+1] = 'resEl.innerHTML="<div style=\\"color:#7aa2f7;font-size:12px;padding:8px;\\">⏳ 正在恢复配置...</div>";'
html[#html+1] = '(new XHR()).post("' .. backup_url .. '?action=restore&file="+encodeURIComponent(filename),{token:ocCsrfToken},function(x){'
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
html[#html+1] = 'if(r.status==="ok"){'
html[#html+1] = 'resEl.innerHTML="<div style=\\"border:1px solid #c6e9c9;background:#e6f7e9;padding:10px 14px;border-radius:6px;font-size:12px;\\">"+'
html[#html+1] = '"<strong style=\\"color:#1a7f37;\\">✅ 配置已恢复</strong><br/>"+'
html[#html+1] = '"<span style=\\"color:#555;\\">"+r.message+"</span><br/>"+'
html[#html+1] = '"<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"location.reload()\\" style=\\"margin-top:6px;font-size:12px;\\">🔄 刷新页面</button></div>";'
html[#html+1] = '}else{'
html[#html+1] = 'resEl.innerHTML="<div style=\\"color:#cf222e;font-size:12px;padding:8px;\\">❌ "+(r.message||"恢复失败")+"</div>";'
html[#html+1] = '}'
html[#html+1] = '}catch(e){resEl.innerHTML="<div style=\\"color:#cf222e;font-size:12px;padding:8px;\\">❌ 恢复失败,请检查日志</div>";}'
html[#html+1] = '});}'
-- 删除指定备份
html[#html+1] = 'function ocDeleteBackup(filename){'
html[#html+1] = 'if(!confirm("确定要删除此备份?\\n\\n"+filename+"\\n\\n删除后无法恢复。"))return;'
html[#html+1] = 'var resEl=document.getElementById("oc-backup-result");'
html[#html+1] = 'resEl.style.display="block";'
html[#html+1] = 'resEl.innerHTML="<div style=\\"color:#7aa2f7;font-size:12px;padding:8px;\\">⏳ 正在删除...</div>";'
html[#html+1] = '(new XHR()).post("' .. backup_url .. '?action=delete&file="+encodeURIComponent(filename),{token:ocCsrfToken},function(x){'
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
html[#html+1] = 'if(r.status==="ok"){'
html[#html+1] = 'resEl.innerHTML="<div style=\\"border:1px solid #c6e9c9;background:#e6f7e9;padding:10px 14px;border-radius:6px;font-size:12px;\\">"+'
html[#html+1] = '"<strong style=\\"color:#1a7f37;\\">✅ "+r.message+"</strong></div>";'
html[#html+1] = 'ocLoadBackupList();'
html[#html+1] = '}else{'
html[#html+1] = 'resEl.innerHTML="<div style=\\"color:#cf222e;font-size:12px;padding:8px;\\">❌ "+(r.message||"删除失败")+"</div>";'
html[#html+1] = '}'
html[#html+1] = '}catch(e){resEl.innerHTML="<div style=\\"color:#cf222e;font-size:12px;padding:8px;\\">❌ 删除失败</div>";}'
html[#html+1] = '});}'
-- 页面加载时静默检查是否有更新 (仅显示小红点提示)
html[#html+1] = '(function(){'
html[#html+1] = 'setTimeout(function(){'
html[#html+1] = '(new XHR()).get("' .. check_url .. '",null,function(x){'
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
html[#html+1] = 'if(r.plugin_has_update){'
html[#html+1] = 'var dot=document.getElementById("update-dot");'
html[#html+1] = 'if(dot)dot.style.display="block";'
html[#html+1] = '}'
html[#html+1] = '}catch(e){}'
html[#html+1] = '});'
html[#html+1] = '},2000);'
html[#html+1] = '})();'
html[#html+1] = '</script>'
return table.concat(html, "\n")
end
-- ═══════════════════════════════════════════
-- 使用指南
-- ═══════════════════════════════════════════
s4 = m:section(SimpleSection, nil)
s4.template = "cbi/nullsection"
guide = s4:option(DummyValue, "_guide")
guide.rawhtml = true
guide.cfgvalue = function()
local html = {}
html[#html+1] = '<div style="border:1px solid #d0e8ff;background:#f0f7ff;padding:14px 18px;border-radius:6px;margin-top:12px;line-height:1.8;font-size:13px;">'
html[#html+1] = '<strong style="font-size:14px;">📖 使用指南</strong><br/>'
html[#html+1] = '<span style="color:#555;">'
html[#html+1] = '① 首次使用请点击 <b>「安装运行环境」</b>,安装完成后服务会自动启动<br/>'
html[#html+1] = '② 进入 <b>「配置管理」</b> 使用交互式向导快速配置 AI 模型和 API Key<br/>'
html[#html+1] = '③ 进入 <b>「Web 控制台」</b> 配置消息渠道,直接开始对话</span>'
html[#html+1] = '<div style="margin-top:10px;padding-top:10px;border-top:1px solid #d0e8ff;">'
html[#html+1] = '<span style="color:#888;">有疑问?请关注B站并留言:</span>'
html[#html+1] = '<a href="https://space.bilibili.com/59438380" target="_blank" rel="noopener" style="color:#00a1d6;font-weight:bold;text-decoration:none;">'
html[#html+1] = '🔗 space.bilibili.com/59438380</a>'
html[#html+1] = '<span style="margin-left:16px;color:#888;">GitHub 项目:</span>'
html[#html+1] = '<a href="https://github.com/10000ge10000/luci-app-openclaw" target="_blank" rel="noopener" style="color:#24292f;font-weight:bold;text-decoration:none;">'
html[#html+1] = '🐙 10000ge10000/luci-app-openclaw</a></div></div>'
return table.concat(html, "\n")
end
return m