✨ Sync 2026-03-14 19:52:12

This commit is contained in:
github-actions[bot] committed 2026-03-14 19:52:12 +08:00
1 parent aa7946ff93
commit 80bd1ecded
43 files changed
+7870 -35

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
#!/bin/sh
# 在 Alpine ARM64 Docker 容器内运行
# 环境变量: NODE_VER (目标版本号), /output (输出目录)
#
# 打包策略:
# 使用 patchelf 修改 node 二进制的 ELF interpreter 和 rpath,
# 使其直接使用打包的 musl 链接器和共享库,无需 LD_LIBRARY_PATH。
# 这样 process.execPath 返回正确的 node 路径,子进程 fork 也能正常工作。
# 安装路径固定为 /opt/openclaw/node (与 openclaw-env 一致)。
set -e
INSTALL_PREFIX="/opt/openclaw/node"
apk add --no-cache nodejs npm xz icu-data-full patchelf
ACTUAL_VER=$(node --version | sed 's/^v//')
echo "Alpine Node.js version: v${ACTUAL_VER} (requested: v${NODE_VER})"
# 使用实际版本号作为文件名 (Alpine apk 的 nodejs 版本可能与请求版本不同)
if [ "$ACTUAL_VER" != "$NODE_VER" ]; then
echo "WARNING: Actual version (${ACTUAL_VER}) differs from requested (${NODE_VER})"
echo " Using actual version for package name"
fi
PKG_NAME="node-v${ACTUAL_VER}-linux-arm64-musl"
PKG_DIR="/tmp/${PKG_NAME}"
mkdir -p "${PKG_DIR}/bin" "${PKG_DIR}/lib/node_modules" "${PKG_DIR}/include/node"
# 复制 node 二进制
cp "$(which node)" "${PKG_DIR}/bin/node"
chmod +x "${PKG_DIR}/bin/node"
# 收集 node 依赖的所有共享库 (Alpine node 是动态链接的)
echo "=== Collecting shared libraries ==="
LIB_DIR="${PKG_DIR}/lib"
ldd "$(which node)" 2>/dev/null | while read -r line; do
# 解析 ldd 输出: libxxx.so => /usr/lib/libxxx.so (0x...)
lib_path=$(echo "$line" | grep -oE '/[^ ]+\.so[^ ]*' | head -1)
if [ -n "$lib_path" ] && [ -f "$lib_path" ]; then
cp -L "$lib_path" "$LIB_DIR/" 2>/dev/null || true
echo " + $(basename "$lib_path")"
fi
done
# 确保 musl 动态链接器也在
if [ -f /lib/ld-musl-aarch64.so.1 ]; then
cp -L /lib/ld-musl-aarch64.so.1 "$LIB_DIR/" 2>/dev/null || true
echo " + ld-musl-aarch64.so.1"
fi
echo "Libraries collected: $(ls "$LIB_DIR"/*.so* 2>/dev/null | wc -l) files"
# 用 patchelf 修改 node 二进制:
# - interpreter 指向打包的 musl 链接器 (绝对路径,对应安装后的位置)
# - rpath 指向打包的 lib 目录
echo "=== Patching ELF binary ==="
patchelf --set-interpreter "${INSTALL_PREFIX}/lib/ld-musl-aarch64.so.1" "${PKG_DIR}/bin/node"
patchelf --set-rpath "${INSTALL_PREFIX}/lib" "${PKG_DIR}/bin/node"
echo " interpreter: ${INSTALL_PREFIX}/lib/ld-musl-aarch64.so.1"
echo " rpath: ${INSTALL_PREFIX}/lib"
# 复制 ICU 完整数据 (npm 的 Intl.Collator 需要)
echo "=== Copying ICU data ==="
ICU_DAT=$(find /usr/share/icu -name "icudt*.dat" 2>/dev/null | head -1)
if [ -n "$ICU_DAT" ] && [ -f "$ICU_DAT" ]; then
mkdir -p "${PKG_DIR}/share/icu"
cp "$ICU_DAT" "${PKG_DIR}/share/icu/"
echo " + $(basename "$ICU_DAT") ($(du -h "$ICU_DAT" | cut -f1))"
else
echo " WARNING: ICU data file not found"
fi
# 创建 node wrapper 脚本 (只设置 NODE_ICU_DATA,ELF 层面已解决链接器和库路径)
cat > "${PKG_DIR}/bin/node-wrapper" << 'NODEWRAPPER'
#!/bin/sh
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
export NODE_ICU_DATA="${SELF_DIR}/../share/icu"
exec "${SELF_DIR}/node" "$@"
NODEWRAPPER
chmod +x "${PKG_DIR}/bin/node-wrapper"
# 复制 npm
if [ -d /usr/lib/node_modules/npm ]; then
cp -r /usr/lib/node_modules/npm "${PKG_DIR}/lib/node_modules/"
fi
# 创建 npm wrapper (直接调用 patchelf 后的 node,只需设置 ICU)
cat > "${PKG_DIR}/bin/npm" << 'NPMWRAPPER'
#!/bin/sh
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
export NODE_ICU_DATA="${SELF_DIR}/../share/icu"
exec "${SELF_DIR}/node" "${SELF_DIR}/../lib/node_modules/npm/bin/npm-cli.js" "$@"
NPMWRAPPER
# 创建 npx wrapper
cat > "${PKG_DIR}/bin/npx" << 'NPXWRAPPER'
#!/bin/sh
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"
export NODE_ICU_DATA="${SELF_DIR}/../share/icu"
exec "${SELF_DIR}/node" "${SELF_DIR}/../lib/node_modules/npm/bin/npx-cli.js" "$@"
NPXWRAPPER
chmod +x "${PKG_DIR}/bin/npm" "${PKG_DIR}/bin/npx"
# 验证 (需要将打包内容放到目标路径来测试 patchelf 结果)
echo "=== Verification ==="
mkdir -p "${INSTALL_PREFIX}"
cp -a "${PKG_DIR}"/* "${INSTALL_PREFIX}/"
"${INSTALL_PREFIX}/bin/node" --version
"${INSTALL_PREFIX}/bin/node" -e "console.log('execPath:', process.execPath)"
"${INSTALL_PREFIX}/bin/node" -e "console.log(process.arch, process.platform, process.versions.modules)"
NODE_ICU_DATA="${INSTALL_PREFIX}/share/icu" "${INSTALL_PREFIX}/bin/npm" --version 2>/dev/null || echo "npm needs ICU data"
rm -rf "${INSTALL_PREFIX}"
# 打包
cd /tmp
tar cJf "/output/${PKG_NAME}.tar.xz" "${PKG_NAME}"
ls -lh "/output/${PKG_NAME}.tar.xz"
echo "=== Done: ${PKG_NAME}.tar.xz ==="
+147
View File
@@ -0,0 +1,147 @@
#!/bin/sh
# ============================================================================
# 本地构建 .ipk 包 (无需 OpenWrt SDK)
# 用法: sh scripts/build_ipk.sh [output_dir]
# ============================================================================
set -e
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PKG_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
OUT_DIR="${1:-$PKG_DIR/dist}"
# 确保 OUT_DIR 是绝对路径
case "$OUT_DIR" in
/*) ;;
*) OUT_DIR="$PKG_DIR/$OUT_DIR" ;;
esac
mkdir -p "$OUT_DIR"
PKG_NAME="luci-app-openclaw"
PKG_VERSION=$(cat "$PKG_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "1.0.0")
PKG_RELEASE="1"
echo "=== 构建 ${PKG_NAME} .ipk 包 ==="
STAGING=$(mktemp -d)
trap "rm -rf '$STAGING'" EXIT
# ── 构建 data.tar.gz ──
DATA_DIR="$STAGING/data"
mkdir -p "$DATA_DIR"
# UCI config
mkdir -p "$DATA_DIR/etc/config"
cp "$PKG_DIR/root/etc/config/openclaw" "$DATA_DIR/etc/config/"
# UCI defaults
mkdir -p "$DATA_DIR/etc/uci-defaults"
cp "$PKG_DIR/root/etc/uci-defaults/99-openclaw" "$DATA_DIR/etc/uci-defaults/"
chmod +x "$DATA_DIR/etc/uci-defaults/99-openclaw"
# init.d
mkdir -p "$DATA_DIR/etc/init.d"
cp "$PKG_DIR/root/etc/init.d/openclaw" "$DATA_DIR/etc/init.d/"
chmod +x "$DATA_DIR/etc/init.d/openclaw"
# bin
mkdir -p "$DATA_DIR/usr/bin"
cp "$PKG_DIR/root/usr/bin/openclaw-env" "$DATA_DIR/usr/bin/"
chmod +x "$DATA_DIR/usr/bin/openclaw-env"
# LuCI controller
mkdir -p "$DATA_DIR/usr/lib/lua/luci/controller"
cp "$PKG_DIR/luasrc/controller/openclaw.lua" "$DATA_DIR/usr/lib/lua/luci/controller/"
# LuCI CBI
mkdir -p "$DATA_DIR/usr/lib/lua/luci/model/cbi/openclaw"
cp "$PKG_DIR/luasrc/model/cbi/openclaw/"*.lua "$DATA_DIR/usr/lib/lua/luci/model/cbi/openclaw/"
# LuCI views
mkdir -p "$DATA_DIR/usr/lib/lua/luci/view/openclaw"
cp "$PKG_DIR/luasrc/view/openclaw/"*.htm "$DATA_DIR/usr/lib/lua/luci/view/openclaw/"
# oc-config assets
mkdir -p "$DATA_DIR/usr/share/openclaw"
cp "$PKG_DIR/VERSION" "$DATA_DIR/usr/share/openclaw/VERSION"
cp "$PKG_DIR/root/usr/share/openclaw/oc-config.sh" "$DATA_DIR/usr/share/openclaw/"
chmod +x "$DATA_DIR/usr/share/openclaw/oc-config.sh"
cp "$PKG_DIR/root/usr/share/openclaw/web-pty.js" "$DATA_DIR/usr/share/openclaw/"
# Web PTY UI
cp -r "$PKG_DIR/root/usr/share/openclaw/ui" "$DATA_DIR/usr/share/openclaw/"
# i18n (po2lmo 可选)
mkdir -p "$DATA_DIR/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" "$DATA_DIR/usr/lib/lua/luci/i18n/openclaw.zh-cn.lmo" 2>/dev/null || true
fi
# 计算安装大小
INSTALLED_SIZE=$(du -sk "$DATA_DIR" | awk '{print $1}')
(cd "$DATA_DIR" && tar czf "$STAGING/data.tar.gz" .)
# ── 构建 control.tar.gz ──
CTRL_DIR="$STAGING/control"
mkdir -p "$CTRL_DIR"
cat > "$CTRL_DIR/control" << EOF
Package: ${PKG_NAME}
Version: ${PKG_VERSION}-${PKG_RELEASE}
Depends: luci-compat, luci-base, curl, openssl-util, script-utils, tar
Source: https://github.com/10000ge10000/luci-app-openclaw
SourceName: ${PKG_NAME}
License: GPL-3.0
Section: luci
SourceDateEpoch: $(date +%s)
Maintainer: 10000ge10000 <10000ge10000@users.noreply.github.com>
Architecture: all
Installed-Size: ${INSTALLED_SIZE}
Description: OpenClaw AI 网关 LuCI 管理插件
EOF
cat > "$CTRL_DIR/postinst" << 'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT}" ] || {
( . /etc/uci-defaults/99-openclaw ) && rm -f /etc/uci-defaults/99-openclaw
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* /tmp/luci-indexcache.*.json 2>/dev/null
# 重启 Web PTY (使其加载新文件和新 token)
PTY_PID=$(pgrep -f 'web-pty.js' 2>/dev/null | head -1)
[ -n "$PTY_PID" ] && kill "$PTY_PID" 2>/dev/null || true
exit 0
}
EOF
chmod +x "$CTRL_DIR/postinst"
cat > "$CTRL_DIR/postrm" << 'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT}" ] || {
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null
}
EOF
chmod +x "$CTRL_DIR/postrm"
cat > "$CTRL_DIR/conffiles" << 'EOF'
/etc/config/openclaw
EOF
(cd "$CTRL_DIR" && tar czf "$STAGING/control.tar.gz" .)
# ── 组装 .ipk (ar 格式) ──
mkdir -p "$OUT_DIR"
IPK_FILE="$OUT_DIR/${PKG_NAME}_${PKG_VERSION}-${PKG_RELEASE}_all.ipk"
echo "2.0" > "$STAGING/debian-binary"
# 清理旧文件
rm -f "$IPK_FILE"
# 组装 .ipk — OpenWrt opkg 使用 tar.gz 格式 (非 Debian 的 ar 格式)
(cd "$STAGING" && tar czf "$IPK_FILE" debian-binary control.tar.gz data.tar.gz)
IPK_SIZE=$(wc -c < "$IPK_FILE" | tr -d ' ')
echo ""
echo "=== 构建完成 ==="
echo "输出文件: $IPK_FILE"
echo "文件大小: ${IPK_SIZE} bytes"
echo "安装大小: ${INSTALLED_SIZE} KB"
echo ""
echo "安装方法: opkg install ${PKG_NAME}_${PKG_VERSION}-${PKG_RELEASE}_all.ipk"
+257
View File
@@ -0,0 +1,257 @@
#!/bin/sh
# ============================================================================
# iStoreOS .run 自解压包构建脚本
# 用法: sh scripts/build_run.sh [output_dir]
# ============================================================================
set -e
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PKG_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
OUT_DIR="${1:-$PKG_DIR/dist}"
# 确保 OUT_DIR 是绝对路径
case "$OUT_DIR" in
/*) ;;
*) OUT_DIR="$PKG_DIR/$OUT_DIR" ;;
esac
mkdir -p "$OUT_DIR"
PKG_NAME="luci-app-openclaw"
PKG_VERSION=$(cat "$PKG_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "1.0.0")
echo "=== 构建 iStoreOS .run 安装包 ==="
echo "源目录: $PKG_DIR"
echo "输出到: $OUT_DIR"
# 创建临时打包目录
STAGING=$(mktemp -d)
trap "rm -rf '$STAGING'" EXIT
# 安装文件到暂存区
install_files() {
local dest="$1"
# UCI config (仅首次安装时部署默认配置; 升级时保留用户配置)
# 安装器会在解压后检测并跳过已有配置, 见 install.sh 中的逻辑
mkdir -p "$dest/etc/config"
cp "$PKG_DIR/root/etc/config/openclaw" "$dest/etc/config/openclaw.default"
# UCI defaults
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"
# init.d
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"
# bin
mkdir -p "$dest/usr/bin"
cp "$PKG_DIR/root/usr/bin/openclaw-env" "$dest/usr/bin/"
chmod +x "$dest/usr/bin/openclaw-env"
# LuCI controller
mkdir -p "$dest/usr/lib/lua/luci/controller"
cp "$PKG_DIR/luasrc/controller/openclaw.lua" "$dest/usr/lib/lua/luci/controller/"
# LuCI CBI
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/"
# LuCI views
mkdir -p "$dest/usr/lib/lua/luci/view/openclaw"
cp "$PKG_DIR/luasrc/view/openclaw/"*.htm "$dest/usr/lib/lua/luci/view/openclaw/"
# oc-config assets
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/"
# Web PTY UI (recursive copy)
cp -r "$PKG_DIR/root/usr/share/openclaw/ui" "$dest/usr/share/openclaw/"
}
# 创建安装器脚本头部
create_installer() {
cat > "$STAGING/install.sh" << 'INSTALLER_EOF'
#!/bin/sh
# luci-app-openclaw iStoreOS 安装器
set -e
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ luci-app-openclaw — OpenClaw AI Gateway 插件 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
# 检查系统
if [ ! -f /etc/openwrt_release ]; then
echo "错误: 此安装包仅适用于 OpenWrt/iStoreOS 系统"
exit 1
fi
# 检查架构
ARCH=$(uname -m)
case "$ARCH" in
x86_64|aarch64) ;;
*) echo "错误: 不支持的架构 $ARCH (仅支持 x86_64/aarch64)"; exit 1 ;;
esac
# 检查依赖
for dep in luci-compat luci-base; do
if ! opkg list-installed 2>/dev/null | grep -q "^${dep} "; then
echo "警告: 缺少依赖 $dep,尝试安装..."
opkg update >/dev/null 2>&1 || true
opkg install "$dep" 2>/dev/null || echo " 安装 $dep 失败,请手动安装"
fi
done
echo "正在安装文件..."
# 解压 payload (从 MARKER 行之后)
ARCHIVE=$(awk '/^__ARCHIVE_BELOW__/ {print NR + 1; exit 0; }' "$0")
tail -n +$ARCHIVE "$0" | tar xzf - -C / 2>/dev/null
# 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
# 注册到 opkg,使 iStore 和 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"
# 写入 control 文件
cat > "$INFO_DIR/$PKG.control" << CTLEOF
Package: $PKG
Version: $PKG_VER
Depends: luci-compat, luci-base, curl, openssl-util, script-utils, tar
Section: luci
Architecture: all
Installed-Size: 0
Description: OpenClaw AI Gateway — LuCI 界面
CTLEOF
# 写入文件列表 (payload 中已安装的文件)
cat > "$INFO_DIR/$PKG.list" << LISTEOF
__FILE_LIST__
LISTEOF
# 写入 prerm 脚本 (卸载前执行)
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, curl, openssl-util, script-utils, tar
Status: install user installed
Architecture: all
Conffiles:
/etc/config/openclaw 0
Installed-Time: $INSTALL_TIME
STEOF
echo "已注册到 opkg (iStore 可管理)"
# 执行 uci-defaults
if [ -f /etc/uci-defaults/99-openclaw ]; then
echo "执行初始化脚本..."
( . /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
# 重启 Web PTY 服务 (使其加载新文件和新 token)
# PTY 是 procd 管理的实例, kill 后 procd 会自动 respawn
PTY_PID=$(pgrep -f 'web-pty.js' 2>/dev/null | head -1)
if [ -n "$PTY_PID" ]; then
echo "重启配置终端服务..."
kill "$PTY_PID" 2>/dev/null
sleep 1
fi
echo ""
echo "✅ 安装完成!"
echo ""
echo "后续步骤:"
echo " 1. 运行 openclaw-env setup — 下载 Node.js 并安装 OpenClaw"
echo " 2. 访问 LuCI → 服务 → OpenClaw 进行配置"
echo " 3. 或执行 /etc/init.d/openclaw enable && /etc/init.d/openclaw start"
echo ""
exit 0
__ARCHIVE_BELOW__
INSTALLER_EOF
}
# 构建
echo ""
echo "[1/4] 安装文件到暂存区..."
install_files "$STAGING/payload"
echo "[2/4] 生成文件列表..."
# 生成安装文件列表 (供 opkg 卸载时使用)
# 注: openclaw.default 安装后会变为 openclaw, 文件列表中记录最终路径
FILE_LIST=$(cd "$STAGING/payload" && find . -type f | sed 's|^\./|/|' | sed 's|/etc/config/openclaw.default|/etc/config/openclaw|' | sort)
echo " 共 $(echo "$FILE_LIST" | wc -l | tr -d ' ') 个文件"
echo "[3/4] 创建安装器..."
create_installer
# 替换安装器中的占位符
sed -i "s|__PKG_VERSION__|${PKG_VERSION}|g" "$STAGING/install.sh"
# 替换文件列表占位符 — 使用临时文件拼接避免 sed/awk 多行问题
{
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"
echo "[4/4] 打包..."
mkdir -p "$OUT_DIR"
# 创建 payload tarball
(cd "$STAGING/payload" && tar czf "$STAGING/payload.tar.gz" .)
# 组合: installer header + payload
RUN_FILE="$OUT_DIR/${PKG_NAME}_${PKG_VERSION}.run"
cat "$STAGING/install.sh" "$STAGING/payload.tar.gz" > "$RUN_FILE"
chmod +x "$RUN_FILE"
FILE_SIZE=$(wc -c < "$RUN_FILE" | tr -d ' ')
echo ""
echo "=== 构建完成 ==="
echo "输出文件: $RUN_FILE"
echo "文件大小: $FILE_SIZE bytes"
echo ""
echo "安装方法: sh ${PKG_NAME}_${PKG_VERSION}.run"
@@ -0,0 +1,41 @@
#!/bin/bash
# 用法: gen-release-body.sh <版本号> <CHANGELOG路径> <输出目录>
# 为指定版本生成 GitHub Release body markdown 文件
set -e
VER="$1"
CHANGELOG_FILE="$2"
OUT_DIR="$3"
if [ -z "$VER" ] || [ -z "$CHANGELOG_FILE" ] || [ -z "$OUT_DIR" ]; then
echo "用法: $0 <版本号> <CHANGELOG路径> <输出目录>"
exit 1
fi
mkdir -p "$OUT_DIR"
# 提取该版本的 changelog
CONTENT=$(awk "/^## \\[${VER}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" "$CHANGELOG_FILE")
if [ -z "$CONTENT" ]; then
CONTENT="暂无更新日志"
fi
# 写入文件
{
printf '%s\n' "$CONTENT"
echo ""
echo "---"
echo ""
echo '**在线安装** (需联网,自动下载 Node.js + OpenClaw)'
echo '```'
echo '# iStoreOS'
echo "sh luci-app-openclaw_${VER}.run"
echo ''
echo '# OpenWrt'
echo "opkg install luci-app-openclaw_${VER}-1_all.ipk"
echo '```'
echo ''
echo '[使用文档](https://github.com/10000ge10000/luci-app-openclaw#readme) · [问题反馈](https://github.com/10000ge10000/luci-app-openclaw/issues) · [B站](https://space.bilibili.com/59438380) · [博客](https://blog.910501.xyz/)'
} > "${OUT_DIR}/${VER}.md"
echo "✓ ${VER}.md ($(wc -l < "${OUT_DIR}/${VER}.md") 行)"
+230
View File
@@ -0,0 +1,230 @@
#!/bin/sh
# ============================================================================
# OpenList 网盘同步脚本 — 补齐所有历史版本 + 上传更新记录
#
# 功能:
# 1. 从 GitHub Releases 下载所有版本的 .run + .ipk
# 2. 从 CHANGELOG.md 提取每个版本的更新记录,生成 更新记录.txt
# 3. 上传到 OpenList 网盘的 openclaw-在线安装 目录
#
# 用法:
# sh scripts/sync_openlist.sh
# ============================================================================
set -e
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PKG_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
# ── 配置 ──
GITHUB_REPO="10000ge10000/luci-app-openclaw"
OPENLIST_URL="http://124.243.178.237:15244"
OPENLIST_USER="admin"
OPENLIST_PASS="mingmenmama"
OPENLIST_ROOT="/Quark"
UPLOAD_SUBDIR="openclaw-在线安装"
CHANGELOG="$PKG_DIR/CHANGELOG.md"
WORK_DIR="/tmp/openlist-sync"
# 所有已发布的版本 (按时间顺序)
ALL_VERSIONS="1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.0.10 1.0.11 1.0.12 1.0.14 1.0.15"
log_info() { printf " [\033[32m✓\033[0m] %s\n" "$1"; }
log_warn() { printf " [\033[33m!\033[0m] %s\n" "$1"; }
log_error() { printf " [\033[31m✗\033[0m] %s\n" "$1"; }
log_skip() { printf " [\033[36m-\033[0m] %s\n" "$1"; }
# ── 获取 Token ──
get_token() {
local resp
resp=$(curl -s -X POST "${OPENLIST_URL}/api/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${OPENLIST_USER}\",\"password\":\"${OPENLIST_PASS}\"}")
local token
token=$(echo "$resp" | grep -o '"token":"[^"]*"' | sed 's/"token":"//;s/"//')
if [ -z "$token" ]; then
log_error "OpenList 登录失败"
echo " 响应: $resp"
exit 1
fi
echo "$token"
}
# ── 创建远程目录 ──
create_remote_dir() {
local token="$1"
local remote_path="$2"
curl -s -X POST "${OPENLIST_URL}/api/fs/mkdir" \
-H "Authorization: ${token}" \
-H "Content-Type: application/json" \
-d "{\"path\":\"${remote_path}\"}" >/dev/null 2>&1 || true
}
# ── 检查远程文件是否存在 ──
remote_file_exists() {
local token="$1"
local remote_path="$2"
local filename="$3"
local resp
resp=$(curl -s -X POST "${OPENLIST_URL}/api/fs/list" \
-H "Authorization: ${token}" \
-H "Content-Type: application/json" \
-d "{\"path\":\"${remote_path}\",\"refresh\":false}")
echo "$resp" | grep -q "\"name\":\"${filename}\""
}
# ── 上传单个文件 ──
upload_file() {
local token="$1"
local local_file="$2"
local remote_path="$3"
local filename=$(basename "$local_file")
local fsize=$(du -h "$local_file" | cut -f1)
local resp
resp=$(curl -s -X PUT "${OPENLIST_URL}/api/fs/put" \
-H "Authorization: ${token}" \
-H "File-Path: ${remote_path}/${filename}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${local_file}" \
--max-time 300 2>/dev/null)
local code=""
code=$(echo "$resp" | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
if [ "$code" = "200" ]; then
log_info "${filename} (${fsize}) 上传成功"
else
log_error "${filename} 上传失败: $resp"
fi
}
# ── 从 CHANGELOG.md 提取指定版本的更新日志 ──
extract_changelog() {
local version="$1"
local output_file="$2"
awk "/^## \\[${version}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" \
"$CHANGELOG" > "$output_file"
# 去掉首尾空行
sed -i '/./,$!d' "$output_file" # 去掉开头空行
sed -i ':a; /^[[:space:]]*$/{ $d; N; ba }' "$output_file" # 去掉末尾空行 (GNU sed)
if [ ! -s "$output_file" ]; then
echo "暂无更新日志" > "$output_file"
fi
}
# ── 从 GitHub 下载文件 ──
download_release_file() {
local version="$1"
local filename="$2"
local output="$3"
local url="https://github.com/${GITHUB_REPO}/releases/download/v${version}/${filename}"
if [ -f "$output" ]; then
log_skip "${filename} 已在本地缓存"
return 0
fi
if curl -sL --fail -o "$output" "$url" 2>/dev/null; then
# 检查是否为有效文件 (排除 GitHub 返回 Not Found HTML)
local size=$(wc -c < "$output")
if [ "$size" -lt 1000 ]; then
local content=$(cat "$output")
if echo "$content" | grep -qi "not found"; then
rm -f "$output"
return 1
fi
fi
return 0
else
rm -f "$output"
return 1
fi
}
# ══════════════════════════════════════════════════════════════
# 主流程
# ══════════════════════════════════════════════════════════════
echo ""
echo "================================================================"
echo " OpenList 网盘同步 — 补齐所有历史版本"
echo "================================================================"
echo ""
# 准备工作目录
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
# 登录
echo "正在登录 OpenList..."
TOKEN=$(get_token)
log_info "登录成功"
echo ""
TOTAL_UPLOADED=0
for VER in $ALL_VERSIONS; do
echo "── v${VER} ──────────────────────────────────────"
REMOTE_DIR="${OPENLIST_ROOT}/${UPLOAD_SUBDIR}/v${VER}"
VER_DIR="${WORK_DIR}/v${VER}"
mkdir -p "$VER_DIR"
# 创建远程目录
create_remote_dir "$TOKEN" "$REMOTE_DIR"
# 1) 下载 .run
RUN_FILE="luci-app-openclaw_${VER}.run"
if remote_file_exists "$TOKEN" "$REMOTE_DIR" "$RUN_FILE"; then
log_skip "${RUN_FILE} 已存在于网盘"
else
echo " 下载 ${RUN_FILE}..."
if download_release_file "$VER" "$RUN_FILE" "${VER_DIR}/${RUN_FILE}"; then
upload_file "$TOKEN" "${VER_DIR}/${RUN_FILE}" "$REMOTE_DIR"
TOTAL_UPLOADED=$((TOTAL_UPLOADED + 1))
else
log_error "${RUN_FILE} 下载失败"
fi
fi
# 2) 下载 .ipk
IPK_FILE="luci-app-openclaw_${VER}-1_all.ipk"
if remote_file_exists "$TOKEN" "$REMOTE_DIR" "$IPK_FILE"; then
log_skip "${IPK_FILE} 已存在于网盘"
else
echo " 下载 ${IPK_FILE}..."
if download_release_file "$VER" "$IPK_FILE" "${VER_DIR}/${IPK_FILE}"; then
upload_file "$TOKEN" "${VER_DIR}/${IPK_FILE}" "$REMOTE_DIR"
TOTAL_UPLOADED=$((TOTAL_UPLOADED + 1))
else
log_error "${IPK_FILE} 下载失败"
fi
fi
# 3) 生成并上传 更新记录.txt
CHANGELOG_FILE="更新记录.txt"
if remote_file_exists "$TOKEN" "$REMOTE_DIR" "$CHANGELOG_FILE"; then
log_skip "${CHANGELOG_FILE} 已存在于网盘"
else
extract_changelog "$VER" "${VER_DIR}/${CHANGELOG_FILE}"
upload_file "$TOKEN" "${VER_DIR}/${CHANGELOG_FILE}" "$REMOTE_DIR"
TOTAL_UPLOADED=$((TOTAL_UPLOADED + 1))
fi
echo ""
done
# 清理
rm -rf "$WORK_DIR"
echo "================================================================"
echo " 同步完成!共上传 ${TOTAL_UPLOADED} 个文件"
echo "================================================================"
echo ""
+67
View File
@@ -0,0 +1,67 @@
#!/bin/sh
set -e
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PKG_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
DIST_DIR="${1:-$PKG_DIR/dist}"
if [ -z "$OPENLIST_URL" ]; then
echo "错误: 请设置 OPENLIST_URL 环境变量"
exit 1
fi
if [ -z "$OPENLIST_TOKEN" ] && { [ -z "$OPENLIST_USER" ] || [ -z "$OPENLIST_PASS" ]; }; then
echo "错误: 请设置登录凭据"
exit 1
fi
UPLOAD_ROOT="${OPENLIST_PATH:-/}"
PKG_VERSION=$(cat "$PKG_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "unknown")
UPLOAD_ROOT="${UPLOAD_ROOT%/}"
UPLOAD_SUBDIR="openclaw-在线安装"
OPENLIST_URL="${OPENLIST_URL%/}"
get_token() {
if [ -n "$OPENLIST_TOKEN" ]; then echo "$OPENLIST_TOKEN"; return; fi
local resp=$(curl -s -X POST "${OPENLIST_URL}/api/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${OPENLIST_USER}\",\"password\":\"${OPENLIST_PASS}\"}")
local token=$(echo "$resp" | grep -o '"token":"[^"]*"' | sed 's/"token":"//;s/"//' | head -n 1)
if [ -z "$token" ]; then exit 1; fi
echo "$token"
}
create_remote_dir() {
curl -s -X POST "${OPENLIST_URL}/api/fs/mkdir" \
-H "Authorization: ${1}" \
-H "Content-Type: application/json" \
-d "{\"path\":\"${2}\"}" >/dev/null 2>&1 || true
}
upload_file() {
local filename=$(basename "$2")
echo " 上传: ${filename} ..."
local resp=$(curl -s -X PUT "${OPENLIST_URL}/api/fs/put" \
-H "Authorization: ${1}" \
-H "File-Path: ${3}/${filename}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${2}" \
--max-time 3600)
local code=$(echo "$resp" | grep -o '"code":[0-9]*' | grep -o '[0-9]*' | head -n 1)
if [ "$code" = "200" ]; then echo " [✓] 上传成功"; else echo " [✗] 失败: $resp"; fi
}
UPLOAD_FILES=$(find "$DIST_DIR" -type f -name "*.run" -o -name "*.ipk" 2>/dev/null)
if [ -z "$UPLOAD_FILES" ]; then echo "错误: 未找到可上传文件"; exit 1; fi
TOKEN=$(get_token)
REMOTE_DIR="${UPLOAD_ROOT}/${UPLOAD_SUBDIR}/v${PKG_VERSION}"
REMOTE_DIR=$(echo "$REMOTE_DIR" | sed 's#^//#/#g')
echo "创建远程目录: ${REMOTE_DIR}"
create_remote_dir "$TOKEN" "$REMOTE_DIR"
for f in $UPLOAD_FILES; do
upload_file "$TOKEN" "$f" "$REMOTE_DIR"
done
echo "✅ 上传完成!"