feat(storage): support configurable install root

Add a LuCI install-root input, persist the selected path in UCI,
and route install, status, backup, uninstall, and runtime scripts
through the configured storage root for new installs.

Reference: custom install root flow
This commit is contained in:
2026-03-18 13:48:07 +08:00
parent ee10bb0bd5
commit 68f24e6658
17 changed files with 739 additions and 122 deletions

View File

@@ -1,5 +1,6 @@
config openclaw 'main'
option enabled '0'
option install_root '/opt'
option port '18789'
option bind 'lan'
option token ''

View File

@@ -10,15 +10,15 @@ 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"
. /usr/libexec/openclaw-paths.sh
oc_load_paths "$OPENCLAW_INSTALL_ROOT"
# ── OverlayFS 兼容性修复 ──
# Docker bind mount (/overlay/upper/opt/docker) 会导致 /opt 不可写
# 解决: bind mount upper 层的 /opt 到合并视图的 /opt
_oc_fix_opt() {
mkdir -p /opt/openclaw/.probe 2>/dev/null && { rmdir /opt/openclaw/.probe 2>/dev/null; return 0; }
oc_install_root_uses_opt_workaround "$OPENCLAW_INSTALL_ROOT" || return 0
mkdir -p "${OC_ROOT}/.probe" 2>/dev/null && { rmdir "${OC_ROOT}/.probe" 2>/dev/null; return 0; }
if [ -d /overlay/upper/opt ]; then
mkdir -p /overlay/upper/opt/openclaw 2>/dev/null
mount --bind /overlay/upper/opt /opt 2>/dev/null && return 0
@@ -306,6 +306,7 @@ HOME="$OC_DATA" \
OPENCLAW_HOME="$OC_DATA" \
OPENCLAW_STATE_DIR="${OC_DATA}/.openclaw" \
OPENCLAW_CONFIG_PATH="$CONFIG_FILE" \
OPENCLAW_INSTALL_ROOT="$OPENCLAW_INSTALL_ROOT" \
NODE_ICU_DATA="${NODE_BASE}/share/icu" \
NODE_BASE="$NODE_BASE" \
OC_GLOBAL="$OC_GLOBAL" \
@@ -335,6 +336,7 @@ procd_set_param env \
OC_CONFIG_PORT="$pty_port" \
OC_PTY_TOKEN="$pty_token" \
OC_CONFIG_SCRIPT="/usr/share/openclaw/oc-config.sh" \
OPENCLAW_INSTALL_ROOT="$OPENCLAW_INSTALL_ROOT" \
NODE_ICU_DATA="${NODE_BASE}/share/icu" \
NODE_BASE="$NODE_BASE" \
OC_GLOBAL="$OC_GLOBAL" \

View File

@@ -2,12 +2,11 @@
# ============================================================================
# luci-app-openclaw — 全局环境变量
# 仅在 Node.js 已安装时生效,为 SSH 登录用户提供正确的运行环境
# 解决 Issue #42: 统一配置文件路径,避免 /root/.openclaw 与 /opt/openclaw/data/.openclaw 混乱
# 解决 Issue #42: 统一配置文件路径,避免 /root/.openclaw 与运行目录混乱
# ============================================================================
NODE_BASE="/opt/openclaw/node"
OC_GLOBAL="/opt/openclaw/global"
OC_DATA="/opt/openclaw/data"
. /usr/libexec/openclaw-paths.sh
oc_load_paths "$OPENCLAW_INSTALL_ROOT"
# 检查 Node.js 是否已安装
[ -x "${NODE_BASE}/bin/node" ] || return 0

View File

@@ -1,11 +1,20 @@
#!/bin/sh
# luci-app-openclaw — 首次安装/升级初始化脚本
CURRENT_INSTALL_ROOT=$(uci -q get openclaw.main.install_root)
if [ -z "$CURRENT_INSTALL_ROOT" ]; then
uci set openclaw.main.install_root='/opt'
uci commit openclaw
CURRENT_INSTALL_ROOT='/opt'
fi
. /usr/libexec/openclaw-paths.sh
oc_load_paths "$CURRENT_INSTALL_ROOT"
# ── v1.0.16: 清理错误路径下的配置文件 (Issue #42) ──
# 用户在 SSH 中直接运行 openclaw 命令时,可能创建了 /root/.openclaw/ 目录
# 需要迁移数据并清理,避免路径混乱
if [ -d "/root/.openclaw" ]; then
OC_DATA="/opt/openclaw/data"
# 迁移 skills 目录 (如果存在且目标不存在)
if [ -d "/root/.openclaw/skills" ] && [ ! -d "${OC_DATA}/.openclaw/skills" ]; then
mkdir -p "${OC_DATA}/.openclaw"
@@ -41,7 +50,7 @@ if ! id openclaw >/dev/null 2>&1; then
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
echo "openclaw:x:${OC_UID}:${OC_GID}:openclaw:${OC_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
@@ -53,19 +62,21 @@ fi
# 创建数据目录
# ── OverlayFS 兼容: Docker bind mount 可能导致 /opt 不可写 ──
if ! mkdir -p /opt/openclaw/.probe 2>/dev/null; then
if [ -d /overlay/upper/opt ]; then
mkdir -p /overlay/upper/opt/openclaw 2>/dev/null
mount --bind /overlay/upper/opt /opt 2>/dev/null
if oc_install_root_uses_opt_workaround "$OPENCLAW_INSTALL_ROOT"; then
if ! mkdir -p "${OC_ROOT}/.probe" 2>/dev/null; then
if [ -d /overlay/upper/opt ]; then
mkdir -p /overlay/upper/opt/openclaw 2>/dev/null
mount --bind /overlay/upper/opt /opt 2>/dev/null
fi
rmdir "${OC_ROOT}/.probe" 2>/dev/null
else
rmdir "${OC_ROOT}/.probe" 2>/dev/null
fi
rmdir /opt/openclaw/.probe 2>/dev/null
else
rmdir /opt/openclaw/.probe 2>/dev/null
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
mkdir -p "${OC_DATA}/.openclaw"
mkdir -p "$NODE_BASE"
mkdir -p "$OC_GLOBAL"
chown -R openclaw:openclaw "$OC_ROOT" 2>/dev/null || true
# 生成随机 Token (如果尚未设置)
CURRENT_TOKEN=$(uci -q get openclaw.main.token)

View File

@@ -11,6 +11,8 @@
# ============================================================================
set -e
. /usr/libexec/openclaw-paths.sh
# ── Node.js 版本策略 (双版本兼容) ──
# V2: 当前推荐版本,用于 OpenClaw v2026.3.11+ (要求 >= 22.16.0)
# V1: 旧版兼容,用于 OpenClaw v2026.3.8 及更早版本
@@ -22,18 +24,17 @@ NODE_VERSION="${NODE_VERSION:-${NODE_VERSION_V2}}"
OC_TESTED_VERSION="2026.3.13"
# 用户可通过 OC_VERSION 环境变量覆盖安装版本
OC_VERSION="${OC_VERSION:-}"
NODE_BASE="/opt/openclaw/node"
OC_GLOBAL="/opt/openclaw/global"
OC_DATA="/opt/openclaw/data"
oc_load_paths "$OPENCLAW_INSTALL_ROOT"
# ── OverlayFS 兼容性修复 ──
# iStoreOS/OpenWrt 上 Docker 的 bind mount (/overlay/upper/opt/docker)
# 会导致 OverlayFS 合并视图中 /opt 完全不可写 (mkdir 报 "Directory not empty")。
# 解决方案: 将 /overlay/upper/opt bind mount 到 /opt绕过 OverlayFS 冲突。
_oc_fix_opt() {
oc_install_root_uses_opt_workaround "$OPENCLAW_INSTALL_ROOT" || return 0
# 如果 /opt 可正常写入,无需修复
if mkdir -p /opt/openclaw/.probe 2>/dev/null; then
rmdir /opt/openclaw/.probe 2>/dev/null
if mkdir -p "${OC_ROOT}/.probe" 2>/dev/null; then
rmdir "${OC_ROOT}/.probe" 2>/dev/null
return 0
fi
# /opt 不可写且 overlay upper 层存在 — 执行 bind mount 修复
@@ -72,7 +73,9 @@ ensure_mkdir() {
[ -d "$target" ] && return 0
if ! mkdir -p "$target" 2>/dev/null; then
log_error "无法创建目录: $target"
log_error "如果安装了 Docker可能需要手动执行: mount --bind /overlay/upper/opt /opt"
if oc_install_root_uses_opt_workaround "$OPENCLAW_INSTALL_ROOT"; then
log_error "如果安装了 Docker可能需要手动执行: mount --bind /overlay/upper/opt /opt"
fi
return 1
fi
}
@@ -495,7 +498,7 @@ do_check() {
# 磁盘使用
local used
used=$(du -sh /opt/openclaw 2>/dev/null | awk '{print $1}')
used=$(du -sh "$OC_ROOT" 2>/dev/null | awk '{print $1}')
echo " 磁盘: ${used:-N/A}"
# libc 类型

View File

@@ -0,0 +1,78 @@
#!/bin/sh
# Shared OpenClaw install-root and derived-path helpers.
OPENCLAW_DEFAULT_INSTALL_ROOT="${OPENCLAW_DEFAULT_INSTALL_ROOT:-/opt}"
oc_normalize_install_root() {
local path="$1"
if [ -z "$path" ]; then
path="$OPENCLAW_DEFAULT_INSTALL_ROOT"
fi
case "$path" in
/*) ;;
*) path="$OPENCLAW_DEFAULT_INSTALL_ROOT" ;;
esac
while [ "$path" != "/" ] && [ "${path%/}" != "$path" ]; do
path="${path%/}"
done
[ -n "$path" ] || path="/"
printf '%s\n' "$path"
}
oc_read_install_root_from_uci() {
if command -v uci >/dev/null 2>&1; then
uci -q get openclaw.main.install_root 2>/dev/null
fi
}
oc_load_paths() {
local requested_root="$1"
local install_root="$requested_root"
[ -n "$install_root" ] || install_root="${OPENCLAW_INSTALL_ROOT:-}"
[ -n "$install_root" ] || install_root="$(oc_read_install_root_from_uci)"
OPENCLAW_INSTALL_ROOT="$(oc_normalize_install_root "$install_root")"
if [ "$OPENCLAW_INSTALL_ROOT" = "/" ]; then
OC_ROOT="/openclaw"
else
OC_ROOT="${OPENCLAW_INSTALL_ROOT}/openclaw"
fi
NODE_BASE="${OC_ROOT}/node"
OC_GLOBAL="${OC_ROOT}/global"
OC_DATA="${OC_ROOT}/data"
export OPENCLAW_INSTALL_ROOT OC_ROOT NODE_BASE OC_GLOBAL OC_DATA
}
oc_find_existing_path() {
local path
path="$(oc_normalize_install_root "$1")"
while [ "$path" != "/" ] && [ ! -e "$path" ]; do
path="${path%/*}"
[ -n "$path" ] || path="/"
done
printf '%s\n' "$path"
}
oc_install_root_uses_opt_workaround() {
[ "$(oc_normalize_install_root "${1:-$OPENCLAW_INSTALL_ROOT}")" = "/opt" ]
}
oc_print_env() {
oc_load_paths "$1"
cat <<EOF
OPENCLAW_INSTALL_ROOT=$OPENCLAW_INSTALL_ROOT
OC_ROOT=$OC_ROOT
NODE_BASE=$NODE_BASE
OC_GLOBAL=$OC_GLOBAL
OC_DATA=$OC_DATA
EOF
}

View File

@@ -29,9 +29,8 @@ get_pid_by_port() {
}
# ── 路径 (OpenWrt 适配) ──
NODE_BASE="${NODE_BASE:-/opt/openclaw/node}"
OC_GLOBAL="${OC_GLOBAL:-/opt/openclaw/global}"
OC_DATA="${OC_DATA:-/opt/openclaw/data}"
. /usr/libexec/openclaw-paths.sh
oc_load_paths "$OPENCLAW_INSTALL_ROOT"
NODE_BIN="${NODE_BASE}/bin/node"
OC_STATE_DIR="${OC_DATA}/.openclaw"
CONFIG_FILE="${OC_STATE_DIR}/openclaw.json"
@@ -99,7 +98,7 @@ json_set() {
local parent_dir="$(dirname "$CONFIG_FILE")"
if ! mkdir -p "$parent_dir" 2>/dev/null; then
echo "ERROR: 无法创建配置目录 $parent_dir" >&2
echo "HINT: 请检查 /opt/openclaw/data 是否存在且有写权限" >&2
echo "HINT: 请检查 ${OC_DATA} 是否存在且有写权限" >&2
return 1
fi
@@ -2362,7 +2361,7 @@ backup_restore_menu() {
# 提取 payload 到根目录 (还原到原始绝对路径)
tar -xzf "$latest" --strip-components=3 -C / "${backup_name}/payload/posix/" 2>&1
# 修复权限
chown -R openclaw:openclaw /opt/openclaw/data/.openclaw 2>/dev/null
chown -R openclaw:openclaw "${OC_DATA}/.openclaw" 2>/dev/null
echo -e " ${GREEN}✅ 配置和数据已完整恢复!原配置已保存为 openclaw.json.pre-restore${NC}"
echo ""
prompt_with_default "是否重启服务使配置生效? (Y/n)" "Y" do_restart

View File

@@ -14,12 +14,36 @@ const fs = require('fs');
const path = require('path');
const os = require('os');
function loadInstallRoot() {
if (process.env.OPENCLAW_INSTALL_ROOT) {
return normalizeInstallRoot(process.env.OPENCLAW_INSTALL_ROOT);
}
try {
const { execSync } = require('child_process');
return normalizeInstallRoot(execSync('uci -q get openclaw.main.install_root 2>/dev/null', {
encoding: 'utf8',
timeout: 3000,
}).trim());
} catch {
return '/opt';
}
}
function normalizeInstallRoot(value) {
const cleaned = (value || '').trim();
if (!cleaned || cleaned[0] !== '/' || /\s/.test(cleaned)) return '/opt';
const normalized = cleaned.replace(/\/+$/, '');
return normalized || '/';
}
// ── 配置 (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 INSTALL_ROOT = loadInstallRoot();
const OC_ROOT = INSTALL_ROOT === '/' ? '/openclaw' : `${INSTALL_ROOT}/openclaw`;
const NODE_BASE = process.env.NODE_BASE || `${OC_ROOT}/node`;
const OC_GLOBAL = process.env.OC_GLOBAL || `${OC_ROOT}/global`;
const OC_DATA = process.env.OC_DATA || `${OC_ROOT}/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';
@@ -170,6 +194,7 @@ class PtySession {
const env = {
...process.env, TERM: 'xterm-256color', COLUMNS: String(this.cols), LINES: String(this.rows),
COLORTERM: 'truecolor', LANG: 'en_US.UTF-8',
OPENCLAW_INSTALL_ROOT: INSTALL_ROOT,
NODE_BASE, OC_GLOBAL, OC_DATA,
HOME: OC_DATA,
OPENCLAW_HOME: OC_DATA,