2026-08-10 05:03:43 +00:00
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# OpenClaw 一键安装配置脚本 (install.sh)
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 用途:
|
|
|
|
|
|
# 在一台全新的 VPS 上,全自动完成 OpenClaw 的安装与基础配置:
|
|
|
|
|
|
# 1. 系统 / 架构 / 内存 前置检查
|
|
|
|
|
|
# 2. 安装系统依赖 (curl git build-essential python3) 与 Node.js 24 (NodeSource)
|
|
|
|
|
|
# 3. 通过 npm 全局安装 openclaw
|
|
|
|
|
|
# 4. 中文交互式向导:Telegram Bot Token / 用户 ID / 模型 provider
|
|
|
|
|
|
# 5. 使用 python3 安全读写 ~/.openclaw/openclaw.json (先备份,只改填写字段)
|
|
|
|
|
|
# 6. 启动 daemon 并验证运行状态
|
|
|
|
|
|
# 7. 输出安装位置 / 配置路径 / 启停与日志命令
|
|
|
|
|
|
#
|
|
|
|
|
|
# 支持系统:
|
|
|
|
|
|
# - Debian 12 / 13
|
|
|
|
|
|
# - Ubuntu 22.04 / 24.04
|
|
|
|
|
|
# - (尽力兼容 dnf 系: RHEL / Rocky / Alma / Fedora,不做保证)
|
|
|
|
|
|
# - 架构: x86_64 (amd64) / aarch64 (arm64)
|
|
|
|
|
|
#
|
|
|
|
|
|
# 用法:
|
|
|
|
|
|
# 以 root 身份运行:
|
|
|
|
|
|
# bash install.sh
|
|
|
|
|
|
# 非交互(仅安装、跳过向导):
|
|
|
|
|
|
# OPENCLAW_NONINTERACTIVE=1 bash install.sh
|
|
|
|
|
|
#
|
|
|
|
|
|
# 隐私声明:
|
|
|
|
|
|
# 本脚本不包含任何埋点、统计、遥测或向第三方上报的逻辑。
|
|
|
|
|
|
# 除了 apt/dnf 官方源、NodeSource 与 npm registry 之外,不访问任何外部服务。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 免责声明:
|
|
|
|
|
|
# 本脚本按“现状”提供,不附带任何担保。请在了解其行为后自行承担使用风险。
|
|
|
|
|
|
# 脚本会修改系统软件包与 OpenClaw 配置文件;生产环境请先备份。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 日志: /var/log/openclaw-installer.log
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
set -uo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 全局常量
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
SCRIPT_VERSION="1.0.0"
|
|
|
|
|
|
LOG_FILE="/var/log/openclaw-installer.log"
|
|
|
|
|
|
NODE_MAJOR_REQUIRED=20
|
|
|
|
|
|
NODE_MAJOR_INSTALL=24
|
|
|
|
|
|
OPENCLAW_HOME="" # 稍后按真实用户 HOME 推导
|
|
|
|
|
|
OPENCLAW_CONFIG="" # 稍后探测
|
2026-08-10 05:15:03 +00:00
|
|
|
|
PKG_MGR=""
|
|
|
|
|
|
PLATFORM=""
|
|
|
|
|
|
IS_WSL="0"
|
|
|
|
|
|
CFG_PNAME=""
|
|
|
|
|
|
CFG_DEFMODEL=""
|
2026-08-10 05:03:43 +00:00
|
|
|
|
OS_ID=""
|
|
|
|
|
|
OS_VERSION=""
|
|
|
|
|
|
ARCH=""
|
|
|
|
|
|
NONINTERACTIVE="${OPENCLAW_NONINTERACTIVE:-0}"
|
|
|
|
|
|
|
|
|
|
|
|
# 向导收集到的值
|
|
|
|
|
|
CFG_TG_TOKEN=""
|
|
|
|
|
|
CFG_TG_USERID=""
|
|
|
|
|
|
CFG_PROVIDER="" # openai-compat | anthropic | skip
|
|
|
|
|
|
CFG_BASEURL=""
|
|
|
|
|
|
CFG_APIKEY=""
|
|
|
|
|
|
CFG_MODEL=""
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 颜色 (无 TTY 时禁用)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
|
|
|
|
|
C_RED=$'\033[0;31m'
|
|
|
|
|
|
C_GREEN=$'\033[0;32m'
|
|
|
|
|
|
C_YELLOW=$'\033[0;33m'
|
|
|
|
|
|
C_BLUE=$'\033[0;36m'
|
|
|
|
|
|
C_BOLD=$'\033[1m'
|
|
|
|
|
|
C_RESET=$'\033[0m'
|
|
|
|
|
|
else
|
|
|
|
|
|
C_RED=""; C_GREEN=""; C_YELLOW=""; C_BLUE=""; C_BOLD=""; C_RESET=""
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 日志与输出
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
_log() {
|
|
|
|
|
|
# 只写文件,不上屏
|
|
|
|
|
|
local ts
|
|
|
|
|
|
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
|
|
|
|
printf '[%s] %s\n' "$ts" "$*" >>"$LOG_FILE" 2>/dev/null || true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
info() { printf '%s\n' "${C_BLUE}==>${C_RESET} $*"; _log "INFO: $*"; }
|
|
|
|
|
|
ok() { printf '%s\n' "${C_GREEN}[✓]${C_RESET} $*"; _log "OK: $*"; }
|
|
|
|
|
|
warn() { printf '%s\n' "${C_YELLOW}[!]${C_RESET} $*"; _log "WARN: $*"; }
|
|
|
|
|
|
err() { printf '%s\n' "${C_RED}[✗]${C_RESET} $*" >&2; _log "ERROR: $*"; }
|
|
|
|
|
|
hint() { printf '%s\n' " ${C_YELLOW}提示:${C_RESET}$*"; _log "HINT: $*"; }
|
|
|
|
|
|
plain() { printf '%s\n' "$*"; }
|
|
|
|
|
|
|
|
|
|
|
|
title() {
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
printf '%s\n' "${C_BOLD}──── $* ────${C_RESET}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 运行一条命令:输出进日志,屏幕保持安静;失败返回非零
|
|
|
|
|
|
run_quiet() {
|
|
|
|
|
|
_log "RUN: $*"
|
|
|
|
|
|
if "$@" >>"$LOG_FILE" 2>&1; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
local rc=$?
|
|
|
|
|
|
_log "RUN FAILED (rc=$rc): $*"
|
|
|
|
|
|
return "$rc"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 致命错误:给中文提示后退出
|
|
|
|
|
|
die() {
|
|
|
|
|
|
err "$1"
|
|
|
|
|
|
if [ -n "${2:-}" ]; then hint "$2"; fi
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
plain "完整日志:${LOG_FILE}"
|
|
|
|
|
|
plain "可执行 ${C_BOLD}tail -n 50 ${LOG_FILE}${C_RESET} 查看最后的错误细节。"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 交互读取
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# ask <提示> <默认值> -> 结果放入 REPLY_VALUE
|
|
|
|
|
|
REPLY_VALUE=""
|
|
|
|
|
|
ask() {
|
|
|
|
|
|
local prompt="$1" default="${2:-}" ans=""
|
|
|
|
|
|
REPLY_VALUE=""
|
|
|
|
|
|
if [ "$NONINTERACTIVE" = "1" ] || [ ! -t 0 ]; then
|
|
|
|
|
|
REPLY_VALUE="$default"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [ -n "$default" ]; then
|
|
|
|
|
|
printf '%s' "${prompt} [默认: ${default}]: "
|
|
|
|
|
|
else
|
|
|
|
|
|
printf '%s' "${prompt}(直接回车跳过): "
|
|
|
|
|
|
fi
|
|
|
|
|
|
IFS= read -r ans || ans=""
|
|
|
|
|
|
if [ -z "$ans" ]; then ans="$default"; fi
|
|
|
|
|
|
REPLY_VALUE="$ans"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ask_secret <提示> -> 不回显
|
|
|
|
|
|
ask_secret() {
|
|
|
|
|
|
local prompt="$1" ans=""
|
|
|
|
|
|
REPLY_VALUE=""
|
|
|
|
|
|
if [ "$NONINTERACTIVE" = "1" ] || [ ! -t 0 ]; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
printf '%s' "${prompt}(输入不显示,直接回车跳过): "
|
|
|
|
|
|
IFS= read -rs ans || ans=""
|
|
|
|
|
|
printf '\n'
|
|
|
|
|
|
REPLY_VALUE="$ans"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ask_yes_no <提示> <默认 y|n> -> 返回 0 表示 yes
|
|
|
|
|
|
ask_yes_no() {
|
|
|
|
|
|
local prompt="$1" default="${2:-n}" ans=""
|
|
|
|
|
|
if [ "$NONINTERACTIVE" = "1" ] || [ ! -t 0 ]; then
|
|
|
|
|
|
[ "$default" = "y" ] && return 0 || return 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
local hintstr="[y/N]"
|
|
|
|
|
|
[ "$default" = "y" ] && hintstr="[Y/n]"
|
|
|
|
|
|
while true; do
|
|
|
|
|
|
printf '%s' "${prompt} ${hintstr}: "
|
|
|
|
|
|
IFS= read -r ans || ans=""
|
|
|
|
|
|
[ -z "$ans" ] && ans="$default"
|
|
|
|
|
|
case "$ans" in
|
|
|
|
|
|
y|Y|yes|YES|是) return 0 ;;
|
|
|
|
|
|
n|N|no|NO|否) return 1 ;;
|
|
|
|
|
|
*) plain "请输入 y 或 n。" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
done
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 0. 初始化日志
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
init_log() {
|
|
|
|
|
|
if ! touch "$LOG_FILE" 2>/dev/null; then
|
|
|
|
|
|
LOG_FILE="/tmp/openclaw-installer.log"
|
|
|
|
|
|
touch "$LOG_FILE" 2>/dev/null || LOG_FILE="/dev/null"
|
|
|
|
|
|
fi
|
|
|
|
|
|
chmod 600 "$LOG_FILE" 2>/dev/null || true
|
|
|
|
|
|
_log "=========== OpenClaw installer v${SCRIPT_VERSION} 启动 ==========="
|
|
|
|
|
|
_log "命令行: $0 $*"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 1. 前置检查
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
check_root() {
|
|
|
|
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
|
|
|
|
err "本脚本需要 root 权限运行。"
|
|
|
|
|
|
hint "请改用:${C_BOLD}sudo bash $0${C_RESET}"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
ok "权限检查通过(当前为 root)"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
detect_os() {
|
2026-08-10 05:15:03 +00:00
|
|
|
|
PLATFORM="unknown"; IS_WSL="0"; PKG_MGR=""
|
2026-08-10 05:03:43 +00:00
|
|
|
|
|
2026-08-10 05:15:03 +00:00
|
|
|
|
# 原生 Windows(Git Bash / MSYS / Cygwin)
|
|
|
|
|
|
case "${OSTYPE:-}" in
|
|
|
|
|
|
msys*|cygwin*|win32*) PLATFORM="windows" ;;
|
2026-08-10 05:03:43 +00:00
|
|
|
|
esac
|
2026-08-10 05:15:03 +00:00
|
|
|
|
if [ "$PLATFORM" = "windows" ]; then
|
|
|
|
|
|
warn "检测到原生 Windows 环境。"
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
plain "${C_BOLD}Windows 请用官方 PowerShell 安装器(在 PowerShell 中执行):${C_RESET}"
|
|
|
|
|
|
plain " iwr -useb https://openclaw.ai/install.ps1 | iex"
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
plain "${C_BOLD}或者装 WSL2(推荐,体验与 Linux 一致):${C_RESET}"
|
|
|
|
|
|
plain " 1) 管理员 PowerShell 执行: wsl --install -d Debian"
|
|
|
|
|
|
plain " 2) 重启后进入 Debian,再执行本脚本同一条命令"
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
hint "装完 OpenClaw 后,可再回来运行本脚本,选择「仅配置」补齐中文配置向导。"
|
|
|
|
|
|
exit 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
case "$(uname -s 2>/dev/null || echo unknown)" in
|
|
|
|
|
|
Darwin) PLATFORM="macos" ;;
|
|
|
|
|
|
Linux) PLATFORM="linux" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
|
|
# WSL 识别
|
|
|
|
|
|
if [ "$PLATFORM" = "linux" ]; then
|
|
|
|
|
|
if [ -n "${WSL_DISTRO_NAME:-}" ] || grep -qi microsoft /proc/version 2>/dev/null; then
|
|
|
|
|
|
IS_WSL="1"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$PLATFORM" = "macos" ]; then
|
|
|
|
|
|
OS_ID="macos"; OS_VERSION="$(sw_vers -productVersion 2>/dev/null || echo unknown)"
|
|
|
|
|
|
command -v brew >/dev/null 2>&1 && PKG_MGR="brew" || PKG_MGR="none"
|
|
|
|
|
|
ok "系统:macOS ${OS_VERSION}"
|
|
|
|
|
|
[ "$PKG_MGR" = "none" ] && warn "未检测到 Homebrew,官方安装器会在需要时自行安装。"
|
|
|
|
|
|
elif [ "$PLATFORM" = "linux" ]; then
|
|
|
|
|
|
if [ -r /etc/os-release ]; then
|
|
|
|
|
|
# shellcheck disable=SC1091
|
|
|
|
|
|
. /etc/os-release
|
|
|
|
|
|
OS_ID="${ID:-unknown}"; OS_VERSION="${VERSION_ID:-unknown}"
|
|
|
|
|
|
else
|
|
|
|
|
|
OS_ID="unknown"; OS_VERSION="unknown"
|
|
|
|
|
|
fi
|
|
|
|
|
|
if command -v apt-get >/dev/null 2>&1; then PKG_MGR="apt"
|
|
|
|
|
|
elif command -v dnf >/dev/null 2>&1; then PKG_MGR="dnf"
|
|
|
|
|
|
elif command -v yum >/dev/null 2>&1; then PKG_MGR="yum"
|
|
|
|
|
|
elif command -v apk >/dev/null 2>&1; then PKG_MGR="apk"
|
|
|
|
|
|
else PKG_MGR="none"; fi
|
|
|
|
|
|
local wsl_tag=""; [ "$IS_WSL" = "1" ] && wsl_tag="(WSL)"
|
|
|
|
|
|
case "$OS_ID" in
|
|
|
|
|
|
debian|ubuntu) ok "系统:${PRETTY_NAME:-$OS_ID $OS_VERSION}${wsl_tag}(受支持)" ;;
|
|
|
|
|
|
*) warn "系统:${PRETTY_NAME:-$OS_ID $OS_VERSION}${wsl_tag}(由官方安装器尽力兼容)" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
else
|
|
|
|
|
|
die "无法识别当前操作系统。" "本脚本支持 macOS、Linux(含 WSL);Windows 请用官方 install.ps1。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
_log "PLATFORM=$PLATFORM IS_WSL=$IS_WSL OS_ID=${OS_ID:-} PKG_MGR=$PKG_MGR"
|
2026-08-10 05:03:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
detect_arch() {
|
|
|
|
|
|
ARCH="$(uname -m 2>/dev/null || echo unknown)"
|
|
|
|
|
|
case "$ARCH" in
|
|
|
|
|
|
x86_64|amd64) ok "架构:${ARCH}(受支持)" ;;
|
|
|
|
|
|
aarch64|arm64) ok "架构:${ARCH}(受支持)" ;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
warn "架构:${ARCH} 不在测试范围内,Node.js 官方源可能没有对应包。"
|
|
|
|
|
|
if ! ask_yes_no "仍要继续吗?" "n"; then
|
|
|
|
|
|
die "已按你的选择中止。" "如需在此架构上安装,请手动准备 Node.js ${NODE_MAJOR_REQUIRED}+ 后再运行本脚本。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
check_memory() {
|
|
|
|
|
|
local mem_kb mem_mb swap_kb
|
|
|
|
|
|
mem_kb="$(awk '/^MemTotal:/{print $2}' /proc/meminfo 2>/dev/null)"
|
|
|
|
|
|
swap_kb="$(awk '/^SwapTotal:/{print $2}' /proc/meminfo 2>/dev/null)"
|
|
|
|
|
|
[ -z "$mem_kb" ] && { warn "无法读取内存信息,跳过内存检查。"; return 0; }
|
|
|
|
|
|
mem_mb=$(( mem_kb / 1024 ))
|
|
|
|
|
|
local swap_mb=$(( ${swap_kb:-0} / 1024 ))
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$mem_mb" -lt 1024 ]; then
|
|
|
|
|
|
warn "物理内存仅 ${mem_mb} MB(低于 1 GB),npm 安装阶段可能因内存不足被 OOM 杀掉。"
|
|
|
|
|
|
if [ "$swap_mb" -lt 512 ]; then
|
|
|
|
|
|
hint "建议先创建 1 GB swap 再继续,命令如下(复制整段执行):"
|
|
|
|
|
|
plain " fallocate -l 1G /swapfile && chmod 600 /swapfile && \\"
|
|
|
|
|
|
plain " mkswap /swapfile && swapon /swapfile && \\"
|
|
|
|
|
|
plain " echo '/swapfile none swap sw 0 0' >> /etc/fstab"
|
|
|
|
|
|
else
|
|
|
|
|
|
hint "已检测到 ${swap_mb} MB swap,风险较低,可继续。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
if ! ask_yes_no "现在继续安装吗?" "y"; then
|
|
|
|
|
|
die "已按你的选择中止。" "加好 swap 后重新运行本脚本即可。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "内存:${mem_mb} MB(swap ${swap_mb} MB)"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 2. 系统依赖 + Node.js
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
install_base_deps() {
|
|
|
|
|
|
title "安装系统依赖"
|
|
|
|
|
|
local missing=()
|
|
|
|
|
|
local c
|
|
|
|
|
|
for c in curl git python3; do
|
|
|
|
|
|
command -v "$c" >/dev/null 2>&1 || missing+=("$c")
|
|
|
|
|
|
done
|
|
|
|
|
|
# 编译工具链(node-gyp 可能需要)
|
|
|
|
|
|
if ! command -v cc >/dev/null 2>&1 && ! command -v gcc >/dev/null 2>&1; then
|
|
|
|
|
|
missing+=("__buildtools__")
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [ "${#missing[@]}" -eq 0 ]; then
|
|
|
|
|
|
ok "依赖已齐全(curl / git / python3 / 编译工具)"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
info "需要安装:${missing[*]//__buildtools__/编译工具链}"
|
|
|
|
|
|
if [ "$PKG_MGR" = "apt" ]; then
|
|
|
|
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
|
|
|
|
if ! run_quiet apt-get update; then
|
|
|
|
|
|
warn "apt-get update 失败,可能是软件源不可达。"
|
|
|
|
|
|
hint "请检查网络/DNS,或更换为国内镜像源后重试。脚本将继续尝试安装。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
local pkgs=(ca-certificates curl git python3)
|
|
|
|
|
|
printf '%s\n' "${missing[@]}" | grep -q '__buildtools__' && pkgs+=(build-essential)
|
|
|
|
|
|
if ! run_quiet apt-get install -y "${pkgs[@]}"; then
|
|
|
|
|
|
die "系统依赖安装失败。" \
|
|
|
|
|
|
"请手动执行:apt-get update && apt-get install -y ca-certificates curl git python3 build-essential"
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
if ! run_quiet dnf install -y ca-certificates curl git python3; then
|
|
|
|
|
|
die "系统依赖安装失败。" "请手动执行:dnf install -y ca-certificates curl git python3"
|
|
|
|
|
|
fi
|
|
|
|
|
|
printf '%s\n' "${missing[@]}" | grep -q '__buildtools__' && \
|
|
|
|
|
|
run_quiet dnf groupinstall -y "Development Tools" || true
|
|
|
|
|
|
fi
|
|
|
|
|
|
ok "系统依赖安装完成"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
node_major() {
|
|
|
|
|
|
local v
|
|
|
|
|
|
v="$(node -v 2>/dev/null)" || return 1
|
|
|
|
|
|
v="${v#v}"
|
|
|
|
|
|
printf '%s' "${v%%.*}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
install_node() {
|
2026-08-10 05:15:03 +00:00
|
|
|
|
title "安装 Node.js 与 OpenClaw(官方安装器)"
|
|
|
|
|
|
info "调用官方安装器 https://openclaw.ai/install.sh"
|
|
|
|
|
|
info "它会自动处理 Node.js(macOS 用 Homebrew,Linux 用 NodeSource/apk)"
|
|
|
|
|
|
|
|
|
|
|
|
local tmp_inst
|
|
|
|
|
|
tmp_inst="$(mktemp /tmp/openclaw_official.XXXXXX.sh)" || \
|
|
|
|
|
|
die "无法创建临时文件。" "请检查 /tmp 是否可写、磁盘是否已满。"
|
|
|
|
|
|
|
|
|
|
|
|
if ! curl -fsSL --proto '=https' --tlsv1.2 -m 120 \
|
|
|
|
|
|
https://openclaw.ai/install.sh -o "$tmp_inst"; then
|
|
|
|
|
|
rm -f "$tmp_inst"
|
|
|
|
|
|
die "下载官方安装器失败。" \
|
|
|
|
|
|
"排查:1) 检查网络能否访问 openclaw.ai;2) 国内网络可能需要代理;3) 稍后重试。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if ! head -1 "$tmp_inst" | grep -q '^#!'; then
|
|
|
|
|
|
rm -f "$tmp_inst"
|
|
|
|
|
|
die "官方安装器内容异常(不是脚本)。" "可能被网络劫持,请检查网络环境后重试。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
_log "RUN: bash official install.sh --yes"
|
|
|
|
|
|
if bash "$tmp_inst" --yes 2>&1 | tee -a "$LOG_FILE"; then
|
|
|
|
|
|
ok "官方安装器执行完成"
|
|
|
|
|
|
else
|
|
|
|
|
|
warn "官方安装器返回非零状态,继续检查实际安装结果。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
rm -f "$tmp_inst"
|
|
|
|
|
|
|
|
|
|
|
|
# 官方装完可能只在当前 shell 之外的 PATH 里,补几个常见位置
|
|
|
|
|
|
for d in /usr/local/bin /opt/homebrew/bin "$HOME/.local/bin" "$HOME/.openclaw/bin"; do
|
|
|
|
|
|
[ -d "$d" ] && case ":$PATH:" in *":$d:"*) : ;; *) PATH="$d:$PATH" ;; esac
|
|
|
|
|
|
done
|
|
|
|
|
|
export PATH
|
|
|
|
|
|
|
2026-08-10 05:03:43 +00:00
|
|
|
|
if command -v node >/dev/null 2>&1; then
|
2026-08-10 05:15:03 +00:00
|
|
|
|
ok "Node.js $(node -v) 就绪"
|
2026-08-10 05:03:43 +00:00
|
|
|
|
else
|
2026-08-10 05:15:03 +00:00
|
|
|
|
die "Node.js 安装后仍找不到 node 命令。" \
|
|
|
|
|
|
"请打开一个新终端后重试,或手动安装 Node 22.22.3+ / 24.15+ / 26。"
|
2026-08-10 05:03:43 +00:00
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 05:15:03 +00:00
|
|
|
|
install_openclaw() {
|
2026-08-10 05:03:43 +00:00
|
|
|
|
info "正在执行 npm install -g openclaw@latest(首次安装可能需要几分钟)…"
|
|
|
|
|
|
if run_quiet npm install -g openclaw@latest --no-update-notifier; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
warn "npm 安装失败,尝试清理缓存后重试一次…"
|
|
|
|
|
|
run_quiet npm cache clean --force || true
|
|
|
|
|
|
if run_quiet npm install -g openclaw@latest --no-update-notifier; then
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
install_openclaw() {
|
2026-08-10 05:15:03 +00:00
|
|
|
|
title "校验 OpenClaw"
|
2026-08-10 05:03:43 +00:00
|
|
|
|
local local_ver remote_ver
|
|
|
|
|
|
local_ver="$(openclaw_local_version)"
|
|
|
|
|
|
|
|
|
|
|
|
if [ -n "$local_ver" ]; then
|
|
|
|
|
|
remote_ver="$(openclaw_remote_version)"
|
|
|
|
|
|
if [ -n "$remote_ver" ] && [ "$local_ver" = "$remote_ver" ]; then
|
|
|
|
|
|
ok "已安装 OpenClaw v${local_ver},已是最新版本,跳过安装。"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
if [ -n "$remote_ver" ]; then
|
|
|
|
|
|
warn "检测到已安装 OpenClaw v${local_ver},最新版本为 v${remote_ver}。"
|
|
|
|
|
|
else
|
|
|
|
|
|
warn "检测到已安装 OpenClaw v${local_ver}(无法查询最新版本,可能网络受限)。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
if ask_yes_no "是否更新到最新版本?(选 n 将保留现有版本继续配置)" "y"; then
|
|
|
|
|
|
if ! npm_install_openclaw; then
|
|
|
|
|
|
warn "更新失败,将继续使用已安装的 v${local_ver}。"
|
|
|
|
|
|
hint "稍后可手动重试:npm install -g openclaw@latest"
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "OpenClaw 已更新到 v$(openclaw_local_version)"
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "保留现有版本 v${local_ver},跳过安装。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
if ! npm_install_openclaw; then
|
|
|
|
|
|
die "OpenClaw 安装失败。" \
|
|
|
|
|
|
"排查建议:1) 检查网络能否访问 registry.npmjs.org;2) 内存不足请加 swap;3) 查看详细报错 tail -n 60 ${LOG_FILE}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
ok "OpenClaw 安装完成"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# 验证
|
|
|
|
|
|
if ! command -v openclaw >/dev/null 2>&1; then
|
|
|
|
|
|
die "找不到 openclaw 命令。" \
|
|
|
|
|
|
"npm 全局 bin 目录可能不在 PATH 中。请执行:export PATH=\"\$(npm prefix -g)/bin:\$PATH\",然后重跑本脚本。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
local ver_out
|
|
|
|
|
|
ver_out="$(openclaw --version 2>&1 | head -n 1)"
|
|
|
|
|
|
if [ -z "$ver_out" ]; then
|
|
|
|
|
|
warn "openclaw --version 无输出,安装可能不完整,但将继续。"
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "版本验证:openclaw ${ver_out}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 4. 配置文件探测
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
resolve_home() {
|
|
|
|
|
|
# root 运行时 daemon 通常也以 root 身份,配置放在 root 的 HOME
|
|
|
|
|
|
OPENCLAW_HOME="${HOME:-/root}"
|
|
|
|
|
|
[ -d "$OPENCLAW_HOME" ] || OPENCLAW_HOME="/root"
|
|
|
|
|
|
_log "OPENCLAW_HOME=$OPENCLAW_HOME"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
detect_config_path() {
|
|
|
|
|
|
local dir="${OPENCLAW_HOME}/.openclaw"
|
|
|
|
|
|
mkdir -p "$dir" 2>/dev/null || true
|
|
|
|
|
|
chmod 700 "$dir" 2>/dev/null || true
|
|
|
|
|
|
|
|
|
|
|
|
# 优先使用已存在的文件;openclaw.json 优先于 config.json
|
|
|
|
|
|
local candidate
|
|
|
|
|
|
for candidate in "$dir/openclaw.json" "$dir/config.json"; do
|
|
|
|
|
|
if [ -f "$candidate" ]; then
|
|
|
|
|
|
OPENCLAW_CONFIG="$candidate"
|
|
|
|
|
|
_log "探测到已有配置: $OPENCLAW_CONFIG"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
|
|
|
|
|
# 都不存在 -> 使用默认名
|
|
|
|
|
|
OPENCLAW_CONFIG="$dir/openclaw.json"
|
|
|
|
|
|
_log "未发现已有配置,将使用: $OPENCLAW_CONFIG"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 5. 交互式配置向导
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
wizard() {
|
|
|
|
|
|
title "配置向导"
|
|
|
|
|
|
if [ "$NONINTERACTIVE" = "1" ] || [ ! -t 0 ]; then
|
|
|
|
|
|
warn "当前为非交互模式,跳过配置向导。"
|
|
|
|
|
|
hint "稍后可手动编辑配置文件:${OPENCLAW_CONFIG}"
|
|
|
|
|
|
CFG_PROVIDER="skip"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
plain "下面几步都可以直接回车跳过,之后随时能改配置文件。"
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
|
|
|
|
|
|
# --- Telegram ---
|
|
|
|
|
|
plain "${C_BOLD}[1/3] Telegram 机器人${C_RESET}"
|
|
|
|
|
|
plain " 获取方式:在 Telegram 里搜索 ${C_BOLD}@BotFather${C_RESET} → 发送 /newbot → 按提示创建"
|
|
|
|
|
|
plain " 创建成功后它会给你一串形如 123456:AAxxxxxxxx 的 Token"
|
|
|
|
|
|
ask_secret " 请粘贴 Bot Token"
|
|
|
|
|
|
CFG_TG_TOKEN="$REPLY_VALUE"
|
|
|
|
|
|
if [ -n "$CFG_TG_TOKEN" ]; then
|
|
|
|
|
|
if printf '%s' "$CFG_TG_TOKEN" | grep -Eq '^[0-9]{5,}:[A-Za-z0-9_-]{20,}$'; then
|
|
|
|
|
|
ok " Token 已记录(格式校验通过,内容不显示)"
|
|
|
|
|
|
else
|
|
|
|
|
|
warn " Token 格式看起来不太对(通常是 数字:字母数字)。仍会写入,请稍后自行确认。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
plain " 已跳过 Telegram Token。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
plain "${C_BOLD}[2/3] 你的 Telegram 用户 ID${C_RESET}(用于白名单,只允许你本人使用机器人)"
|
|
|
|
|
|
plain " 获取方式:在 Telegram 里搜索 ${C_BOLD}@userinfobot${C_RESET} → 发送任意消息 → 它会回复你的数字 ID"
|
|
|
|
|
|
while true; do
|
|
|
|
|
|
ask " 请输入你的数字用户 ID" ""
|
|
|
|
|
|
CFG_TG_USERID="$REPLY_VALUE"
|
|
|
|
|
|
[ -z "$CFG_TG_USERID" ] && { plain " 已跳过用户 ID(注意:不设白名单意味着任何人都能用你的机器人)。"; break; }
|
|
|
|
|
|
if printf '%s' "$CFG_TG_USERID" | grep -Eq '^[0-9]{5,}$'; then
|
|
|
|
|
|
ok " 用户 ID 已记录:${CFG_TG_USERID}"
|
|
|
|
|
|
break
|
|
|
|
|
|
fi
|
|
|
|
|
|
warn " 用户 ID 应该是纯数字(一般 8-11 位),请重新输入,或直接回车跳过。"
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
# --- Provider ---
|
|
|
|
|
|
plain ""
|
2026-08-10 05:15:03 +00:00
|
|
|
|
plain "${C_BOLD}[3/3] AI 模型服务(API 配置)${C_RESET}"
|
|
|
|
|
|
plain " 1) OpenAI 官方"
|
2026-08-10 05:03:43 +00:00
|
|
|
|
plain " 2) Anthropic 官方(Claude)"
|
2026-08-10 05:15:03 +00:00
|
|
|
|
plain " 3) DeepSeek"
|
|
|
|
|
|
plain " 4) 硅基流动 SiliconFlow"
|
|
|
|
|
|
plain " 5) OpenRouter"
|
|
|
|
|
|
plain " 6) 自定义 OpenAI 兼容接口(中转站/自建,手填 baseURL)"
|
|
|
|
|
|
plain " 7) 稍后手动配置"
|
2026-08-10 05:03:43 +00:00
|
|
|
|
local choice
|
|
|
|
|
|
while true; do
|
2026-08-10 05:15:03 +00:00
|
|
|
|
ask " 请输入编号" "7"
|
2026-08-10 05:03:43 +00:00
|
|
|
|
choice="$REPLY_VALUE"
|
|
|
|
|
|
case "$choice" in
|
2026-08-10 05:15:03 +00:00
|
|
|
|
1) CFG_PROVIDER="openai-compat"; CFG_BASEURL="https://api.openai.com/v1"
|
|
|
|
|
|
CFG_DEFMODEL="gpt-4o-mini"; CFG_PNAME="OpenAI 官方"; break ;;
|
|
|
|
|
|
2) CFG_PROVIDER="anthropic"; CFG_BASEURL=""
|
|
|
|
|
|
CFG_DEFMODEL="claude-sonnet-4-5"; CFG_PNAME="Anthropic 官方"; break ;;
|
|
|
|
|
|
3) CFG_PROVIDER="openai-compat"; CFG_BASEURL="https://api.deepseek.com/v1"
|
|
|
|
|
|
CFG_DEFMODEL="deepseek-chat"; CFG_PNAME="DeepSeek"; break ;;
|
|
|
|
|
|
4) CFG_PROVIDER="openai-compat"; CFG_BASEURL="https://api.siliconflow.cn/v1"
|
|
|
|
|
|
CFG_DEFMODEL="deepseek-ai/DeepSeek-V3"; CFG_PNAME="硅基流动"; break ;;
|
|
|
|
|
|
5) CFG_PROVIDER="openai-compat"; CFG_BASEURL="https://openrouter.ai/api/v1"
|
|
|
|
|
|
CFG_DEFMODEL="anthropic/claude-sonnet-4.5"; CFG_PNAME="OpenRouter"; break ;;
|
|
|
|
|
|
6) CFG_PROVIDER="openai-compat"; CFG_BASEURL=""
|
|
|
|
|
|
CFG_DEFMODEL="gpt-4o-mini"; CFG_PNAME="自定义"; break ;;
|
|
|
|
|
|
7) CFG_PROVIDER="skip"; break ;;
|
|
|
|
|
|
*) warn " 请输入 1-7 之间的编号。" ;;
|
2026-08-10 05:03:43 +00:00
|
|
|
|
esac
|
|
|
|
|
|
done
|
|
|
|
|
|
|
2026-08-10 05:15:03 +00:00
|
|
|
|
if [ "$CFG_PROVIDER" = "skip" ]; then
|
|
|
|
|
|
plain " 已跳过模型配置。稍后可重跑本脚本选择「仅配置」补上。"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
ok " 已选择:${CFG_PNAME}"
|
|
|
|
|
|
|
|
|
|
|
|
# baseURL:官方预设直接用,自定义才问
|
|
|
|
|
|
if [ "$CFG_PROVIDER" = "openai-compat" ]; then
|
|
|
|
|
|
if [ -z "$CFG_BASEURL" ]; then
|
|
|
|
|
|
while true; do
|
|
|
|
|
|
ask " 接口地址 baseURL(例:https://xxx.com/v1)" ""
|
|
|
|
|
|
CFG_BASEURL="$REPLY_VALUE"
|
|
|
|
|
|
case "$CFG_BASEURL" in
|
|
|
|
|
|
http://*|https://*) break ;;
|
|
|
|
|
|
"") warn " baseURL 不能为空,中转站地址一般以 /v1 结尾。" ;;
|
|
|
|
|
|
*) warn " 地址需以 http:// 或 https:// 开头。" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
done
|
|
|
|
|
|
else
|
|
|
|
|
|
plain " 接口地址:${CFG_BASEURL}"
|
|
|
|
|
|
if ask_yes_no " 需要改成别的地址吗?" "n"; then
|
|
|
|
|
|
ask " 新的 baseURL" "$CFG_BASEURL"
|
|
|
|
|
|
CFG_BASEURL="$REPLY_VALUE"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# API Key
|
|
|
|
|
|
while true; do
|
|
|
|
|
|
ask_secret " API Key(输入不显示,直接回车可跳过)"
|
|
|
|
|
|
CFG_APIKEY="$REPLY_VALUE"
|
|
|
|
|
|
if [ -z "$CFG_APIKEY" ]; then
|
|
|
|
|
|
warn " 未填写 API Key,模型将无法调用。"
|
|
|
|
|
|
ask_yes_no " 确定跳过吗?" "n" && break
|
|
|
|
|
|
else
|
|
|
|
|
|
ok " API Key 已记录(长度 ${#CFG_APIKEY},内容不显示)"
|
|
|
|
|
|
break
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
# 模型名
|
|
|
|
|
|
ask " 模型名称" "$CFG_DEFMODEL"
|
|
|
|
|
|
CFG_MODEL="$REPLY_VALUE"
|
|
|
|
|
|
[ -z "$CFG_MODEL" ] && CFG_MODEL="$CFG_DEFMODEL"
|
|
|
|
|
|
|
|
|
|
|
|
# 连通性测试(仅 openai-compat 且有 key 时)
|
|
|
|
|
|
if [ -n "$CFG_APIKEY" ] && [ "$CFG_PROVIDER" = "openai-compat" ] && [ -n "$CFG_BASEURL" ]; then
|
|
|
|
|
|
if ask_yes_no " 现在测试一下这个 API 通不通?" "y"; then
|
|
|
|
|
|
info " 正在测试 ${CFG_BASEURL}/models ..."
|
|
|
|
|
|
local code
|
|
|
|
|
|
code="$(curl -s -o /dev/null -m 15 -w '%{http_code}' \
|
|
|
|
|
|
-H "Authorization: Bearer ${CFG_APIKEY}" \
|
|
|
|
|
|
"${CFG_BASEURL%/}/models" 2>/dev/null || echo 000)"
|
|
|
|
|
|
case "$code" in
|
|
|
|
|
|
200) ok " API 测试通过(HTTP 200),Key 有效。" ;;
|
|
|
|
|
|
401|403) warn " API 返回 ${code}:Key 可能无效或没权限,配置仍会写入,稍后可改。" ;;
|
|
|
|
|
|
000) warn " 连不上该地址(超时/DNS 失败),请确认地址与网络,配置仍会写入。" ;;
|
|
|
|
|
|
*) warn " API 返回 HTTP ${code},不确定是否可用,配置仍会写入。" ;;
|
2026-08-10 05:03:43 +00:00
|
|
|
|
esac
|
2026-08-10 05:15:03 +00:00
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
ok " 模型配置已记录:${CFG_PNAME} / ${CFG_MODEL}"
|
2026-08-10 05:03:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 6. 写配置(python3 + 先备份 + 只改填写字段)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
backup_config() {
|
|
|
|
|
|
[ -f "$OPENCLAW_CONFIG" ] || return 0
|
|
|
|
|
|
local bak
|
|
|
|
|
|
bak="${OPENCLAW_CONFIG}.bak.$(date '+%Y%m%d-%H%M%S')"
|
|
|
|
|
|
if cp -p "$OPENCLAW_CONFIG" "$bak" 2>/dev/null; then
|
|
|
|
|
|
ok "原配置已备份:${bak}"
|
|
|
|
|
|
_log "backup -> $bak"
|
|
|
|
|
|
else
|
|
|
|
|
|
warn "备份原配置失败,为安全起见不修改配置文件。"
|
|
|
|
|
|
hint "请检查磁盘空间与目录权限:ls -ld $(dirname "$OPENCLAW_CONFIG")"
|
|
|
|
|
|
return 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
return 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
write_config() {
|
|
|
|
|
|
title "写入配置"
|
|
|
|
|
|
|
|
|
|
|
|
if [ -z "$CFG_TG_TOKEN" ] && [ -z "$CFG_TG_USERID" ] && [ "$CFG_PROVIDER" = "skip" ]; then
|
|
|
|
|
|
info "没有需要写入的配置项,跳过。"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if ! backup_config; then
|
|
|
|
|
|
return 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# 值通过环境变量传给 python,避免出现在命令行/ps 中,也避免引号注入
|
|
|
|
|
|
OC_CFG_PATH="$OPENCLAW_CONFIG" \
|
|
|
|
|
|
OC_TG_TOKEN="$CFG_TG_TOKEN" \
|
|
|
|
|
|
OC_TG_USERID="$CFG_TG_USERID" \
|
|
|
|
|
|
OC_PROVIDER="$CFG_PROVIDER" \
|
|
|
|
|
|
OC_BASEURL="$CFG_BASEURL" \
|
|
|
|
|
|
OC_APIKEY="$CFG_APIKEY" \
|
|
|
|
|
|
OC_MODEL="$CFG_MODEL" \
|
|
|
|
|
|
python3 - <<'PYEOF' >>"$LOG_FILE" 2>&1
|
|
|
|
|
|
import json, os, sys, tempfile
|
|
|
|
|
|
|
|
|
|
|
|
path = os.environ["OC_CFG_PATH"]
|
|
|
|
|
|
token = os.environ.get("OC_TG_TOKEN", "")
|
|
|
|
|
|
userid = os.environ.get("OC_TG_USERID", "")
|
|
|
|
|
|
provider = os.environ.get("OC_PROVIDER", "skip")
|
|
|
|
|
|
baseurl = os.environ.get("OC_BASEURL", "")
|
|
|
|
|
|
apikey = os.environ.get("OC_APIKEY", "")
|
|
|
|
|
|
model = os.environ.get("OC_MODEL", "")
|
|
|
|
|
|
|
|
|
|
|
|
# 1) 读取已有配置;坏 JSON 不覆盖,直接报错退出
|
|
|
|
|
|
cfg = {}
|
|
|
|
|
|
if os.path.exists(path) and os.path.getsize(path) > 0:
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
|
|
|
|
cfg = json.load(f)
|
|
|
|
|
|
if not isinstance(cfg, dict):
|
|
|
|
|
|
print("ERROR: existing config root is not a JSON object")
|
|
|
|
|
|
sys.exit(3)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print("ERROR: cannot parse existing config: %s" % e)
|
|
|
|
|
|
sys.exit(2)
|
|
|
|
|
|
|
|
|
|
|
|
def node(parent, key):
|
|
|
|
|
|
"""取出 dict 子节点;已存在但类型不对则不动它,返回 None。"""
|
|
|
|
|
|
cur = parent.get(key)
|
|
|
|
|
|
if cur is None:
|
|
|
|
|
|
cur = {}
|
|
|
|
|
|
parent[key] = cur
|
|
|
|
|
|
if not isinstance(cur, dict):
|
|
|
|
|
|
return None
|
|
|
|
|
|
return cur
|
|
|
|
|
|
|
|
|
|
|
|
changed = []
|
|
|
|
|
|
|
|
|
|
|
|
# 2) Telegram:只填用户给了值的字段
|
|
|
|
|
|
if token or userid:
|
|
|
|
|
|
channels = node(cfg, "channels")
|
|
|
|
|
|
tg = node(channels, "telegram") if channels is not None else None
|
|
|
|
|
|
if tg is None:
|
|
|
|
|
|
print("WARN: channels.telegram exists with unexpected type; skipped")
|
|
|
|
|
|
else:
|
|
|
|
|
|
if token:
|
|
|
|
|
|
tg["botToken"] = token
|
|
|
|
|
|
tg["enabled"] = True
|
|
|
|
|
|
changed.append("channels.telegram.botToken")
|
|
|
|
|
|
changed.append("channels.telegram.enabled")
|
|
|
|
|
|
if userid:
|
|
|
|
|
|
existing = tg.get("allowFrom")
|
|
|
|
|
|
if not isinstance(existing, list):
|
|
|
|
|
|
existing = []
|
|
|
|
|
|
vals = [str(x) for x in existing]
|
|
|
|
|
|
if userid not in vals:
|
|
|
|
|
|
vals.append(userid)
|
|
|
|
|
|
tg["allowFrom"] = vals
|
|
|
|
|
|
changed.append("channels.telegram.allowFrom")
|
|
|
|
|
|
|
|
|
|
|
|
# 3) 模型 provider
|
|
|
|
|
|
if provider == "openai-compat" and (baseurl or apikey or model):
|
|
|
|
|
|
models_root = node(cfg, "models")
|
|
|
|
|
|
providers = node(models_root, "providers") if models_root is not None else None
|
|
|
|
|
|
p = node(providers, "openai-compat") if providers is not None else None
|
|
|
|
|
|
if p is None:
|
|
|
|
|
|
print("WARN: models.providers.openai-compat unexpected type; skipped")
|
|
|
|
|
|
else:
|
|
|
|
|
|
if baseurl:
|
|
|
|
|
|
p["baseURL"] = baseurl
|
|
|
|
|
|
changed.append("models.providers.openai-compat.baseURL")
|
|
|
|
|
|
if apikey:
|
|
|
|
|
|
p["apiKey"] = apikey
|
|
|
|
|
|
changed.append("models.providers.openai-compat.apiKey")
|
|
|
|
|
|
if model:
|
|
|
|
|
|
models = p.get("models")
|
|
|
|
|
|
if not isinstance(models, list):
|
|
|
|
|
|
models = []
|
|
|
|
|
|
if model not in models:
|
|
|
|
|
|
models.append(model)
|
|
|
|
|
|
p["models"] = models
|
|
|
|
|
|
changed.append("models.providers.openai-compat.models")
|
|
|
|
|
|
agents_root = node(cfg, "agents")
|
|
|
|
|
|
adef = node(agents_root, "defaults") if agents_root is not None else None
|
|
|
|
|
|
if adef is not None:
|
|
|
|
|
|
adef["model"] = "openai-compat/%s" % model
|
|
|
|
|
|
changed.append("agents.defaults.model")
|
|
|
|
|
|
elif provider == "anthropic" and (apikey or model):
|
|
|
|
|
|
models_root = node(cfg, "models")
|
|
|
|
|
|
providers = node(models_root, "providers") if models_root is not None else None
|
|
|
|
|
|
p = node(providers, "anthropic") if providers is not None else None
|
|
|
|
|
|
if p is None:
|
|
|
|
|
|
print("WARN: models.providers.anthropic unexpected type; skipped")
|
|
|
|
|
|
else:
|
|
|
|
|
|
if apikey:
|
|
|
|
|
|
p["apiKey"] = apikey
|
|
|
|
|
|
changed.append("models.providers.anthropic.apiKey")
|
|
|
|
|
|
if model:
|
|
|
|
|
|
agents_root = node(cfg, "agents")
|
|
|
|
|
|
adef = node(agents_root, "defaults") if agents_root is not None else None
|
|
|
|
|
|
if adef is not None:
|
|
|
|
|
|
adef["model"] = "anthropic/%s" % model
|
|
|
|
|
|
changed.append("agents.defaults.model")
|
|
|
|
|
|
|
|
|
|
|
|
# 4) 原子写入 + 权限 600
|
|
|
|
|
|
d = os.path.dirname(path) or "."
|
|
|
|
|
|
try:
|
|
|
|
|
|
fd, tmp = tempfile.mkstemp(dir=d, prefix=".openclaw.json.")
|
|
|
|
|
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
|
|
|
|
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
|
|
|
|
|
f.write("\n")
|
|
|
|
|
|
os.chmod(tmp, 0o600)
|
|
|
|
|
|
os.replace(tmp, path)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print("ERROR: write failed: %s" % e)
|
|
|
|
|
|
sys.exit(4)
|
|
|
|
|
|
|
|
|
|
|
|
print("CHANGED_FIELDS: %s" % (", ".join(changed) if changed else "(none)"))
|
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
PYEOF
|
|
|
|
|
|
|
|
|
|
|
|
local rc=$?
|
|
|
|
|
|
case "$rc" in
|
|
|
|
|
|
0)
|
|
|
|
|
|
chmod 600 "$OPENCLAW_CONFIG" 2>/dev/null || true
|
|
|
|
|
|
ok "配置已写入:${OPENCLAW_CONFIG}(权限 600)"
|
|
|
|
|
|
local fields
|
|
|
|
|
|
fields="$(grep 'CHANGED_FIELDS:' "$LOG_FILE" 2>/dev/null | tail -n 1 | sed 's/.*CHANGED_FIELDS: //')"
|
|
|
|
|
|
[ -n "$fields" ] && plain " 本次改动字段:${fields}"
|
|
|
|
|
|
;;
|
|
|
|
|
|
2)
|
|
|
|
|
|
err "已有配置文件不是合法 JSON,为避免破坏,脚本没有修改它。"
|
|
|
|
|
|
hint "请检查 ${OPENCLAW_CONFIG} 的内容(或从刚才的 .bak 备份恢复),修好后重跑本脚本。"
|
|
|
|
|
|
return 1 ;;
|
|
|
|
|
|
3)
|
|
|
|
|
|
err "已有配置文件的顶层结构不是对象(应为 { ... }),未做修改。"
|
|
|
|
|
|
hint "请人工检查 ${OPENCLAW_CONFIG}。"
|
|
|
|
|
|
return 1 ;;
|
|
|
|
|
|
4)
|
|
|
|
|
|
err "写入配置文件失败(可能是磁盘满或权限不足)。"
|
|
|
|
|
|
hint "请检查:df -h $(dirname "$OPENCLAW_CONFIG") 与 ls -ld $(dirname "$OPENCLAW_CONFIG")"
|
|
|
|
|
|
return 1 ;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
err "写入配置时发生未知错误(rc=${rc})。"
|
|
|
|
|
|
hint "查看日志末尾:tail -n 30 ${LOG_FILE};原配置已备份,可随时回滚。"
|
|
|
|
|
|
return 1 ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
return 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 7. 初始化 / 启动 / 验证
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
is_running() {
|
|
|
|
|
|
pgrep -f "openclaw.*gateway" >/dev/null 2>&1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onboard_and_start() {
|
|
|
|
|
|
title "初始化并启动服务"
|
|
|
|
|
|
|
|
|
|
|
|
if is_running; then
|
|
|
|
|
|
ok "检测到 OpenClaw gateway 已在运行,跳过初始化。"
|
|
|
|
|
|
else
|
|
|
|
|
|
if [ "$NONINTERACTIVE" = "1" ] || [ ! -t 0 ]; then
|
|
|
|
|
|
info "非交互模式:执行 openclaw onboard --install-daemon(自动模式)…"
|
|
|
|
|
|
run_quiet openclaw onboard --install-daemon || \
|
|
|
|
|
|
warn "onboard 未成功完成,稍后可手动运行:openclaw onboard --install-daemon"
|
|
|
|
|
|
else
|
|
|
|
|
|
plain "接下来运行 ${C_BOLD}openclaw onboard --install-daemon${C_RESET} 完成初始化并安装 systemd 服务。"
|
|
|
|
|
|
plain "该命令可能会有自己的交互提问,按屏幕提示回答即可。"
|
|
|
|
|
|
if ask_yes_no "现在执行吗?" "y"; then
|
|
|
|
|
|
_log "RUN(interactive): openclaw onboard --install-daemon"
|
|
|
|
|
|
if openclaw onboard --install-daemon 2>&1 | tee -a "$LOG_FILE"; then
|
|
|
|
|
|
ok "初始化流程结束"
|
|
|
|
|
|
else
|
|
|
|
|
|
warn "onboard 返回了非零状态,可能部分步骤未完成。"
|
|
|
|
|
|
hint "可稍后单独重试:openclaw onboard --install-daemon"
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
warn "已跳过 onboard,daemon 可能未安装。"
|
|
|
|
|
|
hint "稍后请手动执行:openclaw onboard --install-daemon"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
info "启动 gateway…"
|
|
|
|
|
|
if ! run_quiet openclaw gateway start; then
|
|
|
|
|
|
warn "openclaw gateway start 返回失败,稍后统一检查运行状态。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
info "等待服务就绪(最多约 20 秒)…"
|
|
|
|
|
|
local i
|
|
|
|
|
|
for i in 1 2 3 4 5 6 7 8 9 10; do
|
|
|
|
|
|
if is_running; then break; fi
|
|
|
|
|
|
sleep 2
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
info "运行状态(openclaw status):"
|
|
|
|
|
|
local status_out
|
|
|
|
|
|
status_out="$(openclaw status 2>&1)"
|
|
|
|
|
|
_log "STATUS OUTPUT: $status_out"
|
|
|
|
|
|
printf '%s\n' "$status_out" | sed 's/^/ /' | head -n 30
|
|
|
|
|
|
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
if is_running; then
|
|
|
|
|
|
ok "OpenClaw gateway 正在运行。"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
err "没有检测到运行中的 OpenClaw gateway 进程。"
|
|
|
|
|
|
hint "按顺序排查:"
|
|
|
|
|
|
plain " 1) 手动启动看报错:openclaw gateway start"
|
|
|
|
|
|
plain " 2) 看服务日志: journalctl -u openclaw -n 50 --no-pager"
|
|
|
|
|
|
plain " 3) 看安装日志: tail -n 50 ${LOG_FILE}"
|
|
|
|
|
|
plain " 4) 确认配置正确: ${OPENCLAW_CONFIG}"
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 8. 结尾摘要
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
summary() {
|
|
|
|
|
|
local bin_path npm_prefix webui
|
|
|
|
|
|
bin_path="$(command -v openclaw 2>/dev/null || echo '未找到')"
|
|
|
|
|
|
npm_prefix="$(npm prefix -g 2>/dev/null || echo '未知')"
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试从配置里读出 WebUI 端口(读不到就给默认说明)
|
|
|
|
|
|
webui=""
|
|
|
|
|
|
if [ -f "$OPENCLAW_CONFIG" ]; then
|
|
|
|
|
|
webui="$(OC_CFG_PATH="$OPENCLAW_CONFIG" python3 -c '
|
|
|
|
|
|
import json,os
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(os.environ["OC_CFG_PATH"],encoding="utf-8") as f:
|
|
|
|
|
|
c=json.load(f)
|
|
|
|
|
|
g=c.get("gateway") if isinstance(c.get("gateway"),dict) else {}
|
|
|
|
|
|
p=g.get("port") or c.get("port")
|
|
|
|
|
|
print(p if p else "")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
print("")
|
|
|
|
|
|
' 2>/dev/null)"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
title "安装完成"
|
|
|
|
|
|
plain "${C_BOLD}安装位置${C_RESET}"
|
|
|
|
|
|
plain " openclaw 命令: ${bin_path}"
|
|
|
|
|
|
plain " npm 全局目录: ${npm_prefix}"
|
|
|
|
|
|
plain " 版本: $(openclaw_local_version 2>/dev/null || echo '未知')"
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
plain "${C_BOLD}配置文件${C_RESET}"
|
|
|
|
|
|
plain " ${OPENCLAW_CONFIG}"
|
|
|
|
|
|
plain " 备份文件: ${OPENCLAW_CONFIG}.bak.<时间戳>(如有改动)"
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
plain "${C_BOLD}常用命令${C_RESET}"
|
|
|
|
|
|
plain " 启动: openclaw gateway start"
|
|
|
|
|
|
plain " 停止: openclaw gateway stop"
|
|
|
|
|
|
plain " 重启: openclaw gateway stop && openclaw gateway start"
|
|
|
|
|
|
plain " 状态: openclaw status"
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
plain "${C_BOLD}查看日志${C_RESET}"
|
|
|
|
|
|
plain " 服务日志: journalctl -u openclaw -f"
|
|
|
|
|
|
plain " 安装日志: ${LOG_FILE}"
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
plain "${C_BOLD}WebUI${C_RESET}"
|
|
|
|
|
|
if [ -n "$webui" ]; then
|
|
|
|
|
|
plain " http://<你的服务器IP>:${webui}"
|
|
|
|
|
|
plain " ${C_YELLOW}注意${C_RESET}:如需公网访问,请确认防火墙规则并务必启用鉴权。"
|
|
|
|
|
|
else
|
|
|
|
|
|
plain " 未在配置中读到端口。若已启用 WebUI,地址形如 http://<你的服务器IP>:<端口>"
|
|
|
|
|
|
plain " 可用 openclaw status 或配置文件确认端口。"
|
|
|
|
|
|
fi
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
plain "${C_RED}${C_BOLD}安全提醒${C_RESET}"
|
|
|
|
|
|
plain " ${OPENCLAW_CONFIG} 中含有 Bot Token 与 API Key(已设为 600 权限)。"
|
|
|
|
|
|
plain " ${C_BOLD}请勿把该文件、其备份或截图发给任何人${C_RESET},也不要提交到 Git 仓库。"
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
plain "本脚本不含任何埋点、统计或数据上报。"
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# main
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
main() {
|
|
|
|
|
|
init_log "$@"
|
|
|
|
|
|
|
|
|
|
|
|
plain ""
|
|
|
|
|
|
plain "${C_BOLD}OpenClaw 一键安装配置脚本 v${SCRIPT_VERSION}${C_RESET}"
|
|
|
|
|
|
plain "日志:${LOG_FILE}"
|
|
|
|
|
|
|
|
|
|
|
|
title "环境检查"
|
|
|
|
|
|
check_root
|
|
|
|
|
|
detect_os
|
|
|
|
|
|
detect_arch
|
|
|
|
|
|
check_memory
|
|
|
|
|
|
|
|
|
|
|
|
install_base_deps
|
|
|
|
|
|
install_node
|
|
|
|
|
|
install_openclaw
|
|
|
|
|
|
|
|
|
|
|
|
resolve_home
|
|
|
|
|
|
detect_config_path
|
|
|
|
|
|
|
|
|
|
|
|
wizard
|
|
|
|
|
|
write_config || warn "配置未完整写入,请按上面的提示处理后再重跑本脚本。"
|
|
|
|
|
|
|
|
|
|
|
|
onboard_and_start
|
|
|
|
|
|
local start_rc=$?
|
|
|
|
|
|
|
|
|
|
|
|
summary
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$start_rc" -eq 0 ]; then
|
|
|
|
|
|
ok "全部完成,祝使用愉快。"
|
|
|
|
|
|
exit 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
warn "安装已完成,但服务未确认启动,请按上面的排查步骤处理。"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main "$@"
|