diff --git a/luci-app-openclawmgr/Makefile b/luci-app-openclawmgr/Makefile index f7bb5cdd..128278c0 100644 --- a/luci-app-openclawmgr/Makefile +++ b/luci-app-openclawmgr/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk -PKG_VERSION:=0.1.1 -PKG_RELEASE:=3 +PKG_VERSION:=0.1.2 +PKG_RELEASE:=4 LUCI_TITLE:=LuCI support for OpenClaw Launcher LUCI_PKGARCH:=all diff --git a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.css b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.css index 0fd9fbf3..826a8540 100644 --- a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.css +++ b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.css @@ -74,6 +74,18 @@ font-weight: 700; } +.oclm-section-heading { + margin-bottom: 0; +} + +.oclm-section-heading h2 { + margin: 0 0 2px; +} + +.oclm-section-heading .oclm-hint { + margin: 0 0 14px; +} + .oclm-tabs { display: flex; flex-wrap: wrap; @@ -281,6 +293,14 @@ gap: 14px; } +.oclm-form-grid-ai { + margin-top: 0; +} + +.oclm-form-grid-ai > .oclm-field:first-child { + margin-top: 18px; +} + .oclm-field { display: grid; grid-template-columns: 150px minmax(0, 1fr); diff --git a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js index fbf6d6b7..029e94c1 100644 --- a/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js +++ b/luci-app-openclawmgr/htdocs/luci-static/resources/openclawmgr/app.js @@ -295,7 +295,7 @@ function tick(remaining) { request(config.readyUrl).then(function(rv) { state.consoleReady = !!(rv && rv.ok && rv.ready); - render(); + updateStatusDom(state.status); if (!state.consoleReady && remaining > 1) { state.consoleCheckTimer = window.setTimeout(function() { tick(remaining - 1); }, 1000); } else { @@ -303,7 +303,7 @@ } }).catch(function() { state.consoleReady = false; - render(); + updateStatusDom(state.status); if (remaining > 1) { state.consoleCheckTimer = window.setTimeout(function() { tick(remaining - 1); }, 1500); } else { @@ -379,6 +379,7 @@ var showServiceActions = status.installed && !status.installing; var activeTab = state.activeTab || "basic"; var savingBasic = state.savingSection === "basic"; + var savingAi = state.savingSection === "ai"; var savingAccess = state.savingSection === "access"; var serviceButtons = showServiceActions ? ( @@ -400,30 +401,36 @@ '
' + '

服务状态

' + '
' + - '
状态' + escapeHtml(statusText(status)) + '' + (status.running && status.uptime_human ? ('运行时间 ' + escapeHtml(status.uptime_human) + '') : '') + '
' + - '
' + ((status.running && status.pid) ? ('
PID ' + escapeHtml(status.pid) + '
') : '') + '
' + - '
地址
' + escapeHtml(maskedTokenUrl(status.token_url || status.base_url || "")) + '' + ((status.token_url || status.base_url) ? ('') : '') + '
' + - '
目录
' + escapeHtml(status.base_dir || "-") + '
' + - '
版本
OpenClaw ' + escapeHtml(status.openclaw_version || "-") + 'Node ' + escapeHtml(status.node_version || "-") + '
' + + '
状态' + escapeHtml(statusText(status)) + '' + + '运行时间 ' + escapeHtml(status.uptime_human || "") + '' + + '
' + + '
PID ' + escapeHtml(status.pid || "") + '
' + + '
地址
' + escapeHtml(maskedTokenUrl(status.token_url || status.base_url || "")) + '
' + + '
目录
' + escapeHtml(status.base_dir || "-") + '
' + + '
版本
OpenClaw ' + escapeHtml(status.openclaw_version || "-") + 'Node ' + escapeHtml(status.node_version || "-") + '
' + '
' + '
' + - '
' + - (showInstallAction ? '
' : '') + - (status.running ? '' : '') + - (status.installing ? '' : '') + + '
' + + '
' + + '' + + '' + '
' + - (status.installing ? '
安装任务正在后台运行,点击“安装中”可继续查看日志。
' : '') + + '' + + '' + + '
' + + '
安装任务正在后台运行,点击“安装中”可继续查看日志。
' + '
' + '
' + '
' + '' + + '' + '' + '' + '
' + '
' + '

基础配置

' + - '
' + + '
' + fieldInput("监听端口", '') + fieldInput("监听范围", selectHtml("oclm-bind", form.bind, [ ["lan", "所有地址"], @@ -431,16 +438,22 @@ ["auto", "自动"] ])) + fieldInput("数据目录", '' + baseDirOptions + '') + + '
' + serviceButtons + (state.lastAppliedAt ? '已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置' : '') + '
' + + '
' + + + '
' + + '

AI配置

先配好一个能马上聊起来的默认 AI,再让openclaw协助你设置更多的AGENT、加角色等操作
' + + '
' + fieldInput("默认服务提供商", selectHtml("oclm-agent", form.default_agent, [ ["openai", "OpenAI"], ["anthropic", "Anthropic"], ["minimax-cn", "MiniMax CN"], ["moonshot", "Moonshot CN"] - ])) + + ]) + '
如果你的供应商是兼容 OpenAI 等协议,可在下方填写中转地址
') + fieldInput("API 密钥", passwordHtml("oclm-api-key", form.provider_api_key || "", "sk-...")) + fieldInput("中转地址(可选)", '') + fieldInput("默认模型", '') + - '
' + serviceButtons + (state.lastAppliedAt ? '已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置' : '') + '
' + + '
' + (state.lastAppliedAt ? '已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置' : '') + '
' + '
' + '
' + @@ -470,6 +483,99 @@ bindEvents(); } + function updateStatusDom(status) { + status = status || {}; + + function byId(id) { + if (root.getElementById) return root.getElementById(id); + return root.querySelector("#" + id); + } + + var pill = byId("oclm-status-pill"); + var dot = byId("oclm-status-dot"); + var textEl = byId("oclm-status-text"); + if (!pill || !dot || !textEl) { + render(); + return; + } + + pill.style.setProperty("--oclm-spin-duration", statusSpinSeconds(status) + "s"); + dot.classList.remove("is-success"); + dot.classList.remove("is-danger"); + var cls = statusDotClass(status); + if (cls) dot.classList.add(cls); + textEl.textContent = statusText(status); + + var uptimeWrap = byId("oclm-uptime"); + var uptimeText = byId("oclm-uptime-text"); + if (uptimeWrap && uptimeText) { + if (status.running && status.uptime_human) { + uptimeText.textContent = String(status.uptime_human); + uptimeWrap.classList.remove("oclm-hidden"); + } else { + uptimeText.textContent = ""; + uptimeWrap.classList.add("oclm-hidden"); + } + } + + var pidRow = byId("oclm-pid-row"); + var pidText = byId("oclm-pid"); + if (pidRow && pidText) { + if (status.running && status.pid) { + pidText.textContent = String(status.pid); + pidRow.classList.remove("oclm-hidden"); + } else { + pidText.textContent = ""; + pidRow.classList.add("oclm-hidden"); + } + } + + var addrText = byId("oclm-address-text"); + if (addrText) addrText.textContent = maskedTokenUrl(status.token_url || status.base_url || ""); + var copyBtn = byId("oclm-copy-token-url"); + if (copyBtn) { + if (status.token_url || status.base_url) copyBtn.classList.remove("oclm-hidden"); + else copyBtn.classList.add("oclm-hidden"); + } + + var baseDirEl = byId("oclm-status-base-dir"); + if (baseDirEl) baseDirEl.textContent = String(status.base_dir || "-"); + var openclawVerEl = byId("oclm-openclaw-version"); + if (openclawVerEl) openclawVerEl.textContent = String(status.openclaw_version || "-"); + var nodeVerEl = byId("oclm-node-version"); + if (nodeVerEl) nodeVerEl.textContent = String(status.node_version || "-"); + + var installInline = byId("oclm-install-inline"); + var installBtn = byId("oclm-install-btn"); + var cancelInstallBtn = byId("oclm-cancel-install"); + var openConsoleBtn = byId("oclm-open-console"); + var noteEl = byId("oclm-status-note"); + + var showInstallAction = !status.installed || status.installing; + if (installInline) { + if (showInstallAction) installInline.classList.remove("oclm-hidden"); + else installInline.classList.add("oclm-hidden"); + } + if (installBtn) { + installBtn.textContent = status.installing ? "安装中" : "立即安装"; + installBtn.disabled = !!status.installing; + } + if (cancelInstallBtn) { + if (status.installing) cancelInstallBtn.classList.remove("oclm-hidden"); + else cancelInstallBtn.classList.add("oclm-hidden"); + } + if (openConsoleBtn) { + if (status.running) openConsoleBtn.classList.remove("oclm-hidden"); + else openConsoleBtn.classList.add("oclm-hidden"); + openConsoleBtn.disabled = !(status.running && state.consoleReady); + openConsoleBtn.innerHTML = openclawIcon("oclm-button-icon") + (state.consoleReady ? "打开控制台" : "控制台准备中…"); + } + if (noteEl) { + if (status.installing) noteEl.classList.remove("oclm-hidden"); + else noteEl.classList.add("oclm-hidden"); + } + } + function fieldInput(label, control) { return '
' + label + '
' + control + '
'; } @@ -514,9 +620,56 @@ ''; } + function syncDraftFromDom() { + if (!state.form) { + state.form = {}; + } + + function getEl(id) { + return root.getElementById ? root.getElementById(id) : root.querySelector("#" + id); + } + + var portEl = getEl("oclm-port"); + if (portEl) state.form.port = portEl.value; + + var bindEl = getEl("oclm-bind"); + if (bindEl) state.form.bind = bindEl.value; + + var baseDirEl = getEl("oclm-base-dir"); + if (baseDirEl) state.form.base_dir = baseDirEl.value; + + var installAccelEl = getEl("oclm-install-accelerated"); + if (installAccelEl) state.form.install_accelerated = !!installAccelEl.checked; + + var agentEl = getEl("oclm-agent"); + if (agentEl) state.form.default_agent = agentEl.value; + + var apiKeyEl = getEl("oclm-api-key"); + if (apiKeyEl) state.form.provider_api_key = apiKeyEl.value; + + var baseUrlEl = getEl("oclm-base-url"); + if (baseUrlEl) state.form.provider_base_url = baseUrlEl.value; + + var modelEl = getEl("oclm-model"); + if (modelEl) state.form.default_model = modelEl.value; + + var tokenEl = getEl("oclm-token"); + if (tokenEl) state.form.token = tokenEl.value; + + var allowInsecureEl = getEl("oclm-allow_insecure_auth"); + if (allowInsecureEl) state.form.allow_insecure_auth = !!allowInsecureEl.checked; + + var disableDeviceEl = getEl("oclm-disable_device_auth"); + if (disableDeviceEl) state.form.disable_device_auth = !!disableDeviceEl.checked; + + var newOriginEl = getEl("oclm-new-origin"); + if (newOriginEl) state.newOrigin = newOriginEl.value; + } + function bindEvents() { Array.prototype.forEach.call(root.querySelectorAll("[data-tab]"), function(el) { el.onclick = function() { + syncDraftFromDom(); state.activeTab = el.getAttribute("data-tab") || "basic"; render(); }; @@ -652,6 +805,27 @@ }; } + var apiKey = root.getElementById("oclm-api-key"); + if (apiKey) { + apiKey.oninput = function() { + state.form.provider_api_key = apiKey.value; + }; + } + + var baseUrl = root.getElementById("oclm-base-url"); + if (baseUrl) { + baseUrl.oninput = function() { + state.form.provider_base_url = baseUrl.value; + }; + } + + var modelInput = root.getElementById("oclm-model"); + if (modelInput) { + modelInput.oninput = function() { + state.form.default_model = modelInput.value; + }; + } + var addOrigin = root.getElementById("oclm-add-origin"); if (addOrigin) { addOrigin.onclick = function() { @@ -665,6 +839,13 @@ }; } + var newOrigin = root.getElementById("oclm-new-origin"); + if (newOrigin) { + newOrigin.oninput = function() { + state.newOrigin = newOrigin.value; + }; + } + Array.prototype.forEach.call(root.querySelectorAll("[data-remove-origin]"), function(el) { el.onclick = function() { var index = parseInt(el.getAttribute("data-remove-origin"), 10); @@ -690,11 +871,7 @@ port: root.getElementById("oclm-port").value, bind: root.getElementById("oclm-bind").value, base_dir: root.getElementById("oclm-base-dir").value, - default_agent: root.getElementById("oclm-agent").value, - default_model: root.getElementById("oclm-model").value, install_accelerated: !!(root.getElementById("oclm-install-accelerated") && root.getElementById("oclm-install-accelerated").checked), - provider_api_key: root.getElementById("oclm-api-key").value, - provider_base_url: root.getElementById("oclm-base-url").value }; state.savingSection = "basic"; render(); @@ -706,6 +883,25 @@ }; } + var saveAi = root.getElementById("oclm-save-ai"); + if (saveAi) { + saveAi.onclick = function() { + var payload = { + default_agent: root.getElementById("oclm-agent").value, + default_model: root.getElementById("oclm-model").value, + provider_api_key: root.getElementById("oclm-api-key").value, + provider_base_url: root.getElementById("oclm-base-url").value + }; + state.savingSection = "ai"; + render(); + postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "ai"); }).catch(function() { + state.savingSection = ""; + render(); + window.alert("保存失败"); + }); + }; + } + var saveAccess = root.getElementById("oclm-save-access"); if (saveAccess) { saveAccess.onclick = function() { @@ -770,14 +966,14 @@ showTaskLog("openclawmgr"); } state.lastTaskRunning = !!(state.status && state.status.task_running); - render(); + updateStatusDom(state.status); ensureInstallWatch(); if (typeof done === "function") { done(); } }).catch(function() { state.status = state.status || {}; - render(); + updateStatusDom(state.status); ensureInstallWatch(); if (typeof done === "function") { done(); diff --git a/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh b/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh index 2f3fc139..164c897b 100755 --- a/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh +++ b/luci-app-openclawmgr/root/usr/libexec/istorec/openclawmgr.sh @@ -890,6 +890,15 @@ install_openclaw() { # Node version is bundled/managed by installer code; do not allow UCI override. uci -q delete "${UCI_NS}.main.node_version" >/dev/null 2>&1 || true uci -q commit "$UCI_NS" >/dev/null 2>&1 || true + + # Ensure token exists even if runtime is already installed (service start requires it). + TOKEN="$(uci_get token)" + if [ -z "$TOKEN" ]; then + TOKEN="$(gen_token)" + uci -q set "${UCI_NS}.main.token=$TOKEN" && uci -q commit "$UCI_NS" || true + fi + ensure_gateway_config || true + if have_openclaw_runtime; then write_installer_log "OpenClaw is already installed: $(openclaw_version || echo unknown)" write_installer_log "Skip install. Use restart/apply config if you only need to refresh configuration." @@ -902,12 +911,6 @@ install_openclaw() { fi check_space_mb "$BASE_DIR" 2048 || return 1 - TOKEN="$(uci_get token)" - if [ -z "$TOKEN" ]; then - TOKEN="$(gen_token)" - uci -q set "${UCI_NS}.main.token=$TOKEN" && uci -q commit "$UCI_NS" || true - fi - install_node install_openclaw ensure_gateway_config || true @@ -1134,6 +1137,7 @@ ACTION="${1:-help}" shift 1 || true BASE_DIR="$(uci_get base_dir)" +ENABLED="$(uci_get enabled)" PORT="$(uci_get port)" BIND="$(uci_get bind)" TOKEN="$(uci_get token)" @@ -1148,6 +1152,7 @@ PROVIDER_BASE_URL="$(uci_get provider_base_url)" [ -n "$PORT" ] || PORT="18789" [ -n "$BIND" ] || BIND="lan" +[ -n "$ENABLED" ] || ENABLED="0" NODE_VERSION="24.14.0" [ -n "$ALLOW_INSECURE_AUTH" ] || ALLOW_INSECURE_AUTH="1" [ -n "$DISABLE_DEVICE_AUTH" ] || DISABLE_DEVICE_AUTH="1" @@ -1164,6 +1169,10 @@ case "$BIND" in loopback|lan|auto|tailnet|custom) ;; *) BIND="lan" ;; esac +case "$ENABLED" in + 1|true|yes|on) ENABLED="1" ;; + *) ENABLED="0" ;; +esac case "$ALLOW_INSECURE_AUTH" in 1|true|yes|on) ALLOW_INSECURE_AUTH="1" ;; *) ALLOW_INSECURE_AUTH="0" ;; diff --git a/miniupnpd/Makefile b/miniupnpd/Makefile index d0557237..c8a23c0b 100644 --- a/miniupnpd/Makefile +++ b/miniupnpd/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=miniupnpd PKG_VERSION:=2.3.9 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE_URL:=https://github.com/miniupnp/miniupnp/releases/download/miniupnpd_$(subst .,_,$(PKG_VERSION)) PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz diff --git a/miniupnpd/files/upnpd.config b/miniupnpd/files/upnpd.config index 098b3572..b283ff6f 100644 --- a/miniupnpd/files/upnpd.config +++ b/miniupnpd/files/upnpd.config @@ -13,7 +13,7 @@ config upnpd config option upnp_lease_file /var/run/miniupnpd.leases option igdv1 1 option ipv6_disable 1 - option use_stun 1 + option use_stun 0 option stun_host stun.miwifi.com option stun_port 3478 option force_forwarding 1