mirror of
https://github.com/hotwa/luci-app-openclaw.git
synced 2026-04-01 05:25:42 +00:00
release: v1.0.0 — LuCI 管理界面、一键安装、12+ AI 模型提供商
This commit is contained in:
114
.github/workflows/build.yml
vendored
Normal file
114
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
name: Build & Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: '版本号 (留空自动生成,格式: 1.0.0)'
|
||||
required: false
|
||||
default: ''
|
||||
create_release:
|
||||
description: '是否创建 Release'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build packages
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
INPUT_VER="${{ github.event.inputs.version }}"
|
||||
if [ -n "$INPUT_VER" ]; then
|
||||
VER="$INPUT_VER"
|
||||
elif [ -f VERSION ]; then
|
||||
VER="$(cat VERSION | tr -d '[:space:]')"
|
||||
else
|
||||
VER="$(date -u +%Y.%m.%d)"
|
||||
fi
|
||||
echo "version=$VER" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=v$VER" >> "$GITHUB_OUTPUT"
|
||||
echo "Version: $VER"
|
||||
|
||||
- name: Inject version
|
||||
run: |
|
||||
VER="${{ steps.version.outputs.version }}"
|
||||
echo "$VER" > VERSION
|
||||
# 同步到 Makefile
|
||||
sed -i "s/^PKG_VERSION:=.*/PKG_VERSION:=$VER/" Makefile
|
||||
|
||||
- name: Build .run installer
|
||||
run: |
|
||||
chmod +x scripts/build_run.sh
|
||||
sh scripts/build_run.sh dist
|
||||
|
||||
- name: Build .ipk package
|
||||
run: |
|
||||
chmod +x scripts/build_ipk.sh
|
||||
sh scripts/build_ipk.sh dist
|
||||
|
||||
- name: List outputs
|
||||
run: ls -lh dist/
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: luci-app-openclaw-${{ steps.version.outputs.version }}
|
||||
path: dist/*
|
||||
|
||||
- name: Create Release
|
||||
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 }}
|
||||
files: dist/*
|
||||
draft: false
|
||||
prerelease: false
|
||||
body: |
|
||||
## luci-app-openclaw ${{ steps.version.outputs.tag }}
|
||||
|
||||
OpenClaw AI 网关的 OpenWrt LuCI 管理插件。
|
||||
|
||||
### 下载说明
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `luci-app-openclaw_*.run` | 自解压安装包,适用于已运行的 OpenWrt / iStoreOS 系统 |
|
||||
| `luci-app-openclaw_*.ipk` | 标准 opkg 安装包 |
|
||||
|
||||
### 安装方法
|
||||
|
||||
**方式一:.run 安装包(推荐)**
|
||||
```bash
|
||||
# 上传到路由器后执行
|
||||
sh luci-app-openclaw_${{ steps.version.outputs.version }}.run
|
||||
```
|
||||
|
||||
**方式二:.ipk 安装**
|
||||
```bash
|
||||
opkg install luci-app-openclaw_${{ steps.version.outputs.version }}-1_all.ipk
|
||||
```
|
||||
|
||||
### 安装后使用
|
||||
|
||||
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/)
|
||||
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Node
|
||||
node_modules/
|
||||
*.tgz
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
*.ipk
|
||||
*.run
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
39
CHANGELOG.md
Normal file
39
CHANGELOG.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
本项目所有重大变更都将记录在此文件中。
|
||||
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
|
||||
|
||||
## [1.0.0] - 2026-03-02
|
||||
|
||||
### 新增
|
||||
- LuCI 管理界面:基本设置、配置管理(Web 终端)、Web 控制台
|
||||
- 一键安装 Node.js + OpenClaw 运行环境
|
||||
- 支持 x86_64 和 aarch64 架构,glibc / musl 自动检测
|
||||
- 支持 12+ AI 模型提供商配置向导
|
||||
- 支持 Telegram / Discord / 飞书 / Slack 消息渠道
|
||||
- `.run` 自解压包和 `.ipk` 安装包两种分发方式
|
||||
- OpenWrt SDK feeds 集成支持
|
||||
- GitHub Actions 自动构建与发布
|
||||
|
||||
### 安全
|
||||
- WebSocket PTY 服务添加 token 认证
|
||||
- WebSocket 最大并发会话限制(默认 5)
|
||||
- PTY 服务默认绑定 127.0.0.1,不对外暴露
|
||||
- Token 不再嵌入 HTML 源码,改为 AJAX 动态获取
|
||||
- sync_uci_to_json 通过环境变量传递 token,避免 ps 泄露
|
||||
- 所有渠道 Token 输入统一 sanitize_input 清洗
|
||||
|
||||
### 修复
|
||||
- Telegram Bot Token 粘贴时被 bracketed paste 转义序列污染
|
||||
- Web PTY 终端粘贴包含 ANSI 转义序列问题
|
||||
- 恢复出厂配置流程异常退出
|
||||
- Gemini CLI OAuth 登录在 OpenWrt 上失败
|
||||
- init.d status_service() 在无 netstat 的系统上报错
|
||||
- Makefile 损坏导致 OpenWrt SDK 编译失败
|
||||
|
||||
### 改进
|
||||
- 所有 AI 提供商模型列表更新到最新版本
|
||||
- UID/GID 动态分配,避免与已有系统用户冲突
|
||||
- 版本号统一由 VERSION 文件管理
|
||||
- README.md 完善安装说明、FAQ 和项目结构
|
||||
26
LICENSE
Normal file
26
LICENSE
Normal file
@@ -0,0 +1,26 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (c) 2025-2026 10000ge10000
|
||||
Project: luci-app-openclaw — OpenClaw AI Gateway LuCI Plugin for OpenWrt
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
The full text of the GNU General Public License version 3 is available at:
|
||||
https://www.gnu.org/licenses/gpl-3.0.html
|
||||
SOFTWARE.
|
||||
|
||||
This project integrates with OpenClaw (https://github.com/nicepkg/openclaw),
|
||||
which is licensed under its own terms. Node.js runtime downloaded during setup
|
||||
is subject to the Node.js license (https://github.com/nodejs/node/blob/main/LICENSE).
|
||||
92
Makefile
Normal file
92
Makefile
Normal file
@@ -0,0 +1,92 @@
|
||||
# luci-app-openclaw — OpenWrt package Makefile
|
||||
# 兼容两种集成方式:
|
||||
# 1. 作为 feeds 源: echo "src-git openclaw ..." >> feeds.conf.default
|
||||
# 2. 直接放入 package/ 目录: git clone ... package/luci-app-openclaw
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-openclaw
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=10000ge10000 <10000ge10000@users.noreply.github.com>
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
|
||||
LUCI_TITLE:=OpenClaw AI 网关 LuCI 管理插件
|
||||
LUCI_DEPENDS:=+luci-compat +luci-base +curl +openssl-util
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
# 优先使用 luci.mk (feeds 模式), 不可用时回退 package.mk
|
||||
ifeq ($(wildcard $(TOPDIR)/feeds/luci/luci.mk),)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=$(LUCI_TITLE)
|
||||
DEPENDS:=$(LUCI_DEPENDS)
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
OpenClaw AI Gateway 的 LuCI 管理插件。
|
||||
支持 12+ AI 模型提供商和 Telegram/Discord 等多种消息渠道。
|
||||
endef
|
||||
|
||||
else
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
endif
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/openclaw
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./root/etc/config/openclaw $(1)/etc/config/openclaw
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_BIN) ./root/etc/uci-defaults/99-openclaw $(1)/etc/uci-defaults/99-openclaw
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./root/etc/init.d/openclaw $(1)/etc/init.d/openclaw
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) ./root/usr/bin/openclaw-env $(1)/usr/bin/openclaw-env
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller
|
||||
$(INSTALL_DATA) ./luasrc/controller/openclaw.lua $(1)/usr/lib/lua/luci/controller/openclaw.lua
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/model/cbi/openclaw
|
||||
$(INSTALL_DATA) ./luasrc/model/cbi/openclaw/basic.lua $(1)/usr/lib/lua/luci/model/cbi/openclaw/basic.lua
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/view/openclaw
|
||||
$(INSTALL_DATA) ./luasrc/view/openclaw/status.htm $(1)/usr/lib/lua/luci/view/openclaw/status.htm
|
||||
$(INSTALL_DATA) ./luasrc/view/openclaw/advanced.htm $(1)/usr/lib/lua/luci/view/openclaw/advanced.htm
|
||||
$(INSTALL_DATA) ./luasrc/view/openclaw/console.htm $(1)/usr/lib/lua/luci/view/openclaw/console.htm
|
||||
$(INSTALL_DIR) $(1)/usr/share/openclaw
|
||||
$(INSTALL_BIN) ./root/usr/share/openclaw/oc-config.sh $(1)/usr/share/openclaw/oc-config.sh
|
||||
$(INSTALL_DATA) ./root/usr/share/openclaw/web-pty.js $(1)/usr/share/openclaw/web-pty.js
|
||||
$(INSTALL_DIR) $(1)/usr/share/openclaw/ui
|
||||
$(CP) ./root/usr/share/openclaw/ui/* $(1)/usr/share/openclaw/ui/
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
|
||||
if [ -f ./po/zh-cn/openclaw.po ]; then \
|
||||
po2lmo ./po/zh-cn/openclaw.po $(1)/usr/lib/lua/luci/i18n/openclaw.zh-cn.lmo 2>/dev/null || true; \
|
||||
fi
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
( . /etc/uci-defaults/99-openclaw ) && rm -f /etc/uci-defaults/99-openclaw
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postrm
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null
|
||||
}
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
155
README.md
Normal file
155
README.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# luci-app-openclaw
|
||||
|
||||
[](https://space.bilibili.com/59438380)
|
||||
[](https://blog.910501.xyz/)
|
||||
[](https://github.com/10000ge10000/luci-app-openclaw/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
|
||||
[OpenClaw](https://github.com/nicepkg/openclaw) AI 网关的 OpenWrt LuCI 管理插件。
|
||||
|
||||
在路由器上运行 OpenClaw,通过 LuCI 管理界面完成安装、配置和服务管理。
|
||||
|
||||
**系统要求**
|
||||
|
||||
| 项目 | 要求 |
|
||||
|------|------|
|
||||
| 架构 | x86_64 或 aarch64 |
|
||||
| C 库 | glibc 或 musl(自动检测) |
|
||||
| 依赖 | luci-compat, luci-base, curl, openssl-util |
|
||||
| 存储 | 1.5GB 以上可用空间 |
|
||||
| 内存 | 推荐 2GB 及以上 |
|
||||
|
||||
## 📦 安装
|
||||
|
||||
### 方式一:.run 自解压包(推荐)
|
||||
|
||||
无需 SDK,适用于已安装好的系统。
|
||||
|
||||
```bash
|
||||
wget https://github.com/10000ge10000/luci-app-openclaw/releases/latest/download/luci-app-openclaw.run
|
||||
sh luci-app-openclaw.run
|
||||
```
|
||||
|
||||
### 方式二:.ipk 安装
|
||||
|
||||
```bash
|
||||
wget https://github.com/10000ge10000/luci-app-openclaw/releases/latest/download/luci-app-openclaw.ipk
|
||||
opkg install luci-app-openclaw.ipk
|
||||
```
|
||||
|
||||
### 方式三:集成到固件编译
|
||||
|
||||
适用于自行编译固件或使用在线编译平台的用户。
|
||||
|
||||
```bash
|
||||
cd /path/to/openwrt
|
||||
|
||||
# 添加 feeds
|
||||
echo "src-git openclaw https://github.com/10000ge10000/luci-app-openclaw.git" >> feeds.conf.default
|
||||
|
||||
# 更新安装
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install -a
|
||||
|
||||
# 选择插件
|
||||
make menuconfig
|
||||
# LuCI → Applications → luci-app-openclaw
|
||||
|
||||
# 编译
|
||||
make package/luci-app-openclaw/compile V=s
|
||||
```
|
||||
|
||||
使用 OpenWrt SDK 单独编译:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/10000ge10000/luci-app-openclaw.git package/luci-app-openclaw
|
||||
make defconfig
|
||||
make package/luci-app-openclaw/compile V=s
|
||||
find bin/ -name "luci-app-openclaw*.ipk"
|
||||
```
|
||||
|
||||
### 方式四:手动安装
|
||||
|
||||
```bash
|
||||
git clone https://github.com/10000ge10000/luci-app-openclaw.git
|
||||
cd luci-app-openclaw
|
||||
|
||||
cp -r root/* /
|
||||
mkdir -p /usr/lib/lua/luci/controller /usr/lib/lua/luci/model/cbi/openclaw /usr/lib/lua/luci/view/openclaw
|
||||
cp luasrc/controller/openclaw.lua /usr/lib/lua/luci/controller/
|
||||
cp luasrc/model/cbi/openclaw/*.lua /usr/lib/lua/luci/model/cbi/openclaw/
|
||||
cp luasrc/view/openclaw/*.htm /usr/lib/lua/luci/view/openclaw/
|
||||
|
||||
chmod +x /etc/init.d/openclaw /usr/bin/openclaw-env /usr/share/openclaw/oc-config.sh
|
||||
sh /etc/uci-defaults/99-openclaw
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/*
|
||||
```
|
||||
|
||||
## 🔰 首次使用
|
||||
|
||||
1. 打开 LuCI → 服务 → OpenClaw,点击「安装运行环境」
|
||||
2. 安装完成后服务会自动启动,点击「刷新页面」查看状态
|
||||
3. 进入「Web 控制台」添加 AI 模型和 API Key
|
||||
4. 进入「配置管理」可使用向导配置消息渠道
|
||||
|
||||
## 📂 目录结构
|
||||
|
||||
```
|
||||
luci-app-openclaw/
|
||||
├── Makefile # OpenWrt 包定义
|
||||
├── luasrc/
|
||||
│ ├── controller/openclaw.lua # LuCI 路由和 API
|
||||
│ ├── model/cbi/openclaw/basic.lua # 主页面
|
||||
│ └── view/openclaw/
|
||||
│ ├── status.htm # 状态面板
|
||||
│ ├── advanced.htm # 配置管理(终端)
|
||||
│ └── console.htm # Web 控制台
|
||||
├── root/
|
||||
│ ├── etc/
|
||||
│ │ ├── config/openclaw # UCI 配置
|
||||
│ │ ├── init.d/openclaw # 服务脚本
|
||||
│ │ └── uci-defaults/99-openclaw # 初始化脚本
|
||||
│ └── usr/
|
||||
│ ├── bin/openclaw-env # 环境管理工具
|
||||
│ └── share/openclaw/ # 配置终端资源
|
||||
├── scripts/
|
||||
│ ├── build_ipk.sh # 本地 IPK 构建
|
||||
│ └── build_run.sh # .run 安装包构建
|
||||
└── .github/workflows/build.yml # GitHub Actions
|
||||
```
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
**安装后 LuCI 菜单没有出现**
|
||||
|
||||
```bash
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/*
|
||||
```
|
||||
|
||||
刷新浏览器即可。
|
||||
|
||||
**提示缺少依赖 luci-compat**
|
||||
|
||||
```bash
|
||||
opkg update && opkg install luci-compat
|
||||
```
|
||||
|
||||
**Node.js 下载失败**
|
||||
|
||||
网络问题,可指定国内镜像:
|
||||
|
||||
```bash
|
||||
NODE_MIRROR=https://npmmirror.com/mirrors/node openclaw-env setup
|
||||
```
|
||||
|
||||
**是否支持 ARM 路由器**
|
||||
|
||||
支持 aarch64(ARM64)。不支持 32 位 ARM,Node.js 22 没有 32 位预编译包。
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 📄 License
|
||||
|
||||
[GPL-3.0](LICENSE)
|
||||
384
luasrc/controller/openclaw.lua
Normal file
384
luasrc/controller/openclaw.lua
Normal file
@@ -0,0 +1,384 @@
|
||||
-- luci-app-openclaw — LuCI Controller
|
||||
module("luci.controller.openclaw", package.seeall)
|
||||
|
||||
-- 公共辅助: 获取 OpenClaw 版本号
|
||||
local function get_openclaw_version()
|
||||
local sys = require "luci.sys"
|
||||
local ver = sys.exec("[ -x /opt/openclaw/node/bin/node ] && for d in /opt/openclaw/global/lib/node_modules/openclaw /opt/openclaw/global/node_modules/openclaw /opt/openclaw/global/*/node_modules/openclaw; do [ -f \"$d/openclaw.mjs\" ] && /opt/openclaw/node/bin/node \"$d/openclaw.mjs\" --version 2>/dev/null && break; [ -f \"$d/dist/cli.js\" ] && /opt/openclaw/node/bin/node \"$d/dist/cli.js\" --version 2>/dev/null && break; done"):gsub("%s+", "")
|
||||
return ver
|
||||
end
|
||||
|
||||
function index()
|
||||
-- 主入口: 服务 → OpenClaw (🧠 作为菜单图标)
|
||||
local page = entry({"admin", "services", "openclaw"}, alias("admin", "services", "openclaw", "basic"), _("OpenClaw"), 90)
|
||||
page.dependent = false
|
||||
|
||||
-- 基本设置 (CBI)
|
||||
entry({"admin", "services", "openclaw", "basic"}, cbi("openclaw/basic"), _("基本设置"), 10).leaf = true
|
||||
|
||||
-- 配置管理 (View — 嵌入 oc-config Web 终端)
|
||||
entry({"admin", "services", "openclaw", "advanced"}, template("openclaw/advanced"), _("配置管理"), 20).leaf = true
|
||||
|
||||
-- Web 控制台 (View — 嵌入 OpenClaw Web UI)
|
||||
entry({"admin", "services", "openclaw", "console"}, template("openclaw/console"), _("Web 控制台"), 30).leaf = true
|
||||
|
||||
-- 状态 API (AJAX 接口, 供前端 XHR 调用)
|
||||
entry({"admin", "services", "openclaw", "status_api"}, call("action_status"), nil).leaf = true
|
||||
|
||||
-- 服务控制 API
|
||||
entry({"admin", "services", "openclaw", "service_ctl"}, call("action_service_ctl"), nil).leaf = true
|
||||
|
||||
-- 安装/升级日志 API (轮询)
|
||||
entry({"admin", "services", "openclaw", "setup_log"}, call("action_setup_log"), nil).leaf = true
|
||||
|
||||
-- 版本检查 API
|
||||
entry({"admin", "services", "openclaw", "check_update"}, call("action_check_update"), nil).leaf = true
|
||||
|
||||
-- 执行升级 API
|
||||
entry({"admin", "services", "openclaw", "do_update"}, call("action_do_update"), nil).leaf = true
|
||||
|
||||
-- 升级日志 API (轮询)
|
||||
entry({"admin", "services", "openclaw", "upgrade_log"}, call("action_upgrade_log"), nil).leaf = true
|
||||
|
||||
-- 卸载运行环境 API
|
||||
entry({"admin", "services", "openclaw", "uninstall"}, call("action_uninstall"), nil).leaf = true
|
||||
|
||||
-- 获取网关 Token API (仅认证用户可访问)
|
||||
entry({"admin", "services", "openclaw", "get_token"}, call("action_get_token"), nil).leaf = true
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════
|
||||
-- 状态查询 API: 返回 JSON
|
||||
-- ═══════════════════════════════════════════
|
||||
function action_status()
|
||||
local http = require "luci.http"
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
local pty_port = uci:get("openclaw", "main", "pty_port") or "18793"
|
||||
local enabled = uci:get("openclaw", "main", "enabled") or "0"
|
||||
|
||||
-- 验证端口值为纯数字,防止命令注入
|
||||
if not port:match("^%d+$") then port = "18789" end
|
||||
if not pty_port:match("^%d+$") then pty_port = "18793" end
|
||||
|
||||
local result = {
|
||||
enabled = enabled,
|
||||
port = port,
|
||||
pty_port = pty_port,
|
||||
gateway_running = false,
|
||||
pty_running = false,
|
||||
pid = "",
|
||||
memory_kb = 0,
|
||||
uptime = "",
|
||||
node_version = "",
|
||||
openclaw_version = "",
|
||||
}
|
||||
|
||||
-- 检查 Node.js
|
||||
local node_bin = "/opt/openclaw/node/bin/node"
|
||||
local f = io.open(node_bin, "r")
|
||||
if f then
|
||||
f:close()
|
||||
local node_ver = sys.exec(node_bin .. " --version 2>/dev/null"):gsub("%s+", "")
|
||||
result.node_version = node_ver
|
||||
end
|
||||
|
||||
-- 检查 OpenClaw 版本
|
||||
local oc_ver = get_openclaw_version()
|
||||
if oc_ver and oc_ver ~= "" then
|
||||
result.openclaw_version = "v" .. oc_ver
|
||||
end
|
||||
|
||||
-- 网关端口检查
|
||||
local gw_check = sys.exec("netstat -tlnp 2>/dev/null | grep -c ':" .. port .. " ' || echo 0"):gsub("%s+", "")
|
||||
result.gateway_running = (tonumber(gw_check) or 0) > 0
|
||||
|
||||
-- PTY 端口检查
|
||||
local pty_check = sys.exec("netstat -tlnp 2>/dev/null | grep -c ':" .. pty_port .. " ' || echo 0"):gsub("%s+", "")
|
||||
result.pty_running = (tonumber(pty_check) or 0) > 0
|
||||
|
||||
-- PID 和内存
|
||||
if result.gateway_running then
|
||||
local pid = sys.exec("netstat -tlnp 2>/dev/null | grep ':" .. port .. " ' | head -1 | sed 's|.* \\([0-9]*\\)/.*|\\1|'"):gsub("%s+", "")
|
||||
if pid and pid ~= "" then
|
||||
result.pid = pid
|
||||
-- 内存 (VmRSS from /proc)
|
||||
local rss = sys.exec("awk '/VmRSS/{print $2}' /proc/" .. pid .. "/status 2>/dev/null"):gsub("%s+", "")
|
||||
result.memory_kb = tonumber(rss) or 0
|
||||
-- 运行时间
|
||||
local stat_time = sys.exec("stat -c %Y /proc/" .. pid .. " 2>/dev/null"):gsub("%s+", "")
|
||||
local start_ts = tonumber(stat_time) or 0
|
||||
if start_ts > 0 then
|
||||
local uptime_s = os.time() - start_ts
|
||||
local hours = math.floor(uptime_s / 3600)
|
||||
local mins = math.floor((uptime_s % 3600) / 60)
|
||||
local secs = uptime_s % 60
|
||||
if hours > 0 then
|
||||
result.uptime = string.format("%dh %dm %ds", hours, mins, secs)
|
||||
elseif mins > 0 then
|
||||
result.uptime = string.format("%dm %ds", mins, secs)
|
||||
else
|
||||
result.uptime = string.format("%ds", secs)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(result)
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════
|
||||
-- 服务控制 API: start/stop/restart/setup
|
||||
-- ═══════════════════════════════════════════
|
||||
function action_service_ctl()
|
||||
local http = require "luci.http"
|
||||
local sys = require "luci.sys"
|
||||
|
||||
local action = http.formvalue("action") or ""
|
||||
|
||||
if action == "start" then
|
||||
sys.exec("/etc/init.d/openclaw start >/dev/null 2>&1 &")
|
||||
elseif action == "stop" then
|
||||
sys.exec("/etc/init.d/openclaw stop >/dev/null 2>&1")
|
||||
elseif action == "restart" then
|
||||
sys.exec("/etc/init.d/openclaw restart >/dev/null 2>&1 &")
|
||||
elseif action == "enable" then
|
||||
sys.exec("/etc/init.d/openclaw enable 2>/dev/null")
|
||||
elseif action == "disable" then
|
||||
sys.exec("/etc/init.d/openclaw disable 2>/dev/null")
|
||||
elseif action == "setup" then
|
||||
-- 先清理旧日志和状态
|
||||
sys.exec("rm -f /tmp/openclaw-setup.log /tmp/openclaw-setup.pid /tmp/openclaw-setup.exit")
|
||||
-- 获取用户选择的版本 (stable=指定版本, latest=最新版)
|
||||
local version = http.formvalue("version") or ""
|
||||
local env_prefix = ""
|
||||
if version == "stable" then
|
||||
-- 稳定版: 读取 openclaw-env 中定义的 OC_TESTED_VERSION
|
||||
local tested_ver = sys.exec("grep '^OC_TESTED_VERSION=' /usr/bin/openclaw-env 2>/dev/null | cut -d'\"' -f2"):gsub("%s+", "")
|
||||
if tested_ver ~= "" then
|
||||
env_prefix = "OC_VERSION=" .. tested_ver .. " "
|
||||
end
|
||||
elseif version ~= "" and version ~= "latest" then
|
||||
-- 校验版本号格式 (仅允许数字、点、横线、字母)
|
||||
if version:match("^[%d%.%-a-zA-Z]+$") then
|
||||
env_prefix = "OC_VERSION=" .. version .. " "
|
||||
end
|
||||
end
|
||||
-- 后台安装,成功后自动启用并启动服务
|
||||
-- 注: openclaw-env 脚本有 set -e,init_openclaw 中的非关键失败不应阻止启动
|
||||
sys.exec("( " .. env_prefix .. "/usr/bin/openclaw-env setup > /tmp/openclaw-setup.log 2>&1; RC=$?; echo $RC > /tmp/openclaw-setup.exit; if [ $RC -eq 0 ]; then uci set openclaw.main.enabled=1; uci commit openclaw; /etc/init.d/openclaw enable 2>/dev/null; sleep 1; /etc/init.d/openclaw start >> /tmp/openclaw-setup.log 2>&1; fi ) & echo $! > /tmp/openclaw-setup.pid")
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "ok", message = "安装已启动,请查看安装日志..." })
|
||||
return
|
||||
else
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "error", message = "未知操作: " .. action })
|
||||
return
|
||||
end
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ status = "ok", action = action })
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════
|
||||
-- 安装日志轮询 API
|
||||
-- ═══════════════════════════════════════════
|
||||
function action_setup_log()
|
||||
local http = require "luci.http"
|
||||
local sys = require "luci.sys"
|
||||
|
||||
-- 读取日志内容
|
||||
local log = ""
|
||||
local f = io.open("/tmp/openclaw-setup.log", "r")
|
||||
if f then
|
||||
log = f:read("*a") or ""
|
||||
f:close()
|
||||
end
|
||||
|
||||
-- 检查进程是否还在运行
|
||||
local running = false
|
||||
local pid_file = io.open("/tmp/openclaw-setup.pid", "r")
|
||||
if pid_file then
|
||||
local pid = pid_file:read("*a"):gsub("%s+", "")
|
||||
pid_file:close()
|
||||
if pid ~= "" then
|
||||
local check = sys.exec("kill -0 " .. pid .. " 2>/dev/null && echo yes || echo no"):gsub("%s+", "")
|
||||
running = (check == "yes")
|
||||
end
|
||||
end
|
||||
|
||||
-- 读取退出码
|
||||
local exit_code = -1
|
||||
if not running then
|
||||
local exit_file = io.open("/tmp/openclaw-setup.exit", "r")
|
||||
if exit_file then
|
||||
local code = exit_file:read("*a"):gsub("%s+", "")
|
||||
exit_file:close()
|
||||
exit_code = tonumber(code) or -1
|
||||
end
|
||||
end
|
||||
|
||||
-- 判断状态
|
||||
local state = "idle"
|
||||
if running then
|
||||
state = "running"
|
||||
elseif exit_code == 0 then
|
||||
state = "success"
|
||||
elseif exit_code > 0 then
|
||||
state = "failed"
|
||||
end
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({
|
||||
state = state,
|
||||
exit_code = exit_code,
|
||||
log = log
|
||||
})
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════
|
||||
-- 版本检查 API
|
||||
-- ═══════════════════════════════════════════
|
||||
function action_check_update()
|
||||
local http = require "luci.http"
|
||||
local sys = require "luci.sys"
|
||||
|
||||
-- 当前版本
|
||||
local current = get_openclaw_version()
|
||||
|
||||
-- 最新版本 (从 npm registry 查询)
|
||||
local latest = sys.exec("PATH=/opt/openclaw/node/bin:/opt/openclaw/global/bin:$PATH npm view openclaw version 2>/dev/null"):gsub("%s+", "")
|
||||
|
||||
local has_update = false
|
||||
if current ~= "" and latest ~= "" and current ~= latest then
|
||||
has_update = true
|
||||
end
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({
|
||||
status = "ok",
|
||||
current = current,
|
||||
latest = latest,
|
||||
has_update = has_update
|
||||
})
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════
|
||||
-- 执行升级 API (后台执行 + 日志轮询)
|
||||
-- ═══════════════════════════════════════════
|
||||
function action_do_update()
|
||||
local http = require "luci.http"
|
||||
local sys = require "luci.sys"
|
||||
|
||||
-- 清理旧日志和状态
|
||||
sys.exec("rm -f /tmp/openclaw-upgrade.log /tmp/openclaw-upgrade.pid /tmp/openclaw-upgrade.exit")
|
||||
|
||||
-- 后台执行升级,升级完成后自动重启服务
|
||||
sys.exec("( /usr/bin/openclaw-env upgrade > /tmp/openclaw-upgrade.log 2>&1; RC=$?; echo $RC > /tmp/openclaw-upgrade.exit; if [ $RC -eq 0 ]; then echo '' >> /tmp/openclaw-upgrade.log; echo '正在重启服务...' >> /tmp/openclaw-upgrade.log; /etc/init.d/openclaw restart >> /tmp/openclaw-upgrade.log 2>&1; echo ' [✓] 服务已重启' >> /tmp/openclaw-upgrade.log; fi ) & echo $! > /tmp/openclaw-upgrade.pid")
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({
|
||||
status = "ok",
|
||||
message = "升级已在后台启动,请查看升级日志..."
|
||||
})
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════
|
||||
-- 升级日志轮询 API
|
||||
-- ═══════════════════════════════════════════
|
||||
function action_upgrade_log()
|
||||
local http = require "luci.http"
|
||||
local sys = require "luci.sys"
|
||||
|
||||
-- 读取日志内容
|
||||
local log = ""
|
||||
local f = io.open("/tmp/openclaw-upgrade.log", "r")
|
||||
if f then
|
||||
log = f:read("*a") or ""
|
||||
f:close()
|
||||
end
|
||||
|
||||
-- 检查进程是否还在运行
|
||||
local running = false
|
||||
local pid_file = io.open("/tmp/openclaw-upgrade.pid", "r")
|
||||
if pid_file then
|
||||
local pid = pid_file:read("*a"):gsub("%s+", "")
|
||||
pid_file:close()
|
||||
if pid ~= "" then
|
||||
local check = sys.exec("kill -0 " .. pid .. " 2>/dev/null && echo yes || echo no"):gsub("%s+", "")
|
||||
running = (check == "yes")
|
||||
end
|
||||
end
|
||||
|
||||
-- 读取退出码
|
||||
local exit_code = -1
|
||||
if not running then
|
||||
local exit_file = io.open("/tmp/openclaw-upgrade.exit", "r")
|
||||
if exit_file then
|
||||
local code = exit_file:read("*a"):gsub("%s+", "")
|
||||
exit_file:close()
|
||||
exit_code = tonumber(code) or -1
|
||||
end
|
||||
end
|
||||
|
||||
-- 判断状态
|
||||
local state = "idle"
|
||||
if running then
|
||||
state = "running"
|
||||
elseif exit_code == 0 then
|
||||
state = "success"
|
||||
elseif exit_code > 0 then
|
||||
state = "failed"
|
||||
end
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({
|
||||
state = state,
|
||||
exit_code = exit_code,
|
||||
log = log
|
||||
})
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════
|
||||
-- 卸载运行环境 API
|
||||
-- ═══════════════════════════════════════════
|
||||
function action_uninstall()
|
||||
local http = require "luci.http"
|
||||
local sys = require "luci.sys"
|
||||
|
||||
-- 停止服务
|
||||
sys.exec("/etc/init.d/openclaw stop >/dev/null 2>&1")
|
||||
-- 禁用开机启动
|
||||
sys.exec("/etc/init.d/openclaw disable 2>/dev/null")
|
||||
-- 设置 UCI enabled=0
|
||||
sys.exec("uci set openclaw.main.enabled=0; uci commit openclaw 2>/dev/null")
|
||||
-- 删除 Node.js + OpenClaw 运行环境
|
||||
sys.exec("rm -rf /opt/openclaw")
|
||||
-- 清理临时文件
|
||||
sys.exec("rm -f /tmp/openclaw-setup.* /tmp/openclaw-update.log /var/run/openclaw*.pid")
|
||||
-- 删除 openclaw 系统用户
|
||||
sys.exec("sed -i '/^openclaw:/d' /etc/passwd /etc/shadow /etc/group 2>/dev/null")
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({
|
||||
status = "ok",
|
||||
message = "运行环境已卸载。Node.js、OpenClaw 及相关数据已清理。"
|
||||
})
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════
|
||||
-- 获取 Token API
|
||||
-- 仅通过 LuCI 认证后可调用,避免 Token 嵌入 HTML 源码
|
||||
-- 返回网关 Token 和 PTY Token
|
||||
-- ═══════════════════════════════════════════
|
||||
function action_get_token()
|
||||
local http = require "luci.http"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local token = uci:get("openclaw", "main", "token") or ""
|
||||
local pty_token = uci:get("openclaw", "main", "pty_token") or ""
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({ token = token, pty_token = pty_token })
|
||||
end
|
||||
366
luasrc/model/cbi/openclaw/basic.lua
Normal file
366
luasrc/model/cbi/openclaw/basic.lua
Normal file
@@ -0,0 +1,366 @@
|
||||
-- luci-app-openclaw — 基本设置 CBI Model
|
||||
local sys = require "luci.sys"
|
||||
|
||||
m = Map("openclaw", "OpenClaw AI 网关",
|
||||
"OpenClaw 是一个 AI 编程代理网关,支持 GitHub Copilot、Claude、GPT、Gemini 等大模型以及 Telegram、Discord 等多种消息渠道。")
|
||||
|
||||
-- 隐藏底部的「保存并应用」「保存」「复位」按钮 (本页无可编辑的 UCI 选项)
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
|
||||
-- ═══════════════════════════════════════════
|
||||
-- 状态面板
|
||||
-- ═══════════════════════════════════════════
|
||||
m:section(SimpleSection).template = "openclaw/status"
|
||||
|
||||
-- ═══════════════════════════════════════════
|
||||
-- 快捷操作
|
||||
-- ═══════════════════════════════════════════
|
||||
s3 = m:section(SimpleSection, nil, "快捷操作")
|
||||
s3.template = "cbi/nullsection"
|
||||
|
||||
act = s3:option(DummyValue, "_actions")
|
||||
act.rawhtml = true
|
||||
act.cfgvalue = function(self, section)
|
||||
local ctl_url = luci.dispatcher.build_url("admin", "services", "openclaw", "service_ctl")
|
||||
local log_url = luci.dispatcher.build_url("admin", "services", "openclaw", "setup_log")
|
||||
local check_url = luci.dispatcher.build_url("admin", "services", "openclaw", "check_update")
|
||||
local update_url = luci.dispatcher.build_url("admin", "services", "openclaw", "do_update")
|
||||
local upgrade_log_url = luci.dispatcher.build_url("admin", "services", "openclaw", "upgrade_log")
|
||||
local uninstall_url = luci.dispatcher.build_url("admin", "services", "openclaw", "uninstall")
|
||||
local html = {}
|
||||
|
||||
-- 按钮区域
|
||||
html[#html+1] = '<div style="display:flex;gap:10px;flex-wrap:wrap;margin:10px 0;">'
|
||||
html[#html+1] = '<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocShowSetupDialog()" id="btn-setup" title="下载 Node.js 并安装 OpenClaw">📦 安装运行环境</button>'
|
||||
html[#html+1] = '<button class="btn cbi-button cbi-button-action" type="button" onclick="ocServiceCtl(\'restart\')">🔄 重启服务</button>'
|
||||
html[#html+1] = '<button class="btn cbi-button cbi-button-action" type="button" onclick="ocServiceCtl(\'stop\')">⏹️ 停止服务</button>'
|
||||
html[#html+1] = '<button class="btn cbi-button cbi-button-action" type="button" onclick="ocCheckUpdate()" id="btn-check-update">🔍 检测升级</button>'
|
||||
html[#html+1] = '<button class="btn cbi-button cbi-button-remove" type="button" onclick="ocUninstall()" id="btn-uninstall" title="删除 Node.js、OpenClaw 运行环境及相关数据">🗑️ 卸载环境</button>'
|
||||
html[#html+1] = '</div>'
|
||||
html[#html+1] = '<div id="action-result" style="margin-top:8px;"></div>'
|
||||
html[#html+1] = '<div id="oc-update-action" style="margin-top:8px;display:none;"></div>'
|
||||
|
||||
-- 版本选择对话框 (默认隐藏)
|
||||
html[#html+1] = '<div id="oc-setup-dialog" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;align-items:center;justify-content:center;">'
|
||||
html[#html+1] = '<div style="background:#fff;border-radius:12px;padding:24px 28px;max-width:480px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.2);">'
|
||||
html[#html+1] = '<h3 style="margin:0 0 16px 0;font-size:16px;color:#333;">📦 选择安装版本</h3>'
|
||||
html[#html+1] = '<div style="display:flex;flex-direction:column;gap:12px;">'
|
||||
-- 稳定版选项
|
||||
html[#html+1] = '<label style="display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border:2px solid #4a90d9;border-radius:8px;cursor:pointer;background:#f0f7ff;" id="oc-opt-stable">'
|
||||
html[#html+1] = '<input type="radio" name="oc-ver-choice" value="stable" checked style="margin-top:2px;">'
|
||||
html[#html+1] = '<div><strong style="color:#333;">✅ 稳定版 (推荐)</strong>'
|
||||
html[#html+1] = '<div style="font-size:12px;color:#666;margin-top:4px;">版本 v' .. luci.sys.exec("sed -n 's/^OC_TESTED_VERSION=\"\\(.*\\)\"/\\1/p' /usr/bin/openclaw-env 2>/dev/null"):gsub("%s+", "") .. ',已经过完整测试,兼容性良好。</div>'
|
||||
html[#html+1] = '</div></label>'
|
||||
-- 最新版选项
|
||||
html[#html+1] = '<label style="display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border:2px solid #e0e0e0;border-radius:8px;cursor:pointer;background:#fff;" id="oc-opt-latest">'
|
||||
html[#html+1] = '<input type="radio" name="oc-ver-choice" value="latest" style="margin-top:2px;">'
|
||||
html[#html+1] = '<div><strong style="color:#333;">🆕 最新版</strong>'
|
||||
html[#html+1] = '<div style="font-size:12px;color:#e36209;margin-top:4px;">⚠️ 安装 npm 上的最新发布版本,可能存在未经验证的兼容性问题。</div>'
|
||||
html[#html+1] = '</div></label>'
|
||||
html[#html+1] = '</div>'
|
||||
-- 按钮区
|
||||
html[#html+1] = '<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:20px;">'
|
||||
html[#html+1] = '<button class="btn cbi-button" type="button" onclick="ocCloseSetupDialog()" style="min-width:80px;">取消</button>'
|
||||
html[#html+1] = '<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocConfirmSetup()" style="min-width:80px;">开始安装</button>'
|
||||
html[#html+1] = '</div>'
|
||||
html[#html+1] = '</div></div>'
|
||||
|
||||
-- 安装日志面板 (默认隐藏)
|
||||
html[#html+1] = '<div id="setup-log-panel" style="display:none;margin-top:12px;">'
|
||||
html[#html+1] = '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">'
|
||||
html[#html+1] = '<span id="setup-log-title" style="font-weight:600;font-size:14px;">📋 安装日志</span>'
|
||||
html[#html+1] = '<span id="setup-log-status" style="font-size:12px;color:#999;"></span>'
|
||||
html[#html+1] = '</div>'
|
||||
html[#html+1] = '<pre id="setup-log-content" style="background:#1a1b26;color:#a9b1d6;padding:14px 16px;border-radius:6px;font-size:12px;line-height:1.6;max-height:400px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;border:1px solid #2d333b;margin:0;"></pre>'
|
||||
html[#html+1] = '<div id="setup-log-result" style="margin-top:10px;display:none;"></div>'
|
||||
html[#html+1] = '</div>'
|
||||
|
||||
-- JavaScript
|
||||
html[#html+1] = '<script type="text/javascript">'
|
||||
|
||||
-- 版本选择对话框逻辑
|
||||
html[#html+1] = 'var _setupTimer=null;'
|
||||
html[#html+1] = 'function ocShowSetupDialog(){'
|
||||
html[#html+1] = 'var dlg=document.getElementById("oc-setup-dialog");'
|
||||
html[#html+1] = 'dlg.style.display="flex";'
|
||||
html[#html+1] = 'var radios=document.getElementsByName("oc-ver-choice");'
|
||||
html[#html+1] = 'for(var i=0;i<radios.length;i++){if(radios[i].value==="stable")radios[i].checked=true;}'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = 'function ocCloseSetupDialog(){'
|
||||
html[#html+1] = 'document.getElementById("oc-setup-dialog").style.display="none";'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = 'function ocConfirmSetup(){'
|
||||
html[#html+1] = 'ocCloseSetupDialog();'
|
||||
html[#html+1] = 'var radios=document.getElementsByName("oc-ver-choice");'
|
||||
html[#html+1] = 'var choice="stable";'
|
||||
html[#html+1] = 'for(var i=0;i<radios.length;i++){if(radios[i].checked){choice=radios[i].value;break;}}'
|
||||
html[#html+1] = 'var verParam=(choice==="stable")?"stable":"latest";'
|
||||
html[#html+1] = 'ocSetup(verParam);'
|
||||
html[#html+1] = '}'
|
||||
|
||||
-- 安装运行环境 (带实时日志)
|
||||
html[#html+1] = 'function ocSetup(version){'
|
||||
html[#html+1] = 'var btn=document.getElementById("btn-setup");'
|
||||
html[#html+1] = 'var panel=document.getElementById("setup-log-panel");'
|
||||
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
|
||||
html[#html+1] = 'var titleEl=document.getElementById("setup-log-title");'
|
||||
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
|
||||
html[#html+1] = 'var resultEl=document.getElementById("setup-log-result");'
|
||||
html[#html+1] = 'var actionEl=document.getElementById("action-result");'
|
||||
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 安装中...";'
|
||||
html[#html+1] = 'actionEl.textContent="";'
|
||||
html[#html+1] = 'panel.style.display="block";'
|
||||
html[#html+1] = 'logEl.textContent="正在启动安装 ("+((version==="stable")?"稳定版":"最新版")+")...\\n";'
|
||||
html[#html+1] = 'titleEl.textContent="📋 安装日志";'
|
||||
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 安装进行中...</span>";'
|
||||
html[#html+1] = 'resultEl.style.display="none";'
|
||||
html[#html+1] = '(new XHR()).get("' .. ctl_url .. '?action=setup&version="+encodeURIComponent(version),null,function(x){'
|
||||
html[#html+1] = 'try{JSON.parse(x.responseText);}catch(e){}'
|
||||
html[#html+1] = 'ocPollSetupLog();'
|
||||
html[#html+1] = '});'
|
||||
html[#html+1] = '}'
|
||||
|
||||
-- 轮询安装日志
|
||||
html[#html+1] = 'function ocPollSetupLog(){'
|
||||
html[#html+1] = 'if(_setupTimer)clearInterval(_setupTimer);'
|
||||
html[#html+1] = '_setupTimer=setInterval(function(){'
|
||||
html[#html+1] = '(new XHR()).get("' .. log_url .. '",null,function(x){'
|
||||
html[#html+1] = 'try{'
|
||||
html[#html+1] = 'var r=JSON.parse(x.responseText);'
|
||||
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
|
||||
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
|
||||
html[#html+1] = 'if(r.log)logEl.textContent=r.log;'
|
||||
html[#html+1] = 'logEl.scrollTop=logEl.scrollHeight;'
|
||||
html[#html+1] = 'if(r.state==="running"){'
|
||||
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 安装进行中...</span>";'
|
||||
html[#html+1] = '}else if(r.state==="success"){'
|
||||
html[#html+1] = 'clearInterval(_setupTimer);_setupTimer=null;'
|
||||
html[#html+1] = 'ocSetupDone(true,r.log);'
|
||||
html[#html+1] = '}else if(r.state==="failed"){'
|
||||
html[#html+1] = 'clearInterval(_setupTimer);_setupTimer=null;'
|
||||
html[#html+1] = 'ocSetupDone(false,r.log);'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = '}catch(e){}'
|
||||
html[#html+1] = '});'
|
||||
html[#html+1] = '},1500);'
|
||||
html[#html+1] = '}'
|
||||
|
||||
-- 安装完成处理
|
||||
html[#html+1] = 'function ocSetupDone(ok,log){'
|
||||
html[#html+1] = 'var btn=document.getElementById("btn-setup");'
|
||||
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
|
||||
html[#html+1] = 'var resultEl=document.getElementById("setup-log-result");'
|
||||
html[#html+1] = 'btn.disabled=false;btn.textContent="📦 安装运行环境";'
|
||||
html[#html+1] = 'resultEl.style.display="block";'
|
||||
html[#html+1] = 'if(ok){'
|
||||
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#1a7f37;\\">✅ 安装完成</span>";'
|
||||
html[#html+1] = 'resultEl.innerHTML="<div style=\\"border:1px solid #c6e9c9;background:#e6f7e9;padding:12px 16px;border-radius:6px;\\">"+'
|
||||
html[#html+1] = '"<strong style=\\"color:#1a7f37;font-size:14px;\\">🎉 恭喜!OpenClaw 运行环境安装成功!</strong><br/>"+'
|
||||
html[#html+1] = '"<span style=\\"color:#555;font-size:13px;line-height:1.8;\\">服务已自动启用并启动,点击下方按钮刷新页面查看运行状态。</span><br/>"+'
|
||||
html[#html+1] = '"<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"location.reload()\\" style=\\"margin-top:10px;\\">🔄 刷新页面</button></div>";'
|
||||
html[#html+1] = '}else{'
|
||||
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#cf222e;\\">❌ 安装失败</span>";'
|
||||
-- 分析失败原因
|
||||
html[#html+1] = 'var reasons=ocAnalyzeFailure(log);'
|
||||
html[#html+1] = 'resultEl.innerHTML="<div style=\\"border:1px solid #f5c6cb;background:#ffeef0;padding:12px 16px;border-radius:6px;\\">"+'
|
||||
html[#html+1] = '"<strong style=\\"color:#cf222e;font-size:14px;\\">❌ 安装失败</strong><br/>"+'
|
||||
html[#html+1] = '"<div style=\\"margin:8px 0;padding:10px 14px;background:#fff5f5;border-radius:4px;font-size:13px;line-height:1.8;\\">"+'
|
||||
html[#html+1] = '"<strong>🔍 可能的失败原因:</strong><br/>"+reasons+"</div>"+'
|
||||
html[#html+1] = '"<div style=\\"margin-top:8px;font-size:12px;color:#666;\\">💡 完整日志见上方终端输出,也可在终端查看:<code>cat /tmp/openclaw-setup.log</code></div></div>";'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = '}'
|
||||
|
||||
-- 分析失败原因
|
||||
html[#html+1] = 'function ocAnalyzeFailure(log){'
|
||||
html[#html+1] = 'var reasons=[];'
|
||||
html[#html+1] = 'if(!log)return"未知错误,请检查日志。";'
|
||||
html[#html+1] = 'var ll=log.toLowerCase();'
|
||||
-- 网络问题
|
||||
html[#html+1] = 'if(ll.indexOf("could not resolve")>=0||ll.indexOf("connection timed out")>=0||ll.indexOf("curl")>=0&&ll.indexOf("fail")>=0||ll.indexOf("wget")>=0&&ll.indexOf("fail")>=0||ll.indexOf("所有镜像均下载失败")>=0){'
|
||||
html[#html+1] = 'reasons.push("🌐 <b>网络连接失败</b> — 无法下载 Node.js。请检查路由器是否能访问外网。<br/> 💡 解决: 检查 DNS 设置和网络连接,或手动指定镜像: <code>NODE_MIRROR=https://npmmirror.com/mirrors/node openclaw-env setup</code>");'
|
||||
html[#html+1] = '}'
|
||||
-- 磁盘空间
|
||||
html[#html+1] = 'if(ll.indexOf("no space")>=0||ll.indexOf("disk full")>=0||ll.indexOf("enospc")>=0){'
|
||||
html[#html+1] = 'reasons.push("💾 <b>磁盘空间不足</b> — Node.js + OpenClaw 需要约 200MB 空间。<br/> 💡 解决: 运行 <code>df -h</code> 检查可用空间,清理不需要的文件或使用外部存储。");'
|
||||
html[#html+1] = '}'
|
||||
-- 架构不支持
|
||||
html[#html+1] = 'if(ll.indexOf("不支持的 cpu 架构")>=0||ll.indexOf("不支持的架构")>=0){'
|
||||
html[#html+1] = 'reasons.push("🔧 <b>CPU 架构不支持</b> — 仅支持 x86_64 和 aarch64 (ARM64)。<br/> 💡 当前设备架构可能是 32 位 ARM 或 MIPS,无法运行 Node.js 22。");'
|
||||
html[#html+1] = '}'
|
||||
-- npm 安装失败
|
||||
html[#html+1] = 'if(ll.indexOf("npm err")>=0||ll.indexOf("npm warn")>=0&&ll.indexOf("openclaw 安装验证失败")>=0){'
|
||||
html[#html+1] = 'reasons.push("📦 <b>npm 安装 OpenClaw 失败</b> — npm 包下载或安装出错。<br/> 💡 解决: 尝试手动安装 <code>PATH=/opt/openclaw/node/bin:$PATH npm install -g openclaw@latest --prefix=/opt/openclaw/global</code>");'
|
||||
html[#html+1] = '}'
|
||||
-- 权限问题
|
||||
html[#html+1] = 'if(ll.indexOf("permission denied")>=0||ll.indexOf("eacces")>=0){'
|
||||
html[#html+1] = 'reasons.push("🔒 <b>权限不足</b> — 文件或目录权限问题。<br/> 💡 解决: 运行 <code>chown -R openclaw:openclaw /opt/openclaw</code> 或以 root 用户重试。");'
|
||||
html[#html+1] = '}'
|
||||
-- tar 解压失败
|
||||
html[#html+1] = 'if(ll.indexOf("tar")>=0&&(ll.indexOf("error")>=0||ll.indexOf("fail")>=0)){'
|
||||
html[#html+1] = 'reasons.push("📂 <b>解压失败</b> — Node.js 安装包可能下载不完整。<br/> 💡 解决: 删除缓存重试 <code>rm -rf /opt/openclaw/node && openclaw-env setup</code>");'
|
||||
html[#html+1] = '}'
|
||||
-- 验证失败
|
||||
html[#html+1] = 'if(ll.indexOf("安装验证失败")>=0){'
|
||||
html[#html+1] = 'reasons.push("⚠️ <b>安装验证失败</b> — 程序已下载但无法正常运行。<br/> 💡 可能是 glibc/musl 不兼容,请确认系统 C 库类型: <code>ldd --version 2>&1 | head -1</code>");'
|
||||
html[#html+1] = '}'
|
||||
-- 兜底
|
||||
html[#html+1] = 'if(reasons.length===0){'
|
||||
html[#html+1] = 'reasons.push("⚠️ <b>未识别的错误</b> — 请查看上方完整日志分析具体原因。<br/> 💡 您也可以尝试手动执行: <code>openclaw-env setup</code> 查看详细输出。");'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = 'return reasons.join("<br/><br/>");'
|
||||
html[#html+1] = '}'
|
||||
|
||||
-- 普通服务操作 (restart/stop)
|
||||
html[#html+1] = 'function ocServiceCtl(action){'
|
||||
html[#html+1] = 'var el=document.getElementById("action-result");'
|
||||
html[#html+1] = 'el.innerHTML="<span style=\\"color:#999\\">⏳ 正在执行...</span>";'
|
||||
html[#html+1] = '(new XHR()).get("' .. ctl_url .. '?action="+action,null,function(x){'
|
||||
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
|
||||
html[#html+1] = 'if(r.status==="ok"){el.innerHTML="<span style=\\"color:green\\">✅ "+action+" 已完成</span>";}'
|
||||
html[#html+1] = 'else{el.innerHTML="<span style=\\"color:red\\">❌ "+(r.message||"失败")+"</span>";}'
|
||||
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 错误</span>";}'
|
||||
html[#html+1] = '});}'
|
||||
|
||||
-- 检测升级
|
||||
html[#html+1] = 'function ocCheckUpdate(){'
|
||||
html[#html+1] = 'var btn=document.getElementById("btn-check-update");'
|
||||
html[#html+1] = 'var el=document.getElementById("action-result");'
|
||||
html[#html+1] = 'var act=document.getElementById("oc-update-action");'
|
||||
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 正在检测...";el.textContent="";act.style.display="none";'
|
||||
html[#html+1] = '(new XHR()).get("' .. check_url .. '",null,function(x){'
|
||||
html[#html+1] = 'btn.disabled=false;btn.textContent="🔍 检测升级";'
|
||||
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
|
||||
html[#html+1] = 'if(!r.current){el.innerHTML="<span style=\\"color:#999\\">⚠️ OpenClaw 未安装</span>";return;}'
|
||||
html[#html+1] = 'if(r.has_update){'
|
||||
html[#html+1] = 'el.innerHTML="<span style=\\"color:#e36209\\">📦 当前: v"+r.current+" → 最新: v"+r.latest+"</span>";'
|
||||
html[#html+1] = 'act.style.display="block";'
|
||||
html[#html+1] = 'act.innerHTML=\'<button class="btn cbi-button cbi-button-apply" type="button" onclick="ocDoUpdate()" id="btn-do-update">⬆️ 立即升级</button>\';'
|
||||
html[#html+1] = '}else{'
|
||||
html[#html+1] = 'el.innerHTML="<span style=\\"color:green\\">✅ 已是最新版本 (v"+r.current+")</span>";'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 检测失败</span>";}'
|
||||
html[#html+1] = '});}'
|
||||
|
||||
-- 执行升级 (带实时日志, 和安装一样的体验)
|
||||
html[#html+1] = 'var _upgradeTimer=null;'
|
||||
html[#html+1] = 'function ocDoUpdate(){'
|
||||
html[#html+1] = 'var btn=document.getElementById("btn-do-update");'
|
||||
html[#html+1] = 'var panel=document.getElementById("setup-log-panel");'
|
||||
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
|
||||
html[#html+1] = 'var titleEl=document.getElementById("setup-log-title");'
|
||||
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
|
||||
html[#html+1] = 'var resultEl=document.getElementById("setup-log-result");'
|
||||
html[#html+1] = 'var actionEl=document.getElementById("action-result");'
|
||||
html[#html+1] = 'if(!confirm("确定要升级 OpenClaw?升级期间服务将短暂中断。"))return;'
|
||||
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 正在升级...";'
|
||||
html[#html+1] = 'actionEl.textContent="";'
|
||||
html[#html+1] = 'panel.style.display="block";'
|
||||
html[#html+1] = 'logEl.textContent="正在启动升级...\\n";'
|
||||
html[#html+1] = 'titleEl.textContent="📋 升级日志";'
|
||||
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 升级进行中...</span>";'
|
||||
html[#html+1] = 'resultEl.style.display="none";'
|
||||
html[#html+1] = '(new XHR()).get("' .. update_url .. '",null,function(x){'
|
||||
html[#html+1] = 'try{JSON.parse(x.responseText);}catch(e){}'
|
||||
html[#html+1] = 'ocPollUpgradeLog();'
|
||||
html[#html+1] = '});'
|
||||
html[#html+1] = '}'
|
||||
|
||||
-- 轮询升级日志
|
||||
html[#html+1] = 'function ocPollUpgradeLog(){'
|
||||
html[#html+1] = 'if(_upgradeTimer)clearInterval(_upgradeTimer);'
|
||||
html[#html+1] = '_upgradeTimer=setInterval(function(){'
|
||||
html[#html+1] = '(new XHR()).get("' .. upgrade_log_url .. '",null,function(x){'
|
||||
html[#html+1] = 'try{'
|
||||
html[#html+1] = 'var r=JSON.parse(x.responseText);'
|
||||
html[#html+1] = 'var logEl=document.getElementById("setup-log-content");'
|
||||
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
|
||||
html[#html+1] = 'if(r.log)logEl.textContent=r.log;'
|
||||
html[#html+1] = 'logEl.scrollTop=logEl.scrollHeight;'
|
||||
html[#html+1] = 'if(r.state==="running"){'
|
||||
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#7aa2f7;\\">⏳ 升级进行中...</span>";'
|
||||
html[#html+1] = '}else if(r.state==="success"){'
|
||||
html[#html+1] = 'clearInterval(_upgradeTimer);_upgradeTimer=null;'
|
||||
html[#html+1] = 'ocUpgradeDone(true);'
|
||||
html[#html+1] = '}else if(r.state==="failed"){'
|
||||
html[#html+1] = 'clearInterval(_upgradeTimer);_upgradeTimer=null;'
|
||||
html[#html+1] = 'ocUpgradeDone(false);'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = '}catch(e){}'
|
||||
html[#html+1] = '});'
|
||||
html[#html+1] = '},1500);'
|
||||
html[#html+1] = '}'
|
||||
|
||||
-- 升级完成处理
|
||||
html[#html+1] = 'function ocUpgradeDone(ok){'
|
||||
html[#html+1] = 'var btn=document.getElementById("btn-do-update");'
|
||||
html[#html+1] = 'var statusEl=document.getElementById("setup-log-status");'
|
||||
html[#html+1] = 'var resultEl=document.getElementById("setup-log-result");'
|
||||
html[#html+1] = 'var actEl=document.getElementById("oc-update-action");'
|
||||
html[#html+1] = 'if(btn){btn.disabled=false;btn.textContent="⬆️ 立即升级";}'
|
||||
html[#html+1] = 'resultEl.style.display="block";'
|
||||
html[#html+1] = 'if(ok){'
|
||||
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#1a7f37;\\">✅ 升级完成</span>";'
|
||||
html[#html+1] = 'resultEl.innerHTML="<div style=\\"border:1px solid #c6e9c9;background:#e6f7e9;padding:12px 16px;border-radius:6px;\\">"+'
|
||||
html[#html+1] = '"<strong style=\\"color:#1a7f37;font-size:14px;\\">🎉 升级成功!服务已自动重启。</strong><br/>"+'
|
||||
html[#html+1] = '"<span style=\\"color:#555;font-size:13px;line-height:1.8;\\">点击下方按钮刷新页面查看最新状态。</span><br/>"+'
|
||||
html[#html+1] = '"<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"location.reload()\\" style=\\"margin-top:10px;\\">🔄 刷新页面</button></div>";'
|
||||
html[#html+1] = 'actEl.style.display="none";'
|
||||
html[#html+1] = '}else{'
|
||||
html[#html+1] = 'statusEl.innerHTML="<span style=\\"color:#cf222e;\\">❌ 升级失败</span>";'
|
||||
html[#html+1] = 'resultEl.innerHTML="<div style=\\"border:1px solid #f5c6cb;background:#ffeef0;padding:12px 16px;border-radius:6px;\\">"+'
|
||||
html[#html+1] = '"<strong style=\\"color:#cf222e;font-size:14px;\\">❌ 升级失败</strong><br/>"+'
|
||||
html[#html+1] = '"<span style=\\"color:#555;font-size:13px;\\">请查看上方日志了解详情。也可在终端查看:<code>cat /tmp/openclaw-upgrade.log</code></span><br/>"+'
|
||||
html[#html+1] = '"<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"location.reload()\\" style=\\"margin-top:10px;\\">🔄 刷新页面</button></div>";'
|
||||
html[#html+1] = '}'
|
||||
html[#html+1] = '}'
|
||||
|
||||
-- 卸载运行环境
|
||||
html[#html+1] = 'function ocUninstall(){'
|
||||
html[#html+1] = 'if(!confirm("确定要卸载 OpenClaw 运行环境?\\n\\n将删除 Node.js、OpenClaw 程序及配置数据(/opt/openclaw 目录),服务将停止运行。\\n\\n插件本身不会被删除,之后可重新安装运行环境。"))return;'
|
||||
html[#html+1] = 'var btn=document.getElementById("btn-uninstall");'
|
||||
html[#html+1] = 'var el=document.getElementById("action-result");'
|
||||
html[#html+1] = 'btn.disabled=true;btn.textContent="⏳ 正在卸载...";'
|
||||
html[#html+1] = 'el.innerHTML="<span style=\\"color:#999\\">正在停止服务并清理文件...</span>";'
|
||||
html[#html+1] = '(new XHR()).get("' .. uninstall_url .. '",null,function(x){'
|
||||
html[#html+1] = 'btn.disabled=false;btn.textContent="🗑️ 卸载环境";'
|
||||
html[#html+1] = 'try{var r=JSON.parse(x.responseText);'
|
||||
html[#html+1] = 'if(r.status==="ok"){'
|
||||
html[#html+1] = 'el.innerHTML="<div style=\\"border:1px solid #d0d7de;background:#f6f8fa;padding:12px 16px;border-radius:6px;\\">"+'
|
||||
html[#html+1] = '"<strong style=\\"color:#1a7f37;\\">✅ 卸载完成</strong><br/>"+'
|
||||
html[#html+1] = '"<span style=\\"color:#555;font-size:13px;\\">"+r.message+"</span><br/>"+'
|
||||
html[#html+1] = '"<button class=\\"btn cbi-button cbi-button-apply\\" type=\\"button\\" onclick=\\"location.reload()\\" style=\\"margin-top:8px;\\">🔄 刷新页面</button></div>";'
|
||||
html[#html+1] = '}else{el.innerHTML="<span style=\\"color:red\\">❌ "+(r.message||"卸载失败")+"</span>";}'
|
||||
html[#html+1] = '}catch(e){el.innerHTML="<span style=\\"color:red\\">❌ 请求失败</span>";}'
|
||||
html[#html+1] = '});}'
|
||||
|
||||
html[#html+1] = '</script>'
|
||||
return table.concat(html, "\n")
|
||||
end
|
||||
|
||||
-- ═══════════════════════════════════════════
|
||||
-- 使用指南
|
||||
-- ═══════════════════════════════════════════
|
||||
s4 = m:section(SimpleSection, nil)
|
||||
s4.template = "cbi/nullsection"
|
||||
guide = s4:option(DummyValue, "_guide")
|
||||
guide.rawhtml = true
|
||||
guide.cfgvalue = function()
|
||||
local html = {}
|
||||
html[#html+1] = '<div style="border:1px solid #d0e8ff;background:#f0f7ff;padding:14px 18px;border-radius:6px;margin-top:12px;line-height:1.8;font-size:13px;">'
|
||||
html[#html+1] = '<strong style="font-size:14px;">📖 使用指南</strong><br/>'
|
||||
html[#html+1] = '<span style="color:#555;">'
|
||||
html[#html+1] = '① 首次使用请点击 <b>「安装运行环境」</b>,安装完成后服务会自动启动<br/>'
|
||||
html[#html+1] = '② 进入 <b>「Web 控制台」</b> 配置 AI 模型、消息渠道,直接开始对话<br/>'
|
||||
html[#html+1] = '③ 进入 <b>「配置管理」</b> 可使用交互式向导进行高级配置</span>'
|
||||
html[#html+1] = '<div style="margin-top:10px;padding-top:10px;border-top:1px solid #d0e8ff;">'
|
||||
html[#html+1] = '<span style="color:#888;">有疑问?请关注B站并留言:</span>'
|
||||
html[#html+1] = '<a href="https://space.bilibili.com/59438380" target="_blank" rel="noopener" style="color:#00a1d6;font-weight:bold;text-decoration:none;">'
|
||||
html[#html+1] = '🔗 space.bilibili.com/59438380</a>'
|
||||
html[#html+1] = '<span style="margin-left:16px;color:#888;">GitHub 项目:</span>'
|
||||
html[#html+1] = '<a href="https://github.com/10000ge10000/luci-app-openclaw" target="_blank" rel="noopener" style="color:#24292f;font-weight:bold;text-decoration:none;">'
|
||||
html[#html+1] = '🐙 10000ge10000/luci-app-openclaw</a></div></div>'
|
||||
return table.concat(html, "\n")
|
||||
end
|
||||
|
||||
return m
|
||||
121
luasrc/view/openclaw/advanced.htm
Normal file
121
luasrc/view/openclaw/advanced.htm
Normal file
@@ -0,0 +1,121 @@
|
||||
<%#
|
||||
luci-app-openclaw — 终端配置页面 (简洁版)
|
||||
-%>
|
||||
<%+header%>
|
||||
|
||||
<%
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local pty_port = uci:get("openclaw", "main", "pty_port") or "18793"
|
||||
%>
|
||||
|
||||
<style type="text/css">
|
||||
.oc-page-header { margin: 0 0 16px 0; }
|
||||
.oc-page-header h2 { font-size: 18px; font-weight: 600; color: #333; margin: 0 0 6px 0; }
|
||||
.oc-page-header p { font-size: 13px; color: #666; margin: 0; line-height: 1.6; }
|
||||
|
||||
.oc-info-box {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #f6f8fa;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-left: 4px solid #4a90d9;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: #555;
|
||||
}
|
||||
.oc-info-box ul { margin: 6px 0 0 0; padding-left: 18px; list-style: none; }
|
||||
.oc-info-box li { margin-bottom: 2px; }
|
||||
|
||||
.oc-terminal-wrap {
|
||||
border: 2px solid #2d333b;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #1a1b26;
|
||||
}
|
||||
|
||||
#oc-terminal-iframe { width: 100%; height: 600px; border: none; display: block; }
|
||||
|
||||
.oc-terminal-loading {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
height: 200px; color: #7aa2f7; font-size: 14px; background: #1a1b26;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="oc-page-header">
|
||||
<h2>⚙️ 终端配置</h2>
|
||||
<p>通过内嵌的交互式终端 (oc-config) 进行 OpenClaw 的完整配置管理。支持 AI 模型配置、消息渠道设置、健康检查等。</p>
|
||||
</div>
|
||||
|
||||
<div class="oc-info-box">
|
||||
<strong>💡 菜单功能说明:</strong>
|
||||
<ul>
|
||||
<li><strong>1)</strong> 查看当前配置 <strong>2)</strong> 配置 AI 模型提供商 <strong>3)</strong> 设定当前活跃模型</li>
|
||||
<li><strong>4)</strong> 配置消息渠道 <strong>5)</strong> Telegram 配对向导 <strong>6)</strong> 健康检查 / 诊断</li>
|
||||
<li><strong>7)</strong> 重启网关 <strong>8)</strong> 查看/编辑原始配置 <strong>9)</strong> 恢复默认配置</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="oc-terminal-wrap">
|
||||
<div id="oc-terminal-container">
|
||||
<div class="oc-terminal-loading" id="oc-terminal-loading">
|
||||
⏳ 正在连接配置终端...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
(function() {
|
||||
var ptyPort = '<%=pty_port%>';
|
||||
var statusUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "status_api")%>';
|
||||
var tokenUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "get_token")%>';
|
||||
var container = document.getElementById('oc-terminal-container');
|
||||
var loading = document.getElementById('oc-terminal-loading');
|
||||
var ptyToken = '';
|
||||
|
||||
function checkAndLoadTerminal() {
|
||||
// 先获取 PTY token
|
||||
(new XHR()).get(tokenUrl, null, function(tx) {
|
||||
try {
|
||||
var td = JSON.parse(tx.responseText);
|
||||
ptyToken = td.pty_token || '';
|
||||
} catch(e) {}
|
||||
|
||||
(new XHR()).get(statusUrl, null, function(x) {
|
||||
try {
|
||||
var d = JSON.parse(x.responseText);
|
||||
if (d.pty_running) {
|
||||
showIframe();
|
||||
} else {
|
||||
loading.innerHTML = '❌ 配置终端未运行<br/>' +
|
||||
'<span style="font-size:12px;color:#999;">请先在「基本设置」中启用并启动服务。</span>';
|
||||
}
|
||||
} catch(e) {
|
||||
loading.textContent = '检查终端状态失败';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showIframe() {
|
||||
var proto = window.location.protocol;
|
||||
var host = window.location.hostname;
|
||||
var url = proto + '//' + host + ':' + ptyPort + '/';
|
||||
if (ptyToken) url += '?pty_token=' + encodeURIComponent(ptyToken);
|
||||
|
||||
loading.style.display = 'none';
|
||||
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.id = 'oc-terminal-iframe';
|
||||
iframe.src = url;
|
||||
iframe.setAttribute('allowfullscreen', 'true');
|
||||
container.appendChild(iframe);
|
||||
}
|
||||
|
||||
checkAndLoadTerminal();
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
|
||||
<%+footer%>
|
||||
168
luasrc/view/openclaw/console.htm
Normal file
168
luasrc/view/openclaw/console.htm
Normal file
@@ -0,0 +1,168 @@
|
||||
<%#
|
||||
luci-app-openclaw — Web 控制台页面
|
||||
嵌入 OpenClaw 官方 Web UI,用户可在此配置模型/渠道并直接对话
|
||||
-%>
|
||||
<%+header%>
|
||||
|
||||
<%
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port = uci:get("openclaw", "main", "port") or "18789"
|
||||
%>
|
||||
|
||||
<style type="text/css">
|
||||
.oc-page-header { margin: 0 0 16px 0; }
|
||||
.oc-page-header h2 { font-size: 18px; font-weight: 600; color: #333; margin: 0 0 6px 0; }
|
||||
.oc-page-header p { font-size: 13px; color: #666; margin: 0; line-height: 1.6; }
|
||||
|
||||
.oc-console-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #f6f8fa;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.oc-console-info .label { color: #888; }
|
||||
.oc-console-info .value { font-family: monospace; color: #333; font-weight: 500; }
|
||||
.oc-console-info .sep { color: #ddd; }
|
||||
.oc-console-info .btn-open {
|
||||
margin-left: auto;
|
||||
padding: 4px 14px;
|
||||
background: #4a90d9;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.oc-console-info .btn-open:hover { background: #357abd; }
|
||||
|
||||
.oc-console-wrap {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
#oc-console-iframe { width: 100%; height: 700px; border: none; display: block; }
|
||||
|
||||
.oc-console-loading {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
height: 300px; color: #666; font-size: 14px; background: #fafafa;
|
||||
}
|
||||
.oc-console-loading .spinner {
|
||||
width: 32px; height: 32px; border: 3px solid #e0e0e0; border-top: 3px solid #4a90d9;
|
||||
border-radius: 50%; animation: oc-spin .8s linear infinite; margin-bottom: 12px;
|
||||
}
|
||||
@keyframes oc-spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
|
||||
<div class="oc-page-header">
|
||||
<h2>🖥️ Web 控制台</h2>
|
||||
<p>OpenClaw 官方 Web 管理界面 — 在这里可以配置 AI 模型、消息渠道,直接与 AI 进行对话,以及管理所有功能。</p>
|
||||
</div>
|
||||
|
||||
<div class="oc-console-info">
|
||||
<span class="label">网关地址:</span>
|
||||
<span class="value" id="oc-console-addr">-</span>
|
||||
<span class="sep">|</span>
|
||||
<span class="label">状态:</span>
|
||||
<span id="oc-console-status-text">检查中...</span>
|
||||
<a id="oc-console-open-btn" class="btn-open" href="#" target="_blank" rel="noopener" style="display:none;">
|
||||
↗ 新窗口打开
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="oc-console-wrap">
|
||||
<div id="oc-console-container">
|
||||
<div class="oc-console-loading" id="oc-console-loading">
|
||||
<div class="spinner"></div>
|
||||
<span>正在连接 OpenClaw 控制台...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
(function() {
|
||||
var gwPort = '<%=port%>';
|
||||
var gwToken = '';
|
||||
var statusUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "status_api")%>';
|
||||
var tokenUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "get_token")%>';
|
||||
var container = document.getElementById('oc-console-container');
|
||||
var loading = document.getElementById('oc-console-loading');
|
||||
var addrEl = document.getElementById('oc-console-addr');
|
||||
var statusTextEl = document.getElementById('oc-console-status-text');
|
||||
var openBtn = document.getElementById('oc-console-open-btn');
|
||||
|
||||
function getConsoleUrl() {
|
||||
var proto = window.location.protocol;
|
||||
var host = window.location.hostname;
|
||||
var url = proto + '//' + host + ':' + gwPort + '/';
|
||||
if (gwToken) url += '#token=' + encodeURIComponent(gwToken);
|
||||
return url;
|
||||
}
|
||||
|
||||
function checkAndLoad() {
|
||||
addrEl.textContent = window.location.hostname + ':' + gwPort;
|
||||
|
||||
// 先获取 token,再检查状态
|
||||
(new XHR()).get(tokenUrl, null, function(tx) {
|
||||
try {
|
||||
var td = JSON.parse(tx.responseText);
|
||||
gwToken = td.token || '';
|
||||
} catch(e) {}
|
||||
|
||||
(new XHR()).get(statusUrl, null, function(x) {
|
||||
try {
|
||||
var d = JSON.parse(x.responseText);
|
||||
var url = getConsoleUrl();
|
||||
if (d.gateway_running) {
|
||||
statusTextEl.innerHTML = '<span style="color:#1a7f37;">● 网关运行中</span>';
|
||||
openBtn.href = url;
|
||||
openBtn.style.display = '';
|
||||
showIframe(url);
|
||||
} else {
|
||||
statusTextEl.innerHTML = '<span style="color:#cf222e;">● 网关未运行</span>';
|
||||
openBtn.style.display = 'none';
|
||||
loading.innerHTML = '<div style="text-align:center;color:#666;">' +
|
||||
'<div style="font-size:40px;margin-bottom:12px;">🧠</div>' +
|
||||
'<div style="font-size:15px;margin-bottom:6px;">OpenClaw 网关未运行</div>' +
|
||||
'<div style="font-size:12px;color:#999;">请先在「基本设置」页面启用服务并启动。</div>' +
|
||||
'</div>';
|
||||
}
|
||||
} catch(e) {
|
||||
statusTextEl.textContent = '查询失败';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showIframe(url) {
|
||||
loading.style.display = 'none';
|
||||
var existing = document.getElementById('oc-console-iframe');
|
||||
if (existing) return;
|
||||
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.id = 'oc-console-iframe';
|
||||
iframe.src = url;
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.height = '700px';
|
||||
iframe.style.border = 'none';
|
||||
iframe.setAttribute('allowfullscreen', 'true');
|
||||
container.appendChild(iframe);
|
||||
}
|
||||
|
||||
checkAndLoad();
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
|
||||
<%+footer%>
|
||||
138
luasrc/view/openclaw/status.htm
Normal file
138
luasrc/view/openclaw/status.htm
Normal file
@@ -0,0 +1,138 @@
|
||||
<%#
|
||||
luci-app-openclaw — 运行状态面板 (全面汉化 + 界面优化)
|
||||
-%>
|
||||
|
||||
<style type="text/css">
|
||||
#oc-status-panel {
|
||||
margin: 0 0 20px 0;
|
||||
padding: 0;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
}
|
||||
#oc-status-panel .panel-title {
|
||||
background: linear-gradient(135deg, #4a90d9, #357abd);
|
||||
color: #fff;
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
#oc-status-panel .panel-body {
|
||||
padding: 0;
|
||||
}
|
||||
#oc-status-panel table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
#oc-status-panel td {
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
font-size: 13px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#oc-status-panel tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
#oc-status-panel td:first-child {
|
||||
width: 120px;
|
||||
color: #888;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#oc-status-panel td:last-child {
|
||||
color: #333;
|
||||
}
|
||||
.oc-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.oc-badge-running { background: #e6f7e9; color: #1a7f37; }
|
||||
.oc-badge-stopped { background: #ffeef0; color: #cf222e; }
|
||||
.oc-badge-disabled { background: #f0f0f0; color: #656d76; }
|
||||
.oc-badge-unknown { background: #fff8c5; color: #9a6700; }
|
||||
.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; }
|
||||
.oc-dot-gray { background: #999; }
|
||||
</style>
|
||||
|
||||
<div id="oc-status-panel">
|
||||
<div class="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>
|
||||
<tr><td>网关服务</td><td id="oc-st-gateway">-</td></tr>
|
||||
<tr><td>配置终端</td><td id="oc-st-pty">-</td></tr>
|
||||
<tr><td>进程 PID</td><td id="oc-st-pid">-</td></tr>
|
||||
<tr><td>内存占用</td><td id="oc-st-mem">-</td></tr>
|
||||
<tr><td>运行时间</td><td id="oc-st-uptime">-</td></tr>
|
||||
<tr><td>Node.js</td><td id="oc-st-node">-</td></tr>
|
||||
<tr><td>OpenClaw</td><td id="oc-st-ocver">-</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
(function() {
|
||||
var statusUrl = '<%=luci.dispatcher.build_url("admin", "services", "openclaw", "status_api")%>';
|
||||
|
||||
function updateStatus() {
|
||||
(new XHR()).get(statusUrl, null, function(x) {
|
||||
try {
|
||||
var d = JSON.parse(x.responseText);
|
||||
|
||||
var stEl = document.getElementById('oc-st-status');
|
||||
if (d.enabled !== '1') {
|
||||
stEl.innerHTML = '<span class="oc-badge oc-badge-disabled">已禁用</span>';
|
||||
} else if (d.gateway_running) {
|
||||
stEl.innerHTML = '<span class="oc-badge oc-badge-running">运行中</span>';
|
||||
} else {
|
||||
stEl.innerHTML = '<span class="oc-badge oc-badge-stopped">已停止</span>';
|
||||
}
|
||||
|
||||
var gwEl = document.getElementById('oc-st-gateway');
|
||||
if (d.gateway_running) {
|
||||
gwEl.innerHTML = '<span class="oc-dot oc-dot-green"></span>监听中 :' + d.port;
|
||||
} else {
|
||||
gwEl.innerHTML = '<span class="oc-dot oc-dot-red"></span>未监听';
|
||||
}
|
||||
|
||||
var ptyEl = document.getElementById('oc-st-pty');
|
||||
if (d.pty_running) {
|
||||
ptyEl.innerHTML = '<span class="oc-dot oc-dot-green"></span>监听中 :' + d.pty_port;
|
||||
} else {
|
||||
ptyEl.innerHTML = '<span class="oc-dot oc-dot-gray"></span>未监听';
|
||||
}
|
||||
|
||||
document.getElementById('oc-st-pid').textContent = d.pid || '-';
|
||||
|
||||
var memEl = document.getElementById('oc-st-mem');
|
||||
if (d.memory_kb > 0) {
|
||||
var mb = (d.memory_kb / 1024).toFixed(1);
|
||||
memEl.textContent = mb + ' MB';
|
||||
} else {
|
||||
memEl.textContent = '-';
|
||||
}
|
||||
|
||||
document.getElementById('oc-st-uptime').textContent = d.uptime || '-';
|
||||
document.getElementById('oc-st-node').textContent = d.node_version || '未安装';
|
||||
document.getElementById('oc-st-ocver').textContent = d.openclaw_version || '未安装';
|
||||
|
||||
} catch(e) {
|
||||
document.getElementById('oc-st-status').innerHTML = '<span class="oc-badge oc-badge-unknown">查询失败</span>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateStatus();
|
||||
setInterval(updateStatus, 5000);
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
482
po/zh-cn/openclaw.po
Normal file
482
po/zh-cn/openclaw.po
Normal file
@@ -0,0 +1,482 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: zh-cn\n"
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# controller/openclaw.lua — 菜单 & API
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
msgid "OpenClaw"
|
||||
msgstr "OpenClaw AI 网关"
|
||||
|
||||
msgid "OpenClaw AI Gateway"
|
||||
msgstr "OpenClaw AI 网关"
|
||||
|
||||
msgid "Basic Settings"
|
||||
msgstr "基本设置"
|
||||
|
||||
msgid "Model Settings"
|
||||
msgstr "模型设置"
|
||||
|
||||
msgid "Channel Settings"
|
||||
msgstr "渠道设置"
|
||||
|
||||
msgid "Advanced"
|
||||
msgstr "高级配置"
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# basic.lua — 基本设置页
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
msgid "OpenClaw is an AI coding agent gateway that supports GitHub Copilot, Claude, GPT, Gemini and various messaging channels."
|
||||
msgstr "OpenClaw 是一个 AI 编程代理网关,支持 GitHub Copilot、Claude、GPT、Gemini 等大模型以及多种消息渠道。"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "Enable OpenClaw AI Gateway"
|
||||
msgstr "启用 OpenClaw AI 网关"
|
||||
|
||||
msgid "Gateway Port"
|
||||
msgstr "网关端口"
|
||||
|
||||
msgid "OpenClaw API listening port"
|
||||
msgstr "OpenClaw API 监听端口"
|
||||
|
||||
msgid "Bind Interface"
|
||||
msgstr "绑定接口"
|
||||
|
||||
msgid "Network binding mode"
|
||||
msgstr "网络绑定模式"
|
||||
|
||||
msgid "LAN only"
|
||||
msgstr "仅局域网"
|
||||
|
||||
msgid "Loopback only"
|
||||
msgstr "仅本地回环"
|
||||
|
||||
msgid "All interfaces (WAN accessible)"
|
||||
msgstr "所有接口(WAN 可访问)"
|
||||
|
||||
msgid "Auto"
|
||||
msgstr "自动"
|
||||
|
||||
msgid "Config Terminal Port"
|
||||
msgstr "配置终端端口"
|
||||
|
||||
msgid "Web PTY terminal port for oc-config"
|
||||
msgstr "oc-config Web PTY 终端端口"
|
||||
|
||||
msgid "Node.js Version"
|
||||
msgstr "Node.js 版本"
|
||||
|
||||
msgid "Node.js version to download (requires v22+)"
|
||||
msgstr "要下载的 Node.js 版本(需要 v22+)"
|
||||
|
||||
msgid "Authentication"
|
||||
msgstr "认证"
|
||||
|
||||
msgid "Gateway Token"
|
||||
msgstr "网关令牌"
|
||||
|
||||
msgid "Authentication token for API access. Keep it secret."
|
||||
msgstr "API 访问认证令牌,请妥善保管。"
|
||||
|
||||
msgid "Reset Token"
|
||||
msgstr "重置令牌"
|
||||
|
||||
msgid "Generate New Token"
|
||||
msgstr "生成新令牌"
|
||||
|
||||
msgid "Are you sure you want to generate a new token? Current connections will be disconnected."
|
||||
msgstr "确定要生成新令牌吗?当前连接将会断开。"
|
||||
|
||||
msgid "Generating..."
|
||||
msgstr "正在生成..."
|
||||
|
||||
msgid "Token updated"
|
||||
msgstr "令牌已更新"
|
||||
|
||||
msgid "Quick Actions"
|
||||
msgstr "快捷操作"
|
||||
|
||||
msgid "Download Node.js and install OpenClaw"
|
||||
msgstr "下载 Node.js 并安装 OpenClaw"
|
||||
|
||||
msgid "Install Environment"
|
||||
msgstr "安装环境"
|
||||
|
||||
msgid "Restart Service"
|
||||
msgstr "重启服务"
|
||||
|
||||
msgid "Stop Service"
|
||||
msgstr "停止服务"
|
||||
|
||||
msgid "Advanced Configuration"
|
||||
msgstr "高级配置"
|
||||
|
||||
msgid "Executing..."
|
||||
msgstr "正在执行..."
|
||||
|
||||
msgid "completed"
|
||||
msgstr "已完成"
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# model.lua — 模型设置页
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
msgid "Configure your AI model provider and API credentials. After saving, the configuration will be synced to OpenClaw."
|
||||
msgstr "配置您的 AI 模型提供商和 API 凭证。保存后配置将自动同步到 OpenClaw。"
|
||||
|
||||
msgid "AI Model Provider"
|
||||
msgstr "AI 模型提供商"
|
||||
|
||||
msgid "Provider"
|
||||
msgstr "提供商"
|
||||
|
||||
msgid "Select your AI model provider"
|
||||
msgstr "选择您的 AI 模型提供商"
|
||||
|
||||
msgid "-- Not configured --"
|
||||
msgstr "-- 未配置 --"
|
||||
|
||||
msgid "Multi-provider aggregator"
|
||||
msgstr "多提供商聚合器"
|
||||
|
||||
msgid "Custom OpenAI-compatible API"
|
||||
msgstr "自定义 OpenAI 兼容 API"
|
||||
|
||||
msgid "API Key"
|
||||
msgstr "API 密钥"
|
||||
|
||||
msgid "Your provider API key"
|
||||
msgstr "您的提供商 API 密钥"
|
||||
|
||||
msgid "API Base URL"
|
||||
msgstr "API 基础地址"
|
||||
|
||||
msgid "Custom OpenAI-compatible API endpoint"
|
||||
msgstr "自定义 OpenAI 兼容 API 端点"
|
||||
|
||||
msgid "Active Model"
|
||||
msgstr "当前模型"
|
||||
|
||||
msgid "Model"
|
||||
msgstr "模型"
|
||||
|
||||
msgid "Multimodal flagship (recommended)"
|
||||
msgstr "多模态旗舰(推荐)"
|
||||
|
||||
msgid "Cost effective"
|
||||
msgstr "经济实惠"
|
||||
|
||||
msgid "Reasoning flagship"
|
||||
msgstr "推理旗舰"
|
||||
|
||||
msgid "Reasoning lightweight"
|
||||
msgstr "轻量推理"
|
||||
|
||||
msgid "Reasoning classic"
|
||||
msgstr "经典推理"
|
||||
|
||||
msgid "Latest Sonnet (recommended)"
|
||||
msgstr "最新 Sonnet(推荐)"
|
||||
|
||||
msgid "Top reasoning"
|
||||
msgstr "顶级推理"
|
||||
|
||||
msgid "Fast"
|
||||
msgstr "快速"
|
||||
|
||||
msgid "Flagship reasoning (recommended)"
|
||||
msgstr "旗舰推理(推荐)"
|
||||
|
||||
msgid "Fast balanced"
|
||||
msgstr "快速均衡"
|
||||
|
||||
msgid "Low latency"
|
||||
msgstr "低延迟"
|
||||
|
||||
msgid "General chat"
|
||||
msgstr "通用对话"
|
||||
|
||||
msgid "Deep reasoning"
|
||||
msgstr "深度推理"
|
||||
|
||||
msgid "(ultra fast)"
|
||||
msgstr "(超快)"
|
||||
|
||||
msgid "Model Name"
|
||||
msgstr "模型名称"
|
||||
|
||||
msgid "For multi-provider setup, advanced model allowlist, or OAuth login (Qwen Portal, Gemini CLI), use the"
|
||||
msgstr "如需多提供商配置、高级模型白名单或 OAuth 登录(Qwen Portal、Gemini CLI),请使用"
|
||||
|
||||
msgid "page which provides the full interactive wizard."
|
||||
msgstr "页面,其中提供完整的交互式配置向导。"
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# channel.lua — 渠道设置页
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
msgid "Configure messaging channels for OpenClaw. Telegram is most popular."
|
||||
msgstr "配置 OpenClaw 的消息渠道。Telegram 是最常用的渠道。"
|
||||
|
||||
msgid "Bot Token"
|
||||
msgstr "Bot 令牌"
|
||||
|
||||
msgid "Get from @BotFather on Telegram: /newbot → copy token"
|
||||
msgstr "从 Telegram 的 @BotFather 获取:/newbot → 复制令牌"
|
||||
|
||||
msgid "After setting Bot Token, restart the service, then send /start to your Bot on Telegram."
|
||||
msgstr "设置 Bot 令牌后,重启服务,然后在 Telegram 上向您的 Bot 发送 /start。"
|
||||
|
||||
msgid "For pairing wizard, go to"
|
||||
msgstr "如需配对向导,请前往"
|
||||
|
||||
msgid "Get from discord.com/developers/applications → Bot → Reset Token"
|
||||
msgstr "从 discord.com/developers/applications → Bot → Reset Token 获取"
|
||||
|
||||
msgid "Feishu (Lark)"
|
||||
msgstr "飞书"
|
||||
|
||||
msgid "App ID"
|
||||
msgstr "应用 ID"
|
||||
|
||||
msgid "Get from open.feishu.cn → Create App"
|
||||
msgstr "从 open.feishu.cn → 创建应用 获取"
|
||||
|
||||
msgid "App Secret"
|
||||
msgstr "应用密钥"
|
||||
|
||||
msgid "Get from api.slack.com/apps → Create App (xoxb-...)"
|
||||
msgstr "从 api.slack.com/apps → Create App 获取(xoxb-...)"
|
||||
|
||||
msgid "WhatsApp requires QR code pairing through the Web console."
|
||||
msgstr "WhatsApp 需要通过 Web 控制台扫码配对。"
|
||||
|
||||
msgid "After starting the service, visit:"
|
||||
msgstr "启动服务后,请访问:"
|
||||
|
||||
msgid "Then go to Channels → WhatsApp to scan QR code."
|
||||
msgstr "然后进入 Channels → WhatsApp 扫描二维码。"
|
||||
|
||||
msgid "For official interactive channel wizard, Telegram pairing helper, or Signal/Line/MS Teams configuration, use the"
|
||||
msgstr "如需官方交互式渠道向导、Telegram 配对助手或 Signal/Line/MS Teams 配置,请使用"
|
||||
|
||||
msgid "page."
|
||||
msgstr "页面。"
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# status.htm — 状态面板
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
msgid "Service Status"
|
||||
msgstr "服务状态"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
msgid "Loading..."
|
||||
msgstr "加载中..."
|
||||
|
||||
msgid "Gateway"
|
||||
msgstr "网关"
|
||||
|
||||
msgid "Config Terminal"
|
||||
msgstr "配置终端"
|
||||
|
||||
msgid "Memory"
|
||||
msgstr "内存"
|
||||
|
||||
msgid "Uptime"
|
||||
msgstr "运行时间"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "已禁用"
|
||||
|
||||
msgid "Running"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "Stopped"
|
||||
msgstr "已停止"
|
||||
|
||||
msgid "Listening"
|
||||
msgstr "监听中"
|
||||
|
||||
msgid "Not listening"
|
||||
msgstr "未监听"
|
||||
|
||||
msgid "Not installed"
|
||||
msgstr "未安装"
|
||||
|
||||
msgid "Error"
|
||||
msgstr "错误"
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# advanced.htm — 高级配置页
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
msgid "What is this?"
|
||||
msgstr "这是什么?"
|
||||
|
||||
msgid "This page embeds the OpenClaw interactive configuration terminal (oc-config). It provides the full-featured management menu including:"
|
||||
msgstr "此页面嵌入了 OpenClaw 交互式配置终端(oc-config),提供完整的管理菜单,包括:"
|
||||
|
||||
msgid "Multi-provider AI model setup with OAuth support"
|
||||
msgstr "支持 OAuth 的多提供商 AI 模型配置"
|
||||
|
||||
msgid "Telegram pairing wizard"
|
||||
msgstr "Telegram 配对向导"
|
||||
|
||||
msgid "Health check and diagnostics"
|
||||
msgstr "健康检查和诊断"
|
||||
|
||||
msgid "Raw configuration file editor"
|
||||
msgstr "原始配置文件编辑器"
|
||||
|
||||
msgid "Factory reset"
|
||||
msgstr "恢复出厂设置"
|
||||
|
||||
msgid "Configuration Terminal"
|
||||
msgstr "配置终端"
|
||||
|
||||
msgid "Checking..."
|
||||
msgstr "检查中..."
|
||||
|
||||
msgid "Reload"
|
||||
msgstr "刷新"
|
||||
|
||||
msgid "Connecting to configuration terminal..."
|
||||
msgstr "正在连接配置终端..."
|
||||
|
||||
msgid "Available menu options in terminal"
|
||||
msgstr "终端中可用的菜单选项"
|
||||
|
||||
msgid "View current config"
|
||||
msgstr "查看当前配置"
|
||||
|
||||
msgid "Configure AI model provider"
|
||||
msgstr "配置 AI 模型提供商"
|
||||
|
||||
msgid "Set active model"
|
||||
msgstr "设置当前模型"
|
||||
|
||||
msgid "Configure channels"
|
||||
msgstr "配置消息渠道"
|
||||
|
||||
msgid "pairing wizard"
|
||||
msgstr "配对向导"
|
||||
|
||||
msgid "Health check"
|
||||
msgstr "健康检查"
|
||||
|
||||
msgid "Restart Gateway"
|
||||
msgstr "重启网关"
|
||||
|
||||
msgid "View/edit raw config"
|
||||
msgstr "查看/编辑原始配置"
|
||||
|
||||
msgid "Reset to defaults"
|
||||
msgstr "恢复默认设置"
|
||||
|
||||
msgid "Connected"
|
||||
msgstr "已连接"
|
||||
|
||||
msgid "Terminal not running"
|
||||
msgstr "终端未运行"
|
||||
|
||||
msgid "Web PTY terminal is not running."
|
||||
msgstr "Web PTY 终端未运行。"
|
||||
|
||||
msgid "Please enable and start the service first from Basic Settings page."
|
||||
msgstr "请先在基本设置页面启用并启动服务。"
|
||||
|
||||
msgid "Failed to check terminal status"
|
||||
msgstr "检查终端状态失败"
|
||||
|
||||
msgid "Reconnecting..."
|
||||
msgstr "正在重新连接..."
|
||||
|
||||
# ═══════════════════════════════════════════
|
||||
# 其他
|
||||
# ═══════════════════════════════════════════
|
||||
|
||||
msgid "Port"
|
||||
msgstr "端口"
|
||||
|
||||
msgid "PID"
|
||||
msgstr "进程 ID"
|
||||
|
||||
msgid "Node.js"
|
||||
msgstr "Node.js"
|
||||
|
||||
msgid "Web Console"
|
||||
msgstr "Web 控制台"
|
||||
|
||||
msgid "Verify Token"
|
||||
msgstr "验证令牌"
|
||||
|
||||
msgid "Allow insecure auth"
|
||||
msgstr "允许不安全认证"
|
||||
|
||||
msgid "Disable device auth"
|
||||
msgstr "禁用设备认证"
|
||||
|
||||
msgid "Allow host header fallback"
|
||||
msgstr "允许主机头回退"
|
||||
|
||||
msgid "Docker sandbox"
|
||||
msgstr "Docker 沙箱"
|
||||
|
||||
msgid "Enable Docker sandbox for code execution"
|
||||
msgstr "启用 Docker 沙箱执行代码"
|
||||
|
||||
msgid "Node.js Mirror"
|
||||
msgstr "Node.js 镜像源"
|
||||
|
||||
msgid "China mirror (npmmirror.com)"
|
||||
msgstr "中国镜像 (npmmirror.com)"
|
||||
|
||||
msgid "Official (nodejs.org)"
|
||||
msgstr "官方 (nodejs.org)"
|
||||
|
||||
msgid "Check Environment"
|
||||
msgstr "检查环境"
|
||||
|
||||
msgid "Install / Upgrade"
|
||||
msgstr "安装 / 升级"
|
||||
|
||||
msgid "Telegram"
|
||||
msgstr "Telegram"
|
||||
|
||||
msgid "Discord"
|
||||
msgstr "Discord"
|
||||
|
||||
msgid "Feishu"
|
||||
msgstr "飞书"
|
||||
|
||||
msgid "Slack"
|
||||
msgstr "Slack"
|
||||
|
||||
msgid "WhatsApp"
|
||||
msgstr "WhatsApp"
|
||||
|
||||
msgid "Regenerate Token"
|
||||
msgstr "重新生成令牌"
|
||||
|
||||
msgid "Custom API Base URL"
|
||||
msgstr "自定义 API 基础地址"
|
||||
|
||||
msgid "Auth Token"
|
||||
msgstr "认证令牌"
|
||||
|
||||
msgid "Gateway authentication token"
|
||||
msgstr "网关认证令牌"
|
||||
|
||||
msgid "Web PTY terminal port for oc-config"
|
||||
msgstr "oc-config Web PTY 终端端口"
|
||||
|
||||
msgid "Select the AI model provider to use"
|
||||
msgstr "选择要使用的 AI 模型提供商"
|
||||
6
root/etc/config/openclaw
Normal file
6
root/etc/config/openclaw
Normal file
@@ -0,0 +1,6 @@
|
||||
config openclaw 'main'
|
||||
option enabled '0'
|
||||
option port '18789'
|
||||
option bind 'lan'
|
||||
option token ''
|
||||
option pty_port '18793'
|
||||
330
root/etc/init.d/openclaw
Executable file
330
root/etc/init.d/openclaw
Executable file
@@ -0,0 +1,330 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# luci-app-openclaw — procd init 脚本
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
STOP=10
|
||||
|
||||
EXTRA_COMMANDS="setup status_service restart_gateway"
|
||||
EXTRA_HELP=" setup 下载 Node.js 并安装 OpenClaw
|
||||
status_service 显示服务状态
|
||||
restart_gateway 仅重启 Gateway 实例 (不影响 Web PTY)"
|
||||
|
||||
NODE_BASE="/opt/openclaw/node"
|
||||
OC_GLOBAL="/opt/openclaw/global"
|
||||
OC_DATA="/opt/openclaw/data"
|
||||
NODE_BIN="${NODE_BASE}/bin/node"
|
||||
CONFIG_FILE="${OC_DATA}/.openclaw/openclaw.json"
|
||||
|
||||
get_oc_entry() {
|
||||
local search_dirs="${OC_GLOBAL}/lib/node_modules/openclaw
|
||||
${OC_GLOBAL}/node_modules/openclaw
|
||||
${NODE_BASE}/lib/node_modules/openclaw"
|
||||
|
||||
# pnpm 全局安装路径形如: $OC_GLOBAL/5/node_modules/openclaw
|
||||
for ver_dir in "${OC_GLOBAL}"/*/node_modules/openclaw; do
|
||||
[ -d "$ver_dir" ] && search_dirs="$search_dirs
|
||||
$ver_dir"
|
||||
done
|
||||
|
||||
local d
|
||||
echo "$search_dirs" | while read -r d; do
|
||||
[ -z "$d" ] && continue
|
||||
if [ -f "${d}/openclaw.mjs" ]; then
|
||||
echo "${d}/openclaw.mjs"
|
||||
return
|
||||
elif [ -f "${d}/dist/cli.js" ]; then
|
||||
echo "${d}/dist/cli.js"
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
patch_iframe_headers() {
|
||||
# 移除 OpenClaw 网关的 X-Frame-Options 和 frame-ancestors 限制,允许 LuCI iframe 嵌入
|
||||
local gw_js
|
||||
for f in $(find "${OC_GLOBAL}" -name "gateway-cli-*.js" -type f 2>/dev/null); do
|
||||
if grep -q "X-Frame-Options.*DENY" "$f" 2>/dev/null; then
|
||||
sed -i "s|res.setHeader(\"X-Frame-Options\", \"DENY\");|// res.setHeader(\"X-Frame-Options\", \"DENY\"); // patched by luci-app-openclaw|g" "$f"
|
||||
sed -i "s|\"frame-ancestors 'none'\"|\"frame-ancestors *\"|g" "$f"
|
||||
logger -t openclaw "Patched iframe headers in $f"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
sync_uci_to_json() {
|
||||
# 将 UCI 配置同步到 openclaw.json,同时确保 token 双向同步
|
||||
local port bind token
|
||||
port=$(uci -q get openclaw.main.port || echo "18789")
|
||||
bind=$(uci -q get openclaw.main.bind || echo "lan")
|
||||
token=$(uci -q get openclaw.main.token || echo "")
|
||||
|
||||
# 确保配置目录和文件存在
|
||||
mkdir -p "$(dirname "$CONFIG_FILE")"
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo '{}' > "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# UCI 没有 token 时,尝试从已有的 JSON 读取
|
||||
if [ -z "$token" ] && [ -x "$NODE_BIN" ]; then
|
||||
token=$("$NODE_BIN" -e "
|
||||
try{const d=JSON.parse(require('fs').readFileSync('${CONFIG_FILE}','utf8'));
|
||||
if(d.gateway&&d.gateway.auth&&d.gateway.auth.token)process.stdout.write(d.gateway.auth.token);}catch(e){}
|
||||
" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# 如果仍然没有 token,生成一个新的
|
||||
if [ -z "$token" ]; then
|
||||
token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || echo "auto_$(date +%s)")
|
||||
fi
|
||||
|
||||
# 确保 token 写回 UCI
|
||||
local uci_token
|
||||
uci_token=$(uci -q get openclaw.main.token || echo "")
|
||||
if [ "$uci_token" != "$token" ]; then
|
||||
uci set openclaw.main.token="$token"
|
||||
uci commit openclaw 2>/dev/null
|
||||
fi
|
||||
|
||||
# 使用 Node.js 写入 JSON (如果可用)
|
||||
if [ -x "$NODE_BIN" ]; then
|
||||
OC_SYNC_PORT="$port" OC_SYNC_BIND="$bind" OC_SYNC_TOKEN="$token" OC_SYNC_FILE="$CONFIG_FILE" \
|
||||
"$NODE_BIN" -e "
|
||||
const fs=require('fs');
|
||||
const f=process.env.OC_SYNC_FILE;
|
||||
let d={};
|
||||
try{d=JSON.parse(fs.readFileSync(f,'utf8'));}catch(e){}
|
||||
if(!d.gateway)d.gateway={};
|
||||
d.gateway.port=parseInt(process.env.OC_SYNC_PORT)||18789;
|
||||
d.gateway.bind=process.env.OC_SYNC_BIND||'lan';
|
||||
d.gateway.mode='local';
|
||||
if(!d.gateway.auth)d.gateway.auth={};
|
||||
d.gateway.auth.mode='token';
|
||||
d.gateway.auth.token=process.env.OC_SYNC_TOKEN||'';
|
||||
if(!d.gateway.controlUi)d.gateway.controlUi={};
|
||||
d.gateway.controlUi.allowInsecureAuth=true;
|
||||
d.gateway.controlUi.dangerouslyDisableDeviceAuth=true;
|
||||
d.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true;
|
||||
// 清理 v2026.3.1+ 已废弃的字段,避免配置验证失败
|
||||
delete d.gateway.name;
|
||||
delete d.gateway.bonjour;
|
||||
delete d.gateway.plugins;
|
||||
fs.writeFileSync(f,JSON.stringify(d,null,2));
|
||||
" 2>/dev/null
|
||||
fi
|
||||
|
||||
chown openclaw:openclaw "$CONFIG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local enabled port bind pty_port
|
||||
config_load openclaw
|
||||
config_get enabled main enabled "0"
|
||||
config_get port main port "18789"
|
||||
config_get bind main bind "lan"
|
||||
config_get pty_port main pty_port "18793"
|
||||
|
||||
[ "$enabled" = "1" ] || {
|
||||
echo "openclaw 已禁用。请在 /etc/config/openclaw 中设置 enabled 为 1"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 检查 Node.js
|
||||
if [ ! -x "$NODE_BIN" ]; then
|
||||
echo "未找到 Node.js: $NODE_BIN"
|
||||
echo "请运行: openclaw-env setup"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 检查 openclaw 入口
|
||||
local oc_entry
|
||||
oc_entry=$(get_oc_entry)
|
||||
if [ -z "$oc_entry" ]; then
|
||||
echo "OpenClaw 未安装。请运行: openclaw-env setup"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 同步 UCI 到 JSON
|
||||
sync_uci_to_json
|
||||
|
||||
# 修复数据目录权限 (防止 root 用户操作后留下无法读取的文件)
|
||||
chown -R openclaw:openclaw "$OC_DATA" 2>/dev/null || true
|
||||
|
||||
# Patch iframe 安全头,允许 LuCI 嵌入
|
||||
patch_iframe_headers
|
||||
|
||||
# 将 UCI bind 映射到 openclaw gateway --bind 参数
|
||||
local gw_bind="loopback"
|
||||
case "$bind" in
|
||||
lan) gw_bind="lan" ;;
|
||||
loopback) gw_bind="loopback" ;;
|
||||
all) gw_bind="custom" ;; # custom = 0.0.0.0
|
||||
*) gw_bind="$bind" ;;
|
||||
esac
|
||||
|
||||
# 启动 OpenClaw Gateway (主服务, 前台运行)
|
||||
procd_open_instance "gateway"
|
||||
procd_set_param command "$NODE_BIN" "$oc_entry" gateway run \
|
||||
--port "$port" --bind "$gw_bind"
|
||||
procd_set_param env \
|
||||
HOME="$OC_DATA" \
|
||||
OPENCLAW_HOME="$OC_DATA" \
|
||||
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
|
||||
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
|
||||
NODE_BASE="$NODE_BASE" \
|
||||
OC_GLOBAL="$OC_GLOBAL" \
|
||||
OC_DATA="$OC_DATA" \
|
||||
PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
procd_set_param user openclaw
|
||||
procd_set_param respawn 3600 5 0
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param pidfile /var/run/openclaw.pid
|
||||
procd_close_instance
|
||||
|
||||
# 启动 Web PTY 配置终端 (辅助服务)
|
||||
# 生成 PTY 会话 token 用于 WebSocket 认证
|
||||
local pty_token
|
||||
pty_token=$(head -c 16 /dev/urandom | hexdump -e '16/1 "%02x"' 2>/dev/null || openssl rand -hex 16 2>/dev/null || echo "pty_$(date +%s)")
|
||||
uci set openclaw.main.pty_token="$pty_token"
|
||||
uci commit openclaw 2>/dev/null
|
||||
|
||||
procd_open_instance "pty"
|
||||
procd_set_param command "$NODE_BIN" /usr/share/openclaw/web-pty.js
|
||||
procd_set_param env \
|
||||
OC_CONFIG_PORT="$pty_port" \
|
||||
OC_PTY_TOKEN="$pty_token" \
|
||||
OC_CONFIG_SCRIPT="/usr/share/openclaw/oc-config.sh" \
|
||||
NODE_BASE="$NODE_BASE" \
|
||||
OC_GLOBAL="$OC_GLOBAL" \
|
||||
OC_DATA="$OC_DATA" \
|
||||
HOME="$OC_DATA" \
|
||||
OPENCLAW_HOME="$OC_DATA" \
|
||||
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
|
||||
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
|
||||
PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
procd_set_param respawn 3600 5 5
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param pidfile /var/run/openclaw-pty.pid
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
# procd 会自动处理进程停止
|
||||
return 0
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "openclaw"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
setup() {
|
||||
echo "正在调用 openclaw-env setup..."
|
||||
/usr/bin/openclaw-env setup
|
||||
}
|
||||
|
||||
restart_gateway() {
|
||||
# 仅重启 Gateway procd 实例,不影响 Web PTY
|
||||
# 使用 stop+start 而非 kill,可重置 procd crash loop 计数器
|
||||
local port
|
||||
port=$(uci -q get openclaw.main.port || echo "18789")
|
||||
|
||||
# 停止 gateway 实例 (通过 ubus,不影响 PTY 实例)
|
||||
ubus call service stop '{"name":"openclaw","instance":"gateway"}' 2>/dev/null || true
|
||||
|
||||
# 等待端口释放
|
||||
local i=0
|
||||
while [ $i -lt 8 ]; do
|
||||
netstat -tln 2>/dev/null | grep -q ":${port} " || break
|
||||
sleep 1; i=$((i+1))
|
||||
done
|
||||
|
||||
# 重新启动整个服务 (procd 会重建 gateway 实例,同时保留 PTY 实例)
|
||||
# procd 的 start_service 幂等:已运行的实例不会重复启动
|
||||
/etc/init.d/openclaw start >/dev/null 2>&1
|
||||
}
|
||||
|
||||
status_service() {
|
||||
local port pty_port
|
||||
port=$(uci -q get openclaw.main.port || echo "18789")
|
||||
pty_port=$(uci -q get openclaw.main.pty_port || echo "18793")
|
||||
|
||||
echo "=== OpenClaw 服务状态 ==="
|
||||
|
||||
# Node.js
|
||||
if [ -x "$NODE_BIN" ]; then
|
||||
echo "Node.js: $($NODE_BIN --version 2>/dev/null)"
|
||||
else
|
||||
echo "Node.js: 未安装"
|
||||
fi
|
||||
|
||||
# OpenClaw
|
||||
local oc_entry
|
||||
oc_entry=$(get_oc_entry)
|
||||
if [ -n "$oc_entry" ]; then
|
||||
local ver
|
||||
ver=$("$NODE_BIN" "$oc_entry" --version 2>/dev/null | tr -d '[:space:]')
|
||||
echo "OpenClaw: v${ver:-未知}"
|
||||
else
|
||||
echo "OpenClaw: 未安装"
|
||||
fi
|
||||
|
||||
# 端口检测函数 (ss 优先, 回退 netstat)
|
||||
_check_port() {
|
||||
local p="$1"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -tlnp 2>/dev/null | grep -q ":${p} "
|
||||
else
|
||||
netstat -tlnp 2>/dev/null | grep -q ":${p} "
|
||||
fi
|
||||
}
|
||||
|
||||
_get_pid_by_port() {
|
||||
local p="$1"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -tlnp 2>/dev/null | grep ":${p} " | head -1 | sed -n 's/.*pid=\([0-9]*\).*/\1/p'
|
||||
else
|
||||
netstat -tlnp 2>/dev/null | grep ":${p} " | head -1 | sed 's|.* \([0-9]*\)/.*|\1|'
|
||||
fi
|
||||
}
|
||||
|
||||
# Gateway port
|
||||
if _check_port "$port"; then
|
||||
echo "网关: 运行中 (端口 $port)"
|
||||
# PID
|
||||
local pid
|
||||
pid=$(_get_pid_by_port "$port")
|
||||
if [ -n "$pid" ]; then
|
||||
echo "进程ID: $pid"
|
||||
# Memory
|
||||
local rss
|
||||
rss=$(awk '/VmRSS/{print $2}' /proc/$pid/status 2>/dev/null)
|
||||
[ -n "$rss" ] && echo "内存: ${rss} kB"
|
||||
# Uptime
|
||||
local start_time now_time
|
||||
start_time=$(stat -c %Y /proc/$pid 2>/dev/null || echo "0")
|
||||
now_time=$(date +%s)
|
||||
if [ "$start_time" -gt 0 ]; then
|
||||
local uptime=$((now_time - start_time))
|
||||
local hours=$((uptime / 3600))
|
||||
local mins=$(( (uptime % 3600) / 60 ))
|
||||
echo "运行时间: ${hours}小时 ${mins}分钟"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "网关: 已停止"
|
||||
fi
|
||||
|
||||
# PTY port
|
||||
if _check_port "$pty_port"; then
|
||||
echo "Web PTY: 运行中 (端口 $pty_port)"
|
||||
else
|
||||
echo "Web PTY: 已停止"
|
||||
fi
|
||||
}
|
||||
41
root/etc/uci-defaults/99-openclaw
Executable file
41
root/etc/uci-defaults/99-openclaw
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
# luci-app-openclaw — 首次安装初始化脚本
|
||||
|
||||
# 创建 openclaw 系统用户 (无 home, 无 shell)
|
||||
if ! id openclaw >/dev/null 2>&1; then
|
||||
# 动态查找可用 UID/GID (从 1000 开始,避免与已有用户冲突)
|
||||
OC_UID=1000
|
||||
while grep -q "^[^:]*:x:${OC_UID}:" /etc/passwd 2>/dev/null; do
|
||||
OC_UID=$((OC_UID + 1))
|
||||
done
|
||||
OC_GID=$OC_UID
|
||||
while grep -q "^[^:]*:x:${OC_GID}:" /etc/group 2>/dev/null; do
|
||||
OC_GID=$((OC_GID + 1))
|
||||
done
|
||||
# OpenWrt 方式:直接写入 /etc/passwd 和 /etc/shadow
|
||||
if ! grep -q '^openclaw:' /etc/passwd 2>/dev/null; then
|
||||
echo "openclaw:x:${OC_UID}:${OC_GID}:openclaw:/opt/openclaw/data:/bin/false" >> /etc/passwd
|
||||
fi
|
||||
if ! grep -q '^openclaw:' /etc/shadow 2>/dev/null; then
|
||||
echo 'openclaw:x:0:0:99999:7:::' >> /etc/shadow
|
||||
fi
|
||||
if ! grep -q '^openclaw:' /etc/group 2>/dev/null; then
|
||||
echo "openclaw:x:${OC_GID}:" >> /etc/group
|
||||
fi
|
||||
fi
|
||||
|
||||
# 创建数据目录
|
||||
mkdir -p /opt/openclaw/data/.openclaw
|
||||
mkdir -p /opt/openclaw/node
|
||||
mkdir -p /opt/openclaw/global
|
||||
chown -R openclaw:openclaw /opt/openclaw 2>/dev/null || true
|
||||
|
||||
# 生成随机 Token (如果尚未设置)
|
||||
CURRENT_TOKEN=$(uci -q get openclaw.main.token)
|
||||
if [ -z "$CURRENT_TOKEN" ]; then
|
||||
TOKEN=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || echo "changeme_$(date +%s)")
|
||||
uci set openclaw.main.token="$TOKEN"
|
||||
uci commit openclaw
|
||||
fi
|
||||
|
||||
exit 0
|
||||
478
root/usr/bin/openclaw-env
Executable file
478
root/usr/bin/openclaw-env
Executable file
@@ -0,0 +1,478 @@
|
||||
#!/bin/sh
|
||||
# ============================================================================
|
||||
# openclaw-env — Node.js 环境自动检测/下载 + OpenClaw 安装
|
||||
# 用法:
|
||||
# openclaw-env setup — 完整安装 (Node.js + pnpm + OpenClaw)
|
||||
# openclaw-env check — 检查环境状态
|
||||
# openclaw-env upgrade — 升级 OpenClaw 到最新版
|
||||
# openclaw-env node — 仅下载/更新 Node.js
|
||||
# 环境变量:
|
||||
# OC_VERSION — 指定 OpenClaw 版本 (如 2026.3.1),不设置则安装最新版
|
||||
# ============================================================================
|
||||
set -e
|
||||
|
||||
NODE_VERSION="${NODE_VERSION:-22.16.0}"
|
||||
# 经过验证的 OpenClaw 稳定版本 (更新此值需同步测试)
|
||||
OC_TESTED_VERSION="2026.3.1"
|
||||
# 用户可通过 OC_VERSION 环境变量覆盖安装版本
|
||||
OC_VERSION="${OC_VERSION:-}"
|
||||
NODE_BASE="/opt/openclaw/node"
|
||||
OC_GLOBAL="/opt/openclaw/global"
|
||||
OC_DATA="/opt/openclaw/data"
|
||||
NODE_BIN="${NODE_BASE}/bin/node"
|
||||
NPM_BIN="${NODE_BASE}/bin/npm"
|
||||
PNPM_BIN="${OC_GLOBAL}/bin/pnpm"
|
||||
|
||||
# Node.js 官方镜像 + musl 非官方构建
|
||||
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"
|
||||
|
||||
export PATH="${NODE_BASE}/bin:${OC_GLOBAL}/bin:$PATH"
|
||||
|
||||
log_info() { echo " [✓] $1"; }
|
||||
log_warn() { echo " [!] $1"; }
|
||||
log_error() { echo " [✗] $1"; }
|
||||
|
||||
# 检测 C 运行时类型 (glibc vs musl)
|
||||
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
|
||||
}
|
||||
|
||||
# 在所有可能的位置查找 openclaw 入口文件
|
||||
# pnpm 全局安装路径形如: $OC_GLOBAL/5/node_modules/openclaw
|
||||
find_oc_entry() {
|
||||
local search_dirs="${OC_GLOBAL}/lib/node_modules/openclaw
|
||||
${OC_GLOBAL}/node_modules/openclaw
|
||||
${NODE_BASE}/lib/node_modules/openclaw"
|
||||
|
||||
# 添加 pnpm 版本化目录 (如 /opt/openclaw/global/5/node_modules/openclaw)
|
||||
for ver_dir in "${OC_GLOBAL}"/*/node_modules/openclaw; do
|
||||
[ -d "$ver_dir" ] 2>/dev/null && search_dirs="$search_dirs
|
||||
$ver_dir"
|
||||
done
|
||||
|
||||
local d
|
||||
echo "$search_dirs" | while read -r d; do
|
||||
[ -z "$d" ] && continue
|
||||
if [ -f "${d}/openclaw.mjs" ]; then
|
||||
echo "${d}/openclaw.mjs"
|
||||
return
|
||||
elif [ -f "${d}/dist/cli.js" ]; then
|
||||
echo "${d}/dist/cli.js"
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
detect_arch() {
|
||||
local arch
|
||||
arch=$(uname -m)
|
||||
case "$arch" in
|
||||
x86_64) echo "linux-x64" ;;
|
||||
aarch64) echo "linux-arm64" ;;
|
||||
armv7l|armv6l)
|
||||
log_error "不支持的 CPU 架构: $arch"
|
||||
log_error "Node.js v22+ 不提供 32 位 ARM 预编译包。"
|
||||
log_error "建议: 使用 aarch64 (ARM64) 版本的固件,或使用 x86_64 设备。"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
log_error "不支持的 CPU 架构: $arch (仅支持 x86_64/aarch64)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
download_node() {
|
||||
local node_ver="$1"
|
||||
local node_arch
|
||||
node_arch=$(detect_arch)
|
||||
local libc_type
|
||||
libc_type=$(detect_libc)
|
||||
|
||||
local tarball=""
|
||||
local url="" url_fallback=""
|
||||
|
||||
if [ "$libc_type" = "musl" ]; then
|
||||
tarball="node-v${node_ver}-${node_arch}-musl.tar.xz"
|
||||
url="${NODE_MUSL_MIRROR}/v${node_ver}/${tarball}"
|
||||
url_fallback=""
|
||||
echo ""
|
||||
echo "=== 下载 Node.js v${node_ver} (${node_arch}, musl libc) ==="
|
||||
else
|
||||
tarball="node-v${node_ver}-${node_arch}.tar.xz"
|
||||
url="${NODE_MIRROR}/v${node_ver}/${tarball}"
|
||||
url_fallback="${NODE_MIRROR_CN}/v${node_ver}/${tarball}"
|
||||
echo ""
|
||||
echo "=== 下载 Node.js v${node_ver} (${node_arch}, glibc) ==="
|
||||
fi
|
||||
|
||||
local tmp_file="/tmp/${tarball}"
|
||||
|
||||
# 如果已存在且版本正确, 跳过
|
||||
if [ -x "$NODE_BIN" ]; then
|
||||
local current_ver
|
||||
current_ver=$("$NODE_BIN" --version 2>/dev/null | sed 's/^v//')
|
||||
if [ "$current_ver" = "$node_ver" ]; then
|
||||
log_info "Node.js v${node_ver} 已安装, 跳过下载"
|
||||
return 0
|
||||
fi
|
||||
log_warn "当前 Node.js v${current_ver}, 将更新到 v${node_ver}"
|
||||
fi
|
||||
|
||||
# 下载 (带重试)
|
||||
local downloaded=0
|
||||
local mirror_list="$url"
|
||||
[ -n "$url_fallback" ] && mirror_list="$url $url_fallback"
|
||||
for mirror_url in $mirror_list; do
|
||||
echo " 正在从 ${mirror_url} 下载..."
|
||||
if curl -fSL --connect-timeout 15 --max-time 300 -o "$tmp_file" "$mirror_url" 2>/dev/null || \
|
||||
wget -q --timeout=15 -O "$tmp_file" "$mirror_url" 2>/dev/null; then
|
||||
downloaded=1
|
||||
break
|
||||
fi
|
||||
log_warn "下载失败, 尝试备用镜像..."
|
||||
done
|
||||
|
||||
if [ "$downloaded" -eq 0 ]; then
|
||||
log_error "所有镜像均下载失败"
|
||||
rm -f "$tmp_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 解压
|
||||
echo " 正在解压到 ${NODE_BASE}..."
|
||||
rm -rf "$NODE_BASE"
|
||||
mkdir -p "$NODE_BASE"
|
||||
tar xf "$tmp_file" -C "$NODE_BASE" --strip-components=1
|
||||
rm -f "$tmp_file"
|
||||
|
||||
# 验证
|
||||
if [ -x "$NODE_BIN" ]; then
|
||||
log_info "Node.js $($NODE_BIN --version) 安装成功"
|
||||
else
|
||||
log_error "Node.js 安装验证失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_pnpm() {
|
||||
echo ""
|
||||
echo "=== 安装 pnpm ==="
|
||||
|
||||
if [ -x "$PNPM_BIN" ]; then
|
||||
log_info "pnpm 已安装: $($PNPM_BIN --version 2>/dev/null)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 使用 npm 安装 pnpm 到全局目录
|
||||
mkdir -p "$OC_GLOBAL"
|
||||
"$NPM_BIN" install -g pnpm --prefix="$OC_GLOBAL" 2>/dev/null
|
||||
|
||||
if [ -x "$OC_GLOBAL/bin/pnpm" ]; then
|
||||
PNPM_BIN="$OC_GLOBAL/bin/pnpm"
|
||||
log_info "pnpm $($PNPM_BIN --version 2>/dev/null) 安装成功"
|
||||
elif [ -x "$NODE_BASE/bin/pnpm" ]; then
|
||||
PNPM_BIN="$NODE_BASE/bin/pnpm"
|
||||
log_info "pnpm $($PNPM_BIN --version 2>/dev/null) 安装成功"
|
||||
else
|
||||
log_warn "pnpm 安装失败, 将使用 npm 作为回退"
|
||||
fi
|
||||
}
|
||||
|
||||
install_openclaw() {
|
||||
echo ""
|
||||
echo "=== 安装 OpenClaw ==="
|
||||
|
||||
# 确定安装版本
|
||||
local oc_pkg="openclaw"
|
||||
if [ -n "$OC_VERSION" ]; then
|
||||
oc_pkg="openclaw@${OC_VERSION}"
|
||||
log_info "指定版本: v${OC_VERSION}"
|
||||
else
|
||||
oc_pkg="openclaw@latest"
|
||||
log_info "安装最新版本"
|
||||
fi
|
||||
|
||||
local libc_type
|
||||
libc_type=$(detect_libc)
|
||||
|
||||
# musl 系统使用 npm + --ignore-scripts 避免 node-llama-cpp 编译失败
|
||||
# glibc 系统正常安装
|
||||
local install_flags=""
|
||||
if [ "$libc_type" = "musl" ]; then
|
||||
log_warn "检测到 musl libc,将跳过本地编译依赖 (不影响核心功能)"
|
||||
install_flags="--ignore-scripts"
|
||||
fi
|
||||
|
||||
# 检查 git 是否可用 (openclaw 部分依赖可能使用 git:// 协议)
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
log_warn "未检测到 git,正在尝试安装..."
|
||||
opkg update >/dev/null 2>&1
|
||||
opkg install git git-http 2>&1 | tail -3 || true
|
||||
if command -v git >/dev/null 2>&1; then
|
||||
log_info "git 安装成功"
|
||||
else
|
||||
log_warn "git 安装失败,将尝试无 git 模式安装"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 优先用 npm 安装 (pnpm 在 musl 上全局安装可能有路径问题)
|
||||
local npm_ok=0
|
||||
if [ -x "$NPM_BIN" ]; then
|
||||
mkdir -p "$OC_GLOBAL"
|
||||
"$NPM_BIN" install -g "$oc_pkg" --prefix="$OC_GLOBAL" $install_flags 2>&1 | tail -10
|
||||
# 检查是否安装成功
|
||||
if [ -n "$(find_oc_entry)" ]; then
|
||||
npm_ok=1
|
||||
else
|
||||
log_warn "首次安装未成功,尝试 --no-optional 模式重试..."
|
||||
"$NPM_BIN" install -g "$oc_pkg" --prefix="$OC_GLOBAL" $install_flags --no-optional 2>&1 | tail -10
|
||||
[ -n "$(find_oc_entry)" ] && npm_ok=1
|
||||
fi
|
||||
elif [ -x "$PNPM_BIN" ]; then
|
||||
mkdir -p "$OC_GLOBAL"
|
||||
"$PNPM_BIN" install -g "$oc_pkg" --prefix="$OC_GLOBAL" 2>&1 | tail -5
|
||||
else
|
||||
log_error "npm 和 pnpm 均不可用"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 验证
|
||||
local oc_ver=""
|
||||
local oc_found
|
||||
oc_found=$(find_oc_entry)
|
||||
if [ -n "$oc_found" ]; then
|
||||
oc_ver=$("$NODE_BIN" "$oc_found" --version 2>/dev/null | tr -d '[:space:]')
|
||||
fi
|
||||
|
||||
if [ -n "$oc_ver" ]; then
|
||||
log_info "OpenClaw v${oc_ver} 安装成功"
|
||||
else
|
||||
log_error "OpenClaw 安装验证失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
init_openclaw() {
|
||||
echo ""
|
||||
echo "=== 初始化 OpenClaw ==="
|
||||
|
||||
# 创建数据目录
|
||||
mkdir -p "$OC_DATA/.openclaw"
|
||||
|
||||
# 运行 onboard
|
||||
local oc_entry=""
|
||||
oc_entry=$(find_oc_entry)
|
||||
|
||||
if [ -n "$oc_entry" ]; then
|
||||
HOME="$OC_DATA" \
|
||||
OPENCLAW_HOME="$OC_DATA" \
|
||||
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
|
||||
OPENCLAW_CONFIG_PATH="${OC_DATA}/.openclaw/openclaw.json" \
|
||||
"$NODE_BIN" "$oc_entry" onboard --non-interactive --accept-risk 2>/dev/null || true
|
||||
log_info "初始化完成"
|
||||
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
|
||||
}
|
||||
|
||||
do_setup() {
|
||||
local node_ver="$NODE_VERSION"
|
||||
|
||||
# 检查是否已安装
|
||||
if [ -x "$NODE_BIN" ] && [ -n "$(find_oc_entry)" ]; then
|
||||
local oc_ver=""
|
||||
local oc_entry="$(find_oc_entry)"
|
||||
local oc_pkg_dir="$(dirname "$oc_entry")"
|
||||
[ -f "$oc_pkg_dir/package.json" ] && \
|
||||
oc_ver=$("$NODE_BIN" -e "try{console.log(require('$oc_pkg_dir/package.json').version)}catch(e){}" 2>/dev/null) || true
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ⚠️ OpenClaw 运行环境已安装,无需重复安装 ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " Node.js: $($NODE_BIN --version 2>/dev/null)"
|
||||
[ -n "$oc_ver" ] && echo " OpenClaw: v${oc_ver}"
|
||||
echo ""
|
||||
echo " 如需升级,请使用: openclaw-env upgrade"
|
||||
echo " 如需重装,请先卸载: 在 LuCI 界面点击「卸载环境」"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 一万AI分享 OpenClaw 环境安装 ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo " 架构: $(uname -m)"
|
||||
echo " Node 版本: v${node_ver}"
|
||||
if [ -n "$OC_VERSION" ]; then
|
||||
echo " OpenClaw: v${OC_VERSION} (稳定版)"
|
||||
else
|
||||
echo " OpenClaw: 最新版"
|
||||
fi
|
||||
echo " 安装路径: ${NODE_BASE}"
|
||||
echo " 数据路径: ${OC_DATA}"
|
||||
echo ""
|
||||
|
||||
download_node "$node_ver"
|
||||
install_pnpm
|
||||
install_openclaw
|
||||
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 "╚══════════════════════════════════════════════════════════════╝"
|
||||
}
|
||||
|
||||
do_check() {
|
||||
echo "=== OpenClaw 环境检查 ==="
|
||||
echo ""
|
||||
|
||||
# Node.js
|
||||
if [ -x "$NODE_BIN" ]; then
|
||||
echo " Node.js: $($NODE_BIN --version 2>/dev/null)"
|
||||
else
|
||||
echo " Node.js: 未安装"
|
||||
fi
|
||||
|
||||
# pnpm
|
||||
if [ -x "$PNPM_BIN" ]; then
|
||||
echo " pnpm: $($PNPM_BIN --version 2>/dev/null)"
|
||||
elif [ -x "${NODE_BASE}/bin/pnpm" ]; then
|
||||
echo " pnpm: $(${NODE_BASE}/bin/pnpm --version 2>/dev/null)"
|
||||
else
|
||||
echo " pnpm: 未安装"
|
||||
fi
|
||||
|
||||
# OpenClaw
|
||||
local oc_entry=""
|
||||
oc_entry=$(find_oc_entry)
|
||||
if [ -n "$oc_entry" ] && [ -x "$NODE_BIN" ]; then
|
||||
echo " OpenClaw: v$($NODE_BIN $oc_entry --version 2>/dev/null | tr -d '[:space:]')"
|
||||
else
|
||||
echo " OpenClaw: 未安装"
|
||||
fi
|
||||
|
||||
# 配置文件
|
||||
if [ -f "$OC_DATA/.openclaw/openclaw.json" ]; then
|
||||
echo " 配置: $OC_DATA/.openclaw/openclaw.json (存在)"
|
||||
else
|
||||
echo " 配置: 未初始化"
|
||||
fi
|
||||
|
||||
# 磁盘使用
|
||||
local used
|
||||
used=$(du -sh /opt/openclaw 2>/dev/null | awk '{print $1}')
|
||||
echo " 磁盘: ${used:-N/A}"
|
||||
|
||||
# libc 类型
|
||||
echo " C库: $(detect_libc)"
|
||||
}
|
||||
|
||||
do_upgrade() {
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ 一万AI分享 OpenClaw 升级 ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
if [ ! -x "$NODE_BIN" ]; then
|
||||
log_error "Node.js 未安装, 请先运行: openclaw-env setup"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 获取当前版本
|
||||
local current_ver=""
|
||||
local oc_entry=""
|
||||
oc_entry=$(find_oc_entry)
|
||||
if [ -n "$oc_entry" ]; then
|
||||
local oc_pkg_dir="$(dirname "$oc_entry")"
|
||||
[ -f "$oc_pkg_dir/package.json" ] && \
|
||||
current_ver=$("$NODE_BIN" -e "try{console.log(require('$oc_pkg_dir/package.json').version)}catch(e){}" 2>/dev/null) || true
|
||||
fi
|
||||
|
||||
echo " Node.js: $($NODE_BIN --version 2>/dev/null)"
|
||||
[ -n "$current_ver" ] && echo " 当前版本: v${current_ver}"
|
||||
echo ""
|
||||
|
||||
local libc_type
|
||||
libc_type=$(detect_libc)
|
||||
local install_flags=""
|
||||
[ "$libc_type" = "musl" ] && install_flags="--ignore-scripts"
|
||||
|
||||
echo "=== 正在升级 OpenClaw ==="
|
||||
echo ""
|
||||
"$NPM_BIN" install -g openclaw@latest --prefix="$OC_GLOBAL" $install_flags 2>&1
|
||||
|
||||
# 验证升级结果
|
||||
local new_ver=""
|
||||
local new_entry=""
|
||||
new_entry=$(find_oc_entry)
|
||||
if [ -n "$new_entry" ]; then
|
||||
local new_pkg_dir="$(dirname "$new_entry")"
|
||||
[ -f "$new_pkg_dir/package.json" ] && \
|
||||
new_ver=$("$NODE_BIN" -e "try{console.log(require('$new_pkg_dir/package.json').version)}catch(e){}" 2>/dev/null) || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [ -n "$new_ver" ]; then
|
||||
if [ "$current_ver" = "$new_ver" ]; then
|
||||
log_info "当前已是最新版本 v${new_ver}"
|
||||
else
|
||||
log_info "升级成功: v${current_ver} → v${new_ver}"
|
||||
fi
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ✅ 升级完成! ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
else
|
||||
log_error "升级验证失败,OpenClaw 入口文件未找到"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── 主入口 ──
|
||||
case "${1:-}" in
|
||||
setup)
|
||||
do_setup
|
||||
;;
|
||||
check)
|
||||
do_check
|
||||
;;
|
||||
upgrade)
|
||||
do_upgrade
|
||||
;;
|
||||
node)
|
||||
download_node "$NODE_VERSION"
|
||||
;;
|
||||
*)
|
||||
echo "用法: openclaw-env {setup|check|upgrade|node}"
|
||||
echo ""
|
||||
echo " setup — 完整安装 (下载 Node.js + pnpm + OpenClaw)"
|
||||
echo " check — 检查环境状态"
|
||||
echo " upgrade — 升级 OpenClaw 到最新版"
|
||||
echo " node — 仅下载/更新 Node.js"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
1326
root/usr/share/openclaw/oc-config.sh
Executable file
1326
root/usr/share/openclaw/oc-config.sh
Executable file
File diff suppressed because it is too large
Load Diff
BIN
root/usr/share/openclaw/ui/images/icon_256.png
Normal file
BIN
root/usr/share/openclaw/ui/images/icon_256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
BIN
root/usr/share/openclaw/ui/images/icon_64.png
Normal file
BIN
root/usr/share/openclaw/ui/images/icon_64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
280
root/usr/share/openclaw/ui/index.html
Normal file
280
root/usr/share/openclaw/ui/index.html
Normal file
@@ -0,0 +1,280 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>一万AI分享 OpenClaw 配置管理</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:#1a1b26;font-family:system-ui,-apple-system,sans-serif}
|
||||
#loading{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#7aa2f7;z-index:100;background:#1a1b26;transition:opacity .4s}
|
||||
#loading.hidden{opacity:0;pointer-events:none}
|
||||
#loading .spinner{width:40px;height:40px;border:3px solid #2a2b3d;border-top:3px solid #ff9e64;border-radius:50%;animation:spin .8s linear infinite;margin-bottom:16px}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
#loading h2{font-size:18px;font-weight:500;margin-bottom:8px}
|
||||
#loading p{font-size:13px;color:#565f89}
|
||||
#loading .debug{font-size:11px;color:#3b3d57;margin-top:12px;max-width:90%;word-break:break-all;text-align:center}
|
||||
#terminal-container{width:100%;height:calc(100% - 36px);position:absolute;top:36px;left:0;right:0;bottom:0;padding:4px;overflow:hidden}
|
||||
.xterm{height:100%!important}
|
||||
.xterm-viewport::-webkit-scrollbar{width:8px}
|
||||
.xterm-viewport::-webkit-scrollbar-thumb{background:#2a2b3d;border-radius:4px}
|
||||
.xterm-viewport::-webkit-scrollbar-thumb:hover{background:#3b3d57}
|
||||
#topbar{position:fixed;top:0;left:0;right:0;height:36px;background:#16161e;border-bottom:1px solid #2a2b3d;display:flex;align-items:center;padding:0 12px;z-index:50;gap:8px}
|
||||
#topbar .logo{font-size:14px;color:#ff9e64;font-weight:600}
|
||||
#topbar .status{font-size:11px;padding:2px 8px;border-radius:10px;background:#2a2b3d}
|
||||
#topbar .status.connected{color:#9ece6a}
|
||||
#topbar .status.disconnected{color:#f7768e}
|
||||
#topbar .btn{font-size:12px;color:#7aa2f7;background:none;border:1px solid #3b3d57;border-radius:4px;padding:3px 10px;cursor:pointer;margin-left:auto}
|
||||
#topbar .btn:hover{background:#2a2b3d}
|
||||
#reconnect-overlay{display:none;position:absolute;inset:0;background:rgba(26,27,38,.9);z-index:80;flex-direction:column;align-items:center;justify-content:center;color:#c0caf5}
|
||||
#reconnect-overlay.show{display:flex}
|
||||
#reconnect-overlay button{margin-top:16px;padding:8px 24px;background:#ff9e64;color:#1a1b26;border:none;border-radius:6px;font-size:14px;cursor:pointer;font-weight:600}
|
||||
#reconnect-overlay button:hover{background:#e0884a}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="topbar">
|
||||
<span class="logo">🦞 一万AI分享 OpenClaw 配置管理</span>
|
||||
<span id="status" class="status disconnected">● 连接中...</span>
|
||||
<button class="btn" onclick="location.reload()" title="重新启动配置脚本">🔄 重启</button>
|
||||
</div>
|
||||
|
||||
<div id="loading">
|
||||
<div class="spinner"></div>
|
||||
<h2>🦞 一万AI分享 OpenClaw 配置管理工具</h2>
|
||||
<p id="loading-msg">正在连接终端...</p>
|
||||
<div id="loading-debug" class="debug"></div>
|
||||
</div>
|
||||
|
||||
<div id="terminal-container"></div>
|
||||
|
||||
<div id="reconnect-overlay">
|
||||
<p style="font-size:16px;margin-bottom:4px">⚡ 连接已断开</p>
|
||||
<p style="font-size:13px;color:#565f89">配置脚本已退出或连接中断</p>
|
||||
<button onclick="location.reload()">🔄 重新启动</button>
|
||||
</div>
|
||||
|
||||
<link rel="stylesheet" href="/lib/xterm.min.css">
|
||||
<script src="/lib/xterm.min.js"></script>
|
||||
<script src="/lib/addon-fit.min.js"></script>
|
||||
<script src="/lib/addon-web-links.min.js"></script>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const statusEl = document.getElementById('status');
|
||||
const loadingEl = document.getElementById('loading');
|
||||
const reconnectEl = document.getElementById('reconnect-overlay');
|
||||
const containerEl = document.getElementById('terminal-container');
|
||||
const loadingMsg = document.getElementById('loading-msg');
|
||||
const loadingDebug = document.getElementById('loading-debug');
|
||||
|
||||
// ── 创建终端 ──
|
||||
const term = new window.Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'bar',
|
||||
fontSize: 15,
|
||||
fontFamily: '"Cascadia Code", "Fira Code", "JetBrains Mono", "Source Code Pro", Menlo, Monaco, "Courier New", monospace',
|
||||
lineHeight: 1.2,
|
||||
scrollback: 5000,
|
||||
allowProposedApi: true,
|
||||
theme: {
|
||||
background: '#1a1b26',
|
||||
foreground: '#c0caf5',
|
||||
cursor: '#ff9e64',
|
||||
cursorAccent: '#1a1b26',
|
||||
selectionBackground: '#33467c',
|
||||
selectionForeground: '#c0caf5',
|
||||
black: '#15161e',
|
||||
red: '#f7768e',
|
||||
green: '#9ece6a',
|
||||
yellow: '#e0af68',
|
||||
blue: '#7aa2f7',
|
||||
magenta: '#bb9af7',
|
||||
cyan: '#7dcfff',
|
||||
white: '#a9b1d6',
|
||||
brightBlack: '#414868',
|
||||
brightRed: '#f7768e',
|
||||
brightGreen: '#9ece6a',
|
||||
brightYellow: '#e0af68',
|
||||
brightBlue: '#7aa2f7',
|
||||
brightMagenta: '#bb9af7',
|
||||
brightCyan: '#7dcfff',
|
||||
brightWhite: '#c0caf5',
|
||||
}
|
||||
});
|
||||
|
||||
const fitAddon = new window.FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
|
||||
try {
|
||||
const webLinksAddon = new window.WebLinksAddon.WebLinksAddon();
|
||||
term.loadAddon(webLinksAddon);
|
||||
} catch(e) { /* optional */ }
|
||||
|
||||
term.open(containerEl);
|
||||
|
||||
function doFit() {
|
||||
try { fitAddon.fit(); } catch(e) { /* ignore */ }
|
||||
try { term.scrollToBottom(); } catch(e) { /* ignore */ }
|
||||
}
|
||||
doFit();
|
||||
window.addEventListener('resize', () => { setTimeout(doFit, 100); });
|
||||
|
||||
// 监听 iframe 容器大小变化 (FnOS 桌面可能调整窗口大小)
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
new ResizeObserver(function() { setTimeout(doFit, 50); }).observe(containerEl);
|
||||
}
|
||||
|
||||
// ── WebSocket 连接 ──
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
// 从 URL 参数或 hash 获取 PTY token 用于 WebSocket 认证
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
const ptyToken = urlParams.get('pty_token') || '';
|
||||
const wsUrl = proto + '//' + location.host + '/ws' + (ptyToken ? '?token=' + encodeURIComponent(ptyToken) : '');
|
||||
let ws = null;
|
||||
let connected = false;
|
||||
let retryCount = 0;
|
||||
const MAX_RETRY = 20;
|
||||
let retryTimer = null;
|
||||
let wasEverConnected = false;
|
||||
|
||||
console.log('[oc-config] protocol:', location.protocol);
|
||||
console.log('[oc-config] host:', location.host, 'port:', location.port);
|
||||
console.log('[oc-config] WebSocket URL:', wsUrl);
|
||||
loadingDebug.textContent = wsUrl;
|
||||
|
||||
function connect() {
|
||||
if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
|
||||
if (ws) { try { ws.close(); } catch(e){} ws = null; }
|
||||
|
||||
retryCount++;
|
||||
console.log('[oc-config] Connecting to', wsUrl, '(attempt', retryCount + ')');
|
||||
loadingMsg.textContent = retryCount > 1
|
||||
? '正在重新连接... (第 ' + retryCount + ' 次)'
|
||||
: '正在连接终端...';
|
||||
loadingDebug.textContent = wsUrl;
|
||||
|
||||
// 先做一个 HTTP 预检确认服务器可达
|
||||
fetch('/health').then(function(r) {
|
||||
return r.json();
|
||||
}).then(function(data) {
|
||||
console.log('[oc-config] Health check OK:', JSON.stringify(data));
|
||||
doWebSocket();
|
||||
}).catch(function(err) {
|
||||
console.log('[oc-config] Health check failed:', err.message || err);
|
||||
// 服务器不可达,稍后重试
|
||||
scheduleRetry();
|
||||
});
|
||||
}
|
||||
|
||||
function doWebSocket() {
|
||||
try {
|
||||
ws = new WebSocket(wsUrl);
|
||||
} catch(e) {
|
||||
console.error('[oc-config] WebSocket constructor error:', e);
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[oc-config] WebSocket created, readyState:', ws.readyState);
|
||||
|
||||
// 连接超时保护: 5秒没连上就重试
|
||||
var connectTimeout = setTimeout(function() {
|
||||
if (ws && ws.readyState !== WebSocket.OPEN) {
|
||||
console.log('[oc-config] Connection timeout, readyState:', ws.readyState);
|
||||
try { ws.close(); } catch(e){}
|
||||
scheduleRetry();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
ws.onopen = function() {
|
||||
clearTimeout(connectTimeout);
|
||||
connected = true;
|
||||
wasEverConnected = true;
|
||||
retryCount = 0;
|
||||
loadingEl.classList.add('hidden');
|
||||
reconnectEl.classList.remove('show');
|
||||
statusEl.textContent = '● 已连接';
|
||||
statusEl.className = 'status connected';
|
||||
// 连接后重新 fit,确保尺寸正确
|
||||
setTimeout(doFit, 50);
|
||||
term.focus();
|
||||
try {
|
||||
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
ws.onmessage = function(ev) {
|
||||
if (typeof ev.data === 'string') {
|
||||
term.write(ev.data, function() { term.scrollToBottom(); });
|
||||
} else if (ev.data instanceof Blob) {
|
||||
ev.data.text().then(function(text) { term.write(text, function() { term.scrollToBottom(); }); });
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function(ev) {
|
||||
clearTimeout(connectTimeout);
|
||||
connected = false;
|
||||
statusEl.textContent = '● 已断开';
|
||||
statusEl.className = 'status disconnected';
|
||||
console.log('[oc-config] WebSocket closed, code:', ev.code, 'reason:', ev.reason);
|
||||
if (wasEverConnected) {
|
||||
// 自动重连,不显示断连覆盖层
|
||||
console.log('[oc-config] Auto-reconnecting in 2s...');
|
||||
statusEl.textContent = '● 重连中...';
|
||||
retryCount = 0;
|
||||
setTimeout(function() {
|
||||
connect();
|
||||
}, 2000);
|
||||
} else {
|
||||
// 从未连接成功过,自动重试
|
||||
scheduleRetry();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function(ev) {
|
||||
clearTimeout(connectTimeout);
|
||||
connected = false;
|
||||
console.error('[oc-config] WebSocket error, will retry');
|
||||
// 不要在这里做UI更新, onclose 会紧跟着触发
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleRetry() {
|
||||
if (retryCount >= MAX_RETRY) {
|
||||
loadingMsg.textContent = '连接失败,请检查服务状态';
|
||||
loadingDebug.textContent = '已重试 ' + MAX_RETRY + ' 次。请刷新页面重试。';
|
||||
statusEl.textContent = '● 连接失败';
|
||||
statusEl.className = 'status disconnected';
|
||||
return;
|
||||
}
|
||||
// 逐步增加重试间隔: 1s, 1s, 2s, 2s, 3s, 3s, ...
|
||||
var delay = Math.min(Math.floor(retryCount / 2) + 1, 5) * 1000;
|
||||
loadingMsg.textContent = '等待服务就绪... ' + Math.ceil(delay/1000) + '秒后重试';
|
||||
console.log('[oc-config] Retry in', delay, 'ms');
|
||||
retryTimer = setTimeout(connect, delay);
|
||||
}
|
||||
|
||||
term.onData(function(data) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'stdin', data: data }));
|
||||
}
|
||||
});
|
||||
|
||||
term.onResize(function(size) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'resize', cols: size.cols, rows: size.rows }));
|
||||
}
|
||||
});
|
||||
|
||||
// 等 DOM 和资源就绪后再连接
|
||||
if (document.readyState === 'complete') {
|
||||
connect();
|
||||
} else {
|
||||
window.addEventListener('load', connect);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
8
root/usr/share/openclaw/ui/lib/addon-fit.min.js
vendored
Normal file
8
root/usr/share/openclaw/ui/lib/addon-fit.min.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Skipped minification because the original files appears to be already minified.
|
||||
* Original file: /npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
|
||||
//# sourceMappingURL=addon-fit.js.map
|
||||
8
root/usr/share/openclaw/ui/lib/addon-web-links.min.js
vendored
Normal file
8
root/usr/share/openclaw/ui/lib/addon-web-links.min.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Skipped minification because the original files appears to be already minified.
|
||||
* Original file: /npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.js
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.WebLinksAddon=t():e.WebLinksAddon=t()}(self,(()=>(()=>{"use strict";var e={6:(e,t)=>{function n(e){try{const t=new URL(e),n=t.password&&t.username?`${t.protocol}//${t.username}:${t.password}@${t.host}`:t.username?`${t.protocol}//${t.username}@${t.host}`:`${t.protocol}//${t.host}`;return e.toLocaleLowerCase().startsWith(n.toLocaleLowerCase())}catch(e){return!1}}Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,n,o={}){this._terminal=e,this._regex=t,this._handler=n,this._options=o}provideLinks(e,t){const n=o.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(n))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,n)=>{if(this._options.hover){const{range:o}=e;this._options.hover(t,n,o)}},e)))}};class o{static computeLink(e,t,r,i){const s=new RegExp(t.source,(t.flags||"")+"g"),[a,c]=o._getWindowedLineStrings(e-1,r),l=a.join("");let d;const p=[];for(;d=s.exec(l);){const e=d[0];if(!n(e))continue;const[t,s]=o._mapStrIdx(r,c,0,d.index),[a,l]=o._mapStrIdx(r,t,s,e.length);if(-1===t||-1===s||-1===a||-1===l)continue;const h={start:{x:s+1,y:t+1},end:{x:l,y:a+1}};p.push({range:h,text:e,activate:i})}return p}static _getWindowedLineStrings(e,t){let n,o=e,r=e,i=0,s="";const a=[];if(n=t.buffer.active.getLine(e)){const e=n.translateToString(!0);if(n.isWrapped&&" "!==e[0]){for(i=0;(n=t.buffer.active.getLine(--o))&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),n.isWrapped&&-1===s.indexOf(" ")););a.reverse()}for(a.push(e),i=0;(n=t.buffer.active.getLine(++r))&&n.isWrapped&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),-1===s.indexOf(" ")););}return[a,o]}static _mapStrIdx(e,t,n,o){const r=e.buffer.active,i=r.getNullCell();let s=n;for(;o;){const e=r.getLine(t);if(!e)return[-1,-1];for(let n=s;n<e.length;++n){e.getCell(n,i);const s=i.getChars();if(i.getWidth()&&(o-=s.length||1,n===e.length-1&&""===s)){const e=r.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,i),2===i.getWidth()&&(o+=1))}if(o<0)return[t,n]}t++,s=0}return[t,s]}}t.LinkComputer=o}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}var o={};return(()=>{var e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=n(6),r=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function i(e,t){const n=window.open();if(n){try{n.opener=null}catch{}n.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=i,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const n=this._options,o=n.urlRegex||r;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,o,this._handler,n))}dispose(){this._linkProvider?.dispose()}}})(),o})()));
|
||||
//# sourceMappingURL=addon-web-links.js.map
|
||||
8
root/usr/share/openclaw/ui/lib/xterm.min.css
vendored
Normal file
8
root/usr/share/openclaw/ui/lib/xterm.min.css
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Minified by jsDelivr using clean-css v5.3.3.
|
||||
* Original file: /npm/@xterm/xterm@5.5.0/css/xterm.css
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
|
||||
/*# sourceMappingURL=/sm/97377c0c258e109358121823f5790146c714989366481f90e554c42277efb500.map */
|
||||
8
root/usr/share/openclaw/ui/lib/xterm.min.js
vendored
Normal file
8
root/usr/share/openclaw/ui/lib/xterm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
285
root/usr/share/openclaw/web-pty.js
Normal file
285
root/usr/share/openclaw/web-pty.js
Normal file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env node
|
||||
// ============================================================================
|
||||
// OpenClaw 配置工具 — Web PTY 服务器
|
||||
// 纯 Node.js 实现,零外部依赖
|
||||
// 通过 WebSocket 将 oc-config.sh 的 TTY 输出推送给浏览器 xterm.js
|
||||
// HTTP 端口 18793, HTTPS 可选端口 18794
|
||||
// ============================================================================
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const crypto = require('crypto');
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// ── 配置 (OpenWrt 适配) ──
|
||||
const PORT = parseInt(process.env.OC_CONFIG_PORT || '18793', 10);
|
||||
const HOST = process.env.OC_CONFIG_HOST || '0.0.0.0'; // token 认证保护,可安全绑定所有接口
|
||||
const NODE_BASE = process.env.NODE_BASE || '/opt/openclaw/node';
|
||||
const OC_GLOBAL = process.env.OC_GLOBAL || '/opt/openclaw/global';
|
||||
const OC_DATA = process.env.OC_DATA || '/opt/openclaw/data';
|
||||
const SCRIPT_PATH = process.env.OC_CONFIG_SCRIPT || '/usr/share/openclaw/oc-config.sh';
|
||||
const SSL_CERT = '/etc/uhttpd.crt';
|
||||
const SSL_KEY = '/etc/uhttpd.key';
|
||||
const MAX_SESSIONS = parseInt(process.env.OC_MAX_SESSIONS || '5', 10);
|
||||
|
||||
// ── 认证令牌 (从 UCI 或环境变量读取) ──
|
||||
function loadAuthToken() {
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
const t = execSync('uci -q get openclaw.main.luci_token 2>/dev/null', { encoding: 'utf8', timeout: 3000 }).trim();
|
||||
return t || '';
|
||||
} catch { return ''; }
|
||||
}
|
||||
let AUTH_TOKEN = process.env.OC_PTY_TOKEN || loadAuthToken();
|
||||
|
||||
// ── 会话计数 ──
|
||||
let activeSessions = 0;
|
||||
|
||||
// ── 静态文件 ──
|
||||
const UI_DIR = path.join(__dirname, 'ui');
|
||||
|
||||
function getMimeType(ext) {
|
||||
const types = {
|
||||
'.html': 'text/html; charset=utf-8', '.css': 'text/css',
|
||||
'.js': 'application/javascript', '.png': 'image/png',
|
||||
'.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.json': 'application/json',
|
||||
};
|
||||
return types[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
const IFRAME_HEADERS = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'X-Frame-Options': 'ALLOWALL',
|
||||
'Content-Security-Policy': "default-src * 'unsafe-inline' 'unsafe-eval' data: blob: ws: wss:; frame-ancestors *",
|
||||
};
|
||||
|
||||
// ── WebSocket 帧处理 (RFC 6455) ──
|
||||
function decodeWSFrame(buf) {
|
||||
if (buf.length < 2) return null;
|
||||
const opcode = buf[0] & 0x0f;
|
||||
const masked = !!(buf[1] & 0x80);
|
||||
let payloadLen = buf[1] & 0x7f;
|
||||
let offset = 2;
|
||||
if (payloadLen === 126) {
|
||||
if (buf.length < 4) return null;
|
||||
payloadLen = buf.readUInt16BE(2); offset = 4;
|
||||
} else if (payloadLen === 127) {
|
||||
if (buf.length < 10) return null;
|
||||
payloadLen = Number(buf.readBigUInt64BE(2)); offset = 10;
|
||||
}
|
||||
let mask = null;
|
||||
if (masked) {
|
||||
if (buf.length < offset + 4) return null;
|
||||
mask = buf.slice(offset, offset + 4); offset += 4;
|
||||
}
|
||||
if (buf.length < offset + payloadLen) return null;
|
||||
const data = buf.slice(offset, offset + payloadLen);
|
||||
if (mask) { for (let i = 0; i < data.length; i++) data[i] ^= mask[i & 3]; }
|
||||
return { opcode, data, totalLen: offset + payloadLen };
|
||||
}
|
||||
|
||||
function encodeWSFrame(data, opcode = 0x01) {
|
||||
const payload = typeof data === 'string' ? Buffer.from(data) : data;
|
||||
const len = payload.length;
|
||||
let header;
|
||||
if (len < 126) {
|
||||
header = Buffer.alloc(2); header[0] = 0x80 | opcode; header[1] = len;
|
||||
} else if (len < 65536) {
|
||||
header = Buffer.alloc(4); header[0] = 0x80 | opcode; header[1] = 126; header.writeUInt16BE(len, 2);
|
||||
} else {
|
||||
header = Buffer.alloc(10); header[0] = 0x80 | opcode; header[1] = 127; header.writeBigUInt64BE(BigInt(len), 2);
|
||||
}
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
// ── PTY 进程管理 ──
|
||||
class PtySession {
|
||||
constructor(socket) {
|
||||
this.socket = socket;
|
||||
this.proc = null;
|
||||
this.cols = 80;
|
||||
this.rows = 24;
|
||||
this.buffer = Buffer.alloc(0);
|
||||
this.alive = true;
|
||||
activeSessions++;
|
||||
console.log(`[oc-config] Session created (active: ${activeSessions}/${MAX_SESSIONS})`);
|
||||
this._setupWSReader();
|
||||
this._spawnPty();
|
||||
}
|
||||
|
||||
_setupWSReader() {
|
||||
this.socket.on('data', (chunk) => {
|
||||
this.buffer = Buffer.concat([this.buffer, chunk]);
|
||||
while (this.buffer.length > 0) {
|
||||
const frame = decodeWSFrame(this.buffer);
|
||||
if (!frame) break;
|
||||
this.buffer = this.buffer.slice(frame.totalLen);
|
||||
if (frame.opcode === 0x01) this._handleMessage(frame.data.toString());
|
||||
else if (frame.opcode === 0x02 && this.proc && this.proc.stdin.writable) this.proc.stdin.write(frame.data);
|
||||
else if (frame.opcode === 0x08) { console.log('[oc-config] WS close frame received'); this._cleanup(); }
|
||||
else if (frame.opcode === 0x09) this.socket.write(encodeWSFrame(frame.data, 0x0a));
|
||||
}
|
||||
});
|
||||
this.socket.on('close', (hadError) => { console.log(`[oc-config] Socket closed, hadError=${hadError}`); this._cleanup(); });
|
||||
this.socket.on('error', (err) => { console.log(`[oc-config] Socket error: ${err.message}`); this._cleanup(); });
|
||||
}
|
||||
|
||||
_handleMessage(text) {
|
||||
try {
|
||||
const msg = JSON.parse(text);
|
||||
if (msg.type === 'stdin' && this.proc && this.proc.stdin.writable) {
|
||||
// 去除 bracketed paste 转义序列,避免污染 shell read 输入
|
||||
const cleaned = msg.data.replace(/\x1b\[\?2004[hl]/g, '').replace(/\x1b\[20[01]~/g, '');
|
||||
this.proc.stdin.write(cleaned);
|
||||
}
|
||||
else if (msg.type === 'resize') {
|
||||
this.cols = msg.cols || 80; this.rows = msg.rows || 24;
|
||||
if (this.proc && this.proc.pid) { try { process.kill(-this.proc.pid, 'SIGWINCH'); } catch(e){} }
|
||||
}
|
||||
} catch(e) { if (this.proc && this.proc.stdin.writable) this.proc.stdin.write(text); }
|
||||
}
|
||||
|
||||
_spawnPty() {
|
||||
const env = {
|
||||
...process.env, TERM: 'xterm-256color', COLUMNS: String(this.cols), LINES: String(this.rows),
|
||||
COLORTERM: 'truecolor', LANG: 'en_US.UTF-8',
|
||||
NODE_BASE, OC_GLOBAL, OC_DATA,
|
||||
HOME: OC_DATA,
|
||||
OPENCLAW_HOME: OC_DATA,
|
||||
OPENCLAW_STATE_DIR: `${OC_DATA}/.openclaw`,
|
||||
OPENCLAW_CONFIG_PATH: `${OC_DATA}/.openclaw/openclaw.json`,
|
||||
PATH: `${NODE_BASE}/bin:${OC_GLOBAL}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`,
|
||||
};
|
||||
this.proc = spawn('script', ['-qc', `stty rows ${this.rows} cols ${this.cols} 2>/dev/null; printf '\\e[?2004l'; sh "${SCRIPT_PATH}"`, '/dev/null'],
|
||||
{ stdio: ['pipe', 'pipe', 'pipe'], env, detached: true });
|
||||
|
||||
this.proc.stdout.on('data', (d) => { if (this.alive) this.socket.write(encodeWSFrame(d, 0x01)); });
|
||||
this.proc.stderr.on('data', (d) => { if (this.alive) this.socket.write(encodeWSFrame(d, 0x01)); });
|
||||
this.proc.on('close', (code) => {
|
||||
if (!this.alive) return;
|
||||
console.log(`[oc-config] Script exited with code ${code}, auto-restarting...`);
|
||||
this.socket.write(encodeWSFrame(`\r\n\x1b[33m配置脚本已退出 (code: ${code}),正在自动重启...\x1b[0m\r\n`, 0x01));
|
||||
this.proc = null;
|
||||
// 自动重启脚本,保持 WebSocket 连接
|
||||
setTimeout(() => {
|
||||
if (this.alive) {
|
||||
this._spawnPty();
|
||||
}
|
||||
}, 1500);
|
||||
});
|
||||
this.proc.on('error', (err) => {
|
||||
if (this.alive) this.socket.write(encodeWSFrame(`\r\n\x1b[31m启动失败: ${err.message}\x1b[0m\r\n`, 0x01));
|
||||
});
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
if (!this.alive) return; this.alive = false;
|
||||
activeSessions = Math.max(0, activeSessions - 1);
|
||||
console.log(`[oc-config] Session ended (active: ${activeSessions}/${MAX_SESSIONS})`);
|
||||
if (this.proc) { try { process.kill(-this.proc.pid, 'SIGTERM'); } catch(e){} try { this.proc.kill('SIGTERM'); } catch(e){} }
|
||||
try { this.socket.destroy(); } catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
// ── HTTP 请求处理 ──
|
||||
function handleRequest(req, res) {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
let fp = url.pathname;
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': '*' });
|
||||
return res.end();
|
||||
}
|
||||
if (fp === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
||||
return res.end(JSON.stringify({ status: 'ok', port: PORT, uptime: process.uptime() }));
|
||||
}
|
||||
if (fp === '/' || fp === '') fp = '/index.html';
|
||||
|
||||
const fullPath = path.join(UI_DIR, fp);
|
||||
if (!fullPath.startsWith(UI_DIR)) { res.writeHead(403); return res.end('Forbidden'); }
|
||||
|
||||
fs.readFile(fullPath, (err, data) => {
|
||||
if (err) {
|
||||
if (fp !== '/index.html') {
|
||||
fs.readFile(path.join(UI_DIR, 'index.html'), (e2, d2) => {
|
||||
if (e2) { res.writeHead(404); res.end('Not Found'); }
|
||||
else { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', ...IFRAME_HEADERS }); res.end(d2); }
|
||||
});
|
||||
} else { res.writeHead(404); res.end('Not Found'); }
|
||||
return;
|
||||
}
|
||||
const ext = path.extname(fullPath);
|
||||
res.writeHead(200, { 'Content-Type': getMimeType(ext), 'Cache-Control': ext === '.html' ? 'no-cache' : 'max-age=3600', ...IFRAME_HEADERS });
|
||||
res.end(data);
|
||||
});
|
||||
}
|
||||
|
||||
// ── WebSocket Upgrade ──
|
||||
function handleUpgrade(req, socket, head) {
|
||||
console.log(`[oc-config] WS upgrade: ${req.url} remote=${socket.remoteAddress}:${socket.remotePort}`);
|
||||
if (req.url !== '/ws' && !req.url.startsWith('/ws?')) { socket.destroy(); return; }
|
||||
|
||||
// 认证: 验证查询参数中的 token
|
||||
if (AUTH_TOKEN) {
|
||||
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
||||
const clientToken = url.searchParams.get('token') || '';
|
||||
if (clientToken !== AUTH_TOKEN) {
|
||||
console.log(`[oc-config] WS auth failed from ${socket.remoteAddress}`);
|
||||
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 并发会话限制
|
||||
if (activeSessions >= MAX_SESSIONS) {
|
||||
console.log(`[oc-config] Max sessions reached (${activeSessions}/${MAX_SESSIONS}), rejecting`);
|
||||
socket.write('HTTP/1.1 503 Service Unavailable\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const key = req.headers['sec-websocket-key'];
|
||||
if (!key) { console.log('[oc-config] Missing Sec-WebSocket-Key'); socket.destroy(); return; }
|
||||
|
||||
const accept = crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');
|
||||
|
||||
socket.setNoDelay(true);
|
||||
socket.setTimeout(0);
|
||||
|
||||
const handshake = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ' + accept + '\r\n\r\n';
|
||||
|
||||
socket.write(handshake, () => {
|
||||
if (head && head.length > 0) socket.unshift(head);
|
||||
new PtySession(socket);
|
||||
console.log('[oc-config] PTY session started');
|
||||
});
|
||||
}
|
||||
|
||||
// ── 服务器实例 ──
|
||||
const httpServer = http.createServer(handleRequest);
|
||||
httpServer.on('upgrade', handleUpgrade);
|
||||
|
||||
httpServer.listen(PORT, HOST, () => {
|
||||
console.log(`[oc-config] HTTP listening on ${HOST}:${PORT}`);
|
||||
console.log(`[oc-config] Script: ${SCRIPT_PATH}`);
|
||||
});
|
||||
|
||||
// HTTPS 可选端口 PORT+1
|
||||
const HTTPS_PORT = PORT + 1;
|
||||
try {
|
||||
if (fs.existsSync(SSL_CERT) && fs.existsSync(SSL_KEY)) {
|
||||
const httpsServer = https.createServer({ cert: fs.readFileSync(SSL_CERT), key: fs.readFileSync(SSL_KEY) }, handleRequest);
|
||||
httpsServer.on('upgrade', handleUpgrade);
|
||||
httpsServer.listen(HTTPS_PORT, HOST, () => console.log(`[oc-config] HTTPS listening on ${HOST}:${HTTPS_PORT}`));
|
||||
httpsServer.on('error', (e) => console.log(`[oc-config] HTTPS port ${HTTPS_PORT}: ${e.message}`));
|
||||
}
|
||||
} catch (e) { console.log(`[oc-config] SSL init: ${e.message}`); }
|
||||
|
||||
httpServer.on('error', (e) => { console.error(`[oc-config] Fatal: ${e.message}`); process.exit(1); });
|
||||
process.on('SIGTERM', () => { console.log('[oc-config] Shutdown'); httpServer.close(); process.exit(0); });
|
||||
process.on('SIGINT', () => { httpServer.close(); process.exit(0); });
|
||||
143
scripts/build_ipk.sh
Executable file
143
scripts/build_ipk.sh
Executable file
@@ -0,0 +1,143 @@
|
||||
#!/bin/sh
|
||||
# ============================================================================
|
||||
# 本地构建 .ipk 包 (无需 OpenWrt SDK)
|
||||
# 用法: sh scripts/build_ipk.sh [output_dir]
|
||||
# ============================================================================
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
PKG_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
|
||||
OUT_DIR="${1:-$PKG_DIR/dist}"
|
||||
# 确保 OUT_DIR 是绝对路径
|
||||
case "$OUT_DIR" in
|
||||
/*) ;;
|
||||
*) OUT_DIR="$PKG_DIR/$OUT_DIR" ;;
|
||||
esac
|
||||
mkdir -p "$OUT_DIR"
|
||||
PKG_NAME="luci-app-openclaw"
|
||||
PKG_VERSION=$(cat "$PKG_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "1.0.0")
|
||||
PKG_RELEASE="1"
|
||||
|
||||
echo "=== 构建 ${PKG_NAME} .ipk 包 ==="
|
||||
|
||||
STAGING=$(mktemp -d)
|
||||
trap "rm -rf '$STAGING'" EXIT
|
||||
|
||||
# ── 构建 data.tar.gz ──
|
||||
DATA_DIR="$STAGING/data"
|
||||
mkdir -p "$DATA_DIR"
|
||||
|
||||
# UCI config
|
||||
mkdir -p "$DATA_DIR/etc/config"
|
||||
cp "$PKG_DIR/root/etc/config/openclaw" "$DATA_DIR/etc/config/"
|
||||
|
||||
# UCI defaults
|
||||
mkdir -p "$DATA_DIR/etc/uci-defaults"
|
||||
cp "$PKG_DIR/root/etc/uci-defaults/99-openclaw" "$DATA_DIR/etc/uci-defaults/"
|
||||
chmod +x "$DATA_DIR/etc/uci-defaults/99-openclaw"
|
||||
|
||||
# init.d
|
||||
mkdir -p "$DATA_DIR/etc/init.d"
|
||||
cp "$PKG_DIR/root/etc/init.d/openclaw" "$DATA_DIR/etc/init.d/"
|
||||
chmod +x "$DATA_DIR/etc/init.d/openclaw"
|
||||
|
||||
# bin
|
||||
mkdir -p "$DATA_DIR/usr/bin"
|
||||
cp "$PKG_DIR/root/usr/bin/openclaw-env" "$DATA_DIR/usr/bin/"
|
||||
chmod +x "$DATA_DIR/usr/bin/openclaw-env"
|
||||
|
||||
# LuCI controller
|
||||
mkdir -p "$DATA_DIR/usr/lib/lua/luci/controller"
|
||||
cp "$PKG_DIR/luasrc/controller/openclaw.lua" "$DATA_DIR/usr/lib/lua/luci/controller/"
|
||||
|
||||
# LuCI CBI
|
||||
mkdir -p "$DATA_DIR/usr/lib/lua/luci/model/cbi/openclaw"
|
||||
cp "$PKG_DIR/luasrc/model/cbi/openclaw/"*.lua "$DATA_DIR/usr/lib/lua/luci/model/cbi/openclaw/"
|
||||
|
||||
# LuCI views
|
||||
mkdir -p "$DATA_DIR/usr/lib/lua/luci/view/openclaw"
|
||||
cp "$PKG_DIR/luasrc/view/openclaw/"*.htm "$DATA_DIR/usr/lib/lua/luci/view/openclaw/"
|
||||
|
||||
# oc-config assets
|
||||
mkdir -p "$DATA_DIR/usr/share/openclaw"
|
||||
cp "$PKG_DIR/root/usr/share/openclaw/oc-config.sh" "$DATA_DIR/usr/share/openclaw/"
|
||||
chmod +x "$DATA_DIR/usr/share/openclaw/oc-config.sh"
|
||||
cp "$PKG_DIR/root/usr/share/openclaw/web-pty.js" "$DATA_DIR/usr/share/openclaw/"
|
||||
|
||||
# Web PTY UI
|
||||
cp -r "$PKG_DIR/root/usr/share/openclaw/ui" "$DATA_DIR/usr/share/openclaw/"
|
||||
|
||||
# i18n (po2lmo 可选)
|
||||
mkdir -p "$DATA_DIR/usr/lib/lua/luci/i18n"
|
||||
if command -v po2lmo >/dev/null 2>&1 && [ -f "$PKG_DIR/po/zh-cn/openclaw.po" ]; then
|
||||
po2lmo "$PKG_DIR/po/zh-cn/openclaw.po" "$DATA_DIR/usr/lib/lua/luci/i18n/openclaw.zh-cn.lmo" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 计算安装大小
|
||||
INSTALLED_SIZE=$(du -sk "$DATA_DIR" | awk '{print $1}')
|
||||
|
||||
(cd "$DATA_DIR" && tar czf "$STAGING/data.tar.gz" .)
|
||||
|
||||
# ── 构建 control.tar.gz ──
|
||||
CTRL_DIR="$STAGING/control"
|
||||
mkdir -p "$CTRL_DIR"
|
||||
|
||||
cat > "$CTRL_DIR/control" << EOF
|
||||
Package: ${PKG_NAME}
|
||||
Version: ${PKG_VERSION}-${PKG_RELEASE}
|
||||
Depends: luci-compat, luci-base, curl, openssl-util
|
||||
Source: https://github.com/10000ge10000/luci-app-openclaw
|
||||
SourceName: ${PKG_NAME}
|
||||
License: GPL-3.0
|
||||
Section: luci
|
||||
SourceDateEpoch: $(date +%s)
|
||||
Maintainer: 10000ge10000 <10000ge10000@users.noreply.github.com>
|
||||
Architecture: all
|
||||
Installed-Size: ${INSTALLED_SIZE}
|
||||
Description: OpenClaw AI 网关 LuCI 管理插件
|
||||
EOF
|
||||
|
||||
cat > "$CTRL_DIR/postinst" << 'EOF'
|
||||
#!/bin/sh
|
||||
[ -n "${IPKG_INSTROOT}" ] || {
|
||||
( . /etc/uci-defaults/99-openclaw ) && rm -f /etc/uci-defaults/99-openclaw
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
EOF
|
||||
chmod +x "$CTRL_DIR/postinst"
|
||||
|
||||
cat > "$CTRL_DIR/postrm" << 'EOF'
|
||||
#!/bin/sh
|
||||
[ -n "${IPKG_INSTROOT}" ] || {
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null
|
||||
}
|
||||
EOF
|
||||
chmod +x "$CTRL_DIR/postrm"
|
||||
|
||||
cat > "$CTRL_DIR/conffiles" << 'EOF'
|
||||
/etc/config/openclaw
|
||||
EOF
|
||||
|
||||
(cd "$CTRL_DIR" && tar czf "$STAGING/control.tar.gz" .)
|
||||
|
||||
# ── 组装 .ipk (ar 格式) ──
|
||||
mkdir -p "$OUT_DIR"
|
||||
IPK_FILE="$OUT_DIR/${PKG_NAME}_${PKG_VERSION}-${PKG_RELEASE}_all.ipk"
|
||||
|
||||
echo "2.0" > "$STAGING/debian-binary"
|
||||
|
||||
# 清理旧文件
|
||||
rm -f "$IPK_FILE"
|
||||
|
||||
# 组装 .ipk — OpenWrt opkg 使用 tar.gz 格式 (非 Debian 的 ar 格式)
|
||||
(cd "$STAGING" && tar czf "$IPK_FILE" debian-binary control.tar.gz data.tar.gz)
|
||||
|
||||
IPK_SIZE=$(wc -c < "$IPK_FILE" | tr -d ' ')
|
||||
echo ""
|
||||
echo "=== 构建完成 ==="
|
||||
echo "输出文件: $IPK_FILE"
|
||||
echo "文件大小: ${IPK_SIZE} bytes"
|
||||
echo "安装大小: ${INSTALLED_SIZE} KB"
|
||||
echo ""
|
||||
echo "安装方法: opkg install ${PKG_NAME}_${PKG_VERSION}-${PKG_RELEASE}_all.ipk"
|
||||
235
scripts/build_run.sh
Executable file
235
scripts/build_run.sh
Executable file
@@ -0,0 +1,235 @@
|
||||
#!/bin/sh
|
||||
# ============================================================================
|
||||
# iStoreOS .run 自解压包构建脚本
|
||||
# 用法: sh scripts/build_run.sh [output_dir]
|
||||
# ============================================================================
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
PKG_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
|
||||
OUT_DIR="${1:-$PKG_DIR/dist}"
|
||||
# 确保 OUT_DIR 是绝对路径
|
||||
case "$OUT_DIR" in
|
||||
/*) ;;
|
||||
*) OUT_DIR="$PKG_DIR/$OUT_DIR" ;;
|
||||
esac
|
||||
mkdir -p "$OUT_DIR"
|
||||
PKG_NAME="luci-app-openclaw"
|
||||
PKG_VERSION=$(cat "$PKG_DIR/VERSION" 2>/dev/null | tr -d '[:space:]' || echo "1.0.0")
|
||||
|
||||
echo "=== 构建 iStoreOS .run 安装包 ==="
|
||||
echo "源目录: $PKG_DIR"
|
||||
echo "输出到: $OUT_DIR"
|
||||
|
||||
# 创建临时打包目录
|
||||
STAGING=$(mktemp -d)
|
||||
trap "rm -rf '$STAGING'" EXIT
|
||||
|
||||
# 安装文件到暂存区
|
||||
install_files() {
|
||||
local dest="$1"
|
||||
|
||||
# UCI config
|
||||
mkdir -p "$dest/etc/config"
|
||||
cp "$PKG_DIR/root/etc/config/openclaw" "$dest/etc/config/"
|
||||
|
||||
# UCI defaults
|
||||
mkdir -p "$dest/etc/uci-defaults"
|
||||
cp "$PKG_DIR/root/etc/uci-defaults/99-openclaw" "$dest/etc/uci-defaults/"
|
||||
chmod +x "$dest/etc/uci-defaults/99-openclaw"
|
||||
|
||||
# init.d
|
||||
mkdir -p "$dest/etc/init.d"
|
||||
cp "$PKG_DIR/root/etc/init.d/openclaw" "$dest/etc/init.d/"
|
||||
chmod +x "$dest/etc/init.d/openclaw"
|
||||
|
||||
# bin
|
||||
mkdir -p "$dest/usr/bin"
|
||||
cp "$PKG_DIR/root/usr/bin/openclaw-env" "$dest/usr/bin/"
|
||||
chmod +x "$dest/usr/bin/openclaw-env"
|
||||
|
||||
# LuCI controller
|
||||
mkdir -p "$dest/usr/lib/lua/luci/controller"
|
||||
cp "$PKG_DIR/luasrc/controller/openclaw.lua" "$dest/usr/lib/lua/luci/controller/"
|
||||
|
||||
# LuCI CBI
|
||||
mkdir -p "$dest/usr/lib/lua/luci/model/cbi/openclaw"
|
||||
cp "$PKG_DIR/luasrc/model/cbi/openclaw/"*.lua "$dest/usr/lib/lua/luci/model/cbi/openclaw/"
|
||||
|
||||
# LuCI views
|
||||
mkdir -p "$dest/usr/lib/lua/luci/view/openclaw"
|
||||
cp "$PKG_DIR/luasrc/view/openclaw/"*.htm "$dest/usr/lib/lua/luci/view/openclaw/"
|
||||
|
||||
# oc-config assets
|
||||
mkdir -p "$dest/usr/share/openclaw"
|
||||
cp "$PKG_DIR/root/usr/share/openclaw/oc-config.sh" "$dest/usr/share/openclaw/"
|
||||
chmod +x "$dest/usr/share/openclaw/oc-config.sh"
|
||||
cp "$PKG_DIR/root/usr/share/openclaw/web-pty.js" "$dest/usr/share/openclaw/"
|
||||
|
||||
# Web PTY UI (recursive copy)
|
||||
cp -r "$PKG_DIR/root/usr/share/openclaw/ui" "$dest/usr/share/openclaw/"
|
||||
}
|
||||
|
||||
# 创建安装器脚本头部
|
||||
create_installer() {
|
||||
cat > "$STAGING/install.sh" << 'INSTALLER_EOF'
|
||||
#!/bin/sh
|
||||
# luci-app-openclaw iStoreOS 安装器
|
||||
set -e
|
||||
|
||||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ luci-app-openclaw — OpenClaw AI Gateway 插件 ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# 检查系统
|
||||
if [ ! -f /etc/openwrt_release ]; then
|
||||
echo "错误: 此安装包仅适用于 OpenWrt/iStoreOS 系统"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查架构
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64|aarch64) ;;
|
||||
*) echo "错误: 不支持的架构 $ARCH (仅支持 x86_64/aarch64)"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# 检查依赖
|
||||
for dep in luci-compat luci-base; do
|
||||
if ! opkg list-installed 2>/dev/null | grep -q "^${dep} "; then
|
||||
echo "警告: 缺少依赖 $dep,尝试安装..."
|
||||
opkg update >/dev/null 2>&1 || true
|
||||
opkg install "$dep" 2>/dev/null || echo " 安装 $dep 失败,请手动安装"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "正在安装文件..."
|
||||
|
||||
# 解压 payload (从 MARKER 行之后)
|
||||
ARCHIVE=$(awk '/^__ARCHIVE_BELOW__/ {print NR + 1; exit 0; }' "$0")
|
||||
tail -n +$ARCHIVE "$0" | tar xzf - -C / 2>/dev/null
|
||||
|
||||
# 注册到 opkg,使 iStore 和 opkg 能识别此包
|
||||
PKG="luci-app-openclaw"
|
||||
PKG_VER="__PKG_VERSION__"
|
||||
INFO_DIR="/usr/lib/opkg/info"
|
||||
STATUS_FILE="/usr/lib/opkg/status"
|
||||
INSTALL_TIME=$(date +%s)
|
||||
|
||||
mkdir -p "$INFO_DIR"
|
||||
|
||||
# 写入 control 文件
|
||||
cat > "$INFO_DIR/$PKG.control" << CTLEOF
|
||||
Package: $PKG
|
||||
Version: $PKG_VER
|
||||
Depends: luci-compat, luci-base, curl, openssl-util
|
||||
Section: luci
|
||||
Architecture: all
|
||||
Installed-Size: 0
|
||||
Description: OpenClaw AI Gateway — LuCI 界面
|
||||
CTLEOF
|
||||
|
||||
# 写入文件列表 (payload 中已安装的文件)
|
||||
cat > "$INFO_DIR/$PKG.list" << LISTEOF
|
||||
__FILE_LIST__
|
||||
LISTEOF
|
||||
|
||||
# 写入 prerm 脚本 (卸载前执行)
|
||||
cat > "$INFO_DIR/$PKG.prerm" << 'RMEOF'
|
||||
#!/bin/sh
|
||||
/etc/init.d/openclaw stop 2>/dev/null
|
||||
/etc/init.d/openclaw disable 2>/dev/null
|
||||
exit 0
|
||||
RMEOF
|
||||
chmod +x "$INFO_DIR/$PKG.prerm"
|
||||
|
||||
# 追加到 opkg status 数据库 (先移除旧记录)
|
||||
if [ -f "$STATUS_FILE" ]; then
|
||||
awk -v pkg="$PKG" '
|
||||
BEGIN { skip=0 }
|
||||
/^Package:/ { skip=($2==pkg) }
|
||||
/^$/ { if(skip){skip=0; next} }
|
||||
!skip { print }
|
||||
' "$STATUS_FILE" > "${STATUS_FILE}.tmp"
|
||||
mv "${STATUS_FILE}.tmp" "$STATUS_FILE"
|
||||
fi
|
||||
|
||||
cat >> "$STATUS_FILE" << STEOF
|
||||
|
||||
Package: $PKG
|
||||
Version: $PKG_VER
|
||||
Depends: luci-compat, luci-base, curl, openssl-util
|
||||
Status: install user installed
|
||||
Architecture: all
|
||||
Conffiles:
|
||||
/etc/config/openclaw 0
|
||||
Installed-Time: $INSTALL_TIME
|
||||
STEOF
|
||||
|
||||
echo "已注册到 opkg (iStore 可管理)"
|
||||
|
||||
# 执行 uci-defaults
|
||||
if [ -f /etc/uci-defaults/99-openclaw ]; then
|
||||
echo "执行初始化脚本..."
|
||||
( . /etc/uci-defaults/99-openclaw ) && rm -f /etc/uci-defaults/99-openclaw
|
||||
fi
|
||||
|
||||
# 清除 LuCI 缓存
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo "✅ 安装完成!"
|
||||
echo ""
|
||||
echo "后续步骤:"
|
||||
echo " 1. 运行 openclaw-env setup — 下载 Node.js 并安装 OpenClaw"
|
||||
echo " 2. 访问 LuCI → 服务 → OpenClaw 进行配置"
|
||||
echo " 3. 或执行 /etc/init.d/openclaw enable && /etc/init.d/openclaw start"
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
__ARCHIVE_BELOW__
|
||||
INSTALLER_EOF
|
||||
}
|
||||
|
||||
# 构建
|
||||
echo ""
|
||||
echo "[1/4] 安装文件到暂存区..."
|
||||
install_files "$STAGING/payload"
|
||||
|
||||
echo "[2/4] 生成文件列表..."
|
||||
# 生成安装文件列表 (供 opkg 卸载时使用)
|
||||
FILE_LIST=$(cd "$STAGING/payload" && find . -type f | sed 's|^\./|/|' | sort)
|
||||
echo " 共 $(echo "$FILE_LIST" | wc -l | tr -d ' ') 个文件"
|
||||
|
||||
echo "[3/4] 创建安装器..."
|
||||
create_installer
|
||||
|
||||
# 替换安装器中的占位符
|
||||
sed -i "s|__PKG_VERSION__|${PKG_VERSION}|g" "$STAGING/install.sh"
|
||||
# 替换文件列表占位符 — 使用临时文件拼接避免 sed/awk 多行问题
|
||||
{
|
||||
sed '/__FILE_LIST__/,$d' "$STAGING/install.sh"
|
||||
echo "$FILE_LIST"
|
||||
sed '1,/__FILE_LIST__/d' "$STAGING/install.sh"
|
||||
} > "$STAGING/install_final.sh"
|
||||
mv "$STAGING/install_final.sh" "$STAGING/install.sh"
|
||||
|
||||
echo "[4/4] 打包..."
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
# 创建 payload tarball
|
||||
(cd "$STAGING/payload" && tar czf "$STAGING/payload.tar.gz" .)
|
||||
|
||||
# 组合: installer header + payload
|
||||
RUN_FILE="$OUT_DIR/${PKG_NAME}_${PKG_VERSION}.run"
|
||||
cat "$STAGING/install.sh" "$STAGING/payload.tar.gz" > "$RUN_FILE"
|
||||
chmod +x "$RUN_FILE"
|
||||
|
||||
FILE_SIZE=$(wc -c < "$RUN_FILE" | tr -d ' ')
|
||||
echo ""
|
||||
echo "=== 构建完成 ==="
|
||||
echo "输出文件: $RUN_FILE"
|
||||
echo "文件大小: $FILE_SIZE bytes"
|
||||
echo ""
|
||||
echo "安装方法: sh ${PKG_NAME}_${PKG_VERSION}.run"
|
||||
Reference in New Issue
Block a user