diff --git a/.github/workflows/build-node-musl.yml b/.github/workflows/build-node-musl.yml index 8cedbaf..9b271b6 100644 --- a/.github/workflows/build-node-musl.yml +++ b/.github/workflows/build-node-musl.yml @@ -2,15 +2,10 @@ name: Build Node.js ARM64 musl on: workflow_dispatch: - inputs: - node_version: - description: 'Node.js 版本 (如 22.16.0)' - required: true - default: '22.16.0' jobs: build: - name: Build Node.js ${{ github.event.inputs.node_version }} ARM64 musl + name: Build Node.js ARM64 musl runs-on: ubuntu-latest permissions: contents: write @@ -19,6 +14,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Extract Node.js version from source + id: node_ver + run: | + NODE_VER=$(grep -oP 'NODE_VERSION="\$\{NODE_VERSION:-\K[0-9.]+' root/usr/bin/openclaw-env) + echo "version=${NODE_VER}" >> "$GITHUB_OUTPUT" + echo "Node.js version from openclaw-env: v${NODE_VER}" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: @@ -26,7 +28,7 @@ jobs: - name: Build Node.js ARM64 musl portable run: | - NODE_VER="${{ github.event.inputs.node_version }}" + NODE_VER="${{ steps.node_ver.outputs.version }}" mkdir -p dist echo "=== Building Node.js v${NODE_VER} ARM64 musl portable tarball ===" docker run --rm --platform linux/arm64 \ @@ -64,7 +66,7 @@ jobs: Node.js 官方 [unofficial-builds](https://unofficial-builds.nodejs.org/) 仅提供 x64 musl 构建,不提供 ARM64 musl。 此 Release 使用 Alpine Linux ARM64 (musl libc) 环境打包。 - **注意**: 实际 Node.js 版本可能与文件名中的版本略有差异(取决于 Alpine 仓库提供的版本), - 但主版本号 (v22.x) 保证兼容。 + **注意**: 文件名中的版本号为 Alpine `apk add nodejs` 实际安装的版本, + 构建时自动从 `openclaw-env` 读取。 `openclaw-env setup` 会在 ARM64 musl 设备上自动从此处下载。 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b9ab84f..0ff245a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,14 +4,24 @@ on: workflow_dispatch: inputs: version: - description: '版本号 (留空自动生成,格式: 1.0.0)' + description: '版本号 (留空则读取 VERSION 文件)' required: false default: '' + build_offline: + description: '是否构建离线安装包 (~130MB/架构)' + required: false + type: boolean + default: false create_release: description: '是否创建 Release' required: false type: boolean default: true + upload_openlist: + description: '是否上传到 OpenList 网盘' + required: false + type: boolean + default: false jobs: build: @@ -38,6 +48,17 @@ jobs: echo "tag=v$VER" >> "$GITHUB_OUTPUT" echo "Version: $VER" + - name: Extract build versions from source + id: build_versions + run: | + # 从 openclaw-env 中提取版本号 (唯一的版本真相源) + NODE_VER=$(grep -oP 'NODE_VERSION="\$\{NODE_VERSION:-\K[0-9.]+' root/usr/bin/openclaw-env) + OC_VER=$(grep -oP 'OC_TESTED_VERSION="\K[0-9.]+' root/usr/bin/openclaw-env) + echo "node_version=${NODE_VER}" >> "$GITHUB_OUTPUT" + echo "oc_version=${OC_VER}" >> "$GITHUB_OUTPUT" + echo "Node.js: v${NODE_VER}" + echo "OpenClaw: v${OC_VER}" + - name: Inject version run: | VER="${{ steps.version.outputs.version }}" @@ -45,7 +66,9 @@ jobs: # 同步到 Makefile sed -i "s/^PKG_VERSION:=.*/PKG_VERSION:=$VER/" Makefile - - name: Build .run installer + # ── 在线安装包 (始终构建) ── + + - name: Build .run installer (online) run: | chmod +x scripts/build_run.sh sh scripts/build_run.sh dist @@ -55,19 +78,50 @@ jobs: chmod +x scripts/build_ipk.sh sh scripts/build_ipk.sh dist + # ── 离线安装包 (可选) ── + + - name: Setup Node.js (for offline build) + if: github.event.inputs.build_offline == 'true' + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Download offline dependencies + if: github.event.inputs.build_offline == 'true' + run: | + chmod +x scripts/download_deps.sh + sh scripts/download_deps.sh .offline-cache + env: + NODE_VERSION: ${{ steps.build_versions.outputs.node_version }} + OC_VERSION: ${{ steps.build_versions.outputs.oc_version }} + + - name: Build offline .run (musl only) + if: github.event.inputs.build_offline == 'true' + run: | + chmod +x scripts/build_offline_run.sh + sh scripts/build_offline_run.sh dist/ + env: + CACHE_DIR: .offline-cache + NODE_VERSION: ${{ steps.build_versions.outputs.node_version }} + + # ── 通用步骤 ── + - name: List outputs - run: ls -lh dist/ + run: | + echo "=== Build artifacts ===" + ls -lh dist/ + echo "" + echo "=== SHA256 checksums ===" + sha256sum dist/*.run dist/*.ipk 2>/dev/null || true - name: Extract changelog id: changelog run: | VER="${{ steps.version.outputs.version }}" - # 提取当前版本的 CHANGELOG 内容 (从 ## [version] 到下一个 ## [ 之间) CHANGELOG=$(awk "/^## \\[${VER}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" CHANGELOG.md) if [ -z "$CHANGELOG" ]; then CHANGELOG="暂无更新日志" fi - # 写入多行输出 { echo "content< + OpenClaw LuCI 管理界面 + + **系统要求** | 项目 | 要求 | |------|------| | 架构 | x86_64 或 aarch64 (ARM64) | -| C 库 | glibc 或 musl(自动检测) | +| C 库 | musl(自动检测;离线包仅支持 musl) | | 依赖 | luci-compat, luci-base, curl, openssl-util | | 存储 | **1.5GB 以上可用空间** | | 内存 | 推荐 1GB 及以上 | -### 🖥️ 兼容性矩阵 - -#### 支持的架构 × C 库组合 - -| 架构 | C 库 | Node.js 来源 | 状态 | -|------|------|-------------|------| -| x86_64 | musl | [unofficial-builds.nodejs.org](https://unofficial-builds.nodejs.org/) | ✅ 已验证 | -| x86_64 | glibc | [nodejs.org](https://nodejs.org/) 官方 | ✅ 支持 | -| aarch64 | musl | 项目自托管(Alpine 打包,含完整依赖) | ✅ 已验证 | -| aarch64 | glibc | [nodejs.org](https://nodejs.org/) 官方 | ✅ 支持 | -| mips / mipsel | - | — | ❌ 不支持 | -| armv7l / armv6l | - | — | ❌ 不支持 | - -> **说明**:Node.js 22+ 仅提供 x86_64 和 aarch64 预编译包,不支持 MIPS(如 MT7620/MT7621 路由器)和 32 位 ARM(armv7l/armv6l)。大部分老旧路由器(MT76xx 系列)为 MIPS 架构,无法运行。 - -#### 支持的 OpenWrt 版本 - -| OpenWrt 版本 | LuCI 版本 | 验证状态 | 说明 | -|-------------|-----------|---------|------| -| 24.x (iStoreOS 24.10) | LuCI 24.x | ✅ 已验证 | 推荐版本 | -| 23.05 | LuCI openwrt-23.05 | ✅ 支持 | | -| 22.03 (iStoreOS 22.03) | LuCI openwrt-22.03 | ✅ 已验证 | 需自托管 Node.js(ARM64 musl) | -| 21.02 | LuCI openwrt-21.02 | ⚠️ 应兼容 | 未测试,procd / LuCI API 兼容 | -| 19.07 | LuCI openwrt-19.07 | ⚠️ 应兼容 | 未测试 | -| 18.06 及更早 | LuCI 旧版 | ❌ 不保证 | procd API 可能不兼容 | - -> 插件使用标准 procd init 和 LuCI CBI (luci-compat) 接口,理论上兼容 OpenWrt 19.07+。 - -#### 已验证的典型设备 - -| 设备 / 平台 | 架构 | 系统 | 验证结果 | -|------------|------|------|---------| -| N100 / N5105 软路由 | x86_64 musl | iStoreOS 24.10.5 | ✅ 通过 | -| 晶晨 S905 系列 (Cortex-A53) | aarch64 musl | iStoreOS 22.03.7 | ✅ 通过 | -| Raspberry Pi 4/5 | aarch64 | OpenWrt 23.05+ | ✅ 应支持 | -| FriendlyElec R4S/R5S | aarch64 | OpenWrt / FriendlyWrt | ✅ 应支持 | -| 通用 x86 虚拟机 (PVE/ESXi) | x86_64 | OpenWrt 22.03+ | ✅ 应支持 | -| MT7621 路由器 (如 Redmi AC2100) | mipsel | — | ❌ 不支持 (MIPS) | -| MT7620/MT7628 路由器 | mipsel | — | ❌ 不支持 (MIPS) | - -#### ARM64 musl 特别说明 - -ARM64 + musl 的 OpenWrt 设备(绝大多数 ARM64 路由器)使用**项目自托管的 Node.js 包**: - -- 基于 Alpine Linux 3.21 ARM64 环境打包 -- 包含完整的共享库(libstdc++、libssl、libicu 等)和 musl 动态链接器 -- 包含完整 ICU 国际化数据(`icudt74l.dat`) -- 通过 `patchelf` 将 ELF interpreter 和 rpath 指向打包的 musl 链接器,**不依赖系统库版本** -- 因此即使系统是 OpenWrt 22.03(musl 1.2.3)也能正常运行 Alpine 3.21 编译的 Node.js - ## 📦 安装 ### 方式一:.run 自解压包(推荐) @@ -135,6 +90,32 @@ sh /etc/uci-defaults/99-openclaw rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* ``` +### 方式五:离线安装包(无需联网) + +适用于**无法联网**的路由器。安装包中已包含 Node.js + OpenClaw 运行环境,全程离线完成。 + +**下载离线包**(在联网的电脑上): + +前往 [Releases](https://github.com/10000ge10000/luci-app-openclaw/releases) 页面下载对应架构的 `_offline.run` 文件: + +| 架构 | 文件名 | +|------|--------| +| x86_64 | `luci-app-openclaw_*_x86_64-musl_offline.run` | +| aarch64 (ARM64) | `luci-app-openclaw_*_aarch64-musl_offline.run` | + +**传输到路由器并安装**: + +```bash +# 从电脑传输到路由器(替换为实际文件名和路由器 IP) +scp luci-app-openclaw_*_offline.run root@192.168.1.1:/tmp/ + +# SSH 登录路由器后执行安装 +sh /tmp/luci-app-openclaw_*_offline.run +``` + +> **提示**:离线包约 130MB,ARM 设备上安装需要 3-5 分钟(主要是解压时间)。 +> 安装完成后无需再运行 `openclaw-env setup`,直接进入 LuCI 配置即可。 + ## 🔰 首次使用 1. 打开 LuCI → 服务 → OpenClaw,点击「安装运行环境」 @@ -164,8 +145,15 @@ luci-app-openclaw/ │ └── share/openclaw/ # 配置终端资源 ├── scripts/ │ ├── build_ipk.sh # 本地 IPK 构建 -│ └── build_run.sh # .run 安装包构建 -└── .github/workflows/build.yml # GitHub Actions +│ ├── build_run.sh # .run 安装包构建 +│ ├── build_offline_run.sh # 离线 .run 安装包构建 +│ ├── download_deps.sh # 下载离线依赖 (Node.js + OpenClaw) +│ ├── upload_openlist.sh # 上传到网盘 (OpenList) +│ └── build-node-musl.sh # 编译 Node.js musl 静态链接版本 +└── .github/workflows/ + ├── build.yml # 在线构建 + 发布 + ├── build-offline.yml # 离线包构建 + 发布 + └── build-node-musl.yml # Node.js musl 构建 ``` ## 🤝 贡献 diff --git a/VERSION b/VERSION index 5b09c67..a970716 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.14 +1.0.15 diff --git a/docs/images/1.png b/docs/images/1.png new file mode 100644 index 0000000..34b3972 Binary files /dev/null and b/docs/images/1.png differ diff --git a/luasrc/controller/openclaw.lua b/luasrc/controller/openclaw.lua index ef32288..6b7acf5 100644 --- a/luasrc/controller/openclaw.lua +++ b/luasrc/controller/openclaw.lua @@ -81,6 +81,18 @@ function action_status() pvf:close() end + -- 安装方式检测 (离线 / 在线) + local olf = io.open("/usr/share/openclaw/.offline-install", "r") + if olf then + local content = olf:read("*a") + olf:close() + result.install_type = "offline" + result.install_date = content:match("date=([^\n]+)") or "" + result.install_arch = content:match("arch=([^\n]+)") or "" + else + result.install_type = "online" + end + -- 检查 Node.js local node_bin = "/opt/openclaw/node/bin/node" local f = io.open(node_bin, "r") @@ -682,31 +694,73 @@ function action_backup() http.write_json({ status = "error", message = "未找到备份文件,请先创建备份" }) return end - local config_path = "/opt/openclaw/data/.openclaw/openclaw.json" - -- 先备份当前配置 - sys.exec("cp -f " .. config_path .. " " .. config_path .. ".pre-restore 2>/dev/null") - -- 从 tar.gz 中提取 openclaw.json - local extract_cmd = "tar -xzf " .. restore_path .. " --wildcards '*/openclaw.json' -O > " .. config_path .. ".tmp 2>/dev/null" - sys.exec(extract_cmd) - -- 验证提取的文件是否有效 JSON - local check = sys.exec(node_bin .. " -e \"try{JSON.parse(require('fs').readFileSync('" .. config_path .. ".tmp','utf8'));console.log('OK')}catch(e){console.log('FAIL')}\" 2>/dev/null"):gsub("%s+", "") - if check == "OK" then - sys.exec("mv -f " .. config_path .. ".tmp " .. config_path) - sys.exec("chown openclaw:openclaw " .. config_path .. " 2>/dev/null") - -- 重启服务使配置生效 - sys.exec("/etc/init.d/openclaw restart >/dev/null 2>&1 &") + local oc_data_dir = "/opt/openclaw/data/.openclaw" + local config_path = oc_data_dir .. "/openclaw.json" + + -- 1) 先验证备份中的 openclaw.json 是否有效 + local check_cmd = "tar -xzf " .. restore_path .. " --wildcards '*/openclaw.json' -O 2>/dev/null" + local json_content = sys.exec(check_cmd) + if not json_content or json_content == "" then http.prepare_content("application/json") - http.write_json({ - status = "ok", - action = "restore", - message = "配置已从备份恢复,服务正在重启。原配置已保存为 openclaw.json.pre-restore", - backup_path = restore_path - }) - else - sys.exec("rm -f " .. config_path .. ".tmp") + http.write_json({ status = "error", message = "备份文件中未找到 openclaw.json" }) + return + end + -- 写入临时文件并用 node 验证 + local tmpfile = "/tmp/oc-restore-check.json" + local f = io.open(tmpfile, "w") + if f then f:write(json_content); f:close() end + local check = sys.exec(node_bin .. " -e \"try{JSON.parse(require('fs').readFileSync('" .. tmpfile .. "','utf8'));console.log('OK')}catch(e){console.log('FAIL')}\" 2>/dev/null"):gsub("%s+", "") + os.remove(tmpfile) + if check ~= "OK" then http.prepare_content("application/json") http.write_json({ status = "error", message = "备份文件中的配置无效,恢复已取消" }) + return end + + -- 2) 备份当前配置 + sys.exec("cp -f " .. config_path .. " " .. config_path .. ".pre-restore 2>/dev/null") + + -- 3) 获取备份名前缀 (如: 2026-03-11T18-21-17.209Z-openclaw-backup) + -- 备份结构: /payload/posix/<绝对路径> + local first_entry = sys.exec("tar -tzf " .. restore_path .. " 2>/dev/null | head -1"):gsub("%s+", "") + local backup_name = first_entry:match("^([^/]+)/") or "" + if backup_name == "" then + http.prepare_content("application/json") + http.write_json({ status = "error", message = "备份文件格式无法识别" }) + return + end + local payload_prefix = backup_name .. "/payload/posix/" + -- strip 3 层: / payload / posix + local strip_count = 3 + + -- 4) 停止服务 + sys.exec("/etc/init.d/openclaw stop >/dev/null 2>&1") + -- 等待端口释放 + sys.exec("sleep 2") + + -- 5) 提取 payload 文件到根目录 (还原到原始绝对路径) + -- 注: --wildcards 与 --strip-components 组合在某些 tar 版本不兼容 + -- 使用精确路径前缀代替 wildcards + local extract_cmd = string.format( + "tar -xzf %s --strip-components=%d -C / '%s' 2>&1", + restore_path, strip_count, payload_prefix + ) + local extract_out = sys.exec(extract_cmd) + + -- 6) 修复权限 + sys.exec("chown -R openclaw:openclaw " .. oc_data_dir .. " 2>/dev/null") + + -- 7) 重启服务 + sys.exec("/etc/init.d/openclaw start >/dev/null 2>&1 &") + + http.prepare_content("application/json") + http.write_json({ + status = "ok", + action = "restore", + message = "已从备份完整恢复所有配置和数据,服务正在重启。原配置已保存为 openclaw.json.pre-restore", + backup_path = restore_path, + extract_output = extract_out or "" + }) elseif action == "list" then -- 返回结构化的备份文件列表(含类型/大小/时间) local files_raw = sys.exec("ls -t " .. backup_dir .. "/*-openclaw-backup.tar.gz 2>/dev/null"):gsub("%s+$", "") diff --git a/luasrc/view/openclaw/status.htm b/luasrc/view/openclaw/status.htm index 96e5f9d..3132909 100644 --- a/luasrc/view/openclaw/status.htm +++ b/luasrc/view/openclaw/status.htm @@ -57,6 +57,8 @@ .oc-badge-starting { background: #fff8c5; color: #9a6700; } .oc-badge-disabled { background: #f0f0f0; color: #656d76; } .oc-badge-unknown { background: #fff8c5; color: #9a6700; } +.oc-badge-offline { background: #ddf4ff; color: #0969da; border: 1px solid #54aeff; } +.oc-badge-online { background: #f0f0f0; color: #656d76; } .oc-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; } .oc-dot-green { background: #1a7f37; } .oc-dot-red { background: #cf222e; } @@ -64,7 +66,7 @@
-
🦞 OpenClaw 服务状态
+
🦞 OpenClaw 服务状态
@@ -78,6 +80,7 @@ +
运行状态加载中...
Node.js-
OpenClaw-
插件版本-
安装方式-
@@ -148,6 +151,16 @@ document.getElementById('oc-st-oc-ver').textContent = d.oc_version ? ('v' + d.oc_version) : '未安装'; document.getElementById('oc-st-plugin').textContent = d.plugin_version ? ('v' + d.plugin_version) : '-'; + var itEl = document.getElementById('oc-st-install-type'); + if (d.install_type === 'offline') { + var tip = '📦 离线安装'; + if (d.install_arch) tip += ' (' + d.install_arch + ')'; + if (d.install_date) tip += '\n安装时间: ' + d.install_date; + itEl.innerHTML = '📦 离线版'; + } else { + itEl.innerHTML = '🌐 在线版'; + } + } catch(e) { document.getElementById('oc-st-status').innerHTML = '查询失败'; } diff --git a/root/usr/bin/openclaw-env b/root/usr/bin/openclaw-env index f18a17a..1d1574a 100755 --- a/root/usr/bin/openclaw-env +++ b/root/usr/bin/openclaw-env @@ -11,7 +11,7 @@ # ============================================================================ set -e -NODE_VERSION="${NODE_VERSION:-22.16.0}" +NODE_VERSION="${NODE_VERSION:-22.15.1}" # 经过验证的 OpenClaw 稳定版本 (更新此值需同步测试) OC_TESTED_VERSION="2026.3.8" # 用户可通过 OC_VERSION 环境变量覆盖安装版本 @@ -145,7 +145,7 @@ download_node() { return 0 fi # ARM64 musl 使用 Alpine 打包,版本号可能不完全匹配 - # 只要主版本号相同即认为兼容 (如 22.15.1 vs 22.16.0) + # 只要主版本号相同即认为兼容 (如 22.15.1 vs 22.15.0) local cur_major=$(echo "$current_ver" | cut -d. -f1) local want_major=$(echo "$node_ver" | cut -d. -f1) if [ "$cur_major" = "$want_major" ]; then @@ -418,7 +418,7 @@ do_setup() { fi echo "╔══════════════════════════════════════════════════════════════╗" - echo "║ 一万AI分享 OpenClaw 环境安装 ║" + echo "║ 一万AI分享 OpenClaw 环境安装 ║" echo "╚══════════════════════════════════════════════════════════════╝" echo "" echo " 架构: $(uname -m)" @@ -439,15 +439,15 @@ do_setup() { echo "" echo "╔══════════════════════════════════════════════════════════════╗" - echo "║ ✅ 安装完成! ║" - echo "║ ║" - echo "║ 下一步: ║" - echo "║ uci set openclaw.main.enabled=1 ║" - echo "║ uci commit openclaw ║" - echo "║ /etc/init.d/openclaw enable ║" - echo "║ /etc/init.d/openclaw start ║" - echo "║ ║" - echo "║ 或在 LuCI → 服务 → OpenClaw 中启用 ║" + echo "║ ✅ 安装完成! ║" + echo "║ ║" + echo "║ 下一步: ║" + echo "║ uci set openclaw.main.enabled=1 ║" + echo "║ uci commit openclaw ║" + echo "║ /etc/init.d/openclaw enable ║" + echo "║ /etc/init.d/openclaw start ║" + echo "║ ║" + echo "║ 或在 LuCI → 服务 → OpenClaw 中启用 ║" echo "╚══════════════════════════════════════════════════════════════╝" } @@ -498,7 +498,7 @@ do_check() { do_upgrade() { echo "╔══════════════════════════════════════════════════════════════╗" - echo "║ 一万AI分享 OpenClaw 升级 ║" + echo "║ 一万AI分享 OpenClaw 升级 ║" echo "╚══════════════════════════════════════════════════════════════╝" echo "" @@ -549,7 +549,7 @@ do_upgrade() { fi echo "" echo "╔══════════════════════════════════════════════════════════════╗" - echo "║ ✅ 升级完成! ║" + echo "║ ✅ 升级完成! ║" echo "╚══════════════════════════════════════════════════════════════╝" else log_error "升级验证失败,OpenClaw 入口文件未找到" @@ -623,11 +623,114 @@ do_factory_reset() { 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 " 安装路径: ${NODE_BASE}" + echo " 数据路径: ${OC_DATA}" + echo " 离线包: ${offline_dir}" + echo "" + + # [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) 安装成功" + 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_ver=$("$NODE_BIN" "$oc_entry" --version 2>/dev/null | tr -d '[:space:]') + log_info "OpenClaw v${oc_ver} 安装成功" + else + log_error "OpenClaw 安装验证失败" + exit 1 + fi + else + log_error "未找到 OpenClaw 离线包: $oc_tarball" + exit 1 + fi + + # [3] 初始化 + init_openclaw + + echo "" + echo "╔══════════════════════════════════════════════════════════════╗" + echo "║ ✅ 离线安装完成! ║" + echo "║ ║" + echo "║ 下一步: ║" + echo "║ uci set openclaw.main.enabled=1 ║" + echo "║ uci commit openclaw ║" + echo "║ /etc/init.d/openclaw enable ║" + echo "║ /etc/init.d/openclaw start ║" + echo "║ ║" + echo "║ 或在 LuCI → 服务 → OpenClaw 中启用 ║" + echo "╚══════════════════════════════════════════════════════════════╝" +} + # ── 主入口 ── case "${1:-}" in setup) do_setup ;; + setup-offline) + do_setup_offline "$@" + ;; check) do_check ;; @@ -641,9 +744,10 @@ case "${1:-}" in do_factory_reset ;; *) - echo "用法: openclaw-env {setup|check|upgrade|node|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" diff --git a/root/usr/share/openclaw/oc-config.sh b/root/usr/share/openclaw/oc-config.sh index 44e8e76..cdfc19f 100755 --- a/root/usr/share/openclaw/oc-config.sh +++ b/root/usr/share/openclaw/oc-config.sh @@ -1195,13 +1195,33 @@ configure_qq() { echo -e " ${BOLD}🐧 QQ 机器人配置${NC}" echo "" - # 检查 qqbot 插件是否已安装 + # 检查 qqbot 插件是否已安装并正常加载 local plugin_installed=0 + local plugin_blocked=0 + local qqbot_ext_dir="${OC_STATE_DIR}/extensions/openclaw-qqbot" if [ -n "$OC_ENTRY" ] && [ -x "$NODE_BIN" ]; then - local plugin_check=$(oc_cmd plugins list 2>/dev/null | grep -i "qqbot" | grep -i "loaded\|disabled") - if [ -n "$plugin_check" ]; then + local plugin_list=$(oc_cmd plugins list 2>&1) + # 在表格输出中查找含 qqbot 的行是否也包含 loaded + if echo "$plugin_list" | grep -i "qqbot" | grep -qi "loaded"; then + plugin_installed=1 + echo -e " ${GREEN}✅ qqbot 插件已安装并加载${NC}" + elif echo "$plugin_list" | grep -qi "plugin not found.*openclaw-qqbot\|suspicious ownership"; then + # 插件目录存在但被阻止 (权限问题或 stale config) + if [ -d "$qqbot_ext_dir" ]; then + plugin_blocked=1 + echo -e " ${YELLOW}⚠️ qqbot 插件已安装但未能正常加载${NC}" + echo -e " ${CYAN}正在修复插件目录权限...${NC}" + chown -R root:root "$qqbot_ext_dir" 2>/dev/null + echo -e " ${GREEN}✅ 权限已修复,重启 Gateway 后生效${NC}" + plugin_installed=1 + fi + elif [ -d "$qqbot_ext_dir" ] && [ -f "${qqbot_ext_dir}/openclaw.plugin.json" ]; then + # 目录存在、有 plugin.json 但未出现在插件列表 — 修复权限 + echo -e " ${YELLOW}⚠️ qqbot 插件目录存在但未能加载${NC}" + echo -e " ${CYAN}正在修复插件目录权限...${NC}" + chown -R root:root "$qqbot_ext_dir" 2>/dev/null + echo -e " ${GREEN}✅ 权限已修复${NC}" plugin_installed=1 - echo -e " ${GREEN}✅ qqbot 插件已安装${NC}" fi fi @@ -1216,20 +1236,36 @@ configure_qq() { local install_out install_out=$(oc_cmd plugins install @tencent-connect/openclaw-qqbot@latest 2>&1) local install_rc=$? + + # 关键: 安装后立即修复插件目录权限为 root (OpenClaw 安全策略要求) + if [ -d "$qqbot_ext_dir" ]; then + chown -R root:root "$qqbot_ext_dir" 2>/dev/null + fi + if [ $install_rc -eq 0 ]; then echo -e " ${GREEN}✅ qqbot 插件安装成功${NC}" plugin_installed=1 else - echo -e " ${RED}❌ 插件安装失败 (exit: $install_rc)${NC}" - echo -e " ${DIM}${install_out}${NC}" | tail -5 + # 安装命令返回非零,可能是因为 config invalid (死锁) + # 检查插件目录是否实际已存在 (说明下载成功但校验报错) + if [ -d "$qqbot_ext_dir" ] && [ -f "${qqbot_ext_dir}/openclaw.plugin.json" ]; then + echo -e " ${YELLOW}⚠️ 插件已下载但加载校验未通过 (exit: $install_rc)${NC}" + echo -e " ${CYAN}这通常是因为配置中已有 qqbot 设置但插件未被信任。${NC}" + echo -e " ${CYAN}已自动修复权限,重启 Gateway 后应能正常加载。${NC}" + plugin_installed=1 + else + echo -e " ${RED}❌ 插件安装失败 (exit: $install_rc)${NC}" + echo -e " ${DIM}${install_out}${NC}" | tail -5 + echo "" + echo -e " ${YELLOW}插件安装失败,但你仍然可以先配置 QQ 机器人参数。${NC}" + echo -e " ${YELLOW}稍后可手动安装: openclaw plugins install @tencent-connect/openclaw-qqbot@latest${NC}" + fi echo "" - echo -e " ${YELLOW}请手动安装: openclaw plugins install @tencent-connect/openclaw-qqbot@latest${NC}" - return fi else - echo -e " ${YELLOW}已跳过插件安装。请先安装 qqbot 插件后再配置。${NC}" - echo -e " ${CYAN}安装命令: openclaw plugins install @tencent-connect/openclaw-qqbot@latest${NC}" - return + echo -e " ${YELLOW}已跳过插件安装,继续配置 QQ 机器人参数。${NC}" + echo -e " ${CYAN}稍后安装命令: openclaw plugins install @tencent-connect/openclaw-qqbot@latest${NC}" + echo "" fi fi @@ -1483,7 +1519,7 @@ telegram_pairing() { echo "" echo -e " ${GREEN}╔══════════════════════════════════════════════════╗${NC}" - echo -e " ${GREEN}║ 请在 Telegram 中向 Bot 发送 /start ║${NC}" + echo -e " ${GREEN}║ 请在 Telegram 中向 Bot 发送 /start ║${NC}" echo -e " ${GREEN}║ 然后回到这里按回车,脚本自动检测配对请求 ║${NC}" echo -e " ${GREEN}╚══════════════════════════════════════════════════╝${NC}" echo "" @@ -1999,26 +2035,39 @@ backup_restore_menu() { echo -e " ${CYAN}将从以下备份恢复:${NC}" echo -e " ${DIM}${latest}${NC}" echo "" - echo -e " ${YELLOW}⚠️ 这会覆盖当前的 openclaw.json 配置!${NC}" + echo -e " ${YELLOW}⚠️ 这会还原备份中的所有配置和数据文件到原路径!${NC}" prompt_with_default "确认恢复? (y/N)" "N" confirm_restore if [ "$confirm_restore" = "y" ] || [ "$confirm_restore" = "Y" ]; then - # 备份当前配置 - cp -f "$CONFIG_FILE" "${CONFIG_FILE}.pre-restore" 2>/dev/null - # 从 tar.gz 中提取 openclaw.json - local tmp_json="${CONFIG_FILE}.tmp" + # 验证备份中 openclaw.json 有效 + local tmp_json="/tmp/oc-restore-check.json" tar -xzf "$latest" --wildcards '*/openclaw.json' -O > "$tmp_json" 2>/dev/null - if [ -s "$tmp_json" ] && "$NODE_BIN" -e "JSON.parse(require('fs').readFileSync('${tmp_json}','utf8'))" 2>/dev/null; then - mv -f "$tmp_json" "$CONFIG_FILE" - chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null - echo -e " ${GREEN}✅ 配置已恢复!原配置已保存为 openclaw.json.pre-restore${NC}" - echo "" - prompt_with_default "是否重启服务使配置生效? (Y/n)" "Y" do_restart - if [ "$do_restart" != "n" ] && [ "$do_restart" != "N" ]; then - restart_gateway - fi - else + if [ ! -s "$tmp_json" ] || ! "$NODE_BIN" -e "JSON.parse(require('fs').readFileSync('${tmp_json}','utf8'))" 2>/dev/null; then rm -f "$tmp_json" echo -e " ${RED}❌ 备份中的配置文件无效,恢复已取消${NC}" + else + rm -f "$tmp_json" + # 备份当前配置 + cp -f "$CONFIG_FILE" "${CONFIG_FILE}.pre-restore" 2>/dev/null + # 获取备份名前缀 + local backup_name=$(tar -tzf "$latest" 2>/dev/null | head -1 | cut -d/ -f1) + if [ -z "$backup_name" ]; then + echo -e " ${RED}❌ 备份文件格式无法识别${NC}" + else + echo -e " ${DIM}正在还原文件...${NC}" + # 停止服务 + /etc/init.d/openclaw stop >/dev/null 2>&1 + sleep 2 + # 提取 payload 到根目录 (还原到原始绝对路径) + tar -xzf "$latest" --strip-components=3 -C / "${backup_name}/payload/posix/" 2>&1 + # 修复权限 + chown -R openclaw:openclaw /opt/openclaw/data/.openclaw 2>/dev/null + echo -e " ${GREEN}✅ 配置和数据已完整恢复!原配置已保存为 openclaw.json.pre-restore${NC}" + echo "" + prompt_with_default "是否重启服务使配置生效? (Y/n)" "Y" do_restart + if [ "$do_restart" != "n" ] && [ "$do_restart" != "N" ]; then + restart_gateway + fi + fi fi else echo -e " ${DIM}已取消${NC}" @@ -2034,7 +2083,7 @@ main_menu() { while true; do echo "" echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" - echo -e "${GREEN}║${NC} ${BOLD}OpenClaw AI Gateway — OpenWrt 配置管理${NC} ${GREEN}║${NC}" + echo -e "${GREEN}║${NC} ${BOLD}OpenClaw AI Gateway — OpenWrt 配置管理${NC} ${GREEN}║${NC}" echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" echo "" echo -e " ${CYAN}1)${NC} 📋 查看当前配置" diff --git a/scripts/build-node-musl.sh b/scripts/build-node-musl.sh index d3cd442..6fbea3c 100755 --- a/scripts/build-node-musl.sh +++ b/scripts/build-node-musl.sh @@ -16,8 +16,12 @@ 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})" -# 打包为 portable tarball (与官方 tarball 相同结构) -PKG_NAME="node-v${NODE_VER}-linux-arm64-musl" +# 使用实际版本号作为文件名 (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" diff --git a/scripts/build_offline_run.sh b/scripts/build_offline_run.sh new file mode 100755 index 0000000..058291d --- /dev/null +++ b/scripts/build_offline_run.sh @@ -0,0 +1,594 @@ +#!/bin/sh +# ============================================================================ +# OpenClaw 离线 .run 自解压包构建脚本 +# 构建包含所有离线依赖的全架构 .run 安装包 +# +# 用法: +# sh scripts/build_offline_run.sh [output_dir] +# +# 前置条件: +# 先运行 sh scripts/download_deps.sh 下载离线依赖到 .offline-cache/ +# +# 产出: +# dist/luci-app-openclaw__x86_64-musl_offline.run +# dist/luci-app-openclaw__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 "openclaw-gateway|openclaw" 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" diff --git a/scripts/build_run.sh b/scripts/build_run.sh index 3af5768..a5b35dc 100755 --- a/scripts/build_run.sh +++ b/scripts/build_run.sh @@ -80,7 +80,7 @@ create_installer() { set -e echo "╔══════════════════════════════════════════════════════════════╗" -echo "║ luci-app-openclaw — OpenClaw AI Gateway 插件 ║" +echo "║ luci-app-openclaw — OpenClaw AI Gateway 插件 ║" echo "╚══════════════════════════════════════════════════════════════╝" echo "" diff --git a/scripts/download_deps.sh b/scripts/download_deps.sh new file mode 100755 index 0000000..8b0b766 --- /dev/null +++ b/scripts/download_deps.sh @@ -0,0 +1,304 @@ +#!/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.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 "╚══════════════════════════════════════════════════════════════╝" diff --git a/scripts/gen-release-body.sh b/scripts/gen-release-body.sh new file mode 100644 index 0000000..7bf0e28 --- /dev/null +++ b/scripts/gen-release-body.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# 用法: gen-release-body.sh <版本号> <输出目录> +# 为指定版本生成 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 <版本号> <输出目录>" + 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 '**离线安装** (无需联网,包含全部依赖)' + echo '```bash' + echo '# 将对应架构的 *_offline.run 传到路由器' + echo 'scp luci-app-openclaw_*_offline.run root@路由器IP:/tmp/' + echo 'ssh root@路由器IP "sh /tmp/luci-app-openclaw_*_offline.run"' + 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") 行)" diff --git a/scripts/sync_openlist.sh b/scripts/sync_openlist.sh new file mode 100755 index 0000000..bc98894 --- /dev/null +++ b/scripts/sync_openlist.sh @@ -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 "" diff --git a/scripts/upload_openlist.sh b/scripts/upload_openlist.sh new file mode 100755 index 0000000..b410374 --- /dev/null +++ b/scripts/upload_openlist.sh @@ -0,0 +1,221 @@ +#!/bin/sh +# ============================================================================ +# OpenList 网盘上传脚本 +# 将构建产物上传到 OpenList (AList) 网盘,便于国内用户下载 +# +# 用法: +# sh scripts/upload_openlist.sh [dist_dir] +# +# 环境变量 (必须): +# OPENLIST_URL — OpenList 服务地址 (如 https://pan.example.com) +# OPENLIST_USER — 登录用户名 +# OPENLIST_PASS — 登录密码 +# +# 环境变量 (可选): +# OPENLIST_PATH — 上传根路径 (默认: /Quark) +# OPENLIST_TOKEN — 直接提供 token, 跳过登录 +# UPLOAD_MODE — 上传模式: offline / online / auto (默认 auto) +# auto 模式自动检测: 有 *_offline.run 则离线, 有 .run/.ipk 则在线 +# ============================================================================ +set -e + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +PKG_DIR=$(cd "$SCRIPT_DIR/.." && pwd) +DIST_DIR="${1:-$PKG_DIR/dist}" + +case "$DIST_DIR" in + /*) ;; + *) DIST_DIR="$PKG_DIR/$DIST_DIR" ;; +esac + +# 检查必要的环境变量 +if [ -z "$OPENLIST_URL" ]; then + echo "错误: 请设置 OPENLIST_URL 环境变量" + echo " 例: export OPENLIST_URL=https://pan.example.com" + exit 1 +fi + +if [ -z "$OPENLIST_TOKEN" ] && { [ -z "$OPENLIST_USER" ] || [ -z "$OPENLIST_PASS" ]; }; then + echo "错误: 请设置登录凭据" + echo " 方式一: export OPENLIST_USER=xxx OPENLIST_PASS=xxx" + echo " 方式二: export OPENLIST_TOKEN=xxx" + exit 1 +fi + +UPLOAD_ROOT="${OPENLIST_PATH:-/Quark}" +UPLOAD_MODE="${UPLOAD_MODE:-auto}" +PKG_VERSION=$(cat "$PKG_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "unknown") + +# 去除路径末尾的 / +UPLOAD_ROOT="${UPLOAD_ROOT%/}" + +# 自动检测上传模式 +if [ "$UPLOAD_MODE" = "auto" ]; then + if ls "$DIST_DIR"/*_offline.run >/dev/null 2>&1; then + UPLOAD_MODE="offline" + elif ls "$DIST_DIR"/*.run >/dev/null 2>&1 || ls "$DIST_DIR"/*.ipk >/dev/null 2>&1; then + UPLOAD_MODE="online" + else + echo "错误: 无法自动检测上传模式,dist 目录中无可识别文件" + exit 1 + fi +fi + +# 根据模式设置子目录和文件匹配规则 +case "$UPLOAD_MODE" in + offline) + UPLOAD_SUBDIR="openclaw-离线安装" + ;; + online) + UPLOAD_SUBDIR="openclaw-在线安装" + ;; + *) + echo "错误: 无效的 UPLOAD_MODE: $UPLOAD_MODE (可选: offline / online / auto)" + exit 1 + ;; +esac + +OPENLIST_URL="${OPENLIST_URL%/}" + +log_info() { echo " [✓] $1"; } +log_warn() { echo " [!] $1"; } +log_error() { echo " [✗] $1"; } + +# ── 获取 Token ── +get_token() { + if [ -n "$OPENLIST_TOKEN" ]; then + echo "$OPENLIST_TOKEN" + return + fi + + log_info "正在登录 OpenList..." >&2 + local resp + resp=$(curl -s -X POST "${OPENLIST_URL}/api/auth/login" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"${OPENLIST_USER}\",\"password\":\"${OPENLIST_PASS}\"}" 2>/dev/null) + + local token + # 尝试解析 JSON 响应 (兼容多种 alist 版本) + # alist v3 响应格式: {"code":200,"data":{"token":"xxx"},"message":"success"} + if command -v jq >/dev/null 2>&1; then + token=$(echo "$resp" | jq -r '.data.token // empty' 2>/dev/null) + else + # 无 jq 时用 grep/sed 提取 + token=$(echo "$resp" | grep -o '"token":"[^"]*"' | sed 's/"token":"//;s/"//') + fi + + if [ -z "$token" ]; then + log_error "登录失败" >&2 + echo " 响应: $resp" >&2 + exit 1 + fi + + log_info "登录成功" >&2 + 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 +} + +# ── 上传单个文件 ── +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) + + echo " 上传: ${filename} (${fsize})..." + + 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 3600 2>/dev/null) + + # 检查响应 + local code="" + if command -v jq >/dev/null 2>&1; then + code=$(echo "$resp" | jq -r '.code // empty' 2>/dev/null) + else + code=$(echo "$resp" | grep -o '"code":[0-9]*' | grep -o '[0-9]*') + fi + + if [ "$code" = "200" ]; then + log_info "${filename} 上传成功" + else + log_error "${filename} 上传失败" + echo " 响应: $resp" + fi +} + +# ── 主流程 ── +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ 上传到 OpenList 网盘 ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" +echo " 服务地址: ${OPENLIST_URL}" +echo " 上传模式: ${UPLOAD_MODE}" +echo " 上传路径: ${UPLOAD_ROOT}/${UPLOAD_SUBDIR}/v${PKG_VERSION}" +echo " 本地目录: ${DIST_DIR}" +echo "" + +# 查找要上传的文件 +UPLOAD_FILES="" +case "$UPLOAD_MODE" in + offline) + # 离线包: 仅 *_offline.run 文件 + UPLOAD_FILES=$(find "$DIST_DIR" -name "*_offline.run" 2>/dev/null) + ;; + online) + # 在线包: .run (非 offline) + .ipk + for f in "$DIST_DIR"/*.run "$DIST_DIR"/*.ipk; do + [ -f "$f" ] || continue + case "$(basename "$f")" in *_offline.run) continue ;; esac + UPLOAD_FILES="$UPLOAD_FILES $f" + done + UPLOAD_FILES=$(echo "$UPLOAD_FILES" | sed 's/^ //') + ;; +esac + +if [ -z "$UPLOAD_FILES" ]; then + echo "错误: 未找到可上传的文件" + echo " 模式: $UPLOAD_MODE" + echo " 目录: $DIST_DIR" + exit 1 +fi + +# 获取 token +TOKEN=$(get_token) + +# 创建远程目录 +REMOTE_DIR="${UPLOAD_ROOT}/${UPLOAD_SUBDIR}/v${PKG_VERSION}" +echo "" +echo "创建远程目录: ${REMOTE_DIR}" +create_remote_dir "$TOKEN" "$REMOTE_DIR" + +# 上传文件 +echo "" +echo "开始上传..." +UPLOAD_COUNT=0 +for f in $UPLOAD_FILES; do + upload_file "$TOKEN" "$f" "$REMOTE_DIR" + UPLOAD_COUNT=$((UPLOAD_COUNT + 1)) +done + +echo "" +echo "╔══════════════════════════════════════════════════════════════╗" +printf "║ ✅ 上传完成!共 %-2s 个文件 ║\n" "$UPLOAD_COUNT" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" +echo "下载地址: ${OPENLIST_URL}${REMOTE_DIR}"