Files

1 line
1.0 MiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "byjoey/openclawctl", "files": {"openclaw.sh": {"e7d4ac0a90cbf1eda180b6a782823e8095182365": {"t": 1785747375.2717836, "n": 99050, "h": "12831f847ef6429a", "c": "#!/usr/bin/env bash\n#\n# ╔══════════════════════════════════════════════════════════════════╗\n# ║ OpenClaw CTL / MoltBot 管理脚本 ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 作者 GitHub : by Joey ║\n# ║ YouTube : @joeyblog ║\n# ║ Telegram: https://t.me/+ft-zI76oovgwNmRh ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 致谢 / 引用 ║\n# ║ · 原始脚本基础来自 kejilion(@kejilion) ║\n# ║ · CLIProxyAPI 安装器来自 cliproxyapi-installer ║\n# ║ github.com/brokechubb/cliproxyapi-installer ║\n# ╚══════════════════════════════════════════════════════════════════╝\n#\n: \"${gl_hui:='\\e[37m'}\"\n: \"${gl_hong:='\\033[31m'}\"\n: \"${gl_lv:='\\033[32m'}\"\n: \"${gl_huang:='\\033[33m'}\"\n: \"${gl_lan:='\\033[34m'}\"\n: \"${gl_bai:='\\033[0m'}\"\n: \"${gl_zi:='\\033[35m'}\"\n: \"${gl_kjlan:='\\033[96m'}\"\n\nif ! declare -f break_end > /dev/null 2>&1; then\nbreak_end() {\n\tif command -v gum >/dev/null 2>&1; then\n\t\techo\n\t\tgum style --foreground 240 \" ─────────────────────────────────────── \"\n\t\tgum input --placeholder \"» 按回车继续 «\" --prompt \" \" > /dev/null\n\telse\n\t\techo \" ─── 按任意键继续 ───\"\n\t\tread -n 1 -s -r -p \"\"\n\t\techo\n\tfi\n\tclear\n}\nfi\n\nif ! declare -f install > /dev/null 2>&1; then\ninstall() {\n\tif [[ $# -eq 0 ]]; then\n\t\techo \"未提供软件包参数\"\n\t\treturn 1\n\tfi\n\tfor package in \"$@\"; do\n\t\tif ! command -v \"$package\" &>/dev/null; then\n\t\t\techo -e \"${gl_kjlan}正在安装 $package...${gl_bai}\"\n\t\t\tif [[ \"$(uname -s)\" == \"Darwin\" ]]; then\n\t\t\t\t_ensure_brew &>/dev/null\n\t\t\t\tcommand -v brew &>/dev/null && brew install \"$package\" &>/dev/null\n\t\t\telif command -v dnf &>/dev/null; then\n\t\t\t\tdnf install -y \"$package\" &>/dev/null\n\t\t\telif command -v yum &>/dev/null; then\n\t\t\t\tyum install -y \"$package\" &>/dev/null\n\t\t\telif command -v apt &>/dev/null; then\n\t\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"$package\" &>/dev/null\n\t\t\telif command -v apk &>/dev/null; then\n\t\t\t\tapk add \"$package\" &>/dev/null\n\t\t\telif command -v pacman &>/dev/null; then\n\t\t\t\tpacman -S --noconfirm \"$package\" &>/dev/null\n\t\t\telif command -v zypper &>/dev/null; then\n\t\t\t\tzypper install -y \"$package\" &>/dev/null\n\t\t\telif command -v opkg &>/dev/null; then\n\t\t\t\topkg install \"$package\" &>/dev/null\n\t\t\telif command -v pkg &>/dev/null; then\n\t\t\t\tpkg install -y \"$package\" &>/dev/null\n\t\t\telse\n\t\t\t\techo \"未知的包管理器,无法安装 $package\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\tdone\n}\nfi\n\nis_macos() { [[ \"$(uname -s)\" == \"Darwin\" ]]; }\n\n_ensure_brew() {\n\tif ! command -v brew &>/dev/null; then\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tif ! command -v brew &>/dev/null; then\n\t\techo \"正在安装 Homebrew...\"\n\t\t/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tcommand -v brew &>/dev/null || { echo \"Homebrew 安装失败,请手动安装:https://brew.sh\"; return 1; }\n}\n\n_sed_i() {\n\tif is_macos; then\n\t\tsed -i '' \"$@\"\n\telse\n\t\tsed -i \"$@\"\n\tfi\n}\n\ninstall_base_deps() {\n\tif is_macos; then\n\t\t_ensure_brew || return 1\n\t\tlocal missing_brew=()\n\t\tfor cmd in curl git nano jq python3 gpg; do\n\t\t\tcommand -v \"$cmd\" &>/dev/null || missing_brew+=(\"$cmd\")\n\t\tdone\n\t\tfor i in \"${!missing_brew[@]}\"; do\n\t\t\t[[ \"${missing_brew[$i]}\" == \"gpg\" ]] && missing_brew[$i]=\"gnupg\"\n\t\tdone\n\t\t[[ ${#missing_brew[@]} -eq 0 ]] && return 0\n\t\techo \"正在安装缺失依赖:${missing_brew[*]}...\"\n\t\tbrew install \"${missing_brew[@]}\" &>/dev/null\n\t\treturn 0\n\tfi\n\n\tlocal -a dep_map=(\n\t\t\"curl:curl:curl:curl:curl:curl\"\n\t\t\"git:git:git:git:git:git\"\n\t\t\"nano:nano:nano:nano:nano:nano\"\n\t\t\"jq:jq:jq:jq:jq:jq\"\n\t\t\"python3:python3:python3:python3:python:python3\"\n\t\t\"tar:tar:tar:tar:tar:tar\"\n\t\t\"gpg:gnupg:gnupg2:gnupg:gnupg:gpg2\"\n\t)\n\n\tlocal missing_apt=() missing_dnf=() missing_apk=() missing_pacman=() missing_zypper=()\n\n\tfor entry in \"${dep_map[@]}\"; do\n\t\tIFS=: read -r cmd pkg_apt pkg_dnf pkg_apk pkg_pacman pkg_zypper <<< \"$entry\"\n\t\tcommand -v \"$cmd\" &>/dev/null && continue\n\t\tmissing_apt+=(\"$pkg_apt\")\n\t\tmissing_dnf+=(\"$pkg_dnf\")\n\t\tmissing_apk+=(\"$pkg_apk\")\n\t\tmissing_pacman+=(\"$pkg_pacman\")\n\t\tmissing_zypper+=(\"$pkg_zypper\")\n\tdone\n\n\t[[ ${#missing_apt[@]} -eq 0 ]] && return 0\n\n\techo \"正在安装缺失依赖:${missing_apt[*]}...\"\n\n\tif command -v apt &>/dev/null; then\n\t\tapt update -y &>/dev/null\n\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"${missing_apt[@]}\" &>/dev/null\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y epel-release &>/dev/null || true\n\t\tdnf install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y epel-release &>/dev/null || true\n\t\tyum install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v apk &>/dev/null; then\n\t\tapk update &>/dev/null\n\t\tapk add \"${missing_apk[@]}\" &>/dev/null\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -Sy --noconfirm --needed \"${missing_pacman[@]}\" &>/dev/null\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y \"${missing_zypper[@]}\" &>/dev/null\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\tlocal py\n\t\tpy=$(compgen -c 2>/dev/null | grep -E '^python3\\.[0-9]+$' | sort -V | tail -1)\n\t\tif [[ -n \"$py\" ]]; then\n\t\t\tln -sf \"$(command -v \"$py\")\" /usr/local/bin/python3 2>/dev/null || true\n\t\tfi\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\techo \"正在安装 python3...\"\n\t\tif command -v apt &>/dev/null; then\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y python3 &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tdnf install -y python3 &>/dev/null\n\t\telif command -v yum &>/dev/null; then\n\t\t\tyum install -y python3 &>/dev/null\n\t\telif command -v apk &>/dev/null; then\n\t\t\tapk add python3 &>/dev/null\n\t\telif command -v pacman &>/dev/null; then\n\t\t\tpacman -S --noconfirm python &>/dev/null\n\t\telif command -v zypper &>/dev/null; then\n\t\t\tzypper install -y python3 &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\tfi\n}\n\n_install_gum_binary() {\n\tlocal arch\n\tcase \"$(uname -m)\" in\n\t\tx86_64) arch=\"amd64\" ;;\n\t\taarch64) arch=\"arm64\" ;;\n\t\tarmv7l) arch=\"armv7\" ;;\n\t\t*) echo \"不支持的架构: $(uname -m)\"; return 1 ;;\n\tesac\n\tlocal latest\n\tlatest=$(curl -fsSL https://api.github.com/repos/charmbracelet/gum/releases/latest \\\n\t\t| grep '\"tag_name\"' | sed 's/.*\"v\\([^\"]*\\)\".*/\\1/')\n\t[[ -z \"$latest\" ]] && { echo \"获取 gum 版本失败\"; return 1; }\n\tlocal url=\"https://github.com/charmbracelet/gum/releases/download/v${latest}/gum_${latest}_linux_${arch}.tar.gz\"\n\tlocal tmp\n\ttmp=$(mktemp -d)\n\tcurl -fsSL \"$url\" -o \"$tmp/gum.tar.gz\" \\\n\t\t&& tar -xzf \"$tmp/gum.tar.gz\" -C \"$tmp\" \\\n\t\t&& install -m 755 \"$tmp/gum\" /usr/local/bin/gum\n\trm -rf \"$tmp\"\n\tcommand -v gum >/dev/null 2>&1\n}\n\ninstall_gum() {\n\tcommand -v gum >/dev/null 2>&1 && return 0\n\techo \"正在安装 gum...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install gum\n\telif command -v apt >/dev/null 2>&1; then\n\t\tcommand -v gpg >/dev/null 2>&1 || apt install -y gnupg 2>/dev/null\n\t\tmkdir -p /etc/apt/keyrings\n\t\tif curl -fsSL https://repo.charm.sh/apt/gpg.key \\\n\t\t\t| gpg --dearmor -o /etc/apt/keyrings/charm.gpg 2>/dev/null; then\n\t\t\techo \"deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *\" \\\n\t\t\t\t| tee /etc/apt/sources.list.d/charm.list >/dev/null\n\t\t\tapt update -y 2>/dev/null && apt install -y gum 2>/dev/null\n\t\tfi\n\t\tcommand -v gum >/dev/null 2>&1 || _install_gum_binary\n\telif command -v dnf &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tdnf install -y gum\n\telif command -v yum &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tyum install -y gum\n\telif command -v apk &>/dev/null; then\n\t\tapk add gum 2>/dev/null || _install_gum_binary\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm gum 2>/dev/null || _install_gum_binary\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y gum 2>/dev/null || _install_gum_binary\n\telse\n\t\t_install_gum_binary\n\tfi\n\tcommand -v gum >/dev/null 2>&1 || { echo \"gum 安装失败,请手动安装: https://github.com/charmbracelet/gum\"; return 1; }\n}\n\ninstall_fzf() {\n\tcommand -v fzf >/dev/null 2>&1 && return 0\n\techo \"正在安装 fzf...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install fzf\n\telif command -v apt >/dev/null 2>&1; then\n\t\tapt install -y fzf\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y fzf\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y fzf\n\telif command -v apk &>/dev/null; then\n\t\tapk add fzf\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm fzf\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y fzf\n\telse\n\t\techo \"无法自动安装 fzf,请手动安装: https://github.com/junegunn/fzf\"\n\t\treturn 1\n\tfi\n}\n\n_install_shortcut() {\n\tlocal store_dir shortcut_dir shortcut\n\n\tstore_dir=\"$HOME/.local/bin\"\n\tmkdir -p \"$store_dir\"\n\n\tlocal _candidate_dirs=(\"/opt/homebrew/bin\" \"/usr/local/bin\")\n\tshortcut_dir=\"\"\n\tfor _d in \"${_candidate_dirs[@]}\"; do\n\t\tif [[ -d \"$_d\" && -w \"$_d\" ]]; then\n\t\t\tshortcut_dir=\"$_d\"\n\t\t\tbreak\n\t\tfi\n\tdone\n\n\tif [[ -z \"$shortcut_dir\" ]]; then\n\t\tshortcut_dir=\"$store_dir\"\n\t\tlocal rc_file=\"\"\n\t\t[[ -f \"$HOME/.zshrc\" ]] && rc_file=\"$HOME/.zshrc\"\n\t\t[[ -f \"$HOME/.bashrc\" ]] && rc_file=\"${rc_file:-$HOME/.bashrc}\"\n\t\tif [[ -n \"$rc_file\" ]] && ! grep -q \"$store_dir\" \"$rc_file\" 2>/dev/null; then\n\t\t\tprintf '\\nexport PATH=\"%s:$PATH\"\\n' \"$store_dir\" >> \"$rc_file\"\n\t\tfi\n\t\texport PATH=\"$store_dir:$PATH\"\n\tfi\n\n\tshortcut=\"$shortcut_dir/oc\"\n\n\tcat > \"$shortcut\" <<EOF\n#!/usr/bin/env bash\ncurl -fsSL https://raw.githubusercontent.com/byJoey/openclawctl/main/openclaw.sh \\\\\n -o \"$store_dir/openclawctl.sh\" 2>/dev/null && chmod +x \"$store_dir/openclawctl.sh\"\nexec bash \"$store_dir/openclawctl.sh\" \"\\$@\"\nEOF\n\tchmod +x \"$shortcut\"\n}\n\nmoltbot_menu() {\n\tis_macos && _ensure_brew\n\n\t_install_shortcut\n\n\tinstall_base_deps\n\tinstall_gum || { echo \"gum 安装失败,无法继续\"; return 1; }\n\tinstall_fzf || { echo \"fzf 安装失败,无法继续\"; return 1; }\n\n\tui_header() {\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border rounded --border-foreground 51 \\\n\t\t\t--padding \"0 2\" \"$*\"\n\t\techo\n\t}\n\n\t_fallback_input() {\n\t\tlocal prompt=\"${1:-输入}\" placeholder=\"${2:-}\"\n\t\tlocal val\n\t\t[[ -n \"$placeholder\" ]] && echo -e \" \\033[90m($placeholder)\\033[0m\"\n\t\tread -rp \" $prompt: \" val\n\t\techo \"$val\"\n\t}\n\n\t_fallback_confirm() {\n\t\tlocal msg=\"${1:-确认?}\"\n\t\tlocal yn\n\t\tread -rp \" $msg [y/N]: \" yn\n\t\t[[ \"$yn\" == \"y\" || \"$yn\" == \"Y\" ]]\n\t}\n\n\t_fallback_spin() {\n\t\tlocal title=\"$1\"; shift\n\t\techo -e \" \\033[90m$title\\033[0m\"\n\t\t\"$@\"\n\t}\n\tui_ok() { gum style --foreground 46 \" ◉ $*\"; }\n\tui_err() { gum style --foreground 196 \" ✗ $*\"; }\n\tui_warn() { gum style --foreground 208 \" ⚡ $*\"; }\n\tui_info() { gum style --foreground 51 \" ◈ $*\"; }\n\tui_step() { echo; gum style --bold --foreground 201 \" ▶ $*\"; echo; }\n\n\tif _has_gum; then\n\t\tui_header() {\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 51 \\\n\t\t\t\t--border rounded --border-foreground 51 \\\n\t\t\t\t--padding \"0 2\" \"$*\"\n\t\t\techo\n\t\t}\n\t\tui_ok() { gum style --foreground 46 \" ◉ $*\"; }\n\t\tui_err() { gum style --foreground 196 \" ✗ $*\"; }\n\t\tui_warn() { gum style --foreground 208 \" ⚡ $*\"; }\n\t\tui_info() { gum style --foreground 51 \" ◈ $*\"; }\n\t\tui_step() { echo; gum style --bold --foreground 201 \" ▶ $*\"; echo; }\n\telse\n\t\tui_header() { echo -e \"\\n \\033[1;36m══ $* ══\\033[0m\\n\"; }\n\t\tui_ok() { echo -e \" \\033[32m[OK]\\033[0m $*\"; }\n\t\tui_err() { echo -e \" \\033[31m[ERR]\\033[0m $*\"; }\n\t\tui_warn() { echo -e \" \\033[33m[WARN]\\033[0m $*\"; }\n\t\tui_info() { echo -e \" \\033[36m[i]\\033[0m $*\"; }\n\t\tui_step() { echo -e \"\\n \\033[35m>>\\033[0m $*\\n\"; }\n\tfi\n\n\tcheck_openclaw_update() {\n\t\tif ! command -v npm >/dev/null 2>&1; then\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal local_version remote_version\n\t\tlocal_version=$(npm list -g openclaw --depth=0 --no-update-notifier 2>/dev/null \\\n\t\t\t| grep openclaw | awk '{print $NF}' | sed 's/^.*@//')\n\n\t\t[[ -z \"$local_version\" ]] && return 1\n\n\t\tremote_version=$(npm view openclaw version --no-update-notifier 2>/dev/null)\n\n\t\t[[ -z \"$remote_version\" ]] && return 1\n\n\t\tif [[ \"$local_version\" != \"$remote_version\" ]]; then\n\t\t\techo -e \"\\033[38;5;208m⚡ UPDATE AVAILABLE $remote_version\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m✦ v$local_version\\033[0m\"\n\t\tfi\n\t}\n\n\tget_install_status() {\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m◉ INSTALLED\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m○ NOT FOUND\\033[0m\"\n\t\tfi\n\t}\n\n\tget_running_status() {\n\t\tif pgrep -f \"openclaw.*gatewa\" >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m■ STOPPED\\033[0m\"\n\t\tfi\n\t}\n\n\tstart_gateway() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tlaunchctl unload \"$plist\" >/dev/null 2>&1 || true\n\t\t\tif [[ -f \"$plist\" ]]; then\n\t\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- \\\n\t\t\t\t\tbash -c \"launchctl load '$plist' 2>/dev/null; sleep 2\"\n\t\t\telse\n\t\t\t\tgum spin --spinner pulse --title \"正在安装并启动网关...\" -- \\\n\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\telse\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- openclaw gateway start\n\t\t\tsleep 1\n\t\tfi\n\t}\n\n\tinstall_node_and_tools() {\n\t\tif command -v node &>/dev/null && command -v npm &>/dev/null; then\n\t\t\treturn 0\n\t\tfi\n\t\techo \"正在安装 Node.js...\"\n\t\tif is_macos; then\n\t\t\t_ensure_brew || return 1\n\t\t\tbrew install node &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tcurl -fsSL https://rpm.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tdnf install -y cmake libatomic nodejs &>/dev/null\n\t\telif command -v apt &>/dev/null; then\n\t\t\tcurl -fsSL https://deb.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y build-essential python3 libatomic1 nodejs &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t}\n\n\tconfigure_openclaw_session_policy() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 1\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport json, sys\npath = sys.argv[1]\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nsession = obj.setdefault('session', {})\nsession['dmScope'] = session.get('dmScope', 'per-channel-peer')\nsession['resetTriggers'] = ['/new', '/reset']\nsession['reset'] = {\n 'mode': 'idle',\n 'idleMinutes': 10080\n}\nsession['resetByType'] = {\n 'direct': {'mode': 'idle', 'idleMinutes': 10080},\n 'thread': {'mode': 'idle', 'idleMinutes': 1440},\n 'group': {'mode': 'idle', 'idleMinutes': 120}\n}\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(obj, f, ensure_ascii=False, indent=2)\n f.write('\\n')\nPY\n\t}\n\n\tsync_openclaw_api_models() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 0\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('未检测到 API providers,跳过模型同步')\n raise SystemExit(0)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nchanged = False\nfatal_errors = []\nsummary = []\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\ndef prompt_delete_provider(name):\n prompt = f\"{name} /models 探测连续失败 3 次。是否删除该 API 供应商及其全部相关模型?[y/N]: \"\n try:\n ans = input(prompt).strip().lower()\n except EOFError:\n return False\n return ans in ('y', 'yes')\n\n\ndef rebind_defaults_before_delete(name):\n global changed\n\n replacement = None\n\n def get_replacement():\n nonlocal replacement\n if replacement is None:\n candidates = collect_available_refs(exclude_provider=name)\n replacement = candidates[0] if candidates else None\n return replacement\n\n primary_ref = get_primary_ref(defaults)\n if ref_provider(primary_ref) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: 默认主模型指向该 provider,但无可用替代模型,已中止删除')\n return False\n set_primary_ref(defaults, repl)\n changed = True\n summary.append(f'已切换默认主模型: {primary_ref} -> {repl}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: {fk} 指向该 provider,但无可用替代模型,已中止删除')\n return False\n defaults[fk] = repl\n changed = True\n summary.append(f'已切换 {fk}: {val} -> {repl}')\n\n return True\n\n\ndef delete_provider_and_refs(name):\n global changed\n\n if not rebind_defaults_before_delete(name):\n return False\n\n removed_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\n for r in removed_refs:\n defaults_models.pop(r, None)\n if removed_refs:\n changed = True\n\n if name in providers:\n providers.pop(name, None)\n changed = True\n\n summary.append(f'已删除 provider {name},并移除 defaults.models 下 {len(removed_refs)} 个模型引用')\n return True\n\n\ndef fetch_remote_models_with_retry(name, base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n data = json.loads(payload)\n return data, None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\nfor name, provider in list(providers.items()):\n if not isinstance(provider, dict):\n summary.append(f'跳过 {name}: provider 结构非法')\n continue\n\n api = provider.get('api', '')\n base_url = provider.get('baseUrl')\n api_key = provider.get('apiKey')\n model_list = provider.get('models', [])\n\n if not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n summary.append(f'跳过 {name}: 无 baseUrl/apiKey/models')\n continue\n\n if api not in SUPPORTED_APIS:\n summary.append(f'跳过 {name}: 不支持直接 /models 校验 (api={api})')\n continue\n\n data, err, attempts = fetch_remote_models_with_retry(name, base_url, api_key, retries=3)\n if err is not None:\n summary.append(f'警告 - {name}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n if prompt_delete_provider(name):\n deleted = delete_provider_and_refs(name)\n if deleted:\n summary.append(f'{name}: 用户已确认删除该 provider 及全部相关模型引用')\n else:\n summary.append(f'{name}: 用户未确认删除,保留现有 provider 配置')\n continue\n\n if attempts > 1:\n summary.append(f'{name}: /models 第 {attempts} 次重试后成功')\n\n if not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n summary.append(f'警告 - 跳过 {name}: /models 返回结构不可识别')\n continue\n\n remote_ids = []\n for item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\n remote_set = set(remote_ids)\n\n if not remote_set:\n fatal_errors.append(f'错误 - {name} 上游 /models 为空,无法为该 provider 提供兜底模型')\n continue\n\n local_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\n local_ids = [str(m['id']) for m in local_models]\n local_set = set(local_ids)\n\n template = None\n for m in local_models:\n template = copy.deepcopy(m)\n break\n if template is None:\n summary.append(f'警告 - 跳过 {name}: 本地 models 无有效模板模型')\n continue\n\n removed_ids = [mid for mid in local_ids if mid not in remote_set]\n added_ids = [mid for mid in remote_ids if mid not in local_set]\n\n kept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\n new_models = kept_models[:]\n\n for mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{name} / {mid}'\n new_models.append(nm)\n\n if not new_models:\n fatal_errors.append(f'错误 - {name} 同步后无可用模型,无法保障默认模型/回退模型兜底')\n continue\n\n expected_refs = {model_ref(name, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\n local_refs = {model_ref(name, mid) for mid in local_ids}\n\n first_ref = model_ref(name, str(new_models[0]['id']))\n\n primary_ref = get_primary_ref(defaults)\n if isinstance(primary_ref, str) and primary_ref in (local_refs - expected_refs):\n set_primary_ref(defaults, first_ref)\n changed = True\n summary.append(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in (local_refs - expected_refs):\n defaults[fk] = first_ref\n changed = True\n summary.append(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\n stale_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/') and r not in expected_refs]\n for r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\n for r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\n if removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\n summary.append(f'{name}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\n\nif fatal_errors:\n for line in summary:\n print(line)\n for err in fatal_errors:\n print(err)\n print('模型同步失败:存在 provider 同步后无可用模型,已中止写入')\n raise SystemExit(2)\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n for line in summary:\n print(line)\n print('OpenClaw API 模型一致性同步完成')\nelse:\n for line in summary:\n print(line)\n print('无需同步:配置已与上游 /models 保持一致')\nPY\n\t}\n\n\t_install_openclaw_core() {\n\t\tinstall_node_and_tools\n\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf ssh://git@github.com/\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf git@github.com:\n\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_prefix npm_bin\n\t\tnpm_prefix=$(npm prefix -g 2>/dev/null)\n\t\tnpm_bin=\"${npm_prefix}/bin\"\n\t\tif [[ -n \"$npm_bin\" ]]; then\n\t\t\texport PATH=\"$npm_bin:$PATH\"\n\t\t\tfor rc in \"$HOME/.bashrc\" \"$HOME/.zshrc\" \"$HOME/.profile\"; do\n\t\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t\tgrep -qF \"$npm_bin\" \"$rc\" 2>/dev/null && continue\n\t\t\t\techo \"export PATH=\\\"${npm_bin}:\\$PATH\\\"\" >> \"$rc\"\n\t\t\tdone\n\t\tfi\n\t\topenclaw onboard --install-daemon\n\t\t_sed_i 's|\"profile\": \"messaging\"|\"profile\": \"full\"|g' ~/.openclaw/openclaw.json\n\t\tconfigure_openclaw_session_policy\n\t\tif ! is_macos; then\n\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tstart_gateway\n\t}\n\n\tinstall_moltbot() {\n\t\t_install_openclaw_core\n\t\tbreak_end\n\t}\n\n\t_cliproxy_dir() {\n\t\tif is_macos; then\n\t\t\techo \"$(brew --prefix 2>/dev/null || echo /usr/local)/etc\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi\"\n\t\tfi\n\t}\n\n\t_cliproxy_conf() {\n\t\tif is_macos; then\n\t\t\techo \"$(_cliproxy_dir)/cliproxyapi.conf\"\n\t\telse\n\t\t\techo \"$(_cliproxy_dir)/config.yaml\"\n\t\tfi\n\t}\n\n\t_cliproxy_bin() {\n\t\tif is_macos; then\n\t\t\tcommand -v cliproxyapi 2>/dev/null || echo \"cliproxyapi\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi/cli-proxy-api\"\n\t\tfi\n\t}\n\n\t_cliproxy_running() {\n\t\tif is_macos; then\n\t\t\tpgrep -f \"cliproxyapi\" >/dev/null 2>&1\n\t\telse\n\t\t\tpgrep -f \"cli-proxy-api\" >/dev/null 2>&1\n\t\tfi\n\t}\n\n\t_cliproxy_start_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services start cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tsystemctl --user enable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"systemctl --user start cliproxyapi.service 2>/dev/null; sleep 3\"\n\t\t\tif ! _cliproxy_running; then\n\t\t\t\tui_warn \"systemd 启动失败,改用后台直接运行...\"\n\t\t\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\t\t\t(cd \"$cliproxy_dir\" && nohup ./cli-proxy-api > /tmp/cliproxyapi.log 2>&1 &)\n\t\t\t\tsleep 3\n\t\t\tfi\n\t\tfi\n\t\tif _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已启动\"\n\t\t\treturn 0\n\t\telse\n\t\t\tui_err \"CLIProxyAPI 启动失败,请检查 $(_cliproxy_conf) 后手动启动\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\t_cliproxy_stop_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services stop cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbash -c \"systemctl --user stop cliproxyapi.service 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\tfi\n\t\tlocal _pat; _pat=$(is_macos && echo \"cliproxyapi\" || echo \"cli-proxy-api\")\n\t\tlocal pids; pids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\tif [[ -n \"$pids\" ]]; then\n\t\t\techo \"$pids\" | xargs kill 2>/dev/null || true\n\t\t\tsleep 2\n\t\t\tpids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\t\t[[ -n \"$pids\" ]] && echo \"$pids\" | xargs kill -9 2>/dev/null || true\n\t\tfi\n\t\tif ! _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已停止\"\n\t\telse\n\t\t\tui_err \"停止失败,仍有进程残留\"\n\t\tfi\n\t}\n\n\t_cliproxy_oauth_login() {\n\t\tlocal cliproxy_bin; cliproxy_bin=$(_cliproxy_bin)\n\t\tif ! command -v \"$cliproxy_bin\" &>/dev/null && [[ ! -x \"$cliproxy_bin\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 未安装:$cliproxy_bin\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_choice\n\t\tprovider_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' 选择要登录的 AI 提供商\\n ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"Claude (Anthropic)\" \\\n\t\t\t\"Gemini (Google)\" \\\n\t\t\t\"OpenAI / Codex\" \\\n\t\t\t\"Qwen (通义千问)\" \\\n\t\t\t\"iFlow\" \\\n\t\t\t\"取消\") || return 0\n\n\t\tlocal login_cmd=\"\" login_port=\"\"\n\t\tcase \"$provider_choice\" in\n\t\t\t\"Claude (Anthropic)\") login_cmd=\"--claude-login\"; login_port=\"54545\" ;;\n\t\t\t\"Gemini (Google)\") login_cmd=\"--login\"; login_port=\"8085\" ;;\n\t\t\t\"OpenAI / Codex\") login_cmd=\"--codex-login\"; login_port=\"1455\" ;;\n\t\t\t\"Qwen (通义千问)\") login_cmd=\"--qwen-login\"; login_port=\"\" ;;\n\t\t\t\"iFlow\") login_cmd=\"--iflow-login\"; login_port=\"11451\" ;;\n\t\t\t\"取消\"|*) return 0 ;;\n\t\tesac\n\n\t\techo\n\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\n\t\tlocal mode_input=\"\" proj_input=\"\"\n\t\tif [[ \"$login_cmd\" == \"--login\" ]]; then\n\t\t\tlocal mode_choice\n\t\t\tmode_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择 Gemini 登录模式\\n ↑↓ 移动 · Enter 确认(默认:Google One)' \\\n\t\t\t\t\"Google One(个人账号,自动发现项目)\" \\\n\t\t\t\t\"Code Assist(GCP 项目,手动选择)\") || return 0\n\t\t\t[[ \"$mode_choice\" == \"Code Assist\"* ]] && mode_input=\"1\" || mode_input=\"2\"\n\n\t\t\tif [[ \"$mode_input\" == \"1\" ]]; then\n\t\t\t\techo\n\t\t\t\tlocal proj_list proj_choice proj_num=\"1\"\n\t\t\t\tif command -v gcloud &>/dev/null; then\n\t\t\t\t\tproj_list=$(gcloud projects list --format=\"value(projectId,name)\" 2>/dev/null)\n\t\t\t\tfi\n\t\t\t\tif [[ -n \"$proj_list\" ]]; then\n\t\t\t\t\tproj_choice=$(printf \"%s\\n\" \"$proj_list\" | \\\n\t\t\t\t\t\tgum choose --cursor \"❯ \" \\\n\t\t\t\t\t\t--header $' 选择 GCP 项目\\n ↑↓ 移动 · Enter 确认') || return 0\n\t\t\t\t\tproj_num=$(printf \"%s\\n\" \"$proj_list\" | \\\n\t\t\t\t\t\tawk -v sel=\"$proj_choice\" 'NR==sel_line || $0==sel {print NR; exit}' \\\n\t\t\t\t\t\tsel=\"$proj_choice\" 2>/dev/null || echo \"1\")\n\t\t\t\t\t[[ -z \"$proj_num\" || \"$proj_num\" == \"0\" ]] && proj_num=\"1\"\n\t\t\t\telse\n\t\t\t\t\tlocal proj_raw\n\t\t\t\t\tproj_raw=$(gum input \\\n\t\t\t\t\t\t--placeholder \"项目编号或直接按 Enter 使用默认(1)\" \\\n\t\t\t\t\t\t--prompt \" ❯ \" --width 60) || return 0\n\t\t\t\t\tproj_num=\"${proj_raw:-1}\"\n\t\t\t\tfi\n\t\t\t\tproj_input=\"$proj_num\"\n\t\t\tfi\n\t\tfi\n\n\t\tif [[ -n \"$login_port\" ]]; then\n\t\t\tif is_macos; then\n\t\t\t\tgum style \\\n\t\t\t\t\t--border rounded --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 本地浏览器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 浏览器将自动打开,完成授权后自动返回,请稍候...\"\n\t\t\t\techo\n\t\t\t\tif [[ -n \"$mode_input\" ]]; then\n\t\t\t\t\tprintf \"%s\\n%s\\n\" \"$mode_input\" \"$proj_input\" | \"$cliproxy_bin\" \"$login_cmd\"\n\t\t\t\telse\n\t\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\t\tfi\n\t\t\telse\n\t\t\t\tgum style \\\n\t\t\t\t\t--border rounded --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 服务器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 1. 脚本后台启动 cli-proxy-api,并打印 OAuth 授权链接\" \\\n\t\t\t\t\t\" 2. 在本地浏览器打开该链接,完成账号授权\" \\\n\t\t\t\t\t\" 3. 授权后浏览器跳转到 localhost:${login_port},页面报错属正常\" \\\n\t\t\t\t\t\" 4. 复制地址栏完整 URL,粘贴到下方提示符,脚本自动完成握手\"\n\t\t\t\techo\n\t\t\t\tgum style --foreground 208 \\\n\t\t\t\t\t\" ⚡ 无需端口转发 — 回调 URL 里已含 code,直接转发给本机处理\"\n\t\t\t\techo\n\n\t\t\t\tif [[ -n \"$mode_input\" ]]; then\n\t\t\t\t\tlocal fifo; fifo=$(mktemp -u /tmp/cp_login_XXXXXX)\n\t\t\t\t\tmkfifo \"$fifo\"\n\t\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser < \"$fifo\") &\n\t\t\t\t\tlocal cli_pid=$!\n\t\t\t\t\texec 9>\"$fifo\"\n\t\t\t\t\tprintf \"%s\\n\" \"$mode_input\" >&9\n\t\t\t\t\tsleep 0.3\n\t\t\t\t\t[[ -n \"$proj_input\" ]] && { printf \"%s\\n\" \"$proj_input\" >&9; sleep 0.3; }\n\t\t\t\t\texec 9>&-\n\t\t\t\t\trm -f \"$fifo\"\n\t\t\t\telse\n\t\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser) &\n\t\t\t\t\tlocal cli_pid=$!\n\t\t\t\tfi\n\t\t\t\tsleep 2\n\n\t\t\t\techo\n\t\t\t\tlocal callback_url\n\t\t\t\tcallback_url=$(gum input \\\n\t\t\t\t\t--placeholder \"http://localhost:${login_port}/oauth2callback?code=...\" \\\n\t\t\t\t\t--prompt \" ❯ \" \\\n\t\t\t\t\t--width 120 \\\n\t\t\t\t\t--char-limit 2000) || true\n\n\t\t\t\tif [[ -n \"$callback_url\" ]]; then\n\t\t\t\t\tui_info \"正在向本机发送回调...\"\n\t\t\t\t\tcurl -sf \"$callback_url\" > /dev/null 2>&1 || true\n\t\t\t\t\tsleep 1\n\t\t\t\tfi\n\t\t\t\twait \"$cli_pid\" 2>/dev/null || true\n\t\t\tfi\n\t\telse\n\t\t\tui_info \"Qwen 使用 device flow,无需粘贴回调\"\n\t\t\techo\n\t\t\tgum style --foreground 240 \"将打印授权码 + URL,在任意浏览器中打开并输入授权码即可\"\n\t\t\techo\n\t\t\tif is_macos; then\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser)\n\t\t\tfi\n\t\tfi\n\t}\n\n\tcliproxyapi_manage_menu() {\n\t\tlocal config_file; config_file=$(_cliproxy_conf)\n\n\t\twhile true; do\n\t\t\tclear\n\n\t\t\tlocal cp_installed cp_running cp_version cp_port cp_keys\n\t\t\tif is_macos; then\n\t\t\t\tcp_version=$(brew info cliproxyapi 2>/dev/null | awk 'NR==1{print $4}')\n\t\t\telse\n\t\t\t\tcp_version=$(cat \"$(_cliproxy_dir)/version.txt\" 2>/dev/null || echo \"\")\n\t\t\tfi\n\t\t\tif [[ -n \"$cp_version\" ]]; then\n\t\t\t\tcp_installed=\"\\033[38;5;46m◉ INSTALLED v${cp_version}\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_installed=\"\\033[90m○ NOT INSTALLED\\033[0m\"\n\t\t\tfi\n\t\t\tif _cliproxy_running; then\n\t\t\t\tcp_running=\"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_running=\"\\033[90m■ STOPPED\\033[0m\"\n\t\t\tfi\n\t\t\tcp_port=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\t\t\tcp_port=\"${cp_port:-8317}\"\n\t\t\tcp_keys=$(awk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{n++} END{print n+0}' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 51 \\\n\t\t\t\t--border rounded --border-foreground 51 \\\n\t\t\t\t--padding \"1 4\" --align center \\\n\t\t\t\t\"C L I P R O X Y A P I\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ AI OAuth Proxy Manager ]\"\n\t\t\techo -e \" $cp_installed $cp_running\"\n\t\t\techo -e \" \\033[90m端口 :${cp_port} API Keys: ${cp_keys}\\033[0m\"\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' ─── CLIProxyAPI ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"启动\" \\\n\t\t\t\t\"停止\" \\\n\t\t\t\t\"重启\" \\\n\t\t\t\t\"查看日志\" \\\n\t\t\t\t\"账号授权登录\" \\\n\t\t\t\t\"生成并添加 API Key\" \\\n\t\t\t\t\"查看 API Keys\" \\\n\t\t\t\t\"编辑配置文件\" \\\n\t\t\t\t\"更新\" \\\n\t\t\t\t\"卸载\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"启动\")\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"停止\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"重启\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tsleep 1\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看日志\")\n\t\t\t\t\techo\n\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\tlocal brew_log\n\t\t\t\t\t\tbrew_log=\"$(brew --prefix 2>/dev/null)/var/log/cliproxyapi.log\"\n\t\t\t\t\t\tif [[ -f \"$brew_log\" ]]; then\n\t\t\t\t\t\t\ttail -80 \"$brew_log\"\n\t\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\telif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\t\t\tjournalctl --user -u cliproxyapi.service --no-pager -n 80\n\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"账号授权登录\")\n\t\t\t\t\t_cliproxy_oauth_login\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"生成并添加 API Key\")\n\t\t\t\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\t\tlocal new_key\n\t\t\t\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\t\t\t\techo\n\t\t\t\t\tgum style --bold --foreground 46 \" ◉ 已生成新 API Key:\"\n\t\t\t\t\tgum style --bold --foreground 51 \" $new_key\"\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 将此 Key 自动写入 config.yaml?\"; then\n\t\t\t\t\t\tpython3 - \"$config_file\" \"$new_key\" <<'PY'\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n)', r'\\1 - \"' + key + '\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nprint(\"写入成功\")\nPY\n\t\t\t\t\t\tui_ok \"API Key 已写入配置\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看 API Keys\")\n\t\t\t\t\techo\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tgum style --foreground 240 \" config.yaml 中的 API Keys:\"\n\t\t\t\t\t\techo\n\t\t\t\t\t\tawk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{print \" \" $0}' \\\n\t\t\t\t\t\t\t\"$config_file\"\n\t\t\t\t\t\techo\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到配置文件:$config_file\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"编辑配置文件\")\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tnano \"$config_file\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\tfi\n\t\t\t\t\t;;\n\t\t\t\t\"更新\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 确认更新 CLIProxyAPI 到最新版本?\"; then\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew upgrade cliproxyapi && ui_ok \"更新完成\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tcurl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer \\\n\t\t\t\t\t\t\t\t| bash -s -- upgrade\n\t\t\t\t\t\t\tui_ok \"更新完成\"\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"卸载\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" ⚡ 确认卸载 CLIProxyAPI?\"; then\n\t\t\t\t\t\t_cliproxy_stop_service 2>/dev/null || true\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew uninstall cliproxyapi 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$HOME/.cli-proxy-api\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tsystemctl --user disable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\t\t\t\t\trm -f \"$HOME/.config/systemd/user/cliproxyapi.service\" 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$(_cliproxy_dir)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\t\tui_ok \"CLIProxyAPI 已卸载\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tbeginner_mode_install() {\n\t\tif is_macos; then\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border rounded --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ API Key 配置 ]\"\n\t\telse\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border rounded --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ CLIProxyAPI ⟶ AI Auth ⟶ Auto Config ]\"\n\t\tfi\n\t\techo\n\n\t\tui_step \"第 1 步:安装 OpenClaw\"\n\t\t_install_openclaw_core\n\n\t\tui_step \"第 2 步:安装 CLIProxyAPI\"\n\t\tif is_macos; then\n\t\t\tif ! gum spin --spinner globe --title \"正在安装 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbrew install cliproxyapi; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败(brew install cliproxyapi)\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\telse\n\t\t\tif ! curl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer | bash; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\n\t\tlocal cliproxy_config; cliproxy_config=$(_cliproxy_conf)\n\t\tif [[ ! -f \"$cliproxy_config\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 安装后未找到配置文件: $cliproxy_config\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\t\tui_ok \"CLIProxyAPI 安装完成\"\n\n\t\tui_step \"第 3 步:登录 AI 账号\"\n\t\tui_info \"通过 OAuth 授权即可免费使用,无需付费 API Key\"\n\t\techo\n\t\t_cliproxy_oauth_login\n\n\t\tui_step \"第 4 步:生成 API Key 并启动服务\"\n\t\tlocal new_key\n\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\tpython3 - \"$cliproxy_config\" \"$new_key\" <<'PY' 2>/dev/null\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n(?:\\s*-\\s*\"[^\"]*\"\\s*\\n)*)', lambda m: m.group(0) + f' - \"{key}\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nPY\n\t\tif ! _cliproxy_start_service; then\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_step \"第 5 步:配置 OpenClaw API\"\n\n\t\tlocal api_key port base_url\n\t\tapi_key=$(awk '\n\t\t\t/^api-keys:/ { in_keys=1; next }\n\t\t\tin_keys && /^[^ \\t]/ { exit }\n\t\t\tin_keys && /\"sk-/ { match($0, /sk-[^\"]+/); print substr($0, RSTART, RLENGTH); exit }\n\t\t' \"$cliproxy_config\" 2>/dev/null)\n\n\t\tport=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \"$cliproxy_config\" 2>/dev/null)\n\t\tport=\"${port:-8317}\"\n\t\tbase_url=\"http://localhost:${port}/v1\"\n\n\t\tif [[ -z \"$api_key\" ]]; then\n\t\t\tui_err \"无法从 $cliproxy_config 读取 API Key,请手动在「CLIProxyAPI 管理」中添加\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_info \"地址:$base_url\"\n\t\tui_info \"Key:${api_key:0:12}****\"\n\t\techo\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"cliproxy\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tstart_gateway\n\n\t\t\tui_step \"第 6 步:选择默认模型\"\n\t\t\tui_info \"请从可用模型中选择一个作为默认,选完后网关自动重启\"\n\t\t\techo\n\t\t\tchange_model\n\n\t\t\techo\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 46 \\\n\t\t\t\t--border rounded --border-foreground 46 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◉ INSTALL COMPLETE ◉\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw 已自动接入 CLIProxyAPI,可直接开始使用 ]\" \\\n\t\t\t\t\"[ 更多提供商:cd ~/cliproxyapi && ./cli-proxy-api --help ]\"\n\t\tfi\n\n\t\techo\n\t\tif gum confirm \" 现在去对接机器人?\"; then\n\t\t\tchange_tg_bot_code\n\t\telse\n\t\t\tbreak_end\n\t\tfi\n\t}\n\n\tstart_bot() {\n\t\tstart_gateway\n\t\tbreak_end\n\t}\n\n\tstop_bot() {\n\t\ttmux kill-session -t gateway > /dev/null 2>&1\n\t\tgum spin --spinner pulse --title \"正在停止网关...\" -- openclaw gateway stop\n\t\tbreak_end\n\t}\n\n\tview_logs() {\n\t\topenclaw status\n\t\topenclaw gateway status\n\t\topenclaw logs\n\t\tbreak_end\n\t}\n\n\tadd-all-models-from-provider() {\n\t\tlocal provider_name=\"$1\"\n\t\tlocal base_url=\"$2\"\n\t\tlocal api_key=\"$3\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tlocal models_tmpfile\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取 $provider_name 的模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\n\t\tlocal models_json\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tif [[ -z \"$models_json\" ]]; then\n\t\t\techo \"错误:无法获取模型列表\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_ids\n\t\tmodel_ids=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n for m in models:\n if isinstance(m, dict) and \"id\" in m:\n print(m[\"id\"])\nexcept Exception:\n pass\n')\n\n\t\tif [[ -z \"$model_ids\" ]]; then\n\t\t\techo \"错误:未找到任何模型\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_count\n\t\tmodel_count=$(echo \"$model_ids\" | wc -l | tr -d ' ')\n\t\techo \"发现 $model_count 个模型\"\n\n\t\tlocal models_array=\"[\"\n\t\tlocal first=true\n\n\t\twhile read -r model_id; do\n\t\t\t[[ $first == false ]] && models_array+=\",\"\n\t\t\tfirst=false\n\n\t\t\tlocal context_window=1048576\n\t\t\tlocal max_tokens=128000\n\t\t\tlocal input_cost=0.15\n\t\t\tlocal output_cost=0.60\n\n\t\t\tcase \"$model_id\" in\n\t\t\t\t*opus*|*pro*|*preview*|*thinking*|*sonnet*)\n\t\t\t\t\tinput_cost=2.00\n\t\t\t\t\toutput_cost=12.00\n\t\t\t\t\t;;\n\t\t\t\t*gpt-5*|*codex*)\n\t\t\t\t\tinput_cost=1.25\n\t\t\t\t\toutput_cost=10.00\n\t\t\t\t\t;;\n\t\t\t\t*flash*|*lite*|*haiku*|*mini*|*nano*)\n\t\t\t\t\tinput_cost=0.10\n\t\t\t\t\toutput_cost=0.40\n\t\t\t\t\t;;\n\t\t\tesac\n\n\t\t\tmodels_array+=$(cat <<EOF\n{\n\t\"id\": \"$model_id\",\n\t\"name\": \"$provider_name / $model_id\",\n\t\"input\": [\"text\", \"image\"],\n\t\"contextWindow\": $context_window,\n\t\"maxTokens\": $max_tokens,\n\t\"cost\": {\n\t\t\"input\": $input_cost,\n\t\t\"output\": $output_cost,\n\t\t\"cacheRead\": 0,\n\t\t\"cacheWrite\": 0\n\t}\n}\nEOF\n)\n\t\tdone <<< \"$model_ids\"\n\n\t\tmodels_array+=\"]\"\n\n\t\t[[ -f \"$config_file\" ]] && cp \"$config_file\" \"${config_file}.bak.$(date +%s)\"\n\n\t\tjq --arg prov \"$provider_name\" \\\n\t\t --arg url \"$base_url\" \\\n\t\t --arg key \"$api_key\" \\\n\t\t --argjson models \"$models_array\" \\\n\t\t'\n\t\t.models |= (\n\t\t\t(. // { mode: \"merge\", providers: {} })\n\t\t\t| .mode = \"merge\"\n\t\t\t| .providers[$prov] = {\n\t\t\t\tbaseUrl: $url,\n\t\t\t\tapiKey: $key,\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tmodels: $models\n\t\t\t}\n\t\t)\n\t\t| .agents |= (. // {})\n\t\t| .agents.defaults |= (. // {})\n\t\t| .agents.defaults.models |= (\n\t\t\t(if type == \"object\" then .\n\t\t\t elif type == \"array\" then reduce .[] as $m ({}; if ($m|type) == \"string\" then .[$m] = {} else . end)\n\t\t\t else {}\n\t\t\t end) as $existing\n\t\t\t| reduce ($models[]? | .id? // empty | tostring) as $mid (\n\t\t\t\t$existing;\n\t\t\t\tif ($mid | length) > 0 then\n\t\t\t\t\t.[\"\\($prov)/\\($mid)\"] //= {}\n\t\t\t\telse\n\t\t\t\t\t.\n\t\t\t\tend\n\t\t\t)\n\t\t)\n\t\t' \"$config_file\" > \"${config_file}.tmp\" && mv \"${config_file}.tmp\" \"$config_file\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\techo \"成功添加 $model_count 个模型到 $provider_name\"\n\t\t\techo \"模型引用格式: $provider_name/<model-id>\"\n\t\t\treturn 0\n\t\telse\n\t\t\techo \"错误:配置注入失败\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\tadd-openclaw-provider-interactive() {\n\t\tlocal provider_name base_url api_key\n\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"Provider 名称 (如: deepseek)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\t[[ -z \"$provider_name\" ]] && return 1\n\n\t\tbase_url=$(gum input \\\n\t\t\t--placeholder \"Base URL (如: https://api.xxx.com/v1)\" \\\n\t\t\t--prompt \"Base URL > \")\n\t\t[[ -z \"$base_url\" ]] && return 1\n\t\tbase_url=\"${base_url%/}\"\n\n\t\tapi_key=$(gum input --password \\\n\t\t\t--placeholder \"API Key\" \\\n\t\t\t--prompt \"API Key > \")\n\t\t[[ -z \"$api_key\" ]] && return 1\n\n\t\tlocal models_tmpfile models_json available_models model_count\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tlocal default_model=\"\"\n\t\tif [[ -n \"$models_json\" ]]; then\n\t\t\tavailable_models=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n ids = sorted(m[\"id\"] for m in models if isinstance(m, dict) and \"id\" in m)\n print(\"\\n\".join(ids))\nexcept Exception:\n pass\n')\n\t\t\tmodel_count=$(echo \"$available_models\" | wc -l | tr -d ' ')\n\t\tfi\n\n\t\tif [[ -n \"$available_models\" ]]; then\n\t\t\tdefault_model=$(echo \"$available_models\" | fzf \\\n\t\t\t\t--prompt=\" ❯ \" \\\n\t\t\t\t--header=\" 发现 $model_count 个模型 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 选第一个\" \\\n\t\t\t\t--header-first \\\n\t\t\t\t--height=15 \\\n\t\t\t\t--layout=reverse \\\n\t\t\t\t--border=rounded \\\n\t\t\t\t--border-label=\" ◈ DEFAULT MODEL \" \\\n\t\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || default_model=$(echo \"$available_models\" | head -1)\n\t\t\t[[ -z \"$default_model\" ]] && default_model=$(echo \"$available_models\" | head -1)\n\t\t\tdefault_model=$(awk '{print $1}' <<< \"$default_model\")\n\t\tfi\n\n\t\techo\n\t\tgum style \\\n\t\t\t--border normal --border-foreground 99 \\\n\t\t\t--padding \"0 2\" \\\n\t\t\t\"Provider : $provider_name\" \\\n\t\t\t\"Base URL : $base_url\" \\\n\t\t\t\"API Key : ${api_key:0:8}****\" \\\n\t\t\t\"默认模型 : $default_model\" \\\n\t\t\t\"模型总数 : $model_count\"\n\t\techo\n\n\t\tgum confirm \"确认添加所有 $model_count 个模型?\" || { echo \"已取消\"; return 1; }\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"$provider_name\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tgum spin --spinner minidot --title \"设置默认模型并重启网关...\" -- \\\n\t\t\t\topenclaw models set \"$provider_name/$default_model\"\n\t\t\tstart_gateway\n\t\t\techo \"完成:所有 $model_count 个模型已加载\"\n\t\tfi\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_list() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tlocal _py_list; _py_list=$(mktemp /tmp/oc_list_XXXXXX.py)\n\t\tcat > \"$_py_list\" << 'PYEOF'\nimport json, sys, time, urllib.request\n\npath = sys.argv[1]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\ndef ping_models(base_url, api_key):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': 'Bearer ' + api_key,\n 'User-Agent': 'OpenClaw-API-Manage/1.0',\n },\n )\n start = time.perf_counter()\n with urllib.request.urlopen(req, timeout=4) as resp:\n resp.read(2048)\n return int((time.perf_counter() - start) * 1000)\n\ndef classify_latency(latency):\n if latency == '不可用':\n return '不可用', 'unavailable'\n if latency == '未检测':\n return '未检测', 'unchecked'\n if isinstance(latency, int):\n level = 'low' if latency <= 800 else ('medium' if latency <= 2000 else 'high')\n return str(latency) + 'ms', level\n return str(latency), 'unchecked'\n\ntry:\n with open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\nexcept FileNotFoundError:\n print('MSG\\t未找到 openclaw.json,请先完成安装/初始化。')\n raise SystemExit(0)\nexcept Exception as e:\n print('MSG\\t读取配置失败: ' + type(e).__name__ + ': ' + str(e))\n raise SystemExit(0)\n\nproviders = ((obj.get('models') or {}).get('providers') or {})\nif not isinstance(providers, dict) or not providers:\n print('MSG\\t当前未配置任何 API provider。')\n raise SystemExit(0)\n\nprint('MSG\\t--- 已配置 API 列表 ---')\n\nfor idx, name in enumerate(sorted(providers.keys()), start=1):\n provider = providers.get(name)\n if not isinstance(provider, dict):\n base_url = '-'; model_count = 0; latency_raw = '不可用'\n else:\n base_url = provider.get('baseUrl') or provider.get('url') or provider.get('endpoint') or '-'\n models = provider.get('models') if isinstance(provider.get('models'), list) else []\n model_count = sum(1 for m in models if isinstance(m, dict) and m.get('id'))\n api = provider.get('api', '')\n api_key = provider.get('apiKey')\n latency_raw = '未检测'\n if api in SUPPORTED_APIS and isinstance(base_url, str) and base_url != '-' and isinstance(api_key, str) and api_key:\n try:\n latency_raw = ping_models(base_url, api_key)\n except Exception:\n latency_raw = '不可用'\n elif api in SUPPORTED_APIS:\n latency_raw = '不可用'\n\n latency_text, latency_level = classify_latency(latency_raw)\n print('ROW\\t' + '\\t'.join([str(idx), str(name), str(base_url), str(model_count), str(latency_text), str(latency_level)]))\nPYEOF\n\n\t\twhile IFS=$'\\t' read -r rec_type idx name base_url model_count latency_txt latency_level; do\n\t\t\tcase \"$rec_type\" in\n\t\t\t\tMSG)\n\t\t\t\t\techo \"$idx\"\n\t\t\t\t\t;;\n\t\t\t\tROW)\n\t\t\t\t\tlocal latency_color=\"$gl_bai\"\n\t\t\t\t\tcase \"$latency_level\" in\n\t\t\t\t\t\tlow) latency_color=\"$gl_lv\" ;;\n\t\t\t\t\t\tmedium) latency_color=\"$gl_huang\" ;;\n\t\t\t\t\t\thigh|unavailable) latency_color=\"$gl_hong\" ;;\n\t\t\t\t\t\tunchecked) latency_color=\"$gl_bai\" ;;\n\t\t\t\t\tesac\n\n\t\t\t\t\tprintf '%b\\n' \"[$idx] ${name} | API: ${base_url} | 模型数量: ${gl_huang}${model_count}${gl_bai} | 延迟/状态: ${latency_color}${latency_txt}${gl_bai}\"\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone < <(python3 \"$_py_list\" \"$config_file\")\n\t\trm -f \"$_py_list\"\n\t}\n\n\tsync-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要同步的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tinstall jq curl >/dev/null 2>&1\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY2'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\ntarget = sys.argv[2]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('错误:未检测到 API providers,无法同步')\n raise SystemExit(2)\n\nprovider = providers.get(target)\nif not isinstance(provider, dict):\n print(f'错误:未找到 provider: {target}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef fetch_remote_models_with_retry(base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n return json.loads(payload), None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\napi = provider.get('api', '')\nbase_url = provider.get('baseUrl')\napi_key = provider.get('apiKey')\nmodel_list = provider.get('models', [])\n\nif not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n print(f'错误:provider {target} 缺少 baseUrl/apiKey/models,无法执行同步')\n raise SystemExit(3)\n\nif api not in SUPPORTED_APIS:\n print(f'错误:provider {target} 当前 api={api},不支持直接 /models 同步')\n raise SystemExit(3)\n\ndata, err, attempts = fetch_remote_models_with_retry(base_url, api_key, retries=3)\nif err is not None:\n print(f'错误:{target}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n raise SystemExit(4)\n\nif not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n print(f'错误:{target}: /models 返回结构不可识别')\n raise SystemExit(4)\n\nremote_ids = []\nfor item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\nremote_set = set(remote_ids)\nif not remote_set:\n print(f'错误:{target}: 上游 /models 为空,已中止同步')\n raise SystemExit(5)\n\nlocal_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\nlocal_ids = [str(m['id']) for m in local_models]\nlocal_set = set(local_ids)\n\ntemplate = copy.deepcopy(local_models[0]) if local_models else None\nif template is None:\n print(f'错误:{target}: 本地 models 无有效模板模型,无法补全新增模型')\n raise SystemExit(3)\n\nremoved_ids = [mid for mid in local_ids if mid not in remote_set]\nadded_ids = [mid for mid in remote_ids if mid not in local_set]\n\nkept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\nnew_models = kept_models[:]\nfor mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{target} / {mid}'\n new_models.append(nm)\n\nif not new_models:\n print(f'错误:{target}: 同步后无可用模型,已中止写入')\n raise SystemExit(5)\n\nexpected_refs = {model_ref(target, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\nlocal_refs = {model_ref(target, mid) for mid in local_ids}\nremoved_refs = local_refs - expected_refs\nfirst_ref = model_ref(target, str(new_models[0]['id']))\n\nchanged = False\nprimary_ref = get_primary_ref(defaults)\nif isinstance(primary_ref, str) and primary_ref in removed_refs:\n set_primary_ref(defaults, first_ref)\n changed = True\n print(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in removed_refs:\n defaults[fk] = first_ref\n changed = True\n print(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\nstale_refs = [r for r in list(defaults_models.keys()) if r.startswith(target + '/') and r not in expected_refs]\nfor r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\nfor r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\nif removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'{target}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\nif changed:\n print('指定 provider 模型一致性同步完成')\nelse:\n print('无需同步:该 provider 配置已与上游 /models 保持一致')\nPY2\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"同步完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在或未配置\" ;;\n\t\t\t3) echo \"错误:provider 配置不完整或类型不支持\" ;;\n\t\t\t4) echo \"错误:上游 /models 请求失败\" ;;\n\t\t\t5) echo \"错误:上游模型为空或同步后无可用模型\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\tdelete-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要删除的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY'\nimport copy\nimport json\nimport sys\n\npath = sys.argv[1]\nname = sys.argv[2]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or name not in providers:\n print(f'错误:未找到 provider: {name}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\nreplacement_candidates = collect_available_refs(exclude_provider=name)\nreplacement = replacement_candidates[0] if replacement_candidates else None\n\nprimary_ref = get_primary_ref(defaults)\nif ref_provider(primary_ref) == name:\n if not replacement:\n print('错误:删除中止:默认主模型指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n set_primary_ref(defaults, replacement)\n print(f'默认主模型切换: {primary_ref} -> {replacement}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n if not replacement:\n print(f'错误:删除中止:{fk} 指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n defaults[fk] = replacement\n print(f'{fk} 切换: {val} -> {replacement}')\n\nremoved_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\nfor r in removed_refs:\n defaults_models.pop(r, None)\n\nproviders.pop(name, None)\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'已删除 provider: {name}')\nprint(f'已清理 defaults.models 中 {len(removed_refs)} 个关联模型引用')\nPY\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"删除完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在\" ;;\n\t\t\t3) echo \"错误:无可用替代模型,已保持原配置\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"API 管理\"\n\t\t\topenclaw_api_manage_list\n\t\t\techo\n\n\t\t\tlocal api_choice\n\t\t\tapi_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加 API\" \\\n\t\t\t\t\"同步 API 供应商模型列表\" \\\n\t\t\t\t\"删除 API\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$api_choice\" in\n\t\t\t\t\"添加 API\") add-openclaw-provider-interactive ;;\n\t\t\t\t\"同步 API 供应商模型列表\") sync-openclaw-provider-interactive ;;\n\t\t\t\t\"删除 API\") delete-openclaw-provider-interactive ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tchange_model() {\n\t\tlocal all_models model\n\t\tall_models=$(openclaw models list --all 2>/dev/null | grep \"configured\")\n\n\t\tif [[ -z \"$all_models\" ]]; then\n\t\t\tui_err \"无法获取可用模型列表(请先添加 API 提供商)\"\n\t\t\tbreak_end\n\t\t\treturn\n\t\tfi\n\n\t\tmodel=$(echo \"$all_models\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 当前: $(openclaw models list 2>/dev/null | awk '{print $1}' | head -1) │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=20 \\\n\t\t\t--layout=reverse \\\n\t\t\t--border=rounded \\\n\t\t\t--border-label=\" ◈ MODEL SELECT \" \\\n\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || return\n\n\t\t[[ -z \"$model\" ]] && return\n\t\tmodel=$(awk '{print $1}' <<< \"$model\")\n\n\t\tgum spin --spinner minidot --title \"正在切换模型...\" -- openclaw models set \"$model\"\n\t\tstart_gateway\n\t\tui_ok \"已切换至:$model\"\n\t\tbreak_end\n\t}\n\n\tresolve_openclaw_plugin_id() {\n\t\tlocal raw_input=\"$1\"\n\t\tlocal plugin_id=\"$raw_input\"\n\n\t\tplugin_id=\"${plugin_id#@openclaw/}\"\n\t\tif [[ \"$plugin_id\" == @*/* ]]; then\n\t\t\tplugin_id=\"${plugin_id##*/}\"\n\t\tfi\n\t\tplugin_id=\"${plugin_id%%@*}\"\n\t\techo \"$plugin_id\"\n\t}\n\n\tsync_openclaw_plugin_allowlist() {\n\t\tlocal plugin_id=\"$1\"\n\t\t[[ -z \"$plugin_id\" ]] && return 1\n\n\t\tlocal home_config=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal root_config=\"/root/.openclaw/openclaw.json\"\n\t\tlocal config_file=\"$home_config\"\n\t\tif [[ ! -f \"$home_config\" && -f \"$root_config\" ]]; then\n\t\t\tconfig_file=\"$root_config\"\n\t\tfi\n\n\t\tmkdir -p \"$(dirname \"$config_file\")\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo '{}' > \"$config_file\"\n\t\tfi\n\n\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\tlocal tmp_json\n\t\t\ttmp_json=$(mktemp)\n\t\t\tif jq --arg pid \"$plugin_id\" '\n\t\t\t\t.plugins = (if (.plugins | type) == \"object\" then .plugins else {} end)\n\t\t\t\t| .plugins.allow = (if (.plugins.allow | type) == \"array\" then .plugins.allow else [] end)\n\t\t\t\t| if (.plugins.allow | index($pid)) == null then .plugins.allow += [$pid] else . end\n\t\t\t' \"$config_file\" > \"$tmp_json\" 2>/dev/null && mv \"$tmp_json\" \"$config_file\"; then\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\t\trm -f \"$tmp_json\"\n\t\tfi\n\n\t\tif command -v python3 >/dev/null 2>&1; then\n\t\t\tif python3 - \"$config_file\" \"$plugin_id\" <<'PYTHON_EOF'\nimport json\nimport sys\nfrom pathlib import Path\n\nconfig_file = Path(sys.argv[1])\nplugin_id = sys.argv[2]\n\ntry:\n data = json.loads(config_file.read_text(encoding='utf-8')) if config_file.exists() else {}\n if not isinstance(data, dict):\n data = {}\nexcept Exception:\n data = {}\n\nplugins = data.get('plugins')\nif not isinstance(plugins, dict):\n plugins = {}\n\na = plugins.get('allow')\nif not isinstance(a, list):\n a = []\n\nif plugin_id not in a:\n a.append(plugin_id)\n\nplugins['allow'] = a\ndata['plugins'] = plugins\nconfig_file.write_text(json.dumps(data, ensure_ascii=False, indent=2) + \"\\n\", encoding='utf-8')\nPYTHON_EOF\n\t\t\tthen\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\tfi\n\n\t\techo \"警告:已安装插件,但同步 plugins.allow 失败,请手动检查: $config_file\"\n\t\treturn 1\n\t}\n\n\tinstall_plugin() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"插件管理\"\n\t\t\tgum style --foreground 240 \"当前已安装插件:\"\n\t\t\topenclaw plugins list\n\t\t\techo\n\n\t\t\tlocal raw_input\n\t\t\traw_input=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择要安装的插件\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"feishu 飞书/Lark 集成\" \\\n\t\t\t\t\"telegram Telegram 机器人\" \\\n\t\t\t\t\"slack Slack 企业通讯\" \\\n\t\t\t\t\"msteams Microsoft Teams\" \\\n\t\t\t\t\"discord Discord 社区管理\" \\\n\t\t\t\t\"whatsapp WhatsApp 自动化\" \\\n\t\t\t\t\"memory-core 基础记忆 (文件检索)\" \\\n\t\t\t\t\"memory-lancedb 增强记忆 (向量数据库)\" \\\n\t\t\t\t\"copilot-proxy Copilot 接口转发\" \\\n\t\t\t\t\"lobster 审批流 (带人工确认)\" \\\n\t\t\t\t\"voice-call 语音通话能力\" \\\n\t\t\t\t\"nostr 加密隐私聊天\" \\\n\t\t\t\t\"手动输入插件 ID\") || break\n\n\t\t\tif [[ \"$raw_input\" == \"手动输入插件 ID\" ]]; then\n\t\t\t\traw_input=$(gum input \\\n\t\t\t\t\t--placeholder \"插件 ID (留空退出)\" \\\n\t\t\t\t\t--prompt \"Plugin > \") || break\n\t\t\t\t[[ -z \"$raw_input\" ]] && break\n\t\t\telse\n\t\t\t\traw_input=$(echo \"$raw_input\" | awk '{print $1}')\n\t\t\tfi\n\t\t\t[[ -z \"$raw_input\" ]] && break\n\n\t\t\tlocal plugin_id plugin_full\n\t\t\tplugin_id=$(resolve_openclaw_plugin_id \"$raw_input\")\n\t\t\tplugin_full=\"$raw_input\"\n\n\t\t\tlocal plugin_list\n\t\t\tplugin_list=$(openclaw plugins list 2>/dev/null)\n\n\t\t\tif echo \"$plugin_list\" | grep -qw \"$plugin_id\" && echo \"$plugin_list\" | grep \"$plugin_id\" | grep -q \"disabled\"; then\n\t\t\t\tui_info \"插件 [$plugin_id] 已预装,正在激活...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\" && ui_ok \"激活成功\" || ui_err \"激活失败\"\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/extensions/$plugin_id\" ]]; then\n\t\t\t\tui_info \"发现系统内置插件,尝试直接启用...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\telse\n\t\t\t\tui_info \"本地未发现,尝试下载安装...\"\n\t\t\t\trm -rf \"/root/.openclaw/extensions/$plugin_id\"\n\n\t\t\t\tif openclaw plugins install \"$plugin_full\"; then\n\t\t\t\t\tui_ok \"下载成功,正在启用...\"\n\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\telse\n\t\t\t\t\tui_warn \"官方渠道下载失败,尝试 npm 备选方案...\"\n\t\t\t\t\tif npm install -g \"$plugin_full\" --unsafe-perm; then\n\t\t\t\t\t\tui_ok \"npm 安装成功,尝试启用...\"\n\t\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"无法获取该插件,请检查 ID 是否正确或网络是否可用\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\tfi\n\t\t\tfi\n\n\t\t\tsync_openclaw_plugin_allowlist \"$plugin_id\"\n\t\t\tstart_gateway\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\tinstall_skill() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"技能管理\"\n\t\t\tgum style --foreground 240 \"当前已安装技能:\"\n\t\t\topenclaw skills list\n\t\t\techo\n\n\t\t\tlocal skill_name\n\t\t\tskill_name=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择要安装的技能\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"github 管理 GitHub Issues/PR/CI\" \\\n\t\t\t\t\"notion 操作 Notion 页面与数据库\" \\\n\t\t\t\t\"apple-notes macOS 原生笔记管理\" \\\n\t\t\t\t\"apple-reminders macOS 提醒事项管理\" \\\n\t\t\t\t\"1password 自动化读取 1Password 密钥\" \\\n\t\t\t\t\"gog Google Workspace 全能助手\" \\\n\t\t\t\t\"things-mac Things 3 任务管理\" \\\n\t\t\t\t\"bluebubbles 通过 BlueBubbles 收发 iMessage\" \\\n\t\t\t\t\"himalaya 终端邮件管理 (IMAP/SMTP)\" \\\n\t\t\t\t\"summarize 网页/播客/YouTube 内容总结\" \\\n\t\t\t\t\"openai-whisper 本地音频转文字\" \\\n\t\t\t\t\"coding-agent 运行 Claude Code/Codex 等编程助手\" \\\n\t\t\t\t\"手动输入技能名称\") || break\n\n\t\t\tif [[ \"$skill_name\" == \"手动输入技能名称\" ]]; then\n\t\t\t\tskill_name=$(gum input \\\n\t\t\t\t\t--placeholder \"技能名称 (留空退出)\" \\\n\t\t\t\t\t--prompt \"Skill > \") || break\n\t\t\t\t[[ -z \"$skill_name\" ]] && break\n\t\t\telse\n\t\t\t\tskill_name=$(echo \"$skill_name\" | awk '{print $1}')\n\t\t\tfi\n\t\t\t[[ -z \"$skill_name\" ]] && break\n\n\t\t\tlocal skill_found=false\n\t\t\tif [[ -d \"${HOME}/.openclaw/workspace/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在用户目录安装\"\n\t\t\t\tskill_found=true\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在系统目录安装\"\n\t\t\t\tskill_found=true\n\t\t\tfi\n\n\t\t\tif [[ \"$skill_found\" == true ]]; then\n\t\t\t\tgum confirm \"是否重新安装?\" || { break_end; continue; }\n\t\t\tfi\n\n\t\t\tgum spin --spinner globe --title \"正在安装技能 $skill_name...\" -- npx clawhub install \"$skill_name\"\n\t\t\tif [[ $? -eq 0 ]]; then\n\t\t\t\techo \"技能 $skill_name 安装成功\"\n\t\t\t\tstart_gateway\n\t\t\telse\n\t\t\t\techo \"错误:安装失败,请检查技能名称是否正确\"\n\t\t\tfi\n\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\topenclaw_json_get_bool() {\n\t\tlocal expr=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r \"$expr\" \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_channel_has_cfg() {\n\t\tlocal channel=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r --arg c \"$channel\" '\n\t\t\t(.channels[$c] // null) as $v\n\t\t\t| if ($v | type) != \"object\" then\n\t\t\t\tfalse\n\t\t\t else\n\t\t\t\t([ $v\n\t\t\t\t | to_entries[]\n\t\t\t\t | select((.key == \"enabled\" or .key == \"dmPolicy\" or .key == \"groupPolicy\" or .key == \"streaming\") | not)\n\t\t\t\t | .value\n\t\t\t\t | select(. != null and . != \"\" and . != false)\n\t\t\t\t ] | length) > 0\n\t\t\t end\n\t\t' \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_dir_has_files() {\n\t\tlocal dir=\"$1\"\n\t\t[[ -d \"$dir\" ]] && find \"$dir\" -type f -print -quit 2>/dev/null | grep -q .\n\t}\n\n\topenclaw_plugin_local_installed() {\n\t\tlocal plugin=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ -s \"$config_file\" ]] && jq -e --arg p \"$plugin\" '.plugins.installs[$p]' \"$config_file\" >/dev/null 2>&1; then\n\t\t\treturn 0\n\t\tfi\n\t\t[[ -d \"${HOME}/.openclaw/extensions/${plugin}\" ]] || [[ -d \"/usr/lib/node_modules/openclaw/extensions/${plugin}\" ]]\n\t}\n\n\topenclaw_bot_status_text() {\n\t\tlocal enabled=\"$1\"\n\t\tlocal configured=\"$2\"\n\t\tlocal connected=\"$3\"\n\t\tlocal abnormal=\"$4\"\n\t\tif [[ \"$abnormal\" == \"true\" ]]; then\n\t\t\techo \"异常\"\n\t\telif [[ \"$enabled\" != \"true\" ]]; then\n\t\t\techo \"未启用\"\n\t\telif [[ \"$connected\" == \"true\" ]]; then\n\t\t\techo \"已连接\"\n\t\telif [[ \"$configured\" == \"true\" ]]; then\n\t\t\techo \"已配置\"\n\t\telse\n\t\t\techo \"未配置\"\n\t\tfi\n\t}\n\n\topenclaw_colorize_bot_status() {\n\t\tlocal status=\"$1\"\n\t\tcase \"$status\" in\n\t\t\t已连接) echo -e \"${gl_lv}${status}${gl_bai}\" ;;\n\t\t\t已配置) echo -e \"${gl_huang}${status}${gl_bai}\" ;;\n\t\t\t异常) echo -e \"${gl_hong}${status}${gl_bai}\" ;;\n\t\t\t*) echo \"$status\" ;;\n\t\tesac\n\t}\n\n\topenclaw_print_bot_status_line() {\n\t\tlocal label=\"$1\"\n\t\tlocal status=\"$2\"\n\t\techo -e \"- ${label}: $(openclaw_colorize_bot_status \"$status\")\"\n\t}\n\n\topenclaw_show_bot_local_status_block() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal json_ok=\"false\"\n\t\tif [[ -s \"$config_file\" ]] && jq empty \"$config_file\" >/dev/null 2>&1; then\n\t\t\tjson_ok=\"true\"\n\t\tfi\n\n\t\tlocal tg_enabled tg_cfg tg_connected tg_abnormal tg_status\n\t\ttg_enabled=$(openclaw_json_get_bool '.channels.telegram.enabled // .plugins.entries.telegram.enabled // false')\n\t\ttg_cfg=$(openclaw_channel_has_cfg \"telegram\")\n\t\ttg_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/telegram\"; then\n\t\t\ttg_connected=\"true\"\n\t\tfi\n\t\ttg_abnormal=\"false\"\n\t\tif [[ \"$tg_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\ttg_abnormal=\"true\"\n\t\tfi\n\t\ttg_status=$(openclaw_bot_status_text \"$tg_enabled\" \"$tg_cfg\" \"$tg_connected\" \"$tg_abnormal\")\n\n\t\tlocal feishu_enabled feishu_cfg feishu_connected feishu_abnormal feishu_status\n\t\tfeishu_enabled=$(openclaw_json_get_bool '.plugins.entries.feishu.enabled // .channels.feishu.enabled // false')\n\t\tfeishu_cfg=$(openclaw_channel_has_cfg \"feishu\")\n\t\tfeishu_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/feishu\"; then\n\t\t\tfeishu_connected=\"true\"\n\t\tfi\n\t\tfeishu_abnormal=\"false\"\n\t\tif [[ \"$feishu_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"feishu\"; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$feishu_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tfeishu_status=$(openclaw_bot_status_text \"$feishu_enabled\" \"$feishu_cfg\" \"$feishu_connected\" \"$feishu_abnormal\")\n\n\t\tlocal wa_enabled wa_cfg wa_connected wa_abnormal wa_status\n\t\twa_enabled=$(openclaw_json_get_bool '.plugins.entries.whatsapp.enabled // .channels.whatsapp.enabled // false')\n\t\twa_cfg=$(openclaw_channel_has_cfg \"whatsapp\")\n\t\twa_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/whatsapp\"; then\n\t\t\twa_connected=\"true\"\n\t\tfi\n\t\twa_abnormal=\"false\"\n\t\tif [[ \"$wa_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"whatsapp\"; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$wa_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\twa_status=$(openclaw_bot_status_text \"$wa_enabled\" \"$wa_cfg\" \"$wa_connected\" \"$wa_abnormal\")\n\n\t\tlocal dc_enabled dc_cfg dc_connected dc_abnormal dc_status\n\t\tdc_enabled=$(openclaw_json_get_bool '.channels.discord.enabled // .plugins.entries.discord.enabled // false')\n\t\tdc_cfg=$(openclaw_channel_has_cfg \"discord\")\n\t\tdc_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/discord\"; then\n\t\t\tdc_connected=\"true\"\n\t\tfi\n\t\tdc_abnormal=\"false\"\n\t\tif [[ \"$dc_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tdc_abnormal=\"true\"\n\t\tfi\n\t\tdc_status=$(openclaw_bot_status_text \"$dc_enabled\" \"$dc_cfg\" \"$dc_connected\" \"$dc_abnormal\")\n\n\t\tlocal slack_enabled slack_cfg slack_connected slack_abnormal slack_status\n\t\tslack_enabled=$(openclaw_json_get_bool '.plugins.entries.slack.enabled // .channels.slack.enabled // false')\n\t\tslack_cfg=$(openclaw_channel_has_cfg \"slack\")\n\t\tslack_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/slack\"; then\n\t\t\tslack_connected=\"true\"\n\t\tfi\n\t\tslack_abnormal=\"false\"\n\t\tif [[ \"$slack_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"slack\"; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$slack_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tslack_status=$(openclaw_bot_status_text \"$slack_enabled\" \"$slack_cfg\" \"$slack_connected\" \"$slack_abnormal\")\n\n\t\tlocal qq_enabled qq_cfg qq_connected qq_abnormal qq_status\n\t\tqq_enabled=$(openclaw_json_get_bool '.plugins.entries.qqbot.enabled // .channels.qqbot.enabled // false')\n\t\tqq_cfg=$(openclaw_channel_has_cfg \"qqbot\")\n\t\tqq_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/sessions\" || openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/data\"; then\n\t\t\tqq_connected=\"true\"\n\t\tfi\n\t\tqq_abnormal=\"false\"\n\t\tif [[ \"$qq_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"qqbot\"; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$qq_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tqq_status=$(openclaw_bot_status_text \"$qq_enabled\" \"$qq_cfg\" \"$qq_connected\" \"$qq_abnormal\")\n\n\t\techo \"本地状态(仅本机配置/缓存,不做网络探测):\"\n\t\topenclaw_print_bot_status_line \"Telegram\" \"$tg_status\"\n\t\topenclaw_print_bot_status_line \"飞书(Lark)\" \"$feishu_status\"\n\t\topenclaw_print_bot_status_line \"WhatsApp\" \"$wa_status\"\n\t\topenclaw_print_bot_status_line \"Discord\" \"$dc_status\"\n\t\topenclaw_print_bot_status_line \"Slack\" \"$slack_status\"\n\t\topenclaw_print_bot_status_line \"QQ Bot\" \"$qq_status\"\n\t}\n\n\tchange_tg_bot_code() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"机器人连接对接\"\n\t\t\topenclaw_show_bot_local_status_block\n\t\t\techo\n\n\t\t\tlocal bot_choice\n\t\t\tbot_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择要对接的平台\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"Telegram 机器人对接\" \\\n\t\t\t\t\"飞书 (Lark) 机器人对接\" \\\n\t\t\t\t\"WhatsApp 机器人对接\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tlocal code\n\t\t\tcase \"$bot_choice\" in\n\t\t\t\t\"Telegram 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"TG机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve telegram \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"飞书 (Lark) 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"飞书机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve feishu \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"WhatsApp 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"WhatsApp连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve whatsapp \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"返回\"|*)\n\t\t\t\t\treturn 0\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\topenclaw_backup_root() {\n\t\techo \"${HOME}/.openclaw/backups\"\n\t}\n\n\topenclaw_is_interactive_terminal() {\n\t\t[[ -t 0 && -t 1 ]]\n\t}\n\n\topenclaw_has_command() {\n\t\tcommand -v \"$1\" >/dev/null 2>&1\n\t}\n\n\topenclaw_is_safe_relpath() {\n\t\tlocal rel=\"$1\"\n\t\t[[ -z \"$rel\" ]] && return 1\n\t\t[[ \"$rel\" == /* ]] && return 1\n\t\t[[ \"$rel\" == *//* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\n'* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\r'* ]] && return 1\n\t\tcase \"$rel\" in\n\t\t\t../*|*/../*|*/..|..)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t\treturn 0\n\t}\n\n\topenclaw_restore_path_allowed() {\n\t\tlocal mode=\"$1\"\n\t\tlocal rel=\"$2\"\n\t\tcase \"$mode\" in\n\t\t\tmemory)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\tMEMORY.md|AGENTS.md|USER.md|SOUL.md|TOOLS.md|memory/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\tproject)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\topenclaw.json|workspace/*|extensions/*|skills/*|prompts/*|tools/*|telegram/*|feishu/*|whatsapp/*|discord/*|slack/*|qqbot/*|logs/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\t*)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t}\n\n\topenclaw_pack_backup_archive() {\n\t\tlocal backup_type=\"$1\"\n\t\tlocal export_mode=\"$2\"\n\t\tlocal payload_dir=\"$3\"\n\t\tlocal output_file=\"$4\"\n\n\t\tlocal tmp_root\n\t\ttmp_root=$(mktemp -d) || return 1\n\t\tlocal pack_dir=\"$tmp_root/package\"\n\t\tmkdir -p \"$pack_dir\"\n\n\t\tcp -a \"$payload_dir\" \"$pack_dir/payload\"\n\n\t\t(\n\t\t\tcd \"$pack_dir/payload\" || exit 1\n\t\t\tfind . -type f | sed 's|^\\./||' | sort > \"$pack_dir/manifest.files\"\n\t\t\t: > \"$pack_dir/manifest.sha256\"\n\t\t\tlocal f\n\t\t\twhile IFS= read -r f; do\n\t\t\t\t[[ -z \"$f\" ]] && continue\n\t\t\t\tsha256sum \"$f\" >> \"$pack_dir/manifest.sha256\"\n\t\t\tdone < \"$pack_dir/manifest.files\"\n\t\t) || { rm -rf \"$tmp_root\"; return 1; }\n\n\t\tcat > \"$pack_dir/backup.meta\" <<EOF\nTYPE=$backup_type\nMODE=$export_mode\nCREATED_AT=$(date -u +'%Y-%m-%dT%H:%M:%SZ')\nHOST=$(hostname)\nEOF\n\n\t\tmkdir -p \"$(dirname \"$output_file\")\"\n\t\ttar -C \"$pack_dir\" -czf \"$output_file\" backup.meta manifest.files manifest.sha256 payload\n\t\tlocal rc=$?\n\t\trm -rf \"$tmp_root\"\n\t\treturn $rc\n\t}\n\n\topenclaw_offer_transfer_hint() {\n\t\tlocal file_path=\"$1\"\n\t\techo \"可使用以下方式下载备份文件:\"\n\t\techo \" 本地路径: $file_path\"\n\t\techo \" scp 示例: scp root@你的服务器:$file_path ./\"\n\t\techo \" 或使用 SFTP 客户端下载\"\n\t}\n\n\topenclaw_prepare_import_archive() {\n\t\tlocal expected_type=\"$1\"\n\t\tlocal archive_path=\"$2\"\n\t\tlocal unpack_root=\"$3\"\n\n\t\tif [[ ! -f \"$archive_path\" ]]; then\n\t\t\techo \"错误:文件不存在: $archive_path\"\n\t\t\treturn 1\n\t\tfi\n\t\tmkdir -p \"$unpack_root\"\n\t\tif ! tar -xzf \"$archive_path\" -C \"$unpack_root\"; then\n\t\t\techo \"错误:备份包解压失败\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal pkg_dir=\"$unpack_root/package\"\n\t\tif [[ -f \"$unpack_root/backup.meta\" ]]; then\n\t\t\tpkg_dir=\"$unpack_root\"\n\t\tfi\n\n\t\tlocal required\n\t\tfor required in backup.meta manifest.files manifest.sha256 payload; do\n\t\t\tif [[ ! -e \"$pkg_dir/$required\" ]]; then\n\t\t\t\techo \"错误:备份包缺少必要文件: $required\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tdone\n\n\t\tlocal real_type\n\t\treal_type=$(grep '^TYPE=' \"$pkg_dir/backup.meta\" | head -n1 | cut -d'=' -f2-)\n\t\tif [[ \"$real_type\" != \"$expected_type\" ]]; then\n\t\t\techo \"错误:备份类型不匹配,期望: $expected_type,实际: ${real_type:-未知}\"\n\t\t\treturn 1\n\t\tfi\n\n\t\t(\n\t\t\tcd \"$pkg_dir/payload\" || exit 1\n\t\t\tsha256sum -c ../manifest.sha256 >/dev/null\n\t\t) || { echo \"错误:sha256 校验失败,拒绝还原\"; return 1; }\n\n\t\techo \"$pkg_dir\"\n\t\treturn 0\n\t}\n\n\topenclaw_memory_backup_export() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tlocal ts\n\t\tts=$(date +%Y%m%d-%H%M%S)\n\t\tlocal out_file=\"$backup_root/openclaw-memory-full-${ts}.tar.gz\"\n\n\t\tmkdir -p \"$backup_root\"\n\t\tif [[ ! -d \"$workspace_dir\" ]]; then\n\t\t\techo \"错误:未找到 workspace 目录: $workspace_dir\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\t[[ -f \"$workspace_dir/MEMORY.md\" ]] && cp -a \"$workspace_dir/MEMORY.md\" \"$tmp_payload/\"\n\t\t[[ -d \"$workspace_dir/memory\" ]] && cp -a \"$workspace_dir/memory\" \"$tmp_payload/\"\n\n\t\tif gum confirm \"是否附带 AGENTS/USER/SOUL/TOOLS 文件?\"; then\n\t\t\tlocal f\n\t\t\tfor f in AGENTS.md USER.md SOUL.md TOOLS.md; do\n\t\t\t\t[[ -f \"$workspace_dir/$f\" ]] && cp -a \"$workspace_dir/$f\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的记忆文件\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"memory-full\" \"default\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"记忆全量备份完成: $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:记忆全量备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_read_import_path() {\n\t\tlocal file_input file_path backup_root\n\t\techo \"提示:输入文件名时默认在备份目录查找;输入含 / 的路径时按完整路径校验。\" >&2\n\t\techo \"scp 示例: scp /本地/备份包.tar.gz root@你的服务器:/tmp/\" >&2\n\n\t\tfile_input=$(gum input \\\n\t\t\t--placeholder \"备份文件名或完整路径\" \\\n\t\t\t--prompt \"Path > \")\n\t\t[[ -z \"$file_input\" ]] && { echo \"\"; return 0; }\n\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\n\t\tif [[ \"$file_input\" == */* ]]; then\n\t\t\tfile_path=\"$file_input\"\n\t\telse\n\t\t\tfile_path=\"$backup_root/$file_input\"\n\t\tfi\n\n\t\tif [[ ! -f \"$file_path\" ]]; then\n\t\t\techo \"错误:备份文件不存在: $file_path\" >&2\n\t\t\techo \"\"\n\t\t\treturn 1\n\t\tfi\n\n\t\techo \"$file_path\"\n\t}\n\n\topenclaw_memory_backup_import() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tmkdir -p \"$workspace_dir\"\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"memory-full\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed memory \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$workspace_dir/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$workspace_dir/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"记忆全量还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_export() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tif [[ ! -d \"$openclaw_root\" ]]; then\n\t\t\techo \"错误:未找到 OpenClaw 根目录: $openclaw_root\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal export_mode_label\n\t\texport_mode_label=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $'选择备份模式\\n↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"安全模式(推荐):workspace + openclaw.json + extensions/skills/prompts/tools\" \\\n\t\t\t\"完整模式(含更多状态,敏感风险更高)\") || return 1\n\n\t\tlocal mode_label=\"safe\"\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\tlocal d\n\t\tif [[ \"$export_mode_label\" == 完整* ]]; then\n\t\t\tmode_label=\"full\"\n\t\t\tfor d in workspace extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in telegram feishu whatsapp discord slack qqbot logs; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\telse\n\t\t\t[[ -d \"$openclaw_root/workspace\" ]] && cp -a \"$openclaw_root/workspace\" \"$tmp_payload/\"\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的 OpenClaw 项目内容\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tlocal out_file=\"$backup_root/openclaw-project-${mode_label}-$(date +%Y%m%d-%H%M%S).tar.gz\"\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"openclaw-project\" \"$mode_label\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"OpenClaw 项目备份完成 (${mode_label}): $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:OpenClaw 项目备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_import() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tmkdir -p \"$openclaw_root\"\n\n\t\techo \"警告:高风险操作,项目还原会覆盖 OpenClaw 配置与工作区内容。\"\n\t\tgum confirm \"确认继续还原?此操作不可逆。\" || { echo \"已取消\"; break_end; return 1; }\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"openclaw-project\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed project \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tgum spin --spinner pulse --title \"停止 OpenClaw gateway...\" -- openclaw gateway stop\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$openclaw_root/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$openclaw_root/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tstart_gateway\n\t\t\techo \"gateway 健康检查:\"\n\t\t\topenclaw gateway status || true\n\t\tfi\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"OpenClaw 项目还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_detect_type() {\n\t\tlocal file_name=\"$1\"\n\t\tif [[ \"$file_name\" == openclaw-memory-full-*.tar.gz ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\telif [[ \"$file_name\" == openclaw-project-*.tar.gz ]]; then\n\t\t\techo \"项目备份文件\"\n\t\telse\n\t\t\techo \"其他备份文件\"\n\t\tfi\n\t}\n\n\topenclaw_backup_collect_files() {\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tmapfile -t OPENCLAW_BACKUP_FILES < <(find \"$backup_root\" -maxdepth 1 -type f -name '*.tar.gz' | xargs -I{} basename {} | sort -r)\n\t}\n\n\topenclaw_backup_render_file_list() {\n\t\tlocal backup_root i file_name file_path file_type file_size file_time\n\t\tlocal has_memory=0 has_project=0 has_other=0\n\t\tbackup_root=$(openclaw_backup_root)\n\t\topenclaw_backup_collect_files\n\n\t\techo \"备份目录: $backup_root\"\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\techo \"暂无备份文件\"\n\t\t\treturn 0\n\t\tfi\n\n\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\tfile_type=$(openclaw_backup_detect_type \"${OPENCLAW_BACKUP_FILES[$i]}\")\n\t\t\tcase \"$file_type\" in\n\t\t\t\t\"记忆备份文件\") has_memory=1 ;;\n\t\t\t\t\"项目备份文件\") has_project=1 ;;\n\t\t\t\t\"其他备份文件\") has_other=1 ;;\n\t\t\tesac\n\t\tdone\n\n\t\tif [[ \"$has_memory\" -eq 1 ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"记忆备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_project\" -eq 1 ]]; then\n\t\t\techo \"项目备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"项目备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_other\" -eq 1 ]]; then\n\t\t\techo \"其他备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"其他备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_backup_file_exists_in_list() {\n\t\tlocal target_file=\"$1\"\n\t\tlocal item\n\t\tfor item in \"${OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t[[ \"$item\" == \"$target_file\" ]] && return 0\n\t\tdone\n\t\treturn 1\n\t}\n\n\topenclaw_backup_delete_file() {\n\t\tlocal backup_root backup_root_real user_input target_file target_path target_type\n\t\tbackup_root=$(openclaw_backup_root)\n\n\t\topenclaw_backup_render_file_list\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\tbreak_end\n\t\t\treturn 0\n\t\tfi\n\n\t\tuser_input=$(printf '%s\\n' \"${OPENCLAW_BACKUP_FILES[@]}\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 选择要删除的备份 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=15 \\\n\t\t\t--border=rounded \\\n\t\t\t--border-label=\" ⚡ DELETE BACKUP \" \\\n\t\t\t--color=border:196,label:196,header:196,prompt:208,pointer:196,marker:208,hl:208,hl+:208) || { echo \"已取消\"; break_end; return 0; }\n\n\t\t[[ -z \"$user_input\" ]] && { echo \"已取消\"; break_end; return 0; }\n\n\t\tbackup_root_real=$(realpath -m \"$backup_root\")\n\t\ttarget_file=$(basename -- \"$user_input\")\n\t\ttarget_path=\"$backup_root/$target_file\"\n\n\t\tif [[ ! -f \"$target_path\" ]]; then\n\t\t\techo \"错误:目标文件不存在: $target_path\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\ttarget_type=$(openclaw_backup_detect_type \"$target_file\")\n\n\t\techo \"即将删除: [$target_type] $target_path\"\n\t\tgum confirm \"确认删除?\" || { echo \"已取消删除\"; break_end; return 0; }\n\t\tgum confirm \"请再次确认,删除后不可恢复\" || { echo \"已取消删除\"; break_end; return 0; }\n\n\t\tif rm -f -- \"$target_path\"; then\n\t\t\techo \"删除成功: $target_file\"\n\t\telse\n\t\t\techo \"错误:删除失败: $target_file\"\n\t\tfi\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_list_files() {\n\t\topenclaw_backup_render_file_list\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_restore_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"备份与还原\"\n\t\t\topenclaw_backup_render_file_list\n\t\t\techo\n\n\t\t\tlocal backup_choice\n\t\t\tbackup_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"备份记忆全量\" \\\n\t\t\t\t\"还原记忆全量\" \\\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\" \\\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\" \\\n\t\t\t\t\"删除备份文件\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$backup_choice\" in\n\t\t\t\t\"备份记忆全量\") openclaw_memory_backup_export ;;\n\t\t\t\t\"还原记忆全量\") openclaw_memory_backup_import ;;\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\") openclaw_project_backup_export ;;\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\") openclaw_project_backup_import ;;\n\t\t\t\t\"删除备份文件\") openclaw_backup_delete_file ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\t_autostart_status() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\techo \"未安装\"\n\t\t\t\treturn\n\t\t\tfi\n\t\t\tif launchctl list 2>/dev/null | grep -q \"ai.openclaw.gateway\"; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\telse\n\t\t\tif systemctl --user is-enabled --quiet openclaw-gateway.service 2>/dev/null; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\tfi\n\t}\n\n\ttoggle_autostart() {\n\t\tlocal status; status=$(_autostart_status)\n\t\tclear\n\t\tui_header \"开机自启动管理\"\n\t\techo -e \" 当前状态:\\033[1m${status}\\033[0m\"\n\t\techo\n\n\t\tlocal action\n\t\tif [[ \"$status\" == \"已开启\" ]]; then\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"关闭自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\telse\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"开启自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\tfi\n\n\t\tcase \"$action\" in\n\t\t\t\"开启自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\t\t\tgum spin --spinner globe --title \"正在安装服务...\" -- \\\n\t\t\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 1\"\n\t\t\t\t\tfi\n\t\t\t\t\tlaunchctl load \"$plist\" 2>/dev/null && ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t\"关闭自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tlaunchctl unload \"$plist\" 2>/dev/null && ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user disable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t*) return 0 ;;\n\t\tesac\n\t\tbreak_end\n\t}\n\n\tupdate_moltbot() {\n\t\tinstall_node_and_tools\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_bin\n\t\tnpm_bin=\"$(npm prefix -g 2>/dev/null)/bin\"\n\t\tif [[ -n \"$npm_bin\" ]]; then\n\t\t\texport PATH=\"$npm_bin:$PATH\"\n\t\t\tfor rc in \"$HOME/.bashrc\" \"$HOME/.zshrc\" \"$HOME/.profile\"; do\n\t\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t\tgrep -qF \"$npm_bin\" \"$rc\" 2>/dev/null && continue\n\t\t\t\techo \"export PATH=\\\"${npm_bin}:\\$PATH\\\"\" >> \"$rc\"\n\t\t\tdone\n\t\tfi\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab -\n\t\tstart_gateway\n\t\tui_ok \"更新完成\"\n\t\tbreak_end\n\t}\n\n\tuninstall_moltbot() {\n\t\tgum confirm \"确认卸载 OpenClaw?此操作不可逆。\" || return 0\n\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tlaunchctl unload \"$plist\" 2>/dev/null || true\n\t\telse\n\t\t\tsystemctl --user disable --now openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tpkill -f \"openclaw.*gatewa\" 2>/dev/null || true\n\n\t\tgum spin --spinner meter --title \"正在卸载 OpenClaw...\" -- \\\n\t\t\tbash -c 'timeout 15 openclaw uninstall 2>/dev/null; true'\n\t\tnpm uninstall -g openclaw 2>/dev/null || true\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab - 2>/dev/null || true\n\t\trm -rf /root/.openclaw ~/.openclaw\n\n\t\trm -f \"$HOME/.local/bin/oc\" \"$HOME/.local/bin/openclawctl.sh\" /usr/local/bin/oc\n\t\tfor rc in \"$HOME/.zshrc\" \"$HOME/.bashrc\" \"$HOME/.profile\"; do\n\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t_sed_i '/\\.local\\/bin.*PATH/d' \"$rc\"\n\t\t\t_sed_i '/npm-global.*PATH\\|npm\\/bin.*PATH/d' \"$rc\"\n\t\tdone\n\n\t\techo\n\t\tgum style --foreground 46 --bold \" 卸载完成,脚本已退出。\"\n\t\texit 0\n\t}\n\n\tnano_openclaw_json() {\n\t\tinstall nano\n\t\tnano ~/.openclaw/openclaw.json\n\t\tstart_gateway\n\t}\n\n\topenclaw_find_webui_domain() {\n\t\tlocal conf domain_list\n\t\tdomain_list=$(\n\t\t\tgrep -R \"18789\" /home/web/conf.d/*.conf 2>/dev/null \\\n\t\t\t| awk -F: '{print $1}' \\\n\t\t\t| sort -u \\\n\t\t\t| while read -r conf; do\n\t\t\t\tbasename \"$conf\" .conf\n\t\t\tdone\n\t\t)\n\n\t\tif [[ -n \"$domain_list\" ]]; then\n\t\t\techo \"$domain_list\"\n\t\tfi\n\t}\n\n\topenclaw_show_webui_addr() {\n\t\tlocal local_ip token domains\n\n\t\tlocal_ip=\"127.0.0.1\"\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tgum style --border normal --border-foreground 99 --padding \"0 2\" \\\n\t\t\t\"OpenClaw WebUI 访问地址\" \\\n\t\t\t\"本机: http://${local_ip}:18789/#token=${token}\"\n\n\t\tdomains=$(openclaw_find_webui_domain)\n\t\tif [[ -n \"$domains\" ]]; then\n\t\t\techo \"$domains\" | while read -r d; do\n\t\t\t\techo \"域名: https://${d}/#token=${token}\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_domain_webui() {\n\t\tadd_yuming\n\t\tldnmp_Proxy \"${yuming}\" 127.0.0.1 18789\n\n\t\tlocal token\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tclear\n\t\techo \"访问地址: https://${yuming}/#token=$token\"\n\t\techo \"先访问URL触发设备ID,然后回车下一步进行配对。\"\n\t\tgum input --placeholder \"按回车继续...\" > /dev/null\n\n\t\techo -e \"${gl_kjlan}正在加载设备列表……${gl_bai}\"\n\n\t\tlocal config_file=\"$HOME/.openclaw/openclaw.json\"\n\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\tlocal new_origin=\"https://${yuming}\"\n\t\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\t\tlocal tmp_json\n\t\t\t\ttmp_json=$(mktemp)\n\t\t\t\tjq 'if .gateway.controlUi == null then .gateway.controlUi = {\"allowedOrigins\": [\"http://127.0.0.1\"]} else . end | if (.gateway.controlUi.allowedOrigins | contains([$origin]) | not) then .gateway.controlUi.allowedOrigins += [$origin] else . end' \\\n\t\t\t\t\t--arg origin \"$new_origin\" \"$config_file\" > \"$tmp_json\" && mv \"$tmp_json\" \"$config_file\"\n\t\t\t\techo -e \"${gl_kjlan}已将域名 ${yuming} 加入 allowedOrigins 配置${gl_bai}\"\n\t\t\t\topenclaw gateway restart >/dev/null 2>&1\n\t\t\tfi\n\t\tfi\n\n\t\topenclaw devices list\n\n\t\tlocal Request_Key\n\t\tRequest_Key=$(gum input \\\n\t\t\t--placeholder \"Request_Key\" \\\n\t\t\t--prompt \"> \")\n\n\t\tif [[ -z \"$Request_Key\" ]]; then\n\t\t\techo \"Request_Key 不能为空\"\n\t\t\treturn 1\n\t\tfi\n\n\t\topenclaw devices approve \"$Request_Key\"\n\t}\n\n\topenclaw_remove_domain() {\n\t\techo \"域名格式 example.com 不带https://\"\n\t\tweb_del\n\t}\n\n\topenclaw_webui_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"WebUI 访问与设置\"\n\t\t\topenclaw_show_webui_addr\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加域名访问\" \\\n\t\t\t\t\"删除域名访问\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"添加域名访问\")\n\t\t\t\t\topenclaw_domain_webui\n\t\t\t\t\techo\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"删除域名访问\")\n\t\t\t\t\topenclaw_remove_domain\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\twhile true; do\n\t\tclear\n\n\t\tlocal install_status running_status update_info\n\t\tinstall_status=$(get_install_status)\n\t\trunning_status=$(get_running_status)\n\t\tupdate_info=$(check_openclaw_update)\n\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border rounded --border-foreground 51 \\\n\t\t\t--padding \"1 8\" --align center \\\n\t\t\t\"O P E N C L A W\" \\\n\t\t\t\"\" \\\n\t\t\t\"[ AI Agent Gateway Manager ]\"\n\t\techo -e \" $install_status $running_status $update_info\"\n\t\tgum style --foreground 240 \" 输入 oc 可快速启动\"\n\t\techo\n\t\tgum style \\\n\t\t\t--bold \\\n\t\t\t--border rounded --border-foreground 201 \\\n\t\t\t--padding \"0 4\" --align center \\\n\t\t\t\"$(gum style --bold --foreground 201 '◈ by Joey ◈')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ YouTube @joeyblog')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ Telegram t.me/+ft-zI76oovgwNmRh')\" \\\n\t\t\t\"$(gum style --foreground 208 '⚡ 基于:kejilion · cliproxyapi-installer')\"\n\t\techo\n\n\t\tlocal choice\n\t\tlocal autostart_label\n\t\tautostart_label=\"开机自启动 [$(_autostart_status)]\"\n\n\t\tchoice=$(gum choose \\\n\t\t\t--height 24 \\\n\t\t\t--cursor \"❯ \" \\\n\t\t\t--header $' ─── CONTROL CENTER ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\"小白模式安装(推荐)\" \\\n\t\t\t\"安装\" \\\n\t\t\t\"启动\" \\\n\t\t\t\"停止\" \\\n\t\t\t\"状态日志查看\" \\\n\t\t\t\"$autostart_label\" \\\n\t\t\t\"换模型\" \\\n\t\t\t\"API管理\" \\\n\t\t\t\"CLIProxyAPI 管理\" \\\n\t\t\t\"机器人连接对接\" \\\n\t\t\t\"安装插件\" \\\n\t\t\t\"安装技能\" \\\n\t\t\t\"编辑主配置文件\" \\\n\t\t\t\"配置向导\" \\\n\t\t\t\"健康检测与修复\" \\\n\t\t\t\"WebUI访问与设置\" \\\n\t\t\t\"TUI命令行对话\" \\\n\t\t\t\"备份与还原\" \\\n\t\t\t\"更新\" \\\n\t\t\t\"卸载\" \\\n\t\t\t\"退出\") || break\n\n\t\tcase \"$choice\" in\n\t\t\t\"小白模式安装(推荐)\") beginner_mode_install ;;\n\t\t\t\"安装\") install_moltbot ;;\n\t\t\t\"启动\") start_bot ;;\n\t\t\t\"停止\") stop_bot ;;\n\t\t\t\"状态日志查看\") view_logs ;;\n\t\t\t\"开机自启动 [\"*\"]\") toggle_autostart ;;\n\t\t\t\"换模型\") change_model ;;\n\t\t\"API管理\") openclaw_api_manage_menu ;;\n\t\t\"CLIProxyAPI 管理\") cliproxyapi_manage_menu ;;\n\t\t\"机器人连接对接\") change_tg_bot_code ;;\n\t\t\t\"安装插件\") install_plugin ;;\n\t\t\t\"安装技能\") install_skill ;;\n\t\t\t\"编辑主配置文件\") nano_openclaw_json ;;\n\t\t\t\"配置向导\")\n\t\t\t\topenclaw onboard --install-daemon\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"健康检测与修复\")\n\t\t\t\tgum spin --spinner meter --title \"正在执行健康检测...\" -- openclaw doctor --fix\n\t\t\t\tif sync_openclaw_api_models; then\n\t\t\t\t\tstart_gateway\n\t\t\t\telse\n\t\t\t\t\tui_err \"API 模型同步失败,已中止重启网关,请检查 provider /models 返回后重试\"\n\t\t\t\tfi\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"WebUI访问与设置\") openclaw_webui_menu ;;\n\t\t\t\"TUI命令行对话\")\n\t\t\t\topenclaw tui\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"备份与还原\") openclaw_backup_restore_menu ;;\n\t\t\t\"更新\") update_moltbot ;;\n\t\t\t\"卸载\") uninstall_moltbot ;;\n\t\t\t\"退出\"|*) break ;;\n\t\tesac\n\tdone\n}\n\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n\tmoltbot_menu\nfi\n"}, "f2e28c358b148bd40f4eab72f1fe05b0592342bf": {"t": 1785772915.7292552, "n": 97830, "h": "36f0826fe2fc94a9", "c": "#!/usr/bin/env bash\n#\n# ╔══════════════════════════════════════════════════════════════════╗\n# ║ OpenClaw CTL / MoltBot 管理脚本 ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 作者 GitHub : by Joey ║\n# ║ YouTube : @joeyblog ║\n# ║ Telegram: https://t.me/+ft-zI76oovgwNmRh ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 致谢 / 引用 ║\n# ║ · 原始脚本基础来自 kejilion(@kejilion) ║\n# ║ · CLIProxyAPI 安装器来自 cliproxyapi-installer ║\n# ║ github.com/brokechubb/cliproxyapi-installer ║\n# ╚══════════════════════════════════════════════════════════════════╝\n#\n: \"${gl_hui:='\\e[37m'}\"\n: \"${gl_hong:='\\033[31m'}\"\n: \"${gl_lv:='\\033[32m'}\"\n: \"${gl_huang:='\\033[33m'}\"\n: \"${gl_lan:='\\033[34m'}\"\n: \"${gl_bai:='\\033[0m'}\"\n: \"${gl_zi:='\\033[35m'}\"\n: \"${gl_kjlan:='\\033[96m'}\"\n\nif ! declare -f break_end > /dev/null 2>&1; then\nbreak_end() {\n\tif command -v gum >/dev/null 2>&1; then\n\t\techo\n\t\tgum style --foreground 240 \" ─────────────────────────────────────── \"\n\t\tgum input --placeholder \"» 按回车继续 «\" --prompt \" \" > /dev/null\n\telse\n\t\techo \" ─── 按任意键继续 ───\"\n\t\tread -n 1 -s -r -p \"\"\n\t\techo\n\tfi\n\tclear\n}\nfi\n\nif ! declare -f install > /dev/null 2>&1; then\ninstall() {\n\tif [[ $# -eq 0 ]]; then\n\t\techo \"未提供软件包参数\"\n\t\treturn 1\n\tfi\n\tfor package in \"$@\"; do\n\t\tif ! command -v \"$package\" &>/dev/null; then\n\t\t\techo -e \"${gl_kjlan}正在安装 $package...${gl_bai}\"\n\t\t\tif [[ \"$(uname -s)\" == \"Darwin\" ]]; then\n\t\t\t\t_ensure_brew &>/dev/null\n\t\t\t\tcommand -v brew &>/dev/null && brew install \"$package\" &>/dev/null\n\t\t\telif command -v dnf &>/dev/null; then\n\t\t\t\tdnf install -y \"$package\" &>/dev/null\n\t\t\telif command -v yum &>/dev/null; then\n\t\t\t\tyum install -y \"$package\" &>/dev/null\n\t\t\telif command -v apt &>/dev/null; then\n\t\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"$package\" &>/dev/null\n\t\t\telif command -v apk &>/dev/null; then\n\t\t\t\tapk add \"$package\" &>/dev/null\n\t\t\telif command -v pacman &>/dev/null; then\n\t\t\t\tpacman -S --noconfirm \"$package\" &>/dev/null\n\t\t\telif command -v zypper &>/dev/null; then\n\t\t\t\tzypper install -y \"$package\" &>/dev/null\n\t\t\telif command -v opkg &>/dev/null; then\n\t\t\t\topkg install \"$package\" &>/dev/null\n\t\t\telif command -v pkg &>/dev/null; then\n\t\t\t\tpkg install -y \"$package\" &>/dev/null\n\t\t\telse\n\t\t\t\techo \"未知的包管理器,无法安装 $package\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\tdone\n}\nfi\n\nis_macos() { [[ \"$(uname -s)\" == \"Darwin\" ]]; }\n\n_ensure_brew() {\n\tif ! command -v brew &>/dev/null; then\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tif ! command -v brew &>/dev/null; then\n\t\techo \"正在安装 Homebrew...\"\n\t\t/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tcommand -v brew &>/dev/null || { echo \"Homebrew 安装失败,请手动安装:https://brew.sh\"; return 1; }\n}\n\n_sed_i() {\n\tif is_macos; then\n\t\tsed -i '' \"$@\"\n\telse\n\t\tsed -i \"$@\"\n\tfi\n}\n\ninstall_base_deps() {\n\tif is_macos; then\n\t\t_ensure_brew || return 1\n\t\tlocal missing_brew=()\n\t\tfor cmd in curl git nano jq python3 gpg; do\n\t\t\tcommand -v \"$cmd\" &>/dev/null || missing_brew+=(\"$cmd\")\n\t\tdone\n\t\tfor i in \"${!missing_brew[@]}\"; do\n\t\t\t[[ \"${missing_brew[$i]}\" == \"gpg\" ]] && missing_brew[$i]=\"gnupg\"\n\t\tdone\n\t\t[[ ${#missing_brew[@]} -eq 0 ]] && return 0\n\t\techo \"正在安装缺失依赖:${missing_brew[*]}...\"\n\t\tbrew install \"${missing_brew[@]}\" &>/dev/null\n\t\treturn 0\n\tfi\n\n\tlocal -a dep_map=(\n\t\t\"curl:curl:curl:curl:curl:curl\"\n\t\t\"git:git:git:git:git:git\"\n\t\t\"nano:nano:nano:nano:nano:nano\"\n\t\t\"jq:jq:jq:jq:jq:jq\"\n\t\t\"python3:python3:python3:python3:python:python3\"\n\t\t\"tar:tar:tar:tar:tar:tar\"\n\t\t\"gpg:gnupg:gnupg2:gnupg:gnupg:gpg2\"\n\t)\n\n\tlocal missing_apt=() missing_dnf=() missing_apk=() missing_pacman=() missing_zypper=()\n\n\tfor entry in \"${dep_map[@]}\"; do\n\t\tIFS=: read -r cmd pkg_apt pkg_dnf pkg_apk pkg_pacman pkg_zypper <<< \"$entry\"\n\t\tcommand -v \"$cmd\" &>/dev/null && continue\n\t\tmissing_apt+=(\"$pkg_apt\")\n\t\tmissing_dnf+=(\"$pkg_dnf\")\n\t\tmissing_apk+=(\"$pkg_apk\")\n\t\tmissing_pacman+=(\"$pkg_pacman\")\n\t\tmissing_zypper+=(\"$pkg_zypper\")\n\tdone\n\n\t[[ ${#missing_apt[@]} -eq 0 ]] && return 0\n\n\techo \"正在安装缺失依赖:${missing_apt[*]}...\"\n\n\tif command -v apt &>/dev/null; then\n\t\tapt update -y &>/dev/null\n\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"${missing_apt[@]}\" &>/dev/null\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y epel-release &>/dev/null || true\n\t\tdnf install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y epel-release &>/dev/null || true\n\t\tyum install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v apk &>/dev/null; then\n\t\tapk update &>/dev/null\n\t\tapk add \"${missing_apk[@]}\" &>/dev/null\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -Sy --noconfirm --needed \"${missing_pacman[@]}\" &>/dev/null\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y \"${missing_zypper[@]}\" &>/dev/null\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\tlocal py\n\t\tpy=$(compgen -c 2>/dev/null | grep -E '^python3\\.[0-9]+$' | sort -V | tail -1)\n\t\tif [[ -n \"$py\" ]]; then\n\t\t\tln -sf \"$(command -v \"$py\")\" /usr/local/bin/python3 2>/dev/null || true\n\t\tfi\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\techo \"正在安装 python3...\"\n\t\tif command -v apt &>/dev/null; then\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y python3 &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tdnf install -y python3 &>/dev/null\n\t\telif command -v yum &>/dev/null; then\n\t\t\tyum install -y python3 &>/dev/null\n\t\telif command -v apk &>/dev/null; then\n\t\t\tapk add python3 &>/dev/null\n\t\telif command -v pacman &>/dev/null; then\n\t\t\tpacman -S --noconfirm python &>/dev/null\n\t\telif command -v zypper &>/dev/null; then\n\t\t\tzypper install -y python3 &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\tfi\n}\n\n_install_gum_binary() {\n\tlocal arch\n\tcase \"$(uname -m)\" in\n\t\tx86_64) arch=\"amd64\" ;;\n\t\taarch64) arch=\"arm64\" ;;\n\t\tarmv7l) arch=\"armv7\" ;;\n\t\t*) echo \"不支持的架构: $(uname -m)\"; return 1 ;;\n\tesac\n\tlocal latest\n\tlatest=$(curl -fsSL https://api.github.com/repos/charmbracelet/gum/releases/latest \\\n\t\t| grep '\"tag_name\"' | sed 's/.*\"v\\([^\"]*\\)\".*/\\1/')\n\t[[ -z \"$latest\" ]] && { echo \"获取 gum 版本失败\"; return 1; }\n\tlocal url=\"https://github.com/charmbracelet/gum/releases/download/v${latest}/gum_${latest}_linux_${arch}.tar.gz\"\n\tlocal tmp\n\ttmp=$(mktemp -d)\n\tcurl -fsSL \"$url\" -o \"$tmp/gum.tar.gz\" \\\n\t\t&& tar -xzf \"$tmp/gum.tar.gz\" -C \"$tmp\" \\\n\t\t&& install -m 755 \"$tmp/gum\" /usr/local/bin/gum\n\trm -rf \"$tmp\"\n\tcommand -v gum >/dev/null 2>&1\n}\n\ninstall_gum() {\n\tcommand -v gum >/dev/null 2>&1 && return 0\n\techo \"正在安装 gum...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install gum\n\telif command -v apt >/dev/null 2>&1; then\n\t\tcommand -v gpg >/dev/null 2>&1 || apt install -y gnupg 2>/dev/null\n\t\tmkdir -p /etc/apt/keyrings\n\t\tif curl -fsSL https://repo.charm.sh/apt/gpg.key \\\n\t\t\t| gpg --dearmor -o /etc/apt/keyrings/charm.gpg 2>/dev/null; then\n\t\t\techo \"deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *\" \\\n\t\t\t\t| tee /etc/apt/sources.list.d/charm.list >/dev/null\n\t\t\tapt update -y 2>/dev/null && apt install -y gum 2>/dev/null\n\t\tfi\n\t\tcommand -v gum >/dev/null 2>&1 || _install_gum_binary\n\telif command -v dnf &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tdnf install -y gum\n\telif command -v yum &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tyum install -y gum\n\telif command -v apk &>/dev/null; then\n\t\tapk add gum 2>/dev/null || _install_gum_binary\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm gum 2>/dev/null || _install_gum_binary\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y gum 2>/dev/null || _install_gum_binary\n\telse\n\t\t_install_gum_binary\n\tfi\n\tcommand -v gum >/dev/null 2>&1 || { echo \"gum 安装失败,请手动安装: https://github.com/charmbracelet/gum\"; return 1; }\n}\n\ninstall_fzf() {\n\tcommand -v fzf >/dev/null 2>&1 && return 0\n\techo \"正在安装 fzf...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install fzf\n\telif command -v apt >/dev/null 2>&1; then\n\t\tapt install -y fzf\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y fzf\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y fzf\n\telif command -v apk &>/dev/null; then\n\t\tapk add fzf\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm fzf\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y fzf\n\telse\n\t\techo \"无法自动安装 fzf,请手动安装: https://github.com/junegunn/fzf\"\n\t\treturn 1\n\tfi\n}\n\n_install_shortcut() {\n\tlocal store_dir shortcut_dir shortcut\n\n\tstore_dir=\"$HOME/.local/bin\"\n\tmkdir -p \"$store_dir\"\n\n\tlocal _candidate_dirs=(\"/opt/homebrew/bin\" \"/usr/local/bin\")\n\tshortcut_dir=\"\"\n\tfor _d in \"${_candidate_dirs[@]}\"; do\n\t\tif [[ -d \"$_d\" && -w \"$_d\" ]]; then\n\t\t\tshortcut_dir=\"$_d\"\n\t\t\tbreak\n\t\tfi\n\tdone\n\n\tif [[ -z \"$shortcut_dir\" ]]; then\n\t\tshortcut_dir=\"$store_dir\"\n\t\tlocal rc_file=\"\"\n\t\t[[ -f \"$HOME/.zshrc\" ]] && rc_file=\"$HOME/.zshrc\"\n\t\t[[ -f \"$HOME/.bashrc\" ]] && rc_file=\"${rc_file:-$HOME/.bashrc}\"\n\t\tif [[ -n \"$rc_file\" ]] && ! grep -q \"$store_dir\" \"$rc_file\" 2>/dev/null; then\n\t\t\tprintf '\\nexport PATH=\"%s:$PATH\"\\n' \"$store_dir\" >> \"$rc_file\"\n\t\tfi\n\t\texport PATH=\"$store_dir:$PATH\"\n\tfi\n\n\tshortcut=\"$shortcut_dir/oc\"\n\n\tcat > \"$shortcut\" <<EOF\n#!/usr/bin/env bash\ncurl -fsSL https://raw.githubusercontent.com/byJoey/openclawctl/main/openclaw.sh \\\\\n -o \"$store_dir/openclawctl.sh\" 2>/dev/null && chmod +x \"$store_dir/openclawctl.sh\"\nexec bash \"$store_dir/openclawctl.sh\" \"\\$@\"\nEOF\n\tchmod +x \"$shortcut\"\n}\n\nmoltbot_menu() {\n\tis_macos && _ensure_brew\n\n\t_install_shortcut\n\n\tinstall_base_deps\n\tinstall_gum || { echo \"gum 安装失败,无法继续\"; return 1; }\n\tinstall_fzf || { echo \"fzf 安装失败,无法继续\"; return 1; }\n\n\tui_header() {\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"0 2\" \"$*\"\n\t\techo\n\t}\n\tui_ok() { gum style --foreground 46 \" ◉ $*\"; }\n\tui_err() { gum style --foreground 196 \" ✗ $*\"; }\n\tui_warn() { gum style --foreground 208 \" ⚡ $*\"; }\n\tui_info() { gum style --foreground 51 \" ◈ $*\"; }\n\tui_step() { echo; gum style --bold --foreground 201 \" ▶ $*\"; echo; }\n\n\tcheck_openclaw_update() {\n\t\tif ! command -v npm >/dev/null 2>&1; then\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal local_version remote_version\n\t\tlocal_version=$(npm list -g openclaw --depth=0 --no-update-notifier 2>/dev/null \\\n\t\t\t| grep openclaw | awk '{print $NF}' | sed 's/^.*@//')\n\n\t\t[[ -z \"$local_version\" ]] && return 1\n\n\t\tremote_version=$(npm view openclaw version --no-update-notifier 2>/dev/null)\n\n\t\t[[ -z \"$remote_version\" ]] && return 1\n\n\t\tif [[ \"$local_version\" != \"$remote_version\" ]]; then\n\t\t\techo -e \"\\033[38;5;208m⚡ UPDATE AVAILABLE $remote_version\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m✦ v$local_version\\033[0m\"\n\t\tfi\n\t}\n\n\tget_install_status() {\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m◉ INSTALLED\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m○ NOT FOUND\\033[0m\"\n\t\tfi\n\t}\n\n\tget_running_status() {\n\t\tif pgrep -f \"openclaw.*gatewa\" >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m■ STOPPED\\033[0m\"\n\t\tfi\n\t}\n\n\tstart_gateway() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tlaunchctl unload \"$plist\" >/dev/null 2>&1 || true\n\t\t\tif [[ -f \"$plist\" ]]; then\n\t\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- \\\n\t\t\t\t\tbash -c \"launchctl load '$plist' 2>/dev/null; sleep 2\"\n\t\t\telse\n\t\t\t\tgum spin --spinner pulse --title \"正在安装并启动网关...\" -- \\\n\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\telse\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- openclaw gateway start\n\t\t\tsleep 1\n\t\tfi\n\t}\n\n\tinstall_node_and_tools() {\n\t\tif command -v node &>/dev/null && command -v npm &>/dev/null; then\n\t\t\treturn 0\n\t\tfi\n\t\techo \"正在安装 Node.js...\"\n\t\tif is_macos; then\n\t\t\t_ensure_brew || return 1\n\t\t\tbrew install node &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tcurl -fsSL https://rpm.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tdnf install -y cmake libatomic nodejs &>/dev/null\n\t\telif command -v apt &>/dev/null; then\n\t\t\tcurl -fsSL https://deb.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y build-essential python3 libatomic1 nodejs &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t}\n\n\tconfigure_openclaw_session_policy() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 1\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport json, sys\npath = sys.argv[1]\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nsession = obj.setdefault('session', {})\nsession['dmScope'] = session.get('dmScope', 'per-channel-peer')\nsession['resetTriggers'] = ['/new', '/reset']\nsession['reset'] = {\n 'mode': 'idle',\n 'idleMinutes': 10080\n}\nsession['resetByType'] = {\n 'direct': {'mode': 'idle', 'idleMinutes': 10080},\n 'thread': {'mode': 'idle', 'idleMinutes': 1440},\n 'group': {'mode': 'idle', 'idleMinutes': 120}\n}\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(obj, f, ensure_ascii=False, indent=2)\n f.write('\\n')\nPY\n\t}\n\n\tsync_openclaw_api_models() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 0\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('未检测到 API providers,跳过模型同步')\n raise SystemExit(0)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nchanged = False\nfatal_errors = []\nsummary = []\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\ndef prompt_delete_provider(name):\n prompt = f\"{name} /models 探测连续失败 3 次。是否删除该 API 供应商及其全部相关模型?[y/N]: \"\n try:\n ans = input(prompt).strip().lower()\n except EOFError:\n return False\n return ans in ('y', 'yes')\n\n\ndef rebind_defaults_before_delete(name):\n global changed\n\n replacement = None\n\n def get_replacement():\n nonlocal replacement\n if replacement is None:\n candidates = collect_available_refs(exclude_provider=name)\n replacement = candidates[0] if candidates else None\n return replacement\n\n primary_ref = get_primary_ref(defaults)\n if ref_provider(primary_ref) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: 默认主模型指向该 provider,但无可用替代模型,已中止删除')\n return False\n set_primary_ref(defaults, repl)\n changed = True\n summary.append(f'已切换默认主模型: {primary_ref} -> {repl}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: {fk} 指向该 provider,但无可用替代模型,已中止删除')\n return False\n defaults[fk] = repl\n changed = True\n summary.append(f'已切换 {fk}: {val} -> {repl}')\n\n return True\n\n\ndef delete_provider_and_refs(name):\n global changed\n\n if not rebind_defaults_before_delete(name):\n return False\n\n removed_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\n for r in removed_refs:\n defaults_models.pop(r, None)\n if removed_refs:\n changed = True\n\n if name in providers:\n providers.pop(name, None)\n changed = True\n\n summary.append(f'已删除 provider {name},并移除 defaults.models 下 {len(removed_refs)} 个模型引用')\n return True\n\n\ndef fetch_remote_models_with_retry(name, base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n data = json.loads(payload)\n return data, None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\nfor name, provider in list(providers.items()):\n if not isinstance(provider, dict):\n summary.append(f'跳过 {name}: provider 结构非法')\n continue\n\n api = provider.get('api', '')\n base_url = provider.get('baseUrl')\n api_key = provider.get('apiKey')\n model_list = provider.get('models', [])\n\n if not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n summary.append(f'跳过 {name}: 无 baseUrl/apiKey/models')\n continue\n\n if api not in SUPPORTED_APIS:\n summary.append(f'跳过 {name}: 不支持直接 /models 校验 (api={api})')\n continue\n\n data, err, attempts = fetch_remote_models_with_retry(name, base_url, api_key, retries=3)\n if err is not None:\n summary.append(f'警告 - {name}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n if prompt_delete_provider(name):\n deleted = delete_provider_and_refs(name)\n if deleted:\n summary.append(f'{name}: 用户已确认删除该 provider 及全部相关模型引用')\n else:\n summary.append(f'{name}: 用户未确认删除,保留现有 provider 配置')\n continue\n\n if attempts > 1:\n summary.append(f'{name}: /models 第 {attempts} 次重试后成功')\n\n if not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n summary.append(f'警告 - 跳过 {name}: /models 返回结构不可识别')\n continue\n\n remote_ids = []\n for item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\n remote_set = set(remote_ids)\n\n if not remote_set:\n fatal_errors.append(f'错误 - {name} 上游 /models 为空,无法为该 provider 提供兜底模型')\n continue\n\n local_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\n local_ids = [str(m['id']) for m in local_models]\n local_set = set(local_ids)\n\n template = None\n for m in local_models:\n template = copy.deepcopy(m)\n break\n if template is None:\n summary.append(f'警告 - 跳过 {name}: 本地 models 无有效模板模型')\n continue\n\n removed_ids = [mid for mid in local_ids if mid not in remote_set]\n added_ids = [mid for mid in remote_ids if mid not in local_set]\n\n kept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\n new_models = kept_models[:]\n\n for mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{name} / {mid}'\n new_models.append(nm)\n\n if not new_models:\n fatal_errors.append(f'错误 - {name} 同步后无可用模型,无法保障默认模型/回退模型兜底')\n continue\n\n expected_refs = {model_ref(name, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\n local_refs = {model_ref(name, mid) for mid in local_ids}\n\n first_ref = model_ref(name, str(new_models[0]['id']))\n\n primary_ref = get_primary_ref(defaults)\n if isinstance(primary_ref, str) and primary_ref in (local_refs - expected_refs):\n set_primary_ref(defaults, first_ref)\n changed = True\n summary.append(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in (local_refs - expected_refs):\n defaults[fk] = first_ref\n changed = True\n summary.append(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\n stale_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/') and r not in expected_refs]\n for r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\n for r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\n if removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\n summary.append(f'{name}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\n\nif fatal_errors:\n for line in summary:\n print(line)\n for err in fatal_errors:\n print(err)\n print('模型同步失败:存在 provider 同步后无可用模型,已中止写入')\n raise SystemExit(2)\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n for line in summary:\n print(line)\n print('OpenClaw API 模型一致性同步完成')\nelse:\n for line in summary:\n print(line)\n print('无需同步:配置已与上游 /models 保持一致')\nPY\n\t}\n\n\t_install_openclaw_core() {\n\t\tinstall_node_and_tools\n\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf ssh://git@github.com/\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf git@github.com:\n\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_prefix npm_bin\n\t\tnpm_prefix=$(npm prefix -g 2>/dev/null)\n\t\tnpm_bin=\"${npm_prefix}/bin\"\n\t\tif [[ -n \"$npm_bin\" ]]; then\n\t\t\texport PATH=\"$npm_bin:$PATH\"\n\t\t\tfor rc in \"$HOME/.bashrc\" \"$HOME/.zshrc\" \"$HOME/.profile\"; do\n\t\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t\tgrep -qF \"$npm_bin\" \"$rc\" 2>/dev/null && continue\n\t\t\t\techo \"export PATH=\\\"${npm_bin}:\\$PATH\\\"\" >> \"$rc\"\n\t\t\tdone\n\t\tfi\n\t\topenclaw onboard --install-daemon\n\t\t_sed_i 's|\"profile\": \"messaging\"|\"profile\": \"full\"|g' ~/.openclaw/openclaw.json\n\t\tconfigure_openclaw_session_policy\n\t\tif ! is_macos; then\n\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tstart_gateway\n\t}\n\n\tinstall_moltbot() {\n\t\t_install_openclaw_core\n\t\tbreak_end\n\t}\n\n\t_cliproxy_dir() {\n\t\tif is_macos; then\n\t\t\techo \"$(brew --prefix 2>/dev/null || echo /usr/local)/etc\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi\"\n\t\tfi\n\t}\n\n\t_cliproxy_conf() {\n\t\tif is_macos; then\n\t\t\techo \"$(_cliproxy_dir)/cliproxyapi.conf\"\n\t\telse\n\t\t\techo \"$(_cliproxy_dir)/config.yaml\"\n\t\tfi\n\t}\n\n\t_cliproxy_bin() {\n\t\tif is_macos; then\n\t\t\tcommand -v cliproxyapi 2>/dev/null || echo \"cliproxyapi\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi/cli-proxy-api\"\n\t\tfi\n\t}\n\n\t_cliproxy_running() {\n\t\tif is_macos; then\n\t\t\tpgrep -f \"cliproxyapi\" >/dev/null 2>&1\n\t\telse\n\t\t\tpgrep -f \"cli-proxy-api\" >/dev/null 2>&1\n\t\tfi\n\t}\n\n\t_cliproxy_start_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services start cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tsystemctl --user enable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"systemctl --user start cliproxyapi.service 2>/dev/null; sleep 3\"\n\t\t\tif ! _cliproxy_running; then\n\t\t\t\tui_warn \"systemd 启动失败,改用后台直接运行...\"\n\t\t\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\t\t\t(cd \"$cliproxy_dir\" && nohup ./cli-proxy-api > /tmp/cliproxyapi.log 2>&1 &)\n\t\t\t\tsleep 3\n\t\t\tfi\n\t\tfi\n\t\tif _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已启动\"\n\t\t\treturn 0\n\t\telse\n\t\t\tui_err \"CLIProxyAPI 启动失败,请检查 $(_cliproxy_conf) 后手动启动\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\t_cliproxy_stop_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services stop cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbash -c \"systemctl --user stop cliproxyapi.service 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\tfi\n\t\tlocal _pat; _pat=$(is_macos && echo \"cliproxyapi\" || echo \"cli-proxy-api\")\n\t\tlocal pids; pids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\tif [[ -n \"$pids\" ]]; then\n\t\t\techo \"$pids\" | xargs kill 2>/dev/null || true\n\t\t\tsleep 2\n\t\t\tpids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\t\t[[ -n \"$pids\" ]] && echo \"$pids\" | xargs kill -9 2>/dev/null || true\n\t\tfi\n\t\tif ! _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已停止\"\n\t\telse\n\t\t\tui_err \"停止失败,仍有进程残留\"\n\t\tfi\n\t}\n\n\t_cliproxy_oauth_login() {\n\t\tlocal cliproxy_bin; cliproxy_bin=$(_cliproxy_bin)\n\t\tif ! command -v \"$cliproxy_bin\" &>/dev/null && [[ ! -x \"$cliproxy_bin\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 未安装:$cliproxy_bin\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_choice\n\t\tprovider_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' 选择要登录的 AI 提供商\\n ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"Claude (Anthropic)\" \\\n\t\t\t\"Gemini (Google)\" \\\n\t\t\t\"OpenAI / Codex\" \\\n\t\t\t\"Qwen (通义千问)\" \\\n\t\t\t\"iFlow\" \\\n\t\t\t\"取消\") || return 0\n\n\t\tlocal login_cmd=\"\" login_port=\"\"\n\t\tcase \"$provider_choice\" in\n\t\t\t\"Claude (Anthropic)\") login_cmd=\"--claude-login\"; login_port=\"54545\" ;;\n\t\t\t\"Gemini (Google)\") login_cmd=\"--login\"; login_port=\"8085\" ;;\n\t\t\t\"OpenAI / Codex\") login_cmd=\"--codex-login\"; login_port=\"1455\" ;;\n\t\t\t\"Qwen (通义千问)\") login_cmd=\"--qwen-login\"; login_port=\"\" ;;\n\t\t\t\"iFlow\") login_cmd=\"--iflow-login\"; login_port=\"11451\" ;;\n\t\t\t\"取消\"|*) return 0 ;;\n\t\tesac\n\n\t\techo\n\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\n\t\tlocal mode_input=\"\" proj_input=\"\"\n\t\tif [[ \"$login_cmd\" == \"--login\" ]]; then\n\t\t\tlocal mode_choice\n\t\t\tmode_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择 Gemini 登录模式\\n ↑↓ 移动 · Enter 确认(默认:Google One)' \\\n\t\t\t\t\"Google One(个人账号,自动发现项目)\" \\\n\t\t\t\t\"Code Assist(GCP 项目,手动选择)\") || return 0\n\t\t\t[[ \"$mode_choice\" == \"Code Assist\"* ]] && mode_input=\"1\" || mode_input=\"2\"\n\n\t\t\tif [[ \"$mode_input\" == \"1\" ]]; then\n\t\t\t\techo\n\t\t\t\tlocal proj_list proj_choice proj_num=\"1\"\n\t\t\t\tif command -v gcloud &>/dev/null; then\n\t\t\t\t\tproj_list=$(gcloud projects list --format=\"value(projectId,name)\" 2>/dev/null)\n\t\t\t\tfi\n\t\t\t\tif [[ -n \"$proj_list\" ]]; then\n\t\t\t\t\tproj_choice=$(printf \"%s\\n\" \"$proj_list\" | \\\n\t\t\t\t\t\tgum choose --cursor \"❯ \" \\\n\t\t\t\t\t\t--header $' 选择 GCP 项目\\n ↑↓ 移动 · Enter 确认') || return 0\n\t\t\t\t\tproj_num=$(printf \"%s\\n\" \"$proj_list\" | \\\n\t\t\t\t\t\tawk -v sel=\"$proj_choice\" 'NR==sel_line || $0==sel {print NR; exit}' \\\n\t\t\t\t\t\tsel=\"$proj_choice\" 2>/dev/null || echo \"1\")\n\t\t\t\t\t[[ -z \"$proj_num\" || \"$proj_num\" == \"0\" ]] && proj_num=\"1\"\n\t\t\t\telse\n\t\t\t\t\tlocal proj_raw\n\t\t\t\t\tproj_raw=$(gum input \\\n\t\t\t\t\t\t--placeholder \"项目编号或直接按 Enter 使用默认(1)\" \\\n\t\t\t\t\t\t--prompt \" ❯ \" --width 60) || return 0\n\t\t\t\t\tproj_num=\"${proj_raw:-1}\"\n\t\t\t\tfi\n\t\t\t\tproj_input=\"$proj_num\"\n\t\t\tfi\n\t\tfi\n\n\t\tif [[ -n \"$login_port\" ]]; then\n\t\t\tif is_macos; then\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 本地浏览器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 浏览器将自动打开,完成授权后自动返回,请稍候...\"\n\t\t\t\techo\n\t\t\t\tif [[ -n \"$mode_input\" ]]; then\n\t\t\t\t\tprintf \"%s\\n%s\\n\" \"$mode_input\" \"$proj_input\" | \"$cliproxy_bin\" \"$login_cmd\"\n\t\t\t\telse\n\t\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\t\tfi\n\t\t\telse\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 服务器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 1. 脚本后台启动 cli-proxy-api,并打印 OAuth 授权链接\" \\\n\t\t\t\t\t\" 2. 在本地浏览器打开该链接,完成账号授权\" \\\n\t\t\t\t\t\" 3. 授权后浏览器跳转到 localhost:${login_port},页面报错属正常\" \\\n\t\t\t\t\t\" 4. 复制地址栏完整 URL,粘贴到下方提示符,脚本自动完成握手\"\n\t\t\t\techo\n\t\t\t\tgum style --foreground 208 \\\n\t\t\t\t\t\" ⚡ 无需端口转发 — 回调 URL 里已含 code,直接转发给本机处理\"\n\t\t\t\techo\n\n\t\t\t\tif [[ -n \"$mode_input\" ]]; then\n\t\t\t\t\tlocal fifo; fifo=$(mktemp -u /tmp/cp_login_XXXXXX)\n\t\t\t\t\tmkfifo \"$fifo\"\n\t\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser < \"$fifo\") &\n\t\t\t\t\tlocal cli_pid=$!\n\t\t\t\t\texec 9>\"$fifo\"\n\t\t\t\t\tprintf \"%s\\n\" \"$mode_input\" >&9\n\t\t\t\t\tsleep 0.3\n\t\t\t\t\t[[ -n \"$proj_input\" ]] && { printf \"%s\\n\" \"$proj_input\" >&9; sleep 0.3; }\n\t\t\t\t\texec 9>&-\n\t\t\t\t\trm -f \"$fifo\"\n\t\t\t\telse\n\t\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser) &\n\t\t\t\t\tlocal cli_pid=$!\n\t\t\t\tfi\n\t\t\t\tsleep 2\n\n\t\t\t\techo\n\t\t\t\tlocal callback_url\n\t\t\t\tcallback_url=$(gum input \\\n\t\t\t\t\t--placeholder \"http://localhost:${login_port}/oauth2callback?code=...\" \\\n\t\t\t\t\t--prompt \" ❯ \" \\\n\t\t\t\t\t--width 120 \\\n\t\t\t\t\t--char-limit 2000) || true\n\n\t\t\t\tif [[ -n \"$callback_url\" ]]; then\n\t\t\t\t\tui_info \"正在向本机发送回调...\"\n\t\t\t\t\tcurl -sf \"$callback_url\" > /dev/null 2>&1 || true\n\t\t\t\t\tsleep 1\n\t\t\t\tfi\n\t\t\t\twait \"$cli_pid\" 2>/dev/null || true\n\t\t\tfi\n\t\telse\n\t\t\tui_info \"Qwen 使用 device flow,无需粘贴回调\"\n\t\t\techo\n\t\t\tgum style --foreground 240 \"将打印授权码 + URL,在任意浏览器中打开并输入授权码即可\"\n\t\t\techo\n\t\t\tif is_macos; then\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser)\n\t\t\tfi\n\t\tfi\n\t}\n\n\tcliproxyapi_manage_menu() {\n\t\tlocal config_file; config_file=$(_cliproxy_conf)\n\n\t\twhile true; do\n\t\t\tclear\n\n\t\t\tlocal cp_installed cp_running cp_version cp_port cp_keys\n\t\t\tif is_macos; then\n\t\t\t\tcp_version=$(brew info cliproxyapi 2>/dev/null | awk 'NR==1{print $4}')\n\t\t\telse\n\t\t\t\tcp_version=$(cat \"$(_cliproxy_dir)/version.txt\" 2>/dev/null || echo \"\")\n\t\t\tfi\n\t\t\tif [[ -n \"$cp_version\" ]]; then\n\t\t\t\tcp_installed=\"\\033[38;5;46m◉ INSTALLED v${cp_version}\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_installed=\"\\033[90m○ NOT INSTALLED\\033[0m\"\n\t\t\tfi\n\t\t\tif _cliproxy_running; then\n\t\t\t\tcp_running=\"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_running=\"\\033[90m■ STOPPED\\033[0m\"\n\t\t\tfi\n\t\t\tcp_port=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\t\t\tcp_port=\"${cp_port:-8317}\"\n\t\t\tcp_keys=$(awk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{n++} END{print n+0}' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 51 \\\n\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t--padding \"1 4\" --align center \\\n\t\t\t\t\"C L I P R O X Y A P I\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ AI OAuth Proxy Manager ]\"\n\t\t\techo -e \" $cp_installed $cp_running\"\n\t\t\techo -e \" \\033[90m端口 :${cp_port} API Keys: ${cp_keys}\\033[0m\"\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' ─── CLIProxyAPI ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"启动\" \\\n\t\t\t\t\"停止\" \\\n\t\t\t\t\"重启\" \\\n\t\t\t\t\"查看日志\" \\\n\t\t\t\t\"账号授权登录\" \\\n\t\t\t\t\"生成并添加 API Key\" \\\n\t\t\t\t\"查看 API Keys\" \\\n\t\t\t\t\"编辑配置文件\" \\\n\t\t\t\t\"更新\" \\\n\t\t\t\t\"卸载\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"启动\")\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"停止\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"重启\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tsleep 1\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看日志\")\n\t\t\t\t\techo\n\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\tlocal brew_log\n\t\t\t\t\t\tbrew_log=\"$(brew --prefix 2>/dev/null)/var/log/cliproxyapi.log\"\n\t\t\t\t\t\tif [[ -f \"$brew_log\" ]]; then\n\t\t\t\t\t\t\ttail -80 \"$brew_log\"\n\t\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\telif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\t\t\tjournalctl --user -u cliproxyapi.service --no-pager -n 80\n\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"账号授权登录\")\n\t\t\t\t\t_cliproxy_oauth_login\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"生成并添加 API Key\")\n\t\t\t\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\t\tlocal new_key\n\t\t\t\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\t\t\t\techo\n\t\t\t\t\tgum style --bold --foreground 46 \" ◉ 已生成新 API Key:\"\n\t\t\t\t\tgum style --bold --foreground 51 \" $new_key\"\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 将此 Key 自动写入 config.yaml?\"; then\n\t\t\t\t\t\tpython3 - \"$config_file\" \"$new_key\" <<'PY'\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n)', r'\\1 - \"' + key + '\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nprint(\"写入成功\")\nPY\n\t\t\t\t\t\tui_ok \"API Key 已写入配置\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看 API Keys\")\n\t\t\t\t\techo\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tgum style --foreground 240 \" config.yaml 中的 API Keys:\"\n\t\t\t\t\t\techo\n\t\t\t\t\t\tawk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{print \" \" $0}' \\\n\t\t\t\t\t\t\t\"$config_file\"\n\t\t\t\t\t\techo\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到配置文件:$config_file\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"编辑配置文件\")\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tnano \"$config_file\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\tfi\n\t\t\t\t\t;;\n\t\t\t\t\"更新\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 确认更新 CLIProxyAPI 到最新版本?\"; then\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew upgrade cliproxyapi && ui_ok \"更新完成\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tcurl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer \\\n\t\t\t\t\t\t\t\t| bash -s -- upgrade\n\t\t\t\t\t\t\tui_ok \"更新完成\"\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"卸载\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" ⚡ 确认卸载 CLIProxyAPI?\"; then\n\t\t\t\t\t\t_cliproxy_stop_service 2>/dev/null || true\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew uninstall cliproxyapi 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$HOME/.cli-proxy-api\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tsystemctl --user disable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\t\t\t\t\trm -f \"$HOME/.config/systemd/user/cliproxyapi.service\" 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$(_cliproxy_dir)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\t\tui_ok \"CLIProxyAPI 已卸载\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tbeginner_mode_install() {\n\t\tif is_macos; then\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ API Key 配置 ]\"\n\t\telse\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ CLIProxyAPI ⟶ AI Auth ⟶ Auto Config ]\"\n\t\tfi\n\t\techo\n\n\t\tui_step \"第 1 步:安装 OpenClaw\"\n\t\t_install_openclaw_core\n\n\t\tui_step \"第 2 步:安装 CLIProxyAPI\"\n\t\tif is_macos; then\n\t\t\tif ! gum spin --spinner globe --title \"正在安装 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbrew install cliproxyapi; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败(brew install cliproxyapi)\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\telse\n\t\t\tif ! curl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer | bash; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\n\t\tlocal cliproxy_config; cliproxy_config=$(_cliproxy_conf)\n\t\tif [[ ! -f \"$cliproxy_config\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 安装后未找到配置文件: $cliproxy_config\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\t\tui_ok \"CLIProxyAPI 安装完成\"\n\n\t\tui_step \"第 3 步:登录 AI 账号\"\n\t\tui_info \"通过 OAuth 授权即可免费使用,无需付费 API Key\"\n\t\techo\n\t\t_cliproxy_oauth_login\n\n\t\tui_step \"第 4 步:生成 API Key 并启动服务\"\n\t\tlocal new_key\n\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\tpython3 - \"$cliproxy_config\" \"$new_key\" <<'PY' 2>/dev/null\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n(?:\\s*-\\s*\"[^\"]*\"\\s*\\n)*)', lambda m: m.group(0) + f' - \"{key}\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nPY\n\t\tif ! _cliproxy_start_service; then\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_step \"第 5 步:配置 OpenClaw API\"\n\n\t\tlocal api_key port base_url\n\t\tapi_key=$(awk '\n\t\t\t/^api-keys:/ { in_keys=1; next }\n\t\t\tin_keys && /^[^ \\t]/ { exit }\n\t\t\tin_keys && /\"sk-/ { match($0, /sk-[^\"]+/); print substr($0, RSTART, RLENGTH); exit }\n\t\t' \"$cliproxy_config\" 2>/dev/null)\n\n\t\tport=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \"$cliproxy_config\" 2>/dev/null)\n\t\tport=\"${port:-8317}\"\n\t\tbase_url=\"http://localhost:${port}/v1\"\n\n\t\tif [[ -z \"$api_key\" ]]; then\n\t\t\tui_err \"无法从 $cliproxy_config 读取 API Key,请手动在「CLIProxyAPI 管理」中添加\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_info \"地址:$base_url\"\n\t\tui_info \"Key:${api_key:0:12}****\"\n\t\techo\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"cliproxy\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tstart_gateway\n\n\t\t\tui_step \"第 6 步:选择默认模型\"\n\t\t\tui_info \"请从可用模型中选择一个作为默认,选完后网关自动重启\"\n\t\t\techo\n\t\t\tchange_model\n\n\t\t\techo\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 46 \\\n\t\t\t\t--border double --border-foreground 46 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◉ INSTALL COMPLETE ◉\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw 已自动接入 CLIProxyAPI,可直接开始使用 ]\" \\\n\t\t\t\t\"[ 更多提供商:cd ~/cliproxyapi && ./cli-proxy-api --help ]\"\n\t\tfi\n\n\t\techo\n\t\tif gum confirm \" 现在去对接机器人?\"; then\n\t\t\tchange_tg_bot_code\n\t\telse\n\t\t\tbreak_end\n\t\tfi\n\t}\n\n\tstart_bot() {\n\t\tstart_gateway\n\t\tbreak_end\n\t}\n\n\tstop_bot() {\n\t\ttmux kill-session -t gateway > /dev/null 2>&1\n\t\tgum spin --spinner pulse --title \"正在停止网关...\" -- openclaw gateway stop\n\t\tbreak_end\n\t}\n\n\tview_logs() {\n\t\topenclaw status\n\t\topenclaw gateway status\n\t\topenclaw logs\n\t\tbreak_end\n\t}\n\n\tadd-all-models-from-provider() {\n\t\tlocal provider_name=\"$1\"\n\t\tlocal base_url=\"$2\"\n\t\tlocal api_key=\"$3\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tlocal models_tmpfile\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取 $provider_name 的模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\n\t\tlocal models_json\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tif [[ -z \"$models_json\" ]]; then\n\t\t\techo \"错误:无法获取模型列表\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_ids\n\t\tmodel_ids=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n for m in models:\n if isinstance(m, dict) and \"id\" in m:\n print(m[\"id\"])\nexcept Exception:\n pass\n')\n\n\t\tif [[ -z \"$model_ids\" ]]; then\n\t\t\techo \"错误:未找到任何模型\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_count\n\t\tmodel_count=$(echo \"$model_ids\" | wc -l | tr -d ' ')\n\t\techo \"发现 $model_count 个模型\"\n\n\t\tlocal models_array=\"[\"\n\t\tlocal first=true\n\n\t\twhile read -r model_id; do\n\t\t\t[[ $first == false ]] && models_array+=\",\"\n\t\t\tfirst=false\n\n\t\t\tlocal context_window=1048576\n\t\t\tlocal max_tokens=128000\n\t\t\tlocal input_cost=0.15\n\t\t\tlocal output_cost=0.60\n\n\t\t\tcase \"$model_id\" in\n\t\t\t\t*opus*|*pro*|*preview*|*thinking*|*sonnet*)\n\t\t\t\t\tinput_cost=2.00\n\t\t\t\t\toutput_cost=12.00\n\t\t\t\t\t;;\n\t\t\t\t*gpt-5*|*codex*)\n\t\t\t\t\tinput_cost=1.25\n\t\t\t\t\toutput_cost=10.00\n\t\t\t\t\t;;\n\t\t\t\t*flash*|*lite*|*haiku*|*mini*|*nano*)\n\t\t\t\t\tinput_cost=0.10\n\t\t\t\t\toutput_cost=0.40\n\t\t\t\t\t;;\n\t\t\tesac\n\n\t\t\tmodels_array+=$(cat <<EOF\n{\n\t\"id\": \"$model_id\",\n\t\"name\": \"$provider_name / $model_id\",\n\t\"input\": [\"text\", \"image\"],\n\t\"contextWindow\": $context_window,\n\t\"maxTokens\": $max_tokens,\n\t\"cost\": {\n\t\t\"input\": $input_cost,\n\t\t\"output\": $output_cost,\n\t\t\"cacheRead\": 0,\n\t\t\"cacheWrite\": 0\n\t}\n}\nEOF\n)\n\t\tdone <<< \"$model_ids\"\n\n\t\tmodels_array+=\"]\"\n\n\t\t[[ -f \"$config_file\" ]] && cp \"$config_file\" \"${config_file}.bak.$(date +%s)\"\n\n\t\tjq --arg prov \"$provider_name\" \\\n\t\t --arg url \"$base_url\" \\\n\t\t --arg key \"$api_key\" \\\n\t\t --argjson models \"$models_array\" \\\n\t\t'\n\t\t.models |= (\n\t\t\t(. // { mode: \"merge\", providers: {} })\n\t\t\t| .mode = \"merge\"\n\t\t\t| .providers[$prov] = {\n\t\t\t\tbaseUrl: $url,\n\t\t\t\tapiKey: $key,\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tmodels: $models\n\t\t\t}\n\t\t)\n\t\t| .agents |= (. // {})\n\t\t| .agents.defaults |= (. // {})\n\t\t| .agents.defaults.models |= (\n\t\t\t(if type == \"object\" then .\n\t\t\t elif type == \"array\" then reduce .[] as $m ({}; if ($m|type) == \"string\" then .[$m] = {} else . end)\n\t\t\t else {}\n\t\t\t end) as $existing\n\t\t\t| reduce ($models[]? | .id? // empty | tostring) as $mid (\n\t\t\t\t$existing;\n\t\t\t\tif ($mid | length) > 0 then\n\t\t\t\t\t.[\"\\($prov)/\\($mid)\"] //= {}\n\t\t\t\telse\n\t\t\t\t\t.\n\t\t\t\tend\n\t\t\t)\n\t\t)\n\t\t' \"$config_file\" > \"${config_file}.tmp\" && mv \"${config_file}.tmp\" \"$config_file\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\techo \"成功添加 $model_count 个模型到 $provider_name\"\n\t\t\techo \"模型引用格式: $provider_name/<model-id>\"\n\t\t\treturn 0\n\t\telse\n\t\t\techo \"错误:配置注入失败\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\tadd-openclaw-provider-interactive() {\n\t\tlocal provider_name base_url api_key\n\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"Provider 名称 (如: deepseek)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\t[[ -z \"$provider_name\" ]] && return 1\n\n\t\tbase_url=$(gum input \\\n\t\t\t--placeholder \"Base URL (如: https://api.xxx.com/v1)\" \\\n\t\t\t--prompt \"Base URL > \")\n\t\t[[ -z \"$base_url\" ]] && return 1\n\t\tbase_url=\"${base_url%/}\"\n\n\t\tapi_key=$(gum input --password \\\n\t\t\t--placeholder \"API Key\" \\\n\t\t\t--prompt \"API Key > \")\n\t\t[[ -z \"$api_key\" ]] && return 1\n\n\t\tlocal models_tmpfile models_json available_models model_count\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tlocal default_model=\"\"\n\t\tif [[ -n \"$models_json\" ]]; then\n\t\t\tavailable_models=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n ids = sorted(m[\"id\"] for m in models if isinstance(m, dict) and \"id\" in m)\n print(\"\\n\".join(ids))\nexcept Exception:\n pass\n')\n\t\t\tmodel_count=$(echo \"$available_models\" | wc -l | tr -d ' ')\n\t\tfi\n\n\t\tif [[ -n \"$available_models\" ]]; then\n\t\t\tdefault_model=$(echo \"$available_models\" | fzf \\\n\t\t\t\t--prompt=\" ❯ \" \\\n\t\t\t\t--header=\" 发现 $model_count 个模型 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 选第一个\" \\\n\t\t\t\t--header-first \\\n\t\t\t\t--height=15 \\\n\t\t\t\t--layout=reverse \\\n\t\t\t\t--border=double \\\n\t\t\t\t--border-label=\" ◈ DEFAULT MODEL \" \\\n\t\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || default_model=$(echo \"$available_models\" | head -1)\n\t\t\t[[ -z \"$default_model\" ]] && default_model=$(echo \"$available_models\" | head -1)\n\t\t\tdefault_model=$(awk '{print $1}' <<< \"$default_model\")\n\t\tfi\n\n\t\techo\n\t\tgum style \\\n\t\t\t--border normal --border-foreground 99 \\\n\t\t\t--padding \"0 2\" \\\n\t\t\t\"Provider : $provider_name\" \\\n\t\t\t\"Base URL : $base_url\" \\\n\t\t\t\"API Key : ${api_key:0:8}****\" \\\n\t\t\t\"默认模型 : $default_model\" \\\n\t\t\t\"模型总数 : $model_count\"\n\t\techo\n\n\t\tgum confirm \"确认添加所有 $model_count 个模型?\" || { echo \"已取消\"; return 1; }\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"$provider_name\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tgum spin --spinner minidot --title \"设置默认模型并重启网关...\" -- \\\n\t\t\t\topenclaw models set \"$provider_name/$default_model\"\n\t\t\tstart_gateway\n\t\t\techo \"完成:所有 $model_count 个模型已加载\"\n\t\tfi\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_list() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tlocal _py_list; _py_list=$(mktemp /tmp/oc_list_XXXXXX.py)\n\t\tcat > \"$_py_list\" << 'PYEOF'\nimport json, sys, time, urllib.request\n\npath = sys.argv[1]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\ndef ping_models(base_url, api_key):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': 'Bearer ' + api_key,\n 'User-Agent': 'OpenClaw-API-Manage/1.0',\n },\n )\n start = time.perf_counter()\n with urllib.request.urlopen(req, timeout=4) as resp:\n resp.read(2048)\n return int((time.perf_counter() - start) * 1000)\n\ndef classify_latency(latency):\n if latency == '不可用':\n return '不可用', 'unavailable'\n if latency == '未检测':\n return '未检测', 'unchecked'\n if isinstance(latency, int):\n level = 'low' if latency <= 800 else ('medium' if latency <= 2000 else 'high')\n return str(latency) + 'ms', level\n return str(latency), 'unchecked'\n\ntry:\n with open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\nexcept FileNotFoundError:\n print('MSG\\t未找到 openclaw.json,请先完成安装/初始化。')\n raise SystemExit(0)\nexcept Exception as e:\n print('MSG\\t读取配置失败: ' + type(e).__name__ + ': ' + str(e))\n raise SystemExit(0)\n\nproviders = ((obj.get('models') or {}).get('providers') or {})\nif not isinstance(providers, dict) or not providers:\n print('MSG\\t当前未配置任何 API provider。')\n raise SystemExit(0)\n\nprint('MSG\\t--- 已配置 API 列表 ---')\n\nfor idx, name in enumerate(sorted(providers.keys()), start=1):\n provider = providers.get(name)\n if not isinstance(provider, dict):\n base_url = '-'; model_count = 0; latency_raw = '不可用'\n else:\n base_url = provider.get('baseUrl') or provider.get('url') or provider.get('endpoint') or '-'\n models = provider.get('models') if isinstance(provider.get('models'), list) else []\n model_count = sum(1 for m in models if isinstance(m, dict) and m.get('id'))\n api = provider.get('api', '')\n api_key = provider.get('apiKey')\n latency_raw = '未检测'\n if api in SUPPORTED_APIS and isinstance(base_url, str) and base_url != '-' and isinstance(api_key, str) and api_key:\n try:\n latency_raw = ping_models(base_url, api_key)\n except Exception:\n latency_raw = '不可用'\n elif api in SUPPORTED_APIS:\n latency_raw = '不可用'\n\n latency_text, latency_level = classify_latency(latency_raw)\n print('ROW\\t' + '\\t'.join([str(idx), str(name), str(base_url), str(model_count), str(latency_text), str(latency_level)]))\nPYEOF\n\n\t\twhile IFS=$'\\t' read -r rec_type idx name base_url model_count latency_txt latency_level; do\n\t\t\tcase \"$rec_type\" in\n\t\t\t\tMSG)\n\t\t\t\t\techo \"$idx\"\n\t\t\t\t\t;;\n\t\t\t\tROW)\n\t\t\t\t\tlocal latency_color=\"$gl_bai\"\n\t\t\t\t\tcase \"$latency_level\" in\n\t\t\t\t\t\tlow) latency_color=\"$gl_lv\" ;;\n\t\t\t\t\t\tmedium) latency_color=\"$gl_huang\" ;;\n\t\t\t\t\t\thigh|unavailable) latency_color=\"$gl_hong\" ;;\n\t\t\t\t\t\tunchecked) latency_color=\"$gl_bai\" ;;\n\t\t\t\t\tesac\n\n\t\t\t\t\tprintf '%b\\n' \"[$idx] ${name} | API: ${base_url} | 模型数量: ${gl_huang}${model_count}${gl_bai} | 延迟/状态: ${latency_color}${latency_txt}${gl_bai}\"\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone < <(python3 \"$_py_list\" \"$config_file\")\n\t\trm -f \"$_py_list\"\n\t}\n\n\tsync-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要同步的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tinstall jq curl >/dev/null 2>&1\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY2'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\ntarget = sys.argv[2]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('错误:未检测到 API providers,无法同步')\n raise SystemExit(2)\n\nprovider = providers.get(target)\nif not isinstance(provider, dict):\n print(f'错误:未找到 provider: {target}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef fetch_remote_models_with_retry(base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n return json.loads(payload), None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\napi = provider.get('api', '')\nbase_url = provider.get('baseUrl')\napi_key = provider.get('apiKey')\nmodel_list = provider.get('models', [])\n\nif not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n print(f'错误:provider {target} 缺少 baseUrl/apiKey/models,无法执行同步')\n raise SystemExit(3)\n\nif api not in SUPPORTED_APIS:\n print(f'错误:provider {target} 当前 api={api},不支持直接 /models 同步')\n raise SystemExit(3)\n\ndata, err, attempts = fetch_remote_models_with_retry(base_url, api_key, retries=3)\nif err is not None:\n print(f'错误:{target}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n raise SystemExit(4)\n\nif not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n print(f'错误:{target}: /models 返回结构不可识别')\n raise SystemExit(4)\n\nremote_ids = []\nfor item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\nremote_set = set(remote_ids)\nif not remote_set:\n print(f'错误:{target}: 上游 /models 为空,已中止同步')\n raise SystemExit(5)\n\nlocal_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\nlocal_ids = [str(m['id']) for m in local_models]\nlocal_set = set(local_ids)\n\ntemplate = copy.deepcopy(local_models[0]) if local_models else None\nif template is None:\n print(f'错误:{target}: 本地 models 无有效模板模型,无法补全新增模型')\n raise SystemExit(3)\n\nremoved_ids = [mid for mid in local_ids if mid not in remote_set]\nadded_ids = [mid for mid in remote_ids if mid not in local_set]\n\nkept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\nnew_models = kept_models[:]\nfor mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{target} / {mid}'\n new_models.append(nm)\n\nif not new_models:\n print(f'错误:{target}: 同步后无可用模型,已中止写入')\n raise SystemExit(5)\n\nexpected_refs = {model_ref(target, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\nlocal_refs = {model_ref(target, mid) for mid in local_ids}\nremoved_refs = local_refs - expected_refs\nfirst_ref = model_ref(target, str(new_models[0]['id']))\n\nchanged = False\nprimary_ref = get_primary_ref(defaults)\nif isinstance(primary_ref, str) and primary_ref in removed_refs:\n set_primary_ref(defaults, first_ref)\n changed = True\n print(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in removed_refs:\n defaults[fk] = first_ref\n changed = True\n print(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\nstale_refs = [r for r in list(defaults_models.keys()) if r.startswith(target + '/') and r not in expected_refs]\nfor r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\nfor r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\nif removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'{target}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\nif changed:\n print('指定 provider 模型一致性同步完成')\nelse:\n print('无需同步:该 provider 配置已与上游 /models 保持一致')\nPY2\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"同步完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在或未配置\" ;;\n\t\t\t3) echo \"错误:provider 配置不完整或类型不支持\" ;;\n\t\t\t4) echo \"错误:上游 /models 请求失败\" ;;\n\t\t\t5) echo \"错误:上游模型为空或同步后无可用模型\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\tdelete-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要删除的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY'\nimport copy\nimport json\nimport sys\n\npath = sys.argv[1]\nname = sys.argv[2]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or name not in providers:\n print(f'错误:未找到 provider: {name}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\nreplacement_candidates = collect_available_refs(exclude_provider=name)\nreplacement = replacement_candidates[0] if replacement_candidates else None\n\nprimary_ref = get_primary_ref(defaults)\nif ref_provider(primary_ref) == name:\n if not replacement:\n print('错误:删除中止:默认主模型指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n set_primary_ref(defaults, replacement)\n print(f'默认主模型切换: {primary_ref} -> {replacement}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n if not replacement:\n print(f'错误:删除中止:{fk} 指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n defaults[fk] = replacement\n print(f'{fk} 切换: {val} -> {replacement}')\n\nremoved_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\nfor r in removed_refs:\n defaults_models.pop(r, None)\n\nproviders.pop(name, None)\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'已删除 provider: {name}')\nprint(f'已清理 defaults.models 中 {len(removed_refs)} 个关联模型引用')\nPY\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"删除完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在\" ;;\n\t\t\t3) echo \"错误:无可用替代模型,已保持原配置\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"API 管理\"\n\t\t\topenclaw_api_manage_list\n\t\t\techo\n\n\t\t\tlocal api_choice\n\t\t\tapi_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加 API\" \\\n\t\t\t\t\"同步 API 供应商模型列表\" \\\n\t\t\t\t\"删除 API\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$api_choice\" in\n\t\t\t\t\"添加 API\") add-openclaw-provider-interactive ;;\n\t\t\t\t\"同步 API 供应商模型列表\") sync-openclaw-provider-interactive ;;\n\t\t\t\t\"删除 API\") delete-openclaw-provider-interactive ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tchange_model() {\n\t\tlocal all_models model\n\t\tall_models=$(openclaw models list --all 2>/dev/null | grep \"configured\")\n\n\t\tif [[ -z \"$all_models\" ]]; then\n\t\t\tui_err \"无法获取可用模型列表(请先添加 API 提供商)\"\n\t\t\tbreak_end\n\t\t\treturn\n\t\tfi\n\n\t\tmodel=$(echo \"$all_models\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 当前: $(openclaw models list 2>/dev/null | awk '{print $1}' | head -1) │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=20 \\\n\t\t\t--layout=reverse \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ◈ MODEL SELECT \" \\\n\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || return\n\n\t\t[[ -z \"$model\" ]] && return\n\t\tmodel=$(awk '{print $1}' <<< \"$model\")\n\n\t\tgum spin --spinner minidot --title \"正在切换模型...\" -- openclaw models set \"$model\"\n\t\tstart_gateway\n\t\tui_ok \"已切换至:$model\"\n\t\tbreak_end\n\t}\n\n\tresolve_openclaw_plugin_id() {\n\t\tlocal raw_input=\"$1\"\n\t\tlocal plugin_id=\"$raw_input\"\n\n\t\tplugin_id=\"${plugin_id#@openclaw/}\"\n\t\tif [[ \"$plugin_id\" == @*/* ]]; then\n\t\t\tplugin_id=\"${plugin_id##*/}\"\n\t\tfi\n\t\tplugin_id=\"${plugin_id%%@*}\"\n\t\techo \"$plugin_id\"\n\t}\n\n\tsync_openclaw_plugin_allowlist() {\n\t\tlocal plugin_id=\"$1\"\n\t\t[[ -z \"$plugin_id\" ]] && return 1\n\n\t\tlocal home_config=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal root_config=\"/root/.openclaw/openclaw.json\"\n\t\tlocal config_file=\"$home_config\"\n\t\tif [[ ! -f \"$home_config\" && -f \"$root_config\" ]]; then\n\t\t\tconfig_file=\"$root_config\"\n\t\tfi\n\n\t\tmkdir -p \"$(dirname \"$config_file\")\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo '{}' > \"$config_file\"\n\t\tfi\n\n\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\tlocal tmp_json\n\t\t\ttmp_json=$(mktemp)\n\t\t\tif jq --arg pid \"$plugin_id\" '\n\t\t\t\t.plugins = (if (.plugins | type) == \"object\" then .plugins else {} end)\n\t\t\t\t| .plugins.allow = (if (.plugins.allow | type) == \"array\" then .plugins.allow else [] end)\n\t\t\t\t| if (.plugins.allow | index($pid)) == null then .plugins.allow += [$pid] else . end\n\t\t\t' \"$config_file\" > \"$tmp_json\" 2>/dev/null && mv \"$tmp_json\" \"$config_file\"; then\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\t\trm -f \"$tmp_json\"\n\t\tfi\n\n\t\tif command -v python3 >/dev/null 2>&1; then\n\t\t\tif python3 - \"$config_file\" \"$plugin_id\" <<'PYTHON_EOF'\nimport json\nimport sys\nfrom pathlib import Path\n\nconfig_file = Path(sys.argv[1])\nplugin_id = sys.argv[2]\n\ntry:\n data = json.loads(config_file.read_text(encoding='utf-8')) if config_file.exists() else {}\n if not isinstance(data, dict):\n data = {}\nexcept Exception:\n data = {}\n\nplugins = data.get('plugins')\nif not isinstance(plugins, dict):\n plugins = {}\n\na = plugins.get('allow')\nif not isinstance(a, list):\n a = []\n\nif plugin_id not in a:\n a.append(plugin_id)\n\nplugins['allow'] = a\ndata['plugins'] = plugins\nconfig_file.write_text(json.dumps(data, ensure_ascii=False, indent=2) + \"\\n\", encoding='utf-8')\nPYTHON_EOF\n\t\t\tthen\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\tfi\n\n\t\techo \"警告:已安装插件,但同步 plugins.allow 失败,请手动检查: $config_file\"\n\t\treturn 1\n\t}\n\n\tinstall_plugin() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"插件管理\"\n\t\t\tgum style --foreground 240 \"当前已安装插件:\"\n\t\t\topenclaw plugins list\n\t\t\techo\n\n\t\t\tlocal raw_input\n\t\t\traw_input=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择要安装的插件\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"feishu 飞书/Lark 集成\" \\\n\t\t\t\t\"telegram Telegram 机器人\" \\\n\t\t\t\t\"slack Slack 企业通讯\" \\\n\t\t\t\t\"msteams Microsoft Teams\" \\\n\t\t\t\t\"discord Discord 社区管理\" \\\n\t\t\t\t\"whatsapp WhatsApp 自动化\" \\\n\t\t\t\t\"memory-core 基础记忆 (文件检索)\" \\\n\t\t\t\t\"memory-lancedb 增强记忆 (向量数据库)\" \\\n\t\t\t\t\"copilot-proxy Copilot 接口转发\" \\\n\t\t\t\t\"lobster 审批流 (带人工确认)\" \\\n\t\t\t\t\"voice-call 语音通话能力\" \\\n\t\t\t\t\"nostr 加密隐私聊天\" \\\n\t\t\t\t\"手动输入插件 ID\") || break\n\n\t\t\tif [[ \"$raw_input\" == \"手动输入插件 ID\" ]]; then\n\t\t\t\traw_input=$(gum input \\\n\t\t\t\t\t--placeholder \"插件 ID (留空退出)\" \\\n\t\t\t\t\t--prompt \"Plugin > \") || break\n\t\t\t\t[[ -z \"$raw_input\" ]] && break\n\t\t\telse\n\t\t\t\traw_input=$(echo \"$raw_input\" | awk '{print $1}')\n\t\t\tfi\n\t\t\t[[ -z \"$raw_input\" ]] && break\n\n\t\t\tlocal plugin_id plugin_full\n\t\t\tplugin_id=$(resolve_openclaw_plugin_id \"$raw_input\")\n\t\t\tplugin_full=\"$raw_input\"\n\n\t\t\tlocal plugin_list\n\t\t\tplugin_list=$(openclaw plugins list 2>/dev/null)\n\n\t\t\tif echo \"$plugin_list\" | grep -qw \"$plugin_id\" && echo \"$plugin_list\" | grep \"$plugin_id\" | grep -q \"disabled\"; then\n\t\t\t\tui_info \"插件 [$plugin_id] 已预装,正在激活...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\" && ui_ok \"激活成功\" || ui_err \"激活失败\"\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/extensions/$plugin_id\" ]]; then\n\t\t\t\tui_info \"发现系统内置插件,尝试直接启用...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\telse\n\t\t\t\tui_info \"本地未发现,尝试下载安装...\"\n\t\t\t\trm -rf \"/root/.openclaw/extensions/$plugin_id\"\n\n\t\t\t\tif openclaw plugins install \"$plugin_full\"; then\n\t\t\t\t\tui_ok \"下载成功,正在启用...\"\n\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\telse\n\t\t\t\t\tui_warn \"官方渠道下载失败,尝试 npm 备选方案...\"\n\t\t\t\t\tif npm install -g \"$plugin_full\" --unsafe-perm; then\n\t\t\t\t\t\tui_ok \"npm 安装成功,尝试启用...\"\n\t\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"无法获取该插件,请检查 ID 是否正确或网络是否可用\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\tfi\n\t\t\tfi\n\n\t\t\tsync_openclaw_plugin_allowlist \"$plugin_id\"\n\t\t\tstart_gateway\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\tinstall_skill() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"技能管理\"\n\t\t\tgum style --foreground 240 \"当前已安装技能:\"\n\t\t\topenclaw skills list\n\t\t\techo\n\n\t\t\tlocal skill_name\n\t\t\tskill_name=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择要安装的技能\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"github 管理 GitHub Issues/PR/CI\" \\\n\t\t\t\t\"notion 操作 Notion 页面与数据库\" \\\n\t\t\t\t\"apple-notes macOS 原生笔记管理\" \\\n\t\t\t\t\"apple-reminders macOS 提醒事项管理\" \\\n\t\t\t\t\"1password 自动化读取 1Password 密钥\" \\\n\t\t\t\t\"gog Google Workspace 全能助手\" \\\n\t\t\t\t\"things-mac Things 3 任务管理\" \\\n\t\t\t\t\"bluebubbles 通过 BlueBubbles 收发 iMessage\" \\\n\t\t\t\t\"himalaya 终端邮件管理 (IMAP/SMTP)\" \\\n\t\t\t\t\"summarize 网页/播客/YouTube 内容总结\" \\\n\t\t\t\t\"openai-whisper 本地音频转文字\" \\\n\t\t\t\t\"coding-agent 运行 Claude Code/Codex 等编程助手\" \\\n\t\t\t\t\"手动输入技能名称\") || break\n\n\t\t\tif [[ \"$skill_name\" == \"手动输入技能名称\" ]]; then\n\t\t\t\tskill_name=$(gum input \\\n\t\t\t\t\t--placeholder \"技能名称 (留空退出)\" \\\n\t\t\t\t\t--prompt \"Skill > \") || break\n\t\t\t\t[[ -z \"$skill_name\" ]] && break\n\t\t\telse\n\t\t\t\tskill_name=$(echo \"$skill_name\" | awk '{print $1}')\n\t\t\tfi\n\t\t\t[[ -z \"$skill_name\" ]] && break\n\n\t\t\tlocal skill_found=false\n\t\t\tif [[ -d \"${HOME}/.openclaw/workspace/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在用户目录安装\"\n\t\t\t\tskill_found=true\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在系统目录安装\"\n\t\t\t\tskill_found=true\n\t\t\tfi\n\n\t\t\tif [[ \"$skill_found\" == true ]]; then\n\t\t\t\tgum confirm \"是否重新安装?\" || { break_end; continue; }\n\t\t\tfi\n\n\t\t\tgum spin --spinner globe --title \"正在安装技能 $skill_name...\" -- npx clawhub install \"$skill_name\"\n\t\t\tif [[ $? -eq 0 ]]; then\n\t\t\t\techo \"技能 $skill_name 安装成功\"\n\t\t\t\tstart_gateway\n\t\t\telse\n\t\t\t\techo \"错误:安装失败,请检查技能名称是否正确\"\n\t\t\tfi\n\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\topenclaw_json_get_bool() {\n\t\tlocal expr=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r \"$expr\" \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_channel_has_cfg() {\n\t\tlocal channel=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r --arg c \"$channel\" '\n\t\t\t(.channels[$c] // null) as $v\n\t\t\t| if ($v | type) != \"object\" then\n\t\t\t\tfalse\n\t\t\t else\n\t\t\t\t([ $v\n\t\t\t\t | to_entries[]\n\t\t\t\t | select((.key == \"enabled\" or .key == \"dmPolicy\" or .key == \"groupPolicy\" or .key == \"streaming\") | not)\n\t\t\t\t | .value\n\t\t\t\t | select(. != null and . != \"\" and . != false)\n\t\t\t\t ] | length) > 0\n\t\t\t end\n\t\t' \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_dir_has_files() {\n\t\tlocal dir=\"$1\"\n\t\t[[ -d \"$dir\" ]] && find \"$dir\" -type f -print -quit 2>/dev/null | grep -q .\n\t}\n\n\topenclaw_plugin_local_installed() {\n\t\tlocal plugin=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ -s \"$config_file\" ]] && jq -e --arg p \"$plugin\" '.plugins.installs[$p]' \"$config_file\" >/dev/null 2>&1; then\n\t\t\treturn 0\n\t\tfi\n\t\t[[ -d \"${HOME}/.openclaw/extensions/${plugin}\" ]] || [[ -d \"/usr/lib/node_modules/openclaw/extensions/${plugin}\" ]]\n\t}\n\n\topenclaw_bot_status_text() {\n\t\tlocal enabled=\"$1\"\n\t\tlocal configured=\"$2\"\n\t\tlocal connected=\"$3\"\n\t\tlocal abnormal=\"$4\"\n\t\tif [[ \"$abnormal\" == \"true\" ]]; then\n\t\t\techo \"异常\"\n\t\telif [[ \"$enabled\" != \"true\" ]]; then\n\t\t\techo \"未启用\"\n\t\telif [[ \"$connected\" == \"true\" ]]; then\n\t\t\techo \"已连接\"\n\t\telif [[ \"$configured\" == \"true\" ]]; then\n\t\t\techo \"已配置\"\n\t\telse\n\t\t\techo \"未配置\"\n\t\tfi\n\t}\n\n\topenclaw_colorize_bot_status() {\n\t\tlocal status=\"$1\"\n\t\tcase \"$status\" in\n\t\t\t已连接) echo -e \"${gl_lv}${status}${gl_bai}\" ;;\n\t\t\t已配置) echo -e \"${gl_huang}${status}${gl_bai}\" ;;\n\t\t\t异常) echo -e \"${gl_hong}${status}${gl_bai}\" ;;\n\t\t\t*) echo \"$status\" ;;\n\t\tesac\n\t}\n\n\topenclaw_print_bot_status_line() {\n\t\tlocal label=\"$1\"\n\t\tlocal status=\"$2\"\n\t\techo -e \"- ${label}: $(openclaw_colorize_bot_status \"$status\")\"\n\t}\n\n\topenclaw_show_bot_local_status_block() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal json_ok=\"false\"\n\t\tif [[ -s \"$config_file\" ]] && jq empty \"$config_file\" >/dev/null 2>&1; then\n\t\t\tjson_ok=\"true\"\n\t\tfi\n\n\t\tlocal tg_enabled tg_cfg tg_connected tg_abnormal tg_status\n\t\ttg_enabled=$(openclaw_json_get_bool '.channels.telegram.enabled // .plugins.entries.telegram.enabled // false')\n\t\ttg_cfg=$(openclaw_channel_has_cfg \"telegram\")\n\t\ttg_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/telegram\"; then\n\t\t\ttg_connected=\"true\"\n\t\tfi\n\t\ttg_abnormal=\"false\"\n\t\tif [[ \"$tg_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\ttg_abnormal=\"true\"\n\t\tfi\n\t\ttg_status=$(openclaw_bot_status_text \"$tg_enabled\" \"$tg_cfg\" \"$tg_connected\" \"$tg_abnormal\")\n\n\t\tlocal feishu_enabled feishu_cfg feishu_connected feishu_abnormal feishu_status\n\t\tfeishu_enabled=$(openclaw_json_get_bool '.plugins.entries.feishu.enabled // .channels.feishu.enabled // false')\n\t\tfeishu_cfg=$(openclaw_channel_has_cfg \"feishu\")\n\t\tfeishu_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/feishu\"; then\n\t\t\tfeishu_connected=\"true\"\n\t\tfi\n\t\tfeishu_abnormal=\"false\"\n\t\tif [[ \"$feishu_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"feishu\"; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$feishu_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tfeishu_status=$(openclaw_bot_status_text \"$feishu_enabled\" \"$feishu_cfg\" \"$feishu_connected\" \"$feishu_abnormal\")\n\n\t\tlocal wa_enabled wa_cfg wa_connected wa_abnormal wa_status\n\t\twa_enabled=$(openclaw_json_get_bool '.plugins.entries.whatsapp.enabled // .channels.whatsapp.enabled // false')\n\t\twa_cfg=$(openclaw_channel_has_cfg \"whatsapp\")\n\t\twa_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/whatsapp\"; then\n\t\t\twa_connected=\"true\"\n\t\tfi\n\t\twa_abnormal=\"false\"\n\t\tif [[ \"$wa_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"whatsapp\"; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$wa_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\twa_status=$(openclaw_bot_status_text \"$wa_enabled\" \"$wa_cfg\" \"$wa_connected\" \"$wa_abnormal\")\n\n\t\tlocal dc_enabled dc_cfg dc_connected dc_abnormal dc_status\n\t\tdc_enabled=$(openclaw_json_get_bool '.channels.discord.enabled // .plugins.entries.discord.enabled // false')\n\t\tdc_cfg=$(openclaw_channel_has_cfg \"discord\")\n\t\tdc_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/discord\"; then\n\t\t\tdc_connected=\"true\"\n\t\tfi\n\t\tdc_abnormal=\"false\"\n\t\tif [[ \"$dc_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tdc_abnormal=\"true\"\n\t\tfi\n\t\tdc_status=$(openclaw_bot_status_text \"$dc_enabled\" \"$dc_cfg\" \"$dc_connected\" \"$dc_abnormal\")\n\n\t\tlocal slack_enabled slack_cfg slack_connected slack_abnormal slack_status\n\t\tslack_enabled=$(openclaw_json_get_bool '.plugins.entries.slack.enabled // .channels.slack.enabled // false')\n\t\tslack_cfg=$(openclaw_channel_has_cfg \"slack\")\n\t\tslack_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/slack\"; then\n\t\t\tslack_connected=\"true\"\n\t\tfi\n\t\tslack_abnormal=\"false\"\n\t\tif [[ \"$slack_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"slack\"; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$slack_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tslack_status=$(openclaw_bot_status_text \"$slack_enabled\" \"$slack_cfg\" \"$slack_connected\" \"$slack_abnormal\")\n\n\t\tlocal qq_enabled qq_cfg qq_connected qq_abnormal qq_status\n\t\tqq_enabled=$(openclaw_json_get_bool '.plugins.entries.qqbot.enabled // .channels.qqbot.enabled // false')\n\t\tqq_cfg=$(openclaw_channel_has_cfg \"qqbot\")\n\t\tqq_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/sessions\" || openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/data\"; then\n\t\t\tqq_connected=\"true\"\n\t\tfi\n\t\tqq_abnormal=\"false\"\n\t\tif [[ \"$qq_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"qqbot\"; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$qq_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tqq_status=$(openclaw_bot_status_text \"$qq_enabled\" \"$qq_cfg\" \"$qq_connected\" \"$qq_abnormal\")\n\n\t\techo \"本地状态(仅本机配置/缓存,不做网络探测):\"\n\t\topenclaw_print_bot_status_line \"Telegram\" \"$tg_status\"\n\t\topenclaw_print_bot_status_line \"飞书(Lark)\" \"$feishu_status\"\n\t\topenclaw_print_bot_status_line \"WhatsApp\" \"$wa_status\"\n\t\topenclaw_print_bot_status_line \"Discord\" \"$dc_status\"\n\t\topenclaw_print_bot_status_line \"Slack\" \"$slack_status\"\n\t\topenclaw_print_bot_status_line \"QQ Bot\" \"$qq_status\"\n\t}\n\n\tchange_tg_bot_code() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"机器人连接对接\"\n\t\t\topenclaw_show_bot_local_status_block\n\t\t\techo\n\n\t\t\tlocal bot_choice\n\t\t\tbot_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择要对接的平台\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"Telegram 机器人对接\" \\\n\t\t\t\t\"飞书 (Lark) 机器人对接\" \\\n\t\t\t\t\"WhatsApp 机器人对接\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tlocal code\n\t\t\tcase \"$bot_choice\" in\n\t\t\t\t\"Telegram 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"TG机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve telegram \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"飞书 (Lark) 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"飞书机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve feishu \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"WhatsApp 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"WhatsApp连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve whatsapp \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"返回\"|*)\n\t\t\t\t\treturn 0\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\topenclaw_backup_root() {\n\t\techo \"${HOME}/.openclaw/backups\"\n\t}\n\n\topenclaw_is_interactive_terminal() {\n\t\t[[ -t 0 && -t 1 ]]\n\t}\n\n\topenclaw_has_command() {\n\t\tcommand -v \"$1\" >/dev/null 2>&1\n\t}\n\n\topenclaw_is_safe_relpath() {\n\t\tlocal rel=\"$1\"\n\t\t[[ -z \"$rel\" ]] && return 1\n\t\t[[ \"$rel\" == /* ]] && return 1\n\t\t[[ \"$rel\" == *//* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\n'* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\r'* ]] && return 1\n\t\tcase \"$rel\" in\n\t\t\t../*|*/../*|*/..|..)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t\treturn 0\n\t}\n\n\topenclaw_restore_path_allowed() {\n\t\tlocal mode=\"$1\"\n\t\tlocal rel=\"$2\"\n\t\tcase \"$mode\" in\n\t\t\tmemory)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\tMEMORY.md|AGENTS.md|USER.md|SOUL.md|TOOLS.md|memory/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\tproject)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\topenclaw.json|workspace/*|extensions/*|skills/*|prompts/*|tools/*|telegram/*|feishu/*|whatsapp/*|discord/*|slack/*|qqbot/*|logs/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\t*)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t}\n\n\topenclaw_pack_backup_archive() {\n\t\tlocal backup_type=\"$1\"\n\t\tlocal export_mode=\"$2\"\n\t\tlocal payload_dir=\"$3\"\n\t\tlocal output_file=\"$4\"\n\n\t\tlocal tmp_root\n\t\ttmp_root=$(mktemp -d) || return 1\n\t\tlocal pack_dir=\"$tmp_root/package\"\n\t\tmkdir -p \"$pack_dir\"\n\n\t\tcp -a \"$payload_dir\" \"$pack_dir/payload\"\n\n\t\t(\n\t\t\tcd \"$pack_dir/payload\" || exit 1\n\t\t\tfind . -type f | sed 's|^\\./||' | sort > \"$pack_dir/manifest.files\"\n\t\t\t: > \"$pack_dir/manifest.sha256\"\n\t\t\tlocal f\n\t\t\twhile IFS= read -r f; do\n\t\t\t\t[[ -z \"$f\" ]] && continue\n\t\t\t\tsha256sum \"$f\" >> \"$pack_dir/manifest.sha256\"\n\t\t\tdone < \"$pack_dir/manifest.files\"\n\t\t) || { rm -rf \"$tmp_root\"; return 1; }\n\n\t\tcat > \"$pack_dir/backup.meta\" <<EOF\nTYPE=$backup_type\nMODE=$export_mode\nCREATED_AT=$(date -u +'%Y-%m-%dT%H:%M:%SZ')\nHOST=$(hostname)\nEOF\n\n\t\tmkdir -p \"$(dirname \"$output_file\")\"\n\t\ttar -C \"$pack_dir\" -czf \"$output_file\" backup.meta manifest.files manifest.sha256 payload\n\t\tlocal rc=$?\n\t\trm -rf \"$tmp_root\"\n\t\treturn $rc\n\t}\n\n\topenclaw_offer_transfer_hint() {\n\t\tlocal file_path=\"$1\"\n\t\techo \"可使用以下方式下载备份文件:\"\n\t\techo \" 本地路径: $file_path\"\n\t\techo \" scp 示例: scp root@你的服务器:$file_path ./\"\n\t\techo \" 或使用 SFTP 客户端下载\"\n\t}\n\n\topenclaw_prepare_import_archive() {\n\t\tlocal expected_type=\"$1\"\n\t\tlocal archive_path=\"$2\"\n\t\tlocal unpack_root=\"$3\"\n\n\t\tif [[ ! -f \"$archive_path\" ]]; then\n\t\t\techo \"错误:文件不存在: $archive_path\"\n\t\t\treturn 1\n\t\tfi\n\t\tmkdir -p \"$unpack_root\"\n\t\tif ! tar -xzf \"$archive_path\" -C \"$unpack_root\"; then\n\t\t\techo \"错误:备份包解压失败\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal pkg_dir=\"$unpack_root/package\"\n\t\tif [[ -f \"$unpack_root/backup.meta\" ]]; then\n\t\t\tpkg_dir=\"$unpack_root\"\n\t\tfi\n\n\t\tlocal required\n\t\tfor required in backup.meta manifest.files manifest.sha256 payload; do\n\t\t\tif [[ ! -e \"$pkg_dir/$required\" ]]; then\n\t\t\t\techo \"错误:备份包缺少必要文件: $required\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tdone\n\n\t\tlocal real_type\n\t\treal_type=$(grep '^TYPE=' \"$pkg_dir/backup.meta\" | head -n1 | cut -d'=' -f2-)\n\t\tif [[ \"$real_type\" != \"$expected_type\" ]]; then\n\t\t\techo \"错误:备份类型不匹配,期望: $expected_type,实际: ${real_type:-未知}\"\n\t\t\treturn 1\n\t\tfi\n\n\t\t(\n\t\t\tcd \"$pkg_dir/payload\" || exit 1\n\t\t\tsha256sum -c ../manifest.sha256 >/dev/null\n\t\t) || { echo \"错误:sha256 校验失败,拒绝还原\"; return 1; }\n\n\t\techo \"$pkg_dir\"\n\t\treturn 0\n\t}\n\n\topenclaw_memory_backup_export() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tlocal ts\n\t\tts=$(date +%Y%m%d-%H%M%S)\n\t\tlocal out_file=\"$backup_root/openclaw-memory-full-${ts}.tar.gz\"\n\n\t\tmkdir -p \"$backup_root\"\n\t\tif [[ ! -d \"$workspace_dir\" ]]; then\n\t\t\techo \"错误:未找到 workspace 目录: $workspace_dir\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\t[[ -f \"$workspace_dir/MEMORY.md\" ]] && cp -a \"$workspace_dir/MEMORY.md\" \"$tmp_payload/\"\n\t\t[[ -d \"$workspace_dir/memory\" ]] && cp -a \"$workspace_dir/memory\" \"$tmp_payload/\"\n\n\t\tif gum confirm \"是否附带 AGENTS/USER/SOUL/TOOLS 文件?\"; then\n\t\t\tlocal f\n\t\t\tfor f in AGENTS.md USER.md SOUL.md TOOLS.md; do\n\t\t\t\t[[ -f \"$workspace_dir/$f\" ]] && cp -a \"$workspace_dir/$f\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的记忆文件\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"memory-full\" \"default\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"记忆全量备份完成: $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:记忆全量备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_read_import_path() {\n\t\tlocal file_input file_path backup_root\n\t\techo \"提示:输入文件名时默认在备份目录查找;输入含 / 的路径时按完整路径校验。\" >&2\n\t\techo \"scp 示例: scp /本地/备份包.tar.gz root@你的服务器:/tmp/\" >&2\n\n\t\tfile_input=$(gum input \\\n\t\t\t--placeholder \"备份文件名或完整路径\" \\\n\t\t\t--prompt \"Path > \")\n\t\t[[ -z \"$file_input\" ]] && { echo \"\"; return 0; }\n\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\n\t\tif [[ \"$file_input\" == */* ]]; then\n\t\t\tfile_path=\"$file_input\"\n\t\telse\n\t\t\tfile_path=\"$backup_root/$file_input\"\n\t\tfi\n\n\t\tif [[ ! -f \"$file_path\" ]]; then\n\t\t\techo \"错误:备份文件不存在: $file_path\" >&2\n\t\t\techo \"\"\n\t\t\treturn 1\n\t\tfi\n\n\t\techo \"$file_path\"\n\t}\n\n\topenclaw_memory_backup_import() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tmkdir -p \"$workspace_dir\"\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"memory-full\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed memory \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$workspace_dir/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$workspace_dir/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"记忆全量还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_export() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tif [[ ! -d \"$openclaw_root\" ]]; then\n\t\t\techo \"错误:未找到 OpenClaw 根目录: $openclaw_root\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal export_mode_label\n\t\texport_mode_label=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $'选择备份模式\\n↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"安全模式(推荐):workspace + openclaw.json + extensions/skills/prompts/tools\" \\\n\t\t\t\"完整模式(含更多状态,敏感风险更高)\") || return 1\n\n\t\tlocal mode_label=\"safe\"\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\tlocal d\n\t\tif [[ \"$export_mode_label\" == 完整* ]]; then\n\t\t\tmode_label=\"full\"\n\t\t\tfor d in workspace extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in telegram feishu whatsapp discord slack qqbot logs; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\telse\n\t\t\t[[ -d \"$openclaw_root/workspace\" ]] && cp -a \"$openclaw_root/workspace\" \"$tmp_payload/\"\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的 OpenClaw 项目内容\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tlocal out_file=\"$backup_root/openclaw-project-${mode_label}-$(date +%Y%m%d-%H%M%S).tar.gz\"\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"openclaw-project\" \"$mode_label\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"OpenClaw 项目备份完成 (${mode_label}): $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:OpenClaw 项目备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_import() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tmkdir -p \"$openclaw_root\"\n\n\t\techo \"警告:高风险操作,项目还原会覆盖 OpenClaw 配置与工作区内容。\"\n\t\tgum confirm \"确认继续还原?此操作不可逆。\" || { echo \"已取消\"; break_end; return 1; }\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"openclaw-project\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed project \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tgum spin --spinner pulse --title \"停止 OpenClaw gateway...\" -- openclaw gateway stop\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$openclaw_root/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$openclaw_root/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tstart_gateway\n\t\t\techo \"gateway 健康检查:\"\n\t\t\topenclaw gateway status || true\n\t\tfi\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"OpenClaw 项目还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_detect_type() {\n\t\tlocal file_name=\"$1\"\n\t\tif [[ \"$file_name\" == openclaw-memory-full-*.tar.gz ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\telif [[ \"$file_name\" == openclaw-project-*.tar.gz ]]; then\n\t\t\techo \"项目备份文件\"\n\t\telse\n\t\t\techo \"其他备份文件\"\n\t\tfi\n\t}\n\n\topenclaw_backup_collect_files() {\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tmapfile -t OPENCLAW_BACKUP_FILES < <(find \"$backup_root\" -maxdepth 1 -type f -name '*.tar.gz' | xargs -I{} basename {} | sort -r)\n\t}\n\n\topenclaw_backup_render_file_list() {\n\t\tlocal backup_root i file_name file_path file_type file_size file_time\n\t\tlocal has_memory=0 has_project=0 has_other=0\n\t\tbackup_root=$(openclaw_backup_root)\n\t\topenclaw_backup_collect_files\n\n\t\techo \"备份目录: $backup_root\"\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\techo \"暂无备份文件\"\n\t\t\treturn 0\n\t\tfi\n\n\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\tfile_type=$(openclaw_backup_detect_type \"${OPENCLAW_BACKUP_FILES[$i]}\")\n\t\t\tcase \"$file_type\" in\n\t\t\t\t\"记忆备份文件\") has_memory=1 ;;\n\t\t\t\t\"项目备份文件\") has_project=1 ;;\n\t\t\t\t\"其他备份文件\") has_other=1 ;;\n\t\t\tesac\n\t\tdone\n\n\t\tif [[ \"$has_memory\" -eq 1 ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"记忆备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_project\" -eq 1 ]]; then\n\t\t\techo \"项目备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"项目备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_other\" -eq 1 ]]; then\n\t\t\techo \"其他备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"其他备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_backup_file_exists_in_list() {\n\t\tlocal target_file=\"$1\"\n\t\tlocal item\n\t\tfor item in \"${OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t[[ \"$item\" == \"$target_file\" ]] && return 0\n\t\tdone\n\t\treturn 1\n\t}\n\n\topenclaw_backup_delete_file() {\n\t\tlocal backup_root backup_root_real user_input target_file target_path target_type\n\t\tbackup_root=$(openclaw_backup_root)\n\n\t\topenclaw_backup_render_file_list\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\tbreak_end\n\t\t\treturn 0\n\t\tfi\n\n\t\tuser_input=$(printf '%s\\n' \"${OPENCLAW_BACKUP_FILES[@]}\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 选择要删除的备份 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=15 \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ⚡ DELETE BACKUP \" \\\n\t\t\t--color=border:196,label:196,header:196,prompt:208,pointer:196,marker:208,hl:208,hl+:208) || { echo \"已取消\"; break_end; return 0; }\n\n\t\t[[ -z \"$user_input\" ]] && { echo \"已取消\"; break_end; return 0; }\n\n\t\tbackup_root_real=$(realpath -m \"$backup_root\")\n\t\ttarget_file=$(basename -- \"$user_input\")\n\t\ttarget_path=\"$backup_root/$target_file\"\n\n\t\tif [[ ! -f \"$target_path\" ]]; then\n\t\t\techo \"错误:目标文件不存在: $target_path\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\ttarget_type=$(openclaw_backup_detect_type \"$target_file\")\n\n\t\techo \"即将删除: [$target_type] $target_path\"\n\t\tgum confirm \"确认删除?\" || { echo \"已取消删除\"; break_end; return 0; }\n\t\tgum confirm \"请再次确认,删除后不可恢复\" || { echo \"已取消删除\"; break_end; return 0; }\n\n\t\tif rm -f -- \"$target_path\"; then\n\t\t\techo \"删除成功: $target_file\"\n\t\telse\n\t\t\techo \"错误:删除失败: $target_file\"\n\t\tfi\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_list_files() {\n\t\topenclaw_backup_render_file_list\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_restore_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"备份与还原\"\n\t\t\topenclaw_backup_render_file_list\n\t\t\techo\n\n\t\t\tlocal backup_choice\n\t\t\tbackup_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"备份记忆全量\" \\\n\t\t\t\t\"还原记忆全量\" \\\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\" \\\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\" \\\n\t\t\t\t\"删除备份文件\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$backup_choice\" in\n\t\t\t\t\"备份记忆全量\") openclaw_memory_backup_export ;;\n\t\t\t\t\"还原记忆全量\") openclaw_memory_backup_import ;;\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\") openclaw_project_backup_export ;;\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\") openclaw_project_backup_import ;;\n\t\t\t\t\"删除备份文件\") openclaw_backup_delete_file ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\t_autostart_status() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\techo \"未安装\"\n\t\t\t\treturn\n\t\t\tfi\n\t\t\tif launchctl list 2>/dev/null | grep -q \"ai.openclaw.gateway\"; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\telse\n\t\t\tif systemctl --user is-enabled --quiet openclaw-gateway.service 2>/dev/null; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\tfi\n\t}\n\n\ttoggle_autostart() {\n\t\tlocal status; status=$(_autostart_status)\n\t\tclear\n\t\tui_header \"开机自启动管理\"\n\t\techo -e \" 当前状态:\\033[1m${status}\\033[0m\"\n\t\techo\n\n\t\tlocal action\n\t\tif [[ \"$status\" == \"已开启\" ]]; then\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"关闭自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\telse\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"开启自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\tfi\n\n\t\tcase \"$action\" in\n\t\t\t\"开启自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\t\t\tgum spin --spinner globe --title \"正在安装服务...\" -- \\\n\t\t\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 1\"\n\t\t\t\t\tfi\n\t\t\t\t\tlaunchctl load \"$plist\" 2>/dev/null && ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t\"关闭自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tlaunchctl unload \"$plist\" 2>/dev/null && ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user disable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t*) return 0 ;;\n\t\tesac\n\t\tbreak_end\n\t}\n\n\tupdate_moltbot() {\n\t\tinstall_node_and_tools\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_bin\n\t\tnpm_bin=\"$(npm prefix -g 2>/dev/null)/bin\"\n\t\tif [[ -n \"$npm_bin\" ]]; then\n\t\t\texport PATH=\"$npm_bin:$PATH\"\n\t\t\tfor rc in \"$HOME/.bashrc\" \"$HOME/.zshrc\" \"$HOME/.profile\"; do\n\t\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t\tgrep -qF \"$npm_bin\" \"$rc\" 2>/dev/null && continue\n\t\t\t\techo \"export PATH=\\\"${npm_bin}:\\$PATH\\\"\" >> \"$rc\"\n\t\t\tdone\n\t\tfi\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab -\n\t\tstart_gateway\n\t\tui_ok \"更新完成\"\n\t\tbreak_end\n\t}\n\n\tuninstall_moltbot() {\n\t\tgum confirm \"确认卸载 OpenClaw?此操作不可逆。\" || return 0\n\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tlaunchctl unload \"$plist\" 2>/dev/null || true\n\t\telse\n\t\t\tsystemctl --user disable --now openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tpkill -f \"openclaw.*gatewa\" 2>/dev/null || true\n\n\t\tgum spin --spinner meter --title \"正在卸载 OpenClaw...\" -- \\\n\t\t\tbash -c 'timeout 15 openclaw uninstall 2>/dev/null; true'\n\t\tnpm uninstall -g openclaw 2>/dev/null || true\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab - 2>/dev/null || true\n\t\trm -rf /root/.openclaw ~/.openclaw\n\n\t\trm -f \"$HOME/.local/bin/oc\" \"$HOME/.local/bin/openclawctl.sh\" /usr/local/bin/oc\n\t\tfor rc in \"$HOME/.zshrc\" \"$HOME/.bashrc\" \"$HOME/.profile\"; do\n\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t_sed_i '/\\.local\\/bin.*PATH/d' \"$rc\"\n\t\t\t_sed_i '/npm-global.*PATH\\|npm\\/bin.*PATH/d' \"$rc\"\n\t\tdone\n\n\t\techo\n\t\tgum style --foreground 46 --bold \" 卸载完成,脚本已退出。\"\n\t\texit 0\n\t}\n\n\tnano_openclaw_json() {\n\t\tinstall nano\n\t\tnano ~/.openclaw/openclaw.json\n\t\tstart_gateway\n\t}\n\n\topenclaw_find_webui_domain() {\n\t\tlocal conf domain_list\n\t\tdomain_list=$(\n\t\t\tgrep -R \"18789\" /home/web/conf.d/*.conf 2>/dev/null \\\n\t\t\t| awk -F: '{print $1}' \\\n\t\t\t| sort -u \\\n\t\t\t| while read -r conf; do\n\t\t\t\tbasename \"$conf\" .conf\n\t\t\tdone\n\t\t)\n\n\t\tif [[ -n \"$domain_list\" ]]; then\n\t\t\techo \"$domain_list\"\n\t\tfi\n\t}\n\n\topenclaw_show_webui_addr() {\n\t\tlocal local_ip token domains\n\n\t\tlocal_ip=\"127.0.0.1\"\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tgum style --border normal --border-foreground 99 --padding \"0 2\" \\\n\t\t\t\"OpenClaw WebUI 访问地址\" \\\n\t\t\t\"本机: http://${local_ip}:18789/#token=${token}\"\n\n\t\tdomains=$(openclaw_find_webui_domain)\n\t\tif [[ -n \"$domains\" ]]; then\n\t\t\techo \"$domains\" | while read -r d; do\n\t\t\t\techo \"域名: https://${d}/#token=${token}\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_domain_webui() {\n\t\tadd_yuming\n\t\tldnmp_Proxy \"${yuming}\" 127.0.0.1 18789\n\n\t\tlocal token\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tclear\n\t\techo \"访问地址: https://${yuming}/#token=$token\"\n\t\techo \"先访问URL触发设备ID,然后回车下一步进行配对。\"\n\t\tgum input --placeholder \"按回车继续...\" > /dev/null\n\n\t\techo -e \"${gl_kjlan}正在加载设备列表……${gl_bai}\"\n\n\t\tlocal config_file=\"$HOME/.openclaw/openclaw.json\"\n\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\tlocal new_origin=\"https://${yuming}\"\n\t\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\t\tlocal tmp_json\n\t\t\t\ttmp_json=$(mktemp)\n\t\t\t\tjq 'if .gateway.controlUi == null then .gateway.controlUi = {\"allowedOrigins\": [\"http://127.0.0.1\"]} else . end | if (.gateway.controlUi.allowedOrigins | contains([$origin]) | not) then .gateway.controlUi.allowedOrigins += [$origin] else . end' \\\n\t\t\t\t\t--arg origin \"$new_origin\" \"$config_file\" > \"$tmp_json\" && mv \"$tmp_json\" \"$config_file\"\n\t\t\t\techo -e \"${gl_kjlan}已将域名 ${yuming} 加入 allowedOrigins 配置${gl_bai}\"\n\t\t\t\topenclaw gateway restart >/dev/null 2>&1\n\t\t\tfi\n\t\tfi\n\n\t\topenclaw devices list\n\n\t\tlocal Request_Key\n\t\tRequest_Key=$(gum input \\\n\t\t\t--placeholder \"Request_Key\" \\\n\t\t\t--prompt \"> \")\n\n\t\tif [[ -z \"$Request_Key\" ]]; then\n\t\t\techo \"Request_Key 不能为空\"\n\t\t\treturn 1\n\t\tfi\n\n\t\topenclaw devices approve \"$Request_Key\"\n\t}\n\n\topenclaw_remove_domain() {\n\t\techo \"域名格式 example.com 不带https://\"\n\t\tweb_del\n\t}\n\n\topenclaw_webui_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"WebUI 访问与设置\"\n\t\t\topenclaw_show_webui_addr\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加域名访问\" \\\n\t\t\t\t\"删除域名访问\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"添加域名访问\")\n\t\t\t\t\topenclaw_domain_webui\n\t\t\t\t\techo\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"删除域名访问\")\n\t\t\t\t\topenclaw_remove_domain\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\twhile true; do\n\t\tclear\n\n\t\tlocal install_status running_status update_info\n\t\tinstall_status=$(get_install_status)\n\t\trunning_status=$(get_running_status)\n\t\tupdate_info=$(check_openclaw_update)\n\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"1 8\" --align center \\\n\t\t\t\"O P E N C L A W\" \\\n\t\t\t\"\" \\\n\t\t\t\"[ AI Agent Gateway Manager ]\"\n\t\techo -e \" $install_status $running_status $update_info\"\n\t\tgum style --foreground 240 \" 输入 oc 可快速启动\"\n\t\techo\n\t\tgum style \\\n\t\t\t--bold \\\n\t\t\t--border double --border-foreground 201 \\\n\t\t\t--padding \"0 4\" --align center \\\n\t\t\t\"$(gum style --bold --foreground 201 '◈ by Joey ◈')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ YouTube @joeyblog')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ Telegram t.me/+ft-zI76oovgwNmRh')\" \\\n\t\t\t\"$(gum style --foreground 208 '⚡ 基于:kejilion · cliproxyapi-installer')\"\n\t\techo\n\n\t\tlocal choice\n\t\tlocal autostart_label\n\t\tautostart_label=\"开机自启动 [$(_autostart_status)]\"\n\n\t\tchoice=$(gum choose \\\n\t\t\t--height 24 \\\n\t\t\t--cursor \"❯ \" \\\n\t\t\t--header $' ─── CONTROL CENTER ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\"小白模式安装(推荐)\" \\\n\t\t\t\"安装\" \\\n\t\t\t\"启动\" \\\n\t\t\t\"停止\" \\\n\t\t\t\"状态日志查看\" \\\n\t\t\t\"$autostart_label\" \\\n\t\t\t\"换模型\" \\\n\t\t\t\"API管理\" \\\n\t\t\t\"CLIProxyAPI 管理\" \\\n\t\t\t\"机器人连接对接\" \\\n\t\t\t\"安装插件\" \\\n\t\t\t\"安装技能\" \\\n\t\t\t\"编辑主配置文件\" \\\n\t\t\t\"配置向导\" \\\n\t\t\t\"健康检测与修复\" \\\n\t\t\t\"WebUI访问与设置\" \\\n\t\t\t\"TUI命令行对话\" \\\n\t\t\t\"备份与还原\" \\\n\t\t\t\"更新\" \\\n\t\t\t\"卸载\" \\\n\t\t\t\"退出\") || break\n\n\t\tcase \"$choice\" in\n\t\t\t\"小白模式安装(推荐)\") beginner_mode_install ;;\n\t\t\t\"安装\") install_moltbot ;;\n\t\t\t\"启动\") start_bot ;;\n\t\t\t\"停止\") stop_bot ;;\n\t\t\t\"状态日志查看\") view_logs ;;\n\t\t\t\"开机自启动 [\"*\"]\") toggle_autostart ;;\n\t\t\t\"换模型\") change_model ;;\n\t\t\"API管理\") openclaw_api_manage_menu ;;\n\t\t\"CLIProxyAPI 管理\") cliproxyapi_manage_menu ;;\n\t\t\"机器人连接对接\") change_tg_bot_code ;;\n\t\t\t\"安装插件\") install_plugin ;;\n\t\t\t\"安装技能\") install_skill ;;\n\t\t\t\"编辑主配置文件\") nano_openclaw_json ;;\n\t\t\t\"配置向导\")\n\t\t\t\topenclaw onboard --install-daemon\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"健康检测与修复\")\n\t\t\t\tgum spin --spinner meter --title \"正在执行健康检测...\" -- openclaw doctor --fix\n\t\t\t\tif sync_openclaw_api_models; then\n\t\t\t\t\tstart_gateway\n\t\t\t\telse\n\t\t\t\t\tui_err \"API 模型同步失败,已中止重启网关,请检查 provider /models 返回后重试\"\n\t\t\t\tfi\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"WebUI访问与设置\") openclaw_webui_menu ;;\n\t\t\t\"TUI命令行对话\")\n\t\t\t\topenclaw tui\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"备份与还原\") openclaw_backup_restore_menu ;;\n\t\t\t\"更新\") update_moltbot ;;\n\t\t\t\"卸载\") uninstall_moltbot ;;\n\t\t\t\"退出\"|*) break ;;\n\t\tesac\n\tdone\n}\n\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n\tmoltbot_menu\nfi\n"}, "eb80a436e01b8d7844bdc18d7a78a8f5d77055e5": {"t": 1785772917.2789176, "n": 97815, "h": "84289f9937dcf937", "c": "#!/usr/bin/env bash\n#\n# ╔══════════════════════════════════════════════════════════════════╗\n# ║ OpenClaw CTL / MoltBot 管理脚本 ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 作者 GitHub : by Joey ║\n# ║ YouTube : @joeyblog ║\n# ║ Telegram: https://t.me/+ft-zI76oovgwNmRh ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 致谢 / 引用 ║\n# ║ · 原始脚本基础来自 kejilion(@kejilion) ║\n# ║ · CLIProxyAPI 安装器来自 cliproxyapi-installer ║\n# ║ github.com/brokechubb/cliproxyapi-installer ║\n# ╚══════════════════════════════════════════════════════════════════╝\n#\n: \"${gl_hui:='\\e[37m'}\"\n: \"${gl_hong:='\\033[31m'}\"\n: \"${gl_lv:='\\033[32m'}\"\n: \"${gl_huang:='\\033[33m'}\"\n: \"${gl_lan:='\\033[34m'}\"\n: \"${gl_bai:='\\033[0m'}\"\n: \"${gl_zi:='\\033[35m'}\"\n: \"${gl_kjlan:='\\033[96m'}\"\n\nif ! declare -f break_end > /dev/null 2>&1; then\nbreak_end() {\n\tif command -v gum >/dev/null 2>&1; then\n\t\techo\n\t\tgum style --foreground 240 \" ─────────────────────────────────────── \"\n\t\tgum input --placeholder \"» 按回车继续 «\" --prompt \" \" > /dev/null\n\telse\n\t\techo \" ─── 按任意键继续 ───\"\n\t\tread -n 1 -s -r -p \"\"\n\t\techo\n\tfi\n\tclear\n}\nfi\n\nif ! declare -f install > /dev/null 2>&1; then\ninstall() {\n\tif [[ $# -eq 0 ]]; then\n\t\techo \"未提供软件包参数\"\n\t\treturn 1\n\tfi\n\tfor package in \"$@\"; do\n\t\tif ! command -v \"$package\" &>/dev/null; then\n\t\t\techo -e \"${gl_kjlan}正在安装 $package...${gl_bai}\"\n\t\t\tif [[ \"$(uname -s)\" == \"Darwin\" ]]; then\n\t\t\t\t_ensure_brew &>/dev/null\n\t\t\t\tcommand -v brew &>/dev/null && brew install \"$package\" &>/dev/null\n\t\t\telif command -v dnf &>/dev/null; then\n\t\t\t\tdnf install -y \"$package\" &>/dev/null\n\t\t\telif command -v yum &>/dev/null; then\n\t\t\t\tyum install -y \"$package\" &>/dev/null\n\t\t\telif command -v apt &>/dev/null; then\n\t\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"$package\" &>/dev/null\n\t\t\telif command -v apk &>/dev/null; then\n\t\t\t\tapk add \"$package\" &>/dev/null\n\t\t\telif command -v pacman &>/dev/null; then\n\t\t\t\tpacman -S --noconfirm \"$package\" &>/dev/null\n\t\t\telif command -v zypper &>/dev/null; then\n\t\t\t\tzypper install -y \"$package\" &>/dev/null\n\t\t\telif command -v opkg &>/dev/null; then\n\t\t\t\topkg install \"$package\" &>/dev/null\n\t\t\telif command -v pkg &>/dev/null; then\n\t\t\t\tpkg install -y \"$package\" &>/dev/null\n\t\t\telse\n\t\t\t\techo \"未知的包管理器,无法安装 $package\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\tdone\n}\nfi\n\nis_macos() { [[ \"$(uname -s)\" == \"Darwin\" ]]; }\n\n_ensure_brew() {\n\tif ! command -v brew &>/dev/null; then\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tif ! command -v brew &>/dev/null; then\n\t\techo \"正在安装 Homebrew...\"\n\t\t/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tcommand -v brew &>/dev/null || { echo \"Homebrew 安装失败,请手动安装:https://brew.sh\"; return 1; }\n}\n\n_sed_i() {\n\tif is_macos; then\n\t\tsed -i '' \"$@\"\n\telse\n\t\tsed -i \"$@\"\n\tfi\n}\n\ninstall_base_deps() {\n\tif is_macos; then\n\t\t_ensure_brew || return 1\n\t\tlocal missing_brew=()\n\t\tfor cmd in curl git nano jq python3 gpg; do\n\t\t\tcommand -v \"$cmd\" &>/dev/null || missing_brew+=(\"$cmd\")\n\t\tdone\n\t\tfor i in \"${!missing_brew[@]}\"; do\n\t\t\t[[ \"${missing_brew[$i]}\" == \"gpg\" ]] && missing_brew[$i]=\"gnupg\"\n\t\tdone\n\t\t[[ ${#missing_brew[@]} -eq 0 ]] && return 0\n\t\techo \"正在安装缺失依赖:${missing_brew[*]}...\"\n\t\tbrew install \"${missing_brew[@]}\" &>/dev/null\n\t\treturn 0\n\tfi\n\n\tlocal -a dep_map=(\n\t\t\"curl:curl:curl:curl:curl:curl\"\n\t\t\"git:git:git:git:git:git\"\n\t\t\"nano:nano:nano:nano:nano:nano\"\n\t\t\"jq:jq:jq:jq:jq:jq\"\n\t\t\"python3:python3:python3:python3:python:python3\"\n\t\t\"tar:tar:tar:tar:tar:tar\"\n\t\t\"gpg:gnupg:gnupg2:gnupg:gnupg:gpg2\"\n\t)\n\n\tlocal missing_apt=() missing_dnf=() missing_apk=() missing_pacman=() missing_zypper=()\n\n\tfor entry in \"${dep_map[@]}\"; do\n\t\tIFS=: read -r cmd pkg_apt pkg_dnf pkg_apk pkg_pacman pkg_zypper <<< \"$entry\"\n\t\tcommand -v \"$cmd\" &>/dev/null && continue\n\t\tmissing_apt+=(\"$pkg_apt\")\n\t\tmissing_dnf+=(\"$pkg_dnf\")\n\t\tmissing_apk+=(\"$pkg_apk\")\n\t\tmissing_pacman+=(\"$pkg_pacman\")\n\t\tmissing_zypper+=(\"$pkg_zypper\")\n\tdone\n\n\t[[ ${#missing_apt[@]} -eq 0 ]] && return 0\n\n\techo \"正在安装缺失依赖:${missing_apt[*]}...\"\n\n\tif command -v apt &>/dev/null; then\n\t\tapt update -y &>/dev/null\n\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"${missing_apt[@]}\" &>/dev/null\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y epel-release &>/dev/null || true\n\t\tdnf install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y epel-release &>/dev/null || true\n\t\tyum install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v apk &>/dev/null; then\n\t\tapk update &>/dev/null\n\t\tapk add \"${missing_apk[@]}\" &>/dev/null\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -Sy --noconfirm --needed \"${missing_pacman[@]}\" &>/dev/null\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y \"${missing_zypper[@]}\" &>/dev/null\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\tlocal py\n\t\tpy=$(compgen -c 2>/dev/null | grep -E '^python3\\.[0-9]+$' | sort -V | tail -1)\n\t\tif [[ -n \"$py\" ]]; then\n\t\t\tln -sf \"$(command -v \"$py\")\" /usr/local/bin/python3 2>/dev/null || true\n\t\tfi\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\techo \"正在安装 python3...\"\n\t\tif command -v apt &>/dev/null; then\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y python3 &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tdnf install -y python3 &>/dev/null\n\t\telif command -v yum &>/dev/null; then\n\t\t\tyum install -y python3 &>/dev/null\n\t\telif command -v apk &>/dev/null; then\n\t\t\tapk add python3 &>/dev/null\n\t\telif command -v pacman &>/dev/null; then\n\t\t\tpacman -S --noconfirm python &>/dev/null\n\t\telif command -v zypper &>/dev/null; then\n\t\t\tzypper install -y python3 &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\tfi\n}\n\n_install_gum_binary() {\n\tlocal arch\n\tcase \"$(uname -m)\" in\n\t\tx86_64) arch=\"amd64\" ;;\n\t\taarch64) arch=\"arm64\" ;;\n\t\tarmv7l) arch=\"armv7\" ;;\n\t\t*) echo \"不支持的架构: $(uname -m)\"; return 1 ;;\n\tesac\n\tlocal latest\n\tlatest=$(curl -fsSL https://api.github.com/repos/charmbracelet/gum/releases/latest \\\n\t\t| grep '\"tag_name\"' | sed 's/.*\"v\\([^\"]*\\)\".*/\\1/')\n\t[[ -z \"$latest\" ]] && { echo \"获取 gum 版本失败\"; return 1; }\n\tlocal url=\"https://github.com/charmbracelet/gum/releases/download/v${latest}/gum_${latest}_linux_${arch}.tar.gz\"\n\tlocal tmp\n\ttmp=$(mktemp -d)\n\tcurl -fsSL \"$url\" -o \"$tmp/gum.tar.gz\" \\\n\t\t&& tar -xzf \"$tmp/gum.tar.gz\" -C \"$tmp\" \\\n\t\t&& install -m 755 \"$tmp/gum\" /usr/local/bin/gum\n\trm -rf \"$tmp\"\n\tcommand -v gum >/dev/null 2>&1\n}\n\ninstall_gum() {\n\tcommand -v gum >/dev/null 2>&1 && return 0\n\techo \"正在安装 gum...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install gum\n\telif command -v apt >/dev/null 2>&1; then\n\t\tcommand -v gpg >/dev/null 2>&1 || apt install -y gnupg 2>/dev/null\n\t\tmkdir -p /etc/apt/keyrings\n\t\tif curl -fsSL https://repo.charm.sh/apt/gpg.key \\\n\t\t\t| gpg --dearmor -o /etc/apt/keyrings/charm.gpg 2>/dev/null; then\n\t\t\techo \"deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *\" \\\n\t\t\t\t| tee /etc/apt/sources.list.d/charm.list >/dev/null\n\t\t\tapt update -y 2>/dev/null && apt install -y gum 2>/dev/null\n\t\tfi\n\t\tcommand -v gum >/dev/null 2>&1 || _install_gum_binary\n\telif command -v dnf &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tdnf install -y gum\n\telif command -v yum &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tyum install -y gum\n\telif command -v apk &>/dev/null; then\n\t\tapk add gum 2>/dev/null || _install_gum_binary\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm gum 2>/dev/null || _install_gum_binary\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y gum 2>/dev/null || _install_gum_binary\n\telse\n\t\t_install_gum_binary\n\tfi\n\tcommand -v gum >/dev/null 2>&1 || { echo \"gum 安装失败,请手动安装: https://github.com/charmbracelet/gum\"; return 1; }\n}\n\ninstall_fzf() {\n\tcommand -v fzf >/dev/null 2>&1 && return 0\n\techo \"正在安装 fzf...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install fzf\n\telif command -v apt >/dev/null 2>&1; then\n\t\tapt install -y fzf\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y fzf\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y fzf\n\telif command -v apk &>/dev/null; then\n\t\tapk add fzf\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm fzf\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y fzf\n\telse\n\t\techo \"无法自动安装 fzf,请手动安装: https://github.com/junegunn/fzf\"\n\t\treturn 1\n\tfi\n}\n\n_install_shortcut() {\n\tlocal store_dir shortcut_dir shortcut\n\n\tstore_dir=\"$HOME/.local/bin\"\n\tmkdir -p \"$store_dir\"\n\n\tlocal _candidate_dirs=(\"/opt/homebrew/bin\" \"/usr/local/bin\")\n\tshortcut_dir=\"\"\n\tfor _d in \"${_candidate_dirs[@]}\"; do\n\t\tif [[ -d \"$_d\" && -w \"$_d\" ]]; then\n\t\t\tshortcut_dir=\"$_d\"\n\t\t\tbreak\n\t\tfi\n\tdone\n\n\tif [[ -z \"$shortcut_dir\" ]]; then\n\t\tshortcut_dir=\"$store_dir\"\n\t\tlocal rc_file=\"\"\n\t\t[[ -f \"$HOME/.zshrc\" ]] && rc_file=\"$HOME/.zshrc\"\n\t\t[[ -f \"$HOME/.bashrc\" ]] && rc_file=\"${rc_file:-$HOME/.bashrc}\"\n\t\tif [[ -n \"$rc_file\" ]] && ! grep -q \"$store_dir\" \"$rc_file\" 2>/dev/null; then\n\t\t\tprintf '\\nexport PATH=\"%s:$PATH\"\\n' \"$store_dir\" >> \"$rc_file\"\n\t\tfi\n\t\texport PATH=\"$store_dir:$PATH\"\n\tfi\n\n\tshortcut=\"$shortcut_dir/oc\"\n\n\tcat > \"$shortcut\" <<EOF\n#!/usr/bin/env bash\ncurl -fsSL https://raw.githubusercontent.com/byJoey/openclawctl/main/openclaw.sh \\\\\n -o \"$store_dir/openclawctl.sh\" 2>/dev/null && chmod +x \"$store_dir/openclawctl.sh\"\nexec bash \"$store_dir/openclawctl.sh\" \"\\$@\"\nEOF\n\tchmod +x \"$shortcut\"\n}\n\nmoltbot_menu() {\n\tis_macos && _ensure_brew\n\n\t_install_shortcut\n\n\tinstall_base_deps\n\tinstall_gum || { echo \"gum 安装失败,无法继续\"; return 1; }\n\tinstall_fzf || { echo \"fzf 安装失败,无法继续\"; return 1; }\n\n\tui_header() {\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"0 2\" \"$*\"\n\t\techo\n\t}\n\tui_ok() { gum style --foreground 46 \" ◉ $*\"; }\n\tui_err() { gum style --foreground 196 \" ✗ $*\"; }\n\tui_warn() { gum style --foreground 208 \" ⚡ $*\"; }\n\tui_info() { gum style --foreground 51 \" ◈ $*\"; }\n\tui_step() { echo; gum style --bold --foreground 201 \" ▶ $*\"; echo; }\n\n\tcheck_openclaw_update() {\n\t\tif ! command -v npm >/dev/null 2>&1; then\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal local_version remote_version\n\t\tlocal_version=$(npm list -g openclaw --depth=0 --no-update-notifier 2>/dev/null \\\n\t\t\t| grep openclaw | awk '{print $NF}' | sed 's/^.*@//')\n\n\t\t[[ -z \"$local_version\" ]] && return 1\n\n\t\tremote_version=$(npm view openclaw version --no-update-notifier 2>/dev/null)\n\n\t\t[[ -z \"$remote_version\" ]] && return 1\n\n\t\tif [[ \"$local_version\" != \"$remote_version\" ]]; then\n\t\t\techo -e \"\\033[38;5;208m⚡ UPDATE AVAILABLE $remote_version\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m✦ v$local_version\\033[0m\"\n\t\tfi\n\t}\n\n\tget_install_status() {\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m◉ INSTALLED\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m○ NOT FOUND\\033[0m\"\n\t\tfi\n\t}\n\n\tget_running_status() {\n\t\tif pgrep -f \"openclaw.*gatewa\" >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m■ STOPPED\\033[0m\"\n\t\tfi\n\t}\n\n\tstart_gateway() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tlaunchctl unload \"$plist\" >/dev/null 2>&1 || true\n\t\t\tif [[ -f \"$plist\" ]]; then\n\t\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- \\\n\t\t\t\t\tbash -c \"launchctl load '$plist' 2>/dev/null; sleep 2\"\n\t\t\telse\n\t\t\t\tgum spin --spinner pulse --title \"正在安装并启动网关...\" -- \\\n\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\telse\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- openclaw gateway start\n\t\t\tsleep 1\n\t\tfi\n\t}\n\n\tinstall_node_and_tools() {\n\t\tif command -v node &>/dev/null && command -v npm &>/dev/null; then\n\t\t\treturn 0\n\t\tfi\n\t\techo \"正在安装 Node.js...\"\n\t\tif is_macos; then\n\t\t\t_ensure_brew || return 1\n\t\t\tbrew install node &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tcurl -fsSL https://rpm.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tdnf install -y cmake libatomic nodejs &>/dev/null\n\t\telif command -v apt &>/dev/null; then\n\t\t\tcurl -fsSL https://deb.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y build-essential python3 libatomic1 nodejs &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t}\n\n\tconfigure_openclaw_session_policy() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 1\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport json, sys\npath = sys.argv[1]\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nsession = obj.setdefault('session', {})\nsession['dmScope'] = session.get('dmScope', 'per-channel-peer')\nsession['resetTriggers'] = ['/new', '/reset']\nsession['reset'] = {\n 'mode': 'idle',\n 'idleMinutes': 10080\n}\nsession['resetByType'] = {\n 'direct': {'mode': 'idle', 'idleMinutes': 10080},\n 'thread': {'mode': 'idle', 'idleMinutes': 1440},\n 'group': {'mode': 'idle', 'idleMinutes': 120}\n}\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(obj, f, ensure_ascii=False, indent=2)\n f.write('\\n')\nPY\n\t}\n\n\tsync_openclaw_api_models() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 0\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('未检测到 API providers,跳过模型同步')\n raise SystemExit(0)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nchanged = False\nfatal_errors = []\nsummary = []\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\ndef prompt_delete_provider(name):\n prompt = f\"{name} /models 探测连续失败 3 次。是否删除该 API 供应商及其全部相关模型?[y/N]: \"\n try:\n ans = input(prompt).strip().lower()\n except EOFError:\n return False\n return ans in ('y', 'yes')\n\n\ndef rebind_defaults_before_delete(name):\n global changed\n\n replacement = None\n\n def get_replacement():\n nonlocal replacement\n if replacement is None:\n candidates = collect_available_refs(exclude_provider=name)\n replacement = candidates[0] if candidates else None\n return replacement\n\n primary_ref = get_primary_ref(defaults)\n if ref_provider(primary_ref) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: 默认主模型指向该 provider,但无可用替代模型,已中止删除')\n return False\n set_primary_ref(defaults, repl)\n changed = True\n summary.append(f'已切换默认主模型: {primary_ref} -> {repl}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: {fk} 指向该 provider,但无可用替代模型,已中止删除')\n return False\n defaults[fk] = repl\n changed = True\n summary.append(f'已切换 {fk}: {val} -> {repl}')\n\n return True\n\n\ndef delete_provider_and_refs(name):\n global changed\n\n if not rebind_defaults_before_delete(name):\n return False\n\n removed_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\n for r in removed_refs:\n defaults_models.pop(r, None)\n if removed_refs:\n changed = True\n\n if name in providers:\n providers.pop(name, None)\n changed = True\n\n summary.append(f'已删除 provider {name},并移除 defaults.models 下 {len(removed_refs)} 个模型引用')\n return True\n\n\ndef fetch_remote_models_with_retry(name, base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n data = json.loads(payload)\n return data, None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\nfor name, provider in list(providers.items()):\n if not isinstance(provider, dict):\n summary.append(f'跳过 {name}: provider 结构非法')\n continue\n\n api = provider.get('api', '')\n base_url = provider.get('baseUrl')\n api_key = provider.get('apiKey')\n model_list = provider.get('models', [])\n\n if not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n summary.append(f'跳过 {name}: 无 baseUrl/apiKey/models')\n continue\n\n if api not in SUPPORTED_APIS:\n summary.append(f'跳过 {name}: 不支持直接 /models 校验 (api={api})')\n continue\n\n data, err, attempts = fetch_remote_models_with_retry(name, base_url, api_key, retries=3)\n if err is not None:\n summary.append(f'警告 - {name}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n if prompt_delete_provider(name):\n deleted = delete_provider_and_refs(name)\n if deleted:\n summary.append(f'{name}: 用户已确认删除该 provider 及全部相关模型引用')\n else:\n summary.append(f'{name}: 用户未确认删除,保留现有 provider 配置')\n continue\n\n if attempts > 1:\n summary.append(f'{name}: /models 第 {attempts} 次重试后成功')\n\n if not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n summary.append(f'警告 - 跳过 {name}: /models 返回结构不可识别')\n continue\n\n remote_ids = []\n for item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\n remote_set = set(remote_ids)\n\n if not remote_set:\n fatal_errors.append(f'错误 - {name} 上游 /models 为空,无法为该 provider 提供兜底模型')\n continue\n\n local_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\n local_ids = [str(m['id']) for m in local_models]\n local_set = set(local_ids)\n\n template = None\n for m in local_models:\n template = copy.deepcopy(m)\n break\n if template is None:\n summary.append(f'警告 - 跳过 {name}: 本地 models 无有效模板模型')\n continue\n\n removed_ids = [mid for mid in local_ids if mid not in remote_set]\n added_ids = [mid for mid in remote_ids if mid not in local_set]\n\n kept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\n new_models = kept_models[:]\n\n for mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{name} / {mid}'\n new_models.append(nm)\n\n if not new_models:\n fatal_errors.append(f'错误 - {name} 同步后无可用模型,无法保障默认模型/回退模型兜底')\n continue\n\n expected_refs = {model_ref(name, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\n local_refs = {model_ref(name, mid) for mid in local_ids}\n\n first_ref = model_ref(name, str(new_models[0]['id']))\n\n primary_ref = get_primary_ref(defaults)\n if isinstance(primary_ref, str) and primary_ref in (local_refs - expected_refs):\n set_primary_ref(defaults, first_ref)\n changed = True\n summary.append(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in (local_refs - expected_refs):\n defaults[fk] = first_ref\n changed = True\n summary.append(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\n stale_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/') and r not in expected_refs]\n for r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\n for r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\n if removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\n summary.append(f'{name}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\n\nif fatal_errors:\n for line in summary:\n print(line)\n for err in fatal_errors:\n print(err)\n print('模型同步失败:存在 provider 同步后无可用模型,已中止写入')\n raise SystemExit(2)\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n for line in summary:\n print(line)\n print('OpenClaw API 模型一致性同步完成')\nelse:\n for line in summary:\n print(line)\n print('无需同步:配置已与上游 /models 保持一致')\nPY\n\t}\n\n\t_install_openclaw_core() {\n\t\tinstall_node_and_tools\n\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf ssh://git@github.com/\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf git@github.com:\n\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_prefix npm_bin\n\t\tnpm_prefix=$(npm prefix -g 2>/dev/null)\n\t\tnpm_bin=\"${npm_prefix}/bin\"\n\t\tif [[ -n \"$npm_bin\" ]]; then\n\t\t\texport PATH=\"$npm_bin:$PATH\"\n\t\t\tfor rc in \"$HOME/.bashrc\" \"$HOME/.zshrc\" \"$HOME/.profile\"; do\n\t\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t\tgrep -qF \"$npm_bin\" \"$rc\" 2>/dev/null && continue\n\t\t\t\techo \"export PATH=\\\"${npm_bin}:\\$PATH\\\"\" >> \"$rc\"\n\t\t\tdone\n\t\tfi\n\t\topenclaw onboard --install-daemon\n\t\t_sed_i 's|\"profile\": \"messaging\"|\"profile\": \"full\"|g' ~/.openclaw/openclaw.json\n\t\tconfigure_openclaw_session_policy\n\t\tif ! is_macos; then\n\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tstart_gateway\n\t}\n\n\tinstall_moltbot() {\n\t\t_install_openclaw_core\n\t\tbreak_end\n\t}\n\n\t_cliproxy_dir() {\n\t\tif is_macos; then\n\t\t\techo \"$(brew --prefix 2>/dev/null || echo /usr/local)/etc\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi\"\n\t\tfi\n\t}\n\n\t_cliproxy_conf() {\n\t\tif is_macos; then\n\t\t\techo \"$(_cliproxy_dir)/cliproxyapi.conf\"\n\t\telse\n\t\t\techo \"$(_cliproxy_dir)/config.yaml\"\n\t\tfi\n\t}\n\n\t_cliproxy_bin() {\n\t\tif is_macos; then\n\t\t\tcommand -v cliproxyapi 2>/dev/null || echo \"cliproxyapi\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi/cli-proxy-api\"\n\t\tfi\n\t}\n\n\t_cliproxy_running() {\n\t\tif is_macos; then\n\t\t\tpgrep -f \"cliproxyapi\" >/dev/null 2>&1\n\t\telse\n\t\t\tpgrep -f \"cli-proxy-api\" >/dev/null 2>&1\n\t\tfi\n\t}\n\n\t_cliproxy_start_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services start cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tsystemctl --user enable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"systemctl --user start cliproxyapi.service 2>/dev/null; sleep 3\"\n\t\t\tif ! _cliproxy_running; then\n\t\t\t\tui_warn \"systemd 启动失败,改用后台直接运行...\"\n\t\t\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\t\t\t(cd \"$cliproxy_dir\" && nohup ./cli-proxy-api > /tmp/cliproxyapi.log 2>&1 &)\n\t\t\t\tsleep 3\n\t\t\tfi\n\t\tfi\n\t\tif _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已启动\"\n\t\t\treturn 0\n\t\telse\n\t\t\tui_err \"CLIProxyAPI 启动失败,请检查 $(_cliproxy_conf) 后手动启动\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\t_cliproxy_stop_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services stop cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbash -c \"systemctl --user stop cliproxyapi.service 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\tfi\n\t\tlocal _pat; _pat=$(is_macos && echo \"cliproxyapi\" || echo \"cli-proxy-api\")\n\t\tlocal pids; pids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\tif [[ -n \"$pids\" ]]; then\n\t\t\techo \"$pids\" | xargs kill 2>/dev/null || true\n\t\t\tsleep 2\n\t\t\tpids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\t\t[[ -n \"$pids\" ]] && echo \"$pids\" | xargs kill -9 2>/dev/null || true\n\t\tfi\n\t\tif ! _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已停止\"\n\t\telse\n\t\t\tui_err \"停止失败,仍有进程残留\"\n\t\tfi\n\t}\n\n\t_cliproxy_oauth_login() {\n\t\tlocal cliproxy_bin; cliproxy_bin=$(_cliproxy_bin)\n\t\tif ! command -v \"$cliproxy_bin\" &>/dev/null && [[ ! -x \"$cliproxy_bin\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 未安装:$cliproxy_bin\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_choice\n\t\tprovider_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' 选择要登录的 AI 提供商\\n ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"Claude (Anthropic)\" \\\n\t\t\t\"Gemini (Google)\" \\\n\t\t\t\"OpenAI / Codex\" \\\n\t\t\t\"Qwen (通义千问)\" \\\n\t\t\t\"iFlow\" \\\n\t\t\t\"取消\") || return 0\n\n\t\tlocal login_cmd=\"\" login_port=\"\"\n\t\tcase \"$provider_choice\" in\n\t\t\t\"Claude (Anthropic)\") login_cmd=\"--claude-login\"; login_port=\"54545\" ;;\n\t\t\t\"Gemini (Google)\") login_cmd=\"--login\"; login_port=\"8085\" ;;\n\t\t\t\"OpenAI / Codex\") login_cmd=\"--codex-login\"; login_port=\"1455\" ;;\n\t\t\t\"Qwen (通义千问)\") login_cmd=\"--qwen-login\"; login_port=\"\" ;;\n\t\t\t\"iFlow\") login_cmd=\"--iflow-login\"; login_port=\"11451\" ;;\n\t\t\t\"取消\"|*) return 0 ;;\n\t\tesac\n\n\t\techo\n\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\n\t\tlocal mode_input=\"\" proj_input=\"\"\n\t\tif [[ \"$login_cmd\" == \"--login\" ]]; then\n\t\t\tlocal mode_choice\n\t\t\tmode_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择 Gemini 登录模式\\n ↑↓ 移动 · Enter 确认' \\\n\t\t\t\t\"Code Assist(GCP 项目,手动选择)\" \\\n\t\t\t\t\"Google One(个人账号,自动发现项目)\") || return 0\n\t\t\t[[ \"$mode_choice\" == \"Code Assist\"* ]] && mode_input=\"1\" || mode_input=\"2\"\n\n\t\t\tif [[ \"$mode_input\" == \"1\" ]]; then\n\t\t\t\techo\n\t\t\t\tlocal proj_list proj_choice proj_num=\"1\"\n\t\t\t\tif command -v gcloud &>/dev/null; then\n\t\t\t\t\tproj_list=$(gcloud projects list --format=\"value(projectId,name)\" 2>/dev/null)\n\t\t\t\tfi\n\t\t\t\tif [[ -n \"$proj_list\" ]]; then\n\t\t\t\t\tproj_choice=$(printf \"%s\\n\" \"$proj_list\" | \\\n\t\t\t\t\t\tgum choose --cursor \"❯ \" \\\n\t\t\t\t\t\t--header $' 选择 GCP 项目\\n ↑↓ 移动 · Enter 确认') || return 0\n\t\t\t\t\tproj_num=$(printf \"%s\\n\" \"$proj_list\" | \\\n\t\t\t\t\t\tawk -v sel=\"$proj_choice\" 'NR==sel_line || $0==sel {print NR; exit}' \\\n\t\t\t\t\t\tsel=\"$proj_choice\" 2>/dev/null || echo \"1\")\n\t\t\t\t\t[[ -z \"$proj_num\" || \"$proj_num\" == \"0\" ]] && proj_num=\"1\"\n\t\t\t\telse\n\t\t\t\t\tlocal proj_raw\n\t\t\t\t\tproj_raw=$(gum input \\\n\t\t\t\t\t\t--placeholder \"项目编号或直接按 Enter 使用默认(1)\" \\\n\t\t\t\t\t\t--prompt \" ❯ \" --width 60) || return 0\n\t\t\t\t\tproj_num=\"${proj_raw:-1}\"\n\t\t\t\tfi\n\t\t\t\tproj_input=\"$proj_num\"\n\t\t\tfi\n\t\tfi\n\n\t\tif [[ -n \"$login_port\" ]]; then\n\t\t\tif is_macos; then\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 本地浏览器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 浏览器将自动打开,完成授权后自动返回,请稍候...\"\n\t\t\t\techo\n\t\t\t\tif [[ -n \"$mode_input\" ]]; then\n\t\t\t\t\tprintf \"%s\\n%s\\n\" \"$mode_input\" \"$proj_input\" | \"$cliproxy_bin\" \"$login_cmd\"\n\t\t\t\telse\n\t\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\t\tfi\n\t\t\telse\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 服务器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 1. 脚本后台启动 cli-proxy-api,并打印 OAuth 授权链接\" \\\n\t\t\t\t\t\" 2. 在本地浏览器打开该链接,完成账号授权\" \\\n\t\t\t\t\t\" 3. 授权后浏览器跳转到 localhost:${login_port},页面报错属正常\" \\\n\t\t\t\t\t\" 4. 复制地址栏完整 URL,粘贴到下方提示符,脚本自动完成握手\"\n\t\t\t\techo\n\t\t\t\tgum style --foreground 208 \\\n\t\t\t\t\t\" ⚡ 无需端口转发 — 回调 URL 里已含 code,直接转发给本机处理\"\n\t\t\t\techo\n\n\t\t\t\tif [[ -n \"$mode_input\" ]]; then\n\t\t\t\t\tlocal fifo; fifo=$(mktemp -u /tmp/cp_login_XXXXXX)\n\t\t\t\t\tmkfifo \"$fifo\"\n\t\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser < \"$fifo\") &\n\t\t\t\t\tlocal cli_pid=$!\n\t\t\t\t\texec 9>\"$fifo\"\n\t\t\t\t\tprintf \"%s\\n\" \"$mode_input\" >&9\n\t\t\t\t\tsleep 0.3\n\t\t\t\t\t[[ -n \"$proj_input\" ]] && { printf \"%s\\n\" \"$proj_input\" >&9; sleep 0.3; }\n\t\t\t\t\texec 9>&-\n\t\t\t\t\trm -f \"$fifo\"\n\t\t\t\telse\n\t\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser) &\n\t\t\t\t\tlocal cli_pid=$!\n\t\t\t\tfi\n\t\t\t\tsleep 2\n\n\t\t\t\techo\n\t\t\t\tlocal callback_url\n\t\t\t\tcallback_url=$(gum input \\\n\t\t\t\t\t--placeholder \"http://localhost:${login_port}/oauth2callback?code=...\" \\\n\t\t\t\t\t--prompt \" ❯ \" \\\n\t\t\t\t\t--width 120 \\\n\t\t\t\t\t--char-limit 2000) || true\n\n\t\t\t\tif [[ -n \"$callback_url\" ]]; then\n\t\t\t\t\tui_info \"正在向本机发送回调...\"\n\t\t\t\t\tcurl -sf \"$callback_url\" > /dev/null 2>&1 || true\n\t\t\t\t\tsleep 1\n\t\t\t\tfi\n\t\t\t\twait \"$cli_pid\" 2>/dev/null || true\n\t\t\tfi\n\t\telse\n\t\t\tui_info \"Qwen 使用 device flow,无需粘贴回调\"\n\t\t\techo\n\t\t\tgum style --foreground 240 \"将打印授权码 + URL,在任意浏览器中打开并输入授权码即可\"\n\t\t\techo\n\t\t\tif is_macos; then\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser)\n\t\t\tfi\n\t\tfi\n\t}\n\n\tcliproxyapi_manage_menu() {\n\t\tlocal config_file; config_file=$(_cliproxy_conf)\n\n\t\twhile true; do\n\t\t\tclear\n\n\t\t\tlocal cp_installed cp_running cp_version cp_port cp_keys\n\t\t\tif is_macos; then\n\t\t\t\tcp_version=$(brew info cliproxyapi 2>/dev/null | awk 'NR==1{print $4}')\n\t\t\telse\n\t\t\t\tcp_version=$(cat \"$(_cliproxy_dir)/version.txt\" 2>/dev/null || echo \"\")\n\t\t\tfi\n\t\t\tif [[ -n \"$cp_version\" ]]; then\n\t\t\t\tcp_installed=\"\\033[38;5;46m◉ INSTALLED v${cp_version}\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_installed=\"\\033[90m○ NOT INSTALLED\\033[0m\"\n\t\t\tfi\n\t\t\tif _cliproxy_running; then\n\t\t\t\tcp_running=\"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_running=\"\\033[90m■ STOPPED\\033[0m\"\n\t\t\tfi\n\t\t\tcp_port=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\t\t\tcp_port=\"${cp_port:-8317}\"\n\t\t\tcp_keys=$(awk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{n++} END{print n+0}' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 51 \\\n\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t--padding \"1 4\" --align center \\\n\t\t\t\t\"C L I P R O X Y A P I\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ AI OAuth Proxy Manager ]\"\n\t\t\techo -e \" $cp_installed $cp_running\"\n\t\t\techo -e \" \\033[90m端口 :${cp_port} API Keys: ${cp_keys}\\033[0m\"\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' ─── CLIProxyAPI ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"启动\" \\\n\t\t\t\t\"停止\" \\\n\t\t\t\t\"重启\" \\\n\t\t\t\t\"查看日志\" \\\n\t\t\t\t\"账号授权登录\" \\\n\t\t\t\t\"生成并添加 API Key\" \\\n\t\t\t\t\"查看 API Keys\" \\\n\t\t\t\t\"编辑配置文件\" \\\n\t\t\t\t\"更新\" \\\n\t\t\t\t\"卸载\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"启动\")\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"停止\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"重启\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tsleep 1\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看日志\")\n\t\t\t\t\techo\n\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\tlocal brew_log\n\t\t\t\t\t\tbrew_log=\"$(brew --prefix 2>/dev/null)/var/log/cliproxyapi.log\"\n\t\t\t\t\t\tif [[ -f \"$brew_log\" ]]; then\n\t\t\t\t\t\t\ttail -80 \"$brew_log\"\n\t\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\telif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\t\t\tjournalctl --user -u cliproxyapi.service --no-pager -n 80\n\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"账号授权登录\")\n\t\t\t\t\t_cliproxy_oauth_login\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"生成并添加 API Key\")\n\t\t\t\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\t\tlocal new_key\n\t\t\t\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\t\t\t\techo\n\t\t\t\t\tgum style --bold --foreground 46 \" ◉ 已生成新 API Key:\"\n\t\t\t\t\tgum style --bold --foreground 51 \" $new_key\"\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 将此 Key 自动写入 config.yaml?\"; then\n\t\t\t\t\t\tpython3 - \"$config_file\" \"$new_key\" <<'PY'\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n)', r'\\1 - \"' + key + '\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nprint(\"写入成功\")\nPY\n\t\t\t\t\t\tui_ok \"API Key 已写入配置\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看 API Keys\")\n\t\t\t\t\techo\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tgum style --foreground 240 \" config.yaml 中的 API Keys:\"\n\t\t\t\t\t\techo\n\t\t\t\t\t\tawk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{print \" \" $0}' \\\n\t\t\t\t\t\t\t\"$config_file\"\n\t\t\t\t\t\techo\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到配置文件:$config_file\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"编辑配置文件\")\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tnano \"$config_file\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\tfi\n\t\t\t\t\t;;\n\t\t\t\t\"更新\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 确认更新 CLIProxyAPI 到最新版本?\"; then\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew upgrade cliproxyapi && ui_ok \"更新完成\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tcurl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer \\\n\t\t\t\t\t\t\t\t| bash -s -- upgrade\n\t\t\t\t\t\t\tui_ok \"更新完成\"\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"卸载\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" ⚡ 确认卸载 CLIProxyAPI?\"; then\n\t\t\t\t\t\t_cliproxy_stop_service 2>/dev/null || true\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew uninstall cliproxyapi 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$HOME/.cli-proxy-api\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tsystemctl --user disable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\t\t\t\t\trm -f \"$HOME/.config/systemd/user/cliproxyapi.service\" 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$(_cliproxy_dir)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\t\tui_ok \"CLIProxyAPI 已卸载\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tbeginner_mode_install() {\n\t\tif is_macos; then\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ API Key 配置 ]\"\n\t\telse\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ CLIProxyAPI ⟶ AI Auth ⟶ Auto Config ]\"\n\t\tfi\n\t\techo\n\n\t\tui_step \"第 1 步:安装 OpenClaw\"\n\t\t_install_openclaw_core\n\n\t\tui_step \"第 2 步:安装 CLIProxyAPI\"\n\t\tif is_macos; then\n\t\t\tif ! gum spin --spinner globe --title \"正在安装 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbrew install cliproxyapi; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败(brew install cliproxyapi)\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\telse\n\t\t\tif ! curl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer | bash; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\n\t\tlocal cliproxy_config; cliproxy_config=$(_cliproxy_conf)\n\t\tif [[ ! -f \"$cliproxy_config\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 安装后未找到配置文件: $cliproxy_config\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\t\tui_ok \"CLIProxyAPI 安装完成\"\n\n\t\tui_step \"第 3 步:登录 AI 账号\"\n\t\tui_info \"通过 OAuth 授权即可免费使用,无需付费 API Key\"\n\t\techo\n\t\t_cliproxy_oauth_login\n\n\t\tui_step \"第 4 步:生成 API Key 并启动服务\"\n\t\tlocal new_key\n\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\tpython3 - \"$cliproxy_config\" \"$new_key\" <<'PY' 2>/dev/null\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n(?:\\s*-\\s*\"[^\"]*\"\\s*\\n)*)', lambda m: m.group(0) + f' - \"{key}\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nPY\n\t\tif ! _cliproxy_start_service; then\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_step \"第 5 步:配置 OpenClaw API\"\n\n\t\tlocal api_key port base_url\n\t\tapi_key=$(awk '\n\t\t\t/^api-keys:/ { in_keys=1; next }\n\t\t\tin_keys && /^[^ \\t]/ { exit }\n\t\t\tin_keys && /\"sk-/ { match($0, /sk-[^\"]+/); print substr($0, RSTART, RLENGTH); exit }\n\t\t' \"$cliproxy_config\" 2>/dev/null)\n\n\t\tport=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \"$cliproxy_config\" 2>/dev/null)\n\t\tport=\"${port:-8317}\"\n\t\tbase_url=\"http://localhost:${port}/v1\"\n\n\t\tif [[ -z \"$api_key\" ]]; then\n\t\t\tui_err \"无法从 $cliproxy_config 读取 API Key,请手动在「CLIProxyAPI 管理」中添加\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_info \"地址:$base_url\"\n\t\tui_info \"Key:${api_key:0:12}****\"\n\t\techo\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"cliproxy\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tstart_gateway\n\n\t\t\tui_step \"第 6 步:选择默认模型\"\n\t\t\tui_info \"请从可用模型中选择一个作为默认,选完后网关自动重启\"\n\t\t\techo\n\t\t\tchange_model\n\n\t\t\techo\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 46 \\\n\t\t\t\t--border double --border-foreground 46 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◉ INSTALL COMPLETE ◉\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw 已自动接入 CLIProxyAPI,可直接开始使用 ]\" \\\n\t\t\t\t\"[ 更多提供商:cd ~/cliproxyapi && ./cli-proxy-api --help ]\"\n\t\tfi\n\n\t\techo\n\t\tif gum confirm \" 现在去对接机器人?\"; then\n\t\t\tchange_tg_bot_code\n\t\telse\n\t\t\tbreak_end\n\t\tfi\n\t}\n\n\tstart_bot() {\n\t\tstart_gateway\n\t\tbreak_end\n\t}\n\n\tstop_bot() {\n\t\ttmux kill-session -t gateway > /dev/null 2>&1\n\t\tgum spin --spinner pulse --title \"正在停止网关...\" -- openclaw gateway stop\n\t\tbreak_end\n\t}\n\n\tview_logs() {\n\t\topenclaw status\n\t\topenclaw gateway status\n\t\topenclaw logs\n\t\tbreak_end\n\t}\n\n\tadd-all-models-from-provider() {\n\t\tlocal provider_name=\"$1\"\n\t\tlocal base_url=\"$2\"\n\t\tlocal api_key=\"$3\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tlocal models_tmpfile\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取 $provider_name 的模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\n\t\tlocal models_json\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tif [[ -z \"$models_json\" ]]; then\n\t\t\techo \"错误:无法获取模型列表\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_ids\n\t\tmodel_ids=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n for m in models:\n if isinstance(m, dict) and \"id\" in m:\n print(m[\"id\"])\nexcept Exception:\n pass\n')\n\n\t\tif [[ -z \"$model_ids\" ]]; then\n\t\t\techo \"错误:未找到任何模型\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_count\n\t\tmodel_count=$(echo \"$model_ids\" | wc -l | tr -d ' ')\n\t\techo \"发现 $model_count 个模型\"\n\n\t\tlocal models_array=\"[\"\n\t\tlocal first=true\n\n\t\twhile read -r model_id; do\n\t\t\t[[ $first == false ]] && models_array+=\",\"\n\t\t\tfirst=false\n\n\t\t\tlocal context_window=1048576\n\t\t\tlocal max_tokens=128000\n\t\t\tlocal input_cost=0.15\n\t\t\tlocal output_cost=0.60\n\n\t\t\tcase \"$model_id\" in\n\t\t\t\t*opus*|*pro*|*preview*|*thinking*|*sonnet*)\n\t\t\t\t\tinput_cost=2.00\n\t\t\t\t\toutput_cost=12.00\n\t\t\t\t\t;;\n\t\t\t\t*gpt-5*|*codex*)\n\t\t\t\t\tinput_cost=1.25\n\t\t\t\t\toutput_cost=10.00\n\t\t\t\t\t;;\n\t\t\t\t*flash*|*lite*|*haiku*|*mini*|*nano*)\n\t\t\t\t\tinput_cost=0.10\n\t\t\t\t\toutput_cost=0.40\n\t\t\t\t\t;;\n\t\t\tesac\n\n\t\t\tmodels_array+=$(cat <<EOF\n{\n\t\"id\": \"$model_id\",\n\t\"name\": \"$provider_name / $model_id\",\n\t\"input\": [\"text\", \"image\"],\n\t\"contextWindow\": $context_window,\n\t\"maxTokens\": $max_tokens,\n\t\"cost\": {\n\t\t\"input\": $input_cost,\n\t\t\"output\": $output_cost,\n\t\t\"cacheRead\": 0,\n\t\t\"cacheWrite\": 0\n\t}\n}\nEOF\n)\n\t\tdone <<< \"$model_ids\"\n\n\t\tmodels_array+=\"]\"\n\n\t\t[[ -f \"$config_file\" ]] && cp \"$config_file\" \"${config_file}.bak.$(date +%s)\"\n\n\t\tjq --arg prov \"$provider_name\" \\\n\t\t --arg url \"$base_url\" \\\n\t\t --arg key \"$api_key\" \\\n\t\t --argjson models \"$models_array\" \\\n\t\t'\n\t\t.models |= (\n\t\t\t(. // { mode: \"merge\", providers: {} })\n\t\t\t| .mode = \"merge\"\n\t\t\t| .providers[$prov] = {\n\t\t\t\tbaseUrl: $url,\n\t\t\t\tapiKey: $key,\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tmodels: $models\n\t\t\t}\n\t\t)\n\t\t| .agents |= (. // {})\n\t\t| .agents.defaults |= (. // {})\n\t\t| .agents.defaults.models |= (\n\t\t\t(if type == \"object\" then .\n\t\t\t elif type == \"array\" then reduce .[] as $m ({}; if ($m|type) == \"string\" then .[$m] = {} else . end)\n\t\t\t else {}\n\t\t\t end) as $existing\n\t\t\t| reduce ($models[]? | .id? // empty | tostring) as $mid (\n\t\t\t\t$existing;\n\t\t\t\tif ($mid | length) > 0 then\n\t\t\t\t\t.[\"\\($prov)/\\($mid)\"] //= {}\n\t\t\t\telse\n\t\t\t\t\t.\n\t\t\t\tend\n\t\t\t)\n\t\t)\n\t\t' \"$config_file\" > \"${config_file}.tmp\" && mv \"${config_file}.tmp\" \"$config_file\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\techo \"成功添加 $model_count 个模型到 $provider_name\"\n\t\t\techo \"模型引用格式: $provider_name/<model-id>\"\n\t\t\treturn 0\n\t\telse\n\t\t\techo \"错误:配置注入失败\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\tadd-openclaw-provider-interactive() {\n\t\tlocal provider_name base_url api_key\n\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"Provider 名称 (如: deepseek)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\t[[ -z \"$provider_name\" ]] && return 1\n\n\t\tbase_url=$(gum input \\\n\t\t\t--placeholder \"Base URL (如: https://api.xxx.com/v1)\" \\\n\t\t\t--prompt \"Base URL > \")\n\t\t[[ -z \"$base_url\" ]] && return 1\n\t\tbase_url=\"${base_url%/}\"\n\n\t\tapi_key=$(gum input --password \\\n\t\t\t--placeholder \"API Key\" \\\n\t\t\t--prompt \"API Key > \")\n\t\t[[ -z \"$api_key\" ]] && return 1\n\n\t\tlocal models_tmpfile models_json available_models model_count\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tlocal default_model=\"\"\n\t\tif [[ -n \"$models_json\" ]]; then\n\t\t\tavailable_models=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n ids = sorted(m[\"id\"] for m in models if isinstance(m, dict) and \"id\" in m)\n print(\"\\n\".join(ids))\nexcept Exception:\n pass\n')\n\t\t\tmodel_count=$(echo \"$available_models\" | wc -l | tr -d ' ')\n\t\tfi\n\n\t\tif [[ -n \"$available_models\" ]]; then\n\t\t\tdefault_model=$(echo \"$available_models\" | fzf \\\n\t\t\t\t--prompt=\" ❯ \" \\\n\t\t\t\t--header=\" 发现 $model_count 个模型 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 选第一个\" \\\n\t\t\t\t--header-first \\\n\t\t\t\t--height=15 \\\n\t\t\t\t--layout=reverse \\\n\t\t\t\t--border=double \\\n\t\t\t\t--border-label=\" ◈ DEFAULT MODEL \" \\\n\t\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || default_model=$(echo \"$available_models\" | head -1)\n\t\t\t[[ -z \"$default_model\" ]] && default_model=$(echo \"$available_models\" | head -1)\n\t\t\tdefault_model=$(awk '{print $1}' <<< \"$default_model\")\n\t\tfi\n\n\t\techo\n\t\tgum style \\\n\t\t\t--border normal --border-foreground 99 \\\n\t\t\t--padding \"0 2\" \\\n\t\t\t\"Provider : $provider_name\" \\\n\t\t\t\"Base URL : $base_url\" \\\n\t\t\t\"API Key : ${api_key:0:8}****\" \\\n\t\t\t\"默认模型 : $default_model\" \\\n\t\t\t\"模型总数 : $model_count\"\n\t\techo\n\n\t\tgum confirm \"确认添加所有 $model_count 个模型?\" || { echo \"已取消\"; return 1; }\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"$provider_name\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tgum spin --spinner minidot --title \"设置默认模型并重启网关...\" -- \\\n\t\t\t\topenclaw models set \"$provider_name/$default_model\"\n\t\t\tstart_gateway\n\t\t\techo \"完成:所有 $model_count 个模型已加载\"\n\t\tfi\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_list() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tlocal _py_list; _py_list=$(mktemp /tmp/oc_list_XXXXXX.py)\n\t\tcat > \"$_py_list\" << 'PYEOF'\nimport json, sys, time, urllib.request\n\npath = sys.argv[1]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\ndef ping_models(base_url, api_key):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': 'Bearer ' + api_key,\n 'User-Agent': 'OpenClaw-API-Manage/1.0',\n },\n )\n start = time.perf_counter()\n with urllib.request.urlopen(req, timeout=4) as resp:\n resp.read(2048)\n return int((time.perf_counter() - start) * 1000)\n\ndef classify_latency(latency):\n if latency == '不可用':\n return '不可用', 'unavailable'\n if latency == '未检测':\n return '未检测', 'unchecked'\n if isinstance(latency, int):\n level = 'low' if latency <= 800 else ('medium' if latency <= 2000 else 'high')\n return str(latency) + 'ms', level\n return str(latency), 'unchecked'\n\ntry:\n with open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\nexcept FileNotFoundError:\n print('MSG\\t未找到 openclaw.json,请先完成安装/初始化。')\n raise SystemExit(0)\nexcept Exception as e:\n print('MSG\\t读取配置失败: ' + type(e).__name__ + ': ' + str(e))\n raise SystemExit(0)\n\nproviders = ((obj.get('models') or {}).get('providers') or {})\nif not isinstance(providers, dict) or not providers:\n print('MSG\\t当前未配置任何 API provider。')\n raise SystemExit(0)\n\nprint('MSG\\t--- 已配置 API 列表 ---')\n\nfor idx, name in enumerate(sorted(providers.keys()), start=1):\n provider = providers.get(name)\n if not isinstance(provider, dict):\n base_url = '-'; model_count = 0; latency_raw = '不可用'\n else:\n base_url = provider.get('baseUrl') or provider.get('url') or provider.get('endpoint') or '-'\n models = provider.get('models') if isinstance(provider.get('models'), list) else []\n model_count = sum(1 for m in models if isinstance(m, dict) and m.get('id'))\n api = provider.get('api', '')\n api_key = provider.get('apiKey')\n latency_raw = '未检测'\n if api in SUPPORTED_APIS and isinstance(base_url, str) and base_url != '-' and isinstance(api_key, str) and api_key:\n try:\n latency_raw = ping_models(base_url, api_key)\n except Exception:\n latency_raw = '不可用'\n elif api in SUPPORTED_APIS:\n latency_raw = '不可用'\n\n latency_text, latency_level = classify_latency(latency_raw)\n print('ROW\\t' + '\\t'.join([str(idx), str(name), str(base_url), str(model_count), str(latency_text), str(latency_level)]))\nPYEOF\n\n\t\twhile IFS=$'\\t' read -r rec_type idx name base_url model_count latency_txt latency_level; do\n\t\t\tcase \"$rec_type\" in\n\t\t\t\tMSG)\n\t\t\t\t\techo \"$idx\"\n\t\t\t\t\t;;\n\t\t\t\tROW)\n\t\t\t\t\tlocal latency_color=\"$gl_bai\"\n\t\t\t\t\tcase \"$latency_level\" in\n\t\t\t\t\t\tlow) latency_color=\"$gl_lv\" ;;\n\t\t\t\t\t\tmedium) latency_color=\"$gl_huang\" ;;\n\t\t\t\t\t\thigh|unavailable) latency_color=\"$gl_hong\" ;;\n\t\t\t\t\t\tunchecked) latency_color=\"$gl_bai\" ;;\n\t\t\t\t\tesac\n\n\t\t\t\t\tprintf '%b\\n' \"[$idx] ${name} | API: ${base_url} | 模型数量: ${gl_huang}${model_count}${gl_bai} | 延迟/状态: ${latency_color}${latency_txt}${gl_bai}\"\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone < <(python3 \"$_py_list\" \"$config_file\")\n\t\trm -f \"$_py_list\"\n\t}\n\n\tsync-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要同步的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tinstall jq curl >/dev/null 2>&1\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY2'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\ntarget = sys.argv[2]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('错误:未检测到 API providers,无法同步')\n raise SystemExit(2)\n\nprovider = providers.get(target)\nif not isinstance(provider, dict):\n print(f'错误:未找到 provider: {target}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef fetch_remote_models_with_retry(base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n return json.loads(payload), None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\napi = provider.get('api', '')\nbase_url = provider.get('baseUrl')\napi_key = provider.get('apiKey')\nmodel_list = provider.get('models', [])\n\nif not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n print(f'错误:provider {target} 缺少 baseUrl/apiKey/models,无法执行同步')\n raise SystemExit(3)\n\nif api not in SUPPORTED_APIS:\n print(f'错误:provider {target} 当前 api={api},不支持直接 /models 同步')\n raise SystemExit(3)\n\ndata, err, attempts = fetch_remote_models_with_retry(base_url, api_key, retries=3)\nif err is not None:\n print(f'错误:{target}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n raise SystemExit(4)\n\nif not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n print(f'错误:{target}: /models 返回结构不可识别')\n raise SystemExit(4)\n\nremote_ids = []\nfor item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\nremote_set = set(remote_ids)\nif not remote_set:\n print(f'错误:{target}: 上游 /models 为空,已中止同步')\n raise SystemExit(5)\n\nlocal_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\nlocal_ids = [str(m['id']) for m in local_models]\nlocal_set = set(local_ids)\n\ntemplate = copy.deepcopy(local_models[0]) if local_models else None\nif template is None:\n print(f'错误:{target}: 本地 models 无有效模板模型,无法补全新增模型')\n raise SystemExit(3)\n\nremoved_ids = [mid for mid in local_ids if mid not in remote_set]\nadded_ids = [mid for mid in remote_ids if mid not in local_set]\n\nkept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\nnew_models = kept_models[:]\nfor mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{target} / {mid}'\n new_models.append(nm)\n\nif not new_models:\n print(f'错误:{target}: 同步后无可用模型,已中止写入')\n raise SystemExit(5)\n\nexpected_refs = {model_ref(target, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\nlocal_refs = {model_ref(target, mid) for mid in local_ids}\nremoved_refs = local_refs - expected_refs\nfirst_ref = model_ref(target, str(new_models[0]['id']))\n\nchanged = False\nprimary_ref = get_primary_ref(defaults)\nif isinstance(primary_ref, str) and primary_ref in removed_refs:\n set_primary_ref(defaults, first_ref)\n changed = True\n print(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in removed_refs:\n defaults[fk] = first_ref\n changed = True\n print(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\nstale_refs = [r for r in list(defaults_models.keys()) if r.startswith(target + '/') and r not in expected_refs]\nfor r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\nfor r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\nif removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'{target}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\nif changed:\n print('指定 provider 模型一致性同步完成')\nelse:\n print('无需同步:该 provider 配置已与上游 /models 保持一致')\nPY2\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"同步完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在或未配置\" ;;\n\t\t\t3) echo \"错误:provider 配置不完整或类型不支持\" ;;\n\t\t\t4) echo \"错误:上游 /models 请求失败\" ;;\n\t\t\t5) echo \"错误:上游模型为空或同步后无可用模型\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\tdelete-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要删除的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY'\nimport copy\nimport json\nimport sys\n\npath = sys.argv[1]\nname = sys.argv[2]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or name not in providers:\n print(f'错误:未找到 provider: {name}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\nreplacement_candidates = collect_available_refs(exclude_provider=name)\nreplacement = replacement_candidates[0] if replacement_candidates else None\n\nprimary_ref = get_primary_ref(defaults)\nif ref_provider(primary_ref) == name:\n if not replacement:\n print('错误:删除中止:默认主模型指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n set_primary_ref(defaults, replacement)\n print(f'默认主模型切换: {primary_ref} -> {replacement}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n if not replacement:\n print(f'错误:删除中止:{fk} 指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n defaults[fk] = replacement\n print(f'{fk} 切换: {val} -> {replacement}')\n\nremoved_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\nfor r in removed_refs:\n defaults_models.pop(r, None)\n\nproviders.pop(name, None)\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'已删除 provider: {name}')\nprint(f'已清理 defaults.models 中 {len(removed_refs)} 个关联模型引用')\nPY\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"删除完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在\" ;;\n\t\t\t3) echo \"错误:无可用替代模型,已保持原配置\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"API 管理\"\n\t\t\topenclaw_api_manage_list\n\t\t\techo\n\n\t\t\tlocal api_choice\n\t\t\tapi_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加 API\" \\\n\t\t\t\t\"同步 API 供应商模型列表\" \\\n\t\t\t\t\"删除 API\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$api_choice\" in\n\t\t\t\t\"添加 API\") add-openclaw-provider-interactive ;;\n\t\t\t\t\"同步 API 供应商模型列表\") sync-openclaw-provider-interactive ;;\n\t\t\t\t\"删除 API\") delete-openclaw-provider-interactive ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tchange_model() {\n\t\tlocal all_models model\n\t\tall_models=$(openclaw models list --all 2>/dev/null | grep \"configured\")\n\n\t\tif [[ -z \"$all_models\" ]]; then\n\t\t\tui_err \"无法获取可用模型列表(请先添加 API 提供商)\"\n\t\t\tbreak_end\n\t\t\treturn\n\t\tfi\n\n\t\tmodel=$(echo \"$all_models\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 当前: $(openclaw models list 2>/dev/null | awk '{print $1}' | head -1) │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=20 \\\n\t\t\t--layout=reverse \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ◈ MODEL SELECT \" \\\n\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || return\n\n\t\t[[ -z \"$model\" ]] && return\n\t\tmodel=$(awk '{print $1}' <<< \"$model\")\n\n\t\tgum spin --spinner minidot --title \"正在切换模型...\" -- openclaw models set \"$model\"\n\t\tstart_gateway\n\t\tui_ok \"已切换至:$model\"\n\t\tbreak_end\n\t}\n\n\tresolve_openclaw_plugin_id() {\n\t\tlocal raw_input=\"$1\"\n\t\tlocal plugin_id=\"$raw_input\"\n\n\t\tplugin_id=\"${plugin_id#@openclaw/}\"\n\t\tif [[ \"$plugin_id\" == @*/* ]]; then\n\t\t\tplugin_id=\"${plugin_id##*/}\"\n\t\tfi\n\t\tplugin_id=\"${plugin_id%%@*}\"\n\t\techo \"$plugin_id\"\n\t}\n\n\tsync_openclaw_plugin_allowlist() {\n\t\tlocal plugin_id=\"$1\"\n\t\t[[ -z \"$plugin_id\" ]] && return 1\n\n\t\tlocal home_config=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal root_config=\"/root/.openclaw/openclaw.json\"\n\t\tlocal config_file=\"$home_config\"\n\t\tif [[ ! -f \"$home_config\" && -f \"$root_config\" ]]; then\n\t\t\tconfig_file=\"$root_config\"\n\t\tfi\n\n\t\tmkdir -p \"$(dirname \"$config_file\")\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo '{}' > \"$config_file\"\n\t\tfi\n\n\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\tlocal tmp_json\n\t\t\ttmp_json=$(mktemp)\n\t\t\tif jq --arg pid \"$plugin_id\" '\n\t\t\t\t.plugins = (if (.plugins | type) == \"object\" then .plugins else {} end)\n\t\t\t\t| .plugins.allow = (if (.plugins.allow | type) == \"array\" then .plugins.allow else [] end)\n\t\t\t\t| if (.plugins.allow | index($pid)) == null then .plugins.allow += [$pid] else . end\n\t\t\t' \"$config_file\" > \"$tmp_json\" 2>/dev/null && mv \"$tmp_json\" \"$config_file\"; then\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\t\trm -f \"$tmp_json\"\n\t\tfi\n\n\t\tif command -v python3 >/dev/null 2>&1; then\n\t\t\tif python3 - \"$config_file\" \"$plugin_id\" <<'PYTHON_EOF'\nimport json\nimport sys\nfrom pathlib import Path\n\nconfig_file = Path(sys.argv[1])\nplugin_id = sys.argv[2]\n\ntry:\n data = json.loads(config_file.read_text(encoding='utf-8')) if config_file.exists() else {}\n if not isinstance(data, dict):\n data = {}\nexcept Exception:\n data = {}\n\nplugins = data.get('plugins')\nif not isinstance(plugins, dict):\n plugins = {}\n\na = plugins.get('allow')\nif not isinstance(a, list):\n a = []\n\nif plugin_id not in a:\n a.append(plugin_id)\n\nplugins['allow'] = a\ndata['plugins'] = plugins\nconfig_file.write_text(json.dumps(data, ensure_ascii=False, indent=2) + \"\\n\", encoding='utf-8')\nPYTHON_EOF\n\t\t\tthen\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\tfi\n\n\t\techo \"警告:已安装插件,但同步 plugins.allow 失败,请手动检查: $config_file\"\n\t\treturn 1\n\t}\n\n\tinstall_plugin() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"插件管理\"\n\t\t\tgum style --foreground 240 \"当前已安装插件:\"\n\t\t\topenclaw plugins list\n\t\t\techo\n\n\t\t\tlocal raw_input\n\t\t\traw_input=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择要安装的插件\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"feishu 飞书/Lark 集成\" \\\n\t\t\t\t\"telegram Telegram 机器人\" \\\n\t\t\t\t\"slack Slack 企业通讯\" \\\n\t\t\t\t\"msteams Microsoft Teams\" \\\n\t\t\t\t\"discord Discord 社区管理\" \\\n\t\t\t\t\"whatsapp WhatsApp 自动化\" \\\n\t\t\t\t\"memory-core 基础记忆 (文件检索)\" \\\n\t\t\t\t\"memory-lancedb 增强记忆 (向量数据库)\" \\\n\t\t\t\t\"copilot-proxy Copilot 接口转发\" \\\n\t\t\t\t\"lobster 审批流 (带人工确认)\" \\\n\t\t\t\t\"voice-call 语音通话能力\" \\\n\t\t\t\t\"nostr 加密隐私聊天\" \\\n\t\t\t\t\"手动输入插件 ID\") || break\n\n\t\t\tif [[ \"$raw_input\" == \"手动输入插件 ID\" ]]; then\n\t\t\t\traw_input=$(gum input \\\n\t\t\t\t\t--placeholder \"插件 ID (留空退出)\" \\\n\t\t\t\t\t--prompt \"Plugin > \") || break\n\t\t\t\t[[ -z \"$raw_input\" ]] && break\n\t\t\telse\n\t\t\t\traw_input=$(echo \"$raw_input\" | awk '{print $1}')\n\t\t\tfi\n\t\t\t[[ -z \"$raw_input\" ]] && break\n\n\t\t\tlocal plugin_id plugin_full\n\t\t\tplugin_id=$(resolve_openclaw_plugin_id \"$raw_input\")\n\t\t\tplugin_full=\"$raw_input\"\n\n\t\t\tlocal plugin_list\n\t\t\tplugin_list=$(openclaw plugins list 2>/dev/null)\n\n\t\t\tif echo \"$plugin_list\" | grep -qw \"$plugin_id\" && echo \"$plugin_list\" | grep \"$plugin_id\" | grep -q \"disabled\"; then\n\t\t\t\tui_info \"插件 [$plugin_id] 已预装,正在激活...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\" && ui_ok \"激活成功\" || ui_err \"激活失败\"\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/extensions/$plugin_id\" ]]; then\n\t\t\t\tui_info \"发现系统内置插件,尝试直接启用...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\telse\n\t\t\t\tui_info \"本地未发现,尝试下载安装...\"\n\t\t\t\trm -rf \"/root/.openclaw/extensions/$plugin_id\"\n\n\t\t\t\tif openclaw plugins install \"$plugin_full\"; then\n\t\t\t\t\tui_ok \"下载成功,正在启用...\"\n\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\telse\n\t\t\t\t\tui_warn \"官方渠道下载失败,尝试 npm 备选方案...\"\n\t\t\t\t\tif npm install -g \"$plugin_full\" --unsafe-perm; then\n\t\t\t\t\t\tui_ok \"npm 安装成功,尝试启用...\"\n\t\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"无法获取该插件,请检查 ID 是否正确或网络是否可用\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\tfi\n\t\t\tfi\n\n\t\t\tsync_openclaw_plugin_allowlist \"$plugin_id\"\n\t\t\tstart_gateway\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\tinstall_skill() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"技能管理\"\n\t\t\tgum style --foreground 240 \"当前已安装技能:\"\n\t\t\topenclaw skills list\n\t\t\techo\n\n\t\t\tlocal skill_name\n\t\t\tskill_name=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择要安装的技能\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"github 管理 GitHub Issues/PR/CI\" \\\n\t\t\t\t\"notion 操作 Notion 页面与数据库\" \\\n\t\t\t\t\"apple-notes macOS 原生笔记管理\" \\\n\t\t\t\t\"apple-reminders macOS 提醒事项管理\" \\\n\t\t\t\t\"1password 自动化读取 1Password 密钥\" \\\n\t\t\t\t\"gog Google Workspace 全能助手\" \\\n\t\t\t\t\"things-mac Things 3 任务管理\" \\\n\t\t\t\t\"bluebubbles 通过 BlueBubbles 收发 iMessage\" \\\n\t\t\t\t\"himalaya 终端邮件管理 (IMAP/SMTP)\" \\\n\t\t\t\t\"summarize 网页/播客/YouTube 内容总结\" \\\n\t\t\t\t\"openai-whisper 本地音频转文字\" \\\n\t\t\t\t\"coding-agent 运行 Claude Code/Codex 等编程助手\" \\\n\t\t\t\t\"手动输入技能名称\") || break\n\n\t\t\tif [[ \"$skill_name\" == \"手动输入技能名称\" ]]; then\n\t\t\t\tskill_name=$(gum input \\\n\t\t\t\t\t--placeholder \"技能名称 (留空退出)\" \\\n\t\t\t\t\t--prompt \"Skill > \") || break\n\t\t\t\t[[ -z \"$skill_name\" ]] && break\n\t\t\telse\n\t\t\t\tskill_name=$(echo \"$skill_name\" | awk '{print $1}')\n\t\t\tfi\n\t\t\t[[ -z \"$skill_name\" ]] && break\n\n\t\t\tlocal skill_found=false\n\t\t\tif [[ -d \"${HOME}/.openclaw/workspace/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在用户目录安装\"\n\t\t\t\tskill_found=true\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在系统目录安装\"\n\t\t\t\tskill_found=true\n\t\t\tfi\n\n\t\t\tif [[ \"$skill_found\" == true ]]; then\n\t\t\t\tgum confirm \"是否重新安装?\" || { break_end; continue; }\n\t\t\tfi\n\n\t\t\tgum spin --spinner globe --title \"正在安装技能 $skill_name...\" -- npx clawhub install \"$skill_name\"\n\t\t\tif [[ $? -eq 0 ]]; then\n\t\t\t\techo \"技能 $skill_name 安装成功\"\n\t\t\t\tstart_gateway\n\t\t\telse\n\t\t\t\techo \"错误:安装失败,请检查技能名称是否正确\"\n\t\t\tfi\n\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\topenclaw_json_get_bool() {\n\t\tlocal expr=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r \"$expr\" \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_channel_has_cfg() {\n\t\tlocal channel=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r --arg c \"$channel\" '\n\t\t\t(.channels[$c] // null) as $v\n\t\t\t| if ($v | type) != \"object\" then\n\t\t\t\tfalse\n\t\t\t else\n\t\t\t\t([ $v\n\t\t\t\t | to_entries[]\n\t\t\t\t | select((.key == \"enabled\" or .key == \"dmPolicy\" or .key == \"groupPolicy\" or .key == \"streaming\") | not)\n\t\t\t\t | .value\n\t\t\t\t | select(. != null and . != \"\" and . != false)\n\t\t\t\t ] | length) > 0\n\t\t\t end\n\t\t' \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_dir_has_files() {\n\t\tlocal dir=\"$1\"\n\t\t[[ -d \"$dir\" ]] && find \"$dir\" -type f -print -quit 2>/dev/null | grep -q .\n\t}\n\n\topenclaw_plugin_local_installed() {\n\t\tlocal plugin=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ -s \"$config_file\" ]] && jq -e --arg p \"$plugin\" '.plugins.installs[$p]' \"$config_file\" >/dev/null 2>&1; then\n\t\t\treturn 0\n\t\tfi\n\t\t[[ -d \"${HOME}/.openclaw/extensions/${plugin}\" ]] || [[ -d \"/usr/lib/node_modules/openclaw/extensions/${plugin}\" ]]\n\t}\n\n\topenclaw_bot_status_text() {\n\t\tlocal enabled=\"$1\"\n\t\tlocal configured=\"$2\"\n\t\tlocal connected=\"$3\"\n\t\tlocal abnormal=\"$4\"\n\t\tif [[ \"$abnormal\" == \"true\" ]]; then\n\t\t\techo \"异常\"\n\t\telif [[ \"$enabled\" != \"true\" ]]; then\n\t\t\techo \"未启用\"\n\t\telif [[ \"$connected\" == \"true\" ]]; then\n\t\t\techo \"已连接\"\n\t\telif [[ \"$configured\" == \"true\" ]]; then\n\t\t\techo \"已配置\"\n\t\telse\n\t\t\techo \"未配置\"\n\t\tfi\n\t}\n\n\topenclaw_colorize_bot_status() {\n\t\tlocal status=\"$1\"\n\t\tcase \"$status\" in\n\t\t\t已连接) echo -e \"${gl_lv}${status}${gl_bai}\" ;;\n\t\t\t已配置) echo -e \"${gl_huang}${status}${gl_bai}\" ;;\n\t\t\t异常) echo -e \"${gl_hong}${status}${gl_bai}\" ;;\n\t\t\t*) echo \"$status\" ;;\n\t\tesac\n\t}\n\n\topenclaw_print_bot_status_line() {\n\t\tlocal label=\"$1\"\n\t\tlocal status=\"$2\"\n\t\techo -e \"- ${label}: $(openclaw_colorize_bot_status \"$status\")\"\n\t}\n\n\topenclaw_show_bot_local_status_block() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal json_ok=\"false\"\n\t\tif [[ -s \"$config_file\" ]] && jq empty \"$config_file\" >/dev/null 2>&1; then\n\t\t\tjson_ok=\"true\"\n\t\tfi\n\n\t\tlocal tg_enabled tg_cfg tg_connected tg_abnormal tg_status\n\t\ttg_enabled=$(openclaw_json_get_bool '.channels.telegram.enabled // .plugins.entries.telegram.enabled // false')\n\t\ttg_cfg=$(openclaw_channel_has_cfg \"telegram\")\n\t\ttg_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/telegram\"; then\n\t\t\ttg_connected=\"true\"\n\t\tfi\n\t\ttg_abnormal=\"false\"\n\t\tif [[ \"$tg_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\ttg_abnormal=\"true\"\n\t\tfi\n\t\ttg_status=$(openclaw_bot_status_text \"$tg_enabled\" \"$tg_cfg\" \"$tg_connected\" \"$tg_abnormal\")\n\n\t\tlocal feishu_enabled feishu_cfg feishu_connected feishu_abnormal feishu_status\n\t\tfeishu_enabled=$(openclaw_json_get_bool '.plugins.entries.feishu.enabled // .channels.feishu.enabled // false')\n\t\tfeishu_cfg=$(openclaw_channel_has_cfg \"feishu\")\n\t\tfeishu_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/feishu\"; then\n\t\t\tfeishu_connected=\"true\"\n\t\tfi\n\t\tfeishu_abnormal=\"false\"\n\t\tif [[ \"$feishu_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"feishu\"; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$feishu_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tfeishu_status=$(openclaw_bot_status_text \"$feishu_enabled\" \"$feishu_cfg\" \"$feishu_connected\" \"$feishu_abnormal\")\n\n\t\tlocal wa_enabled wa_cfg wa_connected wa_abnormal wa_status\n\t\twa_enabled=$(openclaw_json_get_bool '.plugins.entries.whatsapp.enabled // .channels.whatsapp.enabled // false')\n\t\twa_cfg=$(openclaw_channel_has_cfg \"whatsapp\")\n\t\twa_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/whatsapp\"; then\n\t\t\twa_connected=\"true\"\n\t\tfi\n\t\twa_abnormal=\"false\"\n\t\tif [[ \"$wa_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"whatsapp\"; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$wa_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\twa_status=$(openclaw_bot_status_text \"$wa_enabled\" \"$wa_cfg\" \"$wa_connected\" \"$wa_abnormal\")\n\n\t\tlocal dc_enabled dc_cfg dc_connected dc_abnormal dc_status\n\t\tdc_enabled=$(openclaw_json_get_bool '.channels.discord.enabled // .plugins.entries.discord.enabled // false')\n\t\tdc_cfg=$(openclaw_channel_has_cfg \"discord\")\n\t\tdc_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/discord\"; then\n\t\t\tdc_connected=\"true\"\n\t\tfi\n\t\tdc_abnormal=\"false\"\n\t\tif [[ \"$dc_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tdc_abnormal=\"true\"\n\t\tfi\n\t\tdc_status=$(openclaw_bot_status_text \"$dc_enabled\" \"$dc_cfg\" \"$dc_connected\" \"$dc_abnormal\")\n\n\t\tlocal slack_enabled slack_cfg slack_connected slack_abnormal slack_status\n\t\tslack_enabled=$(openclaw_json_get_bool '.plugins.entries.slack.enabled // .channels.slack.enabled // false')\n\t\tslack_cfg=$(openclaw_channel_has_cfg \"slack\")\n\t\tslack_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/slack\"; then\n\t\t\tslack_connected=\"true\"\n\t\tfi\n\t\tslack_abnormal=\"false\"\n\t\tif [[ \"$slack_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"slack\"; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$slack_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tslack_status=$(openclaw_bot_status_text \"$slack_enabled\" \"$slack_cfg\" \"$slack_connected\" \"$slack_abnormal\")\n\n\t\tlocal qq_enabled qq_cfg qq_connected qq_abnormal qq_status\n\t\tqq_enabled=$(openclaw_json_get_bool '.plugins.entries.qqbot.enabled // .channels.qqbot.enabled // false')\n\t\tqq_cfg=$(openclaw_channel_has_cfg \"qqbot\")\n\t\tqq_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/sessions\" || openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/data\"; then\n\t\t\tqq_connected=\"true\"\n\t\tfi\n\t\tqq_abnormal=\"false\"\n\t\tif [[ \"$qq_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"qqbot\"; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$qq_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tqq_status=$(openclaw_bot_status_text \"$qq_enabled\" \"$qq_cfg\" \"$qq_connected\" \"$qq_abnormal\")\n\n\t\techo \"本地状态(仅本机配置/缓存,不做网络探测):\"\n\t\topenclaw_print_bot_status_line \"Telegram\" \"$tg_status\"\n\t\topenclaw_print_bot_status_line \"飞书(Lark)\" \"$feishu_status\"\n\t\topenclaw_print_bot_status_line \"WhatsApp\" \"$wa_status\"\n\t\topenclaw_print_bot_status_line \"Discord\" \"$dc_status\"\n\t\topenclaw_print_bot_status_line \"Slack\" \"$slack_status\"\n\t\topenclaw_print_bot_status_line \"QQ Bot\" \"$qq_status\"\n\t}\n\n\tchange_tg_bot_code() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"机器人连接对接\"\n\t\t\topenclaw_show_bot_local_status_block\n\t\t\techo\n\n\t\t\tlocal bot_choice\n\t\t\tbot_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择要对接的平台\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"Telegram 机器人对接\" \\\n\t\t\t\t\"飞书 (Lark) 机器人对接\" \\\n\t\t\t\t\"WhatsApp 机器人对接\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tlocal code\n\t\t\tcase \"$bot_choice\" in\n\t\t\t\t\"Telegram 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"TG机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve telegram \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"飞书 (Lark) 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"飞书机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve feishu \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"WhatsApp 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"WhatsApp连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve whatsapp \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"返回\"|*)\n\t\t\t\t\treturn 0\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\topenclaw_backup_root() {\n\t\techo \"${HOME}/.openclaw/backups\"\n\t}\n\n\topenclaw_is_interactive_terminal() {\n\t\t[[ -t 0 && -t 1 ]]\n\t}\n\n\topenclaw_has_command() {\n\t\tcommand -v \"$1\" >/dev/null 2>&1\n\t}\n\n\topenclaw_is_safe_relpath() {\n\t\tlocal rel=\"$1\"\n\t\t[[ -z \"$rel\" ]] && return 1\n\t\t[[ \"$rel\" == /* ]] && return 1\n\t\t[[ \"$rel\" == *//* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\n'* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\r'* ]] && return 1\n\t\tcase \"$rel\" in\n\t\t\t../*|*/../*|*/..|..)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t\treturn 0\n\t}\n\n\topenclaw_restore_path_allowed() {\n\t\tlocal mode=\"$1\"\n\t\tlocal rel=\"$2\"\n\t\tcase \"$mode\" in\n\t\t\tmemory)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\tMEMORY.md|AGENTS.md|USER.md|SOUL.md|TOOLS.md|memory/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\tproject)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\topenclaw.json|workspace/*|extensions/*|skills/*|prompts/*|tools/*|telegram/*|feishu/*|whatsapp/*|discord/*|slack/*|qqbot/*|logs/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\t*)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t}\n\n\topenclaw_pack_backup_archive() {\n\t\tlocal backup_type=\"$1\"\n\t\tlocal export_mode=\"$2\"\n\t\tlocal payload_dir=\"$3\"\n\t\tlocal output_file=\"$4\"\n\n\t\tlocal tmp_root\n\t\ttmp_root=$(mktemp -d) || return 1\n\t\tlocal pack_dir=\"$tmp_root/package\"\n\t\tmkdir -p \"$pack_dir\"\n\n\t\tcp -a \"$payload_dir\" \"$pack_dir/payload\"\n\n\t\t(\n\t\t\tcd \"$pack_dir/payload\" || exit 1\n\t\t\tfind . -type f | sed 's|^\\./||' | sort > \"$pack_dir/manifest.files\"\n\t\t\t: > \"$pack_dir/manifest.sha256\"\n\t\t\tlocal f\n\t\t\twhile IFS= read -r f; do\n\t\t\t\t[[ -z \"$f\" ]] && continue\n\t\t\t\tsha256sum \"$f\" >> \"$pack_dir/manifest.sha256\"\n\t\t\tdone < \"$pack_dir/manifest.files\"\n\t\t) || { rm -rf \"$tmp_root\"; return 1; }\n\n\t\tcat > \"$pack_dir/backup.meta\" <<EOF\nTYPE=$backup_type\nMODE=$export_mode\nCREATED_AT=$(date -u +'%Y-%m-%dT%H:%M:%SZ')\nHOST=$(hostname)\nEOF\n\n\t\tmkdir -p \"$(dirname \"$output_file\")\"\n\t\ttar -C \"$pack_dir\" -czf \"$output_file\" backup.meta manifest.files manifest.sha256 payload\n\t\tlocal rc=$?\n\t\trm -rf \"$tmp_root\"\n\t\treturn $rc\n\t}\n\n\topenclaw_offer_transfer_hint() {\n\t\tlocal file_path=\"$1\"\n\t\techo \"可使用以下方式下载备份文件:\"\n\t\techo \" 本地路径: $file_path\"\n\t\techo \" scp 示例: scp root@你的服务器:$file_path ./\"\n\t\techo \" 或使用 SFTP 客户端下载\"\n\t}\n\n\topenclaw_prepare_import_archive() {\n\t\tlocal expected_type=\"$1\"\n\t\tlocal archive_path=\"$2\"\n\t\tlocal unpack_root=\"$3\"\n\n\t\tif [[ ! -f \"$archive_path\" ]]; then\n\t\t\techo \"错误:文件不存在: $archive_path\"\n\t\t\treturn 1\n\t\tfi\n\t\tmkdir -p \"$unpack_root\"\n\t\tif ! tar -xzf \"$archive_path\" -C \"$unpack_root\"; then\n\t\t\techo \"错误:备份包解压失败\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal pkg_dir=\"$unpack_root/package\"\n\t\tif [[ -f \"$unpack_root/backup.meta\" ]]; then\n\t\t\tpkg_dir=\"$unpack_root\"\n\t\tfi\n\n\t\tlocal required\n\t\tfor required in backup.meta manifest.files manifest.sha256 payload; do\n\t\t\tif [[ ! -e \"$pkg_dir/$required\" ]]; then\n\t\t\t\techo \"错误:备份包缺少必要文件: $required\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tdone\n\n\t\tlocal real_type\n\t\treal_type=$(grep '^TYPE=' \"$pkg_dir/backup.meta\" | head -n1 | cut -d'=' -f2-)\n\t\tif [[ \"$real_type\" != \"$expected_type\" ]]; then\n\t\t\techo \"错误:备份类型不匹配,期望: $expected_type,实际: ${real_type:-未知}\"\n\t\t\treturn 1\n\t\tfi\n\n\t\t(\n\t\t\tcd \"$pkg_dir/payload\" || exit 1\n\t\t\tsha256sum -c ../manifest.sha256 >/dev/null\n\t\t) || { echo \"错误:sha256 校验失败,拒绝还原\"; return 1; }\n\n\t\techo \"$pkg_dir\"\n\t\treturn 0\n\t}\n\n\topenclaw_memory_backup_export() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tlocal ts\n\t\tts=$(date +%Y%m%d-%H%M%S)\n\t\tlocal out_file=\"$backup_root/openclaw-memory-full-${ts}.tar.gz\"\n\n\t\tmkdir -p \"$backup_root\"\n\t\tif [[ ! -d \"$workspace_dir\" ]]; then\n\t\t\techo \"错误:未找到 workspace 目录: $workspace_dir\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\t[[ -f \"$workspace_dir/MEMORY.md\" ]] && cp -a \"$workspace_dir/MEMORY.md\" \"$tmp_payload/\"\n\t\t[[ -d \"$workspace_dir/memory\" ]] && cp -a \"$workspace_dir/memory\" \"$tmp_payload/\"\n\n\t\tif gum confirm \"是否附带 AGENTS/USER/SOUL/TOOLS 文件?\"; then\n\t\t\tlocal f\n\t\t\tfor f in AGENTS.md USER.md SOUL.md TOOLS.md; do\n\t\t\t\t[[ -f \"$workspace_dir/$f\" ]] && cp -a \"$workspace_dir/$f\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的记忆文件\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"memory-full\" \"default\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"记忆全量备份完成: $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:记忆全量备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_read_import_path() {\n\t\tlocal file_input file_path backup_root\n\t\techo \"提示:输入文件名时默认在备份目录查找;输入含 / 的路径时按完整路径校验。\" >&2\n\t\techo \"scp 示例: scp /本地/备份包.tar.gz root@你的服务器:/tmp/\" >&2\n\n\t\tfile_input=$(gum input \\\n\t\t\t--placeholder \"备份文件名或完整路径\" \\\n\t\t\t--prompt \"Path > \")\n\t\t[[ -z \"$file_input\" ]] && { echo \"\"; return 0; }\n\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\n\t\tif [[ \"$file_input\" == */* ]]; then\n\t\t\tfile_path=\"$file_input\"\n\t\telse\n\t\t\tfile_path=\"$backup_root/$file_input\"\n\t\tfi\n\n\t\tif [[ ! -f \"$file_path\" ]]; then\n\t\t\techo \"错误:备份文件不存在: $file_path\" >&2\n\t\t\techo \"\"\n\t\t\treturn 1\n\t\tfi\n\n\t\techo \"$file_path\"\n\t}\n\n\topenclaw_memory_backup_import() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tmkdir -p \"$workspace_dir\"\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"memory-full\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed memory \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$workspace_dir/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$workspace_dir/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"记忆全量还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_export() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tif [[ ! -d \"$openclaw_root\" ]]; then\n\t\t\techo \"错误:未找到 OpenClaw 根目录: $openclaw_root\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal export_mode_label\n\t\texport_mode_label=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $'选择备份模式\\n↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"安全模式(推荐):workspace + openclaw.json + extensions/skills/prompts/tools\" \\\n\t\t\t\"完整模式(含更多状态,敏感风险更高)\") || return 1\n\n\t\tlocal mode_label=\"safe\"\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\tlocal d\n\t\tif [[ \"$export_mode_label\" == 完整* ]]; then\n\t\t\tmode_label=\"full\"\n\t\t\tfor d in workspace extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in telegram feishu whatsapp discord slack qqbot logs; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\telse\n\t\t\t[[ -d \"$openclaw_root/workspace\" ]] && cp -a \"$openclaw_root/workspace\" \"$tmp_payload/\"\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的 OpenClaw 项目内容\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tlocal out_file=\"$backup_root/openclaw-project-${mode_label}-$(date +%Y%m%d-%H%M%S).tar.gz\"\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"openclaw-project\" \"$mode_label\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"OpenClaw 项目备份完成 (${mode_label}): $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:OpenClaw 项目备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_import() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tmkdir -p \"$openclaw_root\"\n\n\t\techo \"警告:高风险操作,项目还原会覆盖 OpenClaw 配置与工作区内容。\"\n\t\tgum confirm \"确认继续还原?此操作不可逆。\" || { echo \"已取消\"; break_end; return 1; }\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"openclaw-project\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed project \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tgum spin --spinner pulse --title \"停止 OpenClaw gateway...\" -- openclaw gateway stop\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$openclaw_root/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$openclaw_root/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tstart_gateway\n\t\t\techo \"gateway 健康检查:\"\n\t\t\topenclaw gateway status || true\n\t\tfi\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"OpenClaw 项目还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_detect_type() {\n\t\tlocal file_name=\"$1\"\n\t\tif [[ \"$file_name\" == openclaw-memory-full-*.tar.gz ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\telif [[ \"$file_name\" == openclaw-project-*.tar.gz ]]; then\n\t\t\techo \"项目备份文件\"\n\t\telse\n\t\t\techo \"其他备份文件\"\n\t\tfi\n\t}\n\n\topenclaw_backup_collect_files() {\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tmapfile -t OPENCLAW_BACKUP_FILES < <(find \"$backup_root\" -maxdepth 1 -type f -name '*.tar.gz' | xargs -I{} basename {} | sort -r)\n\t}\n\n\topenclaw_backup_render_file_list() {\n\t\tlocal backup_root i file_name file_path file_type file_size file_time\n\t\tlocal has_memory=0 has_project=0 has_other=0\n\t\tbackup_root=$(openclaw_backup_root)\n\t\topenclaw_backup_collect_files\n\n\t\techo \"备份目录: $backup_root\"\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\techo \"暂无备份文件\"\n\t\t\treturn 0\n\t\tfi\n\n\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\tfile_type=$(openclaw_backup_detect_type \"${OPENCLAW_BACKUP_FILES[$i]}\")\n\t\t\tcase \"$file_type\" in\n\t\t\t\t\"记忆备份文件\") has_memory=1 ;;\n\t\t\t\t\"项目备份文件\") has_project=1 ;;\n\t\t\t\t\"其他备份文件\") has_other=1 ;;\n\t\t\tesac\n\t\tdone\n\n\t\tif [[ \"$has_memory\" -eq 1 ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"记忆备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_project\" -eq 1 ]]; then\n\t\t\techo \"项目备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"项目备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_other\" -eq 1 ]]; then\n\t\t\techo \"其他备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"其他备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_backup_file_exists_in_list() {\n\t\tlocal target_file=\"$1\"\n\t\tlocal item\n\t\tfor item in \"${OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t[[ \"$item\" == \"$target_file\" ]] && return 0\n\t\tdone\n\t\treturn 1\n\t}\n\n\topenclaw_backup_delete_file() {\n\t\tlocal backup_root backup_root_real user_input target_file target_path target_type\n\t\tbackup_root=$(openclaw_backup_root)\n\n\t\topenclaw_backup_render_file_list\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\tbreak_end\n\t\t\treturn 0\n\t\tfi\n\n\t\tuser_input=$(printf '%s\\n' \"${OPENCLAW_BACKUP_FILES[@]}\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 选择要删除的备份 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=15 \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ⚡ DELETE BACKUP \" \\\n\t\t\t--color=border:196,label:196,header:196,prompt:208,pointer:196,marker:208,hl:208,hl+:208) || { echo \"已取消\"; break_end; return 0; }\n\n\t\t[[ -z \"$user_input\" ]] && { echo \"已取消\"; break_end; return 0; }\n\n\t\tbackup_root_real=$(realpath -m \"$backup_root\")\n\t\ttarget_file=$(basename -- \"$user_input\")\n\t\ttarget_path=\"$backup_root/$target_file\"\n\n\t\tif [[ ! -f \"$target_path\" ]]; then\n\t\t\techo \"错误:目标文件不存在: $target_path\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\ttarget_type=$(openclaw_backup_detect_type \"$target_file\")\n\n\t\techo \"即将删除: [$target_type] $target_path\"\n\t\tgum confirm \"确认删除?\" || { echo \"已取消删除\"; break_end; return 0; }\n\t\tgum confirm \"请再次确认,删除后不可恢复\" || { echo \"已取消删除\"; break_end; return 0; }\n\n\t\tif rm -f -- \"$target_path\"; then\n\t\t\techo \"删除成功: $target_file\"\n\t\telse\n\t\t\techo \"错误:删除失败: $target_file\"\n\t\tfi\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_list_files() {\n\t\topenclaw_backup_render_file_list\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_restore_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"备份与还原\"\n\t\t\topenclaw_backup_render_file_list\n\t\t\techo\n\n\t\t\tlocal backup_choice\n\t\t\tbackup_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"备份记忆全量\" \\\n\t\t\t\t\"还原记忆全量\" \\\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\" \\\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\" \\\n\t\t\t\t\"删除备份文件\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$backup_choice\" in\n\t\t\t\t\"备份记忆全量\") openclaw_memory_backup_export ;;\n\t\t\t\t\"还原记忆全量\") openclaw_memory_backup_import ;;\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\") openclaw_project_backup_export ;;\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\") openclaw_project_backup_import ;;\n\t\t\t\t\"删除备份文件\") openclaw_backup_delete_file ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\t_autostart_status() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\techo \"未安装\"\n\t\t\t\treturn\n\t\t\tfi\n\t\t\tif launchctl list 2>/dev/null | grep -q \"ai.openclaw.gateway\"; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\telse\n\t\t\tif systemctl --user is-enabled --quiet openclaw-gateway.service 2>/dev/null; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\tfi\n\t}\n\n\ttoggle_autostart() {\n\t\tlocal status; status=$(_autostart_status)\n\t\tclear\n\t\tui_header \"开机自启动管理\"\n\t\techo -e \" 当前状态:\\033[1m${status}\\033[0m\"\n\t\techo\n\n\t\tlocal action\n\t\tif [[ \"$status\" == \"已开启\" ]]; then\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"关闭自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\telse\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"开启自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\tfi\n\n\t\tcase \"$action\" in\n\t\t\t\"开启自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\t\t\tgum spin --spinner globe --title \"正在安装服务...\" -- \\\n\t\t\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 1\"\n\t\t\t\t\tfi\n\t\t\t\t\tlaunchctl load \"$plist\" 2>/dev/null && ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t\"关闭自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tlaunchctl unload \"$plist\" 2>/dev/null && ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user disable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t*) return 0 ;;\n\t\tesac\n\t\tbreak_end\n\t}\n\n\tupdate_moltbot() {\n\t\tinstall_node_and_tools\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_bin\n\t\tnpm_bin=\"$(npm prefix -g 2>/dev/null)/bin\"\n\t\tif [[ -n \"$npm_bin\" ]]; then\n\t\t\texport PATH=\"$npm_bin:$PATH\"\n\t\t\tfor rc in \"$HOME/.bashrc\" \"$HOME/.zshrc\" \"$HOME/.profile\"; do\n\t\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t\tgrep -qF \"$npm_bin\" \"$rc\" 2>/dev/null && continue\n\t\t\t\techo \"export PATH=\\\"${npm_bin}:\\$PATH\\\"\" >> \"$rc\"\n\t\t\tdone\n\t\tfi\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab -\n\t\tstart_gateway\n\t\tui_ok \"更新完成\"\n\t\tbreak_end\n\t}\n\n\tuninstall_moltbot() {\n\t\tgum confirm \"确认卸载 OpenClaw?此操作不可逆。\" || return 0\n\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tlaunchctl unload \"$plist\" 2>/dev/null || true\n\t\telse\n\t\t\tsystemctl --user disable --now openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tpkill -f \"openclaw.*gatewa\" 2>/dev/null || true\n\n\t\tgum spin --spinner meter --title \"正在卸载 OpenClaw...\" -- \\\n\t\t\tbash -c 'timeout 15 openclaw uninstall 2>/dev/null; true'\n\t\tnpm uninstall -g openclaw 2>/dev/null || true\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab - 2>/dev/null || true\n\t\trm -rf /root/.openclaw ~/.openclaw\n\n\t\trm -f \"$HOME/.local/bin/oc\" \"$HOME/.local/bin/openclawctl.sh\" /usr/local/bin/oc\n\t\tfor rc in \"$HOME/.zshrc\" \"$HOME/.bashrc\" \"$HOME/.profile\"; do\n\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t_sed_i '/\\.local\\/bin.*PATH/d' \"$rc\"\n\t\t\t_sed_i '/npm-global.*PATH\\|npm\\/bin.*PATH/d' \"$rc\"\n\t\tdone\n\n\t\techo\n\t\tgum style --foreground 46 --bold \" 卸载完成,脚本已退出。\"\n\t\texit 0\n\t}\n\n\tnano_openclaw_json() {\n\t\tinstall nano\n\t\tnano ~/.openclaw/openclaw.json\n\t\tstart_gateway\n\t}\n\n\topenclaw_find_webui_domain() {\n\t\tlocal conf domain_list\n\t\tdomain_list=$(\n\t\t\tgrep -R \"18789\" /home/web/conf.d/*.conf 2>/dev/null \\\n\t\t\t| awk -F: '{print $1}' \\\n\t\t\t| sort -u \\\n\t\t\t| while read -r conf; do\n\t\t\t\tbasename \"$conf\" .conf\n\t\t\tdone\n\t\t)\n\n\t\tif [[ -n \"$domain_list\" ]]; then\n\t\t\techo \"$domain_list\"\n\t\tfi\n\t}\n\n\topenclaw_show_webui_addr() {\n\t\tlocal local_ip token domains\n\n\t\tlocal_ip=\"127.0.0.1\"\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tgum style --border normal --border-foreground 99 --padding \"0 2\" \\\n\t\t\t\"OpenClaw WebUI 访问地址\" \\\n\t\t\t\"本机: http://${local_ip}:18789/#token=${token}\"\n\n\t\tdomains=$(openclaw_find_webui_domain)\n\t\tif [[ -n \"$domains\" ]]; then\n\t\t\techo \"$domains\" | while read -r d; do\n\t\t\t\techo \"域名: https://${d}/#token=${token}\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_domain_webui() {\n\t\tadd_yuming\n\t\tldnmp_Proxy \"${yuming}\" 127.0.0.1 18789\n\n\t\tlocal token\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tclear\n\t\techo \"访问地址: https://${yuming}/#token=$token\"\n\t\techo \"先访问URL触发设备ID,然后回车下一步进行配对。\"\n\t\tgum input --placeholder \"按回车继续...\" > /dev/null\n\n\t\techo -e \"${gl_kjlan}正在加载设备列表……${gl_bai}\"\n\n\t\tlocal config_file=\"$HOME/.openclaw/openclaw.json\"\n\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\tlocal new_origin=\"https://${yuming}\"\n\t\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\t\tlocal tmp_json\n\t\t\t\ttmp_json=$(mktemp)\n\t\t\t\tjq 'if .gateway.controlUi == null then .gateway.controlUi = {\"allowedOrigins\": [\"http://127.0.0.1\"]} else . end | if (.gateway.controlUi.allowedOrigins | contains([$origin]) | not) then .gateway.controlUi.allowedOrigins += [$origin] else . end' \\\n\t\t\t\t\t--arg origin \"$new_origin\" \"$config_file\" > \"$tmp_json\" && mv \"$tmp_json\" \"$config_file\"\n\t\t\t\techo -e \"${gl_kjlan}已将域名 ${yuming} 加入 allowedOrigins 配置${gl_bai}\"\n\t\t\t\topenclaw gateway restart >/dev/null 2>&1\n\t\t\tfi\n\t\tfi\n\n\t\topenclaw devices list\n\n\t\tlocal Request_Key\n\t\tRequest_Key=$(gum input \\\n\t\t\t--placeholder \"Request_Key\" \\\n\t\t\t--prompt \"> \")\n\n\t\tif [[ -z \"$Request_Key\" ]]; then\n\t\t\techo \"Request_Key 不能为空\"\n\t\t\treturn 1\n\t\tfi\n\n\t\topenclaw devices approve \"$Request_Key\"\n\t}\n\n\topenclaw_remove_domain() {\n\t\techo \"域名格式 example.com 不带https://\"\n\t\tweb_del\n\t}\n\n\topenclaw_webui_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"WebUI 访问与设置\"\n\t\t\topenclaw_show_webui_addr\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加域名访问\" \\\n\t\t\t\t\"删除域名访问\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"添加域名访问\")\n\t\t\t\t\topenclaw_domain_webui\n\t\t\t\t\techo\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"删除域名访问\")\n\t\t\t\t\topenclaw_remove_domain\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\twhile true; do\n\t\tclear\n\n\t\tlocal install_status running_status update_info\n\t\tinstall_status=$(get_install_status)\n\t\trunning_status=$(get_running_status)\n\t\tupdate_info=$(check_openclaw_update)\n\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"1 8\" --align center \\\n\t\t\t\"O P E N C L A W\" \\\n\t\t\t\"\" \\\n\t\t\t\"[ AI Agent Gateway Manager ]\"\n\t\techo -e \" $install_status $running_status $update_info\"\n\t\tgum style --foreground 240 \" 输入 oc 可快速启动\"\n\t\techo\n\t\tgum style \\\n\t\t\t--bold \\\n\t\t\t--border double --border-foreground 201 \\\n\t\t\t--padding \"0 4\" --align center \\\n\t\t\t\"$(gum style --bold --foreground 201 '◈ by Joey ◈')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ YouTube @joeyblog')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ Telegram t.me/+ft-zI76oovgwNmRh')\" \\\n\t\t\t\"$(gum style --foreground 208 '⚡ 基于:kejilion · cliproxyapi-installer')\"\n\t\techo\n\n\t\tlocal choice\n\t\tlocal autostart_label\n\t\tautostart_label=\"开机自启动 [$(_autostart_status)]\"\n\n\t\tchoice=$(gum choose \\\n\t\t\t--height 24 \\\n\t\t\t--cursor \"❯ \" \\\n\t\t\t--header $' ─── CONTROL CENTER ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\"小白模式安装(推荐)\" \\\n\t\t\t\"安装\" \\\n\t\t\t\"启动\" \\\n\t\t\t\"停止\" \\\n\t\t\t\"状态日志查看\" \\\n\t\t\t\"$autostart_label\" \\\n\t\t\t\"换模型\" \\\n\t\t\t\"API管理\" \\\n\t\t\t\"CLIProxyAPI 管理\" \\\n\t\t\t\"机器人连接对接\" \\\n\t\t\t\"安装插件\" \\\n\t\t\t\"安装技能\" \\\n\t\t\t\"编辑主配置文件\" \\\n\t\t\t\"配置向导\" \\\n\t\t\t\"健康检测与修复\" \\\n\t\t\t\"WebUI访问与设置\" \\\n\t\t\t\"TUI命令行对话\" \\\n\t\t\t\"备份与还原\" \\\n\t\t\t\"更新\" \\\n\t\t\t\"卸载\" \\\n\t\t\t\"退出\") || break\n\n\t\tcase \"$choice\" in\n\t\t\t\"小白模式安装(推荐)\") beginner_mode_install ;;\n\t\t\t\"安装\") install_moltbot ;;\n\t\t\t\"启动\") start_bot ;;\n\t\t\t\"停止\") stop_bot ;;\n\t\t\t\"状态日志查看\") view_logs ;;\n\t\t\t\"开机自启动 [\"*\"]\") toggle_autostart ;;\n\t\t\t\"换模型\") change_model ;;\n\t\t\"API管理\") openclaw_api_manage_menu ;;\n\t\t\"CLIProxyAPI 管理\") cliproxyapi_manage_menu ;;\n\t\t\"机器人连接对接\") change_tg_bot_code ;;\n\t\t\t\"安装插件\") install_plugin ;;\n\t\t\t\"安装技能\") install_skill ;;\n\t\t\t\"编辑主配置文件\") nano_openclaw_json ;;\n\t\t\t\"配置向导\")\n\t\t\t\topenclaw onboard --install-daemon\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"健康检测与修复\")\n\t\t\t\tgum spin --spinner meter --title \"正在执行健康检测...\" -- openclaw doctor --fix\n\t\t\t\tif sync_openclaw_api_models; then\n\t\t\t\t\tstart_gateway\n\t\t\t\telse\n\t\t\t\t\tui_err \"API 模型同步失败,已中止重启网关,请检查 provider /models 返回后重试\"\n\t\t\t\tfi\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"WebUI访问与设置\") openclaw_webui_menu ;;\n\t\t\t\"TUI命令行对话\")\n\t\t\t\topenclaw tui\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"备份与还原\") openclaw_backup_restore_menu ;;\n\t\t\t\"更新\") update_moltbot ;;\n\t\t\t\"卸载\") uninstall_moltbot ;;\n\t\t\t\"退出\"|*) break ;;\n\t\tesac\n\tdone\n}\n\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n\tmoltbot_menu\nfi\n"}, "0d226ded0771d6e78863f3141f8f4e3d0b658f24": {"t": 1785772919.6505425, "n": 97814, "h": "01299b08d3bddd73", "c": "#!/usr/bin/env bash\n#\n# ╔══════════════════════════════════════════════════════════════════╗\n# ║ OpenClaw CTL / MoltBot 管理脚本 ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 作者 GitHub : by Joey ║\n# ║ YouTube : @joeyblog ║\n# ║ Telegram: https://t.me/+ft-zI76oovgwNmRh ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 致谢 / 引用 ║\n# ║ · 原始脚本基础来自 kejilion(@kejilion) ║\n# ║ · CLIProxyAPI 安装器来自 cliproxyapi-installer ║\n# ║ github.com/brokechubb/cliproxyapi-installer ║\n# ╚══════════════════════════════════════════════════════════════════╝\n#\n: \"${gl_hui:='\\e[37m'}\"\n: \"${gl_hong:='\\033[31m'}\"\n: \"${gl_lv:='\\033[32m'}\"\n: \"${gl_huang:='\\033[33m'}\"\n: \"${gl_lan:='\\033[34m'}\"\n: \"${gl_bai:='\\033[0m'}\"\n: \"${gl_zi:='\\033[35m'}\"\n: \"${gl_kjlan:='\\033[96m'}\"\n\nif ! declare -f break_end > /dev/null 2>&1; then\nbreak_end() {\n\tif command -v gum >/dev/null 2>&1; then\n\t\techo\n\t\tgum style --foreground 240 \" ─────────────────────────────────────── \"\n\t\tgum input --placeholder \"» 按回车继续 «\" --prompt \" \" > /dev/null\n\telse\n\t\techo \" ─── 按任意键继续 ───\"\n\t\tread -n 1 -s -r -p \"\"\n\t\techo\n\tfi\n\tclear\n}\nfi\n\nif ! declare -f install > /dev/null 2>&1; then\ninstall() {\n\tif [[ $# -eq 0 ]]; then\n\t\techo \"未提供软件包参数\"\n\t\treturn 1\n\tfi\n\tfor package in \"$@\"; do\n\t\tif ! command -v \"$package\" &>/dev/null; then\n\t\t\techo -e \"${gl_kjlan}正在安装 $package...${gl_bai}\"\n\t\t\tif [[ \"$(uname -s)\" == \"Darwin\" ]]; then\n\t\t\t\t_ensure_brew &>/dev/null\n\t\t\t\tcommand -v brew &>/dev/null && brew install \"$package\" &>/dev/null\n\t\t\telif command -v dnf &>/dev/null; then\n\t\t\t\tdnf install -y \"$package\" &>/dev/null\n\t\t\telif command -v yum &>/dev/null; then\n\t\t\t\tyum install -y \"$package\" &>/dev/null\n\t\t\telif command -v apt &>/dev/null; then\n\t\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"$package\" &>/dev/null\n\t\t\telif command -v apk &>/dev/null; then\n\t\t\t\tapk add \"$package\" &>/dev/null\n\t\t\telif command -v pacman &>/dev/null; then\n\t\t\t\tpacman -S --noconfirm \"$package\" &>/dev/null\n\t\t\telif command -v zypper &>/dev/null; then\n\t\t\t\tzypper install -y \"$package\" &>/dev/null\n\t\t\telif command -v opkg &>/dev/null; then\n\t\t\t\topkg install \"$package\" &>/dev/null\n\t\t\telif command -v pkg &>/dev/null; then\n\t\t\t\tpkg install -y \"$package\" &>/dev/null\n\t\t\telse\n\t\t\t\techo \"未知的包管理器,无法安装 $package\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\tdone\n}\nfi\n\nis_macos() { [[ \"$(uname -s)\" == \"Darwin\" ]]; }\n\n_ensure_brew() {\n\tif ! command -v brew &>/dev/null; then\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tif ! command -v brew &>/dev/null; then\n\t\techo \"正在安装 Homebrew...\"\n\t\t/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tcommand -v brew &>/dev/null || { echo \"Homebrew 安装失败,请手动安装:https://brew.sh\"; return 1; }\n}\n\n_sed_i() {\n\tif is_macos; then\n\t\tsed -i '' \"$@\"\n\telse\n\t\tsed -i \"$@\"\n\tfi\n}\n\ninstall_base_deps() {\n\tif is_macos; then\n\t\t_ensure_brew || return 1\n\t\tlocal missing_brew=()\n\t\tfor cmd in curl git nano jq python3 gpg; do\n\t\t\tcommand -v \"$cmd\" &>/dev/null || missing_brew+=(\"$cmd\")\n\t\tdone\n\t\tfor i in \"${!missing_brew[@]}\"; do\n\t\t\t[[ \"${missing_brew[$i]}\" == \"gpg\" ]] && missing_brew[$i]=\"gnupg\"\n\t\tdone\n\t\t[[ ${#missing_brew[@]} -eq 0 ]] && return 0\n\t\techo \"正在安装缺失依赖:${missing_brew[*]}...\"\n\t\tbrew install \"${missing_brew[@]}\" &>/dev/null\n\t\treturn 0\n\tfi\n\n\tlocal -a dep_map=(\n\t\t\"curl:curl:curl:curl:curl:curl\"\n\t\t\"git:git:git:git:git:git\"\n\t\t\"nano:nano:nano:nano:nano:nano\"\n\t\t\"jq:jq:jq:jq:jq:jq\"\n\t\t\"python3:python3:python3:python3:python:python3\"\n\t\t\"tar:tar:tar:tar:tar:tar\"\n\t\t\"gpg:gnupg:gnupg2:gnupg:gnupg:gpg2\"\n\t)\n\n\tlocal missing_apt=() missing_dnf=() missing_apk=() missing_pacman=() missing_zypper=()\n\n\tfor entry in \"${dep_map[@]}\"; do\n\t\tIFS=: read -r cmd pkg_apt pkg_dnf pkg_apk pkg_pacman pkg_zypper <<< \"$entry\"\n\t\tcommand -v \"$cmd\" &>/dev/null && continue\n\t\tmissing_apt+=(\"$pkg_apt\")\n\t\tmissing_dnf+=(\"$pkg_dnf\")\n\t\tmissing_apk+=(\"$pkg_apk\")\n\t\tmissing_pacman+=(\"$pkg_pacman\")\n\t\tmissing_zypper+=(\"$pkg_zypper\")\n\tdone\n\n\t[[ ${#missing_apt[@]} -eq 0 ]] && return 0\n\n\techo \"正在安装缺失依赖:${missing_apt[*]}...\"\n\n\tif command -v apt &>/dev/null; then\n\t\tapt update -y &>/dev/null\n\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"${missing_apt[@]}\" &>/dev/null\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y epel-release &>/dev/null || true\n\t\tdnf install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y epel-release &>/dev/null || true\n\t\tyum install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v apk &>/dev/null; then\n\t\tapk update &>/dev/null\n\t\tapk add \"${missing_apk[@]}\" &>/dev/null\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -Sy --noconfirm --needed \"${missing_pacman[@]}\" &>/dev/null\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y \"${missing_zypper[@]}\" &>/dev/null\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\tlocal py\n\t\tpy=$(compgen -c 2>/dev/null | grep -E '^python3\\.[0-9]+$' | sort -V | tail -1)\n\t\tif [[ -n \"$py\" ]]; then\n\t\t\tln -sf \"$(command -v \"$py\")\" /usr/local/bin/python3 2>/dev/null || true\n\t\tfi\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\techo \"正在安装 python3...\"\n\t\tif command -v apt &>/dev/null; then\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y python3 &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tdnf install -y python3 &>/dev/null\n\t\telif command -v yum &>/dev/null; then\n\t\t\tyum install -y python3 &>/dev/null\n\t\telif command -v apk &>/dev/null; then\n\t\t\tapk add python3 &>/dev/null\n\t\telif command -v pacman &>/dev/null; then\n\t\t\tpacman -S --noconfirm python &>/dev/null\n\t\telif command -v zypper &>/dev/null; then\n\t\t\tzypper install -y python3 &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\tfi\n}\n\n_install_gum_binary() {\n\tlocal arch\n\tcase \"$(uname -m)\" in\n\t\tx86_64) arch=\"amd64\" ;;\n\t\taarch64) arch=\"arm64\" ;;\n\t\tarmv7l) arch=\"armv7\" ;;\n\t\t*) echo \"不支持的架构: $(uname -m)\"; return 1 ;;\n\tesac\n\tlocal latest\n\tlatest=$(curl -fsSL https://api.github.com/repos/charmbracelet/gum/releases/latest \\\n\t\t| grep '\"tag_name\"' | sed 's/.*\"v\\([^\"]*\\)\".*/\\1/')\n\t[[ -z \"$latest\" ]] && { echo \"获取 gum 版本失败\"; return 1; }\n\tlocal url=\"https://github.com/charmbracelet/gum/releases/download/v${latest}/gum_${latest}_linux_${arch}.tar.gz\"\n\tlocal tmp\n\ttmp=$(mktemp -d)\n\tcurl -fsSL \"$url\" -o \"$tmp/gum.tar.gz\" \\\n\t\t&& tar -xzf \"$tmp/gum.tar.gz\" -C \"$tmp\" \\\n\t\t&& install -m 755 \"$tmp/gum\" /usr/local/bin/gum\n\trm -rf \"$tmp\"\n\tcommand -v gum >/dev/null 2>&1\n}\n\ninstall_gum() {\n\tcommand -v gum >/dev/null 2>&1 && return 0\n\techo \"正在安装 gum...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install gum\n\telif command -v apt >/dev/null 2>&1; then\n\t\tcommand -v gpg >/dev/null 2>&1 || apt install -y gnupg 2>/dev/null\n\t\tmkdir -p /etc/apt/keyrings\n\t\tif curl -fsSL https://repo.charm.sh/apt/gpg.key \\\n\t\t\t| gpg --dearmor -o /etc/apt/keyrings/charm.gpg 2>/dev/null; then\n\t\t\techo \"deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *\" \\\n\t\t\t\t| tee /etc/apt/sources.list.d/charm.list >/dev/null\n\t\t\tapt update -y 2>/dev/null && apt install -y gum 2>/dev/null\n\t\tfi\n\t\tcommand -v gum >/dev/null 2>&1 || _install_gum_binary\n\telif command -v dnf &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tdnf install -y gum\n\telif command -v yum &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tyum install -y gum\n\telif command -v apk &>/dev/null; then\n\t\tapk add gum 2>/dev/null || _install_gum_binary\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm gum 2>/dev/null || _install_gum_binary\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y gum 2>/dev/null || _install_gum_binary\n\telse\n\t\t_install_gum_binary\n\tfi\n\tcommand -v gum >/dev/null 2>&1 || { echo \"gum 安装失败,请手动安装: https://github.com/charmbracelet/gum\"; return 1; }\n}\n\ninstall_fzf() {\n\tcommand -v fzf >/dev/null 2>&1 && return 0\n\techo \"正在安装 fzf...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install fzf\n\telif command -v apt >/dev/null 2>&1; then\n\t\tapt install -y fzf\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y fzf\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y fzf\n\telif command -v apk &>/dev/null; then\n\t\tapk add fzf\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm fzf\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y fzf\n\telse\n\t\techo \"无法自动安装 fzf,请手动安装: https://github.com/junegunn/fzf\"\n\t\treturn 1\n\tfi\n}\n\n_install_shortcut() {\n\tlocal store_dir shortcut_dir shortcut\n\n\tstore_dir=\"$HOME/.local/bin\"\n\tmkdir -p \"$store_dir\"\n\n\tlocal _candidate_dirs=(\"/opt/homebrew/bin\" \"/usr/local/bin\")\n\tshortcut_dir=\"\"\n\tfor _d in \"${_candidate_dirs[@]}\"; do\n\t\tif [[ -d \"$_d\" && -w \"$_d\" ]]; then\n\t\t\tshortcut_dir=\"$_d\"\n\t\t\tbreak\n\t\tfi\n\tdone\n\n\tif [[ -z \"$shortcut_dir\" ]]; then\n\t\tshortcut_dir=\"$store_dir\"\n\t\tlocal rc_file=\"\"\n\t\t[[ -f \"$HOME/.zshrc\" ]] && rc_file=\"$HOME/.zshrc\"\n\t\t[[ -f \"$HOME/.bashrc\" ]] && rc_file=\"${rc_file:-$HOME/.bashrc}\"\n\t\tif [[ -n \"$rc_file\" ]] && ! grep -q \"$store_dir\" \"$rc_file\" 2>/dev/null; then\n\t\t\tprintf '\\nexport PATH=\"%s:$PATH\"\\n' \"$store_dir\" >> \"$rc_file\"\n\t\tfi\n\t\texport PATH=\"$store_dir:$PATH\"\n\tfi\n\n\tshortcut=\"$shortcut_dir/oc\"\n\n\tcat > \"$shortcut\" <<EOF\n#!/usr/bin/env bash\ncurl -fsSL https://raw.githubusercontent.com/byJoey/openclawctl/main/openclaw.sh \\\\\n -o \"$store_dir/openclawctl.sh\" 2>/dev/null && chmod +x \"$store_dir/openclawctl.sh\"\nexec bash \"$store_dir/openclawctl.sh\" \"\\$@\"\nEOF\n\tchmod +x \"$shortcut\"\n}\n\nmoltbot_menu() {\n\tis_macos && _ensure_brew\n\n\t_install_shortcut\n\n\tinstall_base_deps\n\tinstall_gum || { echo \"gum 安装失败,无法继续\"; return 1; }\n\tinstall_fzf || { echo \"fzf 安装失败,无法继续\"; return 1; }\n\n\tui_header() {\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"0 2\" \"$*\"\n\t\techo\n\t}\n\tui_ok() { gum style --foreground 46 \" ◉ $*\"; }\n\tui_err() { gum style --foreground 196 \" ✗ $*\"; }\n\tui_warn() { gum style --foreground 208 \" ⚡ $*\"; }\n\tui_info() { gum style --foreground 51 \" ◈ $*\"; }\n\tui_step() { echo; gum style --bold --foreground 201 \" ▶ $*\"; echo; }\n\n\tcheck_openclaw_update() {\n\t\tif ! command -v npm >/dev/null 2>&1; then\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal local_version remote_version\n\t\tlocal_version=$(npm list -g openclaw --depth=0 --no-update-notifier 2>/dev/null \\\n\t\t\t| grep openclaw | awk '{print $NF}' | sed 's/^.*@//')\n\n\t\t[[ -z \"$local_version\" ]] && return 1\n\n\t\tremote_version=$(npm view openclaw version --no-update-notifier 2>/dev/null)\n\n\t\t[[ -z \"$remote_version\" ]] && return 1\n\n\t\tif [[ \"$local_version\" != \"$remote_version\" ]]; then\n\t\t\techo -e \"\\033[38;5;208m⚡ UPDATE AVAILABLE $remote_version\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m✦ v$local_version\\033[0m\"\n\t\tfi\n\t}\n\n\tget_install_status() {\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m◉ INSTALLED\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m○ NOT FOUND\\033[0m\"\n\t\tfi\n\t}\n\n\tget_running_status() {\n\t\tif pgrep -f \"openclaw.*gatewa\" >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m■ STOPPED\\033[0m\"\n\t\tfi\n\t}\n\n\tstart_gateway() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tlaunchctl unload \"$plist\" >/dev/null 2>&1 || true\n\t\t\tif [[ -f \"$plist\" ]]; then\n\t\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- \\\n\t\t\t\t\tbash -c \"launchctl load '$plist' 2>/dev/null; sleep 2\"\n\t\t\telse\n\t\t\t\tgum spin --spinner pulse --title \"正在安装并启动网关...\" -- \\\n\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\telse\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- openclaw gateway start\n\t\t\tsleep 1\n\t\tfi\n\t}\n\n\tinstall_node_and_tools() {\n\t\tif command -v node &>/dev/null && command -v npm &>/dev/null; then\n\t\t\treturn 0\n\t\tfi\n\t\techo \"正在安装 Node.js...\"\n\t\tif is_macos; then\n\t\t\t_ensure_brew || return 1\n\t\t\tbrew install node &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tcurl -fsSL https://rpm.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tdnf install -y cmake libatomic nodejs &>/dev/null\n\t\telif command -v apt &>/dev/null; then\n\t\t\tcurl -fsSL https://deb.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y build-essential python3 libatomic1 nodejs &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t}\n\n\tconfigure_openclaw_session_policy() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 1\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport json, sys\npath = sys.argv[1]\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nsession = obj.setdefault('session', {})\nsession['dmScope'] = session.get('dmScope', 'per-channel-peer')\nsession['resetTriggers'] = ['/new', '/reset']\nsession['reset'] = {\n 'mode': 'idle',\n 'idleMinutes': 10080\n}\nsession['resetByType'] = {\n 'direct': {'mode': 'idle', 'idleMinutes': 10080},\n 'thread': {'mode': 'idle', 'idleMinutes': 1440},\n 'group': {'mode': 'idle', 'idleMinutes': 120}\n}\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(obj, f, ensure_ascii=False, indent=2)\n f.write('\\n')\nPY\n\t}\n\n\tsync_openclaw_api_models() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 0\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('未检测到 API providers,跳过模型同步')\n raise SystemExit(0)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nchanged = False\nfatal_errors = []\nsummary = []\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\ndef prompt_delete_provider(name):\n prompt = f\"{name} /models 探测连续失败 3 次。是否删除该 API 供应商及其全部相关模型?[y/N]: \"\n try:\n ans = input(prompt).strip().lower()\n except EOFError:\n return False\n return ans in ('y', 'yes')\n\n\ndef rebind_defaults_before_delete(name):\n global changed\n\n replacement = None\n\n def get_replacement():\n nonlocal replacement\n if replacement is None:\n candidates = collect_available_refs(exclude_provider=name)\n replacement = candidates[0] if candidates else None\n return replacement\n\n primary_ref = get_primary_ref(defaults)\n if ref_provider(primary_ref) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: 默认主模型指向该 provider,但无可用替代模型,已中止删除')\n return False\n set_primary_ref(defaults, repl)\n changed = True\n summary.append(f'已切换默认主模型: {primary_ref} -> {repl}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: {fk} 指向该 provider,但无可用替代模型,已中止删除')\n return False\n defaults[fk] = repl\n changed = True\n summary.append(f'已切换 {fk}: {val} -> {repl}')\n\n return True\n\n\ndef delete_provider_and_refs(name):\n global changed\n\n if not rebind_defaults_before_delete(name):\n return False\n\n removed_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\n for r in removed_refs:\n defaults_models.pop(r, None)\n if removed_refs:\n changed = True\n\n if name in providers:\n providers.pop(name, None)\n changed = True\n\n summary.append(f'已删除 provider {name},并移除 defaults.models 下 {len(removed_refs)} 个模型引用')\n return True\n\n\ndef fetch_remote_models_with_retry(name, base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n data = json.loads(payload)\n return data, None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\nfor name, provider in list(providers.items()):\n if not isinstance(provider, dict):\n summary.append(f'跳过 {name}: provider 结构非法')\n continue\n\n api = provider.get('api', '')\n base_url = provider.get('baseUrl')\n api_key = provider.get('apiKey')\n model_list = provider.get('models', [])\n\n if not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n summary.append(f'跳过 {name}: 无 baseUrl/apiKey/models')\n continue\n\n if api not in SUPPORTED_APIS:\n summary.append(f'跳过 {name}: 不支持直接 /models 校验 (api={api})')\n continue\n\n data, err, attempts = fetch_remote_models_with_retry(name, base_url, api_key, retries=3)\n if err is not None:\n summary.append(f'警告 - {name}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n if prompt_delete_provider(name):\n deleted = delete_provider_and_refs(name)\n if deleted:\n summary.append(f'{name}: 用户已确认删除该 provider 及全部相关模型引用')\n else:\n summary.append(f'{name}: 用户未确认删除,保留现有 provider 配置')\n continue\n\n if attempts > 1:\n summary.append(f'{name}: /models 第 {attempts} 次重试后成功')\n\n if not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n summary.append(f'警告 - 跳过 {name}: /models 返回结构不可识别')\n continue\n\n remote_ids = []\n for item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\n remote_set = set(remote_ids)\n\n if not remote_set:\n fatal_errors.append(f'错误 - {name} 上游 /models 为空,无法为该 provider 提供兜底模型')\n continue\n\n local_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\n local_ids = [str(m['id']) for m in local_models]\n local_set = set(local_ids)\n\n template = None\n for m in local_models:\n template = copy.deepcopy(m)\n break\n if template is None:\n summary.append(f'警告 - 跳过 {name}: 本地 models 无有效模板模型')\n continue\n\n removed_ids = [mid for mid in local_ids if mid not in remote_set]\n added_ids = [mid for mid in remote_ids if mid not in local_set]\n\n kept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\n new_models = kept_models[:]\n\n for mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{name} / {mid}'\n new_models.append(nm)\n\n if not new_models:\n fatal_errors.append(f'错误 - {name} 同步后无可用模型,无法保障默认模型/回退模型兜底')\n continue\n\n expected_refs = {model_ref(name, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\n local_refs = {model_ref(name, mid) for mid in local_ids}\n\n first_ref = model_ref(name, str(new_models[0]['id']))\n\n primary_ref = get_primary_ref(defaults)\n if isinstance(primary_ref, str) and primary_ref in (local_refs - expected_refs):\n set_primary_ref(defaults, first_ref)\n changed = True\n summary.append(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in (local_refs - expected_refs):\n defaults[fk] = first_ref\n changed = True\n summary.append(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\n stale_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/') and r not in expected_refs]\n for r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\n for r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\n if removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\n summary.append(f'{name}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\n\nif fatal_errors:\n for line in summary:\n print(line)\n for err in fatal_errors:\n print(err)\n print('模型同步失败:存在 provider 同步后无可用模型,已中止写入')\n raise SystemExit(2)\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n for line in summary:\n print(line)\n print('OpenClaw API 模型一致性同步完成')\nelse:\n for line in summary:\n print(line)\n print('无需同步:配置已与上游 /models 保持一致')\nPY\n\t}\n\n\t_install_openclaw_core() {\n\t\tinstall_node_and_tools\n\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf ssh://git@github.com/\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf git@github.com:\n\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_prefix npm_bin\n\t\tnpm_prefix=$(npm prefix -g 2>/dev/null)\n\t\tnpm_bin=\"${npm_prefix}/bin\"\n\t\tif [[ -n \"$npm_bin\" ]]; then\n\t\t\texport PATH=\"$npm_bin:$PATH\"\n\t\t\tfor rc in \"$HOME/.bashrc\" \"$HOME/.zshrc\" \"$HOME/.profile\"; do\n\t\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t\tgrep -qF \"$npm_bin\" \"$rc\" 2>/dev/null && continue\n\t\t\t\techo \"export PATH=\\\"${npm_bin}:\\$PATH\\\"\" >> \"$rc\"\n\t\t\tdone\n\t\tfi\n\t\topenclaw onboard --install-daemon\n\t\t_sed_i 's|\"profile\": \"messaging\"|\"profile\": \"full\"|g' ~/.openclaw/openclaw.json\n\t\tconfigure_openclaw_session_policy\n\t\tif ! is_macos; then\n\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tstart_gateway\n\t}\n\n\tinstall_moltbot() {\n\t\t_install_openclaw_core\n\t\tbreak_end\n\t}\n\n\t_cliproxy_dir() {\n\t\tif is_macos; then\n\t\t\techo \"$(brew --prefix 2>/dev/null || echo /usr/local)/etc\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi\"\n\t\tfi\n\t}\n\n\t_cliproxy_conf() {\n\t\tif is_macos; then\n\t\t\techo \"$(_cliproxy_dir)/cliproxyapi.conf\"\n\t\telse\n\t\t\techo \"$(_cliproxy_dir)/config.yaml\"\n\t\tfi\n\t}\n\n\t_cliproxy_bin() {\n\t\tif is_macos; then\n\t\t\tcommand -v cliproxyapi 2>/dev/null || echo \"cliproxyapi\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi/cli-proxy-api\"\n\t\tfi\n\t}\n\n\t_cliproxy_running() {\n\t\tif is_macos; then\n\t\t\tpgrep -f \"cliproxyapi\" >/dev/null 2>&1\n\t\telse\n\t\t\tpgrep -f \"cli-proxy-api\" >/dev/null 2>&1\n\t\tfi\n\t}\n\n\t_cliproxy_start_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services start cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tsystemctl --user enable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"systemctl --user start cliproxyapi.service 2>/dev/null; sleep 3\"\n\t\t\tif ! _cliproxy_running; then\n\t\t\t\tui_warn \"systemd 启动失败,改用后台直接运行...\"\n\t\t\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\t\t\t(cd \"$cliproxy_dir\" && nohup ./cli-proxy-api > /tmp/cliproxyapi.log 2>&1 &)\n\t\t\t\tsleep 3\n\t\t\tfi\n\t\tfi\n\t\tif _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已启动\"\n\t\t\treturn 0\n\t\telse\n\t\t\tui_err \"CLIProxyAPI 启动失败,请检查 $(_cliproxy_conf) 后手动启动\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\t_cliproxy_stop_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services stop cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbash -c \"systemctl --user stop cliproxyapi.service 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\tfi\n\t\tlocal _pat; _pat=$(is_macos && echo \"cliproxyapi\" || echo \"cli-proxy-api\")\n\t\tlocal pids; pids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\tif [[ -n \"$pids\" ]]; then\n\t\t\techo \"$pids\" | xargs kill 2>/dev/null || true\n\t\t\tsleep 2\n\t\t\tpids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\t\t[[ -n \"$pids\" ]] && echo \"$pids\" | xargs kill -9 2>/dev/null || true\n\t\tfi\n\t\tif ! _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已停止\"\n\t\telse\n\t\t\tui_err \"停止失败,仍有进程残留\"\n\t\tfi\n\t}\n\n\t_cliproxy_oauth_login() {\n\t\tlocal cliproxy_bin; cliproxy_bin=$(_cliproxy_bin)\n\t\tif ! command -v \"$cliproxy_bin\" &>/dev/null && [[ ! -x \"$cliproxy_bin\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 未安装:$cliproxy_bin\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_choice\n\t\tprovider_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' 选择要登录的 AI 提供商\\n ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"Claude (Anthropic)\" \\\n\t\t\t\"Gemini (Google)\" \\\n\t\t\t\"OpenAI / Codex\" \\\n\t\t\t\"Qwen (通义千问)\" \\\n\t\t\t\"iFlow\" \\\n\t\t\t\"取消\") || return 0\n\n\t\tlocal login_cmd=\"\" login_port=\"\"\n\t\tcase \"$provider_choice\" in\n\t\t\t\"Claude (Anthropic)\") login_cmd=\"--claude-login\"; login_port=\"54545\" ;;\n\t\t\t\"Gemini (Google)\") login_cmd=\"--login\"; login_port=\"8085\" ;;\n\t\t\t\"OpenAI / Codex\") login_cmd=\"--codex-login\"; login_port=\"1455\" ;;\n\t\t\t\"Qwen (通义千问)\") login_cmd=\"--qwen-login\"; login_port=\"\" ;;\n\t\t\t\"iFlow\") login_cmd=\"--iflow-login\"; login_port=\"11451\" ;;\n\t\t\t\"取消\"|*) return 0 ;;\n\t\tesac\n\n\t\techo\n\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\n\t\tlocal mode_input=\"\" proj_input=\"\"\n\t\tif [[ \"$login_cmd\" == \"--login\" ]]; then\n\t\t\tlocal mode_choice\n\t\t\tmode_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择 Gemini 登录模式\\n ↑↓ 移动 · Enter 确认' \\\n\t\t\t\t\"Code Assist(GCP 项目,手动选择)\" \\\n\t\t\t\t\"Google One(个人账号,自动发现项目)\") || return 0\n\t\t\t[[ \"$mode_choice\" == \"Code Assist\"* ]] && mode_input=\"1\" || mode_input=\"2\"\n\n\t\t\tif [[ \"$mode_input\" == \"1\" ]]; then\n\t\t\t\techo\n\t\t\t\tlocal proj_list proj_choice proj_num=\"1\"\n\t\t\t\tif command -v gcloud &>/dev/null; then\n\t\t\t\t\tproj_list=$(gcloud projects list --format=\"value(projectId,name)\" 2>/dev/null)\n\t\t\t\tfi\n\t\t\t\tif [[ -n \"$proj_list\" ]]; then\n\t\t\t\t\tproj_choice=$(printf \"%s\\n\" \"$proj_list\" | \\\n\t\t\t\t\t\tgum choose --cursor \"❯ \" \\\n\t\t\t\t\t\t--header $' 选择 GCP 项目\\n ↑↓ 移动 · Enter 确认') || return 0\n\t\t\t\t\tproj_num=$(printf \"%s\\n\" \"$proj_list\" | \\\n\t\t\t\t\t\tawk -v sel=\"$proj_choice\" 'NR==sel_line || $0==sel {print NR; exit}' \\\n\t\t\t\t\t\tsel=\"$proj_choice\" 2>/dev/null || echo \"1\")\n\t\t\t\t\t[[ -z \"$proj_num\" || \"$proj_num\" == \"0\" ]] && proj_num=\"1\"\n\t\t\t\telse\n\t\t\t\t\tlocal proj_raw\n\t\t\t\t\tproj_raw=$(gum input \\\n\t\t\t\t\t\t--placeholder \"项目编号或直接按 Enter 使用默认(1)\" \\\n\t\t\t\t\t\t--prompt \" ❯ \" --width 60) || return 0\n\t\t\t\t\tproj_num=\"${proj_raw:-1}\"\n\t\t\t\tfi\n\t\t\t\tproj_input=\"$proj_num\"\n\t\t\tfi\n\t\tfi\n\n\t\tif [[ -n \"$login_port\" ]]; then\n\t\t\tif is_macos; then\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 本地浏览器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 浏览器将自动打开,完成授权后自动返回,请稍候...\"\n\t\t\t\techo\n\t\t\t\tif [[ -n \"$mode_input\" ]]; then\n\t\t\t\t\tprintf \"%s\\n%s\\n\" \"$mode_input\" \"$proj_input\" | \"$cliproxy_bin\" \"$login_cmd\"\n\t\t\t\telse\n\t\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\t\tfi\n\t\t\telse\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 服务器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 1. 脚本后台启动 cli-proxy-api,并打印 OAuth 授权链接\" \\\n\t\t\t\t\t\" 2. 在本地浏览器打开该链接,完成账号授权\" \\\n\t\t\t\t\t\" 3. 授权后浏览器跳转到 localhost:${login_port},页面报错属正常\" \\\n\t\t\t\t\t\" 4. 复制地址栏完整 URL,粘贴到下方提示符,脚本自动完成握手\"\n\t\t\t\techo\n\t\t\t\tgum style --foreground 208 \\\n\t\t\t\t\t\" ⚡ 无需端口转发 — 回调 URL 里已含 code,直接转发给本机处理\"\n\t\t\t\techo\n\n\t\t\t\tif [[ -n \"$mode_input\" ]]; then\n\t\t\t\t\tlocal fifo; fifo=$(mktemp -u /tmp/cp_login_XXXXXX)\n\t\t\t\t\tmkfifo \"$fifo\"\n\t\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser < \"$fifo\") &\n\t\t\t\t\tlocal cli_pid=$!\n\t\t\t\t\texec 9>\"$fifo\"\n\t\t\t\t\tprintf \"%s\\n\" \"$mode_input\" >&9\n\t\t\t\t\tsleep 0.3\n\t\t\t\t\t[[ -n \"$proj_input\" ]] && { printf \"%s\\n\" \"$proj_input\" >&9; sleep 0.3; }\n\t\t\t\t\texec 9>&-\n\t\t\t\t\trm -f \"$fifo\"\n\t\t\t\telse\n\t\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser) &\n\t\t\t\t\tlocal cli_pid=$!\n\t\t\t\tfi\n\t\t\t\tsleep 2\n\n\t\t\t\techo\n\t\t\t\tlocal callback_url\n\t\t\t\tcallback_url=$(gum input \\\n\t\t\t\t\t--placeholder \"http://localhost:${login_port}/oauth2callback?code=...\" \\\n\t\t\t\t\t--prompt \" ❯ \" \\\n\t\t\t\t\t--width 120 \\\n\t\t\t\t\t--char-limit 2000) || true\n\n\t\t\t\tif [[ -n \"$callback_url\" ]]; then\n\t\t\t\t\tui_info \"正在向本机发送回调...\"\n\t\t\t\t\tcurl -sf \"$callback_url\" > /dev/null 2>&1 || true\n\t\t\t\t\tsleep 1\n\t\t\t\tfi\n\t\t\t\twait \"$cli_pid\" 2>/dev/null || true\n\t\t\tfi\n\t\telse\n\t\t\tui_info \"Qwen 使用 device flow,无需粘贴回调\"\n\t\t\techo\n\t\t\tgum style --foreground 240 \"将打印授权码 + URL,在任意浏览器中打开并输入授权码即可\"\n\t\t\techo\n\t\t\tif is_macos; then\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser)\n\t\t\tfi\n\t\tfi\n\t}\n\n\tcliproxyapi_manage_menu() {\n\t\tlocal config_file; config_file=$(_cliproxy_conf)\n\n\t\twhile true; do\n\t\t\tclear\n\n\t\t\tlocal cp_installed cp_running cp_version cp_port cp_keys\n\t\t\tif is_macos; then\n\t\t\t\tcp_version=$(brew info cliproxyapi 2>/dev/null | awk 'NR==1{print $4}')\n\t\t\telse\n\t\t\t\tcp_version=$(cat \"$(_cliproxy_dir)/version.txt\" 2>/dev/null || echo \"\")\n\t\t\tfi\n\t\t\tif [[ -n \"$cp_version\" ]]; then\n\t\t\t\tcp_installed=\"\\033[38;5;46m◉ INSTALLED v${cp_version}\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_installed=\"\\033[90m○ NOT INSTALLED\\033[0m\"\n\t\t\tfi\n\t\t\tif _cliproxy_running; then\n\t\t\t\tcp_running=\"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_running=\"\\033[90m■ STOPPED\\033[0m\"\n\t\t\tfi\n\t\t\tcp_port=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\t\t\tcp_port=\"${cp_port:-8317}\"\n\t\t\tcp_keys=$(awk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{n++} END{print n+0}' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 51 \\\n\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t--padding \"1 4\" --align center \\\n\t\t\t\t\"C L I P R O X Y A P I\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ AI OAuth Proxy Manager ]\"\n\t\t\techo -e \" $cp_installed $cp_running\"\n\t\t\techo -e \" \\033[90m端口 :${cp_port} API Keys: ${cp_keys}\\033[0m\"\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' ─── CLIProxyAPI ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"启动\" \\\n\t\t\t\t\"停止\" \\\n\t\t\t\t\"重启\" \\\n\t\t\t\t\"查看日志\" \\\n\t\t\t\t\"账号授权登录\" \\\n\t\t\t\t\"生成并添加 API Key\" \\\n\t\t\t\t\"查看 API Keys\" \\\n\t\t\t\t\"编辑配置文件\" \\\n\t\t\t\t\"更新\" \\\n\t\t\t\t\"卸载\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"启动\")\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"停止\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"重启\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tsleep 1\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看日志\")\n\t\t\t\t\techo\n\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\tlocal brew_log\n\t\t\t\t\t\tbrew_log=\"$(brew --prefix 2>/dev/null)/var/log/cliproxyapi.log\"\n\t\t\t\t\t\tif [[ -f \"$brew_log\" ]]; then\n\t\t\t\t\t\t\ttail -80 \"$brew_log\"\n\t\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\telif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\t\t\tjournalctl --user -u cliproxyapi.service --no-pager -n 80\n\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"账号授权登录\")\n\t\t\t\t\t_cliproxy_oauth_login\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"生成并添加 API Key\")\n\t\t\t\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\t\tlocal new_key\n\t\t\t\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\t\t\t\techo\n\t\t\t\t\tgum style --bold --foreground 46 \" ◉ 已生成新 API Key:\"\n\t\t\t\t\tgum style --bold --foreground 51 \" $new_key\"\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 将此 Key 自动写入 config.yaml?\"; then\n\t\t\t\t\t\tpython3 - \"$config_file\" \"$new_key\" <<'PY'\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n)', r'\\1 - \"' + key + '\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nprint(\"写入成功\")\nPY\n\t\t\t\t\t\tui_ok \"API Key 已写入配置\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看 API Keys\")\n\t\t\t\t\techo\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tgum style --foreground 240 \" config.yaml 中的 API Keys:\"\n\t\t\t\t\t\techo\n\t\t\t\t\t\tawk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{print \" \" $0}' \\\n\t\t\t\t\t\t\t\"$config_file\"\n\t\t\t\t\t\techo\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到配置文件:$config_file\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"编辑配置文件\")\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tnano \"$config_file\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\tfi\n\t\t\t\t\t;;\n\t\t\t\t\"更新\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 确认更新 CLIProxyAPI 到最新版本?\"; then\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew upgrade cliproxyapi && ui_ok \"更新完成\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tcurl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer \\\n\t\t\t\t\t\t\t\t| bash -s -- upgrade\n\t\t\t\t\t\t\tui_ok \"更新完成\"\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"卸载\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" ⚡ 确认卸载 CLIProxyAPI?\"; then\n\t\t\t\t\t\t_cliproxy_stop_service 2>/dev/null || true\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew uninstall cliproxyapi 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$HOME/.cli-proxy-api\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tsystemctl --user disable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\t\t\t\t\trm -f \"$HOME/.config/systemd/user/cliproxyapi.service\" 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$(_cliproxy_dir)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\t\tui_ok \"CLIProxyAPI 已卸载\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tbeginner_mode_install() {\n\t\tif is_macos; then\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ API Key 配置 ]\"\n\t\telse\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ CLIProxyAPI ⟶ AI Auth ⟶ Auto Config ]\"\n\t\tfi\n\t\techo\n\n\t\tui_step \"第 1 步:安装 OpenClaw\"\n\t\t_install_openclaw_core\n\n\t\tui_step \"第 2 步:安装 CLIProxyAPI\"\n\t\tif is_macos; then\n\t\t\tif ! gum spin --spinner globe --title \"正在安装 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbrew install cliproxyapi; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败(brew install cliproxyapi)\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\telse\n\t\t\tif ! curl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer | bash; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\n\t\tlocal cliproxy_config; cliproxy_config=$(_cliproxy_conf)\n\t\tif [[ ! -f \"$cliproxy_config\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 安装后未找到配置文件: $cliproxy_config\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\t\tui_ok \"CLIProxyAPI 安装完成\"\n\n\t\tui_step \"第 3 步:登录 AI 账号\"\n\t\tui_info \"通过 OAuth 授权即可免费使用,无需付费 API Key\"\n\t\techo\n\t\t_cliproxy_oauth_login\n\n\t\tui_step \"第 4 步:生成 API Key 并启动服务\"\n\t\tlocal new_key\n\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\tpython3 - \"$cliproxy_config\" \"$new_key\" <<'PY' 2>/dev/null\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n(?:\\s*-\\s*\"[^\"]*\"\\s*\\n)*)', lambda m: m.group(0) + f' - \"{key}\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nPY\n\t\tif ! _cliproxy_start_service; then\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_step \"第 5 步:配置 OpenClaw API\"\n\n\t\tlocal api_key port base_url\n\t\tapi_key=$(awk '\n\t\t\t/^api-keys:/ { in_keys=1; next }\n\t\t\tin_keys && /^[^ \\t]/ { exit }\n\t\t\tin_keys && /\"sk-/ { match($0, /sk-[^\"]+/); print substr($0, RSTART, RLENGTH); exit }\n\t\t' \"$cliproxy_config\" 2>/dev/null)\n\n\t\tport=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \"$cliproxy_config\" 2>/dev/null)\n\t\tport=\"${port:-8317}\"\n\t\tbase_url=\"http://localhost:${port}/v1\"\n\n\t\tif [[ -z \"$api_key\" ]]; then\n\t\t\tui_err \"无法从 $cliproxy_config 读取 API Key,请手动在「CLIProxyAPI 管理」中添加\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_info \"地址:$base_url\"\n\t\tui_info \"Key:${api_key:0:12}****\"\n\t\techo\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"cliproxy\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tstart_gateway\n\n\t\t\tui_step \"第 6 步:选择默认模型\"\n\t\t\tui_info \"请从可用模型中选择一个作为默认,选完后网关自动重启\"\n\t\t\techo\n\t\t\tchange_model\n\n\t\t\techo\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 46 \\\n\t\t\t\t--border double --border-foreground 46 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◉ INSTALL COMPLETE ◉\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw 已自动接入 CLIProxyAPI,可直接开始使用 ]\" \\\n\t\t\t\t\"[ 更多提供商:cd ~/cliproxyapi && ./cli-proxy-api --help ]\"\n\t\tfi\n\n\t\techo\n\t\tif gum confirm \" 现在去对接机器人?\"; then\n\t\t\tchange_tg_bot_code\n\t\telse\n\t\t\tbreak_end\n\t\tfi\n\t}\n\n\tstart_bot() {\n\t\tstart_gateway\n\t\tbreak_end\n\t}\n\n\tstop_bot() {\n\t\ttmux kill-session -t gateway > /dev/null 2>&1\n\t\tgum spin --spinner pulse --title \"正在停止网关...\" -- openclaw gateway stop\n\t\tbreak_end\n\t}\n\n\tview_logs() {\n\t\topenclaw status\n\t\topenclaw gateway status\n\t\topenclaw logs\n\t\tbreak_end\n\t}\n\n\tadd-all-models-from-provider() {\n\t\tlocal provider_name=\"$1\"\n\t\tlocal base_url=\"$2\"\n\t\tlocal api_key=\"$3\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tlocal models_tmpfile\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取 $provider_name 的模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\n\t\tlocal models_json\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tif [[ -z \"$models_json\" ]]; then\n\t\t\techo \"错误:无法获取模型列表\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_ids\n\t\tmodel_ids=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n for m in models:\n if isinstance(m, dict) and \"id\" in m:\n print(m[\"id\"])\nexcept Exception:\n pass\n')\n\n\t\tif [[ -z \"$model_ids\" ]]; then\n\t\t\techo \"错误:未找到任何模型\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_count\n\t\tmodel_count=$(echo \"$model_ids\" | wc -l | tr -d ' ')\n\t\techo \"发现 $model_count 个模型\"\n\n\t\tlocal models_array=\"[\"\n\t\tlocal first=true\n\n\t\twhile read -r model_id; do\n\t\t\t[[ $first == false ]] && models_array+=\",\"\n\t\t\tfirst=false\n\n\t\t\tlocal context_window=1048576\n\t\t\tlocal max_tokens=128000\n\t\t\tlocal input_cost=0.15\n\t\t\tlocal output_cost=0.60\n\n\t\t\tcase \"$model_id\" in\n\t\t\t\t*opus*|*pro*|*preview*|*thinking*|*sonnet*)\n\t\t\t\t\tinput_cost=2.00\n\t\t\t\t\toutput_cost=12.00\n\t\t\t\t\t;;\n\t\t\t\t*gpt-5*|*codex*)\n\t\t\t\t\tinput_cost=1.25\n\t\t\t\t\toutput_cost=10.00\n\t\t\t\t\t;;\n\t\t\t\t*flash*|*lite*|*haiku*|*mini*|*nano*)\n\t\t\t\t\tinput_cost=0.10\n\t\t\t\t\toutput_cost=0.40\n\t\t\t\t\t;;\n\t\t\tesac\n\n\t\t\tmodels_array+=$(cat <<EOF\n{\n\t\"id\": \"$model_id\",\n\t\"name\": \"$provider_name / $model_id\",\n\t\"input\": [\"text\", \"image\"],\n\t\"contextWindow\": $context_window,\n\t\"maxTokens\": $max_tokens,\n\t\"cost\": {\n\t\t\"input\": $input_cost,\n\t\t\"output\": $output_cost,\n\t\t\"cacheRead\": 0,\n\t\t\"cacheWrite\": 0\n\t}\n}\nEOF\n)\n\t\tdone <<< \"$model_ids\"\n\n\t\tmodels_array+=\"]\"\n\n\t\t[[ -f \"$config_file\" ]] && cp \"$config_file\" \"${config_file}.bak.$(date +%s)\"\n\n\t\tjq --arg prov \"$provider_name\" \\\n\t\t --arg url \"$base_url\" \\\n\t\t --arg key \"$api_key\" \\\n\t\t --argjson models \"$models_array\" \\\n\t\t'\n\t\t.models |= (\n\t\t\t(. // { mode: \"merge\", providers: {} })\n\t\t\t| .mode = \"merge\"\n\t\t\t| .providers[$prov] = {\n\t\t\t\tbaseUrl: $url,\n\t\t\t\tapiKey: $key,\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tmodels: $models\n\t\t\t}\n\t\t)\n\t\t| .agents |= (. // {})\n\t\t| .agents.defaults |= (. // {})\n\t\t| .agents.defaults.models |= (\n\t\t\t(if type == \"object\" then .\n\t\t\t elif type == \"array\" then reduce .[] as $m ({}; if ($m|type) == \"string\" then .[$m] = {} else . end)\n\t\t\t else {}\n\t\t\t end) as $existing\n\t\t\t| reduce ($models[]? | .id? // empty | tostring) as $mid (\n\t\t\t\t$existing;\n\t\t\t\tif ($mid | length) > 0 then\n\t\t\t\t\t.[\"\\($prov)/\\($mid)\"] //= {}\n\t\t\t\telse\n\t\t\t\t\t.\n\t\t\t\tend\n\t\t\t)\n\t\t)\n\t\t' \"$config_file\" > \"${config_file}.tmp\" && mv \"${config_file}.tmp\" \"$config_file\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\techo \"成功添加 $model_count 个模型到 $provider_name\"\n\t\t\techo \"模型引用格式: $provider_name/<model-id>\"\n\t\t\treturn 0\n\t\telse\n\t\t\techo \"错误:配置注入失败\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\tadd-openclaw-provider-interactive() {\n\t\tlocal provider_name base_url api_key\n\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"Provider 名称 (如: deepseek)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\t[[ -z \"$provider_name\" ]] && return 1\n\n\t\tbase_url=$(gum input \\\n\t\t\t--placeholder \"Base URL (如: https://api.xxx.com/v1)\" \\\n\t\t\t--prompt \"Base URL > \")\n\t\t[[ -z \"$base_url\" ]] && return 1\n\t\tbase_url=\"${base_url%/}\"\n\n\t\tapi_key=$(gum input --password \\\n\t\t\t--placeholder \"API Key\" \\\n\t\t\t--prompt \"API Key > \")\n\t\t[[ -z \"$api_key\" ]] && return 1\n\n\t\tlocal models_tmpfile models_json available_models model_count\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tlocal default_model=\"\"\n\t\tif [[ -n \"$models_json\" ]]; then\n\t\t\tavailable_models=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n ids = sorted(m[\"id\"] for m in models if isinstance(m, dict) and \"id\" in m)\n print(\"\\n\".join(ids))\nexcept Exception:\n pass\n')\n\t\t\tmodel_count=$(echo \"$available_models\" | wc -l | tr -d ' ')\n\t\tfi\n\n\t\tif [[ -n \"$available_models\" ]]; then\n\t\t\tdefault_model=$(echo \"$available_models\" | fzf \\\n\t\t\t\t--prompt=\" ❯ \" \\\n\t\t\t\t--header=\" 发现 $model_count 个模型 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 选第一个\" \\\n\t\t\t\t--header-first \\\n\t\t\t\t--height=15 \\\n\t\t\t\t--layout=reverse \\\n\t\t\t\t--border=double \\\n\t\t\t\t--border-label=\" ◈ DEFAULT MODEL \" \\\n\t\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || default_model=$(echo \"$available_models\" | head -1)\n\t\t\t[[ -z \"$default_model\" ]] && default_model=$(echo \"$available_models\" | head -1)\n\t\t\tdefault_model=$(awk '{print $1}' <<< \"$default_model\")\n\t\tfi\n\n\t\techo\n\t\tgum style \\\n\t\t\t--border normal --border-foreground 99 \\\n\t\t\t--padding \"0 2\" \\\n\t\t\t\"Provider : $provider_name\" \\\n\t\t\t\"Base URL : $base_url\" \\\n\t\t\t\"API Key : ${api_key:0:8}****\" \\\n\t\t\t\"默认模型 : $default_model\" \\\n\t\t\t\"模型总数 : $model_count\"\n\t\techo\n\n\t\tgum confirm \"确认添加所有 $model_count 个模型?\" || { echo \"已取消\"; return 1; }\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"$provider_name\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tgum spin --spinner minidot --title \"设置默认模型并重启网关...\" -- \\\n\t\t\t\topenclaw models set \"$provider_name/$default_model\"\n\t\t\tstart_gateway\n\t\t\techo \"完成:所有 $model_count 个模型已加载\"\n\t\tfi\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_list() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\twhile IFS=$'\\t' read -r rec_type idx name base_url model_count latency_txt latency_level; do\n\t\t\tcase \"$rec_type\" in\n\t\t\t\tMSG)\n\t\t\t\t\techo \"$idx\"\n\t\t\t\t\t;;\n\t\t\t\tROW)\n\t\t\t\t\tlocal latency_color=\"$gl_bai\"\n\t\t\t\t\tcase \"$latency_level\" in\n\t\t\t\t\t\tlow) latency_color=\"$gl_lv\" ;;\n\t\t\t\t\t\tmedium) latency_color=\"$gl_huang\" ;;\n\t\t\t\t\t\thigh|unavailable) latency_color=\"$gl_hong\" ;;\n\t\t\t\t\t\tunchecked) latency_color=\"$gl_bai\" ;;\n\t\t\t\t\tesac\n\n\t\t\t\t\tprintf '%b\\n' \"[$idx] ${name} | API: ${base_url} | 模型数量: ${gl_huang}${model_count}${gl_bai} | 延迟/状态: ${latency_color}${latency_txt}${gl_bai}\"\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tlocal _py_list; _py_list=$(mktemp /tmp/oc_list_XXXXXX.py)\n\t\tcat > \"$_py_list\" << 'PYEOF'\nimport json, sys, time, urllib.request\n\npath = sys.argv[1]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\ndef ping_models(base_url, api_key):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': 'Bearer ' + api_key,\n 'User-Agent': 'OpenClaw-API-Manage/1.0',\n },\n )\n start = time.perf_counter()\n with urllib.request.urlopen(req, timeout=4) as resp:\n resp.read(2048)\n return int((time.perf_counter() - start) * 1000)\n\ndef classify_latency(latency):\n if latency == '不可用':\n return '不可用', 'unavailable'\n if latency == '未检测':\n return '未检测', 'unchecked'\n if isinstance(latency, int):\n level = 'low' if latency <= 800 else ('medium' if latency <= 2000 else 'high')\n return str(latency) + 'ms', level\n return str(latency), 'unchecked'\n\ntry:\n with open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\nexcept FileNotFoundError:\n print('MSG\\t未找到 openclaw.json,请先完成安装/初始化。')\n raise SystemExit(0)\nexcept Exception as e:\n print('MSG\\t读取配置失败: ' + type(e).__name__ + ': ' + str(e))\n raise SystemExit(0)\n\nproviders = ((obj.get('models') or {}).get('providers') or {})\nif not isinstance(providers, dict) or not providers:\n print('MSG\\t当前未配置任何 API provider。')\n raise SystemExit(0)\n\nprint('MSG\\t--- 已配置 API 列表 ---')\n\nfor idx, name in enumerate(sorted(providers.keys()), start=1):\n provider = providers.get(name)\n if not isinstance(provider, dict):\n base_url = '-'; model_count = 0; latency_raw = '不可用'\n else:\n base_url = provider.get('baseUrl') or provider.get('url') or provider.get('endpoint') or '-'\n models = provider.get('models') if isinstance(provider.get('models'), list) else []\n model_count = sum(1 for m in models if isinstance(m, dict) and m.get('id'))\n api = provider.get('api', '')\n api_key = provider.get('apiKey')\n latency_raw = '未检测'\n if api in SUPPORTED_APIS and isinstance(base_url, str) and base_url != '-' and isinstance(api_key, str) and api_key:\n try:\n latency_raw = ping_models(base_url, api_key)\n except Exception:\n latency_raw = '不可用'\n elif api in SUPPORTED_APIS:\n latency_raw = '不可用'\n\n latency_text, latency_level = classify_latency(latency_raw)\n print('ROW\\t' + '\\t'.join([str(idx), str(name), str(base_url), str(model_count), str(latency_text), str(latency_level)]))\nPYEOF\n\t\tdone < <(python3 \"$_py_list\" \"$config_file\")\n\t\trm -f \"$_py_list\"\n\t}\n\n\tsync-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要同步的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tinstall jq curl >/dev/null 2>&1\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY2'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\ntarget = sys.argv[2]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('错误:未检测到 API providers,无法同步')\n raise SystemExit(2)\n\nprovider = providers.get(target)\nif not isinstance(provider, dict):\n print(f'错误:未找到 provider: {target}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef fetch_remote_models_with_retry(base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n return json.loads(payload), None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\napi = provider.get('api', '')\nbase_url = provider.get('baseUrl')\napi_key = provider.get('apiKey')\nmodel_list = provider.get('models', [])\n\nif not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n print(f'错误:provider {target} 缺少 baseUrl/apiKey/models,无法执行同步')\n raise SystemExit(3)\n\nif api not in SUPPORTED_APIS:\n print(f'错误:provider {target} 当前 api={api},不支持直接 /models 同步')\n raise SystemExit(3)\n\ndata, err, attempts = fetch_remote_models_with_retry(base_url, api_key, retries=3)\nif err is not None:\n print(f'错误:{target}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n raise SystemExit(4)\n\nif not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n print(f'错误:{target}: /models 返回结构不可识别')\n raise SystemExit(4)\n\nremote_ids = []\nfor item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\nremote_set = set(remote_ids)\nif not remote_set:\n print(f'错误:{target}: 上游 /models 为空,已中止同步')\n raise SystemExit(5)\n\nlocal_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\nlocal_ids = [str(m['id']) for m in local_models]\nlocal_set = set(local_ids)\n\ntemplate = copy.deepcopy(local_models[0]) if local_models else None\nif template is None:\n print(f'错误:{target}: 本地 models 无有效模板模型,无法补全新增模型')\n raise SystemExit(3)\n\nremoved_ids = [mid for mid in local_ids if mid not in remote_set]\nadded_ids = [mid for mid in remote_ids if mid not in local_set]\n\nkept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\nnew_models = kept_models[:]\nfor mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{target} / {mid}'\n new_models.append(nm)\n\nif not new_models:\n print(f'错误:{target}: 同步后无可用模型,已中止写入')\n raise SystemExit(5)\n\nexpected_refs = {model_ref(target, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\nlocal_refs = {model_ref(target, mid) for mid in local_ids}\nremoved_refs = local_refs - expected_refs\nfirst_ref = model_ref(target, str(new_models[0]['id']))\n\nchanged = False\nprimary_ref = get_primary_ref(defaults)\nif isinstance(primary_ref, str) and primary_ref in removed_refs:\n set_primary_ref(defaults, first_ref)\n changed = True\n print(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in removed_refs:\n defaults[fk] = first_ref\n changed = True\n print(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\nstale_refs = [r for r in list(defaults_models.keys()) if r.startswith(target + '/') and r not in expected_refs]\nfor r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\nfor r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\nif removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'{target}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\nif changed:\n print('指定 provider 模型一致性同步完成')\nelse:\n print('无需同步:该 provider 配置已与上游 /models 保持一致')\nPY2\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"同步完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在或未配置\" ;;\n\t\t\t3) echo \"错误:provider 配置不完整或类型不支持\" ;;\n\t\t\t4) echo \"错误:上游 /models 请求失败\" ;;\n\t\t\t5) echo \"错误:上游模型为空或同步后无可用模型\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\tdelete-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要删除的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY'\nimport copy\nimport json\nimport sys\n\npath = sys.argv[1]\nname = sys.argv[2]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or name not in providers:\n print(f'错误:未找到 provider: {name}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\nreplacement_candidates = collect_available_refs(exclude_provider=name)\nreplacement = replacement_candidates[0] if replacement_candidates else None\n\nprimary_ref = get_primary_ref(defaults)\nif ref_provider(primary_ref) == name:\n if not replacement:\n print('错误:删除中止:默认主模型指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n set_primary_ref(defaults, replacement)\n print(f'默认主模型切换: {primary_ref} -> {replacement}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n if not replacement:\n print(f'错误:删除中止:{fk} 指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n defaults[fk] = replacement\n print(f'{fk} 切换: {val} -> {replacement}')\n\nremoved_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\nfor r in removed_refs:\n defaults_models.pop(r, None)\n\nproviders.pop(name, None)\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'已删除 provider: {name}')\nprint(f'已清理 defaults.models 中 {len(removed_refs)} 个关联模型引用')\nPY\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"删除完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在\" ;;\n\t\t\t3) echo \"错误:无可用替代模型,已保持原配置\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"API 管理\"\n\t\t\topenclaw_api_manage_list\n\t\t\techo\n\n\t\t\tlocal api_choice\n\t\t\tapi_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加 API\" \\\n\t\t\t\t\"同步 API 供应商模型列表\" \\\n\t\t\t\t\"删除 API\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$api_choice\" in\n\t\t\t\t\"添加 API\") add-openclaw-provider-interactive ;;\n\t\t\t\t\"同步 API 供应商模型列表\") sync-openclaw-provider-interactive ;;\n\t\t\t\t\"删除 API\") delete-openclaw-provider-interactive ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tchange_model() {\n\t\tlocal all_models model\n\t\tall_models=$(openclaw models list --all 2>/dev/null | grep \"configured\")\n\n\t\tif [[ -z \"$all_models\" ]]; then\n\t\t\tui_err \"无法获取可用模型列表(请先添加 API 提供商)\"\n\t\t\tbreak_end\n\t\t\treturn\n\t\tfi\n\n\t\tmodel=$(echo \"$all_models\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 当前: $(openclaw models list 2>/dev/null | awk '{print $1}' | head -1) │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=20 \\\n\t\t\t--layout=reverse \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ◈ MODEL SELECT \" \\\n\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || return\n\n\t\t[[ -z \"$model\" ]] && return\n\t\tmodel=$(awk '{print $1}' <<< \"$model\")\n\n\t\tgum spin --spinner minidot --title \"正在切换模型...\" -- openclaw models set \"$model\"\n\t\tstart_gateway\n\t\tui_ok \"已切换至:$model\"\n\t\tbreak_end\n\t}\n\n\tresolve_openclaw_plugin_id() {\n\t\tlocal raw_input=\"$1\"\n\t\tlocal plugin_id=\"$raw_input\"\n\n\t\tplugin_id=\"${plugin_id#@openclaw/}\"\n\t\tif [[ \"$plugin_id\" == @*/* ]]; then\n\t\t\tplugin_id=\"${plugin_id##*/}\"\n\t\tfi\n\t\tplugin_id=\"${plugin_id%%@*}\"\n\t\techo \"$plugin_id\"\n\t}\n\n\tsync_openclaw_plugin_allowlist() {\n\t\tlocal plugin_id=\"$1\"\n\t\t[[ -z \"$plugin_id\" ]] && return 1\n\n\t\tlocal home_config=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal root_config=\"/root/.openclaw/openclaw.json\"\n\t\tlocal config_file=\"$home_config\"\n\t\tif [[ ! -f \"$home_config\" && -f \"$root_config\" ]]; then\n\t\t\tconfig_file=\"$root_config\"\n\t\tfi\n\n\t\tmkdir -p \"$(dirname \"$config_file\")\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo '{}' > \"$config_file\"\n\t\tfi\n\n\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\tlocal tmp_json\n\t\t\ttmp_json=$(mktemp)\n\t\t\tif jq --arg pid \"$plugin_id\" '\n\t\t\t\t.plugins = (if (.plugins | type) == \"object\" then .plugins else {} end)\n\t\t\t\t| .plugins.allow = (if (.plugins.allow | type) == \"array\" then .plugins.allow else [] end)\n\t\t\t\t| if (.plugins.allow | index($pid)) == null then .plugins.allow += [$pid] else . end\n\t\t\t' \"$config_file\" > \"$tmp_json\" 2>/dev/null && mv \"$tmp_json\" \"$config_file\"; then\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\t\trm -f \"$tmp_json\"\n\t\tfi\n\n\t\tif command -v python3 >/dev/null 2>&1; then\n\t\t\tif python3 - \"$config_file\" \"$plugin_id\" <<'PYTHON_EOF'\nimport json\nimport sys\nfrom pathlib import Path\n\nconfig_file = Path(sys.argv[1])\nplugin_id = sys.argv[2]\n\ntry:\n data = json.loads(config_file.read_text(encoding='utf-8')) if config_file.exists() else {}\n if not isinstance(data, dict):\n data = {}\nexcept Exception:\n data = {}\n\nplugins = data.get('plugins')\nif not isinstance(plugins, dict):\n plugins = {}\n\na = plugins.get('allow')\nif not isinstance(a, list):\n a = []\n\nif plugin_id not in a:\n a.append(plugin_id)\n\nplugins['allow'] = a\ndata['plugins'] = plugins\nconfig_file.write_text(json.dumps(data, ensure_ascii=False, indent=2) + \"\\n\", encoding='utf-8')\nPYTHON_EOF\n\t\t\tthen\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\tfi\n\n\t\techo \"警告:已安装插件,但同步 plugins.allow 失败,请手动检查: $config_file\"\n\t\treturn 1\n\t}\n\n\tinstall_plugin() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"插件管理\"\n\t\t\tgum style --foreground 240 \"当前已安装插件:\"\n\t\t\topenclaw plugins list\n\t\t\techo\n\n\t\t\tlocal raw_input\n\t\t\traw_input=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择要安装的插件\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"feishu 飞书/Lark 集成\" \\\n\t\t\t\t\"telegram Telegram 机器人\" \\\n\t\t\t\t\"slack Slack 企业通讯\" \\\n\t\t\t\t\"msteams Microsoft Teams\" \\\n\t\t\t\t\"discord Discord 社区管理\" \\\n\t\t\t\t\"whatsapp WhatsApp 自动化\" \\\n\t\t\t\t\"memory-core 基础记忆 (文件检索)\" \\\n\t\t\t\t\"memory-lancedb 增强记忆 (向量数据库)\" \\\n\t\t\t\t\"copilot-proxy Copilot 接口转发\" \\\n\t\t\t\t\"lobster 审批流 (带人工确认)\" \\\n\t\t\t\t\"voice-call 语音通话能力\" \\\n\t\t\t\t\"nostr 加密隐私聊天\" \\\n\t\t\t\t\"手动输入插件 ID\") || break\n\n\t\t\tif [[ \"$raw_input\" == \"手动输入插件 ID\" ]]; then\n\t\t\t\traw_input=$(gum input \\\n\t\t\t\t\t--placeholder \"插件 ID (留空退出)\" \\\n\t\t\t\t\t--prompt \"Plugin > \") || break\n\t\t\t\t[[ -z \"$raw_input\" ]] && break\n\t\t\telse\n\t\t\t\traw_input=$(echo \"$raw_input\" | awk '{print $1}')\n\t\t\tfi\n\t\t\t[[ -z \"$raw_input\" ]] && break\n\n\t\t\tlocal plugin_id plugin_full\n\t\t\tplugin_id=$(resolve_openclaw_plugin_id \"$raw_input\")\n\t\t\tplugin_full=\"$raw_input\"\n\n\t\t\tlocal plugin_list\n\t\t\tplugin_list=$(openclaw plugins list 2>/dev/null)\n\n\t\t\tif echo \"$plugin_list\" | grep -qw \"$plugin_id\" && echo \"$plugin_list\" | grep \"$plugin_id\" | grep -q \"disabled\"; then\n\t\t\t\tui_info \"插件 [$plugin_id] 已预装,正在激活...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\" && ui_ok \"激活成功\" || ui_err \"激活失败\"\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/extensions/$plugin_id\" ]]; then\n\t\t\t\tui_info \"发现系统内置插件,尝试直接启用...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\telse\n\t\t\t\tui_info \"本地未发现,尝试下载安装...\"\n\t\t\t\trm -rf \"/root/.openclaw/extensions/$plugin_id\"\n\n\t\t\t\tif openclaw plugins install \"$plugin_full\"; then\n\t\t\t\t\tui_ok \"下载成功,正在启用...\"\n\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\telse\n\t\t\t\t\tui_warn \"官方渠道下载失败,尝试 npm 备选方案...\"\n\t\t\t\t\tif npm install -g \"$plugin_full\" --unsafe-perm; then\n\t\t\t\t\t\tui_ok \"npm 安装成功,尝试启用...\"\n\t\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"无法获取该插件,请检查 ID 是否正确或网络是否可用\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\tfi\n\t\t\tfi\n\n\t\t\tsync_openclaw_plugin_allowlist \"$plugin_id\"\n\t\t\tstart_gateway\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\tinstall_skill() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"技能管理\"\n\t\t\tgum style --foreground 240 \"当前已安装技能:\"\n\t\t\topenclaw skills list\n\t\t\techo\n\n\t\t\tlocal skill_name\n\t\t\tskill_name=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择要安装的技能\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"github 管理 GitHub Issues/PR/CI\" \\\n\t\t\t\t\"notion 操作 Notion 页面与数据库\" \\\n\t\t\t\t\"apple-notes macOS 原生笔记管理\" \\\n\t\t\t\t\"apple-reminders macOS 提醒事项管理\" \\\n\t\t\t\t\"1password 自动化读取 1Password 密钥\" \\\n\t\t\t\t\"gog Google Workspace 全能助手\" \\\n\t\t\t\t\"things-mac Things 3 任务管理\" \\\n\t\t\t\t\"bluebubbles 通过 BlueBubbles 收发 iMessage\" \\\n\t\t\t\t\"himalaya 终端邮件管理 (IMAP/SMTP)\" \\\n\t\t\t\t\"summarize 网页/播客/YouTube 内容总结\" \\\n\t\t\t\t\"openai-whisper 本地音频转文字\" \\\n\t\t\t\t\"coding-agent 运行 Claude Code/Codex 等编程助手\" \\\n\t\t\t\t\"手动输入技能名称\") || break\n\n\t\t\tif [[ \"$skill_name\" == \"手动输入技能名称\" ]]; then\n\t\t\t\tskill_name=$(gum input \\\n\t\t\t\t\t--placeholder \"技能名称 (留空退出)\" \\\n\t\t\t\t\t--prompt \"Skill > \") || break\n\t\t\t\t[[ -z \"$skill_name\" ]] && break\n\t\t\telse\n\t\t\t\tskill_name=$(echo \"$skill_name\" | awk '{print $1}')\n\t\t\tfi\n\t\t\t[[ -z \"$skill_name\" ]] && break\n\n\t\t\tlocal skill_found=false\n\t\t\tif [[ -d \"${HOME}/.openclaw/workspace/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在用户目录安装\"\n\t\t\t\tskill_found=true\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在系统目录安装\"\n\t\t\t\tskill_found=true\n\t\t\tfi\n\n\t\t\tif [[ \"$skill_found\" == true ]]; then\n\t\t\t\tgum confirm \"是否重新安装?\" || { break_end; continue; }\n\t\t\tfi\n\n\t\t\tgum spin --spinner globe --title \"正在安装技能 $skill_name...\" -- npx clawhub install \"$skill_name\"\n\t\t\tif [[ $? -eq 0 ]]; then\n\t\t\t\techo \"技能 $skill_name 安装成功\"\n\t\t\t\tstart_gateway\n\t\t\telse\n\t\t\t\techo \"错误:安装失败,请检查技能名称是否正确\"\n\t\t\tfi\n\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\topenclaw_json_get_bool() {\n\t\tlocal expr=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r \"$expr\" \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_channel_has_cfg() {\n\t\tlocal channel=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r --arg c \"$channel\" '\n\t\t\t(.channels[$c] // null) as $v\n\t\t\t| if ($v | type) != \"object\" then\n\t\t\t\tfalse\n\t\t\t else\n\t\t\t\t([ $v\n\t\t\t\t | to_entries[]\n\t\t\t\t | select((.key == \"enabled\" or .key == \"dmPolicy\" or .key == \"groupPolicy\" or .key == \"streaming\") | not)\n\t\t\t\t | .value\n\t\t\t\t | select(. != null and . != \"\" and . != false)\n\t\t\t\t ] | length) > 0\n\t\t\t end\n\t\t' \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_dir_has_files() {\n\t\tlocal dir=\"$1\"\n\t\t[[ -d \"$dir\" ]] && find \"$dir\" -type f -print -quit 2>/dev/null | grep -q .\n\t}\n\n\topenclaw_plugin_local_installed() {\n\t\tlocal plugin=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ -s \"$config_file\" ]] && jq -e --arg p \"$plugin\" '.plugins.installs[$p]' \"$config_file\" >/dev/null 2>&1; then\n\t\t\treturn 0\n\t\tfi\n\t\t[[ -d \"${HOME}/.openclaw/extensions/${plugin}\" ]] || [[ -d \"/usr/lib/node_modules/openclaw/extensions/${plugin}\" ]]\n\t}\n\n\topenclaw_bot_status_text() {\n\t\tlocal enabled=\"$1\"\n\t\tlocal configured=\"$2\"\n\t\tlocal connected=\"$3\"\n\t\tlocal abnormal=\"$4\"\n\t\tif [[ \"$abnormal\" == \"true\" ]]; then\n\t\t\techo \"异常\"\n\t\telif [[ \"$enabled\" != \"true\" ]]; then\n\t\t\techo \"未启用\"\n\t\telif [[ \"$connected\" == \"true\" ]]; then\n\t\t\techo \"已连接\"\n\t\telif [[ \"$configured\" == \"true\" ]]; then\n\t\t\techo \"已配置\"\n\t\telse\n\t\t\techo \"未配置\"\n\t\tfi\n\t}\n\n\topenclaw_colorize_bot_status() {\n\t\tlocal status=\"$1\"\n\t\tcase \"$status\" in\n\t\t\t已连接) echo -e \"${gl_lv}${status}${gl_bai}\" ;;\n\t\t\t已配置) echo -e \"${gl_huang}${status}${gl_bai}\" ;;\n\t\t\t异常) echo -e \"${gl_hong}${status}${gl_bai}\" ;;\n\t\t\t*) echo \"$status\" ;;\n\t\tesac\n\t}\n\n\topenclaw_print_bot_status_line() {\n\t\tlocal label=\"$1\"\n\t\tlocal status=\"$2\"\n\t\techo -e \"- ${label}: $(openclaw_colorize_bot_status \"$status\")\"\n\t}\n\n\topenclaw_show_bot_local_status_block() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal json_ok=\"false\"\n\t\tif [[ -s \"$config_file\" ]] && jq empty \"$config_file\" >/dev/null 2>&1; then\n\t\t\tjson_ok=\"true\"\n\t\tfi\n\n\t\tlocal tg_enabled tg_cfg tg_connected tg_abnormal tg_status\n\t\ttg_enabled=$(openclaw_json_get_bool '.channels.telegram.enabled // .plugins.entries.telegram.enabled // false')\n\t\ttg_cfg=$(openclaw_channel_has_cfg \"telegram\")\n\t\ttg_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/telegram\"; then\n\t\t\ttg_connected=\"true\"\n\t\tfi\n\t\ttg_abnormal=\"false\"\n\t\tif [[ \"$tg_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\ttg_abnormal=\"true\"\n\t\tfi\n\t\ttg_status=$(openclaw_bot_status_text \"$tg_enabled\" \"$tg_cfg\" \"$tg_connected\" \"$tg_abnormal\")\n\n\t\tlocal feishu_enabled feishu_cfg feishu_connected feishu_abnormal feishu_status\n\t\tfeishu_enabled=$(openclaw_json_get_bool '.plugins.entries.feishu.enabled // .channels.feishu.enabled // false')\n\t\tfeishu_cfg=$(openclaw_channel_has_cfg \"feishu\")\n\t\tfeishu_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/feishu\"; then\n\t\t\tfeishu_connected=\"true\"\n\t\tfi\n\t\tfeishu_abnormal=\"false\"\n\t\tif [[ \"$feishu_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"feishu\"; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$feishu_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tfeishu_status=$(openclaw_bot_status_text \"$feishu_enabled\" \"$feishu_cfg\" \"$feishu_connected\" \"$feishu_abnormal\")\n\n\t\tlocal wa_enabled wa_cfg wa_connected wa_abnormal wa_status\n\t\twa_enabled=$(openclaw_json_get_bool '.plugins.entries.whatsapp.enabled // .channels.whatsapp.enabled // false')\n\t\twa_cfg=$(openclaw_channel_has_cfg \"whatsapp\")\n\t\twa_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/whatsapp\"; then\n\t\t\twa_connected=\"true\"\n\t\tfi\n\t\twa_abnormal=\"false\"\n\t\tif [[ \"$wa_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"whatsapp\"; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$wa_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\twa_status=$(openclaw_bot_status_text \"$wa_enabled\" \"$wa_cfg\" \"$wa_connected\" \"$wa_abnormal\")\n\n\t\tlocal dc_enabled dc_cfg dc_connected dc_abnormal dc_status\n\t\tdc_enabled=$(openclaw_json_get_bool '.channels.discord.enabled // .plugins.entries.discord.enabled // false')\n\t\tdc_cfg=$(openclaw_channel_has_cfg \"discord\")\n\t\tdc_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/discord\"; then\n\t\t\tdc_connected=\"true\"\n\t\tfi\n\t\tdc_abnormal=\"false\"\n\t\tif [[ \"$dc_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tdc_abnormal=\"true\"\n\t\tfi\n\t\tdc_status=$(openclaw_bot_status_text \"$dc_enabled\" \"$dc_cfg\" \"$dc_connected\" \"$dc_abnormal\")\n\n\t\tlocal slack_enabled slack_cfg slack_connected slack_abnormal slack_status\n\t\tslack_enabled=$(openclaw_json_get_bool '.plugins.entries.slack.enabled // .channels.slack.enabled // false')\n\t\tslack_cfg=$(openclaw_channel_has_cfg \"slack\")\n\t\tslack_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/slack\"; then\n\t\t\tslack_connected=\"true\"\n\t\tfi\n\t\tslack_abnormal=\"false\"\n\t\tif [[ \"$slack_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"slack\"; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$slack_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tslack_status=$(openclaw_bot_status_text \"$slack_enabled\" \"$slack_cfg\" \"$slack_connected\" \"$slack_abnormal\")\n\n\t\tlocal qq_enabled qq_cfg qq_connected qq_abnormal qq_status\n\t\tqq_enabled=$(openclaw_json_get_bool '.plugins.entries.qqbot.enabled // .channels.qqbot.enabled // false')\n\t\tqq_cfg=$(openclaw_channel_has_cfg \"qqbot\")\n\t\tqq_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/sessions\" || openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/data\"; then\n\t\t\tqq_connected=\"true\"\n\t\tfi\n\t\tqq_abnormal=\"false\"\n\t\tif [[ \"$qq_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"qqbot\"; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$qq_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tqq_status=$(openclaw_bot_status_text \"$qq_enabled\" \"$qq_cfg\" \"$qq_connected\" \"$qq_abnormal\")\n\n\t\techo \"本地状态(仅本机配置/缓存,不做网络探测):\"\n\t\topenclaw_print_bot_status_line \"Telegram\" \"$tg_status\"\n\t\topenclaw_print_bot_status_line \"飞书(Lark)\" \"$feishu_status\"\n\t\topenclaw_print_bot_status_line \"WhatsApp\" \"$wa_status\"\n\t\topenclaw_print_bot_status_line \"Discord\" \"$dc_status\"\n\t\topenclaw_print_bot_status_line \"Slack\" \"$slack_status\"\n\t\topenclaw_print_bot_status_line \"QQ Bot\" \"$qq_status\"\n\t}\n\n\tchange_tg_bot_code() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"机器人连接对接\"\n\t\t\topenclaw_show_bot_local_status_block\n\t\t\techo\n\n\t\t\tlocal bot_choice\n\t\t\tbot_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择要对接的平台\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"Telegram 机器人对接\" \\\n\t\t\t\t\"飞书 (Lark) 机器人对接\" \\\n\t\t\t\t\"WhatsApp 机器人对接\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tlocal code\n\t\t\tcase \"$bot_choice\" in\n\t\t\t\t\"Telegram 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"TG机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve telegram \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"飞书 (Lark) 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"飞书机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve feishu \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"WhatsApp 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"WhatsApp连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve whatsapp \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"返回\"|*)\n\t\t\t\t\treturn 0\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\topenclaw_backup_root() {\n\t\techo \"${HOME}/.openclaw/backups\"\n\t}\n\n\topenclaw_is_interactive_terminal() {\n\t\t[[ -t 0 && -t 1 ]]\n\t}\n\n\topenclaw_has_command() {\n\t\tcommand -v \"$1\" >/dev/null 2>&1\n\t}\n\n\topenclaw_is_safe_relpath() {\n\t\tlocal rel=\"$1\"\n\t\t[[ -z \"$rel\" ]] && return 1\n\t\t[[ \"$rel\" == /* ]] && return 1\n\t\t[[ \"$rel\" == *//* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\n'* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\r'* ]] && return 1\n\t\tcase \"$rel\" in\n\t\t\t../*|*/../*|*/..|..)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t\treturn 0\n\t}\n\n\topenclaw_restore_path_allowed() {\n\t\tlocal mode=\"$1\"\n\t\tlocal rel=\"$2\"\n\t\tcase \"$mode\" in\n\t\t\tmemory)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\tMEMORY.md|AGENTS.md|USER.md|SOUL.md|TOOLS.md|memory/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\tproject)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\topenclaw.json|workspace/*|extensions/*|skills/*|prompts/*|tools/*|telegram/*|feishu/*|whatsapp/*|discord/*|slack/*|qqbot/*|logs/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\t*)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t}\n\n\topenclaw_pack_backup_archive() {\n\t\tlocal backup_type=\"$1\"\n\t\tlocal export_mode=\"$2\"\n\t\tlocal payload_dir=\"$3\"\n\t\tlocal output_file=\"$4\"\n\n\t\tlocal tmp_root\n\t\ttmp_root=$(mktemp -d) || return 1\n\t\tlocal pack_dir=\"$tmp_root/package\"\n\t\tmkdir -p \"$pack_dir\"\n\n\t\tcp -a \"$payload_dir\" \"$pack_dir/payload\"\n\n\t\t(\n\t\t\tcd \"$pack_dir/payload\" || exit 1\n\t\t\tfind . -type f | sed 's|^\\./||' | sort > \"$pack_dir/manifest.files\"\n\t\t\t: > \"$pack_dir/manifest.sha256\"\n\t\t\tlocal f\n\t\t\twhile IFS= read -r f; do\n\t\t\t\t[[ -z \"$f\" ]] && continue\n\t\t\t\tsha256sum \"$f\" >> \"$pack_dir/manifest.sha256\"\n\t\t\tdone < \"$pack_dir/manifest.files\"\n\t\t) || { rm -rf \"$tmp_root\"; return 1; }\n\n\t\tcat > \"$pack_dir/backup.meta\" <<EOF\nTYPE=$backup_type\nMODE=$export_mode\nCREATED_AT=$(date -u +'%Y-%m-%dT%H:%M:%SZ')\nHOST=$(hostname)\nEOF\n\n\t\tmkdir -p \"$(dirname \"$output_file\")\"\n\t\ttar -C \"$pack_dir\" -czf \"$output_file\" backup.meta manifest.files manifest.sha256 payload\n\t\tlocal rc=$?\n\t\trm -rf \"$tmp_root\"\n\t\treturn $rc\n\t}\n\n\topenclaw_offer_transfer_hint() {\n\t\tlocal file_path=\"$1\"\n\t\techo \"可使用以下方式下载备份文件:\"\n\t\techo \" 本地路径: $file_path\"\n\t\techo \" scp 示例: scp root@你的服务器:$file_path ./\"\n\t\techo \" 或使用 SFTP 客户端下载\"\n\t}\n\n\topenclaw_prepare_import_archive() {\n\t\tlocal expected_type=\"$1\"\n\t\tlocal archive_path=\"$2\"\n\t\tlocal unpack_root=\"$3\"\n\n\t\tif [[ ! -f \"$archive_path\" ]]; then\n\t\t\techo \"错误:文件不存在: $archive_path\"\n\t\t\treturn 1\n\t\tfi\n\t\tmkdir -p \"$unpack_root\"\n\t\tif ! tar -xzf \"$archive_path\" -C \"$unpack_root\"; then\n\t\t\techo \"错误:备份包解压失败\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal pkg_dir=\"$unpack_root/package\"\n\t\tif [[ -f \"$unpack_root/backup.meta\" ]]; then\n\t\t\tpkg_dir=\"$unpack_root\"\n\t\tfi\n\n\t\tlocal required\n\t\tfor required in backup.meta manifest.files manifest.sha256 payload; do\n\t\t\tif [[ ! -e \"$pkg_dir/$required\" ]]; then\n\t\t\t\techo \"错误:备份包缺少必要文件: $required\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tdone\n\n\t\tlocal real_type\n\t\treal_type=$(grep '^TYPE=' \"$pkg_dir/backup.meta\" | head -n1 | cut -d'=' -f2-)\n\t\tif [[ \"$real_type\" != \"$expected_type\" ]]; then\n\t\t\techo \"错误:备份类型不匹配,期望: $expected_type,实际: ${real_type:-未知}\"\n\t\t\treturn 1\n\t\tfi\n\n\t\t(\n\t\t\tcd \"$pkg_dir/payload\" || exit 1\n\t\t\tsha256sum -c ../manifest.sha256 >/dev/null\n\t\t) || { echo \"错误:sha256 校验失败,拒绝还原\"; return 1; }\n\n\t\techo \"$pkg_dir\"\n\t\treturn 0\n\t}\n\n\topenclaw_memory_backup_export() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tlocal ts\n\t\tts=$(date +%Y%m%d-%H%M%S)\n\t\tlocal out_file=\"$backup_root/openclaw-memory-full-${ts}.tar.gz\"\n\n\t\tmkdir -p \"$backup_root\"\n\t\tif [[ ! -d \"$workspace_dir\" ]]; then\n\t\t\techo \"错误:未找到 workspace 目录: $workspace_dir\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\t[[ -f \"$workspace_dir/MEMORY.md\" ]] && cp -a \"$workspace_dir/MEMORY.md\" \"$tmp_payload/\"\n\t\t[[ -d \"$workspace_dir/memory\" ]] && cp -a \"$workspace_dir/memory\" \"$tmp_payload/\"\n\n\t\tif gum confirm \"是否附带 AGENTS/USER/SOUL/TOOLS 文件?\"; then\n\t\t\tlocal f\n\t\t\tfor f in AGENTS.md USER.md SOUL.md TOOLS.md; do\n\t\t\t\t[[ -f \"$workspace_dir/$f\" ]] && cp -a \"$workspace_dir/$f\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的记忆文件\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"memory-full\" \"default\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"记忆全量备份完成: $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:记忆全量备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_read_import_path() {\n\t\tlocal file_input file_path backup_root\n\t\techo \"提示:输入文件名时默认在备份目录查找;输入含 / 的路径时按完整路径校验。\" >&2\n\t\techo \"scp 示例: scp /本地/备份包.tar.gz root@你的服务器:/tmp/\" >&2\n\n\t\tfile_input=$(gum input \\\n\t\t\t--placeholder \"备份文件名或完整路径\" \\\n\t\t\t--prompt \"Path > \")\n\t\t[[ -z \"$file_input\" ]] && { echo \"\"; return 0; }\n\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\n\t\tif [[ \"$file_input\" == */* ]]; then\n\t\t\tfile_path=\"$file_input\"\n\t\telse\n\t\t\tfile_path=\"$backup_root/$file_input\"\n\t\tfi\n\n\t\tif [[ ! -f \"$file_path\" ]]; then\n\t\t\techo \"错误:备份文件不存在: $file_path\" >&2\n\t\t\techo \"\"\n\t\t\treturn 1\n\t\tfi\n\n\t\techo \"$file_path\"\n\t}\n\n\topenclaw_memory_backup_import() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tmkdir -p \"$workspace_dir\"\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"memory-full\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed memory \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$workspace_dir/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$workspace_dir/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"记忆全量还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_export() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tif [[ ! -d \"$openclaw_root\" ]]; then\n\t\t\techo \"错误:未找到 OpenClaw 根目录: $openclaw_root\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal export_mode_label\n\t\texport_mode_label=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $'选择备份模式\\n↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"安全模式(推荐):workspace + openclaw.json + extensions/skills/prompts/tools\" \\\n\t\t\t\"完整模式(含更多状态,敏感风险更高)\") || return 1\n\n\t\tlocal mode_label=\"safe\"\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\tlocal d\n\t\tif [[ \"$export_mode_label\" == 完整* ]]; then\n\t\t\tmode_label=\"full\"\n\t\t\tfor d in workspace extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in telegram feishu whatsapp discord slack qqbot logs; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\telse\n\t\t\t[[ -d \"$openclaw_root/workspace\" ]] && cp -a \"$openclaw_root/workspace\" \"$tmp_payload/\"\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的 OpenClaw 项目内容\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tlocal out_file=\"$backup_root/openclaw-project-${mode_label}-$(date +%Y%m%d-%H%M%S).tar.gz\"\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"openclaw-project\" \"$mode_label\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"OpenClaw 项目备份完成 (${mode_label}): $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:OpenClaw 项目备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_import() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tmkdir -p \"$openclaw_root\"\n\n\t\techo \"警告:高风险操作,项目还原会覆盖 OpenClaw 配置与工作区内容。\"\n\t\tgum confirm \"确认继续还原?此操作不可逆。\" || { echo \"已取消\"; break_end; return 1; }\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"openclaw-project\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed project \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tgum spin --spinner pulse --title \"停止 OpenClaw gateway...\" -- openclaw gateway stop\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$openclaw_root/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$openclaw_root/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tstart_gateway\n\t\t\techo \"gateway 健康检查:\"\n\t\t\topenclaw gateway status || true\n\t\tfi\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"OpenClaw 项目还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_detect_type() {\n\t\tlocal file_name=\"$1\"\n\t\tif [[ \"$file_name\" == openclaw-memory-full-*.tar.gz ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\telif [[ \"$file_name\" == openclaw-project-*.tar.gz ]]; then\n\t\t\techo \"项目备份文件\"\n\t\telse\n\t\t\techo \"其他备份文件\"\n\t\tfi\n\t}\n\n\topenclaw_backup_collect_files() {\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tmapfile -t OPENCLAW_BACKUP_FILES < <(find \"$backup_root\" -maxdepth 1 -type f -name '*.tar.gz' | xargs -I{} basename {} | sort -r)\n\t}\n\n\topenclaw_backup_render_file_list() {\n\t\tlocal backup_root i file_name file_path file_type file_size file_time\n\t\tlocal has_memory=0 has_project=0 has_other=0\n\t\tbackup_root=$(openclaw_backup_root)\n\t\topenclaw_backup_collect_files\n\n\t\techo \"备份目录: $backup_root\"\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\techo \"暂无备份文件\"\n\t\t\treturn 0\n\t\tfi\n\n\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\tfile_type=$(openclaw_backup_detect_type \"${OPENCLAW_BACKUP_FILES[$i]}\")\n\t\t\tcase \"$file_type\" in\n\t\t\t\t\"记忆备份文件\") has_memory=1 ;;\n\t\t\t\t\"项目备份文件\") has_project=1 ;;\n\t\t\t\t\"其他备份文件\") has_other=1 ;;\n\t\t\tesac\n\t\tdone\n\n\t\tif [[ \"$has_memory\" -eq 1 ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"记忆备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_project\" -eq 1 ]]; then\n\t\t\techo \"项目备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"项目备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_other\" -eq 1 ]]; then\n\t\t\techo \"其他备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"其他备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_backup_file_exists_in_list() {\n\t\tlocal target_file=\"$1\"\n\t\tlocal item\n\t\tfor item in \"${OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t[[ \"$item\" == \"$target_file\" ]] && return 0\n\t\tdone\n\t\treturn 1\n\t}\n\n\topenclaw_backup_delete_file() {\n\t\tlocal backup_root backup_root_real user_input target_file target_path target_type\n\t\tbackup_root=$(openclaw_backup_root)\n\n\t\topenclaw_backup_render_file_list\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\tbreak_end\n\t\t\treturn 0\n\t\tfi\n\n\t\tuser_input=$(printf '%s\\n' \"${OPENCLAW_BACKUP_FILES[@]}\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 选择要删除的备份 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=15 \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ⚡ DELETE BACKUP \" \\\n\t\t\t--color=border:196,label:196,header:196,prompt:208,pointer:196,marker:208,hl:208,hl+:208) || { echo \"已取消\"; break_end; return 0; }\n\n\t\t[[ -z \"$user_input\" ]] && { echo \"已取消\"; break_end; return 0; }\n\n\t\tbackup_root_real=$(realpath -m \"$backup_root\")\n\t\ttarget_file=$(basename -- \"$user_input\")\n\t\ttarget_path=\"$backup_root/$target_file\"\n\n\t\tif [[ ! -f \"$target_path\" ]]; then\n\t\t\techo \"错误:目标文件不存在: $target_path\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\ttarget_type=$(openclaw_backup_detect_type \"$target_file\")\n\n\t\techo \"即将删除: [$target_type] $target_path\"\n\t\tgum confirm \"确认删除?\" || { echo \"已取消删除\"; break_end; return 0; }\n\t\tgum confirm \"请再次确认,删除后不可恢复\" || { echo \"已取消删除\"; break_end; return 0; }\n\n\t\tif rm -f -- \"$target_path\"; then\n\t\t\techo \"删除成功: $target_file\"\n\t\telse\n\t\t\techo \"错误:删除失败: $target_file\"\n\t\tfi\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_list_files() {\n\t\topenclaw_backup_render_file_list\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_restore_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"备份与还原\"\n\t\t\topenclaw_backup_render_file_list\n\t\t\techo\n\n\t\t\tlocal backup_choice\n\t\t\tbackup_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"备份记忆全量\" \\\n\t\t\t\t\"还原记忆全量\" \\\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\" \\\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\" \\\n\t\t\t\t\"删除备份文件\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$backup_choice\" in\n\t\t\t\t\"备份记忆全量\") openclaw_memory_backup_export ;;\n\t\t\t\t\"还原记忆全量\") openclaw_memory_backup_import ;;\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\") openclaw_project_backup_export ;;\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\") openclaw_project_backup_import ;;\n\t\t\t\t\"删除备份文件\") openclaw_backup_delete_file ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\t_autostart_status() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\techo \"未安装\"\n\t\t\t\treturn\n\t\t\tfi\n\t\t\tif launchctl list 2>/dev/null | grep -q \"ai.openclaw.gateway\"; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\telse\n\t\t\tif systemctl --user is-enabled --quiet openclaw-gateway.service 2>/dev/null; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\tfi\n\t}\n\n\ttoggle_autostart() {\n\t\tlocal status; status=$(_autostart_status)\n\t\tclear\n\t\tui_header \"开机自启动管理\"\n\t\techo -e \" 当前状态:\\033[1m${status}\\033[0m\"\n\t\techo\n\n\t\tlocal action\n\t\tif [[ \"$status\" == \"已开启\" ]]; then\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"关闭自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\telse\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"开启自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\tfi\n\n\t\tcase \"$action\" in\n\t\t\t\"开启自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\t\t\tgum spin --spinner globe --title \"正在安装服务...\" -- \\\n\t\t\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 1\"\n\t\t\t\t\tfi\n\t\t\t\t\tlaunchctl load \"$plist\" 2>/dev/null && ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t\"关闭自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tlaunchctl unload \"$plist\" 2>/dev/null && ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user disable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t*) return 0 ;;\n\t\tesac\n\t\tbreak_end\n\t}\n\n\tupdate_moltbot() {\n\t\tinstall_node_and_tools\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_bin\n\t\tnpm_bin=\"$(npm prefix -g 2>/dev/null)/bin\"\n\t\tif [[ -n \"$npm_bin\" ]]; then\n\t\t\texport PATH=\"$npm_bin:$PATH\"\n\t\t\tfor rc in \"$HOME/.bashrc\" \"$HOME/.zshrc\" \"$HOME/.profile\"; do\n\t\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t\tgrep -qF \"$npm_bin\" \"$rc\" 2>/dev/null && continue\n\t\t\t\techo \"export PATH=\\\"${npm_bin}:\\$PATH\\\"\" >> \"$rc\"\n\t\t\tdone\n\t\tfi\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab -\n\t\tstart_gateway\n\t\tui_ok \"更新完成\"\n\t\tbreak_end\n\t}\n\n\tuninstall_moltbot() {\n\t\tgum confirm \"确认卸载 OpenClaw?此操作不可逆。\" || return 0\n\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tlaunchctl unload \"$plist\" 2>/dev/null || true\n\t\telse\n\t\t\tsystemctl --user disable --now openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tpkill -f \"openclaw.*gatewa\" 2>/dev/null || true\n\n\t\tgum spin --spinner meter --title \"正在卸载 OpenClaw...\" -- \\\n\t\t\tbash -c 'timeout 15 openclaw uninstall 2>/dev/null; true'\n\t\tnpm uninstall -g openclaw 2>/dev/null || true\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab - 2>/dev/null || true\n\t\trm -rf /root/.openclaw ~/.openclaw\n\n\t\trm -f \"$HOME/.local/bin/oc\" \"$HOME/.local/bin/openclawctl.sh\" /usr/local/bin/oc\n\t\tfor rc in \"$HOME/.zshrc\" \"$HOME/.bashrc\" \"$HOME/.profile\"; do\n\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t_sed_i '/\\.local\\/bin.*PATH/d' \"$rc\"\n\t\t\t_sed_i '/npm-global.*PATH\\|npm\\/bin.*PATH/d' \"$rc\"\n\t\tdone\n\n\t\techo\n\t\tgum style --foreground 46 --bold \" 卸载完成,脚本已退出。\"\n\t\texit 0\n\t}\n\n\tnano_openclaw_json() {\n\t\tinstall nano\n\t\tnano ~/.openclaw/openclaw.json\n\t\tstart_gateway\n\t}\n\n\topenclaw_find_webui_domain() {\n\t\tlocal conf domain_list\n\t\tdomain_list=$(\n\t\t\tgrep -R \"18789\" /home/web/conf.d/*.conf 2>/dev/null \\\n\t\t\t| awk -F: '{print $1}' \\\n\t\t\t| sort -u \\\n\t\t\t| while read -r conf; do\n\t\t\t\tbasename \"$conf\" .conf\n\t\t\tdone\n\t\t)\n\n\t\tif [[ -n \"$domain_list\" ]]; then\n\t\t\techo \"$domain_list\"\n\t\tfi\n\t}\n\n\topenclaw_show_webui_addr() {\n\t\tlocal local_ip token domains\n\n\t\tlocal_ip=\"127.0.0.1\"\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tgum style --border normal --border-foreground 99 --padding \"0 2\" \\\n\t\t\t\"OpenClaw WebUI 访问地址\" \\\n\t\t\t\"本机: http://${local_ip}:18789/#token=${token}\"\n\n\t\tdomains=$(openclaw_find_webui_domain)\n\t\tif [[ -n \"$domains\" ]]; then\n\t\t\techo \"$domains\" | while read -r d; do\n\t\t\t\techo \"域名: https://${d}/#token=${token}\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_domain_webui() {\n\t\tadd_yuming\n\t\tldnmp_Proxy \"${yuming}\" 127.0.0.1 18789\n\n\t\tlocal token\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tclear\n\t\techo \"访问地址: https://${yuming}/#token=$token\"\n\t\techo \"先访问URL触发设备ID,然后回车下一步进行配对。\"\n\t\tgum input --placeholder \"按回车继续...\" > /dev/null\n\n\t\techo -e \"${gl_kjlan}正在加载设备列表……${gl_bai}\"\n\n\t\tlocal config_file=\"$HOME/.openclaw/openclaw.json\"\n\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\tlocal new_origin=\"https://${yuming}\"\n\t\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\t\tlocal tmp_json\n\t\t\t\ttmp_json=$(mktemp)\n\t\t\t\tjq 'if .gateway.controlUi == null then .gateway.controlUi = {\"allowedOrigins\": [\"http://127.0.0.1\"]} else . end | if (.gateway.controlUi.allowedOrigins | contains([$origin]) | not) then .gateway.controlUi.allowedOrigins += [$origin] else . end' \\\n\t\t\t\t\t--arg origin \"$new_origin\" \"$config_file\" > \"$tmp_json\" && mv \"$tmp_json\" \"$config_file\"\n\t\t\t\techo -e \"${gl_kjlan}已将域名 ${yuming} 加入 allowedOrigins 配置${gl_bai}\"\n\t\t\t\topenclaw gateway restart >/dev/null 2>&1\n\t\t\tfi\n\t\tfi\n\n\t\topenclaw devices list\n\n\t\tlocal Request_Key\n\t\tRequest_Key=$(gum input \\\n\t\t\t--placeholder \"Request_Key\" \\\n\t\t\t--prompt \"> \")\n\n\t\tif [[ -z \"$Request_Key\" ]]; then\n\t\t\techo \"Request_Key 不能为空\"\n\t\t\treturn 1\n\t\tfi\n\n\t\topenclaw devices approve \"$Request_Key\"\n\t}\n\n\topenclaw_remove_domain() {\n\t\techo \"域名格式 example.com 不带https://\"\n\t\tweb_del\n\t}\n\n\topenclaw_webui_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"WebUI 访问与设置\"\n\t\t\topenclaw_show_webui_addr\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加域名访问\" \\\n\t\t\t\t\"删除域名访问\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"添加域名访问\")\n\t\t\t\t\topenclaw_domain_webui\n\t\t\t\t\techo\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"删除域名访问\")\n\t\t\t\t\topenclaw_remove_domain\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\twhile true; do\n\t\tclear\n\n\t\tlocal install_status running_status update_info\n\t\tinstall_status=$(get_install_status)\n\t\trunning_status=$(get_running_status)\n\t\tupdate_info=$(check_openclaw_update)\n\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"1 8\" --align center \\\n\t\t\t\"O P E N C L A W\" \\\n\t\t\t\"\" \\\n\t\t\t\"[ AI Agent Gateway Manager ]\"\n\t\techo -e \" $install_status $running_status $update_info\"\n\t\tgum style --foreground 240 \" 输入 oc 可快速启动\"\n\t\techo\n\t\tgum style \\\n\t\t\t--bold \\\n\t\t\t--border double --border-foreground 201 \\\n\t\t\t--padding \"0 4\" --align center \\\n\t\t\t\"$(gum style --bold --foreground 201 '◈ by Joey ◈')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ YouTube @joeyblog')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ Telegram t.me/+ft-zI76oovgwNmRh')\" \\\n\t\t\t\"$(gum style --foreground 208 '⚡ 基于:kejilion · cliproxyapi-installer')\"\n\t\techo\n\n\t\tlocal choice\n\t\tlocal autostart_label\n\t\tautostart_label=\"开机自启动 [$(_autostart_status)]\"\n\n\t\tchoice=$(gum choose \\\n\t\t\t--height 24 \\\n\t\t\t--cursor \"❯ \" \\\n\t\t\t--header $' ─── CONTROL CENTER ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\"小白模式安装(推荐)\" \\\n\t\t\t\"安装\" \\\n\t\t\t\"启动\" \\\n\t\t\t\"停止\" \\\n\t\t\t\"状态日志查看\" \\\n\t\t\t\"$autostart_label\" \\\n\t\t\t\"换模型\" \\\n\t\t\t\"API管理\" \\\n\t\t\t\"CLIProxyAPI 管理\" \\\n\t\t\t\"机器人连接对接\" \\\n\t\t\t\"安装插件\" \\\n\t\t\t\"安装技能\" \\\n\t\t\t\"编辑主配置文件\" \\\n\t\t\t\"配置向导\" \\\n\t\t\t\"健康检测与修复\" \\\n\t\t\t\"WebUI访问与设置\" \\\n\t\t\t\"TUI命令行对话\" \\\n\t\t\t\"备份与还原\" \\\n\t\t\t\"更新\" \\\n\t\t\t\"卸载\" \\\n\t\t\t\"退出\") || break\n\n\t\tcase \"$choice\" in\n\t\t\t\"小白模式安装(推荐)\") beginner_mode_install ;;\n\t\t\t\"安装\") install_moltbot ;;\n\t\t\t\"启动\") start_bot ;;\n\t\t\t\"停止\") stop_bot ;;\n\t\t\t\"状态日志查看\") view_logs ;;\n\t\t\t\"开机自启动 [\"*\"]\") toggle_autostart ;;\n\t\t\t\"换模型\") change_model ;;\n\t\t\"API管理\") openclaw_api_manage_menu ;;\n\t\t\"CLIProxyAPI 管理\") cliproxyapi_manage_menu ;;\n\t\t\"机器人连接对接\") change_tg_bot_code ;;\n\t\t\t\"安装插件\") install_plugin ;;\n\t\t\t\"安装技能\") install_skill ;;\n\t\t\t\"编辑主配置文件\") nano_openclaw_json ;;\n\t\t\t\"配置向导\")\n\t\t\t\topenclaw onboard --install-daemon\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"健康检测与修复\")\n\t\t\t\tgum spin --spinner meter --title \"正在执行健康检测...\" -- openclaw doctor --fix\n\t\t\t\tif sync_openclaw_api_models; then\n\t\t\t\t\tstart_gateway\n\t\t\t\telse\n\t\t\t\t\tui_err \"API 模型同步失败,已中止重启网关,请检查 provider /models 返回后重试\"\n\t\t\t\tfi\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"WebUI访问与设置\") openclaw_webui_menu ;;\n\t\t\t\"TUI命令行对话\")\n\t\t\t\topenclaw tui\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"备份与还原\") openclaw_backup_restore_menu ;;\n\t\t\t\"更新\") update_moltbot ;;\n\t\t\t\"卸载\") uninstall_moltbot ;;\n\t\t\t\"退出\"|*) break ;;\n\t\tesac\n\tdone\n}\n\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n\tmoltbot_menu\nfi\n"}, "d2c286b02d3f275c6caf95288222abeffbcd8ff1": {"t": 1785772921.8543236, "n": 97814, "h": "19a023a9c852841b", "c": "#!/usr/bin/env bash\n#\n# ╔══════════════════════════════════════════════════════════════════╗\n# ║ OpenClaw / MoltBot 管理脚本 ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 作者 GitHub : by Joey ║\n# ║ YouTube : @joeyblog ║\n# ║ Telegram: https://t.me/+ft-zI76oovgwNmRh ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 致谢 / 引用 ║\n# ║ · 原始脚本基础来自 kejilion(@kejilion) ║\n# ║ · CLIProxyAPI 安装器来自 cliproxyapi-installer ║\n# ║ github.com/brokechubb/cliproxyapi-installer ║\n# ╚══════════════════════════════════════════════════════════════════╝\n#\n: \"${gl_hui:='\\e[37m'}\"\n: \"${gl_hong:='\\033[31m'}\"\n: \"${gl_lv:='\\033[32m'}\"\n: \"${gl_huang:='\\033[33m'}\"\n: \"${gl_lan:='\\033[34m'}\"\n: \"${gl_bai:='\\033[0m'}\"\n: \"${gl_zi:='\\033[35m'}\"\n: \"${gl_kjlan:='\\033[96m'}\"\n\nif ! declare -f break_end > /dev/null 2>&1; then\nbreak_end() {\n\tif command -v gum >/dev/null 2>&1; then\n\t\techo\n\t\tgum style --foreground 240 \" ─────────────────────────────────────── \"\n\t\tgum input --placeholder \"» 按回车继续 «\" --prompt \" \" > /dev/null\n\telse\n\t\techo \" ─── 按任意键继续 ───\"\n\t\tread -n 1 -s -r -p \"\"\n\t\techo\n\tfi\n\tclear\n}\nfi\n\nif ! declare -f install > /dev/null 2>&1; then\ninstall() {\n\tif [[ $# -eq 0 ]]; then\n\t\techo \"未提供软件包参数\"\n\t\treturn 1\n\tfi\n\tfor package in \"$@\"; do\n\t\tif ! command -v \"$package\" &>/dev/null; then\n\t\t\techo -e \"${gl_kjlan}正在安装 $package...${gl_bai}\"\n\t\t\tif [[ \"$(uname -s)\" == \"Darwin\" ]]; then\n\t\t\t\t_ensure_brew &>/dev/null\n\t\t\t\tcommand -v brew &>/dev/null && brew install \"$package\" &>/dev/null\n\t\t\telif command -v dnf &>/dev/null; then\n\t\t\t\tdnf install -y \"$package\" &>/dev/null\n\t\t\telif command -v yum &>/dev/null; then\n\t\t\t\tyum install -y \"$package\" &>/dev/null\n\t\t\telif command -v apt &>/dev/null; then\n\t\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"$package\" &>/dev/null\n\t\t\telif command -v apk &>/dev/null; then\n\t\t\t\tapk add \"$package\" &>/dev/null\n\t\t\telif command -v pacman &>/dev/null; then\n\t\t\t\tpacman -S --noconfirm \"$package\" &>/dev/null\n\t\t\telif command -v zypper &>/dev/null; then\n\t\t\t\tzypper install -y \"$package\" &>/dev/null\n\t\t\telif command -v opkg &>/dev/null; then\n\t\t\t\topkg install \"$package\" &>/dev/null\n\t\t\telif command -v pkg &>/dev/null; then\n\t\t\t\tpkg install -y \"$package\" &>/dev/null\n\t\t\telse\n\t\t\t\techo \"未知的包管理器,无法安装 $package\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\tdone\n}\nfi\n\nis_macos() { [[ \"$(uname -s)\" == \"Darwin\" ]]; }\n\n_ensure_brew() {\n\tif ! command -v brew &>/dev/null; then\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tif ! command -v brew &>/dev/null; then\n\t\techo \"正在安装 Homebrew...\"\n\t\t/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tcommand -v brew &>/dev/null || { echo \"Homebrew 安装失败,请手动安装:https://brew.sh\"; return 1; }\n}\n\n_sed_i() {\n\tif is_macos; then\n\t\tsed -i '' \"$@\"\n\telse\n\t\tsed -i \"$@\"\n\tfi\n}\n\ninstall_base_deps() {\n\tif is_macos; then\n\t\t_ensure_brew || return 1\n\t\tlocal missing_brew=()\n\t\tfor cmd in curl git nano jq python3 gpg; do\n\t\t\tcommand -v \"$cmd\" &>/dev/null || missing_brew+=(\"$cmd\")\n\t\tdone\n\t\tfor i in \"${!missing_brew[@]}\"; do\n\t\t\t[[ \"${missing_brew[$i]}\" == \"gpg\" ]] && missing_brew[$i]=\"gnupg\"\n\t\tdone\n\t\t[[ ${#missing_brew[@]} -eq 0 ]] && return 0\n\t\techo \"正在安装缺失依赖:${missing_brew[*]}...\"\n\t\tbrew install \"${missing_brew[@]}\" &>/dev/null\n\t\treturn 0\n\tfi\n\n\tlocal -a dep_map=(\n\t\t\"curl:curl:curl:curl:curl:curl\"\n\t\t\"git:git:git:git:git:git\"\n\t\t\"nano:nano:nano:nano:nano:nano\"\n\t\t\"jq:jq:jq:jq:jq:jq\"\n\t\t\"python3:python3:python3:python3:python:python3\"\n\t\t\"tar:tar:tar:tar:tar:tar\"\n\t\t\"gpg:gnupg:gnupg2:gnupg:gnupg:gpg2\"\n\t)\n\n\tlocal missing_apt=() missing_dnf=() missing_apk=() missing_pacman=() missing_zypper=()\n\n\tfor entry in \"${dep_map[@]}\"; do\n\t\tIFS=: read -r cmd pkg_apt pkg_dnf pkg_apk pkg_pacman pkg_zypper <<< \"$entry\"\n\t\tcommand -v \"$cmd\" &>/dev/null && continue\n\t\tmissing_apt+=(\"$pkg_apt\")\n\t\tmissing_dnf+=(\"$pkg_dnf\")\n\t\tmissing_apk+=(\"$pkg_apk\")\n\t\tmissing_pacman+=(\"$pkg_pacman\")\n\t\tmissing_zypper+=(\"$pkg_zypper\")\n\tdone\n\n\t[[ ${#missing_apt[@]} -eq 0 ]] && return 0\n\n\techo \"正在安装缺失依赖:${missing_apt[*]}...\"\n\n\tif command -v apt &>/dev/null; then\n\t\tapt update -y &>/dev/null\n\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"${missing_apt[@]}\" &>/dev/null\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y epel-release &>/dev/null || true\n\t\tdnf install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y epel-release &>/dev/null || true\n\t\tyum install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v apk &>/dev/null; then\n\t\tapk update &>/dev/null\n\t\tapk add \"${missing_apk[@]}\" &>/dev/null\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -Sy --noconfirm --needed \"${missing_pacman[@]}\" &>/dev/null\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y \"${missing_zypper[@]}\" &>/dev/null\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\tlocal py\n\t\tpy=$(compgen -c 2>/dev/null | grep -E '^python3\\.[0-9]+$' | sort -V | tail -1)\n\t\tif [[ -n \"$py\" ]]; then\n\t\t\tln -sf \"$(command -v \"$py\")\" /usr/local/bin/python3 2>/dev/null || true\n\t\tfi\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\techo \"正在安装 python3...\"\n\t\tif command -v apt &>/dev/null; then\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y python3 &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tdnf install -y python3 &>/dev/null\n\t\telif command -v yum &>/dev/null; then\n\t\t\tyum install -y python3 &>/dev/null\n\t\telif command -v apk &>/dev/null; then\n\t\t\tapk add python3 &>/dev/null\n\t\telif command -v pacman &>/dev/null; then\n\t\t\tpacman -S --noconfirm python &>/dev/null\n\t\telif command -v zypper &>/dev/null; then\n\t\t\tzypper install -y python3 &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\tfi\n}\n\n_install_gum_binary() {\n\tlocal arch\n\tcase \"$(uname -m)\" in\n\t\tx86_64) arch=\"amd64\" ;;\n\t\taarch64) arch=\"arm64\" ;;\n\t\tarmv7l) arch=\"armv7\" ;;\n\t\t*) echo \"不支持的架构: $(uname -m)\"; return 1 ;;\n\tesac\n\tlocal latest\n\tlatest=$(curl -fsSL https://api.github.com/repos/charmbracelet/gum/releases/latest \\\n\t\t| grep '\"tag_name\"' | sed 's/.*\"v\\([^\"]*\\)\".*/\\1/')\n\t[[ -z \"$latest\" ]] && { echo \"获取 gum 版本失败\"; return 1; }\n\tlocal url=\"https://github.com/charmbracelet/gum/releases/download/v${latest}/gum_${latest}_linux_${arch}.tar.gz\"\n\tlocal tmp\n\ttmp=$(mktemp -d)\n\tcurl -fsSL \"$url\" -o \"$tmp/gum.tar.gz\" \\\n\t\t&& tar -xzf \"$tmp/gum.tar.gz\" -C \"$tmp\" \\\n\t\t&& install -m 755 \"$tmp/gum\" /usr/local/bin/gum\n\trm -rf \"$tmp\"\n\tcommand -v gum >/dev/null 2>&1\n}\n\ninstall_gum() {\n\tcommand -v gum >/dev/null 2>&1 && return 0\n\techo \"正在安装 gum...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install gum\n\telif command -v apt >/dev/null 2>&1; then\n\t\tcommand -v gpg >/dev/null 2>&1 || apt install -y gnupg 2>/dev/null\n\t\tmkdir -p /etc/apt/keyrings\n\t\tif curl -fsSL https://repo.charm.sh/apt/gpg.key \\\n\t\t\t| gpg --dearmor -o /etc/apt/keyrings/charm.gpg 2>/dev/null; then\n\t\t\techo \"deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *\" \\\n\t\t\t\t| tee /etc/apt/sources.list.d/charm.list >/dev/null\n\t\t\tapt update -y 2>/dev/null && apt install -y gum 2>/dev/null\n\t\tfi\n\t\tcommand -v gum >/dev/null 2>&1 || _install_gum_binary\n\telif command -v dnf &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tdnf install -y gum\n\telif command -v yum &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tyum install -y gum\n\telif command -v apk &>/dev/null; then\n\t\tapk add gum 2>/dev/null || _install_gum_binary\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm gum 2>/dev/null || _install_gum_binary\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y gum 2>/dev/null || _install_gum_binary\n\telse\n\t\t_install_gum_binary\n\tfi\n\tcommand -v gum >/dev/null 2>&1 || { echo \"gum 安装失败,请手动安装: https://github.com/charmbracelet/gum\"; return 1; }\n}\n\ninstall_fzf() {\n\tcommand -v fzf >/dev/null 2>&1 && return 0\n\techo \"正在安装 fzf...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install fzf\n\telif command -v apt >/dev/null 2>&1; then\n\t\tapt install -y fzf\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y fzf\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y fzf\n\telif command -v apk &>/dev/null; then\n\t\tapk add fzf\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm fzf\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y fzf\n\telse\n\t\techo \"无法自动安装 fzf,请手动安装: https://github.com/junegunn/fzf\"\n\t\treturn 1\n\tfi\n}\n\n_install_shortcut() {\n\tlocal store_dir shortcut_dir shortcut\n\n\tstore_dir=\"$HOME/.local/bin\"\n\tmkdir -p \"$store_dir\"\n\n\tlocal _candidate_dirs=(\"/opt/homebrew/bin\" \"/usr/local/bin\")\n\tshortcut_dir=\"\"\n\tfor _d in \"${_candidate_dirs[@]}\"; do\n\t\tif [[ -d \"$_d\" && -w \"$_d\" ]]; then\n\t\t\tshortcut_dir=\"$_d\"\n\t\t\tbreak\n\t\tfi\n\tdone\n\n\tif [[ -z \"$shortcut_dir\" ]]; then\n\t\tshortcut_dir=\"$store_dir\"\n\t\tlocal rc_file=\"\"\n\t\t[[ -f \"$HOME/.zshrc\" ]] && rc_file=\"$HOME/.zshrc\"\n\t\t[[ -f \"$HOME/.bashrc\" ]] && rc_file=\"${rc_file:-$HOME/.bashrc}\"\n\t\tif [[ -n \"$rc_file\" ]] && ! grep -q \"$store_dir\" \"$rc_file\" 2>/dev/null; then\n\t\t\tprintf '\\nexport PATH=\"%s:$PATH\"\\n' \"$store_dir\" >> \"$rc_file\"\n\t\tfi\n\t\texport PATH=\"$store_dir:$PATH\"\n\tfi\n\n\tshortcut=\"$shortcut_dir/oc\"\n\n\tcat > \"$shortcut\" <<EOF\n#!/usr/bin/env bash\ncurl -fsSL https://raw.githubusercontent.com/byJoey/openclawctl/main/openclaw.sh \\\\\n -o \"$store_dir/openclawctl.sh\" 2>/dev/null && chmod +x \"$store_dir/openclawctl.sh\"\nexec bash \"$store_dir/openclawctl.sh\" \"\\$@\"\nEOF\n\tchmod +x \"$shortcut\"\n}\n\nmoltbot_menu() {\n\tis_macos && _ensure_brew\n\n\t_install_shortcut\n\n\tinstall_base_deps\n\tinstall_gum || { echo \"gum 安装失败,无法继续\"; return 1; }\n\tinstall_fzf || { echo \"fzf 安装失败,无法继续\"; return 1; }\n\n\tui_header() {\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"0 2\" \"$*\"\n\t\techo\n\t}\n\tui_ok() { gum style --foreground 46 \" ◉ $*\"; }\n\tui_err() { gum style --foreground 196 \" ✗ $*\"; }\n\tui_warn() { gum style --foreground 208 \" ⚡ $*\"; }\n\tui_info() { gum style --foreground 51 \" ◈ $*\"; }\n\tui_step() { echo; gum style --bold --foreground 201 \" ▶ $*\"; echo; }\n\n\tcheck_openclaw_update() {\n\t\tif ! command -v npm >/dev/null 2>&1; then\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal local_version remote_version\n\t\tlocal_version=$(npm list -g openclaw --depth=0 --no-update-notifier 2>/dev/null \\\n\t\t\t| grep openclaw | awk '{print $NF}' | sed 's/^.*@//')\n\n\t\t[[ -z \"$local_version\" ]] && return 1\n\n\t\tremote_version=$(npm view openclaw version --no-update-notifier 2>/dev/null)\n\n\t\t[[ -z \"$remote_version\" ]] && return 1\n\n\t\tif [[ \"$local_version\" != \"$remote_version\" ]]; then\n\t\t\techo -e \"\\033[38;5;208m⚡ UPDATE AVAILABLE $remote_version\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m✦ v$local_version\\033[0m\"\n\t\tfi\n\t}\n\n\tget_install_status() {\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m◉ INSTALLED\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m○ NOT FOUND\\033[0m\"\n\t\tfi\n\t}\n\n\tget_running_status() {\n\t\tif pgrep -f \"openclaw.*gatewa\" >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m■ STOPPED\\033[0m\"\n\t\tfi\n\t}\n\n\tstart_gateway() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tlaunchctl unload \"$plist\" >/dev/null 2>&1 || true\n\t\t\tif [[ -f \"$plist\" ]]; then\n\t\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- \\\n\t\t\t\t\tbash -c \"launchctl load '$plist' 2>/dev/null; sleep 2\"\n\t\t\telse\n\t\t\t\tgum spin --spinner pulse --title \"正在安装并启动网关...\" -- \\\n\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\telse\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- openclaw gateway start\n\t\t\tsleep 1\n\t\tfi\n\t}\n\n\tinstall_node_and_tools() {\n\t\tif command -v node &>/dev/null && command -v npm &>/dev/null; then\n\t\t\treturn 0\n\t\tfi\n\t\techo \"正在安装 Node.js...\"\n\t\tif is_macos; then\n\t\t\t_ensure_brew || return 1\n\t\t\tbrew install node &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tcurl -fsSL https://rpm.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tdnf install -y cmake libatomic nodejs &>/dev/null\n\t\telif command -v apt &>/dev/null; then\n\t\t\tcurl -fsSL https://deb.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y build-essential python3 libatomic1 nodejs &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t}\n\n\tconfigure_openclaw_session_policy() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 1\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport json, sys\npath = sys.argv[1]\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nsession = obj.setdefault('session', {})\nsession['dmScope'] = session.get('dmScope', 'per-channel-peer')\nsession['resetTriggers'] = ['/new', '/reset']\nsession['reset'] = {\n 'mode': 'idle',\n 'idleMinutes': 10080\n}\nsession['resetByType'] = {\n 'direct': {'mode': 'idle', 'idleMinutes': 10080},\n 'thread': {'mode': 'idle', 'idleMinutes': 1440},\n 'group': {'mode': 'idle', 'idleMinutes': 120}\n}\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(obj, f, ensure_ascii=False, indent=2)\n f.write('\\n')\nPY\n\t}\n\n\tsync_openclaw_api_models() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 0\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('未检测到 API providers,跳过模型同步')\n raise SystemExit(0)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nchanged = False\nfatal_errors = []\nsummary = []\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\ndef prompt_delete_provider(name):\n prompt = f\"{name} /models 探测连续失败 3 次。是否删除该 API 供应商及其全部相关模型?[y/N]: \"\n try:\n ans = input(prompt).strip().lower()\n except EOFError:\n return False\n return ans in ('y', 'yes')\n\n\ndef rebind_defaults_before_delete(name):\n global changed\n\n replacement = None\n\n def get_replacement():\n nonlocal replacement\n if replacement is None:\n candidates = collect_available_refs(exclude_provider=name)\n replacement = candidates[0] if candidates else None\n return replacement\n\n primary_ref = get_primary_ref(defaults)\n if ref_provider(primary_ref) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: 默认主模型指向该 provider,但无可用替代模型,已中止删除')\n return False\n set_primary_ref(defaults, repl)\n changed = True\n summary.append(f'已切换默认主模型: {primary_ref} -> {repl}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: {fk} 指向该 provider,但无可用替代模型,已中止删除')\n return False\n defaults[fk] = repl\n changed = True\n summary.append(f'已切换 {fk}: {val} -> {repl}')\n\n return True\n\n\ndef delete_provider_and_refs(name):\n global changed\n\n if not rebind_defaults_before_delete(name):\n return False\n\n removed_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\n for r in removed_refs:\n defaults_models.pop(r, None)\n if removed_refs:\n changed = True\n\n if name in providers:\n providers.pop(name, None)\n changed = True\n\n summary.append(f'已删除 provider {name},并移除 defaults.models 下 {len(removed_refs)} 个模型引用')\n return True\n\n\ndef fetch_remote_models_with_retry(name, base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n data = json.loads(payload)\n return data, None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\nfor name, provider in list(providers.items()):\n if not isinstance(provider, dict):\n summary.append(f'跳过 {name}: provider 结构非法')\n continue\n\n api = provider.get('api', '')\n base_url = provider.get('baseUrl')\n api_key = provider.get('apiKey')\n model_list = provider.get('models', [])\n\n if not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n summary.append(f'跳过 {name}: 无 baseUrl/apiKey/models')\n continue\n\n if api not in SUPPORTED_APIS:\n summary.append(f'跳过 {name}: 不支持直接 /models 校验 (api={api})')\n continue\n\n data, err, attempts = fetch_remote_models_with_retry(name, base_url, api_key, retries=3)\n if err is not None:\n summary.append(f'警告 - {name}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n if prompt_delete_provider(name):\n deleted = delete_provider_and_refs(name)\n if deleted:\n summary.append(f'{name}: 用户已确认删除该 provider 及全部相关模型引用')\n else:\n summary.append(f'{name}: 用户未确认删除,保留现有 provider 配置')\n continue\n\n if attempts > 1:\n summary.append(f'{name}: /models 第 {attempts} 次重试后成功')\n\n if not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n summary.append(f'警告 - 跳过 {name}: /models 返回结构不可识别')\n continue\n\n remote_ids = []\n for item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\n remote_set = set(remote_ids)\n\n if not remote_set:\n fatal_errors.append(f'错误 - {name} 上游 /models 为空,无法为该 provider 提供兜底模型')\n continue\n\n local_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\n local_ids = [str(m['id']) for m in local_models]\n local_set = set(local_ids)\n\n template = None\n for m in local_models:\n template = copy.deepcopy(m)\n break\n if template is None:\n summary.append(f'警告 - 跳过 {name}: 本地 models 无有效模板模型')\n continue\n\n removed_ids = [mid for mid in local_ids if mid not in remote_set]\n added_ids = [mid for mid in remote_ids if mid not in local_set]\n\n kept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\n new_models = kept_models[:]\n\n for mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{name} / {mid}'\n new_models.append(nm)\n\n if not new_models:\n fatal_errors.append(f'错误 - {name} 同步后无可用模型,无法保障默认模型/回退模型兜底')\n continue\n\n expected_refs = {model_ref(name, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\n local_refs = {model_ref(name, mid) for mid in local_ids}\n\n first_ref = model_ref(name, str(new_models[0]['id']))\n\n primary_ref = get_primary_ref(defaults)\n if isinstance(primary_ref, str) and primary_ref in (local_refs - expected_refs):\n set_primary_ref(defaults, first_ref)\n changed = True\n summary.append(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in (local_refs - expected_refs):\n defaults[fk] = first_ref\n changed = True\n summary.append(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\n stale_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/') and r not in expected_refs]\n for r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\n for r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\n if removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\n summary.append(f'{name}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\n\nif fatal_errors:\n for line in summary:\n print(line)\n for err in fatal_errors:\n print(err)\n print('模型同步失败:存在 provider 同步后无可用模型,已中止写入')\n raise SystemExit(2)\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n for line in summary:\n print(line)\n print('OpenClaw API 模型一致性同步完成')\nelse:\n for line in summary:\n print(line)\n print('无需同步:配置已与上游 /models 保持一致')\nPY\n\t}\n\n\t_install_openclaw_core() {\n\t\tinstall_node_and_tools\n\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf ssh://git@github.com/\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf git@github.com:\n\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_prefix npm_bin\n\t\tnpm_prefix=$(npm prefix -g 2>/dev/null)\n\t\tnpm_bin=\"${npm_prefix}/bin\"\n\t\tif [[ -n \"$npm_bin\" ]]; then\n\t\t\texport PATH=\"$npm_bin:$PATH\"\n\t\t\tfor rc in \"$HOME/.bashrc\" \"$HOME/.zshrc\" \"$HOME/.profile\"; do\n\t\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t\tgrep -qF \"$npm_bin\" \"$rc\" 2>/dev/null && continue\n\t\t\t\techo \"export PATH=\\\"${npm_bin}:\\$PATH\\\"\" >> \"$rc\"\n\t\t\tdone\n\t\tfi\n\t\topenclaw onboard --install-daemon\n\t\t_sed_i 's|\"profile\": \"messaging\"|\"profile\": \"full\"|g' ~/.openclaw/openclaw.json\n\t\tconfigure_openclaw_session_policy\n\t\tif ! is_macos; then\n\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tstart_gateway\n\t}\n\n\tinstall_moltbot() {\n\t\t_install_openclaw_core\n\t\tbreak_end\n\t}\n\n\t_cliproxy_dir() {\n\t\tif is_macos; then\n\t\t\techo \"$(brew --prefix 2>/dev/null || echo /usr/local)/etc\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi\"\n\t\tfi\n\t}\n\n\t_cliproxy_conf() {\n\t\tif is_macos; then\n\t\t\techo \"$(_cliproxy_dir)/cliproxyapi.conf\"\n\t\telse\n\t\t\techo \"$(_cliproxy_dir)/config.yaml\"\n\t\tfi\n\t}\n\n\t_cliproxy_bin() {\n\t\tif is_macos; then\n\t\t\tcommand -v cliproxyapi 2>/dev/null || echo \"cliproxyapi\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi/cli-proxy-api\"\n\t\tfi\n\t}\n\n\t_cliproxy_running() {\n\t\tif is_macos; then\n\t\t\tpgrep -f \"cliproxyapi\" >/dev/null 2>&1\n\t\telse\n\t\t\tpgrep -f \"cli-proxy-api\" >/dev/null 2>&1\n\t\tfi\n\t}\n\n\t_cliproxy_start_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services start cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tsystemctl --user enable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"systemctl --user start cliproxyapi.service 2>/dev/null; sleep 3\"\n\t\t\tif ! _cliproxy_running; then\n\t\t\t\tui_warn \"systemd 启动失败,改用后台直接运行...\"\n\t\t\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\t\t\t(cd \"$cliproxy_dir\" && nohup ./cli-proxy-api > /tmp/cliproxyapi.log 2>&1 &)\n\t\t\t\tsleep 3\n\t\t\tfi\n\t\tfi\n\t\tif _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已启动\"\n\t\t\treturn 0\n\t\telse\n\t\t\tui_err \"CLIProxyAPI 启动失败,请检查 $(_cliproxy_conf) 后手动启动\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\t_cliproxy_stop_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services stop cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbash -c \"systemctl --user stop cliproxyapi.service 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\tfi\n\t\tlocal _pat; _pat=$(is_macos && echo \"cliproxyapi\" || echo \"cli-proxy-api\")\n\t\tlocal pids; pids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\tif [[ -n \"$pids\" ]]; then\n\t\t\techo \"$pids\" | xargs kill 2>/dev/null || true\n\t\t\tsleep 2\n\t\t\tpids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\t\t[[ -n \"$pids\" ]] && echo \"$pids\" | xargs kill -9 2>/dev/null || true\n\t\tfi\n\t\tif ! _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已停止\"\n\t\telse\n\t\t\tui_err \"停止失败,仍有进程残留\"\n\t\tfi\n\t}\n\n\t_cliproxy_oauth_login() {\n\t\tlocal cliproxy_bin; cliproxy_bin=$(_cliproxy_bin)\n\t\tif ! command -v \"$cliproxy_bin\" &>/dev/null && [[ ! -x \"$cliproxy_bin\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 未安装:$cliproxy_bin\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_choice\n\t\tprovider_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' 选择要登录的 AI 提供商\\n ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"Claude (Anthropic)\" \\\n\t\t\t\"Gemini (Google)\" \\\n\t\t\t\"OpenAI / Codex\" \\\n\t\t\t\"Qwen (通义千问)\" \\\n\t\t\t\"iFlow\" \\\n\t\t\t\"取消\") || return 0\n\n\t\tlocal login_cmd=\"\" login_port=\"\"\n\t\tcase \"$provider_choice\" in\n\t\t\t\"Claude (Anthropic)\") login_cmd=\"--claude-login\"; login_port=\"54545\" ;;\n\t\t\t\"Gemini (Google)\") login_cmd=\"--login\"; login_port=\"8085\" ;;\n\t\t\t\"OpenAI / Codex\") login_cmd=\"--codex-login\"; login_port=\"1455\" ;;\n\t\t\t\"Qwen (通义千问)\") login_cmd=\"--qwen-login\"; login_port=\"\" ;;\n\t\t\t\"iFlow\") login_cmd=\"--iflow-login\"; login_port=\"11451\" ;;\n\t\t\t\"取消\"|*) return 0 ;;\n\t\tesac\n\n\t\techo\n\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\n\t\tlocal mode_input=\"\" proj_input=\"\"\n\t\tif [[ \"$login_cmd\" == \"--login\" ]]; then\n\t\t\tlocal mode_choice\n\t\t\tmode_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择 Gemini 登录模式\\n ↑↓ 移动 · Enter 确认' \\\n\t\t\t\t\"Code Assist(GCP 项目,手动选择)\" \\\n\t\t\t\t\"Google One(个人账号,自动发现项目)\") || return 0\n\t\t\t[[ \"$mode_choice\" == \"Code Assist\"* ]] && mode_input=\"1\" || mode_input=\"2\"\n\n\t\t\tif [[ \"$mode_input\" == \"1\" ]]; then\n\t\t\t\techo\n\t\t\t\tlocal proj_list proj_choice proj_num=\"1\"\n\t\t\t\tif command -v gcloud &>/dev/null; then\n\t\t\t\t\tproj_list=$(gcloud projects list --format=\"value(projectId,name)\" 2>/dev/null)\n\t\t\t\tfi\n\t\t\t\tif [[ -n \"$proj_list\" ]]; then\n\t\t\t\t\tproj_choice=$(printf \"%s\\n\" \"$proj_list\" | \\\n\t\t\t\t\t\tgum choose --cursor \"❯ \" \\\n\t\t\t\t\t\t--header $' 选择 GCP 项目\\n ↑↓ 移动 · Enter 确认') || return 0\n\t\t\t\t\tproj_num=$(printf \"%s\\n\" \"$proj_list\" | \\\n\t\t\t\t\t\tawk -v sel=\"$proj_choice\" 'NR==sel_line || $0==sel {print NR; exit}' \\\n\t\t\t\t\t\tsel=\"$proj_choice\" 2>/dev/null || echo \"1\")\n\t\t\t\t\t[[ -z \"$proj_num\" || \"$proj_num\" == \"0\" ]] && proj_num=\"1\"\n\t\t\t\telse\n\t\t\t\t\tlocal proj_raw\n\t\t\t\t\tproj_raw=$(gum input \\\n\t\t\t\t\t\t--placeholder \"项目编号或直接按 Enter 使用默认(1)\" \\\n\t\t\t\t\t\t--prompt \" ❯ \" --width 60) || return 0\n\t\t\t\t\tproj_num=\"${proj_raw:-1}\"\n\t\t\t\tfi\n\t\t\t\tproj_input=\"$proj_num\"\n\t\t\tfi\n\t\tfi\n\n\t\tif [[ -n \"$login_port\" ]]; then\n\t\t\tif is_macos; then\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 本地浏览器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 浏览器将自动打开,完成授权后自动返回,请稍候...\"\n\t\t\t\techo\n\t\t\t\tif [[ -n \"$mode_input\" ]]; then\n\t\t\t\t\tprintf \"%s\\n%s\\n\" \"$mode_input\" \"$proj_input\" | \"$cliproxy_bin\" \"$login_cmd\"\n\t\t\t\telse\n\t\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\t\tfi\n\t\t\telse\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 服务器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 1. 脚本后台启动 cli-proxy-api,并打印 OAuth 授权链接\" \\\n\t\t\t\t\t\" 2. 在本地浏览器打开该链接,完成账号授权\" \\\n\t\t\t\t\t\" 3. 授权后浏览器跳转到 localhost:${login_port},页面报错属正常\" \\\n\t\t\t\t\t\" 4. 复制地址栏完整 URL,粘贴到下方提示符,脚本自动完成握手\"\n\t\t\t\techo\n\t\t\t\tgum style --foreground 208 \\\n\t\t\t\t\t\" ⚡ 无需端口转发 — 回调 URL 里已含 code,直接转发给本机处理\"\n\t\t\t\techo\n\n\t\t\t\tif [[ -n \"$mode_input\" ]]; then\n\t\t\t\t\tlocal fifo; fifo=$(mktemp -u /tmp/cp_login_XXXXXX)\n\t\t\t\t\tmkfifo \"$fifo\"\n\t\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser < \"$fifo\") &\n\t\t\t\t\tlocal cli_pid=$!\n\t\t\t\t\texec 9>\"$fifo\"\n\t\t\t\t\tprintf \"%s\\n\" \"$mode_input\" >&9\n\t\t\t\t\tsleep 0.3\n\t\t\t\t\t[[ -n \"$proj_input\" ]] && { printf \"%s\\n\" \"$proj_input\" >&9; sleep 0.3; }\n\t\t\t\t\texec 9>&-\n\t\t\t\t\trm -f \"$fifo\"\n\t\t\t\telse\n\t\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser) &\n\t\t\t\t\tlocal cli_pid=$!\n\t\t\t\tfi\n\t\t\t\tsleep 2\n\n\t\t\t\techo\n\t\t\t\tlocal callback_url\n\t\t\t\tcallback_url=$(gum input \\\n\t\t\t\t\t--placeholder \"http://localhost:${login_port}/oauth2callback?code=...\" \\\n\t\t\t\t\t--prompt \" ❯ \" \\\n\t\t\t\t\t--width 120 \\\n\t\t\t\t\t--char-limit 2000) || true\n\n\t\t\t\tif [[ -n \"$callback_url\" ]]; then\n\t\t\t\t\tui_info \"正在向本机发送回调...\"\n\t\t\t\t\tcurl -sf \"$callback_url\" > /dev/null 2>&1 || true\n\t\t\t\t\tsleep 1\n\t\t\t\tfi\n\t\t\t\twait \"$cli_pid\" 2>/dev/null || true\n\t\t\tfi\n\t\telse\n\t\t\tui_info \"Qwen 使用 device flow,无需粘贴回调\"\n\t\t\techo\n\t\t\tgum style --foreground 240 \"将打印授权码 + URL,在任意浏览器中打开并输入授权码即可\"\n\t\t\techo\n\t\t\tif is_macos; then\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser)\n\t\t\tfi\n\t\tfi\n\t}\n\n\tcliproxyapi_manage_menu() {\n\t\tlocal config_file; config_file=$(_cliproxy_conf)\n\n\t\twhile true; do\n\t\t\tclear\n\n\t\t\tlocal cp_installed cp_running cp_version cp_port cp_keys\n\t\t\tif is_macos; then\n\t\t\t\tcp_version=$(brew info cliproxyapi 2>/dev/null | awk 'NR==1{print $4}')\n\t\t\telse\n\t\t\t\tcp_version=$(cat \"$(_cliproxy_dir)/version.txt\" 2>/dev/null || echo \"\")\n\t\t\tfi\n\t\t\tif [[ -n \"$cp_version\" ]]; then\n\t\t\t\tcp_installed=\"\\033[38;5;46m◉ INSTALLED v${cp_version}\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_installed=\"\\033[90m○ NOT INSTALLED\\033[0m\"\n\t\t\tfi\n\t\t\tif _cliproxy_running; then\n\t\t\t\tcp_running=\"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_running=\"\\033[90m■ STOPPED\\033[0m\"\n\t\t\tfi\n\t\t\tcp_port=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\t\t\tcp_port=\"${cp_port:-8317}\"\n\t\t\tcp_keys=$(awk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{n++} END{print n+0}' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 51 \\\n\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t--padding \"1 4\" --align center \\\n\t\t\t\t\"C L I P R O X Y A P I\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ AI OAuth Proxy Manager ]\"\n\t\t\techo -e \" $cp_installed $cp_running\"\n\t\t\techo -e \" \\033[90m端口 :${cp_port} API Keys: ${cp_keys}\\033[0m\"\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' ─── CLIProxyAPI ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"启动\" \\\n\t\t\t\t\"停止\" \\\n\t\t\t\t\"重启\" \\\n\t\t\t\t\"查看日志\" \\\n\t\t\t\t\"账号授权登录\" \\\n\t\t\t\t\"生成并添加 API Key\" \\\n\t\t\t\t\"查看 API Keys\" \\\n\t\t\t\t\"编辑配置文件\" \\\n\t\t\t\t\"更新\" \\\n\t\t\t\t\"卸载\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"启动\")\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"停止\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"重启\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tsleep 1\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看日志\")\n\t\t\t\t\techo\n\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\tlocal brew_log\n\t\t\t\t\t\tbrew_log=\"$(brew --prefix 2>/dev/null)/var/log/cliproxyapi.log\"\n\t\t\t\t\t\tif [[ -f \"$brew_log\" ]]; then\n\t\t\t\t\t\t\ttail -80 \"$brew_log\"\n\t\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\telif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\t\t\tjournalctl --user -u cliproxyapi.service --no-pager -n 80\n\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"账号授权登录\")\n\t\t\t\t\t_cliproxy_oauth_login\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"生成并添加 API Key\")\n\t\t\t\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\t\tlocal new_key\n\t\t\t\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\t\t\t\techo\n\t\t\t\t\tgum style --bold --foreground 46 \" ◉ 已生成新 API Key:\"\n\t\t\t\t\tgum style --bold --foreground 51 \" $new_key\"\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 将此 Key 自动写入 config.yaml?\"; then\n\t\t\t\t\t\tpython3 - \"$config_file\" \"$new_key\" <<'PY'\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n)', r'\\1 - \"' + key + '\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nprint(\"写入成功\")\nPY\n\t\t\t\t\t\tui_ok \"API Key 已写入配置\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看 API Keys\")\n\t\t\t\t\techo\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tgum style --foreground 240 \" config.yaml 中的 API Keys:\"\n\t\t\t\t\t\techo\n\t\t\t\t\t\tawk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{print \" \" $0}' \\\n\t\t\t\t\t\t\t\"$config_file\"\n\t\t\t\t\t\techo\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到配置文件:$config_file\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"编辑配置文件\")\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tnano \"$config_file\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\tfi\n\t\t\t\t\t;;\n\t\t\t\t\"更新\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 确认更新 CLIProxyAPI 到最新版本?\"; then\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew upgrade cliproxyapi && ui_ok \"更新完成\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tcurl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer \\\n\t\t\t\t\t\t\t\t| bash -s -- upgrade\n\t\t\t\t\t\t\tui_ok \"更新完成\"\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"卸载\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" ⚡ 确认卸载 CLIProxyAPI?\"; then\n\t\t\t\t\t\t_cliproxy_stop_service 2>/dev/null || true\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew uninstall cliproxyapi 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$HOME/.cli-proxy-api\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tsystemctl --user disable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\t\t\t\t\trm -f \"$HOME/.config/systemd/user/cliproxyapi.service\" 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$(_cliproxy_dir)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\t\tui_ok \"CLIProxyAPI 已卸载\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tbeginner_mode_install() {\n\t\tif is_macos; then\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ API Key 配置 ]\"\n\t\telse\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ CLIProxyAPI ⟶ AI Auth ⟶ Auto Config ]\"\n\t\tfi\n\t\techo\n\n\t\tui_step \"第 1 步:安装 OpenClaw\"\n\t\t_install_openclaw_core\n\n\t\tui_step \"第 2 步:安装 CLIProxyAPI\"\n\t\tif is_macos; then\n\t\t\tif ! gum spin --spinner globe --title \"正在安装 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbrew install cliproxyapi; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败(brew install cliproxyapi)\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\telse\n\t\t\tif ! curl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer | bash; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\n\t\tlocal cliproxy_config; cliproxy_config=$(_cliproxy_conf)\n\t\tif [[ ! -f \"$cliproxy_config\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 安装后未找到配置文件: $cliproxy_config\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\t\tui_ok \"CLIProxyAPI 安装完成\"\n\n\t\tui_step \"第 3 步:登录 AI 账号\"\n\t\tui_info \"通过 OAuth 授权即可免费使用,无需付费 API Key\"\n\t\techo\n\t\t_cliproxy_oauth_login\n\n\t\tui_step \"第 4 步:生成 API Key 并启动服务\"\n\t\tlocal new_key\n\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\tpython3 - \"$cliproxy_config\" \"$new_key\" <<'PY' 2>/dev/null\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n(?:\\s*-\\s*\"[^\"]*\"\\s*\\n)*)', lambda m: m.group(0) + f' - \"{key}\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nPY\n\t\tif ! _cliproxy_start_service; then\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_step \"第 5 步:配置 OpenClaw API\"\n\n\t\tlocal api_key port base_url\n\t\tapi_key=$(awk '\n\t\t\t/^api-keys:/ { in_keys=1; next }\n\t\t\tin_keys && /^[^ \\t]/ { exit }\n\t\t\tin_keys && /\"sk-/ { match($0, /sk-[^\"]+/); print substr($0, RSTART, RLENGTH); exit }\n\t\t' \"$cliproxy_config\" 2>/dev/null)\n\n\t\tport=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \"$cliproxy_config\" 2>/dev/null)\n\t\tport=\"${port:-8317}\"\n\t\tbase_url=\"http://localhost:${port}/v1\"\n\n\t\tif [[ -z \"$api_key\" ]]; then\n\t\t\tui_err \"无法从 $cliproxy_config 读取 API Key,请手动在「CLIProxyAPI 管理」中添加\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_info \"地址:$base_url\"\n\t\tui_info \"Key:${api_key:0:12}****\"\n\t\techo\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"cliproxy\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tstart_gateway\n\n\t\t\tui_step \"第 6 步:选择默认模型\"\n\t\t\tui_info \"请从可用模型中选择一个作为默认,选完后网关自动重启\"\n\t\t\techo\n\t\t\tchange_model\n\n\t\t\techo\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 46 \\\n\t\t\t\t--border double --border-foreground 46 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◉ INSTALL COMPLETE ◉\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw 已自动接入 CLIProxyAPI,可直接开始使用 ]\" \\\n\t\t\t\t\"[ 更多提供商:cd ~/cliproxyapi && ./cli-proxy-api --help ]\"\n\t\tfi\n\n\t\techo\n\t\tif gum confirm \" 现在去对接机器人?\"; then\n\t\t\tchange_tg_bot_code\n\t\telse\n\t\t\tbreak_end\n\t\tfi\n\t}\n\n\tstart_bot() {\n\t\tstart_gateway\n\t\tbreak_end\n\t}\n\n\tstop_bot() {\n\t\ttmux kill-session -t gateway > /dev/null 2>&1\n\t\tgum spin --spinner pulse --title \"正在停止网关...\" -- openclaw gateway stop\n\t\tbreak_end\n\t}\n\n\tview_logs() {\n\t\topenclaw status\n\t\topenclaw gateway status\n\t\topenclaw logs\n\t\tbreak_end\n\t}\n\n\tadd-all-models-from-provider() {\n\t\tlocal provider_name=\"$1\"\n\t\tlocal base_url=\"$2\"\n\t\tlocal api_key=\"$3\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tlocal models_tmpfile\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取 $provider_name 的模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\n\t\tlocal models_json\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tif [[ -z \"$models_json\" ]]; then\n\t\t\techo \"错误:无法获取模型列表\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_ids\n\t\tmodel_ids=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n for m in models:\n if isinstance(m, dict) and \"id\" in m:\n print(m[\"id\"])\nexcept Exception:\n pass\n')\n\n\t\tif [[ -z \"$model_ids\" ]]; then\n\t\t\techo \"错误:未找到任何模型\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_count\n\t\tmodel_count=$(echo \"$model_ids\" | wc -l | tr -d ' ')\n\t\techo \"发现 $model_count 个模型\"\n\n\t\tlocal models_array=\"[\"\n\t\tlocal first=true\n\n\t\twhile read -r model_id; do\n\t\t\t[[ $first == false ]] && models_array+=\",\"\n\t\t\tfirst=false\n\n\t\t\tlocal context_window=1048576\n\t\t\tlocal max_tokens=128000\n\t\t\tlocal input_cost=0.15\n\t\t\tlocal output_cost=0.60\n\n\t\t\tcase \"$model_id\" in\n\t\t\t\t*opus*|*pro*|*preview*|*thinking*|*sonnet*)\n\t\t\t\t\tinput_cost=2.00\n\t\t\t\t\toutput_cost=12.00\n\t\t\t\t\t;;\n\t\t\t\t*gpt-5*|*codex*)\n\t\t\t\t\tinput_cost=1.25\n\t\t\t\t\toutput_cost=10.00\n\t\t\t\t\t;;\n\t\t\t\t*flash*|*lite*|*haiku*|*mini*|*nano*)\n\t\t\t\t\tinput_cost=0.10\n\t\t\t\t\toutput_cost=0.40\n\t\t\t\t\t;;\n\t\t\tesac\n\n\t\t\tmodels_array+=$(cat <<EOF\n{\n\t\"id\": \"$model_id\",\n\t\"name\": \"$provider_name / $model_id\",\n\t\"input\": [\"text\", \"image\"],\n\t\"contextWindow\": $context_window,\n\t\"maxTokens\": $max_tokens,\n\t\"cost\": {\n\t\t\"input\": $input_cost,\n\t\t\"output\": $output_cost,\n\t\t\"cacheRead\": 0,\n\t\t\"cacheWrite\": 0\n\t}\n}\nEOF\n)\n\t\tdone <<< \"$model_ids\"\n\n\t\tmodels_array+=\"]\"\n\n\t\t[[ -f \"$config_file\" ]] && cp \"$config_file\" \"${config_file}.bak.$(date +%s)\"\n\n\t\tjq --arg prov \"$provider_name\" \\\n\t\t --arg url \"$base_url\" \\\n\t\t --arg key \"$api_key\" \\\n\t\t --argjson models \"$models_array\" \\\n\t\t'\n\t\t.models |= (\n\t\t\t(. // { mode: \"merge\", providers: {} })\n\t\t\t| .mode = \"merge\"\n\t\t\t| .providers[$prov] = {\n\t\t\t\tbaseUrl: $url,\n\t\t\t\tapiKey: $key,\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tmodels: $models\n\t\t\t}\n\t\t)\n\t\t| .agents |= (. // {})\n\t\t| .agents.defaults |= (. // {})\n\t\t| .agents.defaults.models |= (\n\t\t\t(if type == \"object\" then .\n\t\t\t elif type == \"array\" then reduce .[] as $m ({}; if ($m|type) == \"string\" then .[$m] = {} else . end)\n\t\t\t else {}\n\t\t\t end) as $existing\n\t\t\t| reduce ($models[]? | .id? // empty | tostring) as $mid (\n\t\t\t\t$existing;\n\t\t\t\tif ($mid | length) > 0 then\n\t\t\t\t\t.[\"\\($prov)/\\($mid)\"] //= {}\n\t\t\t\telse\n\t\t\t\t\t.\n\t\t\t\tend\n\t\t\t)\n\t\t)\n\t\t' \"$config_file\" > \"${config_file}.tmp\" && mv \"${config_file}.tmp\" \"$config_file\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\techo \"成功添加 $model_count 个模型到 $provider_name\"\n\t\t\techo \"模型引用格式: $provider_name/<model-id>\"\n\t\t\treturn 0\n\t\telse\n\t\t\techo \"错误:配置注入失败\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\tadd-openclaw-provider-interactive() {\n\t\tlocal provider_name base_url api_key\n\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"Provider 名称 (如: deepseek)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\t[[ -z \"$provider_name\" ]] && return 1\n\n\t\tbase_url=$(gum input \\\n\t\t\t--placeholder \"Base URL (如: https://api.xxx.com/v1)\" \\\n\t\t\t--prompt \"Base URL > \")\n\t\t[[ -z \"$base_url\" ]] && return 1\n\t\tbase_url=\"${base_url%/}\"\n\n\t\tapi_key=$(gum input --password \\\n\t\t\t--placeholder \"API Key\" \\\n\t\t\t--prompt \"API Key > \")\n\t\t[[ -z \"$api_key\" ]] && return 1\n\n\t\tlocal models_tmpfile models_json available_models model_count\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tlocal default_model=\"\"\n\t\tif [[ -n \"$models_json\" ]]; then\n\t\t\tavailable_models=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n ids = sorted(m[\"id\"] for m in models if isinstance(m, dict) and \"id\" in m)\n print(\"\\n\".join(ids))\nexcept Exception:\n pass\n')\n\t\t\tmodel_count=$(echo \"$available_models\" | wc -l | tr -d ' ')\n\t\tfi\n\n\t\tif [[ -n \"$available_models\" ]]; then\n\t\t\tdefault_model=$(echo \"$available_models\" | fzf \\\n\t\t\t\t--prompt=\" ❯ \" \\\n\t\t\t\t--header=\" 发现 $model_count 个模型 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 选第一个\" \\\n\t\t\t\t--header-first \\\n\t\t\t\t--height=15 \\\n\t\t\t\t--layout=reverse \\\n\t\t\t\t--border=double \\\n\t\t\t\t--border-label=\" ◈ DEFAULT MODEL \" \\\n\t\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || default_model=$(echo \"$available_models\" | head -1)\n\t\t\t[[ -z \"$default_model\" ]] && default_model=$(echo \"$available_models\" | head -1)\n\t\t\tdefault_model=$(awk '{print $1}' <<< \"$default_model\")\n\t\tfi\n\n\t\techo\n\t\tgum style \\\n\t\t\t--border normal --border-foreground 99 \\\n\t\t\t--padding \"0 2\" \\\n\t\t\t\"Provider : $provider_name\" \\\n\t\t\t\"Base URL : $base_url\" \\\n\t\t\t\"API Key : ${api_key:0:8}****\" \\\n\t\t\t\"默认模型 : $default_model\" \\\n\t\t\t\"模型总数 : $model_count\"\n\t\techo\n\n\t\tgum confirm \"确认添加所有 $model_count 个模型?\" || { echo \"已取消\"; return 1; }\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"$provider_name\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tgum spin --spinner minidot --title \"设置默认模型并重启网关...\" -- \\\n\t\t\t\topenclaw models set \"$provider_name/$default_model\"\n\t\t\tstart_gateway\n\t\t\techo \"完成:所有 $model_count 个模型已加载\"\n\t\tfi\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_list() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\twhile IFS=$'\\t' read -r rec_type idx name base_url model_count latency_txt latency_level; do\n\t\t\tcase \"$rec_type\" in\n\t\t\t\tMSG)\n\t\t\t\t\techo \"$idx\"\n\t\t\t\t\t;;\n\t\t\t\tROW)\n\t\t\t\t\tlocal latency_color=\"$gl_bai\"\n\t\t\t\t\tcase \"$latency_level\" in\n\t\t\t\t\t\tlow) latency_color=\"$gl_lv\" ;;\n\t\t\t\t\t\tmedium) latency_color=\"$gl_huang\" ;;\n\t\t\t\t\t\thigh|unavailable) latency_color=\"$gl_hong\" ;;\n\t\t\t\t\t\tunchecked) latency_color=\"$gl_bai\" ;;\n\t\t\t\t\tesac\n\n\t\t\t\t\tprintf '%b\\n' \"[$idx] ${name} | API: ${base_url} | 模型数量: ${gl_huang}${model_count}${gl_bai} | 延迟/状态: ${latency_color}${latency_txt}${gl_bai}\"\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tlocal _py_list; _py_list=$(mktemp /tmp/oc_list_XXXXXX.py)\n\t\tcat > \"$_py_list\" << 'PYEOF'\nimport json, sys, time, urllib.request\n\npath = sys.argv[1]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\ndef ping_models(base_url, api_key):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': 'Bearer ' + api_key,\n 'User-Agent': 'OpenClaw-API-Manage/1.0',\n },\n )\n start = time.perf_counter()\n with urllib.request.urlopen(req, timeout=4) as resp:\n resp.read(2048)\n return int((time.perf_counter() - start) * 1000)\n\ndef classify_latency(latency):\n if latency == '不可用':\n return '不可用', 'unavailable'\n if latency == '未检测':\n return '未检测', 'unchecked'\n if isinstance(latency, int):\n level = 'low' if latency <= 800 else ('medium' if latency <= 2000 else 'high')\n return str(latency) + 'ms', level\n return str(latency), 'unchecked'\n\ntry:\n with open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\nexcept FileNotFoundError:\n print('MSG\\t未找到 openclaw.json,请先完成安装/初始化。')\n raise SystemExit(0)\nexcept Exception as e:\n print('MSG\\t读取配置失败: ' + type(e).__name__ + ': ' + str(e))\n raise SystemExit(0)\n\nproviders = ((obj.get('models') or {}).get('providers') or {})\nif not isinstance(providers, dict) or not providers:\n print('MSG\\t当前未配置任何 API provider。')\n raise SystemExit(0)\n\nprint('MSG\\t--- 已配置 API 列表 ---')\n\nfor idx, name in enumerate(sorted(providers.keys()), start=1):\n provider = providers.get(name)\n if not isinstance(provider, dict):\n base_url = '-'; model_count = 0; latency_raw = '不可用'\n else:\n base_url = provider.get('baseUrl') or provider.get('url') or provider.get('endpoint') or '-'\n models = provider.get('models') if isinstance(provider.get('models'), list) else []\n model_count = sum(1 for m in models if isinstance(m, dict) and m.get('id'))\n api = provider.get('api', '')\n api_key = provider.get('apiKey')\n latency_raw = '未检测'\n if api in SUPPORTED_APIS and isinstance(base_url, str) and base_url != '-' and isinstance(api_key, str) and api_key:\n try:\n latency_raw = ping_models(base_url, api_key)\n except Exception:\n latency_raw = '不可用'\n elif api in SUPPORTED_APIS:\n latency_raw = '不可用'\n\n latency_text, latency_level = classify_latency(latency_raw)\n print('ROW\\t' + '\\t'.join([str(idx), str(name), str(base_url), str(model_count), str(latency_text), str(latency_level)]))\nPYEOF\n\t\tdone < <(python3 \"$_py_list\" \"$config_file\")\n\t\trm -f \"$_py_list\"\n\t}\n\n\tsync-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要同步的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tinstall jq curl >/dev/null 2>&1\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY2'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\ntarget = sys.argv[2]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('错误:未检测到 API providers,无法同步')\n raise SystemExit(2)\n\nprovider = providers.get(target)\nif not isinstance(provider, dict):\n print(f'错误:未找到 provider: {target}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef fetch_remote_models_with_retry(base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n return json.loads(payload), None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\napi = provider.get('api', '')\nbase_url = provider.get('baseUrl')\napi_key = provider.get('apiKey')\nmodel_list = provider.get('models', [])\n\nif not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n print(f'错误:provider {target} 缺少 baseUrl/apiKey/models,无法执行同步')\n raise SystemExit(3)\n\nif api not in SUPPORTED_APIS:\n print(f'错误:provider {target} 当前 api={api},不支持直接 /models 同步')\n raise SystemExit(3)\n\ndata, err, attempts = fetch_remote_models_with_retry(base_url, api_key, retries=3)\nif err is not None:\n print(f'错误:{target}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n raise SystemExit(4)\n\nif not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n print(f'错误:{target}: /models 返回结构不可识别')\n raise SystemExit(4)\n\nremote_ids = []\nfor item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\nremote_set = set(remote_ids)\nif not remote_set:\n print(f'错误:{target}: 上游 /models 为空,已中止同步')\n raise SystemExit(5)\n\nlocal_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\nlocal_ids = [str(m['id']) for m in local_models]\nlocal_set = set(local_ids)\n\ntemplate = copy.deepcopy(local_models[0]) if local_models else None\nif template is None:\n print(f'错误:{target}: 本地 models 无有效模板模型,无法补全新增模型')\n raise SystemExit(3)\n\nremoved_ids = [mid for mid in local_ids if mid not in remote_set]\nadded_ids = [mid for mid in remote_ids if mid not in local_set]\n\nkept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\nnew_models = kept_models[:]\nfor mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{target} / {mid}'\n new_models.append(nm)\n\nif not new_models:\n print(f'错误:{target}: 同步后无可用模型,已中止写入')\n raise SystemExit(5)\n\nexpected_refs = {model_ref(target, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\nlocal_refs = {model_ref(target, mid) for mid in local_ids}\nremoved_refs = local_refs - expected_refs\nfirst_ref = model_ref(target, str(new_models[0]['id']))\n\nchanged = False\nprimary_ref = get_primary_ref(defaults)\nif isinstance(primary_ref, str) and primary_ref in removed_refs:\n set_primary_ref(defaults, first_ref)\n changed = True\n print(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in removed_refs:\n defaults[fk] = first_ref\n changed = True\n print(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\nstale_refs = [r for r in list(defaults_models.keys()) if r.startswith(target + '/') and r not in expected_refs]\nfor r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\nfor r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\nif removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'{target}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\nif changed:\n print('指定 provider 模型一致性同步完成')\nelse:\n print('无需同步:该 provider 配置已与上游 /models 保持一致')\nPY2\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"同步完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在或未配置\" ;;\n\t\t\t3) echo \"错误:provider 配置不完整或类型不支持\" ;;\n\t\t\t4) echo \"错误:上游 /models 请求失败\" ;;\n\t\t\t5) echo \"错误:上游模型为空或同步后无可用模型\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\tdelete-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要删除的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY'\nimport copy\nimport json\nimport sys\n\npath = sys.argv[1]\nname = sys.argv[2]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or name not in providers:\n print(f'错误:未找到 provider: {name}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\nreplacement_candidates = collect_available_refs(exclude_provider=name)\nreplacement = replacement_candidates[0] if replacement_candidates else None\n\nprimary_ref = get_primary_ref(defaults)\nif ref_provider(primary_ref) == name:\n if not replacement:\n print('错误:删除中止:默认主模型指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n set_primary_ref(defaults, replacement)\n print(f'默认主模型切换: {primary_ref} -> {replacement}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n if not replacement:\n print(f'错误:删除中止:{fk} 指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n defaults[fk] = replacement\n print(f'{fk} 切换: {val} -> {replacement}')\n\nremoved_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\nfor r in removed_refs:\n defaults_models.pop(r, None)\n\nproviders.pop(name, None)\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'已删除 provider: {name}')\nprint(f'已清理 defaults.models 中 {len(removed_refs)} 个关联模型引用')\nPY\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"删除完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在\" ;;\n\t\t\t3) echo \"错误:无可用替代模型,已保持原配置\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"API 管理\"\n\t\t\topenclaw_api_manage_list\n\t\t\techo\n\n\t\t\tlocal api_choice\n\t\t\tapi_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加 API\" \\\n\t\t\t\t\"同步 API 供应商模型列表\" \\\n\t\t\t\t\"删除 API\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$api_choice\" in\n\t\t\t\t\"添加 API\") add-openclaw-provider-interactive ;;\n\t\t\t\t\"同步 API 供应商模型列表\") sync-openclaw-provider-interactive ;;\n\t\t\t\t\"删除 API\") delete-openclaw-provider-interactive ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tchange_model() {\n\t\tlocal all_models model\n\t\tall_models=$(openclaw models list --all 2>/dev/null | grep \"configured\")\n\n\t\tif [[ -z \"$all_models\" ]]; then\n\t\t\tui_err \"无法获取可用模型列表(请先添加 API 提供商)\"\n\t\t\tbreak_end\n\t\t\treturn\n\t\tfi\n\n\t\tmodel=$(echo \"$all_models\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 当前: $(openclaw models list 2>/dev/null | awk '{print $1}' | head -1) │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=20 \\\n\t\t\t--layout=reverse \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ◈ MODEL SELECT \" \\\n\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || return\n\n\t\t[[ -z \"$model\" ]] && return\n\t\tmodel=$(awk '{print $1}' <<< \"$model\")\n\n\t\tgum spin --spinner minidot --title \"正在切换模型...\" -- openclaw models set \"$model\"\n\t\tstart_gateway\n\t\tui_ok \"已切换至:$model\"\n\t\tbreak_end\n\t}\n\n\tresolve_openclaw_plugin_id() {\n\t\tlocal raw_input=\"$1\"\n\t\tlocal plugin_id=\"$raw_input\"\n\n\t\tplugin_id=\"${plugin_id#@openclaw/}\"\n\t\tif [[ \"$plugin_id\" == @*/* ]]; then\n\t\t\tplugin_id=\"${plugin_id##*/}\"\n\t\tfi\n\t\tplugin_id=\"${plugin_id%%@*}\"\n\t\techo \"$plugin_id\"\n\t}\n\n\tsync_openclaw_plugin_allowlist() {\n\t\tlocal plugin_id=\"$1\"\n\t\t[[ -z \"$plugin_id\" ]] && return 1\n\n\t\tlocal home_config=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal root_config=\"/root/.openclaw/openclaw.json\"\n\t\tlocal config_file=\"$home_config\"\n\t\tif [[ ! -f \"$home_config\" && -f \"$root_config\" ]]; then\n\t\t\tconfig_file=\"$root_config\"\n\t\tfi\n\n\t\tmkdir -p \"$(dirname \"$config_file\")\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo '{}' > \"$config_file\"\n\t\tfi\n\n\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\tlocal tmp_json\n\t\t\ttmp_json=$(mktemp)\n\t\t\tif jq --arg pid \"$plugin_id\" '\n\t\t\t\t.plugins = (if (.plugins | type) == \"object\" then .plugins else {} end)\n\t\t\t\t| .plugins.allow = (if (.plugins.allow | type) == \"array\" then .plugins.allow else [] end)\n\t\t\t\t| if (.plugins.allow | index($pid)) == null then .plugins.allow += [$pid] else . end\n\t\t\t' \"$config_file\" > \"$tmp_json\" 2>/dev/null && mv \"$tmp_json\" \"$config_file\"; then\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\t\trm -f \"$tmp_json\"\n\t\tfi\n\n\t\tif command -v python3 >/dev/null 2>&1; then\n\t\t\tif python3 - \"$config_file\" \"$plugin_id\" <<'PYTHON_EOF'\nimport json\nimport sys\nfrom pathlib import Path\n\nconfig_file = Path(sys.argv[1])\nplugin_id = sys.argv[2]\n\ntry:\n data = json.loads(config_file.read_text(encoding='utf-8')) if config_file.exists() else {}\n if not isinstance(data, dict):\n data = {}\nexcept Exception:\n data = {}\n\nplugins = data.get('plugins')\nif not isinstance(plugins, dict):\n plugins = {}\n\na = plugins.get('allow')\nif not isinstance(a, list):\n a = []\n\nif plugin_id not in a:\n a.append(plugin_id)\n\nplugins['allow'] = a\ndata['plugins'] = plugins\nconfig_file.write_text(json.dumps(data, ensure_ascii=False, indent=2) + \"\\n\", encoding='utf-8')\nPYTHON_EOF\n\t\t\tthen\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\tfi\n\n\t\techo \"警告:已安装插件,但同步 plugins.allow 失败,请手动检查: $config_file\"\n\t\treturn 1\n\t}\n\n\tinstall_plugin() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"插件管理\"\n\t\t\tgum style --foreground 240 \"当前已安装插件:\"\n\t\t\topenclaw plugins list\n\t\t\techo\n\n\t\t\tlocal raw_input\n\t\t\traw_input=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择要安装的插件\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"feishu 飞书/Lark 集成\" \\\n\t\t\t\t\"telegram Telegram 机器人\" \\\n\t\t\t\t\"slack Slack 企业通讯\" \\\n\t\t\t\t\"msteams Microsoft Teams\" \\\n\t\t\t\t\"discord Discord 社区管理\" \\\n\t\t\t\t\"whatsapp WhatsApp 自动化\" \\\n\t\t\t\t\"memory-core 基础记忆 (文件检索)\" \\\n\t\t\t\t\"memory-lancedb 增强记忆 (向量数据库)\" \\\n\t\t\t\t\"copilot-proxy Copilot 接口转发\" \\\n\t\t\t\t\"lobster 审批流 (带人工确认)\" \\\n\t\t\t\t\"voice-call 语音通话能力\" \\\n\t\t\t\t\"nostr 加密隐私聊天\" \\\n\t\t\t\t\"手动输入插件 ID\") || break\n\n\t\t\tif [[ \"$raw_input\" == \"手动输入插件 ID\" ]]; then\n\t\t\t\traw_input=$(gum input \\\n\t\t\t\t\t--placeholder \"插件 ID (留空退出)\" \\\n\t\t\t\t\t--prompt \"Plugin > \") || break\n\t\t\t\t[[ -z \"$raw_input\" ]] && break\n\t\t\telse\n\t\t\t\traw_input=$(echo \"$raw_input\" | awk '{print $1}')\n\t\t\tfi\n\t\t\t[[ -z \"$raw_input\" ]] && break\n\n\t\t\tlocal plugin_id plugin_full\n\t\t\tplugin_id=$(resolve_openclaw_plugin_id \"$raw_input\")\n\t\t\tplugin_full=\"$raw_input\"\n\n\t\t\tlocal plugin_list\n\t\t\tplugin_list=$(openclaw plugins list 2>/dev/null)\n\n\t\t\tif echo \"$plugin_list\" | grep -qw \"$plugin_id\" && echo \"$plugin_list\" | grep \"$plugin_id\" | grep -q \"disabled\"; then\n\t\t\t\tui_info \"插件 [$plugin_id] 已预装,正在激活...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\" && ui_ok \"激活成功\" || ui_err \"激活失败\"\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/extensions/$plugin_id\" ]]; then\n\t\t\t\tui_info \"发现系统内置插件,尝试直接启用...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\telse\n\t\t\t\tui_info \"本地未发现,尝试下载安装...\"\n\t\t\t\trm -rf \"/root/.openclaw/extensions/$plugin_id\"\n\n\t\t\t\tif openclaw plugins install \"$plugin_full\"; then\n\t\t\t\t\tui_ok \"下载成功,正在启用...\"\n\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\telse\n\t\t\t\t\tui_warn \"官方渠道下载失败,尝试 npm 备选方案...\"\n\t\t\t\t\tif npm install -g \"$plugin_full\" --unsafe-perm; then\n\t\t\t\t\t\tui_ok \"npm 安装成功,尝试启用...\"\n\t\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"无法获取该插件,请检查 ID 是否正确或网络是否可用\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\tfi\n\t\t\tfi\n\n\t\t\tsync_openclaw_plugin_allowlist \"$plugin_id\"\n\t\t\tstart_gateway\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\tinstall_skill() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"技能管理\"\n\t\t\tgum style --foreground 240 \"当前已安装技能:\"\n\t\t\topenclaw skills list\n\t\t\techo\n\n\t\t\tlocal skill_name\n\t\t\tskill_name=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' 选择要安装的技能\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"github 管理 GitHub Issues/PR/CI\" \\\n\t\t\t\t\"notion 操作 Notion 页面与数据库\" \\\n\t\t\t\t\"apple-notes macOS 原生笔记管理\" \\\n\t\t\t\t\"apple-reminders macOS 提醒事项管理\" \\\n\t\t\t\t\"1password 自动化读取 1Password 密钥\" \\\n\t\t\t\t\"gog Google Workspace 全能助手\" \\\n\t\t\t\t\"things-mac Things 3 任务管理\" \\\n\t\t\t\t\"bluebubbles 通过 BlueBubbles 收发 iMessage\" \\\n\t\t\t\t\"himalaya 终端邮件管理 (IMAP/SMTP)\" \\\n\t\t\t\t\"summarize 网页/播客/YouTube 内容总结\" \\\n\t\t\t\t\"openai-whisper 本地音频转文字\" \\\n\t\t\t\t\"coding-agent 运行 Claude Code/Codex 等编程助手\" \\\n\t\t\t\t\"手动输入技能名称\") || break\n\n\t\t\tif [[ \"$skill_name\" == \"手动输入技能名称\" ]]; then\n\t\t\t\tskill_name=$(gum input \\\n\t\t\t\t\t--placeholder \"技能名称 (留空退出)\" \\\n\t\t\t\t\t--prompt \"Skill > \") || break\n\t\t\t\t[[ -z \"$skill_name\" ]] && break\n\t\t\telse\n\t\t\t\tskill_name=$(echo \"$skill_name\" | awk '{print $1}')\n\t\t\tfi\n\t\t\t[[ -z \"$skill_name\" ]] && break\n\n\t\t\tlocal skill_found=false\n\t\t\tif [[ -d \"${HOME}/.openclaw/workspace/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在用户目录安装\"\n\t\t\t\tskill_found=true\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在系统目录安装\"\n\t\t\t\tskill_found=true\n\t\t\tfi\n\n\t\t\tif [[ \"$skill_found\" == true ]]; then\n\t\t\t\tgum confirm \"是否重新安装?\" || { break_end; continue; }\n\t\t\tfi\n\n\t\t\tgum spin --spinner globe --title \"正在安装技能 $skill_name...\" -- npx clawhub install \"$skill_name\"\n\t\t\tif [[ $? -eq 0 ]]; then\n\t\t\t\techo \"技能 $skill_name 安装成功\"\n\t\t\t\tstart_gateway\n\t\t\telse\n\t\t\t\techo \"错误:安装失败,请检查技能名称是否正确\"\n\t\t\tfi\n\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\topenclaw_json_get_bool() {\n\t\tlocal expr=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r \"$expr\" \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_channel_has_cfg() {\n\t\tlocal channel=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r --arg c \"$channel\" '\n\t\t\t(.channels[$c] // null) as $v\n\t\t\t| if ($v | type) != \"object\" then\n\t\t\t\tfalse\n\t\t\t else\n\t\t\t\t([ $v\n\t\t\t\t | to_entries[]\n\t\t\t\t | select((.key == \"enabled\" or .key == \"dmPolicy\" or .key == \"groupPolicy\" or .key == \"streaming\") | not)\n\t\t\t\t | .value\n\t\t\t\t | select(. != null and . != \"\" and . != false)\n\t\t\t\t ] | length) > 0\n\t\t\t end\n\t\t' \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_dir_has_files() {\n\t\tlocal dir=\"$1\"\n\t\t[[ -d \"$dir\" ]] && find \"$dir\" -type f -print -quit 2>/dev/null | grep -q .\n\t}\n\n\topenclaw_plugin_local_installed() {\n\t\tlocal plugin=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ -s \"$config_file\" ]] && jq -e --arg p \"$plugin\" '.plugins.installs[$p]' \"$config_file\" >/dev/null 2>&1; then\n\t\t\treturn 0\n\t\tfi\n\t\t[[ -d \"${HOME}/.openclaw/extensions/${plugin}\" ]] || [[ -d \"/usr/lib/node_modules/openclaw/extensions/${plugin}\" ]]\n\t}\n\n\topenclaw_bot_status_text() {\n\t\tlocal enabled=\"$1\"\n\t\tlocal configured=\"$2\"\n\t\tlocal connected=\"$3\"\n\t\tlocal abnormal=\"$4\"\n\t\tif [[ \"$abnormal\" == \"true\" ]]; then\n\t\t\techo \"异常\"\n\t\telif [[ \"$enabled\" != \"true\" ]]; then\n\t\t\techo \"未启用\"\n\t\telif [[ \"$connected\" == \"true\" ]]; then\n\t\t\techo \"已连接\"\n\t\telif [[ \"$configured\" == \"true\" ]]; then\n\t\t\techo \"已配置\"\n\t\telse\n\t\t\techo \"未配置\"\n\t\tfi\n\t}\n\n\topenclaw_colorize_bot_status() {\n\t\tlocal status=\"$1\"\n\t\tcase \"$status\" in\n\t\t\t已连接) echo -e \"${gl_lv}${status}${gl_bai}\" ;;\n\t\t\t已配置) echo -e \"${gl_huang}${status}${gl_bai}\" ;;\n\t\t\t异常) echo -e \"${gl_hong}${status}${gl_bai}\" ;;\n\t\t\t*) echo \"$status\" ;;\n\t\tesac\n\t}\n\n\topenclaw_print_bot_status_line() {\n\t\tlocal label=\"$1\"\n\t\tlocal status=\"$2\"\n\t\techo -e \"- ${label}: $(openclaw_colorize_bot_status \"$status\")\"\n\t}\n\n\topenclaw_show_bot_local_status_block() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal json_ok=\"false\"\n\t\tif [[ -s \"$config_file\" ]] && jq empty \"$config_file\" >/dev/null 2>&1; then\n\t\t\tjson_ok=\"true\"\n\t\tfi\n\n\t\tlocal tg_enabled tg_cfg tg_connected tg_abnormal tg_status\n\t\ttg_enabled=$(openclaw_json_get_bool '.channels.telegram.enabled // .plugins.entries.telegram.enabled // false')\n\t\ttg_cfg=$(openclaw_channel_has_cfg \"telegram\")\n\t\ttg_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/telegram\"; then\n\t\t\ttg_connected=\"true\"\n\t\tfi\n\t\ttg_abnormal=\"false\"\n\t\tif [[ \"$tg_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\ttg_abnormal=\"true\"\n\t\tfi\n\t\ttg_status=$(openclaw_bot_status_text \"$tg_enabled\" \"$tg_cfg\" \"$tg_connected\" \"$tg_abnormal\")\n\n\t\tlocal feishu_enabled feishu_cfg feishu_connected feishu_abnormal feishu_status\n\t\tfeishu_enabled=$(openclaw_json_get_bool '.plugins.entries.feishu.enabled // .channels.feishu.enabled // false')\n\t\tfeishu_cfg=$(openclaw_channel_has_cfg \"feishu\")\n\t\tfeishu_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/feishu\"; then\n\t\t\tfeishu_connected=\"true\"\n\t\tfi\n\t\tfeishu_abnormal=\"false\"\n\t\tif [[ \"$feishu_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"feishu\"; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$feishu_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tfeishu_status=$(openclaw_bot_status_text \"$feishu_enabled\" \"$feishu_cfg\" \"$feishu_connected\" \"$feishu_abnormal\")\n\n\t\tlocal wa_enabled wa_cfg wa_connected wa_abnormal wa_status\n\t\twa_enabled=$(openclaw_json_get_bool '.plugins.entries.whatsapp.enabled // .channels.whatsapp.enabled // false')\n\t\twa_cfg=$(openclaw_channel_has_cfg \"whatsapp\")\n\t\twa_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/whatsapp\"; then\n\t\t\twa_connected=\"true\"\n\t\tfi\n\t\twa_abnormal=\"false\"\n\t\tif [[ \"$wa_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"whatsapp\"; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$wa_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\twa_status=$(openclaw_bot_status_text \"$wa_enabled\" \"$wa_cfg\" \"$wa_connected\" \"$wa_abnormal\")\n\n\t\tlocal dc_enabled dc_cfg dc_connected dc_abnormal dc_status\n\t\tdc_enabled=$(openclaw_json_get_bool '.channels.discord.enabled // .plugins.entries.discord.enabled // false')\n\t\tdc_cfg=$(openclaw_channel_has_cfg \"discord\")\n\t\tdc_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/discord\"; then\n\t\t\tdc_connected=\"true\"\n\t\tfi\n\t\tdc_abnormal=\"false\"\n\t\tif [[ \"$dc_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tdc_abnormal=\"true\"\n\t\tfi\n\t\tdc_status=$(openclaw_bot_status_text \"$dc_enabled\" \"$dc_cfg\" \"$dc_connected\" \"$dc_abnormal\")\n\n\t\tlocal slack_enabled slack_cfg slack_connected slack_abnormal slack_status\n\t\tslack_enabled=$(openclaw_json_get_bool '.plugins.entries.slack.enabled // .channels.slack.enabled // false')\n\t\tslack_cfg=$(openclaw_channel_has_cfg \"slack\")\n\t\tslack_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/slack\"; then\n\t\t\tslack_connected=\"true\"\n\t\tfi\n\t\tslack_abnormal=\"false\"\n\t\tif [[ \"$slack_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"slack\"; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$slack_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tslack_status=$(openclaw_bot_status_text \"$slack_enabled\" \"$slack_cfg\" \"$slack_connected\" \"$slack_abnormal\")\n\n\t\tlocal qq_enabled qq_cfg qq_connected qq_abnormal qq_status\n\t\tqq_enabled=$(openclaw_json_get_bool '.plugins.entries.qqbot.enabled // .channels.qqbot.enabled // false')\n\t\tqq_cfg=$(openclaw_channel_has_cfg \"qqbot\")\n\t\tqq_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/sessions\" || openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/data\"; then\n\t\t\tqq_connected=\"true\"\n\t\tfi\n\t\tqq_abnormal=\"false\"\n\t\tif [[ \"$qq_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"qqbot\"; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$qq_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tqq_status=$(openclaw_bot_status_text \"$qq_enabled\" \"$qq_cfg\" \"$qq_connected\" \"$qq_abnormal\")\n\n\t\techo \"本地状态(仅本机配置/缓存,不做网络探测):\"\n\t\topenclaw_print_bot_status_line \"Telegram\" \"$tg_status\"\n\t\topenclaw_print_bot_status_line \"飞书(Lark)\" \"$feishu_status\"\n\t\topenclaw_print_bot_status_line \"WhatsApp\" \"$wa_status\"\n\t\topenclaw_print_bot_status_line \"Discord\" \"$dc_status\"\n\t\topenclaw_print_bot_status_line \"Slack\" \"$slack_status\"\n\t\topenclaw_print_bot_status_line \"QQ Bot\" \"$qq_status\"\n\t}\n\n\tchange_tg_bot_code() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"机器人连接对接\"\n\t\t\topenclaw_show_bot_local_status_block\n\t\t\techo\n\n\t\t\tlocal bot_choice\n\t\t\tbot_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择要对接的平台\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"Telegram 机器人对接\" \\\n\t\t\t\t\"飞书 (Lark) 机器人对接\" \\\n\t\t\t\t\"WhatsApp 机器人对接\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tlocal code\n\t\t\tcase \"$bot_choice\" in\n\t\t\t\t\"Telegram 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"TG机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve telegram \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"飞书 (Lark) 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"飞书机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve feishu \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"WhatsApp 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"WhatsApp连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve whatsapp \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"返回\"|*)\n\t\t\t\t\treturn 0\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\topenclaw_backup_root() {\n\t\techo \"${HOME}/.openclaw/backups\"\n\t}\n\n\topenclaw_is_interactive_terminal() {\n\t\t[[ -t 0 && -t 1 ]]\n\t}\n\n\topenclaw_has_command() {\n\t\tcommand -v \"$1\" >/dev/null 2>&1\n\t}\n\n\topenclaw_is_safe_relpath() {\n\t\tlocal rel=\"$1\"\n\t\t[[ -z \"$rel\" ]] && return 1\n\t\t[[ \"$rel\" == /* ]] && return 1\n\t\t[[ \"$rel\" == *//* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\n'* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\r'* ]] && return 1\n\t\tcase \"$rel\" in\n\t\t\t../*|*/../*|*/..|..)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t\treturn 0\n\t}\n\n\topenclaw_restore_path_allowed() {\n\t\tlocal mode=\"$1\"\n\t\tlocal rel=\"$2\"\n\t\tcase \"$mode\" in\n\t\t\tmemory)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\tMEMORY.md|AGENTS.md|USER.md|SOUL.md|TOOLS.md|memory/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\tproject)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\topenclaw.json|workspace/*|extensions/*|skills/*|prompts/*|tools/*|telegram/*|feishu/*|whatsapp/*|discord/*|slack/*|qqbot/*|logs/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\t*)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t}\n\n\topenclaw_pack_backup_archive() {\n\t\tlocal backup_type=\"$1\"\n\t\tlocal export_mode=\"$2\"\n\t\tlocal payload_dir=\"$3\"\n\t\tlocal output_file=\"$4\"\n\n\t\tlocal tmp_root\n\t\ttmp_root=$(mktemp -d) || return 1\n\t\tlocal pack_dir=\"$tmp_root/package\"\n\t\tmkdir -p \"$pack_dir\"\n\n\t\tcp -a \"$payload_dir\" \"$pack_dir/payload\"\n\n\t\t(\n\t\t\tcd \"$pack_dir/payload\" || exit 1\n\t\t\tfind . -type f | sed 's|^\\./||' | sort > \"$pack_dir/manifest.files\"\n\t\t\t: > \"$pack_dir/manifest.sha256\"\n\t\t\tlocal f\n\t\t\twhile IFS= read -r f; do\n\t\t\t\t[[ -z \"$f\" ]] && continue\n\t\t\t\tsha256sum \"$f\" >> \"$pack_dir/manifest.sha256\"\n\t\t\tdone < \"$pack_dir/manifest.files\"\n\t\t) || { rm -rf \"$tmp_root\"; return 1; }\n\n\t\tcat > \"$pack_dir/backup.meta\" <<EOF\nTYPE=$backup_type\nMODE=$export_mode\nCREATED_AT=$(date -u +'%Y-%m-%dT%H:%M:%SZ')\nHOST=$(hostname)\nEOF\n\n\t\tmkdir -p \"$(dirname \"$output_file\")\"\n\t\ttar -C \"$pack_dir\" -czf \"$output_file\" backup.meta manifest.files manifest.sha256 payload\n\t\tlocal rc=$?\n\t\trm -rf \"$tmp_root\"\n\t\treturn $rc\n\t}\n\n\topenclaw_offer_transfer_hint() {\n\t\tlocal file_path=\"$1\"\n\t\techo \"可使用以下方式下载备份文件:\"\n\t\techo \" 本地路径: $file_path\"\n\t\techo \" scp 示例: scp root@你的服务器:$file_path ./\"\n\t\techo \" 或使用 SFTP 客户端下载\"\n\t}\n\n\topenclaw_prepare_import_archive() {\n\t\tlocal expected_type=\"$1\"\n\t\tlocal archive_path=\"$2\"\n\t\tlocal unpack_root=\"$3\"\n\n\t\tif [[ ! -f \"$archive_path\" ]]; then\n\t\t\techo \"错误:文件不存在: $archive_path\"\n\t\t\treturn 1\n\t\tfi\n\t\tmkdir -p \"$unpack_root\"\n\t\tif ! tar -xzf \"$archive_path\" -C \"$unpack_root\"; then\n\t\t\techo \"错误:备份包解压失败\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal pkg_dir=\"$unpack_root/package\"\n\t\tif [[ -f \"$unpack_root/backup.meta\" ]]; then\n\t\t\tpkg_dir=\"$unpack_root\"\n\t\tfi\n\n\t\tlocal required\n\t\tfor required in backup.meta manifest.files manifest.sha256 payload; do\n\t\t\tif [[ ! -e \"$pkg_dir/$required\" ]]; then\n\t\t\t\techo \"错误:备份包缺少必要文件: $required\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tdone\n\n\t\tlocal real_type\n\t\treal_type=$(grep '^TYPE=' \"$pkg_dir/backup.meta\" | head -n1 | cut -d'=' -f2-)\n\t\tif [[ \"$real_type\" != \"$expected_type\" ]]; then\n\t\t\techo \"错误:备份类型不匹配,期望: $expected_type,实际: ${real_type:-未知}\"\n\t\t\treturn 1\n\t\tfi\n\n\t\t(\n\t\t\tcd \"$pkg_dir/payload\" || exit 1\n\t\t\tsha256sum -c ../manifest.sha256 >/dev/null\n\t\t) || { echo \"错误:sha256 校验失败,拒绝还原\"; return 1; }\n\n\t\techo \"$pkg_dir\"\n\t\treturn 0\n\t}\n\n\topenclaw_memory_backup_export() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tlocal ts\n\t\tts=$(date +%Y%m%d-%H%M%S)\n\t\tlocal out_file=\"$backup_root/openclaw-memory-full-${ts}.tar.gz\"\n\n\t\tmkdir -p \"$backup_root\"\n\t\tif [[ ! -d \"$workspace_dir\" ]]; then\n\t\t\techo \"错误:未找到 workspace 目录: $workspace_dir\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\t[[ -f \"$workspace_dir/MEMORY.md\" ]] && cp -a \"$workspace_dir/MEMORY.md\" \"$tmp_payload/\"\n\t\t[[ -d \"$workspace_dir/memory\" ]] && cp -a \"$workspace_dir/memory\" \"$tmp_payload/\"\n\n\t\tif gum confirm \"是否附带 AGENTS/USER/SOUL/TOOLS 文件?\"; then\n\t\t\tlocal f\n\t\t\tfor f in AGENTS.md USER.md SOUL.md TOOLS.md; do\n\t\t\t\t[[ -f \"$workspace_dir/$f\" ]] && cp -a \"$workspace_dir/$f\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的记忆文件\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"memory-full\" \"default\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"记忆全量备份完成: $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:记忆全量备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_read_import_path() {\n\t\tlocal file_input file_path backup_root\n\t\techo \"提示:输入文件名时默认在备份目录查找;输入含 / 的路径时按完整路径校验。\" >&2\n\t\techo \"scp 示例: scp /本地/备份包.tar.gz root@你的服务器:/tmp/\" >&2\n\n\t\tfile_input=$(gum input \\\n\t\t\t--placeholder \"备份文件名或完整路径\" \\\n\t\t\t--prompt \"Path > \")\n\t\t[[ -z \"$file_input\" ]] && { echo \"\"; return 0; }\n\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\n\t\tif [[ \"$file_input\" == */* ]]; then\n\t\t\tfile_path=\"$file_input\"\n\t\telse\n\t\t\tfile_path=\"$backup_root/$file_input\"\n\t\tfi\n\n\t\tif [[ ! -f \"$file_path\" ]]; then\n\t\t\techo \"错误:备份文件不存在: $file_path\" >&2\n\t\t\techo \"\"\n\t\t\treturn 1\n\t\tfi\n\n\t\techo \"$file_path\"\n\t}\n\n\topenclaw_memory_backup_import() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tmkdir -p \"$workspace_dir\"\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"memory-full\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed memory \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$workspace_dir/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$workspace_dir/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"记忆全量还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_export() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tif [[ ! -d \"$openclaw_root\" ]]; then\n\t\t\techo \"错误:未找到 OpenClaw 根目录: $openclaw_root\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal export_mode_label\n\t\texport_mode_label=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $'选择备份模式\\n↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"安全模式(推荐):workspace + openclaw.json + extensions/skills/prompts/tools\" \\\n\t\t\t\"完整模式(含更多状态,敏感风险更高)\") || return 1\n\n\t\tlocal mode_label=\"safe\"\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\tlocal d\n\t\tif [[ \"$export_mode_label\" == 完整* ]]; then\n\t\t\tmode_label=\"full\"\n\t\t\tfor d in workspace extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in telegram feishu whatsapp discord slack qqbot logs; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\telse\n\t\t\t[[ -d \"$openclaw_root/workspace\" ]] && cp -a \"$openclaw_root/workspace\" \"$tmp_payload/\"\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的 OpenClaw 项目内容\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tlocal out_file=\"$backup_root/openclaw-project-${mode_label}-$(date +%Y%m%d-%H%M%S).tar.gz\"\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"openclaw-project\" \"$mode_label\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"OpenClaw 项目备份完成 (${mode_label}): $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:OpenClaw 项目备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_import() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tmkdir -p \"$openclaw_root\"\n\n\t\techo \"警告:高风险操作,项目还原会覆盖 OpenClaw 配置与工作区内容。\"\n\t\tgum confirm \"确认继续还原?此操作不可逆。\" || { echo \"已取消\"; break_end; return 1; }\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"openclaw-project\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed project \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tgum spin --spinner pulse --title \"停止 OpenClaw gateway...\" -- openclaw gateway stop\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$openclaw_root/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$openclaw_root/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tstart_gateway\n\t\t\techo \"gateway 健康检查:\"\n\t\t\topenclaw gateway status || true\n\t\tfi\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"OpenClaw 项目还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_detect_type() {\n\t\tlocal file_name=\"$1\"\n\t\tif [[ \"$file_name\" == openclaw-memory-full-*.tar.gz ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\telif [[ \"$file_name\" == openclaw-project-*.tar.gz ]]; then\n\t\t\techo \"项目备份文件\"\n\t\telse\n\t\t\techo \"其他备份文件\"\n\t\tfi\n\t}\n\n\topenclaw_backup_collect_files() {\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tmapfile -t OPENCLAW_BACKUP_FILES < <(find \"$backup_root\" -maxdepth 1 -type f -name '*.tar.gz' | xargs -I{} basename {} | sort -r)\n\t}\n\n\topenclaw_backup_render_file_list() {\n\t\tlocal backup_root i file_name file_path file_type file_size file_time\n\t\tlocal has_memory=0 has_project=0 has_other=0\n\t\tbackup_root=$(openclaw_backup_root)\n\t\topenclaw_backup_collect_files\n\n\t\techo \"备份目录: $backup_root\"\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\techo \"暂无备份文件\"\n\t\t\treturn 0\n\t\tfi\n\n\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\tfile_type=$(openclaw_backup_detect_type \"${OPENCLAW_BACKUP_FILES[$i]}\")\n\t\t\tcase \"$file_type\" in\n\t\t\t\t\"记忆备份文件\") has_memory=1 ;;\n\t\t\t\t\"项目备份文件\") has_project=1 ;;\n\t\t\t\t\"其他备份文件\") has_other=1 ;;\n\t\t\tesac\n\t\tdone\n\n\t\tif [[ \"$has_memory\" -eq 1 ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"记忆备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_project\" -eq 1 ]]; then\n\t\t\techo \"项目备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"项目备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_other\" -eq 1 ]]; then\n\t\t\techo \"其他备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"其他备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_backup_file_exists_in_list() {\n\t\tlocal target_file=\"$1\"\n\t\tlocal item\n\t\tfor item in \"${OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t[[ \"$item\" == \"$target_file\" ]] && return 0\n\t\tdone\n\t\treturn 1\n\t}\n\n\topenclaw_backup_delete_file() {\n\t\tlocal backup_root backup_root_real user_input target_file target_path target_type\n\t\tbackup_root=$(openclaw_backup_root)\n\n\t\topenclaw_backup_render_file_list\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\tbreak_end\n\t\t\treturn 0\n\t\tfi\n\n\t\tuser_input=$(printf '%s\\n' \"${OPENCLAW_BACKUP_FILES[@]}\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 选择要删除的备份 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=15 \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ⚡ DELETE BACKUP \" \\\n\t\t\t--color=border:196,label:196,header:196,prompt:208,pointer:196,marker:208,hl:208,hl+:208) || { echo \"已取消\"; break_end; return 0; }\n\n\t\t[[ -z \"$user_input\" ]] && { echo \"已取消\"; break_end; return 0; }\n\n\t\tbackup_root_real=$(realpath -m \"$backup_root\")\n\t\ttarget_file=$(basename -- \"$user_input\")\n\t\ttarget_path=\"$backup_root/$target_file\"\n\n\t\tif [[ ! -f \"$target_path\" ]]; then\n\t\t\techo \"错误:目标文件不存在: $target_path\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\ttarget_type=$(openclaw_backup_detect_type \"$target_file\")\n\n\t\techo \"即将删除: [$target_type] $target_path\"\n\t\tgum confirm \"确认删除?\" || { echo \"已取消删除\"; break_end; return 0; }\n\t\tgum confirm \"请再次确认,删除后不可恢复\" || { echo \"已取消删除\"; break_end; return 0; }\n\n\t\tif rm -f -- \"$target_path\"; then\n\t\t\techo \"删除成功: $target_file\"\n\t\telse\n\t\t\techo \"错误:删除失败: $target_file\"\n\t\tfi\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_list_files() {\n\t\topenclaw_backup_render_file_list\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_restore_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"备份与还原\"\n\t\t\topenclaw_backup_render_file_list\n\t\t\techo\n\n\t\t\tlocal backup_choice\n\t\t\tbackup_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"备份记忆全量\" \\\n\t\t\t\t\"还原记忆全量\" \\\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\" \\\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\" \\\n\t\t\t\t\"删除备份文件\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$backup_choice\" in\n\t\t\t\t\"备份记忆全量\") openclaw_memory_backup_export ;;\n\t\t\t\t\"还原记忆全量\") openclaw_memory_backup_import ;;\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\") openclaw_project_backup_export ;;\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\") openclaw_project_backup_import ;;\n\t\t\t\t\"删除备份文件\") openclaw_backup_delete_file ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\t_autostart_status() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\techo \"未安装\"\n\t\t\t\treturn\n\t\t\tfi\n\t\t\tif launchctl list 2>/dev/null | grep -q \"ai.openclaw.gateway\"; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\telse\n\t\t\tif systemctl --user is-enabled --quiet openclaw-gateway.service 2>/dev/null; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\tfi\n\t}\n\n\ttoggle_autostart() {\n\t\tlocal status; status=$(_autostart_status)\n\t\tclear\n\t\tui_header \"开机自启动管理\"\n\t\techo -e \" 当前状态:\\033[1m${status}\\033[0m\"\n\t\techo\n\n\t\tlocal action\n\t\tif [[ \"$status\" == \"已开启\" ]]; then\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"关闭自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\telse\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"开启自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\tfi\n\n\t\tcase \"$action\" in\n\t\t\t\"开启自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\t\t\tgum spin --spinner globe --title \"正在安装服务...\" -- \\\n\t\t\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 1\"\n\t\t\t\t\tfi\n\t\t\t\t\tlaunchctl load \"$plist\" 2>/dev/null && ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t\"关闭自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tlaunchctl unload \"$plist\" 2>/dev/null && ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user disable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t*) return 0 ;;\n\t\tesac\n\t\tbreak_end\n\t}\n\n\tupdate_moltbot() {\n\t\tinstall_node_and_tools\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_bin\n\t\tnpm_bin=\"$(npm prefix -g 2>/dev/null)/bin\"\n\t\tif [[ -n \"$npm_bin\" ]]; then\n\t\t\texport PATH=\"$npm_bin:$PATH\"\n\t\t\tfor rc in \"$HOME/.bashrc\" \"$HOME/.zshrc\" \"$HOME/.profile\"; do\n\t\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t\tgrep -qF \"$npm_bin\" \"$rc\" 2>/dev/null && continue\n\t\t\t\techo \"export PATH=\\\"${npm_bin}:\\$PATH\\\"\" >> \"$rc\"\n\t\t\tdone\n\t\tfi\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab -\n\t\tstart_gateway\n\t\tui_ok \"更新完成\"\n\t\tbreak_end\n\t}\n\n\tuninstall_moltbot() {\n\t\tgum confirm \"确认卸载 OpenClaw?此操作不可逆。\" || return 0\n\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tlaunchctl unload \"$plist\" 2>/dev/null || true\n\t\telse\n\t\t\tsystemctl --user disable --now openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tpkill -f \"openclaw.*gatewa\" 2>/dev/null || true\n\n\t\tgum spin --spinner meter --title \"正在卸载 OpenClaw...\" -- \\\n\t\t\tbash -c 'timeout 15 openclaw uninstall 2>/dev/null; true'\n\t\tnpm uninstall -g openclaw 2>/dev/null || true\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab - 2>/dev/null || true\n\t\trm -rf /root/.openclaw ~/.openclaw\n\n\t\trm -f \"$HOME/.local/bin/oc\" \"$HOME/.local/bin/openclawctl.sh\" /usr/local/bin/oc\n\t\tfor rc in \"$HOME/.zshrc\" \"$HOME/.bashrc\" \"$HOME/.profile\"; do\n\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t_sed_i '/\\.local\\/bin.*PATH/d' \"$rc\"\n\t\t\t_sed_i '/npm-global.*PATH\\|npm\\/bin.*PATH/d' \"$rc\"\n\t\tdone\n\n\t\techo\n\t\tgum style --foreground 46 --bold \" 卸载完成,脚本已退出。\"\n\t\texit 0\n\t}\n\n\tnano_openclaw_json() {\n\t\tinstall nano\n\t\tnano ~/.openclaw/openclaw.json\n\t\tstart_gateway\n\t}\n\n\topenclaw_find_webui_domain() {\n\t\tlocal conf domain_list\n\t\tdomain_list=$(\n\t\t\tgrep -R \"18789\" /home/web/conf.d/*.conf 2>/dev/null \\\n\t\t\t| awk -F: '{print $1}' \\\n\t\t\t| sort -u \\\n\t\t\t| while read -r conf; do\n\t\t\t\tbasename \"$conf\" .conf\n\t\t\tdone\n\t\t)\n\n\t\tif [[ -n \"$domain_list\" ]]; then\n\t\t\techo \"$domain_list\"\n\t\tfi\n\t}\n\n\topenclaw_show_webui_addr() {\n\t\tlocal local_ip token domains\n\n\t\tlocal_ip=\"127.0.0.1\"\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tgum style --border normal --border-foreground 99 --padding \"0 2\" \\\n\t\t\t\"OpenClaw WebUI 访问地址\" \\\n\t\t\t\"本机: http://${local_ip}:18789/#token=${token}\"\n\n\t\tdomains=$(openclaw_find_webui_domain)\n\t\tif [[ -n \"$domains\" ]]; then\n\t\t\techo \"$domains\" | while read -r d; do\n\t\t\t\techo \"域名: https://${d}/#token=${token}\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_domain_webui() {\n\t\tadd_yuming\n\t\tldnmp_Proxy \"${yuming}\" 127.0.0.1 18789\n\n\t\tlocal token\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tclear\n\t\techo \"访问地址: https://${yuming}/#token=$token\"\n\t\techo \"先访问URL触发设备ID,然后回车下一步进行配对。\"\n\t\tgum input --placeholder \"按回车继续...\" > /dev/null\n\n\t\techo -e \"${gl_kjlan}正在加载设备列表……${gl_bai}\"\n\n\t\tlocal config_file=\"$HOME/.openclaw/openclaw.json\"\n\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\tlocal new_origin=\"https://${yuming}\"\n\t\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\t\tlocal tmp_json\n\t\t\t\ttmp_json=$(mktemp)\n\t\t\t\tjq 'if .gateway.controlUi == null then .gateway.controlUi = {\"allowedOrigins\": [\"http://127.0.0.1\"]} else . end | if (.gateway.controlUi.allowedOrigins | contains([$origin]) | not) then .gateway.controlUi.allowedOrigins += [$origin] else . end' \\\n\t\t\t\t\t--arg origin \"$new_origin\" \"$config_file\" > \"$tmp_json\" && mv \"$tmp_json\" \"$config_file\"\n\t\t\t\techo -e \"${gl_kjlan}已将域名 ${yuming} 加入 allowedOrigins 配置${gl_bai}\"\n\t\t\t\topenclaw gateway restart >/dev/null 2>&1\n\t\t\tfi\n\t\tfi\n\n\t\topenclaw devices list\n\n\t\tlocal Request_Key\n\t\tRequest_Key=$(gum input \\\n\t\t\t--placeholder \"Request_Key\" \\\n\t\t\t--prompt \"> \")\n\n\t\tif [[ -z \"$Request_Key\" ]]; then\n\t\t\techo \"Request_Key 不能为空\"\n\t\t\treturn 1\n\t\tfi\n\n\t\topenclaw devices approve \"$Request_Key\"\n\t}\n\n\topenclaw_remove_domain() {\n\t\techo \"域名格式 example.com 不带https://\"\n\t\tweb_del\n\t}\n\n\topenclaw_webui_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"WebUI 访问与设置\"\n\t\t\topenclaw_show_webui_addr\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加域名访问\" \\\n\t\t\t\t\"删除域名访问\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"添加域名访问\")\n\t\t\t\t\topenclaw_domain_webui\n\t\t\t\t\techo\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"删除域名访问\")\n\t\t\t\t\topenclaw_remove_domain\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\twhile true; do\n\t\tclear\n\n\t\tlocal install_status running_status update_info\n\t\tinstall_status=$(get_install_status)\n\t\trunning_status=$(get_running_status)\n\t\tupdate_info=$(check_openclaw_update)\n\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"1 8\" --align center \\\n\t\t\t\"O P E N C L A W\" \\\n\t\t\t\"\" \\\n\t\t\t\"[ AI Agent Gateway Manager ]\"\n\t\techo -e \" $install_status $running_status $update_info\"\n\t\tgum style --foreground 240 \" 输入 oc 可快速启动\"\n\t\techo\n\t\tgum style \\\n\t\t\t--bold \\\n\t\t\t--border double --border-foreground 201 \\\n\t\t\t--padding \"0 4\" --align center \\\n\t\t\t\"$(gum style --bold --foreground 201 '◈ by Joey ◈')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ YouTube @joeyblog')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ Telegram t.me/+ft-zI76oovgwNmRh')\" \\\n\t\t\t\"$(gum style --foreground 208 '⚡ 基于:kejilion · cliproxyapi-installer')\"\n\t\techo\n\n\t\tlocal choice\n\t\tlocal autostart_label\n\t\tautostart_label=\"开机自启动 [$(_autostart_status)]\"\n\n\t\tchoice=$(gum choose \\\n\t\t\t--height 24 \\\n\t\t\t--cursor \"❯ \" \\\n\t\t\t--header $' ─── CONTROL CENTER ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\"小白模式安装(推荐)\" \\\n\t\t\t\"安装\" \\\n\t\t\t\"启动\" \\\n\t\t\t\"停止\" \\\n\t\t\t\"状态日志查看\" \\\n\t\t\t\"$autostart_label\" \\\n\t\t\t\"换模型\" \\\n\t\t\t\"API管理\" \\\n\t\t\t\"CLIProxyAPI 管理\" \\\n\t\t\t\"机器人连接对接\" \\\n\t\t\t\"安装插件\" \\\n\t\t\t\"安装技能\" \\\n\t\t\t\"编辑主配置文件\" \\\n\t\t\t\"配置向导\" \\\n\t\t\t\"健康检测与修复\" \\\n\t\t\t\"WebUI访问与设置\" \\\n\t\t\t\"TUI命令行对话\" \\\n\t\t\t\"备份与还原\" \\\n\t\t\t\"更新\" \\\n\t\t\t\"卸载\" \\\n\t\t\t\"退出\") || break\n\n\t\tcase \"$choice\" in\n\t\t\t\"小白模式安装(推荐)\") beginner_mode_install ;;\n\t\t\t\"安装\") install_moltbot ;;\n\t\t\t\"启动\") start_bot ;;\n\t\t\t\"停止\") stop_bot ;;\n\t\t\t\"状态日志查看\") view_logs ;;\n\t\t\t\"开机自启动 [\"*\"]\") toggle_autostart ;;\n\t\t\t\"换模型\") change_model ;;\n\t\t\"API管理\") openclaw_api_manage_menu ;;\n\t\t\"CLIProxyAPI 管理\") cliproxyapi_manage_menu ;;\n\t\t\"机器人连接对接\") change_tg_bot_code ;;\n\t\t\t\"安装插件\") install_plugin ;;\n\t\t\t\"安装技能\") install_skill ;;\n\t\t\t\"编辑主配置文件\") nano_openclaw_json ;;\n\t\t\t\"配置向导\")\n\t\t\t\topenclaw onboard --install-daemon\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"健康检测与修复\")\n\t\t\t\tgum spin --spinner meter --title \"正在执行健康检测...\" -- openclaw doctor --fix\n\t\t\t\tif sync_openclaw_api_models; then\n\t\t\t\t\tstart_gateway\n\t\t\t\telse\n\t\t\t\t\tui_err \"API 模型同步失败,已中止重启网关,请检查 provider /models 返回后重试\"\n\t\t\t\tfi\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"WebUI访问与设置\") openclaw_webui_menu ;;\n\t\t\t\"TUI命令行对话\")\n\t\t\t\topenclaw tui\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"备份与还原\") openclaw_backup_restore_menu ;;\n\t\t\t\"更新\") update_moltbot ;;\n\t\t\t\"卸载\") uninstall_moltbot ;;\n\t\t\t\"退出\"|*) break ;;\n\t\tesac\n\tdone\n}\n\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n\tmoltbot_menu\nfi\n"}, "7efa5e6ceb74298ac3953171d5dcfdacb27c75a4": {"t": 1785772923.5408852, "n": 96438, "h": "840658310af16356", "c": "#!/usr/bin/env bash\n#\n# ╔══════════════════════════════════════════════════════════════════╗\n# ║ OpenClaw / MoltBot 管理脚本 ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 作者 GitHub : by Joey ║\n# ║ YouTube : @joeyblog ║\n# ║ Telegram: https://t.me/+ft-zI76oovgwNmRh ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 致谢 / 引用 ║\n# ║ · 原始脚本基础来自 kejilion(@kejilion) ║\n# ║ · CLIProxyAPI 安装器来自 cliproxyapi-installer ║\n# ║ github.com/brokechubb/cliproxyapi-installer ║\n# ╚══════════════════════════════════════════════════════════════════╝\n#\n: \"${gl_hui:='\\e[37m'}\"\n: \"${gl_hong:='\\033[31m'}\"\n: \"${gl_lv:='\\033[32m'}\"\n: \"${gl_huang:='\\033[33m'}\"\n: \"${gl_lan:='\\033[34m'}\"\n: \"${gl_bai:='\\033[0m'}\"\n: \"${gl_zi:='\\033[35m'}\"\n: \"${gl_kjlan:='\\033[96m'}\"\n\nif ! declare -f break_end > /dev/null 2>&1; then\nbreak_end() {\n\tif command -v gum >/dev/null 2>&1; then\n\t\techo\n\t\tgum style --foreground 240 \" ─────────────────────────────────────── \"\n\t\tgum input --placeholder \"» 按回车继续 «\" --prompt \" \" > /dev/null\n\telse\n\t\techo \" ─── 按任意键继续 ───\"\n\t\tread -n 1 -s -r -p \"\"\n\t\techo\n\tfi\n\tclear\n}\nfi\n\nif ! declare -f install > /dev/null 2>&1; then\ninstall() {\n\tif [[ $# -eq 0 ]]; then\n\t\techo \"未提供软件包参数\"\n\t\treturn 1\n\tfi\n\tfor package in \"$@\"; do\n\t\tif ! command -v \"$package\" &>/dev/null; then\n\t\t\techo -e \"${gl_kjlan}正在安装 $package...${gl_bai}\"\n\t\t\tif [[ \"$(uname -s)\" == \"Darwin\" ]]; then\n\t\t\t\t_ensure_brew &>/dev/null\n\t\t\t\tcommand -v brew &>/dev/null && brew install \"$package\" &>/dev/null\n\t\t\telif command -v dnf &>/dev/null; then\n\t\t\t\tdnf install -y \"$package\" &>/dev/null\n\t\t\telif command -v yum &>/dev/null; then\n\t\t\t\tyum install -y \"$package\" &>/dev/null\n\t\t\telif command -v apt &>/dev/null; then\n\t\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"$package\" &>/dev/null\n\t\t\telif command -v apk &>/dev/null; then\n\t\t\t\tapk add \"$package\" &>/dev/null\n\t\t\telif command -v pacman &>/dev/null; then\n\t\t\t\tpacman -S --noconfirm \"$package\" &>/dev/null\n\t\t\telif command -v zypper &>/dev/null; then\n\t\t\t\tzypper install -y \"$package\" &>/dev/null\n\t\t\telif command -v opkg &>/dev/null; then\n\t\t\t\topkg install \"$package\" &>/dev/null\n\t\t\telif command -v pkg &>/dev/null; then\n\t\t\t\tpkg install -y \"$package\" &>/dev/null\n\t\t\telse\n\t\t\t\techo \"未知的包管理器,无法安装 $package\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\tdone\n}\nfi\n\nis_macos() { [[ \"$(uname -s)\" == \"Darwin\" ]]; }\n\n_ensure_brew() {\n\tif ! command -v brew &>/dev/null; then\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tif ! command -v brew &>/dev/null; then\n\t\techo \"正在安装 Homebrew...\"\n\t\t/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tcommand -v brew &>/dev/null || { echo \"Homebrew 安装失败,请手动安装:https://brew.sh\"; return 1; }\n}\n\n_sed_i() {\n\tif is_macos; then\n\t\tsed -i '' \"$@\"\n\telse\n\t\tsed -i \"$@\"\n\tfi\n}\n\ninstall_base_deps() {\n\tif is_macos; then\n\t\t_ensure_brew || return 1\n\t\tlocal missing_brew=()\n\t\tfor cmd in curl git nano jq python3 gpg; do\n\t\t\tcommand -v \"$cmd\" &>/dev/null || missing_brew+=(\"$cmd\")\n\t\tdone\n\t\tfor i in \"${!missing_brew[@]}\"; do\n\t\t\t[[ \"${missing_brew[$i]}\" == \"gpg\" ]] && missing_brew[$i]=\"gnupg\"\n\t\tdone\n\t\t[[ ${#missing_brew[@]} -eq 0 ]] && return 0\n\t\techo \"正在安装缺失依赖:${missing_brew[*]}...\"\n\t\tbrew install \"${missing_brew[@]}\" &>/dev/null\n\t\treturn 0\n\tfi\n\n\tlocal -a dep_map=(\n\t\t\"curl:curl:curl:curl:curl:curl\"\n\t\t\"git:git:git:git:git:git\"\n\t\t\"nano:nano:nano:nano:nano:nano\"\n\t\t\"jq:jq:jq:jq:jq:jq\"\n\t\t\"python3:python3:python3:python3:python:python3\"\n\t\t\"tar:tar:tar:tar:tar:tar\"\n\t\t\"gpg:gnupg:gnupg2:gnupg:gnupg:gpg2\"\n\t)\n\n\tlocal missing_apt=() missing_dnf=() missing_apk=() missing_pacman=() missing_zypper=()\n\n\tfor entry in \"${dep_map[@]}\"; do\n\t\tIFS=: read -r cmd pkg_apt pkg_dnf pkg_apk pkg_pacman pkg_zypper <<< \"$entry\"\n\t\tcommand -v \"$cmd\" &>/dev/null && continue\n\t\tmissing_apt+=(\"$pkg_apt\")\n\t\tmissing_dnf+=(\"$pkg_dnf\")\n\t\tmissing_apk+=(\"$pkg_apk\")\n\t\tmissing_pacman+=(\"$pkg_pacman\")\n\t\tmissing_zypper+=(\"$pkg_zypper\")\n\tdone\n\n\t[[ ${#missing_apt[@]} -eq 0 ]] && return 0\n\n\techo \"正在安装缺失依赖:${missing_apt[*]}...\"\n\n\tif command -v apt &>/dev/null; then\n\t\tapt update -y &>/dev/null\n\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"${missing_apt[@]}\" &>/dev/null\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y epel-release &>/dev/null || true\n\t\tdnf install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y epel-release &>/dev/null || true\n\t\tyum install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v apk &>/dev/null; then\n\t\tapk update &>/dev/null\n\t\tapk add \"${missing_apk[@]}\" &>/dev/null\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -Sy --noconfirm --needed \"${missing_pacman[@]}\" &>/dev/null\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y \"${missing_zypper[@]}\" &>/dev/null\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\tlocal py\n\t\tpy=$(compgen -c 2>/dev/null | grep -E '^python3\\.[0-9]+$' | sort -V | tail -1)\n\t\tif [[ -n \"$py\" ]]; then\n\t\t\tln -sf \"$(command -v \"$py\")\" /usr/local/bin/python3 2>/dev/null || true\n\t\tfi\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\techo \"正在安装 python3...\"\n\t\tif command -v apt &>/dev/null; then\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y python3 &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tdnf install -y python3 &>/dev/null\n\t\telif command -v yum &>/dev/null; then\n\t\t\tyum install -y python3 &>/dev/null\n\t\telif command -v apk &>/dev/null; then\n\t\t\tapk add python3 &>/dev/null\n\t\telif command -v pacman &>/dev/null; then\n\t\t\tpacman -S --noconfirm python &>/dev/null\n\t\telif command -v zypper &>/dev/null; then\n\t\t\tzypper install -y python3 &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\tfi\n}\n\n_install_gum_binary() {\n\tlocal arch\n\tcase \"$(uname -m)\" in\n\t\tx86_64) arch=\"amd64\" ;;\n\t\taarch64) arch=\"arm64\" ;;\n\t\tarmv7l) arch=\"armv7\" ;;\n\t\t*) echo \"不支持的架构: $(uname -m)\"; return 1 ;;\n\tesac\n\tlocal latest\n\tlatest=$(curl -fsSL https://api.github.com/repos/charmbracelet/gum/releases/latest \\\n\t\t| grep '\"tag_name\"' | sed 's/.*\"v\\([^\"]*\\)\".*/\\1/')\n\t[[ -z \"$latest\" ]] && { echo \"获取 gum 版本失败\"; return 1; }\n\tlocal url=\"https://github.com/charmbracelet/gum/releases/download/v${latest}/gum_${latest}_linux_${arch}.tar.gz\"\n\tlocal tmp\n\ttmp=$(mktemp -d)\n\tcurl -fsSL \"$url\" -o \"$tmp/gum.tar.gz\" \\\n\t\t&& tar -xzf \"$tmp/gum.tar.gz\" -C \"$tmp\" \\\n\t\t&& install -m 755 \"$tmp/gum\" /usr/local/bin/gum\n\trm -rf \"$tmp\"\n\tcommand -v gum >/dev/null 2>&1\n}\n\ninstall_gum() {\n\tcommand -v gum >/dev/null 2>&1 && return 0\n\techo \"正在安装 gum...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install gum\n\telif command -v apt >/dev/null 2>&1; then\n\t\tcommand -v gpg >/dev/null 2>&1 || apt install -y gnupg 2>/dev/null\n\t\tmkdir -p /etc/apt/keyrings\n\t\tif curl -fsSL https://repo.charm.sh/apt/gpg.key \\\n\t\t\t| gpg --dearmor -o /etc/apt/keyrings/charm.gpg 2>/dev/null; then\n\t\t\techo \"deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *\" \\\n\t\t\t\t| tee /etc/apt/sources.list.d/charm.list >/dev/null\n\t\t\tapt update -y 2>/dev/null && apt install -y gum 2>/dev/null\n\t\tfi\n\t\tcommand -v gum >/dev/null 2>&1 || _install_gum_binary\n\telif command -v dnf &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tdnf install -y gum\n\telif command -v yum &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tyum install -y gum\n\telif command -v apk &>/dev/null; then\n\t\tapk add gum 2>/dev/null || _install_gum_binary\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm gum 2>/dev/null || _install_gum_binary\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y gum 2>/dev/null || _install_gum_binary\n\telse\n\t\t_install_gum_binary\n\tfi\n\tcommand -v gum >/dev/null 2>&1 || { echo \"gum 安装失败,请手动安装: https://github.com/charmbracelet/gum\"; return 1; }\n}\n\ninstall_fzf() {\n\tcommand -v fzf >/dev/null 2>&1 && return 0\n\techo \"正在安装 fzf...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install fzf\n\telif command -v apt >/dev/null 2>&1; then\n\t\tapt install -y fzf\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y fzf\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y fzf\n\telif command -v apk &>/dev/null; then\n\t\tapk add fzf\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm fzf\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y fzf\n\telse\n\t\techo \"无法自动安装 fzf,请手动安装: https://github.com/junegunn/fzf\"\n\t\treturn 1\n\tfi\n}\n\n_install_shortcut() {\n\tlocal store_dir shortcut_dir shortcut\n\n\tstore_dir=\"$HOME/.local/bin\"\n\tmkdir -p \"$store_dir\"\n\n\tlocal _candidate_dirs=(\"/opt/homebrew/bin\" \"/usr/local/bin\")\n\tshortcut_dir=\"\"\n\tfor _d in \"${_candidate_dirs[@]}\"; do\n\t\tif [[ -d \"$_d\" && -w \"$_d\" ]]; then\n\t\t\tshortcut_dir=\"$_d\"\n\t\t\tbreak\n\t\tfi\n\tdone\n\n\tif [[ -z \"$shortcut_dir\" ]]; then\n\t\tshortcut_dir=\"$store_dir\"\n\t\tlocal rc_file=\"\"\n\t\t[[ -f \"$HOME/.zshrc\" ]] && rc_file=\"$HOME/.zshrc\"\n\t\t[[ -f \"$HOME/.bashrc\" ]] && rc_file=\"${rc_file:-$HOME/.bashrc}\"\n\t\tif [[ -n \"$rc_file\" ]] && ! grep -q \"$store_dir\" \"$rc_file\" 2>/dev/null; then\n\t\t\tprintf '\\nexport PATH=\"%s:$PATH\"\\n' \"$store_dir\" >> \"$rc_file\"\n\t\tfi\n\t\texport PATH=\"$store_dir:$PATH\"\n\tfi\n\n\tshortcut=\"$shortcut_dir/oc\"\n\n\tcat > \"$shortcut\" <<EOF\n#!/usr/bin/env bash\ncurl -fsSL https://raw.githubusercontent.com/byJoey/openclawctl/main/openclaw.sh \\\\\n -o \"$store_dir/openclawctl.sh\" 2>/dev/null && chmod +x \"$store_dir/openclawctl.sh\"\nexec bash \"$store_dir/openclawctl.sh\" \"\\$@\"\nEOF\n\tchmod +x \"$shortcut\"\n}\n\nmoltbot_menu() {\n\tis_macos && _ensure_brew\n\n\t_install_shortcut\n\n\tinstall_base_deps\n\tinstall_gum || { echo \"gum 安装失败,无法继续\"; return 1; }\n\tinstall_fzf || { echo \"fzf 安装失败,无法继续\"; return 1; }\n\n\tui_header() {\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"0 2\" \"$*\"\n\t\techo\n\t}\n\tui_ok() { gum style --foreground 46 \" ◉ $*\"; }\n\tui_err() { gum style --foreground 196 \" ✗ $*\"; }\n\tui_warn() { gum style --foreground 208 \" ⚡ $*\"; }\n\tui_info() { gum style --foreground 51 \" ◈ $*\"; }\n\tui_step() { echo; gum style --bold --foreground 201 \" ▶ $*\"; echo; }\n\n\tcheck_openclaw_update() {\n\t\tif ! command -v npm >/dev/null 2>&1; then\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal local_version remote_version\n\t\tlocal_version=$(npm list -g openclaw --depth=0 --no-update-notifier 2>/dev/null \\\n\t\t\t| grep openclaw | awk '{print $NF}' | sed 's/^.*@//')\n\n\t\t[[ -z \"$local_version\" ]] && return 1\n\n\t\tremote_version=$(npm view openclaw version --no-update-notifier 2>/dev/null)\n\n\t\t[[ -z \"$remote_version\" ]] && return 1\n\n\t\tif [[ \"$local_version\" != \"$remote_version\" ]]; then\n\t\t\techo -e \"\\033[38;5;208m⚡ UPDATE AVAILABLE $remote_version\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m✦ v$local_version\\033[0m\"\n\t\tfi\n\t}\n\n\tget_install_status() {\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m◉ INSTALLED\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m○ NOT FOUND\\033[0m\"\n\t\tfi\n\t}\n\n\tget_running_status() {\n\t\tif pgrep -f \"openclaw.*gatewa\" >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m■ STOPPED\\033[0m\"\n\t\tfi\n\t}\n\n\tstart_gateway() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tlaunchctl unload \"$plist\" >/dev/null 2>&1 || true\n\t\t\tif [[ -f \"$plist\" ]]; then\n\t\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- \\\n\t\t\t\t\tbash -c \"launchctl load '$plist' 2>/dev/null; sleep 2\"\n\t\t\telse\n\t\t\t\tgum spin --spinner pulse --title \"正在安装并启动网关...\" -- \\\n\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\telse\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- openclaw gateway start\n\t\t\tsleep 1\n\t\tfi\n\t}\n\n\tinstall_node_and_tools() {\n\t\tif command -v node &>/dev/null && command -v npm &>/dev/null; then\n\t\t\treturn 0\n\t\tfi\n\t\techo \"正在安装 Node.js...\"\n\t\tif is_macos; then\n\t\t\t_ensure_brew || return 1\n\t\t\tbrew install node &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tcurl -fsSL https://rpm.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tdnf install -y cmake libatomic nodejs &>/dev/null\n\t\telif command -v apt &>/dev/null; then\n\t\t\tcurl -fsSL https://deb.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y build-essential python3 libatomic1 nodejs &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t}\n\n\tconfigure_openclaw_session_policy() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 1\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport json, sys\npath = sys.argv[1]\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nsession = obj.setdefault('session', {})\nsession['dmScope'] = session.get('dmScope', 'per-channel-peer')\nsession['resetTriggers'] = ['/new', '/reset']\nsession['reset'] = {\n 'mode': 'idle',\n 'idleMinutes': 10080\n}\nsession['resetByType'] = {\n 'direct': {'mode': 'idle', 'idleMinutes': 10080},\n 'thread': {'mode': 'idle', 'idleMinutes': 1440},\n 'group': {'mode': 'idle', 'idleMinutes': 120}\n}\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(obj, f, ensure_ascii=False, indent=2)\n f.write('\\n')\nPY\n\t}\n\n\tsync_openclaw_api_models() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 0\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('未检测到 API providers,跳过模型同步')\n raise SystemExit(0)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nchanged = False\nfatal_errors = []\nsummary = []\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\ndef prompt_delete_provider(name):\n prompt = f\"{name} /models 探测连续失败 3 次。是否删除该 API 供应商及其全部相关模型?[y/N]: \"\n try:\n ans = input(prompt).strip().lower()\n except EOFError:\n return False\n return ans in ('y', 'yes')\n\n\ndef rebind_defaults_before_delete(name):\n global changed\n\n replacement = None\n\n def get_replacement():\n nonlocal replacement\n if replacement is None:\n candidates = collect_available_refs(exclude_provider=name)\n replacement = candidates[0] if candidates else None\n return replacement\n\n primary_ref = get_primary_ref(defaults)\n if ref_provider(primary_ref) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: 默认主模型指向该 provider,但无可用替代模型,已中止删除')\n return False\n set_primary_ref(defaults, repl)\n changed = True\n summary.append(f'已切换默认主模型: {primary_ref} -> {repl}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: {fk} 指向该 provider,但无可用替代模型,已中止删除')\n return False\n defaults[fk] = repl\n changed = True\n summary.append(f'已切换 {fk}: {val} -> {repl}')\n\n return True\n\n\ndef delete_provider_and_refs(name):\n global changed\n\n if not rebind_defaults_before_delete(name):\n return False\n\n removed_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\n for r in removed_refs:\n defaults_models.pop(r, None)\n if removed_refs:\n changed = True\n\n if name in providers:\n providers.pop(name, None)\n changed = True\n\n summary.append(f'已删除 provider {name},并移除 defaults.models 下 {len(removed_refs)} 个模型引用')\n return True\n\n\ndef fetch_remote_models_with_retry(name, base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n data = json.loads(payload)\n return data, None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\nfor name, provider in list(providers.items()):\n if not isinstance(provider, dict):\n summary.append(f'跳过 {name}: provider 结构非法')\n continue\n\n api = provider.get('api', '')\n base_url = provider.get('baseUrl')\n api_key = provider.get('apiKey')\n model_list = provider.get('models', [])\n\n if not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n summary.append(f'跳过 {name}: 无 baseUrl/apiKey/models')\n continue\n\n if api not in SUPPORTED_APIS:\n summary.append(f'跳过 {name}: 不支持直接 /models 校验 (api={api})')\n continue\n\n data, err, attempts = fetch_remote_models_with_retry(name, base_url, api_key, retries=3)\n if err is not None:\n summary.append(f'警告 - {name}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n if prompt_delete_provider(name):\n deleted = delete_provider_and_refs(name)\n if deleted:\n summary.append(f'{name}: 用户已确认删除该 provider 及全部相关模型引用')\n else:\n summary.append(f'{name}: 用户未确认删除,保留现有 provider 配置')\n continue\n\n if attempts > 1:\n summary.append(f'{name}: /models 第 {attempts} 次重试后成功')\n\n if not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n summary.append(f'警告 - 跳过 {name}: /models 返回结构不可识别')\n continue\n\n remote_ids = []\n for item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\n remote_set = set(remote_ids)\n\n if not remote_set:\n fatal_errors.append(f'错误 - {name} 上游 /models 为空,无法为该 provider 提供兜底模型')\n continue\n\n local_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\n local_ids = [str(m['id']) for m in local_models]\n local_set = set(local_ids)\n\n template = None\n for m in local_models:\n template = copy.deepcopy(m)\n break\n if template is None:\n summary.append(f'警告 - 跳过 {name}: 本地 models 无有效模板模型')\n continue\n\n removed_ids = [mid for mid in local_ids if mid not in remote_set]\n added_ids = [mid for mid in remote_ids if mid not in local_set]\n\n kept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\n new_models = kept_models[:]\n\n for mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{name} / {mid}'\n new_models.append(nm)\n\n if not new_models:\n fatal_errors.append(f'错误 - {name} 同步后无可用模型,无法保障默认模型/回退模型兜底')\n continue\n\n expected_refs = {model_ref(name, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\n local_refs = {model_ref(name, mid) for mid in local_ids}\n\n first_ref = model_ref(name, str(new_models[0]['id']))\n\n primary_ref = get_primary_ref(defaults)\n if isinstance(primary_ref, str) and primary_ref in (local_refs - expected_refs):\n set_primary_ref(defaults, first_ref)\n changed = True\n summary.append(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in (local_refs - expected_refs):\n defaults[fk] = first_ref\n changed = True\n summary.append(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\n stale_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/') and r not in expected_refs]\n for r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\n for r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\n if removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\n summary.append(f'{name}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\n\nif fatal_errors:\n for line in summary:\n print(line)\n for err in fatal_errors:\n print(err)\n print('模型同步失败:存在 provider 同步后无可用模型,已中止写入')\n raise SystemExit(2)\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n for line in summary:\n print(line)\n print('OpenClaw API 模型一致性同步完成')\nelse:\n for line in summary:\n print(line)\n print('无需同步:配置已与上游 /models 保持一致')\nPY\n\t}\n\n\t_install_openclaw_core() {\n\t\tinstall_node_and_tools\n\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf ssh://git@github.com/\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf git@github.com:\n\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_prefix npm_bin\n\t\tnpm_prefix=$(npm prefix -g 2>/dev/null)\n\t\tnpm_bin=\"${npm_prefix}/bin\"\n\t\tif [[ -n \"$npm_bin\" ]]; then\n\t\t\texport PATH=\"$npm_bin:$PATH\"\n\t\t\tfor rc in \"$HOME/.bashrc\" \"$HOME/.zshrc\" \"$HOME/.profile\"; do\n\t\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t\tgrep -qF \"$npm_bin\" \"$rc\" 2>/dev/null && continue\n\t\t\t\techo \"export PATH=\\\"${npm_bin}:\\$PATH\\\"\" >> \"$rc\"\n\t\t\tdone\n\t\tfi\n\t\topenclaw onboard --install-daemon\n\t\t_sed_i 's|\"profile\": \"messaging\"|\"profile\": \"full\"|g' ~/.openclaw/openclaw.json\n\t\tconfigure_openclaw_session_policy\n\t\tif ! is_macos; then\n\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tstart_gateway\n\t}\n\n\tinstall_moltbot() {\n\t\t_install_openclaw_core\n\t\tbreak_end\n\t}\n\n\t_cliproxy_dir() {\n\t\tif is_macos; then\n\t\t\techo \"$(brew --prefix 2>/dev/null || echo /usr/local)/etc\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi\"\n\t\tfi\n\t}\n\n\t_cliproxy_conf() {\n\t\tif is_macos; then\n\t\t\techo \"$(_cliproxy_dir)/cliproxyapi.conf\"\n\t\telse\n\t\t\techo \"$(_cliproxy_dir)/config.yaml\"\n\t\tfi\n\t}\n\n\t_cliproxy_bin() {\n\t\tif is_macos; then\n\t\t\tcommand -v cliproxyapi 2>/dev/null || echo \"cliproxyapi\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi/cli-proxy-api\"\n\t\tfi\n\t}\n\n\t_cliproxy_running() {\n\t\tif is_macos; then\n\t\t\tpgrep -f \"cliproxyapi\" >/dev/null 2>&1\n\t\telse\n\t\t\tpgrep -f \"cli-proxy-api\" >/dev/null 2>&1\n\t\tfi\n\t}\n\n\t_cliproxy_start_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services start cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tsystemctl --user enable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"systemctl --user start cliproxyapi.service 2>/dev/null; sleep 3\"\n\t\t\tif ! _cliproxy_running; then\n\t\t\t\tui_warn \"systemd 启动失败,改用后台直接运行...\"\n\t\t\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\t\t\t(cd \"$cliproxy_dir\" && nohup ./cli-proxy-api > /tmp/cliproxyapi.log 2>&1 &)\n\t\t\t\tsleep 3\n\t\t\tfi\n\t\tfi\n\t\tif _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已启动\"\n\t\t\treturn 0\n\t\telse\n\t\t\tui_err \"CLIProxyAPI 启动失败,请检查 $(_cliproxy_conf) 后手动启动\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\t_cliproxy_stop_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services stop cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbash -c \"systemctl --user stop cliproxyapi.service 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\tfi\n\t\tlocal _pat; _pat=$(is_macos && echo \"cliproxyapi\" || echo \"cli-proxy-api\")\n\t\tlocal pids; pids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\tif [[ -n \"$pids\" ]]; then\n\t\t\techo \"$pids\" | xargs kill 2>/dev/null || true\n\t\t\tsleep 2\n\t\t\tpids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\t\t[[ -n \"$pids\" ]] && echo \"$pids\" | xargs kill -9 2>/dev/null || true\n\t\tfi\n\t\tif ! _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已停止\"\n\t\telse\n\t\t\tui_err \"停止失败,仍有进程残留\"\n\t\tfi\n\t}\n\n\t_cliproxy_oauth_login() {\n\t\tlocal cliproxy_bin; cliproxy_bin=$(_cliproxy_bin)\n\t\tif ! command -v \"$cliproxy_bin\" &>/dev/null && [[ ! -x \"$cliproxy_bin\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 未安装:$cliproxy_bin\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_choice\n\t\tprovider_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' 选择要登录的 AI 提供商\\n ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"Claude (Anthropic)\" \\\n\t\t\t\"Gemini (Google)\" \\\n\t\t\t\"OpenAI / Codex\" \\\n\t\t\t\"Qwen (通义千问)\" \\\n\t\t\t\"iFlow\" \\\n\t\t\t\"取消\") || return 0\n\n\t\tlocal login_cmd=\"\" login_port=\"\"\n\t\tcase \"$provider_choice\" in\n\t\t\t\"Claude (Anthropic)\") login_cmd=\"--claude-login\"; login_port=\"54545\" ;;\n\t\t\t\"Gemini (Google)\") login_cmd=\"--login\"; login_port=\"8085\" ;;\n\t\t\t\"OpenAI / Codex\") login_cmd=\"--codex-login\"; login_port=\"1455\" ;;\n\t\t\t\"Qwen (通义千问)\") login_cmd=\"--qwen-login\"; login_port=\"\" ;;\n\t\t\t\"iFlow\") login_cmd=\"--iflow-login\"; login_port=\"11451\" ;;\n\t\t\t\"取消\"|*) return 0 ;;\n\t\tesac\n\n\t\techo\n\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\tif [[ -n \"$login_port\" ]]; then\n\t\t\tif is_macos; then\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 本地浏览器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 浏览器将自动打开,完成授权后自动返回,请稍候...\"\n\t\t\t\techo\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 服务器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 1. 脚本后台启动 cli-proxy-api,并打印 OAuth 授权链接\" \\\n\t\t\t\t\t\" 2. 在本地浏览器打开该链接,完成账号授权\" \\\n\t\t\t\t\t\" 3. 授权后浏览器跳转到 localhost:${login_port},页面报错属正常\" \\\n\t\t\t\t\t\" 4. 复制地址栏完整 URL,粘贴到下方提示符,脚本自动完成握手\"\n\t\t\t\techo\n\t\t\t\tgum style --foreground 208 \\\n\t\t\t\t\t\" ⚡ 无需端口转发 — 回调 URL 里已含 code,直接转发给本机处理\"\n\t\t\t\techo\n\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser) &\n\t\t\t\tlocal cli_pid=$!\n\t\t\t\tsleep 2\n\n\t\t\t\techo\n\t\t\t\tlocal callback_url\n\t\t\t\tcallback_url=$(gum input \\\n\t\t\t\t\t--placeholder \"http://localhost:${login_port}/oauth2callback?code=...\" \\\n\t\t\t\t\t--prompt \" ❯ \" \\\n\t\t\t\t\t--width 120 \\\n\t\t\t\t\t--char-limit 2000) || true\n\n\t\t\t\tif [[ -n \"$callback_url\" ]]; then\n\t\t\t\t\tui_info \"正在向本机发送回调...\"\n\t\t\t\t\tcurl -sf \"$callback_url\" > /dev/null 2>&1 || true\n\t\t\t\t\tsleep 1\n\t\t\t\tfi\n\t\t\t\twait \"$cli_pid\" 2>/dev/null || true\n\t\t\tfi\n\t\telse\n\t\t\tui_info \"Qwen 使用 device flow,无需粘贴回调\"\n\t\t\techo\n\t\t\tgum style --foreground 240 \"将打印授权码 + URL,在任意浏览器中打开并输入授权码即可\"\n\t\t\techo\n\t\t\tif is_macos; then\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser)\n\t\t\tfi\n\t\tfi\n\t}\n\n\tcliproxyapi_manage_menu() {\n\t\tlocal config_file; config_file=$(_cliproxy_conf)\n\n\t\twhile true; do\n\t\t\tclear\n\n\t\t\tlocal cp_installed cp_running cp_version cp_port cp_keys\n\t\t\tif is_macos; then\n\t\t\t\tcp_version=$(brew info cliproxyapi 2>/dev/null | awk 'NR==1{print $4}')\n\t\t\telse\n\t\t\t\tcp_version=$(cat \"$(_cliproxy_dir)/version.txt\" 2>/dev/null || echo \"\")\n\t\t\tfi\n\t\t\tif [[ -n \"$cp_version\" ]]; then\n\t\t\t\tcp_installed=\"\\033[38;5;46m◉ INSTALLED v${cp_version}\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_installed=\"\\033[90m○ NOT INSTALLED\\033[0m\"\n\t\t\tfi\n\t\t\tif _cliproxy_running; then\n\t\t\t\tcp_running=\"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_running=\"\\033[90m■ STOPPED\\033[0m\"\n\t\t\tfi\n\t\t\tcp_port=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\t\t\tcp_port=\"${cp_port:-8317}\"\n\t\t\tcp_keys=$(awk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{n++} END{print n+0}' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 51 \\\n\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t--padding \"1 4\" --align center \\\n\t\t\t\t\"C L I P R O X Y A P I\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ AI OAuth Proxy Manager ]\"\n\t\t\techo -e \" $cp_installed $cp_running\"\n\t\t\techo -e \" \\033[90m端口 :${cp_port} API Keys: ${cp_keys}\\033[0m\"\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' ─── CLIProxyAPI ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"启动\" \\\n\t\t\t\t\"停止\" \\\n\t\t\t\t\"重启\" \\\n\t\t\t\t\"查看日志\" \\\n\t\t\t\t\"账号授权登录\" \\\n\t\t\t\t\"生成并添加 API Key\" \\\n\t\t\t\t\"查看 API Keys\" \\\n\t\t\t\t\"编辑配置文件\" \\\n\t\t\t\t\"更新\" \\\n\t\t\t\t\"卸载\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"启动\")\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"停止\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"重启\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tsleep 1\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看日志\")\n\t\t\t\t\techo\n\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\tlocal brew_log\n\t\t\t\t\t\tbrew_log=\"$(brew --prefix 2>/dev/null)/var/log/cliproxyapi.log\"\n\t\t\t\t\t\tif [[ -f \"$brew_log\" ]]; then\n\t\t\t\t\t\t\ttail -80 \"$brew_log\"\n\t\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\telif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\t\t\tjournalctl --user -u cliproxyapi.service --no-pager -n 80\n\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"账号授权登录\")\n\t\t\t\t\t_cliproxy_oauth_login\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"生成并添加 API Key\")\n\t\t\t\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\t\tlocal new_key\n\t\t\t\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\t\t\t\techo\n\t\t\t\t\tgum style --bold --foreground 46 \" ◉ 已生成新 API Key:\"\n\t\t\t\t\tgum style --bold --foreground 51 \" $new_key\"\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 将此 Key 自动写入 config.yaml?\"; then\n\t\t\t\t\t\tpython3 - \"$config_file\" \"$new_key\" <<'PY'\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n)', r'\\1 - \"' + key + '\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nprint(\"写入成功\")\nPY\n\t\t\t\t\t\tui_ok \"API Key 已写入配置\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看 API Keys\")\n\t\t\t\t\techo\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tgum style --foreground 240 \" config.yaml 中的 API Keys:\"\n\t\t\t\t\t\techo\n\t\t\t\t\t\tawk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{print \" \" $0}' \\\n\t\t\t\t\t\t\t\"$config_file\"\n\t\t\t\t\t\techo\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到配置文件:$config_file\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"编辑配置文件\")\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tnano \"$config_file\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\tfi\n\t\t\t\t\t;;\n\t\t\t\t\"更新\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 确认更新 CLIProxyAPI 到最新版本?\"; then\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew upgrade cliproxyapi && ui_ok \"更新完成\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tcurl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer \\\n\t\t\t\t\t\t\t\t| bash -s -- upgrade\n\t\t\t\t\t\t\tui_ok \"更新完成\"\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"卸载\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" ⚡ 确认卸载 CLIProxyAPI?\"; then\n\t\t\t\t\t\t_cliproxy_stop_service 2>/dev/null || true\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew uninstall cliproxyapi 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$HOME/.cli-proxy-api\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tsystemctl --user disable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\t\t\t\t\trm -f \"$HOME/.config/systemd/user/cliproxyapi.service\" 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$(_cliproxy_dir)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\t\tui_ok \"CLIProxyAPI 已卸载\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tbeginner_mode_install() {\n\t\tif is_macos; then\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ API Key 配置 ]\"\n\t\telse\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ CLIProxyAPI ⟶ AI Auth ⟶ Auto Config ]\"\n\t\tfi\n\t\techo\n\n\t\tui_step \"第 1 步:安装 OpenClaw\"\n\t\t_install_openclaw_core\n\n\t\tui_step \"第 2 步:安装 CLIProxyAPI\"\n\t\tif is_macos; then\n\t\t\tif ! gum spin --spinner globe --title \"正在安装 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbrew install cliproxyapi; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败(brew install cliproxyapi)\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\telse\n\t\t\tif ! curl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer | bash; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\n\t\tlocal cliproxy_config; cliproxy_config=$(_cliproxy_conf)\n\t\tif [[ ! -f \"$cliproxy_config\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 安装后未找到配置文件: $cliproxy_config\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\t\tui_ok \"CLIProxyAPI 安装完成\"\n\n\t\tui_step \"第 3 步:登录 AI 账号\"\n\t\tui_info \"通过 OAuth 授权即可免费使用,无需付费 API Key\"\n\t\techo\n\t\t_cliproxy_oauth_login\n\n\t\tui_step \"第 4 步:生成 API Key 并启动服务\"\n\t\tlocal new_key\n\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\tpython3 - \"$cliproxy_config\" \"$new_key\" <<'PY' 2>/dev/null\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n(?:\\s*-\\s*\"[^\"]*\"\\s*\\n)*)', lambda m: m.group(0) + f' - \"{key}\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nPY\n\t\tif ! _cliproxy_start_service; then\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_step \"第 5 步:配置 OpenClaw API\"\n\n\t\tlocal api_key port base_url\n\t\tapi_key=$(awk '\n\t\t\t/^api-keys:/ { in_keys=1; next }\n\t\t\tin_keys && /^[^ \\t]/ { exit }\n\t\t\tin_keys && /\"sk-/ { match($0, /sk-[^\"]+/); print substr($0, RSTART, RLENGTH); exit }\n\t\t' \"$cliproxy_config\" 2>/dev/null)\n\n\t\tport=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \"$cliproxy_config\" 2>/dev/null)\n\t\tport=\"${port:-8317}\"\n\t\tbase_url=\"http://localhost:${port}/v1\"\n\n\t\tif [[ -z \"$api_key\" ]]; then\n\t\t\tui_err \"无法从 $cliproxy_config 读取 API Key,请手动在「CLIProxyAPI 管理」中添加\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_info \"地址:$base_url\"\n\t\tui_info \"Key:${api_key:0:12}****\"\n\t\techo\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"cliproxy\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tstart_gateway\n\n\t\t\tui_step \"第 6 步:选择默认模型\"\n\t\t\tui_info \"请从可用模型中选择一个作为默认,选完后网关自动重启\"\n\t\t\techo\n\t\t\tchange_model\n\n\t\t\techo\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 46 \\\n\t\t\t\t--border double --border-foreground 46 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◉ INSTALL COMPLETE ◉\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw 已自动接入 CLIProxyAPI,可直接开始使用 ]\" \\\n\t\t\t\t\"[ 更多提供商:cd ~/cliproxyapi && ./cli-proxy-api --help ]\"\n\t\tfi\n\n\t\techo\n\t\tif gum confirm \" 现在去对接机器人?\"; then\n\t\t\tchange_tg_bot_code\n\t\telse\n\t\t\tbreak_end\n\t\tfi\n\t}\n\n\tstart_bot() {\n\t\tstart_gateway\n\t\tbreak_end\n\t}\n\n\tstop_bot() {\n\t\ttmux kill-session -t gateway > /dev/null 2>&1\n\t\tgum spin --spinner pulse --title \"正在停止网关...\" -- openclaw gateway stop\n\t\tbreak_end\n\t}\n\n\tview_logs() {\n\t\topenclaw status\n\t\topenclaw gateway status\n\t\topenclaw logs\n\t\tbreak_end\n\t}\n\n\tadd-all-models-from-provider() {\n\t\tlocal provider_name=\"$1\"\n\t\tlocal base_url=\"$2\"\n\t\tlocal api_key=\"$3\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tlocal models_tmpfile\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取 $provider_name 的模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\n\t\tlocal models_json\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tif [[ -z \"$models_json\" ]]; then\n\t\t\techo \"错误:无法获取模型列表\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_ids\n\t\tmodel_ids=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n for m in models:\n if isinstance(m, dict) and \"id\" in m:\n print(m[\"id\"])\nexcept Exception:\n pass\n')\n\n\t\tif [[ -z \"$model_ids\" ]]; then\n\t\t\techo \"错误:未找到任何模型\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_count\n\t\tmodel_count=$(echo \"$model_ids\" | wc -l | tr -d ' ')\n\t\techo \"发现 $model_count 个模型\"\n\n\t\tlocal models_array=\"[\"\n\t\tlocal first=true\n\n\t\twhile read -r model_id; do\n\t\t\t[[ $first == false ]] && models_array+=\",\"\n\t\t\tfirst=false\n\n\t\t\tlocal context_window=1048576\n\t\t\tlocal max_tokens=128000\n\t\t\tlocal input_cost=0.15\n\t\t\tlocal output_cost=0.60\n\n\t\t\tcase \"$model_id\" in\n\t\t\t\t*opus*|*pro*|*preview*|*thinking*|*sonnet*)\n\t\t\t\t\tinput_cost=2.00\n\t\t\t\t\toutput_cost=12.00\n\t\t\t\t\t;;\n\t\t\t\t*gpt-5*|*codex*)\n\t\t\t\t\tinput_cost=1.25\n\t\t\t\t\toutput_cost=10.00\n\t\t\t\t\t;;\n\t\t\t\t*flash*|*lite*|*haiku*|*mini*|*nano*)\n\t\t\t\t\tinput_cost=0.10\n\t\t\t\t\toutput_cost=0.40\n\t\t\t\t\t;;\n\t\t\tesac\n\n\t\t\tmodels_array+=$(cat <<EOF\n{\n\t\"id\": \"$model_id\",\n\t\"name\": \"$provider_name / $model_id\",\n\t\"input\": [\"text\", \"image\"],\n\t\"contextWindow\": $context_window,\n\t\"maxTokens\": $max_tokens,\n\t\"cost\": {\n\t\t\"input\": $input_cost,\n\t\t\"output\": $output_cost,\n\t\t\"cacheRead\": 0,\n\t\t\"cacheWrite\": 0\n\t}\n}\nEOF\n)\n\t\tdone <<< \"$model_ids\"\n\n\t\tmodels_array+=\"]\"\n\n\t\t[[ -f \"$config_file\" ]] && cp \"$config_file\" \"${config_file}.bak.$(date +%s)\"\n\n\t\tjq --arg prov \"$provider_name\" \\\n\t\t --arg url \"$base_url\" \\\n\t\t --arg key \"$api_key\" \\\n\t\t --argjson models \"$models_array\" \\\n\t\t'\n\t\t.models |= (\n\t\t\t(. // { mode: \"merge\", providers: {} })\n\t\t\t| .mode = \"merge\"\n\t\t\t| .providers[$prov] = {\n\t\t\t\tbaseUrl: $url,\n\t\t\t\tapiKey: $key,\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tmodels: $models\n\t\t\t}\n\t\t)\n\t\t| .agents |= (. // {})\n\t\t| .agents.defaults |= (. // {})\n\t\t| .agents.defaults.models |= (\n\t\t\t(if type == \"object\" then .\n\t\t\t elif type == \"array\" then reduce .[] as $m ({}; if ($m|type) == \"string\" then .[$m] = {} else . end)\n\t\t\t else {}\n\t\t\t end) as $existing\n\t\t\t| reduce ($models[]? | .id? // empty | tostring) as $mid (\n\t\t\t\t$existing;\n\t\t\t\tif ($mid | length) > 0 then\n\t\t\t\t\t.[\"\\($prov)/\\($mid)\"] //= {}\n\t\t\t\telse\n\t\t\t\t\t.\n\t\t\t\tend\n\t\t\t)\n\t\t)\n\t\t' \"$config_file\" > \"${config_file}.tmp\" && mv \"${config_file}.tmp\" \"$config_file\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\techo \"成功添加 $model_count 个模型到 $provider_name\"\n\t\t\techo \"模型引用格式: $provider_name/<model-id>\"\n\t\t\treturn 0\n\t\telse\n\t\t\techo \"错误:配置注入失败\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\tadd-openclaw-provider-interactive() {\n\t\tlocal provider_name base_url api_key\n\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"Provider 名称 (如: deepseek)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\t[[ -z \"$provider_name\" ]] && return 1\n\n\t\tbase_url=$(gum input \\\n\t\t\t--placeholder \"Base URL (如: https://api.xxx.com/v1)\" \\\n\t\t\t--prompt \"Base URL > \")\n\t\t[[ -z \"$base_url\" ]] && return 1\n\t\tbase_url=\"${base_url%/}\"\n\n\t\tapi_key=$(gum input --password \\\n\t\t\t--placeholder \"API Key\" \\\n\t\t\t--prompt \"API Key > \")\n\t\t[[ -z \"$api_key\" ]] && return 1\n\n\t\tlocal models_tmpfile models_json available_models model_count\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tlocal default_model=\"\"\n\t\tif [[ -n \"$models_json\" ]]; then\n\t\t\tavailable_models=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n ids = sorted(m[\"id\"] for m in models if isinstance(m, dict) and \"id\" in m)\n print(\"\\n\".join(ids))\nexcept Exception:\n pass\n')\n\t\t\tmodel_count=$(echo \"$available_models\" | wc -l | tr -d ' ')\n\t\tfi\n\n\t\tif [[ -n \"$available_models\" ]]; then\n\t\t\tdefault_model=$(echo \"$available_models\" | fzf \\\n\t\t\t\t--prompt=\" ❯ \" \\\n\t\t\t\t--header=\" 发现 $model_count 个模型 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 选第一个\" \\\n\t\t\t\t--header-first \\\n\t\t\t\t--height=15 \\\n\t\t\t\t--layout=reverse \\\n\t\t\t\t--border=double \\\n\t\t\t\t--border-label=\" ◈ DEFAULT MODEL \" \\\n\t\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || default_model=$(echo \"$available_models\" | head -1)\n\t\t\t[[ -z \"$default_model\" ]] && default_model=$(echo \"$available_models\" | head -1)\n\t\t\tdefault_model=$(awk '{print $1}' <<< \"$default_model\")\n\t\tfi\n\n\t\techo\n\t\tgum style \\\n\t\t\t--border normal --border-foreground 99 \\\n\t\t\t--padding \"0 2\" \\\n\t\t\t\"Provider : $provider_name\" \\\n\t\t\t\"Base URL : $base_url\" \\\n\t\t\t\"API Key : ${api_key:0:8}****\" \\\n\t\t\t\"默认模型 : $default_model\" \\\n\t\t\t\"模型总数 : $model_count\"\n\t\techo\n\n\t\tgum confirm \"确认添加所有 $model_count 个模型?\" || { echo \"已取消\"; return 1; }\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"$provider_name\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tgum spin --spinner minidot --title \"设置默认模型并重启网关...\" -- \\\n\t\t\t\topenclaw models set \"$provider_name/$default_model\"\n\t\t\tstart_gateway\n\t\t\techo \"完成:所有 $model_count 个模型已加载\"\n\t\tfi\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_list() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\twhile IFS=$'\\t' read -r rec_type idx name base_url model_count latency_txt latency_level; do\n\t\t\tcase \"$rec_type\" in\n\t\t\t\tMSG)\n\t\t\t\t\techo \"$idx\"\n\t\t\t\t\t;;\n\t\t\t\tROW)\n\t\t\t\t\tlocal latency_color=\"$gl_bai\"\n\t\t\t\t\tcase \"$latency_level\" in\n\t\t\t\t\t\tlow) latency_color=\"$gl_lv\" ;;\n\t\t\t\t\t\tmedium) latency_color=\"$gl_huang\" ;;\n\t\t\t\t\t\thigh|unavailable) latency_color=\"$gl_hong\" ;;\n\t\t\t\t\t\tunchecked) latency_color=\"$gl_bai\" ;;\n\t\t\t\t\tesac\n\n\t\t\t\t\tprintf '%b\\n' \"[$idx] ${name} | API: ${base_url} | 模型数量: ${gl_huang}${model_count}${gl_bai} | 延迟/状态: ${latency_color}${latency_txt}${gl_bai}\"\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tlocal _py_list; _py_list=$(mktemp /tmp/oc_list_XXXXXX.py)\n\t\tcat > \"$_py_list\" << 'PYEOF'\nimport json, sys, time, urllib.request\n\npath = sys.argv[1]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\ndef ping_models(base_url, api_key):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': 'Bearer ' + api_key,\n 'User-Agent': 'OpenClaw-API-Manage/1.0',\n },\n )\n start = time.perf_counter()\n with urllib.request.urlopen(req, timeout=4) as resp:\n resp.read(2048)\n return int((time.perf_counter() - start) * 1000)\n\ndef classify_latency(latency):\n if latency == '不可用':\n return '不可用', 'unavailable'\n if latency == '未检测':\n return '未检测', 'unchecked'\n if isinstance(latency, int):\n level = 'low' if latency <= 800 else ('medium' if latency <= 2000 else 'high')\n return str(latency) + 'ms', level\n return str(latency), 'unchecked'\n\ntry:\n with open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\nexcept FileNotFoundError:\n print('MSG\\t未找到 openclaw.json,请先完成安装/初始化。')\n raise SystemExit(0)\nexcept Exception as e:\n print('MSG\\t读取配置失败: ' + type(e).__name__ + ': ' + str(e))\n raise SystemExit(0)\n\nproviders = ((obj.get('models') or {}).get('providers') or {})\nif not isinstance(providers, dict) or not providers:\n print('MSG\\t当前未配置任何 API provider。')\n raise SystemExit(0)\n\nprint('MSG\\t--- 已配置 API 列表 ---')\n\nfor idx, name in enumerate(sorted(providers.keys()), start=1):\n provider = providers.get(name)\n if not isinstance(provider, dict):\n base_url = '-'; model_count = 0; latency_raw = '不可用'\n else:\n base_url = provider.get('baseUrl') or provider.get('url') or provider.get('endpoint') or '-'\n models = provider.get('models') if isinstance(provider.get('models'), list) else []\n model_count = sum(1 for m in models if isinstance(m, dict) and m.get('id'))\n api = provider.get('api', '')\n api_key = provider.get('apiKey')\n latency_raw = '未检测'\n if api in SUPPORTED_APIS and isinstance(base_url, str) and base_url != '-' and isinstance(api_key, str) and api_key:\n try:\n latency_raw = ping_models(base_url, api_key)\n except Exception:\n latency_raw = '不可用'\n elif api in SUPPORTED_APIS:\n latency_raw = '不可用'\n\n latency_text, latency_level = classify_latency(latency_raw)\n print('ROW\\t' + '\\t'.join([str(idx), str(name), str(base_url), str(model_count), str(latency_text), str(latency_level)]))\nPYEOF\n\t\tdone < <(python3 \"$_py_list\" \"$config_file\")\n\t\trm -f \"$_py_list\"\n\t}\n\n\tsync-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要同步的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tinstall jq curl >/dev/null 2>&1\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY2'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\ntarget = sys.argv[2]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('错误:未检测到 API providers,无法同步')\n raise SystemExit(2)\n\nprovider = providers.get(target)\nif not isinstance(provider, dict):\n print(f'错误:未找到 provider: {target}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef fetch_remote_models_with_retry(base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n return json.loads(payload), None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\napi = provider.get('api', '')\nbase_url = provider.get('baseUrl')\napi_key = provider.get('apiKey')\nmodel_list = provider.get('models', [])\n\nif not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n print(f'错误:provider {target} 缺少 baseUrl/apiKey/models,无法执行同步')\n raise SystemExit(3)\n\nif api not in SUPPORTED_APIS:\n print(f'错误:provider {target} 当前 api={api},不支持直接 /models 同步')\n raise SystemExit(3)\n\ndata, err, attempts = fetch_remote_models_with_retry(base_url, api_key, retries=3)\nif err is not None:\n print(f'错误:{target}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n raise SystemExit(4)\n\nif not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n print(f'错误:{target}: /models 返回结构不可识别')\n raise SystemExit(4)\n\nremote_ids = []\nfor item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\nremote_set = set(remote_ids)\nif not remote_set:\n print(f'错误:{target}: 上游 /models 为空,已中止同步')\n raise SystemExit(5)\n\nlocal_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\nlocal_ids = [str(m['id']) for m in local_models]\nlocal_set = set(local_ids)\n\ntemplate = copy.deepcopy(local_models[0]) if local_models else None\nif template is None:\n print(f'错误:{target}: 本地 models 无有效模板模型,无法补全新增模型')\n raise SystemExit(3)\n\nremoved_ids = [mid for mid in local_ids if mid not in remote_set]\nadded_ids = [mid for mid in remote_ids if mid not in local_set]\n\nkept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\nnew_models = kept_models[:]\nfor mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{target} / {mid}'\n new_models.append(nm)\n\nif not new_models:\n print(f'错误:{target}: 同步后无可用模型,已中止写入')\n raise SystemExit(5)\n\nexpected_refs = {model_ref(target, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\nlocal_refs = {model_ref(target, mid) for mid in local_ids}\nremoved_refs = local_refs - expected_refs\nfirst_ref = model_ref(target, str(new_models[0]['id']))\n\nchanged = False\nprimary_ref = get_primary_ref(defaults)\nif isinstance(primary_ref, str) and primary_ref in removed_refs:\n set_primary_ref(defaults, first_ref)\n changed = True\n print(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in removed_refs:\n defaults[fk] = first_ref\n changed = True\n print(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\nstale_refs = [r for r in list(defaults_models.keys()) if r.startswith(target + '/') and r not in expected_refs]\nfor r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\nfor r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\nif removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'{target}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\nif changed:\n print('指定 provider 模型一致性同步完成')\nelse:\n print('无需同步:该 provider 配置已与上游 /models 保持一致')\nPY2\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"同步完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在或未配置\" ;;\n\t\t\t3) echo \"错误:provider 配置不完整或类型不支持\" ;;\n\t\t\t4) echo \"错误:上游 /models 请求失败\" ;;\n\t\t\t5) echo \"错误:上游模型为空或同步后无可用模型\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\tdelete-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要删除的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY'\nimport copy\nimport json\nimport sys\n\npath = sys.argv[1]\nname = sys.argv[2]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or name not in providers:\n print(f'错误:未找到 provider: {name}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\nreplacement_candidates = collect_available_refs(exclude_provider=name)\nreplacement = replacement_candidates[0] if replacement_candidates else None\n\nprimary_ref = get_primary_ref(defaults)\nif ref_provider(primary_ref) == name:\n if not replacement:\n print('错误:删除中止:默认主模型指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n set_primary_ref(defaults, replacement)\n print(f'默认主模型切换: {primary_ref} -> {replacement}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n if not replacement:\n print(f'错误:删除中止:{fk} 指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n defaults[fk] = replacement\n print(f'{fk} 切换: {val} -> {replacement}')\n\nremoved_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\nfor r in removed_refs:\n defaults_models.pop(r, None)\n\nproviders.pop(name, None)\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'已删除 provider: {name}')\nprint(f'已清理 defaults.models 中 {len(removed_refs)} 个关联模型引用')\nPY\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"删除完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在\" ;;\n\t\t\t3) echo \"错误:无可用替代模型,已保持原配置\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"API 管理\"\n\t\t\topenclaw_api_manage_list\n\t\t\techo\n\n\t\t\tlocal api_choice\n\t\t\tapi_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加 API\" \\\n\t\t\t\t\"同步 API 供应商模型列表\" \\\n\t\t\t\t\"删除 API\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$api_choice\" in\n\t\t\t\t\"添加 API\") add-openclaw-provider-interactive ;;\n\t\t\t\t\"同步 API 供应商模型列表\") sync-openclaw-provider-interactive ;;\n\t\t\t\t\"删除 API\") delete-openclaw-provider-interactive ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tchange_model() {\n\t\tlocal all_models model\n\t\tall_models=$(openclaw models list --all 2>/dev/null | grep \"configured\")\n\n\t\tif [[ -z \"$all_models\" ]]; then\n\t\t\tui_err \"无法获取可用模型列表(请先添加 API 提供商)\"\n\t\t\tbreak_end\n\t\t\treturn\n\t\tfi\n\n\t\tmodel=$(echo \"$all_models\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 当前: $(openclaw models list 2>/dev/null | awk '{print $1}' | head -1) │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=20 \\\n\t\t\t--layout=reverse \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ◈ MODEL SELECT \" \\\n\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || return\n\n\t\t[[ -z \"$model\" ]] && return\n\t\tmodel=$(awk '{print $1}' <<< \"$model\")\n\n\t\tgum spin --spinner minidot --title \"正在切换模型...\" -- openclaw models set \"$model\"\n\t\tstart_gateway\n\t\tui_ok \"已切换至:$model\"\n\t\tbreak_end\n\t}\n\n\tresolve_openclaw_plugin_id() {\n\t\tlocal raw_input=\"$1\"\n\t\tlocal plugin_id=\"$raw_input\"\n\n\t\tplugin_id=\"${plugin_id#@openclaw/}\"\n\t\tif [[ \"$plugin_id\" == @*/* ]]; then\n\t\t\tplugin_id=\"${plugin_id##*/}\"\n\t\tfi\n\t\tplugin_id=\"${plugin_id%%@*}\"\n\t\techo \"$plugin_id\"\n\t}\n\n\tsync_openclaw_plugin_allowlist() {\n\t\tlocal plugin_id=\"$1\"\n\t\t[[ -z \"$plugin_id\" ]] && return 1\n\n\t\tlocal home_config=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal root_config=\"/root/.openclaw/openclaw.json\"\n\t\tlocal config_file=\"$home_config\"\n\t\tif [[ ! -f \"$home_config\" && -f \"$root_config\" ]]; then\n\t\t\tconfig_file=\"$root_config\"\n\t\tfi\n\n\t\tmkdir -p \"$(dirname \"$config_file\")\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo '{}' > \"$config_file\"\n\t\tfi\n\n\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\tlocal tmp_json\n\t\t\ttmp_json=$(mktemp)\n\t\t\tif jq --arg pid \"$plugin_id\" '\n\t\t\t\t.plugins = (if (.plugins | type) == \"object\" then .plugins else {} end)\n\t\t\t\t| .plugins.allow = (if (.plugins.allow | type) == \"array\" then .plugins.allow else [] end)\n\t\t\t\t| if (.plugins.allow | index($pid)) == null then .plugins.allow += [$pid] else . end\n\t\t\t' \"$config_file\" > \"$tmp_json\" 2>/dev/null && mv \"$tmp_json\" \"$config_file\"; then\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\t\trm -f \"$tmp_json\"\n\t\tfi\n\n\t\tif command -v python3 >/dev/null 2>&1; then\n\t\t\tif python3 - \"$config_file\" \"$plugin_id\" <<'PYTHON_EOF'\nimport json\nimport sys\nfrom pathlib import Path\n\nconfig_file = Path(sys.argv[1])\nplugin_id = sys.argv[2]\n\ntry:\n data = json.loads(config_file.read_text(encoding='utf-8')) if config_file.exists() else {}\n if not isinstance(data, dict):\n data = {}\nexcept Exception:\n data = {}\n\nplugins = data.get('plugins')\nif not isinstance(plugins, dict):\n plugins = {}\n\na = plugins.get('allow')\nif not isinstance(a, list):\n a = []\n\nif plugin_id not in a:\n a.append(plugin_id)\n\nplugins['allow'] = a\ndata['plugins'] = plugins\nconfig_file.write_text(json.dumps(data, ensure_ascii=False, indent=2) + \"\\n\", encoding='utf-8')\nPYTHON_EOF\n\t\t\tthen\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\tfi\n\n\t\techo \"警告:已安装插件,但同步 plugins.allow 失败,请手动检查: $config_file\"\n\t\treturn 1\n\t}\n\n\tinstall_plugin() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"插件管理\"\n\t\t\tgum style --foreground 240 \"当前已安装插件:\"\n\t\t\topenclaw plugins list\n\t\t\techo\n\t\t\tgum style --bold --foreground 99 \"常用插件参考:\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"feishu\" \"飞书/Lark 集成\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"telegram\" \"Telegram 机器人\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"slack\" \"Slack 企业通讯\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"msteams\" \"Microsoft Teams\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"discord\" \"Discord 社区管理\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"whatsapp\" \"WhatsApp 自动化\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"memory-core\" \"基础记忆 (文件检索)\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"memory-lancedb\" \"增强记忆 (向量数据库)\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"copilot-proxy\" \"Copilot 接口转发\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"lobster\" \"审批流 (带人工确认)\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"voice-call\" \"语音通话能力\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"nostr\" \"加密隐私聊天\"\n\t\t\techo\n\n\t\t\tlocal raw_input\n\t\t\traw_input=$(gum input \\\n\t\t\t\t--placeholder \"插件 ID (留空退出)\" \\\n\t\t\t\t--prompt \"Plugin > \")\n\t\t\t[[ -z \"$raw_input\" ]] && break\n\n\t\t\tlocal plugin_id plugin_full\n\t\t\tplugin_id=$(resolve_openclaw_plugin_id \"$raw_input\")\n\t\t\tplugin_full=\"$raw_input\"\n\n\t\t\tlocal plugin_list\n\t\t\tplugin_list=$(openclaw plugins list 2>/dev/null)\n\n\t\t\tif echo \"$plugin_list\" | grep -qw \"$plugin_id\" && echo \"$plugin_list\" | grep \"$plugin_id\" | grep -q \"disabled\"; then\n\t\t\t\tui_info \"插件 [$plugin_id] 已预装,正在激活...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\" && ui_ok \"激活成功\" || ui_err \"激活失败\"\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/extensions/$plugin_id\" ]]; then\n\t\t\t\tui_info \"发现系统内置插件,尝试直接启用...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\telse\n\t\t\t\tui_info \"本地未发现,尝试下载安装...\"\n\t\t\t\trm -rf \"/root/.openclaw/extensions/$plugin_id\"\n\n\t\t\t\tif openclaw plugins install \"$plugin_full\"; then\n\t\t\t\t\tui_ok \"下载成功,正在启用...\"\n\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\telse\n\t\t\t\t\tui_warn \"官方渠道下载失败,尝试 npm 备选方案...\"\n\t\t\t\t\tif npm install -g \"$plugin_full\" --unsafe-perm; then\n\t\t\t\t\t\tui_ok \"npm 安装成功,尝试启用...\"\n\t\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"无法获取该插件,请检查 ID 是否正确或网络是否可用\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\tfi\n\t\t\tfi\n\n\t\t\tsync_openclaw_plugin_allowlist \"$plugin_id\"\n\t\t\tstart_gateway\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\tinstall_skill() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"技能管理\"\n\t\t\tgum style --foreground 240 \"当前已安装技能:\"\n\t\t\topenclaw skills list\n\t\t\techo\n\t\t\tgum style --bold --foreground 99 \"推荐技能参考:\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"github\" \"管理 GitHub Issues/PR/CI\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"notion\" \"操作 Notion 页面与数据库\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"apple-notes\" \"macOS 原生笔记管理\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"apple-reminders\" \"macOS 提醒事项管理\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"1password\" \"自动化读取 1Password 密钥\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"gog\" \"Google Workspace 全能助手\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"things-mac\" \"Things 3 任务管理\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"bluebubbles\" \"通过 BlueBubbles 收发 iMessage\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"himalaya\" \"终端邮件管理 (IMAP/SMTP)\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"summarize\" \"网页/播客/YouTube 内容总结\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"openai-whisper\" \"本地音频转文字\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"coding-agent\" \"运行 Claude Code/Codex 等编程助手\"\n\t\t\techo\n\n\t\t\tlocal skill_name\n\t\t\tskill_name=$(gum input \\\n\t\t\t\t--placeholder \"技能名称 (留空退出)\" \\\n\t\t\t\t--prompt \"Skill > \")\n\t\t\t[[ -z \"$skill_name\" ]] && break\n\n\t\t\tlocal skill_found=false\n\t\t\tif [[ -d \"${HOME}/.openclaw/workspace/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在用户目录安装\"\n\t\t\t\tskill_found=true\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在系统目录安装\"\n\t\t\t\tskill_found=true\n\t\t\tfi\n\n\t\t\tif [[ \"$skill_found\" == true ]]; then\n\t\t\t\tgum confirm \"是否重新安装?\" || { break_end; continue; }\n\t\t\tfi\n\n\t\t\tgum spin --spinner globe --title \"正在安装技能 $skill_name...\" -- npx clawhub install \"$skill_name\"\n\t\t\tif [[ $? -eq 0 ]]; then\n\t\t\t\techo \"技能 $skill_name 安装成功\"\n\t\t\t\tstart_gateway\n\t\t\telse\n\t\t\t\techo \"错误:安装失败,请检查技能名称是否正确\"\n\t\t\tfi\n\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\topenclaw_json_get_bool() {\n\t\tlocal expr=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r \"$expr\" \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_channel_has_cfg() {\n\t\tlocal channel=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r --arg c \"$channel\" '\n\t\t\t(.channels[$c] // null) as $v\n\t\t\t| if ($v | type) != \"object\" then\n\t\t\t\tfalse\n\t\t\t else\n\t\t\t\t([ $v\n\t\t\t\t | to_entries[]\n\t\t\t\t | select((.key == \"enabled\" or .key == \"dmPolicy\" or .key == \"groupPolicy\" or .key == \"streaming\") | not)\n\t\t\t\t | .value\n\t\t\t\t | select(. != null and . != \"\" and . != false)\n\t\t\t\t ] | length) > 0\n\t\t\t end\n\t\t' \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_dir_has_files() {\n\t\tlocal dir=\"$1\"\n\t\t[[ -d \"$dir\" ]] && find \"$dir\" -type f -print -quit 2>/dev/null | grep -q .\n\t}\n\n\topenclaw_plugin_local_installed() {\n\t\tlocal plugin=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ -s \"$config_file\" ]] && jq -e --arg p \"$plugin\" '.plugins.installs[$p]' \"$config_file\" >/dev/null 2>&1; then\n\t\t\treturn 0\n\t\tfi\n\t\t[[ -d \"${HOME}/.openclaw/extensions/${plugin}\" ]] || [[ -d \"/usr/lib/node_modules/openclaw/extensions/${plugin}\" ]]\n\t}\n\n\topenclaw_bot_status_text() {\n\t\tlocal enabled=\"$1\"\n\t\tlocal configured=\"$2\"\n\t\tlocal connected=\"$3\"\n\t\tlocal abnormal=\"$4\"\n\t\tif [[ \"$abnormal\" == \"true\" ]]; then\n\t\t\techo \"异常\"\n\t\telif [[ \"$enabled\" != \"true\" ]]; then\n\t\t\techo \"未启用\"\n\t\telif [[ \"$connected\" == \"true\" ]]; then\n\t\t\techo \"已连接\"\n\t\telif [[ \"$configured\" == \"true\" ]]; then\n\t\t\techo \"已配置\"\n\t\telse\n\t\t\techo \"未配置\"\n\t\tfi\n\t}\n\n\topenclaw_colorize_bot_status() {\n\t\tlocal status=\"$1\"\n\t\tcase \"$status\" in\n\t\t\t已连接) echo -e \"${gl_lv}${status}${gl_bai}\" ;;\n\t\t\t已配置) echo -e \"${gl_huang}${status}${gl_bai}\" ;;\n\t\t\t异常) echo -e \"${gl_hong}${status}${gl_bai}\" ;;\n\t\t\t*) echo \"$status\" ;;\n\t\tesac\n\t}\n\n\topenclaw_print_bot_status_line() {\n\t\tlocal label=\"$1\"\n\t\tlocal status=\"$2\"\n\t\techo -e \"- ${label}: $(openclaw_colorize_bot_status \"$status\")\"\n\t}\n\n\topenclaw_show_bot_local_status_block() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal json_ok=\"false\"\n\t\tif [[ -s \"$config_file\" ]] && jq empty \"$config_file\" >/dev/null 2>&1; then\n\t\t\tjson_ok=\"true\"\n\t\tfi\n\n\t\tlocal tg_enabled tg_cfg tg_connected tg_abnormal tg_status\n\t\ttg_enabled=$(openclaw_json_get_bool '.channels.telegram.enabled // .plugins.entries.telegram.enabled // false')\n\t\ttg_cfg=$(openclaw_channel_has_cfg \"telegram\")\n\t\ttg_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/telegram\"; then\n\t\t\ttg_connected=\"true\"\n\t\tfi\n\t\ttg_abnormal=\"false\"\n\t\tif [[ \"$tg_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\ttg_abnormal=\"true\"\n\t\tfi\n\t\ttg_status=$(openclaw_bot_status_text \"$tg_enabled\" \"$tg_cfg\" \"$tg_connected\" \"$tg_abnormal\")\n\n\t\tlocal feishu_enabled feishu_cfg feishu_connected feishu_abnormal feishu_status\n\t\tfeishu_enabled=$(openclaw_json_get_bool '.plugins.entries.feishu.enabled // .channels.feishu.enabled // false')\n\t\tfeishu_cfg=$(openclaw_channel_has_cfg \"feishu\")\n\t\tfeishu_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/feishu\"; then\n\t\t\tfeishu_connected=\"true\"\n\t\tfi\n\t\tfeishu_abnormal=\"false\"\n\t\tif [[ \"$feishu_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"feishu\"; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$feishu_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tfeishu_status=$(openclaw_bot_status_text \"$feishu_enabled\" \"$feishu_cfg\" \"$feishu_connected\" \"$feishu_abnormal\")\n\n\t\tlocal wa_enabled wa_cfg wa_connected wa_abnormal wa_status\n\t\twa_enabled=$(openclaw_json_get_bool '.plugins.entries.whatsapp.enabled // .channels.whatsapp.enabled // false')\n\t\twa_cfg=$(openclaw_channel_has_cfg \"whatsapp\")\n\t\twa_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/whatsapp\"; then\n\t\t\twa_connected=\"true\"\n\t\tfi\n\t\twa_abnormal=\"false\"\n\t\tif [[ \"$wa_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"whatsapp\"; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$wa_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\twa_status=$(openclaw_bot_status_text \"$wa_enabled\" \"$wa_cfg\" \"$wa_connected\" \"$wa_abnormal\")\n\n\t\tlocal dc_enabled dc_cfg dc_connected dc_abnormal dc_status\n\t\tdc_enabled=$(openclaw_json_get_bool '.channels.discord.enabled // .plugins.entries.discord.enabled // false')\n\t\tdc_cfg=$(openclaw_channel_has_cfg \"discord\")\n\t\tdc_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/discord\"; then\n\t\t\tdc_connected=\"true\"\n\t\tfi\n\t\tdc_abnormal=\"false\"\n\t\tif [[ \"$dc_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tdc_abnormal=\"true\"\n\t\tfi\n\t\tdc_status=$(openclaw_bot_status_text \"$dc_enabled\" \"$dc_cfg\" \"$dc_connected\" \"$dc_abnormal\")\n\n\t\tlocal slack_enabled slack_cfg slack_connected slack_abnormal slack_status\n\t\tslack_enabled=$(openclaw_json_get_bool '.plugins.entries.slack.enabled // .channels.slack.enabled // false')\n\t\tslack_cfg=$(openclaw_channel_has_cfg \"slack\")\n\t\tslack_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/slack\"; then\n\t\t\tslack_connected=\"true\"\n\t\tfi\n\t\tslack_abnormal=\"false\"\n\t\tif [[ \"$slack_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"slack\"; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$slack_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tslack_status=$(openclaw_bot_status_text \"$slack_enabled\" \"$slack_cfg\" \"$slack_connected\" \"$slack_abnormal\")\n\n\t\tlocal qq_enabled qq_cfg qq_connected qq_abnormal qq_status\n\t\tqq_enabled=$(openclaw_json_get_bool '.plugins.entries.qqbot.enabled // .channels.qqbot.enabled // false')\n\t\tqq_cfg=$(openclaw_channel_has_cfg \"qqbot\")\n\t\tqq_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/sessions\" || openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/data\"; then\n\t\t\tqq_connected=\"true\"\n\t\tfi\n\t\tqq_abnormal=\"false\"\n\t\tif [[ \"$qq_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"qqbot\"; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$qq_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tqq_status=$(openclaw_bot_status_text \"$qq_enabled\" \"$qq_cfg\" \"$qq_connected\" \"$qq_abnormal\")\n\n\t\techo \"本地状态(仅本机配置/缓存,不做网络探测):\"\n\t\topenclaw_print_bot_status_line \"Telegram\" \"$tg_status\"\n\t\topenclaw_print_bot_status_line \"飞书(Lark)\" \"$feishu_status\"\n\t\topenclaw_print_bot_status_line \"WhatsApp\" \"$wa_status\"\n\t\topenclaw_print_bot_status_line \"Discord\" \"$dc_status\"\n\t\topenclaw_print_bot_status_line \"Slack\" \"$slack_status\"\n\t\topenclaw_print_bot_status_line \"QQ Bot\" \"$qq_status\"\n\t}\n\n\tchange_tg_bot_code() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"机器人连接对接\"\n\t\t\topenclaw_show_bot_local_status_block\n\t\t\techo\n\n\t\t\tlocal bot_choice\n\t\t\tbot_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择要对接的平台\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"Telegram 机器人对接\" \\\n\t\t\t\t\"飞书 (Lark) 机器人对接\" \\\n\t\t\t\t\"WhatsApp 机器人对接\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tlocal code\n\t\t\tcase \"$bot_choice\" in\n\t\t\t\t\"Telegram 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"TG机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve telegram \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"飞书 (Lark) 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"飞书机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve feishu \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"WhatsApp 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"WhatsApp连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve whatsapp \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"返回\"|*)\n\t\t\t\t\treturn 0\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\topenclaw_backup_root() {\n\t\techo \"${HOME}/.openclaw/backups\"\n\t}\n\n\topenclaw_is_interactive_terminal() {\n\t\t[[ -t 0 && -t 1 ]]\n\t}\n\n\topenclaw_has_command() {\n\t\tcommand -v \"$1\" >/dev/null 2>&1\n\t}\n\n\topenclaw_is_safe_relpath() {\n\t\tlocal rel=\"$1\"\n\t\t[[ -z \"$rel\" ]] && return 1\n\t\t[[ \"$rel\" == /* ]] && return 1\n\t\t[[ \"$rel\" == *//* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\n'* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\r'* ]] && return 1\n\t\tcase \"$rel\" in\n\t\t\t../*|*/../*|*/..|..)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t\treturn 0\n\t}\n\n\topenclaw_restore_path_allowed() {\n\t\tlocal mode=\"$1\"\n\t\tlocal rel=\"$2\"\n\t\tcase \"$mode\" in\n\t\t\tmemory)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\tMEMORY.md|AGENTS.md|USER.md|SOUL.md|TOOLS.md|memory/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\tproject)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\topenclaw.json|workspace/*|extensions/*|skills/*|prompts/*|tools/*|telegram/*|feishu/*|whatsapp/*|discord/*|slack/*|qqbot/*|logs/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\t*)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t}\n\n\topenclaw_pack_backup_archive() {\n\t\tlocal backup_type=\"$1\"\n\t\tlocal export_mode=\"$2\"\n\t\tlocal payload_dir=\"$3\"\n\t\tlocal output_file=\"$4\"\n\n\t\tlocal tmp_root\n\t\ttmp_root=$(mktemp -d) || return 1\n\t\tlocal pack_dir=\"$tmp_root/package\"\n\t\tmkdir -p \"$pack_dir\"\n\n\t\tcp -a \"$payload_dir\" \"$pack_dir/payload\"\n\n\t\t(\n\t\t\tcd \"$pack_dir/payload\" || exit 1\n\t\t\tfind . -type f | sed 's|^\\./||' | sort > \"$pack_dir/manifest.files\"\n\t\t\t: > \"$pack_dir/manifest.sha256\"\n\t\t\tlocal f\n\t\t\twhile IFS= read -r f; do\n\t\t\t\t[[ -z \"$f\" ]] && continue\n\t\t\t\tsha256sum \"$f\" >> \"$pack_dir/manifest.sha256\"\n\t\t\tdone < \"$pack_dir/manifest.files\"\n\t\t) || { rm -rf \"$tmp_root\"; return 1; }\n\n\t\tcat > \"$pack_dir/backup.meta\" <<EOF\nTYPE=$backup_type\nMODE=$export_mode\nCREATED_AT=$(date -u +'%Y-%m-%dT%H:%M:%SZ')\nHOST=$(hostname)\nEOF\n\n\t\tmkdir -p \"$(dirname \"$output_file\")\"\n\t\ttar -C \"$pack_dir\" -czf \"$output_file\" backup.meta manifest.files manifest.sha256 payload\n\t\tlocal rc=$?\n\t\trm -rf \"$tmp_root\"\n\t\treturn $rc\n\t}\n\n\topenclaw_offer_transfer_hint() {\n\t\tlocal file_path=\"$1\"\n\t\techo \"可使用以下方式下载备份文件:\"\n\t\techo \" 本地路径: $file_path\"\n\t\techo \" scp 示例: scp root@你的服务器:$file_path ./\"\n\t\techo \" 或使用 SFTP 客户端下载\"\n\t}\n\n\topenclaw_prepare_import_archive() {\n\t\tlocal expected_type=\"$1\"\n\t\tlocal archive_path=\"$2\"\n\t\tlocal unpack_root=\"$3\"\n\n\t\tif [[ ! -f \"$archive_path\" ]]; then\n\t\t\techo \"错误:文件不存在: $archive_path\"\n\t\t\treturn 1\n\t\tfi\n\t\tmkdir -p \"$unpack_root\"\n\t\tif ! tar -xzf \"$archive_path\" -C \"$unpack_root\"; then\n\t\t\techo \"错误:备份包解压失败\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal pkg_dir=\"$unpack_root/package\"\n\t\tif [[ -f \"$unpack_root/backup.meta\" ]]; then\n\t\t\tpkg_dir=\"$unpack_root\"\n\t\tfi\n\n\t\tlocal required\n\t\tfor required in backup.meta manifest.files manifest.sha256 payload; do\n\t\t\tif [[ ! -e \"$pkg_dir/$required\" ]]; then\n\t\t\t\techo \"错误:备份包缺少必要文件: $required\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tdone\n\n\t\tlocal real_type\n\t\treal_type=$(grep '^TYPE=' \"$pkg_dir/backup.meta\" | head -n1 | cut -d'=' -f2-)\n\t\tif [[ \"$real_type\" != \"$expected_type\" ]]; then\n\t\t\techo \"错误:备份类型不匹配,期望: $expected_type,实际: ${real_type:-未知}\"\n\t\t\treturn 1\n\t\tfi\n\n\t\t(\n\t\t\tcd \"$pkg_dir/payload\" || exit 1\n\t\t\tsha256sum -c ../manifest.sha256 >/dev/null\n\t\t) || { echo \"错误:sha256 校验失败,拒绝还原\"; return 1; }\n\n\t\techo \"$pkg_dir\"\n\t\treturn 0\n\t}\n\n\topenclaw_memory_backup_export() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tlocal ts\n\t\tts=$(date +%Y%m%d-%H%M%S)\n\t\tlocal out_file=\"$backup_root/openclaw-memory-full-${ts}.tar.gz\"\n\n\t\tmkdir -p \"$backup_root\"\n\t\tif [[ ! -d \"$workspace_dir\" ]]; then\n\t\t\techo \"错误:未找到 workspace 目录: $workspace_dir\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\t[[ -f \"$workspace_dir/MEMORY.md\" ]] && cp -a \"$workspace_dir/MEMORY.md\" \"$tmp_payload/\"\n\t\t[[ -d \"$workspace_dir/memory\" ]] && cp -a \"$workspace_dir/memory\" \"$tmp_payload/\"\n\n\t\tif gum confirm \"是否附带 AGENTS/USER/SOUL/TOOLS 文件?\"; then\n\t\t\tlocal f\n\t\t\tfor f in AGENTS.md USER.md SOUL.md TOOLS.md; do\n\t\t\t\t[[ -f \"$workspace_dir/$f\" ]] && cp -a \"$workspace_dir/$f\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的记忆文件\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"memory-full\" \"default\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"记忆全量备份完成: $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:记忆全量备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_read_import_path() {\n\t\tlocal file_input file_path backup_root\n\t\techo \"提示:输入文件名时默认在备份目录查找;输入含 / 的路径时按完整路径校验。\" >&2\n\t\techo \"scp 示例: scp /本地/备份包.tar.gz root@你的服务器:/tmp/\" >&2\n\n\t\tfile_input=$(gum input \\\n\t\t\t--placeholder \"备份文件名或完整路径\" \\\n\t\t\t--prompt \"Path > \")\n\t\t[[ -z \"$file_input\" ]] && { echo \"\"; return 0; }\n\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\n\t\tif [[ \"$file_input\" == */* ]]; then\n\t\t\tfile_path=\"$file_input\"\n\t\telse\n\t\t\tfile_path=\"$backup_root/$file_input\"\n\t\tfi\n\n\t\tif [[ ! -f \"$file_path\" ]]; then\n\t\t\techo \"错误:备份文件不存在: $file_path\" >&2\n\t\t\techo \"\"\n\t\t\treturn 1\n\t\tfi\n\n\t\techo \"$file_path\"\n\t}\n\n\topenclaw_memory_backup_import() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tmkdir -p \"$workspace_dir\"\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"memory-full\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed memory \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$workspace_dir/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$workspace_dir/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"记忆全量还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_export() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tif [[ ! -d \"$openclaw_root\" ]]; then\n\t\t\techo \"错误:未找到 OpenClaw 根目录: $openclaw_root\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal export_mode_label\n\t\texport_mode_label=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $'选择备份模式\\n↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"安全模式(推荐):workspace + openclaw.json + extensions/skills/prompts/tools\" \\\n\t\t\t\"完整模式(含更多状态,敏感风险更高)\") || return 1\n\n\t\tlocal mode_label=\"safe\"\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\tlocal d\n\t\tif [[ \"$export_mode_label\" == 完整* ]]; then\n\t\t\tmode_label=\"full\"\n\t\t\tfor d in workspace extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in telegram feishu whatsapp discord slack qqbot logs; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\telse\n\t\t\t[[ -d \"$openclaw_root/workspace\" ]] && cp -a \"$openclaw_root/workspace\" \"$tmp_payload/\"\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的 OpenClaw 项目内容\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tlocal out_file=\"$backup_root/openclaw-project-${mode_label}-$(date +%Y%m%d-%H%M%S).tar.gz\"\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"openclaw-project\" \"$mode_label\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"OpenClaw 项目备份完成 (${mode_label}): $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:OpenClaw 项目备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_import() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tmkdir -p \"$openclaw_root\"\n\n\t\techo \"警告:高风险操作,项目还原会覆盖 OpenClaw 配置与工作区内容。\"\n\t\tgum confirm \"确认继续还原?此操作不可逆。\" || { echo \"已取消\"; break_end; return 1; }\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"openclaw-project\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed project \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tgum spin --spinner pulse --title \"停止 OpenClaw gateway...\" -- openclaw gateway stop\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$openclaw_root/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$openclaw_root/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tstart_gateway\n\t\t\techo \"gateway 健康检查:\"\n\t\t\topenclaw gateway status || true\n\t\tfi\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"OpenClaw 项目还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_detect_type() {\n\t\tlocal file_name=\"$1\"\n\t\tif [[ \"$file_name\" == openclaw-memory-full-*.tar.gz ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\telif [[ \"$file_name\" == openclaw-project-*.tar.gz ]]; then\n\t\t\techo \"项目备份文件\"\n\t\telse\n\t\t\techo \"其他备份文件\"\n\t\tfi\n\t}\n\n\topenclaw_backup_collect_files() {\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tmapfile -t OPENCLAW_BACKUP_FILES < <(find \"$backup_root\" -maxdepth 1 -type f -name '*.tar.gz' | xargs -I{} basename {} | sort -r)\n\t}\n\n\topenclaw_backup_render_file_list() {\n\t\tlocal backup_root i file_name file_path file_type file_size file_time\n\t\tlocal has_memory=0 has_project=0 has_other=0\n\t\tbackup_root=$(openclaw_backup_root)\n\t\topenclaw_backup_collect_files\n\n\t\techo \"备份目录: $backup_root\"\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\techo \"暂无备份文件\"\n\t\t\treturn 0\n\t\tfi\n\n\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\tfile_type=$(openclaw_backup_detect_type \"${OPENCLAW_BACKUP_FILES[$i]}\")\n\t\t\tcase \"$file_type\" in\n\t\t\t\t\"记忆备份文件\") has_memory=1 ;;\n\t\t\t\t\"项目备份文件\") has_project=1 ;;\n\t\t\t\t\"其他备份文件\") has_other=1 ;;\n\t\t\tesac\n\t\tdone\n\n\t\tif [[ \"$has_memory\" -eq 1 ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"记忆备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_project\" -eq 1 ]]; then\n\t\t\techo \"项目备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"项目备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_other\" -eq 1 ]]; then\n\t\t\techo \"其他备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"其他备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_backup_file_exists_in_list() {\n\t\tlocal target_file=\"$1\"\n\t\tlocal item\n\t\tfor item in \"${OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t[[ \"$item\" == \"$target_file\" ]] && return 0\n\t\tdone\n\t\treturn 1\n\t}\n\n\topenclaw_backup_delete_file() {\n\t\tlocal backup_root backup_root_real user_input target_file target_path target_type\n\t\tbackup_root=$(openclaw_backup_root)\n\n\t\topenclaw_backup_render_file_list\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\tbreak_end\n\t\t\treturn 0\n\t\tfi\n\n\t\tuser_input=$(printf '%s\\n' \"${OPENCLAW_BACKUP_FILES[@]}\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 选择要删除的备份 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=15 \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ⚡ DELETE BACKUP \" \\\n\t\t\t--color=border:196,label:196,header:196,prompt:208,pointer:196,marker:208,hl:208,hl+:208) || { echo \"已取消\"; break_end; return 0; }\n\n\t\t[[ -z \"$user_input\" ]] && { echo \"已取消\"; break_end; return 0; }\n\n\t\tbackup_root_real=$(realpath -m \"$backup_root\")\n\t\ttarget_file=$(basename -- \"$user_input\")\n\t\ttarget_path=\"$backup_root/$target_file\"\n\n\t\tif [[ ! -f \"$target_path\" ]]; then\n\t\t\techo \"错误:目标文件不存在: $target_path\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\ttarget_type=$(openclaw_backup_detect_type \"$target_file\")\n\n\t\techo \"即将删除: [$target_type] $target_path\"\n\t\tgum confirm \"确认删除?\" || { echo \"已取消删除\"; break_end; return 0; }\n\t\tgum confirm \"请再次确认,删除后不可恢复\" || { echo \"已取消删除\"; break_end; return 0; }\n\n\t\tif rm -f -- \"$target_path\"; then\n\t\t\techo \"删除成功: $target_file\"\n\t\telse\n\t\t\techo \"错误:删除失败: $target_file\"\n\t\tfi\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_list_files() {\n\t\topenclaw_backup_render_file_list\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_restore_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"备份与还原\"\n\t\t\topenclaw_backup_render_file_list\n\t\t\techo\n\n\t\t\tlocal backup_choice\n\t\t\tbackup_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"备份记忆全量\" \\\n\t\t\t\t\"还原记忆全量\" \\\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\" \\\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\" \\\n\t\t\t\t\"删除备份文件\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$backup_choice\" in\n\t\t\t\t\"备份记忆全量\") openclaw_memory_backup_export ;;\n\t\t\t\t\"还原记忆全量\") openclaw_memory_backup_import ;;\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\") openclaw_project_backup_export ;;\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\") openclaw_project_backup_import ;;\n\t\t\t\t\"删除备份文件\") openclaw_backup_delete_file ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\t_autostart_status() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\techo \"未安装\"\n\t\t\t\treturn\n\t\t\tfi\n\t\t\tif launchctl list 2>/dev/null | grep -q \"ai.openclaw.gateway\"; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\telse\n\t\t\tif systemctl --user is-enabled --quiet openclaw-gateway.service 2>/dev/null; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\tfi\n\t}\n\n\ttoggle_autostart() {\n\t\tlocal status; status=$(_autostart_status)\n\t\tclear\n\t\tui_header \"开机自启动管理\"\n\t\techo -e \" 当前状态:\\033[1m${status}\\033[0m\"\n\t\techo\n\n\t\tlocal action\n\t\tif [[ \"$status\" == \"已开启\" ]]; then\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"关闭自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\telse\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"开启自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\tfi\n\n\t\tcase \"$action\" in\n\t\t\t\"开启自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\t\t\tgum spin --spinner globe --title \"正在安装服务...\" -- \\\n\t\t\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 1\"\n\t\t\t\t\tfi\n\t\t\t\t\tlaunchctl load \"$plist\" 2>/dev/null && ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t\"关闭自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tlaunchctl unload \"$plist\" 2>/dev/null && ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user disable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t*) return 0 ;;\n\t\tesac\n\t\tbreak_end\n\t}\n\n\tupdate_moltbot() {\n\t\tinstall_node_and_tools\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_bin\n\t\tnpm_bin=\"$(npm prefix -g 2>/dev/null)/bin\"\n\t\tif [[ -n \"$npm_bin\" ]]; then\n\t\t\texport PATH=\"$npm_bin:$PATH\"\n\t\t\tfor rc in \"$HOME/.bashrc\" \"$HOME/.zshrc\" \"$HOME/.profile\"; do\n\t\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t\tgrep -qF \"$npm_bin\" \"$rc\" 2>/dev/null && continue\n\t\t\t\techo \"export PATH=\\\"${npm_bin}:\\$PATH\\\"\" >> \"$rc\"\n\t\t\tdone\n\t\tfi\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab -\n\t\tstart_gateway\n\t\tui_ok \"更新完成\"\n\t\tbreak_end\n\t}\n\n\tuninstall_moltbot() {\n\t\tgum confirm \"确认卸载 OpenClaw?此操作不可逆。\" || return 0\n\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tlaunchctl unload \"$plist\" 2>/dev/null || true\n\t\telse\n\t\t\tsystemctl --user disable --now openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tpkill -f \"openclaw.*gatewa\" 2>/dev/null || true\n\n\t\tgum spin --spinner meter --title \"正在卸载 OpenClaw...\" -- \\\n\t\t\tbash -c 'timeout 15 openclaw uninstall 2>/dev/null; true'\n\t\tnpm uninstall -g openclaw 2>/dev/null || true\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab - 2>/dev/null || true\n\t\trm -rf /root/.openclaw ~/.openclaw\n\n\t\trm -f \"$HOME/.local/bin/oc\" \"$HOME/.local/bin/openclawctl.sh\" /usr/local/bin/oc\n\t\tfor rc in \"$HOME/.zshrc\" \"$HOME/.bashrc\" \"$HOME/.profile\"; do\n\t\t\t[[ -f \"$rc\" ]] || continue\n\t\t\t_sed_i '/\\.local\\/bin.*PATH/d' \"$rc\"\n\t\t\t_sed_i '/npm-global.*PATH\\|npm\\/bin.*PATH/d' \"$rc\"\n\t\tdone\n\n\t\techo\n\t\tgum style --foreground 46 --bold \" 卸载完成,脚本已退出。\"\n\t\texit 0\n\t}\n\n\tnano_openclaw_json() {\n\t\tinstall nano\n\t\tnano ~/.openclaw/openclaw.json\n\t\tstart_gateway\n\t}\n\n\topenclaw_find_webui_domain() {\n\t\tlocal conf domain_list\n\t\tdomain_list=$(\n\t\t\tgrep -R \"18789\" /home/web/conf.d/*.conf 2>/dev/null \\\n\t\t\t| awk -F: '{print $1}' \\\n\t\t\t| sort -u \\\n\t\t\t| while read -r conf; do\n\t\t\t\tbasename \"$conf\" .conf\n\t\t\tdone\n\t\t)\n\n\t\tif [[ -n \"$domain_list\" ]]; then\n\t\t\techo \"$domain_list\"\n\t\tfi\n\t}\n\n\topenclaw_show_webui_addr() {\n\t\tlocal local_ip token domains\n\n\t\tlocal_ip=\"127.0.0.1\"\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tgum style --border normal --border-foreground 99 --padding \"0 2\" \\\n\t\t\t\"OpenClaw WebUI 访问地址\" \\\n\t\t\t\"本机: http://${local_ip}:18789/#token=${token}\"\n\n\t\tdomains=$(openclaw_find_webui_domain)\n\t\tif [[ -n \"$domains\" ]]; then\n\t\t\techo \"$domains\" | while read -r d; do\n\t\t\t\techo \"域名: https://${d}/#token=${token}\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_domain_webui() {\n\t\tadd_yuming\n\t\tldnmp_Proxy \"${yuming}\" 127.0.0.1 18789\n\n\t\tlocal token\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tclear\n\t\techo \"访问地址: https://${yuming}/#token=$token\"\n\t\techo \"先访问URL触发设备ID,然后回车下一步进行配对。\"\n\t\tgum input --placeholder \"按回车继续...\" > /dev/null\n\n\t\techo -e \"${gl_kjlan}正在加载设备列表……${gl_bai}\"\n\n\t\tlocal config_file=\"$HOME/.openclaw/openclaw.json\"\n\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\tlocal new_origin=\"https://${yuming}\"\n\t\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\t\tlocal tmp_json\n\t\t\t\ttmp_json=$(mktemp)\n\t\t\t\tjq 'if .gateway.controlUi == null then .gateway.controlUi = {\"allowedOrigins\": [\"http://127.0.0.1\"]} else . end | if (.gateway.controlUi.allowedOrigins | contains([$origin]) | not) then .gateway.controlUi.allowedOrigins += [$origin] else . end' \\\n\t\t\t\t\t--arg origin \"$new_origin\" \"$config_file\" > \"$tmp_json\" && mv \"$tmp_json\" \"$config_file\"\n\t\t\t\techo -e \"${gl_kjlan}已将域名 ${yuming} 加入 allowedOrigins 配置${gl_bai}\"\n\t\t\t\topenclaw gateway restart >/dev/null 2>&1\n\t\t\tfi\n\t\tfi\n\n\t\topenclaw devices list\n\n\t\tlocal Request_Key\n\t\tRequest_Key=$(gum input \\\n\t\t\t--placeholder \"Request_Key\" \\\n\t\t\t--prompt \"> \")\n\n\t\tif [[ -z \"$Request_Key\" ]]; then\n\t\t\techo \"Request_Key 不能为空\"\n\t\t\treturn 1\n\t\tfi\n\n\t\topenclaw devices approve \"$Request_Key\"\n\t}\n\n\topenclaw_remove_domain() {\n\t\techo \"域名格式 example.com 不带https://\"\n\t\tweb_del\n\t}\n\n\topenclaw_webui_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"WebUI 访问与设置\"\n\t\t\topenclaw_show_webui_addr\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加域名访问\" \\\n\t\t\t\t\"删除域名访问\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"添加域名访问\")\n\t\t\t\t\topenclaw_domain_webui\n\t\t\t\t\techo\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"删除域名访问\")\n\t\t\t\t\topenclaw_remove_domain\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\twhile true; do\n\t\tclear\n\n\t\tlocal install_status running_status update_info\n\t\tinstall_status=$(get_install_status)\n\t\trunning_status=$(get_running_status)\n\t\tupdate_info=$(check_openclaw_update)\n\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"1 8\" --align center \\\n\t\t\t\"O P E N C L A W\" \\\n\t\t\t\"\" \\\n\t\t\t\"[ AI Agent Gateway Manager ]\"\n\t\techo -e \" $install_status $running_status $update_info\"\n\t\tgum style --foreground 240 \" 输入 oc 可快速启动\"\n\t\techo\n\t\tgum style \\\n\t\t\t--bold \\\n\t\t\t--border double --border-foreground 201 \\\n\t\t\t--padding \"0 4\" --align center \\\n\t\t\t\"$(gum style --bold --foreground 201 '◈ by Joey ◈')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ YouTube @joeyblog')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ Telegram t.me/+ft-zI76oovgwNmRh')\" \\\n\t\t\t\"$(gum style --foreground 208 '⚡ 基于:kejilion · cliproxyapi-installer')\"\n\t\techo\n\n\t\tlocal choice\n\t\tlocal autostart_label\n\t\tautostart_label=\"开机自启动 [$(_autostart_status)]\"\n\n\t\tchoice=$(gum choose \\\n\t\t\t--height 24 \\\n\t\t\t--cursor \"❯ \" \\\n\t\t\t--header $' ─── CONTROL CENTER ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\"小白模式安装(推荐)\" \\\n\t\t\t\"安装\" \\\n\t\t\t\"启动\" \\\n\t\t\t\"停止\" \\\n\t\t\t\"状态日志查看\" \\\n\t\t\t\"$autostart_label\" \\\n\t\t\t\"换模型\" \\\n\t\t\t\"API管理\" \\\n\t\t\t\"CLIProxyAPI 管理\" \\\n\t\t\t\"机器人连接对接\" \\\n\t\t\t\"安装插件\" \\\n\t\t\t\"安装技能\" \\\n\t\t\t\"编辑主配置文件\" \\\n\t\t\t\"配置向导\" \\\n\t\t\t\"健康检测与修复\" \\\n\t\t\t\"WebUI访问与设置\" \\\n\t\t\t\"TUI命令行对话\" \\\n\t\t\t\"备份与还原\" \\\n\t\t\t\"更新\" \\\n\t\t\t\"卸载\" \\\n\t\t\t\"退出\") || break\n\n\t\tcase \"$choice\" in\n\t\t\t\"小白模式安装(推荐)\") beginner_mode_install ;;\n\t\t\t\"安装\") install_moltbot ;;\n\t\t\t\"启动\") start_bot ;;\n\t\t\t\"停止\") stop_bot ;;\n\t\t\t\"状态日志查看\") view_logs ;;\n\t\t\t\"开机自启动 [\"*\"]\") toggle_autostart ;;\n\t\t\t\"换模型\") change_model ;;\n\t\t\"API管理\") openclaw_api_manage_menu ;;\n\t\t\"CLIProxyAPI 管理\") cliproxyapi_manage_menu ;;\n\t\t\"机器人连接对接\") change_tg_bot_code ;;\n\t\t\t\"安装插件\") install_plugin ;;\n\t\t\t\"安装技能\") install_skill ;;\n\t\t\t\"编辑主配置文件\") nano_openclaw_json ;;\n\t\t\t\"配置向导\")\n\t\t\t\topenclaw onboard --install-daemon\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"健康检测与修复\")\n\t\t\t\tgum spin --spinner meter --title \"正在执行健康检测...\" -- openclaw doctor --fix\n\t\t\t\tif sync_openclaw_api_models; then\n\t\t\t\t\tstart_gateway\n\t\t\t\telse\n\t\t\t\t\tui_err \"API 模型同步失败,已中止重启网关,请检查 provider /models 返回后重试\"\n\t\t\t\tfi\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"WebUI访问与设置\") openclaw_webui_menu ;;\n\t\t\t\"TUI命令行对话\")\n\t\t\t\topenclaw tui\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"备份与还原\") openclaw_backup_restore_menu ;;\n\t\t\t\"更新\") update_moltbot ;;\n\t\t\t\"卸载\") uninstall_moltbot ;;\n\t\t\t\"退出\"|*) break ;;\n\t\tesac\n\tdone\n}\n\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n\tmoltbot_menu\nfi\n"}, "0b55716513f6b6fc77ba7ab0aea7d4970c613f2a": {"t": 1785772927.1097467, "n": 95740, "h": "b19c5ebb07a141b9", "c": "#!/usr/bin/env bash\n#\n# ╔══════════════════════════════════════════════════════════════════╗\n# ║ OpenClaw / MoltBot 管理脚本 ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 作者 GitHub : by Joey ║\n# ║ YouTube : @joeyblog ║\n# ║ Telegram: https://t.me/+ft-zI76oovgwNmRh ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 致谢 / 引用 ║\n# ║ · 原始脚本基础来自 kejilion(@kejilion) ║\n# ║ · CLIProxyAPI 安装器来自 cliproxyapi-installer ║\n# ║ github.com/brokechubb/cliproxyapi-installer ║\n# ╚══════════════════════════════════════════════════════════════════╝\n#\n: \"${gl_hui:='\\e[37m'}\"\n: \"${gl_hong:='\\033[31m'}\"\n: \"${gl_lv:='\\033[32m'}\"\n: \"${gl_huang:='\\033[33m'}\"\n: \"${gl_lan:='\\033[34m'}\"\n: \"${gl_bai:='\\033[0m'}\"\n: \"${gl_zi:='\\033[35m'}\"\n: \"${gl_kjlan:='\\033[96m'}\"\n\nif ! declare -f break_end > /dev/null 2>&1; then\nbreak_end() {\n\tif command -v gum >/dev/null 2>&1; then\n\t\techo\n\t\tgum style --foreground 240 \" ─────────────────────────────────────── \"\n\t\tgum input --placeholder \"» 按回车继续 «\" --prompt \" \" > /dev/null\n\telse\n\t\techo \" ─── 按任意键继续 ───\"\n\t\tread -n 1 -s -r -p \"\"\n\t\techo\n\tfi\n\tclear\n}\nfi\n\nif ! declare -f install > /dev/null 2>&1; then\ninstall() {\n\tif [[ $# -eq 0 ]]; then\n\t\techo \"未提供软件包参数\"\n\t\treturn 1\n\tfi\n\tfor package in \"$@\"; do\n\t\tif ! command -v \"$package\" &>/dev/null; then\n\t\t\techo -e \"${gl_kjlan}正在安装 $package...${gl_bai}\"\n\t\t\tif [[ \"$(uname -s)\" == \"Darwin\" ]]; then\n\t\t\t\t_ensure_brew &>/dev/null\n\t\t\t\tcommand -v brew &>/dev/null && brew install \"$package\" &>/dev/null\n\t\t\telif command -v dnf &>/dev/null; then\n\t\t\t\tdnf install -y \"$package\" &>/dev/null\n\t\t\telif command -v yum &>/dev/null; then\n\t\t\t\tyum install -y \"$package\" &>/dev/null\n\t\t\telif command -v apt &>/dev/null; then\n\t\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"$package\" &>/dev/null\n\t\t\telif command -v apk &>/dev/null; then\n\t\t\t\tapk add \"$package\" &>/dev/null\n\t\t\telif command -v pacman &>/dev/null; then\n\t\t\t\tpacman -S --noconfirm \"$package\" &>/dev/null\n\t\t\telif command -v zypper &>/dev/null; then\n\t\t\t\tzypper install -y \"$package\" &>/dev/null\n\t\t\telif command -v opkg &>/dev/null; then\n\t\t\t\topkg install \"$package\" &>/dev/null\n\t\t\telif command -v pkg &>/dev/null; then\n\t\t\t\tpkg install -y \"$package\" &>/dev/null\n\t\t\telse\n\t\t\t\techo \"未知的包管理器,无法安装 $package\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\tdone\n}\nfi\n\nis_macos() { [[ \"$(uname -s)\" == \"Darwin\" ]]; }\n\n_ensure_brew() {\n\tif ! command -v brew &>/dev/null; then\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tif ! command -v brew &>/dev/null; then\n\t\techo \"正在安装 Homebrew...\"\n\t\t/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tcommand -v brew &>/dev/null || { echo \"Homebrew 安装失败,请手动安装:https://brew.sh\"; return 1; }\n}\n\n_sed_i() {\n\tif is_macos; then\n\t\tsed -i '' \"$@\"\n\telse\n\t\tsed -i \"$@\"\n\tfi\n}\n\ninstall_base_deps() {\n\tif is_macos; then\n\t\t_ensure_brew || return 1\n\t\tlocal missing_brew=()\n\t\tfor cmd in curl git nano jq python3 gpg; do\n\t\t\tcommand -v \"$cmd\" &>/dev/null || missing_brew+=(\"$cmd\")\n\t\tdone\n\t\tfor i in \"${!missing_brew[@]}\"; do\n\t\t\t[[ \"${missing_brew[$i]}\" == \"gpg\" ]] && missing_brew[$i]=\"gnupg\"\n\t\tdone\n\t\t[[ ${#missing_brew[@]} -eq 0 ]] && return 0\n\t\techo \"正在安装缺失依赖:${missing_brew[*]}...\"\n\t\tbrew install \"${missing_brew[@]}\" &>/dev/null\n\t\treturn 0\n\tfi\n\n\tlocal -a dep_map=(\n\t\t\"curl:curl:curl:curl:curl:curl\"\n\t\t\"git:git:git:git:git:git\"\n\t\t\"nano:nano:nano:nano:nano:nano\"\n\t\t\"jq:jq:jq:jq:jq:jq\"\n\t\t\"python3:python3:python3:python3:python:python3\"\n\t\t\"tar:tar:tar:tar:tar:tar\"\n\t\t\"gpg:gnupg:gnupg2:gnupg:gnupg:gpg2\"\n\t)\n\n\tlocal missing_apt=() missing_dnf=() missing_apk=() missing_pacman=() missing_zypper=()\n\n\tfor entry in \"${dep_map[@]}\"; do\n\t\tIFS=: read -r cmd pkg_apt pkg_dnf pkg_apk pkg_pacman pkg_zypper <<< \"$entry\"\n\t\tcommand -v \"$cmd\" &>/dev/null && continue\n\t\tmissing_apt+=(\"$pkg_apt\")\n\t\tmissing_dnf+=(\"$pkg_dnf\")\n\t\tmissing_apk+=(\"$pkg_apk\")\n\t\tmissing_pacman+=(\"$pkg_pacman\")\n\t\tmissing_zypper+=(\"$pkg_zypper\")\n\tdone\n\n\t[[ ${#missing_apt[@]} -eq 0 ]] && return 0\n\n\techo \"正在安装缺失依赖:${missing_apt[*]}...\"\n\n\tif command -v apt &>/dev/null; then\n\t\tapt update -y &>/dev/null\n\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"${missing_apt[@]}\" &>/dev/null\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y epel-release &>/dev/null || true\n\t\tdnf install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y epel-release &>/dev/null || true\n\t\tyum install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v apk &>/dev/null; then\n\t\tapk update &>/dev/null\n\t\tapk add \"${missing_apk[@]}\" &>/dev/null\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -Sy --noconfirm --needed \"${missing_pacman[@]}\" &>/dev/null\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y \"${missing_zypper[@]}\" &>/dev/null\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\tlocal py\n\t\tpy=$(compgen -c 2>/dev/null | grep -E '^python3\\.[0-9]+$' | sort -V | tail -1)\n\t\tif [[ -n \"$py\" ]]; then\n\t\t\tln -sf \"$(command -v \"$py\")\" /usr/local/bin/python3 2>/dev/null || true\n\t\tfi\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\techo \"正在安装 python3...\"\n\t\tif command -v apt &>/dev/null; then\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y python3 &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tdnf install -y python3 &>/dev/null\n\t\telif command -v yum &>/dev/null; then\n\t\t\tyum install -y python3 &>/dev/null\n\t\telif command -v apk &>/dev/null; then\n\t\t\tapk add python3 &>/dev/null\n\t\telif command -v pacman &>/dev/null; then\n\t\t\tpacman -S --noconfirm python &>/dev/null\n\t\telif command -v zypper &>/dev/null; then\n\t\t\tzypper install -y python3 &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\tfi\n}\n\n_install_gum_binary() {\n\tlocal arch\n\tcase \"$(uname -m)\" in\n\t\tx86_64) arch=\"amd64\" ;;\n\t\taarch64) arch=\"arm64\" ;;\n\t\tarmv7l) arch=\"armv7\" ;;\n\t\t*) echo \"不支持的架构: $(uname -m)\"; return 1 ;;\n\tesac\n\tlocal latest\n\tlatest=$(curl -fsSL https://api.github.com/repos/charmbracelet/gum/releases/latest \\\n\t\t| grep '\"tag_name\"' | sed 's/.*\"v\\([^\"]*\\)\".*/\\1/')\n\t[[ -z \"$latest\" ]] && { echo \"获取 gum 版本失败\"; return 1; }\n\tlocal url=\"https://github.com/charmbracelet/gum/releases/download/v${latest}/gum_${latest}_linux_${arch}.tar.gz\"\n\tlocal tmp\n\ttmp=$(mktemp -d)\n\tcurl -fsSL \"$url\" -o \"$tmp/gum.tar.gz\" \\\n\t\t&& tar -xzf \"$tmp/gum.tar.gz\" -C \"$tmp\" \\\n\t\t&& install -m 755 \"$tmp/gum\" /usr/local/bin/gum\n\trm -rf \"$tmp\"\n\tcommand -v gum >/dev/null 2>&1\n}\n\ninstall_gum() {\n\tcommand -v gum >/dev/null 2>&1 && return 0\n\techo \"正在安装 gum...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install gum\n\telif command -v apt >/dev/null 2>&1; then\n\t\tcommand -v gpg >/dev/null 2>&1 || apt install -y gnupg 2>/dev/null\n\t\tmkdir -p /etc/apt/keyrings\n\t\tif curl -fsSL https://repo.charm.sh/apt/gpg.key \\\n\t\t\t| gpg --dearmor -o /etc/apt/keyrings/charm.gpg 2>/dev/null; then\n\t\t\techo \"deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *\" \\\n\t\t\t\t| tee /etc/apt/sources.list.d/charm.list >/dev/null\n\t\t\tapt update -y 2>/dev/null && apt install -y gum 2>/dev/null\n\t\tfi\n\t\tcommand -v gum >/dev/null 2>&1 || _install_gum_binary\n\telif command -v dnf &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tdnf install -y gum\n\telif command -v yum &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tyum install -y gum\n\telif command -v apk &>/dev/null; then\n\t\tapk add gum 2>/dev/null || _install_gum_binary\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm gum 2>/dev/null || _install_gum_binary\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y gum 2>/dev/null || _install_gum_binary\n\telse\n\t\t_install_gum_binary\n\tfi\n\tcommand -v gum >/dev/null 2>&1 || { echo \"gum 安装失败,请手动安装: https://github.com/charmbracelet/gum\"; return 1; }\n}\n\ninstall_fzf() {\n\tcommand -v fzf >/dev/null 2>&1 && return 0\n\techo \"正在安装 fzf...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install fzf\n\telif command -v apt >/dev/null 2>&1; then\n\t\tapt install -y fzf\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y fzf\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y fzf\n\telif command -v apk &>/dev/null; then\n\t\tapk add fzf\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm fzf\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y fzf\n\telse\n\t\techo \"无法自动安装 fzf,请手动安装: https://github.com/junegunn/fzf\"\n\t\treturn 1\n\tfi\n}\n\n_install_shortcut() {\n\tlocal store_dir shortcut_dir shortcut\n\n\tstore_dir=\"$HOME/.local/bin\"\n\tmkdir -p \"$store_dir\"\n\n\tlocal _candidate_dirs=(\"/opt/homebrew/bin\" \"/usr/local/bin\")\n\tshortcut_dir=\"\"\n\tfor _d in \"${_candidate_dirs[@]}\"; do\n\t\tif [[ -d \"$_d\" && -w \"$_d\" ]]; then\n\t\t\tshortcut_dir=\"$_d\"\n\t\t\tbreak\n\t\tfi\n\tdone\n\n\tif [[ -z \"$shortcut_dir\" ]]; then\n\t\tshortcut_dir=\"$store_dir\"\n\t\tlocal rc_file=\"\"\n\t\t[[ -f \"$HOME/.zshrc\" ]] && rc_file=\"$HOME/.zshrc\"\n\t\t[[ -f \"$HOME/.bashrc\" ]] && rc_file=\"${rc_file:-$HOME/.bashrc}\"\n\t\tif [[ -n \"$rc_file\" ]] && ! grep -q \"$store_dir\" \"$rc_file\" 2>/dev/null; then\n\t\t\tprintf '\\nexport PATH=\"%s:$PATH\"\\n' \"$store_dir\" >> \"$rc_file\"\n\t\tfi\n\t\texport PATH=\"$store_dir:$PATH\"\n\tfi\n\n\tshortcut=\"$shortcut_dir/oc\"\n\n\tcat > \"$shortcut\" <<EOF\n#!/usr/bin/env bash\ncurl -fsSL https://raw.githubusercontent.com/byJoey/openclawctl/main/openclaw.sh \\\\\n -o \"$store_dir/openclawctl.sh\" 2>/dev/null && chmod +x \"$store_dir/openclawctl.sh\"\nexec bash \"$store_dir/openclawctl.sh\" \"\\$@\"\nEOF\n\tchmod +x \"$shortcut\"\n}\n\nmoltbot_menu() {\n\tis_macos && _ensure_brew\n\n\t_install_shortcut\n\n\tinstall_base_deps\n\tinstall_gum || { echo \"gum 安装失败,无法继续\"; return 1; }\n\tinstall_fzf || { echo \"fzf 安装失败,无法继续\"; return 1; }\n\n\tui_header() {\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"0 2\" \"$*\"\n\t\techo\n\t}\n\tui_ok() { gum style --foreground 46 \" ◉ $*\"; }\n\tui_err() { gum style --foreground 196 \" ✗ $*\"; }\n\tui_warn() { gum style --foreground 208 \" ⚡ $*\"; }\n\tui_info() { gum style --foreground 51 \" ◈ $*\"; }\n\tui_step() { echo; gum style --bold --foreground 201 \" ▶ $*\"; echo; }\n\n\tcheck_openclaw_update() {\n\t\tif ! command -v npm >/dev/null 2>&1; then\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal local_version remote_version\n\t\tlocal_version=$(npm list -g openclaw --depth=0 --no-update-notifier 2>/dev/null \\\n\t\t\t| grep openclaw | awk '{print $NF}' | sed 's/^.*@//')\n\n\t\t[[ -z \"$local_version\" ]] && return 1\n\n\t\tremote_version=$(npm view openclaw version --no-update-notifier 2>/dev/null)\n\n\t\t[[ -z \"$remote_version\" ]] && return 1\n\n\t\tif [[ \"$local_version\" != \"$remote_version\" ]]; then\n\t\t\techo -e \"\\033[38;5;208m⚡ UPDATE AVAILABLE $remote_version\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m✦ v$local_version\\033[0m\"\n\t\tfi\n\t}\n\n\tget_install_status() {\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m◉ INSTALLED\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m○ NOT FOUND\\033[0m\"\n\t\tfi\n\t}\n\n\tget_running_status() {\n\t\tif pgrep -f \"openclaw.*gatewa\" >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m■ STOPPED\\033[0m\"\n\t\tfi\n\t}\n\n\tstart_gateway() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tlaunchctl unload \"$plist\" >/dev/null 2>&1 || true\n\t\t\tif [[ -f \"$plist\" ]]; then\n\t\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- \\\n\t\t\t\t\tbash -c \"launchctl load '$plist' 2>/dev/null; sleep 2\"\n\t\t\telse\n\t\t\t\tgum spin --spinner pulse --title \"正在安装并启动网关...\" -- \\\n\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\telse\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- openclaw gateway start\n\t\t\tsleep 1\n\t\tfi\n\t}\n\n\tinstall_node_and_tools() {\n\t\tif command -v node &>/dev/null && command -v npm &>/dev/null; then\n\t\t\treturn 0\n\t\tfi\n\t\techo \"正在安装 Node.js...\"\n\t\tif is_macos; then\n\t\t\t_ensure_brew || return 1\n\t\t\tbrew install node &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tcurl -fsSL https://rpm.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tdnf install -y cmake libatomic nodejs &>/dev/null\n\t\telif command -v apt &>/dev/null; then\n\t\t\tcurl -fsSL https://deb.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y build-essential python3 libatomic1 nodejs &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t}\n\n\tconfigure_openclaw_session_policy() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 1\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport json, sys\npath = sys.argv[1]\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nsession = obj.setdefault('session', {})\nsession['dmScope'] = session.get('dmScope', 'per-channel-peer')\nsession['resetTriggers'] = ['/new', '/reset']\nsession['reset'] = {\n 'mode': 'idle',\n 'idleMinutes': 10080\n}\nsession['resetByType'] = {\n 'direct': {'mode': 'idle', 'idleMinutes': 10080},\n 'thread': {'mode': 'idle', 'idleMinutes': 1440},\n 'group': {'mode': 'idle', 'idleMinutes': 120}\n}\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(obj, f, ensure_ascii=False, indent=2)\n f.write('\\n')\nPY\n\t}\n\n\tsync_openclaw_api_models() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 0\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('未检测到 API providers,跳过模型同步')\n raise SystemExit(0)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nchanged = False\nfatal_errors = []\nsummary = []\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\ndef prompt_delete_provider(name):\n prompt = f\"{name} /models 探测连续失败 3 次。是否删除该 API 供应商及其全部相关模型?[y/N]: \"\n try:\n ans = input(prompt).strip().lower()\n except EOFError:\n return False\n return ans in ('y', 'yes')\n\n\ndef rebind_defaults_before_delete(name):\n global changed\n\n replacement = None\n\n def get_replacement():\n nonlocal replacement\n if replacement is None:\n candidates = collect_available_refs(exclude_provider=name)\n replacement = candidates[0] if candidates else None\n return replacement\n\n primary_ref = get_primary_ref(defaults)\n if ref_provider(primary_ref) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: 默认主模型指向该 provider,但无可用替代模型,已中止删除')\n return False\n set_primary_ref(defaults, repl)\n changed = True\n summary.append(f'已切换默认主模型: {primary_ref} -> {repl}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: {fk} 指向该 provider,但无可用替代模型,已中止删除')\n return False\n defaults[fk] = repl\n changed = True\n summary.append(f'已切换 {fk}: {val} -> {repl}')\n\n return True\n\n\ndef delete_provider_and_refs(name):\n global changed\n\n if not rebind_defaults_before_delete(name):\n return False\n\n removed_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\n for r in removed_refs:\n defaults_models.pop(r, None)\n if removed_refs:\n changed = True\n\n if name in providers:\n providers.pop(name, None)\n changed = True\n\n summary.append(f'已删除 provider {name},并移除 defaults.models 下 {len(removed_refs)} 个模型引用')\n return True\n\n\ndef fetch_remote_models_with_retry(name, base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n data = json.loads(payload)\n return data, None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\nfor name, provider in list(providers.items()):\n if not isinstance(provider, dict):\n summary.append(f'跳过 {name}: provider 结构非法')\n continue\n\n api = provider.get('api', '')\n base_url = provider.get('baseUrl')\n api_key = provider.get('apiKey')\n model_list = provider.get('models', [])\n\n if not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n summary.append(f'跳过 {name}: 无 baseUrl/apiKey/models')\n continue\n\n if api not in SUPPORTED_APIS:\n summary.append(f'跳过 {name}: 不支持直接 /models 校验 (api={api})')\n continue\n\n data, err, attempts = fetch_remote_models_with_retry(name, base_url, api_key, retries=3)\n if err is not None:\n summary.append(f'警告 - {name}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n if prompt_delete_provider(name):\n deleted = delete_provider_and_refs(name)\n if deleted:\n summary.append(f'{name}: 用户已确认删除该 provider 及全部相关模型引用')\n else:\n summary.append(f'{name}: 用户未确认删除,保留现有 provider 配置')\n continue\n\n if attempts > 1:\n summary.append(f'{name}: /models 第 {attempts} 次重试后成功')\n\n if not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n summary.append(f'警告 - 跳过 {name}: /models 返回结构不可识别')\n continue\n\n remote_ids = []\n for item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\n remote_set = set(remote_ids)\n\n if not remote_set:\n fatal_errors.append(f'错误 - {name} 上游 /models 为空,无法为该 provider 提供兜底模型')\n continue\n\n local_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\n local_ids = [str(m['id']) for m in local_models]\n local_set = set(local_ids)\n\n template = None\n for m in local_models:\n template = copy.deepcopy(m)\n break\n if template is None:\n summary.append(f'警告 - 跳过 {name}: 本地 models 无有效模板模型')\n continue\n\n removed_ids = [mid for mid in local_ids if mid not in remote_set]\n added_ids = [mid for mid in remote_ids if mid not in local_set]\n\n kept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\n new_models = kept_models[:]\n\n for mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{name} / {mid}'\n new_models.append(nm)\n\n if not new_models:\n fatal_errors.append(f'错误 - {name} 同步后无可用模型,无法保障默认模型/回退模型兜底')\n continue\n\n expected_refs = {model_ref(name, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\n local_refs = {model_ref(name, mid) for mid in local_ids}\n\n first_ref = model_ref(name, str(new_models[0]['id']))\n\n primary_ref = get_primary_ref(defaults)\n if isinstance(primary_ref, str) and primary_ref in (local_refs - expected_refs):\n set_primary_ref(defaults, first_ref)\n changed = True\n summary.append(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in (local_refs - expected_refs):\n defaults[fk] = first_ref\n changed = True\n summary.append(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\n stale_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/') and r not in expected_refs]\n for r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\n for r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\n if removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\n summary.append(f'{name}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\n\nif fatal_errors:\n for line in summary:\n print(line)\n for err in fatal_errors:\n print(err)\n print('模型同步失败:存在 provider 同步后无可用模型,已中止写入')\n raise SystemExit(2)\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n for line in summary:\n print(line)\n print('OpenClaw API 模型一致性同步完成')\nelse:\n for line in summary:\n print(line)\n print('无需同步:配置已与上游 /models 保持一致')\nPY\n\t}\n\n\t_install_openclaw_core() {\n\t\tinstall_node_and_tools\n\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf ssh://git@github.com/\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf git@github.com:\n\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_prefix\n\t\tnpm_prefix=$(npm prefix -g 2>/dev/null)\n\t\t[[ -n \"$npm_prefix\" ]] && export PATH=\"$npm_prefix/bin:$PATH\"\n\t\topenclaw onboard --install-daemon\n\t\t_sed_i 's|\"profile\": \"messaging\"|\"profile\": \"full\"|g' ~/.openclaw/openclaw.json\n\t\tconfigure_openclaw_session_policy\n\t\tif ! is_macos; then\n\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tstart_gateway\n\t}\n\n\tinstall_moltbot() {\n\t\t_install_openclaw_core\n\t\tbreak_end\n\t}\n\n\t_cliproxy_dir() {\n\t\tif is_macos; then\n\t\t\techo \"$(brew --prefix 2>/dev/null || echo /usr/local)/etc\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi\"\n\t\tfi\n\t}\n\n\t_cliproxy_conf() {\n\t\tif is_macos; then\n\t\t\techo \"$(_cliproxy_dir)/cliproxyapi.conf\"\n\t\telse\n\t\t\techo \"$(_cliproxy_dir)/config.yaml\"\n\t\tfi\n\t}\n\n\t_cliproxy_bin() {\n\t\tif is_macos; then\n\t\t\tcommand -v cliproxyapi 2>/dev/null || echo \"cliproxyapi\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi/cli-proxy-api\"\n\t\tfi\n\t}\n\n\t_cliproxy_running() {\n\t\tif is_macos; then\n\t\t\tpgrep -f \"cliproxyapi\" >/dev/null 2>&1\n\t\telse\n\t\t\tpgrep -f \"cli-proxy-api\" >/dev/null 2>&1\n\t\tfi\n\t}\n\n\t_cliproxy_start_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services start cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tsystemctl --user enable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"systemctl --user start cliproxyapi.service 2>/dev/null; sleep 3\"\n\t\t\tif ! _cliproxy_running; then\n\t\t\t\tui_warn \"systemd 启动失败,改用后台直接运行...\"\n\t\t\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\t\t\t(cd \"$cliproxy_dir\" && nohup ./cli-proxy-api > /tmp/cliproxyapi.log 2>&1 &)\n\t\t\t\tsleep 3\n\t\t\tfi\n\t\tfi\n\t\tif _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已启动\"\n\t\t\treturn 0\n\t\telse\n\t\t\tui_err \"CLIProxyAPI 启动失败,请检查 $(_cliproxy_conf) 后手动启动\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\t_cliproxy_stop_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services stop cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbash -c \"systemctl --user stop cliproxyapi.service 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\tfi\n\t\tlocal _pat; _pat=$(is_macos && echo \"cliproxyapi\" || echo \"cli-proxy-api\")\n\t\tlocal pids; pids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\tif [[ -n \"$pids\" ]]; then\n\t\t\techo \"$pids\" | xargs kill 2>/dev/null || true\n\t\t\tsleep 2\n\t\t\tpids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\t\t[[ -n \"$pids\" ]] && echo \"$pids\" | xargs kill -9 2>/dev/null || true\n\t\tfi\n\t\tif ! _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已停止\"\n\t\telse\n\t\t\tui_err \"停止失败,仍有进程残留\"\n\t\tfi\n\t}\n\n\t_cliproxy_oauth_login() {\n\t\tlocal cliproxy_bin; cliproxy_bin=$(_cliproxy_bin)\n\t\tif ! command -v \"$cliproxy_bin\" &>/dev/null && [[ ! -x \"$cliproxy_bin\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 未安装:$cliproxy_bin\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_choice\n\t\tprovider_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' 选择要登录的 AI 提供商\\n ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"Claude (Anthropic)\" \\\n\t\t\t\"Gemini (Google)\" \\\n\t\t\t\"OpenAI / Codex\" \\\n\t\t\t\"Qwen (通义千问)\" \\\n\t\t\t\"iFlow\" \\\n\t\t\t\"取消\") || return 0\n\n\t\tlocal login_cmd=\"\" login_port=\"\"\n\t\tcase \"$provider_choice\" in\n\t\t\t\"Claude (Anthropic)\") login_cmd=\"--claude-login\"; login_port=\"54545\" ;;\n\t\t\t\"Gemini (Google)\") login_cmd=\"--login\"; login_port=\"8085\" ;;\n\t\t\t\"OpenAI / Codex\") login_cmd=\"--codex-login\"; login_port=\"1455\" ;;\n\t\t\t\"Qwen (通义千问)\") login_cmd=\"--qwen-login\"; login_port=\"\" ;;\n\t\t\t\"iFlow\") login_cmd=\"--iflow-login\"; login_port=\"11451\" ;;\n\t\t\t\"取消\"|*) return 0 ;;\n\t\tesac\n\n\t\techo\n\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\tif [[ -n \"$login_port\" ]]; then\n\t\t\tif is_macos; then\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 本地浏览器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 浏览器将自动打开,完成授权后自动返回,请稍候...\"\n\t\t\t\techo\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 服务器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 1. 脚本后台启动 cli-proxy-api,并打印 OAuth 授权链接\" \\\n\t\t\t\t\t\" 2. 在本地浏览器打开该链接,完成账号授权\" \\\n\t\t\t\t\t\" 3. 授权后浏览器跳转到 localhost:${login_port},页面报错属正常\" \\\n\t\t\t\t\t\" 4. 复制地址栏完整 URL,粘贴到下方提示符,脚本自动完成握手\"\n\t\t\t\techo\n\t\t\t\tgum style --foreground 208 \\\n\t\t\t\t\t\" ⚡ 无需端口转发 — 回调 URL 里已含 code,直接转发给本机处理\"\n\t\t\t\techo\n\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser) &\n\t\t\t\tlocal cli_pid=$!\n\t\t\t\tsleep 2\n\n\t\t\t\techo\n\t\t\t\tlocal callback_url\n\t\t\t\tcallback_url=$(gum input \\\n\t\t\t\t\t--placeholder \"http://localhost:${login_port}/oauth2callback?code=...\" \\\n\t\t\t\t\t--prompt \" ❯ \" \\\n\t\t\t\t\t--width 120 \\\n\t\t\t\t\t--char-limit 2000) || true\n\n\t\t\t\tif [[ -n \"$callback_url\" ]]; then\n\t\t\t\t\tui_info \"正在向本机发送回调...\"\n\t\t\t\t\tcurl -sf \"$callback_url\" > /dev/null 2>&1 || true\n\t\t\t\t\tsleep 1\n\t\t\t\tfi\n\t\t\t\twait \"$cli_pid\" 2>/dev/null || true\n\t\t\tfi\n\t\telse\n\t\t\tui_info \"Qwen 使用 device flow,无需粘贴回调\"\n\t\t\techo\n\t\t\tgum style --foreground 240 \"将打印授权码 + URL,在任意浏览器中打开并输入授权码即可\"\n\t\t\techo\n\t\t\tif is_macos; then\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser)\n\t\t\tfi\n\t\tfi\n\t}\n\n\tcliproxyapi_manage_menu() {\n\t\tlocal config_file; config_file=$(_cliproxy_conf)\n\n\t\twhile true; do\n\t\t\tclear\n\n\t\t\tlocal cp_installed cp_running cp_version cp_port cp_keys\n\t\t\tif is_macos; then\n\t\t\t\tcp_version=$(brew info cliproxyapi 2>/dev/null | awk 'NR==1{print $4}')\n\t\t\telse\n\t\t\t\tcp_version=$(cat \"$(_cliproxy_dir)/version.txt\" 2>/dev/null || echo \"\")\n\t\t\tfi\n\t\t\tif [[ -n \"$cp_version\" ]]; then\n\t\t\t\tcp_installed=\"\\033[38;5;46m◉ INSTALLED v${cp_version}\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_installed=\"\\033[90m○ NOT INSTALLED\\033[0m\"\n\t\t\tfi\n\t\t\tif _cliproxy_running; then\n\t\t\t\tcp_running=\"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_running=\"\\033[90m■ STOPPED\\033[0m\"\n\t\t\tfi\n\t\t\tcp_port=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\t\t\tcp_port=\"${cp_port:-8317}\"\n\t\t\tcp_keys=$(awk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{n++} END{print n+0}' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 51 \\\n\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t--padding \"1 4\" --align center \\\n\t\t\t\t\"C L I P R O X Y A P I\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ AI OAuth Proxy Manager ]\"\n\t\t\techo -e \" $cp_installed $cp_running\"\n\t\t\techo -e \" \\033[90m端口 :${cp_port} API Keys: ${cp_keys}\\033[0m\"\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' ─── CLIProxyAPI ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"启动\" \\\n\t\t\t\t\"停止\" \\\n\t\t\t\t\"重启\" \\\n\t\t\t\t\"查看日志\" \\\n\t\t\t\t\"账号授权登录\" \\\n\t\t\t\t\"生成并添加 API Key\" \\\n\t\t\t\t\"查看 API Keys\" \\\n\t\t\t\t\"编辑配置文件\" \\\n\t\t\t\t\"更新\" \\\n\t\t\t\t\"卸载\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"启动\")\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"停止\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"重启\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tsleep 1\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看日志\")\n\t\t\t\t\techo\n\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\tlocal brew_log\n\t\t\t\t\t\tbrew_log=\"$(brew --prefix 2>/dev/null)/var/log/cliproxyapi.log\"\n\t\t\t\t\t\tif [[ -f \"$brew_log\" ]]; then\n\t\t\t\t\t\t\ttail -80 \"$brew_log\"\n\t\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\telif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\t\t\tjournalctl --user -u cliproxyapi.service --no-pager -n 80\n\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"账号授权登录\")\n\t\t\t\t\t_cliproxy_oauth_login\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"生成并添加 API Key\")\n\t\t\t\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\t\tlocal new_key\n\t\t\t\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\t\t\t\techo\n\t\t\t\t\tgum style --bold --foreground 46 \" ◉ 已生成新 API Key:\"\n\t\t\t\t\tgum style --bold --foreground 51 \" $new_key\"\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 将此 Key 自动写入 config.yaml?\"; then\n\t\t\t\t\t\tpython3 - \"$config_file\" \"$new_key\" <<'PY'\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n)', r'\\1 - \"' + key + '\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nprint(\"写入成功\")\nPY\n\t\t\t\t\t\tui_ok \"API Key 已写入配置\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看 API Keys\")\n\t\t\t\t\techo\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tgum style --foreground 240 \" config.yaml 中的 API Keys:\"\n\t\t\t\t\t\techo\n\t\t\t\t\t\tawk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{print \" \" $0}' \\\n\t\t\t\t\t\t\t\"$config_file\"\n\t\t\t\t\t\techo\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到配置文件:$config_file\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"编辑配置文件\")\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tnano \"$config_file\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\tfi\n\t\t\t\t\t;;\n\t\t\t\t\"更新\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 确认更新 CLIProxyAPI 到最新版本?\"; then\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew upgrade cliproxyapi && ui_ok \"更新完成\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tcurl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer \\\n\t\t\t\t\t\t\t\t| bash -s -- upgrade\n\t\t\t\t\t\t\tui_ok \"更新完成\"\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"卸载\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" ⚡ 确认卸载 CLIProxyAPI?\"; then\n\t\t\t\t\t\t_cliproxy_stop_service 2>/dev/null || true\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew uninstall cliproxyapi 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$HOME/.cli-proxy-api\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tsystemctl --user disable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\t\t\t\t\trm -f \"$HOME/.config/systemd/user/cliproxyapi.service\" 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$(_cliproxy_dir)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\t\tui_ok \"CLIProxyAPI 已卸载\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tbeginner_mode_install() {\n\t\tif is_macos; then\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ API Key 配置 ]\"\n\t\telse\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ CLIProxyAPI ⟶ AI Auth ⟶ Auto Config ]\"\n\t\tfi\n\t\techo\n\n\t\tui_step \"第 1 步:安装 OpenClaw\"\n\t\t_install_openclaw_core\n\n\t\tui_step \"第 2 步:安装 CLIProxyAPI\"\n\t\tif is_macos; then\n\t\t\tif ! gum spin --spinner globe --title \"正在安装 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbrew install cliproxyapi; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败(brew install cliproxyapi)\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\telse\n\t\t\tif ! curl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer | bash; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\n\t\tlocal cliproxy_config; cliproxy_config=$(_cliproxy_conf)\n\t\tif [[ ! -f \"$cliproxy_config\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 安装后未找到配置文件: $cliproxy_config\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\t\tui_ok \"CLIProxyAPI 安装完成\"\n\n\t\tui_step \"第 3 步:登录 AI 账号\"\n\t\tui_info \"通过 OAuth 授权即可免费使用,无需付费 API Key\"\n\t\techo\n\t\t_cliproxy_oauth_login\n\n\t\tui_step \"第 4 步:生成 API Key 并启动服务\"\n\t\tlocal new_key\n\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\tpython3 - \"$cliproxy_config\" \"$new_key\" <<'PY' 2>/dev/null\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n(?:\\s*-\\s*\"[^\"]*\"\\s*\\n)*)', lambda m: m.group(0) + f' - \"{key}\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nPY\n\t\tif ! _cliproxy_start_service; then\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_step \"第 5 步:配置 OpenClaw API\"\n\n\t\tlocal api_key port base_url\n\t\tapi_key=$(awk '\n\t\t\t/^api-keys:/ { in_keys=1; next }\n\t\t\tin_keys && /^[^ \\t]/ { exit }\n\t\t\tin_keys && /\"sk-/ { match($0, /sk-[^\"]+/); print substr($0, RSTART, RLENGTH); exit }\n\t\t' \"$cliproxy_config\" 2>/dev/null)\n\n\t\tport=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \"$cliproxy_config\" 2>/dev/null)\n\t\tport=\"${port:-8317}\"\n\t\tbase_url=\"http://localhost:${port}/v1\"\n\n\t\tif [[ -z \"$api_key\" ]]; then\n\t\t\tui_err \"无法从 $cliproxy_config 读取 API Key,请手动在「CLIProxyAPI 管理」中添加\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_info \"地址:$base_url\"\n\t\tui_info \"Key:${api_key:0:12}****\"\n\t\techo\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"cliproxy\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tstart_gateway\n\n\t\t\tui_step \"第 6 步:选择默认模型\"\n\t\t\tui_info \"请从可用模型中选择一个作为默认,选完后网关自动重启\"\n\t\t\techo\n\t\t\tchange_model\n\n\t\t\techo\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 46 \\\n\t\t\t\t--border double --border-foreground 46 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◉ INSTALL COMPLETE ◉\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw 已自动接入 CLIProxyAPI,可直接开始使用 ]\" \\\n\t\t\t\t\"[ 更多提供商:cd ~/cliproxyapi && ./cli-proxy-api --help ]\"\n\t\tfi\n\n\t\techo\n\t\tif gum confirm \" 现在去对接机器人?\"; then\n\t\t\tchange_tg_bot_code\n\t\telse\n\t\t\tbreak_end\n\t\tfi\n\t}\n\n\tstart_bot() {\n\t\tstart_gateway\n\t\tbreak_end\n\t}\n\n\tstop_bot() {\n\t\ttmux kill-session -t gateway > /dev/null 2>&1\n\t\tgum spin --spinner pulse --title \"正在停止网关...\" -- openclaw gateway stop\n\t\tbreak_end\n\t}\n\n\tview_logs() {\n\t\topenclaw status\n\t\topenclaw gateway status\n\t\topenclaw logs\n\t\tbreak_end\n\t}\n\n\tadd-all-models-from-provider() {\n\t\tlocal provider_name=\"$1\"\n\t\tlocal base_url=\"$2\"\n\t\tlocal api_key=\"$3\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tlocal models_tmpfile\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取 $provider_name 的模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\n\t\tlocal models_json\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tif [[ -z \"$models_json\" ]]; then\n\t\t\techo \"错误:无法获取模型列表\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_ids\n\t\tmodel_ids=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n for m in models:\n if isinstance(m, dict) and \"id\" in m:\n print(m[\"id\"])\nexcept Exception:\n pass\n')\n\n\t\tif [[ -z \"$model_ids\" ]]; then\n\t\t\techo \"错误:未找到任何模型\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_count\n\t\tmodel_count=$(echo \"$model_ids\" | wc -l | tr -d ' ')\n\t\techo \"发现 $model_count 个模型\"\n\n\t\tlocal models_array=\"[\"\n\t\tlocal first=true\n\n\t\twhile read -r model_id; do\n\t\t\t[[ $first == false ]] && models_array+=\",\"\n\t\t\tfirst=false\n\n\t\t\tlocal context_window=1048576\n\t\t\tlocal max_tokens=128000\n\t\t\tlocal input_cost=0.15\n\t\t\tlocal output_cost=0.60\n\n\t\t\tcase \"$model_id\" in\n\t\t\t\t*opus*|*pro*|*preview*|*thinking*|*sonnet*)\n\t\t\t\t\tinput_cost=2.00\n\t\t\t\t\toutput_cost=12.00\n\t\t\t\t\t;;\n\t\t\t\t*gpt-5*|*codex*)\n\t\t\t\t\tinput_cost=1.25\n\t\t\t\t\toutput_cost=10.00\n\t\t\t\t\t;;\n\t\t\t\t*flash*|*lite*|*haiku*|*mini*|*nano*)\n\t\t\t\t\tinput_cost=0.10\n\t\t\t\t\toutput_cost=0.40\n\t\t\t\t\t;;\n\t\t\tesac\n\n\t\t\tmodels_array+=$(cat <<EOF\n{\n\t\"id\": \"$model_id\",\n\t\"name\": \"$provider_name / $model_id\",\n\t\"input\": [\"text\", \"image\"],\n\t\"contextWindow\": $context_window,\n\t\"maxTokens\": $max_tokens,\n\t\"cost\": {\n\t\t\"input\": $input_cost,\n\t\t\"output\": $output_cost,\n\t\t\"cacheRead\": 0,\n\t\t\"cacheWrite\": 0\n\t}\n}\nEOF\n)\n\t\tdone <<< \"$model_ids\"\n\n\t\tmodels_array+=\"]\"\n\n\t\t[[ -f \"$config_file\" ]] && cp \"$config_file\" \"${config_file}.bak.$(date +%s)\"\n\n\t\tjq --arg prov \"$provider_name\" \\\n\t\t --arg url \"$base_url\" \\\n\t\t --arg key \"$api_key\" \\\n\t\t --argjson models \"$models_array\" \\\n\t\t'\n\t\t.models |= (\n\t\t\t(. // { mode: \"merge\", providers: {} })\n\t\t\t| .mode = \"merge\"\n\t\t\t| .providers[$prov] = {\n\t\t\t\tbaseUrl: $url,\n\t\t\t\tapiKey: $key,\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tmodels: $models\n\t\t\t}\n\t\t)\n\t\t| .agents |= (. // {})\n\t\t| .agents.defaults |= (. // {})\n\t\t| .agents.defaults.models |= (\n\t\t\t(if type == \"object\" then .\n\t\t\t elif type == \"array\" then reduce .[] as $m ({}; if ($m|type) == \"string\" then .[$m] = {} else . end)\n\t\t\t else {}\n\t\t\t end) as $existing\n\t\t\t| reduce ($models[]? | .id? // empty | tostring) as $mid (\n\t\t\t\t$existing;\n\t\t\t\tif ($mid | length) > 0 then\n\t\t\t\t\t.[\"\\($prov)/\\($mid)\"] //= {}\n\t\t\t\telse\n\t\t\t\t\t.\n\t\t\t\tend\n\t\t\t)\n\t\t)\n\t\t' \"$config_file\" > \"${config_file}.tmp\" && mv \"${config_file}.tmp\" \"$config_file\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\techo \"成功添加 $model_count 个模型到 $provider_name\"\n\t\t\techo \"模型引用格式: $provider_name/<model-id>\"\n\t\t\treturn 0\n\t\telse\n\t\t\techo \"错误:配置注入失败\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\tadd-openclaw-provider-interactive() {\n\t\tlocal provider_name base_url api_key\n\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"Provider 名称 (如: deepseek)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\t[[ -z \"$provider_name\" ]] && return 1\n\n\t\tbase_url=$(gum input \\\n\t\t\t--placeholder \"Base URL (如: https://api.xxx.com/v1)\" \\\n\t\t\t--prompt \"Base URL > \")\n\t\t[[ -z \"$base_url\" ]] && return 1\n\t\tbase_url=\"${base_url%/}\"\n\n\t\tapi_key=$(gum input --password \\\n\t\t\t--placeholder \"API Key\" \\\n\t\t\t--prompt \"API Key > \")\n\t\t[[ -z \"$api_key\" ]] && return 1\n\n\t\tlocal models_tmpfile models_json available_models model_count\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tlocal default_model=\"\"\n\t\tif [[ -n \"$models_json\" ]]; then\n\t\t\tavailable_models=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n ids = sorted(m[\"id\"] for m in models if isinstance(m, dict) and \"id\" in m)\n print(\"\\n\".join(ids))\nexcept Exception:\n pass\n')\n\t\t\tmodel_count=$(echo \"$available_models\" | wc -l | tr -d ' ')\n\t\tfi\n\n\t\tif [[ -n \"$available_models\" ]]; then\n\t\t\tdefault_model=$(echo \"$available_models\" | fzf \\\n\t\t\t\t--prompt=\" ❯ \" \\\n\t\t\t\t--header=\" 发现 $model_count 个模型 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 选第一个\" \\\n\t\t\t\t--header-first \\\n\t\t\t\t--height=15 \\\n\t\t\t\t--layout=reverse \\\n\t\t\t\t--border=double \\\n\t\t\t\t--border-label=\" ◈ DEFAULT MODEL \" \\\n\t\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || default_model=$(echo \"$available_models\" | head -1)\n\t\t\t[[ -z \"$default_model\" ]] && default_model=$(echo \"$available_models\" | head -1)\n\t\t\tdefault_model=$(awk '{print $1}' <<< \"$default_model\")\n\t\tfi\n\n\t\techo\n\t\tgum style \\\n\t\t\t--border normal --border-foreground 99 \\\n\t\t\t--padding \"0 2\" \\\n\t\t\t\"Provider : $provider_name\" \\\n\t\t\t\"Base URL : $base_url\" \\\n\t\t\t\"API Key : ${api_key:0:8}****\" \\\n\t\t\t\"默认模型 : $default_model\" \\\n\t\t\t\"模型总数 : $model_count\"\n\t\techo\n\n\t\tgum confirm \"确认添加所有 $model_count 个模型?\" || { echo \"已取消\"; return 1; }\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"$provider_name\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tgum spin --spinner minidot --title \"设置默认模型并重启网关...\" -- \\\n\t\t\t\topenclaw models set \"$provider_name/$default_model\"\n\t\t\tstart_gateway\n\t\t\techo \"完成:所有 $model_count 个模型已加载\"\n\t\tfi\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_list() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\twhile IFS=$'\\t' read -r rec_type idx name base_url model_count latency_txt latency_level; do\n\t\t\tcase \"$rec_type\" in\n\t\t\t\tMSG)\n\t\t\t\t\techo \"$idx\"\n\t\t\t\t\t;;\n\t\t\t\tROW)\n\t\t\t\t\tlocal latency_color=\"$gl_bai\"\n\t\t\t\t\tcase \"$latency_level\" in\n\t\t\t\t\t\tlow) latency_color=\"$gl_lv\" ;;\n\t\t\t\t\t\tmedium) latency_color=\"$gl_huang\" ;;\n\t\t\t\t\t\thigh|unavailable) latency_color=\"$gl_hong\" ;;\n\t\t\t\t\t\tunchecked) latency_color=\"$gl_bai\" ;;\n\t\t\t\t\tesac\n\n\t\t\t\t\tprintf '%b\\n' \"[$idx] ${name} | API: ${base_url} | 模型数量: ${gl_huang}${model_count}${gl_bai} | 延迟/状态: ${latency_color}${latency_txt}${gl_bai}\"\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tlocal _py_list; _py_list=$(mktemp /tmp/oc_list_XXXXXX.py)\n\t\tcat > \"$_py_list\" << 'PYEOF'\nimport json, sys, time, urllib.request\n\npath = sys.argv[1]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\ndef ping_models(base_url, api_key):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': 'Bearer ' + api_key,\n 'User-Agent': 'OpenClaw-API-Manage/1.0',\n },\n )\n start = time.perf_counter()\n with urllib.request.urlopen(req, timeout=4) as resp:\n resp.read(2048)\n return int((time.perf_counter() - start) * 1000)\n\ndef classify_latency(latency):\n if latency == '不可用':\n return '不可用', 'unavailable'\n if latency == '未检测':\n return '未检测', 'unchecked'\n if isinstance(latency, int):\n level = 'low' if latency <= 800 else ('medium' if latency <= 2000 else 'high')\n return str(latency) + 'ms', level\n return str(latency), 'unchecked'\n\ntry:\n with open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\nexcept FileNotFoundError:\n print('MSG\\t未找到 openclaw.json,请先完成安装/初始化。')\n raise SystemExit(0)\nexcept Exception as e:\n print('MSG\\t读取配置失败: ' + type(e).__name__ + ': ' + str(e))\n raise SystemExit(0)\n\nproviders = ((obj.get('models') or {}).get('providers') or {})\nif not isinstance(providers, dict) or not providers:\n print('MSG\\t当前未配置任何 API provider。')\n raise SystemExit(0)\n\nprint('MSG\\t--- 已配置 API 列表 ---')\n\nfor idx, name in enumerate(sorted(providers.keys()), start=1):\n provider = providers.get(name)\n if not isinstance(provider, dict):\n base_url = '-'; model_count = 0; latency_raw = '不可用'\n else:\n base_url = provider.get('baseUrl') or provider.get('url') or provider.get('endpoint') or '-'\n models = provider.get('models') if isinstance(provider.get('models'), list) else []\n model_count = sum(1 for m in models if isinstance(m, dict) and m.get('id'))\n api = provider.get('api', '')\n api_key = provider.get('apiKey')\n latency_raw = '未检测'\n if api in SUPPORTED_APIS and isinstance(base_url, str) and base_url != '-' and isinstance(api_key, str) and api_key:\n try:\n latency_raw = ping_models(base_url, api_key)\n except Exception:\n latency_raw = '不可用'\n elif api in SUPPORTED_APIS:\n latency_raw = '不可用'\n\n latency_text, latency_level = classify_latency(latency_raw)\n print('ROW\\t' + '\\t'.join([str(idx), str(name), str(base_url), str(model_count), str(latency_text), str(latency_level)]))\nPYEOF\n\t\tdone < <(python3 \"$_py_list\" \"$config_file\")\n\t\trm -f \"$_py_list\"\n\t}\n\n\tsync-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要同步的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tinstall jq curl >/dev/null 2>&1\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY2'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\ntarget = sys.argv[2]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('错误:未检测到 API providers,无法同步')\n raise SystemExit(2)\n\nprovider = providers.get(target)\nif not isinstance(provider, dict):\n print(f'错误:未找到 provider: {target}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef fetch_remote_models_with_retry(base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n return json.loads(payload), None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\napi = provider.get('api', '')\nbase_url = provider.get('baseUrl')\napi_key = provider.get('apiKey')\nmodel_list = provider.get('models', [])\n\nif not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n print(f'错误:provider {target} 缺少 baseUrl/apiKey/models,无法执行同步')\n raise SystemExit(3)\n\nif api not in SUPPORTED_APIS:\n print(f'错误:provider {target} 当前 api={api},不支持直接 /models 同步')\n raise SystemExit(3)\n\ndata, err, attempts = fetch_remote_models_with_retry(base_url, api_key, retries=3)\nif err is not None:\n print(f'错误:{target}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n raise SystemExit(4)\n\nif not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n print(f'错误:{target}: /models 返回结构不可识别')\n raise SystemExit(4)\n\nremote_ids = []\nfor item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\nremote_set = set(remote_ids)\nif not remote_set:\n print(f'错误:{target}: 上游 /models 为空,已中止同步')\n raise SystemExit(5)\n\nlocal_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\nlocal_ids = [str(m['id']) for m in local_models]\nlocal_set = set(local_ids)\n\ntemplate = copy.deepcopy(local_models[0]) if local_models else None\nif template is None:\n print(f'错误:{target}: 本地 models 无有效模板模型,无法补全新增模型')\n raise SystemExit(3)\n\nremoved_ids = [mid for mid in local_ids if mid not in remote_set]\nadded_ids = [mid for mid in remote_ids if mid not in local_set]\n\nkept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\nnew_models = kept_models[:]\nfor mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{target} / {mid}'\n new_models.append(nm)\n\nif not new_models:\n print(f'错误:{target}: 同步后无可用模型,已中止写入')\n raise SystemExit(5)\n\nexpected_refs = {model_ref(target, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\nlocal_refs = {model_ref(target, mid) for mid in local_ids}\nremoved_refs = local_refs - expected_refs\nfirst_ref = model_ref(target, str(new_models[0]['id']))\n\nchanged = False\nprimary_ref = get_primary_ref(defaults)\nif isinstance(primary_ref, str) and primary_ref in removed_refs:\n set_primary_ref(defaults, first_ref)\n changed = True\n print(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in removed_refs:\n defaults[fk] = first_ref\n changed = True\n print(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\nstale_refs = [r for r in list(defaults_models.keys()) if r.startswith(target + '/') and r not in expected_refs]\nfor r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\nfor r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\nif removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'{target}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\nif changed:\n print('指定 provider 模型一致性同步完成')\nelse:\n print('无需同步:该 provider 配置已与上游 /models 保持一致')\nPY2\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"同步完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在或未配置\" ;;\n\t\t\t3) echo \"错误:provider 配置不完整或类型不支持\" ;;\n\t\t\t4) echo \"错误:上游 /models 请求失败\" ;;\n\t\t\t5) echo \"错误:上游模型为空或同步后无可用模型\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\tdelete-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要删除的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY'\nimport copy\nimport json\nimport sys\n\npath = sys.argv[1]\nname = sys.argv[2]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or name not in providers:\n print(f'错误:未找到 provider: {name}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\nreplacement_candidates = collect_available_refs(exclude_provider=name)\nreplacement = replacement_candidates[0] if replacement_candidates else None\n\nprimary_ref = get_primary_ref(defaults)\nif ref_provider(primary_ref) == name:\n if not replacement:\n print('错误:删除中止:默认主模型指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n set_primary_ref(defaults, replacement)\n print(f'默认主模型切换: {primary_ref} -> {replacement}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n if not replacement:\n print(f'错误:删除中止:{fk} 指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n defaults[fk] = replacement\n print(f'{fk} 切换: {val} -> {replacement}')\n\nremoved_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\nfor r in removed_refs:\n defaults_models.pop(r, None)\n\nproviders.pop(name, None)\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'已删除 provider: {name}')\nprint(f'已清理 defaults.models 中 {len(removed_refs)} 个关联模型引用')\nPY\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"删除完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在\" ;;\n\t\t\t3) echo \"错误:无可用替代模型,已保持原配置\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"API 管理\"\n\t\t\topenclaw_api_manage_list\n\t\t\techo\n\n\t\t\tlocal api_choice\n\t\t\tapi_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加 API\" \\\n\t\t\t\t\"同步 API 供应商模型列表\" \\\n\t\t\t\t\"删除 API\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$api_choice\" in\n\t\t\t\t\"添加 API\") add-openclaw-provider-interactive ;;\n\t\t\t\t\"同步 API 供应商模型列表\") sync-openclaw-provider-interactive ;;\n\t\t\t\t\"删除 API\") delete-openclaw-provider-interactive ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tchange_model() {\n\t\tlocal all_models model\n\t\tall_models=$(openclaw models list --all 2>/dev/null | grep \"configured\")\n\n\t\tif [[ -z \"$all_models\" ]]; then\n\t\t\tui_err \"无法获取可用模型列表(请先添加 API 提供商)\"\n\t\t\tbreak_end\n\t\t\treturn\n\t\tfi\n\n\t\tmodel=$(echo \"$all_models\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 当前: $(openclaw models list 2>/dev/null | awk '{print $1}' | head -1) │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=20 \\\n\t\t\t--layout=reverse \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ◈ MODEL SELECT \" \\\n\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || return\n\n\t\t[[ -z \"$model\" ]] && return\n\t\tmodel=$(awk '{print $1}' <<< \"$model\")\n\n\t\tgum spin --spinner minidot --title \"正在切换模型...\" -- openclaw models set \"$model\"\n\t\tstart_gateway\n\t\tui_ok \"已切换至:$model\"\n\t\tbreak_end\n\t}\n\n\tresolve_openclaw_plugin_id() {\n\t\tlocal raw_input=\"$1\"\n\t\tlocal plugin_id=\"$raw_input\"\n\n\t\tplugin_id=\"${plugin_id#@openclaw/}\"\n\t\tif [[ \"$plugin_id\" == @*/* ]]; then\n\t\t\tplugin_id=\"${plugin_id##*/}\"\n\t\tfi\n\t\tplugin_id=\"${plugin_id%%@*}\"\n\t\techo \"$plugin_id\"\n\t}\n\n\tsync_openclaw_plugin_allowlist() {\n\t\tlocal plugin_id=\"$1\"\n\t\t[[ -z \"$plugin_id\" ]] && return 1\n\n\t\tlocal home_config=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal root_config=\"/root/.openclaw/openclaw.json\"\n\t\tlocal config_file=\"$home_config\"\n\t\tif [[ ! -f \"$home_config\" && -f \"$root_config\" ]]; then\n\t\t\tconfig_file=\"$root_config\"\n\t\tfi\n\n\t\tmkdir -p \"$(dirname \"$config_file\")\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo '{}' > \"$config_file\"\n\t\tfi\n\n\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\tlocal tmp_json\n\t\t\ttmp_json=$(mktemp)\n\t\t\tif jq --arg pid \"$plugin_id\" '\n\t\t\t\t.plugins = (if (.plugins | type) == \"object\" then .plugins else {} end)\n\t\t\t\t| .plugins.allow = (if (.plugins.allow | type) == \"array\" then .plugins.allow else [] end)\n\t\t\t\t| if (.plugins.allow | index($pid)) == null then .plugins.allow += [$pid] else . end\n\t\t\t' \"$config_file\" > \"$tmp_json\" 2>/dev/null && mv \"$tmp_json\" \"$config_file\"; then\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\t\trm -f \"$tmp_json\"\n\t\tfi\n\n\t\tif command -v python3 >/dev/null 2>&1; then\n\t\t\tif python3 - \"$config_file\" \"$plugin_id\" <<'PYTHON_EOF'\nimport json\nimport sys\nfrom pathlib import Path\n\nconfig_file = Path(sys.argv[1])\nplugin_id = sys.argv[2]\n\ntry:\n data = json.loads(config_file.read_text(encoding='utf-8')) if config_file.exists() else {}\n if not isinstance(data, dict):\n data = {}\nexcept Exception:\n data = {}\n\nplugins = data.get('plugins')\nif not isinstance(plugins, dict):\n plugins = {}\n\na = plugins.get('allow')\nif not isinstance(a, list):\n a = []\n\nif plugin_id not in a:\n a.append(plugin_id)\n\nplugins['allow'] = a\ndata['plugins'] = plugins\nconfig_file.write_text(json.dumps(data, ensure_ascii=False, indent=2) + \"\\n\", encoding='utf-8')\nPYTHON_EOF\n\t\t\tthen\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\tfi\n\n\t\techo \"警告:已安装插件,但同步 plugins.allow 失败,请手动检查: $config_file\"\n\t\treturn 1\n\t}\n\n\tinstall_plugin() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"插件管理\"\n\t\t\tgum style --foreground 240 \"当前已安装插件:\"\n\t\t\topenclaw plugins list\n\t\t\techo\n\t\t\tgum style --bold --foreground 99 \"常用插件参考:\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"feishu\" \"飞书/Lark 集成\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"telegram\" \"Telegram 机器人\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"slack\" \"Slack 企业通讯\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"msteams\" \"Microsoft Teams\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"discord\" \"Discord 社区管理\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"whatsapp\" \"WhatsApp 自动化\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"memory-core\" \"基础记忆 (文件检索)\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"memory-lancedb\" \"增强记忆 (向量数据库)\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"copilot-proxy\" \"Copilot 接口转发\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"lobster\" \"审批流 (带人工确认)\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"voice-call\" \"语音通话能力\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"nostr\" \"加密隐私聊天\"\n\t\t\techo\n\n\t\t\tlocal raw_input\n\t\t\traw_input=$(gum input \\\n\t\t\t\t--placeholder \"插件 ID (留空退出)\" \\\n\t\t\t\t--prompt \"Plugin > \")\n\t\t\t[[ -z \"$raw_input\" ]] && break\n\n\t\t\tlocal plugin_id plugin_full\n\t\t\tplugin_id=$(resolve_openclaw_plugin_id \"$raw_input\")\n\t\t\tplugin_full=\"$raw_input\"\n\n\t\t\tlocal plugin_list\n\t\t\tplugin_list=$(openclaw plugins list 2>/dev/null)\n\n\t\t\tif echo \"$plugin_list\" | grep -qw \"$plugin_id\" && echo \"$plugin_list\" | grep \"$plugin_id\" | grep -q \"disabled\"; then\n\t\t\t\tui_info \"插件 [$plugin_id] 已预装,正在激活...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\" && ui_ok \"激活成功\" || ui_err \"激活失败\"\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/extensions/$plugin_id\" ]]; then\n\t\t\t\tui_info \"发现系统内置插件,尝试直接启用...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\telse\n\t\t\t\tui_info \"本地未发现,尝试下载安装...\"\n\t\t\t\trm -rf \"/root/.openclaw/extensions/$plugin_id\"\n\n\t\t\t\tif openclaw plugins install \"$plugin_full\"; then\n\t\t\t\t\tui_ok \"下载成功,正在启用...\"\n\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\telse\n\t\t\t\t\tui_warn \"官方渠道下载失败,尝试 npm 备选方案...\"\n\t\t\t\t\tif npm install -g \"$plugin_full\" --unsafe-perm; then\n\t\t\t\t\t\tui_ok \"npm 安装成功,尝试启用...\"\n\t\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"无法获取该插件,请检查 ID 是否正确或网络是否可用\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\tfi\n\t\t\tfi\n\n\t\t\tsync_openclaw_plugin_allowlist \"$plugin_id\"\n\t\t\tstart_gateway\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\tinstall_skill() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"技能管理\"\n\t\t\tgum style --foreground 240 \"当前已安装技能:\"\n\t\t\topenclaw skills list\n\t\t\techo\n\t\t\tgum style --bold --foreground 99 \"推荐技能参考:\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"github\" \"管理 GitHub Issues/PR/CI\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"notion\" \"操作 Notion 页面与数据库\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"apple-notes\" \"macOS 原生笔记管理\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"apple-reminders\" \"macOS 提醒事项管理\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"1password\" \"自动化读取 1Password 密钥\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"gog\" \"Google Workspace 全能助手\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"things-mac\" \"Things 3 任务管理\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"bluebubbles\" \"通过 BlueBubbles 收发 iMessage\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"himalaya\" \"终端邮件管理 (IMAP/SMTP)\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"summarize\" \"网页/播客/YouTube 内容总结\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"openai-whisper\" \"本地音频转文字\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"coding-agent\" \"运行 Claude Code/Codex 等编程助手\"\n\t\t\techo\n\n\t\t\tlocal skill_name\n\t\t\tskill_name=$(gum input \\\n\t\t\t\t--placeholder \"技能名称 (留空退出)\" \\\n\t\t\t\t--prompt \"Skill > \")\n\t\t\t[[ -z \"$skill_name\" ]] && break\n\n\t\t\tlocal skill_found=false\n\t\t\tif [[ -d \"${HOME}/.openclaw/workspace/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在用户目录安装\"\n\t\t\t\tskill_found=true\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在系统目录安装\"\n\t\t\t\tskill_found=true\n\t\t\tfi\n\n\t\t\tif [[ \"$skill_found\" == true ]]; then\n\t\t\t\tgum confirm \"是否重新安装?\" || { break_end; continue; }\n\t\t\tfi\n\n\t\t\tgum spin --spinner globe --title \"正在安装技能 $skill_name...\" -- npx clawhub install \"$skill_name\"\n\t\t\tif [[ $? -eq 0 ]]; then\n\t\t\t\techo \"技能 $skill_name 安装成功\"\n\t\t\t\tstart_gateway\n\t\t\telse\n\t\t\t\techo \"错误:安装失败,请检查技能名称是否正确\"\n\t\t\tfi\n\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\topenclaw_json_get_bool() {\n\t\tlocal expr=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r \"$expr\" \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_channel_has_cfg() {\n\t\tlocal channel=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r --arg c \"$channel\" '\n\t\t\t(.channels[$c] // null) as $v\n\t\t\t| if ($v | type) != \"object\" then\n\t\t\t\tfalse\n\t\t\t else\n\t\t\t\t([ $v\n\t\t\t\t | to_entries[]\n\t\t\t\t | select((.key == \"enabled\" or .key == \"dmPolicy\" or .key == \"groupPolicy\" or .key == \"streaming\") | not)\n\t\t\t\t | .value\n\t\t\t\t | select(. != null and . != \"\" and . != false)\n\t\t\t\t ] | length) > 0\n\t\t\t end\n\t\t' \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_dir_has_files() {\n\t\tlocal dir=\"$1\"\n\t\t[[ -d \"$dir\" ]] && find \"$dir\" -type f -print -quit 2>/dev/null | grep -q .\n\t}\n\n\topenclaw_plugin_local_installed() {\n\t\tlocal plugin=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ -s \"$config_file\" ]] && jq -e --arg p \"$plugin\" '.plugins.installs[$p]' \"$config_file\" >/dev/null 2>&1; then\n\t\t\treturn 0\n\t\tfi\n\t\t[[ -d \"${HOME}/.openclaw/extensions/${plugin}\" ]] || [[ -d \"/usr/lib/node_modules/openclaw/extensions/${plugin}\" ]]\n\t}\n\n\topenclaw_bot_status_text() {\n\t\tlocal enabled=\"$1\"\n\t\tlocal configured=\"$2\"\n\t\tlocal connected=\"$3\"\n\t\tlocal abnormal=\"$4\"\n\t\tif [[ \"$abnormal\" == \"true\" ]]; then\n\t\t\techo \"异常\"\n\t\telif [[ \"$enabled\" != \"true\" ]]; then\n\t\t\techo \"未启用\"\n\t\telif [[ \"$connected\" == \"true\" ]]; then\n\t\t\techo \"已连接\"\n\t\telif [[ \"$configured\" == \"true\" ]]; then\n\t\t\techo \"已配置\"\n\t\telse\n\t\t\techo \"未配置\"\n\t\tfi\n\t}\n\n\topenclaw_colorize_bot_status() {\n\t\tlocal status=\"$1\"\n\t\tcase \"$status\" in\n\t\t\t已连接) echo -e \"${gl_lv}${status}${gl_bai}\" ;;\n\t\t\t已配置) echo -e \"${gl_huang}${status}${gl_bai}\" ;;\n\t\t\t异常) echo -e \"${gl_hong}${status}${gl_bai}\" ;;\n\t\t\t*) echo \"$status\" ;;\n\t\tesac\n\t}\n\n\topenclaw_print_bot_status_line() {\n\t\tlocal label=\"$1\"\n\t\tlocal status=\"$2\"\n\t\techo -e \"- ${label}: $(openclaw_colorize_bot_status \"$status\")\"\n\t}\n\n\topenclaw_show_bot_local_status_block() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal json_ok=\"false\"\n\t\tif [[ -s \"$config_file\" ]] && jq empty \"$config_file\" >/dev/null 2>&1; then\n\t\t\tjson_ok=\"true\"\n\t\tfi\n\n\t\tlocal tg_enabled tg_cfg tg_connected tg_abnormal tg_status\n\t\ttg_enabled=$(openclaw_json_get_bool '.channels.telegram.enabled // .plugins.entries.telegram.enabled // false')\n\t\ttg_cfg=$(openclaw_channel_has_cfg \"telegram\")\n\t\ttg_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/telegram\"; then\n\t\t\ttg_connected=\"true\"\n\t\tfi\n\t\ttg_abnormal=\"false\"\n\t\tif [[ \"$tg_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\ttg_abnormal=\"true\"\n\t\tfi\n\t\ttg_status=$(openclaw_bot_status_text \"$tg_enabled\" \"$tg_cfg\" \"$tg_connected\" \"$tg_abnormal\")\n\n\t\tlocal feishu_enabled feishu_cfg feishu_connected feishu_abnormal feishu_status\n\t\tfeishu_enabled=$(openclaw_json_get_bool '.plugins.entries.feishu.enabled // .channels.feishu.enabled // false')\n\t\tfeishu_cfg=$(openclaw_channel_has_cfg \"feishu\")\n\t\tfeishu_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/feishu\"; then\n\t\t\tfeishu_connected=\"true\"\n\t\tfi\n\t\tfeishu_abnormal=\"false\"\n\t\tif [[ \"$feishu_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"feishu\"; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$feishu_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tfeishu_status=$(openclaw_bot_status_text \"$feishu_enabled\" \"$feishu_cfg\" \"$feishu_connected\" \"$feishu_abnormal\")\n\n\t\tlocal wa_enabled wa_cfg wa_connected wa_abnormal wa_status\n\t\twa_enabled=$(openclaw_json_get_bool '.plugins.entries.whatsapp.enabled // .channels.whatsapp.enabled // false')\n\t\twa_cfg=$(openclaw_channel_has_cfg \"whatsapp\")\n\t\twa_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/whatsapp\"; then\n\t\t\twa_connected=\"true\"\n\t\tfi\n\t\twa_abnormal=\"false\"\n\t\tif [[ \"$wa_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"whatsapp\"; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$wa_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\twa_status=$(openclaw_bot_status_text \"$wa_enabled\" \"$wa_cfg\" \"$wa_connected\" \"$wa_abnormal\")\n\n\t\tlocal dc_enabled dc_cfg dc_connected dc_abnormal dc_status\n\t\tdc_enabled=$(openclaw_json_get_bool '.channels.discord.enabled // .plugins.entries.discord.enabled // false')\n\t\tdc_cfg=$(openclaw_channel_has_cfg \"discord\")\n\t\tdc_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/discord\"; then\n\t\t\tdc_connected=\"true\"\n\t\tfi\n\t\tdc_abnormal=\"false\"\n\t\tif [[ \"$dc_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tdc_abnormal=\"true\"\n\t\tfi\n\t\tdc_status=$(openclaw_bot_status_text \"$dc_enabled\" \"$dc_cfg\" \"$dc_connected\" \"$dc_abnormal\")\n\n\t\tlocal slack_enabled slack_cfg slack_connected slack_abnormal slack_status\n\t\tslack_enabled=$(openclaw_json_get_bool '.plugins.entries.slack.enabled // .channels.slack.enabled // false')\n\t\tslack_cfg=$(openclaw_channel_has_cfg \"slack\")\n\t\tslack_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/slack\"; then\n\t\t\tslack_connected=\"true\"\n\t\tfi\n\t\tslack_abnormal=\"false\"\n\t\tif [[ \"$slack_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"slack\"; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$slack_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tslack_status=$(openclaw_bot_status_text \"$slack_enabled\" \"$slack_cfg\" \"$slack_connected\" \"$slack_abnormal\")\n\n\t\tlocal qq_enabled qq_cfg qq_connected qq_abnormal qq_status\n\t\tqq_enabled=$(openclaw_json_get_bool '.plugins.entries.qqbot.enabled // .channels.qqbot.enabled // false')\n\t\tqq_cfg=$(openclaw_channel_has_cfg \"qqbot\")\n\t\tqq_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/sessions\" || openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/data\"; then\n\t\t\tqq_connected=\"true\"\n\t\tfi\n\t\tqq_abnormal=\"false\"\n\t\tif [[ \"$qq_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"qqbot\"; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$qq_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tqq_status=$(openclaw_bot_status_text \"$qq_enabled\" \"$qq_cfg\" \"$qq_connected\" \"$qq_abnormal\")\n\n\t\techo \"本地状态(仅本机配置/缓存,不做网络探测):\"\n\t\topenclaw_print_bot_status_line \"Telegram\" \"$tg_status\"\n\t\topenclaw_print_bot_status_line \"飞书(Lark)\" \"$feishu_status\"\n\t\topenclaw_print_bot_status_line \"WhatsApp\" \"$wa_status\"\n\t\topenclaw_print_bot_status_line \"Discord\" \"$dc_status\"\n\t\topenclaw_print_bot_status_line \"Slack\" \"$slack_status\"\n\t\topenclaw_print_bot_status_line \"QQ Bot\" \"$qq_status\"\n\t}\n\n\tchange_tg_bot_code() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"机器人连接对接\"\n\t\t\topenclaw_show_bot_local_status_block\n\t\t\techo\n\n\t\t\tlocal bot_choice\n\t\t\tbot_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择要对接的平台\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"Telegram 机器人对接\" \\\n\t\t\t\t\"飞书 (Lark) 机器人对接\" \\\n\t\t\t\t\"WhatsApp 机器人对接\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tlocal code\n\t\t\tcase \"$bot_choice\" in\n\t\t\t\t\"Telegram 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"TG机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve telegram \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"飞书 (Lark) 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"飞书机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve feishu \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"WhatsApp 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"WhatsApp连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve whatsapp \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"返回\"|*)\n\t\t\t\t\treturn 0\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\topenclaw_backup_root() {\n\t\techo \"${HOME}/.openclaw/backups\"\n\t}\n\n\topenclaw_is_interactive_terminal() {\n\t\t[[ -t 0 && -t 1 ]]\n\t}\n\n\topenclaw_has_command() {\n\t\tcommand -v \"$1\" >/dev/null 2>&1\n\t}\n\n\topenclaw_is_safe_relpath() {\n\t\tlocal rel=\"$1\"\n\t\t[[ -z \"$rel\" ]] && return 1\n\t\t[[ \"$rel\" == /* ]] && return 1\n\t\t[[ \"$rel\" == *//* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\n'* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\r'* ]] && return 1\n\t\tcase \"$rel\" in\n\t\t\t../*|*/../*|*/..|..)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t\treturn 0\n\t}\n\n\topenclaw_restore_path_allowed() {\n\t\tlocal mode=\"$1\"\n\t\tlocal rel=\"$2\"\n\t\tcase \"$mode\" in\n\t\t\tmemory)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\tMEMORY.md|AGENTS.md|USER.md|SOUL.md|TOOLS.md|memory/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\tproject)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\topenclaw.json|workspace/*|extensions/*|skills/*|prompts/*|tools/*|telegram/*|feishu/*|whatsapp/*|discord/*|slack/*|qqbot/*|logs/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\t*)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t}\n\n\topenclaw_pack_backup_archive() {\n\t\tlocal backup_type=\"$1\"\n\t\tlocal export_mode=\"$2\"\n\t\tlocal payload_dir=\"$3\"\n\t\tlocal output_file=\"$4\"\n\n\t\tlocal tmp_root\n\t\ttmp_root=$(mktemp -d) || return 1\n\t\tlocal pack_dir=\"$tmp_root/package\"\n\t\tmkdir -p \"$pack_dir\"\n\n\t\tcp -a \"$payload_dir\" \"$pack_dir/payload\"\n\n\t\t(\n\t\t\tcd \"$pack_dir/payload\" || exit 1\n\t\t\tfind . -type f | sed 's|^\\./||' | sort > \"$pack_dir/manifest.files\"\n\t\t\t: > \"$pack_dir/manifest.sha256\"\n\t\t\tlocal f\n\t\t\twhile IFS= read -r f; do\n\t\t\t\t[[ -z \"$f\" ]] && continue\n\t\t\t\tsha256sum \"$f\" >> \"$pack_dir/manifest.sha256\"\n\t\t\tdone < \"$pack_dir/manifest.files\"\n\t\t) || { rm -rf \"$tmp_root\"; return 1; }\n\n\t\tcat > \"$pack_dir/backup.meta\" <<EOF\nTYPE=$backup_type\nMODE=$export_mode\nCREATED_AT=$(date -u +'%Y-%m-%dT%H:%M:%SZ')\nHOST=$(hostname)\nEOF\n\n\t\tmkdir -p \"$(dirname \"$output_file\")\"\n\t\ttar -C \"$pack_dir\" -czf \"$output_file\" backup.meta manifest.files manifest.sha256 payload\n\t\tlocal rc=$?\n\t\trm -rf \"$tmp_root\"\n\t\treturn $rc\n\t}\n\n\topenclaw_offer_transfer_hint() {\n\t\tlocal file_path=\"$1\"\n\t\techo \"可使用以下方式下载备份文件:\"\n\t\techo \" 本地路径: $file_path\"\n\t\techo \" scp 示例: scp root@你的服务器:$file_path ./\"\n\t\techo \" 或使用 SFTP 客户端下载\"\n\t}\n\n\topenclaw_prepare_import_archive() {\n\t\tlocal expected_type=\"$1\"\n\t\tlocal archive_path=\"$2\"\n\t\tlocal unpack_root=\"$3\"\n\n\t\tif [[ ! -f \"$archive_path\" ]]; then\n\t\t\techo \"错误:文件不存在: $archive_path\"\n\t\t\treturn 1\n\t\tfi\n\t\tmkdir -p \"$unpack_root\"\n\t\tif ! tar -xzf \"$archive_path\" -C \"$unpack_root\"; then\n\t\t\techo \"错误:备份包解压失败\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal pkg_dir=\"$unpack_root/package\"\n\t\tif [[ -f \"$unpack_root/backup.meta\" ]]; then\n\t\t\tpkg_dir=\"$unpack_root\"\n\t\tfi\n\n\t\tlocal required\n\t\tfor required in backup.meta manifest.files manifest.sha256 payload; do\n\t\t\tif [[ ! -e \"$pkg_dir/$required\" ]]; then\n\t\t\t\techo \"错误:备份包缺少必要文件: $required\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tdone\n\n\t\tlocal real_type\n\t\treal_type=$(grep '^TYPE=' \"$pkg_dir/backup.meta\" | head -n1 | cut -d'=' -f2-)\n\t\tif [[ \"$real_type\" != \"$expected_type\" ]]; then\n\t\t\techo \"错误:备份类型不匹配,期望: $expected_type,实际: ${real_type:-未知}\"\n\t\t\treturn 1\n\t\tfi\n\n\t\t(\n\t\t\tcd \"$pkg_dir/payload\" || exit 1\n\t\t\tsha256sum -c ../manifest.sha256 >/dev/null\n\t\t) || { echo \"错误:sha256 校验失败,拒绝还原\"; return 1; }\n\n\t\techo \"$pkg_dir\"\n\t\treturn 0\n\t}\n\n\topenclaw_memory_backup_export() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tlocal ts\n\t\tts=$(date +%Y%m%d-%H%M%S)\n\t\tlocal out_file=\"$backup_root/openclaw-memory-full-${ts}.tar.gz\"\n\n\t\tmkdir -p \"$backup_root\"\n\t\tif [[ ! -d \"$workspace_dir\" ]]; then\n\t\t\techo \"错误:未找到 workspace 目录: $workspace_dir\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\t[[ -f \"$workspace_dir/MEMORY.md\" ]] && cp -a \"$workspace_dir/MEMORY.md\" \"$tmp_payload/\"\n\t\t[[ -d \"$workspace_dir/memory\" ]] && cp -a \"$workspace_dir/memory\" \"$tmp_payload/\"\n\n\t\tif gum confirm \"是否附带 AGENTS/USER/SOUL/TOOLS 文件?\"; then\n\t\t\tlocal f\n\t\t\tfor f in AGENTS.md USER.md SOUL.md TOOLS.md; do\n\t\t\t\t[[ -f \"$workspace_dir/$f\" ]] && cp -a \"$workspace_dir/$f\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的记忆文件\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"memory-full\" \"default\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"记忆全量备份完成: $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:记忆全量备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_read_import_path() {\n\t\tlocal file_input file_path backup_root\n\t\techo \"提示:输入文件名时默认在备份目录查找;输入含 / 的路径时按完整路径校验。\" >&2\n\t\techo \"scp 示例: scp /本地/备份包.tar.gz root@你的服务器:/tmp/\" >&2\n\n\t\tfile_input=$(gum input \\\n\t\t\t--placeholder \"备份文件名或完整路径\" \\\n\t\t\t--prompt \"Path > \")\n\t\t[[ -z \"$file_input\" ]] && { echo \"\"; return 0; }\n\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\n\t\tif [[ \"$file_input\" == */* ]]; then\n\t\t\tfile_path=\"$file_input\"\n\t\telse\n\t\t\tfile_path=\"$backup_root/$file_input\"\n\t\tfi\n\n\t\tif [[ ! -f \"$file_path\" ]]; then\n\t\t\techo \"错误:备份文件不存在: $file_path\" >&2\n\t\t\techo \"\"\n\t\t\treturn 1\n\t\tfi\n\n\t\techo \"$file_path\"\n\t}\n\n\topenclaw_memory_backup_import() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tmkdir -p \"$workspace_dir\"\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"memory-full\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed memory \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$workspace_dir/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$workspace_dir/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"记忆全量还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_export() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tif [[ ! -d \"$openclaw_root\" ]]; then\n\t\t\techo \"错误:未找到 OpenClaw 根目录: $openclaw_root\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal export_mode_label\n\t\texport_mode_label=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $'选择备份模式\\n↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"安全模式(推荐):workspace + openclaw.json + extensions/skills/prompts/tools\" \\\n\t\t\t\"完整模式(含更多状态,敏感风险更高)\") || return 1\n\n\t\tlocal mode_label=\"safe\"\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\tlocal d\n\t\tif [[ \"$export_mode_label\" == 完整* ]]; then\n\t\t\tmode_label=\"full\"\n\t\t\tfor d in workspace extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in telegram feishu whatsapp discord slack qqbot logs; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\telse\n\t\t\t[[ -d \"$openclaw_root/workspace\" ]] && cp -a \"$openclaw_root/workspace\" \"$tmp_payload/\"\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的 OpenClaw 项目内容\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tlocal out_file=\"$backup_root/openclaw-project-${mode_label}-$(date +%Y%m%d-%H%M%S).tar.gz\"\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"openclaw-project\" \"$mode_label\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"OpenClaw 项目备份完成 (${mode_label}): $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:OpenClaw 项目备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_import() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tmkdir -p \"$openclaw_root\"\n\n\t\techo \"警告:高风险操作,项目还原会覆盖 OpenClaw 配置与工作区内容。\"\n\t\tgum confirm \"确认继续还原?此操作不可逆。\" || { echo \"已取消\"; break_end; return 1; }\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"openclaw-project\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed project \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tgum spin --spinner pulse --title \"停止 OpenClaw gateway...\" -- openclaw gateway stop\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$openclaw_root/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$openclaw_root/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tstart_gateway\n\t\t\techo \"gateway 健康检查:\"\n\t\t\topenclaw gateway status || true\n\t\tfi\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"OpenClaw 项目还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_detect_type() {\n\t\tlocal file_name=\"$1\"\n\t\tif [[ \"$file_name\" == openclaw-memory-full-*.tar.gz ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\telif [[ \"$file_name\" == openclaw-project-*.tar.gz ]]; then\n\t\t\techo \"项目备份文件\"\n\t\telse\n\t\t\techo \"其他备份文件\"\n\t\tfi\n\t}\n\n\topenclaw_backup_collect_files() {\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tmapfile -t OPENCLAW_BACKUP_FILES < <(find \"$backup_root\" -maxdepth 1 -type f -name '*.tar.gz' | xargs -I{} basename {} | sort -r)\n\t}\n\n\topenclaw_backup_render_file_list() {\n\t\tlocal backup_root i file_name file_path file_type file_size file_time\n\t\tlocal has_memory=0 has_project=0 has_other=0\n\t\tbackup_root=$(openclaw_backup_root)\n\t\topenclaw_backup_collect_files\n\n\t\techo \"备份目录: $backup_root\"\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\techo \"暂无备份文件\"\n\t\t\treturn 0\n\t\tfi\n\n\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\tfile_type=$(openclaw_backup_detect_type \"${OPENCLAW_BACKUP_FILES[$i]}\")\n\t\t\tcase \"$file_type\" in\n\t\t\t\t\"记忆备份文件\") has_memory=1 ;;\n\t\t\t\t\"项目备份文件\") has_project=1 ;;\n\t\t\t\t\"其他备份文件\") has_other=1 ;;\n\t\t\tesac\n\t\tdone\n\n\t\tif [[ \"$has_memory\" -eq 1 ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"记忆备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_project\" -eq 1 ]]; then\n\t\t\techo \"项目备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"项目备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_other\" -eq 1 ]]; then\n\t\t\techo \"其他备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"其他备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_backup_file_exists_in_list() {\n\t\tlocal target_file=\"$1\"\n\t\tlocal item\n\t\tfor item in \"${OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t[[ \"$item\" == \"$target_file\" ]] && return 0\n\t\tdone\n\t\treturn 1\n\t}\n\n\topenclaw_backup_delete_file() {\n\t\tlocal backup_root backup_root_real user_input target_file target_path target_type\n\t\tbackup_root=$(openclaw_backup_root)\n\n\t\topenclaw_backup_render_file_list\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\tbreak_end\n\t\t\treturn 0\n\t\tfi\n\n\t\tuser_input=$(printf '%s\\n' \"${OPENCLAW_BACKUP_FILES[@]}\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 选择要删除的备份 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=15 \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ⚡ DELETE BACKUP \" \\\n\t\t\t--color=border:196,label:196,header:196,prompt:208,pointer:196,marker:208,hl:208,hl+:208) || { echo \"已取消\"; break_end; return 0; }\n\n\t\t[[ -z \"$user_input\" ]] && { echo \"已取消\"; break_end; return 0; }\n\n\t\tbackup_root_real=$(realpath -m \"$backup_root\")\n\t\ttarget_file=$(basename -- \"$user_input\")\n\t\ttarget_path=\"$backup_root/$target_file\"\n\n\t\tif [[ ! -f \"$target_path\" ]]; then\n\t\t\techo \"错误:目标文件不存在: $target_path\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\ttarget_type=$(openclaw_backup_detect_type \"$target_file\")\n\n\t\techo \"即将删除: [$target_type] $target_path\"\n\t\tgum confirm \"确认删除?\" || { echo \"已取消删除\"; break_end; return 0; }\n\t\tgum confirm \"请再次确认,删除后不可恢复\" || { echo \"已取消删除\"; break_end; return 0; }\n\n\t\tif rm -f -- \"$target_path\"; then\n\t\t\techo \"删除成功: $target_file\"\n\t\telse\n\t\t\techo \"错误:删除失败: $target_file\"\n\t\tfi\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_list_files() {\n\t\topenclaw_backup_render_file_list\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_restore_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"备份与还原\"\n\t\t\topenclaw_backup_render_file_list\n\t\t\techo\n\n\t\t\tlocal backup_choice\n\t\t\tbackup_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"备份记忆全量\" \\\n\t\t\t\t\"还原记忆全量\" \\\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\" \\\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\" \\\n\t\t\t\t\"删除备份文件\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$backup_choice\" in\n\t\t\t\t\"备份记忆全量\") openclaw_memory_backup_export ;;\n\t\t\t\t\"还原记忆全量\") openclaw_memory_backup_import ;;\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\") openclaw_project_backup_export ;;\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\") openclaw_project_backup_import ;;\n\t\t\t\t\"删除备份文件\") openclaw_backup_delete_file ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\t_autostart_status() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\techo \"未安装\"\n\t\t\t\treturn\n\t\t\tfi\n\t\t\tif launchctl list 2>/dev/null | grep -q \"ai.openclaw.gateway\"; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\telse\n\t\t\tif systemctl --user is-enabled --quiet openclaw-gateway.service 2>/dev/null; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\tfi\n\t}\n\n\ttoggle_autostart() {\n\t\tlocal status; status=$(_autostart_status)\n\t\tclear\n\t\tui_header \"开机自启动管理\"\n\t\techo -e \" 当前状态:\\033[1m${status}\\033[0m\"\n\t\techo\n\n\t\tlocal action\n\t\tif [[ \"$status\" == \"已开启\" ]]; then\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"关闭自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\telse\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"开启自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\tfi\n\n\t\tcase \"$action\" in\n\t\t\t\"开启自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\t\t\tgum spin --spinner globe --title \"正在安装服务...\" -- \\\n\t\t\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 1\"\n\t\t\t\t\tfi\n\t\t\t\t\tlaunchctl load \"$plist\" 2>/dev/null && ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t\"关闭自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tlaunchctl unload \"$plist\" 2>/dev/null && ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user disable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t*) return 0 ;;\n\t\tesac\n\t\tbreak_end\n\t}\n\n\tupdate_moltbot() {\n\t\tinstall_node_and_tools\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab -\n\t\tstart_gateway\n\t\thash -r\n\t\tui_ok \"更新完成\"\n\t\tbreak_end\n\t}\n\n\tuninstall_moltbot() {\n\t\tgum confirm \"确认卸载 OpenClaw?此操作不可逆。\" || return 0\n\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tlaunchctl unload \"$plist\" 2>/dev/null || true\n\t\telse\n\t\t\tsystemctl --user disable --now openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tpkill -f \"openclaw.*gatewa\" 2>/dev/null || true\n\n\t\tgum spin --spinner meter --title \"正在卸载 OpenClaw...\" -- \\\n\t\t\tbash -c 'timeout 15 openclaw uninstall 2>/dev/null; true'\n\t\tnpm uninstall -g openclaw 2>/dev/null || true\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab - 2>/dev/null || true\n\t\trm -rf /root/.openclaw ~/.openclaw\n\n\t\trm -f \"$HOME/.local/bin/oc\" \"$HOME/.local/bin/openclawctl.sh\" /usr/local/bin/oc\n\t\tfor rc in \"$HOME/.zshrc\" \"$HOME/.bashrc\"; do\n\t\t\t[[ -f \"$rc\" ]] && _sed_i '/\\.local\\/bin.*PATH/d' \"$rc\"\n\t\tdone\n\n\t\techo\n\t\tgum style --foreground 46 --bold \" 卸载完成,脚本已退出。\"\n\t\texit 0\n\t}\n\n\tnano_openclaw_json() {\n\t\tinstall nano\n\t\tnano ~/.openclaw/openclaw.json\n\t\tstart_gateway\n\t}\n\n\topenclaw_find_webui_domain() {\n\t\tlocal conf domain_list\n\t\tdomain_list=$(\n\t\t\tgrep -R \"18789\" /home/web/conf.d/*.conf 2>/dev/null \\\n\t\t\t| awk -F: '{print $1}' \\\n\t\t\t| sort -u \\\n\t\t\t| while read -r conf; do\n\t\t\t\tbasename \"$conf\" .conf\n\t\t\tdone\n\t\t)\n\n\t\tif [[ -n \"$domain_list\" ]]; then\n\t\t\techo \"$domain_list\"\n\t\tfi\n\t}\n\n\topenclaw_show_webui_addr() {\n\t\tlocal local_ip token domains\n\n\t\tlocal_ip=\"127.0.0.1\"\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tgum style --border normal --border-foreground 99 --padding \"0 2\" \\\n\t\t\t\"OpenClaw WebUI 访问地址\" \\\n\t\t\t\"本机: http://${local_ip}:18789/#token=${token}\"\n\n\t\tdomains=$(openclaw_find_webui_domain)\n\t\tif [[ -n \"$domains\" ]]; then\n\t\t\techo \"$domains\" | while read -r d; do\n\t\t\t\techo \"域名: https://${d}/#token=${token}\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_domain_webui() {\n\t\tadd_yuming\n\t\tldnmp_Proxy \"${yuming}\" 127.0.0.1 18789\n\n\t\tlocal token\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tclear\n\t\techo \"访问地址: https://${yuming}/#token=$token\"\n\t\techo \"先访问URL触发设备ID,然后回车下一步进行配对。\"\n\t\tgum input --placeholder \"按回车继续...\" > /dev/null\n\n\t\techo -e \"${gl_kjlan}正在加载设备列表……${gl_bai}\"\n\n\t\tlocal config_file=\"$HOME/.openclaw/openclaw.json\"\n\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\tlocal new_origin=\"https://${yuming}\"\n\t\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\t\tlocal tmp_json\n\t\t\t\ttmp_json=$(mktemp)\n\t\t\t\tjq 'if .gateway.controlUi == null then .gateway.controlUi = {\"allowedOrigins\": [\"http://127.0.0.1\"]} else . end | if (.gateway.controlUi.allowedOrigins | contains([$origin]) | not) then .gateway.controlUi.allowedOrigins += [$origin] else . end' \\\n\t\t\t\t\t--arg origin \"$new_origin\" \"$config_file\" > \"$tmp_json\" && mv \"$tmp_json\" \"$config_file\"\n\t\t\t\techo -e \"${gl_kjlan}已将域名 ${yuming} 加入 allowedOrigins 配置${gl_bai}\"\n\t\t\t\topenclaw gateway restart >/dev/null 2>&1\n\t\t\tfi\n\t\tfi\n\n\t\topenclaw devices list\n\n\t\tlocal Request_Key\n\t\tRequest_Key=$(gum input \\\n\t\t\t--placeholder \"Request_Key\" \\\n\t\t\t--prompt \"> \")\n\n\t\tif [[ -z \"$Request_Key\" ]]; then\n\t\t\techo \"Request_Key 不能为空\"\n\t\t\treturn 1\n\t\tfi\n\n\t\topenclaw devices approve \"$Request_Key\"\n\t}\n\n\topenclaw_remove_domain() {\n\t\techo \"域名格式 example.com 不带https://\"\n\t\tweb_del\n\t}\n\n\topenclaw_webui_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"WebUI 访问与设置\"\n\t\t\topenclaw_show_webui_addr\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加域名访问\" \\\n\t\t\t\t\"删除域名访问\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"添加域名访问\")\n\t\t\t\t\topenclaw_domain_webui\n\t\t\t\t\techo\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"删除域名访问\")\n\t\t\t\t\topenclaw_remove_domain\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\twhile true; do\n\t\tclear\n\n\t\tlocal install_status running_status update_info\n\t\tinstall_status=$(get_install_status)\n\t\trunning_status=$(get_running_status)\n\t\tupdate_info=$(check_openclaw_update)\n\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"1 8\" --align center \\\n\t\t\t\"O P E N C L A W\" \\\n\t\t\t\"\" \\\n\t\t\t\"[ AI Agent Gateway Manager ]\"\n\t\techo -e \" $install_status $running_status $update_info\"\n\t\tgum style --foreground 240 \" 输入 oc 可快速启动\"\n\t\techo\n\t\tgum style \\\n\t\t\t--bold \\\n\t\t\t--border double --border-foreground 201 \\\n\t\t\t--padding \"0 4\" --align center \\\n\t\t\t\"$(gum style --bold --foreground 201 '◈ by Joey ◈')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ YouTube @joeyblog')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ Telegram t.me/+ft-zI76oovgwNmRh')\" \\\n\t\t\t\"$(gum style --foreground 208 '⚡ 基于:kejilion · cliproxyapi-installer')\"\n\t\techo\n\n\t\tlocal choice\n\t\tlocal autostart_label\n\t\tautostart_label=\"开机自启动 [$(_autostart_status)]\"\n\n\t\tchoice=$(gum choose \\\n\t\t\t--height 24 \\\n\t\t\t--cursor \"❯ \" \\\n\t\t\t--header $' ─── CONTROL CENTER ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\"小白模式安装(推荐)\" \\\n\t\t\t\"安装\" \\\n\t\t\t\"启动\" \\\n\t\t\t\"停止\" \\\n\t\t\t\"状态日志查看\" \\\n\t\t\t\"$autostart_label\" \\\n\t\t\t\"换模型\" \\\n\t\t\t\"API管理\" \\\n\t\t\t\"CLIProxyAPI 管理\" \\\n\t\t\t\"机器人连接对接\" \\\n\t\t\t\"安装插件\" \\\n\t\t\t\"安装技能\" \\\n\t\t\t\"编辑主配置文件\" \\\n\t\t\t\"配置向导\" \\\n\t\t\t\"健康检测与修复\" \\\n\t\t\t\"WebUI访问与设置\" \\\n\t\t\t\"TUI命令行对话\" \\\n\t\t\t\"备份与还原\" \\\n\t\t\t\"更新\" \\\n\t\t\t\"卸载\" \\\n\t\t\t\"退出\") || break\n\n\t\tcase \"$choice\" in\n\t\t\t\"小白模式安装(推荐)\") beginner_mode_install ;;\n\t\t\t\"安装\") install_moltbot ;;\n\t\t\t\"启动\") start_bot ;;\n\t\t\t\"停止\") stop_bot ;;\n\t\t\t\"状态日志查看\") view_logs ;;\n\t\t\t\"开机自启动 [\"*\"]\") toggle_autostart ;;\n\t\t\t\"换模型\") change_model ;;\n\t\t\"API管理\") openclaw_api_manage_menu ;;\n\t\t\"CLIProxyAPI 管理\") cliproxyapi_manage_menu ;;\n\t\t\"机器人连接对接\") change_tg_bot_code ;;\n\t\t\t\"安装插件\") install_plugin ;;\n\t\t\t\"安装技能\") install_skill ;;\n\t\t\t\"编辑主配置文件\") nano_openclaw_json ;;\n\t\t\t\"配置向导\")\n\t\t\t\topenclaw onboard --install-daemon\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"健康检测与修复\")\n\t\t\t\tgum spin --spinner meter --title \"正在执行健康检测...\" -- openclaw doctor --fix\n\t\t\t\tif sync_openclaw_api_models; then\n\t\t\t\t\tstart_gateway\n\t\t\t\telse\n\t\t\t\t\tui_err \"API 模型同步失败,已中止重启网关,请检查 provider /models 返回后重试\"\n\t\t\t\tfi\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"WebUI访问与设置\") openclaw_webui_menu ;;\n\t\t\t\"TUI命令行对话\")\n\t\t\t\topenclaw tui\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"备份与还原\") openclaw_backup_restore_menu ;;\n\t\t\t\"更新\") update_moltbot ;;\n\t\t\t\"卸载\") uninstall_moltbot ;;\n\t\t\t\"退出\"|*) break ;;\n\t\tesac\n\tdone\n}\n\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n\tmoltbot_menu\nfi\n"}, "3378cd815aa28e339f1975a812ae4ce07de2b60b": {"t": 1785772929.7161825, "n": 95345, "h": "556d16619e17787c", "c": "#!/usr/bin/env bash\n#\n# ╔══════════════════════════════════════════════════════════════════╗\n# ║ OpenClaw / MoltBot 管理脚本 ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 作者 GitHub : by Joey ║\n# ║ YouTube : @joeyblog ║\n# ║ Telegram: https://t.me/+ft-zI76oovgwNmRh ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 致谢 / 引用 ║\n# ║ · 原始脚本基础来自 kejilion(@kejilion) ║\n# ║ · CLIProxyAPI 安装器来自 cliproxyapi-installer ║\n# ║ github.com/brokechubb/cliproxyapi-installer ║\n# ╚══════════════════════════════════════════════════════════════════╝\n#\n: \"${gl_hui:='\\e[37m'}\"\n: \"${gl_hong:='\\033[31m'}\"\n: \"${gl_lv:='\\033[32m'}\"\n: \"${gl_huang:='\\033[33m'}\"\n: \"${gl_lan:='\\033[34m'}\"\n: \"${gl_bai:='\\033[0m'}\"\n: \"${gl_zi:='\\033[35m'}\"\n: \"${gl_kjlan:='\\033[96m'}\"\n\nif ! declare -f break_end > /dev/null 2>&1; then\nbreak_end() {\n\tif command -v gum >/dev/null 2>&1; then\n\t\techo\n\t\tgum style --foreground 240 \" ─────────────────────────────────────── \"\n\t\tgum input --placeholder \"» 按回车继续 «\" --prompt \" \" > /dev/null\n\telse\n\t\techo \" ─── 按任意键继续 ───\"\n\t\tread -n 1 -s -r -p \"\"\n\t\techo\n\tfi\n\tclear\n}\nfi\n\nif ! declare -f install > /dev/null 2>&1; then\ninstall() {\n\tif [[ $# -eq 0 ]]; then\n\t\techo \"未提供软件包参数\"\n\t\treturn 1\n\tfi\n\tfor package in \"$@\"; do\n\t\tif ! command -v \"$package\" &>/dev/null; then\n\t\t\techo -e \"${gl_kjlan}正在安装 $package...${gl_bai}\"\n\t\t\tif [[ \"$(uname -s)\" == \"Darwin\" ]]; then\n\t\t\t\t_ensure_brew &>/dev/null\n\t\t\t\tcommand -v brew &>/dev/null && brew install \"$package\" &>/dev/null\n\t\t\telif command -v dnf &>/dev/null; then\n\t\t\t\tdnf install -y \"$package\" &>/dev/null\n\t\t\telif command -v yum &>/dev/null; then\n\t\t\t\tyum install -y \"$package\" &>/dev/null\n\t\t\telif command -v apt &>/dev/null; then\n\t\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"$package\" &>/dev/null\n\t\t\telif command -v apk &>/dev/null; then\n\t\t\t\tapk add \"$package\" &>/dev/null\n\t\t\telif command -v pacman &>/dev/null; then\n\t\t\t\tpacman -S --noconfirm \"$package\" &>/dev/null\n\t\t\telif command -v zypper &>/dev/null; then\n\t\t\t\tzypper install -y \"$package\" &>/dev/null\n\t\t\telif command -v opkg &>/dev/null; then\n\t\t\t\topkg install \"$package\" &>/dev/null\n\t\t\telif command -v pkg &>/dev/null; then\n\t\t\t\tpkg install -y \"$package\" &>/dev/null\n\t\t\telse\n\t\t\t\techo \"未知的包管理器,无法安装 $package\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\tdone\n}\nfi\n\nis_macos() { [[ \"$(uname -s)\" == \"Darwin\" ]]; }\n\n_ensure_brew() {\n\tif ! command -v brew &>/dev/null; then\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tif ! command -v brew &>/dev/null; then\n\t\techo \"正在安装 Homebrew...\"\n\t\t/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tcommand -v brew &>/dev/null || { echo \"Homebrew 安装失败,请手动安装:https://brew.sh\"; return 1; }\n}\n\n_sed_i() {\n\tif is_macos; then\n\t\tsed -i '' \"$@\"\n\telse\n\t\tsed -i \"$@\"\n\tfi\n}\n\ninstall_base_deps() {\n\tif is_macos; then\n\t\t_ensure_brew || return 1\n\t\tlocal missing_brew=()\n\t\tfor cmd in curl git nano jq python3 gpg; do\n\t\t\tcommand -v \"$cmd\" &>/dev/null || missing_brew+=(\"$cmd\")\n\t\tdone\n\t\tfor i in \"${!missing_brew[@]}\"; do\n\t\t\t[[ \"${missing_brew[$i]}\" == \"gpg\" ]] && missing_brew[$i]=\"gnupg\"\n\t\tdone\n\t\t[[ ${#missing_brew[@]} -eq 0 ]] && return 0\n\t\techo \"正在安装缺失依赖:${missing_brew[*]}...\"\n\t\tbrew install \"${missing_brew[@]}\" &>/dev/null\n\t\treturn 0\n\tfi\n\n\tlocal -a dep_map=(\n\t\t\"curl:curl:curl:curl:curl:curl\"\n\t\t\"git:git:git:git:git:git\"\n\t\t\"nano:nano:nano:nano:nano:nano\"\n\t\t\"jq:jq:jq:jq:jq:jq\"\n\t\t\"python3:python3:python3:python3:python:python3\"\n\t\t\"tar:tar:tar:tar:tar:tar\"\n\t\t\"gpg:gnupg:gnupg2:gnupg:gnupg:gpg2\"\n\t)\n\n\tlocal missing_apt=() missing_dnf=() missing_apk=() missing_pacman=() missing_zypper=()\n\n\tfor entry in \"${dep_map[@]}\"; do\n\t\tIFS=: read -r cmd pkg_apt pkg_dnf pkg_apk pkg_pacman pkg_zypper <<< \"$entry\"\n\t\tcommand -v \"$cmd\" &>/dev/null && continue\n\t\tmissing_apt+=(\"$pkg_apt\")\n\t\tmissing_dnf+=(\"$pkg_dnf\")\n\t\tmissing_apk+=(\"$pkg_apk\")\n\t\tmissing_pacman+=(\"$pkg_pacman\")\n\t\tmissing_zypper+=(\"$pkg_zypper\")\n\tdone\n\n\t[[ ${#missing_apt[@]} -eq 0 ]] && return 0\n\n\techo \"正在安装缺失依赖:${missing_apt[*]}...\"\n\n\tif command -v apt &>/dev/null; then\n\t\tapt update -y &>/dev/null\n\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"${missing_apt[@]}\" &>/dev/null\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y epel-release &>/dev/null || true\n\t\tdnf install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y epel-release &>/dev/null || true\n\t\tyum install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v apk &>/dev/null; then\n\t\tapk update &>/dev/null\n\t\tapk add \"${missing_apk[@]}\" &>/dev/null\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -Sy --noconfirm --needed \"${missing_pacman[@]}\" &>/dev/null\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y \"${missing_zypper[@]}\" &>/dev/null\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\tlocal py\n\t\tpy=$(compgen -c 2>/dev/null | grep -E '^python3\\.[0-9]+$' | sort -V | tail -1)\n\t\tif [[ -n \"$py\" ]]; then\n\t\t\tln -sf \"$(command -v \"$py\")\" /usr/local/bin/python3 2>/dev/null || true\n\t\tfi\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\techo \"正在安装 python3...\"\n\t\tif command -v apt &>/dev/null; then\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y python3 &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tdnf install -y python3 &>/dev/null\n\t\telif command -v yum &>/dev/null; then\n\t\t\tyum install -y python3 &>/dev/null\n\t\telif command -v apk &>/dev/null; then\n\t\t\tapk add python3 &>/dev/null\n\t\telif command -v pacman &>/dev/null; then\n\t\t\tpacman -S --noconfirm python &>/dev/null\n\t\telif command -v zypper &>/dev/null; then\n\t\t\tzypper install -y python3 &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\tfi\n}\n\n_install_gum_binary() {\n\tlocal arch\n\tcase \"$(uname -m)\" in\n\t\tx86_64) arch=\"amd64\" ;;\n\t\taarch64) arch=\"arm64\" ;;\n\t\tarmv7l) arch=\"armv7\" ;;\n\t\t*) echo \"不支持的架构: $(uname -m)\"; return 1 ;;\n\tesac\n\tlocal latest\n\tlatest=$(curl -fsSL https://api.github.com/repos/charmbracelet/gum/releases/latest \\\n\t\t| grep '\"tag_name\"' | sed 's/.*\"v\\([^\"]*\\)\".*/\\1/')\n\t[[ -z \"$latest\" ]] && { echo \"获取 gum 版本失败\"; return 1; }\n\tlocal url=\"https://github.com/charmbracelet/gum/releases/download/v${latest}/gum_${latest}_linux_${arch}.tar.gz\"\n\tlocal tmp\n\ttmp=$(mktemp -d)\n\tcurl -fsSL \"$url\" -o \"$tmp/gum.tar.gz\" \\\n\t\t&& tar -xzf \"$tmp/gum.tar.gz\" -C \"$tmp\" \\\n\t\t&& install -m 755 \"$tmp/gum\" /usr/local/bin/gum\n\trm -rf \"$tmp\"\n\tcommand -v gum >/dev/null 2>&1\n}\n\ninstall_gum() {\n\tcommand -v gum >/dev/null 2>&1 && return 0\n\techo \"正在安装 gum...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install gum\n\telif command -v apt >/dev/null 2>&1; then\n\t\tcommand -v gpg >/dev/null 2>&1 || apt install -y gnupg 2>/dev/null\n\t\tmkdir -p /etc/apt/keyrings\n\t\tif curl -fsSL https://repo.charm.sh/apt/gpg.key \\\n\t\t\t| gpg --dearmor -o /etc/apt/keyrings/charm.gpg 2>/dev/null; then\n\t\t\techo \"deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *\" \\\n\t\t\t\t| tee /etc/apt/sources.list.d/charm.list >/dev/null\n\t\t\tapt update -y 2>/dev/null && apt install -y gum 2>/dev/null\n\t\tfi\n\t\tcommand -v gum >/dev/null 2>&1 || _install_gum_binary\n\telif command -v dnf &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tdnf install -y gum\n\telif command -v yum &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tyum install -y gum\n\telif command -v apk &>/dev/null; then\n\t\tapk add gum 2>/dev/null || _install_gum_binary\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm gum 2>/dev/null || _install_gum_binary\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y gum 2>/dev/null || _install_gum_binary\n\telse\n\t\t_install_gum_binary\n\tfi\n\tcommand -v gum >/dev/null 2>&1 || { echo \"gum 安装失败,请手动安装: https://github.com/charmbracelet/gum\"; return 1; }\n}\n\ninstall_fzf() {\n\tcommand -v fzf >/dev/null 2>&1 && return 0\n\techo \"正在安装 fzf...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install fzf\n\telif command -v apt >/dev/null 2>&1; then\n\t\tapt install -y fzf\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y fzf\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y fzf\n\telif command -v apk &>/dev/null; then\n\t\tapk add fzf\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm fzf\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y fzf\n\telse\n\t\techo \"无法自动安装 fzf,请手动安装: https://github.com/junegunn/fzf\"\n\t\treturn 1\n\tfi\n}\n\n_install_shortcut() {\n\tlocal store_dir shortcut_dir shortcut\n\n\tstore_dir=\"$HOME/.local/bin\"\n\tmkdir -p \"$store_dir\"\n\n\tlocal _candidate_dirs=(\"/opt/homebrew/bin\" \"/usr/local/bin\")\n\tshortcut_dir=\"\"\n\tfor _d in \"${_candidate_dirs[@]}\"; do\n\t\tif [[ -d \"$_d\" && -w \"$_d\" ]]; then\n\t\t\tshortcut_dir=\"$_d\"\n\t\t\tbreak\n\t\tfi\n\tdone\n\n\tif [[ -z \"$shortcut_dir\" ]]; then\n\t\tshortcut_dir=\"$store_dir\"\n\t\tlocal rc_file=\"\"\n\t\t[[ -f \"$HOME/.zshrc\" ]] && rc_file=\"$HOME/.zshrc\"\n\t\t[[ -f \"$HOME/.bashrc\" ]] && rc_file=\"${rc_file:-$HOME/.bashrc}\"\n\t\tif [[ -n \"$rc_file\" ]] && ! grep -q \"$store_dir\" \"$rc_file\" 2>/dev/null; then\n\t\t\tprintf '\\nexport PATH=\"%s:$PATH\"\\n' \"$store_dir\" >> \"$rc_file\"\n\t\tfi\n\t\texport PATH=\"$store_dir:$PATH\"\n\tfi\n\n\tshortcut=\"$shortcut_dir/oc\"\n\n\tcat > \"$shortcut\" <<EOF\n#!/usr/bin/env bash\ncurl -fsSL https://raw.githubusercontent.com/byJoey/openclawctl/main/openclaw.sh \\\\\n -o \"$store_dir/openclawctl.sh\" 2>/dev/null && chmod +x \"$store_dir/openclawctl.sh\"\nexec bash \"$store_dir/openclawctl.sh\" \"\\$@\"\nEOF\n\tchmod +x \"$shortcut\"\n}\n\nmoltbot_menu() {\n\tis_macos && _ensure_brew\n\n\t_install_shortcut\n\n\tinstall_base_deps\n\tinstall_gum || { echo \"gum 安装失败,无法继续\"; return 1; }\n\tinstall_fzf || { echo \"fzf 安装失败,无法继续\"; return 1; }\n\n\tui_header() {\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"0 2\" \"$*\"\n\t\techo\n\t}\n\tui_ok() { gum style --foreground 46 \" ◉ $*\"; }\n\tui_err() { gum style --foreground 196 \" ✗ $*\"; }\n\tui_warn() { gum style --foreground 208 \" ⚡ $*\"; }\n\tui_info() { gum style --foreground 51 \" ◈ $*\"; }\n\tui_step() { echo; gum style --bold --foreground 201 \" ▶ $*\"; echo; }\n\n\tcheck_openclaw_update() {\n\t\tif ! command -v npm >/dev/null 2>&1; then\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal local_version remote_version\n\t\tlocal_version=$(npm list -g openclaw --depth=0 --no-update-notifier 2>/dev/null \\\n\t\t\t| grep openclaw | awk '{print $NF}' | sed 's/^.*@//')\n\n\t\t[[ -z \"$local_version\" ]] && return 1\n\n\t\tremote_version=$(npm view openclaw version --no-update-notifier 2>/dev/null)\n\n\t\t[[ -z \"$remote_version\" ]] && return 1\n\n\t\tif [[ \"$local_version\" != \"$remote_version\" ]]; then\n\t\t\techo -e \"\\033[38;5;208m⚡ UPDATE AVAILABLE $remote_version\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m✦ v$local_version\\033[0m\"\n\t\tfi\n\t}\n\n\tget_install_status() {\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m◉ INSTALLED\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m○ NOT FOUND\\033[0m\"\n\t\tfi\n\t}\n\n\tget_running_status() {\n\t\tif pgrep -f \"openclaw.*gatewa\" >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m■ STOPPED\\033[0m\"\n\t\tfi\n\t}\n\n\tstart_gateway() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tlaunchctl unload \"$plist\" >/dev/null 2>&1 || true\n\t\t\tif [[ -f \"$plist\" ]]; then\n\t\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- \\\n\t\t\t\t\tbash -c \"launchctl load '$plist' 2>/dev/null; sleep 2\"\n\t\t\telse\n\t\t\t\tgum spin --spinner pulse --title \"正在安装并启动网关...\" -- \\\n\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\telse\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- openclaw gateway start\n\t\t\tsleep 1\n\t\tfi\n\t}\n\n\tinstall_node_and_tools() {\n\t\tif command -v node &>/dev/null && command -v npm &>/dev/null; then\n\t\t\treturn 0\n\t\tfi\n\t\techo \"正在安装 Node.js...\"\n\t\tif is_macos; then\n\t\t\t_ensure_brew || return 1\n\t\t\tbrew install node &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tcurl -fsSL https://rpm.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tdnf install -y cmake libatomic nodejs &>/dev/null\n\t\telif command -v apt &>/dev/null; then\n\t\t\tcurl -fsSL https://deb.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y build-essential python3 libatomic1 nodejs &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t}\n\n\tconfigure_openclaw_session_policy() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 1\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport json, sys\npath = sys.argv[1]\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nsession = obj.setdefault('session', {})\nsession['dmScope'] = session.get('dmScope', 'per-channel-peer')\nsession['resetTriggers'] = ['/new', '/reset']\nsession['reset'] = {\n 'mode': 'idle',\n 'idleMinutes': 10080\n}\nsession['resetByType'] = {\n 'direct': {'mode': 'idle', 'idleMinutes': 10080},\n 'thread': {'mode': 'idle', 'idleMinutes': 1440},\n 'group': {'mode': 'idle', 'idleMinutes': 120}\n}\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(obj, f, ensure_ascii=False, indent=2)\n f.write('\\n')\nPY\n\t}\n\n\tsync_openclaw_api_models() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 0\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('未检测到 API providers,跳过模型同步')\n raise SystemExit(0)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nchanged = False\nfatal_errors = []\nsummary = []\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\ndef prompt_delete_provider(name):\n prompt = f\"{name} /models 探测连续失败 3 次。是否删除该 API 供应商及其全部相关模型?[y/N]: \"\n try:\n ans = input(prompt).strip().lower()\n except EOFError:\n return False\n return ans in ('y', 'yes')\n\n\ndef rebind_defaults_before_delete(name):\n global changed\n\n replacement = None\n\n def get_replacement():\n nonlocal replacement\n if replacement is None:\n candidates = collect_available_refs(exclude_provider=name)\n replacement = candidates[0] if candidates else None\n return replacement\n\n primary_ref = get_primary_ref(defaults)\n if ref_provider(primary_ref) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: 默认主模型指向该 provider,但无可用替代模型,已中止删除')\n return False\n set_primary_ref(defaults, repl)\n changed = True\n summary.append(f'已切换默认主模型: {primary_ref} -> {repl}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: {fk} 指向该 provider,但无可用替代模型,已中止删除')\n return False\n defaults[fk] = repl\n changed = True\n summary.append(f'已切换 {fk}: {val} -> {repl}')\n\n return True\n\n\ndef delete_provider_and_refs(name):\n global changed\n\n if not rebind_defaults_before_delete(name):\n return False\n\n removed_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\n for r in removed_refs:\n defaults_models.pop(r, None)\n if removed_refs:\n changed = True\n\n if name in providers:\n providers.pop(name, None)\n changed = True\n\n summary.append(f'已删除 provider {name},并移除 defaults.models 下 {len(removed_refs)} 个模型引用')\n return True\n\n\ndef fetch_remote_models_with_retry(name, base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n data = json.loads(payload)\n return data, None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\nfor name, provider in list(providers.items()):\n if not isinstance(provider, dict):\n summary.append(f'跳过 {name}: provider 结构非法')\n continue\n\n api = provider.get('api', '')\n base_url = provider.get('baseUrl')\n api_key = provider.get('apiKey')\n model_list = provider.get('models', [])\n\n if not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n summary.append(f'跳过 {name}: 无 baseUrl/apiKey/models')\n continue\n\n if api not in SUPPORTED_APIS:\n summary.append(f'跳过 {name}: 不支持直接 /models 校验 (api={api})')\n continue\n\n data, err, attempts = fetch_remote_models_with_retry(name, base_url, api_key, retries=3)\n if err is not None:\n summary.append(f'警告 - {name}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n if prompt_delete_provider(name):\n deleted = delete_provider_and_refs(name)\n if deleted:\n summary.append(f'{name}: 用户已确认删除该 provider 及全部相关模型引用')\n else:\n summary.append(f'{name}: 用户未确认删除,保留现有 provider 配置')\n continue\n\n if attempts > 1:\n summary.append(f'{name}: /models 第 {attempts} 次重试后成功')\n\n if not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n summary.append(f'警告 - 跳过 {name}: /models 返回结构不可识别')\n continue\n\n remote_ids = []\n for item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\n remote_set = set(remote_ids)\n\n if not remote_set:\n fatal_errors.append(f'错误 - {name} 上游 /models 为空,无法为该 provider 提供兜底模型')\n continue\n\n local_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\n local_ids = [str(m['id']) for m in local_models]\n local_set = set(local_ids)\n\n template = None\n for m in local_models:\n template = copy.deepcopy(m)\n break\n if template is None:\n summary.append(f'警告 - 跳过 {name}: 本地 models 无有效模板模型')\n continue\n\n removed_ids = [mid for mid in local_ids if mid not in remote_set]\n added_ids = [mid for mid in remote_ids if mid not in local_set]\n\n kept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\n new_models = kept_models[:]\n\n for mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{name} / {mid}'\n new_models.append(nm)\n\n if not new_models:\n fatal_errors.append(f'错误 - {name} 同步后无可用模型,无法保障默认模型/回退模型兜底')\n continue\n\n expected_refs = {model_ref(name, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\n local_refs = {model_ref(name, mid) for mid in local_ids}\n\n first_ref = model_ref(name, str(new_models[0]['id']))\n\n primary_ref = get_primary_ref(defaults)\n if isinstance(primary_ref, str) and primary_ref in (local_refs - expected_refs):\n set_primary_ref(defaults, first_ref)\n changed = True\n summary.append(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in (local_refs - expected_refs):\n defaults[fk] = first_ref\n changed = True\n summary.append(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\n stale_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/') and r not in expected_refs]\n for r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\n for r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\n if removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\n summary.append(f'{name}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\n\nif fatal_errors:\n for line in summary:\n print(line)\n for err in fatal_errors:\n print(err)\n print('模型同步失败:存在 provider 同步后无可用模型,已中止写入')\n raise SystemExit(2)\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n for line in summary:\n print(line)\n print('OpenClaw API 模型一致性同步完成')\nelse:\n for line in summary:\n print(line)\n print('无需同步:配置已与上游 /models 保持一致')\nPY\n\t}\n\n\t_install_openclaw_core() {\n\t\tinstall_node_and_tools\n\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf ssh://git@github.com/\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf git@github.com:\n\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_prefix\n\t\tnpm_prefix=$(npm prefix -g 2>/dev/null)\n\t\t[[ -n \"$npm_prefix\" ]] && export PATH=\"$npm_prefix/bin:$PATH\"\n\t\topenclaw onboard --install-daemon\n\t\t_sed_i 's|\"profile\": \"messaging\"|\"profile\": \"full\"|g' ~/.openclaw/openclaw.json\n\t\tconfigure_openclaw_session_policy\n\t\tif ! is_macos; then\n\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null || true\n\t\tfi\n\t\tstart_gateway\n\t}\n\n\tinstall_moltbot() {\n\t\t_install_openclaw_core\n\t\tbreak_end\n\t}\n\n\t_cliproxy_dir() {\n\t\tif is_macos; then\n\t\t\techo \"$(brew --prefix 2>/dev/null || echo /usr/local)/etc\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi\"\n\t\tfi\n\t}\n\n\t_cliproxy_conf() {\n\t\tif is_macos; then\n\t\t\techo \"$(_cliproxy_dir)/cliproxyapi.conf\"\n\t\telse\n\t\t\techo \"$(_cliproxy_dir)/config.yaml\"\n\t\tfi\n\t}\n\n\t_cliproxy_bin() {\n\t\tif is_macos; then\n\t\t\tcommand -v cliproxyapi 2>/dev/null || echo \"cliproxyapi\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi/cli-proxy-api\"\n\t\tfi\n\t}\n\n\t_cliproxy_running() {\n\t\tif is_macos; then\n\t\t\tpgrep -f \"cliproxyapi\" >/dev/null 2>&1\n\t\telse\n\t\t\tpgrep -f \"cli-proxy-api\" >/dev/null 2>&1\n\t\tfi\n\t}\n\n\t_cliproxy_start_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services start cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tsystemctl --user enable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"systemctl --user start cliproxyapi.service 2>/dev/null; sleep 3\"\n\t\t\tif ! _cliproxy_running; then\n\t\t\t\tui_warn \"systemd 启动失败,改用后台直接运行...\"\n\t\t\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\t\t\t(cd \"$cliproxy_dir\" && nohup ./cli-proxy-api > /tmp/cliproxyapi.log 2>&1 &)\n\t\t\t\tsleep 3\n\t\t\tfi\n\t\tfi\n\t\tif _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已启动\"\n\t\t\treturn 0\n\t\telse\n\t\t\tui_err \"CLIProxyAPI 启动失败,请检查 $(_cliproxy_conf) 后手动启动\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\t_cliproxy_stop_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services stop cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbash -c \"systemctl --user stop cliproxyapi.service 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\tfi\n\t\tlocal _pat; _pat=$(is_macos && echo \"cliproxyapi\" || echo \"cli-proxy-api\")\n\t\tlocal pids; pids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\tif [[ -n \"$pids\" ]]; then\n\t\t\techo \"$pids\" | xargs kill 2>/dev/null || true\n\t\t\tsleep 2\n\t\t\tpids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\t\t[[ -n \"$pids\" ]] && echo \"$pids\" | xargs kill -9 2>/dev/null || true\n\t\tfi\n\t\tif ! _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已停止\"\n\t\telse\n\t\t\tui_err \"停止失败,仍有进程残留\"\n\t\tfi\n\t}\n\n\t_cliproxy_oauth_login() {\n\t\tlocal cliproxy_bin; cliproxy_bin=$(_cliproxy_bin)\n\t\tif ! command -v \"$cliproxy_bin\" &>/dev/null && [[ ! -x \"$cliproxy_bin\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 未安装:$cliproxy_bin\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_choice\n\t\tprovider_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' 选择要登录的 AI 提供商\\n ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"Claude (Anthropic)\" \\\n\t\t\t\"Gemini (Google)\" \\\n\t\t\t\"OpenAI / Codex\" \\\n\t\t\t\"Qwen (通义千问)\" \\\n\t\t\t\"iFlow\" \\\n\t\t\t\"取消\") || return 0\n\n\t\tlocal login_cmd=\"\" login_port=\"\"\n\t\tcase \"$provider_choice\" in\n\t\t\t\"Claude (Anthropic)\") login_cmd=\"--claude-login\"; login_port=\"54545\" ;;\n\t\t\t\"Gemini (Google)\") login_cmd=\"--login\"; login_port=\"8085\" ;;\n\t\t\t\"OpenAI / Codex\") login_cmd=\"--codex-login\"; login_port=\"1455\" ;;\n\t\t\t\"Qwen (通义千问)\") login_cmd=\"--qwen-login\"; login_port=\"\" ;;\n\t\t\t\"iFlow\") login_cmd=\"--iflow-login\"; login_port=\"11451\" ;;\n\t\t\t\"取消\"|*) return 0 ;;\n\t\tesac\n\n\t\techo\n\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\tif [[ -n \"$login_port\" ]]; then\n\t\t\tif is_macos; then\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 本地浏览器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 浏览器将自动打开,完成授权后自动返回,请稍候...\"\n\t\t\t\techo\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 服务器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 1. 脚本后台启动 cli-proxy-api,并打印 OAuth 授权链接\" \\\n\t\t\t\t\t\" 2. 在本地浏览器打开该链接,完成账号授权\" \\\n\t\t\t\t\t\" 3. 授权后浏览器跳转到 localhost:${login_port},页面报错属正常\" \\\n\t\t\t\t\t\" 4. 复制地址栏完整 URL,粘贴到下方提示符,脚本自动完成握手\"\n\t\t\t\techo\n\t\t\t\tgum style --foreground 208 \\\n\t\t\t\t\t\" ⚡ 无需端口转发 — 回调 URL 里已含 code,直接转发给本机处理\"\n\t\t\t\techo\n\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser) &\n\t\t\t\tlocal cli_pid=$!\n\t\t\t\tsleep 2\n\n\t\t\t\techo\n\t\t\t\tlocal callback_url\n\t\t\t\tcallback_url=$(gum input \\\n\t\t\t\t\t--placeholder \"http://localhost:${login_port}/oauth2callback?code=...\" \\\n\t\t\t\t\t--prompt \" ❯ \" \\\n\t\t\t\t\t--width 120 \\\n\t\t\t\t\t--char-limit 2000) || true\n\n\t\t\t\tif [[ -n \"$callback_url\" ]]; then\n\t\t\t\t\tui_info \"正在向本机发送回调...\"\n\t\t\t\t\tcurl -sf \"$callback_url\" > /dev/null 2>&1 || true\n\t\t\t\t\tsleep 1\n\t\t\t\tfi\n\t\t\t\twait \"$cli_pid\" 2>/dev/null || true\n\t\t\tfi\n\t\telse\n\t\t\tui_info \"Qwen 使用 device flow,无需粘贴回调\"\n\t\t\techo\n\t\t\tgum style --foreground 240 \"将打印授权码 + URL,在任意浏览器中打开并输入授权码即可\"\n\t\t\techo\n\t\t\tif is_macos; then\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser)\n\t\t\tfi\n\t\tfi\n\t}\n\n\tcliproxyapi_manage_menu() {\n\t\tlocal config_file; config_file=$(_cliproxy_conf)\n\n\t\twhile true; do\n\t\t\tclear\n\n\t\t\tlocal cp_installed cp_running cp_version cp_port cp_keys\n\t\t\tif is_macos; then\n\t\t\t\tcp_version=$(brew info cliproxyapi 2>/dev/null | awk 'NR==1{print $4}')\n\t\t\telse\n\t\t\t\tcp_version=$(cat \"$(_cliproxy_dir)/version.txt\" 2>/dev/null || echo \"\")\n\t\t\tfi\n\t\t\tif [[ -n \"$cp_version\" ]]; then\n\t\t\t\tcp_installed=\"\\033[38;5;46m◉ INSTALLED v${cp_version}\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_installed=\"\\033[90m○ NOT INSTALLED\\033[0m\"\n\t\t\tfi\n\t\t\tif _cliproxy_running; then\n\t\t\t\tcp_running=\"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_running=\"\\033[90m■ STOPPED\\033[0m\"\n\t\t\tfi\n\t\t\tcp_port=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\t\t\tcp_port=\"${cp_port:-8317}\"\n\t\t\tcp_keys=$(awk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{n++} END{print n+0}' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 51 \\\n\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t--padding \"1 4\" --align center \\\n\t\t\t\t\"C L I P R O X Y A P I\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ AI OAuth Proxy Manager ]\"\n\t\t\techo -e \" $cp_installed $cp_running\"\n\t\t\techo -e \" \\033[90m端口 :${cp_port} API Keys: ${cp_keys}\\033[0m\"\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' ─── CLIProxyAPI ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"启动\" \\\n\t\t\t\t\"停止\" \\\n\t\t\t\t\"重启\" \\\n\t\t\t\t\"查看日志\" \\\n\t\t\t\t\"账号授权登录\" \\\n\t\t\t\t\"生成并添加 API Key\" \\\n\t\t\t\t\"查看 API Keys\" \\\n\t\t\t\t\"编辑配置文件\" \\\n\t\t\t\t\"更新\" \\\n\t\t\t\t\"卸载\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"启动\")\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"停止\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"重启\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tsleep 1\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看日志\")\n\t\t\t\t\techo\n\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\tlocal brew_log\n\t\t\t\t\t\tbrew_log=\"$(brew --prefix 2>/dev/null)/var/log/cliproxyapi.log\"\n\t\t\t\t\t\tif [[ -f \"$brew_log\" ]]; then\n\t\t\t\t\t\t\ttail -80 \"$brew_log\"\n\t\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\telif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\t\t\tjournalctl --user -u cliproxyapi.service --no-pager -n 80\n\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"账号授权登录\")\n\t\t\t\t\t_cliproxy_oauth_login\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"生成并添加 API Key\")\n\t\t\t\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\t\tlocal new_key\n\t\t\t\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\t\t\t\techo\n\t\t\t\t\tgum style --bold --foreground 46 \" ◉ 已生成新 API Key:\"\n\t\t\t\t\tgum style --bold --foreground 51 \" $new_key\"\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 将此 Key 自动写入 config.yaml?\"; then\n\t\t\t\t\t\tpython3 - \"$config_file\" \"$new_key\" <<'PY'\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n)', r'\\1 - \"' + key + '\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nprint(\"写入成功\")\nPY\n\t\t\t\t\t\tui_ok \"API Key 已写入配置\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看 API Keys\")\n\t\t\t\t\techo\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tgum style --foreground 240 \" config.yaml 中的 API Keys:\"\n\t\t\t\t\t\techo\n\t\t\t\t\t\tawk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{print \" \" $0}' \\\n\t\t\t\t\t\t\t\"$config_file\"\n\t\t\t\t\t\techo\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到配置文件:$config_file\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"编辑配置文件\")\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tnano \"$config_file\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\tfi\n\t\t\t\t\t;;\n\t\t\t\t\"更新\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 确认更新 CLIProxyAPI 到最新版本?\"; then\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew upgrade cliproxyapi && ui_ok \"更新完成\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tcurl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer \\\n\t\t\t\t\t\t\t\t| bash -s -- upgrade\n\t\t\t\t\t\t\tui_ok \"更新完成\"\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"卸载\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" ⚡ 确认卸载 CLIProxyAPI?\"; then\n\t\t\t\t\t\t_cliproxy_stop_service 2>/dev/null || true\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew uninstall cliproxyapi 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$HOME/.cli-proxy-api\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tsystemctl --user disable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\t\t\t\t\trm -f \"$HOME/.config/systemd/user/cliproxyapi.service\" 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$(_cliproxy_dir)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\t\tui_ok \"CLIProxyAPI 已卸载\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tbeginner_mode_install() {\n\t\tif is_macos; then\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ API Key 配置 ]\"\n\t\telse\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ CLIProxyAPI ⟶ AI Auth ⟶ Auto Config ]\"\n\t\tfi\n\t\techo\n\n\t\tui_step \"第 1 步:安装 OpenClaw\"\n\t\t_install_openclaw_core\n\n\t\tui_step \"第 2 步:安装 CLIProxyAPI\"\n\t\tif is_macos; then\n\t\t\tif ! gum spin --spinner globe --title \"正在安装 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbrew install cliproxyapi; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败(brew install cliproxyapi)\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\telse\n\t\t\tif ! curl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer | bash; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\n\t\tlocal cliproxy_config; cliproxy_config=$(_cliproxy_conf)\n\t\tif [[ ! -f \"$cliproxy_config\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 安装后未找到配置文件: $cliproxy_config\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\t\tui_ok \"CLIProxyAPI 安装完成\"\n\n\t\tui_step \"第 3 步:登录 AI 账号\"\n\t\tui_info \"通过 OAuth 授权即可免费使用,无需付费 API Key\"\n\t\techo\n\t\t_cliproxy_oauth_login\n\n\t\tui_step \"第 4 步:生成 API Key 并启动服务\"\n\t\tlocal new_key\n\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\tpython3 - \"$cliproxy_config\" \"$new_key\" <<'PY' 2>/dev/null\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n(?:\\s*-\\s*\"[^\"]*\"\\s*\\n)*)', lambda m: m.group(0) + f' - \"{key}\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nPY\n\t\tif ! _cliproxy_start_service; then\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_step \"第 5 步:配置 OpenClaw API\"\n\n\t\tlocal api_key port base_url\n\t\tapi_key=$(awk '\n\t\t\t/^api-keys:/ { in_keys=1; next }\n\t\t\tin_keys && /^[^ \\t]/ { exit }\n\t\t\tin_keys && /\"sk-/ { match($0, /sk-[^\"]+/); print substr($0, RSTART, RLENGTH); exit }\n\t\t' \"$cliproxy_config\" 2>/dev/null)\n\n\t\tport=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \"$cliproxy_config\" 2>/dev/null)\n\t\tport=\"${port:-8317}\"\n\t\tbase_url=\"http://localhost:${port}/v1\"\n\n\t\tif [[ -z \"$api_key\" ]]; then\n\t\t\tui_err \"无法从 $cliproxy_config 读取 API Key,请手动在「CLIProxyAPI 管理」中添加\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_info \"地址:$base_url\"\n\t\tui_info \"Key:${api_key:0:12}****\"\n\t\techo\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"cliproxy\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tstart_gateway\n\n\t\t\tui_step \"第 6 步:选择默认模型\"\n\t\t\tui_info \"请从可用模型中选择一个作为默认,选完后网关自动重启\"\n\t\t\techo\n\t\t\tchange_model\n\n\t\t\techo\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 46 \\\n\t\t\t\t--border double --border-foreground 46 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◉ INSTALL COMPLETE ◉\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw 已自动接入 CLIProxyAPI,可直接开始使用 ]\" \\\n\t\t\t\t\"[ 更多提供商:cd ~/cliproxyapi && ./cli-proxy-api --help ]\"\n\t\tfi\n\n\t\techo\n\t\tif gum confirm \" 现在去对接机器人?\"; then\n\t\t\tchange_tg_bot_code\n\t\telse\n\t\t\tbreak_end\n\t\tfi\n\t}\n\n\tstart_bot() {\n\t\tstart_gateway\n\t\tbreak_end\n\t}\n\n\tstop_bot() {\n\t\ttmux kill-session -t gateway > /dev/null 2>&1\n\t\tgum spin --spinner pulse --title \"正在停止网关...\" -- openclaw gateway stop\n\t\tbreak_end\n\t}\n\n\tview_logs() {\n\t\topenclaw status\n\t\topenclaw gateway status\n\t\topenclaw logs\n\t\tbreak_end\n\t}\n\n\tadd-all-models-from-provider() {\n\t\tlocal provider_name=\"$1\"\n\t\tlocal base_url=\"$2\"\n\t\tlocal api_key=\"$3\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tlocal models_tmpfile\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取 $provider_name 的模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\n\t\tlocal models_json\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tif [[ -z \"$models_json\" ]]; then\n\t\t\techo \"错误:无法获取模型列表\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_ids\n\t\tmodel_ids=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n for m in models:\n if isinstance(m, dict) and \"id\" in m:\n print(m[\"id\"])\nexcept Exception:\n pass\n')\n\n\t\tif [[ -z \"$model_ids\" ]]; then\n\t\t\techo \"错误:未找到任何模型\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_count\n\t\tmodel_count=$(echo \"$model_ids\" | wc -l | tr -d ' ')\n\t\techo \"发现 $model_count 个模型\"\n\n\t\tlocal models_array=\"[\"\n\t\tlocal first=true\n\n\t\twhile read -r model_id; do\n\t\t\t[[ $first == false ]] && models_array+=\",\"\n\t\t\tfirst=false\n\n\t\t\tlocal context_window=1048576\n\t\t\tlocal max_tokens=128000\n\t\t\tlocal input_cost=0.15\n\t\t\tlocal output_cost=0.60\n\n\t\t\tcase \"$model_id\" in\n\t\t\t\t*opus*|*pro*|*preview*|*thinking*|*sonnet*)\n\t\t\t\t\tinput_cost=2.00\n\t\t\t\t\toutput_cost=12.00\n\t\t\t\t\t;;\n\t\t\t\t*gpt-5*|*codex*)\n\t\t\t\t\tinput_cost=1.25\n\t\t\t\t\toutput_cost=10.00\n\t\t\t\t\t;;\n\t\t\t\t*flash*|*lite*|*haiku*|*mini*|*nano*)\n\t\t\t\t\tinput_cost=0.10\n\t\t\t\t\toutput_cost=0.40\n\t\t\t\t\t;;\n\t\t\tesac\n\n\t\t\tmodels_array+=$(cat <<EOF\n{\n\t\"id\": \"$model_id\",\n\t\"name\": \"$provider_name / $model_id\",\n\t\"input\": [\"text\", \"image\"],\n\t\"contextWindow\": $context_window,\n\t\"maxTokens\": $max_tokens,\n\t\"cost\": {\n\t\t\"input\": $input_cost,\n\t\t\"output\": $output_cost,\n\t\t\"cacheRead\": 0,\n\t\t\"cacheWrite\": 0\n\t}\n}\nEOF\n)\n\t\tdone <<< \"$model_ids\"\n\n\t\tmodels_array+=\"]\"\n\n\t\t[[ -f \"$config_file\" ]] && cp \"$config_file\" \"${config_file}.bak.$(date +%s)\"\n\n\t\tjq --arg prov \"$provider_name\" \\\n\t\t --arg url \"$base_url\" \\\n\t\t --arg key \"$api_key\" \\\n\t\t --argjson models \"$models_array\" \\\n\t\t'\n\t\t.models |= (\n\t\t\t(. // { mode: \"merge\", providers: {} })\n\t\t\t| .mode = \"merge\"\n\t\t\t| .providers[$prov] = {\n\t\t\t\tbaseUrl: $url,\n\t\t\t\tapiKey: $key,\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tmodels: $models\n\t\t\t}\n\t\t)\n\t\t| .agents |= (. // {})\n\t\t| .agents.defaults |= (. // {})\n\t\t| .agents.defaults.models |= (\n\t\t\t(if type == \"object\" then .\n\t\t\t elif type == \"array\" then reduce .[] as $m ({}; if ($m|type) == \"string\" then .[$m] = {} else . end)\n\t\t\t else {}\n\t\t\t end) as $existing\n\t\t\t| reduce ($models[]? | .id? // empty | tostring) as $mid (\n\t\t\t\t$existing;\n\t\t\t\tif ($mid | length) > 0 then\n\t\t\t\t\t.[\"\\($prov)/\\($mid)\"] //= {}\n\t\t\t\telse\n\t\t\t\t\t.\n\t\t\t\tend\n\t\t\t)\n\t\t)\n\t\t' \"$config_file\" > \"${config_file}.tmp\" && mv \"${config_file}.tmp\" \"$config_file\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\techo \"成功添加 $model_count 个模型到 $provider_name\"\n\t\t\techo \"模型引用格式: $provider_name/<model-id>\"\n\t\t\treturn 0\n\t\telse\n\t\t\techo \"错误:配置注入失败\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\tadd-openclaw-provider-interactive() {\n\t\tlocal provider_name base_url api_key\n\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"Provider 名称 (如: deepseek)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\t[[ -z \"$provider_name\" ]] && return 1\n\n\t\tbase_url=$(gum input \\\n\t\t\t--placeholder \"Base URL (如: https://api.xxx.com/v1)\" \\\n\t\t\t--prompt \"Base URL > \")\n\t\t[[ -z \"$base_url\" ]] && return 1\n\t\tbase_url=\"${base_url%/}\"\n\n\t\tapi_key=$(gum input --password \\\n\t\t\t--placeholder \"API Key\" \\\n\t\t\t--prompt \"API Key > \")\n\t\t[[ -z \"$api_key\" ]] && return 1\n\n\t\tlocal models_tmpfile models_json available_models model_count\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tlocal default_model=\"\"\n\t\tif [[ -n \"$models_json\" ]]; then\n\t\t\tavailable_models=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n ids = sorted(m[\"id\"] for m in models if isinstance(m, dict) and \"id\" in m)\n print(\"\\n\".join(ids))\nexcept Exception:\n pass\n')\n\t\t\tmodel_count=$(echo \"$available_models\" | wc -l | tr -d ' ')\n\t\tfi\n\n\t\tif [[ -n \"$available_models\" ]]; then\n\t\t\tdefault_model=$(echo \"$available_models\" | fzf \\\n\t\t\t\t--prompt=\" ❯ \" \\\n\t\t\t\t--header=\" 发现 $model_count 个模型 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 选第一个\" \\\n\t\t\t\t--header-first \\\n\t\t\t\t--height=15 \\\n\t\t\t\t--layout=reverse \\\n\t\t\t\t--border=double \\\n\t\t\t\t--border-label=\" ◈ DEFAULT MODEL \" \\\n\t\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || default_model=$(echo \"$available_models\" | head -1)\n\t\t\t[[ -z \"$default_model\" ]] && default_model=$(echo \"$available_models\" | head -1)\n\t\t\tdefault_model=$(awk '{print $1}' <<< \"$default_model\")\n\t\tfi\n\n\t\techo\n\t\tgum style \\\n\t\t\t--border normal --border-foreground 99 \\\n\t\t\t--padding \"0 2\" \\\n\t\t\t\"Provider : $provider_name\" \\\n\t\t\t\"Base URL : $base_url\" \\\n\t\t\t\"API Key : ${api_key:0:8}****\" \\\n\t\t\t\"默认模型 : $default_model\" \\\n\t\t\t\"模型总数 : $model_count\"\n\t\techo\n\n\t\tgum confirm \"确认添加所有 $model_count 个模型?\" || { echo \"已取消\"; return 1; }\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"$provider_name\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tgum spin --spinner minidot --title \"设置默认模型并重启网关...\" -- \\\n\t\t\t\topenclaw models set \"$provider_name/$default_model\"\n\t\t\tstart_gateway\n\t\t\techo \"完成:所有 $model_count 个模型已加载\"\n\t\tfi\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_list() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\twhile IFS=$'\\t' read -r rec_type idx name base_url model_count latency_txt latency_level; do\n\t\t\tcase \"$rec_type\" in\n\t\t\t\tMSG)\n\t\t\t\t\techo \"$idx\"\n\t\t\t\t\t;;\n\t\t\t\tROW)\n\t\t\t\t\tlocal latency_color=\"$gl_bai\"\n\t\t\t\t\tcase \"$latency_level\" in\n\t\t\t\t\t\tlow) latency_color=\"$gl_lv\" ;;\n\t\t\t\t\t\tmedium) latency_color=\"$gl_huang\" ;;\n\t\t\t\t\t\thigh|unavailable) latency_color=\"$gl_hong\" ;;\n\t\t\t\t\t\tunchecked) latency_color=\"$gl_bai\" ;;\n\t\t\t\t\tesac\n\n\t\t\t\t\tprintf '%b\\n' \"[$idx] ${name} | API: ${base_url} | 模型数量: ${gl_huang}${model_count}${gl_bai} | 延迟/状态: ${latency_color}${latency_txt}${gl_bai}\"\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tlocal _py_list; _py_list=$(mktemp /tmp/oc_list_XXXXXX.py)\n\t\tcat > \"$_py_list\" << 'PYEOF'\nimport json, sys, time, urllib.request\n\npath = sys.argv[1]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\ndef ping_models(base_url, api_key):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': 'Bearer ' + api_key,\n 'User-Agent': 'OpenClaw-API-Manage/1.0',\n },\n )\n start = time.perf_counter()\n with urllib.request.urlopen(req, timeout=4) as resp:\n resp.read(2048)\n return int((time.perf_counter() - start) * 1000)\n\ndef classify_latency(latency):\n if latency == '不可用':\n return '不可用', 'unavailable'\n if latency == '未检测':\n return '未检测', 'unchecked'\n if isinstance(latency, int):\n level = 'low' if latency <= 800 else ('medium' if latency <= 2000 else 'high')\n return str(latency) + 'ms', level\n return str(latency), 'unchecked'\n\ntry:\n with open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\nexcept FileNotFoundError:\n print('MSG\\t未找到 openclaw.json,请先完成安装/初始化。')\n raise SystemExit(0)\nexcept Exception as e:\n print('MSG\\t读取配置失败: ' + type(e).__name__ + ': ' + str(e))\n raise SystemExit(0)\n\nproviders = ((obj.get('models') or {}).get('providers') or {})\nif not isinstance(providers, dict) or not providers:\n print('MSG\\t当前未配置任何 API provider。')\n raise SystemExit(0)\n\nprint('MSG\\t--- 已配置 API 列表 ---')\n\nfor idx, name in enumerate(sorted(providers.keys()), start=1):\n provider = providers.get(name)\n if not isinstance(provider, dict):\n base_url = '-'; model_count = 0; latency_raw = '不可用'\n else:\n base_url = provider.get('baseUrl') or provider.get('url') or provider.get('endpoint') or '-'\n models = provider.get('models') if isinstance(provider.get('models'), list) else []\n model_count = sum(1 for m in models if isinstance(m, dict) and m.get('id'))\n api = provider.get('api', '')\n api_key = provider.get('apiKey')\n latency_raw = '未检测'\n if api in SUPPORTED_APIS and isinstance(base_url, str) and base_url != '-' and isinstance(api_key, str) and api_key:\n try:\n latency_raw = ping_models(base_url, api_key)\n except Exception:\n latency_raw = '不可用'\n elif api in SUPPORTED_APIS:\n latency_raw = '不可用'\n\n latency_text, latency_level = classify_latency(latency_raw)\n print('ROW\\t' + '\\t'.join([str(idx), str(name), str(base_url), str(model_count), str(latency_text), str(latency_level)]))\nPYEOF\n\t\tdone < <(python3 \"$_py_list\" \"$config_file\")\n\t\trm -f \"$_py_list\"\n\t}\n\n\tsync-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要同步的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tinstall jq curl >/dev/null 2>&1\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY2'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\ntarget = sys.argv[2]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('错误:未检测到 API providers,无法同步')\n raise SystemExit(2)\n\nprovider = providers.get(target)\nif not isinstance(provider, dict):\n print(f'错误:未找到 provider: {target}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef fetch_remote_models_with_retry(base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n return json.loads(payload), None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\napi = provider.get('api', '')\nbase_url = provider.get('baseUrl')\napi_key = provider.get('apiKey')\nmodel_list = provider.get('models', [])\n\nif not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n print(f'错误:provider {target} 缺少 baseUrl/apiKey/models,无法执行同步')\n raise SystemExit(3)\n\nif api not in SUPPORTED_APIS:\n print(f'错误:provider {target} 当前 api={api},不支持直接 /models 同步')\n raise SystemExit(3)\n\ndata, err, attempts = fetch_remote_models_with_retry(base_url, api_key, retries=3)\nif err is not None:\n print(f'错误:{target}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n raise SystemExit(4)\n\nif not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n print(f'错误:{target}: /models 返回结构不可识别')\n raise SystemExit(4)\n\nremote_ids = []\nfor item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\nremote_set = set(remote_ids)\nif not remote_set:\n print(f'错误:{target}: 上游 /models 为空,已中止同步')\n raise SystemExit(5)\n\nlocal_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\nlocal_ids = [str(m['id']) for m in local_models]\nlocal_set = set(local_ids)\n\ntemplate = copy.deepcopy(local_models[0]) if local_models else None\nif template is None:\n print(f'错误:{target}: 本地 models 无有效模板模型,无法补全新增模型')\n raise SystemExit(3)\n\nremoved_ids = [mid for mid in local_ids if mid not in remote_set]\nadded_ids = [mid for mid in remote_ids if mid not in local_set]\n\nkept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\nnew_models = kept_models[:]\nfor mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{target} / {mid}'\n new_models.append(nm)\n\nif not new_models:\n print(f'错误:{target}: 同步后无可用模型,已中止写入')\n raise SystemExit(5)\n\nexpected_refs = {model_ref(target, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\nlocal_refs = {model_ref(target, mid) for mid in local_ids}\nremoved_refs = local_refs - expected_refs\nfirst_ref = model_ref(target, str(new_models[0]['id']))\n\nchanged = False\nprimary_ref = get_primary_ref(defaults)\nif isinstance(primary_ref, str) and primary_ref in removed_refs:\n set_primary_ref(defaults, first_ref)\n changed = True\n print(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in removed_refs:\n defaults[fk] = first_ref\n changed = True\n print(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\nstale_refs = [r for r in list(defaults_models.keys()) if r.startswith(target + '/') and r not in expected_refs]\nfor r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\nfor r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\nif removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'{target}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\nif changed:\n print('指定 provider 模型一致性同步完成')\nelse:\n print('无需同步:该 provider 配置已与上游 /models 保持一致')\nPY2\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"同步完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在或未配置\" ;;\n\t\t\t3) echo \"错误:provider 配置不完整或类型不支持\" ;;\n\t\t\t4) echo \"错误:上游 /models 请求失败\" ;;\n\t\t\t5) echo \"错误:上游模型为空或同步后无可用模型\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\tdelete-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要删除的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY'\nimport copy\nimport json\nimport sys\n\npath = sys.argv[1]\nname = sys.argv[2]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or name not in providers:\n print(f'错误:未找到 provider: {name}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\nreplacement_candidates = collect_available_refs(exclude_provider=name)\nreplacement = replacement_candidates[0] if replacement_candidates else None\n\nprimary_ref = get_primary_ref(defaults)\nif ref_provider(primary_ref) == name:\n if not replacement:\n print('错误:删除中止:默认主模型指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n set_primary_ref(defaults, replacement)\n print(f'默认主模型切换: {primary_ref} -> {replacement}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n if not replacement:\n print(f'错误:删除中止:{fk} 指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n defaults[fk] = replacement\n print(f'{fk} 切换: {val} -> {replacement}')\n\nremoved_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\nfor r in removed_refs:\n defaults_models.pop(r, None)\n\nproviders.pop(name, None)\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'已删除 provider: {name}')\nprint(f'已清理 defaults.models 中 {len(removed_refs)} 个关联模型引用')\nPY\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"删除完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在\" ;;\n\t\t\t3) echo \"错误:无可用替代模型,已保持原配置\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"API 管理\"\n\t\t\topenclaw_api_manage_list\n\t\t\techo\n\n\t\t\tlocal api_choice\n\t\t\tapi_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加 API\" \\\n\t\t\t\t\"同步 API 供应商模型列表\" \\\n\t\t\t\t\"删除 API\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$api_choice\" in\n\t\t\t\t\"添加 API\") add-openclaw-provider-interactive ;;\n\t\t\t\t\"同步 API 供应商模型列表\") sync-openclaw-provider-interactive ;;\n\t\t\t\t\"删除 API\") delete-openclaw-provider-interactive ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tchange_model() {\n\t\tlocal all_models model\n\t\tall_models=$(openclaw models list --all 2>/dev/null | grep \"configured\")\n\n\t\tif [[ -z \"$all_models\" ]]; then\n\t\t\tui_err \"无法获取可用模型列表(请先添加 API 提供商)\"\n\t\t\tbreak_end\n\t\t\treturn\n\t\tfi\n\n\t\tmodel=$(echo \"$all_models\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 当前: $(openclaw models list 2>/dev/null | awk '{print $1}' | head -1) │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=20 \\\n\t\t\t--layout=reverse \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ◈ MODEL SELECT \" \\\n\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || return\n\n\t\t[[ -z \"$model\" ]] && return\n\t\tmodel=$(awk '{print $1}' <<< \"$model\")\n\n\t\tgum spin --spinner minidot --title \"正在切换模型...\" -- openclaw models set \"$model\"\n\t\tstart_gateway\n\t\tui_ok \"已切换至:$model\"\n\t\tbreak_end\n\t}\n\n\tresolve_openclaw_plugin_id() {\n\t\tlocal raw_input=\"$1\"\n\t\tlocal plugin_id=\"$raw_input\"\n\n\t\tplugin_id=\"${plugin_id#@openclaw/}\"\n\t\tif [[ \"$plugin_id\" == @*/* ]]; then\n\t\t\tplugin_id=\"${plugin_id##*/}\"\n\t\tfi\n\t\tplugin_id=\"${plugin_id%%@*}\"\n\t\techo \"$plugin_id\"\n\t}\n\n\tsync_openclaw_plugin_allowlist() {\n\t\tlocal plugin_id=\"$1\"\n\t\t[[ -z \"$plugin_id\" ]] && return 1\n\n\t\tlocal home_config=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal root_config=\"/root/.openclaw/openclaw.json\"\n\t\tlocal config_file=\"$home_config\"\n\t\tif [[ ! -f \"$home_config\" && -f \"$root_config\" ]]; then\n\t\t\tconfig_file=\"$root_config\"\n\t\tfi\n\n\t\tmkdir -p \"$(dirname \"$config_file\")\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo '{}' > \"$config_file\"\n\t\tfi\n\n\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\tlocal tmp_json\n\t\t\ttmp_json=$(mktemp)\n\t\t\tif jq --arg pid \"$plugin_id\" '\n\t\t\t\t.plugins = (if (.plugins | type) == \"object\" then .plugins else {} end)\n\t\t\t\t| .plugins.allow = (if (.plugins.allow | type) == \"array\" then .plugins.allow else [] end)\n\t\t\t\t| if (.plugins.allow | index($pid)) == null then .plugins.allow += [$pid] else . end\n\t\t\t' \"$config_file\" > \"$tmp_json\" 2>/dev/null && mv \"$tmp_json\" \"$config_file\"; then\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\t\trm -f \"$tmp_json\"\n\t\tfi\n\n\t\tif command -v python3 >/dev/null 2>&1; then\n\t\t\tif python3 - \"$config_file\" \"$plugin_id\" <<'PYTHON_EOF'\nimport json\nimport sys\nfrom pathlib import Path\n\nconfig_file = Path(sys.argv[1])\nplugin_id = sys.argv[2]\n\ntry:\n data = json.loads(config_file.read_text(encoding='utf-8')) if config_file.exists() else {}\n if not isinstance(data, dict):\n data = {}\nexcept Exception:\n data = {}\n\nplugins = data.get('plugins')\nif not isinstance(plugins, dict):\n plugins = {}\n\na = plugins.get('allow')\nif not isinstance(a, list):\n a = []\n\nif plugin_id not in a:\n a.append(plugin_id)\n\nplugins['allow'] = a\ndata['plugins'] = plugins\nconfig_file.write_text(json.dumps(data, ensure_ascii=False, indent=2) + \"\\n\", encoding='utf-8')\nPYTHON_EOF\n\t\t\tthen\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\tfi\n\n\t\techo \"警告:已安装插件,但同步 plugins.allow 失败,请手动检查: $config_file\"\n\t\treturn 1\n\t}\n\n\tinstall_plugin() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"插件管理\"\n\t\t\tgum style --foreground 240 \"当前已安装插件:\"\n\t\t\topenclaw plugins list\n\t\t\techo\n\t\t\tgum style --bold --foreground 99 \"常用插件参考:\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"feishu\" \"飞书/Lark 集成\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"telegram\" \"Telegram 机器人\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"slack\" \"Slack 企业通讯\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"msteams\" \"Microsoft Teams\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"discord\" \"Discord 社区管理\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"whatsapp\" \"WhatsApp 自动化\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"memory-core\" \"基础记忆 (文件检索)\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"memory-lancedb\" \"增强记忆 (向量数据库)\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"copilot-proxy\" \"Copilot 接口转发\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"lobster\" \"审批流 (带人工确认)\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"voice-call\" \"语音通话能力\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"nostr\" \"加密隐私聊天\"\n\t\t\techo\n\n\t\t\tlocal raw_input\n\t\t\traw_input=$(gum input \\\n\t\t\t\t--placeholder \"插件 ID (留空退出)\" \\\n\t\t\t\t--prompt \"Plugin > \")\n\t\t\t[[ -z \"$raw_input\" ]] && break\n\n\t\t\tlocal plugin_id plugin_full\n\t\t\tplugin_id=$(resolve_openclaw_plugin_id \"$raw_input\")\n\t\t\tplugin_full=\"$raw_input\"\n\n\t\t\tlocal plugin_list\n\t\t\tplugin_list=$(openclaw plugins list 2>/dev/null)\n\n\t\t\tif echo \"$plugin_list\" | grep -qw \"$plugin_id\" && echo \"$plugin_list\" | grep \"$plugin_id\" | grep -q \"disabled\"; then\n\t\t\t\tui_info \"插件 [$plugin_id] 已预装,正在激活...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\" && ui_ok \"激活成功\" || ui_err \"激活失败\"\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/extensions/$plugin_id\" ]]; then\n\t\t\t\tui_info \"发现系统内置插件,尝试直接启用...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\telse\n\t\t\t\tui_info \"本地未发现,尝试下载安装...\"\n\t\t\t\trm -rf \"/root/.openclaw/extensions/$plugin_id\"\n\n\t\t\t\tif openclaw plugins install \"$plugin_full\"; then\n\t\t\t\t\tui_ok \"下载成功,正在启用...\"\n\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\telse\n\t\t\t\t\tui_warn \"官方渠道下载失败,尝试 npm 备选方案...\"\n\t\t\t\t\tif npm install -g \"$plugin_full\" --unsafe-perm; then\n\t\t\t\t\t\tui_ok \"npm 安装成功,尝试启用...\"\n\t\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"无法获取该插件,请检查 ID 是否正确或网络是否可用\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\tfi\n\t\t\tfi\n\n\t\t\tsync_openclaw_plugin_allowlist \"$plugin_id\"\n\t\t\tstart_gateway\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\tinstall_skill() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"技能管理\"\n\t\t\tgum style --foreground 240 \"当前已安装技能:\"\n\t\t\topenclaw skills list\n\t\t\techo\n\t\t\tgum style --bold --foreground 99 \"推荐技能参考:\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"github\" \"管理 GitHub Issues/PR/CI\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"notion\" \"操作 Notion 页面与数据库\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"apple-notes\" \"macOS 原生笔记管理\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"apple-reminders\" \"macOS 提醒事项管理\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"1password\" \"自动化读取 1Password 密钥\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"gog\" \"Google Workspace 全能助手\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"things-mac\" \"Things 3 任务管理\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"bluebubbles\" \"通过 BlueBubbles 收发 iMessage\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"himalaya\" \"终端邮件管理 (IMAP/SMTP)\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"summarize\" \"网页/播客/YouTube 内容总结\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"openai-whisper\" \"本地音频转文字\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"coding-agent\" \"运行 Claude Code/Codex 等编程助手\"\n\t\t\techo\n\n\t\t\tlocal skill_name\n\t\t\tskill_name=$(gum input \\\n\t\t\t\t--placeholder \"技能名称 (留空退出)\" \\\n\t\t\t\t--prompt \"Skill > \")\n\t\t\t[[ -z \"$skill_name\" ]] && break\n\n\t\t\tlocal skill_found=false\n\t\t\tif [[ -d \"${HOME}/.openclaw/workspace/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在用户目录安装\"\n\t\t\t\tskill_found=true\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在系统目录安装\"\n\t\t\t\tskill_found=true\n\t\t\tfi\n\n\t\t\tif [[ \"$skill_found\" == true ]]; then\n\t\t\t\tgum confirm \"是否重新安装?\" || { break_end; continue; }\n\t\t\tfi\n\n\t\t\tgum spin --spinner globe --title \"正在安装技能 $skill_name...\" -- npx clawhub install \"$skill_name\"\n\t\t\tif [[ $? -eq 0 ]]; then\n\t\t\t\techo \"技能 $skill_name 安装成功\"\n\t\t\t\tstart_gateway\n\t\t\telse\n\t\t\t\techo \"错误:安装失败,请检查技能名称是否正确\"\n\t\t\tfi\n\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\topenclaw_json_get_bool() {\n\t\tlocal expr=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r \"$expr\" \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_channel_has_cfg() {\n\t\tlocal channel=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r --arg c \"$channel\" '\n\t\t\t(.channels[$c] // null) as $v\n\t\t\t| if ($v | type) != \"object\" then\n\t\t\t\tfalse\n\t\t\t else\n\t\t\t\t([ $v\n\t\t\t\t | to_entries[]\n\t\t\t\t | select((.key == \"enabled\" or .key == \"dmPolicy\" or .key == \"groupPolicy\" or .key == \"streaming\") | not)\n\t\t\t\t | .value\n\t\t\t\t | select(. != null and . != \"\" and . != false)\n\t\t\t\t ] | length) > 0\n\t\t\t end\n\t\t' \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_dir_has_files() {\n\t\tlocal dir=\"$1\"\n\t\t[[ -d \"$dir\" ]] && find \"$dir\" -type f -print -quit 2>/dev/null | grep -q .\n\t}\n\n\topenclaw_plugin_local_installed() {\n\t\tlocal plugin=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ -s \"$config_file\" ]] && jq -e --arg p \"$plugin\" '.plugins.installs[$p]' \"$config_file\" >/dev/null 2>&1; then\n\t\t\treturn 0\n\t\tfi\n\t\t[[ -d \"${HOME}/.openclaw/extensions/${plugin}\" ]] || [[ -d \"/usr/lib/node_modules/openclaw/extensions/${plugin}\" ]]\n\t}\n\n\topenclaw_bot_status_text() {\n\t\tlocal enabled=\"$1\"\n\t\tlocal configured=\"$2\"\n\t\tlocal connected=\"$3\"\n\t\tlocal abnormal=\"$4\"\n\t\tif [[ \"$abnormal\" == \"true\" ]]; then\n\t\t\techo \"异常\"\n\t\telif [[ \"$enabled\" != \"true\" ]]; then\n\t\t\techo \"未启用\"\n\t\telif [[ \"$connected\" == \"true\" ]]; then\n\t\t\techo \"已连接\"\n\t\telif [[ \"$configured\" == \"true\" ]]; then\n\t\t\techo \"已配置\"\n\t\telse\n\t\t\techo \"未配置\"\n\t\tfi\n\t}\n\n\topenclaw_colorize_bot_status() {\n\t\tlocal status=\"$1\"\n\t\tcase \"$status\" in\n\t\t\t已连接) echo -e \"${gl_lv}${status}${gl_bai}\" ;;\n\t\t\t已配置) echo -e \"${gl_huang}${status}${gl_bai}\" ;;\n\t\t\t异常) echo -e \"${gl_hong}${status}${gl_bai}\" ;;\n\t\t\t*) echo \"$status\" ;;\n\t\tesac\n\t}\n\n\topenclaw_print_bot_status_line() {\n\t\tlocal label=\"$1\"\n\t\tlocal status=\"$2\"\n\t\techo -e \"- ${label}: $(openclaw_colorize_bot_status \"$status\")\"\n\t}\n\n\topenclaw_show_bot_local_status_block() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal json_ok=\"false\"\n\t\tif [[ -s \"$config_file\" ]] && jq empty \"$config_file\" >/dev/null 2>&1; then\n\t\t\tjson_ok=\"true\"\n\t\tfi\n\n\t\tlocal tg_enabled tg_cfg tg_connected tg_abnormal tg_status\n\t\ttg_enabled=$(openclaw_json_get_bool '.channels.telegram.enabled // .plugins.entries.telegram.enabled // false')\n\t\ttg_cfg=$(openclaw_channel_has_cfg \"telegram\")\n\t\ttg_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/telegram\"; then\n\t\t\ttg_connected=\"true\"\n\t\tfi\n\t\ttg_abnormal=\"false\"\n\t\tif [[ \"$tg_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\ttg_abnormal=\"true\"\n\t\tfi\n\t\ttg_status=$(openclaw_bot_status_text \"$tg_enabled\" \"$tg_cfg\" \"$tg_connected\" \"$tg_abnormal\")\n\n\t\tlocal feishu_enabled feishu_cfg feishu_connected feishu_abnormal feishu_status\n\t\tfeishu_enabled=$(openclaw_json_get_bool '.plugins.entries.feishu.enabled // .channels.feishu.enabled // false')\n\t\tfeishu_cfg=$(openclaw_channel_has_cfg \"feishu\")\n\t\tfeishu_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/feishu\"; then\n\t\t\tfeishu_connected=\"true\"\n\t\tfi\n\t\tfeishu_abnormal=\"false\"\n\t\tif [[ \"$feishu_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"feishu\"; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$feishu_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tfeishu_status=$(openclaw_bot_status_text \"$feishu_enabled\" \"$feishu_cfg\" \"$feishu_connected\" \"$feishu_abnormal\")\n\n\t\tlocal wa_enabled wa_cfg wa_connected wa_abnormal wa_status\n\t\twa_enabled=$(openclaw_json_get_bool '.plugins.entries.whatsapp.enabled // .channels.whatsapp.enabled // false')\n\t\twa_cfg=$(openclaw_channel_has_cfg \"whatsapp\")\n\t\twa_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/whatsapp\"; then\n\t\t\twa_connected=\"true\"\n\t\tfi\n\t\twa_abnormal=\"false\"\n\t\tif [[ \"$wa_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"whatsapp\"; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$wa_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\twa_status=$(openclaw_bot_status_text \"$wa_enabled\" \"$wa_cfg\" \"$wa_connected\" \"$wa_abnormal\")\n\n\t\tlocal dc_enabled dc_cfg dc_connected dc_abnormal dc_status\n\t\tdc_enabled=$(openclaw_json_get_bool '.channels.discord.enabled // .plugins.entries.discord.enabled // false')\n\t\tdc_cfg=$(openclaw_channel_has_cfg \"discord\")\n\t\tdc_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/discord\"; then\n\t\t\tdc_connected=\"true\"\n\t\tfi\n\t\tdc_abnormal=\"false\"\n\t\tif [[ \"$dc_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tdc_abnormal=\"true\"\n\t\tfi\n\t\tdc_status=$(openclaw_bot_status_text \"$dc_enabled\" \"$dc_cfg\" \"$dc_connected\" \"$dc_abnormal\")\n\n\t\tlocal slack_enabled slack_cfg slack_connected slack_abnormal slack_status\n\t\tslack_enabled=$(openclaw_json_get_bool '.plugins.entries.slack.enabled // .channels.slack.enabled // false')\n\t\tslack_cfg=$(openclaw_channel_has_cfg \"slack\")\n\t\tslack_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/slack\"; then\n\t\t\tslack_connected=\"true\"\n\t\tfi\n\t\tslack_abnormal=\"false\"\n\t\tif [[ \"$slack_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"slack\"; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$slack_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tslack_status=$(openclaw_bot_status_text \"$slack_enabled\" \"$slack_cfg\" \"$slack_connected\" \"$slack_abnormal\")\n\n\t\tlocal qq_enabled qq_cfg qq_connected qq_abnormal qq_status\n\t\tqq_enabled=$(openclaw_json_get_bool '.plugins.entries.qqbot.enabled // .channels.qqbot.enabled // false')\n\t\tqq_cfg=$(openclaw_channel_has_cfg \"qqbot\")\n\t\tqq_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/sessions\" || openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/data\"; then\n\t\t\tqq_connected=\"true\"\n\t\tfi\n\t\tqq_abnormal=\"false\"\n\t\tif [[ \"$qq_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"qqbot\"; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$qq_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tqq_status=$(openclaw_bot_status_text \"$qq_enabled\" \"$qq_cfg\" \"$qq_connected\" \"$qq_abnormal\")\n\n\t\techo \"本地状态(仅本机配置/缓存,不做网络探测):\"\n\t\topenclaw_print_bot_status_line \"Telegram\" \"$tg_status\"\n\t\topenclaw_print_bot_status_line \"飞书(Lark)\" \"$feishu_status\"\n\t\topenclaw_print_bot_status_line \"WhatsApp\" \"$wa_status\"\n\t\topenclaw_print_bot_status_line \"Discord\" \"$dc_status\"\n\t\topenclaw_print_bot_status_line \"Slack\" \"$slack_status\"\n\t\topenclaw_print_bot_status_line \"QQ Bot\" \"$qq_status\"\n\t}\n\n\tchange_tg_bot_code() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"机器人连接对接\"\n\t\t\topenclaw_show_bot_local_status_block\n\t\t\techo\n\n\t\t\tlocal bot_choice\n\t\t\tbot_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择要对接的平台\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"Telegram 机器人对接\" \\\n\t\t\t\t\"飞书 (Lark) 机器人对接\" \\\n\t\t\t\t\"WhatsApp 机器人对接\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tlocal code\n\t\t\tcase \"$bot_choice\" in\n\t\t\t\t\"Telegram 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"TG机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve telegram \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"飞书 (Lark) 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"飞书机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve feishu \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"WhatsApp 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"WhatsApp连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve whatsapp \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"返回\"|*)\n\t\t\t\t\treturn 0\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\topenclaw_backup_root() {\n\t\techo \"${HOME}/.openclaw/backups\"\n\t}\n\n\topenclaw_is_interactive_terminal() {\n\t\t[[ -t 0 && -t 1 ]]\n\t}\n\n\topenclaw_has_command() {\n\t\tcommand -v \"$1\" >/dev/null 2>&1\n\t}\n\n\topenclaw_is_safe_relpath() {\n\t\tlocal rel=\"$1\"\n\t\t[[ -z \"$rel\" ]] && return 1\n\t\t[[ \"$rel\" == /* ]] && return 1\n\t\t[[ \"$rel\" == *//* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\n'* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\r'* ]] && return 1\n\t\tcase \"$rel\" in\n\t\t\t../*|*/../*|*/..|..)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t\treturn 0\n\t}\n\n\topenclaw_restore_path_allowed() {\n\t\tlocal mode=\"$1\"\n\t\tlocal rel=\"$2\"\n\t\tcase \"$mode\" in\n\t\t\tmemory)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\tMEMORY.md|AGENTS.md|USER.md|SOUL.md|TOOLS.md|memory/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\tproject)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\topenclaw.json|workspace/*|extensions/*|skills/*|prompts/*|tools/*|telegram/*|feishu/*|whatsapp/*|discord/*|slack/*|qqbot/*|logs/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\t*)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t}\n\n\topenclaw_pack_backup_archive() {\n\t\tlocal backup_type=\"$1\"\n\t\tlocal export_mode=\"$2\"\n\t\tlocal payload_dir=\"$3\"\n\t\tlocal output_file=\"$4\"\n\n\t\tlocal tmp_root\n\t\ttmp_root=$(mktemp -d) || return 1\n\t\tlocal pack_dir=\"$tmp_root/package\"\n\t\tmkdir -p \"$pack_dir\"\n\n\t\tcp -a \"$payload_dir\" \"$pack_dir/payload\"\n\n\t\t(\n\t\t\tcd \"$pack_dir/payload\" || exit 1\n\t\t\tfind . -type f | sed 's|^\\./||' | sort > \"$pack_dir/manifest.files\"\n\t\t\t: > \"$pack_dir/manifest.sha256\"\n\t\t\tlocal f\n\t\t\twhile IFS= read -r f; do\n\t\t\t\t[[ -z \"$f\" ]] && continue\n\t\t\t\tsha256sum \"$f\" >> \"$pack_dir/manifest.sha256\"\n\t\t\tdone < \"$pack_dir/manifest.files\"\n\t\t) || { rm -rf \"$tmp_root\"; return 1; }\n\n\t\tcat > \"$pack_dir/backup.meta\" <<EOF\nTYPE=$backup_type\nMODE=$export_mode\nCREATED_AT=$(date -u +'%Y-%m-%dT%H:%M:%SZ')\nHOST=$(hostname)\nEOF\n\n\t\tmkdir -p \"$(dirname \"$output_file\")\"\n\t\ttar -C \"$pack_dir\" -czf \"$output_file\" backup.meta manifest.files manifest.sha256 payload\n\t\tlocal rc=$?\n\t\trm -rf \"$tmp_root\"\n\t\treturn $rc\n\t}\n\n\topenclaw_offer_transfer_hint() {\n\t\tlocal file_path=\"$1\"\n\t\techo \"可使用以下方式下载备份文件:\"\n\t\techo \" 本地路径: $file_path\"\n\t\techo \" scp 示例: scp root@你的服务器:$file_path ./\"\n\t\techo \" 或使用 SFTP 客户端下载\"\n\t}\n\n\topenclaw_prepare_import_archive() {\n\t\tlocal expected_type=\"$1\"\n\t\tlocal archive_path=\"$2\"\n\t\tlocal unpack_root=\"$3\"\n\n\t\tif [[ ! -f \"$archive_path\" ]]; then\n\t\t\techo \"错误:文件不存在: $archive_path\"\n\t\t\treturn 1\n\t\tfi\n\t\tmkdir -p \"$unpack_root\"\n\t\tif ! tar -xzf \"$archive_path\" -C \"$unpack_root\"; then\n\t\t\techo \"错误:备份包解压失败\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal pkg_dir=\"$unpack_root/package\"\n\t\tif [[ -f \"$unpack_root/backup.meta\" ]]; then\n\t\t\tpkg_dir=\"$unpack_root\"\n\t\tfi\n\n\t\tlocal required\n\t\tfor required in backup.meta manifest.files manifest.sha256 payload; do\n\t\t\tif [[ ! -e \"$pkg_dir/$required\" ]]; then\n\t\t\t\techo \"错误:备份包缺少必要文件: $required\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tdone\n\n\t\tlocal real_type\n\t\treal_type=$(grep '^TYPE=' \"$pkg_dir/backup.meta\" | head -n1 | cut -d'=' -f2-)\n\t\tif [[ \"$real_type\" != \"$expected_type\" ]]; then\n\t\t\techo \"错误:备份类型不匹配,期望: $expected_type,实际: ${real_type:-未知}\"\n\t\t\treturn 1\n\t\tfi\n\n\t\t(\n\t\t\tcd \"$pkg_dir/payload\" || exit 1\n\t\t\tsha256sum -c ../manifest.sha256 >/dev/null\n\t\t) || { echo \"错误:sha256 校验失败,拒绝还原\"; return 1; }\n\n\t\techo \"$pkg_dir\"\n\t\treturn 0\n\t}\n\n\topenclaw_memory_backup_export() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tlocal ts\n\t\tts=$(date +%Y%m%d-%H%M%S)\n\t\tlocal out_file=\"$backup_root/openclaw-memory-full-${ts}.tar.gz\"\n\n\t\tmkdir -p \"$backup_root\"\n\t\tif [[ ! -d \"$workspace_dir\" ]]; then\n\t\t\techo \"错误:未找到 workspace 目录: $workspace_dir\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\t[[ -f \"$workspace_dir/MEMORY.md\" ]] && cp -a \"$workspace_dir/MEMORY.md\" \"$tmp_payload/\"\n\t\t[[ -d \"$workspace_dir/memory\" ]] && cp -a \"$workspace_dir/memory\" \"$tmp_payload/\"\n\n\t\tif gum confirm \"是否附带 AGENTS/USER/SOUL/TOOLS 文件?\"; then\n\t\t\tlocal f\n\t\t\tfor f in AGENTS.md USER.md SOUL.md TOOLS.md; do\n\t\t\t\t[[ -f \"$workspace_dir/$f\" ]] && cp -a \"$workspace_dir/$f\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的记忆文件\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"memory-full\" \"default\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"记忆全量备份完成: $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:记忆全量备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_read_import_path() {\n\t\tlocal file_input file_path backup_root\n\t\techo \"提示:输入文件名时默认在备份目录查找;输入含 / 的路径时按完整路径校验。\" >&2\n\t\techo \"scp 示例: scp /本地/备份包.tar.gz root@你的服务器:/tmp/\" >&2\n\n\t\tfile_input=$(gum input \\\n\t\t\t--placeholder \"备份文件名或完整路径\" \\\n\t\t\t--prompt \"Path > \")\n\t\t[[ -z \"$file_input\" ]] && { echo \"\"; return 0; }\n\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\n\t\tif [[ \"$file_input\" == */* ]]; then\n\t\t\tfile_path=\"$file_input\"\n\t\telse\n\t\t\tfile_path=\"$backup_root/$file_input\"\n\t\tfi\n\n\t\tif [[ ! -f \"$file_path\" ]]; then\n\t\t\techo \"错误:备份文件不存在: $file_path\" >&2\n\t\t\techo \"\"\n\t\t\treturn 1\n\t\tfi\n\n\t\techo \"$file_path\"\n\t}\n\n\topenclaw_memory_backup_import() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tmkdir -p \"$workspace_dir\"\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"memory-full\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed memory \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$workspace_dir/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$workspace_dir/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"记忆全量还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_export() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tif [[ ! -d \"$openclaw_root\" ]]; then\n\t\t\techo \"错误:未找到 OpenClaw 根目录: $openclaw_root\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal export_mode_label\n\t\texport_mode_label=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $'选择备份模式\\n↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"安全模式(推荐):workspace + openclaw.json + extensions/skills/prompts/tools\" \\\n\t\t\t\"完整模式(含更多状态,敏感风险更高)\") || return 1\n\n\t\tlocal mode_label=\"safe\"\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\tlocal d\n\t\tif [[ \"$export_mode_label\" == 完整* ]]; then\n\t\t\tmode_label=\"full\"\n\t\t\tfor d in workspace extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in telegram feishu whatsapp discord slack qqbot logs; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\telse\n\t\t\t[[ -d \"$openclaw_root/workspace\" ]] && cp -a \"$openclaw_root/workspace\" \"$tmp_payload/\"\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的 OpenClaw 项目内容\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tlocal out_file=\"$backup_root/openclaw-project-${mode_label}-$(date +%Y%m%d-%H%M%S).tar.gz\"\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"openclaw-project\" \"$mode_label\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"OpenClaw 项目备份完成 (${mode_label}): $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:OpenClaw 项目备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_import() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tmkdir -p \"$openclaw_root\"\n\n\t\techo \"警告:高风险操作,项目还原会覆盖 OpenClaw 配置与工作区内容。\"\n\t\tgum confirm \"确认继续还原?此操作不可逆。\" || { echo \"已取消\"; break_end; return 1; }\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"openclaw-project\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed project \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tgum spin --spinner pulse --title \"停止 OpenClaw gateway...\" -- openclaw gateway stop\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$openclaw_root/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$openclaw_root/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tstart_gateway\n\t\t\techo \"gateway 健康检查:\"\n\t\t\topenclaw gateway status || true\n\t\tfi\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"OpenClaw 项目还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_detect_type() {\n\t\tlocal file_name=\"$1\"\n\t\tif [[ \"$file_name\" == openclaw-memory-full-*.tar.gz ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\telif [[ \"$file_name\" == openclaw-project-*.tar.gz ]]; then\n\t\t\techo \"项目备份文件\"\n\t\telse\n\t\t\techo \"其他备份文件\"\n\t\tfi\n\t}\n\n\topenclaw_backup_collect_files() {\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tmapfile -t OPENCLAW_BACKUP_FILES < <(find \"$backup_root\" -maxdepth 1 -type f -name '*.tar.gz' | xargs -I{} basename {} | sort -r)\n\t}\n\n\topenclaw_backup_render_file_list() {\n\t\tlocal backup_root i file_name file_path file_type file_size file_time\n\t\tlocal has_memory=0 has_project=0 has_other=0\n\t\tbackup_root=$(openclaw_backup_root)\n\t\topenclaw_backup_collect_files\n\n\t\techo \"备份目录: $backup_root\"\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\techo \"暂无备份文件\"\n\t\t\treturn 0\n\t\tfi\n\n\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\tfile_type=$(openclaw_backup_detect_type \"${OPENCLAW_BACKUP_FILES[$i]}\")\n\t\t\tcase \"$file_type\" in\n\t\t\t\t\"记忆备份文件\") has_memory=1 ;;\n\t\t\t\t\"项目备份文件\") has_project=1 ;;\n\t\t\t\t\"其他备份文件\") has_other=1 ;;\n\t\t\tesac\n\t\tdone\n\n\t\tif [[ \"$has_memory\" -eq 1 ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"记忆备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_project\" -eq 1 ]]; then\n\t\t\techo \"项目备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"项目备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_other\" -eq 1 ]]; then\n\t\t\techo \"其他备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"其他备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_backup_file_exists_in_list() {\n\t\tlocal target_file=\"$1\"\n\t\tlocal item\n\t\tfor item in \"${OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t[[ \"$item\" == \"$target_file\" ]] && return 0\n\t\tdone\n\t\treturn 1\n\t}\n\n\topenclaw_backup_delete_file() {\n\t\tlocal backup_root backup_root_real user_input target_file target_path target_type\n\t\tbackup_root=$(openclaw_backup_root)\n\n\t\topenclaw_backup_render_file_list\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\tbreak_end\n\t\t\treturn 0\n\t\tfi\n\n\t\tuser_input=$(printf '%s\\n' \"${OPENCLAW_BACKUP_FILES[@]}\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 选择要删除的备份 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=15 \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ⚡ DELETE BACKUP \" \\\n\t\t\t--color=border:196,label:196,header:196,prompt:208,pointer:196,marker:208,hl:208,hl+:208) || { echo \"已取消\"; break_end; return 0; }\n\n\t\t[[ -z \"$user_input\" ]] && { echo \"已取消\"; break_end; return 0; }\n\n\t\tbackup_root_real=$(realpath -m \"$backup_root\")\n\t\ttarget_file=$(basename -- \"$user_input\")\n\t\ttarget_path=\"$backup_root/$target_file\"\n\n\t\tif [[ ! -f \"$target_path\" ]]; then\n\t\t\techo \"错误:目标文件不存在: $target_path\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\ttarget_type=$(openclaw_backup_detect_type \"$target_file\")\n\n\t\techo \"即将删除: [$target_type] $target_path\"\n\t\tgum confirm \"确认删除?\" || { echo \"已取消删除\"; break_end; return 0; }\n\t\tgum confirm \"请再次确认,删除后不可恢复\" || { echo \"已取消删除\"; break_end; return 0; }\n\n\t\tif rm -f -- \"$target_path\"; then\n\t\t\techo \"删除成功: $target_file\"\n\t\telse\n\t\t\techo \"错误:删除失败: $target_file\"\n\t\tfi\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_list_files() {\n\t\topenclaw_backup_render_file_list\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_restore_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"备份与还原\"\n\t\t\topenclaw_backup_render_file_list\n\t\t\techo\n\n\t\t\tlocal backup_choice\n\t\t\tbackup_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"备份记忆全量\" \\\n\t\t\t\t\"还原记忆全量\" \\\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\" \\\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\" \\\n\t\t\t\t\"删除备份文件\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$backup_choice\" in\n\t\t\t\t\"备份记忆全量\") openclaw_memory_backup_export ;;\n\t\t\t\t\"还原记忆全量\") openclaw_memory_backup_import ;;\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\") openclaw_project_backup_export ;;\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\") openclaw_project_backup_import ;;\n\t\t\t\t\"删除备份文件\") openclaw_backup_delete_file ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\t_autostart_status() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\techo \"未安装\"\n\t\t\t\treturn\n\t\t\tfi\n\t\t\tif launchctl list 2>/dev/null | grep -q \"ai.openclaw.gateway\"; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\telse\n\t\t\tif systemctl --user is-enabled --quiet openclaw-gateway.service 2>/dev/null; then\n\t\t\t\techo \"已开启\"\n\t\t\telse\n\t\t\t\techo \"已关闭\"\n\t\t\tfi\n\t\tfi\n\t}\n\n\ttoggle_autostart() {\n\t\tlocal status; status=$(_autostart_status)\n\t\tclear\n\t\tui_header \"开机自启动管理\"\n\t\techo -e \" 当前状态:\\033[1m${status}\\033[0m\"\n\t\techo\n\n\t\tlocal action\n\t\tif [[ \"$status\" == \"已开启\" ]]; then\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"关闭自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\telse\n\t\t\taction=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $' ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\t\"开启自启动\" \\\n\t\t\t\t\"取消\") || return 0\n\t\tfi\n\n\t\tcase \"$action\" in\n\t\t\t\"开启自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tif [[ ! -f \"$plist\" ]]; then\n\t\t\t\t\t\tgum spin --spinner globe --title \"正在安装服务...\" -- \\\n\t\t\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 1\"\n\t\t\t\t\tfi\n\t\t\t\t\tlaunchctl load \"$plist\" 2>/dev/null && ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user enable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已开启开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t\"关闭自启动\")\n\t\t\t\tif is_macos; then\n\t\t\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\t\t\tlaunchctl unload \"$plist\" 2>/dev/null && ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\telse\n\t\t\t\t\tsystemctl --user disable openclaw-gateway.service 2>/dev/null \\\n\t\t\t\t\t\t&& ui_ok \"已关闭开机自启动\" || ui_err \"操作失败\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\t*) return 0 ;;\n\t\tesac\n\t\tbreak_end\n\t}\n\n\tupdate_moltbot() {\n\t\tinstall_node_and_tools\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab -\n\t\tstart_gateway\n\t\thash -r\n\t\tui_ok \"更新完成\"\n\t\tbreak_end\n\t}\n\n\tuninstall_moltbot() {\n\t\tgum confirm \"确认卸载 OpenClaw?此操作不可逆。\" || return 0\n\n\t\tgum spin --spinner meter --title \"正在卸载 OpenClaw...\" -- openclaw uninstall\n\t\tnpm uninstall -g openclaw\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab -\n\t\trm -rf /root/.openclaw ~/.openclaw\n\t\thash -r\n\n\t\trm -f \"$HOME/.local/bin/oc\" \"$HOME/.local/bin/openclawctl.sh\" /usr/local/bin/oc\n\t\tfor rc in \"$HOME/.zshrc\" \"$HOME/.bashrc\"; do\n\t\t\t[[ -f \"$rc\" ]] && _sed_i '/\\.local\\/bin.*PATH/d' \"$rc\"\n\t\tdone\n\n\t\tui_ok \"卸载完成\"\n\t\tbreak_end\n\t}\n\n\tnano_openclaw_json() {\n\t\tinstall nano\n\t\tnano ~/.openclaw/openclaw.json\n\t\tstart_gateway\n\t}\n\n\topenclaw_find_webui_domain() {\n\t\tlocal conf domain_list\n\t\tdomain_list=$(\n\t\t\tgrep -R \"18789\" /home/web/conf.d/*.conf 2>/dev/null \\\n\t\t\t| awk -F: '{print $1}' \\\n\t\t\t| sort -u \\\n\t\t\t| while read -r conf; do\n\t\t\t\tbasename \"$conf\" .conf\n\t\t\tdone\n\t\t)\n\n\t\tif [[ -n \"$domain_list\" ]]; then\n\t\t\techo \"$domain_list\"\n\t\tfi\n\t}\n\n\topenclaw_show_webui_addr() {\n\t\tlocal local_ip token domains\n\n\t\tlocal_ip=\"127.0.0.1\"\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tgum style --border normal --border-foreground 99 --padding \"0 2\" \\\n\t\t\t\"OpenClaw WebUI 访问地址\" \\\n\t\t\t\"本机: http://${local_ip}:18789/#token=${token}\"\n\n\t\tdomains=$(openclaw_find_webui_domain)\n\t\tif [[ -n \"$domains\" ]]; then\n\t\t\techo \"$domains\" | while read -r d; do\n\t\t\t\techo \"域名: https://${d}/#token=${token}\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_domain_webui() {\n\t\tadd_yuming\n\t\tldnmp_Proxy \"${yuming}\" 127.0.0.1 18789\n\n\t\tlocal token\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tclear\n\t\techo \"访问地址: https://${yuming}/#token=$token\"\n\t\techo \"先访问URL触发设备ID,然后回车下一步进行配对。\"\n\t\tgum input --placeholder \"按回车继续...\" > /dev/null\n\n\t\techo -e \"${gl_kjlan}正在加载设备列表……${gl_bai}\"\n\n\t\tlocal config_file=\"$HOME/.openclaw/openclaw.json\"\n\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\tlocal new_origin=\"https://${yuming}\"\n\t\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\t\tlocal tmp_json\n\t\t\t\ttmp_json=$(mktemp)\n\t\t\t\tjq 'if .gateway.controlUi == null then .gateway.controlUi = {\"allowedOrigins\": [\"http://127.0.0.1\"]} else . end | if (.gateway.controlUi.allowedOrigins | contains([$origin]) | not) then .gateway.controlUi.allowedOrigins += [$origin] else . end' \\\n\t\t\t\t\t--arg origin \"$new_origin\" \"$config_file\" > \"$tmp_json\" && mv \"$tmp_json\" \"$config_file\"\n\t\t\t\techo -e \"${gl_kjlan}已将域名 ${yuming} 加入 allowedOrigins 配置${gl_bai}\"\n\t\t\t\topenclaw gateway restart >/dev/null 2>&1\n\t\t\tfi\n\t\tfi\n\n\t\topenclaw devices list\n\n\t\tlocal Request_Key\n\t\tRequest_Key=$(gum input \\\n\t\t\t--placeholder \"Request_Key\" \\\n\t\t\t--prompt \"> \")\n\n\t\tif [[ -z \"$Request_Key\" ]]; then\n\t\t\techo \"Request_Key 不能为空\"\n\t\t\treturn 1\n\t\tfi\n\n\t\topenclaw devices approve \"$Request_Key\"\n\t}\n\n\topenclaw_remove_domain() {\n\t\techo \"域名格式 example.com 不带https://\"\n\t\tweb_del\n\t}\n\n\topenclaw_webui_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"WebUI 访问与设置\"\n\t\t\topenclaw_show_webui_addr\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加域名访问\" \\\n\t\t\t\t\"删除域名访问\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"添加域名访问\")\n\t\t\t\t\topenclaw_domain_webui\n\t\t\t\t\techo\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"删除域名访问\")\n\t\t\t\t\topenclaw_remove_domain\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\twhile true; do\n\t\tclear\n\n\t\tlocal install_status running_status update_info\n\t\tinstall_status=$(get_install_status)\n\t\trunning_status=$(get_running_status)\n\t\tupdate_info=$(check_openclaw_update)\n\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"1 8\" --align center \\\n\t\t\t\"O P E N C L A W\" \\\n\t\t\t\"\" \\\n\t\t\t\"[ AI Agent Gateway Manager ]\"\n\t\techo -e \" $install_status $running_status $update_info\"\n\t\tgum style --foreground 240 \" 输入 oc 可快速启动\"\n\t\techo\n\t\tgum style \\\n\t\t\t--bold \\\n\t\t\t--border double --border-foreground 201 \\\n\t\t\t--padding \"0 4\" --align center \\\n\t\t\t\"$(gum style --bold --foreground 201 '◈ by Joey ◈')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ YouTube @joeyblog')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ Telegram t.me/+ft-zI76oovgwNmRh')\" \\\n\t\t\t\"$(gum style --foreground 208 '⚡ 基于:kejilion · cliproxyapi-installer')\"\n\t\techo\n\n\t\tlocal choice\n\t\tlocal autostart_label\n\t\tautostart_label=\"开机自启动 [$(_autostart_status)]\"\n\n\t\tchoice=$(gum choose \\\n\t\t\t--height 24 \\\n\t\t\t--cursor \"❯ \" \\\n\t\t\t--header $' ─── CONTROL CENTER ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\"小白模式安装(推荐)\" \\\n\t\t\t\"安装\" \\\n\t\t\t\"启动\" \\\n\t\t\t\"停止\" \\\n\t\t\t\"状态日志查看\" \\\n\t\t\t\"$autostart_label\" \\\n\t\t\t\"换模型\" \\\n\t\t\t\"API管理\" \\\n\t\t\t\"CLIProxyAPI 管理\" \\\n\t\t\t\"机器人连接对接\" \\\n\t\t\t\"安装插件\" \\\n\t\t\t\"安装技能\" \\\n\t\t\t\"编辑主配置文件\" \\\n\t\t\t\"配置向导\" \\\n\t\t\t\"健康检测与修复\" \\\n\t\t\t\"WebUI访问与设置\" \\\n\t\t\t\"TUI命令行对话\" \\\n\t\t\t\"备份与还原\" \\\n\t\t\t\"更新\" \\\n\t\t\t\"卸载\" \\\n\t\t\t\"退出\") || break\n\n\t\tcase \"$choice\" in\n\t\t\t\"小白模式安装(推荐)\") beginner_mode_install ;;\n\t\t\t\"安装\") install_moltbot ;;\n\t\t\t\"启动\") start_bot ;;\n\t\t\t\"停止\") stop_bot ;;\n\t\t\t\"状态日志查看\") view_logs ;;\n\t\t\t\"开机自启动 [\"*\"]\") toggle_autostart ;;\n\t\t\t\"换模型\") change_model ;;\n\t\t\"API管理\") openclaw_api_manage_menu ;;\n\t\t\"CLIProxyAPI 管理\") cliproxyapi_manage_menu ;;\n\t\t\"机器人连接对接\") change_tg_bot_code ;;\n\t\t\t\"安装插件\") install_plugin ;;\n\t\t\t\"安装技能\") install_skill ;;\n\t\t\t\"编辑主配置文件\") nano_openclaw_json ;;\n\t\t\t\"配置向导\")\n\t\t\t\topenclaw onboard --install-daemon\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"健康检测与修复\")\n\t\t\t\tgum spin --spinner meter --title \"正在执行健康检测...\" -- openclaw doctor --fix\n\t\t\t\tif sync_openclaw_api_models; then\n\t\t\t\t\tstart_gateway\n\t\t\t\telse\n\t\t\t\t\tui_err \"API 模型同步失败,已中止重启网关,请检查 provider /models 返回后重试\"\n\t\t\t\tfi\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"WebUI访问与设置\") openclaw_webui_menu ;;\n\t\t\t\"TUI命令行对话\")\n\t\t\t\topenclaw tui\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"备份与还原\") openclaw_backup_restore_menu ;;\n\t\t\t\"更新\") update_moltbot ;;\n\t\t\t\"卸载\") uninstall_moltbot ;;\n\t\t\t\"退出\"|*) break ;;\n\t\tesac\n\tdone\n}\n\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n\tmoltbot_menu\nfi\n"}, "417c906bfd67a6b25f8dec19a7cd5d92229617c9": {"t": 1785772932.786462, "n": 93363, "h": "95ffce082084806e", "c": "#!/usr/bin/env bash\n#\n# ╔══════════════════════════════════════════════════════════════════╗\n# ║ OpenClaw / MoltBot 管理脚本 ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 作者 GitHub : by Joey ║\n# ║ YouTube : @joeyblog ║\n# ║ Telegram: https://t.me/+ft-zI76oovgwNmRh ║\n# ╠══════════════════════════════════════════════════════════════════╣\n# ║ 致谢 / 引用 ║\n# ║ · 原始脚本基础来自 kejilion(@kejilion) ║\n# ║ · CLIProxyAPI 安装器来自 cliproxyapi-installer ║\n# ║ github.com/brokechubb/cliproxyapi-installer ║\n# ╚══════════════════════════════════════════════════════════════════╝\n#\n: \"${gl_hui:='\\e[37m'}\"\n: \"${gl_hong:='\\033[31m'}\"\n: \"${gl_lv:='\\033[32m'}\"\n: \"${gl_huang:='\\033[33m'}\"\n: \"${gl_lan:='\\033[34m'}\"\n: \"${gl_bai:='\\033[0m'}\"\n: \"${gl_zi:='\\033[35m'}\"\n: \"${gl_kjlan:='\\033[96m'}\"\n\nif ! declare -f break_end > /dev/null 2>&1; then\nbreak_end() {\n\tif command -v gum >/dev/null 2>&1; then\n\t\techo\n\t\tgum style --foreground 240 \" ─────────────────────────────────────── \"\n\t\tgum input --placeholder \"» 按回车继续 «\" --prompt \" \" > /dev/null\n\telse\n\t\techo \" ─── 按任意键继续 ───\"\n\t\tread -n 1 -s -r -p \"\"\n\t\techo\n\tfi\n\tclear\n}\nfi\n\nif ! declare -f install > /dev/null 2>&1; then\ninstall() {\n\tif [[ $# -eq 0 ]]; then\n\t\techo \"未提供软件包参数\"\n\t\treturn 1\n\tfi\n\tfor package in \"$@\"; do\n\t\tif ! command -v \"$package\" &>/dev/null; then\n\t\t\techo -e \"${gl_kjlan}正在安装 $package...${gl_bai}\"\n\t\t\tif [[ \"$(uname -s)\" == \"Darwin\" ]]; then\n\t\t\t\t_ensure_brew &>/dev/null\n\t\t\t\tcommand -v brew &>/dev/null && brew install \"$package\" &>/dev/null\n\t\t\telif command -v dnf &>/dev/null; then\n\t\t\t\tdnf install -y \"$package\" &>/dev/null\n\t\t\telif command -v yum &>/dev/null; then\n\t\t\t\tyum install -y \"$package\" &>/dev/null\n\t\t\telif command -v apt &>/dev/null; then\n\t\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"$package\" &>/dev/null\n\t\t\telif command -v apk &>/dev/null; then\n\t\t\t\tapk add \"$package\" &>/dev/null\n\t\t\telif command -v pacman &>/dev/null; then\n\t\t\t\tpacman -S --noconfirm \"$package\" &>/dev/null\n\t\t\telif command -v zypper &>/dev/null; then\n\t\t\t\tzypper install -y \"$package\" &>/dev/null\n\t\t\telif command -v opkg &>/dev/null; then\n\t\t\t\topkg install \"$package\" &>/dev/null\n\t\t\telif command -v pkg &>/dev/null; then\n\t\t\t\tpkg install -y \"$package\" &>/dev/null\n\t\t\telse\n\t\t\t\techo \"未知的包管理器,无法安装 $package\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\tdone\n}\nfi\n\nis_macos() { [[ \"$(uname -s)\" == \"Darwin\" ]]; }\n\n_ensure_brew() {\n\tif ! command -v brew &>/dev/null; then\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tif ! command -v brew &>/dev/null; then\n\t\techo \"正在安装 Homebrew...\"\n\t\t/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\t\tif [[ -f /opt/homebrew/bin/brew ]]; then\n\t\t\teval \"$(/opt/homebrew/bin/brew shellenv)\"\n\t\telif [[ -f /usr/local/bin/brew ]]; then\n\t\t\teval \"$(/usr/local/bin/brew shellenv)\"\n\t\tfi\n\tfi\n\n\tcommand -v brew &>/dev/null || { echo \"Homebrew 安装失败,请手动安装:https://brew.sh\"; return 1; }\n}\n\n_sed_i() {\n\tif is_macos; then\n\t\tsed -i '' \"$@\"\n\telse\n\t\tsed -i \"$@\"\n\tfi\n}\n\ninstall_base_deps() {\n\tif is_macos; then\n\t\t_ensure_brew || return 1\n\t\tlocal missing_brew=()\n\t\tfor cmd in curl git nano jq python3 gpg; do\n\t\t\tcommand -v \"$cmd\" &>/dev/null || missing_brew+=(\"$cmd\")\n\t\tdone\n\t\tfor i in \"${!missing_brew[@]}\"; do\n\t\t\t[[ \"${missing_brew[$i]}\" == \"gpg\" ]] && missing_brew[$i]=\"gnupg\"\n\t\tdone\n\t\t[[ ${#missing_brew[@]} -eq 0 ]] && return 0\n\t\techo \"正在安装缺失依赖:${missing_brew[*]}...\"\n\t\tbrew install \"${missing_brew[@]}\" &>/dev/null\n\t\treturn 0\n\tfi\n\n\tlocal -a dep_map=(\n\t\t\"curl:curl:curl:curl:curl:curl\"\n\t\t\"git:git:git:git:git:git\"\n\t\t\"nano:nano:nano:nano:nano:nano\"\n\t\t\"jq:jq:jq:jq:jq:jq\"\n\t\t\"python3:python3:python3:python3:python:python3\"\n\t\t\"tar:tar:tar:tar:tar:tar\"\n\t\t\"gpg:gnupg:gnupg2:gnupg:gnupg:gpg2\"\n\t)\n\n\tlocal missing_apt=() missing_dnf=() missing_apk=() missing_pacman=() missing_zypper=()\n\n\tfor entry in \"${dep_map[@]}\"; do\n\t\tIFS=: read -r cmd pkg_apt pkg_dnf pkg_apk pkg_pacman pkg_zypper <<< \"$entry\"\n\t\tcommand -v \"$cmd\" &>/dev/null && continue\n\t\tmissing_apt+=(\"$pkg_apt\")\n\t\tmissing_dnf+=(\"$pkg_dnf\")\n\t\tmissing_apk+=(\"$pkg_apk\")\n\t\tmissing_pacman+=(\"$pkg_pacman\")\n\t\tmissing_zypper+=(\"$pkg_zypper\")\n\tdone\n\n\t[[ ${#missing_apt[@]} -eq 0 ]] && return 0\n\n\techo \"正在安装缺失依赖:${missing_apt[*]}...\"\n\n\tif command -v apt &>/dev/null; then\n\t\tapt update -y &>/dev/null\n\t\tDEBIAN_FRONTEND=noninteractive apt install -y \"${missing_apt[@]}\" &>/dev/null\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y epel-release &>/dev/null || true\n\t\tdnf install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y epel-release &>/dev/null || true\n\t\tyum install -y \"${missing_dnf[@]}\" &>/dev/null\n\telif command -v apk &>/dev/null; then\n\t\tapk update &>/dev/null\n\t\tapk add \"${missing_apk[@]}\" &>/dev/null\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -Sy --noconfirm --needed \"${missing_pacman[@]}\" &>/dev/null\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y \"${missing_zypper[@]}\" &>/dev/null\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\tlocal py\n\t\tpy=$(compgen -c 2>/dev/null | grep -E '^python3\\.[0-9]+$' | sort -V | tail -1)\n\t\tif [[ -n \"$py\" ]]; then\n\t\t\tln -sf \"$(command -v \"$py\")\" /usr/local/bin/python3 2>/dev/null || true\n\t\tfi\n\tfi\n\n\tif ! command -v python3 &>/dev/null; then\n\t\techo \"正在安装 python3...\"\n\t\tif command -v apt &>/dev/null; then\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y python3 &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tdnf install -y python3 &>/dev/null\n\t\telif command -v yum &>/dev/null; then\n\t\t\tyum install -y python3 &>/dev/null\n\t\telif command -v apk &>/dev/null; then\n\t\t\tapk add python3 &>/dev/null\n\t\telif command -v pacman &>/dev/null; then\n\t\t\tpacman -S --noconfirm python &>/dev/null\n\t\telif command -v zypper &>/dev/null; then\n\t\t\tzypper install -y python3 &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\tfi\n}\n\n_install_gum_binary() {\n\tlocal arch\n\tcase \"$(uname -m)\" in\n\t\tx86_64) arch=\"amd64\" ;;\n\t\taarch64) arch=\"arm64\" ;;\n\t\tarmv7l) arch=\"armv7\" ;;\n\t\t*) echo \"不支持的架构: $(uname -m)\"; return 1 ;;\n\tesac\n\tlocal latest\n\tlatest=$(curl -fsSL https://api.github.com/repos/charmbracelet/gum/releases/latest \\\n\t\t| grep '\"tag_name\"' | sed 's/.*\"v\\([^\"]*\\)\".*/\\1/')\n\t[[ -z \"$latest\" ]] && { echo \"获取 gum 版本失败\"; return 1; }\n\tlocal url=\"https://github.com/charmbracelet/gum/releases/download/v${latest}/gum_${latest}_linux_${arch}.tar.gz\"\n\tlocal tmp\n\ttmp=$(mktemp -d)\n\tcurl -fsSL \"$url\" -o \"$tmp/gum.tar.gz\" \\\n\t\t&& tar -xzf \"$tmp/gum.tar.gz\" -C \"$tmp\" \\\n\t\t&& install -m 755 \"$tmp/gum\" /usr/local/bin/gum\n\trm -rf \"$tmp\"\n\tcommand -v gum >/dev/null 2>&1\n}\n\ninstall_gum() {\n\tcommand -v gum >/dev/null 2>&1 && return 0\n\techo \"正在安装 gum...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install gum\n\telif command -v apt >/dev/null 2>&1; then\n\t\tcommand -v gpg >/dev/null 2>&1 || apt install -y gnupg 2>/dev/null\n\t\tmkdir -p /etc/apt/keyrings\n\t\tif curl -fsSL https://repo.charm.sh/apt/gpg.key \\\n\t\t\t| gpg --dearmor -o /etc/apt/keyrings/charm.gpg 2>/dev/null; then\n\t\t\techo \"deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *\" \\\n\t\t\t\t| tee /etc/apt/sources.list.d/charm.list >/dev/null\n\t\t\tapt update -y 2>/dev/null && apt install -y gum 2>/dev/null\n\t\tfi\n\t\tcommand -v gum >/dev/null 2>&1 || _install_gum_binary\n\telif command -v dnf &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tdnf install -y gum\n\telif command -v yum &>/dev/null; then\n\t\tcat > /etc/yum.repos.d/charm.repo <<'REPO'\n[charm]\nname=Charm\nbaseurl=https://repo.charm.sh/yum/\nenabled=1\ngpgcheck=1\ngpgkey=https://repo.charm.sh/yum/gpg.key\nREPO\n\t\tyum install -y gum\n\telif command -v apk &>/dev/null; then\n\t\tapk add gum 2>/dev/null || _install_gum_binary\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm gum 2>/dev/null || _install_gum_binary\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y gum 2>/dev/null || _install_gum_binary\n\telse\n\t\t_install_gum_binary\n\tfi\n\tcommand -v gum >/dev/null 2>&1 || { echo \"gum 安装失败,请手动安装: https://github.com/charmbracelet/gum\"; return 1; }\n}\n\ninstall_fzf() {\n\tcommand -v fzf >/dev/null 2>&1 && return 0\n\techo \"正在安装 fzf...\"\n\tif command -v brew >/dev/null 2>&1; then\n\t\tbrew install fzf\n\telif command -v apt >/dev/null 2>&1; then\n\t\tapt install -y fzf\n\telif command -v dnf &>/dev/null; then\n\t\tdnf install -y fzf\n\telif command -v yum &>/dev/null; then\n\t\tyum install -y fzf\n\telif command -v apk &>/dev/null; then\n\t\tapk add fzf\n\telif command -v pacman &>/dev/null; then\n\t\tpacman -S --noconfirm fzf\n\telif command -v zypper &>/dev/null; then\n\t\tzypper install -y fzf\n\telse\n\t\techo \"无法自动安装 fzf,请手动安装: https://github.com/junegunn/fzf\"\n\t\treturn 1\n\tfi\n}\n\n_install_shortcut() {\n\tlocal store_dir shortcut_dir shortcut\n\n\tstore_dir=\"$HOME/.local/bin\"\n\tmkdir -p \"$store_dir\"\n\n\tlocal _candidate_dirs=(\"/opt/homebrew/bin\" \"/usr/local/bin\")\n\tshortcut_dir=\"\"\n\tfor _d in \"${_candidate_dirs[@]}\"; do\n\t\tif [[ -d \"$_d\" && -w \"$_d\" ]]; then\n\t\t\tshortcut_dir=\"$_d\"\n\t\t\tbreak\n\t\tfi\n\tdone\n\n\tif [[ -z \"$shortcut_dir\" ]]; then\n\t\tshortcut_dir=\"$store_dir\"\n\t\tlocal rc_file=\"\"\n\t\t[[ -f \"$HOME/.zshrc\" ]] && rc_file=\"$HOME/.zshrc\"\n\t\t[[ -f \"$HOME/.bashrc\" ]] && rc_file=\"${rc_file:-$HOME/.bashrc}\"\n\t\tif [[ -n \"$rc_file\" ]] && ! grep -q \"$store_dir\" \"$rc_file\" 2>/dev/null; then\n\t\t\tprintf '\\nexport PATH=\"%s:$PATH\"\\n' \"$store_dir\" >> \"$rc_file\"\n\t\tfi\n\t\texport PATH=\"$store_dir:$PATH\"\n\tfi\n\n\tshortcut=\"$shortcut_dir/oc\"\n\n\tcat > \"$shortcut\" <<EOF\n#!/usr/bin/env bash\ncurl -fsSL https://raw.githubusercontent.com/byJoey/openclawctl/main/openclaw.sh \\\\\n -o \"$store_dir/openclawctl.sh\" 2>/dev/null && chmod +x \"$store_dir/openclawctl.sh\"\nexec bash \"$store_dir/openclawctl.sh\" \"\\$@\"\nEOF\n\tchmod +x \"$shortcut\"\n}\n\nmoltbot_menu() {\n\tis_macos && _ensure_brew\n\n\t_install_shortcut\n\n\tinstall_base_deps\n\tinstall_gum || { echo \"gum 安装失败,无法继续\"; return 1; }\n\tinstall_fzf || { echo \"fzf 安装失败,无法继续\"; return 1; }\n\n\tui_header() {\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"0 2\" \"$*\"\n\t\techo\n\t}\n\tui_ok() { gum style --foreground 46 \" ◉ $*\"; }\n\tui_err() { gum style --foreground 196 \" ✗ $*\"; }\n\tui_warn() { gum style --foreground 208 \" ⚡ $*\"; }\n\tui_info() { gum style --foreground 51 \" ◈ $*\"; }\n\tui_step() { echo; gum style --bold --foreground 201 \" ▶ $*\"; echo; }\n\n\tcheck_openclaw_update() {\n\t\tif ! command -v npm >/dev/null 2>&1; then\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal local_version remote_version\n\t\tlocal_version=$(npm list -g openclaw --depth=0 --no-update-notifier 2>/dev/null \\\n\t\t\t| grep openclaw | awk '{print $NF}' | sed 's/^.*@//')\n\n\t\t[[ -z \"$local_version\" ]] && return 1\n\n\t\tremote_version=$(npm view openclaw version --no-update-notifier 2>/dev/null)\n\n\t\t[[ -z \"$remote_version\" ]] && return 1\n\n\t\tif [[ \"$local_version\" != \"$remote_version\" ]]; then\n\t\t\techo -e \"\\033[38;5;208m⚡ UPDATE AVAILABLE $remote_version\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m✦ v$local_version\\033[0m\"\n\t\tfi\n\t}\n\n\tget_install_status() {\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m◉ INSTALLED\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m○ NOT FOUND\\033[0m\"\n\t\tfi\n\t}\n\n\tget_running_status() {\n\t\tif pgrep -f \"openclaw.*gatewa\" >/dev/null 2>&1; then\n\t\t\techo -e \"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\telse\n\t\t\techo -e \"\\033[90m■ STOPPED\\033[0m\"\n\t\tfi\n\t}\n\n\tstart_gateway() {\n\t\tif is_macos; then\n\t\t\tlocal plist=\"$HOME/Library/LaunchAgents/ai.openclaw.gateway.plist\"\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tlaunchctl unload \"$plist\" >/dev/null 2>&1 || true\n\t\t\tif [[ -f \"$plist\" ]]; then\n\t\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- \\\n\t\t\t\t\tbash -c \"launchctl load '$plist' 2>/dev/null; sleep 2\"\n\t\t\telse\n\t\t\t\tgum spin --spinner pulse --title \"正在安装并启动网关...\" -- \\\n\t\t\t\t\tbash -c \"openclaw gateway install 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\telse\n\t\t\topenclaw gateway stop >/dev/null 2>&1\n\t\t\tgum spin --spinner pulse --title \"正在启动网关...\" -- openclaw gateway start\n\t\t\tsleep 1\n\t\tfi\n\t}\n\n\tinstall_node_and_tools() {\n\t\tif command -v node &>/dev/null && command -v npm &>/dev/null; then\n\t\t\treturn 0\n\t\tfi\n\t\techo \"正在安装 Node.js...\"\n\t\tif is_macos; then\n\t\t\t_ensure_brew || return 1\n\t\t\tbrew install node &>/dev/null\n\t\telif command -v dnf &>/dev/null; then\n\t\t\tcurl -fsSL https://rpm.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tdnf install -y cmake libatomic nodejs &>/dev/null\n\t\telif command -v apt &>/dev/null; then\n\t\t\tcurl -fsSL https://deb.nodesource.com/setup_24.x | bash - &>/dev/null\n\t\t\tDEBIAN_FRONTEND=noninteractive apt install -y build-essential python3 libatomic1 nodejs &>/dev/null\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t}\n\n\tconfigure_openclaw_session_policy() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 1\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport json, sys\npath = sys.argv[1]\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nsession = obj.setdefault('session', {})\nsession['dmScope'] = session.get('dmScope', 'per-channel-peer')\nsession['resetTriggers'] = ['/new', '/reset']\nsession['reset'] = {\n 'mode': 'idle',\n 'idleMinutes': 10080\n}\nsession['resetByType'] = {\n 'direct': {'mode': 'idle', 'idleMinutes': 10080},\n 'thread': {'mode': 'idle', 'idleMinutes': 1440},\n 'group': {'mode': 'idle', 'idleMinutes': 120}\n}\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(obj, f, ensure_ascii=False, indent=2)\n f.write('\\n')\nPY\n\t}\n\n\tsync_openclaw_api_models() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\t[[ ! -f \"$config_file\" ]] && return 0\n\t\tcommand -v python3 &>/dev/null || install_base_deps\n\n\t\tpython3 - \"$config_file\" <<'PY'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('未检测到 API providers,跳过模型同步')\n raise SystemExit(0)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nchanged = False\nfatal_errors = []\nsummary = []\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\ndef prompt_delete_provider(name):\n prompt = f\"{name} /models 探测连续失败 3 次。是否删除该 API 供应商及其全部相关模型?[y/N]: \"\n try:\n ans = input(prompt).strip().lower()\n except EOFError:\n return False\n return ans in ('y', 'yes')\n\n\ndef rebind_defaults_before_delete(name):\n global changed\n\n replacement = None\n\n def get_replacement():\n nonlocal replacement\n if replacement is None:\n candidates = collect_available_refs(exclude_provider=name)\n replacement = candidates[0] if candidates else None\n return replacement\n\n primary_ref = get_primary_ref(defaults)\n if ref_provider(primary_ref) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: 默认主模型指向该 provider,但无可用替代模型,已中止删除')\n return False\n set_primary_ref(defaults, repl)\n changed = True\n summary.append(f'已切换默认主模型: {primary_ref} -> {repl}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n repl = get_replacement()\n if not repl:\n summary.append(f'错误 - {name}: {fk} 指向该 provider,但无可用替代模型,已中止删除')\n return False\n defaults[fk] = repl\n changed = True\n summary.append(f'已切换 {fk}: {val} -> {repl}')\n\n return True\n\n\ndef delete_provider_and_refs(name):\n global changed\n\n if not rebind_defaults_before_delete(name):\n return False\n\n removed_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\n for r in removed_refs:\n defaults_models.pop(r, None)\n if removed_refs:\n changed = True\n\n if name in providers:\n providers.pop(name, None)\n changed = True\n\n summary.append(f'已删除 provider {name},并移除 defaults.models 下 {len(removed_refs)} 个模型引用')\n return True\n\n\ndef fetch_remote_models_with_retry(name, base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n data = json.loads(payload)\n return data, None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\nfor name, provider in list(providers.items()):\n if not isinstance(provider, dict):\n summary.append(f'跳过 {name}: provider 结构非法')\n continue\n\n api = provider.get('api', '')\n base_url = provider.get('baseUrl')\n api_key = provider.get('apiKey')\n model_list = provider.get('models', [])\n\n if not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n summary.append(f'跳过 {name}: 无 baseUrl/apiKey/models')\n continue\n\n if api not in SUPPORTED_APIS:\n summary.append(f'跳过 {name}: 不支持直接 /models 校验 (api={api})')\n continue\n\n data, err, attempts = fetch_remote_models_with_retry(name, base_url, api_key, retries=3)\n if err is not None:\n summary.append(f'警告 - {name}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n if prompt_delete_provider(name):\n deleted = delete_provider_and_refs(name)\n if deleted:\n summary.append(f'{name}: 用户已确认删除该 provider 及全部相关模型引用')\n else:\n summary.append(f'{name}: 用户未确认删除,保留现有 provider 配置')\n continue\n\n if attempts > 1:\n summary.append(f'{name}: /models 第 {attempts} 次重试后成功')\n\n if not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n summary.append(f'警告 - 跳过 {name}: /models 返回结构不可识别')\n continue\n\n remote_ids = []\n for item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\n remote_set = set(remote_ids)\n\n if not remote_set:\n fatal_errors.append(f'错误 - {name} 上游 /models 为空,无法为该 provider 提供兜底模型')\n continue\n\n local_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\n local_ids = [str(m['id']) for m in local_models]\n local_set = set(local_ids)\n\n template = None\n for m in local_models:\n template = copy.deepcopy(m)\n break\n if template is None:\n summary.append(f'警告 - 跳过 {name}: 本地 models 无有效模板模型')\n continue\n\n removed_ids = [mid for mid in local_ids if mid not in remote_set]\n added_ids = [mid for mid in remote_ids if mid not in local_set]\n\n kept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\n new_models = kept_models[:]\n\n for mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{name} / {mid}'\n new_models.append(nm)\n\n if not new_models:\n fatal_errors.append(f'错误 - {name} 同步后无可用模型,无法保障默认模型/回退模型兜底')\n continue\n\n expected_refs = {model_ref(name, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\n local_refs = {model_ref(name, mid) for mid in local_ids}\n\n first_ref = model_ref(name, str(new_models[0]['id']))\n\n primary_ref = get_primary_ref(defaults)\n if isinstance(primary_ref, str) and primary_ref in (local_refs - expected_refs):\n set_primary_ref(defaults, first_ref)\n changed = True\n summary.append(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\n for fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in (local_refs - expected_refs):\n defaults[fk] = first_ref\n changed = True\n summary.append(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\n stale_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/') and r not in expected_refs]\n for r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\n for r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\n if removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\n summary.append(f'{name}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\n\nif fatal_errors:\n for line in summary:\n print(line)\n for err in fatal_errors:\n print(err)\n print('模型同步失败:存在 provider 同步后无可用模型,已中止写入')\n raise SystemExit(2)\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n for line in summary:\n print(line)\n print('OpenClaw API 模型一致性同步完成')\nelse:\n for line in summary:\n print(line)\n print('无需同步:配置已与上游 /models 保持一致')\nPY\n\t}\n\n\t_install_openclaw_core() {\n\t\tinstall_node_and_tools\n\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf ssh://git@github.com/\n\t\tgit config --global url.\"${gh_https_url}github.com/\".insteadOf git@github.com:\n\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在安装 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\thash -r 2>/dev/null || true\n\t\tlocal npm_prefix\n\t\tnpm_prefix=$(npm prefix -g 2>/dev/null)\n\t\t[[ -n \"$npm_prefix\" ]] && export PATH=\"$npm_prefix/bin:$PATH\"\n\t\topenclaw onboard --install-daemon\n\t\t_sed_i 's|\"profile\": \"messaging\"|\"profile\": \"full\"|g' ~/.openclaw/openclaw.json\n\t\tconfigure_openclaw_session_policy\n\t\tstart_gateway\n\t}\n\n\tinstall_moltbot() {\n\t\t_install_openclaw_core\n\t\tbreak_end\n\t}\n\n\t_cliproxy_dir() {\n\t\tif is_macos; then\n\t\t\techo \"$(brew --prefix 2>/dev/null || echo /usr/local)/etc\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi\"\n\t\tfi\n\t}\n\n\t_cliproxy_conf() {\n\t\tif is_macos; then\n\t\t\techo \"$(_cliproxy_dir)/cliproxyapi.conf\"\n\t\telse\n\t\t\techo \"$(_cliproxy_dir)/config.yaml\"\n\t\tfi\n\t}\n\n\t_cliproxy_bin() {\n\t\tif is_macos; then\n\t\t\tcommand -v cliproxyapi 2>/dev/null || echo \"cliproxyapi\"\n\t\telse\n\t\t\techo \"$HOME/cliproxyapi/cli-proxy-api\"\n\t\tfi\n\t}\n\n\t_cliproxy_running() {\n\t\tif is_macos; then\n\t\t\tpgrep -f \"cliproxyapi\" >/dev/null 2>&1\n\t\telse\n\t\t\tpgrep -f \"cli-proxy-api\" >/dev/null 2>&1\n\t\tfi\n\t}\n\n\t_cliproxy_start_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services start cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tsystemctl --user enable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\tgum spin --spinner pulse --title \"正在启动 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"systemctl --user start cliproxyapi.service 2>/dev/null; sleep 3\"\n\t\t\tif ! _cliproxy_running; then\n\t\t\t\tui_warn \"systemd 启动失败,改用后台直接运行...\"\n\t\t\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\t\t\t(cd \"$cliproxy_dir\" && nohup ./cli-proxy-api > /tmp/cliproxyapi.log 2>&1 &)\n\t\t\t\tsleep 3\n\t\t\tfi\n\t\tfi\n\t\tif _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已启动\"\n\t\t\treturn 0\n\t\telse\n\t\t\tui_err \"CLIProxyAPI 启动失败,请检查 $(_cliproxy_conf) 后手动启动\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\t_cliproxy_stop_service() {\n\t\tif is_macos; then\n\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\tbash -c \"brew services stop cliproxyapi 2>/dev/null; sleep 2\"\n\t\telse\n\t\t\tif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\tgum spin --spinner pulse --title \"正在停止 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbash -c \"systemctl --user stop cliproxyapi.service 2>/dev/null; sleep 2\"\n\t\t\tfi\n\t\tfi\n\t\tlocal _pat; _pat=$(is_macos && echo \"cliproxyapi\" || echo \"cli-proxy-api\")\n\t\tlocal pids; pids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\tif [[ -n \"$pids\" ]]; then\n\t\t\techo \"$pids\" | xargs kill 2>/dev/null || true\n\t\t\tsleep 2\n\t\t\tpids=$(pgrep -f \"$_pat\" 2>/dev/null || true)\n\t\t\t[[ -n \"$pids\" ]] && echo \"$pids\" | xargs kill -9 2>/dev/null || true\n\t\tfi\n\t\tif ! _cliproxy_running; then\n\t\t\tui_ok \"CLIProxyAPI 已停止\"\n\t\telse\n\t\t\tui_err \"停止失败,仍有进程残留\"\n\t\tfi\n\t}\n\n\t_cliproxy_oauth_login() {\n\t\tlocal cliproxy_bin; cliproxy_bin=$(_cliproxy_bin)\n\t\tif ! command -v \"$cliproxy_bin\" &>/dev/null && [[ ! -x \"$cliproxy_bin\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 未安装:$cliproxy_bin\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_choice\n\t\tprovider_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' 选择要登录的 AI 提供商\\n ↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"Claude (Anthropic)\" \\\n\t\t\t\"Gemini (Google)\" \\\n\t\t\t\"OpenAI / Codex\" \\\n\t\t\t\"Qwen (通义千问)\" \\\n\t\t\t\"iFlow\" \\\n\t\t\t\"取消\") || return 0\n\n\t\tlocal login_cmd=\"\" login_port=\"\"\n\t\tcase \"$provider_choice\" in\n\t\t\t\"Claude (Anthropic)\") login_cmd=\"--claude-login\"; login_port=\"54545\" ;;\n\t\t\t\"Gemini (Google)\") login_cmd=\"--login\"; login_port=\"8085\" ;;\n\t\t\t\"OpenAI / Codex\") login_cmd=\"--codex-login\"; login_port=\"1455\" ;;\n\t\t\t\"Qwen (通义千问)\") login_cmd=\"--qwen-login\"; login_port=\"\" ;;\n\t\t\t\"iFlow\") login_cmd=\"--iflow-login\"; login_port=\"11451\" ;;\n\t\t\t\"取消\"|*) return 0 ;;\n\t\tesac\n\n\t\techo\n\t\tlocal cliproxy_dir; cliproxy_dir=$(_cliproxy_dir)\n\t\tif [[ -n \"$login_port\" ]]; then\n\t\t\tif is_macos; then\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 本地浏览器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 浏览器将自动打开,完成授权后自动返回,请稍候...\"\n\t\t\t\techo\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\tgum style \\\n\t\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t\t--foreground 51 --bold --padding \"0 2\" \\\n\t\t\t\t\t\"◈ 服务器登录流程\"\n\t\t\t\techo\n\t\t\t\tgum style --bold --foreground 240 \\\n\t\t\t\t\t\" 1. 脚本后台启动 cli-proxy-api,并打印 OAuth 授权链接\" \\\n\t\t\t\t\t\" 2. 在本地浏览器打开该链接,完成账号授权\" \\\n\t\t\t\t\t\" 3. 授权后浏览器跳转到 localhost:${login_port},页面报错属正常\" \\\n\t\t\t\t\t\" 4. 复制地址栏完整 URL,粘贴到下方提示符,脚本自动完成握手\"\n\t\t\t\techo\n\t\t\t\tgum style --foreground 208 \\\n\t\t\t\t\t\" ⚡ 无需端口转发 — 回调 URL 里已含 code,直接转发给本机处理\"\n\t\t\t\techo\n\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser) &\n\t\t\t\tlocal cli_pid=$!\n\t\t\t\tsleep 2\n\n\t\t\t\techo\n\t\t\t\tlocal callback_url\n\t\t\t\tcallback_url=$(gum input \\\n\t\t\t\t\t--placeholder \"http://localhost:${login_port}/oauth2callback?code=...\" \\\n\t\t\t\t\t--prompt \" ❯ \" \\\n\t\t\t\t\t--width 120 \\\n\t\t\t\t\t--char-limit 2000) || true\n\n\t\t\t\tif [[ -n \"$callback_url\" ]]; then\n\t\t\t\t\tui_info \"正在向本机发送回调...\"\n\t\t\t\t\tcurl -sf \"$callback_url\" > /dev/null 2>&1 || true\n\t\t\t\t\tsleep 1\n\t\t\t\tfi\n\t\t\t\twait \"$cli_pid\" 2>/dev/null || true\n\t\t\tfi\n\t\telse\n\t\t\tui_info \"Qwen 使用 device flow,无需粘贴回调\"\n\t\t\techo\n\t\t\tgum style --foreground 240 \"将打印授权码 + URL,在任意浏览器中打开并输入授权码即可\"\n\t\t\techo\n\t\t\tif is_macos; then\n\t\t\t\t\"$cliproxy_bin\" \"$login_cmd\"\n\t\t\telse\n\t\t\t\t(cd \"$cliproxy_dir\" && \"$cliproxy_bin\" \"$login_cmd\" --no-browser)\n\t\t\tfi\n\t\tfi\n\t}\n\n\tcliproxyapi_manage_menu() {\n\t\tlocal config_file; config_file=$(_cliproxy_conf)\n\n\t\twhile true; do\n\t\t\tclear\n\n\t\t\tlocal cp_installed cp_running cp_version cp_port cp_keys\n\t\t\tif is_macos; then\n\t\t\t\tcp_version=$(brew info cliproxyapi 2>/dev/null | awk 'NR==1{print $4}')\n\t\t\telse\n\t\t\t\tcp_version=$(cat \"$(_cliproxy_dir)/version.txt\" 2>/dev/null || echo \"\")\n\t\t\tfi\n\t\t\tif [[ -n \"$cp_version\" ]]; then\n\t\t\t\tcp_installed=\"\\033[38;5;46m◉ INSTALLED v${cp_version}\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_installed=\"\\033[90m○ NOT INSTALLED\\033[0m\"\n\t\t\tfi\n\t\t\tif _cliproxy_running; then\n\t\t\t\tcp_running=\"\\033[38;5;46m▶ RUNNING\\033[0m\"\n\t\t\telse\n\t\t\t\tcp_running=\"\\033[90m■ STOPPED\\033[0m\"\n\t\t\tfi\n\t\t\tcp_port=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\t\t\tcp_port=\"${cp_port:-8317}\"\n\t\t\tcp_keys=$(awk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{n++} END{print n+0}' \\\n\t\t\t\t\"$config_file\" 2>/dev/null)\n\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 51 \\\n\t\t\t\t--border double --border-foreground 51 \\\n\t\t\t\t--padding \"1 4\" --align center \\\n\t\t\t\t\"C L I P R O X Y A P I\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ AI OAuth Proxy Manager ]\"\n\t\t\techo -e \" $cp_installed $cp_running\"\n\t\t\techo -e \" \\033[90m端口 :${cp_port} API Keys: ${cp_keys}\\033[0m\"\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $' ─── CLIProxyAPI ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\t\"启动\" \\\n\t\t\t\t\"停止\" \\\n\t\t\t\t\"重启\" \\\n\t\t\t\t\"查看日志\" \\\n\t\t\t\t\"账号授权登录\" \\\n\t\t\t\t\"生成并添加 API Key\" \\\n\t\t\t\t\"查看 API Keys\" \\\n\t\t\t\t\"编辑配置文件\" \\\n\t\t\t\t\"更新\" \\\n\t\t\t\t\"卸载\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"启动\")\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"停止\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"重启\")\n\t\t\t\t\t_cliproxy_stop_service\n\t\t\t\t\tsleep 1\n\t\t\t\t\t_cliproxy_start_service\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看日志\")\n\t\t\t\t\techo\n\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\tlocal brew_log\n\t\t\t\t\t\tbrew_log=\"$(brew --prefix 2>/dev/null)/var/log/cliproxyapi.log\"\n\t\t\t\t\t\tif [[ -f \"$brew_log\" ]]; then\n\t\t\t\t\t\t\ttail -80 \"$brew_log\"\n\t\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\telif systemctl --user is-active --quiet cliproxyapi.service 2>/dev/null; then\n\t\t\t\t\t\tjournalctl --user -u cliproxyapi.service --no-pager -n 80\n\t\t\t\t\telif [[ -f /tmp/cliproxyapi.log ]]; then\n\t\t\t\t\t\ttail -80 /tmp/cliproxyapi.log\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到日志(服务尚未运行过)\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"账号授权登录\")\n\t\t\t\t\t_cliproxy_oauth_login\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"生成并添加 API Key\")\n\t\t\t\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\t\tlocal new_key\n\t\t\t\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\t\t\t\techo\n\t\t\t\t\tgum style --bold --foreground 46 \" ◉ 已生成新 API Key:\"\n\t\t\t\t\tgum style --bold --foreground 51 \" $new_key\"\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 将此 Key 自动写入 config.yaml?\"; then\n\t\t\t\t\t\tpython3 - \"$config_file\" \"$new_key\" <<'PY'\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n)', r'\\1 - \"' + key + '\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nprint(\"写入成功\")\nPY\n\t\t\t\t\t\tui_ok \"API Key 已写入配置\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"查看 API Keys\")\n\t\t\t\t\techo\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tgum style --foreground 240 \" config.yaml 中的 API Keys:\"\n\t\t\t\t\t\techo\n\t\t\t\t\t\tawk '/^api-keys:/{f=1;next} f&&/^[^ \\t]/{exit} f&&/\"sk-/{print \" \" $0}' \\\n\t\t\t\t\t\t\t\"$config_file\"\n\t\t\t\t\t\techo\n\t\t\t\t\telse\n\t\t\t\t\t\tui_warn \"未找到配置文件:$config_file\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"编辑配置文件\")\n\t\t\t\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\t\t\t\tnano \"$config_file\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"未找到配置文件:$config_file\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\tfi\n\t\t\t\t\t;;\n\t\t\t\t\"更新\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" 确认更新 CLIProxyAPI 到最新版本?\"; then\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew upgrade cliproxyapi && ui_ok \"更新完成\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tcurl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer \\\n\t\t\t\t\t\t\t\t| bash -s -- upgrade\n\t\t\t\t\t\t\tui_ok \"更新完成\"\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"卸载\")\n\t\t\t\t\techo\n\t\t\t\t\tif gum confirm \" ⚡ 确认卸载 CLIProxyAPI?\"; then\n\t\t\t\t\t\t_cliproxy_stop_service 2>/dev/null || true\n\t\t\t\t\t\tif is_macos; then\n\t\t\t\t\t\t\tbrew uninstall cliproxyapi 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$HOME/.cli-proxy-api\"\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tsystemctl --user disable cliproxyapi.service >/dev/null 2>&1 || true\n\t\t\t\t\t\t\trm -f \"$HOME/.config/systemd/user/cliproxyapi.service\" 2>/dev/null || true\n\t\t\t\t\t\t\trm -rf \"$(_cliproxy_dir)\"\n\t\t\t\t\t\tfi\n\t\t\t\t\t\tui_ok \"CLIProxyAPI 已卸载\"\n\t\t\t\t\tfi\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tbeginner_mode_install() {\n\t\tif is_macos; then\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ API Key 配置 ]\"\n\t\telse\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 201 \\\n\t\t\t\t--border double --border-foreground 201 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◈ EASY INSTALL ◈\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw ⟶ CLIProxyAPI ⟶ AI Auth ⟶ Auto Config ]\"\n\t\tfi\n\t\techo\n\n\t\tui_step \"第 1 步:安装 OpenClaw\"\n\t\t_install_openclaw_core\n\n\t\tui_step \"第 2 步:安装 CLIProxyAPI\"\n\t\tif is_macos; then\n\t\t\tif ! gum spin --spinner globe --title \"正在安装 CLIProxyAPI...\" -- \\\n\t\t\t\t\tbrew install cliproxyapi; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败(brew install cliproxyapi)\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\telse\n\t\t\tif ! curl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer | bash; then\n\t\t\t\tui_err \"CLIProxyAPI 安装失败\"\n\t\t\t\tbreak_end\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tfi\n\n\t\tlocal cliproxy_config; cliproxy_config=$(_cliproxy_conf)\n\t\tif [[ ! -f \"$cliproxy_config\" ]]; then\n\t\t\tui_err \"CLIProxyAPI 安装后未找到配置文件: $cliproxy_config\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\t\tui_ok \"CLIProxyAPI 安装完成\"\n\n\t\tui_step \"第 3 步:登录 AI 账号\"\n\t\tui_info \"通过 OAuth 授权即可免费使用,无需付费 API Key\"\n\t\techo\n\t\t_cliproxy_oauth_login\n\n\t\tui_step \"第 4 步:生成 API Key 并启动服务\"\n\t\tlocal new_key\n\t\tnew_key=\"sk-$(python3 -c 'import secrets,string; print(\"\".join(secrets.choice(string.ascii_letters+string.digits) for _ in range(48)))')\"\n\t\tpython3 - \"$cliproxy_config\" \"$new_key\" <<'PY' 2>/dev/null\nimport sys, re\npath, key = sys.argv[1], sys.argv[2]\ntxt = open(path).read()\ntxt = re.sub(r'(api-keys:\\s*\\n(?:\\s*-\\s*\"[^\"]*\"\\s*\\n)*)', lambda m: m.group(0) + f' - \"{key}\"\\n', txt, count=1)\nopen(path, 'w').write(txt)\nPY\n\t\tif ! _cliproxy_start_service; then\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_step \"第 5 步:配置 OpenClaw API\"\n\n\t\tlocal api_key port base_url\n\t\tapi_key=$(awk '\n\t\t\t/^api-keys:/ { in_keys=1; next }\n\t\t\tin_keys && /^[^ \\t]/ { exit }\n\t\t\tin_keys && /\"sk-/ { match($0, /sk-[^\"]+/); print substr($0, RSTART, RLENGTH); exit }\n\t\t' \"$cliproxy_config\" 2>/dev/null)\n\n\t\tport=$(awk '/^port:/ { gsub(/[^0-9]/, \"\", $2); if ($2 != \"\") print $2; exit }' \"$cliproxy_config\" 2>/dev/null)\n\t\tport=\"${port:-8317}\"\n\t\tbase_url=\"http://localhost:${port}/v1\"\n\n\t\tif [[ -z \"$api_key\" ]]; then\n\t\t\tui_err \"无法从 $cliproxy_config 读取 API Key,请手动在「CLIProxyAPI 管理」中添加\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tui_info \"地址:$base_url\"\n\t\tui_info \"Key:${api_key:0:12}****\"\n\t\techo\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"cliproxy\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tstart_gateway\n\n\t\t\tui_step \"第 6 步:选择默认模型\"\n\t\t\tui_info \"请从可用模型中选择一个作为默认,选完后网关自动重启\"\n\t\t\techo\n\t\t\tchange_model\n\n\t\t\techo\n\t\t\tgum style \\\n\t\t\t\t--bold --foreground 46 \\\n\t\t\t\t--border double --border-foreground 46 \\\n\t\t\t\t--padding \"1 6\" --align center \\\n\t\t\t\t\"◉ INSTALL COMPLETE ◉\" \\\n\t\t\t\t\"\" \\\n\t\t\t\t\"[ OpenClaw 已自动接入 CLIProxyAPI,可直接开始使用 ]\" \\\n\t\t\t\t\"[ 更多提供商:cd ~/cliproxyapi && ./cli-proxy-api --help ]\"\n\t\tfi\n\n\t\techo\n\t\tif gum confirm \" 现在去对接机器人?\"; then\n\t\t\tchange_tg_bot_code\n\t\telse\n\t\t\tbreak_end\n\t\tfi\n\t}\n\n\tstart_bot() {\n\t\tstart_gateway\n\t\tbreak_end\n\t}\n\n\tstop_bot() {\n\t\ttmux kill-session -t gateway > /dev/null 2>&1\n\t\tgum spin --spinner pulse --title \"正在停止网关...\" -- openclaw gateway stop\n\t\tbreak_end\n\t}\n\n\tview_logs() {\n\t\topenclaw status\n\t\topenclaw gateway status\n\t\topenclaw logs\n\t\tbreak_end\n\t}\n\n\tadd-all-models-from-provider() {\n\t\tlocal provider_name=\"$1\"\n\t\tlocal base_url=\"$2\"\n\t\tlocal api_key=\"$3\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tlocal models_tmpfile\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取 $provider_name 的模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\n\t\tlocal models_json\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tif [[ -z \"$models_json\" ]]; then\n\t\t\techo \"错误:无法获取模型列表\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_ids\n\t\tmodel_ids=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n for m in models:\n if isinstance(m, dict) and \"id\" in m:\n print(m[\"id\"])\nexcept Exception:\n pass\n')\n\n\t\tif [[ -z \"$model_ids\" ]]; then\n\t\t\techo \"错误:未找到任何模型\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal model_count\n\t\tmodel_count=$(echo \"$model_ids\" | wc -l | tr -d ' ')\n\t\techo \"发现 $model_count 个模型\"\n\n\t\tlocal models_array=\"[\"\n\t\tlocal first=true\n\n\t\twhile read -r model_id; do\n\t\t\t[[ $first == false ]] && models_array+=\",\"\n\t\t\tfirst=false\n\n\t\t\tlocal context_window=1048576\n\t\t\tlocal max_tokens=128000\n\t\t\tlocal input_cost=0.15\n\t\t\tlocal output_cost=0.60\n\n\t\t\tcase \"$model_id\" in\n\t\t\t\t*opus*|*pro*|*preview*|*thinking*|*sonnet*)\n\t\t\t\t\tinput_cost=2.00\n\t\t\t\t\toutput_cost=12.00\n\t\t\t\t\t;;\n\t\t\t\t*gpt-5*|*codex*)\n\t\t\t\t\tinput_cost=1.25\n\t\t\t\t\toutput_cost=10.00\n\t\t\t\t\t;;\n\t\t\t\t*flash*|*lite*|*haiku*|*mini*|*nano*)\n\t\t\t\t\tinput_cost=0.10\n\t\t\t\t\toutput_cost=0.40\n\t\t\t\t\t;;\n\t\t\tesac\n\n\t\t\tmodels_array+=$(cat <<EOF\n{\n\t\"id\": \"$model_id\",\n\t\"name\": \"$provider_name / $model_id\",\n\t\"input\": [\"text\", \"image\"],\n\t\"contextWindow\": $context_window,\n\t\"maxTokens\": $max_tokens,\n\t\"cost\": {\n\t\t\"input\": $input_cost,\n\t\t\"output\": $output_cost,\n\t\t\"cacheRead\": 0,\n\t\t\"cacheWrite\": 0\n\t}\n}\nEOF\n)\n\t\tdone <<< \"$model_ids\"\n\n\t\tmodels_array+=\"]\"\n\n\t\t[[ -f \"$config_file\" ]] && cp \"$config_file\" \"${config_file}.bak.$(date +%s)\"\n\n\t\tjq --arg prov \"$provider_name\" \\\n\t\t --arg url \"$base_url\" \\\n\t\t --arg key \"$api_key\" \\\n\t\t --argjson models \"$models_array\" \\\n\t\t'\n\t\t.models |= (\n\t\t\t(. // { mode: \"merge\", providers: {} })\n\t\t\t| .mode = \"merge\"\n\t\t\t| .providers[$prov] = {\n\t\t\t\tbaseUrl: $url,\n\t\t\t\tapiKey: $key,\n\t\t\t\tapi: \"openai-completions\",\n\t\t\t\tmodels: $models\n\t\t\t}\n\t\t)\n\t\t| .agents |= (. // {})\n\t\t| .agents.defaults |= (. // {})\n\t\t| .agents.defaults.models |= (\n\t\t\t(if type == \"object\" then .\n\t\t\t elif type == \"array\" then reduce .[] as $m ({}; if ($m|type) == \"string\" then .[$m] = {} else . end)\n\t\t\t else {}\n\t\t\t end) as $existing\n\t\t\t| reduce ($models[]? | .id? // empty | tostring) as $mid (\n\t\t\t\t$existing;\n\t\t\t\tif ($mid | length) > 0 then\n\t\t\t\t\t.[\"\\($prov)/\\($mid)\"] //= {}\n\t\t\t\telse\n\t\t\t\t\t.\n\t\t\t\tend\n\t\t\t)\n\t\t)\n\t\t' \"$config_file\" > \"${config_file}.tmp\" && mv \"${config_file}.tmp\" \"$config_file\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\techo \"成功添加 $model_count 个模型到 $provider_name\"\n\t\t\techo \"模型引用格式: $provider_name/<model-id>\"\n\t\t\treturn 0\n\t\telse\n\t\t\techo \"错误:配置注入失败\"\n\t\t\treturn 1\n\t\tfi\n\t}\n\n\tadd-openclaw-provider-interactive() {\n\t\tlocal provider_name base_url api_key\n\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"Provider 名称 (如: deepseek)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\t[[ -z \"$provider_name\" ]] && return 1\n\n\t\tbase_url=$(gum input \\\n\t\t\t--placeholder \"Base URL (如: https://api.xxx.com/v1)\" \\\n\t\t\t--prompt \"Base URL > \")\n\t\t[[ -z \"$base_url\" ]] && return 1\n\t\tbase_url=\"${base_url%/}\"\n\n\t\tapi_key=$(gum input --password \\\n\t\t\t--placeholder \"API Key\" \\\n\t\t\t--prompt \"API Key > \")\n\t\t[[ -z \"$api_key\" ]] && return 1\n\n\t\tlocal models_tmpfile models_json available_models model_count\n\t\tmodels_tmpfile=$(mktemp)\n\t\tgum spin --spinner globe --title \"正在获取模型列表...\" -- \\\n\t\t\tcurl -s -m 10 -o \"$models_tmpfile\" \\\n\t\t\t-H \"Authorization: Bearer $api_key\" \\\n\t\t\t\"${base_url}/models\"\n\t\tmodels_json=$(cat \"$models_tmpfile\")\n\t\trm -f \"$models_tmpfile\"\n\n\t\tlocal default_model=\"\"\n\t\tif [[ -n \"$models_json\" ]]; then\n\t\t\tavailable_models=$(echo \"$models_json\" | python3 -c '\nimport sys, json\ntry:\n data = json.load(sys.stdin)\n models = data.get(\"data\", data) if isinstance(data, dict) else data\n ids = sorted(m[\"id\"] for m in models if isinstance(m, dict) and \"id\" in m)\n print(\"\\n\".join(ids))\nexcept Exception:\n pass\n')\n\t\t\tmodel_count=$(echo \"$available_models\" | wc -l | tr -d ' ')\n\t\tfi\n\n\t\tif [[ -n \"$available_models\" ]]; then\n\t\t\tdefault_model=$(echo \"$available_models\" | fzf \\\n\t\t\t\t--prompt=\" ❯ \" \\\n\t\t\t\t--header=\" 发现 $model_count 个模型 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 选第一个\" \\\n\t\t\t\t--header-first \\\n\t\t\t\t--height=15 \\\n\t\t\t\t--layout=reverse \\\n\t\t\t\t--border=double \\\n\t\t\t\t--border-label=\" ◈ DEFAULT MODEL \" \\\n\t\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || default_model=$(echo \"$available_models\" | head -1)\n\t\t\t[[ -z \"$default_model\" ]] && default_model=$(echo \"$available_models\" | head -1)\n\t\t\tdefault_model=$(awk '{print $1}' <<< \"$default_model\")\n\t\tfi\n\n\t\techo\n\t\tgum style \\\n\t\t\t--border normal --border-foreground 99 \\\n\t\t\t--padding \"0 2\" \\\n\t\t\t\"Provider : $provider_name\" \\\n\t\t\t\"Base URL : $base_url\" \\\n\t\t\t\"API Key : ${api_key:0:8}****\" \\\n\t\t\t\"默认模型 : $default_model\" \\\n\t\t\t\"模型总数 : $model_count\"\n\t\techo\n\n\t\tgum confirm \"确认添加所有 $model_count 个模型?\" || { echo \"已取消\"; return 1; }\n\n\t\tinstall jq\n\t\tadd-all-models-from-provider \"$provider_name\" \"$base_url\" \"$api_key\"\n\n\t\tif [[ $? -eq 0 ]]; then\n\t\t\tgum spin --spinner minidot --title \"设置默认模型并重启网关...\" -- \\\n\t\t\t\topenclaw models set \"$provider_name/$default_model\"\n\t\t\tstart_gateway\n\t\t\techo \"完成:所有 $model_count 个模型已加载\"\n\t\tfi\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_list() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\twhile IFS=$'\\t' read -r rec_type idx name base_url model_count latency_txt latency_level; do\n\t\t\tcase \"$rec_type\" in\n\t\t\t\tMSG)\n\t\t\t\t\techo \"$idx\"\n\t\t\t\t\t;;\n\t\t\t\tROW)\n\t\t\t\t\tlocal latency_color=\"$gl_bai\"\n\t\t\t\t\tcase \"$latency_level\" in\n\t\t\t\t\t\tlow) latency_color=\"$gl_lv\" ;;\n\t\t\t\t\t\tmedium) latency_color=\"$gl_huang\" ;;\n\t\t\t\t\t\thigh|unavailable) latency_color=\"$gl_hong\" ;;\n\t\t\t\t\t\tunchecked) latency_color=\"$gl_bai\" ;;\n\t\t\t\t\tesac\n\n\t\t\t\t\tprintf '%b\\n' \"[$idx] ${name} | API: ${base_url} | 模型数量: ${gl_huang}${model_count}${gl_bai} | 延迟/状态: ${latency_color}${latency_txt}${gl_bai}\"\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tlocal _py_list; _py_list=$(mktemp /tmp/oc_list_XXXXXX.py)\n\t\tcat > \"$_py_list\" << 'PYEOF'\nimport json, sys, time, urllib.request\n\npath = sys.argv[1]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\ndef ping_models(base_url, api_key):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': 'Bearer ' + api_key,\n 'User-Agent': 'OpenClaw-API-Manage/1.0',\n },\n )\n start = time.perf_counter()\n with urllib.request.urlopen(req, timeout=4) as resp:\n resp.read(2048)\n return int((time.perf_counter() - start) * 1000)\n\ndef classify_latency(latency):\n if latency == '不可用':\n return '不可用', 'unavailable'\n if latency == '未检测':\n return '未检测', 'unchecked'\n if isinstance(latency, int):\n level = 'low' if latency <= 800 else ('medium' if latency <= 2000 else 'high')\n return str(latency) + 'ms', level\n return str(latency), 'unchecked'\n\ntry:\n with open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\nexcept FileNotFoundError:\n print('MSG\\t未找到 openclaw.json,请先完成安装/初始化。')\n raise SystemExit(0)\nexcept Exception as e:\n print('MSG\\t读取配置失败: ' + type(e).__name__ + ': ' + str(e))\n raise SystemExit(0)\n\nproviders = ((obj.get('models') or {}).get('providers') or {})\nif not isinstance(providers, dict) or not providers:\n print('MSG\\t当前未配置任何 API provider。')\n raise SystemExit(0)\n\nprint('MSG\\t--- 已配置 API 列表 ---')\n\nfor idx, name in enumerate(sorted(providers.keys()), start=1):\n provider = providers.get(name)\n if not isinstance(provider, dict):\n base_url = '-'; model_count = 0; latency_raw = '不可用'\n else:\n base_url = provider.get('baseUrl') or provider.get('url') or provider.get('endpoint') or '-'\n models = provider.get('models') if isinstance(provider.get('models'), list) else []\n model_count = sum(1 for m in models if isinstance(m, dict) and m.get('id'))\n api = provider.get('api', '')\n api_key = provider.get('apiKey')\n latency_raw = '未检测'\n if api in SUPPORTED_APIS and isinstance(base_url, str) and base_url != '-' and isinstance(api_key, str) and api_key:\n try:\n latency_raw = ping_models(base_url, api_key)\n except Exception:\n latency_raw = '不可用'\n elif api in SUPPORTED_APIS:\n latency_raw = '不可用'\n\n latency_text, latency_level = classify_latency(latency_raw)\n print('ROW\\t' + '\\t'.join([str(idx), str(name), str(base_url), str(model_count), str(latency_text), str(latency_level)]))\nPYEOF\n\t\tdone < <(python3 \"$_py_list\" \"$config_file\")\n\t\trm -f \"$_py_list\"\n\t}\n\n\tsync-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要同步的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tinstall jq curl >/dev/null 2>&1\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY2'\nimport copy\nimport json\nimport sys\nimport time\nimport urllib.request\n\npath = sys.argv[1]\ntarget = sys.argv[2]\nSUPPORTED_APIS = {'openai-completions', 'openai-responses', 'openai-chat-completions'}\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or not providers:\n print('错误:未检测到 API providers,无法同步')\n raise SystemExit(2)\n\nprovider = providers.get(target)\nif not isinstance(provider, dict):\n print(f'错误:未找到 provider: {target}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef fetch_remote_models_with_retry(base_url, api_key, retries=3):\n last_error = None\n for attempt in range(1, retries + 1):\n req = urllib.request.Request(\n base_url.rstrip('/') + '/models',\n headers={\n 'Authorization': f'Bearer {api_key}',\n 'User-Agent': 'Mozilla/5.0',\n },\n )\n try:\n with urllib.request.urlopen(req, timeout=12) as resp:\n payload = resp.read().decode('utf-8', 'ignore')\n return json.loads(payload), None, attempt\n except Exception as e:\n last_error = e\n if attempt < retries:\n time.sleep(1)\n return None, last_error, retries\n\n\napi = provider.get('api', '')\nbase_url = provider.get('baseUrl')\napi_key = provider.get('apiKey')\nmodel_list = provider.get('models', [])\n\nif not base_url or not api_key or not isinstance(model_list, list) or not model_list:\n print(f'错误:provider {target} 缺少 baseUrl/apiKey/models,无法执行同步')\n raise SystemExit(3)\n\nif api not in SUPPORTED_APIS:\n print(f'错误:provider {target} 当前 api={api},不支持直接 /models 同步')\n raise SystemExit(3)\n\ndata, err, attempts = fetch_remote_models_with_retry(base_url, api_key, retries=3)\nif err is not None:\n print(f'错误:{target}: /models 探测失败,已重试 {attempts} 次 ({type(err).__name__}: {err})')\n raise SystemExit(4)\n\nif not (isinstance(data, dict) and isinstance(data.get('data'), list)):\n print(f'错误:{target}: /models 返回结构不可识别')\n raise SystemExit(4)\n\nremote_ids = []\nfor item in data['data']:\n if isinstance(item, dict) and item.get('id'):\n remote_ids.append(str(item['id']))\nremote_set = set(remote_ids)\nif not remote_set:\n print(f'错误:{target}: 上游 /models 为空,已中止同步')\n raise SystemExit(5)\n\nlocal_models = [m for m in model_list if isinstance(m, dict) and m.get('id')]\nlocal_ids = [str(m['id']) for m in local_models]\nlocal_set = set(local_ids)\n\ntemplate = copy.deepcopy(local_models[0]) if local_models else None\nif template is None:\n print(f'错误:{target}: 本地 models 无有效模板模型,无法补全新增模型')\n raise SystemExit(3)\n\nremoved_ids = [mid for mid in local_ids if mid not in remote_set]\nadded_ids = [mid for mid in remote_ids if mid not in local_set]\n\nkept_models = [copy.deepcopy(m) for m in local_models if str(m['id']) in remote_set]\nnew_models = kept_models[:]\nfor mid in added_ids:\n nm = copy.deepcopy(template)\n nm['id'] = mid\n if isinstance(nm.get('name'), str):\n nm['name'] = f'{target} / {mid}'\n new_models.append(nm)\n\nif not new_models:\n print(f'错误:{target}: 同步后无可用模型,已中止写入')\n raise SystemExit(5)\n\nexpected_refs = {model_ref(target, str(m['id'])) for m in new_models if isinstance(m, dict) and m.get('id')}\nlocal_refs = {model_ref(target, mid) for mid in local_ids}\nremoved_refs = local_refs - expected_refs\nfirst_ref = model_ref(target, str(new_models[0]['id']))\n\nchanged = False\nprimary_ref = get_primary_ref(defaults)\nif isinstance(primary_ref, str) and primary_ref in removed_refs:\n set_primary_ref(defaults, first_ref)\n changed = True\n print(f'默认模型已兜底替换: {primary_ref} -> {first_ref}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if isinstance(val, str) and val in removed_refs:\n defaults[fk] = first_ref\n changed = True\n print(f'{fk} 已兜底替换: {val} -> {first_ref}')\n\nstale_refs = [r for r in list(defaults_models.keys()) if r.startswith(target + '/') and r not in expected_refs]\nfor r in stale_refs:\n defaults_models.pop(r, None)\n changed = True\n\nfor r in sorted(expected_refs):\n if r not in defaults_models:\n defaults_models[r] = {}\n changed = True\n\nif removed_ids or added_ids or len(local_models) != len(new_models):\n provider['models'] = new_models\n changed = True\n\nif changed:\n with open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'{target}: 删除 {len(removed_ids)} 个,新增 {len(added_ids)} 个,当前 {len(new_models)} 个')\nif changed:\n print('指定 provider 模型一致性同步完成')\nelse:\n print('无需同步:该 provider 配置已与上游 /models 保持一致')\nPY2\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"同步完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在或未配置\" ;;\n\t\t\t3) echo \"错误:provider 配置不完整或类型不支持\" ;;\n\t\t\t4) echo \"错误:上游 /models 请求失败\" ;;\n\t\t\t5) echo \"错误:上游模型为空或同步后无可用模型\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\tdelete-openclaw-provider-interactive() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\n\t\tif [[ ! -f \"$config_file\" ]]; then\n\t\t\techo \"错误:未找到配置文件: $config_file\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal provider_name\n\t\tprovider_name=$(gum input \\\n\t\t\t--placeholder \"要删除的 API 名称 (provider)\" \\\n\t\t\t--prompt \"Provider > \")\n\t\tif [[ -z \"$provider_name\" ]]; then\n\t\t\techo \"错误:provider 名称不能为空\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tpython3 - \"$config_file\" \"$provider_name\" <<'PY'\nimport copy\nimport json\nimport sys\n\npath = sys.argv[1]\nname = sys.argv[2]\n\nwith open(path, 'r', encoding='utf-8') as f:\n obj = json.load(f)\n\nwork = copy.deepcopy(obj)\nmodels_cfg = work.setdefault('models', {})\nproviders = models_cfg.get('providers', {})\nif not isinstance(providers, dict) or name not in providers:\n print(f'错误:未找到 provider: {name}')\n raise SystemExit(2)\n\nagents = work.setdefault('agents', {})\ndefaults = agents.setdefault('defaults', {})\ndefaults_models_raw = defaults.get('models')\nif isinstance(defaults_models_raw, dict):\n defaults_models = defaults_models_raw\nelif isinstance(defaults_models_raw, list):\n defaults_models = {str(x): {} for x in defaults_models_raw if isinstance(x, str)}\nelse:\n defaults_models = {}\ndefaults['models'] = defaults_models\n\n\ndef model_ref(provider_name, model_id):\n return f\"{provider_name}/{model_id}\"\n\n\ndef ref_provider(ref):\n if not isinstance(ref, str) or '/' not in ref:\n return None\n return ref.split('/', 1)[0]\n\n\ndef get_primary_ref(defaults_obj):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n return model_obj\n if isinstance(model_obj, dict):\n primary = model_obj.get('primary')\n if isinstance(primary, str):\n return primary\n return None\n\n\ndef set_primary_ref(defaults_obj, new_ref):\n model_obj = defaults_obj.get('model')\n if isinstance(model_obj, str):\n defaults_obj['model'] = new_ref\n elif isinstance(model_obj, dict):\n model_obj['primary'] = new_ref\n else:\n defaults_obj['model'] = {'primary': new_ref}\n\n\ndef collect_available_refs(exclude_provider=None):\n refs = []\n if not isinstance(providers, dict):\n return refs\n for pname, p in providers.items():\n if exclude_provider and pname == exclude_provider:\n continue\n if not isinstance(p, dict):\n continue\n for m in p.get('models', []) or []:\n if isinstance(m, dict) and m.get('id'):\n refs.append(model_ref(pname, str(m['id'])))\n return refs\n\n\nreplacement_candidates = collect_available_refs(exclude_provider=name)\nreplacement = replacement_candidates[0] if replacement_candidates else None\n\nprimary_ref = get_primary_ref(defaults)\nif ref_provider(primary_ref) == name:\n if not replacement:\n print('错误:删除中止:默认主模型指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n set_primary_ref(defaults, replacement)\n print(f'默认主模型切换: {primary_ref} -> {replacement}')\n\nfor fk in ('modelFallback', 'imageModelFallback'):\n val = defaults.get(fk)\n if ref_provider(val) == name:\n if not replacement:\n print(f'错误:删除中止:{fk} 指向该 provider,且无可用替代模型')\n raise SystemExit(3)\n defaults[fk] = replacement\n print(f'{fk} 切换: {val} -> {replacement}')\n\nremoved_refs = [r for r in list(defaults_models.keys()) if r.startswith(name + '/')]\nfor r in removed_refs:\n defaults_models.pop(r, None)\n\nproviders.pop(name, None)\n\nwith open(path, 'w', encoding='utf-8') as f:\n json.dump(work, f, ensure_ascii=False, indent=2)\n f.write('\\n')\n\nprint(f'已删除 provider: {name}')\nprint(f'已清理 defaults.models 中 {len(removed_refs)} 个关联模型引用')\nPY\n\t\tlocal rc=$?\n\t\tcase \"$rc\" in\n\t\t\t0)\n\t\t\t\techo \"删除完成\"\n\t\t\t\tstart_gateway\n\t\t\t\t;;\n\t\t\t2) echo \"错误:provider 不存在\" ;;\n\t\t\t3) echo \"错误:无可用替代模型,已保持原配置\" ;;\n\t\t\t*) echo \"错误:请检查配置文件结构或日志输出\" ;;\n\t\tesac\n\n\t\tbreak_end\n\t}\n\n\topenclaw_api_manage_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"API 管理\"\n\t\t\topenclaw_api_manage_list\n\t\t\techo\n\n\t\t\tlocal api_choice\n\t\t\tapi_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加 API\" \\\n\t\t\t\t\"同步 API 供应商模型列表\" \\\n\t\t\t\t\"删除 API\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$api_choice\" in\n\t\t\t\t\"添加 API\") add-openclaw-provider-interactive ;;\n\t\t\t\t\"同步 API 供应商模型列表\") sync-openclaw-provider-interactive ;;\n\t\t\t\t\"删除 API\") delete-openclaw-provider-interactive ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tchange_model() {\n\t\tlocal all_models model\n\t\tall_models=$(openclaw models list --all 2>/dev/null | grep \"configured\")\n\n\t\tif [[ -z \"$all_models\" ]]; then\n\t\t\tui_err \"无法获取可用模型列表(请先添加 API 提供商)\"\n\t\t\tbreak_end\n\t\t\treturn\n\t\tfi\n\n\t\tmodel=$(echo \"$all_models\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 当前: $(openclaw models list 2>/dev/null | awk '{print $1}' | head -1) │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=20 \\\n\t\t\t--layout=reverse \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ◈ MODEL SELECT \" \\\n\t\t\t--color=border:51,label:51,header:51,prompt:201,pointer:46,marker:208,hl:208,hl+:208) || return\n\n\t\t[[ -z \"$model\" ]] && return\n\t\tmodel=$(awk '{print $1}' <<< \"$model\")\n\n\t\tgum spin --spinner minidot --title \"正在切换模型...\" -- openclaw models set \"$model\"\n\t\tstart_gateway\n\t\tui_ok \"已切换至:$model\"\n\t\tbreak_end\n\t}\n\n\tresolve_openclaw_plugin_id() {\n\t\tlocal raw_input=\"$1\"\n\t\tlocal plugin_id=\"$raw_input\"\n\n\t\tplugin_id=\"${plugin_id#@openclaw/}\"\n\t\tif [[ \"$plugin_id\" == @*/* ]]; then\n\t\t\tplugin_id=\"${plugin_id##*/}\"\n\t\tfi\n\t\tplugin_id=\"${plugin_id%%@*}\"\n\t\techo \"$plugin_id\"\n\t}\n\n\tsync_openclaw_plugin_allowlist() {\n\t\tlocal plugin_id=\"$1\"\n\t\t[[ -z \"$plugin_id\" ]] && return 1\n\n\t\tlocal home_config=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal root_config=\"/root/.openclaw/openclaw.json\"\n\t\tlocal config_file=\"$home_config\"\n\t\tif [[ ! -f \"$home_config\" && -f \"$root_config\" ]]; then\n\t\t\tconfig_file=\"$root_config\"\n\t\tfi\n\n\t\tmkdir -p \"$(dirname \"$config_file\")\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo '{}' > \"$config_file\"\n\t\tfi\n\n\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\tlocal tmp_json\n\t\t\ttmp_json=$(mktemp)\n\t\t\tif jq --arg pid \"$plugin_id\" '\n\t\t\t\t.plugins = (if (.plugins | type) == \"object\" then .plugins else {} end)\n\t\t\t\t| .plugins.allow = (if (.plugins.allow | type) == \"array\" then .plugins.allow else [] end)\n\t\t\t\t| if (.plugins.allow | index($pid)) == null then .plugins.allow += [$pid] else . end\n\t\t\t' \"$config_file\" > \"$tmp_json\" 2>/dev/null && mv \"$tmp_json\" \"$config_file\"; then\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\t\trm -f \"$tmp_json\"\n\t\tfi\n\n\t\tif command -v python3 >/dev/null 2>&1; then\n\t\t\tif python3 - \"$config_file\" \"$plugin_id\" <<'PYTHON_EOF'\nimport json\nimport sys\nfrom pathlib import Path\n\nconfig_file = Path(sys.argv[1])\nplugin_id = sys.argv[2]\n\ntry:\n data = json.loads(config_file.read_text(encoding='utf-8')) if config_file.exists() else {}\n if not isinstance(data, dict):\n data = {}\nexcept Exception:\n data = {}\n\nplugins = data.get('plugins')\nif not isinstance(plugins, dict):\n plugins = {}\n\na = plugins.get('allow')\nif not isinstance(a, list):\n a = []\n\nif plugin_id not in a:\n a.append(plugin_id)\n\nplugins['allow'] = a\ndata['plugins'] = plugins\nconfig_file.write_text(json.dumps(data, ensure_ascii=False, indent=2) + \"\\n\", encoding='utf-8')\nPYTHON_EOF\n\t\t\tthen\n\t\t\t\techo \"已同步 plugins.allow 白名单: $plugin_id\"\n\t\t\t\treturn 0\n\t\t\tfi\n\t\tfi\n\n\t\techo \"警告:已安装插件,但同步 plugins.allow 失败,请手动检查: $config_file\"\n\t\treturn 1\n\t}\n\n\tinstall_plugin() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"插件管理\"\n\t\t\tgum style --foreground 240 \"当前已安装插件:\"\n\t\t\topenclaw plugins list\n\t\t\techo\n\t\t\tgum style --bold --foreground 99 \"常用插件参考:\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"feishu\" \"飞书/Lark 集成\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"telegram\" \"Telegram 机器人\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"slack\" \"Slack 企业通讯\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"msteams\" \"Microsoft Teams\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"discord\" \"Discord 社区管理\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"whatsapp\" \"WhatsApp 自动化\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"memory-core\" \"基础记忆 (文件检索)\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"memory-lancedb\" \"增强记忆 (向量数据库)\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"copilot-proxy\" \"Copilot 接口转发\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"lobster\" \"审批流 (带人工确认)\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"voice-call\" \"语音通话能力\"\n\t\t\tprintf \" \\033[96m%-20s\\033[0m %s\\n\" \"nostr\" \"加密隐私聊天\"\n\t\t\techo\n\n\t\t\tlocal raw_input\n\t\t\traw_input=$(gum input \\\n\t\t\t\t--placeholder \"插件 ID (留空退出)\" \\\n\t\t\t\t--prompt \"Plugin > \")\n\t\t\t[[ -z \"$raw_input\" ]] && break\n\n\t\t\tlocal plugin_id plugin_full\n\t\t\tplugin_id=$(resolve_openclaw_plugin_id \"$raw_input\")\n\t\t\tplugin_full=\"$raw_input\"\n\n\t\t\tlocal plugin_list\n\t\t\tplugin_list=$(openclaw plugins list 2>/dev/null)\n\n\t\t\tif echo \"$plugin_list\" | grep -qw \"$plugin_id\" && echo \"$plugin_list\" | grep \"$plugin_id\" | grep -q \"disabled\"; then\n\t\t\t\tui_info \"插件 [$plugin_id] 已预装,正在激活...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\" && ui_ok \"激活成功\" || ui_err \"激活失败\"\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/extensions/$plugin_id\" ]]; then\n\t\t\t\tui_info \"发现系统内置插件,尝试直接启用...\"\n\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\telse\n\t\t\t\tui_info \"本地未发现,尝试下载安装...\"\n\t\t\t\trm -rf \"/root/.openclaw/extensions/$plugin_id\"\n\n\t\t\t\tif openclaw plugins install \"$plugin_full\"; then\n\t\t\t\t\tui_ok \"下载成功,正在启用...\"\n\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\telse\n\t\t\t\t\tui_warn \"官方渠道下载失败,尝试 npm 备选方案...\"\n\t\t\t\t\tif npm install -g \"$plugin_full\" --unsafe-perm; then\n\t\t\t\t\t\tui_ok \"npm 安装成功,尝试启用...\"\n\t\t\t\t\t\topenclaw plugins enable \"$plugin_id\"\n\t\t\t\t\telse\n\t\t\t\t\t\tui_err \"无法获取该插件,请检查 ID 是否正确或网络是否可用\"\n\t\t\t\t\t\tbreak_end\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\tfi\n\t\t\tfi\n\n\t\t\tsync_openclaw_plugin_allowlist \"$plugin_id\"\n\t\t\tstart_gateway\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\tinstall_skill() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"技能管理\"\n\t\t\tgum style --foreground 240 \"当前已安装技能:\"\n\t\t\topenclaw skills list\n\t\t\techo\n\t\t\tgum style --bold --foreground 99 \"推荐技能参考:\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"github\" \"管理 GitHub Issues/PR/CI\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"notion\" \"操作 Notion 页面与数据库\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"apple-notes\" \"macOS 原生笔记管理\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"apple-reminders\" \"macOS 提醒事项管理\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"1password\" \"自动化读取 1Password 密钥\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"gog\" \"Google Workspace 全能助手\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"things-mac\" \"Things 3 任务管理\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"bluebubbles\" \"通过 BlueBubbles 收发 iMessage\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"himalaya\" \"终端邮件管理 (IMAP/SMTP)\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"summarize\" \"网页/播客/YouTube 内容总结\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"openai-whisper\" \"本地音频转文字\"\n\t\t\tprintf \" \\033[96m%-22s\\033[0m %s\\n\" \"coding-agent\" \"运行 Claude Code/Codex 等编程助手\"\n\t\t\techo\n\n\t\t\tlocal skill_name\n\t\t\tskill_name=$(gum input \\\n\t\t\t\t--placeholder \"技能名称 (留空退出)\" \\\n\t\t\t\t--prompt \"Skill > \")\n\t\t\t[[ -z \"$skill_name\" ]] && break\n\n\t\t\tlocal skill_found=false\n\t\t\tif [[ -d \"${HOME}/.openclaw/workspace/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在用户目录安装\"\n\t\t\t\tskill_found=true\n\t\t\telif [[ -d \"/usr/lib/node_modules/openclaw/skills/${skill_name}\" ]]; then\n\t\t\t\techo \"技能 [$skill_name] 已在系统目录安装\"\n\t\t\t\tskill_found=true\n\t\t\tfi\n\n\t\t\tif [[ \"$skill_found\" == true ]]; then\n\t\t\t\tgum confirm \"是否重新安装?\" || { break_end; continue; }\n\t\t\tfi\n\n\t\t\tgum spin --spinner globe --title \"正在安装技能 $skill_name...\" -- npx clawhub install \"$skill_name\"\n\t\t\tif [[ $? -eq 0 ]]; then\n\t\t\t\techo \"技能 $skill_name 安装成功\"\n\t\t\t\tstart_gateway\n\t\t\telse\n\t\t\t\techo \"错误:安装失败,请检查技能名称是否正确\"\n\t\t\tfi\n\n\t\t\tbreak_end\n\t\tdone\n\t}\n\n\topenclaw_json_get_bool() {\n\t\tlocal expr=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r \"$expr\" \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_channel_has_cfg() {\n\t\tlocal channel=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ ! -s \"$config_file\" ]]; then\n\t\t\techo \"false\"\n\t\t\treturn\n\t\tfi\n\t\tjq -r --arg c \"$channel\" '\n\t\t\t(.channels[$c] // null) as $v\n\t\t\t| if ($v | type) != \"object\" then\n\t\t\t\tfalse\n\t\t\t else\n\t\t\t\t([ $v\n\t\t\t\t | to_entries[]\n\t\t\t\t | select((.key == \"enabled\" or .key == \"dmPolicy\" or .key == \"groupPolicy\" or .key == \"streaming\") | not)\n\t\t\t\t | .value\n\t\t\t\t | select(. != null and . != \"\" and . != false)\n\t\t\t\t ] | length) > 0\n\t\t\t end\n\t\t' \"$config_file\" 2>/dev/null || echo \"false\"\n\t}\n\n\topenclaw_dir_has_files() {\n\t\tlocal dir=\"$1\"\n\t\t[[ -d \"$dir\" ]] && find \"$dir\" -type f -print -quit 2>/dev/null | grep -q .\n\t}\n\n\topenclaw_plugin_local_installed() {\n\t\tlocal plugin=\"$1\"\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tif [[ -s \"$config_file\" ]] && jq -e --arg p \"$plugin\" '.plugins.installs[$p]' \"$config_file\" >/dev/null 2>&1; then\n\t\t\treturn 0\n\t\tfi\n\t\t[[ -d \"${HOME}/.openclaw/extensions/${plugin}\" ]] || [[ -d \"/usr/lib/node_modules/openclaw/extensions/${plugin}\" ]]\n\t}\n\n\topenclaw_bot_status_text() {\n\t\tlocal enabled=\"$1\"\n\t\tlocal configured=\"$2\"\n\t\tlocal connected=\"$3\"\n\t\tlocal abnormal=\"$4\"\n\t\tif [[ \"$abnormal\" == \"true\" ]]; then\n\t\t\techo \"异常\"\n\t\telif [[ \"$enabled\" != \"true\" ]]; then\n\t\t\techo \"未启用\"\n\t\telif [[ \"$connected\" == \"true\" ]]; then\n\t\t\techo \"已连接\"\n\t\telif [[ \"$configured\" == \"true\" ]]; then\n\t\t\techo \"已配置\"\n\t\telse\n\t\t\techo \"未配置\"\n\t\tfi\n\t}\n\n\topenclaw_colorize_bot_status() {\n\t\tlocal status=\"$1\"\n\t\tcase \"$status\" in\n\t\t\t已连接) echo -e \"${gl_lv}${status}${gl_bai}\" ;;\n\t\t\t已配置) echo -e \"${gl_huang}${status}${gl_bai}\" ;;\n\t\t\t异常) echo -e \"${gl_hong}${status}${gl_bai}\" ;;\n\t\t\t*) echo \"$status\" ;;\n\t\tesac\n\t}\n\n\topenclaw_print_bot_status_line() {\n\t\tlocal label=\"$1\"\n\t\tlocal status=\"$2\"\n\t\techo -e \"- ${label}: $(openclaw_colorize_bot_status \"$status\")\"\n\t}\n\n\topenclaw_show_bot_local_status_block() {\n\t\tlocal config_file=\"${HOME}/.openclaw/openclaw.json\"\n\t\tlocal json_ok=\"false\"\n\t\tif [[ -s \"$config_file\" ]] && jq empty \"$config_file\" >/dev/null 2>&1; then\n\t\t\tjson_ok=\"true\"\n\t\tfi\n\n\t\tlocal tg_enabled tg_cfg tg_connected tg_abnormal tg_status\n\t\ttg_enabled=$(openclaw_json_get_bool '.channels.telegram.enabled // .plugins.entries.telegram.enabled // false')\n\t\ttg_cfg=$(openclaw_channel_has_cfg \"telegram\")\n\t\ttg_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/telegram\"; then\n\t\t\ttg_connected=\"true\"\n\t\tfi\n\t\ttg_abnormal=\"false\"\n\t\tif [[ \"$tg_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\ttg_abnormal=\"true\"\n\t\tfi\n\t\ttg_status=$(openclaw_bot_status_text \"$tg_enabled\" \"$tg_cfg\" \"$tg_connected\" \"$tg_abnormal\")\n\n\t\tlocal feishu_enabled feishu_cfg feishu_connected feishu_abnormal feishu_status\n\t\tfeishu_enabled=$(openclaw_json_get_bool '.plugins.entries.feishu.enabled // .channels.feishu.enabled // false')\n\t\tfeishu_cfg=$(openclaw_channel_has_cfg \"feishu\")\n\t\tfeishu_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/feishu\"; then\n\t\t\tfeishu_connected=\"true\"\n\t\tfi\n\t\tfeishu_abnormal=\"false\"\n\t\tif [[ \"$feishu_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"feishu\"; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$feishu_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tfeishu_abnormal=\"true\"\n\t\tfi\n\t\tfeishu_status=$(openclaw_bot_status_text \"$feishu_enabled\" \"$feishu_cfg\" \"$feishu_connected\" \"$feishu_abnormal\")\n\n\t\tlocal wa_enabled wa_cfg wa_connected wa_abnormal wa_status\n\t\twa_enabled=$(openclaw_json_get_bool '.plugins.entries.whatsapp.enabled // .channels.whatsapp.enabled // false')\n\t\twa_cfg=$(openclaw_channel_has_cfg \"whatsapp\")\n\t\twa_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/whatsapp\"; then\n\t\t\twa_connected=\"true\"\n\t\tfi\n\t\twa_abnormal=\"false\"\n\t\tif [[ \"$wa_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"whatsapp\"; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$wa_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\twa_abnormal=\"true\"\n\t\tfi\n\t\twa_status=$(openclaw_bot_status_text \"$wa_enabled\" \"$wa_cfg\" \"$wa_connected\" \"$wa_abnormal\")\n\n\t\tlocal dc_enabled dc_cfg dc_connected dc_abnormal dc_status\n\t\tdc_enabled=$(openclaw_json_get_bool '.channels.discord.enabled // .plugins.entries.discord.enabled // false')\n\t\tdc_cfg=$(openclaw_channel_has_cfg \"discord\")\n\t\tdc_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/discord\"; then\n\t\t\tdc_connected=\"true\"\n\t\tfi\n\t\tdc_abnormal=\"false\"\n\t\tif [[ \"$dc_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tdc_abnormal=\"true\"\n\t\tfi\n\t\tdc_status=$(openclaw_bot_status_text \"$dc_enabled\" \"$dc_cfg\" \"$dc_connected\" \"$dc_abnormal\")\n\n\t\tlocal slack_enabled slack_cfg slack_connected slack_abnormal slack_status\n\t\tslack_enabled=$(openclaw_json_get_bool '.plugins.entries.slack.enabled // .channels.slack.enabled // false')\n\t\tslack_cfg=$(openclaw_channel_has_cfg \"slack\")\n\t\tslack_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/slack\"; then\n\t\t\tslack_connected=\"true\"\n\t\tfi\n\t\tslack_abnormal=\"false\"\n\t\tif [[ \"$slack_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"slack\"; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$slack_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tslack_abnormal=\"true\"\n\t\tfi\n\t\tslack_status=$(openclaw_bot_status_text \"$slack_enabled\" \"$slack_cfg\" \"$slack_connected\" \"$slack_abnormal\")\n\n\t\tlocal qq_enabled qq_cfg qq_connected qq_abnormal qq_status\n\t\tqq_enabled=$(openclaw_json_get_bool '.plugins.entries.qqbot.enabled // .channels.qqbot.enabled // false')\n\t\tqq_cfg=$(openclaw_channel_has_cfg \"qqbot\")\n\t\tqq_connected=\"false\"\n\t\tif openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/sessions\" || openclaw_dir_has_files \"${HOME}/.openclaw/qqbot/data\"; then\n\t\t\tqq_connected=\"true\"\n\t\tfi\n\t\tqq_abnormal=\"false\"\n\t\tif [[ \"$qq_enabled\" == \"true\" ]] && ! openclaw_plugin_local_installed \"qqbot\"; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tif [[ \"$qq_enabled\" == \"true\" && \"$json_ok\" != \"true\" ]]; then\n\t\t\tqq_abnormal=\"true\"\n\t\tfi\n\t\tqq_status=$(openclaw_bot_status_text \"$qq_enabled\" \"$qq_cfg\" \"$qq_connected\" \"$qq_abnormal\")\n\n\t\techo \"本地状态(仅本机配置/缓存,不做网络探测):\"\n\t\topenclaw_print_bot_status_line \"Telegram\" \"$tg_status\"\n\t\topenclaw_print_bot_status_line \"飞书(Lark)\" \"$feishu_status\"\n\t\topenclaw_print_bot_status_line \"WhatsApp\" \"$wa_status\"\n\t\topenclaw_print_bot_status_line \"Discord\" \"$dc_status\"\n\t\topenclaw_print_bot_status_line \"Slack\" \"$slack_status\"\n\t\topenclaw_print_bot_status_line \"QQ Bot\" \"$qq_status\"\n\t}\n\n\tchange_tg_bot_code() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"机器人连接对接\"\n\t\t\topenclaw_show_bot_local_status_block\n\t\t\techo\n\n\t\t\tlocal bot_choice\n\t\t\tbot_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择要对接的平台\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"Telegram 机器人对接\" \\\n\t\t\t\t\"飞书 (Lark) 机器人对接\" \\\n\t\t\t\t\"WhatsApp 机器人对接\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tlocal code\n\t\t\tcase \"$bot_choice\" in\n\t\t\t\t\"Telegram 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"TG机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve telegram \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"飞书 (Lark) 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"飞书机器人连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve feishu \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"WhatsApp 机器人对接\")\n\t\t\t\t\tcode=$(gum input \\\n\t\t\t\t\t\t--placeholder \"WhatsApp连接码 (例如 NYA99R2F)\" \\\n\t\t\t\t\t\t--prompt \"连接码 > \")\n\t\t\t\t\t[[ -z \"$code\" ]] && continue\n\t\t\t\t\topenclaw pairing approve whatsapp \"$code\"\n\t\t\t\t\tbreak_end\n\t\t\t\t\t;;\n\t\t\t\t\"返回\"|*)\n\t\t\t\t\treturn 0\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\topenclaw_backup_root() {\n\t\techo \"${HOME}/.openclaw/backups\"\n\t}\n\n\topenclaw_is_interactive_terminal() {\n\t\t[[ -t 0 && -t 1 ]]\n\t}\n\n\topenclaw_has_command() {\n\t\tcommand -v \"$1\" >/dev/null 2>&1\n\t}\n\n\topenclaw_is_safe_relpath() {\n\t\tlocal rel=\"$1\"\n\t\t[[ -z \"$rel\" ]] && return 1\n\t\t[[ \"$rel\" == /* ]] && return 1\n\t\t[[ \"$rel\" == *//* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\n'* ]] && return 1\n\t\t[[ \"$rel\" == *$'\\r'* ]] && return 1\n\t\tcase \"$rel\" in\n\t\t\t../*|*/../*|*/..|..)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t\treturn 0\n\t}\n\n\topenclaw_restore_path_allowed() {\n\t\tlocal mode=\"$1\"\n\t\tlocal rel=\"$2\"\n\t\tcase \"$mode\" in\n\t\t\tmemory)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\tMEMORY.md|AGENTS.md|USER.md|SOUL.md|TOOLS.md|memory/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\tproject)\n\t\t\t\tcase \"$rel\" in\n\t\t\t\t\topenclaw.json|workspace/*|extensions/*|skills/*|prompts/*|tools/*|telegram/*|feishu/*|whatsapp/*|discord/*|slack/*|qqbot/*|logs/*) return 0 ;;\n\t\t\t\t\t*) return 1 ;;\n\t\t\t\tesac\n\t\t\t\t;;\n\t\t\t*)\n\t\t\t\treturn 1\n\t\t\t\t;;\n\t\tesac\n\t}\n\n\topenclaw_pack_backup_archive() {\n\t\tlocal backup_type=\"$1\"\n\t\tlocal export_mode=\"$2\"\n\t\tlocal payload_dir=\"$3\"\n\t\tlocal output_file=\"$4\"\n\n\t\tlocal tmp_root\n\t\ttmp_root=$(mktemp -d) || return 1\n\t\tlocal pack_dir=\"$tmp_root/package\"\n\t\tmkdir -p \"$pack_dir\"\n\n\t\tcp -a \"$payload_dir\" \"$pack_dir/payload\"\n\n\t\t(\n\t\t\tcd \"$pack_dir/payload\" || exit 1\n\t\t\tfind . -type f | sed 's|^\\./||' | sort > \"$pack_dir/manifest.files\"\n\t\t\t: > \"$pack_dir/manifest.sha256\"\n\t\t\tlocal f\n\t\t\twhile IFS= read -r f; do\n\t\t\t\t[[ -z \"$f\" ]] && continue\n\t\t\t\tsha256sum \"$f\" >> \"$pack_dir/manifest.sha256\"\n\t\t\tdone < \"$pack_dir/manifest.files\"\n\t\t) || { rm -rf \"$tmp_root\"; return 1; }\n\n\t\tcat > \"$pack_dir/backup.meta\" <<EOF\nTYPE=$backup_type\nMODE=$export_mode\nCREATED_AT=$(date -u +'%Y-%m-%dT%H:%M:%SZ')\nHOST=$(hostname)\nEOF\n\n\t\tmkdir -p \"$(dirname \"$output_file\")\"\n\t\ttar -C \"$pack_dir\" -czf \"$output_file\" backup.meta manifest.files manifest.sha256 payload\n\t\tlocal rc=$?\n\t\trm -rf \"$tmp_root\"\n\t\treturn $rc\n\t}\n\n\topenclaw_offer_transfer_hint() {\n\t\tlocal file_path=\"$1\"\n\t\techo \"可使用以下方式下载备份文件:\"\n\t\techo \" 本地路径: $file_path\"\n\t\techo \" scp 示例: scp root@你的服务器:$file_path ./\"\n\t\techo \" 或使用 SFTP 客户端下载\"\n\t}\n\n\topenclaw_prepare_import_archive() {\n\t\tlocal expected_type=\"$1\"\n\t\tlocal archive_path=\"$2\"\n\t\tlocal unpack_root=\"$3\"\n\n\t\tif [[ ! -f \"$archive_path\" ]]; then\n\t\t\techo \"错误:文件不存在: $archive_path\"\n\t\t\treturn 1\n\t\tfi\n\t\tmkdir -p \"$unpack_root\"\n\t\tif ! tar -xzf \"$archive_path\" -C \"$unpack_root\"; then\n\t\t\techo \"错误:备份包解压失败\"\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal pkg_dir=\"$unpack_root/package\"\n\t\tif [[ -f \"$unpack_root/backup.meta\" ]]; then\n\t\t\tpkg_dir=\"$unpack_root\"\n\t\tfi\n\n\t\tlocal required\n\t\tfor required in backup.meta manifest.files manifest.sha256 payload; do\n\t\t\tif [[ ! -e \"$pkg_dir/$required\" ]]; then\n\t\t\t\techo \"错误:备份包缺少必要文件: $required\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tdone\n\n\t\tlocal real_type\n\t\treal_type=$(grep '^TYPE=' \"$pkg_dir/backup.meta\" | head -n1 | cut -d'=' -f2-)\n\t\tif [[ \"$real_type\" != \"$expected_type\" ]]; then\n\t\t\techo \"错误:备份类型不匹配,期望: $expected_type,实际: ${real_type:-未知}\"\n\t\t\treturn 1\n\t\tfi\n\n\t\t(\n\t\t\tcd \"$pkg_dir/payload\" || exit 1\n\t\t\tsha256sum -c ../manifest.sha256 >/dev/null\n\t\t) || { echo \"错误:sha256 校验失败,拒绝还原\"; return 1; }\n\n\t\techo \"$pkg_dir\"\n\t\treturn 0\n\t}\n\n\topenclaw_memory_backup_export() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tlocal ts\n\t\tts=$(date +%Y%m%d-%H%M%S)\n\t\tlocal out_file=\"$backup_root/openclaw-memory-full-${ts}.tar.gz\"\n\n\t\tmkdir -p \"$backup_root\"\n\t\tif [[ ! -d \"$workspace_dir\" ]]; then\n\t\t\techo \"错误:未找到 workspace 目录: $workspace_dir\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\t[[ -f \"$workspace_dir/MEMORY.md\" ]] && cp -a \"$workspace_dir/MEMORY.md\" \"$tmp_payload/\"\n\t\t[[ -d \"$workspace_dir/memory\" ]] && cp -a \"$workspace_dir/memory\" \"$tmp_payload/\"\n\n\t\tif gum confirm \"是否附带 AGENTS/USER/SOUL/TOOLS 文件?\"; then\n\t\t\tlocal f\n\t\t\tfor f in AGENTS.md USER.md SOUL.md TOOLS.md; do\n\t\t\t\t[[ -f \"$workspace_dir/$f\" ]] && cp -a \"$workspace_dir/$f\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的记忆文件\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"memory-full\" \"default\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"记忆全量备份完成: $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:记忆全量备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_read_import_path() {\n\t\tlocal file_input file_path backup_root\n\t\techo \"提示:输入文件名时默认在备份目录查找;输入含 / 的路径时按完整路径校验。\" >&2\n\t\techo \"scp 示例: scp /本地/备份包.tar.gz root@你的服务器:/tmp/\" >&2\n\n\t\tfile_input=$(gum input \\\n\t\t\t--placeholder \"备份文件名或完整路径\" \\\n\t\t\t--prompt \"Path > \")\n\t\t[[ -z \"$file_input\" ]] && { echo \"\"; return 0; }\n\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\n\t\tif [[ \"$file_input\" == */* ]]; then\n\t\t\tfile_path=\"$file_input\"\n\t\telse\n\t\t\tfile_path=\"$backup_root/$file_input\"\n\t\tfi\n\n\t\tif [[ ! -f \"$file_path\" ]]; then\n\t\t\techo \"错误:备份文件不存在: $file_path\" >&2\n\t\t\techo \"\"\n\t\t\treturn 1\n\t\tfi\n\n\t\techo \"$file_path\"\n\t}\n\n\topenclaw_memory_backup_import() {\n\t\tlocal workspace_dir=\"${HOME}/.openclaw/workspace\"\n\t\tmkdir -p \"$workspace_dir\"\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"memory-full\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed memory \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$workspace_dir/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$workspace_dir/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"记忆全量还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_export() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tif [[ ! -d \"$openclaw_root\" ]]; then\n\t\t\techo \"错误:未找到 OpenClaw 根目录: $openclaw_root\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal export_mode_label\n\t\texport_mode_label=$(gum choose --cursor \"❯ \" \\\n\t\t\t--header $'选择备份模式\\n↑↓ 移动 · Enter 确认 · q 取消' \\\n\t\t\t\"安全模式(推荐):workspace + openclaw.json + extensions/skills/prompts/tools\" \\\n\t\t\t\"完整模式(含更多状态,敏感风险更高)\") || return 1\n\n\t\tlocal mode_label=\"safe\"\n\t\tlocal tmp_payload\n\t\ttmp_payload=$(mktemp -d) || return 1\n\n\t\tlocal d\n\t\tif [[ \"$export_mode_label\" == 完整* ]]; then\n\t\t\tmode_label=\"full\"\n\t\t\tfor d in workspace extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in telegram feishu whatsapp discord slack qqbot logs; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\telse\n\t\t\t[[ -d \"$openclaw_root/workspace\" ]] && cp -a \"$openclaw_root/workspace\" \"$tmp_payload/\"\n\t\t\t[[ -f \"$openclaw_root/openclaw.json\" ]] && cp -a \"$openclaw_root/openclaw.json\" \"$tmp_payload/\"\n\t\t\tfor d in extensions skills prompts tools; do\n\t\t\t\t[[ -e \"$openclaw_root/$d\" ]] && cp -a \"$openclaw_root/$d\" \"$tmp_payload/\"\n\t\t\tdone\n\t\tfi\n\n\t\tif ! find \"$tmp_payload\" -mindepth 1 -print -quit | grep -q .; then\n\t\t\techo \"错误:未找到可备份的 OpenClaw 项目内容\"\n\t\t\trm -rf \"$tmp_payload\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tlocal out_file=\"$backup_root/openclaw-project-${mode_label}-$(date +%Y%m%d-%H%M%S).tar.gz\"\n\n\t\tif gum spin --spinner meter --title \"正在打包备份...\" -- \\\n\t\t\topenclaw_pack_backup_archive \"openclaw-project\" \"$mode_label\" \"$tmp_payload\" \"$out_file\"; then\n\t\t\techo \"OpenClaw 项目备份完成 (${mode_label}): $out_file\"\n\t\t\topenclaw_offer_transfer_hint \"$out_file\"\n\t\telse\n\t\t\techo \"错误:OpenClaw 项目备份失败\"\n\t\tfi\n\n\t\trm -rf \"$tmp_payload\"\n\t\tbreak_end\n\t}\n\n\topenclaw_project_backup_import() {\n\t\tlocal openclaw_root=\"${HOME}/.openclaw\"\n\t\tmkdir -p \"$openclaw_root\"\n\n\t\techo \"警告:高风险操作,项目还原会覆盖 OpenClaw 配置与工作区内容。\"\n\t\tgum confirm \"确认继续还原?此操作不可逆。\" || { echo \"已取消\"; break_end; return 1; }\n\n\t\tlocal archive_path\n\t\tarchive_path=$(openclaw_read_import_path)\n\t\tif [[ -z \"$archive_path\" ]]; then\n\t\t\techo \"错误:未输入备份路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal tmp_unpack pkg_dir\n\t\ttmp_unpack=$(mktemp -d) || return 1\n\t\tpkg_dir=$(openclaw_prepare_import_archive \"openclaw-project\" \"$archive_path\" \"$tmp_unpack\")\n\t\tif [[ $? -ne 0 ]]; then\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tlocal invalid=0\n\t\tlocal valid_list\n\t\tvalid_list=$(mktemp)\n\t\tlocal rel\n\t\twhile IFS= read -r rel; do\n\t\t\t[[ -z \"$rel\" ]] && continue\n\t\t\tif ! openclaw_is_safe_relpath \"$rel\" || ! openclaw_restore_path_allowed project \"$rel\"; then\n\t\t\t\techo \"错误:检测到非法或越权路径: $rel\"\n\t\t\t\tinvalid=1\n\t\t\t\tbreak\n\t\t\tfi\n\t\t\techo \"$rel\" >> \"$valid_list\"\n\t\tdone < \"$pkg_dir/manifest.files\"\n\n\t\tif [[ \"$invalid\" -ne 0 ]]; then\n\t\t\trm -f \"$valid_list\"\n\t\t\trm -rf \"$tmp_unpack\"\n\t\t\techo \"错误:还原中止:存在不安全路径\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tgum spin --spinner pulse --title \"停止 OpenClaw gateway...\" -- openclaw gateway stop\n\t\tfi\n\n\t\twhile IFS= read -r rel; do\n\t\t\tmkdir -p \"$openclaw_root/$(dirname \"$rel\")\"\n\t\t\tcp -a \"$pkg_dir/payload/$rel\" \"$openclaw_root/$rel\"\n\t\tdone < \"$valid_list\"\n\n\t\tif command -v openclaw >/dev/null 2>&1; then\n\t\t\tstart_gateway\n\t\t\techo \"gateway 健康检查:\"\n\t\t\topenclaw gateway status || true\n\t\tfi\n\n\t\trm -f \"$valid_list\"\n\t\trm -rf \"$tmp_unpack\"\n\t\techo \"OpenClaw 项目还原完成\"\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_detect_type() {\n\t\tlocal file_name=\"$1\"\n\t\tif [[ \"$file_name\" == openclaw-memory-full-*.tar.gz ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\telif [[ \"$file_name\" == openclaw-project-*.tar.gz ]]; then\n\t\t\techo \"项目备份文件\"\n\t\telse\n\t\t\techo \"其他备份文件\"\n\t\tfi\n\t}\n\n\topenclaw_backup_collect_files() {\n\t\tlocal backup_root\n\t\tbackup_root=$(openclaw_backup_root)\n\t\tmkdir -p \"$backup_root\"\n\t\tmapfile -t OPENCLAW_BACKUP_FILES < <(find \"$backup_root\" -maxdepth 1 -type f -name '*.tar.gz' | xargs -I{} basename {} | sort -r)\n\t}\n\n\topenclaw_backup_render_file_list() {\n\t\tlocal backup_root i file_name file_path file_type file_size file_time\n\t\tlocal has_memory=0 has_project=0 has_other=0\n\t\tbackup_root=$(openclaw_backup_root)\n\t\topenclaw_backup_collect_files\n\n\t\techo \"备份目录: $backup_root\"\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\techo \"暂无备份文件\"\n\t\t\treturn 0\n\t\tfi\n\n\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\tfile_type=$(openclaw_backup_detect_type \"${OPENCLAW_BACKUP_FILES[$i]}\")\n\t\t\tcase \"$file_type\" in\n\t\t\t\t\"记忆备份文件\") has_memory=1 ;;\n\t\t\t\t\"项目备份文件\") has_project=1 ;;\n\t\t\t\t\"其他备份文件\") has_other=1 ;;\n\t\t\tesac\n\t\tdone\n\n\t\tif [[ \"$has_memory\" -eq 1 ]]; then\n\t\t\techo \"记忆备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"记忆备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_project\" -eq 1 ]]; then\n\t\t\techo \"项目备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"项目备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\n\t\tif [[ \"$has_other\" -eq 1 ]]; then\n\t\t\techo \"其他备份文件\"\n\t\t\tfor i in \"${!OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t\tfile_name=\"${OPENCLAW_BACKUP_FILES[$i]}\"\n\t\t\t\tfile_type=$(openclaw_backup_detect_type \"$file_name\")\n\t\t\t\t[[ \"$file_type\" != \"其他备份文件\" ]] && continue\n\t\t\t\tfile_path=\"$backup_root/$file_name\"\n\t\t\t\tfile_size=$(ls -lh \"$file_path\" | awk '{print $5}')\n\t\t\t\tfile_time=$(date -d \"$(stat -c %y \"$file_path\")\" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || stat -c %y \"$file_path\" | awk '{print $1\" \"$2}')\n\t\t\t\tprintf \" %s | %s | %s\\n\" \"$file_name\" \"$file_size\" \"$file_time\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_backup_file_exists_in_list() {\n\t\tlocal target_file=\"$1\"\n\t\tlocal item\n\t\tfor item in \"${OPENCLAW_BACKUP_FILES[@]}\"; do\n\t\t\t[[ \"$item\" == \"$target_file\" ]] && return 0\n\t\tdone\n\t\treturn 1\n\t}\n\n\topenclaw_backup_delete_file() {\n\t\tlocal backup_root backup_root_real user_input target_file target_path target_type\n\t\tbackup_root=$(openclaw_backup_root)\n\n\t\topenclaw_backup_render_file_list\n\t\tif [[ ${#OPENCLAW_BACKUP_FILES[@]} -eq 0 ]]; then\n\t\t\tbreak_end\n\t\t\treturn 0\n\t\tfi\n\n\t\tuser_input=$(printf '%s\\n' \"${OPENCLAW_BACKUP_FILES[@]}\" | fzf \\\n\t\t\t--prompt=\" ❯ \" \\\n\t\t\t--header=\" 选择要删除的备份 │ ↑↓ 移动 / 搜索 Enter 确认 Esc 取消\" \\\n\t\t\t--header-first \\\n\t\t\t--height=15 \\\n\t\t\t--border=double \\\n\t\t\t--border-label=\" ⚡ DELETE BACKUP \" \\\n\t\t\t--color=border:196,label:196,header:196,prompt:208,pointer:196,marker:208,hl:208,hl+:208) || { echo \"已取消\"; break_end; return 0; }\n\n\t\t[[ -z \"$user_input\" ]] && { echo \"已取消\"; break_end; return 0; }\n\n\t\tbackup_root_real=$(realpath -m \"$backup_root\")\n\t\ttarget_file=$(basename -- \"$user_input\")\n\t\ttarget_path=\"$backup_root/$target_file\"\n\n\t\tif [[ ! -f \"$target_path\" ]]; then\n\t\t\techo \"错误:目标文件不存在: $target_path\"\n\t\t\tbreak_end\n\t\t\treturn 1\n\t\tfi\n\n\t\ttarget_type=$(openclaw_backup_detect_type \"$target_file\")\n\n\t\techo \"即将删除: [$target_type] $target_path\"\n\t\tgum confirm \"确认删除?\" || { echo \"已取消删除\"; break_end; return 0; }\n\t\tgum confirm \"请再次确认,删除后不可恢复\" || { echo \"已取消删除\"; break_end; return 0; }\n\n\t\tif rm -f -- \"$target_path\"; then\n\t\t\techo \"删除成功: $target_file\"\n\t\telse\n\t\t\techo \"错误:删除失败: $target_file\"\n\t\tfi\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_list_files() {\n\t\topenclaw_backup_render_file_list\n\t\tbreak_end\n\t}\n\n\topenclaw_backup_restore_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"备份与还原\"\n\t\t\topenclaw_backup_render_file_list\n\t\t\techo\n\n\t\t\tlocal backup_choice\n\t\t\tbackup_choice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"备份记忆全量\" \\\n\t\t\t\t\"还原记忆全量\" \\\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\" \\\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\" \\\n\t\t\t\t\"删除备份文件\" \\\n\t\t\t\t\"返回\") || return 0\n\n\t\t\tcase \"$backup_choice\" in\n\t\t\t\t\"备份记忆全量\") openclaw_memory_backup_export ;;\n\t\t\t\t\"还原记忆全量\") openclaw_memory_backup_import ;;\n\t\t\t\t\"备份 OpenClaw 项目(安全模式)\") openclaw_project_backup_export ;;\n\t\t\t\t\"还原 OpenClaw 项目(高风险)\") openclaw_project_backup_import ;;\n\t\t\t\t\"删除备份文件\") openclaw_backup_delete_file ;;\n\t\t\t\t\"返回\"|*) return 0 ;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\tupdate_moltbot() {\n\t\tinstall_node_and_tools\n\t\tif is_macos; then\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- \\\n\t\t\t\tenv SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest\n\t\telse\n\t\t\tgum spin --spinner globe --title \"正在更新 OpenClaw...\" -- npm install -g openclaw@latest\n\t\tfi\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab -\n\t\tstart_gateway\n\t\thash -r\n\t\tui_ok \"更新完成\"\n\t\tbreak_end\n\t}\n\n\tuninstall_moltbot() {\n\t\tgum confirm \"确认卸载 OpenClaw?此操作不可逆。\" || return 0\n\n\t\tgum spin --spinner meter --title \"正在卸载 OpenClaw...\" -- openclaw uninstall\n\t\tnpm uninstall -g openclaw\n\t\tcrontab -l 2>/dev/null | grep -v \"s gateway\" | crontab -\n\t\trm -rf /root/.openclaw ~/.openclaw\n\t\thash -r\n\n\t\trm -f \"$HOME/.local/bin/oc\" \"$HOME/.local/bin/openclawctl.sh\" /usr/local/bin/oc\n\t\tfor rc in \"$HOME/.zshrc\" \"$HOME/.bashrc\"; do\n\t\t\t[[ -f \"$rc\" ]] && _sed_i '/\\.local\\/bin.*PATH/d' \"$rc\"\n\t\tdone\n\n\t\tui_ok \"卸载完成\"\n\t\tbreak_end\n\t}\n\n\tnano_openclaw_json() {\n\t\tinstall nano\n\t\tnano ~/.openclaw/openclaw.json\n\t\tstart_gateway\n\t}\n\n\topenclaw_find_webui_domain() {\n\t\tlocal conf domain_list\n\t\tdomain_list=$(\n\t\t\tgrep -R \"18789\" /home/web/conf.d/*.conf 2>/dev/null \\\n\t\t\t| awk -F: '{print $1}' \\\n\t\t\t| sort -u \\\n\t\t\t| while read -r conf; do\n\t\t\t\tbasename \"$conf\" .conf\n\t\t\tdone\n\t\t)\n\n\t\tif [[ -n \"$domain_list\" ]]; then\n\t\t\techo \"$domain_list\"\n\t\tfi\n\t}\n\n\topenclaw_show_webui_addr() {\n\t\tlocal local_ip token domains\n\n\t\tlocal_ip=\"127.0.0.1\"\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tgum style --border normal --border-foreground 99 --padding \"0 2\" \\\n\t\t\t\"OpenClaw WebUI 访问地址\" \\\n\t\t\t\"本机: http://${local_ip}:18789/#token=${token}\"\n\n\t\tdomains=$(openclaw_find_webui_domain)\n\t\tif [[ -n \"$domains\" ]]; then\n\t\t\techo \"$domains\" | while read -r d; do\n\t\t\t\techo \"域名: https://${d}/#token=${token}\"\n\t\t\tdone\n\t\tfi\n\t}\n\n\topenclaw_domain_webui() {\n\t\tadd_yuming\n\t\tldnmp_Proxy \"${yuming}\" 127.0.0.1 18789\n\n\t\tlocal token\n\t\ttoken=$(\n\t\t\topenclaw dashboard 2>/dev/null \\\n\t\t\t| sed -n 's/.*:18789\\/#token=\\([a-f0-9]\\+\\).*/\\1/p' \\\n\t\t\t| head -n 1\n\t\t)\n\n\t\tclear\n\t\techo \"访问地址: https://${yuming}/#token=$token\"\n\t\techo \"先访问URL触发设备ID,然后回车下一步进行配对。\"\n\t\tgum input --placeholder \"按回车继续...\" > /dev/null\n\n\t\techo -e \"${gl_kjlan}正在加载设备列表……${gl_bai}\"\n\n\t\tlocal config_file=\"$HOME/.openclaw/openclaw.json\"\n\t\tif [[ -f \"$config_file\" ]]; then\n\t\t\tlocal new_origin=\"https://${yuming}\"\n\t\t\tif command -v jq >/dev/null 2>&1; then\n\t\t\t\tlocal tmp_json\n\t\t\t\ttmp_json=$(mktemp)\n\t\t\t\tjq 'if .gateway.controlUi == null then .gateway.controlUi = {\"allowedOrigins\": [\"http://127.0.0.1\"]} else . end | if (.gateway.controlUi.allowedOrigins | contains([$origin]) | not) then .gateway.controlUi.allowedOrigins += [$origin] else . end' \\\n\t\t\t\t\t--arg origin \"$new_origin\" \"$config_file\" > \"$tmp_json\" && mv \"$tmp_json\" \"$config_file\"\n\t\t\t\techo -e \"${gl_kjlan}已将域名 ${yuming} 加入 allowedOrigins 配置${gl_bai}\"\n\t\t\t\topenclaw gateway restart >/dev/null 2>&1\n\t\t\tfi\n\t\tfi\n\n\t\topenclaw devices list\n\n\t\tlocal Request_Key\n\t\tRequest_Key=$(gum input \\\n\t\t\t--placeholder \"Request_Key\" \\\n\t\t\t--prompt \"> \")\n\n\t\tif [[ -z \"$Request_Key\" ]]; then\n\t\t\techo \"Request_Key 不能为空\"\n\t\t\treturn 1\n\t\tfi\n\n\t\topenclaw devices approve \"$Request_Key\"\n\t}\n\n\topenclaw_remove_domain() {\n\t\techo \"域名格式 example.com 不带https://\"\n\t\tweb_del\n\t}\n\n\topenclaw_webui_menu() {\n\t\twhile true; do\n\t\t\tclear\n\t\t\tui_header \"WebUI 访问与设置\"\n\t\t\topenclaw_show_webui_addr\n\t\t\techo\n\n\t\t\tlocal choice\n\t\t\tchoice=$(gum choose --cursor \"❯ \" \\\n\t\t\t\t--header $'选择操作\\n↑↓ 移动 · Enter 确认 · q 退出' \\\n\t\t\t\t\"添加域名访问\" \\\n\t\t\t\t\"删除域名访问\" \\\n\t\t\t\t\"退出\") || break\n\n\t\t\tcase \"$choice\" in\n\t\t\t\t\"添加域名访问\")\n\t\t\t\t\topenclaw_domain_webui\n\t\t\t\t\techo\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"删除域名访问\")\n\t\t\t\t\topenclaw_remove_domain\n\t\t\t\t\tgum input --placeholder \"按回车返回...\" > /dev/null\n\t\t\t\t\t;;\n\t\t\t\t\"退出\"|*)\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\t}\n\n\twhile true; do\n\t\tclear\n\n\t\tlocal install_status running_status update_info\n\t\tinstall_status=$(get_install_status)\n\t\trunning_status=$(get_running_status)\n\t\tupdate_info=$(check_openclaw_update)\n\n\t\tgum style \\\n\t\t\t--bold --foreground 51 \\\n\t\t\t--border double --border-foreground 51 \\\n\t\t\t--padding \"1 8\" --align center \\\n\t\t\t\"O P E N C L A W\" \\\n\t\t\t\"\" \\\n\t\t\t\"[ AI Agent Gateway Manager ]\"\n\t\techo -e \" $install_status $running_status $update_info\"\n\t\tgum style --foreground 240 \" 输入 oc 可快速启动\"\n\t\techo\n\t\tgum style \\\n\t\t\t--bold \\\n\t\t\t--border double --border-foreground 201 \\\n\t\t\t--padding \"0 4\" --align center \\\n\t\t\t\"$(gum style --bold --foreground 201 '◈ by Joey ◈')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ YouTube @joeyblog')\" \\\n\t\t\t\"$(gum style --bold --foreground 51 '▶ Telegram t.me/+ft-zI76oovgwNmRh')\" \\\n\t\t\t\"$(gum style --foreground 208 '⚡ 基于:kejilion · cliproxyapi-installer')\"\n\t\techo\n\n\t\tlocal choice\n\t\tchoice=$(gum choose \\\n\t\t\t--height 24 \\\n\t\t\t--cursor \"❯ \" \\\n\t\t\t--header $' ─── CONTROL CENTER ───\\n ↑↓ 移动 · Enter 确认 · / 搜索 · q 退出' \\\n\t\t\t\"小白模式安装(推荐)\" \\\n\t\t\t\"安装\" \\\n\t\t\t\"启动\" \\\n\t\t\t\"停止\" \\\n\t\t\t\"状态日志查看\" \\\n\t\t\t\"换模型\" \\\n\t\t\t\"API管理\" \\\n\t\t\t\"CLIProxyAPI 管理\" \\\n\t\t\t\"机器人连接对接\" \\\n\t\t\t\"安装插件\" \\\n\t\t\t\"安装技能\" \\\n\t\t\t\"编辑主配置文件\" \\\n\t\t\t\"配置向导\" \\\n\t\t\t\"健康检测与修复\" \\\n\t\t\t\"WebUI访问与设置\" \\\n\t\t\t\"TUI命令行对话\" \\\n\t\t\t\"备份与还原\" \\\n\t\t\t\"更新\" \\\n\t\t\t\"卸载\" \\\n\t\t\t\"退出\") || break\n\n\t\tcase \"$choice\" in\n\t\t\t\"小白模式安装(推荐)\") beginner_mode_install ;;\n\t\t\t\"安装\") install_moltbot ;;\n\t\t\t\"启动\") start_bot ;;\n\t\t\t\"停止\") stop_bot ;;\n\t\t\t\"状态日志查看\") view_logs ;;\n\t\t\t\"换模型\") change_model ;;\n\t\t\"API管理\") openclaw_api_manage_menu ;;\n\t\t\"CLIProxyAPI 管理\") cliproxyapi_manage_menu ;;\n\t\t\"机器人连接对接\") change_tg_bot_code ;;\n\t\t\t\"安装插件\") install_plugin ;;\n\t\t\t\"安装技能\") install_skill ;;\n\t\t\t\"编辑主配置文件\") nano_openclaw_json ;;\n\t\t\t\"配置向导\")\n\t\t\t\topenclaw onboard --install-daemon\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"健康检测与修复\")\n\t\t\t\tgum spin --spinner meter --title \"正在执行健康检测...\" -- openclaw doctor --fix\n\t\t\t\tif sync_openclaw_api_models; then\n\t\t\t\t\tstart_gateway\n\t\t\t\telse\n\t\t\t\t\tui_err \"API 模型同步失败,已中止重启网关,请检查 provider /models 返回后重试\"\n\t\t\t\tfi\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"WebUI访问与设置\") openclaw_webui_menu ;;\n\t\t\t\"TUI命令行对话\")\n\t\t\t\topenclaw tui\n\t\t\t\tbreak_end\n\t\t\t\t;;\n\t\t\t\"备份与还原\") openclaw_backup_restore_menu ;;\n\t\t\t\"更新\") update_moltbot ;;\n\t\t\t\"卸载\") uninstall_moltbot ;;\n\t\t\t\"退出\"|*) break ;;\n\t\tesac\n\tdone\n}\n\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n\tmoltbot_menu\nfi\n"}}}}