1 line
1.0 MiB
Plaintext
1 line
1.0 MiB
Plaintext
{"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"}}}} |