mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 18:52:51 +08:00
745 lines
57 KiB
Lua
745 lines
57 KiB
Lua
-- 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/> 💡 解决: 检查 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/> 💡 解决: 运行 <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/> 💡 当前设备架构可能是 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/> 💡 解决: 尝试手动安装 <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/> 💡 解决: 运行 <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/> 💡 解决: 删除缓存重试 <code>openclaw-env setup</code>");'
|
||
html[#html+1] = '}'
|
||
-- 验证失败
|
||
html[#html+1] = 'if(ll.indexOf("安装验证失败")>=0){'
|
||
html[#html+1] = 'reasons.push("⚠️ <b>安装验证失败</b> — 程序已下载但无法正常运行。<br/> 💡 可能是 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/> 💡 您也可以尝试手动执行: <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,"&").replace(/</g,"<").replace(/>/g,">");'
|
||
-- 代码块 (```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
|