diff --git a/CHANGELOG.md b/CHANGELOG.md index bb45eeb..5b8de2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。 +## [1.0.7] - 2026-03-06 + +### 修复依赖包名错误 & 补充 GNU tar 依赖 (感谢 [@esir](https://github.com/esir) 建议) + +#### 修复 +- **依赖包名修正**: `util-linux-script` 在 OpenWrt/iStoreOS 软件源中不存在,正确的包名是 `script-utils` (提供 `/usr/bin/script` 命令)。此错误会导致通过 iStore/opkg 安装插件时依赖解析失败 +- **补充 GNU tar 依赖**: `openclaw-env` 安装脚本使用 `tar --strip-components=1` 解压 Node.js,但 busybox 内置的 tar 不支持该参数。新增 `tar` (GNU tar) 为必需依赖,确保解压操作正常 + +#### 变更 +- `Makefile`: `LUCI_DEPENDS` 中 `+util-linux-script` → `+script-utils +tar` +- `scripts/build_ipk.sh`: 同步更新 Depends 字段 +- `scripts/build_run.sh`: 同步更新 Depends 字段 + +### 修复重启服务时 Gateway crash loop 端口冲突 + +#### 修复 +- **端口冲突 crash loop**: OpenClaw gateway 的架构是主进程 (`openclaw`) fork 出子进程 (`openclaw-gateway`) 监听端口,restart 时 procd 只杀主进程,子进程退出慢导致新实例端口冲突反复崩溃 + - `stop_service()`: 从空函数改为主动清理 `openclaw-gateway` 子进程 + 等待端口释放 (最长 8 秒) + - `start_service()`: 启动前预检查端口,清理残留进程后再注册 procd 实例 + - `reload_service()`: stop 和 start 之间增加等待确保内核回收端口 + - LuCI controller: restart 改为先同步 stop 等端口释放,再后台 start + - procd respawn 间隔从 5s → 10s,降低连续端口冲突概率 + ## [1.0.6] - 2026-03-06 ### 修复 Docker 环境下安装失败 "mkdir: can't create directory: Directory not empty" diff --git a/Makefile b/Makefile index 03c50ed..23a04c5 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com> PKG_LICENSE:=GPL-3.0 LUCI_TITLE:=OpenClaw AI 网关 LuCI 管理插件 -LUCI_DEPENDS:=+luci-compat +luci-base +curl +openssl-util +util-linux-script +LUCI_DEPENDS:=+luci-compat +luci-base +curl +openssl-util +script-utils +tar LUCI_PKGARCH:=all # 优先使用 luci.mk (feeds 模式), 不可用时回退 package.mk diff --git a/VERSION b/VERSION index af0b7dd..238d6e8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.6 +1.0.7 diff --git a/luasrc/controller/openclaw.lua b/luasrc/controller/openclaw.lua index 4ac4fb9..e775294 100644 --- a/luasrc/controller/openclaw.lua +++ b/luasrc/controller/openclaw.lua @@ -87,6 +87,7 @@ function action_status() port = port, pty_port = pty_port, gateway_running = false, + gateway_starting = false, pty_running = false, pid = "", memory_kb = 0, @@ -122,6 +123,14 @@ function action_status() local gw_check = sys.exec("netstat -tlnp 2>/dev/null | grep -c ':" .. port .. " ' || echo 0"):gsub("%s+", "") result.gateway_running = (tonumber(gw_check) or 0) > 0 + -- 如果端口未监听但 procd 进程存在,说明正在启动中 (gateway 初始化需要数分钟) + if not result.gateway_running and enabled == "1" then + local procd_pid = sys.exec("pgrep -f 'openclaw.*gateway' 2>/dev/null | head -1"):gsub("%s+", "") + if procd_pid ~= "" then + result.gateway_starting = true + end + end + -- PTY 端口检查 local pty_check = sys.exec("netstat -tlnp 2>/dev/null | grep -c ':" .. pty_port .. " ' || echo 0"):gsub("%s+", "") result.pty_running = (tonumber(pty_check) or 0) > 0 @@ -183,8 +192,13 @@ function action_service_ctl() sys.exec("/etc/init.d/openclaw start >/dev/null 2>&1 &") elseif action == "stop" then sys.exec("/etc/init.d/openclaw stop >/dev/null 2>&1") + -- stop 后额外等待确保端口释放 + sys.exec("sleep 2") elseif action == "restart" then - sys.exec("/etc/init.d/openclaw restart >/dev/null 2>&1 &") + -- 先完整 stop (确保端口释放),再后台 start + sys.exec("/etc/init.d/openclaw stop >/dev/null 2>&1") + sys.exec("sleep 2") + sys.exec("/etc/init.d/openclaw start >/dev/null 2>&1 &") elseif action == "enable" then sys.exec("/etc/init.d/openclaw enable 2>/dev/null") elseif action == "disable" then diff --git a/luasrc/view/openclaw/console.htm b/luasrc/view/openclaw/console.htm index ccde507..aaa58ac 100644 --- a/luasrc/view/openclaw/console.htm +++ b/luasrc/view/openclaw/console.htm @@ -136,6 +136,14 @@ local port = uci:get("openclaw", "main", "port") or "18789" openBtn.href = url; openBtn.style.display = ''; showIframe(url); + } else if (d.gateway_starting) { + statusTextEl.innerHTML = '⏳ 网关正在启动'; + openBtn.style.display = 'none'; + loading.innerHTML = '
' + + '
' + + '
OpenClaw 网关正在启动中...
' + + '
首次启动可能需要 2~5 分钟,请耐心等待,页面会自动刷新。
' + + '
'; } else { statusTextEl.innerHTML = '● 网关未运行'; openBtn.style.display = 'none'; diff --git a/luasrc/view/openclaw/status.htm b/luasrc/view/openclaw/status.htm index fc4fca7..17d9980 100644 --- a/luasrc/view/openclaw/status.htm +++ b/luasrc/view/openclaw/status.htm @@ -54,6 +54,7 @@ } .oc-badge-running { background: #e6f7e9; color: #1a7f37; } .oc-badge-stopped { background: #ffeef0; color: #cf222e; } +.oc-badge-starting { background: #fff8c5; color: #9a6700; } .oc-badge-disabled { background: #f0f0f0; color: #656d76; } .oc-badge-unknown { background: #fff8c5; color: #9a6700; } .oc-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; } @@ -95,6 +96,8 @@ stEl.innerHTML = '已禁用'; } else if (d.gateway_running) { stEl.innerHTML = '运行中'; + } else if (d.gateway_starting) { + stEl.innerHTML = '⏳ 正在启动...'; } else { stEl.innerHTML = '已停止'; } @@ -102,6 +105,8 @@ var gwEl = document.getElementById('oc-st-gateway'); if (d.gateway_running) { gwEl.innerHTML = '监听中 :' + d.port; + } else if (d.gateway_starting) { + gwEl.innerHTML = '初始化中,首次启动可能需要 2~5 分钟...'; } else { gwEl.innerHTML = '未监听'; } diff --git a/root/etc/init.d/openclaw b/root/etc/init.d/openclaw index 9a62208..d460e7b 100755 --- a/root/etc/init.d/openclaw +++ b/root/etc/init.d/openclaw @@ -190,6 +190,37 @@ all) gw_bind="custom" ;; # custom = 0.0.0.0 *) gw_bind="$bind" ;; esac +# 确保网关端口未被残留进程占用 (防止 restart 时 crash loop) +_ensure_port_free() { + local p="$1" max_wait="${2:-5}" i=0 + while [ $i -lt $max_wait ]; do + if command -v ss >/dev/null 2>&1; then + ss -tlnp 2>/dev/null | grep -q ":${p} " || return 0 + else + netstat -tlnp 2>/dev/null | grep -q ":${p} " || return 0 + fi + # 尝试杀掉占用端口的 gateway 进程 + if [ $i -eq 0 ]; then + local occ_pid + for occ_pid in $(pgrep -f "openclaw-gateway" 2>/dev/null); do + kill "$occ_pid" 2>/dev/null + done + fi + sleep 1 + i=$((i + 1)) + done + # 最后手段: SIGKILL + local port_pid="" + if command -v ss >/dev/null 2>&1; then + port_pid=$(ss -tlnp 2>/dev/null | grep ":${p} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1) + else + port_pid=$(netstat -tlnp 2>/dev/null | grep ":${p} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1) + fi + [ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null && sleep 1 + return 0 +} +_ensure_port_free "$port" + # 启动 OpenClaw Gateway (主服务, 前台运行) procd_open_instance "gateway" procd_set_param command "$NODE_BIN" "$oc_entry" gateway run \ @@ -205,7 +236,7 @@ OC_GLOBAL="$OC_GLOBAL" \ OC_DATA="$OC_DATA" \ PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" procd_set_param user openclaw -procd_set_param respawn 3600 5 5 +procd_set_param respawn 3600 10 5 procd_set_param stdout 1 procd_set_param stderr 1 procd_set_param pidfile /var/run/openclaw.pid @@ -245,8 +276,44 @@ procd_close_instance } stop_service() { -# procd 会自动处理进程停止 -return 0 +# procd 会自动停止它管理的主进程 (openclaw) +# 但 openclaw 会 fork 出 openclaw-gateway 子进程实际监听端口 +# procd 不一定能正确追踪并杀掉子进程树,需要手动清理 + +local port +port=$(uci -q get openclaw.main.port || echo "18789") + +# 杀掉所有 openclaw / openclaw-gateway 残留进程 +# (排除 web-pty.js 和 oc-config.sh,它们由 pty 实例管理) +local pid +for pid in $(pgrep -f "openclaw-gateway" 2>/dev/null) \ + $(pgrep -f "openclaw.*gateway.*run" 2>/dev/null); do + kill "$pid" 2>/dev/null +done + +# 等待端口真正释放 (最多 8 秒) +local wait_count=0 +while [ $wait_count -lt 8 ]; do + if command -v ss >/dev/null 2>&1; then + ss -tlnp 2>/dev/null | grep -q ":${port} " || break + else + netstat -tlnp 2>/dev/null | grep -q ":${port} " || break + fi + sleep 1 + wait_count=$((wait_count + 1)) +done + +# 如果端口仍被占用,强制杀掉占用者 +if [ $wait_count -ge 8 ]; then + local port_pid="" + if command -v ss >/dev/null 2>&1; then + port_pid=$(ss -tlnp 2>/dev/null | grep ":${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1) + else + port_pid=$(netstat -tlnp 2>/dev/null | grep ":${port} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1) + fi + [ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null + sleep 1 +fi } service_triggers() { @@ -255,6 +322,8 @@ procd_add_reload_trigger "openclaw" reload_service() { stop +# stop_service 已确保端口释放,但额外等待 1 秒让内核回收 +sleep 1 start } @@ -384,6 +453,8 @@ local mins=$(( (uptime % 3600) / 60 )) echo "运行时间: ${hours}小时 ${mins}分钟" fi fi +elif pgrep -f "openclaw.*gateway" >/dev/null 2>&1; then +echo "网关: 正在启动 (首次启动可能需要 2~5 分钟)" else echo "网关: 已停止" fi diff --git a/scripts/build_ipk.sh b/scripts/build_ipk.sh index 28efd34..3acf90e 100755 --- a/scripts/build_ipk.sh +++ b/scripts/build_ipk.sh @@ -86,7 +86,7 @@ mkdir -p "$CTRL_DIR" cat > "$CTRL_DIR/control" << EOF Package: ${PKG_NAME} Version: ${PKG_VERSION}-${PKG_RELEASE} -Depends: luci-compat, luci-base, curl, openssl-util, util-linux-script +Depends: luci-compat, luci-base, curl, openssl-util, script-utils, tar Source: https://github.com/10000ge10000/luci-app-openclaw SourceName: ${PKG_NAME} License: GPL-3.0 diff --git a/scripts/build_run.sh b/scripts/build_run.sh index 4115f61..6080947 100755 --- a/scripts/build_run.sh +++ b/scripts/build_run.sh @@ -124,7 +124,7 @@ mkdir -p "$INFO_DIR" cat > "$INFO_DIR/$PKG.control" << CTLEOF Package: $PKG Version: $PKG_VER -Depends: luci-compat, luci-base, curl, openssl-util +Depends: luci-compat, luci-base, curl, openssl-util, script-utils, tar Section: luci Architecture: all Installed-Size: 0 @@ -160,7 +160,7 @@ cat >> "$STATUS_FILE" << STEOF Package: $PKG Version: $PKG_VER -Depends: luci-compat, luci-base, curl, openssl-util +Depends: luci-compat, luci-base, curl, openssl-util, script-utils, tar Status: install user installed Architecture: all Conffiles: