#!/bin/bash # # ╔═══════════════════════════════════════════════════════════════════════════╗ # ║ ║ # ║ 🦞 OpenClaw 交互式配置菜单 v1.0.0 ║ # ║ 便捷的可视化配置工具 ║ # ║ ║ # ╚═══════════════════════════════════════════════════════════════════════════╝ # # ================================ TTY 检测 ================================ # 当通过 curl | bash 或被其他脚本调用时,stdin 可能不是终端 # 需要优先选择“可读”的输入源,避免 /dev/tty 存在但不可用导致循环报错 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 return 1 } if ! TTY_INPUT="$(resolve_tty_input)"; then echo "错误: 无法获取终端输入,请直接运行此脚本" echo "用法: bash config-menu.sh" exit 1 fi if [ "$TTY_INPUT" = "/dev/stdin" ] && [ ! -t 0 ]; then echo "错误: 当前会话不可交互(stdin 非终端,且 /dev/tty 不可用)" echo "请在可交互终端中运行: bash config-menu.sh" exit 1 fi # 统一的读取函数(支持非 TTY 模式) read_input() { local prompt="$1" local var_name="$2" echo -en "$prompt" if ! read $var_name < "$TTY_INPUT"; then echo "" log_error "输入读取失败,请在可交互终端中重新运行。" exit 1 fi } # 从 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 if ! read $var_name < "$TTY_INPUT"; then stty echo < "$TTY_INPUT" 2>/dev/null || true echo "" log_error "输入读取失败,请在可交互终端中重新运行。" exit 1 fi stty echo < "$TTY_INPUT" 2>/dev/null || true else if ! read $var_name < "$TTY_INPUT"; then echo "" log_error "输入读取失败,请在可交互终端中重新运行。" exit 1 fi fi echo "" } # ================================ 颜色定义 ================================ 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' # 背景色 BG_BLUE='\033[44m' BG_GREEN='\033[42m' BG_RED='\033[41m' # ================================ 配置变量 ================================ CONFIG_DIR="$HOME/.openclaw" # OpenClaw 环境变量配置 OPENCLAW_ENV="$CONFIG_DIR/env" OPENCLAW_JSON="$CONFIG_DIR/openclaw.json" BACKUP_DIR="$CONFIG_DIR/backups" OPENCLAW_BIN_RESOLVED="" DEFAULT_GATEWAY_HOST="${OPENCLAW_GATEWAY_HOST:-127.0.0.1}" DEFAULT_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-13145}" # 飞书插件策略(仅官方插件,支持版本 pin) FEISHU_PLUGIN_OFFICIAL="@openclaw/feishu" # QQ 社区插件策略(可选,不默认) QQ_PLUGIN_COMMUNITY="@sliverp/qqbot" QQ_PLUGIN_VERSION_DEFAULT="${OPENCLAW_QQ_PLUGIN_VERSION:-1.5.4}" # 微信社区插件策略(LangBot WeChatPad 适配) WECHAT_PLUGIN_LANGBOT="openclaw-wechat-channel" WECHAT_PLUGIN_VERSION_DEFAULT="${OPENCLAW_WECHAT_PLUGIN_VERSION:-0.5.0}" WECHATPAD_CALLBACK_PATH_DEFAULT="${OPENCLAW_WECHATPAD_CALLBACK_PATH:-/api/callback/wechatpadpro}" # 企业微信社区插件策略(WeCom) WECOM_PLUGIN_COMMUNITY="@marshulll/openclaw-wecom" WECOM_PLUGIN_VERSION_DEFAULT="${OPENCLAW_WECOM_PLUGIN_VERSION:-0.1.41}" WECOM_WEBHOOK_BOT_DEFAULT="${OPENCLAW_WECOM_WEBHOOK_BOT_PATH:-/wecom/bot}" WECOM_WEBHOOK_APP_DEFAULT="${OPENCLAW_WECOM_WEBHOOK_APP_PATH:-/wecom/app}" INSTALLER_REPO="leecyno1/auto-install-Openclaw" INSTALLER_RAW_URL="https://raw.githubusercontent.com/${INSTALLER_REPO}/main" AUTO_FIX_OPENCLAW_REPO_URL="${AUTO_FIX_OPENCLAW_REPO_URL:-https://github.com/leecyno1/auto-fix-openclaw.git}" AUTO_FIX_OPENCLAW_REPO_MIRROR_URL="${AUTO_FIX_OPENCLAW_REPO_MIRROR_URL:-https://mirror.ghproxy.com/https://github.com/leecyno1/auto-fix-openclaw.git}" AUTO_FIX_OPENCLAW_DIR="${AUTO_FIX_OPENCLAW_DIR:-$HOME/.openclaw/tools/auto-fix-openclaw}" AUTO_FIX_OPENCLAW_BIN="$AUTO_FIX_OPENCLAW_DIR/bin/auto-fix-openclaw" # ================================ 工具函数 ================================ clear_screen() { if [ -n "${TERM:-}" ] && [ "${TERM}" != "dumb" ] && command -v clear >/dev/null 2>&1; then clear else printf '\n' fi } print_header() { echo -e "${CYAN}" cat << 'EOF' ╔═══════════════════════════════════════════════════════════════╗ ║ ║ ║ 🦞 OpenClaw 配置中心 ║ ║ ║ ╚═══════════════════════════════════════════════════════════════╝ EOF echo -e "${NC}" } print_divider() { echo -e "${GRAY}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" } print_menu_item() { local num=$1 local text=$2 local icon=$3 echo -e " ${CYAN}[$num]${NC} $icon $text" } log_info() { echo -e "${GREEN}✓${NC} $1" } log_warn() { echo -e "${YELLOW}⚠${NC} $1" } log_error() { echo -e "${RED}✗${NC} $1" } press_enter() { echo "" echo -en "${GRAY}按 Enter 键继续...${NC}" if ! read < "$TTY_INPUT"; then echo "" log_error "输入读取失败,退出配置菜单。" exit 1 fi } confirm() { local message="$1" local default="${2:-y}" if [ "$default" = "y" ]; then local prompt="[Y/n]" else local prompt="[y/N]" fi echo -en "${YELLOW}$message $prompt: ${NC}" if ! read response < "$TTY_INPUT"; then echo "" log_error "输入读取失败,退出配置菜单。" exit 1 fi response=${response:-$default} case "$response" in [yY][eE][sS]|[yY]) return 0 ;; *) return 1 ;; esac } # 检查依赖 check_dependencies() { if ! command -v yq &> /dev/null; then # 使用简单的 sed/grep 处理 yaml USE_YQ=false else USE_YQ=true fi } # 备份配置 backup_config() { mkdir -p "$BACKUP_DIR" local backup_file="$BACKUP_DIR/env_$(date +%Y%m%d_%H%M%S).bak" if [ -f "$OPENCLAW_ENV" ]; then cp "$OPENCLAW_ENV" "$backup_file" echo "$backup_file" fi } # 确保插件被添加到 plugins.allow 数组中 # 这是启用插件的关键步骤 ensure_plugin_in_allow() { local plugin_id="$1" if [ ! -f "$OPENCLAW_JSON" ]; then log_warn "配置文件不存在: $OPENCLAW_JSON" return 1 fi # 检查是否安装了 jq if ! command -v jq &> /dev/null; then log_warn "未安装 jq,尝试使用 Python 更新配置..." python3 << PYEOF import json import os config_path = os.path.expanduser("$OPENCLAW_JSON") plugin_id = "$plugin_id" try: with open(config_path, 'r') as f: config = json.load(f) # 确保 plugins 结构存在 if 'plugins' not in config: config['plugins'] = {'allow': [], 'entries': {}} if 'allow' not in config['plugins']: config['plugins']['allow'] = [] if 'entries' not in config['plugins']: config['plugins']['entries'] = {} # 添加到 allow 列表 if plugin_id not in config['plugins']['allow']: config['plugins']['allow'].append(plugin_id) print(f"已将 {plugin_id} 添加到 plugins.allow") # 确保 entries 中也启用 config['plugins']['entries'][plugin_id] = {'enabled': True} # 确保 channels.xxx 存在(使用安全的默认策略,不设置 enabled) if 'channels' not in config: config['channels'] = {} if plugin_id not in config['channels']: config['channels'][plugin_id] = {'dmPolicy': 'pairing', 'groupPolicy': 'allowlist'} with open(config_path, 'w') as f: json.dump(config, f, indent=2, ensure_ascii=False) except Exception as e: print(f"更新配置失败: {e}") exit(1) PYEOF return $? fi # 使用 jq 更新配置 local tmp_file=$(mktemp) # 确保 plugins 和 channels 结构存在,并添加到 allow 列表 jq --arg plugin "$plugin_id" ' .plugins //= {"allow": [], "entries": {}} | .plugins.allow //= [] | .plugins.entries //= {} | .channels //= {} | .channels[$plugin] //= {"dmPolicy": "pairing", "groupPolicy": "allowlist"} | if (.plugins.allow | index($plugin)) then . else .plugins.allow += [$plugin] end | .plugins.entries[$plugin] = {"enabled": true} ' "$OPENCLAW_JSON" > "$tmp_file" if [ $? -eq 0 ] && [ -s "$tmp_file" ]; then mv "$tmp_file" "$OPENCLAW_JSON" log_info "已将 $plugin_id 添加到 plugins.allow" return 0 else rm -f "$tmp_file" log_error "更新 plugins.allow 失败" return 1 fi } # 从 plugins.allow / plugins.entries / channels 中移除插件 remove_plugin_from_allow() { local plugin_id="$1" if [ ! -f "$OPENCLAW_JSON" ]; then log_warn "配置文件不存在: $OPENCLAW_JSON" return 1 fi if ! command -v jq &> /dev/null; then python3 << PYEOF import json import os config_path = os.path.expanduser("$OPENCLAW_JSON") plugin_id = "$plugin_id" try: with open(config_path, 'r') as f: config = json.load(f) plugins = config.get('plugins', {}) allow = plugins.get('allow', []) if isinstance(allow, list): plugins['allow'] = [p for p in allow if p != plugin_id] entries = plugins.get('entries', {}) if isinstance(entries, dict) and plugin_id in entries: entries.pop(plugin_id, None) plugins['entries'] = entries config['plugins'] = plugins channels = config.get('channels', {}) if isinstance(channels, dict): channels.pop(plugin_id, None) config['channels'] = channels with open(config_path, 'w') as f: json.dump(config, f, indent=2, ensure_ascii=False) except Exception as e: print(f"更新配置失败: {e}") exit(1) PYEOF return $? fi local tmp_file tmp_file=$(mktemp) jq --arg plugin "$plugin_id" ' .plugins //= {"allow": [], "entries": {}} | .plugins.allow = ((.plugins.allow // []) | map(select(. != $plugin))) | .plugins.entries = ((.plugins.entries // {}) | del(.[$plugin])) | .channels = ((.channels // {}) | del(.[$plugin])) ' "$OPENCLAW_JSON" > "$tmp_file" if [ $? -eq 0 ] && [ -s "$tmp_file" ]; then mv "$tmp_file" "$OPENCLAW_JSON" log_info "已清理 $plugin_id 在本地配置中的残留" return 0 fi rm -f "$tmp_file" log_error "清理 $plugin_id 配置残留失败" return 1 } # 将逗号分隔字符串转换为 JSON 数组 build_json_array_from_csv() { local csv="$1" local json="[" local count=0 local item="" local trimmed="" local escaped="" IFS=',' read -r -a __csv_items <<< "$csv" for item in "${__csv_items[@]}"; do trimmed="$(echo "$item" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" [ -n "$trimmed" ] || continue escaped="${trimmed//\\/\\\\}" escaped="${escaped//\"/\\\"}" if [ $count -gt 0 ]; then json+="," fi json+="\"$escaped\"" count=$((count + 1)) done json+="]" echo "$json" } # 直接写入 channels..allowFrom,确保数组类型准确 set_channel_allow_from_json() { local channel="$1" local allow_json="$2" if [ ! -f "$OPENCLAW_JSON" ]; then openclaw config set "channels.${channel}.allowFrom" "$allow_json" > /dev/null 2>&1 return $? fi if ! command -v jq &> /dev/null; then python3 << PYEOF import json import os config_path = os.path.expanduser("$OPENCLAW_JSON") channel = "$channel" allow_from = json.loads('''$allow_json''') with open(config_path, 'r') as f: cfg = json.load(f) cfg.setdefault('channels', {}) cfg['channels'].setdefault(channel, {}) cfg['channels'][channel]['allowFrom'] = allow_from with open(config_path, 'w') as f: json.dump(cfg, f, indent=2, ensure_ascii=False) PYEOF return $? fi local tmp_file tmp_file=$(mktemp) jq --arg channel "$channel" --argjson allow "$allow_json" ' .channels //= {} | .channels[$channel] //= {} | .channels[$channel].allowFrom = $allow ' "$OPENCLAW_JSON" > "$tmp_file" if [ $? -eq 0 ] && [ -s "$tmp_file" ]; then mv "$tmp_file" "$OPENCLAW_JSON" return 0 fi rm -f "$tmp_file" return 1 } # 从环境变量文件读取配置 get_env_value() { local key=$1 if [ -f "$OPENCLAW_ENV" ]; then grep "^export $key=" "$OPENCLAW_ENV" 2>/dev/null | sed 's/.*=//' | tr -d '"' fi } # ================================ 测试功能 ================================ ensure_openclaw_on_path() { # 补充常见全局 bin,避免“已安装但当前 shell 不可见” local npm_prefix="" local npm_bin="" local candidate="" if command -v npm &> /dev/null; 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 } resolve_openclaw_bin() { ensure_openclaw_on_path if command -v openclaw &> /dev/null; then if [ "$(type -t openclaw 2>/dev/null)" = "function" ] && [ -n "$OPENCLAW_BIN_RESOLVED" ]; then echo "$OPENCLAW_BIN_RESOLVED" return 0 fi command -v openclaw return 0 fi if command -v claw &> /dev/null; then command -v claw return 0 fi if command -v npm &> /dev/null && command -v node &> /dev/null; 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 } ensure_openclaw_alias() { if command -v openclaw &> /dev/null; then return 0 fi OPENCLAW_BIN_RESOLVED="$(resolve_openclaw_bin || true)" if [ -z "$OPENCLAW_BIN_RESOLVED" ]; then return 1 fi # 当官方仅暴露 claw 时,动态注入 openclaw 兼容命令 openclaw() { "$OPENCLAW_BIN_RESOLVED" "$@" } return 0 } # 检查 OpenClaw 是否已安装 check_openclaw_installed() { ensure_openclaw_on_path command -v openclaw &> /dev/null || ensure_openclaw_alias } is_valid_port() { local p="$1" [[ "$p" =~ ^[0-9]+$ ]] && [ "$p" -ge 1 ] && [ "$p" -le 65535 ] } get_gateway_port() { local port="$DEFAULT_GATEWAY_PORT" if [ -f "$OPENCLAW_ENV" ]; then local env_port env_port="$(grep '^export OPENCLAW_GATEWAY_PORT=' "$OPENCLAW_ENV" 2>/dev/null | tail -1 | sed 's/^export OPENCLAW_GATEWAY_PORT=//')" env_port="$(echo "$env_port" | tr -d '"'\''[:space:]')" if is_valid_port "$env_port"; then port="$env_port" fi fi if check_openclaw_installed; then local cfg_port cfg_port="$(openclaw config get gateway.port 2>/dev/null || true)" cfg_port="$(echo "$cfg_port" | tr -d '"'\''[:space:]')" if is_valid_port "$cfg_port"; then port="$cfg_port" fi fi echo "$port" } get_gateway_host() { local host="$DEFAULT_GATEWAY_HOST" if [ -f "$OPENCLAW_ENV" ]; then local env_host env_host="$(grep '^export OPENCLAW_GATEWAY_HOST=' "$OPENCLAW_ENV" 2>/dev/null | tail -1 | sed 's/^export OPENCLAW_GATEWAY_HOST=//')" env_host="$(echo "$env_host" | tr -d '"'\''[:space:]')" if [ -n "$env_host" ]; then host="$env_host" fi fi if check_openclaw_installed; then local cfg_host cfg_host="$(openclaw config get gateway.host 2>/dev/null || true)" cfg_host="$(echo "$cfg_host" | tr -d '"'\''[:space:]')" if [ -n "$cfg_host" ] && [ "$cfg_host" != "undefined" ]; then host="$cfg_host" fi fi echo "$host" } upsert_env_export() { local key="$1" local value="$2" local file="$OPENCLAW_ENV" mkdir -p "$(dirname "$file")" 2>/dev/null || true touch "$file" 2>/dev/null || true local tmp_file tmp_file="$(mktemp)" awk -v k="$key" -v v="$value" ' BEGIN { done=0 } $0 ~ "^export " k "=" { print "export " k "=" v; done=1; next } { print } END { if (!done) print "export " k "=" v } ' "$file" > "$tmp_file" && mv "$tmp_file" "$file" chmod 600 "$file" 2>/dev/null || true } remove_env_export() { local key="$1" local file="$OPENCLAW_ENV" [ -f "$file" ] || return 0 local tmp_file tmp_file="$(mktemp)" awk -v k="$key" '$0 !~ "^export " k "=" { print }' "$file" > "$tmp_file" && mv "$tmp_file" "$file" chmod 600 "$file" 2>/dev/null || true } get_gateway_pid() { local gateway_port gateway_port="$(get_gateway_port)" get_port_pid "$gateway_port" } get_port_pid() { local port="$1" local pid="" if command -v lsof &> /dev/null; then pid=$(lsof -ti :"$port" 2>/dev/null | head -1) fi if [ -z "$pid" ] && command -v pgrep &> /dev/null; then pid=$(pgrep -f "openclaw gateway" 2>/dev/null | head -1) fi echo "$pid" } # 重启 Gateway 使渠道配置生效 restart_gateway_for_channel() { echo "" log_info "正在重启 Gateway..." local gateway_port gateway_port="$(get_gateway_port)" # 加载环境变量 if [ -f "$OPENCLAW_ENV" ]; then source "$OPENCLAW_ENV" fi # 先运行 doctor --fix 确保配置有效 echo -e "${YELLOW}检查配置...${NC}" yes | openclaw doctor --fix > /dev/null 2>&1 || true # 使用官方 restart 命令 local restart_output restart_output=$(openclaw gateway restart 2>&1) || true sleep 2 # 使用端口检测判断服务是否启动成功(更可靠) local gateway_pid gateway_pid=$(get_gateway_pid) if [ -n "$gateway_pid" ]; then log_info "Gateway 已重启!(PID: $gateway_pid)" echo "" # 获取并显示 Dashboard URL(带 token) echo -e "${CYAN}━━━ 获取 Dashboard URL ━━━${NC}" local dashboard_url=$(openclaw dashboard --no-open 2>/dev/null | grep -E "^https?://" | head -1) if [ -n "$dashboard_url" ]; then echo "" echo -e "${GREEN}✓ Dashboard URL (带授权 token):${NC}" echo -e " ${WHITE}$dashboard_url${NC}" echo "" echo -e "${YELLOW}⚠️ 请使用此 URL 访问控制界面,否则会提示 token_missing${NC}" else echo "" echo -e "${YELLOW}提示: 运行以下命令获取带 token 的 Dashboard URL:${NC}" echo -e " ${WHITE}openclaw dashboard${NC}" fi echo "" echo -e "${CYAN}查看日志: ${WHITE}openclaw logs --follow${NC}" echo -e "${CYAN}停止服务: ${WHITE}openclaw gateway stop${NC}" # 渠道状态探针(重启后) echo "" echo -e "${CYAN}━━━ 渠道状态探针 ━━━${NC}" openclaw channels list 2>/dev/null | head -12 | sed 's/^/ /' || echo " (无法获取渠道状态)" else log_warn "Gateway 可能未正常启动" echo "" echo -e "${YELLOW}命令输出:${NC}" echo "$restart_output" | head -10 | sed 's/^/ /' echo "" log_info "尝试按配置端口 ${gateway_port} 直接拉起 Gateway..." if command -v setsid &> /dev/null; then if [ -f "$OPENCLAW_ENV" ]; then setsid bash -c "source $OPENCLAW_ENV && 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 if [ -f "$OPENCLAW_ENV" ]; then nohup bash -c "source $OPENCLAW_ENV && 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 2 gateway_pid="$(get_gateway_pid)" if [ -n "$gateway_pid" ]; then log_info "按端口 ${gateway_port} 启动成功 (PID: $gateway_pid)" else echo -e "${CYAN}建议:${NC}" echo " • 运行 ${WHITE}openclaw doctor --fix${NC} 修复配置问题" echo " • 运行 ${WHITE}openclaw gateway --port ${gateway_port}${NC} 手动启动" fi fi } # 检查 OpenClaw Gateway 是否运行 check_gateway_running() { if check_openclaw_installed; then openclaw health &>/dev/null return $? fi return 1 } # 从 openclaw models status --json 读取当前默认模型 get_current_model_ref() { if ! check_openclaw_installed; then return 1 fi local model_ref="" if command -v node &> /dev/null; 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 command -v python3 &> /dev/null; 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" } # 测试 AI API 连接 test_ai_connection() { local provider=$1 local api_key=$2 local model=$3 local base_url=$4 echo "" echo -e "${CYAN}━━━ 测试 AI 配置 ━━━${NC}" echo "" if ! check_openclaw_installed; then log_error "OpenClaw 未安装" return 1 fi # 确保环境变量已加载 [ -f "$OPENCLAW_ENV" ] && source "$OPENCLAW_ENV" local target_model_ref="" if [ -n "$provider" ] && [ -n "$model" ]; then target_model_ref="${provider}/${model}" else target_model_ref="$(get_current_model_ref || true)" fi echo -e "${CYAN}当前模型配置:${NC}" openclaw models status 2>&1 | head -12 echo "" if [ -n "$target_model_ref" ]; then echo -e "${CYAN}目标模型:${NC} ${WHITE}${target_model_ref}${NC}" fi echo "" echo -e "${YELLOW}运行官方模型探针 (openclaw models status --probe --check)...${NC}" local probe_output="" local probe_exit=0 if [ -n "$provider" ]; then probe_output=$(openclaw models status --probe --check --probe-provider "$provider" --json 2>&1) probe_exit=$? else probe_output=$(openclaw models status --probe --check --json 2>&1) probe_exit=$? fi if [ $probe_exit -eq 0 ]; then log_info "OpenClaw AI 探针通过,模型认证链路正常。" return 0 fi log_warn "模型探针未通过,尝试一次本地 agent 调用获取详细报错..." local result="" local exit_code=1 if [ -n "$target_model_ref" ]; then result=$(openclaw agent --local --model "$target_model_ref" --message "只回复 OK" 2>&1) exit_code=$? else result=$(openclaw agent --local --message "只回复 OK" 2>&1) exit_code=$? fi echo "" if [ $exit_code -eq 0 ] && ! echo "$result" | grep -qiE "error|failed|401|403|Unknown model"; then log_info "OpenClaw AI 测试成功!" return 0 fi log_error "OpenClaw AI 测试失败" echo "" echo -e " ${RED}探针输出:${NC}" echo "$probe_output" | head -10 | sed 's/^/ /' echo "" echo -e " ${RED}调用输出:${NC}" echo "$result" | head -10 | sed 's/^/ /' echo "" echo " 诊断命令:" echo " openclaw doctor" echo " openclaw models status --probe --check --json" return 1 } # 测试 Telegram 机器人 test_telegram_bot() { local token=$1 local user_id=$2 echo "" echo -e "${CYAN}━━━ 测试 Telegram 机器人 ━━━${NC}" echo "" # 1. 验证 Token echo -e "${YELLOW}1. 验证 Bot Token...${NC}" local bot_info=$(curl -s "https://api.telegram.org/bot${token}/getMe" 2>/dev/null) if echo "$bot_info" | grep -q '"ok":true'; then local bot_name=$(echo "$bot_info" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['first_name'])" 2>/dev/null) local bot_username=$(echo "$bot_info" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['username'])" 2>/dev/null) log_info "Bot 验证成功: $bot_name (@$bot_username)" else log_error "Bot Token 无效" return 1 fi # 2. 发送测试消息 echo "" echo -e "${YELLOW}2. 发送测试消息...${NC}" local message="🦞 OpenClaw 测试消息 这是一条来自配置工具的测试消息。 如果你收到这条消息,说明 Telegram 机器人配置成功! 时间: $(date '+%Y-%m-%d %H:%M:%S')" local send_result=$(curl -s -X POST "https://api.telegram.org/bot${token}/sendMessage" \ -H "Content-Type: application/json" \ -d "{ \"chat_id\": \"$user_id\", \"text\": \"$message\", \"parse_mode\": \"HTML\" }" 2>/dev/null) if echo "$send_result" | grep -q '"ok":true'; then log_info "测试消息发送成功!请检查你的 Telegram" return 0 else local error=$(echo "$send_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('description', '未知错误'))" 2>/dev/null) log_error "消息发送失败: $error" echo "" echo -e "${YELLOW}提示: 请确保你已经先向机器人发送过消息${NC}" return 1 fi } # 测试 Discord 机器人 test_discord_bot() { local token=$1 local channel_id=$2 echo "" echo -e "${CYAN}━━━ 测试 Discord 机器人 ━━━${NC}" echo "" # 1. 验证 Token echo -e "${YELLOW}1. 验证 Bot Token...${NC}" local bot_info=$(curl -s "https://discord.com/api/v10/users/@me" \ -H "Authorization: Bot $token" 2>/dev/null) if echo "$bot_info" | grep -q '"id"'; then local bot_name=$(echo "$bot_info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('username', 'Unknown'))" 2>/dev/null) local bot_id=$(echo "$bot_info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id', ''))" 2>/dev/null) log_info "Bot 验证成功: $bot_name (ID: $bot_id)" else log_error "Bot Token 无效" return 1 fi # 2. 检查机器人所在的服务器 echo "" echo -e "${YELLOW}2. 检查机器人所在的服务器...${NC}" local guilds=$(curl -s "https://discord.com/api/v10/users/@me/guilds" \ -H "Authorization: Bot $token" 2>/dev/null) local guild_count=$(echo "$guilds" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) if [ "$guild_count" = "0" ] || [ -z "$guild_count" ]; then log_error "机器人尚未加入任何服务器!" echo "" echo -e "${YELLOW}请先邀请机器人到你的服务器:${NC}" echo " 1. Discord Developer Portal → 你的应用 → OAuth2 → URL Generator" echo " 2. Scopes 勾选: bot" echo " 3. Bot Permissions 勾选: View Channels, Send Messages" echo " 4. 复制链接并在浏览器中打开,选择服务器" echo "" echo -e "${WHITE}邀请链接示例:${NC}" echo " https://discord.com/oauth2/authorize?client_id=${bot_id}&scope=bot&permissions=3072" return 1 else log_info "机器人已加入 $guild_count 个服务器" # 显示服务器列表 echo "$guilds" | python3 -c " import sys, json guilds = json.load(sys.stdin) for g in guilds[:5]: print(f\" • {g['name']} (ID: {g['id']})\") if len(guilds) > 5: print(f' ... 还有 {len(guilds)-5} 个服务器') " 2>/dev/null fi # 3. 检查频道访问权限 echo "" echo -e "${YELLOW}3. 检查频道访问权限...${NC}" local channel_info=$(curl -s "https://discord.com/api/v10/channels/$channel_id" \ -H "Authorization: Bot $token" 2>/dev/null) if echo "$channel_info" | grep -q '"id"'; then local channel_name=$(echo "$channel_info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('name', 'Unknown'))" 2>/dev/null) local guild_id=$(echo "$channel_info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('guild_id', ''))" 2>/dev/null) log_info "频道访问正常: #$channel_name (服务器ID: $guild_id)" else local error=$(echo "$channel_info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('message', '未知错误'))" 2>/dev/null) log_error "无法访问频道: $error" echo "" if echo "$error" | grep -qi "Unknown Channel"; then echo -e "${YELLOW}频道 ID 可能不正确,请重新复制${NC}" else echo -e "${YELLOW}机器人可能不在该频道所在的服务器中${NC}" echo " 请确保机器人已被邀请到正确的服务器" fi return 1 fi # 4. 发送测试消息 echo "" echo -e "${YELLOW}4. 发送测试消息到频道...${NC}" # 使用单行消息避免 JSON 格式问题 local timestamp=$(date '+%Y-%m-%d %H:%M:%S') local message="🦞 **OpenClaw 测试消息** - 配置成功!时间: $timestamp" # 使用 python 正确编码 JSON local json_payload if command -v python3 &> /dev/null; then json_payload=$(python3 -c "import json; print(json.dumps({'content': '$message'}))" 2>/dev/null) else # 备用方案:简单消息 json_payload="{\"content\": \"$message\"}" fi local send_result=$(curl -s -X POST "https://discord.com/api/v10/channels/${channel_id}/messages" \ -H "Authorization: Bot $token" \ -H "Content-Type: application/json" \ -d "$json_payload" 2>/dev/null) if echo "$send_result" | grep -q '"id"'; then log_info "测试消息发送成功!请检查 Discord 频道" return 0 else local error=$(echo "$send_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('message', '未知错误'))" 2>/dev/null) local error_code=$(echo "$send_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('code', 0))" 2>/dev/null) log_error "消息发送失败: $error" echo "" # 根据错误类型给出修复建议 if echo "$error" | grep -qi "Missing Access"; then echo -e "${YELLOW}━━━ 修复 Missing Access 错误 ━━━${NC}" echo "" echo -e "${WHITE}可能原因:${NC}" echo " 1. 机器人未被邀请到该频道所在的服务器" echo " 2. 机器人在该频道没有发送消息权限" echo " 3. 频道 ID 不正确" echo "" echo -e "${WHITE}解决方法:${NC}" echo " 1. 确认机器人已被邀请到服务器:" echo " • 重新生成邀请链接并邀请机器人" echo " • OAuth2 → URL Generator → 勾选 bot" echo " • Bot Permissions 勾选: Send Messages, View Channels" echo "" echo " 2. 检查频道权限:" echo " • 右键频道 → 编辑频道 → 权限" echo " • 添加机器人角色,允许「发送消息」「查看频道」" echo "" echo " 3. 确认频道 ID 正确:" echo " • 开启开发者模式后,右键频道 → 复制 ID" echo " • 当前输入的频道 ID: ${WHITE}$channel_id${NC}" elif echo "$error" | grep -qi "Unknown Channel"; then echo -e "${YELLOW}提示: 频道 ID 无效,请检查是否正确复制${NC}" echo " 当前输入: $channel_id" elif echo "$error" | grep -qi "Cannot send messages"; then echo -e "${YELLOW}提示: 机器人没有在该频道发送消息的权限${NC}" echo " 右键频道 → 编辑频道 → 权限 → 添加机器人并允许发送消息" fi echo "" return 1 fi } # 测试 Slack 机器人 test_slack_bot() { local bot_token=$1 echo "" echo -e "${CYAN}━━━ 测试 Slack 机器人 ━━━${NC}" echo "" # 验证 Token echo -e "${YELLOW}验证 Bot Token...${NC}" local auth_result=$(curl -s "https://slack.com/api/auth.test" \ -H "Authorization: Bearer $bot_token" 2>/dev/null) if echo "$auth_result" | grep -q '"ok":true'; then local team=$(echo "$auth_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('team', 'Unknown'))" 2>/dev/null) local user=$(echo "$auth_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('user', 'Unknown'))" 2>/dev/null) log_info "Slack 验证成功: $user @ $team" return 0 else local error=$(echo "$auth_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('error', '未知错误'))" 2>/dev/null) log_error "验证失败: $error" return 1 fi } # 测试飞书机器人 test_feishu_bot() { local app_id=$1 local app_secret=$2 local chat_id=$3 echo "" echo -e "${CYAN}━━━ 测试飞书机器人 ━━━${NC}" echo "" # 1. 获取 tenant_access_token echo -e "${YELLOW}1. 获取 tenant_access_token...${NC}" local token_result=$(curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" \ -H "Content-Type: application/json" \ -d "{ \"app_id\": \"$app_id\", \"app_secret\": \"$app_secret\" }" 2>/dev/null) local code=$(echo "$token_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('code', -1))" 2>/dev/null) if [ "$code" != "0" ]; then local msg=$(echo "$token_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('msg', '未知错误'))" 2>/dev/null) log_error "获取 Token 失败: $msg" echo "" echo -e "${YELLOW}请检查:${NC}" echo " • App ID 和 App Secret 是否正确" echo " • 应用是否已发布" return 1 fi local access_token=$(echo "$token_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tenant_access_token', ''))" 2>/dev/null) log_info "Token 获取成功!" # 2. 获取机器人信息 echo "" echo -e "${YELLOW}2. 获取机器人信息...${NC}" local bot_info=$(curl -s "https://open.feishu.cn/open-apis/bot/v3/info" \ -H "Authorization: Bearer $access_token" 2>/dev/null) local bot_code=$(echo "$bot_info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('code', -1))" 2>/dev/null) if [ "$bot_code" = "0" ]; then local bot_name=$(echo "$bot_info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('bot', {}).get('app_name', 'Unknown'))" 2>/dev/null) log_info "机器人: $bot_name" else log_warn "无法获取机器人信息(可能需要添加机器人能力)" fi # 3. 发送测试消息(如果提供了 chat_id) if [ -n "$chat_id" ]; then echo "" echo -e "${YELLOW}3. 发送测试消息...${NC}" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') # 使用 Python 正确构建 JSON,确保 content 是字符串化的 JSON local request_body=$(python3 -c " import json message = '''🦞 OpenClaw 测试消息 这是一条来自配置工具的测试消息。 如果你收到这条消息,说明飞书机器人配置成功! 时间: $timestamp''' # content 必须是一个 JSON 字符串(字符串化的 JSON 对象) content_obj = {'text': message} content_str = json.dumps(content_obj, ensure_ascii=False) body = { 'receive_id': '$chat_id', 'msg_type': 'text', 'content': content_str } print(json.dumps(body, ensure_ascii=False)) " 2>/dev/null) echo -e "${GRAY}请求体: $request_body${NC}" local send_result=$(curl -s -X POST "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id" \ -H "Authorization: Bearer $access_token" \ -H "Content-Type: application/json" \ -d "$request_body" 2>/dev/null) echo -e "${GRAY}响应: $send_result${NC}" echo "" local send_code=$(echo "$send_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('code', -1))" 2>/dev/null) if [ "$send_code" = "0" ]; then log_info "测试消息发送成功!请检查飞书群组" else local send_msg=$(echo "$send_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('msg', '未知错误'))" 2>/dev/null) log_error "消息发送失败: $send_msg (code: $send_code)" echo "" echo -e "${YELLOW}提示:${NC}" echo " • 确保机器人已添加到群组" echo " • 确保有 im:message:send_as_bot 权限" echo " • 群组 ID 可在群设置中查看" fi else echo "" echo -e "${GREEN}✓ 飞书应用验证成功!${NC}" echo "" echo -e "${YELLOW}如需发送测试消息,请提供群组 Chat ID${NC}" echo -e "${GRAY}获取方式: 群设置 → 群信息 → 群号${NC}" fi return 0 } # 测试 Ollama 连接 test_ollama_connection() { local base_url=$1 local model=$2 echo "" echo -e "${CYAN}━━━ 测试 Ollama 连接 ━━━${NC}" echo "" # 1. 检查服务是否运行 echo -e "${YELLOW}1. 检查 Ollama 服务...${NC}" local health=$(curl -s "${base_url}/api/tags" 2>/dev/null) if [ -z "$health" ]; then log_error "无法连接到 Ollama 服务: $base_url" echo -e "${YELLOW}请确保 Ollama 正在运行: ollama serve${NC}" return 1 fi log_info "Ollama 服务运行正常" # 2. 检查模型是否存在 echo "" echo -e "${YELLOW}2. 检查模型 $model...${NC}" if echo "$health" | grep -q "\"name\":\"$model\""; then log_info "模型 $model 已安装" else log_warn "模型 $model 可能未安装" echo -e "${YELLOW}运行以下命令安装: ollama pull $model${NC}" fi # 3. 测试生成 echo "" echo -e "${YELLOW}3. 测试模型响应...${NC}" local response=$(curl -s "${base_url}/api/generate" \ -d "{\"model\": \"$model\", \"prompt\": \"Say hello\", \"stream\": false}" 2>/dev/null) if echo "$response" | grep -q '"response"'; then log_info "模型响应测试成功" return 0 else log_error "模型响应测试失败" return 1 fi } # 测试 WhatsApp (通过 openclaw status) test_whatsapp() { echo "" echo -e "${CYAN}━━━ 测试 WhatsApp 连接 ━━━${NC}" echo "" if check_openclaw_installed; then echo -e "${YELLOW}检查 WhatsApp 渠道状态...${NC}" echo "" openclaw status 2>/dev/null | grep -i whatsapp || echo "WhatsApp 渠道未配置" echo "" echo -e "${CYAN}提示: 使用 'openclaw channels login' 配置 WhatsApp${NC}" return 0 else log_warn "WhatsApp 测试需要 OpenClaw 已安装" echo -e "${YELLOW}请先完成 OpenClaw 安装${NC}" return 1 fi } # 测试 iMessage (通过 openclaw status) test_imessage() { echo "" echo -e "${CYAN}━━━ 测试 iMessage 连接 ━━━${NC}" echo "" if check_openclaw_installed; then echo -e "${YELLOW}检查 iMessage 渠道状态...${NC}" echo "" openclaw status 2>/dev/null | grep -i imessage || echo "iMessage 渠道未配置" return 0 else log_warn "iMessage 测试需要 OpenClaw 已安装" echo -e "${YELLOW}请先完成 OpenClaw 安装${NC}" return 1 fi } # 测试微信 (通过 openclaw status) test_wechat() { echo "" echo -e "${CYAN}━━━ 测试微信连接 ━━━${NC}" echo "" if check_openclaw_installed; then echo -e "${YELLOW}检查微信渠道状态...${NC}" echo "" openclaw status 2>/dev/null | grep -i wechat || echo "微信渠道未配置" return 0 else log_warn "微信测试需要 OpenClaw 已安装" echo -e "${YELLOW}请先完成 OpenClaw 安装${NC}" return 1 fi } # 运行 OpenClaw 诊断 (使用 openclaw doctor) run_openclaw_doctor() { echo "" echo -e "${CYAN}━━━ OpenClaw 诊断 ━━━${NC}" echo "" if check_openclaw_installed; then openclaw doctor return $? else log_error "OpenClaw 未安装" echo -e "${YELLOW}请先运行 install.sh 安装 OpenClaw${NC}" return 1 fi } # 运行 OpenClaw 状态检查 (使用 openclaw status) run_openclaw_status() { echo "" echo -e "${CYAN}━━━ OpenClaw 状态 ━━━${NC}" echo "" if check_openclaw_installed; then openclaw status return $? else log_error "OpenClaw 未安装" return 1 fi } # 运行 OpenClaw 健康检查 (使用 openclaw health) run_openclaw_health() { echo "" echo -e "${CYAN}━━━ Gateway 健康检查 ━━━${NC}" echo "" if check_openclaw_installed; then openclaw health return $? else log_error "OpenClaw 未安装" return 1 fi } run_official_model_onboard() { if ! check_openclaw_installed; then log_error "OpenClaw 未安装" return 1 fi echo "" log_info "启动官方模型配置向导: openclaw onboard" if [ -e /dev/tty ]; then openclaw onboard < /dev/tty else openclaw onboard fi } # ================================ 状态显示 ================================ show_status() { clear_screen print_header echo -e "${WHITE}📊 系统状态${NC}" print_divider echo "" # OpenClaw 服务状态 if check_openclaw_installed; then echo -e " ${GREEN}✓${NC} OpenClaw 已安装: $(openclaw --version 2>/dev/null || echo 'unknown')" # 使用端口检测判断服务运行状态(更可靠) local status_pid status_pid=$(get_gateway_pid) if [ -n "$status_pid" ]; then echo -e " ${GREEN}●${NC} 服务状态: ${GREEN}运行中${NC} (PID: $status_pid)" else echo -e " ${RED}●${NC} 服务状态: ${RED}已停止${NC}" fi else echo -e " ${RED}✗${NC} OpenClaw 未安装" fi echo "" # 当前配置 if [ -f "$OPENCLAW_ENV" ]; then echo "" echo -e " ${CYAN}当前配置:${NC}" # 显示 OpenClaw 模型配置 if check_openclaw_installed; then local default_model default_model="$(get_current_model_ref || true)" [ -z "$default_model" ] && default_model="未配置" echo -e " • 默认模型: ${WHITE}$default_model${NC}" fi # 检查 API Key 配置 if grep -q "ANTHROPIC_API_KEY" "$OPENCLAW_ENV" 2>/dev/null; then echo -e " • AI 提供商: ${WHITE}Anthropic${NC}" elif grep -q "OPENAI_API_KEY" "$OPENCLAW_ENV" 2>/dev/null; then echo -e " • AI 提供商: ${WHITE}OpenAI${NC}" elif grep -q "GOOGLE_API_KEY" "$OPENCLAW_ENV" 2>/dev/null; then echo -e " • AI 提供商: ${WHITE}Google${NC}" fi else echo -e " ${YELLOW}⚠${NC} 环境变量未配置" fi echo "" # 目录状态 echo -e " ${CYAN}目录结构:${NC}" [ -d "$CONFIG_DIR" ] && echo -e " ${GREEN}✓${NC} 配置目录: $CONFIG_DIR" || echo -e " ${RED}✗${NC} 配置目录" [ -f "$OPENCLAW_ENV" ] && echo -e " ${GREEN}✓${NC} 环境变量: $OPENCLAW_ENV" || echo -e " ${RED}✗${NC} 环境变量" [ -f "$OPENCLAW_JSON" ] && echo -e " ${GREEN}✓${NC} OpenClaw 配置: $OPENCLAW_JSON" || echo -e " ${YELLOW}⚠${NC} OpenClaw 配置" echo "" print_divider press_enter } # ================================ AI 模型配置 ================================ config_ai_model() { clear_screen print_header echo -e "${WHITE}🤖 AI 模型配置${NC}" print_divider echo "" echo -e "${CYAN}推荐优先使用官方向导(模型列表与参数始终最新)${NC}" if confirm "是否启动官方向导 openclaw onboard?" "y"; then if run_official_model_onboard; then log_info "官方模型配置完成。" press_enter return fi log_warn "官方向导执行失败,继续使用本地兼容配置菜单。" echo "" fi echo -e "${CYAN}选择 AI 提供商:${NC}" echo -e "${GRAY}提示: 支持自定义 API 地址(通过自定义 Provider 配置)${NC}" echo "" echo -e "${WHITE}主流服务商:${NC}" print_menu_item "1" "Anthropic Claude" "🟣" print_menu_item "2" "OpenAI GPT" "🟢" print_menu_item "3" "DeepSeek" "🔵" print_menu_item "4" "Kimi (Moonshot)" "🌙" print_menu_item "5" "Google Gemini" "🔴" echo "" echo -e "${WHITE}多模型网关:${NC}" print_menu_item "6" "OpenRouter (多模型网关)" "🔄" print_menu_item "7" "OpenCode (免费多模型)" "🆓" echo "" echo -e "${WHITE}快速推理:${NC}" print_menu_item "8" "Groq (超快推理)" "⚡" print_menu_item "9" "Mistral AI" "🌬️" echo "" echo -e "${WHITE}本地/企业:${NC}" print_menu_item "10" "Ollama 本地模型" "🟠" print_menu_item "11" "Azure OpenAI" "☁️" echo "" echo -e "${WHITE}国产/其他:${NC}" print_menu_item "12" "xAI Grok" "𝕏" print_menu_item "13" "智谱 GLM (Zai)" "🇨🇳" print_menu_item "14" "MiniMax" "🤖" echo "" echo -e "${WHITE}实验性:${NC}" print_menu_item "15" "Google Gemini CLI" "🧪" print_menu_item "16" "Google Antigravity" "🚀" echo "" echo -e "${WHITE}高性能推理:${NC}" print_menu_item "17" "Novita AI (Kimi/DeepSeek/GLM)" "🚀" echo "" print_menu_item "0" "返回主菜单" "↩️" echo "" echo -en "${YELLOW}请选择 [0-17]: ${NC}" read choice < "$TTY_INPUT" case $choice in 1) config_anthropic ;; 2) config_openai ;; 3) config_deepseek ;; 4) config_kimi ;; 5) config_google_gemini ;; 6) config_openrouter ;; 7) config_opencode ;; 8) config_groq ;; 9) config_mistral ;; 10) config_ollama ;; 11) config_azure_openai ;; 12) config_xai ;; 13) config_zai ;; 14) config_minimax ;; 15) config_google_gemini_cli ;; 16) config_google_antigravity ;; 17) config_novita ;; 0) return ;; *) log_error "无效选择"; press_enter; config_ai_model ;; esac } config_anthropic() { clear_screen print_header echo -e "${WHITE}🟣 配置 Anthropic Claude${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "ANTHROPIC_API_KEY") local current_url=$(get_env_value "ANTHROPIC_BASE_URL") local official_url="https://api.anthropic.com" # 显示当前配置 echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi if [ -n "$current_url" ]; then echo -e " API 地址: ${WHITE}$current_url${NC}" else echo -e " API 地址: ${GRAY}(使用官方)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}$official_url${NC}" echo -e "${GRAY}获取 Key: https://console.anthropic.com/${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key 和地址)" "🔄" print_menu_item "2" "完整配置 (可修改所有设置)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" local base_url="$current_url" if [ "$config_mode" = "2" ]; then echo "" echo -e "${CYAN}API 地址配置:${NC}" [ -n "$current_url" ] && echo -e " 当前地址: ${WHITE}$current_url${NC}" echo -e " 官方地址: ${WHITE}$official_url${NC}" echo "" read -p "$(echo -e "${YELLOW}输入 API 地址 (留空保持当前配置): ${NC}")" input_url < "$TTY_INPUT" # 留空时保持当前配置 if [ -n "$input_url" ]; then base_url="$input_url" fi echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "Claude Sonnet 4.6 (推荐, 官方默认)" "⭐" print_menu_item "2" "Claude Opus 4.6 (最强)" "👑" print_menu_item "3" "Claude 4.5 Haiku (快速)" "⚡" print_menu_item "4" "Claude Sonnet 4.5 (兼容)" "📦" print_menu_item "5" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-5] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="claude-sonnet-4-6" ;; 2) model="claude-opus-4-6" ;; 3) model="claude-haiku-4-5" ;; 4) model="claude-sonnet-4-5" ;; 5) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="claude-sonnet-4-6" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "anthropic" "$api_key" "$model" "$base_url" echo "" log_info "Anthropic Claude 配置完成!" log_info "模型: $model" [ -n "$base_url" ] && log_info "API 地址: $base_url" || log_info "API 地址: 官方" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "anthropic" "$api_key" "$model" "$base_url" fi press_enter } config_openai() { clear_screen print_header echo -e "${WHITE}🟢 配置 OpenAI GPT${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "OPENAI_API_KEY") local current_url=$(get_env_value "OPENAI_BASE_URL") local official_url="https://api.openai.com/v1" # 显示当前配置 echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi if [ -n "$current_url" ]; then echo -e " API 地址: ${WHITE}$current_url${NC}" else echo -e " API 地址: ${GRAY}(使用官方)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}$official_url${NC}" echo -e "${GRAY}获取 Key: https://platform.openai.com/${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key 和地址)" "🔄" print_menu_item "2" "完整配置 (可修改所有设置)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" local base_url="$current_url" if [ "$config_mode" = "2" ]; then echo "" echo -e "${CYAN}API 地址配置:${NC}" [ -n "$current_url" ] && echo -e " 当前地址: ${WHITE}$current_url${NC}" echo -e " 官方地址: ${WHITE}$official_url${NC}" echo "" read -p "$(echo -e "${YELLOW}输入 API 地址 (留空保持当前配置): ${NC}")" input_url < "$TTY_INPUT" if [ -n "$input_url" ]; then base_url="$input_url" fi echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "GPT-5.1-Codex (推荐, 官方默认)" "⭐" print_menu_item "2" "GPT-5.4 (最新通用)" "🚀" print_menu_item "3" "GPT-5.1" "⚡" print_menu_item "4" "GPT-5.1-Codex-Mini (经济)" "💰" print_menu_item "5" "GPT-4.1 (兼容)" "🧠" print_menu_item "6" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-6] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="gpt-5.1-codex" ;; 2) model="gpt-5.4" ;; 3) model="gpt-5.1" ;; 4) model="gpt-5.1-codex-mini" ;; 5) model="gpt-4.1" ;; 6) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="gpt-5.1-codex" ;; esac # 如果使用自定义 API 地址,询问 API 类型 local api_type="" if [ -n "$base_url" ]; then echo "" echo -e "${CYAN}选择 API 兼容格式:${NC}" echo "" print_menu_item "1" "openai-responses (OpenAI 官方 Responses API)" "🔵" print_menu_item "2" "openai-completions (兼容 /v1/chat/completions)" "🟢" echo "" echo -e "${GRAY}提示: 大多数第三方服务使用 openai-completions 格式${NC}" echo "" read -p "$(echo -e "${YELLOW}选择 API 格式 [1-2] (默认: 2): ${NC}")" api_type_choice < "$TTY_INPUT" case $api_type_choice in 1) api_type="openai-responses" ;; *) api_type="openai-completions" ;; esac fi # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "openai" "$api_key" "$model" "$base_url" "$api_type" echo "" log_info "OpenAI GPT 配置完成!" log_info "模型: $model" [ -n "$base_url" ] && log_info "API 地址: $base_url" || log_info "API 地址: 官方" [ -n "$api_type" ] && log_info "API 格式: $api_type" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "openai" "$api_key" "$model" "$base_url" fi press_enter } config_deepseek() { clear_screen print_header echo -e "${WHITE}🔵 配置 DeepSeek${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "DEEPSEEK_API_KEY") local current_url=$(get_env_value "DEEPSEEK_BASE_URL") local official_url="https://api.deepseek.com" # 显示当前配置 echo -e "${CYAN}DeepSeek 提供高性能 AI 模型,支持 OpenAI 兼容格式${NC}" echo "" echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi if [ -n "$current_url" ]; then echo -e " API 地址: ${WHITE}$current_url${NC}" else echo -e " API 地址: ${GRAY}(使用官方)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}$official_url${NC}" echo -e "${GRAY}获取 Key: https://platform.deepseek.com/${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key 和地址)" "🔄" print_menu_item "2" "完整配置 (可修改所有设置)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" local base_url="${current_url:-$official_url}" if [ "$config_mode" = "2" ]; then echo "" echo -e "${CYAN}API 地址配置:${NC}" [ -n "$current_url" ] && echo -e " 当前地址: ${WHITE}$current_url${NC}" echo -e " 官方地址: ${WHITE}$official_url${NC}" echo "" read -p "$(echo -e "${YELLOW}输入 API 地址 (留空保持当前配置): ${NC}")" input_url < "$TTY_INPUT" if [ -n "$input_url" ]; then base_url="$input_url" fi echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "deepseek-chat (V3.2, 推荐)" "⭐" print_menu_item "2" "deepseek-reasoner (R1, 推理)" "🧠" print_menu_item "3" "deepseek-coder (代码)" "💻" print_menu_item "4" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-4] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="deepseek-chat" ;; 2) model="deepseek-reasoner" ;; 3) model="deepseek-coder" ;; 4) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="deepseek-chat" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "deepseek" "$api_key" "$model" "$base_url" echo "" log_info "DeepSeek 配置完成!" log_info "模型: $model" log_info "API 地址: $base_url" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "deepseek" "$api_key" "$model" "$base_url" fi press_enter } config_kimi() { clear_screen print_header echo -e "${WHITE}🌙 配置 Kimi (Moonshot)${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "MOONSHOT_API_KEY") local current_url=$(get_env_value "MOONSHOT_BASE_URL") local official_url="https://api.moonshot.ai/v1" # 显示当前配置 echo -e "${CYAN}Kimi 是月之暗面(Moonshot AI)推出的大语言模型${NC}" echo "" echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi if [ -n "$current_url" ]; then echo -e " API 地址: ${WHITE}$current_url${NC}" else echo -e " API 地址: ${GRAY}(使用官方)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}https://api.moonshot.ai/v1${NC}" echo -e "${CYAN}国内 API: ${WHITE}https://api.moonshot.cn/v1${NC}" echo -e "${GRAY}获取 Key: https://platform.moonshot.cn/${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key 和地址)" "🔄" print_menu_item "2" "完整配置 (可修改所有设置)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" local base_url="${current_url:-$official_url}" if [ "$config_mode" = "2" ]; then echo "" echo -e "${CYAN}API 地址配置:${NC}" print_menu_item "1" "国际版 API (api.moonshot.ai)" "🌍" print_menu_item "2" "国内版 API (api.moonshot.cn)" "🇨🇳" read -p "$(echo -e "${YELLOW}区域选择 [1-2] (默认: 1): ${NC}")" moonshot_region < "$TTY_INPUT" if [ "${moonshot_region:-1}" = "2" ]; then base_url="https://api.moonshot.cn/v1" else base_url="https://api.moonshot.ai/v1" fi echo "" [ -n "$current_url" ] && echo -e " 当前地址: ${WHITE}$current_url${NC}" echo -e " 建议地址: ${WHITE}$base_url${NC}" echo "" read -p "$(echo -e "${YELLOW}输入 API 地址 (留空保持当前配置): ${NC}")" input_url < "$TTY_INPUT" if [ -n "$input_url" ]; then base_url="$input_url" fi echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "kimi-k2.5 (推荐, 官方默认)" "⭐" print_menu_item "2" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="kimi-k2.5" ;; 2) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="kimi-k2.5" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "moonshot" "$api_key" "$model" "$base_url" echo "" log_info "Kimi (Moonshot) 配置完成!" log_info "模型: $model" log_info "API 地址: $base_url" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "moonshot" "$api_key" "$model" "$base_url" fi press_enter } config_ollama() { clear_screen print_header echo -e "${WHITE}🟠 配置 Ollama 本地模型${NC}" print_divider echo "" # 获取当前配置 local current_url=$(get_env_value "OLLAMA_HOST") local default_url="http://localhost:11434" echo -e "${CYAN}Ollama 允许你在本地运行 AI 模型,无需 API Key${NC}" echo "" echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_url" ]; then echo -e " 服务地址: ${WHITE}$current_url${NC}" else echo -e " 服务地址: ${GRAY}(使用默认)${NC}" fi echo "" echo -e "${CYAN}默认地址: ${WHITE}$default_url${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前服务地址)" "🔄" print_menu_item "2" "完整配置 (可修改服务地址)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local ollama_url="${current_url:-$default_url}" if [ "$config_mode" = "2" ]; then echo "" echo -e "${CYAN}服务地址配置:${NC}" [ -n "$current_url" ] && echo -e " 当前地址: ${WHITE}$current_url${NC}" echo -e " 默认地址: ${WHITE}$default_url${NC}" echo "" read -p "$(echo -e "${YELLOW}输入服务地址 (留空保持当前配置): ${NC}")" input_url < "$TTY_INPUT" if [ -n "$input_url" ]; then ollama_url="$input_url" fi fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "Llama 3 (8B)" "🦙" print_menu_item "2" "Llama 3 (70B)" "🦙" print_menu_item "3" "Mistral" "🌬️" print_menu_item "4" "CodeLlama" "💻" print_menu_item "5" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-5] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="llama3" ;; 2) model="llama3:70b" ;; 3) model="mistral" ;; 4) model="codellama" ;; 5) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="llama3" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "ollama" "" "$model" "$ollama_url" echo "" log_info "Ollama 配置完成!" log_info "服务地址: $ollama_url" log_info "模型: $model" # 询问是否测试 echo "" if confirm "是否测试 Ollama 连接?" "y"; then test_ollama_connection "$ollama_url" "$model" fi press_enter } config_openrouter() { clear_screen print_header echo -e "${WHITE}🔵 配置 OpenRouter${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "OPENROUTER_API_KEY") local current_url=$(get_env_value "OPENROUTER_BASE_URL") local official_url="https://openrouter.ai/api/v1" # 显示当前配置 echo -e "${CYAN}OpenRouter 是一个多模型网关,支持多种 AI 模型${NC}" echo "" echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi if [ -n "$current_url" ]; then echo -e " API 地址: ${WHITE}$current_url${NC}" else echo -e " API 地址: ${GRAY}(使用默认)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}$official_url${NC}" echo -e "${GRAY}获取 Key: https://openrouter.ai/${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key 和地址)" "🔄" print_menu_item "2" "完整配置 (可修改所有设置)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" local base_url="${current_url:-$official_url}" if [ "$config_mode" = "2" ]; then echo "" echo -e "${CYAN}API 地址配置:${NC}" [ -n "$current_url" ] && echo -e " 当前地址: ${WHITE}$current_url${NC}" echo -e " 默认地址: ${WHITE}$official_url${NC}" echo "" read -p "$(echo -e "${YELLOW}输入 API 地址 (留空保持当前配置): ${NC}")" input_url < "$TTY_INPUT" if [ -n "$input_url" ]; then base_url="$input_url" fi echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "auto (推荐, 官方默认)" "🧭" print_menu_item "2" "anthropic/claude-opus-4.6" "🟣" print_menu_item "3" "openai/gpt-5.1-codex" "🟢" print_menu_item "4" "google/gemini-3.1-pro-preview" "🔴" print_menu_item "5" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-5] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="auto" ;; 2) model="anthropic/claude-opus-4.6" ;; 3) model="openai/gpt-5.1-codex" ;; 4) model="google/gemini-3.1-pro-preview" ;; 5) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="auto" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "openrouter" "$api_key" "$model" "$base_url" echo "" log_info "OpenRouter 配置完成!" log_info "模型: $model" log_info "API 地址: $base_url" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "openrouter" "$api_key" "$model" "$base_url" fi press_enter } config_google_gemini() { clear_screen print_header echo -e "${WHITE}🔴 配置 Google Gemini${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "GOOGLE_API_KEY") local current_url=$(get_env_value "GOOGLE_BASE_URL") local official_url="https://generativelanguage.googleapis.com" # 显示当前配置 echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi if [ -n "$current_url" ]; then echo -e " API 地址: ${WHITE}$current_url${NC}" else echo -e " API 地址: ${GRAY}(使用官方)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}$official_url${NC}" echo -e "${GRAY}获取 Key: https://aistudio.google.com/apikey${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key 和地址)" "🔄" print_menu_item "2" "完整配置 (可修改所有设置)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" local base_url="$current_url" if [ "$config_mode" = "2" ]; then echo "" echo -e "${CYAN}API 地址配置:${NC}" [ -n "$current_url" ] && echo -e " 当前地址: ${WHITE}$current_url${NC}" echo -e " 官方地址: ${WHITE}$official_url${NC}" echo "" read -p "$(echo -e "${YELLOW}输入 API 地址 (留空保持当前配置): ${NC}")" input_url < "$TTY_INPUT" if [ -n "$input_url" ]; then base_url="$input_url" fi echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "gemini-3.1-pro-preview (推荐, 官方默认)" "⭐" print_menu_item "2" "gemini-3-flash-preview" "🚀" print_menu_item "3" "gemini-2.5-pro" "📦" print_menu_item "4" "gemini-2.5-flash" "⚡" print_menu_item "5" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-5] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="gemini-3.1-pro-preview" ;; 2) model="gemini-3-flash-preview" ;; 3) model="gemini-2.5-pro" ;; 4) model="gemini-2.5-flash" ;; 5) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="gemini-3.1-pro-preview" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "google" "$api_key" "$model" "$base_url" echo "" log_info "Google Gemini 配置完成!" log_info "模型: $model" [ -n "$base_url" ] && log_info "API 地址: $base_url" || log_info "API 地址: 官方" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "google" "$api_key" "$model" "$base_url" fi press_enter } config_azure_openai() { clear_screen print_header echo -e "${WHITE}☁️ 配置 Azure OpenAI${NC}" print_divider echo "" echo -e "${CYAN}Azure OpenAI 需要以下信息:${NC}" echo " - Azure 端点 URL" echo " - API Key" echo " - 部署名称" echo "" read -p "$(echo -e "${YELLOW}输入 Azure 端点 URL: ${NC}")" azure_endpoint read_secret_input "${YELLOW}输入 API Key: ${NC}" api_key read -p "$(echo -e "${YELLOW}输入部署名称 (Deployment Name): ${NC}")" deployment_name read -p "$(echo -e "${YELLOW}API 版本 (默认: 2024-02-15-preview): ${NC}")" api_version api_version=${api_version:-"2024-02-15-preview"} if [ -n "$azure_endpoint" ] && [ -n "$api_key" ] && [ -n "$deployment_name" ]; then echo "" log_info "Azure OpenAI 配置完成!" log_info "端点: $azure_endpoint" log_info "部署: $deployment_name" else log_error "配置不完整,已取消" fi press_enter } config_groq() { clear_screen print_header echo -e "${WHITE}⚡ 配置 Groq${NC}" print_divider echo "" # 获取当前配置 (Groq 使用 OPENAI 环境变量) local current_key=$(get_env_value "OPENAI_API_KEY") local current_url=$(get_env_value "OPENAI_BASE_URL") local official_url="https://api.groq.com/openai/v1" # 显示当前配置 echo -e "${CYAN}Groq 提供超快的推理速度${NC}" echo "" echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi if [ -n "$current_url" ]; then echo -e " API 地址: ${WHITE}$current_url${NC}" else echo -e " API 地址: ${GRAY}(使用默认)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}$official_url${NC}" echo -e "${GRAY}获取 Key: https://console.groq.com/${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key 和地址)" "🔄" print_menu_item "2" "完整配置 (可修改所有设置)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" local base_url="${current_url:-$official_url}" if [ "$config_mode" = "2" ]; then echo "" echo -e "${CYAN}API 地址配置:${NC}" [ -n "$current_url" ] && echo -e " 当前地址: ${WHITE}$current_url${NC}" echo -e " 默认地址: ${WHITE}$official_url${NC}" echo "" read -p "$(echo -e "${YELLOW}输入 API 地址 (留空保持当前配置): ${NC}")" input_url < "$TTY_INPUT" if [ -n "$input_url" ]; then base_url="$input_url" fi echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "llama-3.3-70b-versatile (推荐)" "⭐" print_menu_item "2" "llama-3.1-8b-instant" "⚡" print_menu_item "3" "mixtral-8x7b-32768" "🌬️" print_menu_item "4" "gemma2-9b-it" "💎" print_menu_item "5" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-5] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="llama-3.3-70b-versatile" ;; 2) model="llama-3.1-8b-instant" ;; 3) model="mixtral-8x7b-32768" ;; 4) model="gemma2-9b-it" ;; 5) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="llama-3.3-70b-versatile" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "groq" "$api_key" "$model" "$base_url" echo "" log_info "Groq 配置完成!" log_info "模型: $model" log_info "API 地址: $base_url" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "groq" "$api_key" "$model" "$base_url" fi press_enter } config_mistral() { clear_screen print_header echo -e "${WHITE}🌬️ 配置 Mistral AI${NC}" print_divider echo "" # 获取当前配置 (Mistral 使用 OPENAI 环境变量) local current_key=$(get_env_value "OPENAI_API_KEY") local current_url=$(get_env_value "OPENAI_BASE_URL") local official_url="https://api.mistral.ai/v1" # 显示当前配置 echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi if [ -n "$current_url" ]; then echo -e " API 地址: ${WHITE}$current_url${NC}" else echo -e " API 地址: ${GRAY}(使用默认)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}$official_url${NC}" echo -e "${GRAY}获取 Key: https://console.mistral.ai/${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key 和地址)" "🔄" print_menu_item "2" "完整配置 (可修改所有设置)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" local base_url="${current_url:-$official_url}" if [ "$config_mode" = "2" ]; then echo "" echo -e "${CYAN}API 地址配置:${NC}" [ -n "$current_url" ] && echo -e " 当前地址: ${WHITE}$current_url${NC}" echo -e " 默认地址: ${WHITE}$official_url${NC}" echo "" read -p "$(echo -e "${YELLOW}输入 API 地址 (留空保持当前配置): ${NC}")" input_url < "$TTY_INPUT" if [ -n "$input_url" ]; then base_url="$input_url" fi echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "mistral-large-latest (推荐)" "⭐" print_menu_item "2" "mistral-small-latest" "⚡" print_menu_item "3" "codestral-latest" "💻" print_menu_item "4" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-4] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="mistral-large-latest" ;; 2) model="mistral-small-latest" ;; 3) model="codestral-latest" ;; 4) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="mistral-large-latest" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "mistral" "$api_key" "$model" "$base_url" echo "" log_info "Mistral AI 配置完成!" log_info "模型: $model" log_info "API 地址: $base_url" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "mistral" "$api_key" "$model" "$base_url" fi press_enter } config_xai() { clear_screen print_header echo -e "${WHITE}𝕏 配置 xAI Grok${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "XAI_API_KEY") local official_url="https://api.x.ai/v1" # 显示当前配置 echo -e "${CYAN}xAI 是 Elon Musk 创立的 AI 公司,提供 Grok 系列模型${NC}" echo "" echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}$official_url${NC}" echo -e "${GRAY}获取 Key: https://console.x.ai/${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key)" "🔄" print_menu_item "2" "完整配置 (可修改 API Key)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" if [ "$config_mode" = "2" ]; then echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "grok-4 (推荐, 官方默认)" "⭐" print_menu_item "2" "grok-4-fast" "⚡" print_menu_item "3" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-3] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="grok-4" ;; 2) model="grok-4-fast" ;; 3) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="grok-4" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "xai" "$api_key" "$model" "" echo "" log_info "xAI Grok 配置完成!" log_info "模型: $model" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "xai" "$api_key" "$model" "" fi press_enter } config_zai() { clear_screen print_header echo -e "${WHITE}🇨🇳 配置智谱 GLM (Zai)${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "ZAI_API_KEY") local official_url="https://open.bigmodel.cn/api/paas/v4" # 显示当前配置 echo -e "${CYAN}智谱 AI 是中国领先的 AI 公司,提供 GLM 系列模型${NC}" echo "" echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}$official_url${NC}" echo -e "${GRAY}获取 Key: https://open.bigmodel.cn/${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key)" "🔄" print_menu_item "2" "完整配置 (可修改 API Key)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" if [ "$config_mode" = "2" ]; then echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "glm-5 (推荐,最新)" "⭐" print_menu_item "2" "glm-4.7" "📦" print_menu_item "3" "glm-4.7-flash" "⚡" print_menu_item "4" "glm-4.7-flashx" "🔥" print_menu_item "5" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-5] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="glm-5" ;; 2) model="glm-4.7" ;; 3) model="glm-4.7-flash" ;; 4) model="glm-4.7-flashx" ;; 5) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="glm-5" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "zai" "$api_key" "$model" "" echo "" log_info "智谱 GLM 配置完成!" log_info "模型: $model" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "zai" "$api_key" "$model" "" fi press_enter } config_minimax() { clear_screen print_header echo -e "${WHITE}🤖 配置 MiniMax${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "MINIMAX_API_KEY") # 显示当前配置 echo -e "${CYAN}MiniMax 是中国领先的 AI 公司,提供大语言模型服务${NC}" echo "" echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi echo "" echo -e "${CYAN}获取 API Key:${NC}" echo -e " 🌍 国际版: ${WHITE}https://platform.minimax.io/${NC}" echo -e " 🇨🇳 国内版: ${WHITE}https://platform.minimaxi.com/${NC}" echo "" print_divider echo "" echo -e "${YELLOW}选择区域:${NC}" print_menu_item "1" "国际版 (minimax)" "🌍" print_menu_item "2" "国内版 (minimax-cn)" "🇨🇳" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" region_choice < "$TTY_INPUT" region_choice=${region_choice:-1} local provider="minimax" if [ "$region_choice" = "2" ]; then provider="minimax-cn" fi echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key)" "🔄" print_menu_item "2" "完整配置 (可修改 API Key)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" if [ "$config_mode" = "2" ]; then echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "MiniMax-M2.7 (推荐,最新旗舰)" "⭐" print_menu_item "2" "MiniMax-M2.7-highspeed (高速版)" "⚡" print_menu_item "3" "MiniMax-M2.5" "🔹" print_menu_item "4" "MiniMax-M2.5-highspeed" "🔹" print_menu_item "5" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-5] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="MiniMax-M2.7" ;; 2) model="MiniMax-M2.7-highspeed" ;; 3) model="MiniMax-M2.5" ;; 4) model="MiniMax-M2.5-highspeed" ;; 5) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="MiniMax-M2.7" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "$provider" "$api_key" "$model" "" echo "" log_info "MiniMax 配置完成!" log_info "区域: $provider" log_info "模型: $model" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "$provider" "$api_key" "$model" "" fi press_enter } config_opencode() { clear_screen print_header echo -e "${WHITE}🆓 配置 OpenCode${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "OPENCODE_API_KEY") local official_url="https://opencode.ai/zen/v1" # 显示当前配置 echo -e "${CYAN}OpenCode 是一个免费的多模型 API 网关${NC}" echo -e "${GREEN}✓ 支持多种模型: Claude, GPT, Gemini, GLM 等${NC}" echo -e "${GREEN}✓ 部分模型免费使用${NC}" echo "" echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}$official_url${NC}" echo -e "${GRAY}获取 Key: https://opencode.ai/auth${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key)" "🔄" print_menu_item "2" "完整配置 (可修改 API Key)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" if [ "$config_mode" = "2" ]; then echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择目录:${NC}" print_menu_item "1" "Zen 目录 (Claude/GPT/Gemini)" "🧘" print_menu_item "2" "Go 目录 (Kimi/GLM/MiniMax)" "🏃" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" catalog_choice < "$TTY_INPUT" catalog_choice=${catalog_choice:-1} local provider="opencode" if [ "$catalog_choice" = "2" ]; then provider="opencode-go" fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" if [ "$provider" = "opencode-go" ]; then print_menu_item "1" "kimi-k2.5 (推荐, Go 默认)" "⭐" print_menu_item "2" "glm-5" "🇨🇳" print_menu_item "3" "minimax-m2.5" "🤖" print_menu_item "4" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-4] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="kimi-k2.5" ;; 2) model="glm-5" ;; 3) model="minimax-m2.5" ;; 4) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="kimi-k2.5" ;; esac else print_menu_item "1" "claude-opus-4-6 (推荐, Zen 默认)" "⭐" print_menu_item "2" "gpt-5.1-codex" "💻" print_menu_item "3" "gpt-5.2" "🟢" print_menu_item "4" "gemini-3-pro (Gemini 3)" "🔴" print_menu_item "5" "glm-4.7 (免费)" "🆓" print_menu_item "6" "gpt-5.1-codex-mini" "⚡" print_menu_item "7" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-7] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="claude-opus-4-6" ;; 2) model="gpt-5.1-codex" ;; 3) model="gpt-5.2" ;; 4) model="gemini-3-pro" ;; 5) model="glm-4.7" ;; 6) model="gpt-5.1-codex-mini" ;; 7) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="claude-opus-4-6" ;; esac fi # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "$provider" "$api_key" "$model" "" echo "" log_info "OpenCode 配置完成!" log_info "目录: $provider" log_info "模型: $model" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "$provider" "$api_key" "$model" "" fi press_enter } config_novita() { clear_screen print_header echo -e "${WHITE}🚀 配置 Novita AI${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "NOVITA_API_KEY") local official_url="https://api.novita.ai/openai" # 显示当前配置 echo -e "${CYAN}Novita AI 是高性能 OpenAI 兼容推理平台,提供 Kimi、DeepSeek、GLM 等主流模型${NC}" echo "" echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}$official_url${NC}" echo -e "${GRAY}获取 Key: https://novita.ai/${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key)" "🔄" print_menu_item "2" "完整配置 (可修改 API Key)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" if [ "$config_mode" = "2" ]; then echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read -p "$(echo -e "${YELLOW}输入 API Key (留空保持不变): ${NC}")" input_key < "$TTY_INPUT" if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "moonshotai/kimi-k2.5 (推荐)" "⭐" print_menu_item "2" "deepseek/deepseek-v3.2" "🔵" print_menu_item "3" "zai-org/glm-5" "🇨🇳" print_menu_item "4" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-4] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="moonshotai/kimi-k2.5" ;; 2) model="deepseek/deepseek-v3.2" ;; 3) model="zai-org/glm-5" ;; 4) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="moonshotai/kimi-k2.5" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "novita" "$api_key" "$model" "" echo "" log_info "Novita AI 配置完成!" log_info "模型: $model" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "novita" "$api_key" "$model" "" fi press_enter } config_google_gemini_cli() { clear_screen print_header echo -e "${WHITE}🧪 配置 Google Gemini CLI${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "GOOGLE_API_KEY") local official_url="https://generativelanguage.googleapis.com" echo -e "${YELLOW}⚠️ 实验性功能${NC}" echo "" echo -e "${CYAN}Google Gemini CLI 提供最新的 Gemini 模型预览版${NC}" echo "" echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi echo "" echo -e "${CYAN}官方 API: ${WHITE}$official_url${NC}" echo -e "${GRAY}获取 Key: https://aistudio.google.com/apikey${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key)" "🔄" print_menu_item "2" "完整配置 (可修改 API Key)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" if [ "$config_mode" = "2" ]; then echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "gemini-3.1-pro-preview (推荐)" "⭐" print_menu_item "2" "gemini-3-flash-preview (快速)" "⚡" print_menu_item "3" "gemini-3.1-flash-lite-preview (轻量)" "🚀" print_menu_item "4" "gemini-2.5-pro (稳定)" "📦" print_menu_item "5" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-5] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="gemini-3.1-pro-preview" ;; 2) model="gemini-3-flash-preview" ;; 3) model="gemini-3.1-flash-lite-preview" ;; 4) model="gemini-2.5-pro" ;; 5) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="gemini-3.1-pro-preview" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "google-gemini-cli" "$api_key" "$model" "" echo "" log_info "Google Gemini CLI 配置完成!" log_info "模型: $model" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "google-gemini-cli" "$api_key" "$model" "" fi press_enter } config_google_antigravity() { clear_screen print_header echo -e "${WHITE}🚀 配置 Google Antigravity${NC}" print_divider echo "" # 获取当前配置 local current_key=$(get_env_value "GOOGLE_API_KEY") echo -e "${YELLOW}⚠️ 实验性功能${NC}" echo "" echo -e "${CYAN}Google Antigravity 是 Google 的实验性 AI 服务${NC}" echo -e "${CYAN}提供多种顶级模型的访问${NC}" echo "" echo -e "${CYAN}当前配置:${NC}" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e " API Key: ${WHITE}$masked_key${NC}" else echo -e " API Key: ${GRAY}(未配置)${NC}" fi echo "" echo -e "${GRAY}获取 API Key: 请联系 Google Cloud 获取访问权限${NC}" echo "" print_divider echo "" # 询问配置模式 echo -e "${YELLOW}选择配置模式:${NC}" print_menu_item "1" "仅更改模型 (保留当前 API Key)" "🔄" print_menu_item "2" "完整配置 (可修改 API Key)" "⚙️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-2] (默认: 1): ${NC}")" config_mode < "$TTY_INPUT" config_mode=${config_mode:-1} local api_key="$current_key" if [ "$config_mode" = "2" ]; then echo "" if [ -n "$current_key" ]; then local masked_key="${current_key:0:8}...${current_key: -4}" echo -e "当前 API Key: ${GRAY}$masked_key${NC}" fi read_secret_input "${YELLOW}输入 API Key (留空保持不变): ${NC}" input_key if [ -n "$input_key" ]; then api_key="$input_key" fi fi # 验证 API Key if [ -z "$api_key" ]; then log_error "API Key 不能为空,请先配置 API Key" press_enter return fi echo "" echo -e "${CYAN}选择模型:${NC}" echo "" print_menu_item "1" "gemini-3-pro-high (推荐)" "⭐" print_menu_item "2" "gemini-3-pro-low (快速)" "⚡" print_menu_item "3" "gemini-3-flash (闪电)" "🔥" print_menu_item "4" "claude-opus-4-6-thinking (Claude)" "🟣" print_menu_item "5" "claude-sonnet-4-6-thinking (思考)" "🧠" print_menu_item "6" "gpt-oss-120b-medium (GPT)" "🟢" print_menu_item "7" "自定义模型名称" "✏️" echo "" read -p "$(echo -e "${YELLOW}请选择 [1-7] (默认: 1): ${NC}")" model_choice < "$TTY_INPUT" model_choice=${model_choice:-1} case $model_choice in 1) model="gemini-3-pro-high" ;; 2) model="gemini-3-pro-low" ;; 3) model="gemini-3-flash" ;; 4) model="claude-opus-4-6-thinking" ;; 5) model="claude-sonnet-4-6-thinking" ;; 6) model="gpt-oss-120b-medium" ;; 7) read -p "$(echo -e "${YELLOW}输入模型名称: ${NC}")" model < "$TTY_INPUT" ;; *) model="gemini-3-pro-high" ;; esac # 保存到 OpenClaw 环境变量配置 save_openclaw_ai_config "google-antigravity" "$api_key" "$model" "" echo "" log_info "Google Antigravity 配置完成!" log_info "模型: $model" # 询问是否测试 echo "" if confirm "是否测试 API 连接?" "y"; then test_ai_connection "google-antigravity" "$api_key" "$model" "" fi press_enter } # ================================ 渠道配置 ================================ config_channels() { clear_screen print_header echo -e "${WHITE}📱 消息渠道配置${NC}" print_divider echo "" echo -e "${GRAY}详细文档: ~/.openclaw/docs/channels-configuration-guide.md${NC}" echo "" print_menu_item "1" "Telegram 机器人" "📨" print_menu_item "2" "Discord 机器人" "🎮" print_menu_item "3" "WhatsApp" "💬" print_menu_item "4" "Slack" "💼" print_menu_item "5" "飞书 (Feishu)" "🔷" print_menu_item "6" "Signal(官方)" "🔐" print_menu_item "7" "Microsoft Teams(官方插件)" "🏢" print_menu_item "8" "Mattermost(官方插件)" "🧩" print_menu_item "9" "Google Chat(官方插件)" "🟨" print_menu_item "10" "Matrix(官方插件)" "🔷" print_menu_item "11" "LINE(官方插件)" "🟩" print_menu_item "12" "Nextcloud Talk(官方插件)" "☁️" print_menu_item "13" "更多官方渠道" "🧭" print_menu_item "14" "钉钉/QQ/企业微信 官方状态检查" "🧾" print_menu_item "15" "微信(LangBot WeChatPad,社区)" "🟢" print_menu_item "16" "iMessage(旧版)" "🍎" print_menu_item "17" "QQ(社区插件,可选)" "🐧" print_menu_item "18" "企业微信(WeCom,社区插件)" "🏬" print_menu_item "0" "返回主菜单" "↩️" echo "" echo -en "${YELLOW}请选择 [0-18]: ${NC}" read choice < "$TTY_INPUT" case $choice in 1) config_telegram ;; 2) config_discord ;; 3) config_whatsapp ;; 4) config_slack ;; 5) config_feishu ;; 6) config_signal ;; 7) config_msteams ;; 8) config_mattermost ;; 9) config_googlechat ;; 10) config_matrix ;; 11) config_line ;; 12) config_nextcloud_talk ;; 13) config_more_official_channels ;; 14) check_cn_enterprise_channel_official_status ;; 15) config_wechat ;; 16) config_imessage ;; 17) config_qq_community ;; 18) config_wecom_community ;; 0) return ;; *) log_error "无效选择"; press_enter; config_channels ;; esac } config_telegram() { clear_screen print_header echo -e "${WHITE}📨 配置 Telegram 机器人${NC}" print_divider echo "" echo -e "${CYAN}配置步骤:${NC}" echo " 1. 在 Telegram 中搜索 @BotFather" echo " 2. 发送 /newbot 创建新机器人" echo " 3. 按提示设置名称,获取 Bot Token" echo " 4. 搜索 @userinfobot 获取你的 User ID" echo "" print_divider echo "" read -p "$(echo -e "${YELLOW}输入 Bot Token: ${NC}")" bot_token read -p "$(echo -e "${YELLOW}输入你的 User ID: ${NC}")" user_id if [ -n "$bot_token" ] && [ -n "$user_id" ]; then # 使用 openclaw 命令配置 if check_openclaw_installed; then echo "" log_info "正在配置 OpenClaw Telegram 渠道..." # 启用 Telegram 插件 echo -e "${YELLOW}启用 Telegram 插件...${NC}" openclaw plugins enable telegram 2>/dev/null || true ensure_plugin_in_allow "telegram" # 添加 Telegram channel echo -e "${YELLOW}添加 Telegram 账号...${NC}" if openclaw channels add --channel telegram --token "$bot_token" 2>/dev/null; then log_info "Telegram 渠道配置成功!" else log_warn "Telegram 渠道可能已存在或配置失败" fi echo "" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE}Telegram 配置完成!${NC}" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e "Bot Token: ${WHITE}${bot_token:0:10}...${NC}" echo -e "User ID: ${WHITE}$user_id${NC}" echo "" echo -e "${YELLOW}⚠️ 重要: 需要重启 Gateway 才能生效!${NC}" echo "" if confirm "是否现在重启 Gateway?" "y"; then restart_gateway_for_channel fi else log_error "OpenClaw 未安装,请先安装 OpenClaw" fi # 询问是否测试 echo "" if confirm "是否发送测试消息验证配置?" "y"; then test_telegram_bot "$bot_token" "$user_id" fi else log_error "配置不完整,已取消" fi press_enter } config_discord() { clear_screen print_header echo -e "${WHITE}🎮 配置 Discord 机器人${NC}" print_divider echo "" echo -e "${CYAN}第一步: 创建 Discord 应用和机器人${NC}" echo "" echo " 1. 访问 ${WHITE}https://discord.com/developers/applications${NC}" echo " 2. 点击 ${WHITE}New Application${NC} 创建新应用" echo " 3. 进入应用后,点击左侧 ${WHITE}Bot${NC} 菜单" echo " 4. 点击 ${WHITE}Reset Token${NC} 生成并复制 Bot Token" echo " 5. 开启 ${WHITE}Message Content Intent${NC} (重要!)" echo "" echo -e "${CYAN}第二步: 邀请机器人到服务器${NC}" echo "" echo " 1. 点击左侧 ${WHITE}OAuth2 → URL Generator${NC}" echo " 2. Scopes 勾选: ${WHITE}bot${NC}" echo " 3. Bot Permissions 至少勾选:" echo " • ${WHITE}View Channels${NC} (查看频道)" echo " • ${WHITE}Send Messages${NC} (发送消息)" echo " • ${WHITE}Read Message History${NC} (读取消息历史)" echo " 4. 复制生成的 URL,在浏览器打开并选择服务器" echo " 5. ${YELLOW}确保机器人在目标频道有权限!${NC}" echo "" echo -e "${CYAN}第三步: 获取频道 ID${NC}" echo "" echo " 1. 打开 Discord 客户端,进入 ${WHITE}用户设置 → 高级${NC}" echo " 2. 开启 ${WHITE}开发者模式${NC}" echo " 3. 右键点击你想让机器人响应的频道" echo " 4. 点击 ${WHITE}复制频道 ID${NC}" echo "" print_divider echo "" echo -en "${YELLOW}输入 Bot Token: ${NC}" read bot_token < "$TTY_INPUT" echo -en "${YELLOW}输入频道 ID (右键频道→复制ID): ${NC}" read channel_id < "$TTY_INPUT" if [ -n "$bot_token" ] && [ -n "$channel_id" ]; then # 使用 openclaw 命令配置 if check_openclaw_installed; then echo "" log_info "正在配置 OpenClaw Discord 渠道..." # 启用 Discord 插件 echo -e "${YELLOW}启用 Discord 插件...${NC}" openclaw plugins enable discord 2>/dev/null || true ensure_plugin_in_allow "discord" # 添加 Discord channel echo -e "${YELLOW}添加 Discord 账号...${NC}" if openclaw channels add --channel discord --token "$bot_token" 2>/dev/null; then log_info "Discord 渠道配置成功!" else log_warn "Discord 渠道可能已存在或配置失败" fi # 设置 groupPolicy 为 open(只响应 @ 机器人的消息) echo -e "${YELLOW}设置消息响应策略...${NC}" openclaw config set channels.discord.groupPolicy open 2>/dev/null || true log_info "已设置为: 响应 @机器人 的消息" echo "" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE}Discord 配置完成!${NC}" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e "${CYAN}使用方式: 在频道中 @机器人 发送消息${NC}" echo "" echo -e "${YELLOW}⚠️ 重要: 需要重启 Gateway 才能生效!${NC}" echo "" if confirm "是否现在重启 Gateway?" "y"; then restart_gateway_for_channel fi else log_error "OpenClaw 未安装,请先安装 OpenClaw" fi # 询问是否测试 echo "" if confirm "是否发送测试消息验证配置?" "y"; then test_discord_bot "$bot_token" "$channel_id" fi else log_error "配置不完整,已取消" fi press_enter } config_whatsapp() { clear_screen print_header echo -e "${WHITE}💬 配置 WhatsApp${NC}" print_divider echo "" echo -e "${CYAN}WhatsApp 配置需要扫描二维码登录${NC}" echo "" if ! check_openclaw_installed; then log_error "OpenClaw 未安装,请先运行安装脚本" press_enter return fi echo "配置步骤:" echo " 1. 启用 WhatsApp 插件" echo " 2. 扫描二维码登录" echo " 3. 重启 Gateway" echo "" if confirm "是否继续?"; then # 确保初始化 ensure_openclaw_init # 启用 WhatsApp 插件 echo "" log_info "启用 WhatsApp 插件..." openclaw plugins enable whatsapp 2>/dev/null || true ensure_plugin_in_allow "whatsapp" echo "" log_info "正在启动 WhatsApp 登录向导..." echo -e "${YELLOW}请扫描显示的二维码完成登录${NC}" echo "" # 使用 channels login 命令 openclaw channels login --channel whatsapp --verbose echo "" if confirm "是否重启 Gateway 使配置生效?" "y"; then restart_gateway_for_channel fi fi press_enter } config_slack() { clear_screen print_header echo -e "${WHITE}💼 配置 Slack${NC}" print_divider echo "" echo -e "${CYAN}配置步骤:${NC}" echo " 1. 访问 https://api.slack.com/apps" echo " 2. 创建新应用,选择 'From scratch'" echo " 3. 在 OAuth & Permissions 中添加所需权限" echo " 4. 安装应用到工作区并获取 Bot Token" echo "" print_divider echo "" read_secret_input "${YELLOW}输入 Bot Token (xoxb-...): ${NC}" bot_token read -p "$(echo -e "${YELLOW}输入 App Token (xapp-...): ${NC}")" app_token if [ -n "$bot_token" ] && [ -n "$app_token" ]; then # 使用 openclaw 命令配置 if check_openclaw_installed; then echo "" log_info "正在配置 OpenClaw Slack 渠道..." # 启用 Slack 插件 echo -e "${YELLOW}启用 Slack 插件...${NC}" openclaw plugins enable slack 2>/dev/null || true ensure_plugin_in_allow "slack" # 添加 Slack channel echo -e "${YELLOW}添加 Slack 账号...${NC}" if openclaw channels add --channel slack --bot-token "$bot_token" --app-token "$app_token" 2>/dev/null; then log_info "Slack 渠道配置成功!" else log_warn "Slack 渠道可能已存在或配置失败" fi echo "" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE}Slack 配置完成!${NC}" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e "${YELLOW}⚠️ 重要: 需要重启 Gateway 才能生效!${NC}" echo "" if confirm "是否现在重启 Gateway?" "y"; then restart_gateway_for_channel fi else log_info "Slack 配置完成!" fi # 询问是否测试 echo "" if confirm "是否验证 Slack 连接?" "y"; then test_slack_bot "$bot_token" fi else log_error "配置不完整,已取消" fi press_enter } get_official_channel_package() { local channel="$1" case "$channel" in signal) echo "@openclaw/signal" ;; msteams) echo "@openclaw/msteams" ;; mattermost) echo "@openclaw/mattermost" ;; googlechat) echo "@openclaw/googlechat" ;; matrix) echo "@openclaw/matrix" ;; line) echo "@openclaw/line" ;; nextcloud-talk) echo "@openclaw/nextcloud-talk" ;; irc) echo "@openclaw/irc" ;; twitch) echo "@openclaw/twitch" ;; zalo) echo "@openclaw/zalo" ;; zalouser) echo "@openclaw/zalouser" ;; nostr) echo "@openclaw/nostr" ;; tlon) echo "@openclaw/tlon" ;; synology-chat) echo "@openclaw/synology-chat" ;; bluebubbles) echo "@openclaw/bluebubbles" ;; *) echo "" ;; esac } config_official_plugin_channel() { local channel="$1" local display_name="$2" clear_screen print_header echo -e "${WHITE}🔌 配置 ${display_name}${NC}" print_divider echo "" if ! check_openclaw_installed; then log_error "OpenClaw 未安装" press_enter return fi ensure_openclaw_init local pkg pkg="$(get_official_channel_package "$channel")" if [ -z "$pkg" ]; then log_error "未找到 ${display_name} 对应的官方插件包映射" press_enter return fi echo -e "${CYAN}该渠道使用官方插件: ${WHITE}${pkg}${NC}" echo -e "${CYAN}将执行: 安装插件 -> 启用插件 -> 渠道配置向导${NC}" echo "" if confirm "是否安装/更新官方插件 ${pkg}?" "y"; then if openclaw plugins install "$pkg" --pin; then log_info "官方插件安装成功: $pkg" else log_warn "插件安装失败,继续尝试使用已安装版本配置渠道" fi fi openclaw plugins enable "$channel" 2>/dev/null || true ensure_plugin_in_allow "$channel" echo "" log_info "启动渠道配置向导..." if openclaw channels add --channel "$channel"; then log_info "${display_name} 渠道配置成功!" else log_warn "${display_name} 渠道向导未成功完成,可稍后重试。" echo -e "${CYAN}手动命令:${NC} ${WHITE}openclaw channels add --channel ${channel}${NC}" fi echo "" if confirm "是否重启 Gateway 使配置生效?" "y"; then restart_gateway_for_channel fi press_enter } config_signal() { config_official_plugin_channel "signal" "Signal(官方)"; } config_msteams() { config_official_plugin_channel "msteams" "Microsoft Teams(官方插件)"; } config_mattermost() { config_official_plugin_channel "mattermost" "Mattermost(官方插件)"; } config_googlechat() { config_official_plugin_channel "googlechat" "Google Chat(官方插件)"; } config_matrix() { config_official_plugin_channel "matrix" "Matrix(官方插件)"; } config_line() { config_official_plugin_channel "line" "LINE(官方插件)"; } config_nextcloud_talk() { config_official_plugin_channel "nextcloud-talk" "Nextcloud Talk(官方插件)"; } config_more_official_channels() { clear_screen print_header echo -e "${WHITE}🧭 更多官方渠道${NC}" print_divider echo "" print_menu_item "1" "IRC(官方插件)" "💻" print_menu_item "2" "Twitch(官方插件)" "🟣" print_menu_item "3" "Zalo(官方插件)" "🇻🇳" print_menu_item "4" "Zalo Personal(官方插件)" "📱" print_menu_item "5" "Nostr(官方插件)" "🌐" print_menu_item "6" "Tlon(官方插件)" "🪐" print_menu_item "7" "Synology Chat(官方插件)" "🗄️" print_menu_item "8" "BlueBubbles(官方)" "🔵" print_menu_item "0" "返回" "↩️" echo "" echo -en "${YELLOW}请选择 [0-8]: ${NC}" read choice < "$TTY_INPUT" case $choice in 1) config_official_plugin_channel "irc" "IRC(官方插件)" ;; 2) config_official_plugin_channel "twitch" "Twitch(官方插件)" ;; 3) config_official_plugin_channel "zalo" "Zalo(官方插件)" ;; 4) config_official_plugin_channel "zalouser" "Zalo Personal(官方插件)" ;; 5) config_official_plugin_channel "nostr" "Nostr(官方插件)" ;; 6) config_official_plugin_channel "tlon" "Tlon(官方插件)" ;; 7) config_official_plugin_channel "synology-chat" "Synology Chat(官方插件)" ;; 8) config_official_plugin_channel "bluebubbles" "BlueBubbles(官方)" ;; 0) return ;; *) log_error "无效选择"; press_enter; config_more_official_channels ;; esac } check_cn_enterprise_channel_official_status() { clear_screen print_header echo -e "${WHITE}🧾 国内企业渠道官方适配检查${NC}" print_divider echo "" echo -e "${CYAN}对照基线: 官方扩展目录 + 官方 channels 文档(最新版)${NC}" echo "" echo -e " • 钉钉(DingTalk): ${YELLOW}当前未发现官方插件${NC}" echo -e " • QQ: ${YELLOW}当前未发现官方插件${NC}" echo -e " • 企业微信(WeCom): ${YELLOW}当前未发现官方插件${NC}" echo "" echo -e "${CYAN}可选社区方案(非官方,需自行评估风险):${NC}" echo " • QQ: @sliverp/qqbot(本菜单提供安装/探针/回滚)" echo " • 微信: openclaw-wechat-channel(按 LangBot WeChatPad 适配流程)" echo " • 企业微信: @marshulll/openclaw-wecom(本菜单提供安装/探针/回滚)" echo "" echo -e "${CYAN}已纳入的官方企业渠道替代项:${NC}" echo " • 飞书(Feishu)" echo " • Microsoft Teams(插件)" echo " • Slack" echo " • Google Chat(插件)" echo " • Mattermost(插件)" echo "" echo -e "${YELLOW}说明:${NC} 为避免与官方能力漂移,本安装器仅默认纳入已发布的官方渠道插件。" echo -e "${YELLOW}补充:${NC} 企业微信/QQ/微信目前按社区插件方案接入。" echo "" press_enter } probe_wecom_community_config() { echo "" echo -e "${CYAN}━━━ WeCom 社区插件探针 ━━━${NC}" echo "" if ! check_openclaw_installed; then log_error "OpenClaw 未安装,无法探针" return 1 fi local plugin_ok=false local channel_ok=false local mode="" local bot_token="" local app_corp="" if openclaw plugins list 2>/dev/null | grep -Eqi "openclaw-wecom|wecom"; then plugin_ok=true log_info "WeCom 插件已安装" else log_warn "未检测到 WeCom 插件" fi if openclaw channels list 2>/dev/null | grep -qi "wecom"; then channel_ok=true log_info "WeCom 渠道已注册" else log_warn "WeCom 渠道未在 channels list 中出现" fi mode="$(openclaw config get channels.wecom.mode 2>/dev/null || true)" [ -z "$mode" ] || [ "$mode" = "undefined" ] && mode="both" echo -e "${CYAN}当前模式:${NC} ${WHITE}${mode}${NC}" bot_token="$(openclaw config get channels.wecom.accounts.bot.token 2>/dev/null || true)" app_corp="$(openclaw config get channels.wecom.accounts.app.corpId 2>/dev/null || true)" if [ "$mode" = "bot" ] || [ "$mode" = "both" ]; then if [ -n "$bot_token" ] && [ "$bot_token" != "undefined" ]; then log_info "Bot 模式关键字段已配置" else log_warn "Bot 模式 token 未配置" fi fi if [ "$mode" = "app" ] || [ "$mode" = "both" ]; then if [ -n "$app_corp" ] && [ "$app_corp" != "undefined" ]; then log_info "App 模式关键字段已配置" else log_warn "App 模式 corpId 未配置" fi fi echo "" echo -e "${CYAN}诊断输出:${NC}" openclaw doctor 2>&1 | head -10 | sed 's/^/ /' if [ "$plugin_ok" = true ] && [ "$channel_ok" = true ]; then log_info "WeCom 配置探针通过" return 0 fi echo "" echo -e "${YELLOW}排障建议:${NC}" echo " 1) openclaw doctor --fix" echo " 2) openclaw plugins update --all" echo " 3) 重新执行企业微信配置向导" return 1 } rollback_wecom_community_config() { echo "" echo -e "${WHITE}♻️ 回滚企业微信(WeCom)社区配置${NC}" print_divider echo "" echo -e "${YELLOW}将执行:${NC}" echo " • 禁用并卸载 WeCom 社区插件" echo " • 清理 channels.wecom 与 plugins.allow 残留" echo "" if ! confirm "确认执行回滚?" "n"; then log_info "已取消回滚" return 0 fi openclaw plugins disable wecom > /dev/null 2>&1 || true openclaw plugins disable openclaw-wecom > /dev/null 2>&1 || true openclaw plugins uninstall wecom --keep-files > /dev/null 2>&1 || true openclaw plugins uninstall openclaw-wecom --keep-files > /dev/null 2>&1 || true openclaw plugins uninstall "$WECOM_PLUGIN_COMMUNITY" --keep-files > /dev/null 2>&1 || true if openclaw config --help 2>/dev/null | grep -q "unset"; then openclaw config unset channels.wecom > /dev/null 2>&1 || true openclaw config unset plugins.entries.wecom > /dev/null 2>&1 || true openclaw config unset plugins.entries.openclaw-wecom > /dev/null 2>&1 || true else openclaw config set channels.wecom.enabled false > /dev/null 2>&1 || true fi remove_plugin_from_allow "wecom" || true remove_plugin_from_allow "openclaw-wecom" || true log_info "企业微信配置回滚完成" if confirm "是否重启 Gateway 使回滚生效?" "y"; then restart_gateway_for_channel fi return 0 } config_wecom_community_setup() { if ! check_openclaw_installed; then log_error "OpenClaw 未安装" return 1 fi ensure_openclaw_init local plugin_version="${OPENCLAW_WECOM_PLUGIN_VERSION:-$WECOM_PLUGIN_VERSION_DEFAULT}" local plugin_spec="${WECOM_PLUGIN_COMMUNITY}@${plugin_version}" local mode_choice mode local default_account local bot_webhook="$WECOM_WEBHOOK_BOT_DEFAULT" local bot_token="" local bot_aes="" local bot_receive_id="" local app_webhook="$WECOM_WEBHOOK_APP_DEFAULT" local corp_id="" local corp_secret="" local agent_id="" local callback_token="" local callback_aes="" echo -e "${YELLOW}⚠️ 风险提示:${NC}" echo " • 企业微信当前不是 OpenClaw 官方渠道,依赖社区插件" echo " • 插件固定版本安装,降低升级漂移风险" echo "" echo -e "${CYAN}将安装:${NC} ${WHITE}${plugin_spec}${NC}" if ! confirm "继续安装并配置企业微信插件?" "n"; then log_info "已取消企业微信配置" return 0 fi if openclaw plugins install "$plugin_spec" --pin; then log_info "企业微信插件安装成功" else log_warn "插件安装失败,尝试继续使用已安装版本" if ! openclaw plugins list 2>/dev/null | grep -Eqi "openclaw-wecom|wecom"; then log_error "未检测到企业微信插件,无法继续" return 1 fi fi openclaw plugins enable wecom > /dev/null 2>&1 || openclaw plugins enable openclaw-wecom > /dev/null 2>&1 || true ensure_plugin_in_allow "wecom" ensure_plugin_in_allow "openclaw-wecom" openclaw channels add --channel wecom > /dev/null 2>&1 || true echo "" echo -e "${CYAN}选择企业微信接入模式:${NC}" print_menu_item "1" "Bot 模式(回调 JSON)" "🤖" print_menu_item "2" "App 模式(内部应用 XML 回调)" "🏢" print_menu_item "3" "双模式(推荐)" "🔀" echo "" read_input "${YELLOW}请选择 [1-3] (默认: 3): ${NC}" mode_choice mode_choice="${mode_choice:-3}" case "$mode_choice" in 1) mode="bot"; default_account="bot" ;; 2) mode="app"; default_account="app" ;; *) mode="both"; default_account="bot" ;; esac if [ "$mode" = "bot" ] || [ "$mode" = "both" ]; then echo "" echo -e "${CYAN}Bot 模式参数:${NC}" read_input "${YELLOW}Bot webhookPath(默认 ${bot_webhook}): ${NC}" bot_webhook bot_webhook="${bot_webhook:-$WECOM_WEBHOOK_BOT_DEFAULT}" read_secret_input "${YELLOW}Bot Token: ${NC}" bot_token read_secret_input "${YELLOW}Bot EncodingAESKey: ${NC}" bot_aes read_input "${YELLOW}Bot ReceiveId(aibotid): ${NC}" bot_receive_id if [ -z "$bot_token" ] || [ -z "$bot_aes" ] || [ -z "$bot_receive_id" ]; then log_error "Bot 模式参数不完整" return 1 fi fi if [ "$mode" = "app" ] || [ "$mode" = "both" ]; then echo "" echo -e "${CYAN}App 模式参数:${NC}" read_input "${YELLOW}App webhookPath(默认 ${app_webhook}): ${NC}" app_webhook app_webhook="${app_webhook:-$WECOM_WEBHOOK_APP_DEFAULT}" read_input "${YELLOW}CorpID: ${NC}" corp_id read_secret_input "${YELLOW}CorpSecret: ${NC}" corp_secret read_input "${YELLOW}AgentID(数字): ${NC}" agent_id read_secret_input "${YELLOW}Callback Token: ${NC}" callback_token read_secret_input "${YELLOW}Callback AES Key: ${NC}" callback_aes if [ -z "$corp_id" ] || [ -z "$corp_secret" ] || [ -z "$agent_id" ] || [ -z "$callback_token" ] || [ -z "$callback_aes" ]; then log_error "App 模式参数不完整" return 1 fi if ! [[ "$agent_id" =~ ^[0-9]+$ ]]; then log_error "AgentID 必须是数字" return 1 fi fi openclaw config set channels.wecom.enabled true > /dev/null 2>&1 || true openclaw config set channels.wecom.mode "$mode" > /dev/null 2>&1 || true openclaw config set channels.wecom.defaultAccount "$default_account" > /dev/null 2>&1 || true if [ "$mode" = "bot" ] || [ "$mode" = "both" ]; then openclaw config set channels.wecom.accounts.bot.mode bot > /dev/null 2>&1 || true openclaw config set channels.wecom.accounts.bot.webhookPath "$bot_webhook" > /dev/null 2>&1 || true openclaw config set channels.wecom.accounts.bot.token "$bot_token" > /dev/null 2>&1 || true openclaw config set channels.wecom.accounts.bot.encodingAESKey "$bot_aes" > /dev/null 2>&1 || true openclaw config set channels.wecom.accounts.bot.receiveId "$bot_receive_id" > /dev/null 2>&1 || true fi if [ "$mode" = "app" ] || [ "$mode" = "both" ]; then openclaw config set channels.wecom.accounts.app.mode app > /dev/null 2>&1 || true openclaw config set channels.wecom.accounts.app.webhookPath "$app_webhook" > /dev/null 2>&1 || true openclaw config set channels.wecom.accounts.app.corpId "$corp_id" > /dev/null 2>&1 || true openclaw config set channels.wecom.accounts.app.corpSecret "$corp_secret" > /dev/null 2>&1 || true openclaw config set channels.wecom.accounts.app.agentId "$agent_id" > /dev/null 2>&1 || true openclaw config set channels.wecom.accounts.app.callbackToken "$callback_token" > /dev/null 2>&1 || true openclaw config set channels.wecom.accounts.app.callbackAesKey "$callback_aes" > /dev/null 2>&1 || true fi log_info "企业微信 WeCom 配置完成" if confirm "是否立即执行 WeCom 配置探针?" "y"; then probe_wecom_community_config || true fi if confirm "是否重启 Gateway 使配置生效?" "y"; then restart_gateway_for_channel fi return 0 } config_wecom_community() { clear_screen print_header echo -e "${WHITE}🏬 企业微信(WeCom,社区插件)${NC}" print_divider echo "" echo " 1) 安装并配置企业微信插件" echo " 2) 企业微信配置探针" echo " 3) 回滚企业微信配置" echo " 0) 返回" echo "" read_input "${YELLOW}请选择 [0-3]: ${NC}" wecom_choice case "$wecom_choice" in 1) config_wecom_community_setup ;; 2) probe_wecom_community_config ;; 3) rollback_wecom_community_config ;; 0) return ;; *) log_error "无效选择" ;; esac press_enter } probe_qq_community_config() { echo "" echo -e "${CYAN}━━━ QQ 社区插件探针 ━━━${NC}" if ! check_openclaw_installed; then log_error "OpenClaw 未安装,无法探针" return 1 fi local plugin_ok=false local channel_ok=false local allow_value="" if openclaw plugins list 2>/dev/null | grep -qi "qqbot"; then plugin_ok=true log_info "qqbot 插件已安装" else log_warn "qqbot 插件未在 plugins list 中出现" fi if openclaw channels list 2>/dev/null | grep -qi "qqbot"; then channel_ok=true log_info "qqbot 渠道已注册" else log_warn "qqbot 渠道未在 channels list 中出现" fi allow_value="$(openclaw config get channels.qqbot.allowFrom 2>/dev/null || true)" if [ -n "$allow_value" ] && [ "$allow_value" != "undefined" ]; then echo -e "${CYAN}allowFrom:${NC} ${WHITE}$allow_value${NC}" if echo "$allow_value" | grep -q '\*'; then log_warn "当前 allowFrom 包含 *,表示允许所有来源" fi else log_warn "未检测到 channels.qqbot.allowFrom(插件默认可能为允许全部)" fi echo "" echo -e "${CYAN}诊断输出:${NC}" if openclaw plugins --help 2>/dev/null | grep -q "doctor"; then openclaw plugins doctor 2>&1 | head -10 | sed 's/^/ /' else openclaw doctor 2>&1 | head -10 | sed 's/^/ /' fi if [ "$plugin_ok" = true ] && [ "$channel_ok" = true ]; then log_info "QQ 社区配置探针通过" return 0 fi echo "" echo -e "${YELLOW}排障建议:${NC}" echo " 1) openclaw doctor --fix" echo " 2) openclaw plugins update --all" echo " 3) 重新执行 QQ 社区配置向导" return 1 } rollback_qq_community_config() { echo "" echo -e "${WHITE}♻️ 回滚 QQ 社区配置${NC}" print_divider echo "" echo -e "${YELLOW}将执行:${NC}" echo " • 禁用 qqbot 插件" echo " • 卸载 qqbot 插件(保留插件文件)" echo " • 清理 channels.qqbot 与 plugins.allow 中的 qqbot" echo "" if ! confirm "确认执行回滚?" "n"; then log_info "已取消回滚" return 0 fi openclaw plugins disable qqbot > /dev/null 2>&1 || true openclaw plugins uninstall qqbot --keep-files > /dev/null 2>&1 || true if openclaw config --help 2>/dev/null | grep -q "unset"; then openclaw config unset channels.qqbot > /dev/null 2>&1 || true openclaw config unset plugins.entries.qqbot > /dev/null 2>&1 || true else openclaw config set channels.qqbot.enabled false > /dev/null 2>&1 || true openclaw config set channels.qqbot.allowFrom "[]" > /dev/null 2>&1 || true fi remove_plugin_from_allow "qqbot" || true log_info "QQ 社区配置回滚完成" if confirm "是否重启 Gateway 使回滚生效?" "y"; then restart_gateway_for_channel fi return 0 } config_qq_community_setup() { if ! check_openclaw_installed; then log_error "OpenClaw 未安装" return 1 fi ensure_openclaw_init local plugin_version="${OPENCLAW_QQ_PLUGIN_VERSION:-$QQ_PLUGIN_VERSION_DEFAULT}" local plugin_spec="${QQ_PLUGIN_COMMUNITY}@${plugin_version}" local app_id="" local app_secret="" local allow_input="" local allow_json="" echo -e "${YELLOW}⚠️ 风险提示:${NC}" echo " • QQ 当前不是官方渠道,依赖社区插件" echo " • 仅建议用于测试或内部环境,生产环境请评估安全风险" echo " • 本安装器会固定版本安装,避免 latest 漂移" echo "" if ! confirm "继续安装并配置 QQ 社区插件?" "n"; then log_info "已取消 QQ 配置" return 0 fi echo "" echo -e "${CYAN}安装插件: ${WHITE}${plugin_spec}${NC}" if openclaw plugins install "$plugin_spec" --pin; then log_info "QQ 社区插件安装成功" else log_warn "插件安装失败,尝试继续使用已安装版本" if ! openclaw plugins list 2>/dev/null | grep -qi "qqbot"; then log_error "未检测到 qqbot 已安装,无法继续" return 1 fi fi openclaw plugins enable qqbot > /dev/null 2>&1 || true ensure_plugin_in_allow "qqbot" echo "" echo -e "${CYAN}请输入 QQ 机器人凭据(官方 Bot 平台)${NC}" read_input "${YELLOW}AppID: ${NC}" app_id read_secret_input "${YELLOW}AppSecret: ${NC}" app_secret if [ -z "$app_id" ] || [ -z "$app_secret" ]; then log_error "AppID / AppSecret 不能为空" return 1 fi echo "" echo -e "${YELLOW}注册 QQ 渠道...${NC}" local add_output="" add_output="$(openclaw channels add --channel qqbot --token "${app_id}:${app_secret}" 2>&1)" local add_exit=$? echo "$add_output" | grep -v "^🦞" | grep -v "^$" | head -5 if [ $add_exit -ne 0 ]; then log_warn "QQ 渠道可能已存在或注册失败,继续写入 allowFrom" fi echo "" echo -e "${CYAN}来源白名单(allowFrom)配置:${NC}" echo " • 输入格式: 12345,67890" echo " • 留空表示允许所有来源(高风险)" read_input "${YELLOW}请输入允许来源(可留空): ${NC}" allow_input if [ -n "$allow_input" ]; then allow_json="$(build_json_array_from_csv "$allow_input")" if [ "$allow_json" = "[]" ]; then log_error "allowFrom 解析后为空,请重新输入" return 1 fi if set_channel_allow_from_json "qqbot" "$allow_json"; then log_info "已设置 allowFrom: $allow_json" else log_error "写入 allowFrom 失败" return 1 fi else if confirm "未设置白名单将允许所有来源([\"*\"]),确认继续?" "n"; then if set_channel_allow_from_json "qqbot" "[\"*\"]"; then log_warn "已设置 allowFrom 为 [\"*\"](允许全部来源)" else log_error "写入 allowFrom 失败" return 1 fi else log_info "已取消保存 QQ 配置" return 1 fi fi openclaw config set channels.qqbot.enabled true > /dev/null 2>&1 || true log_info "QQ 社区渠道配置完成" if confirm "是否立即执行 QQ 配置探针?" "y"; then probe_qq_community_config || true fi if confirm "是否重启 Gateway 使配置生效?" "y"; then restart_gateway_for_channel fi return 0 } config_qq_community() { clear_screen print_header echo -e "${WHITE}🐧 QQ(社区插件,可选)${NC}" print_divider echo "" echo " 1) 安装并配置 QQ 社区插件" echo " 2) QQ 配置探针检查" echo " 3) 回滚 QQ 社区配置" echo " 0) 返回" echo "" read_input "${YELLOW}请选择 [0-3]: ${NC}" qq_choice case "$qq_choice" in 1) config_qq_community_setup ;; 2) probe_qq_community_config ;; 3) rollback_qq_community_config ;; 0) return ;; *) log_error "无效选择" ;; esac press_enter } probe_wechat_langbot_config() { echo "" echo -e "${CYAN}━━━ 微信 LangBot/WeChatPad 探针 ━━━${NC}" echo "" if ! check_openclaw_installed; then log_error "OpenClaw 未安装,无法探针" return 1 fi local plugin_ok=false local channel_ok=false local proxy_url="" if openclaw plugins list 2>/dev/null | grep -Eqi "openclaw-wechat-channel|wechat"; then plugin_ok=true log_info "微信社区插件已安装" else log_warn "未检测到微信社区插件" fi if openclaw channels list 2>/dev/null | grep -qi "wechat"; then channel_ok=true log_info "微信渠道已注册" else log_warn "微信渠道未在 channels list 中出现" fi proxy_url="$(openclaw config get channels.wechat.accounts.main.proxyUrl 2>/dev/null || true)" if [ -z "$proxy_url" ] || [ "$proxy_url" = "undefined" ]; then proxy_url="$(openclaw config get channels.wechat.proxyUrl 2>/dev/null || true)" fi if [ -n "$proxy_url" ] && [ "$proxy_url" != "undefined" ]; then echo -e "${CYAN}Proxy URL:${NC} ${WHITE}$proxy_url${NC}" if curl -fsS --max-time 5 "$proxy_url" >/dev/null 2>&1; then log_info "Proxy 连通性检查通过" else log_warn "Proxy URL 无法直接连通,请检查 LangBot 适配器服务状态" fi else log_warn "未检测到 channels.wechat.proxyUrl / accounts.main.proxyUrl" fi if [ "$plugin_ok" = true ] && [ "$channel_ok" = true ]; then log_info "微信配置探针通过" return 0 fi echo "" echo -e "${YELLOW}排障建议:${NC}" echo " 1) openclaw doctor --fix" echo " 2) openclaw plugins update --all" echo " 3) 重新执行微信配置向导" return 1 } rollback_wechat_langbot_config() { echo "" echo -e "${WHITE}♻️ 回滚微信(LangBot/WeChatPad)配置${NC}" print_divider echo "" echo -e "${YELLOW}将执行:${NC}" echo " • 禁用并卸载微信社区插件" echo " • 清理 channels.wechat 与 plugins.allow 残留" echo " • 清理 WECHATPADPRO_* 环境变量" echo "" if ! confirm "确认执行回滚?" "n"; then log_info "已取消回滚" return 0 fi openclaw plugins disable wechat > /dev/null 2>&1 || true openclaw plugins uninstall wechat --keep-files > /dev/null 2>&1 || true openclaw plugins uninstall "$WECHAT_PLUGIN_LANGBOT" --keep-files > /dev/null 2>&1 || true if openclaw config --help 2>/dev/null | grep -q "unset"; then openclaw config unset channels.wechat > /dev/null 2>&1 || true openclaw config unset plugins.entries.wechat > /dev/null 2>&1 || true else openclaw config set channels.wechat.enabled false > /dev/null 2>&1 || true fi remove_plugin_from_allow "wechat" || true remove_env_export "WECHATPADPRO_BASEURL" remove_env_export "WECHATPADPRO_API_KEY" remove_env_export "WECHATPADPRO_CALLBACK_HOST" remove_env_export "WECHATPADPRO_CALLBACK_PORT" remove_env_export "WECHATPADPRO_CALLBACK_PATH" log_info "微信配置回滚完成" if confirm "是否重启 Gateway 使回滚生效?" "y"; then restart_gateway_for_channel fi return 0 } config_wechat_langbot_setup() { if ! check_openclaw_installed; then log_error "OpenClaw 未安装" return 1 fi ensure_openclaw_init local plugin_version="${OPENCLAW_WECHAT_PLUGIN_VERSION:-$WECHAT_PLUGIN_VERSION_DEFAULT}" local plugin_spec="${WECHAT_PLUGIN_LANGBOT}@${plugin_version}" local proxy_url="" local api_key="" local webhook_host="" local webhook_port="" local webhook_path="" webhook_host="$(get_gateway_host)" webhook_port="$(get_gateway_port)" webhook_path="$WECHATPAD_CALLBACK_PATH_DEFAULT" echo -e "${YELLOW}⚠️ 社区适配说明:${NC}" echo " • 微信/WeChatPad 当前不在 OpenClaw 官方渠道列表中" echo " • 此方案按 LangBot 适配器流程对接 WeChatPad 协议" echo " • 插件固定版本安装,降低升级漂移风险" echo "" echo -e "${CYAN}参考字段(LangBot 文档):${NC}" echo " • WECHATPADPRO_BASEURL" echo " • WECHATPADPRO_API_KEY" echo "" if ! confirm "继续安装并配置微信 WeChatPad 适配?" "n"; then log_info "已取消微信配置" return 0 fi echo "" echo -e "${CYAN}安装插件: ${WHITE}${plugin_spec}${NC}" if openclaw plugins install "$plugin_spec" --pin; then log_info "微信社区插件安装成功" else log_warn "插件安装失败,尝试继续使用已安装版本" if ! openclaw plugins list 2>/dev/null | grep -Eqi "openclaw-wechat-channel|wechat"; then log_error "未检测到微信插件,无法继续" return 1 fi fi openclaw plugins enable wechat > /dev/null 2>&1 || true ensure_plugin_in_allow "wechat" echo "" read_input "${YELLOW}LangBot/WeChatPad 代理地址 (如 https://your-proxy.example.com): ${NC}" proxy_url read_secret_input "${YELLOW}微信 API Key: ${NC}" api_key read_input "${YELLOW}回调 Host(默认 ${webhook_host}): ${NC}" webhook_host webhook_host="${webhook_host:-$(get_gateway_host)}" read_input "${YELLOW}回调 Port(默认 ${webhook_port}): ${NC}" webhook_port webhook_port="${webhook_port:-$(get_gateway_port)}" read_input "${YELLOW}回调 Path(默认 ${webhook_path}): ${NC}" webhook_path webhook_path="${webhook_path:-$WECHATPAD_CALLBACK_PATH_DEFAULT}" if [ -z "$proxy_url" ] || [ -z "$api_key" ]; then log_error "proxy_url / api_key 不能为空" return 1 fi if ! is_valid_port "$webhook_port"; then log_error "回调端口无效: $webhook_port" return 1 fi # 注册渠道(部分版本可选) openclaw channels add --channel wechat > /dev/null 2>&1 || true # 写入主账号配置,采用 accounts.main 结构 openclaw config set channels.wechat.enabled true > /dev/null 2>&1 || true openclaw config set channels.wechat.accounts.main.enabled true > /dev/null 2>&1 || true openclaw config set channels.wechat.accounts.main.name "wechatpad-main" > /dev/null 2>&1 || true openclaw config set channels.wechat.accounts.main.apiKey "$api_key" > /dev/null 2>&1 || true openclaw config set channels.wechat.accounts.main.proxyUrl "$proxy_url" > /dev/null 2>&1 || true openclaw config set channels.wechat.accounts.main.webhookHost "$webhook_host" > /dev/null 2>&1 || true openclaw config set channels.wechat.accounts.main.webhookPort "$webhook_port" > /dev/null 2>&1 || true openclaw config set channels.wechat.accounts.main.webhookPath "$webhook_path" > /dev/null 2>&1 || true # 顶层镜像字段,兼容部分社区插件读取路径 openclaw config set channels.wechat.apiKey "$api_key" > /dev/null 2>&1 || true openclaw config set channels.wechat.proxyUrl "$proxy_url" > /dev/null 2>&1 || true openclaw config set channels.wechat.webhookHost "$webhook_host" > /dev/null 2>&1 || true openclaw config set channels.wechat.webhookPort "$webhook_port" > /dev/null 2>&1 || true openclaw config set channels.wechat.webhookPath "$webhook_path" > /dev/null 2>&1 || true upsert_env_export "WECHATPADPRO_BASEURL" "$proxy_url" upsert_env_export "WECHATPADPRO_API_KEY" "$api_key" upsert_env_export "WECHATPADPRO_CALLBACK_HOST" "$webhook_host" upsert_env_export "WECHATPADPRO_CALLBACK_PORT" "$webhook_port" upsert_env_export "WECHATPADPRO_CALLBACK_PATH" "$webhook_path" log_info "微信 WeChatPad 适配配置完成" echo -e "${CYAN}回调地址:${NC} ${WHITE}http://${webhook_host}:${webhook_port}${webhook_path}${NC}" if confirm "是否立即执行微信配置探针?" "y"; then probe_wechat_langbot_config || true fi if confirm "是否重启 Gateway 使配置生效?" "y"; then restart_gateway_for_channel fi return 0 } config_wechat() { clear_screen print_header echo -e "${WHITE}🟢 微信(LangBot WeChatPad)${NC}" print_divider echo "" echo " 1) 配置 LangBot 适配器(WeChatPad 协议)" echo " 2) 微信配置探针" echo " 3) 回滚微信配置" echo " 0) 返回" echo "" read_input "${YELLOW}请选择 [0-3]: ${NC}" wechat_choice case "$wechat_choice" in 1) config_wechat_langbot_setup ;; 2) probe_wechat_langbot_config ;; 3) rollback_wechat_langbot_config ;; 0) return ;; *) log_error "无效选择" ;; esac press_enter } config_imessage() { clear_screen print_header echo -e "${WHITE}🍎 配置 iMessage${NC}" print_divider echo "" echo -e "${YELLOW}⚠️ 注意: iMessage 仅支持 macOS${NC}" echo "" if [[ "$OSTYPE" != "darwin"* ]]; then log_error "iMessage 仅支持 macOS 系统" press_enter return fi if ! check_openclaw_installed; then log_error "OpenClaw 未安装" press_enter return fi echo -e "${CYAN}iMessage 配置需要:${NC}" echo " 1. 授予终端完整磁盘访问权限" echo " 2. 确保 Messages.app 已登录" echo "" echo -e "${YELLOW}系统偏好设置 → 隐私与安全性 → 完整磁盘访问权限 → 添加终端${NC}" echo "" if confirm "是否继续配置?"; then # 确保初始化 ensure_openclaw_init # 启用 iMessage 插件 echo "" log_info "启用 iMessage 插件..." openclaw plugins enable imessage 2>/dev/null || true ensure_plugin_in_allow "imessage" # 添加 iMessage channel echo "" log_info "配置 iMessage 渠道..." openclaw channels add --channel imessage 2>/dev/null || true echo "" log_info "iMessage 配置完成!" if confirm "是否重启 Gateway 使配置生效?" "y"; then restart_gateway_for_channel fi fi press_enter } # 安装飞书插件(仅官方包) install_feishu_plugin() { echo -e "${YELLOW}安装飞书插件...${NC}" echo "" # 先清理同名插件,避免历史社区包与官方包冲突 if openclaw plugins list 2>/dev/null | grep -qi "feishu"; then log_info "检测到已安装飞书插件,先执行重装清理..." openclaw plugins disable feishu > /dev/null 2>&1 || true openclaw plugins uninstall feishu --keep-files > /dev/null 2>&1 || true fi local preferred_version="${OPENCLAW_FEISHU_PLUGIN_VERSION:-}" local preferred_spec="$FEISHU_PLUGIN_OFFICIAL" if [ -n "$preferred_version" ]; then preferred_spec="${FEISHU_PLUGIN_OFFICIAL}@${preferred_version}" fi echo -e "${CYAN}正在安装飞书插件 ${preferred_spec} ...${NC}" echo "" # 仅安装官方插件包,并 pin 版本,降低后续升级漂移风险 local install_output install_output=$(openclaw plugins install "$preferred_spec" --pin 2>&1) local install_exit=$? # 过滤掉 banner,显示关键信息 echo "$install_output" | grep -v "^🦞" | grep -v "^$" | head -5 if [ $install_exit -eq 0 ]; then openclaw plugins enable feishu > /dev/null 2>&1 || true ensure_plugin_in_allow "feishu" echo "" log_info "✅ 飞书插件安装成功!" return 0 else echo "" log_warn "插件安装失败: $preferred_spec" echo "" log_error "飞书插件安装失败,请手动重试" echo -e "${CYAN}官方插件:${NC} ${WHITE}openclaw plugins install $FEISHU_PLUGIN_OFFICIAL${NC}" return 1 fi } # 保存飞书配置(使用 openclaw 原生命令) save_feishu_config() { local app_id="$1" local app_secret="$2" echo -e "${YELLOW}添加飞书渠道...${NC}" # 使用 openclaw channels add 添加飞书渠道 local add_output add_output=$(openclaw channels add --channel feishu 2>&1) local add_exit=$? # 过滤掉 openclaw banner,只显示关键信息 echo "$add_output" | grep -v "^🦞" | grep -v "^$" | head -3 if [ $add_exit -ne 0 ]; then log_warn "飞书渠道可能已存在,继续配置..." fi # 使用官方推荐结构写入 credentials(accounts.main) echo -e "${YELLOW}配置 App ID...${NC}" local set_output set_output=$(openclaw config set channels.feishu.accounts.main.appId "$app_id" 2>&1) local set_exit=$? if [ $set_exit -ne 0 ]; then echo "$set_output" | grep -v "^🦞" | grep -v "^$" log_error "设置 App ID 失败" return 1 fi echo "$set_output" | grep -v "^🦞" | grep -v "^$" | head -1 echo -e "${YELLOW}配置 App Secret...${NC}" set_output=$(openclaw config set channels.feishu.accounts.main.appSecret "$app_secret" 2>&1) set_exit=$? if [ $set_exit -ne 0 ]; then echo "$set_output" | grep -v "^🦞" | grep -v "^$" log_error "设置 App Secret 失败" return 1 fi echo "$set_output" | grep -v "^🦞" | grep -v "^$" | head -1 # 保持最小必要配置,避免覆盖用户现有个性化设置 openclaw config set channels.feishu.enabled true > /dev/null 2>&1 || true openclaw config set channels.feishu.accounts.main.enabled true > /dev/null 2>&1 || true openclaw config set channels.feishu.connectionMode websocket > /dev/null 2>&1 || true log_info "飞书渠道配置完成" return 0 } # 飞书配置探针:检查插件、渠道与关键配置项 probe_feishu_config() { echo "" echo -e "${CYAN}━━━ 飞书配置探针 ━━━${NC}" echo "" if ! check_openclaw_installed; then log_error "OpenClaw 未安装,无法探针" return 1 fi local plugin_ok=false local channel_ok=false if openclaw plugins list 2>/dev/null | grep -qi "feishu"; then plugin_ok=true log_info "飞书插件已安装" else log_warn "飞书插件未在 plugins list 中出现" fi if openclaw channels list 2>/dev/null | grep -qi "feishu"; then channel_ok=true log_info "飞书渠道已注册" else log_warn "飞书渠道未在 channels list 中出现" fi local mode=$(openclaw config get channels.feishu.connectionMode 2>/dev/null || echo "") if [ -n "$mode" ] && [ "$mode" != "undefined" ]; then log_info "连接模式: $mode" else log_warn "未读取到 channels.feishu.connectionMode" fi if [ "$plugin_ok" = true ] && [ "$channel_ok" = true ]; then log_info "飞书配置探针通过" return 0 fi echo "" echo -e "${YELLOW}排障建议:${NC}" echo " 1) openclaw doctor --fix" echo " 2) openclaw plugins update --all" echo " 3) openclaw gateway restart" return 1 } config_feishu() { clear_screen print_header echo -e "${WHITE}🔷 配置飞书 (Feishu)${NC}" print_divider echo "" echo -e "${YELLOW}⚠️ 注意: 飞书接入依赖插件,请优先使用官方插件${NC}" echo "" if ! check_openclaw_installed; then log_error "OpenClaw 未安装" press_enter return fi echo -e "${CYAN}飞书接入说明:${NC}" echo "" echo -e " ${WHITE}仅使用官方插件 @openclaw/feishu${NC}" echo "" echo -e " ${GREEN}✓ 支持 WebSocket 连接(无需公网服务器)${NC}" echo -e " ${GREEN}✓ 支持私聊和群聊${NC}" echo -e " ${GREEN}✓ 支持图片、文件等多媒体${NC}" echo -e " ${GREEN}✓ 个人账号即可使用,无需企业认证${NC}" echo "" echo -e " ${YELLOW}📝 需要在飞书开放平台创建应用(免费,5分钟)${NC}" echo "" print_divider echo "" if confirm "是否开始配置飞书?"; then config_feishu_app fi } # 飞书企业自建应用配置 config_feishu_app() { clear_screen print_header echo -e "${WHITE}🔷 飞书应用配置${NC}" print_divider echo "" echo -e "${GREEN}✓ 个人账号即可使用,无需企业认证!${NC}" echo -e "${CYAN} ("自建应用"只是飞书的命名,任何人都可以创建)${NC}" echo "" echo -e "${CYAN}配置步骤:${NC}" echo "" echo " ${WHITE}第一步: 安装飞书插件${NC} (自动完成)" echo " • 安装官方插件 @openclaw/feishu" echo "" echo " ${WHITE}第二步: 飞书开放平台创建应用${NC}" echo " 1. 访问 https://open.feishu.cn/app (飞书)" echo " 或 https://open.larksuite.com/app (Lark 国际版)" echo " 2. 创建企业自建应用 → 添加「机器人」能力" echo " 3. 获取 App ID 和 App Secret" echo "" echo " ${WHITE}第三步: 配置机器人权限${NC}" echo " • 权限管理 → 建议使用「批量导入」(更稳)" echo " - im:message (收发消息,必须)" echo " - im:message:send_as_bot (发送消息,必须)" echo " - im:resource (图片/文件,多媒体推荐)" echo " - im:chat:readonly (读取群信息,推荐)" echo " • 可选查看官方推荐 scopes JSON(脚本稍后可打印)" echo "" echo " ${WHITE}第四步: 输入配置信息${NC}" echo " • 在此输入 App ID 和 App Secret" echo " • ${GREEN}使用长连接模式,无需 Verification Token${NC}" echo "" echo " ${WHITE}第五步: 配置事件订阅(飞书后台)${NC}" echo " • 事件与回调 → 选择「使用长连接接收事件」" echo " • ${GREEN}无需公网服务器,无需 Webhook 地址${NC}" echo " • 添加事件: im.message.receive_v1" echo "" echo " ${WHITE}第六步: 发布应用并添加到群组${NC}" echo " • 版本管理与发布 → 创建版本 → 发布" echo " • 在飞书群组设置中添加机器人" echo "" print_divider echo "" if ! confirm "是否开始配置?"; then press_enter return fi # ========== 第一步:安装飞书插件 ========== echo "" echo -e "${WHITE}━━━ 第一步: 安装飞书插件 (自动) ━━━${NC}" echo "" if ! install_feishu_plugin; then log_error "飞书插件安装失败,已停止后续飞书配置。" press_enter return fi echo "" log_info "✅ 第一步完成!插件已就绪" echo "" # ========== 第二、三步提示 ========== echo -e "${WHITE}━━━ 第二、三步: 请在飞书开放平台完成 ━━━${NC}" echo "" echo -e "${CYAN}请打开飞书开放平台完成以下操作:${NC}" echo " 1. 访问 https://open.feishu.cn/app (飞书) 或 https://open.larksuite.com/app (Lark)" echo " 2. 创建企业自建应用 → 添加「机器人」能力" echo " 3. 获取 App ID 和 App Secret" echo " 4. 权限管理 → 建议「批量导入」权限 scopes" echo " - 至少包含: im:message / im:message:send_as_bot" echo " - 多媒体建议: im:resource" echo " - 群信息建议: im:chat:readonly" echo "" echo -e "${GREEN}💡 提示: 使用长连接模式,无需配置公网 Webhook 地址${NC}" echo "" if confirm "是否显示官方推荐「权限管理 → 批量导入」JSON?" "n"; then echo "" echo -e "${CYAN}将以下 JSON 粘贴到飞书后台:权限管理 → 批量导入${NC}" cat << 'EOF' { "scopes": { "tenant": [ "aily:file:read", "aily:file:write", "application:application.app_message_stats.overview:readonly", "application:application:self_manage", "application:bot.menu:write", "cardkit:card:read", "cardkit:card:write", "contact:user.employee_id:readonly", "corehr:file:download", "event:ip_list", "im:chat.access_event.bot_p2p_chat:read", "im:chat.members:bot_access", "im:message", "im:message.group_at_msg:readonly", "im:message.p2p_msg:readonly", "im:message:readonly", "im:message:send_as_bot", "im:resource" ], "user": ["aily:file:read", "aily:file:write", "im:chat.access_event.bot_p2p_chat:read"] } } EOF echo "" echo -e "${GRAY}完整版步骤文档: https://github.com/${INSTALLER_REPO}/blob/main/docs/feishu-setup.md${NC}" fi if ! confirm "已完成飞书后台配置,继续输入信息?"; then press_enter return fi # ========== 第五步:输入配置并启动服务 ========== echo "" echo -e "${WHITE}━━━ 第五步: 输入配置并启动服务 ━━━${NC}" echo "" echo -e "${CYAN}📝 使用长连接模式,只需要 App ID 和 App Secret${NC}" echo -e "${GRAY} (无需 Verification Token 和 Encrypt Key)${NC}" echo "" echo -en "${YELLOW}输入 App ID: ${NC}" read feishu_app_id < "$TTY_INPUT" read_secret_input "${YELLOW}输入 App Secret: ${NC}" feishu_app_secret if [ -z "$feishu_app_id" ] || [ -z "$feishu_app_secret" ]; then log_error "App ID 和 App Secret 不能为空" press_enter return fi echo "" log_info "正在保存配置..." # 使用专用函数保存飞书配置到 JSON 文件 echo -e "${YELLOW}配置飞书渠道...${NC}" if save_feishu_config "$feishu_app_id" "$feishu_app_secret"; then log_info "飞书渠道配置成功!" probe_feishu_config || true else log_warn "配置保存失败,请检查" fi echo "" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${WHITE}✅ 配置已保存!${NC}" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e "App ID: ${WHITE}${feishu_app_id:0:15}...${NC}" echo -e "连接模式: ${WHITE}WebSocket 长连接${NC}" echo -e "${GREEN}✓ 无需公网服务器${NC}" echo "" echo -e "${YELLOW}⚠️ 重要: 需要先启动 Gateway 服务!${NC}" echo -e "${CYAN} 启动后才能在飞书后台配置长连接${NC}" echo "" if confirm "是否现在启动/重启 Gateway?" "y"; then restart_gateway_for_channel fi echo "" echo -e "${WHITE}━━━ 第六步: 配置事件订阅 (飞书后台) ━━━${NC}" echo "" echo -e "${YELLOW}⚠️ 请确保 OpenClaw Gateway 服务已启动${NC}" echo "" echo -e "${CYAN}📋 在飞书开放平台完成以下配置:${NC}" echo "" echo -e " ${WHITE}1. 事件与回调 → 选择「使用长连接接收事件」${NC}" echo -e " ${GREEN}✓ 无需公网服务器,无需 Webhook 地址${NC}" echo -e " ${YELLOW}⚠️ 如果无法保存,请确认 Gateway 已启动${NC}" echo "" echo -e " ${WHITE}2. 添加事件订阅:${NC}" echo " • im.message.receive_v1 (接收消息,必须)" echo " • im.message.message_read_v1 (已读回执,可选)" echo " • im.chat.member.bot.added_v1 (机器人入群,可选)" echo "" echo -e "${WHITE}━━━ 第七步: 添加机器人到群组 ━━━${NC}" echo "" echo -e "${CYAN}📋 在飞书客户端添加机器人:${NC}" echo " 1. 打开目标群组 → 设置(右上角 ⚙️)" echo " 2. 群机器人 → 添加机器人" echo " 3. 搜索你的机器人名称并添加" echo "" # 询问是否测试 echo "" if confirm "是否发送测试消息验证配置?" "y"; then echo "" echo -e "${CYAN}如需发送测试消息,请输入群组 Chat ID:${NC}" echo -e "${GRAY}获取方式: 群设置 → 群信息 → 群号${NC}" echo "" echo -en "${YELLOW}Chat ID (留空跳过测试): ${NC}" read feishu_chat_id < "$TTY_INPUT" if [ -n "$feishu_chat_id" ]; then test_feishu_bot "$feishu_app_id" "$feishu_app_secret" "$feishu_chat_id" else test_feishu_bot "$feishu_app_id" "$feishu_app_secret" fi fi press_enter } # ================================ 身份配置 ================================ config_identity() { clear_screen print_header echo -e "${WHITE}👤 身份与个性配置${NC}" print_divider echo "" if ! check_openclaw_installed; then log_error "OpenClaw 未安装" press_enter return fi # 显示当前配置 echo -e "${CYAN}当前配置:${NC}" openclaw config get identity 2>/dev/null || echo " (未配置)" echo "" print_divider echo "" read -p "$(echo -e "${YELLOW}助手名称: ${NC}")" bot_name read -p "$(echo -e "${YELLOW}如何称呼你: ${NC}")" user_name read -p "$(echo -e "${YELLOW}时区 (如 Asia/Shanghai): ${NC}")" timezone # 使用 openclaw 命令设置 [ -n "$bot_name" ] && openclaw config set identity.name "$bot_name" 2>/dev/null [ -n "$user_name" ] && openclaw config set identity.user_name "$user_name" 2>/dev/null [ -n "$timezone" ] && openclaw config set identity.timezone "$timezone" 2>/dev/null echo "" log_info "身份配置已更新!" press_enter } # ================================ 安全配置 ================================ config_security() { clear_screen print_header echo -e "${WHITE}🔒 安全配置${NC}" print_divider echo "" echo -e "${RED}⚠️ 警告: 以下设置涉及安全风险,请谨慎配置${NC}" echo "" print_menu_item "1" "允许执行系统命令" "⚙️" print_menu_item "2" "允许文件访问" "📁" print_menu_item "3" "允许网络浏览" "🌐" print_menu_item "4" "沙箱模式 (推荐开启)" "📦" print_menu_item "5" "配置白名单" "✅" print_menu_item "0" "返回主菜单" "↩️" echo "" echo -en "${YELLOW}请选择 [0-5]: ${NC}" read choice < "$TTY_INPUT" case $choice in 1) local enable_shell="false" if confirm "允许 OpenClaw 执行系统命令?这可能带来安全风险" "n"; then enable_shell="true" fi if check_openclaw_installed; then openclaw config set security.enable_shell_commands "$enable_shell" 2>/dev/null || true fi [ "$enable_shell" = "true" ] && log_info "已启用系统命令执行" || log_info "已禁用系统命令执行" ;; 2) local enable_file="false" if confirm "允许 OpenClaw 读写文件?" "n"; then enable_file="true" fi if check_openclaw_installed; then openclaw config set security.enable_file_access "$enable_file" 2>/dev/null || true fi [ "$enable_file" = "true" ] && log_info "已启用文件访问" || log_info "已禁用文件访问" ;; 3) local enable_web="false" if confirm "允许 OpenClaw 浏览网络?" "y"; then enable_web="true" fi if check_openclaw_installed; then openclaw config set security.enable_web_browsing "$enable_web" 2>/dev/null || true fi [ "$enable_web" = "true" ] && log_info "已启用网络浏览" || log_info "已禁用网络浏览" ;; 4) local sandbox_mode="false" if confirm "启用沙箱模式?(推荐)" "y"; then sandbox_mode="true" fi if check_openclaw_installed; then openclaw config set security.sandbox_mode "$sandbox_mode" 2>/dev/null || true fi [ "$sandbox_mode" = "true" ] && log_info "已启用沙箱模式" || log_warn "已禁用沙箱模式,请注意安全风险" ;; 5) config_whitelist ;; 0) return ;; esac press_enter config_security } config_whitelist() { clear_screen print_header echo -e "${WHITE}✅ 配置白名单${NC}" print_divider echo "" if ! check_openclaw_installed; then log_error "OpenClaw 未安装" press_enter return fi echo -e "${CYAN}使用 openclaw 命令配置白名单:${NC}" echo "" echo " openclaw config set security.allowed_paths '/path/to/dir1,/path/to/dir2'" echo "" read -p "$(echo -e "${YELLOW}输入允许访问的目录 (逗号分隔): ${NC}")" paths if [ -n "$paths" ]; then openclaw config set security.allowed_paths "$paths" 2>/dev/null log_info "白名单配置已保存" fi } # ================================ 服务管理 ================================ stop_openclaw_for_uninstall() { log_info "正在停止 OpenClaw 服务..." if check_openclaw_installed; then openclaw gateway stop 2>/dev/null || true sleep 1 fi local uninstall_pid uninstall_pid="$(get_gateway_pid)" if [ -n "$uninstall_pid" ]; then log_warn "强制停止残留进程 (PID: $uninstall_pid)" kill -9 "$uninstall_pid" 2>/dev/null || true sleep 1 fi } remove_openclaw_system_services() { if [ -f "$HOME/Library/LaunchAgents/com.openclaw.agent.plist" ]; then log_info "移除 macOS 系统服务..." launchctl unload "$HOME/Library/LaunchAgents/com.openclaw.agent.plist" 2>/dev/null || true rm -f "$HOME/Library/LaunchAgents/com.openclaw.agent.plist" 2>/dev/null || true fi if [ -f "/etc/systemd/system/openclaw.service" ]; then log_info "移除 systemd 系统服务..." sudo systemctl stop openclaw 2>/dev/null || true sudo systemctl disable openclaw 2>/dev/null || true sudo rm -f /etc/systemd/system/openclaw.service 2>/dev/null || true sudo systemctl daemon-reload 2>/dev/null || true fi } uninstall_openclaw_global() { echo "" echo -e "${CYAN}执行全局卸载(保留 ~/.openclaw 目录)...${NC}" stop_openclaw_for_uninstall remove_openclaw_system_services log_info "卸载全局 npm 包 openclaw..." npm uninstall -g openclaw 2>&1 | grep -v "^npm" | head -8 || true if command -v openclaw >/dev/null 2>&1; then log_warn "openclaw 命令仍存在,请检查是否有多重安装源" else log_info "全局卸载完成" fi } uninstall_openclaw_directory_preserve_assets() { local oc_dir="$HOME/.openclaw" echo "" echo -e "${CYAN}执行目录卸载(保留 skills / plugins)...${NC}" if [ ! -d "$oc_dir" ]; then log_warn "未找到目录: $oc_dir" return 0 fi local tmp_keep tmp_keep="$(mktemp -d)" if [ -d "$oc_dir/skills" ]; then cp -a "$oc_dir/skills" "$tmp_keep/skills" 2>/dev/null || true fi if [ -d "$oc_dir/plugins" ]; then cp -a "$oc_dir/plugins" "$tmp_keep/plugins" 2>/dev/null || true fi rm -rf "$oc_dir" mkdir -p "$oc_dir" chmod 700 "$oc_dir" 2>/dev/null || true if [ -d "$tmp_keep/skills" ]; then cp -a "$tmp_keep/skills" "$oc_dir/skills" 2>/dev/null || true fi if [ -d "$tmp_keep/plugins" ]; then cp -a "$tmp_keep/plugins" "$oc_dir/plugins" 2>/dev/null || true fi rm -rf "$tmp_keep" log_info "目录卸载完成:已保留 ~/.openclaw/skills 与 ~/.openclaw/plugins" } uninstall_openclaw_directory_full() { local oc_dir="$HOME/.openclaw" if [ -d "$oc_dir" ]; then rm -rf "$oc_dir" log_info "已删除目录: $oc_dir" else log_warn "未找到目录: $oc_dir" fi } openclaw_uninstall_menu() { clear_screen print_header echo -e "${WHITE}🗑️ OpenClaw 卸载中心${NC}" print_divider echo "" echo " 1) 全局卸载(移除命令、服务;保留 ~/.openclaw)" echo " 2) 目录卸载(仅清理 ~/.openclaw,保留 skills/plugins)" echo " 3) 完全卸载(全局 + 删除 ~/.openclaw)" echo " 0) 返回" echo "" read_input "${YELLOW}请选择 [0-3]: ${NC}" uninstall_choice case "$uninstall_choice" in 1) if confirm "确认执行全局卸载?" "n"; then uninstall_openclaw_global else log_info "已取消" fi ;; 2) if confirm "确认执行目录卸载并保留 skills/plugins?" "n"; then uninstall_openclaw_directory_preserve_assets else log_info "已取消" fi ;; 3) if confirm "确认执行完全卸载?该操作不可恢复" "n"; then uninstall_openclaw_global uninstall_openclaw_directory_full else log_info "已取消" fi ;; 0) return ;; *) log_error "无效选择" ;; esac echo "" echo -e "${CYAN}如需重新安装:${NC}" echo " curl -fsSL ${INSTALLER_RAW_URL}/install.sh | bash" press_enter } manage_service() { clear_screen print_header echo -e "${WHITE}⚡ 服务管理${NC}" print_divider echo "" # 使用端口检测判断服务状态(更可靠) local menu_status_pid menu_status_pid=$(get_gateway_pid) if [ -n "$menu_status_pid" ]; then echo -e " 当前状态: ${GREEN}● 运行中${NC} (PID: $menu_status_pid)" else echo -e " 当前状态: ${RED}● 已停止${NC}" fi echo "" print_menu_item "1" "启动服务" "▶️" print_menu_item "2" "停止服务" "⏹️" print_menu_item "3" "重启服务" "🔄" print_menu_item "4" "查看状态" "📊" print_menu_item "5" "查看日志" "📋" print_menu_item "6" "运行诊断并修复" "🔍" print_menu_item "7" "安装为系统服务" "⚙️" print_menu_item "9" "修改 Gateway 地址/端口" "🌐" echo "" echo -e " ${RED}[8]${NC} 🗑️ 卸载 OpenClaw" echo "" print_menu_item "0" "返回主菜单" "↩️" echo "" echo -en "${YELLOW}请选择 [0-9]: ${NC}" read choice < "$TTY_INPUT" case $choice in 1) echo "" if check_openclaw_installed; then # 先检查服务是否已经在运行(使用端口检测,更可靠) local port port="$(get_gateway_port)" local running_pid running_pid=$(get_port_pid "$port") if [ -n "$running_pid" ]; then echo -e "${GREEN}✓ 服务已经在运行中!${NC} (PID: $running_pid)" echo "" # 获取并显示 Dashboard URL local dashboard_url=$(openclaw dashboard --no-open 2>/dev/null | grep -E "^https?://" | head -1) if [ -n "$dashboard_url" ]; then echo -e "${GREEN}Dashboard URL (带授权 token):${NC}" echo -e " ${WHITE}$dashboard_url${NC}" else echo -e "${YELLOW}提示: 运行 ${WHITE}openclaw dashboard${NC} 获取访问 URL" fi echo "" if confirm "是否重启服务?" "n"; then # 使用官方 restart 命令 if [ -f "$OPENCLAW_ENV" ]; then source "$OPENCLAW_ENV" fi openclaw gateway restart 2>&1 | head -5 sleep 2 log_info "服务已重启" fi press_enter manage_service return fi # 检测端口是否被其他进程占用 local port_pid port_pid=$(get_port_pid "$port") if [ -n "$port_pid" ]; then echo -e "${YELLOW}检测到端口 $port 被其他进程占用 (PID: $port_pid)${NC}" if confirm "是否停止占用端口的进程?" "y"; then openclaw gateway stop > /dev/null 2>&1 || true sleep 1 port_pid=$(get_port_pid "$port") if [ -n "$port_pid" ]; then kill -9 $port_pid 2>/dev/null || true sleep 1 fi log_info "已清理端口占用" else log_warn "端口被占用,无法启动新服务" press_enter manage_service return fi fi # 确保基础配置正确 ensure_openclaw_init # 加载环境变量 if [ -f "$OPENCLAW_ENV" ]; then source "$OPENCLAW_ENV" log_info "已加载环境变量" fi # 先运行 doctor --fix 确保配置有效(与重启保持一致) log_info "检查并修复配置..." yes | openclaw doctor --fix > /dev/null 2>&1 || true # 验证修复后的配置 local config_check=$(openclaw doctor 2>&1 | head -5) if echo "$config_check" | grep -qi "Config invalid"; then log_error "配置无效,无法自动修复" echo "" echo -e "${YELLOW}错误详情:${NC}" echo "$config_check" | head -10 echo "" echo -e "${CYAN}请手动运行: openclaw doctor --fix${NC}" press_enter manage_service return fi log_info "正在启动服务..." # 后台启动 Gateway(使用 setsid 完全脱离终端) if command -v setsid &> /dev/null; then if [ -f "$OPENCLAW_ENV" ]; then setsid bash -c "source $OPENCLAW_ENV && exec openclaw gateway --port ${port}" > /tmp/openclaw-gateway.log 2>&1 & else setsid openclaw gateway --port "${port}" > /tmp/openclaw-gateway.log 2>&1 & fi else # 备用方案:nohup + disown if [ -f "$OPENCLAW_ENV" ]; then nohup bash -c "source $OPENCLAW_ENV && exec openclaw gateway --port ${port}" > /tmp/openclaw-gateway.log 2>&1 & else nohup openclaw gateway --port "${port}" > /tmp/openclaw-gateway.log 2>&1 & fi disown 2>/dev/null || true fi # 等待服务启动,多次检测端口 local gateway_pid="" local check_count=0 while [ $check_count -lt 5 ]; do sleep 1 gateway_pid=$(get_gateway_pid) if [ -n "$gateway_pid" ]; then break fi check_count=$((check_count + 1)) done # 最终检测:只要端口有服务就是成功(无论是刚启动的还是之前已运行的) if [ -n "$gateway_pid" ]; then log_info "服务运行中 (PID: $gateway_pid)" echo "" # 获取并显示 Dashboard URL(带 token) echo -e "${CYAN}━━━ 获取 Dashboard URL ━━━${NC}" local dashboard_url=$(openclaw dashboard --no-open 2>/dev/null | grep -E "^https?://" | head -1) if [ -n "$dashboard_url" ]; then echo "" echo -e "${GREEN}✓ Dashboard URL (带授权 token):${NC}" echo -e " ${WHITE}$dashboard_url${NC}" echo "" echo -e "${YELLOW}⚠️ 请使用此 URL 访问控制界面${NC}" else echo "" echo -e "${YELLOW}提示: 运行以下命令获取带 token 的 Dashboard URL:${NC}" echo -e " ${WHITE}openclaw dashboard${NC}" fi echo "" echo -e "${CYAN}日志文件: /tmp/openclaw-gateway.log${NC}" # 显示最近的日志 if [ -s /tmp/openclaw-gateway.log ]; then echo "" echo -e "${GRAY}最近日志:${NC}" tail -5 /tmp/openclaw-gateway.log 2>/dev/null | sed 's/^/ /' fi else log_error "启动失败,端口 ${port} 无服务监听" echo "" # 显示日志文件内容 if [ -s /tmp/openclaw-gateway.log ]; then echo -e "${YELLOW}错误日志:${NC}" tail -15 /tmp/openclaw-gateway.log 2>/dev/null | sed 's/^/ /' fi echo "" echo -e "${CYAN}━━━ 诊断信息 ━━━${NC}" echo "" # 运行 doctor 获取配置状态 echo -e "${YELLOW}配置检查:${NC}" openclaw doctor 2>&1 | head -15 | sed 's/^/ /' echo "" echo -e "${CYAN}建议:${NC}" echo -e " 1. 运行 ${WHITE}openclaw doctor --fix${NC} 修复配置" echo -e " 2. 运行 ${WHITE}openclaw gateway${NC} 手动启动查看详细错误" fi else log_error "OpenClaw 未安装" fi ;; 2) echo "" log_info "正在停止服务..." if check_openclaw_installed; then openclaw gateway stop 2>/dev/null || true sleep 1 # 使用端口检测判断服务是否已停止(更可靠) local stop_pid stop_pid=$(get_gateway_pid) if [ -z "$stop_pid" ]; then log_info "服务已停止" else log_warn "服务可能仍在运行 (PID: $stop_pid)" echo -e " 运行 ${WHITE}kill $stop_pid${NC} 强制停止" fi else log_error "OpenClaw 未安装" fi ;; 3) echo "" log_info "正在重启服务..." if check_openclaw_installed; then # 确保配置正确 ensure_openclaw_init # 加载环境变量 if [ -f "$OPENCLAW_ENV" ]; then source "$OPENCLAW_ENV" fi # 使用官方 restart 命令 local restart_output restart_output=$(openclaw gateway restart 2>&1) || true local restart_exit=$? sleep 2 # 使用端口检测判断服务是否启动成功(更可靠) local gateway_pid gateway_pid=$(get_gateway_pid) if [ -n "$gateway_pid" ]; then log_info "服务已重启 (PID: $gateway_pid)" echo "" # 获取并显示 Dashboard URL local dashboard_url=$(openclaw dashboard --no-open 2>/dev/null | grep -E "^https?://" | head -1) if [ -n "$dashboard_url" ]; then echo -e "${GREEN}✓ Dashboard URL:${NC}" echo -e " ${WHITE}$dashboard_url${NC}" else echo -e "${YELLOW}提示: openclaw dashboard 获取访问 URL${NC}" fi else log_error "重启失败" echo "" echo -e "${YELLOW}命令输出:${NC}" echo "$restart_output" | head -10 | sed 's/^/ /' echo "" # 尝试多个日志来源 echo -e "${YELLOW}诊断信息:${NC}" echo "" # 1. 临时日志 if [ -s /tmp/openclaw-gateway.log ]; then echo -e "${CYAN}启动日志:${NC}" tail -10 /tmp/openclaw-gateway.log 2>/dev/null | sed 's/^/ /' echo "" fi # 2. OpenClaw 系统日志 echo -e "${CYAN}系统日志 (最近 5 条):${NC}" openclaw logs 2>/dev/null | tail -5 | sed 's/^/ /' || echo " (无法获取)" echo "" # 3. 检查 doctor 状态 echo -e "${CYAN}配置状态:${NC}" openclaw doctor 2>&1 | grep -E "error|warning|✗|⚠" | head -5 | sed 's/^/ /' || echo " (正常)" echo "" # 4. 建议 echo -e "${CYAN}建议:${NC}" echo " • 运行 ${WHITE}openclaw doctor --fix${NC} 修复配置问题" echo " • 运行 ${WHITE}openclaw gateway start${NC} 手动启动" echo " • 查看完整日志: ${WHITE}openclaw logs${NC}" fi else log_error "OpenClaw 未安装" fi ;; 4) echo "" if check_openclaw_installed; then openclaw status else log_error "OpenClaw 未安装" fi ;; 5) echo "" if check_openclaw_installed; then echo -e "${CYAN}按 Ctrl+C 退出日志查看${NC}" sleep 1 openclaw logs --follow else log_error "OpenClaw 未安装" fi ;; 6) echo "" if check_openclaw_installed; then openclaw doctor --fix else log_error "OpenClaw 未安装" fi ;; 7) echo "" if check_openclaw_installed; then log_info "正在安装系统服务..." openclaw gateway install log_info "系统服务已安装" echo "" echo -e "${CYAN}现在可以使用以下命令管理服务:${NC}" echo " openclaw gateway start" echo " openclaw gateway stop" echo " openclaw gateway restart" else log_error "OpenClaw 未安装" fi ;; 8) openclaw_uninstall_menu manage_service return ;; 9) echo "" if ! check_openclaw_installed; then log_error "OpenClaw 未安装" press_enter manage_service return fi local current_host current_port new_host new_port current_host="$(get_gateway_host)" current_port="$(get_gateway_port)" echo -e "${CYAN}当前 Gateway 地址: ${WHITE}${current_host}:${current_port}${NC}" echo "" read_input "${YELLOW}新 Host(默认 ${current_host}): ${NC}" new_host new_host="${new_host:-$current_host}" read_input "${YELLOW}新 Port(默认 ${current_port}): ${NC}" new_port new_port="${new_port:-$current_port}" if ! is_valid_port "$new_port"; then log_error "端口无效: $new_port" press_enter manage_service return fi openclaw config set gateway.mode local > /dev/null 2>&1 || true openclaw config set gateway.host "$new_host" > /dev/null 2>&1 || true openclaw config set gateway.port "$new_port" > /dev/null 2>&1 || true openclaw config set gateway.bind "$new_host:$new_port" > /dev/null 2>&1 || true upsert_env_export "OPENCLAW_GATEWAY_HOST" "$new_host" upsert_env_export "OPENCLAW_GATEWAY_PORT" "$new_port" log_info "Gateway 地址已更新为 ${new_host}:${new_port}" if confirm "是否立即重启 Gateway 使新地址生效?" "y"; then restart_gateway_for_channel fi press_enter manage_service return ;; 0) return ;; esac press_enter manage_service } # 确保 OpenClaw 基础配置正确 ensure_openclaw_init() { local OPENCLAW_DIR="$HOME/.openclaw" # 创建必要的目录 mkdir -p "$OPENCLAW_DIR/agents/main/sessions" 2>/dev/null || true mkdir -p "$OPENCLAW_DIR/agents/main/agent" 2>/dev/null || true mkdir -p "$OPENCLAW_DIR/credentials" 2>/dev/null || true # 修复权限 chmod 700 "$OPENCLAW_DIR" 2>/dev/null || true # 确保 gateway.mode 已设置 local current_mode=$(openclaw config get gateway.mode 2>/dev/null) if [ -z "$current_mode" ] || [ "$current_mode" = "undefined" ]; then openclaw config set gateway.mode local 2>/dev/null || true fi # 检查 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 } # 为 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 # 根据 provider 设置对应的环境变量 case "$provider" in anthropic) echo "export ANTHROPIC_API_KEY=$api_key" >> "$env_file" [ -n "$base_url" ] && echo "export ANTHROPIC_BASE_URL=$base_url" >> "$env_file" ;; openai) echo "export OPENAI_API_KEY=$api_key" >> "$env_file" [ -n "$base_url" ] && echo "export OPENAI_BASE_URL=$base_url" >> "$env_file" ;; deepseek) echo "export DEEPSEEK_API_KEY=$api_key" >> "$env_file" echo "export DEEPSEEK_BASE_URL=${base_url:-https://api.deepseek.com}" >> "$env_file" ;; moonshot|kimi) echo "export MOONSHOT_API_KEY=$api_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=$api_key" >> "$env_file" [ -n "$base_url" ] && echo "export GOOGLE_BASE_URL=$base_url" >> "$env_file" ;; groq) echo "export GROQ_API_KEY=$api_key" >> "$env_file" echo "export GROQ_BASE_URL=${base_url:-https://api.groq.com/openai/v1}" >> "$env_file" ;; mistral) echo "export MISTRAL_API_KEY=$api_key" >> "$env_file" echo "export MISTRAL_BASE_URL=${base_url:-https://api.mistral.ai/v1}" >> "$env_file" ;; openrouter) echo "export OPENROUTER_API_KEY=$api_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" ;; xai) echo "export XAI_API_KEY=$api_key" >> "$env_file" ;; zai) echo "export ZAI_API_KEY=$api_key" >> "$env_file" ;; minimax|minimax-cn) echo "export MINIMAX_API_KEY=$api_key" >> "$env_file" ;; opencode|opencode-go) echo "export OPENCODE_API_KEY=$api_key" >> "$env_file" ;; novita) echo "export NOVITA_API_KEY=$api_key" >> "$env_file" echo "export NOVITA_BASE_URL=https://api.novita.ai/openai" >> "$env_file" ;; esac chmod 600 "$env_file" if [ "$provider" = "minimax" ] || [ "$provider" = "minimax-cn" ]; then ensure_minimax_provider_config "$provider" "$model" "$config_file" fi # 设置默认模型 if check_openclaw_installed; then local openclaw_model="" local use_custom_provider=false # 如果使用自定义 BASE_URL,需要配置自定义 provider if [ -n "$base_url" ] && [ "$provider" = "anthropic" ]; then use_custom_provider=true configure_custom_provider "$provider" "$api_key" "$model" "$base_url" "$config_file" openclaw_model="anthropic-custom/$model" elif [ -n "$base_url" ] && [ "$provider" = "openai" ]; then use_custom_provider=true # 传递 API 类型参数(如果已设置) configure_custom_provider "$provider" "$api_key" "$model" "$base_url" "$config_file" "$api_type" openclaw_model="openai-custom/$model" else case "$provider" in anthropic) openclaw_model="anthropic/$model" ;; openai) openclaw_model="openai/$model" ;; groq) openclaw_model="groq/$model" ;; mistral) openclaw_model="mistral/$model" ;; deepseek) openclaw_model="deepseek/$model" ;; moonshot|kimi) openclaw_model="moonshot/$model" ;; openrouter) openclaw_model="openrouter/$model" ;; google) openclaw_model="google/$model" ;; ollama) openclaw_model="ollama/$model" ;; xai) openclaw_model="xai/$model" ;; zai) openclaw_model="zai/$model" ;; minimax) openclaw_model="minimax/$model" ;; minimax-cn) openclaw_model="minimax-cn/$model" ;; opencode) openclaw_model="opencode/$model" ;; opencode-go) openclaw_model="opencode-go/$model" ;; google-gemini-cli) openclaw_model="google-gemini-cli/$model" ;; google-antigravity) openclaw_model="google-antigravity/$model" ;; novita) openclaw_model="novita/$model" ;; esac fi if [ -n "$openclaw_model" ]; then # 加载环境变量并设置模型 source "$env_file" local set_output="" if set_output=$(openclaw models set "$openclaw_model" 2>&1); then log_info "OpenClaw 默认模型已设置为: $openclaw_model" else log_warn "openclaw models set 失败,回退到配置写入" echo "$set_output" | head -3 | sed 's/^/ /' 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 配置文件 local shell_rc="" if [ -f "$HOME/.zshrc" ]; then shell_rc="$HOME/.zshrc" elif [ -f "$HOME/.bashrc" ]; then shell_rc="$HOME/.bashrc" 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" fi fi log_info "环境变量已保存到: $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_info "配置自定义 Provider..." # 确保配置目录存在 local config_dir=$(dirname "$config_file") mkdir -p "$config_dir" 2>/dev/null || true # 确定 API 类型 # OpenClaw 支持: anthropic-messages, openai-responses, openai-completions # 如果传入了自定义 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 # 使用 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 } # ================================ 高级设置 ================================ ensure_auto_fix_openclaw_ready() { if ! command -v git &> /dev/null; then log_error "未检测到 git,无法同步 auto-fix-openclaw" return 1 fi mkdir -p "$(dirname "$AUTO_FIX_OPENCLAW_DIR")" if [ -d "$AUTO_FIX_OPENCLAW_DIR/.git" ]; then log_info "检测到 auto-fix-openclaw,本地路径: $AUTO_FIX_OPENCLAW_DIR" if [ -n "$(git -C "$AUTO_FIX_OPENCLAW_DIR" status --porcelain 2>/dev/null)" ]; then log_warn "auto-fix-openclaw 存在本地改动,跳过自动 pull" else log_info "正在更新 auto-fix-openclaw..." git -C "$AUTO_FIX_OPENCLAW_DIR" pull --ff-only > /dev/null 2>&1 || log_warn "自动更新失败,可稍后手动更新" fi elif [ -d "$AUTO_FIX_OPENCLAW_DIR" ]; then log_warn "目录存在但不是 git 仓库: $AUTO_FIX_OPENCLAW_DIR" else log_info "正在克隆 auto-fix-openclaw..." if ! git clone --depth 1 "$AUTO_FIX_OPENCLAW_REPO_URL" "$AUTO_FIX_OPENCLAW_DIR"; then log_warn "主仓库克隆失败,尝试镜像源..." rm -rf "$AUTO_FIX_OPENCLAW_DIR" 2>/dev/null || true if ! git clone --depth 1 "$AUTO_FIX_OPENCLAW_REPO_MIRROR_URL" "$AUTO_FIX_OPENCLAW_DIR"; then log_error "克隆失败: $AUTO_FIX_OPENCLAW_REPO_URL" log_error "镜像也失败: $AUTO_FIX_OPENCLAW_REPO_MIRROR_URL" return 1 fi fi fi if [ -f "$AUTO_FIX_OPENCLAW_BIN" ] && [ ! -x "$AUTO_FIX_OPENCLAW_BIN" ]; then chmod +x "$AUTO_FIX_OPENCLAW_BIN" 2>/dev/null || true fi if [ ! -x "$AUTO_FIX_OPENCLAW_BIN" ]; then log_error "未找到可执行文件: $AUTO_FIX_OPENCLAW_BIN" return 1 fi log_info "auto-fix-openclaw 已就绪" return 0 } run_auto_fix_openclaw_cmd() { if ! ensure_auto_fix_openclaw_ready; then return 1 fi AUTO_FIX_OPENCLAW_CODEX_BIN="${AUTO_FIX_OPENCLAW_CODEX_BIN:-$(command -v codex || true)}" \ AUTO_FIX_OPENCLAW_CLAUDE_CODE_BIN="${AUTO_FIX_OPENCLAW_CLAUDE_CODE_BIN:-$(command -v claude || command -v claude-code || true)}" \ "$AUTO_FIX_OPENCLAW_BIN" "$@" } check_codex_ready() { if ! command -v codex &> /dev/null; then log_error "未检测到 codex CLI。请先安装 Codex CLI。" echo " 安装后执行: codex login" return 1 fi if ! codex login status > /dev/null 2>&1; then log_error "Codex CLI 未完成登录配置。" echo " 请先执行: codex login" return 1 fi return 0 } run_auto_fix_provider_repair() { local provider="$1" # AI 修复统一要求:Codex 已安装且已登录(作为主修复/兜底引擎) if ! check_codex_ready; then return 1 fi if ! check_openclaw_installed; then log_error "OpenClaw 未安装,无法执行修复" return 1 fi case "$provider" in codex) if ! command -v codex &> /dev/null; then log_error "未检测到 codex CLI,请先安装并登录" return 1 fi ;; claudecode) if ! command -v claude &> /dev/null && ! command -v claude-code &> /dev/null; then log_error "未检测到 claude CLI,请先安装并登录" return 1 fi ;; esac local force_flag="" if confirm "即使当前网关健康也强制执行 AI 修复?" "n"; then force_flag="--force" fi echo "" log_info "将自动读取错误日志摘要,并向 ${provider} CLI 发起修复请求..." log_info "执行修复: auto-fix-openclaw repair-now --provider $provider" if [ -n "$force_flag" ]; then run_auto_fix_openclaw_cmd repair-now --provider "$provider" --source installer-menu "$force_flag" else run_auto_fix_openclaw_cmd repair-now --provider "$provider" --source installer-menu fi } choose_auto_fix_repair_provider() { echo "" echo -e "${WHITE}选择修复引擎${NC}" print_divider print_menu_item "1" "Codex CLI" "🤖" print_menu_item "2" "Claude CLI" "🧠" print_menu_item "0" "取消" "↩️" echo "" echo -en "${YELLOW}请选择 [0-2]: ${NC}" read provider_choice < "$TTY_INPUT" case "$provider_choice" in 1) run_auto_fix_provider_repair codex ;; 2) run_auto_fix_provider_repair claudecode ;; 0) return 0 ;; *) log_error "无效选择" return 1 ;; esac } ai_auto_fix_menu() { clear_screen print_header echo -e "${WHITE}🛠️ AI 自动修复 OpenClaw${NC}" print_divider echo "" echo -e "${GRAY}集成 auto-fix-openclaw,可调用 Codex/Claude CLI 执行修复${NC}" echo -e "${GRAY}仓库: ${AUTO_FIX_OPENCLAW_REPO_URL}${NC}" echo -e "${GRAY}路径: ${AUTO_FIX_OPENCLAW_DIR}${NC}" echo -e "${YELLOW}前置要求: 已安装并配置 Codex CLI(codex login)。${NC}" echo -e "${GRAY}执行 AI 修复时会自动读取错误日志摘要并发送修复请求。${NC}" echo "" print_menu_item "1" "同步/安装 auto-fix-openclaw" "📦" print_menu_item "2" "查看 auto-fix 状态" "📊" print_menu_item "3" "采集诊断 (doctor-dry-run)" "🧪" print_menu_item "4" "执行单次巡检 (run-once)" "🔁" print_menu_item "5" "执行 AI 修复(选择 Claude/Codex)" "🛠️" print_menu_item "0" "返回上级菜单" "↩️" echo "" echo -en "${YELLOW}请选择 [0-5]: ${NC}" read choice < "$TTY_INPUT" case "$choice" in 1) ensure_auto_fix_openclaw_ready ;; 2) run_auto_fix_openclaw_cmd status ;; 3) run_auto_fix_openclaw_cmd doctor-dry-run ;; 4) run_auto_fix_openclaw_cmd run-once --source installer-menu ;; 5) choose_auto_fix_repair_provider ;; 0) return ;; *) log_error "无效选择" ;; esac press_enter ai_auto_fix_menu } backup_runtime_config_for_upgrade() { local backup_root="$HOME/.openclaw-upgrade-backups" mkdir -p "$backup_root" local backup_path="$backup_root/pre_upgrade_$(date +%Y%m%d_%H%M%S)" mkdir -p "$backup_path" if [ -d "$CONFIG_DIR" ]; then cp -R "$CONFIG_DIR" "$backup_path/config_snapshot" 2>/dev/null || { log_error "创建升级备份失败" return 1 } fi echo "$backup_path" return 0 } restore_runtime_config_backup() { local backup_path="$1" if [ -z "$backup_path" ] || [ ! -d "$backup_path" ]; then log_error "备份目录无效,无法恢复" return 1 fi if [ ! -d "$backup_path/config_snapshot" ]; then log_error "备份快照不存在,无法恢复: $backup_path/config_snapshot" return 1 fi mkdir -p "$CONFIG_DIR" cp -R "$backup_path/config_snapshot/." "$CONFIG_DIR/" 2>/dev/null || { log_error "恢复配置备份失败: $backup_path" return 1 } log_info "已恢复配置备份: $backup_path" return 0 } run_openclaw_upgrade_pipeline() { if ! check_openclaw_installed; then log_error "OpenClaw 未安装" return 1 fi local before_version before_version=$(openclaw --version 2>/dev/null || echo "unknown") log_info "当前版本: $before_version" local upgrade_log="$BACKUP_DIR/upgrade_$(date +%Y%m%d_%H%M%S).log" mkdir -p "$BACKUP_DIR" { echo "=== OpenClaw Upgrade Log ===" echo "Time: $(date '+%Y-%m-%d %H:%M:%S')" echo "Before Version: $before_version" } >> "$upgrade_log" local backup_path backup_path=$(backup_runtime_config_for_upgrade) || return 1 log_info "升级备份: $backup_path" echo "Backup Path: $backup_path" >> "$upgrade_log" local update_output="" local updated=false # 官方推荐升级命令 if update_output=$(openclaw update --restart 2>&1); then updated=true log_info "OpenClaw 核心升级成功(openclaw update --restart)" { echo "" echo "[core-update] openclaw update --restart" echo "$update_output" } >> "$upgrade_log" else log_warn "openclaw update --restart 失败,尝试 npm 回退升级" echo "$update_output" | head -8 | sed 's/^/ /' { echo "" echo "[core-update-failed] openclaw update --restart" echo "$update_output" } >> "$upgrade_log" if update_output=$(npm update -g openclaw 2>&1); then updated=true log_warn "已使用 npm update -g openclaw 完成回退升级" { echo "" echo "[core-update-fallback] npm update -g openclaw" echo "$update_output" } >> "$upgrade_log" else log_error "核心升级失败" echo "$update_output" | head -8 | sed 's/^/ /' { echo "" echo "[core-update-fallback-failed] npm update -g openclaw" echo "$update_output" } >> "$upgrade_log" restore_runtime_config_backup "$backup_path" || true return 1 fi fi if [ "$updated" != true ]; then log_error "升级状态未知,已中止" restore_runtime_config_backup "$backup_path" || true return 1 fi # 升级后按官方链路执行 doctor log_info "运行 doctor 迁移与修复..." local doctor_output="" if openclaw doctor --help 2>/dev/null | grep -q -- "--non-interactive"; then if ! doctor_output=$(openclaw doctor --non-interactive 2>&1); then log_error "doctor 失败" { echo "" echo "[doctor-failed] openclaw doctor --non-interactive" echo "$doctor_output" } >> "$upgrade_log" restore_runtime_config_backup "$backup_path" || true return 1 fi { echo "" echo "[doctor] openclaw doctor --non-interactive" echo "$doctor_output" } >> "$upgrade_log" else if ! doctor_output=$(yes | openclaw doctor --fix 2>&1); then log_error "doctor --fix 失败" { echo "" echo "[doctor-failed] yes | openclaw doctor --fix" echo "$doctor_output" } >> "$upgrade_log" restore_runtime_config_backup "$backup_path" || true return 1 fi { echo "" echo "[doctor] yes | openclaw doctor --fix" echo "$doctor_output" } >> "$upgrade_log" fi # 升级后统一更新插件 log_info "更新插件..." local plugins_output="" if ! plugins_output=$(openclaw plugins update --all 2>&1); then log_warn "plugins update --all 执行失败,请稍后手动执行" { echo "" echo "[plugins-update-failed] openclaw plugins update --all" echo "$plugins_output" } >> "$upgrade_log" else { echo "" echo "[plugins-update] openclaw plugins update --all" echo "$plugins_output" } >> "$upgrade_log" fi # 健康检查 if openclaw health >/dev/null 2>&1; then log_info "健康检查通过" else log_warn "健康检查未通过,请运行 openclaw health / openclaw logs 排查" fi local after_version after_version=$(openclaw --version 2>/dev/null || echo "unknown") log_info "升级完成: $before_version -> $after_version" { echo "" echo "After Version: $after_version" echo "Result: success" } >> "$upgrade_log" log_info "升级日志: $upgrade_log" return 0 } advanced_settings() { clear_screen print_header echo -e "${WHITE}🔧 高级设置${NC}" print_divider echo "" print_menu_item "1" "编辑环境变量" "📝" print_menu_item "2" "备份配置" "💾" print_menu_item "3" "恢复配置" "📥" print_menu_item "4" "重置配置" "🔄" print_menu_item "5" "清理日志" "🧹" print_menu_item "6" "更新 OpenClaw" "⬆️" print_menu_item "7" "卸载 OpenClaw" "🗑️" print_menu_item "8" "AI 自动修复 OpenClaw" "🛠️" print_menu_item "0" "返回主菜单" "↩️" echo "" echo -en "${YELLOW}请选择 [0-8]: ${NC}" read choice < "$TTY_INPUT" case $choice in 1) echo "" log_info "正在打开环境变量配置..." if [ -f "$OPENCLAW_ENV" ]; then if [ -n "$EDITOR" ]; then $EDITOR "$OPENCLAW_ENV" elif command -v nano &> /dev/null; then nano "$OPENCLAW_ENV" elif command -v vim &> /dev/null; then vim "$OPENCLAW_ENV" else cat "$OPENCLAW_ENV" fi else log_error "环境变量文件不存在: $OPENCLAW_ENV" fi ;; 2) echo "" local backup_file=$(backup_config) if [ -n "$backup_file" ]; then log_info "配置已备份到: $backup_file" else log_error "备份失败" fi ;; 3) restore_config ;; 4) if confirm "确定要重置所有配置吗?这将删除当前配置" "n"; then rm -f "$OPENCLAW_ENV" rm -rf "$CONFIG_DIR/openclaw.json" 2>/dev/null log_info "配置已重置,请重新运行安装脚本" fi ;; 5) if confirm "确定要清理日志吗?" "n"; then if check_openclaw_installed; then openclaw logs clear 2>/dev/null || log_warn "OpenClaw 日志清理命令不可用" fi rm -f /tmp/openclaw-gateway.log 2>/dev/null log_info "日志已清理" fi ;; 6) echo "" log_info "正在执行官方升级链路(core + doctor + plugins)..." run_openclaw_upgrade_pipeline ;; 7) openclaw_uninstall_menu advanced_settings return ;; 8) ai_auto_fix_menu ;; 0) return ;; esac press_enter advanced_settings } restore_config() { clear_screen print_header echo -e "${WHITE}📥 恢复配置${NC}" print_divider echo "" if [ ! -d "$BACKUP_DIR" ] || [ -z "$(ls -A $BACKUP_DIR 2>/dev/null)" ]; then log_error "没有找到备份文件" return fi echo -e "${CYAN}可用备份:${NC}" echo "" local i=1 local backups=() for file in "$BACKUP_DIR"/*.bak; do if [ -f "$file" ]; then backups+=("$file") local filename=$(basename "$file") local date_str=$(echo "$filename" | grep -oE '[0-9]{8}_[0-9]{6}') echo " [$i] $date_str - $filename" ((i++)) fi done echo "" read -p "$(echo -e "${YELLOW}选择要恢复的备份 [1-$((i-1))]: ${NC}")" choice if [ -n "$choice" ] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then local selected_backup="${backups[$((choice-1))]}" cp "$selected_backup" "$OPENCLAW_ENV" source "$OPENCLAW_ENV" log_info "环境配置已从备份恢复" else log_error "无效选择" fi } # ================================ 查看配置 ================================ view_config() { clear_screen print_header echo -e "${WHITE}📋 当前配置${NC}" print_divider echo "" # 显示环境变量配置 echo -e "${CYAN}环境变量配置 ($OPENCLAW_ENV):${NC}" echo "" if [ -f "$OPENCLAW_ENV" ]; then if command -v bat &> /dev/null; then bat --style=numbers --language=bash "$OPENCLAW_ENV" else cat -n "$OPENCLAW_ENV" fi else echo -e " ${GRAY}(未配置)${NC}" fi echo "" print_divider echo "" # 显示 OpenClaw 配置 if check_openclaw_installed; then echo -e "${CYAN}OpenClaw 配置:${NC}" echo "" openclaw config list 2>/dev/null || echo -e " ${GRAY}(无法获取)${NC}" echo "" echo -e "${CYAN}已配置渠道:${NC}" echo "" openclaw channels list 2>/dev/null || echo -e " ${GRAY}(无渠道)${NC}" echo "" echo -e "${CYAN}当前模型:${NC}" echo "" openclaw models status 2>/dev/null || echo -e " ${GRAY}(未配置)${NC}" fi echo "" print_divider press_enter } # ================================ 快速测试 ================================ quick_test_menu() { clear_screen print_header echo -e "${WHITE}🧪 快速测试${NC}" print_divider echo "" # 显示 OpenClaw 状态 if check_openclaw_installed; then local version=$(openclaw --version 2>/dev/null || echo "unknown") echo -e " ${GREEN}✓${NC} OpenClaw 已安装: $version" else echo -e " ${YELLOW}⚠${NC} OpenClaw 未安装" fi echo "" print_divider echo "" echo -e "${CYAN}API 连接测试:${NC}" print_menu_item "1" "测试 AI API 连接" "🤖" print_menu_item "2" "测试 Telegram 机器人" "📨" print_menu_item "3" "测试 Discord 机器人" "🎮" print_menu_item "4" "测试 Slack 机器人" "💼" print_menu_item "5" "测试飞书机器人" "🔷" print_menu_item "6" "测试 Ollama 本地模型" "🟠" echo "" echo -e "${CYAN}OpenClaw 诊断 (需要已安装):${NC}" print_menu_item "7" "openclaw doctor (诊断)" "🔍" print_menu_item "8" "openclaw status (渠道状态)" "📊" print_menu_item "9" "openclaw health (Gateway 健康)" "💚" echo "" print_menu_item "a" "运行全部 API 测试" "🔄" print_menu_item "0" "返回主菜单" "↩️" echo "" echo -en "${YELLOW}请选择 [0-9/a]: ${NC}" read choice < "$TTY_INPUT" case $choice in 1) quick_test_ai ;; 2) quick_test_telegram ;; 3) quick_test_discord ;; 4) quick_test_slack ;; 5) quick_test_feishu ;; 6) quick_test_ollama ;; 7) quick_test_doctor ;; 8) quick_test_status ;; 9) quick_test_health ;; a|A) run_all_tests ;; 0) return ;; *) log_error "无效选择"; press_enter; quick_test_menu ;; esac } quick_test_ai() { clear_screen print_header echo -e "${WHITE}🤖 测试 AI API 连接${NC}" print_divider echo "" if ! check_openclaw_installed; then log_error "OpenClaw 未安装" press_enter quick_test_menu return fi # 读取环境变量(如果存在) [ -f "$OPENCLAW_ENV" ] && source "$OPENCLAW_ENV" local provider="" local api_key="" local base_url="" local model="" local model_ref="" model_ref="$(get_current_model_ref || true)" if [ -z "$model_ref" ]; then log_error "未检测到已生效默认模型,请先完成模型配置" press_enter quick_test_menu return fi provider="${model_ref%%/*}" model="${model_ref#*/}" # 按 provider 尝试读取对应 key(若为空也允许,因为可能使用 auth profile) case "$provider" in anthropic) api_key="$ANTHROPIC_API_KEY"; base_url="$ANTHROPIC_BASE_URL" ;; openai) api_key="$OPENAI_API_KEY"; base_url="$OPENAI_BASE_URL" ;; deepseek) api_key="$DEEPSEEK_API_KEY"; base_url="$DEEPSEEK_BASE_URL" ;; moonshot|kimi) api_key="$MOONSHOT_API_KEY"; base_url="$MOONSHOT_BASE_URL" ;; google|google-gemini-cli|google-antigravity) api_key="$GOOGLE_API_KEY"; base_url="$GOOGLE_BASE_URL" ;; openrouter) api_key="$OPENROUTER_API_KEY"; base_url="$OPENROUTER_BASE_URL" ;; groq) api_key="$GROQ_API_KEY"; base_url="$GROQ_BASE_URL" ;; mistral) api_key="$MISTRAL_API_KEY"; base_url="$MISTRAL_BASE_URL" ;; xai) api_key="$XAI_API_KEY" ;; zai) api_key="$ZAI_API_KEY" ;; minimax|minimax-cn|minimax-portal) api_key="$MINIMAX_API_KEY" ;; opencode|opencode-zen|opencode-go) api_key="$OPENCODE_API_KEY" ;; ollama|lmstudio) api_key="local-runtime" ;; esac echo -e "当前配置:" echo -e " 提供商: ${WHITE}$provider${NC}" echo -e " 模型: ${WHITE}${model:-未知}${NC}" [ -n "$base_url" ] && echo -e " API 地址: ${WHITE}$base_url${NC}" test_ai_connection "$provider" "$api_key" "$model" "$base_url" press_enter quick_test_menu } quick_test_telegram() { clear_screen print_header echo -e "${WHITE}📨 测试 Telegram 机器人${NC}" print_divider echo "" echo -e "${CYAN}请输入 Telegram Bot Token 和 User ID 进行测试:${NC}" echo "" read -p "$(echo -e "${YELLOW}Bot Token: ${NC}")" token read -p "$(echo -e "${YELLOW}User ID: ${NC}")" user_id if [ -z "$token" ]; then log_error "Token 不能为空" press_enter quick_test_menu return fi test_telegram_bot "$token" "$user_id" press_enter quick_test_menu } quick_test_discord() { clear_screen print_header echo -e "${WHITE}🎮 测试 Discord 机器人${NC}" print_divider echo "" echo -e "${CYAN}请输入 Discord Bot Token 和 Channel ID 进行测试:${NC}" echo "" read -p "$(echo -e "${YELLOW}Bot Token: ${NC}")" token read -p "$(echo -e "${YELLOW}Channel ID: ${NC}")" channel_id if [ -z "$token" ]; then log_error "Token 不能为空" press_enter quick_test_menu return fi test_discord_bot "$token" "$channel_id" press_enter quick_test_menu } quick_test_slack() { clear_screen print_header echo -e "${WHITE}💼 测试 Slack 机器人${NC}" print_divider echo "" echo -e "${CYAN}请输入 Slack Bot Token 进行测试:${NC}" echo "" read_secret_input "${YELLOW}Bot Token (xoxb-...): ${NC}" bot_token if [ -z "$bot_token" ]; then log_error "Token 不能为空" press_enter quick_test_menu return fi test_slack_bot "$bot_token" press_enter quick_test_menu } quick_test_feishu() { clear_screen print_header echo -e "${WHITE}🔷 测试飞书机器人${NC}" print_divider echo "" local app_id="" local app_secret="" # 尝试从 JSON 配置文件中读取 if [ -f "$OPENCLAW_JSON" ]; then if command -v node &> /dev/null; then app_id=$(node -e " try { const config = JSON.parse(require('fs').readFileSync('$OPENCLAW_JSON', 'utf8')); console.log(config.channels?.feishu?.accounts?.main?.appId || config.channels?.feishu?.appId || ''); } catch (e) { console.log(''); } " 2>/dev/null) app_secret=$(node -e " try { const config = JSON.parse(require('fs').readFileSync('$OPENCLAW_JSON', 'utf8')); console.log(config.channels?.feishu?.accounts?.main?.appSecret || config.channels?.feishu?.appSecret || ''); } catch (e) { console.log(''); } " 2>/dev/null) elif command -v python3 &> /dev/null; then app_id=$(python3 -c " import json try: with open('$OPENCLAW_JSON', 'r') as f: config = json.load(f) feishu = config.get('channels', {}).get('feishu', {}) print(feishu.get('accounts', {}).get('main', {}).get('appId', feishu.get('appId', ''))) except: print('') " 2>/dev/null) app_secret=$(python3 -c " import json try: with open('$OPENCLAW_JSON', 'r') as f: config = json.load(f) feishu = config.get('channels', {}).get('feishu', {}) print(feishu.get('accounts', {}).get('main', {}).get('appSecret', feishu.get('appSecret', ''))) except: print('') " 2>/dev/null) fi fi if [ -n "$app_id" ] && [ -n "$app_secret" ]; then echo -e "${GREEN}✓ 检测到已配置的飞书应用${NC}" echo -e " App ID: ${WHITE}${app_id:0:15}...${NC}" echo "" else echo -e "${YELLOW}未检测到飞书配置,请手动输入:${NC}" echo "" echo -en "${YELLOW}App ID: ${NC}" read app_id < "$TTY_INPUT" read_secret_input "${YELLOW}App Secret: ${NC}" app_secret if [ -z "$app_id" ] || [ -z "$app_secret" ]; then log_error "App ID 和 App Secret 不能为空" press_enter quick_test_menu return fi fi echo "" echo -e "${CYAN}如需发送测试消息,请输入群组 Chat ID(留空跳过):${NC}" echo -e "${GRAY}获取方式: 群设置 → 群信息 → 群号${NC}" echo "" echo -en "${YELLOW}Chat ID (可选): ${NC}" read chat_id < "$TTY_INPUT" test_feishu_bot "$app_id" "$app_secret" "$chat_id" press_enter quick_test_menu } quick_test_ollama() { clear_screen print_header echo -e "${WHITE}🟠 测试 Ollama 连接${NC}" print_divider echo "" # 从环境变量读取或使用默认值 local base_url="${OLLAMA_HOST:-http://localhost:11434}" local model="llama3" read -p "$(echo -e "${YELLOW}Ollama 地址 (默认: $base_url): ${NC}")" input_url [ -n "$input_url" ] && base_url="$input_url" read -p "$(echo -e "${YELLOW}模型名称 (默认: $model): ${NC}")" input_model [ -n "$input_model" ] && model="$input_model" test_ollama_connection "$base_url" "$model" press_enter quick_test_menu } quick_test_doctor() { clear_screen print_header echo -e "${WHITE}🔍 OpenClaw 诊断${NC}" print_divider run_openclaw_doctor press_enter quick_test_menu } quick_test_status() { clear_screen print_header echo -e "${WHITE}📊 OpenClaw 渠道状态${NC}" print_divider run_openclaw_status press_enter quick_test_menu } quick_test_health() { clear_screen print_header echo -e "${WHITE}💚 Gateway 健康检查${NC}" print_divider run_openclaw_health press_enter quick_test_menu } run_all_tests() { clear_screen print_header echo -e "${WHITE}🔄 运行全部 API 测试${NC}" print_divider echo "" echo -e "${YELLOW}正在测试已配置的服务...${NC}" echo "" local total_tests=0 local passed_tests=0 # 从环境变量读取 AI 配置 [ -f "$OPENCLAW_ENV" ] && source "$OPENCLAW_ENV" local provider="" local api_key="" local base_url="" local model="" local model_ref="" if check_openclaw_installed; then model_ref="$(get_current_model_ref || true)" fi if [ -n "$model_ref" ]; then provider="${model_ref%%/*}" model="${model_ref#*/}" fi case "$provider" in anthropic) api_key="$ANTHROPIC_API_KEY"; base_url="$ANTHROPIC_BASE_URL" ;; openai) api_key="$OPENAI_API_KEY"; base_url="$OPENAI_BASE_URL" ;; deepseek) api_key="$DEEPSEEK_API_KEY"; base_url="$DEEPSEEK_BASE_URL" ;; moonshot|kimi) api_key="$MOONSHOT_API_KEY"; base_url="$MOONSHOT_BASE_URL" ;; google|google-gemini-cli|google-antigravity) api_key="$GOOGLE_API_KEY"; base_url="$GOOGLE_BASE_URL" ;; openrouter) api_key="$OPENROUTER_API_KEY"; base_url="$OPENROUTER_BASE_URL" ;; groq) api_key="$GROQ_API_KEY"; base_url="$GROQ_BASE_URL" ;; mistral) api_key="$MISTRAL_API_KEY"; base_url="$MISTRAL_BASE_URL" ;; xai) api_key="$XAI_API_KEY" ;; zai) api_key="$ZAI_API_KEY" ;; minimax|minimax-cn|minimax-portal) api_key="$MINIMAX_API_KEY" ;; opencode|opencode-go|opencode-zen) api_key="$OPENCODE_API_KEY" ;; ollama|lmstudio) api_key="local-runtime" ;; esac if [ -n "$provider" ]; then total_tests=$((total_tests + 1)) echo -e "${CYAN}[测试 $total_tests] AI API ($provider / $model)${NC}" if test_ai_connection "$provider" "$api_key" "$model" "$base_url"; then passed_tests=$((passed_tests + 1)) else log_error "AI API 测试失败" fi echo "" else log_warn "未检测到默认模型,跳过 AI API 测试" fi # 渠道测试提示 echo "" echo -e "${CYAN}渠道测试:${NC}" echo -e " 使用 ${WHITE}快速测试${NC} 菜单手动测试各个渠道" echo -e " 或运行 ${WHITE}openclaw channels list${NC} 查看已配置渠道" echo "" # 汇总结果 echo "" print_divider echo "" echo -e "${WHITE}测试结果汇总:${NC}" echo -e " 总测试数: $total_tests" echo -e " 通过: ${GREEN}$passed_tests${NC}" echo -e " 失败: ${RED}$((total_tests - passed_tests))${NC}" if [ $passed_tests -eq $total_tests ] && [ $total_tests -gt 0 ]; then echo "" echo -e "${GREEN}✓ 所有测试通过!${NC}" elif [ $total_tests -eq 0 ]; then echo "" echo -e "${YELLOW}⚠ 没有可测试的配置,请先完成相关配置${NC}" fi # 如果 OpenClaw 已安装,提示可用的诊断命令 if check_openclaw_installed; then echo "" echo -e "${CYAN}提示: 可使用以下命令进行更详细的诊断:${NC}" echo " • openclaw doctor - 健康检查 + 修复建议" echo " • openclaw status - 渠道状态" echo " • openclaw health - Gateway 健康状态" fi press_enter quick_test_menu } # ================================ 主菜单 ================================ show_main_menu() { clear_screen print_header echo -e "${WHITE}请选择操作:${NC}" echo "" print_menu_item "1" "系统状态" "📊" print_menu_item "2" "AI 模型配置" "🤖" print_menu_item "3" "消息渠道配置" "📱" print_menu_item "4" "身份与个性配置" "👤" print_menu_item "5" "安全设置" "🔒" print_menu_item "6" "服务管理" "⚡" print_menu_item "7" "快速测试" "🧪" print_menu_item "8" "高级设置" "🔧" print_menu_item "9" "查看当前配置" "📋" echo "" print_menu_item "0" "退出" "🚪" echo "" print_divider } main() { # 检查依赖 check_dependencies # 确保配置目录存在 mkdir -p "$CONFIG_DIR" mkdir -p "$BACKUP_DIR" # 快捷模式:安装脚本可直接跳转到指定配置页 case "${1:-}" in --model-only) config_ai_model echo "" echo -e "${CYAN}模型配置流程结束。${NC}" exit 0 ;; --channels-only) config_channels echo "" echo -e "${CYAN}消息渠道配置流程结束。${NC}" exit 0 ;; esac # 主循环 while true; do show_main_menu echo -en "${YELLOW}请选择 [0-9]: ${NC}" if ! read choice < "$TTY_INPUT"; then echo "" log_error "无法读取输入(TTY 不可用),退出配置菜单。" exit 1 fi case $choice in 1) show_status ;; 2) config_ai_model ;; 3) config_channels ;; 4) config_identity ;; 5) config_security ;; 6) manage_service ;; 7) quick_test_menu ;; 8) advanced_settings ;; 9) view_config ;; 0) echo "" echo -e "${CYAN}再见!🦞${NC}" exit 0 ;; *) log_error "无效选择" press_enter ;; esac done } # 执行主函数 main "$@"