#!/bin/bash # Hermes Agent 终端管理脚本 (轻量版) # 设计哲学:极简、直观、调用原生功能 # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' # 确保 hermes 命令可用 (处理环境变量未加载的情况) if ! command -v hermes >/dev/null 2>&1; then if [ -d "$HOME/.hermes/hermes-agent/venv/bin" ]; then export PATH="$HOME/.hermes/hermes-agent/venv/bin:$PATH" fi fi # 检查是否安装 # --- 科技lion 增强版 API 管理核心 --- CONFIG_FILE="$HOME/.hermes/config.yaml" config_tool() { python3 - "$CONFIG_FILE" "$@" <<'EOF' import sys, yaml, json, os path = sys.argv[1] action = sys.argv[2] def load(): if not os.path.exists(path): return {} try: with open(path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) or {} except: return {} def save(d): with open(path, 'w', encoding='utf-8') as f: yaml.dump(d, f, sort_keys=False, allow_unicode=True) try: data = load() if action == "get_info": m = data.get('model', {}) res = {"m": m.get('default', '-'), "p": m.get('provider', '-'), "u": m.get('base_url', '-')} print(json.dumps(res)) elif action == "list_p": print(json.dumps(data.get('custom_providers', []))) elif action == "add_p": n, u, k, m = sys.argv[3:7] ps = data.get('custom_providers', []) if not isinstance(ps, list): ps = [] ps = [p for p in ps if p.get('name') != n] ps.append({"name": n, "base_url": u, "api_key": k, "model": m}) data['custom_providers'] = ps save(data) elif action == "bulk_add": n_base, u, k, models_json = sys.argv[3:7] new_m_ids = json.loads(models_json) ps = data.get('custom_providers', []) if not isinstance(ps, list): ps = [] # 移除旧的同前缀条目 ps = [p for p in ps if not (isinstance(p, dict) and p.get('name', '').startswith(n_base + "/"))] # 移除可能存在的同名根条目 ps = [p for p in ps if p.get('name') != n_base] for m_id in new_m_ids: ps.append({"name": f"{n_base}/{m_id}", "base_url": u, "api_key": k, "model": m_id}) data['custom_providers'] = ps save(data) elif action == "del_p": n = sys.argv[3] ps = data.get('custom_providers', []) if isinstance(ps, list): data['custom_providers'] = [p for p in ps if p.get('name') != n and not p.get('name', '').startswith(n + "/")] save(data) elif action == "list_groups": ps = data.get('custom_providers', []) groups = [] seen = set() for p in (ps if isinstance(ps, list) else []): name = p.get('name', '') g = name.split('/')[0] if '/' in name else name if g and g not in seen: seen.add(g) cnt = sum(1 for x in ps if x.get('name', '') == g or x.get('name', '').startswith(g + '/')) groups.append({"name": g, "count": cnt}) print(json.dumps(groups)) elif action == "switch": n, u, k, m = sys.argv[3:7] data['model'] = {"default": m, "provider": "custom", "base_url": u, "api_key": k} save(data) except Exception as e: # 确保出错时返回合法的 JSON 避免 Bash 报错 print(json.dumps([])) sys.exit(1) EOF } install_gum() { if command -v gum >/dev/null 2>&1; then return 0 fi echo -e "${YELLOW}正在安装 gum (交互式选择器)...${NC}" if command -v apt >/dev/null 2>&1; then mkdir -p /etc/apt/keyrings curl -fsSL https://repo.charm.sh/apt/gpg.key | gpg --dearmor -o /etc/apt/keyrings/charm.gpg 2>/dev/null echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | tee /etc/apt/sources.list.d/charm.list > /dev/null apt update -qq && apt install -y -qq gum elif command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then cat > /etc/yum.repos.d/charm.repo <<'REPO' [charm] name=Charm baseurl=https://repo.charm.sh/yum/ enabled=1 gpgcheck=1 gpgkey=https://repo.charm.sh/yum/gpg.key REPO rpm --import https://repo.charm.sh/yum/gpg.key if command -v dnf >/dev/null 2>&1; then dnf install -y gum; else yum install -y gum; fi elif command -v zypper >/dev/null 2>&1; then zypper --non-interactive install gum fi } hermes_model_probe() { local target_model="$1" local ps_json="$2" local probe_timeout=15 local provider_name request_model base_url api_key # 从 custom_providers 中查找对应条目 provider_name="$target_model" local entry=$(echo "$ps_json" | jq -c --arg n "$provider_name" '.[] | select(.name == $n)' 2>/dev/null) if [ -z "$entry" ]; then HERMES_PROBE_STATUS="ERROR" HERMES_PROBE_MESSAGE="未找到模型配置" HERMES_PROBE_LATENCY="-" HERMES_PROBE_REPLY="-" return 1 fi base_url=$(echo "$entry" | jq -r .base_url) api_key=$(echo "$entry" | jq -r .api_key) request_model=$(echo "$entry" | jq -r .model) base_url="${base_url%/}" local tmp_response tmp_response=$(mktemp) # 使用 Python 探测,精确计时 local probe_result probe_result=$(python3 - "$base_url" "$api_key" "$request_model" "$tmp_response" "$probe_timeout" <<'PYEOF' import sys, time, json try: import urllib.request, urllib.error except ImportError: print("1|0|0") sys.exit(0) base_url, api_key, model, resp_path, timeout = sys.argv[1:6] timeout = int(timeout) url = base_url + "/chat/completions" payload = json.dumps({"model": model, "messages": [{"role": "user", "content": "hi"}], "temperature": 0, "max_tokens": 16}).encode() req = urllib.request.Request(url, data=payload, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {api_key}", }, method="POST") start = time.time() body = b"" status = 0 exit_code = 0 try: with urllib.request.urlopen(req, timeout=timeout) as resp: status = getattr(resp, "status", 200) body = resp.read() except urllib.error.HTTPError as e: status = getattr(e, "code", 0) or 0 body = e.read() exit_code = 22 except Exception as e: body = str(e).encode("utf-8", errors="replace") exit_code = 1 elapsed = int((time.time() - start) * 1000) with open(resp_path, "wb") as f: f.write(body) print(f"{exit_code}|{status}|{elapsed}") PYEOF ) local p_exit p_http p_latency p_exit=${probe_result%%|*} p_http=${probe_result#*|} p_http=${p_http%%|*} p_latency=${probe_result##*|} # 提取回复摘要 local reply_preview reply_preview=$(python3 - "$tmp_response" <<'PYEOF' import json, sys from pathlib import Path raw = Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace").strip() reply = "" if raw: try: data = json.loads(raw) if isinstance(data, dict): choices = data.get("choices") or [] if choices and isinstance(choices[0], dict): msg = choices[0].get("message") or {} if isinstance(msg, dict): reply = msg.get("content") or "" if not reply: for key in ("error", "message", "detail"): v = data.get(key) if isinstance(v, str) and v.strip(): reply = v.strip(); break if isinstance(v, dict): n = v.get("message") if isinstance(n, str) and n.strip(): reply = n.strip(); break except: reply = raw reply = " ".join(str(reply).split())[:120] print(reply if reply else "(空返回)") PYEOF ) rm -f "$tmp_response" if [ "$p_exit" = "0" ] && [ "$p_http" -ge 200 ] 2>/dev/null && [ "$p_http" -lt 300 ] 2>/dev/null; then HERMES_PROBE_STATUS="OK" HERMES_PROBE_MESSAGE="HTTP ${p_http}" HERMES_PROBE_LATENCY="${p_latency}ms" HERMES_PROBE_REPLY="$reply_preview" return 0 else HERMES_PROBE_STATUS="FAIL" HERMES_PROBE_MESSAGE="HTTP ${p_http:-0} / exit ${p_exit:-1}" HERMES_PROBE_LATENCY="${p_latency:-?}ms" HERMES_PROBE_REPLY="$reply_preview" return 1 fi } hermes_probe_status_line() { local status_text="$1" local color_ok='\033[32m' color_fail='\033[31m' reset='\033[0m' if [ "$status_text" = "可用" ]; then printf "%b最小检测结果:%s%b\n" "$color_ok" "$status_text" "$reset" else printf "%b最小检测结果:%s%b\n" "$color_fail" "$status_text" "$reset" fi } api_management_submenu() { while true; do clear info=$(config_tool get_info) echo -e "${BLUE}=======================================${NC}" echo -e " ${PURPLE}API & 模型管理 (OpenClaw 风格)${NC}" echo -e "${BLUE}=======================================${NC}" echo -e "${CYAN}当前激活模型:${NC} ${GREEN}$(echo $info | jq -r .m)${NC}" echo -e "---------------------------------------" echo -e "${CYAN}已配置 API 列表:${NC}" ps_list=$(config_tool list_p) if [ "$(echo "$ps_list" | jq '. | length')" -eq 0 ]; then echo -e " ${YELLOW}(暂无配置)${NC}" else echo "$ps_list" | jq -r '.[] | " ● [\(.name)] \(.model) | \(.base_url)"' fi echo -e "---------------------------------------" echo -e "1. ${YELLOW}切换模型 (带测速)${NC}" echo -e "2. 添加 API 供应商 (自动同步)${NC}" echo -e "3. 删除 API 供应商" echo -e "0. 返回主菜单" echo -e "---------------------------------------" read -p "选择序号: " sub_choice case "$sub_choice" in 1) local orange="#FF8C00" local ps_json models_list model_count default_model selected_model confirm_switch ps_json=$(config_tool list_p) model_count=$(echo "$ps_json" | jq '. | length') if [ "$model_count" -eq 0 ] 2>/dev/null || [ -z "$model_count" ]; then echo -e "${RED}无 API 配置! 请先添加供应商。${NC}" sleep 1 continue fi # 构建带编号的模型列表 models_list=$(echo "$ps_json" | jq -r '.[].name' | awk '{print "(" NR ") " $0}') default_model=$(config_tool get_info | jq -r .m) while true; do clear install_gum # 若 gum 不可用,降级为手动输入 if ! command -v gum >/dev/null 2>&1; then echo "--- 模型管理 ---" echo "当前可用模型:" echo "$models_list" echo "当前默认:${default_model}" echo "----------------" read -e -p "请输入模型编号或名称 (输入 0 退出): " selected_model if [ "$selected_model" = "0" ]; then break fi if [ -z "$selected_model" ]; then echo "错误:不能为空,请重试。" sleep 1 continue fi # 如果输入的是纯数字,从列表中取名称 if [[ "$selected_model" =~ ^[0-9]+$ ]]; then selected_model=$(echo "$ps_json" | jq -r --argjson i "$((selected_model-1))" '.[$i].name // empty') if [ -z "$selected_model" ]; then echo "序号无效,请重试。" sleep 1 continue fi fi else # gum 模式 — 完全复刻 openclaw 风格 gum style --foreground "$orange" --bold "模型管理" gum style --foreground "$orange" "可用模型:${model_count}" gum style --foreground "$orange" "当前默认:${default_model}" echo "" gum style --faint "↑↓ 选择 / 输入搜索 / Enter 测试 / Esc 退出" echo "" selected_model=$(echo "$models_list" | gum filter \ --placeholder "搜索模型(如 cli-api/gpt-4o)" \ --prompt "选择模型 > " \ --indicator "➜ " \ --prompt.foreground "$orange" \ --indicator.foreground "$orange" \ --cursor-text.foreground "$orange" \ --match.foreground "$orange" \ --header "" \ --height 35) if [ -z "$selected_model" ] || echo "$selected_model" | head -n 1 | grep -iqE '^(error|usage|gum:)'; then echo "操作已取消,正在退出..." break fi fi # 去掉编号前缀 "(N) " selected_model=$(echo "$selected_model" | sed -E 's/^\([0-9]+\)[[:space:]]+//') echo "" echo "正在检测模型: $selected_model" if hermes_model_probe "$selected_model" "$ps_json"; then hermes_probe_status_line "可用" else hermes_probe_status_line "不可用" fi echo "状态:$HERMES_PROBE_MESSAGE" echo "延迟:$HERMES_PROBE_LATENCY" echo "摘要:$HERMES_PROBE_REPLY" echo "" printf "是否切换到该模型?[y/N,Esc 返回列表]: " IFS= read -rsn1 confirm_switch echo "" if [ "$confirm_switch" = $'\x1b' ]; then confirm_switch="no" else case "$confirm_switch" in [yY]) IFS= read -rsn1 -t 5 _enter_key confirm_switch="yes" ;; *) confirm_switch="no" ;; esac fi if [ "$confirm_switch" != "yes" ]; then echo "已返回模型选择列表。" sleep 1 continue fi # 执行切换 local entry_data entry_data=$(echo "$ps_json" | jq -c --arg n "$selected_model" '.[] | select(.name == $n)') local sw_u sw_k sw_m sw_u=$(echo "$entry_data" | jq -r .base_url) sw_k=$(echo "$entry_data" | jq -r .api_key) sw_m=$(echo "$entry_data" | jq -r .model) echo "正在切换模型为: $selected_model ..." config_tool switch "$selected_model" "$sw_u" "$sw_k" "$sw_m" # 重启 gateway echo -e "${YELLOW}正在重启 Gateway...${NC}" hermes gateway stop >/dev/null 2>&1 hermes gateway start >/dev/null 2>&1 echo -e "${GREEN}✅ 模型已切换为: $sw_m${NC}" sleep 2 break done ;; 2) echo -e "${CYAN}--- 添加新 API 供应商 ---${NC}" read -p "请输入供应商名称 (如: DeepSeek): " n [ -z "$n" ] && continue read -p "请输入 Base URL (如: https://api.deepseek.com/v1): " u [ -z "$u" ] && continue u="${u%/}" echo -ne "${YELLOW}请输入 API Key (输入隐藏): ${NC}" read -s k echo "" [ -z "$k" ] && continue echo -e "${YELLOW}🔍 正在获取完整模型列表...${NC}" m_json=$(curl -s -m 10 -H "Authorization: Bearer $k" "$u/models") # 提取所有 ID m_list_str=$(echo "$m_json" | jq -r '.data[].id' 2>/dev/null | sort) if [ -n "$m_list_str" ]; then # 转换为数组 m_array=() while read -r line; do m_array+=("$line"); done <<< "$m_list_str" m_count=${#m_array[@]} echo -e "${GREEN}✅ 发现 $m_count 个模型。请选择一个作为当前默认:${NC}" PS3="请输入序号: " select m_default in "${m_array[@]}"; do [ -n "$m_default" ] && break done echo -e "---------------------------------------" read -p "是否同时添加该供应商的所有 $m_count 个模型?(y/N): " bulk_confirm if [[ "$bulk_confirm" =~ ^[Yy]$ ]]; then # 转换数组为 JSON m_json_list=$(echo "$m_list_str" | jq -R . | jq -s -c .) config_tool bulk_add "$n" "$u" "$k" "$m_json_list" config_tool switch "$n/$m_default" "$u" "$k" "$m_default" echo -e "${GREEN}✅ 已全量导入 $m_count 个模型。${NC}" else config_tool add_p "$n" "$u" "$k" "$m_default" echo -e "${GREEN}✅ 已添加单个模型: $m_default${NC}" fi else echo -e "${RED}❌ 无法获取列表。${NC}" read -p "请手动输入模型 ID: " m_manual [ -n "$m_manual" ] && config_tool add_p "$n" "$u" "$k" "$m_manual" fi sleep 2 ;; 3) echo -e "${CYAN}已配置的供应商分组:${NC}" groups_json=$(config_tool list_groups) g_count=$(echo "$groups_json" | jq '. | length') if [ "$g_count" -eq 0 ]; then echo -e " ${YELLOW}(暂无配置)${NC}" sleep 1 continue fi # 列出供应商分组 g_names=() while read -r row; do g_name=$(echo "$row" | jq -r .name) g_cnt=$(echo "$row" | jq -r .count) g_names+=("$g_name") echo -e " ${GREEN}${#g_names[@]}.${NC} $g_name (${g_cnt} 个模型)" done < <(echo "$groups_json" | jq -c '.[]') echo -e " ${GREEN}0.${NC} 取消" read -p "选择要删除的供应商序号: " d_idx if [ "$d_idx" == "0" ] || [ -z "$d_idx" ]; then continue; fi d_name="${g_names[$((d_idx-1))]}" if [ -n "$d_name" ]; then read -p "确认删除 [$d_name] 及其所有模型? (y/N): " del_confirm if [[ "$del_confirm" =~ ^[Yy]$ ]]; then config_tool del_p "$d_name" echo -e "${RED}🗑️ 已删除 $d_name${NC}" sleep 1 fi fi ;; 0) break ;; esac done } check_installed() { if command -v hermes >/dev/null 2>&1; then return 0; else return 1; fi } # 获取版本号 get_version() { if check_installed; then hermes --version | head -n 1 fi } # 获取网关状态 get_gateway_status() { if check_installed; then # 匹配后台 gateway 进程或 systemd 服务 if pgrep -f "hermes_cli.main gateway" > /dev/null || pgrep -f "hermes gateway run" > /dev/null || pgrep -f "hermes-gateway" > /dev/null; then echo -e "${GREEN}运行中${NC}" else echo -e "${RED}已停止${NC}" fi else echo -e "${RED}未安装${NC}" fi } refresh_hermes_path() { if ! command -v hermes >/dev/null 2>&1; then if [ -d "$HOME/.hermes/hermes-agent/venv/bin" ]; then export PATH="$HOME/.hermes/hermes-agent/venv/bin:$PATH" fi fi } # 主菜单UI show_menu() { clear echo -e "${CYAN}=================================================${NC}" echo -e "${YELLOW} Hermes Agent 终端管理工具 ${NC}" echo -e "${CYAN}=================================================${NC}" echo -e " 运行状态 : $(get_gateway_status)" echo -e " 当前版本 : $(get_version)" echo -e "${CYAN}-------------------------------------------------${NC}" echo -e "${GREEN}1.${NC} 安装 Hermes Agent" echo -e "${GREEN}2.${NC} 启动 Gateway (消息网关/后台服务)" echo -e "${GREEN}3.${NC} 停止 Gateway" echo -e "${GREEN}4.${NC} API/模型管理 (提供商与模型切换)" echo -e "${GREEN}5.${NC} 启动终端对话UI (Interactive Chat)" echo -e "${GREEN}6.${NC} 运行初始化配置向导 (Setup Wizard)" echo -e "${GREEN}7.${NC} 检查并更新 Hermes" echo -e "${GREEN}8.${NC} 卸载 Hermes" echo -e "${GREEN}0.${NC} 退出" echo -e "${CYAN}=================================================${NC}" if ! read -p " 请输入数字 [0-8]: " choice; then echo -e "\n${GREEN}退出脚本。${NC}" exit 0 fi echo "" case $choice in 1) echo -e "${YELLOW}开始安装 Hermes Agent...${NC}" curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash refresh_hermes_path hermes gateway install hermes gateway start ;; 2) if check_installed; then echo -e "${YELLOW}正在启动 Gateway...${NC}" systemctl --user start hermes-gateway hermes gateway stop hermes gateway start else echo -e "${RED}请先安装 Hermes。${NC}"; fi ;; 3) if check_installed; then echo -e "${YELLOW}正在停止 Gateway...${NC}" hermes gateway stop else echo -e "${RED}请先安装 Hermes。${NC}"; fi ;; 4) if check_installed; then echo -e "${YELLOW}进入模型配置向导...${NC}" api_management_submenu else echo -e "${RED}请先安装 Hermes。${NC}"; fi ;; 5) if check_installed; then echo -e "${YELLOW}即将进入交互式终端,输入 /exit 即可退出返回。${NC}" sleep 1 hermes else echo -e "${RED}请先安装 Hermes。${NC}"; fi ;; 6) if check_installed; then echo -e "${YELLOW}正在启动初始化配置向导...${NC}" hermes setup else echo -e "${RED}请先安装 Hermes。${NC}"; fi ;; 7) if check_installed; then echo -e "${YELLOW}正在检查更新...${NC}" hermes update else echo -e "${RED}请先安装 Hermes。${NC}"; fi ;; 8) if check_installed; then read -p "确定要卸载 Hermes 吗?所有数据将被清除。(y/N): " confirm if [[ "$confirm" =~ ^[Yy]$ ]]; then hermes uninstall else echo "已取消。"; fi else echo -e "${RED}请先安装 Hermes。${NC}"; fi ;; 0) echo -e "${GREEN}感谢使用,再见!${NC}" exit 0 ;; *) echo -e "${RED}输入错误,请重新选择。${NC}" ;; esac echo "" read -p "按回车键返回主菜单..." } # 主循环 while true; do show_menu done