arcadiasuite/server/modules/openclaw/install.sh

3004 lines
108 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.

#!/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 <<EOF
${INSTALLER_NAME} (OpenClaw 安装增强版)
用法:
curl -fsSL https://raw.githubusercontent.com/${GITHUB_REPO}/main/install.sh | bash -s -- [选项]
选项:
--install-method, --method npm|git 安装方式 (默认: npm)
--npm 等价于 --install-method npm
--git, --github 等价于 --install-method git
--version <version|dist-tag> 指定 OpenClaw 版本 (默认: latest)
--beta 优先使用 beta dist-tag
--git-dir, --dir <path> git 安装目录 (默认: ~/openclaw)
--no-git-update 禁止更新已有 git checkout
--no-onboard 跳过本脚本 AI 初始化向导
--onboard 强制执行本脚本 AI 初始化向导
--no-prompt 非交互模式(使用默认值)
--dry-run 只显示执行计划,不做变更
--verbose 详细日志
--gateway-host <host> Gateway 监听地址 (默认: 127.0.0.1)
--gateway-port <port> Gateway 监听端口 (默认: 13145)
--help, -h 显示帮助
环境变量:
OPENCLAW_INSTALL_METHOD=git|npm
OPENCLAW_VERSION=latest|next|<semver>
OPENCLAW_BETA=0|1
OPENCLAW_GIT_DIR=<path>
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=<mirror_raw_url>
OPENCLAW_OFFICIAL_INSTALL_MIRROR_URL=<mirror_install_sh_url>
OPENCLAW_CURL_CONNECT_TIMEOUT=<seconds>
OPENCLAW_CURL_MAX_TIME=<seconds>
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=</swapfile.openclaw>
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 swap2G~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}MBSwap ${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}MBSwap ${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" <<EOF
#!/bin/sh
exec "$claw_bin" "\$@"
EOF
chmod +x "$shim_target" 2>/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 - <<PYEOF
import json, os
file = os.path.expanduser("$config_file")
provider = "$provider"
model = "$model"
base_url = "$base_url"
try:
with open(file, "r") as f:
cfg = json.load(f)
except Exception:
cfg = {}
cfg.setdefault("models", {})
cfg["models"].setdefault("mode", "merge")
cfg["models"].setdefault("providers", {})
p = cfg["models"]["providers"].get(provider, {})
models = p.get("models", []) if isinstance(p.get("models"), list) else []
catalog = {
"MiniMax-M2.5": "MiniMax M2.5",
"MiniMax-M2.5-highspeed": "MiniMax M2.5 Highspeed",
}
existing = {m.get("id") for m in models if isinstance(m, dict)}
for mid in ("MiniMax-M2.5", "MiniMax-M2.5-highspeed"):
if mid not in existing:
models.append({
"id": mid, "name": catalog.get(mid, mid), "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 if isinstance(p, dict) else {}),
"baseUrl": base_url,
"api": "anthropic-messages",
"authHeader": True,
"models": models
}
cfg.setdefault("agents", {}).setdefault("defaults", {}).setdefault("models", {})
cfg["agents"]["defaults"]["models"][f"{provider}/{model}"] = {"alias": "Minimax"}
with open(file, "w") as f:
json.dump(cfg, f, indent=2)
PYEOF
fi
}
# 配置 OpenClaw 使用的 AI 模型和 API Key
configure_openclaw_model() {
log_step "配置 OpenClaw AI 模型..."
local env_file="$HOME/.openclaw/env"
local openclaw_json="$HOME/.openclaw/openclaw.json"
# 创建环境变量文件
cat > "$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://<resource>.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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.openclaw.agent</string>
<key>ProgramArguments</key>
<array>
<string>$(which openclaw)</string>
<string>gateway</string>
<string>start</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>$CONFIG_DIR/stdout.log</string>
<key>StandardErrorPath</key>
<string>$CONFIG_DIR/stderr.log</string>
</dict>
</plist>
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 "$@"