mirror of
https://github.com/hotwa/luci-app-openclaw.git
synced 2026-04-01 13:49:51 +00:00
release: v1.0.15 — QQBot 死锁修复、离线安装包、musl 架构支持
This commit is contained in:
20
.github/workflows/build-node-musl.yml
vendored
20
.github/workflows/build-node-musl.yml
vendored
@@ -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 设备上自动从此处下载。
|
||||
|
||||
136
.github/workflows/build.yml
vendored
136
.github/workflows/build.yml
vendored
@@ -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<<CHANGELOG_EOF"
|
||||
echo "$CHANGELOG"
|
||||
@@ -79,56 +133,60 @@ jobs:
|
||||
with:
|
||||
name: luci-app-openclaw-${{ steps.version.outputs.version }}
|
||||
path: dist/*
|
||||
retention-days: 30
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload to OpenList (online)
|
||||
if: github.event.inputs.upload_openlist == 'true'
|
||||
run: |
|
||||
chmod +x scripts/upload_openlist.sh
|
||||
sh scripts/upload_openlist.sh dist/
|
||||
env:
|
||||
OPENLIST_URL: ${{ secrets.OPENLIST_URL }}
|
||||
OPENLIST_USER: ${{ secrets.OPENLIST_USER }}
|
||||
OPENLIST_PASS: ${{ secrets.OPENLIST_PASS }}
|
||||
OPENLIST_PATH: ${{ secrets.OPENLIST_PATH }}
|
||||
UPLOAD_MODE: online
|
||||
|
||||
- name: Upload to OpenList (offline)
|
||||
if: github.event.inputs.upload_openlist == 'true' && github.event.inputs.build_offline == 'true'
|
||||
run: |
|
||||
sh scripts/upload_openlist.sh dist/
|
||||
env:
|
||||
OPENLIST_URL: ${{ secrets.OPENLIST_URL }}
|
||||
OPENLIST_USER: ${{ secrets.OPENLIST_USER }}
|
||||
OPENLIST_PASS: ${{ secrets.OPENLIST_PASS }}
|
||||
OPENLIST_PATH: ${{ secrets.OPENLIST_PATH }}
|
||||
UPLOAD_MODE: offline
|
||||
|
||||
- name: Create Release
|
||||
if: ${{ github.event.inputs.create_release == 'true' }}
|
||||
if: github.event.inputs.create_release == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.tag }}
|
||||
name: luci-app-openclaw ${{ steps.version.outputs.tag }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
files: dist/*
|
||||
draft: false
|
||||
prerelease: false
|
||||
body: |
|
||||
## luci-app-openclaw ${{ steps.version.outputs.tag }}
|
||||
|
||||
OpenClaw AI 网关的 OpenWrt LuCI 管理插件。
|
||||
|
||||
### 更新日志
|
||||
|
||||
${{ steps.changelog.outputs.content }}
|
||||
|
||||
### 下载说明
|
||||
---
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `luci-app-openclaw_*.run` | 自解压安装包,适用于已运行的 OpenWrt / iStoreOS 系统 |
|
||||
| `luci-app-openclaw_*.ipk` | 标准 opkg 安装包 |
|
||||
|
||||
### 安装方法
|
||||
|
||||
**方式一:.run 安装包(推荐)**
|
||||
```bash
|
||||
# 上传到路由器后执行
|
||||
sh luci-app-openclaw_${{ steps.version.outputs.version }}.run
|
||||
**在线安装** (需联网,自动下载 Node.js + OpenClaw)
|
||||
```
|
||||
# iStoreOS
|
||||
sh luci-app-openclaw_${{ steps.version.outputs.version }}.run
|
||||
|
||||
**方式二:.ipk 安装**
|
||||
```bash
|
||||
# OpenWrt
|
||||
opkg install luci-app-openclaw_${{ steps.version.outputs.version }}-1_all.ipk
|
||||
```
|
||||
|
||||
### 安装后使用
|
||||
**离线安装** (无需联网,包含全部依赖)
|
||||
```bash
|
||||
# 将对应架构的 *_offline.run 传到路由器
|
||||
scp luci-app-openclaw_*_offline.run root@路由器IP:/tmp/
|
||||
ssh root@路由器IP "sh /tmp/luci-app-openclaw_*_offline.run"
|
||||
```
|
||||
|
||||
1. 打开 LuCI → 服务 → OpenClaw
|
||||
2. 点击「安装运行环境」,等待安装完成后服务会自动启动
|
||||
3. 进入「Web 控制台」添加 AI 模型的 API Key 即可使用
|
||||
|
||||
### 兼容性
|
||||
|
||||
- 系统:OpenWrt / iStoreOS / ImmortalWrt / KWRT 23.05+
|
||||
- 架构:x86_64、aarch64
|
||||
- C 库:glibc、musl(自动识别)
|
||||
|
||||
---
|
||||
[使用文档](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/)
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -7,6 +7,12 @@ dist/
|
||||
*.ipk
|
||||
*.run
|
||||
|
||||
# Offline bundle cache (generated by scripts/download_deps.sh)
|
||||
.offline-cache/
|
||||
|
||||
# Runtime data (contains sensitive API keys / tokens)
|
||||
opt/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -4,6 +4,31 @@
|
||||
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
|
||||
|
||||
## [1.0.15] - 2026-03-13
|
||||
|
||||
### 修复
|
||||
- **QQBot 插件 3 层死锁修复**: 解决插件安装后因 uid 权限→安全策略阻止→配置校验失败的连锁问题
|
||||
- 自动检测插件 blocked/loaded/目录存在 3 种状态
|
||||
- 插件安装后自动 `chown root:root` 修复权限
|
||||
- 安装失败但目录存在时不再阻断配置流程
|
||||
|
||||
### 新增
|
||||
- **覆盖安装防护**: 离线安装器在覆盖安装前先停止已有服务,避免文件被占用
|
||||
- **离线 .run 安装包**: 构建包含 Node.js + OpenClaw + LuCI 插件的全合一自解压包,用户**无需联网**即可完成安装
|
||||
- **musl 架构支持**: 离线包支持 x86_64-musl、aarch64-musl 两种架构 (OpenWrt/iStoreOS 均使用 musl)
|
||||
- **依赖预下载脚本** (`scripts/download_deps.sh`): 在构建机上预下载所有离线依赖
|
||||
- **离线构建脚本** (`scripts/build_offline_run.sh`): 将预下载的依赖打包为各架构的离线 .run
|
||||
- **GitHub Actions CI** (`.github/workflows/build-offline.yml`): 离线包自动构建 + 发布
|
||||
- **node_modules 精简**: 自动删除文档、测试、TypeScript 源码等非必要文件,减小 30%+ 体积
|
||||
- **磁盘空间预检查**: 安装前检测可用空间是否满足 500MB 最低要求
|
||||
- **架构/libc 自动检测**: 安装时自动校验当前设备是否匹配安装包架构
|
||||
|
||||
### 文档
|
||||
- **README**: 添加离线安装方式(无需联网),更新目录结构
|
||||
|
||||
### 变更
|
||||
- **离线包不依赖 curl/openssl/git**: 离线安装模式下 opkg 注册的依赖简化为 luci-compat + luci-base
|
||||
|
||||
## [1.0.14] - 2026-03-12
|
||||
|
||||
### 备份管理增强 & QQ 机器人支持
|
||||
|
||||
94
README.md
94
README.md
@@ -3,72 +3,27 @@
|
||||
[](https://space.bilibili.com/59438380)
|
||||
[](https://blog.910501.xyz/)
|
||||
[](https://github.com/10000ge10000/luci-app-openclaw/actions/workflows/build.yml)
|
||||
[](https://github.com/10000ge10000/luci-app-openclaw/actions/workflows/build-offline.yml)
|
||||
[](LICENSE)
|
||||
|
||||
[OpenClaw](https://github.com/nicepkg/openclaw) AI 网关的 OpenWrt LuCI 管理插件。
|
||||
|
||||
在路由器上运行 OpenClaw,通过 LuCI 管理界面完成安装、配置和服务管理。
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/images/1.png" alt="OpenClaw LuCI 管理界面" width="800" style="border-radius:8px;" />
|
||||
</div>
|
||||
|
||||
**系统要求**
|
||||
|
||||
| 项目 | 要求 |
|
||||
|------|------|
|
||||
| 架构 | 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 构建
|
||||
```
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
BIN
docs/images/1.png
Normal file
BIN
docs/images/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 298 KiB |
@@ -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"
|
||||
-- 先备份当前配置
|
||||
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 = "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")
|
||||
-- 从 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 &")
|
||||
|
||||
-- 3) 获取备份名前缀 (如: 2026-03-11T18-21-17.209Z-openclaw-backup)
|
||||
-- 备份结构: <backup_name>/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 层: <backup_name> / 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
|
||||
message = "已从备份完整恢复所有配置和数据,服务正在重启。原配置已保存为 openclaw.json.pre-restore",
|
||||
backup_path = restore_path,
|
||||
extract_output = extract_out or ""
|
||||
})
|
||||
else
|
||||
sys.exec("rm -f " .. config_path .. ".tmp")
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "error", message = "备份文件中的配置无效,恢复已取消" })
|
||||
end
|
||||
elseif action == "list" then
|
||||
-- 返回结构化的备份文件列表(含类型/大小/时间)
|
||||
local files_raw = sys.exec("ls -t " .. backup_dir .. "/*-openclaw-backup.tar.gz 2>/dev/null"):gsub("%s+$", "")
|
||||
|
||||
@@ -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 @@
|
||||
</style>
|
||||
|
||||
<div id="oc-status-panel">
|
||||
<div class="panel-title">🦞 OpenClaw 服务状态</div>
|
||||
<div class="panel-title" id="oc-panel-title">🦞 OpenClaw 服务状态</div>
|
||||
<div class="panel-body">
|
||||
<table>
|
||||
<tr><td>运行状态</td><td id="oc-st-status"><span class="oc-badge oc-badge-unknown">加载中...</span></td></tr>
|
||||
@@ -78,6 +80,7 @@
|
||||
<tr><td>Node.js</td><td id="oc-st-node">-</td></tr>
|
||||
<tr><td>OpenClaw</td><td id="oc-st-oc-ver">-</td></tr>
|
||||
<tr><td>插件版本</td><td id="oc-st-plugin">-</td></tr>
|
||||
<tr><td>安装方式</td><td id="oc-st-install-type">-</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 = '<span class="oc-badge oc-badge-offline" title="' + tip.replace(/"/g,'"') + '">📦 离线版</span>';
|
||||
} else {
|
||||
itEl.innerHTML = '<span class="oc-badge oc-badge-online" title="通过网络在线安装">🌐 在线版</span>';
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
document.getElementById('oc-st-status').innerHTML = '<span class="oc-badge oc-badge-unknown">查询失败</span>';
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
# 安装命令返回非零,可能是因为 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}请手动安装: openclaw plugins install @tencent-connect/openclaw-qqbot@latest${NC}"
|
||||
return
|
||||
echo -e " ${YELLOW}插件安装失败,但你仍然可以先配置 QQ 机器人参数。${NC}"
|
||||
echo -e " ${YELLOW}稍后可手动安装: openclaw plugins install @tencent-connect/openclaw-qqbot@latest${NC}"
|
||||
fi
|
||||
echo ""
|
||||
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
|
||||
|
||||
@@ -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
|
||||
# 验证备份中 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
|
||||
rm -f "$tmp_json"
|
||||
echo -e " ${RED}❌ 备份中的配置文件无效,恢复已取消${NC}"
|
||||
else
|
||||
rm -f "$tmp_json"
|
||||
# 备份当前配置
|
||||
cp -f "$CONFIG_FILE" "${CONFIG_FILE}.pre-restore" 2>/dev/null
|
||||
# 从 tar.gz 中提取 openclaw.json
|
||||
local tmp_json="${CONFIG_FILE}.tmp"
|
||||
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}"
|
||||
# 获取备份名前缀
|
||||
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
|
||||
else
|
||||
rm -f "$tmp_json"
|
||||
echo -e " ${RED}❌ 备份中的配置文件无效,恢复已取消${NC}"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e " ${DIM}已取消${NC}"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
594
scripts/build_offline_run.sh
Executable file
594
scripts/build_offline_run.sh
Executable file
@@ -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_<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 "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"
|
||||
304
scripts/download_deps.sh
Executable file
304
scripts/download_deps.sh
Executable file
@@ -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<version>.tar.gz (完整 node_modules, 跨架构通用, ~150MB)
|
||||
# ============================================================================
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
PKG_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
|
||||
CACHE_DIR="${1:-$PKG_DIR/.offline-cache}"
|
||||
|
||||
# 确保 CACHE_DIR 是绝对路径
|
||||
case "$CACHE_DIR" in
|
||||
/*) ;;
|
||||
*) CACHE_DIR="$PKG_DIR/$CACHE_DIR" ;;
|
||||
esac
|
||||
|
||||
# ── 版本配置 (与 openclaw-env 保持一致) ──
|
||||
NODE_VERSION="${NODE_VERSION:-22.15.1}"
|
||||
OC_VERSION="${OC_VERSION:-2026.3.8}"
|
||||
|
||||
# ── 下载镜像 ──
|
||||
NODE_MIRROR="${NODE_MIRROR:-https://nodejs.org/dist}"
|
||||
NODE_MIRROR_CN="https://npmmirror.com/mirrors/node"
|
||||
NODE_MUSL_MIRROR="https://unofficial-builds.nodejs.org/download/release"
|
||||
NODE_SELF_HOST="https://github.com/10000ge10000/luci-app-openclaw/releases/download/node-bins"
|
||||
NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org}"
|
||||
|
||||
log_info() { echo " [✓] $1"; }
|
||||
log_warn() { echo " [!] $1"; }
|
||||
log_error() { echo " [✗] $1"; }
|
||||
|
||||
# 自动检测 Node.js / npm (兼容 OpenWrt 上已安装的 openclaw 环境)
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
for try_path in /opt/openclaw/node/bin /usr/local/bin; do
|
||||
if [ -x "$try_path/node" ]; then
|
||||
export PATH="$try_path:$PATH"
|
||||
log_info "检测到 Node.js: $try_path/node"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# 下载文件 (支持 curl 和 wget)
|
||||
download_file() {
|
||||
local url="$1" dest="$2"
|
||||
if [ -f "$dest" ]; then
|
||||
local fsize=$(wc -c < "$dest" 2>/dev/null || echo 0)
|
||||
if [ "$fsize" -gt 1000000 ] 2>/dev/null; then
|
||||
log_info "已缓存: $(basename "$dest") ($(du -h "$dest" | cut -f1))"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
echo " 下载: $url"
|
||||
if curl -fSL --connect-timeout 30 --max-time 600 -o "$dest" "$url" 2>/dev/null; then
|
||||
return 0
|
||||
elif wget -q --timeout=30 -O "$dest" "$url" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
rm -f "$dest"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Phase 1: 下载 Node.js (全架构)
|
||||
# ============================================================================
|
||||
download_all_node() {
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
printf "║ [1/3] 下载 Node.js v%-8s (musl 架构) ║\n" "$NODE_VERSION"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
local node_dir="$CACHE_DIR/node"
|
||||
mkdir -p "$node_dir"
|
||||
|
||||
# x86_64 musl
|
||||
echo "=== x86_64 musl ==="
|
||||
local x64_musl="node-v${NODE_VERSION}-linux-x64-musl.tar.xz"
|
||||
download_file "${NODE_MUSL_MIRROR}/v${NODE_VERSION}/${x64_musl}" "$node_dir/$x64_musl" || \
|
||||
download_file "${NODE_MIRROR_CN}/v${NODE_VERSION}/${x64_musl}" "$node_dir/$x64_musl" || \
|
||||
log_error "x86_64 musl 下载失败"
|
||||
|
||||
# aarch64 musl (项目自托管)
|
||||
echo "=== aarch64 musl ==="
|
||||
local arm64_musl="node-v${NODE_VERSION}-linux-arm64-musl.tar.xz"
|
||||
download_file "${NODE_SELF_HOST}/${arm64_musl}" "$node_dir/$arm64_musl" || \
|
||||
download_file "${NODE_MUSL_MIRROR}/v${NODE_VERSION}/${arm64_musl}" "$node_dir/$arm64_musl" || \
|
||||
log_error "aarch64 musl 下载失败"
|
||||
|
||||
echo ""
|
||||
echo "Node.js 下载完成:"
|
||||
ls -lh "$node_dir/"*.tar.xz 2>/dev/null || echo " (无文件)"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Phase 2: 下载并预装 OpenClaw + 依赖
|
||||
# ============================================================================
|
||||
download_openclaw_deps() {
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
printf "║ [2/3] 下载 OpenClaw v%-8s + 全部依赖 ║\n" "$OC_VERSION"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
local oc_dir="$CACHE_DIR/openclaw"
|
||||
mkdir -p "$oc_dir"
|
||||
|
||||
# 检查 npm 是否可用 (构建机上需要 node + npm)
|
||||
local NPM_CMD=""
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
NPM_CMD="npm"
|
||||
elif [ -x /opt/openclaw/node/bin/npm ]; then
|
||||
# OpenWrt 上 npm wrapper 可能需要显式 node 调用
|
||||
NPM_CMD="/opt/openclaw/node/bin/node /opt/openclaw/node/bin/npm"
|
||||
else
|
||||
log_error "构建机上需要 npm"
|
||||
log_error "请执行: apt install -y nodejs npm 或 apk add nodejs npm"
|
||||
log_error "或者确保 /opt/openclaw/node 中有 Node.js"
|
||||
exit 1
|
||||
fi
|
||||
log_info "使用 npm: $NPM_CMD"
|
||||
|
||||
# 方案: 使用 npm install 到临时目录,然后打包整个 node_modules
|
||||
# 这是最可靠的方式,确保所有依赖树完整
|
||||
|
||||
local tmp_install="/tmp/openclaw-offline-$$"
|
||||
trap "rm -rf '$tmp_install'" EXIT
|
||||
|
||||
# ── 为每种架构生成预安装包 ──
|
||||
# 注意: openclaw 的依赖树中可能包含平台特定的 optional dependencies
|
||||
# musl 环境下用 --ignore-scripts 跳过原生编译
|
||||
# 对于离线包,我们在构建机上安装后直接打包 node_modules
|
||||
|
||||
# 通用安装 (忽略平台特定编译脚本)
|
||||
echo "=== 安装 OpenClaw 依赖 (通用包) ==="
|
||||
mkdir -p "$tmp_install/global"
|
||||
|
||||
# ── 强制 npm 使用 musl 平台检测 ──
|
||||
# 目标系统是 OpenWrt/iStoreOS (musl libc),无论构建机是什么系统
|
||||
# 在 glibc 系统 (如 GitHub Actions ubuntu-latest) 上,npm 默认安装 *-gnu 变体
|
||||
# 的原生可选依赖 (如 @napi-rs/canvas-linux-x64-gnu),比 *-musl 变体大得多
|
||||
# 通过设置 npm_config_libc=musl 强制安装 musl 变体,确保跨平台一致的产物大小
|
||||
export npm_config_os=linux
|
||||
export npm_config_libc=musl
|
||||
|
||||
echo " 正在用 npm 安装 openclaw@${OC_VERSION}..."
|
||||
echo " npm 平台覆盖: os=${npm_config_os}, libc=${npm_config_libc}"
|
||||
$NPM_CMD install -g "openclaw@${OC_VERSION}" \
|
||||
--prefix="$tmp_install/global" \
|
||||
--ignore-scripts \
|
||||
--omit=dev \
|
||||
--omit=optional \
|
||||
--no-optional \
|
||||
--registry="$NPM_REGISTRY" 2>&1 | tail -20
|
||||
|
||||
# 验证安装
|
||||
local oc_entry=""
|
||||
for d in "$tmp_install/global/lib/node_modules/openclaw" "$tmp_install/global/node_modules/openclaw"; do
|
||||
if [ -f "${d}/openclaw.mjs" ]; then
|
||||
oc_entry="${d}/openclaw.mjs"
|
||||
break
|
||||
elif [ -f "${d}/dist/cli.js" ]; then
|
||||
oc_entry="${d}/dist/cli.js"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$oc_entry" ]; then
|
||||
log_error "OpenClaw 安装验证失败"
|
||||
echo "目录内容:"
|
||||
find "$tmp_install/global" -maxdepth 4 -type d 2>/dev/null | head -30
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "OpenClaw 安装验证通过: $oc_entry"
|
||||
|
||||
# 获取实际安装的版本号
|
||||
local actual_ver=""
|
||||
local oc_pkg_dir="$(dirname "$oc_entry")"
|
||||
if [ -f "$oc_pkg_dir/package.json" ]; then
|
||||
actual_ver=$(node -e "console.log(require('$oc_pkg_dir/package.json').version)" 2>/dev/null || echo "$OC_VERSION")
|
||||
fi
|
||||
echo " 实际版本: v${actual_ver:-$OC_VERSION}"
|
||||
|
||||
# ── 打包为通用 tarball ──
|
||||
# 不精简 node_modules,保留全部功能(飞书/Slack/Discord 等平台 SDK)
|
||||
# 依靠 gzip 压缩控制包大小: ~670MB → ~150MB
|
||||
# 因为 openclaw 是纯 JS 包 (使用 --ignore-scripts),node_modules 跨架构通用
|
||||
echo ""
|
||||
echo "=== 打包 OpenClaw 依赖 ==="
|
||||
local install_size=$(du -sm "$tmp_install/global" 2>/dev/null | awk '{print $1}')
|
||||
local tarball="$oc_dir/openclaw-deps-v${actual_ver:-$OC_VERSION}.tar.gz"
|
||||
echo " 正在压缩 ${install_size}MB 数据到 tar.gz (可能需要数分钟)..."
|
||||
# 注意: 不在子 shell 中用 set -e, 避免 tar 在处理损坏的符号链接时意外退出
|
||||
# --warning=no-file-changed: 忽略打包过程中文件被修改的警告
|
||||
if ! tar czf "$tarball" -C "$tmp_install/global" . 2>&1; then
|
||||
log_error "tar 打包失败"
|
||||
rm -f "$tarball"
|
||||
exit 1
|
||||
fi
|
||||
# 验证 gzip 完整性
|
||||
if ! gzip -t "$tarball" 2>/dev/null; then
|
||||
log_error "tar.gz 文件完整性检查失败!"
|
||||
rm -f "$tarball"
|
||||
exit 1
|
||||
fi
|
||||
local tgz_size=$(du -h "$tarball" | cut -f1)
|
||||
log_info "依赖包: $tarball ($tgz_size) [完整性已验证]"
|
||||
|
||||
# 保存版本号
|
||||
echo "${actual_ver:-$OC_VERSION}" > "$oc_dir/VERSION"
|
||||
|
||||
rm -rf "$tmp_install"
|
||||
# 清除之前的 trap
|
||||
trap - EXIT
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Phase 3: 生成清单
|
||||
# ============================================================================
|
||||
generate_manifest() {
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ [3/3] 生成构建清单 ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
local manifest="$CACHE_DIR/manifest.txt"
|
||||
local oc_ver=$(cat "$CACHE_DIR/openclaw/VERSION" 2>/dev/null || echo "$OC_VERSION")
|
||||
|
||||
cat > "$manifest" << EOF
|
||||
# OpenClaw Offline Bundle - 依赖清单
|
||||
# 生成时间: $(date -Iseconds 2>/dev/null || date)
|
||||
# Node.js: v${NODE_VERSION}
|
||||
# OpenClaw: v${oc_ver}
|
||||
|
||||
[node]
|
||||
EOF
|
||||
|
||||
# 列出 Node.js 包
|
||||
for f in "$CACHE_DIR/node/"*.tar.xz; do
|
||||
[ -f "$f" ] || continue
|
||||
local fname=$(basename "$f")
|
||||
local fsize=$(du -h "$f" | cut -f1)
|
||||
local sha256=$(sha256sum "$f" 2>/dev/null | awk '{print $1}' || echo "N/A")
|
||||
echo "${fname} size=${fsize} sha256=${sha256}" >> "$manifest"
|
||||
done
|
||||
|
||||
echo "" >> "$manifest"
|
||||
echo "[openclaw]" >> "$manifest"
|
||||
|
||||
# 列出 OpenClaw 包
|
||||
for f in "$CACHE_DIR/openclaw/"*.tar.gz; do
|
||||
[ -f "$f" ] || continue
|
||||
local fname=$(basename "$f")
|
||||
local fsize=$(du -h "$f" | cut -f1)
|
||||
local sha256=$(sha256sum "$f" 2>/dev/null | awk '{print $1}' || echo "N/A")
|
||||
echo "${fname} size=${fsize} sha256=${sha256}" >> "$manifest"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== 依赖清单 ==="
|
||||
cat "$manifest"
|
||||
echo ""
|
||||
|
||||
# 统计总大小
|
||||
local total_size=$(du -sh "$CACHE_DIR" 2>/dev/null | awk '{print $1}')
|
||||
echo "缓存目录: $CACHE_DIR"
|
||||
echo "总大小: $total_size"
|
||||
}
|
||||
|
||||
# ── 主入口 ──
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ OpenClaw 离线依赖下载器 ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " Node.js: v${NODE_VERSION}"
|
||||
echo " OpenClaw: v${OC_VERSION}"
|
||||
echo " 缓存目录: ${CACHE_DIR}"
|
||||
echo " npm 源: ${NPM_REGISTRY}"
|
||||
echo ""
|
||||
|
||||
mkdir -p "$CACHE_DIR"
|
||||
|
||||
download_all_node
|
||||
download_openclaw_deps
|
||||
generate_manifest
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ✅ 依赖下载完成!现在可以运行 build_offline_run.sh ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
48
scripts/gen-release-body.sh
Normal file
48
scripts/gen-release-body.sh
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/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 '**离线安装** (无需联网,包含全部依赖)'
|
||||
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") 行)"
|
||||
230
scripts/sync_openlist.sh
Executable file
230
scripts/sync_openlist.sh
Executable 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 ""
|
||||
221
scripts/upload_openlist.sh
Executable file
221
scripts/upload_openlist.sh
Executable file
@@ -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}"
|
||||
Reference in New Issue
Block a user