Files
openclaw-installer/install.sh
T

990 lines
37 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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="" # 稍后探测
PKG_MGR=""
PLATFORM=""
IS_WSL="0"
CFG_PNAME=""
CFG_DEFMODEL=""
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() {
PLATFORM="unknown"; IS_WSL="0"; PKG_MGR=""
# 原生 WindowsGit Bash / MSYS / Cygwin
case "${OSTYPE:-}" in
msys*|cygwin*|win32*) PLATFORM="windows" ;;
esac
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"
}
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} MBswap ${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() {
title "安装 Node.js 与 OpenClaw(官方安装器)"
info "调用官方安装器 https://openclaw.ai/install.sh"
info "它会自动处理 Node.jsmacOS 用 HomebrewLinux 用 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
if command -v node >/dev/null 2>&1; then
ok "Node.js $(node -v) 就绪"
else
die "Node.js 安装后仍找不到 node 命令。" \
"请打开一个新终端后重试,或手动安装 Node 22.22.3+ / 24.15+ / 26。"
fi
}
install_openclaw() {
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() {
title "校验 OpenClaw"
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.org2) 内存不足请加 swap3) 查看详细报错 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 ""
plain "${C_BOLD}[3/3] AI 模型服务(API 配置)${C_RESET}"
plain " 1) OpenAI 官方"
plain " 2) Anthropic 官方(Claude"
plain " 3) DeepSeek"
plain " 4) 硅基流动 SiliconFlow"
plain " 5) OpenRouter"
plain " 6) 自定义 OpenAI 兼容接口(中转站/自建,手填 baseURL"
plain " 7) 稍后手动配置"
local choice
while true; do
ask " 请输入编号" "7"
choice="$REPLY_VALUE"
case "$choice" in
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 之间的编号。" ;;
esac
done
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},不确定是否可用,配置仍会写入。" ;;
esac
fi
fi
ok " 模型配置已记录:${CFG_PNAME} / ${CFG_MODEL}"
}
# ---------------------------------------------------------------------------
# 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 "已跳过 onboarddaemon 可能未安装。"
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 "$@"