mirror of
https://github.com/hotwa/luci-app-openclaw.git
synced 2026-03-30 20:25:44 +00:00
305 lines
12 KiB
Bash
Executable File
305 lines
12 KiB
Bash
Executable File
#!/bin/sh
|
||
# ============================================================================
|
||
# 离线依赖预下载脚本 (在有网络的构建机上运行)
|
||
# 为所有支持的架构下载 Node.js + OpenClaw + pnpm
|
||
#
|
||
# 用法:
|
||
# sh scripts/download_deps.sh [cache_dir]
|
||
#
|
||
# 产出目录结构:
|
||
# cache_dir/
|
||
# node/
|
||
# node-v22.15.1-linux-x64-musl.tar.xz
|
||
# node-v22.15.1-linux-arm64-musl.tar.xz
|
||
# openclaw/
|
||
# openclaw-deps-v<version>.tar.gz (完整 node_modules, 跨架构通用, ~150MB)
|
||
# ============================================================================
|
||
set -e
|
||
|
||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||
PKG_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
|
||
CACHE_DIR="${1:-$PKG_DIR/.offline-cache}"
|
||
|
||
# 确保 CACHE_DIR 是绝对路径
|
||
case "$CACHE_DIR" in
|
||
/*) ;;
|
||
*) CACHE_DIR="$PKG_DIR/$CACHE_DIR" ;;
|
||
esac
|
||
|
||
# ── 版本配置 (与 openclaw-env 保持一致) ──
|
||
NODE_VERSION="${NODE_VERSION:-22.15.1}"
|
||
OC_VERSION="${OC_VERSION:-2026.3.8}"
|
||
|
||
# ── 下载镜像 ──
|
||
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"
|
||
NODE_SELF_HOST="https://github.com/10000ge10000/luci-app-openclaw/releases/download/node-bins"
|
||
NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org}"
|
||
|
||
log_info() { echo " [✓] $1"; }
|
||
log_warn() { echo " [!] $1"; }
|
||
log_error() { echo " [✗] $1"; }
|
||
|
||
# 自动检测 Node.js / npm (兼容 OpenWrt 上已安装的 openclaw 环境)
|
||
if ! command -v node >/dev/null 2>&1; then
|
||
for try_path in /opt/openclaw/node/bin /usr/local/bin; do
|
||
if [ -x "$try_path/node" ]; then
|
||
export PATH="$try_path:$PATH"
|
||
log_info "检测到 Node.js: $try_path/node"
|
||
break
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# 下载文件 (支持 curl 和 wget)
|
||
download_file() {
|
||
local url="$1" dest="$2"
|
||
if [ -f "$dest" ]; then
|
||
local fsize=$(wc -c < "$dest" 2>/dev/null || echo 0)
|
||
if [ "$fsize" -gt 1000000 ] 2>/dev/null; then
|
||
log_info "已缓存: $(basename "$dest") ($(du -h "$dest" | cut -f1))"
|
||
return 0
|
||
fi
|
||
fi
|
||
echo " 下载: $url"
|
||
if curl -fSL --connect-timeout 30 --max-time 600 -o "$dest" "$url" 2>/dev/null; then
|
||
return 0
|
||
elif wget -q --timeout=30 -O "$dest" "$url" 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
rm -f "$dest"
|
||
return 1
|
||
}
|
||
|
||
# ============================================================================
|
||
# Phase 1: 下载 Node.js (全架构)
|
||
# ============================================================================
|
||
download_all_node() {
|
||
echo ""
|
||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||
printf "║ [1/3] 下载 Node.js v%-8s (musl 架构) ║\n" "$NODE_VERSION"
|
||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||
echo ""
|
||
|
||
local node_dir="$CACHE_DIR/node"
|
||
mkdir -p "$node_dir"
|
||
|
||
# x86_64 musl
|
||
echo "=== x86_64 musl ==="
|
||
local x64_musl="node-v${NODE_VERSION}-linux-x64-musl.tar.xz"
|
||
download_file "${NODE_MUSL_MIRROR}/v${NODE_VERSION}/${x64_musl}" "$node_dir/$x64_musl" || \
|
||
download_file "${NODE_MIRROR_CN}/v${NODE_VERSION}/${x64_musl}" "$node_dir/$x64_musl" || \
|
||
log_error "x86_64 musl 下载失败"
|
||
|
||
# aarch64 musl (项目自托管)
|
||
echo "=== aarch64 musl ==="
|
||
local arm64_musl="node-v${NODE_VERSION}-linux-arm64-musl.tar.xz"
|
||
download_file "${NODE_SELF_HOST}/${arm64_musl}" "$node_dir/$arm64_musl" || \
|
||
download_file "${NODE_MUSL_MIRROR}/v${NODE_VERSION}/${arm64_musl}" "$node_dir/$arm64_musl" || \
|
||
log_error "aarch64 musl 下载失败"
|
||
|
||
echo ""
|
||
echo "Node.js 下载完成:"
|
||
ls -lh "$node_dir/"*.tar.xz 2>/dev/null || echo " (无文件)"
|
||
}
|
||
|
||
# ============================================================================
|
||
# Phase 2: 下载并预装 OpenClaw + 依赖
|
||
# ============================================================================
|
||
download_openclaw_deps() {
|
||
echo ""
|
||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||
printf "║ [2/3] 下载 OpenClaw v%-8s + 全部依赖 ║\n" "$OC_VERSION"
|
||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||
echo ""
|
||
|
||
local oc_dir="$CACHE_DIR/openclaw"
|
||
mkdir -p "$oc_dir"
|
||
|
||
# 检查 npm 是否可用 (构建机上需要 node + npm)
|
||
local NPM_CMD=""
|
||
if command -v npm >/dev/null 2>&1; then
|
||
NPM_CMD="npm"
|
||
elif [ -x /opt/openclaw/node/bin/npm ]; then
|
||
# OpenWrt 上 npm wrapper 可能需要显式 node 调用
|
||
NPM_CMD="/opt/openclaw/node/bin/node /opt/openclaw/node/bin/npm"
|
||
else
|
||
log_error "构建机上需要 npm"
|
||
log_error "请执行: apt install -y nodejs npm 或 apk add nodejs npm"
|
||
log_error "或者确保 /opt/openclaw/node 中有 Node.js"
|
||
exit 1
|
||
fi
|
||
log_info "使用 npm: $NPM_CMD"
|
||
|
||
# 方案: 使用 npm install 到临时目录,然后打包整个 node_modules
|
||
# 这是最可靠的方式,确保所有依赖树完整
|
||
|
||
local tmp_install="/tmp/openclaw-offline-$$"
|
||
trap "rm -rf '$tmp_install'" EXIT
|
||
|
||
# ── 为每种架构生成预安装包 ──
|
||
# 注意: openclaw 的依赖树中可能包含平台特定的 optional dependencies
|
||
# musl 环境下用 --ignore-scripts 跳过原生编译
|
||
# 对于离线包,我们在构建机上安装后直接打包 node_modules
|
||
|
||
# 通用安装 (忽略平台特定编译脚本)
|
||
echo "=== 安装 OpenClaw 依赖 (通用包) ==="
|
||
mkdir -p "$tmp_install/global"
|
||
|
||
# ── 强制 npm 使用 musl 平台检测 ──
|
||
# 目标系统是 OpenWrt/iStoreOS (musl libc),无论构建机是什么系统
|
||
# 在 glibc 系统 (如 GitHub Actions ubuntu-latest) 上,npm 默认安装 *-gnu 变体
|
||
# 的原生可选依赖 (如 @napi-rs/canvas-linux-x64-gnu),比 *-musl 变体大得多
|
||
# 通过设置 npm_config_libc=musl 强制安装 musl 变体,确保跨平台一致的产物大小
|
||
export npm_config_os=linux
|
||
export npm_config_libc=musl
|
||
|
||
echo " 正在用 npm 安装 openclaw@${OC_VERSION}..."
|
||
echo " npm 平台覆盖: os=${npm_config_os}, libc=${npm_config_libc}"
|
||
$NPM_CMD install -g "openclaw@${OC_VERSION}" \
|
||
--prefix="$tmp_install/global" \
|
||
--ignore-scripts \
|
||
--omit=dev \
|
||
--omit=optional \
|
||
--no-optional \
|
||
--registry="$NPM_REGISTRY" 2>&1 | tail -20
|
||
|
||
# 验证安装
|
||
local oc_entry=""
|
||
for d in "$tmp_install/global/lib/node_modules/openclaw" "$tmp_install/global/node_modules/openclaw"; do
|
||
if [ -f "${d}/openclaw.mjs" ]; then
|
||
oc_entry="${d}/openclaw.mjs"
|
||
break
|
||
elif [ -f "${d}/dist/cli.js" ]; then
|
||
oc_entry="${d}/dist/cli.js"
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [ -z "$oc_entry" ]; then
|
||
log_error "OpenClaw 安装验证失败"
|
||
echo "目录内容:"
|
||
find "$tmp_install/global" -maxdepth 4 -type d 2>/dev/null | head -30
|
||
exit 1
|
||
fi
|
||
|
||
log_info "OpenClaw 安装验证通过: $oc_entry"
|
||
|
||
# 获取实际安装的版本号
|
||
local actual_ver=""
|
||
local oc_pkg_dir="$(dirname "$oc_entry")"
|
||
if [ -f "$oc_pkg_dir/package.json" ]; then
|
||
actual_ver=$(node -e "console.log(require('$oc_pkg_dir/package.json').version)" 2>/dev/null || echo "$OC_VERSION")
|
||
fi
|
||
echo " 实际版本: v${actual_ver:-$OC_VERSION}"
|
||
|
||
# ── 打包为通用 tarball ──
|
||
# 不精简 node_modules,保留全部功能(飞书/Slack/Discord 等平台 SDK)
|
||
# 依靠 gzip 压缩控制包大小: ~670MB → ~150MB
|
||
# 因为 openclaw 是纯 JS 包 (使用 --ignore-scripts),node_modules 跨架构通用
|
||
echo ""
|
||
echo "=== 打包 OpenClaw 依赖 ==="
|
||
local install_size=$(du -sm "$tmp_install/global" 2>/dev/null | awk '{print $1}')
|
||
local tarball="$oc_dir/openclaw-deps-v${actual_ver:-$OC_VERSION}.tar.gz"
|
||
echo " 正在压缩 ${install_size}MB 数据到 tar.gz (可能需要数分钟)..."
|
||
# 注意: 不在子 shell 中用 set -e, 避免 tar 在处理损坏的符号链接时意外退出
|
||
# --warning=no-file-changed: 忽略打包过程中文件被修改的警告
|
||
if ! tar czf "$tarball" -C "$tmp_install/global" . 2>&1; then
|
||
log_error "tar 打包失败"
|
||
rm -f "$tarball"
|
||
exit 1
|
||
fi
|
||
# 验证 gzip 完整性
|
||
if ! gzip -t "$tarball" 2>/dev/null; then
|
||
log_error "tar.gz 文件完整性检查失败!"
|
||
rm -f "$tarball"
|
||
exit 1
|
||
fi
|
||
local tgz_size=$(du -h "$tarball" | cut -f1)
|
||
log_info "依赖包: $tarball ($tgz_size) [完整性已验证]"
|
||
|
||
# 保存版本号
|
||
echo "${actual_ver:-$OC_VERSION}" > "$oc_dir/VERSION"
|
||
|
||
rm -rf "$tmp_install"
|
||
# 清除之前的 trap
|
||
trap - EXIT
|
||
}
|
||
|
||
# ============================================================================
|
||
# Phase 3: 生成清单
|
||
# ============================================================================
|
||
generate_manifest() {
|
||
echo ""
|
||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||
echo "║ [3/3] 生成构建清单 ║"
|
||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||
echo ""
|
||
|
||
local manifest="$CACHE_DIR/manifest.txt"
|
||
local oc_ver=$(cat "$CACHE_DIR/openclaw/VERSION" 2>/dev/null || echo "$OC_VERSION")
|
||
|
||
cat > "$manifest" << EOF
|
||
# OpenClaw Offline Bundle - 依赖清单
|
||
# 生成时间: $(date -Iseconds 2>/dev/null || date)
|
||
# Node.js: v${NODE_VERSION}
|
||
# OpenClaw: v${oc_ver}
|
||
|
||
[node]
|
||
EOF
|
||
|
||
# 列出 Node.js 包
|
||
for f in "$CACHE_DIR/node/"*.tar.xz; do
|
||
[ -f "$f" ] || continue
|
||
local fname=$(basename "$f")
|
||
local fsize=$(du -h "$f" | cut -f1)
|
||
local sha256=$(sha256sum "$f" 2>/dev/null | awk '{print $1}' || echo "N/A")
|
||
echo "${fname} size=${fsize} sha256=${sha256}" >> "$manifest"
|
||
done
|
||
|
||
echo "" >> "$manifest"
|
||
echo "[openclaw]" >> "$manifest"
|
||
|
||
# 列出 OpenClaw 包
|
||
for f in "$CACHE_DIR/openclaw/"*.tar.gz; do
|
||
[ -f "$f" ] || continue
|
||
local fname=$(basename "$f")
|
||
local fsize=$(du -h "$f" | cut -f1)
|
||
local sha256=$(sha256sum "$f" 2>/dev/null | awk '{print $1}' || echo "N/A")
|
||
echo "${fname} size=${fsize} sha256=${sha256}" >> "$manifest"
|
||
done
|
||
|
||
echo ""
|
||
echo "=== 依赖清单 ==="
|
||
cat "$manifest"
|
||
echo ""
|
||
|
||
# 统计总大小
|
||
local total_size=$(du -sh "$CACHE_DIR" 2>/dev/null | awk '{print $1}')
|
||
echo "缓存目录: $CACHE_DIR"
|
||
echo "总大小: $total_size"
|
||
}
|
||
|
||
# ── 主入口 ──
|
||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||
echo "║ OpenClaw 离线依赖下载器 ║"
|
||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||
echo ""
|
||
echo " Node.js: v${NODE_VERSION}"
|
||
echo " OpenClaw: v${OC_VERSION}"
|
||
echo " 缓存目录: ${CACHE_DIR}"
|
||
echo " npm 源: ${NPM_REGISTRY}"
|
||
echo ""
|
||
|
||
mkdir -p "$CACHE_DIR"
|
||
|
||
download_all_node
|
||
download_openclaw_deps
|
||
generate_manifest
|
||
|
||
echo ""
|
||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||
echo "║ ✅ 依赖下载完成!现在可以运行 build_offline_run.sh ║"
|
||
echo "╚══════════════════════════════════════════════════════════════╝"
|