Files
op-packages/luci-app-openclaw/root/usr/bin/openclaw-env
T

2122 lines
82 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
# openclaw-env versions — 输出发布通道、上游版本与已验证版本 (JSON)
# 环境变量:
# OC_CHANNEL — 发布通道: extended-stable (默认) / latest
# OC_VERSION — 锁定精确版本 (如 2026.9.36);历史值 stable/latest 分别映射到对应通道
# OC_ALLOW_UNVERIFIED — 1 = 允许安装上游已发布但未经本插件验证的版本
# 通道与已验证版本记录在 /usr/share/openclaw/openclaw-compat.json
# ============================================================================
set -e
# ── Node.js 版本策略 ──
# 24.22.2 (24 LTS) 同时满足 OpenClaw 2026.3.x ~ 2026.10.x 全部 engines.node,
# 因此升级时先换 Node、回滚到旧版 OpenClaw 时也无需降级 Node。
NODE_VERSION_V2="24.22.2"
# 默认使用 V2 版本 (可通过 NODE_VERSION 环境变量覆盖)
NODE_VERSION="${NODE_VERSION:-${NODE_VERSION_V2}}"
# 默认通道 (extended-stable) 的已验证版本,需与 openclaw-compat.json 保持一致 (测试会校验)
OC_TESTED_VERSION="2026.9.36"
# 已验证版本 engines.node 的最低要求。
# 安装成功后还会从 package.json 再做一次强校验。
OC_NODE_MIN_VERSION="${OC_NODE_MIN_VERSION:-24.22.1}"
# 支持底线: 兼容矩阵内最低版本 (2026.9.1) 的要求,已安装包的 engines 不能把要求拉低到它以下
OC_NODE_FLOOR_VERSION="22.22.3"
# 用户可通过 OC_VERSION 环境变量覆盖安装版本
OC_VERSION="${OC_VERSION:-}"
OC_CHANNEL="${OC_CHANNEL:-$(uci -q get openclaw.main.channel 2>/dev/null || true)}"
OC_ALLOW_UNVERIFIED="${OC_ALLOW_UNVERIFIED:-0}"
_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
OC_COMPAT_FILE="$(dirname "$OC_PACKAGE_CONTRACT")/openclaw-compat.json"
export OC_COMPAT_FILE OC_ALLOW_UNVERIFIED
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() {
# 已安装 OpenClaw 时以它自己的 engines 为准 (后面还有完整范围校验),
# 否则存量或预放置的 Node 22 + OpenClaw 2026.9.1 会被新版默认下限误拒 (#113);
# 但不能低于支持底线,防止异常包用宽松的 engines 绕过版本检查。
# OC_NODE_MIN_VERSION 只在尚未安装 OpenClaw 时生效。
local required="$OC_NODE_MIN_VERSION"
local from_pkg
from_pkg=$(detect_oc_node_requirement 2>/dev/null || true)
if [ -n "$from_pkg" ]; then
required="$OC_NODE_FLOOR_VERSION"
if command -v oc_node_version_ge >/dev/null 2>&1 && oc_node_version_ge "$from_pkg" "$OC_NODE_FLOOR_VERSION"; 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=">=24.22.1 <25 || >=26.12.1"
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
}
# npm 自检 (#109)
# Node.js 运行时目录中的 npm 文件不完整时 (解压中断、磁盘写满、被部分覆盖),
# npm 会以 "Class extends value undefined is not a constructor or null" 之类的
# 内部错误退出。仅凭 node --version 无法发现这种损坏。
NPM_SELFCHECK_OUTPUT=""
npm_selfcheck() {
NPM_SELFCHECK_OUTPUT=""
if [ ! -x "$NPM_BIN" ]; then
NPM_SELFCHECK_OUTPUT="npm 可执行文件不存在: $NPM_BIN"
return 1
fi
local out rc=0
out=$("$NPM_BIN" --version 2>&1) || rc=$?
if [ "$rc" -eq 0 ]; then
case "$out" in
[0-9]*) return 0 ;;
esac
fi
NPM_SELFCHECK_OUTPUT=$(printf '%s\n' "$out" | tail -n 5)
return 1
}
npm_runtime_broken_hint() {
log_error "Node.js 运行时自带的 npm 无法运行,通常是运行时文件损坏或不完整:"
printf '%s\n' "$NPM_SELFCHECK_OUTPUT" | sed 's/^/ /'
log_error "修复方法: 在 LuCI → 服务 → OpenClaw 点击「安装运行环境」,会重新下载 Node.js 运行时;配置与数据不受影响"
log_error "命令行等价操作: openclaw-env setup"
}
# ── 发布通道与目标版本解析 ──
# 上游已发布版本来自 npm dist-tags (走用户配置的 npm registry,可使用镜像);
# 本插件已验证版本来自 openclaw-compat.json。两者分开记录,默认只安装已验证版本,
# 上游出现更新版本时只提示,不会自动升级到未验证版本。
OC_UPSTREAM_TAGS_JSON=""
fetch_upstream_disttags() {
OC_UPSTREAM_TAGS_JSON=""
[ -x "$NPM_BIN" ] || return 1
local out
out=$("$NPM_BIN" view openclaw dist-tags --json --fetch-timeout=20000 --fetch-retries=1 2>/dev/null) || return 1
case "$out" in
*"{"*) OC_UPSTREAM_TAGS_JSON="$out"; return 0 ;;
esac
return 1
}
upstream_disttag() {
local tag="$1"
[ -n "$OC_UPSTREAM_TAGS_JSON" ] || fetch_upstream_disttags || return 1
printf '%s' "$OC_UPSTREAM_TAGS_JSON" | "$NODE_BIN" -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>{try{const v=JSON.parse(s)[process.argv[1]];if(typeof v==='string'&&/^\d+\.\d+\.\d+$/.test(v))process.stdout.write(v)}catch(e){}})" "$tag"
}
compat_default_channel() {
"$NODE_BIN" -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).defaultChannel)}catch(e){process.exit(1)}" "$OC_COMPAT_FILE" 2>/dev/null
}
# 结果写入 OC_TARGET_VER / OC_TARGET_CHANNEL / OC_TARGET_VERIFIED (verified|unverified)
# OC_TARGET_NEWER (newer|same) / OC_TARGET_UPSTREAM
resolve_target_version() {
local channel="$OC_CHANNEL" pin="" allow=""
case "$OC_VERSION" in
""|stable) ;;
latest|extended-stable) channel="$OC_VERSION" ;;
*) pin="$OC_VERSION" ;;
esac
[ -n "$pin" ] || pin="$(uci -q get openclaw.main.pin_version 2>/dev/null || true)"
[ "$OC_ALLOW_UNVERIFIED" = "1" ] && allow="--allow-unverified"
local upstream=""
if [ -z "$pin" ]; then
local ch="$channel"
[ -n "$ch" ] && [ "$ch" != "stable" ] || ch=$(compat_default_channel || true)
upstream=$(upstream_disttag "$ch" 2>/dev/null || true)
[ -n "$upstream" ] || log_warn "无法查询上游 ${ch} 通道版本 (npm registry 不可达),按已验证版本安装"
fi
local out
out=$("$NODE_BIN" "$OC_PACKAGE_CONTRACT" resolve-target ${channel:+--channel "$channel"} ${pin:+--pin "$pin"} ${upstream:+--upstream "$upstream"} $allow) || return 1
# shellcheck disable=SC2086
set -- $out
OC_TARGET_VER="$1"
OC_TARGET_CHANNEL="$2"
OC_TARGET_VERIFIED="$3"
OC_TARGET_NEWER="$4"
OC_TARGET_UPSTREAM="$5"
[ "$OC_TARGET_UPSTREAM" = "-" ] && OC_TARGET_UPSTREAM=""
return 0
}
print_target_summary() {
if [ "$OC_TARGET_VERIFIED" = "verified" ]; then
log_info "目标版本: v${OC_TARGET_VER} (${OC_TARGET_CHANNEL},本插件已验证)"
else
log_warn "目标版本: v${OC_TARGET_VER} (${OC_TARGET_CHANNEL},未经本插件验证,按用户明确选择安装)"
fi
if [ "$OC_TARGET_NEWER" = "newer" ] && [ "$OC_TARGET_VERIFIED" = "verified" ]; then
log_warn "上游 ${OC_TARGET_CHANNEL} 通道已发布 v${OC_TARGET_UPSTREAM},尚未经本插件验证,本次仍安装已验证版本"
fi
}
# 正在使用的微信插件版本;未安装或未启用时输出为空
wechat_installed_version() {
local state="${OC_DATA}/.openclaw" pkg
[ -f "$state/openclaw.json" ] && [ -x "$NODE_BIN" ] || return 0
"$NODE_BIN" -e "
const c=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));
const on=(c.channels&&c.channels['openclaw-weixin']&&c.channels['openclaw-weixin'].enabled===true)||
(c.plugins&&c.plugins.entries&&c.plugins.entries['openclaw-weixin']&&c.plugins.entries['openclaw-weixin'].enabled===true);
process.exit(on?0:1);" "$state/openclaw.json" >/dev/null 2>&1 || return 0
pkg=$(find "$state/npm/projects" "$state/extensions/openclaw-weixin" -type f \
\( -path '*/@tencent-weixin/openclaw-weixin/package.json' -o -path '*/extensions/openclaw-weixin/package.json' \) 2>/dev/null | head -n 1)
[ -n "$pkg" ] || return 0
"$NODE_BIN" -p "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version" "$pkg" 2>/dev/null || true
}
# 机器可读的版本信息,供 LuCI 展示「上游已发布」与「本插件已验证」
do_versions() {
local installed="" entry
entry=$(find_oc_entry 2>/dev/null || true)
if [ -n "$entry" ] && [ -f "$(dirname "$entry")/package.json" ] && [ -x "$NODE_BIN" ]; then
installed=$("$NODE_BIN" -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version)}catch(e){}" "$(dirname "$entry")/package.json" 2>/dev/null || true)
fi
if [ ! -x "$NODE_BIN" ] || [ ! -f "$OC_PACKAGE_CONTRACT" ]; then
printf '{"installed":"%s","node":"","nodeRecommended":"%s","channels":{},"upstreamOk":false}\n' "$installed" "$NODE_VERSION"
return 0
fi
local tags=""
[ "${1:-}" = "--offline" ] || { fetch_upstream_disttags && tags="$OC_UPSTREAM_TAGS_JSON"; }
OC_V_INSTALLED="$installed" OC_V_TAGS="$tags" OC_V_CHANNEL="$OC_CHANNEL" \
OC_V_PIN="$(uci -q get openclaw.main.pin_version 2>/dev/null || true)" \
OC_V_NODE="$("$NODE_BIN" --version 2>/dev/null | sed 's/^v//')" \
"$NODE_BIN" -e "
const c=require(process.argv[1]);
const s=c.channelSummary();
let tags={};try{tags=JSON.parse(process.env.OC_V_TAGS||'{}')}catch(e){}
const out={installed:process.env.OC_V_INSTALLED||'',node:process.env.OC_V_NODE||'',nodeRecommended:s.node,
defaultChannel:s.defaultChannel,channel:process.env.OC_V_CHANNEL||s.defaultChannel,pin:process.env.OC_V_PIN||'',
upstreamOk:!!process.env.OC_V_TAGS,channels:{}};
for(const [n,ch] of Object.entries(s.channels)){
const up=typeof tags[n]==='string'?tags[n]:'';
const pu=c.parseStrictSemver(up),pv=c.parseStrictSemver(ch.verified);
out.channels[n]=Object.assign({},ch,{upstream:up,upstreamNewer:!!(pu&&pv&&c.compareSemver(pu,pv)>0)});
}
const iv=c.parseStrictSemver(out.installed);
out.installedVerified=!!(iv&&c.loadCompat().versions[out.installed]);
process.stdout.write(JSON.stringify(out)+'\n');
" "$OC_PACKAGE_CONTRACT"
}
download_node() {
local node_ver="$1"
local node_arch
node_arch=$(detect_arch)
local libc_type
libc_type=$(detect_libc)
# 如果已存在且版本兼容, 跳过 (npm 自检失败时强制重新下载以修复运行时)
if [ -x "$NODE_BIN" ]; then
local current_ver
current_ver=$("$NODE_BIN" --version 2>/dev/null | sed 's/^v//')
local version_ok=0
if [ "$current_ver" = "$node_ver" ]; then
version_ok=1
elif command -v oc_node_version_ge >/dev/null 2>&1 && oc_node_version_ge "$current_ver" "$node_ver"; then
version_ok=1
fi
if [ "$version_ok" -eq 1 ]; then
if npm_selfcheck; then
log_info "Node.js v${current_ver} 已安装 (>= v${node_ver}), 跳过下载"
assert_node_runtime
return 0
fi
log_warn "Node.js v${current_ver} 已安装,但 npm 自检失败,将重新下载运行时修复:"
printf '%s\n' "$NPM_SELFCHECK_OUTPUT" | sed 's/^/ /'
else
log_warn "当前 Node.js v${current_ver}, 将更新到 v${node_ver}"
fi
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
# 已验证版本按兼容矩阵逐项校验 (含 lifecycle 脚本文件存在性);
# 未验证版本仅在 OC_ALLOW_UNVERIFIED=1 时放行 (零执行静态预检)
echo " 正在校验 OpenClaw 包清单不可变契约 (静态预检)..."
if ! "$NODE_BIN" "$OC_PACKAGE_CONTRACT" validate-manifest "$pkg_json" "$target_ver"; then
log_error "OpenClaw 包清单契约静态校验失败"
return 1
fi
return 0
}
# 修正插件历史写入、新版 OpenClaw 不再接受的配置字段 (解析失败时不写入,返回非零)
run_config_fixups() {
local cfg="$1"
local fixups="$(dirname "$OC_PACKAGE_CONTRACT")/oc-config-fixups.js"
[ -f "$cfg" ] || return 0
[ -f "$fixups" ] && [ -x "$NODE_BIN" ] || return 0
local out rc=0
out=$("$NODE_BIN" "$fixups" "$cfg" 2>&1) || rc=$?
[ -n "$out" ] && printf '%s\n' "$out" | sed 's/^/ /'
chown openclaw:openclaw "$cfg" 2>/dev/null || true
return "$rc"
}
# 已验证版本: tarball sha512 必须与兼容矩阵一致,防止镜像源或缓存被篡改
validate_openclaw_tarball() {
local tarball="$1" target_ver="$2"
if ! "$NODE_BIN" "$OC_PACKAGE_CONTRACT" validate-integrity "$tarball" "$target_ver"; then
log_error "OpenClaw 安装包完整性校验失败,可能是镜像源返回了被篡改或损坏的文件"
return 1
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
# 只执行契约批准的 lifecycle 脚本 (受控环境: cwd=pkg_dir, npm_config_ignore_scripts=true)。
# 脚本列表来自兼容矩阵;未验证版本的命令也必须与已验证版本逐字一致,否则上一步已失败关闭。
local vetted
if ! vetted=$("$NODE_BIN" "$OC_PACKAGE_CONTRACT" get-vetted-scripts "$pkg_json" "$target_ver"); then
log_error "无法获取经审核的 lifecycle 脚本列表"
return 1
fi
local script
for script in $vetted; do
case "$script" in
scripts/*.mjs) ;;
*) log_error "拒绝执行非白名单路径的脚本: $script"; return 1 ;;
esac
if [ ! -f "${pkg_dir}/${script}" ]; then
log_error "白名单 lifecycle 脚本文件不存在: ${pkg_dir}/${script}"
return 1
fi
log_info "显式执行已校验的 OpenClaw lifecycle 脚本: ${script} (锁定 npm_config_ignore_scripts=true)"
if ! ( cd "$pkg_dir" && npm_config_ignore_scripts=true "$NODE_BIN" "$script" ); then
log_error "OpenClaw lifecycle 脚本执行失败 (${script},退出码非零)"
return 1
fi
done
log_info "OpenClaw 已校验 lifecycle 脚本执行完成"
if [ "${OC_TARGET_VERIFIED:-verified}" != "verified" ]; then
log_warn "当前安装版本 (${target_ver}) 未列入本插件已验证兼容矩阵,不属于受支持的稳定配置"
fi
return 0
}
install_openclaw() {
echo ""
echo "=== 安装 OpenClaw ==="
# 确定安装版本: 通道 / 锁定版本 → 精确版本 (默认只装本插件已验证的版本)
if ! resolve_target_version; then
log_error "无法确定安装目标版本,安装终止"
exit 1
fi
print_target_summary
local target_ver="$OC_TARGET_VER"
local oc_pkg="openclaw@${OC_TARGET_VER}"
# 检查 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})"
npm_selfcheck || npm_runtime_broken_hint
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
if ! validate_openclaw_tarball "$tarball" "$target_ver"; then
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
# 历史版本传了不存在的 --tools-profile 且缺少 NODE_ICU_DATA,onboard 实际从未成功,
# 配置一直由 init.d 首次启动补全。这里改用上游支持的参数完成工作区与 Gateway 初始化:
# - 显式 --gateway-bind lan/--gateway-port,避免 onboard 默认的 loopback 被 init.d 当作用户设置保留
# - 跳过守护进程/健康检查/渠道/技能等交互步骤,模型与渠道由插件菜单配置
# onboard 失败不影响安装: init.d 启动时仍会补全关键字段,Gateway 在最小配置下也能启动 (已实测)。
local gw_port
gw_port=$(uci -q get openclaw.main.port 2>/dev/null || echo "18789")
case "$gw_port" in ''|*[!0-9]*) gw_port=18789 ;; esac
if HOME="$OC_DATA" \
OPENCLAW_HOME="$OC_DATA" \
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
OPENCLAW_CONFIG_PATH="${OC_DATA}/.openclaw/openclaw.json" \
NODE_ICU_DATA="${NODE_BASE}/share/icu" \
OPENCLAW_SUPERVISOR_MODE=external \
OPENCLAW_SERVICE_REPAIR_POLICY=external \
"$NODE_BIN" "$oc_entry" onboard --non-interactive --accept-risk --mode local \
--auth-choice skip --gateway-bind lan --gateway-port "$gw_port" \
--skip-daemon --skip-health --skip-channels --skip-skills --skip-search --skip-hooks \
>/dev/null 2>&1; then
log_info "初始化完成"
else
log_warn "OpenClaw onboard 未完成,首次启动服务时将由插件补全基础配置"
fi
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
# 已安装但 npm 损坏 (#109): 只重新下载 Node.js 运行时,OpenClaw 与数据保持不变。
# 不启用 OC_SETUP_CLEANUP,失败时不清理已有安装;download_node 仅在新运行时解压校验通过后才替换旧目录。
# 仅在 npm 存在但无法运行时修复: 手工预放置的安装 (#113) 可能没有 npm,
# 此时自动下载 Node.js 恰好会触发这类设备要避免的大量快速写入。
if [ -e "$NPM_BIN" ] && ! npm_selfcheck; then
echo "=== 修复 Node.js 运行时 ==="
log_warn "检测到 npm 无法运行,将重新下载 Node.js 运行时 (OpenClaw 程序与数据保持不变):"
printf '%s\n' "$NPM_SELFCHECK_OUTPUT" | sed 's/^/ /'
download_node "$node_ver" || exit 1
if npm_selfcheck; then
log_info "Node.js 运行时修复完成,可以重新执行升级"
exit 0
fi
npm_runtime_broken_hint
exit 1
fi
install_openclaw_cli_wrapper || true
# 预放置安装 (#113): 程序文件由用户限速拷贝到位但从未初始化 (无 openclaw.json)。
# 补齐系统用户、运行时契约校验与初始化,不触发任何下载或解压。
if [ ! -f "$OC_DATA/.openclaw/openclaw.json" ]; then
echo "=== 初始化预放置的 OpenClaw 运行环境 ==="
ensure_openclaw_user
assert_node_runtime
init_openclaw
echo ""
log_info "预放置安装已完成初始化 (未下载或解压任何文件)"
log_info "请在 LuCI → 服务 → OpenClaw 中启用并启动服务,首次启动时会生成 openclaw.json"
exit 0
fi
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}"
case "$OC_VERSION" in
""|stable|latest|extended-stable)
echo " OpenClaw: 发布通道 ${OC_VERSION:-${OC_CHANNEL:-默认}} (安装本插件已验证版本)" ;;
*)
echo " OpenClaw: 锁定版本 v${OC_VERSION}" ;;
esac
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_wechat="not-enabled"
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 "WECHAT: ${rep_wechat}"
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
# 升级前先确认 npm 可运行,避免在 npm pack 阶段才以内部错误失败 (#109)
if ! npm_selfcheck; then
npm_runtime_broken_hint
record_verified_cmd "npm-selfcheck" 1
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 ""
if ! resolve_target_version; then
log_error "无法确定升级目标版本,升级终止 (原有安装保持不变)"
record_verified_cmd "resolve-target" 1
print_upgrade_report
exit 1
fi
print_target_summary
local target_ver="$OC_TARGET_VER"
local target_pkg="openclaw@${OC_TARGET_VER}"
echo "目标版本: ${target_ver}"
# 先把 Node.js 升级到推荐版本 (2026.9.34+ 要求 >= 24.22.1)。
# 推荐的 24.x 同时满足旧版 OpenClaw 的 engines,后续步骤失败或显式回滚时旧版本仍可运行;
# download_node 只在新运行时完整解压并通过校验后才替换旧目录。
local node_rc=0
download_node "$NODE_VERSION" || node_rc=$?
record_verified_cmd "node-runtime" "$node_rc"
if [ "$node_rc" -ne 0 ]; then
log_error "Node.js 运行时升级失败,OpenClaw 升级终止 (原有安装保持不变)"
print_upgrade_report
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; 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})"
npm_selfcheck || npm_runtime_broken_hint
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 integrity_rc=0
validate_openclaw_tarball "$tarball" "$target_ver" || integrity_rc=1
record_verified_cmd "tarball-integrity" "$integrity_rc"
if [ "$integrity_rc" -ne 0 ]; then
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
# ── 迁移开始阶段: 任何失败必须保留新代码、备份与失败状态,进入 recovery 状态 ──
# 严禁在迁移开始后将旧代码放回已迁移状态,完整回滚必须显式执行
if [ "$state_managed" -eq 1 ]; then
"$state_helper" set-phase migrating "$state_dir" 0 "" "$bk_archive" 2>/dev/null || true
# 0. Doctor 之前修正插件历史写入的不兼容字段 (Discord botToken → token 等);
# 新版 Doctor 会删除未知键,不先迁移就会丢失用户的 Token。解析失败时不写入。
local fix_rc=0
run_config_fixups "$CONFIG_FILE" || fix_rc=$?
record_verified_cmd "config-fixups" "$fix_rc"
# 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 恢复模式 (保留新版本代码、备份与状态,需显式回滚)..."
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 code, state & backup 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"
# 微信插件版本收敛: 核心移除 plugin-sdk 子路径后旧插件的渠道会崩溃循环,
# 而 plugins list 仍显示 loaded,所以按矩阵判断。网络失败不回滚核心,在报告里写明。
local wx_cur wx_plan wx_action wx_pin wx_min wx_new
wx_cur=$(wechat_installed_version)
if [ -n "$wx_cur" ]; then
wx_plan=$("$NODE_BIN" "$OC_PACKAGE_CONTRACT" wechat-plan "$target_ver" "$wx_cur" 2>/dev/null || echo "unknown - -")
wx_action=${wx_plan%% *}
wx_min=${wx_plan##* }
wx_pin=${wx_plan#* }
wx_pin=${wx_pin%% *}
case "$wx_action" in
ok) rep_wechat="ok (${wx_cur})" ;;
upgrade)
log_info "微信插件 ${wx_cur} 低于 OpenClaw ${target_ver} 要求的 ${wx_min},升级到 ${wx_pin}..."
local wx_rc=0
"$state_helper" wechat-install "$state_dir" "$wx_pin" >/tmp/openclaw-wechat-upgrade.log 2>&1 || wx_rc=$?
record_verified_cmd "wechat-plugin" "$wx_rc"
wx_new=$(wechat_installed_version)
if [ "$wx_rc" -eq 0 ] && [ "$wx_new" = "$wx_pin" ]; then
rep_wechat="updated (${wx_cur} -> ${wx_new})"
else
rep_wechat="failed (${wx_new:-unknown}, need ${wx_min}; see /tmp/openclaw-wechat-upgrade.log)"
log_error "微信插件升级失败,微信渠道可能无法运行。可在 LuCI 微信页面重新安装,日志: /tmp/openclaw-wechat-upgrade.log"
fi
;;
*)
rep_wechat="unknown (${wx_cur}, core ${target_ver} not in compat matrix)"
log_warn "OpenClaw ${target_ver} 未经本插件验证,无法确认微信插件 ${wx_cur} 是否兼容"
;;
esac
fi
# 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 code, state & backup 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 providers_js="/usr/share/openclaw/oc-providers.js"
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. 清除认证信息: 凭据保存在上游认证库 (state/openclaw.sqlite),通过官方 logout 逐条注销。
# 旧实现清空的 auth-profiles.json 已在 2026.9.x 退役,实际凭据从未被清除。
# 必须在重置配置之前执行,否则自定义 provider 等上下文已丢失。
if [ -x "$NODE_BIN" ] && [ -f "$providers_js" ]; then
local _pid
for _pid in $(NODE_BIN="$NODE_BIN" NODE_BASE="$NODE_BASE" OC_GLOBAL="$OC_GLOBAL" OC_DATA="$OC_DATA" \
OPENCLAW_STATE_DIR="$config_dir" OPENCLAW_CONFIG_PATH="$config_file" \
"$NODE_BIN" "$providers_js" status 2>/dev/null | awk -F '\t' '$1=="PROFILE" { print $2 }'); do
if NODE_BIN="$NODE_BIN" NODE_BASE="$NODE_BASE" OC_GLOBAL="$OC_GLOBAL" OC_DATA="$OC_DATA" \
OPENCLAW_STATE_DIR="$config_dir" OPENCLAW_CONFIG_PATH="$config_file" \
"$NODE_BIN" "$providers_js" remove-auth "$_pid" 2>/dev/null | grep -q '^OK'; then
log_info "已注销凭据: $_pid"
else
log_warn "凭据注销失败: $_pid"
fi
done
fi
# 4. 重置配置
rm -f "$config_file" "${config_file}.bak" 2>/dev/null || true
echo '{}' > "$config_file"
chown openclaw:openclaw "$config_file" 2>/dev/null || true
# 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 "重新初始化..."
# 参数与 init_openclaw 一致 (历史上的 --tools-profile 不是合法参数,onboard 一直静默失败)
HOME="$OC_DATA" OPENCLAW_HOME="$OC_DATA" OPENCLAW_STATE_DIR="$config_dir" OPENCLAW_CONFIG_PATH="$config_file" \
NODE_ICU_DATA="${NODE_BASE}/share/icu" OPENCLAW_SUPERVISOR_MODE=external OPENCLAW_SERVICE_REPAIR_POLICY=external \
"$NODE_BIN" "$oc_entry" onboard --non-interactive --accept-risk --mode local \
--auth-choice skip --gateway-bind lan --gateway-port 18789 \
--skip-daemon --skip-health --skip-channels --skip-skills --skip-search --skip-hooks >/dev/null 2>&1 \
|| log_warn "onboard 未完成,首次启动服务时将由插件补全基础配置"
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.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
;;
versions)
shift
do_versions "$@"
;;
*)
echo "用法: openclaw-env {setup|setup-offline|check|upgrade|node|versions|factory-reset}"
echo ""
echo " setup — 完整安装 (下载 Node.js + pnpm + OpenClaw)"
echo " setup-offline — 离线安装 (从本地文件安装,由 .run 安装器调用)"
echo " check — 检查环境状态"
echo " upgrade — 升级 OpenClaw 到所选通道的已验证版本"
echo " node — 仅下载/更新 Node.js"
echo " versions — 输出发布通道、上游版本与已验证版本 (JSON,--offline 不联网)"
echo " factory-reset — 恢复出厂设置 (清除所有配置)"
exit 1
;;
esac