#!/bin/bash # # ╔═══════════════════════════════════════════════════════════════════════════╗ # ║ ║ # ║ 🦞 OpenClaw 一键部署脚本 v1.0.5 ║ # ║ 🔥 大圣之怒傻瓜Openclaw安装&配置助手 ║ # ║ 智能 AI 助手部署工具 - 支持多平台多模型 ║ # ║ ║ # ║ GitHub: https://github.com/leecyno1/auto-install-Openclaw ║ # ║ 官方文档: https://docs.openclaw.ai ║ # ║ ║ # ╚═══════════════════════════════════════════════════════════════════════════╝ # # 使用方法: # curl -fsSL https://raw.githubusercontent.com/leecyno1/auto-install-Openclaw/main/install.sh | bash # 或本地执行: chmod +x install.sh && ./install.sh # set -e # ================================ TTY 检测 ================================ # 当通过 curl | bash 运行时,stdin 是管道,需要优先选择可读输入源 resolve_tty_input() { if [ -t 0 ]; then echo "/dev/stdin" return 0 fi if [ -e /dev/tty ] && ( : < /dev/tty ) 2>/dev/null; then echo "/dev/tty" return 0 fi if [ -r /dev/stdin ]; then echo "/dev/stdin" return 0 fi echo "/dev/null" } TTY_INPUT="$(resolve_tty_input)" # ================================ 颜色定义 ================================ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' WHITE='\033[1;37m' GRAY='\033[0;90m' NC='\033[0m' # 无颜色 # ================================ 配置变量 ================================ # 兼容旧环境变量命名(clawdbot -> openclaw) map_legacy_env() { local new_key="$1" local legacy_key="$2" if [ -z "${!new_key:-}" ] && [ -n "${!legacy_key:-}" ]; then export "$new_key=${!legacy_key}" fi } map_legacy_env "OPENCLAW_NO_ONBOARD" "CLAWDBOT_NO_ONBOARD" map_legacy_env "OPENCLAW_NO_PROMPT" "CLAWDBOT_NO_PROMPT" map_legacy_env "OPENCLAW_DRY_RUN" "CLAWDBOT_DRY_RUN" map_legacy_env "OPENCLAW_INSTALL_METHOD" "CLAWDBOT_INSTALL_METHOD" map_legacy_env "OPENCLAW_VERSION" "CLAWDBOT_VERSION" map_legacy_env "OPENCLAW_BETA" "CLAWDBOT_BETA" map_legacy_env "OPENCLAW_GIT_DIR" "CLAWDBOT_GIT_DIR" map_legacy_env "OPENCLAW_GIT_UPDATE" "CLAWDBOT_GIT_UPDATE" map_legacy_env "OPENCLAW_VERBOSE" "CLAWDBOT_VERBOSE" OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}" CONFIG_DIR="$HOME/.openclaw" MIN_NODE_MAJOR=22 MIN_NODE_MINOR=12 INSTALLER_NAME="auto-install-Openclaw" INSTALLER_VERSION="1.0.5" GITHUB_REPO="${GITHUB_REPO:-leecyno1/auto-install-Openclaw}" GITHUB_RAW_URL="https://raw.githubusercontent.com/$GITHUB_REPO/main" OFFICIAL_INSTALL_URL="https://openclaw.ai/install.sh" OFFICIAL_DOCS_URL="https://docs.openclaw.ai" INSTALLER_MIRROR_RAW_URL="${OPENCLAW_INSTALLER_MIRROR_RAW_URL:-https://mirror.ghproxy.com/${GITHUB_RAW_URL}}" OFFICIAL_INSTALL_MIRROR_URL="${OPENCLAW_OFFICIAL_INSTALL_MIRROR_URL:-}" CURL_CONNECT_TIMEOUT="${OPENCLAW_CURL_CONNECT_TIMEOUT:-8}" CURL_MAX_TIME="${OPENCLAW_CURL_MAX_TIME:-30}" GATEWAY_HOST="${OPENCLAW_GATEWAY_HOST:-127.0.0.1}" GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-13145}" AUTO_SWAP_ENABLE="${OPENCLAW_AUTO_SWAP:-1}" SWAP_PERSIST_ENABLE="${OPENCLAW_SWAP_PERSIST:-1}" SWAP_THRESHOLD_MB="${OPENCLAW_SWAP_THRESHOLD_MB:-4096}" SWAP_TARGET_MB="${OPENCLAW_SWAP_TARGET_MB:-0}" SWAP_FILE_BASE="${OPENCLAW_SWAP_FILE:-/swapfile.openclaw}" AUTO_FIX_ATTEMPTED=0 NO_ONBOARD="${OPENCLAW_NO_ONBOARD:-0}" NO_PROMPT="${OPENCLAW_NO_PROMPT:-0}" DRY_RUN="${OPENCLAW_DRY_RUN:-0}" VERBOSE="${OPENCLAW_VERBOSE:-0}" INSTALL_METHOD="${OPENCLAW_INSTALL_METHOD:-npm}" USE_BETA="${OPENCLAW_BETA:-0}" GIT_DIR="${OPENCLAW_GIT_DIR:-$HOME/openclaw}" GIT_UPDATE="${OPENCLAW_GIT_UPDATE:-1}" HELP=0 # ================================ 工具函数 ================================ print_banner() { echo -e "${CYAN}" cat << 'EOF' __ __ ___ _ _ _ ________ __ _______ _ _ ______ __ | \/ |/ _ \| \ | | |/ / ____| / /| _ \|_ _| \ | | ____| / / | \ / | | | | \| | ' /| |__ / /_| |_) | | | | \| | |__ / / | |\/| | | | | . ` | < | __| | '_ \ _ < | | | . ` | __| / / | | | | |_| | |\ | . \| |____| (_) | |_) |_| |_| |\ | |___/ / |_| |_|\___/|_| \_|_|\_\______|\___/|____/|_____|_| \_|______/_/ MONKEY'S-FURY EOF echo -e "${NC}" echo -e "${CYAN}🔥 大圣之怒傻瓜Openclaw安装&配置助手 🔥${NC}" echo -e "${CYAN}🔖 Version: v${INSTALLER_VERSION}${NC}" echo "" } print_exit_hint() { local exit_code="${1:-0}" echo "" if [ "$exit_code" -eq 0 ]; then echo -e "${GREEN}安装脚本执行结束。${NC}" else echo -e "${YELLOW}安装脚本提前退出(状态码: ${exit_code})。${NC}" fi echo -e "${CYAN}后续可执行命令:${NC}" echo " source ~/.openclaw/env && openclaw doctor" echo " source ~/.openclaw/env && openclaw models status --probe --check" echo " bash ~/.openclaw/config-menu.sh # 或 bash ./config-menu.sh" echo "" } log_info() { echo -e "${GREEN}[INFO]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } log_step() { echo -e "${BLUE}[STEP]${NC} $1" } run_auto_fix_once() { if [ "$AUTO_FIX_ATTEMPTED" -ge 1 ]; then log_warn "自动修复已执行过一次,跳过再次修复。" return 1 fi AUTO_FIX_ATTEMPTED=1 log_warn "检测到异常,尝试执行一次自动修复..." if check_command openclaw; then local repair_log repair_log="$(mktemp /tmp/openclaw-auto-fix.XXXXXX.log)" if openclaw doctor --help 2>/dev/null | grep -q -- "--non-interactive"; then set +e openclaw doctor --non-interactive >"$repair_log" 2>&1 local repair_exit=$? set -e if [ $repair_exit -eq 0 ]; then log_info "自动修复成功(openclaw doctor --non-interactive)" return 0 fi fi set +e yes | openclaw doctor --fix >"$repair_log" 2>&1 local repair_exit=$? set -e if [ $repair_exit -eq 0 ]; then log_info "自动修复成功(openclaw doctor --fix)" return 0 fi tail -n 30 "$repair_log" 2>/dev/null || true fi if check_command npm; then set +e npm cache verify >/tmp/openclaw-npm-cache-verify.log 2>&1 local cache_exit=$? set -e if [ $cache_exit -eq 0 ]; then log_info "已执行 npm cache verify,准备重试失败步骤。" return 0 fi fi log_warn "自动修复未生效。" return 1 } run_step_with_auto_fix() { local step_name="$1" shift set +e "$@" local step_exit=$? set -e if [ $step_exit -eq 0 ]; then return 0 fi log_warn "${step_name} 失败(exit=${step_exit}),将执行一次自动修复并重试。" if run_auto_fix_once; then set +e "$@" step_exit=$? set -e if [ $step_exit -eq 0 ]; then log_info "${step_name} 重试成功。" return 0 fi log_error "${step_name} 重试后仍失败(exit=${step_exit})。" fi return $step_exit } download_with_fallback() { local output_path="$1" shift local url="" for url in "$@"; do [ -z "$url" ] && continue if curl -fsSL --proto '=https' --tlsv1.2 --connect-timeout "$CURL_CONNECT_TIMEOUT" --max-time "$CURL_MAX_TIME" "$url" -o "$output_path"; then log_info "下载成功: $url" return 0 fi log_warn "下载失败: $url" done return 1 } spinner() { local pid=$1 local delay=0.1 local spinstr='|/-\' while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do local temp=${spinstr#?} printf " [%c] " "$spinstr" local spinstr=$temp${spinstr%"$temp"} sleep $delay printf "\b\b\b\b\b\b" done printf " \b\b\b\b" } print_usage() { cat < 指定 OpenClaw 版本 (默认: latest) --beta 优先使用 beta dist-tag --git-dir, --dir git 安装目录 (默认: ~/openclaw) --no-git-update 禁止更新已有 git checkout --no-onboard 跳过本脚本 AI 初始化向导 --onboard 强制执行本脚本 AI 初始化向导 --no-prompt 非交互模式(使用默认值) --dry-run 只显示执行计划,不做变更 --verbose 详细日志 --gateway-host Gateway 监听地址 (默认: 127.0.0.1) --gateway-port Gateway 监听端口 (默认: 13145) --help, -h 显示帮助 环境变量: OPENCLAW_INSTALL_METHOD=git|npm OPENCLAW_VERSION=latest|next| OPENCLAW_BETA=0|1 OPENCLAW_GIT_DIR= OPENCLAW_GIT_UPDATE=0|1 OPENCLAW_NO_ONBOARD=0|1 OPENCLAW_NO_PROMPT=0|1 OPENCLAW_DRY_RUN=0|1 OPENCLAW_VERBOSE=0|1 OPENCLAW_INSTALLER_MIRROR_RAW_URL= OPENCLAW_OFFICIAL_INSTALL_MIRROR_URL= OPENCLAW_CURL_CONNECT_TIMEOUT= OPENCLAW_CURL_MAX_TIME= OPENCLAW_GATEWAY_HOST=<默认127.0.0.1> OPENCLAW_GATEWAY_PORT=<默认13145> OPENCLAW_AUTO_SWAP=0|1 OPENCLAW_SWAP_PERSIST=0|1 OPENCLAW_SWAP_THRESHOLD_MB=<默认4096> OPENCLAW_SWAP_TARGET_MB=<默认自动(2G或4G)> OPENCLAW_SWAP_FILE= EOF } parse_args() { while [ $# -gt 0 ]; do case "$1" in --install-method|--method) INSTALL_METHOD="$2" shift 2 ;; --npm) INSTALL_METHOD="npm" shift ;; --git|--github) INSTALL_METHOD="git" shift ;; --version) OPENCLAW_VERSION="$2" shift 2 ;; --beta) USE_BETA=1 shift ;; --git-dir|--dir) GIT_DIR="$2" shift 2 ;; --no-git-update) GIT_UPDATE=0 shift ;; --no-onboard) NO_ONBOARD=1 shift ;; --onboard) NO_ONBOARD=0 shift ;; --no-prompt) NO_PROMPT=1 shift ;; --dry-run) DRY_RUN=1 shift ;; --verbose) VERBOSE=1 shift ;; --gateway-host) GATEWAY_HOST="$2" shift 2 ;; --gateway-port) GATEWAY_PORT="$2" shift 2 ;; --help|-h) HELP=1 shift ;; *) echo "忽略未知参数: $1" shift ;; esac done } # 从 TTY 读取用户输入(支持 curl | bash 模式) read_input() { local prompt="$1" local var_name="$2" echo -en "$prompt" read $var_name < "$TTY_INPUT" } # 从 TTY 读取敏感输入(默认不回显) read_secret_input() { local prompt="$1" local var_name="$2" echo -e "${GRAY}(自动隐藏,直接粘贴后回车即可)${NC}" echo -en "$prompt" if stty -echo < "$TTY_INPUT" 2>/dev/null; then read $var_name < "$TTY_INPUT" stty echo < "$TTY_INPUT" 2>/dev/null || true else read $var_name < "$TTY_INPUT" fi echo "" } confirm() { local message="$1" local default="${2:-y}" if [ "$NO_PROMPT" = "1" ] || [ "$TTY_INPUT" = "/dev/null" ]; then [ "$default" = "y" ] return $? fi if [ "$default" = "y" ]; then local prompt="[Y/n]" else local prompt="[y/N]" fi echo -en "${YELLOW}$message $prompt: ${NC}" read response < "$TTY_INPUT" response=${response:-$default} case "$response" in [yY][eE][sS]|[yY]) return 0 ;; *) return 1 ;; esac } resolve_beta_version() { npm view openclaw dist-tags.beta 2>/dev/null || true } normalize_install_options() { if [ "$INSTALL_METHOD" != "npm" ] && [ "$INSTALL_METHOD" != "git" ]; then log_error "无效安装方式: $INSTALL_METHOD(仅支持 npm|git)" exit 2 fi if [ "$USE_BETA" = "1" ]; then local beta_version beta_version="$(resolve_beta_version)" if [ -n "$beta_version" ] && [ "$beta_version" != "undefined" ] && [ "$beta_version" != "null" ]; then OPENCLAW_VERSION="$beta_version" log_info "检测到 beta 版本: $OPENCLAW_VERSION" else log_warn "未找到 beta dist-tag,回退 latest" OPENCLAW_VERSION="latest" fi fi # 规范化 Gateway 地址参数,默认绑定 127.0.0.1:13145 if [ -z "$GATEWAY_HOST" ]; then GATEWAY_HOST="127.0.0.1" fi if ! [[ "$GATEWAY_PORT" =~ ^[0-9]+$ ]] || [ "$GATEWAY_PORT" -lt 1 ] || [ "$GATEWAY_PORT" -gt 65535 ]; then log_warn "无效 gateway 端口: $GATEWAY_PORT,回退到默认 13145" GATEWAY_PORT="13145" fi export OPENCLAW_GATEWAY_HOST="$GATEWAY_HOST" export OPENCLAW_GATEWAY_PORT="$GATEWAY_PORT" } print_install_plan() { echo "" echo -e "${CYAN}安装计划:${NC}" echo " - installer: $INSTALLER_NAME" echo " - install_method: $INSTALL_METHOD" echo " - openclaw_version: $OPENCLAW_VERSION" echo " - no_onboard: $NO_ONBOARD" echo " - no_prompt: $NO_PROMPT" echo " - dry_run: $DRY_RUN" echo " - verbose: $VERBOSE" echo " - gateway_host: $GATEWAY_HOST" echo " - gateway_port: $GATEWAY_PORT" if [ "$INSTALL_METHOD" = "git" ]; then echo " - git_dir: $GIT_DIR" echo " - git_update: $GIT_UPDATE" fi } # ================================ 系统检测 ================================ detect_os() { log_step "检测操作系统..." if [[ "$OSTYPE" == "linux-gnu"* ]]; then if [ -f /etc/os-release ]; then . /etc/os-release OS=$ID OS_VERSION=$VERSION_ID fi PACKAGE_MANAGER="" if command -v apt-get &> /dev/null; then PACKAGE_MANAGER="apt" elif command -v yum &> /dev/null; then PACKAGE_MANAGER="yum" elif command -v dnf &> /dev/null; then PACKAGE_MANAGER="dnf" elif command -v pacman &> /dev/null; then PACKAGE_MANAGER="pacman" fi log_info "检测到 Linux 系统: $OS $OS_VERSION (包管理器: $PACKAGE_MANAGER)" elif [[ "$OSTYPE" == "darwin"* ]]; then OS="macos" OS_VERSION=$(sw_vers -productVersion) PACKAGE_MANAGER="brew" log_info "检测到 macOS 系统: $OS_VERSION" elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then OS="windows" log_info "检测到 Windows 系统 (Git Bash/Cygwin)" else log_error "不支持的操作系统: $OSTYPE" exit 1 fi } check_root() { if [[ $EUID -eq 0 ]]; then log_warn "检测到以 root 用户运行" if ! confirm "建议使用普通用户运行,是否继续?" "y"; then exit 1 fi fi } ensure_sudo_privileges() { # root 用户无需 sudo if [[ $EUID -eq 0 ]]; then return 0 fi # Linux 下依赖安装和 systemd 操作需要 sudo if [[ "$OS" != "macos" ]]; then if ! check_command sudo; then log_error "未检测到 sudo,无法安装系统依赖。请安装 sudo 或使用 root 运行。" exit 1 fi log_step "检查并请求 sudo 权限..." if ! sudo -v; then log_error "sudo 授权失败,安装已中止。" echo -e "${YELLOW}请确认当前用户在 sudoers 中,或改用 root 运行。${NC}" exit 1 fi # 保持 sudo 会话,避免中途过期导致命令失败 ( while true; do sudo -n true 2>/dev/null || exit 0 sleep 50 done ) & SUDO_KEEPALIVE_PID=$! trap 'if [ -n "${SUDO_KEEPALIVE_PID:-}" ]; then kill "${SUDO_KEEPALIVE_PID}" 2>/dev/null || true; fi' EXIT log_info "sudo 权限已就绪" fi } # ================================ 依赖检查与安装 ================================ check_command() { command -v "$1" &> /dev/null } get_gateway_pid() { get_port_pid "$GATEWAY_PORT" } get_port_pid() { local port="$1" local pid="" if check_command lsof; then pid=$(lsof -ti :"$port" 2>/dev/null | head -1) fi if [ -z "$pid" ] && check_command pgrep; then pid=$(pgrep -f "openclaw gateway" 2>/dev/null | head -1) fi echo "$pid" } install_homebrew() { if ! check_command brew; then log_step "安装 Homebrew..." /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # 添加到 PATH if [[ -f /opt/homebrew/bin/brew ]]; then eval "$(/opt/homebrew/bin/brew shellenv)" elif [[ -f /usr/local/bin/brew ]]; then eval "$(/usr/local/bin/brew shellenv)" fi fi } install_nodejs() { log_step "检查 Node.js..." if check_command node; then local node_major local node_minor node_major=$(node -v | sed 's/^v//' | cut -d'.' -f1) node_minor=$(node -v | sed 's/^v//' | cut -d'.' -f2) if [ "$node_major" -gt "$MIN_NODE_MAJOR" ] || { [ "$node_major" -eq "$MIN_NODE_MAJOR" ] && [ "$node_minor" -ge "$MIN_NODE_MINOR" ]; }; then log_info "Node.js 版本满足要求: $(node -v)" return 0 else log_warn "Node.js 版本过低: $(node -v),需要 v${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}+" fi fi log_step "安装 Node.js ${MIN_NODE_MAJOR}.x ..." case "$OS" in macos) install_homebrew brew install node@22 brew link --overwrite node@22 ;; ubuntu|debian) curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs ;; centos|rhel|fedora) curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash - sudo yum install -y nodejs ;; arch|manjaro) sudo pacman -S nodejs npm --noconfirm ;; *) log_error "无法自动安装 Node.js,请手动安装 v${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}+" exit 1 ;; esac log_info "Node.js 安装完成: $(node -v)" } install_git() { if ! check_command git; then log_step "安装 Git..." case "$OS" in macos) install_homebrew brew install git ;; ubuntu|debian) sudo apt-get update && sudo apt-get install -y git ;; centos|rhel|fedora) sudo yum install -y git ;; arch|manjaro) sudo pacman -S git --noconfirm ;; esac fi log_info "Git 版本: $(git --version)" } install_dependencies() { log_step "检查并安装依赖..." # 安装基础依赖 case "$OS" in ubuntu|debian) sudo apt-get update sudo apt-get install -y curl wget jq ;; centos|rhel|fedora) sudo yum install -y curl wget jq ;; macos) install_homebrew brew install curl wget jq ;; esac install_git install_nodejs } # ================================ OpenClaw 安装 ================================ create_directories() { log_step "创建配置目录..." mkdir -p "$CONFIG_DIR" log_info "配置目录: $CONFIG_DIR" } install_openclaw_via_official() { local -a args args=(--install-method "$INSTALL_METHOD" --no-onboard) if [ "$NO_PROMPT" = "1" ]; then args+=(--no-prompt) fi if [ "$VERBOSE" = "1" ]; then args+=(--verbose) fi if [ "$DRY_RUN" = "1" ]; then args+=(--dry-run) fi if [ "$USE_BETA" = "1" ]; then args+=(--beta) elif [ -n "$OPENCLAW_VERSION" ] && [ "$OPENCLAW_VERSION" != "latest" ]; then args+=(--version "$OPENCLAW_VERSION") fi if [ "$INSTALL_METHOD" = "git" ]; then args+=(--git-dir "$GIT_DIR") if [ "$GIT_UPDATE" = "0" ]; then args+=(--no-git-update) fi fi log_info "调用官方安装器以确保核心安装行为与上游一致..." local tmp_script tmp_script="$(mktemp /tmp/openclaw-install.XXXXXX.sh)" if ! download_with_fallback "$tmp_script" "$OFFICIAL_INSTALL_URL" "$OFFICIAL_INSTALL_MIRROR_URL"; then rm -f "$tmp_script" 2>/dev/null || true return 1 fi bash "$tmp_script" "${args[@]}" local install_exit=$? rm -f "$tmp_script" 2>/dev/null || true return "$install_exit" } ensure_openclaw_on_path() { # 尝试从常见 npm 全局安装位置补充 PATH,避免“已安装但当前 shell 不可见” local npm_prefix="" local npm_bin="" local candidate="" if check_command npm; then npm_prefix="$(npm config get prefix 2>/dev/null || true)" if [ -n "$npm_prefix" ] && [ "$npm_prefix" != "undefined" ] && [ "$npm_prefix" != "null" ]; then npm_bin="$npm_prefix/bin" if [ -d "$npm_bin" ]; then case ":$PATH:" in *":$npm_bin:"*) ;; *) export PATH="$npm_bin:$PATH" ;; esac fi fi fi for candidate in "$HOME/.npm-global/bin" "$HOME/.local/bin" "/usr/local/bin" "/usr/bin"; do if [ -d "$candidate" ]; then case ":$PATH:" in *":$candidate:"*) ;; *) export PATH="$candidate:$PATH" ;; esac fi done } run_as_root() { if [ "$EUID" -eq 0 ]; then "$@" else sudo "$@" fi } get_meminfo_kb() { local key="$1" awk -v k="$key" '$1==k":" {print $2; exit}' /proc/meminfo 2>/dev/null } get_total_mem_mb() { local kb kb="$(get_meminfo_kb MemTotal)" [ -n "$kb" ] && echo $((kb / 1024)) || echo 0 } get_total_swap_mb() { local kb kb="$(get_meminfo_kb SwapTotal)" [ -n "$kb" ] && echo $((kb / 1024)) || echo 0 } is_low_memory_linux() { [ "$(uname -s 2>/dev/null || true)" = "Linux" ] || return 1 local mem_mb swap_mb target_swap_mb mem_mb="$(get_total_mem_mb)" swap_mb="$(get_total_swap_mb)" target_swap_mb="$(get_recommended_swap_mb "$mem_mb")" [ "$mem_mb" -lt "$SWAP_THRESHOLD_MB" ] && [ "$swap_mb" -lt "$target_swap_mb" ] } has_minimum_swap_for_low_memory() { [ "$(uname -s 2>/dev/null || true)" = "Linux" ] || return 0 local mem_mb swap_mb target_swap_mb mem_mb="$(get_total_mem_mb)" swap_mb="$(get_total_swap_mb)" target_swap_mb="$(get_recommended_swap_mb "$mem_mb")" if [ "$mem_mb" -ge "$SWAP_THRESHOLD_MB" ]; then return 0 fi [ "$swap_mb" -ge "$target_swap_mb" ] } get_recommended_swap_mb() { local mem_mb="${1:-0}" local override="${SWAP_TARGET_MB:-0}" if [ "$override" -gt 0 ] 2>/dev/null; then echo "$override" return 0 fi # 默认策略:<2G 配 4G swap;2G~4G 配 2G swap if [ "$mem_mb" -lt 2048 ]; then echo 4096 else echo 2048 fi } create_and_enable_swapfile() { local swap_file="$1" local swap_size_mb="$2" if swapon --show=NAME --noheadings 2>/dev/null | grep -qx "$swap_file"; then return 0 fi if [ ! -f "$swap_file" ]; then if check_command fallocate; then if ! run_as_root fallocate -l "${swap_size_mb}M" "$swap_file"; then run_as_root dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" status=none fi else run_as_root dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" status=none fi fi run_as_root chmod 600 "$swap_file" run_as_root mkswap "$swap_file" >/dev/null 2>&1 || true run_as_root swapon "$swap_file" } persist_swapfile_entry() { local swap_file="$1" [ "$SWAP_PERSIST_ENABLE" = "1" ] || return 0 if [ ! -f "$swap_file" ]; then return 1 fi if [ ! -f /etc/fstab ]; then log_warn "未找到 /etc/fstab,无法持久化 Swap。" return 1 fi if awk -v f="$swap_file" '$1==f && $2=="none" && $3=="swap" {found=1} END{exit !found}' /etc/fstab 2>/dev/null; then log_info "Swap 已存在持久化配置: $swap_file" return 0 fi if printf "%s none swap sw 0 0\n" "$swap_file" | run_as_root tee -a /etc/fstab >/dev/null; then log_info "已写入 Swap 持久化: $swap_file -> /etc/fstab" return 0 fi log_warn "写入 /etc/fstab 失败,重启后需手动 swapon $swap_file" return 1 } ensure_swap_for_install() { is_low_memory_linux || return 0 local mem_mb swap_mb target_swap_mb missing_swap_mb local primary_swap_file extra_swap_file mem_mb="$(get_total_mem_mb)" swap_mb="$(get_total_swap_mb)" target_swap_mb="$(get_recommended_swap_mb "$mem_mb")" missing_swap_mb=$((target_swap_mb - swap_mb)) [ "$missing_swap_mb" -lt 0 ] && missing_swap_mb=0 if [ "$AUTO_SWAP_ENABLE" != "1" ]; then log_warn "检测到内存 ${mem_mb}MB (<${SWAP_THRESHOLD_MB}MB),但 OPENCLAW_AUTO_SWAP=0,跳过自动启用 Swap。" return 1 fi log_warn "检测到低内存环境(内存 ${mem_mb}MB,Swap ${swap_mb}MB)。" log_warn "将自动补齐 Swap 以降低 OOM 风险(目标 Swap: ${target_swap_mb}MB,推荐 2G~4G)。" if ! check_command swapon || ! check_command mkswap; then log_warn "系统缺少 swapon/mkswap,无法自动启用 Swap。" return 1 fi if [ "$missing_swap_mb" -le 0 ]; then log_info "当前 Swap 已满足低内存安装要求(${swap_mb}MB)" return 0 fi primary_swap_file="$SWAP_FILE_BASE" extra_swap_file="${SWAP_FILE_BASE}.extra" if ! swapon --show=NAME --noheadings 2>/dev/null | grep -qx "$primary_swap_file"; then local primary_size_mb="$target_swap_mb" if [ "$primary_size_mb" -gt 4096 ]; then primary_size_mb=4096 fi if [ "$primary_size_mb" -lt 1024 ]; then primary_size_mb=1024 fi if ! create_and_enable_swapfile "$primary_swap_file" "$primary_size_mb"; then log_warn "创建/启用 Swap 失败: $primary_swap_file" fi persist_swapfile_entry "$primary_swap_file" || true log_info "已启用 Swap: $primary_swap_file (${primary_size_mb}MB)" else log_info "检测到已启用 Swap: $primary_swap_file" persist_swapfile_entry "$primary_swap_file" || true fi swap_mb="$(get_total_swap_mb)" missing_swap_mb=$((target_swap_mb - swap_mb)) if [ "$missing_swap_mb" -gt 0 ]; then if [ "$missing_swap_mb" -lt 512 ]; then missing_swap_mb=512 fi if ! create_and_enable_swapfile "$extra_swap_file" "$missing_swap_mb"; then log_warn "补充 Swap 失败: $extra_swap_file" fi persist_swapfile_entry "$extra_swap_file" || true log_info "已补充 Swap: $extra_swap_file (${missing_swap_mb}MB)" fi log_info "当前总 Swap: $(get_total_swap_mb)MB" has_minimum_swap_for_low_memory return $? } is_oom_like_failure() { local exit_code="$1" local log_file="$2" if [ "$exit_code" -eq 137 ] || [ "$exit_code" -eq 143 ]; then return 0 fi if [ -f "$log_file" ] && grep -qiE "killed|out of memory|heap out of memory|cannot allocate memory|ENOMEM|oom" "$log_file"; then return 0 fi return 1 } npm_install_openclaw_with_fallback() { local spec="openclaw@$OPENCLAW_VERSION" local log1 log2 exit_code node_opts log_step "执行 npm 回退安装..." log1="$(mktemp /tmp/openclaw-npm-fallback.XXXXXX.log)" set +e env SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm_config_jobs=1 npm_config_maxsockets=1 npm_config_progress=false UV_THREADPOOL_SIZE=1 NODE_OPTIONS="${NODE_OPTIONS:-} --max-old-space-size=512" npm --loglevel error --no-fund --no-audit install -g "$spec" --unsafe-perm >"$log1" 2>&1 exit_code=$? set -e if [ $exit_code -eq 0 ]; then return 0 fi log_warn "npm 安装失败(第 1 次,exit=$exit_code)" tail -n 40 "$log1" 2>/dev/null || true if is_oom_like_failure "$exit_code" "$log1"; then log_warn "检测到疑似内存不足导致的安装失败,准备启用低内存保护后重试。" ensure_swap_for_install || true fi node_opts="--max-old-space-size=384" if [ -n "${NODE_OPTIONS:-}" ]; then node_opts="${NODE_OPTIONS} ${node_opts}" fi log2="$(mktemp /tmp/openclaw-npm-fallback.XXXXXX.log)" set +e env SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm_config_jobs=1 npm_config_maxsockets=1 npm_config_progress=false UV_THREADPOOL_SIZE=1 NODE_OPTIONS="$node_opts" npm --loglevel error --no-fund --no-audit install -g "$spec" --unsafe-perm >"$log2" 2>&1 exit_code=$? set -e if [ $exit_code -eq 0 ]; then log_info "npm 低内存模式重试成功。" return 0 fi log_error "npm 回退安装仍然失败(第 2 次,exit=$exit_code)" tail -n 80 "$log2" 2>/dev/null || true echo "" echo -e "${YELLOW}建议先手动启用 Swap 后重试(推荐 2G~4G):${NC}" echo " sudo fallocate -l 4G /swapfile.openclaw || sudo dd if=/dev/zero of=/swapfile.openclaw bs=1M count=4096" echo " sudo chmod 600 /swapfile.openclaw && sudo mkswap /swapfile.openclaw && sudo swapon /swapfile.openclaw" return 1 } resolve_openclaw_bin() { ensure_openclaw_on_path if check_command openclaw; then command -v openclaw return 0 fi if check_command claw; then command -v claw return 0 fi if check_command npm && check_command node; then local npm_root="" npm_root="$(npm root -g 2>/dev/null || true)" if [ -n "$npm_root" ]; then local pkg_json="$npm_root/openclaw/package.json" if [ -f "$pkg_json" ]; then local candidate candidate=$(node -e ' const fs=require("fs"); const path=require("path"); const pkg=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); const bin=(pkg.bin&& (pkg.bin.openclaw||pkg.bin.claw)) || ""; if (bin) process.stdout.write(path.resolve(path.dirname(process.argv[1]), bin)); ' "$pkg_json" 2>/dev/null || true) if [ -n "$candidate" ] && [ -f "$candidate" ]; then chmod +x "$candidate" 2>/dev/null || true echo "$candidate" return 0 fi fi fi fi return 1 } get_current_model_ref() { if ! check_command openclaw; then return 1 fi local model_ref="" if check_command node; then model_ref=$(openclaw models status --json 2>/dev/null | node -e ' const fs = require("fs"); try { const raw = fs.readFileSync(0, "utf8"); const data = JSON.parse(raw || "{}"); const v = (data.resolvedDefault || data.defaultModel || "").trim(); if (v) process.stdout.write(v); } catch {} ' 2>/dev/null || true) elif check_command python3; then model_ref=$(openclaw models status --json 2>/dev/null | python3 -c ' import json,sys try: d=json.load(sys.stdin) v=(d.get("resolvedDefault") or d.get("defaultModel") or "").strip() if v: print(v,end="") except Exception: pass ' 2>/dev/null || true) else model_ref=$(openclaw config get agents.defaults.model.primary 2>/dev/null || true) if [ -z "$model_ref" ] || [ "$model_ref" = "undefined" ]; then model_ref=$(openclaw config get models.default 2>/dev/null || true) fi fi [ -n "$model_ref" ] && [ "$model_ref" != "undefined" ] && echo "$model_ref" } install_openclaw() { log_step "安装 OpenClaw..." # 检查是否已安装 if check_command openclaw; then local current_version=$(openclaw --version 2>/dev/null || echo "unknown") log_warn "OpenClaw 已安装 (版本: $current_version)" if ! confirm "是否重新安装/更新?"; then init_openclaw_config return 0 fi fi # 低内存机器优先补齐 Swap,降低 npm 安装被 OOM Kill 的概率 local low_mem_mode=0 if is_low_memory_linux; then low_mem_mode=1 fi if [ "$low_mem_mode" -eq 1 ]; then log_warn "检测到低内存安装场景,将优先使用内存优化安装流程。" if ! ensure_swap_for_install; then local mem_mb swap_mb target_swap_mb mem_mb="$(get_total_mem_mb)" swap_mb="$(get_total_swap_mb)" target_swap_mb="$(get_recommended_swap_mb "$mem_mb")" log_error "当前内存 ${mem_mb}MB,Swap ${swap_mb}MB,低于建议目标 ${target_swap_mb}MB。" log_error "继续安装大概率被 OOM Killer 终止。" if [ "$NO_PROMPT" = "1" ] || ! confirm "是否仍要继续安装(不推荐)?" "n"; then echo "" echo -e "${YELLOW}请先启用 Swap 后重试:${NC}" echo " sudo fallocate -l 4G /swapfile.openclaw || sudo dd if=/dev/zero of=/swapfile.openclaw bs=1M count=4096" echo " sudo chmod 600 /swapfile.openclaw && sudo mkswap /swapfile.openclaw && sudo swapon /swapfile.openclaw" exit 1 fi fi if ! npm_install_openclaw_with_fallback; then log_error "内存优化安装流程失败" exit 1 fi else if ! install_openclaw_via_official; then if [ "$INSTALL_METHOD" != "npm" ]; then log_error "官方安装器执行失败,且当前为 git 安装模式,无法安全回退" exit 1 fi log_warn "官方安装器执行失败,回退到 npm 安装" if ! npm_install_openclaw_with_fallback; then log_error "OpenClaw 回退安装失败" exit 1 fi fi fi # 验证安装 local claw_bin="" claw_bin="$(resolve_openclaw_bin || true)" if [ -n "$claw_bin" ]; then local claw_dir claw_dir="$(dirname "$claw_bin")" case ":$PATH:" in *":$claw_dir:"*) ;; *) export PATH="$claw_dir:$PATH" ;; esac # 某些版本仅暴露 claw 命令;自动提供 openclaw shim if ! check_command openclaw && [ "$(basename "$claw_bin")" = "claw" ]; then local shim_dir="" local shim_target="" shim_dir="$(dirname "$claw_bin")" if [ -d "$shim_dir" ] && [ -w "$shim_dir" ]; then shim_target="$shim_dir/openclaw" else shim_dir="$HOME/.local/bin" mkdir -p "$shim_dir" 2>/dev/null || true shim_target="$shim_dir/openclaw" fi cat > "$shim_target" </dev/null || true case ":$PATH:" in *":$shim_dir:"*) ;; *) export PATH="$shim_dir:$PATH" ;; esac log_info "已创建 openclaw 命令兼容 shim: $shim_target" fi log_info "OpenClaw 安装成功: $("$claw_bin" --version 2>/dev/null || echo 'installed')" init_openclaw_config else log_error "OpenClaw 安装后未在当前 PATH 中发现可执行文件" if check_command npm; then local npm_prefix_hint npm_prefix_hint="$(npm config get prefix 2>/dev/null || true)" if [ -n "$npm_prefix_hint" ] && [ "$npm_prefix_hint" != "undefined" ] && [ "$npm_prefix_hint" != "null" ]; then echo -e "${YELLOW}可能的修复方式:${NC}" echo " export PATH=\"$npm_prefix_hint/bin:\$PATH\"" echo " hash -r" echo " command -v openclaw && openclaw --version" fi fi exit 1 fi } run_official_onboard() { if [ "$NO_PROMPT" = "1" ]; then log_info "NO_PROMPT 模式下跳过交互式官方向导,可稍后手动运行: openclaw onboard" return 0 fi if ! check_command openclaw; then log_error "未检测到 openclaw 命令,无法启动官方向导。" return 1 fi log_step "启动官方配置向导(openclaw onboard)..." if [ -e /dev/tty ]; then openclaw onboard < /dev/tty else openclaw onboard fi } apply_default_security_baseline() { if ! check_command openclaw; then log_warn "未检测到 openclaw,跳过默认安全权限配置。" return 0 fi log_step "应用默认基础权限(系统命令/文件访问/网络浏览)..." openclaw config set security.enable_shell_commands true >/dev/null 2>&1 || true openclaw config set security.enable_file_access true >/dev/null 2>&1 || true openclaw config set security.enable_web_browsing true >/dev/null 2>&1 || true log_info "默认基础权限已启用:system commands / file access / web browsing" } install_channel_assets() { local skill_dir="$CONFIG_DIR/skills/channel-setup-assistant" local skill_file="$skill_dir/SKILL.md" local docs_dir="$CONFIG_DIR/docs" local doc_file="$docs_dir/channels-configuration-guide.md" local script_dir script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local local_doc="$script_dir/docs/channels-configuration-guide.md" mkdir -p "$skill_dir" "$docs_dir" 2>/dev/null || true if [ -f "$local_doc" ]; then cp "$local_doc" "$doc_file" 2>/dev/null || true elif download_with_fallback "$doc_file.tmp" "$GITHUB_RAW_URL/docs/channels-configuration-guide.md" "$INSTALLER_MIRROR_RAW_URL/docs/channels-configuration-guide.md"; then mv "$doc_file.tmp" "$doc_file" else rm -f "$doc_file.tmp" 2>/dev/null || true cat > "$doc_file" <<'EOF' # OpenClaw 渠道配置文档(自动安装器) 请优先使用: 1) `openclaw onboard`(官方模型配置) 2) `bash ~/.openclaw/config-menu.sh`(渠道配置) 关键渠道推荐: - 飞书(官方):`@openclaw/feishu` - 企业微信(社区):`@marshulll/openclaw-wecom` - 微信(社区):`openclaw-wechat-channel` - QQ(社区):`@sliverp/qqbot` 完整文档请查看仓库 `docs/channels-configuration-guide.md`。 EOF fi cat > "$skill_file" <<'EOF' # OpenClaw 渠道配置助手 Skill 目标:当用户提供消息渠道信息时,交互式收集缺失参数,并执行命令行完成配置。 执行原则: 1. 先确认渠道类型(telegram/discord/slack/feishu/wecom/wechat/qq/others)。 2. 明确必填项,缺失项逐个询问,不一次性抛出过多字段。 3. 执行前回显将执行的命令,并让用户确认。 4. 执行后输出:成功/失败、下一步验证命令、常见排障命令。 标准命令: - 状态:`openclaw channels list` - 健康检查:`openclaw doctor --fix` - 重启:`openclaw gateway restart` 重点渠道字段: - Feishu: `appId`, `appSecret` - WeCom(bot): `token`, `encodingAESKey`, `receiveId` - WeCom(app): `corpId`, `corpSecret`, `agentId`, `callbackToken`, `callbackAesKey` - WeChatPad: `proxyUrl`, `apiKey`, `webhookHost`, `webhookPort`, `webhookPath` - QQ: `appId`, `appSecret`, `allowFrom` 配置完成后必须执行: 1) `openclaw doctor --fix` 2) `openclaw gateway restart` 3) `openclaw channels list` EOF chmod 644 "$skill_file" "$doc_file" 2>/dev/null || true log_info "已注入渠道配置文档与 Skill:" log_info " 文档: $doc_file" log_info " Skill: $skill_file" } # 初始化 OpenClaw 配置 init_openclaw_config() { log_step "初始化 OpenClaw 配置..." local OPENCLAW_DIR="$HOME/.openclaw" # 创建必要的目录 mkdir -p "$OPENCLAW_DIR/agents/main/sessions" mkdir -p "$OPENCLAW_DIR/agents/main/agent" mkdir -p "$OPENCLAW_DIR/credentials" # 修复权限 chmod 700 "$OPENCLAW_DIR" 2>/dev/null || true # 设置 gateway.mode 为 local if check_command openclaw; then openclaw config set gateway.mode local 2>/dev/null || true openclaw config set gateway.host "$GATEWAY_HOST" 2>/dev/null || true openclaw config set gateway.port "$GATEWAY_PORT" 2>/dev/null || true openclaw config set gateway.bind "$GATEWAY_HOST:$GATEWAY_PORT" 2>/dev/null || true log_info "Gateway 模式已设置为 local" # 检查 gateway.auth 配置,如果是 token 模式但没有 token,则自动生成 local auth_mode=$(openclaw config get gateway.auth 2>/dev/null) if [ "$auth_mode" = "token" ]; then local auth_token=$(openclaw config get gateway.auth.token 2>/dev/null) if [ -z "$auth_token" ] || [ "$auth_token" = "undefined" ]; then # 自动生成一个随机 token local new_token=$(openssl rand -hex 32 2>/dev/null || cat /dev/urandom | head -c 32 | xxd -p 2>/dev/null || date +%s%N | sha256sum | head -c 64) openclaw config set gateway.auth.token "$new_token" 2>/dev/null || true log_info "已自动生成 Gateway Auth Token" fi fi fi local env_file="$OPENCLAW_DIR/env" touch "$env_file" 2>/dev/null || true if ! grep -q '^# Gateway runtime defaults' "$env_file" 2>/dev/null; then { echo "" echo "# Gateway runtime defaults" } >> "$env_file" fi if grep -q '^export OPENCLAW_GATEWAY_HOST=' "$env_file" 2>/dev/null; then local tmp_env tmp_env="$(mktemp)" awk -v v="$GATEWAY_HOST" ' BEGIN { done=0 } /^export OPENCLAW_GATEWAY_HOST=/ { print "export OPENCLAW_GATEWAY_HOST=" v; done=1; next } { print } END { if (!done) print "export OPENCLAW_GATEWAY_HOST=" v } ' "$env_file" > "$tmp_env" && mv "$tmp_env" "$env_file" else echo "export OPENCLAW_GATEWAY_HOST=$GATEWAY_HOST" >> "$env_file" fi if grep -q '^export OPENCLAW_GATEWAY_PORT=' "$env_file" 2>/dev/null; then local tmp_env2 tmp_env2="$(mktemp)" awk -v v="$GATEWAY_PORT" ' BEGIN { done=0 } /^export OPENCLAW_GATEWAY_PORT=/ { print "export OPENCLAW_GATEWAY_PORT=" v; done=1; next } { print } END { if (!done) print "export OPENCLAW_GATEWAY_PORT=" v } ' "$env_file" > "$tmp_env2" && mv "$tmp_env2" "$env_file" else echo "export OPENCLAW_GATEWAY_PORT=$GATEWAY_PORT" >> "$env_file" fi chmod 600 "$env_file" 2>/dev/null || true } # 为 MiniMax 写入官方兼容 provider 配置,避免旧版本出现 Unknown model ensure_minimax_provider_config() { local provider="$1" # minimax|minimax-cn local model="$2" # MiniMax-M2.5 / MiniMax-M2.5-highspeed local config_file="$3" local base_url="https://api.minimax.io/anthropic" if [ "$provider" = "minimax-cn" ]; then base_url="https://api.minimaxi.com/anthropic" fi mkdir -p "$(dirname "$config_file")" 2>/dev/null || true [ -f "$config_file" ] || echo "{}" > "$config_file" if command -v node &> /dev/null; then node -e " const fs = require('fs'); const file = '$config_file'; const provider = '$provider'; const model = '$model'; const baseUrl = '$base_url'; let cfg = {}; try { cfg = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {} cfg.models ||= {}; cfg.models.mode ||= 'merge'; cfg.models.providers ||= {}; const p = cfg.models.providers[provider] || {}; const models = Array.isArray(p.models) ? p.models : []; const catalog = { 'MiniMax-M2.5': { name: 'MiniMax M2.5' }, 'MiniMax-M2.5-highspeed': { name: 'MiniMax M2.5 Highspeed' }, }; const modelIds = new Set(models.map(m => m.id)); for (const id of ['MiniMax-M2.5', 'MiniMax-M2.5-highspeed']) { if (!modelIds.has(id)) { models.push({ id, name: (catalog[id] && catalog[id].name) || id, reasoning: true, input: ['text'], cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0.12 }, contextWindow: 200000, maxTokens: 8192 }); } } cfg.models.providers[provider] = { ...p, baseUrl, api: 'anthropic-messages', authHeader: true, models }; cfg.agents ||= {}; cfg.agents.defaults ||= {}; cfg.agents.defaults.models ||= {}; const ref = provider + '/' + model; cfg.agents.defaults.models[ref] = { ...(cfg.agents.defaults.models[ref] || {}), alias: 'Minimax' }; fs.writeFileSync(file, JSON.stringify(cfg, null, 2)); " >/dev/null 2>&1 || true elif command -v python3 &> /dev/null; then python3 - < "$env_file" << EOF # OpenClaw 环境变量配置 # 由安装脚本自动生成: $(date '+%Y-%m-%d %H:%M:%S') EOF # 根据 AI_PROVIDER 设置对应的环境变量 case "$AI_PROVIDER" in anthropic) echo "export ANTHROPIC_API_KEY=$AI_KEY" >> "$env_file" [ -n "$BASE_URL" ] && echo "export ANTHROPIC_BASE_URL=$BASE_URL" >> "$env_file" ;; openai) echo "export OPENAI_API_KEY=$AI_KEY" >> "$env_file" [ -n "$BASE_URL" ] && echo "export OPENAI_BASE_URL=$BASE_URL" >> "$env_file" ;; deepseek) echo "export DEEPSEEK_API_KEY=$AI_KEY" >> "$env_file" echo "export DEEPSEEK_BASE_URL=${BASE_URL:-https://api.deepseek.com}" >> "$env_file" ;; moonshot|kimi) echo "export MOONSHOT_API_KEY=$AI_KEY" >> "$env_file" echo "export MOONSHOT_BASE_URL=${BASE_URL:-https://api.moonshot.ai/v1}" >> "$env_file" ;; google|google-gemini-cli|google-antigravity) echo "export GOOGLE_API_KEY=$AI_KEY" >> "$env_file" [ -n "$BASE_URL" ] && echo "export GOOGLE_BASE_URL=$BASE_URL" >> "$env_file" ;; groq) echo "export GROQ_API_KEY=$AI_KEY" >> "$env_file" echo "export GROQ_BASE_URL=${BASE_URL:-https://api.groq.com/openai/v1}" >> "$env_file" ;; mistral) echo "export MISTRAL_API_KEY=$AI_KEY" >> "$env_file" echo "export MISTRAL_BASE_URL=${BASE_URL:-https://api.mistral.ai/v1}" >> "$env_file" ;; openrouter) echo "export OPENROUTER_API_KEY=$AI_KEY" >> "$env_file" echo "export OPENROUTER_BASE_URL=${BASE_URL:-https://openrouter.ai/api/v1}" >> "$env_file" ;; ollama) echo "export OLLAMA_HOST=${BASE_URL:-http://localhost:11434}" >> "$env_file" ;; novita) echo "export NOVITA_API_KEY=$AI_KEY" >> "$env_file" echo "export NOVITA_BASE_URL=${BASE_URL:-https://api.novita.ai/openai}" >> "$env_file" ;; xai) echo "export XAI_API_KEY=$AI_KEY" >> "$env_file" ;; zai) echo "export ZAI_API_KEY=$AI_KEY" >> "$env_file" ;; minimax|minimax-cn) echo "export MINIMAX_API_KEY=$AI_KEY" >> "$env_file" ;; opencode|opencode-go) echo "export OPENCODE_API_KEY=$AI_KEY" >> "$env_file" ;; esac echo "export OPENCLAW_GATEWAY_HOST=$GATEWAY_HOST" >> "$env_file" echo "export OPENCLAW_GATEWAY_PORT=$GATEWAY_PORT" >> "$env_file" chmod 600 "$env_file" log_info "环境变量配置已保存到: $env_file" if [ "$AI_PROVIDER" = "minimax" ] || [ "$AI_PROVIDER" = "minimax-cn" ]; then ensure_minimax_provider_config "$AI_PROVIDER" "$AI_MODEL" "$openclaw_json" fi # 设置默认模型 if check_command openclaw; then local openclaw_model="" local use_custom_provider=false # 如果使用自定义 BASE_URL,需要配置自定义 provider if [ -n "$BASE_URL" ] && [ "$AI_PROVIDER" = "anthropic" ]; then use_custom_provider=true configure_custom_provider "$AI_PROVIDER" "$AI_KEY" "$AI_MODEL" "$BASE_URL" "$openclaw_json" openclaw_model="anthropic-custom/$AI_MODEL" elif [ -n "$BASE_URL" ] && [ "$AI_PROVIDER" = "openai" ]; then use_custom_provider=true # 传递 API 类型参数(如果已设置) configure_custom_provider "$AI_PROVIDER" "$AI_KEY" "$AI_MODEL" "$BASE_URL" "$openclaw_json" "$AI_API_TYPE" openclaw_model="openai-custom/$AI_MODEL" else case "$AI_PROVIDER" in anthropic) openclaw_model="anthropic/$AI_MODEL" ;; openai) openclaw_model="openai/$AI_MODEL" ;; groq) openclaw_model="groq/$AI_MODEL" ;; mistral) openclaw_model="mistral/$AI_MODEL" ;; deepseek) openclaw_model="deepseek/$AI_MODEL" ;; moonshot|kimi) openclaw_model="moonshot/$AI_MODEL" ;; openrouter) openclaw_model="openrouter/$AI_MODEL" ;; google) openclaw_model="google/$AI_MODEL" ;; google-gemini-cli) openclaw_model="google-gemini-cli/$AI_MODEL" ;; google-antigravity) openclaw_model="google-antigravity/$AI_MODEL" ;; ollama) openclaw_model="ollama/$AI_MODEL" ;; novita) openclaw_model="novita/$AI_MODEL" ;; xai) openclaw_model="xai/$AI_MODEL" ;; zai) openclaw_model="zai/$AI_MODEL" ;; minimax) openclaw_model="minimax/$AI_MODEL" ;; minimax-cn) openclaw_model="minimax-cn/$AI_MODEL" ;; opencode) openclaw_model="opencode/$AI_MODEL" ;; opencode-go) openclaw_model="opencode-go/$AI_MODEL" ;; esac fi if [ -n "$openclaw_model" ]; then # 加载环境变量 source "$env_file" # 设置默认模型(显示错误信息以便调试) local set_result local set_exit=0 if set_result=$(openclaw models set "$openclaw_model" 2>&1); then set_exit=0 else set_exit=$? fi if [ $set_exit -eq 0 ]; then log_info "默认模型已设置为: $openclaw_model" else log_warn "模型设置可能失败: $openclaw_model" echo -e " ${GRAY}$set_result${NC}" | head -3 # 尝试直接使用 config set log_info "尝试使用 config set 设置模型..." openclaw config set agents.defaults.model.primary "$openclaw_model" 2>/dev/null || true openclaw config set models.default "$openclaw_model" 2>/dev/null || true fi fi fi # 添加到 shell 配置文件 add_env_to_shell "$env_file" } # 配置自定义 provider(用于支持自定义 API 地址) # 参数: provider api_key model base_url config_file [api_type] configure_custom_provider() { local provider="$1" local api_key="$2" local model="$3" local base_url="$4" local config_file="$5" local custom_api_type="$6" # 可选参数,用于覆盖默认 API 类型 # 参数校验 if [ -z "$model" ]; then log_error "模型名称不能为空" return 1 fi if [ -z "$api_key" ]; then log_error "API Key 不能为空" return 1 fi if [ -z "$base_url" ]; then log_error "API 地址不能为空" return 1 fi log_step "配置自定义 Provider..." # 确保配置目录存在 local config_dir=$(dirname "$config_file") mkdir -p "$config_dir" 2>/dev/null || true # 确定 API 类型 # 如果传入了自定义 API 类型,使用传入的值;否则根据 provider 自动判断 local api_type="" if [ -n "$custom_api_type" ]; then api_type="$custom_api_type" elif [ "$provider" = "anthropic" ]; then api_type="anthropic-messages" else api_type="openai-responses" fi local provider_id="${provider}-custom" # 先检查是否存在旧的自定义配置,并询问是否清理 local do_cleanup="false" if [ -f "$config_file" ]; then # 检查是否有旧的自定义 provider 配置 local has_old_config="false" if grep -q '"anthropic-custom"' "$config_file" 2>/dev/null || \ grep -q '"openai-custom"' "$config_file" 2>/dev/null; then has_old_config="true" fi if [ "$has_old_config" = "true" ]; then echo "" echo -e "${CYAN}当前已有自定义 Provider 配置:${NC}" # 显示当前配置的 provider 和模型 if command -v node &> /dev/null; then node -e " const fs = require('fs'); try { const config = JSON.parse(fs.readFileSync('$config_file', 'utf8')); const providers = config.models?.providers || {}; for (const [id, p] of Object.entries(providers)) { if (id.includes('-custom')) { console.log(' - Provider: ' + id); console.log(' API 地址: ' + p.baseUrl); if (p.models?.length) { console.log(' 模型: ' + p.models.map(m => m.id).join(', ')); } } } } catch (e) {} " 2>/dev/null fi echo "" echo -e "${YELLOW}是否清理旧的自定义配置?${NC}" echo -e "${GRAY}(清理可避免配置累积,推荐选择 Y)${NC}" if confirm "清理旧配置?" "y"; then do_cleanup="true" fi fi fi # 读取现有配置或创建新配置 local config_json="{}" if [ -f "$config_file" ]; then config_json=$(cat "$config_file") fi # 使用 node 或 python 来处理 JSON local config_success=false if command -v node &> /dev/null; then log_info "使用 node 配置自定义 Provider..." # 将变量写入临时文件,避免 shell 转义问题 local tmp_vars="/tmp/openclaw_provider_vars_$$.json" cat > "$tmp_vars" << EOFVARS { "config_file": "$config_file", "provider_id": "$provider_id", "base_url": "$base_url", "api_key": "$api_key", "model": "$model", "api_type": "$api_type", "do_cleanup": "$do_cleanup" } EOFVARS node -e " const fs = require('fs'); const vars = JSON.parse(fs.readFileSync('$tmp_vars', 'utf8')); let config = {}; try { config = JSON.parse(fs.readFileSync(vars.config_file, 'utf8')); } catch (e) { config = {}; } // 确保 models.providers 结构存在 if (!config.models) config.models = {}; if (!config.models.providers) config.models.providers = {}; // 根据用户选择决定是否清理旧配置 if (vars.do_cleanup === 'true') { delete config.models.providers['anthropic-custom']; delete config.models.providers['openai-custom']; if (config.models.configured) { config.models.configured = config.models.configured.filter(m => { if (m.startsWith('openai/claude')) return false; if (m.startsWith('openrouter/claude') && !m.includes('openrouter.ai')) return false; return true; }); } if (config.models.aliases) { delete config.models.aliases['claude-custom']; } console.log('Old configurations cleaned up'); } // 添加自定义 provider config.models.providers[vars.provider_id] = { baseUrl: vars.base_url, apiKey: vars.api_key, models: [ { id: vars.model, name: vars.model, api: vars.api_type, input: ['text','image'], contextWindow: 200000, maxTokens: 8192 } ] }; fs.writeFileSync(vars.config_file, JSON.stringify(config, null, 2)); console.log('Custom provider configured: ' + vars.provider_id); " 2>&1 local node_exit=$? rm -f "$tmp_vars" 2>/dev/null if [ $node_exit -eq 0 ]; then config_success=true log_info "自定义 Provider 已配置: $provider_id" else log_warn "node 配置失败 (exit: $node_exit),尝试使用 python3..." fi fi # 如果 node 失败或不存在,尝试 python3 if [ "$config_success" = false ] && command -v python3 &> /dev/null; then log_info "使用 python3 配置自定义 Provider..." # 将变量写入临时文件,避免 shell 转义问题 local tmp_vars="/tmp/openclaw_provider_vars_$$.json" cat > "$tmp_vars" << EOFVARS { "config_file": "$config_file", "provider_id": "$provider_id", "base_url": "$base_url", "api_key": "$api_key", "model": "$model", "api_type": "$api_type", "do_cleanup": "$do_cleanup" } EOFVARS python3 -c " import json import os # 从临时文件读取变量 with open('$tmp_vars', 'r') as f: vars = json.load(f) config = {} config_file = vars['config_file'] if os.path.exists(config_file): try: with open(config_file, 'r') as f: config = json.load(f) except: config = {} if 'models' not in config: config['models'] = {} if 'providers' not in config['models']: config['models']['providers'] = {} # 根据用户选择决定是否清理旧配置 if vars['do_cleanup'] == 'true': config['models']['providers'].pop('anthropic-custom', None) config['models']['providers'].pop('openai-custom', None) if 'configured' in config['models']: config['models']['configured'] = [ m for m in config['models']['configured'] if not (m.startswith('openai/claude') or (m.startswith('openrouter/claude') and 'openrouter.ai' not in m)) ] if 'aliases' in config['models']: config['models']['aliases'].pop('claude-custom', None) print('Old configurations cleaned up') config['models']['providers'][vars['provider_id']] = { 'baseUrl': vars['base_url'], 'apiKey': vars['api_key'], 'models': [ { 'id': vars['model'], 'name': vars['model'], 'api': vars['api_type'], 'input': ['text','image'], 'contextWindow': 200000, 'maxTokens': 8192 } ] } with open(config_file, 'w') as f: json.dump(config, f, indent=2) print('Custom provider configured: ' + vars['provider_id']) " 2>&1 local py_exit=$? rm -f "$tmp_vars" 2>/dev/null if [ $py_exit -eq 0 ]; then config_success=true log_info "自定义 Provider 已配置: $provider_id" else log_warn "python3 配置失败 (exit: $py_exit)" fi fi if [ "$config_success" = false ]; then log_warn "无法配置自定义 Provider(需要 node 或 python3)" fi # 验证配置文件是否正确写入 if [ -f "$config_file" ]; then if grep -q "$provider_id" "$config_file" 2>/dev/null; then log_info "配置文件验证通过: $config_file" else log_warn "配置文件可能未正确写入,请检查: $config_file" fi fi } # 添加环境变量到 shell 配置 add_env_to_shell() { local env_file="$1" local shell_rc="" if [ -f "$HOME/.zshrc" ]; then shell_rc="$HOME/.zshrc" elif [ -f "$HOME/.bashrc" ]; then shell_rc="$HOME/.bashrc" elif [ -f "$HOME/.bash_profile" ]; then shell_rc="$HOME/.bash_profile" fi if [ -n "$shell_rc" ]; then # 检查是否已添加 if ! grep -q "source.*openclaw/env" "$shell_rc" 2>/dev/null; then echo "" >> "$shell_rc" echo "# OpenClaw 环境变量" >> "$shell_rc" echo "[ -f \"$env_file\" ] && source \"$env_file\"" >> "$shell_rc" log_info "环境变量已添加到: $shell_rc" fi fi } # ================================ 配置向导 ================================ # create_default_config 已移除 - OpenClaw 使用 openclaw.json 和环境变量 run_onboard_wizard() { log_step "运行配置向导..." echo "" echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} 🧙 OpenClaw 核心配置向导${NC}" echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" if confirm "使用官方配置向导 openclaw onboard(推荐,模型列表与官方同步)?" "y"; then if run_step_with_auto_fix "官方配置向导" run_official_onboard; then echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} 第 2 步: 消息渠道配置${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" if confirm "现在进入消息渠道配置?" "y"; then if ! run_step_with_auto_fix "消息渠道配置菜单" run_config_menu --channels-only; then log_warn "消息渠道配置菜单启动失败,可稍后手动运行: bash ./config-menu.sh" fi fi log_info "官方配置流程完成。" return 0 fi log_warn "官方向导执行失败,将回退到内置兼容向导。" fi # 检查是否已有配置 local skip_ai_config=false local skip_identity_config=false local env_file="$HOME/.openclaw/env" if [ -f "$env_file" ]; then echo -e "${YELLOW}检测到已有配置!${NC}" echo "" # 显示当前模型配置 if check_command openclaw; then echo -e "${CYAN}当前 OpenClaw 配置:${NC}" openclaw models status 2>/dev/null | head -10 || true echo "" fi # 询问是否重新配置 AI if ! confirm "是否重新配置 AI 模型提供商?" "n"; then skip_ai_config=true log_info "使用现有 AI 配置" if confirm "是否测试现有 API 连接?" "y"; then # 从 env 文件读取配置进行测试 source "$env_file" # 获取当前模型(优先使用官方 models status JSON) local current_model_ref current_model_ref="$(get_current_model_ref || true)" AI_MODEL="${current_model_ref#*/}" if [ -n "$ANTHROPIC_API_KEY" ]; then AI_PROVIDER="anthropic" AI_KEY="$ANTHROPIC_API_KEY" BASE_URL="$ANTHROPIC_BASE_URL" elif [ -n "$MOONSHOT_API_KEY" ]; then AI_PROVIDER="moonshot" AI_KEY="$MOONSHOT_API_KEY" BASE_URL="$MOONSHOT_BASE_URL" elif [ -n "$MINIMAX_API_KEY" ]; then AI_PROVIDER="minimax" AI_KEY="$MINIMAX_API_KEY" elif [ -n "$OPENROUTER_API_KEY" ]; then AI_PROVIDER="openrouter" AI_KEY="$OPENROUTER_API_KEY" BASE_URL="$OPENROUTER_BASE_URL" elif [ -n "$MISTRAL_API_KEY" ]; then AI_PROVIDER="mistral" AI_KEY="$MISTRAL_API_KEY" BASE_URL="$MISTRAL_BASE_URL" elif [ -n "$GROQ_API_KEY" ]; then AI_PROVIDER="groq" AI_KEY="$GROQ_API_KEY" BASE_URL="$GROQ_BASE_URL" elif [ -n "$OPENAI_API_KEY" ]; then AI_PROVIDER="openai" AI_KEY="$OPENAI_API_KEY" BASE_URL="$OPENAI_BASE_URL" elif [ -n "$GOOGLE_API_KEY" ]; then AI_PROVIDER="google" AI_KEY="$GOOGLE_API_KEY" BASE_URL="$GOOGLE_BASE_URL" elif [ -n "$XAI_API_KEY" ]; then AI_PROVIDER="xai" AI_KEY="$XAI_API_KEY" elif [ -n "$ZAI_API_KEY" ]; then AI_PROVIDER="zai" AI_KEY="$ZAI_API_KEY" fi test_api_connection fi fi echo "" else echo -e "${CYAN}接下来将引导你完成核心配置,包括:${NC}" echo " 1. 选择 AI 模型提供商" echo " 2. 配置 API 连接" echo " 3. 测试 API 连接" echo " 4. 消息渠道配置" echo "" fi # AI 配置 if [ "$skip_ai_config" = false ]; then setup_ai_provider # 先配置 OpenClaw(设置环境变量和自定义 provider),然后再测试 configure_openclaw_model test_api_connection else # 即使跳过配置,也可选择测试连接 if confirm "是否测试现有 API 连接?" "y"; then test_api_connection fi fi # 模型配置完成后,自动进入消息渠道配置 echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} 第 3 步: 消息渠道配置${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" if confirm "现在进入消息渠道配置?" "y"; then if ! run_config_menu --channels-only; then log_warn "消息渠道配置菜单启动失败,可稍后手动运行: bash ./config-menu.sh" fi fi log_info "模型与消息渠道配置流程已完成!" } # ================================ AI Provider 配置 ================================ setup_ai_provider() { echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} 第 1 步: 选择 AI 模型提供商${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo " 1) 🟣 Anthropic Claude" echo " 2) 🟢 OpenAI GPT" echo " 3) 🔵 DeepSeek" echo " 4) 🌙 Kimi (Moonshot)" echo " 5) 🔴 Google Gemini" echo " 6) 🔄 OpenRouter (多模型网关)" echo " 7) ⚡ Groq (超快推理)" echo " 8) 🌬️ Mistral AI" echo " 9) 🟠 Ollama (本地模型)" echo " 10) 𝕏 xAI Grok" echo " 11) 🇨🇳 智谱 GLM (Zai)" echo " 12) 🤖 MiniMax" echo " 13) 🆓 OpenCode (免费多模型)" echo " 14) ☁️ Azure OpenAI" echo " 15) 🧪 Google Gemini CLI" echo " 16) 🚀 Google Antigravity" echo " 17) 🚀 Novita AI (Kimi/DeepSeek/GLM)" echo "" echo -e "${GRAY}说明:${NC}" echo -e "${GRAY} • 本安装向导提供官方常用提供商的快速入口(与官方文档对齐的精简集)${NC}" echo -e "${GRAY} • 更多提供商(如 Venice / Qwen / Vercel Gateway 等)可在安装后运行:${NC}" echo -e "${GRAY} openclaw onboard 或 bash ~/.openclaw/config-menu.sh${NC}" echo -e "${GRAY} • 官方模型文档: https://docs.openclaw.ai/providers/models${NC}" echo -e "${GRAY} • 支持自定义 API 地址(通过 openclaw.json 配置自定义 Provider)${NC}" echo "" echo -en "${YELLOW}请选择 AI 提供商 [1-17] (默认: 1): ${NC}"; read ai_choice < "$TTY_INPUT" ai_choice=${ai_choice:-1} case $ai_choice in 1) AI_PROVIDER="anthropic" echo "" echo -e "${CYAN}配置 Anthropic Claude${NC}" echo -e "${GRAY}官方 API: https://console.anthropic.com/${NC}" echo "" echo -en "${YELLOW}自定义 API 地址 (留空使用官方 API): ${NC}"; read BASE_URL < "$TTY_INPUT" echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo "选择模型:" echo " 1) claude-sonnet-4-6 (推荐, 官方默认)" echo " 2) claude-opus-4-6 (最强)" echo " 3) claude-haiku-4-5 (快速)" echo " 4) claude-sonnet-4-5 (兼容)" echo " 5) 自定义模型名称" echo -en "${YELLOW}选择模型 [1-5] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="claude-opus-4-6" ;; 3) AI_MODEL="claude-haiku-4-5" ;; 4) AI_MODEL="claude-sonnet-4-5" ;; 5) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="claude-sonnet-4-6" ;; esac ;; 2) AI_PROVIDER="openai" echo "" echo -e "${CYAN}配置 OpenAI GPT${NC}" echo -e "${GRAY}官方 API: https://platform.openai.com/${NC}" echo "" echo -en "${YELLOW}自定义 API 地址 (留空使用官方 API): ${NC}"; read BASE_URL < "$TTY_INPUT" echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo "选择模型:" echo " 1) gpt-5.1-codex (推荐, 官方默认)" echo " 2) gpt-5.4 (最新通用)" echo " 3) gpt-5.1" echo " 4) gpt-5.1-codex-mini (经济)" echo " 5) 自定义模型名称" echo -en "${YELLOW}选择模型 [1-5] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="gpt-5.4" ;; 3) AI_MODEL="gpt-5.1" ;; 4) AI_MODEL="gpt-5.1-codex-mini" ;; 5) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="gpt-5.1-codex" ;; esac # 如果使用自定义 API 地址,询问 API 类型 AI_API_TYPE="" if [ -n "$BASE_URL" ]; then echo "" echo -e "${CYAN}选择 API 兼容格式:${NC}" echo " 1) openai-responses (OpenAI 官方 Responses API)" echo " 2) openai-completions (兼容 /v1/chat/completions 端点)" echo -e "${GRAY}提示: 大多数第三方服务使用 openai-completions 格式${NC}" echo -en "${YELLOW}选择 API 格式 [1-2] (默认: 2): ${NC}"; read api_type_choice < "$TTY_INPUT" case $api_type_choice in 1) AI_API_TYPE="openai-responses" ;; *) AI_API_TYPE="openai-completions" ;; esac fi ;; 3) AI_PROVIDER="deepseek" echo "" echo -e "${CYAN}配置 DeepSeek${NC}" echo -e "${GRAY}官方 API: https://platform.deepseek.com/${NC}" echo "" echo -en "${YELLOW}自定义 API 地址 (留空使用官方 API): ${NC}"; read BASE_URL < "$TTY_INPUT" BASE_URL=${BASE_URL:-"https://api.deepseek.com"} echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo "选择模型:" echo " 1) deepseek-chat (V3.2, 推荐)" echo " 2) deepseek-reasoner (R1, 推理)" echo " 3) deepseek-coder" echo " 4) 自定义模型名称" echo -en "${YELLOW}选择模型 [1-4] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="deepseek-reasoner" ;; 3) AI_MODEL="deepseek-coder" ;; 4) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="deepseek-chat" ;; esac ;; 4) AI_PROVIDER="moonshot" echo "" echo -e "${CYAN}配置 Kimi (Moonshot)${NC}" echo -e "${GRAY}官方控制台: https://platform.moonshot.cn/${NC}" echo "" echo "选择区域:" echo " 1) 国际版 API (api.moonshot.ai)" echo " 2) 国内版 API (api.moonshot.cn)" echo -en "${YELLOW}选择区域 [1-2] (默认: 1): ${NC}"; read kimi_region < "$TTY_INPUT" echo -en "${YELLOW}自定义 API 地址 (留空使用官方 API): ${NC}"; read BASE_URL < "$TTY_INPUT" if [ -z "$BASE_URL" ]; then if [ "$kimi_region" = "2" ]; then BASE_URL="https://api.moonshot.cn/v1" else BASE_URL="https://api.moonshot.ai/v1" fi fi echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo "选择模型:" echo " 1) kimi-k2.5 (推荐, 官方默认)" echo " 2) 自定义模型名称" echo -en "${YELLOW}选择模型 [1-2] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="kimi-k2.5" ;; esac ;; 5) AI_PROVIDER="google" echo "" echo -e "${CYAN}配置 Google Gemini${NC}" echo -e "${GRAY}获取 API Key: https://aistudio.google.com/apikey${NC}" echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo -en "${YELLOW}自定义 API 地址 (留空使用官方): ${NC}"; read BASE_URL < "$TTY_INPUT" echo "" echo "选择模型:" echo " 1) gemini-3.1-pro-preview (推荐, 官方默认)" echo " 2) gemini-3-flash-preview" echo " 3) gemini-2.5-pro" echo " 4) 自定义模型名称" echo -en "${YELLOW}选择模型 [1-4] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="gemini-3-flash-preview" ;; 3) AI_MODEL="gemini-2.5-pro" ;; 4) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="gemini-3.1-pro-preview" ;; esac ;; 6) AI_PROVIDER="openrouter" echo "" echo -e "${CYAN}配置 OpenRouter${NC}" echo -e "${GRAY}获取 API Key: https://openrouter.ai/${NC}" echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo -en "${YELLOW}自定义 API 地址 (留空使用官方): ${NC}"; read BASE_URL < "$TTY_INPUT" BASE_URL=${BASE_URL:-"https://openrouter.ai/api/v1"} echo "" echo "选择模型:" echo " 1) auto (推荐, 官方默认)" echo " 2) anthropic/claude-opus-4.6" echo " 3) openai/gpt-5.1-codex" echo " 4) 自定义模型名称" echo -en "${YELLOW}选择模型 [1-4] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="anthropic/claude-opus-4.6" ;; 3) AI_MODEL="openai/gpt-5.1-codex" ;; 4) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="auto" ;; esac ;; 7) AI_PROVIDER="groq" echo "" echo -e "${CYAN}配置 Groq${NC}" echo -e "${GRAY}获取 API Key: https://console.groq.com/${NC}" echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo -en "${YELLOW}自定义 API 地址 (留空使用官方): ${NC}"; read BASE_URL < "$TTY_INPUT" BASE_URL=${BASE_URL:-"https://api.groq.com/openai/v1"} echo "" echo "选择模型:" echo " 1) llama-3.3-70b-versatile (推荐)" echo " 2) llama-3.1-8b-instant" echo " 3) mixtral-8x7b-32768" echo " 4) 自定义" echo -en "${YELLOW}选择模型 [1-4] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="llama-3.1-8b-instant" ;; 3) AI_MODEL="mixtral-8x7b-32768" ;; 4) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="llama-3.3-70b-versatile" ;; esac ;; 8) AI_PROVIDER="mistral" echo "" echo -e "${CYAN}配置 Mistral AI${NC}" echo -e "${GRAY}获取 API Key: https://console.mistral.ai/${NC}" echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo -en "${YELLOW}自定义 API 地址 (留空使用官方): ${NC}"; read BASE_URL < "$TTY_INPUT" BASE_URL=${BASE_URL:-"https://api.mistral.ai/v1"} echo "" echo "选择模型:" echo " 1) mistral-large-latest (推荐)" echo " 2) mistral-small-latest" echo " 3) codestral-latest" echo " 4) 自定义" echo -en "${YELLOW}选择模型 [1-4] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="mistral-small-latest" ;; 3) AI_MODEL="codestral-latest" ;; 4) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="mistral-large-latest" ;; esac ;; 9) AI_PROVIDER="ollama" AI_KEY="" echo "" echo -e "${CYAN}配置 Ollama 本地模型${NC}" echo "" echo -en "${YELLOW}Ollama 地址 (默认: http://localhost:11434): ${NC}"; read BASE_URL < "$TTY_INPUT" BASE_URL=${BASE_URL:-"http://localhost:11434"} echo "" echo "选择模型:" echo " 1) llama3" echo " 2) llama3:70b" echo " 3) mistral" echo " 4) 自定义" echo -en "${YELLOW}选择模型 [1-4] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="llama3:70b" ;; 3) AI_MODEL="mistral" ;; 4) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="llama3" ;; esac ;; 10) AI_PROVIDER="xai" BASE_URL="" echo "" echo -e "${CYAN}配置 xAI Grok${NC}" echo -e "${GRAY}获取 API Key: https://console.x.ai/${NC}" echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo "选择模型:" echo " 1) grok-4 (推荐, 官方默认)" echo " 2) grok-4-fast" echo " 3) 自定义模型名称" echo -en "${YELLOW}选择模型 [1-3] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="grok-4-fast" ;; 3) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="grok-4" ;; esac ;; 17) AI_PROVIDER="novita" echo "" echo -e "${CYAN}配置 Novita AI${NC}" echo -e "${GRAY}获取 API Key: https://novita.ai/${NC}" echo "" echo -en "${YELLOW}输入 API Key: ${NC}"; read AI_KEY < "$TTY_INPUT" BASE_URL="https://api.novita.ai/openai" echo "" echo "选择模型:" echo " 1) moonshotai/kimi-k2.5 (推荐)" echo " 2) deepseek/deepseek-v3.2" echo " 3) zai-org/glm-5" echo " 4) 自定义" echo -en "${YELLOW}选择模型 [1-4] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="deepseek/deepseek-v3.2" ;; 3) AI_MODEL="zai-org/glm-5" ;; 4) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="moonshotai/kimi-k2.5" ;; esac ;; 11) AI_PROVIDER="zai" BASE_URL="" echo "" echo -e "${CYAN}配置 智谱 GLM (Zai)${NC}" echo -e "${GRAY}获取 API Key: https://open.bigmodel.cn/${NC}" echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo "选择模型:" echo " 1) glm-5 (推荐)" echo " 2) glm-4.7" echo " 3) glm-4.7-flash" echo " 4) glm-4.7-flashx" echo " 5) 自定义模型名称" echo -en "${YELLOW}选择模型 [1-5] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="glm-4.7" ;; 3) AI_MODEL="glm-4.7-flash" ;; 4) AI_MODEL="glm-4.7-flashx" ;; 5) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="glm-5" ;; esac ;; 12) AI_PROVIDER="minimax" BASE_URL="" echo "" echo -e "${CYAN}配置 MiniMax${NC}" echo "" echo "选择区域:" echo " 1) 国际版 (minimax)" echo " 2) 国内版 (minimax-cn)" echo -en "${YELLOW}选择区域 [1-2] (默认: 1): ${NC}"; read region_choice < "$TTY_INPUT" if [ "$region_choice" = "2" ]; then AI_PROVIDER="minimax-cn" echo -e "${GRAY}获取 API Key: https://platform.minimaxi.com/${NC}" else echo -e "${GRAY}获取 API Key: https://platform.minimax.io/${NC}" fi echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo "选择模型:" echo " 1) MiniMax-M2.5 (推荐,官方)" echo " 2) MiniMax-M2.5-highspeed (高速)" echo " 3) 自定义模型名称" echo -en "${YELLOW}选择模型 [1-3] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="MiniMax-M2.5-highspeed" ;; 3) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="MiniMax-M2.5" ;; esac ;; 13) AI_PROVIDER="opencode" BASE_URL="" echo "" echo -e "${CYAN}配置 OpenCode${NC}" echo -e "${GRAY}获取 API Key: https://opencode.ai/auth${NC}" echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo "选择模型:" echo " 1) claude-opus-4-6 (推荐, Zen 默认)" echo " 2) gpt-5.1-codex" echo " 3) gpt-5.2" echo " 4) gemini-3-pro" echo " 5) glm-4.7" echo " 6) 自定义模型名称" echo -en "${YELLOW}选择模型 [1-6] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="gpt-5.1-codex" ;; 3) AI_MODEL="gpt-5.2" ;; 4) AI_MODEL="gemini-3-pro" ;; 5) AI_MODEL="glm-4.7" ;; 6) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="claude-opus-4-6" ;; esac ;; 14) # Azure OpenAI 走 OpenAI 兼容协议 AI_PROVIDER="openai" AI_API_TYPE="openai-completions" echo "" echo -e "${CYAN}配置 Azure OpenAI${NC}" echo -e "${GRAY}说明: 请输入 Azure Endpoint(示例: https://.openai.azure.com)${NC}" echo "" echo -en "${YELLOW}Azure Endpoint: ${NC}"; read azure_endpoint < "$TTY_INPUT" echo -en "${YELLOW}Azure 部署名(Deployment Name): ${NC}"; read azure_deployment < "$TTY_INPUT" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY if [ -z "$azure_endpoint" ] || [ -z "$azure_deployment" ] || [ -z "$AI_KEY" ]; then log_warn "Azure OpenAI 信息不完整,回退到 OpenAI 默认配置" BASE_URL="" AI_MODEL="gpt-5.1-codex" else BASE_URL="${azure_endpoint%/}/openai/deployments/${azure_deployment}" AI_MODEL="$azure_deployment" fi ;; 15) AI_PROVIDER="google-gemini-cli" BASE_URL="" echo "" echo -e "${CYAN}配置 Google Gemini CLI${NC}" echo -e "${GRAY}获取 API Key: https://aistudio.google.com/apikey${NC}" echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo "选择模型:" echo " 1) gemini-3.1-pro-preview (推荐)" echo " 2) gemini-3-flash-preview" echo " 3) gemini-3.1-flash-lite-preview" echo " 4) 自定义模型名称" echo -en "${YELLOW}选择模型 [1-4] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="gemini-3-flash-preview" ;; 3) AI_MODEL="gemini-3.1-flash-lite-preview" ;; 4) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="gemini-3.1-pro-preview" ;; esac ;; 16) AI_PROVIDER="google-antigravity" BASE_URL="" echo "" echo -e "${CYAN}配置 Google Antigravity${NC}" echo -e "${GRAY}获取 API Key: https://aistudio.google.com/apikey${NC}" echo "" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY echo "" echo "选择模型:" echo " 1) gemini-3-pro-high (推荐)" echo " 2) gemini-3-pro-low" echo " 3) gemini-3-flash" echo " 4) claude-opus-4-6-thinking" echo " 5) 自定义模型名称" echo -en "${YELLOW}选择模型 [1-5] (默认: 1): ${NC}"; read model_choice < "$TTY_INPUT" case $model_choice in 2) AI_MODEL="gemini-3-pro-low" ;; 3) AI_MODEL="gemini-3-flash" ;; 4) AI_MODEL="claude-opus-4-6-thinking" ;; 5) echo -en "${YELLOW}输入模型名称: ${NC}"; read AI_MODEL < "$TTY_INPUT" ;; *) AI_MODEL="gemini-3-pro-high" ;; esac ;; *) # 默认使用 Anthropic AI_PROVIDER="anthropic" echo "" echo -e "${CYAN}配置 Anthropic Claude${NC}" echo -en "${YELLOW}自定义 API 地址 (留空使用官方): ${NC}"; read BASE_URL < "$TTY_INPUT" read_secret_input "${YELLOW}输入 API Key: ${NC}" AI_KEY AI_MODEL="claude-sonnet-4-6" ;; esac echo "" log_info "AI Provider 配置完成" echo -e " 提供商: ${WHITE}$AI_PROVIDER${NC}" echo -e " 模型: ${WHITE}$AI_MODEL${NC}" [ -n "$BASE_URL" ] && echo -e " API 地址: ${WHITE}$BASE_URL${NC}" } # ================================ API 连接测试 ================================ test_api_connection() { echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} 第 2 步: 测试 API 连接${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" local test_passed=false # 确保环境变量已加载 local env_file="$HOME/.openclaw/env" [ -f "$env_file" ] && source "$env_file" if ! check_command openclaw; then echo -e "${YELLOW}OpenClaw 未安装,跳过测试${NC}" return 0 fi local current_model_ref current_model_ref="$(get_current_model_ref || true)" echo -e "${CYAN}当前模型配置:${NC}" openclaw models status 2>&1 | head -12 echo "" [ -n "$current_model_ref" ] && echo -e "${CYAN}目标模型:${NC} ${WHITE}${current_model_ref}${NC}" && echo "" echo -e "${YELLOW}运行官方模型探针 (openclaw models status --probe --check)...${NC}" local probe_output="" local probe_exit=0 set +e probe_output=$(openclaw models status --probe --check --json 2>&1) probe_exit=$? set -e if [ $probe_exit -eq 0 ]; then test_passed=true echo -e "${GREEN}✓ OpenClaw AI 测试成功(探针通过)${NC}" else echo -e "${RED}✗ 模型探针失败${NC}" echo "$probe_output" | head -10 | sed 's/^/ /' echo "" echo -e "${YELLOW}尝试本地 agent 调用获取详细错误...${NC}" local agent_output="" local agent_exit=1 if [ -n "$current_model_ref" ]; then set +e agent_output=$(openclaw agent --local --model "$current_model_ref" --message "只回复 OK" 2>&1) agent_exit=$? set -e else set +e agent_output=$(openclaw agent --local --message "只回复 OK" 2>&1) agent_exit=$? set -e fi if [ $agent_exit -eq 0 ] && ! echo "$agent_output" | grep -qiE "error|failed|401|403|Unknown model"; then test_passed=true echo -e "${GREEN}✓ OpenClaw AI 测试成功(agent 调用通过)${NC}" else echo -e "${RED}✗ OpenClaw AI 调用失败${NC}" echo "$agent_output" | head -10 | sed 's/^/ /' fi fi if [ "$test_passed" = false ]; then echo -e "${RED}API 连接测试失败${NC}" echo "" echo "建议运行以下命令手动配置:" echo " openclaw configure --section model" echo " openclaw doctor" echo "" if confirm "是否仍然继续安装?" "y"; then log_warn "跳过连接测试,继续安装..." return 0 else echo "安装已取消" exit 1 fi fi return 0 } # ================================ 身份配置 ================================ setup_identity() { echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} 第 3 步: 设置身份信息${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -en "${YELLOW}给你的 AI 助手起个名字 (默认: Clawd): ${NC}"; read BOT_NAME < "$TTY_INPUT" BOT_NAME=${BOT_NAME:-"Clawd"} echo -en "${YELLOW}AI 如何称呼你 (默认: 主人): ${NC}"; read USER_NAME < "$TTY_INPUT" USER_NAME=${USER_NAME:-"主人"} echo -en "${YELLOW}你的时区 (默认: Asia/Shanghai): ${NC}"; read TIMEZONE < "$TTY_INPUT" TIMEZONE=${TIMEZONE:-"Asia/Shanghai"} echo "" log_info "身份配置完成" echo -e " 助手名称: ${WHITE}$BOT_NAME${NC}" echo -e " 你的称呼: ${WHITE}$USER_NAME${NC}" echo -e " 时区: ${WHITE}$TIMEZONE${NC}" # 初始化渠道配置变量 TELEGRAM_ENABLED="false" DISCORD_ENABLED="false" SHELL_ENABLED="false" FILE_ACCESS="false" } # ================================ 服务管理 ================================ setup_daemon() { if confirm "是否设置开机自启动?" "y"; then log_step "配置系统服务..." case "$OS" in macos) setup_launchd ;; *) setup_systemd ;; esac fi } setup_systemd() { cat > /tmp/openclaw.service << EOF [Unit] Description=OpenClaw AI Assistant After=network.target [Service] Type=simple User=$USER ExecStart=$(which openclaw) gateway start Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target EOF sudo mv /tmp/openclaw.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable openclaw log_info "Systemd 服务已配置" } setup_launchd() { mkdir -p "$HOME/Library/LaunchAgents" cat > "$HOME/Library/LaunchAgents/com.openclaw.agent.plist" << EOF Label com.openclaw.agent ProgramArguments $(which openclaw) gateway start RunAtLoad KeepAlive StandardOutPath $CONFIG_DIR/stdout.log StandardErrorPath $CONFIG_DIR/stderr.log EOF launchctl load "$HOME/Library/LaunchAgents/com.openclaw.agent.plist" 2>/dev/null || true log_info "LaunchAgent 已配置" } # ================================ 完成安装 ================================ print_success() { echo "" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${GREEN} 🎉 安装完成!🎉${NC}" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e "${WHITE}配置目录:${NC}" echo " OpenClaw 配置: ~/.openclaw/" echo " 环境变量配置: ~/.openclaw/env" echo "" echo -e "${CYAN}常用命令:${NC}" echo " openclaw gateway start # 后台启动服务" echo " openclaw gateway stop # 停止服务" echo " openclaw gateway status # 查看状态" echo " openclaw models status # 查看模型配置" echo " openclaw channels list # 查看渠道列表" echo " openclaw doctor # 诊断问题" echo " ~/.openclaw/docs/channels-configuration-guide.md # 渠道配置文档" echo " ~/.openclaw/skills/channel-setup-assistant/SKILL.md # 渠道配置 Skill" echo "" echo -e "${PURPLE}📚 官方文档: $OFFICIAL_DOCS_URL${NC}" echo -e "${PURPLE}💬 社区支持: https://github.com/$GITHUB_REPO/discussions${NC}" echo "" } # 启动 OpenClaw Gateway 服务 start_openclaw_service() { echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} 🚀 启动 OpenClaw 服务${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" # 加载环境变量 local env_file="$HOME/.openclaw/env" if [ -f "$env_file" ]; then source "$env_file" log_info "已加载环境变量" fi # 使用端口检测判断是否已有服务在运行(更可靠) local existing_pid existing_pid=$(get_gateway_pid) if [ -n "$existing_pid" ]; then log_warn "OpenClaw Gateway 已在运行 (PID: $existing_pid)" echo "" if confirm "是否重启服务?" "y"; then openclaw gateway stop 2>/dev/null || true sleep 2 else return 0 fi fi # 后台启动 Gateway(使用 setsid 完全脱离终端) log_step "正在后台启动 Gateway..." if command -v setsid &> /dev/null; then if [ -f "$env_file" ]; then setsid bash -c "source $env_file && exec openclaw gateway --port ${GATEWAY_PORT}" > /tmp/openclaw-gateway.log 2>&1 & else setsid openclaw gateway --port "${GATEWAY_PORT}" > /tmp/openclaw-gateway.log 2>&1 & fi else # 备用方案:nohup + disown if [ -f "$env_file" ]; then nohup bash -c "source $env_file && exec openclaw gateway --port ${GATEWAY_PORT}" > /tmp/openclaw-gateway.log 2>&1 & else nohup openclaw gateway --port "${GATEWAY_PORT}" > /tmp/openclaw-gateway.log 2>&1 & fi disown 2>/dev/null || true fi # 等待服务启动 sleep 3 # 使用端口检测判断服务是否启动成功(更可靠) local gateway_pid gateway_pid=$(get_gateway_pid) if [ -n "$gateway_pid" ]; then echo "" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${GREEN} ✓ OpenClaw Gateway 已启动!(PID: $gateway_pid)${NC}" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e " ${CYAN}查看状态:${NC} openclaw gateway status" echo -e " ${CYAN}查看日志:${NC} tail -f /tmp/openclaw-gateway.log" echo -e " ${CYAN}停止服务:${NC} openclaw gateway stop" echo "" log_info "OpenClaw 现在可以接收消息了!" else log_error "Gateway 启动失败" echo "" echo -e "${YELLOW}请查看日志: tail -f /tmp/openclaw-gateway.log${NC}" echo -e "${YELLOW}或手动启动: source ~/.openclaw/env && openclaw gateway --port ${GATEWAY_PORT}${NC}" fi } # 下载并运行配置菜单 run_config_menu() { local menu_args=("$@") local config_menu_path="./config-menu.sh" local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local local_config_menu="$script_dir/config-menu.sh" local menu_script="" echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} 🔧 启动配置菜单${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" # 检查本地是否已有配置菜单 local has_local_menu=false if [ -f "$local_config_menu" ]; then has_local_menu=true menu_script="$local_config_menu" elif [ -f "$config_menu_path" ]; then has_local_menu=true menu_script="$config_menu_path" fi # 如果本地已有配置菜单,询问是否更新 if [ "$has_local_menu" = true ]; then log_info "检测到本地配置菜单: $menu_script" echo "" if confirm "是否从 GitHub 更新到最新版本?" "n"; then log_step "从 GitHub 下载最新配置菜单..." if download_with_fallback "$config_menu_path.tmp" "$GITHUB_RAW_URL/config-menu.sh" "$INSTALLER_MIRROR_RAW_URL/config-menu.sh"; then mv "$config_menu_path.tmp" "$config_menu_path" chmod +x "$config_menu_path" log_info "配置菜单已更新: $config_menu_path" menu_script="$config_menu_path" else rm -f "$config_menu_path.tmp" 2>/dev/null log_warn "下载失败,继续使用本地版本" fi else log_info "使用本地配置菜单" fi else # 本地没有配置菜单,从 GitHub 下载 log_step "从 GitHub 下载配置菜单..." if download_with_fallback "$config_menu_path.tmp" "$GITHUB_RAW_URL/config-menu.sh" "$INSTALLER_MIRROR_RAW_URL/config-menu.sh"; then mv "$config_menu_path.tmp" "$config_menu_path" chmod +x "$config_menu_path" log_info "配置菜单已下载: $config_menu_path" menu_script="$config_menu_path" else rm -f "$config_menu_path.tmp" 2>/dev/null log_error "配置菜单下载失败" echo -e "${YELLOW}你可以稍后手动下载运行:${NC}" echo " bash -c 'set -e; tmp=\"\$(mktemp)\"; for u in \"$GITHUB_RAW_URL/config-menu.sh\" \"$INSTALLER_MIRROR_RAW_URL/config-menu.sh\"; do if curl -fsSL --proto \"=https\" --tlsv1.2 --connect-timeout ${CURL_CONNECT_TIMEOUT} --max-time ${CURL_MAX_TIME} \"\$u\" -o \"\$tmp\"; then bash \"\$tmp\"; rm -f \"\$tmp\"; exit 0; fi; done; rm -f \"\$tmp\"; echo \"All sources failed\"; exit 1'" return 1 fi fi # 确保有执行权限 chmod +x "$menu_script" 2>/dev/null || true # 启动配置菜单(使用 /dev/tty 确保交互正常) echo "" if [ -e /dev/tty ] && ( : < /dev/tty ) 2>/dev/null; then bash "$menu_script" "${menu_args[@]}" < /dev/tty else bash "$menu_script" "${menu_args[@]}" fi return $? } # ================================ 主函数 ================================ main() { parse_args "$@" if [ "$HELP" = "1" ]; then print_usage exit 0 fi normalize_install_options print_banner print_install_plan echo -e "${YELLOW}⚠️ 警告: OpenClaw 需要完全的计算机权限${NC}" echo -e "${YELLOW} 不建议在主要工作电脑上安装,建议使用专用服务器或虚拟机${NC}" echo "" if [ "$DRY_RUN" = "1" ]; then log_info "dry-run 模式:仅输出计划,不执行安装" exit 0 fi if ! confirm "是否继续安装?"; then echo "安装已取消" exit 0 fi echo "" detect_os check_root ensure_sudo_privileges install_dependencies create_directories install_channel_assets if ! run_step_with_auto_fix "安装 OpenClaw" install_openclaw; then log_error "OpenClaw 安装失败" exit 1 fi if [ "$NO_ONBOARD" = "1" ]; then log_info "已按参数跳过 AI 初始化向导 (--no-onboard)" else if ! run_step_with_auto_fix "安装后配置向导" run_onboard_wizard; then log_warn "安装后配置向导未完成,可稍后手动运行: openclaw onboard" fi fi apply_default_security_baseline if ! run_step_with_auto_fix "设置开机守护进程" setup_daemon; then log_error "守护进程设置失败" exit 1 fi print_success # 询问是否启动服务 if confirm "是否现在启动 OpenClaw 服务?" "y"; then start_openclaw_service else echo "" echo -e "${CYAN}稍后可以通过以下命令启动服务:${NC}" echo " source ~/.openclaw/env && openclaw gateway --port ${GATEWAY_PORT}" echo "" fi # 询问是否打开配置菜单进行详细配置 echo "" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE} 📝 配置菜单(命令行版)${NC}" echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e "${GRAY}配置菜单支持: 渠道配置、身份设置、安全配置、服务管理等${NC}" echo "" echo -e "${WHITE}💡 下次可以直接运行配置菜单:${NC}" echo -e " ${CYAN}bash ./config-menu.sh${NC}" echo "" if confirm "是否现在打开配置菜单?" "n"; then run_config_menu else echo "" echo -e "${CYAN}稍后可以通过以下命令打开配置菜单:${NC}" echo " bash ./config-menu.sh" echo "" fi echo "" echo -e "${GREEN}🦞 OpenClaw 安装完成!祝你使用愉快!${NC}" echo "" } # 始终输出收尾提示,避免用户感知“无响应直接退出” trap 'print_exit_hint "$?"' EXIT # 执行主函数 main "$@"