Files

1807 lines
67 KiB
Bash
Executable File
Raw Permalink 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.
#!/bin/sh
# ============================================================================
# openclaw-env — Node.js 环境自动检测/下载 + OpenClaw 安装
# 用法:
# openclaw-env setup — 完整安装 (Node.js + pnpm + OpenClaw)
# openclaw-env check — 检查环境状态
# openclaw-env upgrade — 升级 OpenClaw 到最新版
# openclaw-env node — 仅下载/更新 Node.js
# 环境变量:
# OC_VERSION — 指定 OpenClaw 版本 (如 2026.9.1),不设置则安装已测试稳定版
# ============================================================================
set -e
# ── Node.js 版本策略 ──
# 推荐版本: 用于 OpenClaw v2026.9.1+ (要求 >= 22.22.3,使用 musl 预编译包)
NODE_VERSION_V2="22.23.2"
# 默认使用 V2 版本 (可通过 NODE_VERSION 环境变量覆盖)
NODE_VERSION="${NODE_VERSION:-${NODE_VERSION_V2}}"
# 经过验证的 OpenClaw 稳定版本 (更新此值需同步测试)
OC_TESTED_VERSION="2026.9.1"
# OpenClaw 2026.9.x package.json engines.node 的最低要求。
# 如果未来上游提高要求,安装成功后还会从 package.json 再做一次强校验。
OC_NODE_MIN_VERSION="${OC_NODE_MIN_VERSION:-22.22.3}"
# 用户可通过 OC_VERSION 环境变量覆盖安装版本
OC_VERSION="${OC_VERSION:-}"
_script_dir="$(CDPATH= cd -- "$(dirname "$0")" 2>/dev/null && pwd || echo "")"
if [ -r /usr/libexec/openclaw-paths.sh ]; then
. /usr/libexec/openclaw-paths.sh
elif [ -n "$_script_dir" ] && [ -r "${_script_dir}/../libexec/openclaw-paths.sh" ]; then
. "${_script_dir}/../libexec/openclaw-paths.sh"
elif [ -r ./root/usr/libexec/openclaw-paths.sh ]; then
. ./root/usr/libexec/openclaw-paths.sh
fi
if [ -r /usr/libexec/openclaw-node.sh ]; then
. /usr/libexec/openclaw-node.sh
elif [ -n "$_script_dir" ] && [ -r "${_script_dir}/../libexec/openclaw-node.sh" ]; then
. "${_script_dir}/../libexec/openclaw-node.sh"
elif [ -r ./root/usr/libexec/openclaw-node.sh ]; then
. ./root/usr/libexec/openclaw-node.sh
fi
# ── 安装路径 (支持自定义) ──
# UCI 公开字段继续使用 openclaw.main.install_path,语义为“基础目录”。
# 兼容用户误填 /mnt/data/openclaw:统一剥离末尾 openclaw,避免得到
# /mnt/data/openclaw/openclaw 这种双拼路径。
OC_CONFIGURED_PATH="${OC_INSTALL_PATH:-$(uci -q get openclaw.main.install_path 2>/dev/null || echo '/opt')}"
if command -v oc_load_paths >/dev/null 2>&1; then
if ! oc_load_paths "$OC_CONFIGURED_PATH"; then
echo " [✗] 安装路径无效: $OC_CONFIGURED_PATH"
echo " [!] 请使用绝对路径,例如 /opt、/mnt/data;不要使用相对路径、空格或 shell 特殊字符。"
exit 1
fi
OC_BASE_PATH="$OPENCLAW_INSTALL_PATH"
OC_INSTALL_PATH="$OC_ROOT"
else
OC_BASE_PATH="${OC_CONFIGURED_PATH%/}"
OC_INSTALL_PATH="${OC_BASE_PATH}/openclaw"
NODE_BASE="${OC_INSTALL_PATH}/node"
OC_GLOBAL="${OC_INSTALL_PATH}/global"
OC_DATA="${OC_INSTALL_PATH}/data"
CONFIG_FILE="${OC_DATA}/.openclaw/openclaw.json"
fi
[ -n "${NODE_BASE:-}" ] || NODE_BASE="${OC_INSTALL_PATH}/node"
[ -n "${OC_GLOBAL:-}" ] || OC_GLOBAL="${OC_INSTALL_PATH}/global"
[ -n "${OC_DATA:-}" ] || OC_DATA="${OC_INSTALL_PATH}/data"
[ -n "${CONFIG_FILE:-}" ] || CONFIG_FILE="${OC_DATA}/.openclaw/openclaw.json"
OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-${OC_DATA}/.openclaw}"
OPENCLAW_CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-${CONFIG_FILE}}"
export OPENCLAW_STATE_DIR OPENCLAW_CONFIG_PATH NODE_BASE OC_GLOBAL OC_DATA CONFIG_FILE
# ── OverlayFS 兼容性修复 ──
# iStoreOS/OpenWrt 上 Docker 的 bind mount (/overlay/upper/opt/docker)
# 会导致 OverlayFS 合并视图中 /opt 完全不可写 (mkdir 报 "Directory not empty")。
# 解决方案: 将 /overlay/upper/opt bind mount 到 /opt,绕过 OverlayFS 冲突。
# 注意: 仅当基础路径为 /opt 时才需要此修复
_oc_fix_opt() {
# 如果基础路径不是 /opt,无需修复
[ "$OC_BASE_PATH" != "/opt" ] && return 0
# 如果 /opt 可正常写入,无需修复
if mkdir -p /opt/openclaw/.probe 2>/dev/null; then
rmdir /opt/openclaw/.probe 2>/dev/null
return 0
fi
# /opt 不可写且 overlay upper 层存在 — 执行 bind mount 修复
if [ -d /overlay/upper/opt ]; then
# 确保 upper 层有 openclaw 目录
mkdir -p /overlay/upper/opt/openclaw 2>/dev/null
# 绑定挂载 upper 层的 /opt 到合并视图的 /opt
mount --bind /overlay/upper/opt /opt 2>/dev/null && return 0
fi
return 1
}
_oc_fix_opt
NODE_BIN="${NODE_BASE}/bin/node"
NPM_BIN="${NODE_BASE}/bin/npm"
PNPM_BIN="${OC_GLOBAL}/bin/pnpm"
# Node.js 官方镜像 + musl 非官方构建与自托管兜底
NODE_MIRROR="${NODE_MIRROR:-https://nodejs.org/dist}"
NODE_MIRROR_CN="https://npmmirror.com/mirrors/node"
NODE_MUSL_MIRROR="https://unofficial-builds.nodejs.org/download/release"
# 项目自托管 musl Node.js 兜底资产
NODE_SELF_HOST="https://github.com/10000ge10000/luci-app-openclaw/releases/download/node-bins"
OC_PACKAGE_CONTRACT="/usr/share/openclaw/openclaw-package-contract.js"
if [ ! -f "$OC_PACKAGE_CONTRACT" ]; then
_script_dir="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
if [ -f "${_script_dir}/../share/openclaw/openclaw-package-contract.js" ]; then
OC_PACKAGE_CONTRACT="${_script_dir}/../share/openclaw/openclaw-package-contract.js"
elif [ -f "./root/usr/share/openclaw/openclaw-package-contract.js" ]; then
OC_PACKAGE_CONTRACT="./root/usr/share/openclaw/openclaw-package-contract.js"
fi
fi
export PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:$PATH"
export NODE_ICU_DATA="${NODE_BASE}/share/icu"
log_info() { echo " [✓] $1"; }
log_warn() { echo " [!] $1"; }
log_error() { echo " [✗] $1"; }
# 安全创建目录 (会在 _oc_fix_opt 修复 /opt 后使用标准路径)
ensure_mkdir() {
local target="$1"
[ -d "$target" ] && return 0
if ! mkdir -p "$target" 2>/dev/null; then
log_error "无法创建目录: $target"
local probe_parent="/"
command -v oc_find_existing_path >/dev/null 2>&1 && probe_parent=$(oc_find_existing_path "$target")
log_error "请检查挂载点是否只读、overlay 是否已满,以及 ${probe_parent} 是否可写。"
[ "$OC_BASE_PATH" = "/opt" ] && log_error "如果安装了 Docker,可能需要手动执行: mount --bind /overlay/upper/opt /opt"
return 1
fi
}
ensure_openclaw_user() {
# OpenWrt 默认没有 useradd/adduser 的完整实现,所以沿用 uci-defaults 的轻量写法。
# 这里在安装链路再兜底一次,避免用户删除账号后微信插件安装或 Gateway 启动失败。
if id openclaw >/dev/null 2>&1 || grep -q '^openclaw:' /etc/passwd 2>/dev/null; then
return 0
fi
local oc_uid=1000
while grep -q "^[^:]*:x:${oc_uid}:" /etc/passwd 2>/dev/null; do
oc_uid=$((oc_uid + 1))
done
local oc_gid="$oc_uid"
while grep -q "^[^:]*:x:${oc_gid}:" /etc/group 2>/dev/null; do
oc_gid=$((oc_gid + 1))
done
echo "openclaw:x:${oc_uid}:${oc_gid}:openclaw:${OC_DATA}:/bin/false" >> /etc/passwd
echo 'openclaw:x:0:0:99999:7:::' >> /etc/shadow
echo "openclaw:x:${oc_gid}:" >> /etc/group
log_info "已创建 openclaw 系统用户 (uid=${oc_uid}, gid=${oc_gid})"
}
probe_install_root() {
if command -v oc_probe_writable_root >/dev/null 2>&1; then
if ! oc_probe_writable_root "$OC_BASE_PATH"; then
log_error "安装路径不可写: $OC_BASE_PATH"
log_error "常见原因: overlay 已满、文件系统只读、挂载点未挂载或权限不正确。"
local check_path="/"
command -v oc_find_existing_path >/dev/null 2>&1 && check_path=$(oc_find_existing_path "$OC_BASE_PATH")
df -h "$check_path" 2>/dev/null | tail -1 || true
exit 1
fi
fi
}
cleanup_partial_install() {
[ -n "${OC_STAGE_DIR:-}" ] && rm -rf "$OC_STAGE_DIR" 2>/dev/null || true
# 只在确认目标是规范化后的 openclaw 运行根目录时清理,避免误删用户目录。
if command -v oc_safe_openclaw_root >/dev/null 2>&1 && oc_safe_openclaw_root "$OC_INSTALL_PATH"; then
if [ "${OC_SETUP_FRESH_ROOT:-0}" = "1" ]; then
log_warn "安装失败,清理本次新建的运行目录: $OC_INSTALL_PATH"
rm -rf "$OC_INSTALL_PATH" 2>/dev/null || true
[ -d /overlay/upper ] && rm -rf "/overlay/upper${OC_INSTALL_PATH}" 2>/dev/null || true
else
log_warn "安装失败,检测到已有运行目录,已保留: $OC_INSTALL_PATH"
fi
fi
}
# 检测 C 运行时类型 (glibc vs musl)
detect_libc() {
if ldd --version 2>&1 | grep -qi musl; then
echo "musl"
elif [ -f /lib/ld-musl-*.so.1 ] 2>/dev/null; then
echo "musl"
elif [ -f /etc/openwrt_release ] || grep -qi "openwrt\|istoreos\|lede" /etc/os-release 2>/dev/null; then
echo "musl"
else
echo "glibc"
fi
}
# 在所有可能的位置查找 openclaw 入口文件
# pnpm 全局安装路径形如: $OC_GLOBAL/5/node_modules/openclaw
find_oc_entry() {
local base_global="${1:-$OC_GLOBAL}"
local search_dirs="${base_global}/lib/node_modules/openclaw
${base_global}/node_modules/openclaw
${NODE_BASE}/lib/node_modules/openclaw"
# 添加 pnpm 版本化目录 (如 $base_global/5/node_modules/openclaw)
for ver_dir in "${base_global}"/*/node_modules/openclaw; do
[ -d "$ver_dir" ] 2>/dev/null && search_dirs="$search_dirs
$ver_dir"
done
local d
echo "$search_dirs" | while read -r d; do
[ -z "$d" ] && continue
if [ -f "${d}/openclaw.mjs" ]; then
echo "${d}/openclaw.mjs"
return
elif [ -f "${d}/dist/cli.js" ]; then
echo "${d}/dist/cli.js"
return
fi
done
}
detect_oc_node_requirement() {
local oc_entry pkg_json required
oc_entry=$(find_oc_entry)
[ -n "$oc_entry" ] || return 1
pkg_json="$(dirname "$oc_entry")/package.json"
[ -f "$pkg_json" ] || return 1
[ -x "$NODE_BIN" ] || return 1
required=$(OC_PKG_JSON="$pkg_json" "$NODE_BIN" -e "
const fs=require('fs');
try{
const pkg=JSON.parse(fs.readFileSync(process.env.OC_PKG_JSON,'utf8'));
const spec=(pkg.engines&&pkg.engines.node)||'';
const m=String(spec).match(/([0-9]+\\.[0-9]+\\.[0-9]+)/);
if(m) process.stdout.write(m[1]);
}catch(e){}
" 2>/dev/null)
[ -n "$required" ] || return 1
printf '%s\n' "$required"
}
assert_node_runtime() {
local required="$OC_NODE_MIN_VERSION"
local from_pkg
from_pkg=$(detect_oc_node_requirement 2>/dev/null || true)
if [ -n "$from_pkg" ] && command -v oc_node_version_ge >/dev/null 2>&1; then
if oc_node_version_ge "$from_pkg" "$required"; then
required="$from_pkg"
fi
fi
if command -v oc_assert_node_min_version >/dev/null 2>&1; then
oc_assert_node_min_version "$NODE_BIN" "$required" || exit 1
fi
# 契约校验 helper 与 Node 运行时必须可用,缺失时直接失败关闭
if [ ! -f "$OC_PACKAGE_CONTRACT" ]; then
log_error "契约校验 helper 缺失 ($OC_PACKAGE_CONTRACT),运行时强校验失败关闭"
exit 1
fi
if [ ! -x "$NODE_BIN" ]; then
log_error "Node.js 运行时未就绪 ($NODE_BIN),强校验失败关闭"
exit 1
fi
local engine_spec=">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0"
local oc_entry pkg_json
oc_entry=$(find_oc_entry 2>/dev/null || true)
if [ -n "$oc_entry" ]; then
pkg_json="$(dirname "$oc_entry")/package.json"
if [ -f "$pkg_json" ]; then
local detected_spec
detected_spec=$("$NODE_BIN" -e "try{const p=require(process.argv[1]);process.stdout.write((p.engines&&p.engines.node)||'')}catch(e){}" "$pkg_json" 2>/dev/null || true)
[ -n "$detected_spec" ] && engine_spec="$detected_spec"
fi
fi
if ! "$NODE_BIN" "$OC_PACKAGE_CONTRACT" validate-runtime "$NODE_BIN" "$engine_spec"; then
log_error "Node.js 运行时未满足 engines.node 契约 (${engine_spec}),强校验失败关闭"
exit 1
fi
# OpenClaw CLI 依赖 Unicode property escapes(例如 \p{Extended_Pictographic})。
# ARM64 musl 自托管 Node 包必须带上 NODE_ICU_DATA,否则用户直接执行
# `openclaw status` 等子命令会在 CLI 入口阶段抛 SyntaxError。
if ! NODE_ICU_DATA="${NODE_BASE}/share/icu" "$NODE_BIN" -e "new RegExp('\\\\p{Extended_Pictographic}','u')" >/dev/null 2>&1; then
log_error "Node.js ICU/Unicode 正则能力异常,请重新安装 Node.js 运行时"
exit 1
fi
}
install_openclaw_cli_wrapper() {
local oc_entry
oc_entry=$(find_oc_entry)
[ -n "$oc_entry" ] || return 1
ensure_mkdir "$OC_GLOBAL/bin"
# npm 默认生成的 openclaw 可能是指向 openclaw.mjs 的 symlink。
# 写 wrapper 前必须先 unlink,否则重定向会顺着 symlink 覆盖真实 CLI 入口。
rm -f "$OC_GLOBAL/bin/openclaw" 2>/dev/null || true
# 用 : "${VAR:=default}" 而非 export VAR=... 赋默认值。
#
# 历史实现无条件 export,把 OpenClaw 官方文档承诺的 per-instance 环境变量
# (OPENCLAW_HOME / OPENCLAW_STATE_DIR / OPENCLAW_CONFIG_PATH) 全部覆盖,
# 连 `openclaw --profile <name>` 也随之失效 —— profile 会解析回同一份
# 生产配置。后果是无法做隔离的写入测试,也无法按官方文档跑多实例网关。
#
# 现改为"仅当调用方未设置时才填默认值",默认行为不变,但允许覆盖。
cat > "$OC_GLOBAL/bin/openclaw" << EOF
#!/bin/sh
# luci-app-openclaw 生成的 OpenClaw CLI 包装器。
# 只在调用方未提供时设置默认值,保留官方的 per-instance / --profile 隔离能力。
: "\${HOME:=${OC_DATA}}"
: "\${OPENCLAW_HOME:=${OC_DATA}}"
: "\${OPENCLAW_STATE_DIR:=\${OPENCLAW_HOME}/.openclaw}"
: "\${OPENCLAW_CONFIG_PATH:=\${OPENCLAW_STATE_DIR}/openclaw.json}"
: "\${NODE_ICU_DATA:=${NODE_BASE}/share/icu}"
export HOME OPENCLAW_HOME OPENCLAW_STATE_DIR OPENCLAW_CONFIG_PATH NODE_ICU_DATA
export PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:\$PATH"
exec "${NODE_BIN}" "${oc_entry}" "\$@"
EOF
chmod +x "$OC_GLOBAL/bin/openclaw" 2>/dev/null || true
}
detect_arch() {
local arch
arch=$(uname -m)
case "$arch" in
x86_64) echo "linux-x64" ;;
aarch64) echo "linux-arm64" ;;
armv7l|armv6l)
log_error "不支持的 CPU 架构: $arch"
log_error "Node.js v22+ 不提供 32 位 ARM 预编译包。"
log_error "建议: 使用 aarch64 (ARM64) 版本的固件,或使用 x86_64 设备。"
exit 1
;;
*)
log_error "不支持的 CPU 架构: $arch (仅支持 x86_64/aarch64)"
exit 1
;;
esac
}
# 计算文件 sha256 (优先 sha256sum,兜底 openssl;openssl-util 是本包硬依赖)
oc_sum256() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" 2>/dev/null | awk '{print $1}'
elif command -v openssl >/dev/null 2>&1; then
openssl dgst -sha256 -r "$1" 2>/dev/null | awk '{print $1}'
else
return 1
fi
}
# 校验 Node tarball 完整性:
# - 官方 / unofficial-builds / npmmirror 镜像: 拉取同目录 SHASUMS256.txt 比对
# - 自托管 node-bins: 下载同名 .sha256 边车文件比对
# - 缺失、版本不符、哈希不可验证明确失败 (fail-closed),不得忽略校验
verify_node_tarball() {
local file="$1" mirror_url="$2" filename
filename=$(basename "$mirror_url")
local actual
actual=$(oc_sum256 "$file") || { log_error "无可用 sha256 工具,无法校验文件哈希"; return 1; }
local expected=""
if [ "${mirror_url#*node-bins}" != "$mirror_url" ]; then
# 自托管 Release 资产: 下载 .sha256 边车文件
local fetch_sidecar=0
if command -v curl >/dev/null 2>&1; then
curl -fsSL --connect-timeout 10 --max-time 30 -o "${file}.sha256" "${mirror_url}.sha256" 2>/dev/null && fetch_sidecar=1 || true
elif command -v wget >/dev/null 2>&1; then
wget -q --timeout=10 -O "${file}.sha256" "${mirror_url}.sha256" 2>/dev/null && fetch_sidecar=1 || true
fi
if [ "$fetch_sidecar" -eq 1 ] && [ -s "${file}.sha256" ]; then
expected=$(awk '{print $1}' "${file}.sha256" 2>/dev/null)
rm -f "${file}.sha256"
fi
else
# 官方系镜像: 同版本目录下的 SHASUMS256.txt
local shasum_url="${mirror_url%/*}/SHASUMS256.txt"
local fetch_sums=0
if command -v curl >/dev/null 2>&1; then
curl -fsSL --connect-timeout 10 --max-time 30 -o "${file}.sums" "$shasum_url" 2>/dev/null && fetch_sums=1 || true
elif command -v wget >/dev/null 2>&1; then
wget -q --timeout=10 -O "${file}.sums" "$shasum_url" 2>/dev/null && fetch_sums=1 || true
fi
if [ "$fetch_sums" -eq 1 ] && [ -s "${file}.sums" ]; then
expected=$(awk -v f="$filename" '$2 == f {print $1}' "${file}.sums" 2>/dev/null)
rm -f "${file}.sums"
fi
fi
if [ -z "$expected" ]; then
log_error "未获取到 ${filename} 的有效 SHA256 校验和,哈希不可验证"
return 1
fi
if [ "$actual" = "$expected" ]; then
log_info "SHA256 校验通过"
return 0
fi
log_error "SHA256 校验失败: ${filename} (实际: ${actual}, 期望: ${expected})"
return 1
}
download_node() {
local node_ver="$1"
local node_arch
node_arch=$(detect_arch)
local libc_type
libc_type=$(detect_libc)
# 如果已存在且版本兼容, 跳过
if [ -x "$NODE_BIN" ]; then
local current_ver
current_ver=$("$NODE_BIN" --version 2>/dev/null | sed 's/^v//')
if [ "$current_ver" = "$node_ver" ]; then
log_info "Node.js v${node_ver} 已安装, 跳过下载"
assert_node_runtime
return 0
fi
if command -v oc_node_version_ge >/dev/null 2>&1 && oc_node_version_ge "$current_ver" "$node_ver"; then
log_info "Node.js v${current_ver} 已安装 (>= v${node_ver}), 跳过下载"
assert_node_runtime
return 0
fi
log_warn "当前 Node.js v${current_ver}, 将更新到 v${node_ver}"
fi
# ── 构建下载 URL 列表 (musl 资产名精确固定) ──
local mirror_list=""
local musl_tarball="node-v${node_ver}-${node_arch}-musl.tar.xz"
local glibc_tarball="node-v${node_ver}-${node_arch}.tar.xz"
if [ "$libc_type" = "musl" ]; then
echo ""
echo "=== 下载 Node.js v${node_ver} (${node_arch}, musl libc) ==="
# x64 与 arm64 musl 均优先官方/社区 unofficial-builds,其次 npmmirror,最后 fallback 到项目自托管资产
mirror_list="${NODE_MUSL_MIRROR}/v${node_ver}/${musl_tarball}"
mirror_list="$mirror_list ${NODE_MIRROR_CN}/v${node_ver}/${musl_tarball}"
mirror_list="$mirror_list ${NODE_SELF_HOST}/${musl_tarball}"
else
echo ""
echo "=== 下载 Node.js v${node_ver} (${node_arch}, glibc) ==="
mirror_list="${NODE_MIRROR}/v${node_ver}/${glibc_tarball}"
mirror_list="$mirror_list ${NODE_MIRROR_CN}/v${node_ver}/${glibc_tarball}"
fi
# ── 逐个尝试下载 ──
local downloaded=0
local dl_tmp_dir
dl_tmp_dir=$(mktemp -d /tmp/openclaw-node-XXXXXX 2>/dev/null || mktemp -d 2>/dev/null || echo "/tmp/openclaw-node-$$-${RANDOM:-0}")
ensure_mkdir "$dl_tmp_dir"
local tmp_file="${dl_tmp_dir}/node-v${node_ver}.tar.xz"
local attempt=0
local total=0
for _ in $mirror_list; do total=$((total + 1)); done
for mirror_url in $mirror_list; do
attempt=$((attempt + 1))
echo " 正在从 ${mirror_url} 下载... (${attempt}/${total})"
rm -f "$tmp_file"
local dl_ok=0
if command -v curl >/dev/null 2>&1; then
if curl -fSL --connect-timeout 15 --max-time 300 -o "$tmp_file" "$mirror_url" 2>/dev/null; then
dl_ok=1
fi
elif command -v wget >/dev/null 2>&1; then
if wget -q --timeout=15 -O "$tmp_file" "$mirror_url" 2>/dev/null; then
dl_ok=1
fi
fi
if [ "$dl_ok" -eq 0 ] || [ ! -f "$tmp_file" ]; then
log_warn "从 ${mirror_url} 下载失败 (网络错误或 HTTP 404),尝试备用镜像..."
rm -f "$tmp_file"
continue
fi
# 校验文件大小 (Node.js xz 压缩包至少 5MB; Alpine 精简包约 12MB, 官方完整包约 30MB)
local fsize
fsize=$(wc -c < "$tmp_file" 2>/dev/null || echo 0)
if [ "$fsize" -le 5000000 ]; then
log_warn "下载文件大小异常 (${fsize} bytes),当前镜像产物不完整,淘汰当前源"
rm -f "$tmp_file"
continue
fi
if ! verify_node_tarball "$tmp_file" "$mirror_url"; then
log_warn "文件 SHA256 校验失败或校验清单缺失,淘汰当前镜像源: ${mirror_url}"
rm -f "$tmp_file"
continue
fi
downloaded=1
break
done
if [ "$downloaded" -ne 1 ]; then
log_error "Node.js 运行时所有候选镜像源均下载或校验失败 (候选源已耗尽)"
if [ "$libc_type" = "musl" ]; then
log_error "当前设备为 ${node_arch} musl 架构,候选源均下载或校验失败"
log_error "候选镜像包括 unofficial-builds、npmmirror 以及自托管 assets"
fi
rm -rf "$dl_tmp_dir" 2>/dev/null || true
return 1 2>/dev/null || exit 1
fi
# 解压
echo " 正在解压到 ${NODE_BASE}..."
# 先解压到临时目录并确认完整,再替换 NODE_BASE。
# 这样即使 tar/xz 缺失或压缩包损坏,也不会删除已有 Node.js。
local tmp_extract="${dl_tmp_dir}/extract"
rm -rf "$tmp_extract" 2>/dev/null
ensure_mkdir "$tmp_extract"
if ! tar xf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then
log_error "Node.js 解压失败,请确认已安装 GNU tar(opkg install tar)且下载文件完整"
rm -rf "$dl_tmp_dir" 2>/dev/null || true
return 1 2>/dev/null || exit 1
fi
# 找顶层目录 (node-vX.X.X-linux-xxx)
local top_dir
top_dir=$(ls "$tmp_extract" 2>/dev/null | head -1)
if [ -z "$top_dir" ] || [ ! -d "$tmp_extract/$top_dir" ]; then
log_error "解压后未找到顶层目录,安装失败"
rm -rf "$dl_tmp_dir" 2>/dev/null || true
return 1 2>/dev/null || exit 1
fi
# 校验解压出的产物名称是否与请求版本及架构精确一致
local expected_top="node-v${node_ver}-${node_arch}"
[ "$libc_type" = "musl" ] && expected_top="node-v${node_ver}-${node_arch}-musl"
if [ "$top_dir" != "$expected_top" ]; then
log_error "构建产物顶层目录名称不符: 期望 ${expected_top}, 实际 ${top_dir}"
rm -rf "$dl_tmp_dir" 2>/dev/null || true
return 1 2>/dev/null || exit 1
fi
# OverlayFS 兼容: rm -rf 后可能因 whiteout 导致 mkdir 失败
# 只有在临时目录已解压成功后才替换正式 Node 目录。
rm -rf "$NODE_BASE" 2>/dev/null
if [ -d /overlay/upper ]; then
rm -rf "/overlay/upper${NODE_BASE}" 2>/dev/null
fi
ensure_mkdir "$NODE_BASE"
cp -a "$tmp_extract/$top_dir/." "$NODE_BASE/"
rm -rf "$dl_tmp_dir" 2>/dev/null || true
# 验证
if [ -x "$NODE_BIN" ]; then
local installed_ver
installed_ver=$("$NODE_BIN" --version 2>/dev/null | sed 's/^v//' || echo "unknown")
if [ "$installed_ver" != "$node_ver" ]; then
log_error "Node.js 安装版本不匹配: 期望 v${node_ver}, 实际 v${installed_ver}"
return 1 2>/dev/null || exit 1
fi
local installed_arch
installed_arch=$("$NODE_BIN" -p "process.arch" 2>/dev/null || echo "unknown")
local expected_arch="x64"
[ "$node_arch" = "linux-arm64" ] && expected_arch="arm64"
if [ "$installed_arch" != "$expected_arch" ]; then
log_error "Node.js 架构不匹配: 期望 ${expected_arch}, 实际 ${installed_arch}"
return 1 2>/dev/null || exit 1
fi
log_info "Node.js v${installed_ver} (${installed_arch}) 安装成功"
assert_node_runtime
else
log_error "Node.js 安装验证失败: ${NODE_BIN} 不可执行"
return 1 2>/dev/null || exit 1
fi
}
install_pnpm() {
echo ""
echo "=== 安装 pnpm ==="
if [ -x "$PNPM_BIN" ]; then
log_info "pnpm 已安装: $($PNPM_BIN --version 2>/dev/null)"
return 0
fi
# 使用 npm 安装 pnpm 到全局目录
ensure_mkdir "$OC_GLOBAL"
"$NPM_BIN" install -g pnpm --prefix="$OC_GLOBAL" 2>/dev/null
if [ -x "$OC_GLOBAL/bin/pnpm" ]; then
PNPM_BIN="$OC_GLOBAL/bin/pnpm"
log_info "pnpm $($PNPM_BIN --version 2>/dev/null) 安装成功"
elif [ -x "$NODE_BASE/bin/pnpm" ]; then
PNPM_BIN="$NODE_BASE/bin/pnpm"
log_info "pnpm $($PNPM_BIN --version 2>/dev/null) 安装成功"
else
log_warn "pnpm 安装失败, 将使用 npm 作为回退"
fi
}
validate_openclaw_manifest() {
local pkg_target="$1" target_ver="$2"
local pkg_json=""
local pkg_dir=""
if [ -d "$pkg_target" ]; then
pkg_dir="$pkg_target"
pkg_json="${pkg_dir}/package.json"
else
pkg_json="$pkg_target"
pkg_dir="$(dirname "$pkg_json")"
fi
if [ ! -f "$pkg_json" ]; then
log_error "未找到 OpenClaw package.json: $pkg_json"
return 1
fi
# 校验 helper 和 Node 运行时必须可用,缺失直接失败关闭
if [ ! -f "$OC_PACKAGE_CONTRACT" ]; then
log_error "契约校验 helper 缺失 ($OC_PACKAGE_CONTRACT),无法验证包清单"
return 1
fi
if [ ! -x "$NODE_BIN" ]; then
log_error "Node.js 运行时未就绪 ($NODE_BIN),无法执行契约校验"
return 1
fi
echo " 正在校验 OpenClaw 包清单不可变契约 (静态预检)..."
if ! "$NODE_BIN" "$OC_PACKAGE_CONTRACT" validate-manifest "$pkg_json" "$target_ver"; then
log_error "OpenClaw 包清单契约静态校验失败"
return 1
fi
# 仅对锁定稳定版 2026.9.1 静态校验脚本文件物理存在性 (零执行预检)
if [ "$target_ver" = "2026.9.1" ] || [ -z "$target_ver" ] || [ "$target_ver" = "stable" ]; then
local preinstall_script="${pkg_dir}/scripts/preinstall-package-manager-warning.mjs"
local postinstall_script="${pkg_dir}/scripts/postinstall-bundled-plugins.mjs"
if [ ! -f "$preinstall_script" ]; then
log_error "白名单 preinstall 脚本文件不存在: $preinstall_script"
return 1
fi
if [ ! -f "$postinstall_script" ]; then
log_error "白名单 postinstall 脚本文件不存在: $postinstall_script"
return 1
fi
fi
return 0
}
execute_openclaw_lifecycle() {
local pkg_dir="$1" target_ver="$2"
local pkg_json="${pkg_dir}/package.json"
if [ ! -f "$pkg_json" ]; then
log_error "未找到 OpenClaw package.json: $pkg_json"
return 1
fi
# 校验 helper 和 Node 运行时必须可用,缺失直接失败关闭
if [ ! -f "$OC_PACKAGE_CONTRACT" ]; then
log_error "契约校验 helper 缺失 ($OC_PACKAGE_CONTRACT),无法验证包清单"
return 1
fi
if [ ! -x "$NODE_BIN" ]; then
log_error "Node.js 运行时未就绪 ($NODE_BIN),无法执行契约校验"
return 1
fi
if command -v validate_openclaw_manifest >/dev/null 2>&1; then
if ! validate_openclaw_manifest "$pkg_dir" "$target_ver"; then
return 1
fi
else
echo " 正在校验 OpenClaw 包清单不可变契约..."
if ! "$NODE_BIN" "$OC_PACKAGE_CONTRACT" validate-manifest "$pkg_json" "$target_ver"; then
log_error "OpenClaw 包清单契约校验失败,终止安装"
return 1
fi
fi
# 仅对锁定稳定版 2026.9.1 显式执行两个必要 lifecycle (受控环境: cwd=pkg_dir, npm_config_ignore_scripts=true)
if [ "$target_ver" = "2026.9.1" ] || [ -z "$target_ver" ] || [ "$target_ver" = "stable" ]; then
local preinstall_script="${pkg_dir}/scripts/preinstall-package-manager-warning.mjs"
local postinstall_script="${pkg_dir}/scripts/postinstall-bundled-plugins.mjs"
if [ ! -f "$preinstall_script" ]; then
log_error "白名单 preinstall 脚本文件不存在: $preinstall_script"
return 1
fi
if [ ! -f "$postinstall_script" ]; then
log_error "白名单 postinstall 脚本文件不存在: $postinstall_script"
return 1
fi
log_info "显式执行已校验的 OpenClaw preinstall 脚本 (受控环境)..."
if ! ( cd "$pkg_dir" && npm_config_ignore_scripts=true "$NODE_BIN" scripts/preinstall-package-manager-warning.mjs ); then
log_error "OpenClaw preinstall 脚本执行失败 (退出码非零)"
return 1
fi
log_info "显式执行已校验的 OpenClaw postinstall 脚本 (锁定 npm_config_ignore_scripts=true)..."
if ! ( cd "$pkg_dir" && npm_config_ignore_scripts=true "$NODE_BIN" scripts/postinstall-bundled-plugins.mjs ); then
log_error "OpenClaw postinstall 脚本执行失败 (退出码非零)"
return 1
fi
log_info "OpenClaw 已校验 lifecycle 脚本执行完成"
else
# latest/自定义版本不复用 2026.9.1 白名单: 存在 lifecycle 已在 validate-manifest 中 fail-closed;
# 无 lifecycle 时明确标记未验证,不能宣称完成受支持安装,严禁执行脚本
log_warn "当前安装版本为自定义/未验证版本 (${target_ver}),已严格阻断所有未验证的 lifecycle 脚本"
log_warn "该版本未列入已验证兼容测试矩阵,不属于受支持的稳定配置"
fi
return 0
}
install_openclaw() {
echo ""
echo "=== 安装 OpenClaw ==="
# 确定安装版本
local target_ver=""
local oc_pkg="openclaw"
if [ -z "$OC_VERSION" ] || [ "$OC_VERSION" = "stable" ]; then
target_ver="${OC_TESTED_VERSION}"
oc_pkg="openclaw@${OC_TESTED_VERSION}"
log_info "默认稳定版: v${OC_TESTED_VERSION}"
elif [ "$OC_VERSION" = "latest" ]; then
target_ver="latest"
oc_pkg="openclaw@latest"
log_info "安装 npm latest 稳定标签版本"
else
target_ver="${OC_VERSION}"
oc_pkg="openclaw@${OC_VERSION}"
log_info "指定版本: v${OC_VERSION}"
fi
# 检查 git 是否可用 (openclaw 部分依赖可能使用 git:// 协议)
if ! command -v git >/dev/null 2>&1; then
log_warn "未检测到 git,正在尝试安装..."
opkg update >/dev/null 2>&1
opkg install git git-http 2>&1 | tail -3 || true
if command -v git >/dev/null 2>&1; then
log_info "git 安装成功"
else
log_warn "git 安装失败,将尝试无 git 模式安装"
fi
fi
# 必须要求 helper 和 npm 可用
if [ ! -x "$NPM_BIN" ]; then
log_error "npm 可执行文件不可用 ($NPM_BIN),无法安装 OpenClaw"
exit 1
fi
if [ ! -x "$NODE_BIN" ]; then
log_error "Node.js 可执行文件不可用 ($NODE_BIN),无法安装 OpenClaw"
exit 1
fi
if [ ! -f "$OC_PACKAGE_CONTRACT" ]; then
log_error "契约校验 helper 缺失 ($OC_PACKAGE_CONTRACT),安装终止 (失败关闭)"
exit 1
fi
local stage_dir
stage_dir=$(mktemp -d /tmp/openclaw-stage-XXXXXX 2>/dev/null || mktemp -d 2>/dev/null || echo "/tmp/openclaw-stage-$$-${RANDOM:-0}")
mkdir -p "$stage_dir"
OC_STAGE_DIR="$stage_dir"
OC_CANDIDATE_PREFIX=""
OC_BACKUP_PREFIX=""
trap 'rc=$?; [ -n "${OC_STAGE_DIR:-}" ] && rm -rf "$OC_STAGE_DIR" 2>/dev/null || true; [ -n "${OC_CANDIDATE_PREFIX:-}" ] && rm -rf "$OC_CANDIDATE_PREFIX" 2>/dev/null || true; if [ -n "${OC_BACKUP_PREFIX:-}" ] && [ -d "${OC_BACKUP_PREFIX:-}" ] && [ ! -d "$OC_GLOBAL" ]; then mv "$OC_BACKUP_PREFIX" "$OC_GLOBAL" 2>/dev/null || true; fi; [ -n "${OC_BACKUP_PREFIX:-}" ] && rm -rf "$OC_BACKUP_PREFIX" 2>/dev/null || true; if [ "${OC_SETUP_CLEANUP:-0}" = "1" ] && [ $rc -ne 0 ]; then cleanup_partial_install; fi; exit $rc' EXIT INT TERM
local pack_log="${stage_dir}/npm-pack.log"
local pack_rc=0
echo " 正在下载并打包预检 OpenClaw (${oc_pkg})..."
( cd "$stage_dir" && "$NPM_BIN" pack "$oc_pkg" ) > "$pack_log" 2>&1 || pack_rc=$?
if [ "$pack_rc" -ne 0 ]; then
cat "$pack_log" 2>/dev/null || true
log_error "npm pack 下载失败 (退出码: ${pack_rc})"
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
exit "$pack_rc"
fi
local tarball
tarball=$(ls "$stage_dir"/*.tgz 2>/dev/null | head -1)
if [ -z "$tarball" ] || [ ! -f "$tarball" ]; then
log_error "npm pack 未生成有效 tarball 产物"
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
exit 1
fi
local unpack_dir="${stage_dir}/unpacked"
ensure_mkdir "$unpack_dir"
if ! tar xzf "$tarball" -C "$unpack_dir" 2>/dev/null; then
log_error "解压预检 tarball 失败: $tarball"
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
exit 1
fi
local staged_pkg_dir="${unpack_dir}/package"
if [ ! -f "${staged_pkg_dir}/package.json" ]; then
staged_pkg_dir=$(find "$unpack_dir" -maxdepth 2 -name "package.json" 2>/dev/null | head -1)
[ -n "$staged_pkg_dir" ] && staged_pkg_dir=$(dirname "$staged_pkg_dir")
fi
if [ -z "$staged_pkg_dir" ] || [ ! -f "${staged_pkg_dir}/package.json" ]; then
log_error "预检解包产物中未找到 package.json"
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
exit 1
fi
echo " 正在执行解包清单与运行时预检 (零执行静态预检)..."
if ! validate_openclaw_manifest "$staged_pkg_dir" "$target_ver"; then
log_error "OpenClaw 预检校验失败,终止安装 (未修改现有安装)"
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
exit 1
fi
local global_parent
global_parent="$(dirname "$OC_GLOBAL")"
ensure_mkdir "$global_parent"
local candidate_prefix
candidate_prefix=$(mktemp -d "${global_parent}/.global-candidate-XXXXXX" 2>/dev/null || echo "${global_parent}/.global-candidate-$$-${RANDOM:-0}")
ensure_mkdir "$candidate_prefix"
chmod 755 "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX="$candidate_prefix"
local npm_log="${stage_dir}/npm-install.log"
local npm_rc=0
echo " 正在执行 npm 临时前缀安装 (${tarball} --ignore-scripts --omit=optional)..."
"$NPM_BIN" install -g "$tarball" --prefix="$candidate_prefix" --ignore-scripts --omit=optional > "$npm_log" 2>&1 || npm_rc=$?
if [ "$npm_rc" -ne 0 ]; then
cat "$npm_log" 2>/dev/null || true
log_error "npm 安装失败 (退出码: ${npm_rc})"
rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
exit "$npm_rc"
fi
tail -10 "$npm_log" 2>/dev/null || true
# 检查候选入口文件
local cand_entry
cand_entry=$(find_oc_entry "$candidate_prefix")
if [ -z "$cand_entry" ] || [ ! -f "$cand_entry" ]; then
log_error "候选安装未找到 OpenClaw 入口文件,安装中止"
rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
exit 1
fi
# 校验包清单并在候选依赖树上下文中执行白名单 lifecycle 各一次
local cand_pkg_dir
cand_pkg_dir="$(dirname "$cand_entry")"
echo " 正在候选依赖树上下文中执行已验证 lifecycle..."
if ! execute_openclaw_lifecycle "$cand_pkg_dir" "$target_ver"; then
log_error "OpenClaw 契约校验或 lifecycle 脚本执行失败,安装中止 (未修改现有安装)"
rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
exit 1
fi
# 候选安装与 lifecycle 全部成功后,事务式替换 OC_GLOBAL
echo " 候选验证通过,正在原子切换 OC_GLOBAL..."
local backup_prefix="${global_parent}/.global-backup-$$-${RANDOM:-0}"
local has_existing_global=0
if [ -e "$OC_GLOBAL" ]; then
has_existing_global=1
if ! mv "$OC_GLOBAL" "$backup_prefix" 2>/dev/null; then
log_error "无法备份现有 OC_GLOBAL 目录,安装中止"
rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
exit 1
fi
OC_BACKUP_PREFIX="$backup_prefix"
fi
local switch_ok=0
chmod 755 "$candidate_prefix" 2>/dev/null || true
chmod -R a+rX "$candidate_prefix" 2>/dev/null || true
if mv "$candidate_prefix" "$OC_GLOBAL" 2>/dev/null; then
OC_CANDIDATE_PREFIX=""
local final_entry
final_entry=$(find_oc_entry)
if [ -n "$final_entry" ] && [ -f "$final_entry" ] && install_openclaw_cli_wrapper; then
switch_ok=1
fi
fi
if [ "$switch_ok" -eq 1 ]; then
if [ "$has_existing_global" -eq 1 ]; then
rm -rf "$backup_prefix" 2>/dev/null || true
OC_BACKUP_PREFIX=""
fi
else
log_error "替换 OC_GLOBAL 或 wrapper 验证失败,正在回滚原有版本..."
rm -rf "$OC_GLOBAL" 2>/dev/null || true
[ -e "$candidate_prefix" ] && rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
if [ "$has_existing_global" -eq 1 ]; then
mv "$backup_prefix" "$OC_GLOBAL" 2>/dev/null || true
OC_BACKUP_PREFIX=""
install_openclaw_cli_wrapper 2>/dev/null || true
fi
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
exit 1
fi
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
if [ "${OC_SETUP_CLEANUP:-0}" != "1" ]; then
trap - EXIT INT TERM
fi
# 验证 CLI wrapper 与运行时
if ! install_openclaw_cli_wrapper; then
log_error "OpenClaw CLI wrapper 安装失败"
exit 1
fi
local oc_found
oc_found=$(find_oc_entry)
local oc_ver=""
if [ -n "$oc_found" ]; then
oc_ver=$("$NODE_BIN" "$oc_found" --version 2>/dev/null | tr -d '[:space:]')
fi
if [ -n "$oc_ver" ]; then
log_info "OpenClaw v${oc_ver} 安装成功"
assert_node_runtime
else
log_error "OpenClaw 安装验证失败,无法读取版本号"
exit 1
fi
}
init_openclaw() {
echo ""
echo "=== 初始化 OpenClaw ==="
# 创建数据目录
ensure_mkdir "$OC_DATA/.openclaw"
# 运行 onboard
local oc_entry=""
oc_entry=$(find_oc_entry)
if [ -n "$oc_entry" ]; then
HOME="$OC_DATA" \
OPENCLAW_HOME="$OC_DATA" \
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
OPENCLAW_CONFIG_PATH="${OC_DATA}/.openclaw/openclaw.json" \
"$NODE_BIN" "$oc_entry" onboard --non-interactive --accept-risk --tools-profile coding 2>/dev/null || true
log_info "初始化完成"
fi
# 设置文件权限
chown -R openclaw:openclaw "$OC_DATA" 2>/dev/null || true
chown -R openclaw:openclaw "$OC_GLOBAL" 2>/dev/null || true
chown -R openclaw:openclaw "$NODE_BASE" 2>/dev/null || true
}
do_setup() {
local node_ver="$NODE_VERSION"
# 检查是否已安装
if [ -x "$NODE_BIN" ] && [ -n "$(find_oc_entry)" ]; then
install_openclaw_cli_wrapper || true
local oc_ver=""
local oc_entry="$(find_oc_entry)"
local oc_pkg_dir="$(dirname "$oc_entry")"
if [ -f "$oc_pkg_dir/package.json" ]; then
oc_ver=$("$NODE_BIN" -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version)}catch(e){}" "$oc_pkg_dir/package.json" 2>/dev/null || true)
[ -z "$oc_ver" ] && oc_ver=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$oc_pkg_dir/package.json" 2>/dev/null | head -1)
fi
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ ⚠️ OpenClaw 运行环境已安装,无需重复安装 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo " Node.js: $($NODE_BIN --version 2>/dev/null)"
[ -n "$oc_ver" ] && echo " OpenClaw: v${oc_ver}"
echo ""
echo " 如需升级,请使用: openclaw-env upgrade"
echo " 如需重装,请先卸载: 在 LuCI 界面点击「卸载环境」"
echo ""
exit 0
fi
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ 一万AI分享 OpenClaw 环境安装 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo " 架构: $(uname -m)"
echo " Node 版本: v${node_ver}"
if [ -n "$OC_VERSION" ]; then
echo " OpenClaw: v${OC_VERSION} (稳定版)"
else
echo " OpenClaw: v${OC_TESTED_VERSION} (默认稳定版)"
fi
echo " 安装路径: ${OC_INSTALL_PATH}"
echo " 数据路径: ${OC_DATA}"
echo ""
ensure_openclaw_user
probe_install_root
if [ -e "$OC_INSTALL_PATH" ]; then
OC_SETUP_FRESH_ROOT=0
else
OC_SETUP_FRESH_ROOT=1
fi
OC_SETUP_CLEANUP=1
trap 'rc=$?; if [ $rc -ne 0 ] && [ "${OC_SETUP_CLEANUP:-0}" = "1" ]; then cleanup_partial_install; fi; exit $rc' EXIT
download_node "$node_ver" || exit 1
install_pnpm
install_openclaw
init_openclaw
OC_SETUP_CLEANUP=0
trap - EXIT
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ ✅ 安装完成! ║"
echo "║ ║"
echo "║ 如通过 LuCI 安装,服务已自动启用并启动。 ║"
echo "║ 如通过命令行安装,请在 LuCI → 服务 → OpenClaw 中启用。 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
}
do_check() {
echo "=== OpenClaw 环境检查 ==="
echo ""
# Node.js
if [ -x "$NODE_BIN" ]; then
echo " Node.js: $($NODE_BIN --version 2>/dev/null)"
else
echo " Node.js: 未安装"
fi
# pnpm
if [ -x "$PNPM_BIN" ]; then
echo " pnpm: $($PNPM_BIN --version 2>/dev/null)"
elif [ -x "${NODE_BASE}/bin/pnpm" ]; then
echo " pnpm: $(${NODE_BASE}/bin/pnpm --version 2>/dev/null)"
else
echo " pnpm: 未安装"
fi
# OpenClaw
local oc_entry=""
oc_entry=$(find_oc_entry)
if [ -n "$oc_entry" ] && [ -x "$NODE_BIN" ]; then
echo " OpenClaw: v$($NODE_BIN $oc_entry --version 2>/dev/null | tr -d '[:space:]')"
else
echo " OpenClaw: 未安装"
fi
# 配置文件
if [ -f "$OC_DATA/.openclaw/openclaw.json" ]; then
echo " 配置: $OC_DATA/.openclaw/openclaw.json (存在)"
else
echo " 配置: 未初始化"
fi
# 磁盘使用 (检测实际安装路径)
local used
used=$(du -sh "$OC_INSTALL_PATH" 2>/dev/null | awk '{print $1}')
echo " 磁盘: ${used:-N/A}"
echo " 安装路径: ${OC_INSTALL_PATH}"
# libc 类型
echo " C库: $(detect_libc)"
}
upgrade_openclaw() {
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ 一万AI分享 OpenClaw 升级 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
local rep_result="failed"
local rep_backup="skipped"
local rep_doctor="skipped"
local rep_gateway="skipped"
local rep_rollback="none"
local rep_verified=""
record_verified_cmd() {
local name="$1"
local code="$2"
if [ -z "$rep_verified" ]; then
rep_verified="[${name} rc=${code}]"
else
rep_verified="${rep_verified} [${name} rc=${code}]"
fi
}
print_upgrade_report() {
echo ""
echo "============================================================"
echo "OpenClaw Upgrade Transaction Report"
echo "============================================================"
echo "RESULT: ${rep_result}"
echo "BACKUP: ${rep_backup}"
echo "DOCTOR: ${rep_doctor}"
echo "GATEWAY: ${rep_gateway}"
echo "ROLLBACK: ${rep_rollback}"
echo "VERIFIED: ${rep_verified}"
echo "============================================================"
echo ""
}
if [ ! -x "$NODE_BIN" ]; then
log_error "Node.js 未安装, 请先运行: openclaw-env setup"
print_upgrade_report
exit 1
fi
if [ ! -x "$NPM_BIN" ]; then
log_error "npm 可执行文件不可用 ($NPM_BIN),无法升级 OpenClaw"
print_upgrade_report
exit 1
fi
if [ ! -f "$OC_PACKAGE_CONTRACT" ]; then
log_error "契约校验 helper 缺失 ($OC_PACKAGE_CONTRACT),升级终止 (失败关闭)"
print_upgrade_report
exit 1
fi
# 获取当前版本
local current_ver=""
local oc_entry=""
oc_entry=$(find_oc_entry)
if [ -n "$oc_entry" ]; then
local oc_pkg_dir="$(dirname "$oc_entry")"
[ -f "$oc_pkg_dir/package.json" ] && \
current_ver=$("$NODE_BIN" -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version)}catch(e){}" "$oc_pkg_dir/package.json" 2>/dev/null || true)
[ -z "$current_ver" ] && [ -f "$oc_pkg_dir/package.json" ] && \
current_ver=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$oc_pkg_dir/package.json" 2>/dev/null | head -1)
fi
echo " Node.js: $($NODE_BIN --version 2>/dev/null)"
[ -n "$current_ver" ] && echo " 当前版本: v${current_ver}"
echo ""
echo "=== 正在升级 OpenClaw ==="
echo ""
local target_ver="${OC_TESTED_VERSION}"
local target_pkg="openclaw@${OC_TESTED_VERSION}"
if [ "$OC_VERSION" = "latest" ]; then
target_ver="latest"
target_pkg="openclaw@latest"
elif [ -n "$OC_VERSION" ] && [ "$OC_VERSION" != "stable" ]; then
target_ver="${OC_VERSION}"
target_pkg="openclaw@${OC_VERSION}"
fi
echo "目标版本: ${target_pkg#openclaw@}"
local stage_dir
stage_dir=$(mktemp -d /tmp/openclaw-stage-XXXXXX 2>/dev/null || mktemp -d 2>/dev/null || echo "/tmp/openclaw-stage-$$-${RANDOM:-0}")
mkdir -p "$stage_dir"
OC_STAGE_DIR="$stage_dir"
OC_CANDIDATE_PREFIX=""
OC_BACKUP_PREFIX=""
trap 'rc=$?; [ -n "${OC_STAGE_DIR:-}" ] && rm -rf "$OC_STAGE_DIR" 2>/dev/null || true; [ -n "${OC_CANDIDATE_PREFIX:-}" ] && rm -rf "$OC_CANDIDATE_PREFIX" 2>/dev/null || true; if [ -n "${OC_BACKUP_PREFIX:-}" ] && [ -d "${OC_BACKUP_PREFIX:-}" ] && [ ! -d "$OC_GLOBAL" ]; then mv "$OC_BACKUP_PREFIX" "$OC_GLOBAL" 2>/dev/null || true; fi; [ -n "${OC_BACKUP_PREFIX:-}" ] && rm -rf "$OC_BACKUP_PREFIX" 2>/dev/null || true; exit $rc' EXIT INT TERM
local pack_log="${stage_dir}/npm-pack.log"
local pack_rc=0
echo " 正在下载并打包预检 OpenClaw (${target_pkg})..."
( cd "$stage_dir" && "$NPM_BIN" pack "$target_pkg" ) > "$pack_log" 2>&1 || pack_rc=$?
record_verified_cmd "npm-pack" "$pack_rc"
if [ "$pack_rc" -ne 0 ]; then
cat "$pack_log" 2>/dev/null || true
log_error "npm pack 升级下载失败 (退出码: ${pack_rc})"
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
print_upgrade_report
exit "$pack_rc"
fi
local tarball
tarball=$(ls "$stage_dir"/*.tgz 2>/dev/null | head -1)
if [ -z "$tarball" ] || [ ! -f "$tarball" ]; then
log_error "npm pack 未生成有效 tarball 产物"
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
print_upgrade_report
exit 1
fi
local unpack_dir="${stage_dir}/unpacked"
ensure_mkdir "$unpack_dir"
if ! tar xzf "$tarball" -C "$unpack_dir" 2>/dev/null; then
log_error "解压预检 tarball 失败: $tarball"
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
print_upgrade_report
exit 1
fi
local staged_pkg_dir="${unpack_dir}/package"
if [ ! -f "${staged_pkg_dir}/package.json" ]; then
staged_pkg_dir=$(find "$unpack_dir" -maxdepth 2 -name "package.json" 2>/dev/null | head -1)
[ -n "$staged_pkg_dir" ] && staged_pkg_dir=$(dirname "$staged_pkg_dir")
fi
if [ -z "$staged_pkg_dir" ] || [ ! -f "${staged_pkg_dir}/package.json" ]; then
log_error "预检解包产物中未找到 package.json"
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
print_upgrade_report
exit 1
fi
echo " 正在执行解包清单与运行时预检 (零执行静态预检)..."
local manifest_rc=0
if ! validate_openclaw_manifest "$staged_pkg_dir" "$target_ver"; then
manifest_rc=1
fi
record_verified_cmd "validate-manifest" "$manifest_rc"
if [ "$manifest_rc" -ne 0 ]; then
log_error "OpenClaw 升级预检校验失败,终止升级 (原有安装保持不变)"
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
print_upgrade_report
exit 1
fi
local global_parent
global_parent="$(dirname "$OC_GLOBAL")"
ensure_mkdir "$global_parent"
local candidate_prefix
candidate_prefix=$(mktemp -d "${global_parent}/.global-candidate-XXXXXX" 2>/dev/null || echo "${global_parent}/.global-candidate-$$-${RANDOM:-0}")
ensure_mkdir "$candidate_prefix"
chmod 755 "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX="$candidate_prefix"
# 升级一律先以 --ignore-scripts --omit=optional 阻断全部未知脚本,捕获真实退出码
local npm_log="${stage_dir}/npm-upgrade.log"
local npm_rc=0
echo " 正在执行 npm 升级安装 (${tarball} --ignore-scripts --omit=optional)..."
"$NPM_BIN" install -g "$tarball" --prefix="$candidate_prefix" --ignore-scripts --omit=optional > "$npm_log" 2>&1 || npm_rc=$?
record_verified_cmd "candidate-install" "$npm_rc"
if [ "$npm_rc" -ne 0 ]; then
cat "$npm_log" 2>/dev/null || true
log_error "npm 升级失败 (退出码: ${npm_rc})"
rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
print_upgrade_report
exit "$npm_rc"
fi
tail -10 "$npm_log" 2>/dev/null || true
# 验证升级候选入口文件
local new_entry
new_entry=$(find_oc_entry "$candidate_prefix")
if [ -z "$new_entry" ] || [ ! -f "$new_entry" ]; then
log_error "升级验证失败,候选 OpenClaw 入口文件未找到"
rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
print_upgrade_report
exit 1
fi
local new_pkg_dir
new_pkg_dir="$(dirname "$new_entry")"
echo " 正在候选依赖树上下文中执行已验证 lifecycle..."
local lc_rc=0
if ! execute_openclaw_lifecycle "$new_pkg_dir" "$target_ver"; then
lc_rc=1
fi
record_verified_cmd "lifecycle" "$lc_rc"
if [ "$lc_rc" -ne 0 ]; then
log_error "OpenClaw 升级契约校验或 lifecycle 脚本执行失败,升级中止 (原有安装保持不变)"
rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
print_upgrade_report
exit 1
fi
# ── 状态管理事务 (备份创建与校验) ──
local _script_dir
_script_dir="$(CDPATH= cd -- "$(dirname "$0")" 2>/dev/null && pwd || echo "")"
local state_dir="${OPENCLAW_STATE_DIR:-${OC_DATA:+$OC_DATA/.openclaw}}"
local state_helper="/usr/libexec/openclaw-upgrade-state.sh"
[ -x "$state_helper" ] || state_helper="${_script_dir}/../libexec/openclaw-upgrade-state.sh"
[ -x "$state_helper" ] || state_helper="./root/usr/libexec/openclaw-upgrade-state.sh"
local state_managed=0
local bk_archive=""
if [ -n "$state_dir" ] && [ -d "$state_dir" ] && [ -x "$state_helper" ]; then
state_managed=1
# 0. 初始化升级事务
"$state_helper" init-transaction "$target_ver" "$state_dir" 2>/dev/null || true
# 1. 停止 Gateway,保证 SQLite 静态一致性
local stop_gw_rc=0
if [ -x /etc/init.d/openclaw ]; then
/etc/init.d/openclaw stop >/dev/null 2>&1 || stop_gw_rc=$?
elif command -v openclaw-initd >/dev/null 2>&1; then
openclaw-initd stop >/dev/null 2>&1 || stop_gw_rc=$?
fi
record_verified_cmd "gateway-stop" "$stop_gw_rc"
# 2. 状态备份创建 (位于 State 目录之外,以 openclaw 用户执行)
"$state_helper" set-phase backing_up "$state_dir" 0 "" 2>/dev/null || true
local backup_dir="${OPENCLAW_BACKUP_DIR:-${OC_DATA:+$OC_DATA/backups}}"
[ -z "$backup_dir" ] && backup_dir="$(dirname "$state_dir")/openclaw-backups"
local bk_create_rc=0
local bk_out=""
bk_out=$("$state_helper" backup-create "$backup_dir" "$state_dir" 2>&1) || bk_create_rc=$?
record_verified_cmd "backup-create" "$bk_create_rc"
if [ "$bk_create_rc" -ne 0 ]; then
log_error "升级安全事务中止: 创建完整状态备份失败"
rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
rep_backup="failed (exit=${bk_create_rc})"
rep_rollback="none"
"$state_helper" set-phase failed "$state_dir" "$bk_create_rc" "Backup create failed" "" 2>/dev/null || true
if [ -x /etc/init.d/openclaw ]; then
/etc/init.d/openclaw start >/dev/null 2>&1 || true
elif command -v openclaw-initd >/dev/null 2>&1; then
openclaw-initd start >/dev/null 2>&1 || true
fi
print_upgrade_report
exit "$bk_create_rc"
fi
bk_archive=$(printf '%s\n' "$bk_out" | tail -1)
rep_backup="created (${bk_archive})"
# 3. 官方 backup verify 与完整覆盖清单校验
local bk_verify_rc=0
"$state_helper" backup-verify "$bk_archive" "$state_dir" >/dev/null 2>&1 || bk_verify_rc=$?
record_verified_cmd "backup-verify" "$bk_verify_rc"
if [ "$bk_verify_rc" -ne 0 ]; then
log_error "升级安全事务中止: 备份归档官方验证或覆盖清单校验失败"
rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
rep_backup="failed (verify exit=${bk_verify_rc})"
rep_rollback="none"
"$state_helper" set-phase failed "$state_dir" "$bk_verify_rc" "Backup verify failed" "$bk_archive" 2>/dev/null || true
if [ -x /etc/init.d/openclaw ]; then
/etc/init.d/openclaw start >/dev/null 2>&1 || true
elif command -v openclaw-initd >/dev/null 2>&1; then
openclaw-initd start >/dev/null 2>&1 || true
fi
print_upgrade_report
exit "$bk_verify_rc"
fi
rep_backup="verified (${bk_archive})"
"$state_helper" set-phase backup_verified "$state_dir" 0 "" "$bk_archive" 2>/dev/null || true
# 4. 数据库前向迁移预检 (在 candidate 完成后、code switch 之前)
local db_pre_rc=0
"$state_helper" database-preflight "$bk_archive" "$state_dir" >/dev/null 2>&1 || db_pre_rc=$?
record_verified_cmd "database-preflight" "$db_pre_rc"
if [ "$db_pre_rc" -ne 0 ]; then
log_error "升级安全事务中止: 数据库前向迁移预检未通过"
rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
rep_rollback="none"
"$state_helper" set-phase failed "$state_dir" "$db_pre_rc" "Database preflight failed" "$bk_archive" 2>/dev/null || true
if [ -x /etc/init.d/openclaw ]; then
/etc/init.d/openclaw start >/dev/null 2>&1 || true
elif command -v openclaw-initd >/dev/null 2>&1; then
openclaw-initd start >/dev/null 2>&1 || true
fi
print_upgrade_report
exit "$db_pre_rc"
fi
fi
# ── 原子切换 OC_GLOBAL 代码 (旧代码保留至迁移与健康检查完成) ──
echo " 候选验证通过,正在原子切换 OC_GLOBAL..."
local backup_prefix="${global_parent}/.global-backup-$$-${RANDOM:-0}"
local has_existing_global=0
if [ -e "$OC_GLOBAL" ]; then
has_existing_global=1
if ! mv "$OC_GLOBAL" "$backup_prefix" 2>/dev/null; then
log_error "无法备份现有 OC_GLOBAL 目录,升级中止"
rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
record_verified_cmd "code-switch" 1
rep_result="failed"
rep_rollback="none"
print_upgrade_report
exit 1
fi
OC_BACKUP_PREFIX="$backup_prefix"
fi
local switch_ok=0
chmod 755 "$candidate_prefix" 2>/dev/null || true
chmod -R a+rX "$candidate_prefix" 2>/dev/null || true
if mv "$candidate_prefix" "$OC_GLOBAL" 2>/dev/null; then
OC_CANDIDATE_PREFIX=""
local final_entry
final_entry=$(find_oc_entry)
if [ -n "$final_entry" ] && [ -f "$final_entry" ] && install_openclaw_cli_wrapper; then
switch_ok=1
fi
fi
if [ "$switch_ok" -eq 1 ]; then
record_verified_cmd "code-switch" 0
else
record_verified_cmd "code-switch" 1
log_error "替换 OC_GLOBAL 或 wrapper 验证失败,执行代码回滚 (迁移尚未开始)..."
rm -rf "$OC_GLOBAL" 2>/dev/null || true
[ -e "$candidate_prefix" ] && rm -rf "$candidate_prefix" 2>/dev/null || true
OC_CANDIDATE_PREFIX=""
local code_pre_rb_rc=0
if [ "$has_existing_global" -eq 1 ]; then
mv "$backup_prefix" "$OC_GLOBAL" 2>/dev/null || code_pre_rb_rc=$?
OC_BACKUP_PREFIX=""
install_openclaw_cli_wrapper 2>/dev/null || true
fi
record_verified_cmd "code-rollback" "$code_pre_rb_rc"
[ "$state_managed" -eq 1 ] && "$state_helper" set-phase failed "$state_dir" 1 "Code switch failed" "$bk_archive" 2>/dev/null || true
if [ -x /etc/init.d/openclaw ]; then
/etc/init.d/openclaw start >/dev/null 2>&1 || true
elif command -v openclaw-initd >/dev/null 2>&1; then
openclaw-initd start >/dev/null 2>&1 || true
fi
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
rep_rollback="code: ok, state: none"
print_upgrade_report
exit 1
fi
# ── 迁移开始阶段: 任何失败必须保留 2026.9.1 与失败状态,进入 recovery 状态 ──
# 严禁在迁移开始后将旧代码放回已迁移状态,完整回滚必须显式执行
if [ "$state_managed" -eq 1 ]; then
"$state_helper" set-phase migrating "$state_dir" 0 "" "$bk_archive" 2>/dev/null || true
# 1. 幂等 Doctor migration、配置验证及 Token 对账
local doc_rc=0
"$state_helper" doctor-migration "$state_dir" "$target_ver" >/dev/null 2>&1 || doc_rc=$?
record_verified_cmd "doctor" "$doc_rc"
if [ "$doc_rc" -ne 0 ]; then
log_error "Doctor migration 失败,系统已进入 recovery 恢复模式 (保留 2026.9.1 及状态,需显式回滚)..."
rep_doctor="failed (exit=${doc_rc})"
"$state_helper" set-phase recovery "$state_dir" "$doc_rc" "Doctor migration failed" "$bk_archive" 2>/dev/null || true
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
rep_rollback="recovery_required (manual explicit rollback needed; new state & 2026.9.1 preserved)"
print_upgrade_report
exit "$doc_rc"
fi
rep_doctor="passed"
"$state_helper" set-phase migrated "$state_dir" 0 "" "$bk_archive" 2>/dev/null || true
# 2. 插件收敛事务 (registry refresh / doctor --post-upgrade)
local plug_rc=0
"$state_helper" plugin-convergence "$state_dir" >/dev/null 2>&1 || plug_rc=$?
record_verified_cmd "plugin-convergence" "$plug_rc"
# 3. 启动 Gateway
local gw_start_rc=0
if [ -x /etc/init.d/openclaw ]; then
/etc/init.d/openclaw start >/dev/null 2>&1 || gw_start_rc=$?
elif command -v openclaw-initd >/dev/null 2>&1; then
openclaw-initd start >/dev/null 2>&1 || gw_start_rc=$?
fi
record_verified_cmd "gateway-start" "$gw_start_rc"
# 3. Gateway 健康验证
"$state_helper" set-phase gateway_verifying "$state_dir" 0 "" "$bk_archive" 2>/dev/null || true
local gw_health_rc=0
"$state_helper" gateway-verify "" "$state_dir" >/dev/null 2>&1 || gw_health_rc=$?
record_verified_cmd "gateway-health" "$gw_health_rc"
if [ "$gw_health_rc" -ne 0 ]; then
log_error "Gateway 健康检查未通过,系统已进入 recovery 恢复模式 (需显式回滚)..."
rep_gateway="failed (exit=${gw_health_rc})"
if [ -x /etc/init.d/openclaw ]; then
/etc/init.d/openclaw stop >/dev/null 2>&1 || true
elif command -v openclaw-initd >/dev/null 2>&1; then
openclaw-initd stop >/dev/null 2>&1 || true
fi
"$state_helper" set-phase recovery "$state_dir" "$gw_health_rc" "Gateway health verify failed" "$bk_archive" 2>/dev/null || true
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
rep_result="failed"
rep_rollback="recovery_required (manual explicit rollback needed; new state & 2026.9.1 preserved)"
print_upgrade_report
exit "$gw_health_rc"
fi
rep_gateway="healthy"
"$state_helper" set-phase completed "$state_dir" 0 "" "$bk_archive" 2>/dev/null || true
fi
# 迁移与 Gateway 验证全部成功,清理代码备份 (备份归档持续保留)
if [ "$has_existing_global" -eq 1 ] && [ -n "$backup_prefix" ] && [ -d "$backup_prefix" ]; then
rm -rf "$backup_prefix" 2>/dev/null || true
OC_BACKUP_PREFIX=""
fi
rm -rf "$stage_dir" 2>/dev/null || true
OC_STAGE_DIR=""
trap - EXIT INT TERM
local final_installed_entry
final_installed_entry=$(find_oc_entry)
local final_pkg_dir
final_pkg_dir="$(dirname "$final_installed_entry")"
local new_ver=""
if [ -f "$final_pkg_dir/package.json" ]; then
new_ver=$("$NODE_BIN" -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version)}catch(e){}" "$final_pkg_dir/package.json" 2>/dev/null || true)
[ -z "$new_ver" ] && new_ver=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$final_pkg_dir/package.json" 2>/dev/null | head -1)
fi
if [ -z "$new_ver" ] && [ -n "$final_installed_entry" ]; then
new_ver=$("$NODE_BIN" "$final_installed_entry" --version 2>/dev/null | tr -d '[:space:]' || true)
fi
rep_result="success"
rep_rollback="none"
print_upgrade_report
echo ""
if [ -n "$new_ver" ]; then
assert_node_runtime
if [ "$current_ver" = "$new_ver" ]; then
log_info "当前已是最新版本 v${new_ver}"
else
log_info "升级成功: v${current_ver} → v${new_ver}"
fi
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ ✅ 升级完成! ║"
echo "╚══════════════════════════════════════════════════════════════╝"
else
log_error "升级验证失败,无法读取新版本号"
exit 1
fi
}
do_upgrade() {
upgrade_openclaw "$@"
}
# ── 恢复出厂设置 (非交互式) ──
do_factory_reset() {
local config_dir="${OC_DATA}/.openclaw"
local config_file="${config_dir}/openclaw.json"
local auth_file="${config_dir}/agents/main/agent/auth-profiles.json"
log_info "恢复出厂设置..."
# 1. 停止 Gateway
log_info "停止 Gateway..."
/etc/init.d/openclaw stop >/dev/null 2>&1 || true
sleep 2
# 2. 备份当前配置
if [ -f "$config_file" ]; then
local backup_dir="${config_dir}/backups"
local backup_ts=$(date +%Y%m%d_%H%M%S)
ensure_mkdir "$backup_dir"
cp "$config_file" "${backup_dir}/openclaw_${backup_ts}.json"
log_info "备份已保存: backups/openclaw_${backup_ts}.json"
fi
# 3. 重置配置
rm -f "$config_file" "${config_file}.bak" 2>/dev/null || true
echo '{}' > "$config_file"
chown openclaw:openclaw "$config_file" 2>/dev/null || true
# 4. 重置认证信息
if [ -f "$auth_file" ]; then
echo '{"version":1,"profiles":{},"usageStats":{}}' > "$auth_file"
chown openclaw:openclaw "$auth_file" 2>/dev/null || true
fi
# 5. 重新初始化
if [ -x "$NODE_BIN" ]; then
local oc_entry
oc_entry=$(find "$OC_GLOBAL" -name "openclaw.mjs" -path "*/openclaw/openclaw.mjs" 2>/dev/null | head -1)
if [ -n "$oc_entry" ]; then
log_info "重新初始化..."
OPENCLAW_HOME="$OC_DATA" "$NODE_BIN" "$oc_entry" onboard --non-interactive --accept-risk --tools-profile coding >/dev/null 2>&1 || true
fi
fi
# 6. 应用 OpenWrt 适配配置
if [ -x "$NODE_BIN" ] && [ -f "$config_file" ]; then
local new_token
new_token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || dd if=/dev/urandom bs=24 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' | head -c 48)
_JS_KEY="gateway.port" _JS_VAL="18789" "$NODE_BIN" -e "const fs=require('fs');let d={};try{d=JSON.parse(fs.readFileSync('${config_file}','utf8'));}catch(e){}const ks=process.env._JS_KEY.split('.');let o=d;for(let i=0;i<ks.length-1;i++){if(!o[ks[i]]||typeof o[ks[i]]!=='object')o[ks[i]]={};o=o[ks[i]];}let v=process.env._JS_VAL;try{v=JSON.parse(v);}catch(e){}o[ks[ks.length-1]]=v;fs.writeFileSync('${config_file}',JSON.stringify(d,null,2));" 2>/dev/null
for kv in "gateway.bind=lan" "gateway.mode=local" "gateway.auth.mode=token" "gateway.auth.token=${new_token}" "gateway.controlUi.allowInsecureAuth=true" "gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true" "gateway.tailscale.mode=off" "acp.dispatch.enabled=false" "tools.profile=coding"; do
local k="${kv%%=*}" v="${kv#*=}"
_JS_KEY="$k" _JS_VAL="$v" "$NODE_BIN" -e "const fs=require('fs');let d={};try{d=JSON.parse(fs.readFileSync('${config_file}','utf8'));}catch(e){}const ks=process.env._JS_KEY.split('.');let o=d;for(let i=0;i<ks.length-1;i++){if(!o[ks[i]]||typeof o[ks[i]]!=='object')o[ks[i]]={};o=o[ks[i]];}let v=process.env._JS_VAL;try{v=JSON.parse(v);}catch(e){}o[ks[ks.length-1]]=v;fs.writeFileSync('${config_file}',JSON.stringify(d,null,2));" 2>/dev/null
done
chown openclaw:openclaw "$config_file" 2>/dev/null || true
# 同步 token 到 UCI
. /lib/functions.sh 2>/dev/null || true
uci set openclaw.main.token="$new_token" 2>/dev/null || true
uci commit openclaw 2>/dev/null || true
log_info "新认证令牌: $new_token"
fi
# 7. 确保数据目录权限正确 (防止 root 操作留下的文件)
chown -R openclaw:openclaw "$OC_DATA" 2>/dev/null || true
# 8. 重启服务
/etc/init.d/openclaw start >/dev/null 2>&1 &
log_info "出厂设置已恢复,Gateway 重启中..."
}
# ── 离线安装 (从本地文件安装 Node.js + OpenClaw) ──
do_setup_offline() {
local offline_dir="${2:-/tmp/openclaw-offline}"
if [ ! -d "$offline_dir" ]; then
log_error "离线安装目录不存在: $offline_dir"
log_error "此命令通常由离线 .run 安装器自动调用"
exit 1
fi
# 检查是否已安装
if [ -x "$NODE_BIN" ] && [ -n "$(find_oc_entry)" ]; then
log_warn "OpenClaw 运行环境已安装"
log_warn "如需重新离线安装,请先卸载现有环境"
exit 0
fi
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ 一万AI分享 OpenClaw 离线安装 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo " 架构: $(uname -m)"
echo " 安装路径: ${OC_INSTALL_PATH}"
echo " 数据路径: ${OC_DATA}"
echo " 离线包: ${offline_dir}"
echo ""
ensure_openclaw_user
probe_install_root
# [1] 安装 Node.js
local node_tarball="$offline_dir/node.tar.xz"
if [ -f "$node_tarball" ]; then
echo "=== 安装 Node.js (离线) ==="
rm -rf "$NODE_BASE" 2>/dev/null
[ -d /overlay/upper ] && rm -rf "/overlay/upper${NODE_BASE}" 2>/dev/null
ensure_mkdir "$NODE_BASE"
if tar --strip-components=1 -xf "$node_tarball" -C "$NODE_BASE" 2>/dev/null; then
: # GNU tar
else
local tmp_extract="/tmp/node-extract-$$"
ensure_mkdir "$tmp_extract"
tar xf "$node_tarball" -C "$tmp_extract"
local top_dir=$(ls "$tmp_extract" 2>/dev/null | head -1)
if [ -n "$top_dir" ] && [ -d "$tmp_extract/$top_dir" ]; then
cp -a "$tmp_extract/$top_dir/." "$NODE_BASE/"
fi
rm -rf "$tmp_extract"
fi
if [ -x "$NODE_BIN" ]; then
log_info "Node.js $($NODE_BIN --version 2>/dev/null) 安装成功"
assert_node_runtime
else
log_error "Node.js 安装失败"
exit 1
fi
else
log_error "未找到 Node.js 离线包: $node_tarball"
exit 1
fi
# [2] 安装 OpenClaw
local oc_tarball="$offline_dir/openclaw-deps.tar.gz"
if [ -f "$oc_tarball" ]; then
echo ""
echo "=== 安装 OpenClaw (离线) ==="
rm -rf "$OC_GLOBAL" 2>/dev/null
[ -d /overlay/upper ] && rm -rf "/overlay/upper${OC_GLOBAL}" 2>/dev/null
ensure_mkdir "$OC_GLOBAL"
tar xzf "$oc_tarball" -C "$OC_GLOBAL"
local oc_entry=$(find_oc_entry)
if [ -n "$oc_entry" ]; then
local oc_pkg_dir="$(dirname "$oc_entry")"
if ! execute_openclaw_lifecycle "$oc_pkg_dir" "${OC_TESTED_VERSION}"; then
log_error "OpenClaw 离线包契约校验或 lifecycle 脚本执行失败"
exit 1
fi
local oc_ver=$("$NODE_BIN" "$oc_entry" --version 2>/dev/null | tr -d '[:space:]')
if ! install_openclaw_cli_wrapper; then
log_error "OpenClaw CLI wrapper 安装失败"
exit 1
fi
log_info "OpenClaw v${oc_ver} 安装成功"
assert_node_runtime
else
log_error "OpenClaw 安装验证失败"
exit 1
fi
else
log_error "未找到 OpenClaw 离线包: $oc_tarball"
exit 1
fi
# [3] 初始化
init_openclaw
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ ✅ 离线安装完成! ║"
echo "║ ║"
echo "║ 请在 LuCI → 服务 → OpenClaw 中启用服务。 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
}
# ── 主入口 ──
case "${1:-}" in
setup)
do_setup
;;
setup-offline)
do_setup_offline "$@"
;;
check)
do_check
;;
upgrade)
do_upgrade
;;
node)
download_node "$NODE_VERSION" || exit 1
;;
factory-reset)
do_factory_reset
;;
*)
echo "用法: openclaw-env {setup|setup-offline|check|upgrade|node|factory-reset}"
echo ""
echo " setup — 完整安装 (下载 Node.js + pnpm + OpenClaw)"
echo " setup-offline — 离线安装 (从本地文件安装,由 .run 安装器调用)"
echo " check — 检查环境状态"
echo " upgrade — 升级 OpenClaw 到最新版"
echo " node — 仅下载/更新 Node.js"
echo " factory-reset — 恢复出厂设置 (清除所有配置)"
exit 1
;;
esac