Files
luci-app-openclaw/scripts/build_offline_run.sh

595 lines
19 KiB
Bash
Executable File

#!/bin/sh
# ============================================================================
# OpenClaw 离线 .run 自解压包构建脚本
# 构建包含所有离线依赖的全架构 .run 安装包
#
# 用法:
# sh scripts/build_offline_run.sh [output_dir]
#
# 前置条件:
# 先运行 sh scripts/download_deps.sh 下载离线依赖到 .offline-cache/
#
# 产出:
# dist/luci-app-openclaw_<ver>_x86_64-musl_offline.run
# dist/luci-app-openclaw_<ver>_aarch64-musl_offline.run
# ============================================================================
set -e
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PKG_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
OUT_DIR="${1:-$PKG_DIR/dist}"
CACHE_DIR="${CACHE_DIR:-$PKG_DIR/.offline-cache}"
case "$OUT_DIR" in
/*) ;;
*) OUT_DIR="$PKG_DIR/$OUT_DIR" ;;
esac
case "$CACHE_DIR" in
/*) ;;
*) CACHE_DIR="$PKG_DIR/$CACHE_DIR" ;;
esac
PKG_NAME="luci-app-openclaw"
PKG_VERSION=$(cat "$PKG_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "1.0.0")
NODE_VERSION="${NODE_VERSION:-22.15.1}"
OC_VERSION=$(cat "$CACHE_DIR/openclaw/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "2026.3.8")
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ 构建 OpenClaw 离线 .run 安装包 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo " 插件版本: v${PKG_VERSION}"
echo " Node.js: v${NODE_VERSION}"
echo " OpenClaw: v${OC_VERSION}"
echo " 缓存目录: ${CACHE_DIR}"
echo " 输出目录: ${OUT_DIR}"
echo ""
# 检查缓存目录
if [ ! -d "$CACHE_DIR/node" ] || [ ! -d "$CACHE_DIR/openclaw" ]; then
echo "错误: 离线缓存不存在,请先运行:"
echo " sh scripts/download_deps.sh"
exit 1
fi
mkdir -p "$OUT_DIR"
# ── 安装 LuCI 插件文件到暂存区 ──
install_luci_files() {
local dest="$1"
mkdir -p "$dest/etc/config"
cp "$PKG_DIR/root/etc/config/openclaw" "$dest/etc/config/openclaw.default"
mkdir -p "$dest/etc/uci-defaults"
cp "$PKG_DIR/root/etc/uci-defaults/99-openclaw" "$dest/etc/uci-defaults/"
chmod +x "$dest/etc/uci-defaults/99-openclaw"
mkdir -p "$dest/etc/init.d"
cp "$PKG_DIR/root/etc/init.d/openclaw" "$dest/etc/init.d/"
chmod +x "$dest/etc/init.d/openclaw"
mkdir -p "$dest/usr/bin"
cp "$PKG_DIR/root/usr/bin/openclaw-env" "$dest/usr/bin/"
chmod +x "$dest/usr/bin/openclaw-env"
mkdir -p "$dest/usr/lib/lua/luci/controller"
cp "$PKG_DIR/luasrc/controller/openclaw.lua" "$dest/usr/lib/lua/luci/controller/"
mkdir -p "$dest/usr/lib/lua/luci/model/cbi/openclaw"
cp "$PKG_DIR/luasrc/model/cbi/openclaw/"*.lua "$dest/usr/lib/lua/luci/model/cbi/openclaw/"
mkdir -p "$dest/usr/lib/lua/luci/view/openclaw"
cp "$PKG_DIR/luasrc/view/openclaw/"*.htm "$dest/usr/lib/lua/luci/view/openclaw/"
mkdir -p "$dest/usr/share/openclaw"
cp "$PKG_DIR/VERSION" "$dest/usr/share/openclaw/VERSION"
cp "$PKG_DIR/root/usr/share/openclaw/oc-config.sh" "$dest/usr/share/openclaw/"
chmod +x "$dest/usr/share/openclaw/oc-config.sh"
cp "$PKG_DIR/root/usr/share/openclaw/web-pty.js" "$dest/usr/share/openclaw/"
cp -r "$PKG_DIR/root/usr/share/openclaw/ui" "$dest/usr/share/openclaw/"
# i18n
mkdir -p "$dest/usr/lib/lua/luci/i18n"
if command -v po2lmo >/dev/null 2>&1 && [ -f "$PKG_DIR/po/zh-cn/openclaw.po" ]; then
po2lmo "$PKG_DIR/po/zh-cn/openclaw.po" "$dest/usr/lib/lua/luci/i18n/openclaw.zh-cn.lmo" 2>/dev/null || true
fi
}
# ── 创建离线安装器脚本 ──
create_offline_installer() {
local target_arch="$1" # 如 x86_64
local target_libc="$2" # 如 musl
local staging="$3"
cat > "$staging/install.sh" << 'INSTALLER_HEADER'
#!/bin/sh
# ============================================================================
# luci-app-openclaw 离线安装器
# 包含所有依赖,无需联网即可完成完整安装
# ============================================================================
set -e
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ luci-app-openclaw — OpenClaw AI Gateway 离线安装器 ║"
echo "║ 包含 Node.js + OpenClaw 运行环境,无需联网 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
# ── 基本检查 ──
if [ ! -f /etc/openwrt_release ]; then
echo "错误: 此安装包仅适用于 OpenWrt/iStoreOS 系统"
exit 1
fi
ARCH=$(uname -m)
TARGET_ARCH="__TARGET_ARCH__"
TARGET_LIBC="__TARGET_LIBC__"
# 架构检查
case "$ARCH" in
x86_64|aarch64) ;;
*) echo "错误: 不支持的架构 $ARCH (仅支持 x86_64/aarch64)"; exit 1 ;;
esac
if [ "$ARCH" != "$TARGET_ARCH" ]; then
echo "错误: 架构不匹配!"
echo " 当前设备: $ARCH"
echo " 安装包: ${TARGET_ARCH}-${TARGET_LIBC}"
echo ""
echo "请下载对应架构的安装包。"
exit 1
fi
# libc 检查
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
}
SYS_LIBC=$(detect_libc)
if [ "$SYS_LIBC" != "$TARGET_LIBC" ]; then
echo "警告: C 库类型不匹配 (系统: $SYS_LIBC, 安装包: $TARGET_LIBC)"
echo " 如果安装后 Node.js 无法运行,请下载对应 libc 类型的安装包。"
echo ""
printf "是否继续?[y/N] "
read -r answer
case "$answer" in
y|Y|yes|YES) ;;
*) echo "已取消"; exit 0 ;;
esac
fi
# ── 磁盘空间预检查 ──
echo "检查磁盘空间..."
# 预估解压后大小: Node.js ~100-200MB + OpenClaw ~200-400MB + 插件 ~1MB
NEED_MB=500
# 检查 /opt 所在分区 (OverlayFS 下可能是 /overlay)
AVAIL_KB=0
for mount_point in /opt /overlay /; do
if df "$mount_point" >/dev/null 2>&1; then
AVAIL_KB=$(df "$mount_point" 2>/dev/null | tail -1 | awk '{print $4}')
break
fi
done
AVAIL_MB=$((AVAIL_KB / 1024))
if [ "$AVAIL_MB" -lt "$NEED_MB" ] 2>/dev/null; then
echo "警告: 可用空间不足!"
echo " 需要: 至少 ${NEED_MB}MB"
echo " 当前: ${AVAIL_MB}MB 可用"
echo ""
printf "是否继续?[y/N] "
read -r answer
case "$answer" in
y|Y|yes|YES) ;;
*) echo "已取消"; exit 0 ;;
esac
fi
# ── OverlayFS 修复 ──
_oc_fix_opt() {
mkdir -p /opt/openclaw/.probe 2>/dev/null && { rmdir /opt/openclaw/.probe 2>/dev/null; return 0; }
if [ -d /overlay/upper/opt ]; then
mkdir -p /overlay/upper/opt/openclaw 2>/dev/null
mount --bind /overlay/upper/opt /opt 2>/dev/null && return 0
fi
return 1
}
_oc_fix_opt || true
NODE_BASE="/opt/openclaw/node"
OC_GLOBAL="/opt/openclaw/global"
OC_DATA="/opt/openclaw/data"
ensure_mkdir() {
local target="$1"
[ -d "$target" ] && return 0
if ! mkdir -p "$target" 2>/dev/null; then
echo " [✗] 无法创建目录: $target"
return 1
fi
}
# ── 解压安装 ──
# 先停止已有服务 (避免文件被占用导致覆盖安装失败)
if [ -x /etc/init.d/openclaw ]; then
echo "停止已有服务..."
/etc/init.d/openclaw stop 2>/dev/null || true
# 等待进程退出和端口释放
sleep 2
# 确保 gateway 子进程也已退出
for pid in $(pgrep -f "node.*openclaw|openclaw.*gateway" 2>/dev/null); do
kill "$pid" 2>/dev/null
done
sleep 1
fi
echo ""
echo "正在提取安装文件..."
# 解压 payload (从 MARKER 行之后)
ARCHIVE=$(awk '/^__ARCHIVE_BELOW__/ {print NR + 1; exit 0; }' "$0")
EXTRACT_DIR=$(mktemp -d)
trap "rm -rf '$EXTRACT_DIR'" EXIT
tail -n +$ARCHIVE "$0" | tar xzf - -C "$EXTRACT_DIR" 2>/dev/null
# ── [Step 1/5] 安装 LuCI 插件文件 ──
echo ""
echo "[1/5] 安装 LuCI 插件..."
# 复制插件文件到系统 (从 luci-files/ 子目录)
if [ -d "$EXTRACT_DIR/luci-files" ]; then
cp -a "$EXTRACT_DIR/luci-files/." / 2>/dev/null
fi
# UCI 配置文件保护
if [ -f /etc/config/openclaw ] && [ -f /etc/config/openclaw.default ]; then
rm -f /etc/config/openclaw.default
elif [ -f /etc/config/openclaw.default ]; then
mv /etc/config/openclaw.default /etc/config/openclaw
fi
echo " [✓] LuCI 插件已安装"
# ── [Step 2/5] 安装 Node.js ──
echo ""
echo "[2/5] 安装 Node.js..."
NODE_TARBALL="$EXTRACT_DIR/node.tar.xz"
if [ -f "$NODE_TARBALL" ]; then
# 清理旧安装
rm -rf "$NODE_BASE" 2>/dev/null
[ -d /overlay/upper ] && rm -rf "/overlay/upper${NODE_BASE}" 2>/dev/null
ensure_mkdir "$NODE_BASE"
# 解压 Node.js (兼容 BusyBox tar)
if tar --strip-components=1 -xf "$NODE_TARBALL" -C "$NODE_BASE" 2>/dev/null; then
: # GNU tar
else
# BusyBox tar 回退
local tmp_node="/tmp/node-extract-$$"
ensure_mkdir "$tmp_node"
tar xf "$NODE_TARBALL" -C "$tmp_node"
local top_dir=$(ls "$tmp_node" 2>/dev/null | head -1)
if [ -n "$top_dir" ] && [ -d "$tmp_node/$top_dir" ]; then
cp -a "$tmp_node/$top_dir/." "$NODE_BASE/"
fi
rm -rf "$tmp_node"
fi
if [ -x "$NODE_BASE/bin/node" ]; then
echo " [✓] Node.js $($NODE_BASE/bin/node --version 2>/dev/null) 已安装"
else
echo " [✗] Node.js 安装失败"
exit 1
fi
else
echo " [✗] 安装包中未找到 Node.js"
exit 1
fi
# ── [Step 3/5] 安装 OpenClaw ──
echo ""
echo "[3/5] 安装 OpenClaw..."
OC_DEPS_TARBALL="$EXTRACT_DIR/openclaw-deps.tar.gz"
if [ -f "$OC_DEPS_TARBALL" ]; then
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_DEPS_TARBALL" -C "$OC_GLOBAL"
# 验证 openclaw 入口
OC_ENTRY=""
for d in "$OC_GLOBAL/lib/node_modules/openclaw" "$OC_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 [ -n "$OC_ENTRY" ]; then
OC_VER=$("$NODE_BASE/bin/node" "$OC_ENTRY" --version 2>/dev/null | tr -d '[:space:]' || echo "unknown")
echo " [✓] OpenClaw v${OC_VER} 已安装"
else
echo " [✗] OpenClaw 安装验证失败"
exit 1
fi
else
echo " [✗] 安装包中未找到 OpenClaw 依赖"
exit 1
fi
# ── [Step 4/5] 初始化 OpenClaw ──
echo ""
echo "[4/5] 初始化 OpenClaw..."
ensure_mkdir "$OC_DATA/.openclaw"
# 创建 openclaw 系统用户 (如果不存在)
if ! id openclaw >/dev/null 2>&1; then
# OpenWrt 使用 BusyBox adduser
if command -v adduser >/dev/null 2>&1; then
adduser -D -H -s /bin/false -h "$OC_DATA" openclaw 2>/dev/null || true
fi
fi
# 运行 onboard
if [ -n "$OC_ENTRY" ] && [ -x "$NODE_BASE/bin/node" ]; then
HOME="$OC_DATA" \
OPENCLAW_HOME="$OC_DATA" \
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
OPENCLAW_CONFIG_PATH="${OC_DATA}/.openclaw/openclaw.json" \
"$NODE_BASE/bin/node" "$OC_ENTRY" onboard --non-interactive --accept-risk --tools-profile coding 2>/dev/null || true
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
echo " [✓] 初始化完成"
# ── [Step 5/5] 注册 opkg + 启动服务 ──
echo ""
echo "[5/5] 注册到系统..."
# 注册到 opkg
PKG="luci-app-openclaw"
PKG_VER="__PKG_VERSION__"
INFO_DIR="/usr/lib/opkg/info"
STATUS_FILE="/usr/lib/opkg/status"
INSTALL_TIME=$(date +%s)
mkdir -p "$INFO_DIR"
cat > "$INFO_DIR/$PKG.control" << CTLEOF
Package: $PKG
Version: $PKG_VER
Depends: luci-compat, luci-base
Section: luci
Architecture: all
Installed-Size: 0
Description: OpenClaw AI Gateway — LuCI 界面 (离线安装)
CTLEOF
cat > "$INFO_DIR/$PKG.list" << LISTEOF
__FILE_LIST__
LISTEOF
cat > "$INFO_DIR/$PKG.prerm" << 'RMEOF'
#!/bin/sh
/etc/init.d/openclaw stop 2>/dev/null
/etc/init.d/openclaw disable 2>/dev/null
exit 0
RMEOF
chmod +x "$INFO_DIR/$PKG.prerm"
# 更新 opkg status
if [ -f "$STATUS_FILE" ]; then
awk -v pkg="$PKG" '
BEGIN { skip=0 }
/^Package:/ { skip=($2==pkg) }
/^$/ { if(skip){skip=0; next} }
!skip { print }
' "$STATUS_FILE" > "${STATUS_FILE}.tmp"
mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
fi
cat >> "$STATUS_FILE" << STEOF
Package: $PKG
Version: $PKG_VER
Depends: luci-compat, luci-base
Status: install user installed
Architecture: all
Conffiles:
/etc/config/openclaw 0
Installed-Time: $INSTALL_TIME
STEOF
echo " [✓] 已注册到 opkg"
# 写入离线安装标记 (供 LuCI 界面识别安装方式)
cat > /usr/share/openclaw/.offline-install << OFFEOF
type=offline
date=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date)
arch=${TARGET_ARCH}-${TARGET_LIBC}
node=$($NODE_BASE/bin/node --version 2>/dev/null || echo unknown)
openclaw=${OC_VER:-unknown}
plugin=__PKG_VERSION__
OFFEOF
echo " [✓] 离线安装标记已写入"
# 执行 uci-defaults
if [ -f /etc/uci-defaults/99-openclaw ]; then
( . /etc/uci-defaults/99-openclaw ) && rm -f /etc/uci-defaults/99-openclaw
fi
# 清除 LuCI 缓存
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null
rm -f /tmp/luci-indexcache.*.json 2>/dev/null
# 启用并启动服务
/etc/init.d/openclaw enable 2>/dev/null || true
uci set openclaw.main.enabled=1 2>/dev/null || true
uci commit openclaw 2>/dev/null || true
# 清理
rm -rf "$EXTRACT_DIR"
trap - EXIT
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ ✅ 离线安装完成! ║"
echo "║ ║"
echo "║ Node.js + OpenClaw + LuCI 插件已全部安装 ║"
echo "║ 无需再运行 openclaw-env setup ║"
echo "║ ║"
echo "║ 下一步: ║"
echo "║ 访问 LuCI → 服务 → OpenClaw 进行配置 ║"
echo "║ 或执行: /etc/init.d/openclaw start ║"
echo "║ ║"
echo "║ 配置模型 API 密钥后即可使用,全程无需联网! ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
exit 0
__ARCHIVE_BELOW__
INSTALLER_HEADER
}
# ============================================================================
# 为每种架构构建 .run
# ============================================================================
build_one_variant() {
local label="$1" # 如 x86_64-musl
local uname_arch="$2" # 如 x86_64
local node_suffix="$3" # 如 linux-x64-musl
local libc="$4" # 如 musl
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 构建: ${label}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
local node_tarball="$CACHE_DIR/node/node-v${NODE_VERSION}-${node_suffix}.tar.xz"
if [ ! -f "$node_tarball" ]; then
echo " [!] 跳过: 未找到 Node.js 包 $(basename "$node_tarball")"
return 1
fi
# 查找 OpenClaw 依赖包
local oc_deps=""
for f in "$CACHE_DIR/openclaw/openclaw-deps-"*.tar.gz; do
[ -f "$f" ] && oc_deps="$f" && break
done
if [ -z "$oc_deps" ]; then
echo " [!] 跳过: 未找到 OpenClaw 依赖包"
return 1
fi
# 创建临时暂存区
local staging=$(mktemp -d)
# [1] 准备 payload 结构
local payload="$staging/payload"
mkdir -p "$payload/luci-files"
echo " [1/5] 安装 LuCI 插件文件..."
install_luci_files "$payload/luci-files"
echo " [2/5] 复制 Node.js 包..."
cp "$node_tarball" "$payload/node.tar.xz"
echo " [3/5] 复制 OpenClaw 依赖包..."
cp "$oc_deps" "$payload/openclaw-deps.tar.gz"
# [2] 生成文件列表
echo " [4/5] 生成安装器..."
local file_list=$(cd "$payload/luci-files" && find . -type f | sed 's|^\./|/|' | sed 's|/etc/config/openclaw.default|/etc/config/openclaw|' | sort)
# 创建安装器
create_offline_installer "$uname_arch" "$libc" "$staging"
# 替换占位符
sed -i "s|__TARGET_ARCH__|${uname_arch}|g" "$staging/install.sh"
sed -i "s|__TARGET_LIBC__|${libc}|g" "$staging/install.sh"
sed -i "s|__PKG_VERSION__|${PKG_VERSION}|g" "$staging/install.sh"
# 替换文件列表
{
sed '/__FILE_LIST__/,$d' "$staging/install.sh"
echo "$file_list"
sed '1,/__FILE_LIST__/d' "$staging/install.sh"
} > "$staging/install_final.sh"
mv "$staging/install_final.sh" "$staging/install.sh"
# [3] 打包 payload (gzip 压缩, 减小 .run 文件体积)
echo " [5/5] 打包..."
(cd "$payload" && tar czf "$staging/payload.tar.gz" .)
# [4] 组合: installer + payload
local run_file="$OUT_DIR/${PKG_NAME}_${PKG_VERSION}_${label}_offline.run"
cat "$staging/install.sh" "$staging/payload.tar.gz" > "$run_file"
chmod +x "$run_file"
local file_size=$(wc -c < "$run_file" | tr -d ' ')
local file_size_mb=$((file_size / 1024 / 1024))
echo " [✓] ${run_file}"
echo " 大小: ${file_size_mb}MB (${file_size} bytes)"
# 生成 SHA256
sha256sum "$run_file" > "${run_file}.sha256" 2>/dev/null || true
rm -rf "$staging"
return 0
}
# ── 主构建流程 ──
SUCCESS=0
FAILED=0
# 使用 for 循环避免 BusyBox ash 的 IFS/read 管道问题
for variant in \
"x86_64-musl:x86_64:linux-x64-musl:musl" \
"aarch64-musl:aarch64:linux-arm64-musl:musl" \
; do
label=$(echo "$variant" | cut -d: -f1)
uname_arch=$(echo "$variant" | cut -d: -f2)
node_suffix=$(echo "$variant" | cut -d: -f3)
libc=$(echo "$variant" | cut -d: -f4)
if build_one_variant "$label" "$uname_arch" "$node_suffix" "$libc"; then
SUCCESS=$((SUCCESS + 1))
else
FAILED=$((FAILED + 1))
fi
done
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ 构建完成 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo "输出目录: $OUT_DIR"
echo ""
ls -lh "$OUT_DIR/"*_offline.run 2>/dev/null || echo "(无输出文件)"
echo ""
echo "安装方法: 将 .run 文件传输到路由器后执行:"
echo " sh luci-app-openclaw_*_offline.run"