mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-04 11:03:56 +08:00
1807 lines
67 KiB
Bash
Executable File
1807 lines
67 KiB
Bash
Executable File
#!/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
|