# ============================================================================= # Hermes Agent — HuggingFace Space(Docker) # 你只需上传这一个文件! # ============================================================================= FROM python:3.11-slim # ── 系统依赖 ────────────────────────────────────────────────────────────────── RUN apt-get update && apt-get install -y --no-install-recommends \ git curl wget build-essential libssl-dev libffi-dev \ nodejs npm procps \ && rm -rf /var/lib/apt/lists/* # ── 安装 uv ─────────────────────────────────────────────────────────────────── RUN curl -LsSf https://astral.sh/uv/install.sh | sh ENV PATH="/root/.cargo/bin:/root/.local/bin:$PATH" # ── 克隆 hermes-agent ───────────────────────────────────────────────────────── WORKDIR /opt RUN git clone --depth 1 https://github.com/NousResearch/hermes-agent.git hermes-agent WORKDIR /opt/hermes-agent # ── Python 虚拟环境 + 依赖 ──────────────────────────────────────────────────── RUN uv venv /opt/venv --python 3.11 ENV VIRTUAL_ENV=/opt/venv ENV PATH="/opt/venv/bin:$PATH" # mini-swe-agent 已作为目录内嵌在仓库中(不再是 git submodule) # 若目录存在则安装,否则跳过 RUN uv pip install -e ".[all]" && \ ([ -f "./mini-swe-agent/pyproject.toml" ] && uv pip install -e "./mini-swe-agent" || true) && \ uv pip install aiohttp httpx cryptography "gradio==5.29.0" "huggingface_hub>=0.23" # ── 写入 entrypoint.sh ──────────────────────────────────────────────────────── RUN cat > /entrypoint.sh << 'ENTRY_EOF' #!/bin/bash set -e echo "==========================================================" echo " Hermes Agent · HuggingFace Space" echo "==========================================================" # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # 全局路径 # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ export HERMES_HOME=/data/hermes export HF_DATASET_LOCAL=/data/hf_dataset # Dataset 本地镜像 export HF_HUB_DISABLE_PROGRESS_BARS=1 mkdir -p "$HERMES_HOME"/{logs,cron} mkdir -p "$HF_DATASET_LOCAL"/{skills,memories,sessions,logs,cron} mkdir -p /data/workspace # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # 步骤 1: 初始化 HF Dataset(如不存在则创建) # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ echo "[1/6] 初始化 HuggingFace Dataset..." python /dataset_manager.py init echo "✅ Dataset 初始化完成" # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # 步骤 2: 从 Dataset 克隆到本地镜像目录(实时读写基础) # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ echo "[2/6] 从 Dataset 拉取配置..." python /dataset_manager.py pull echo "✅ 配置已拉取" # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # 步骤 3: 将 HERMES_HOME 里的目录软链到 Dataset 本地镜像 # 实现"直接读写 Dataset 文件"效果 # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ echo "[3/6] 建立目录链接(实时读写)..." for DIR in config.yaml SOUL.md skills memories sessions cron; do SRC="$HF_DATASET_LOCAL/$DIR" DST="$HERMES_HOME/$DIR" # 确保 dataset 本地镜像里有这个路径 if [[ "$DIR" == *.* ]]; then # 文件(如 config.yaml, SOUL.md) touch "$SRC" 2>/dev/null || true else mkdir -p "$SRC" fi # 删除旧的并创建软链 rm -rf "$DST" ln -s "$SRC" "$DST" echo " 🔗 $DST -> $SRC" done echo "✅ 目录链接完成" # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # 步骤 4: 生成 .env(从 HF Space Secrets 写入) # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ echo "[4/6] 生成 .env..." python /dataset_manager.py gen_env echo "✅ .env 已生成" # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # 步骤 5: 启动 WeCom Gateway(后台) # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ echo "[5/6] 启动 WeCom Gateway..." if [ -n "${WECOM_BOT_ID}" ] && [ -n "${WECOM_SECRET}" ]; then cd /opt/hermes-agent HERMES_HOME=/data/hermes python -m hermes_cli.main gateway \ >> /data/hermes/logs/gateway.log 2>&1 & echo "✅ WeCom Gateway 已启动 (PID: $!)" else echo "⚠️ 未配置 WECOM_BOT_ID / WECOM_SECRET,跳过 WeCom Gateway" fi # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # 步骤 6: 启动 Dataset 文件监视器(实时推送变更 + 10分钟推 logs) # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ echo "[6/6] 启动文件监视器..." python /dataset_manager.py watch & echo "✅ 文件监视器已启动" # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # 启动 Gradio Web UI(前台,端口 7860) # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ echo "" echo "==========================================================" echo " 🌐 启动 Gradio Web UI → http://0.0.0.0:7860" echo "==========================================================" exec python /webui.py ENTRY_EOF RUN chmod +x /entrypoint.sh # ── 写入 dataset_manager.py ─────────────────────────────────────────────────── RUN cat > /dataset_manager.py << 'DM_EOF' """ dataset_manager.py 统一管理 HuggingFace Dataset 的初始化、拉取、推送、文件监视。 子命令: init — 创建 Dataset(若不存在)并写入初始文件 pull — 从 Dataset 拉取所有文件到本地镜像 push — 将本地镜像推送到 Dataset gen_env — 从环境变量生成 HERMES_HOME/.env watch — 监视文件变化,实时推送;logs 每10分钟推一次 """ import os, sys, time, hashlib, shutil from pathlib import Path from datetime import datetime # ── 配置 ───────────────────────────────────────────────────────────────────── HF_TOKEN = os.environ.get("HF_TOKEN", "") HF_DATASET_REPO = os.environ.get("HF_DATASET_REPO", "") LOCAL_MIRROR = Path(os.environ.get("HF_DATASET_LOCAL", "/data/hf_dataset")) HERMES_HOME = Path(os.environ.get("HERMES_HOME", "/data/hermes")) # 实时双向同步的路径(相对于 LOCAL_MIRROR / HERMES_HOME) REALTIME_PATHS = ["config.yaml", "SOUL.md", "skills", "memories", "sessions", "cron"] # 仅定时推送(不拉取) LOG_ONLY_PATHS = ["logs"] LOG_PUSH_INTERVAL = 600 # 10 分钟推一次 logs # ── 默认初始文件 ────────────────────────────────────────────────────────────── def _build_default_config_yaml(): """ 动态生成首次部署时写入 Dataset 的 config.yaml。 - 所有有效 Provider 写入 custom_providers: 块(name/url/api_key/models) - model: 块只写 provider(指向 custom_providers 里的 name)和 default 模型 - 留空的 Provider / Model 跳过不写 - 仅在 Dataset 里不存在 config.yaml 时才写入,有备份则不覆盖 """ import os as _os # 收集所有有效 Provider providers = [] for p in range(1, 6): name = _os.environ.get(f"PROVIDER_{p}_NAME", "").strip() url = _os.environ.get(f"PROVIDER_{p}_BASE_URL", "").strip() api_key = _os.environ.get(f"PROVIDER_{p}_API_KEY", "").strip() models = [] for m in range(1, 11): mdl = _os.environ.get(f"PROVIDER_{p}_MODEL_{m}", "").strip() if mdl: models.append(mdl) if name and url and api_key: providers.append({"name": name, "url": url, "api_key": api_key, "models": models}) # model: 块 —— 只引用 custom_providers 里的 name,不重复写凭证 if providers: p0 = providers[0] p0_model = p0["models"][0] if p0["models"] else "" model_block = ( "# -- Hermes 主模型(引用下方 custom_providers 中的第一个 Provider)\n" "model:\n" f" provider: {p0['name']}\n" f" default: {p0_model}\n" ) else: model_block = ( "# -- 主模型(未检测到 PROVIDER_1_* 环境变量,请手动配置)\n" "# model:\n" "# provider: my-provider-name\n" "# default: google/gemma-4-31b-it\n" ) # custom_providers: 块 —— 集中管理所有 Provider 的凭证和模型列表 if providers: cp_lines = [ "# -- 自定义 Provider 列表(凭证集中在此,model: 块通过 name 引用)\n", "custom_providers:\n", ] for pv in providers: cp_lines.append(f" - name: {pv['name']}\n") cp_lines.append(f" base_url: {pv['url']}\n") cp_lines.append(f" api_key: {pv['api_key']}\n") if pv["models"]: cp_lines.append(" models:\n") for mdl in pv["models"]: cp_lines.append(f" {mdl}:\n") cp_lines.append( " context_length: 131072\n") else: cp_lines = ["# custom_providers: []\n"] lines = [ "# =============================================================\n", "# Hermes Agent config.yaml\n", "# 直接在 HF Dataset 里编辑此文件,保存后立即生效(无需重启)\n", "# Provider 配置由 HF Space 的 PROVIDER_{1-5}_* 环境变量自动注入\n", "# 有备份时不会覆盖,删掉 Dataset 里的 config.yaml 后重启即可重新生成\n", "# =============================================================\n", "\n", model_block, "\n", "".join(cp_lines), "\n", "# -- 终端后端(HF Space 用 local)\n", "terminal:\n", " backend: local\n", " cwd: /data/workspace\n", " timeout: 180\n", "\n", "# -- 记忆\n", "memory:\n", " memory_enabled: true\n", " user_profile_enabled: true\n", " memory_char_limit: 2200\n", " user_char_limit: 1375\n", "\n", "# -- 显示\n", "display:\n", " tool_progress: all\n", " streaming: false\n", "\n", "# -- 压缩\n", "compression:\n", " enabled: true\n", " threshold: 0.50\n", "\n", "# -- 时区\n", 'timezone: "Asia/Shanghai"\n', "\n", "# -- 安全(无交互终端,关闭审批)\n", "approvals:\n", " mode: off\n", "\n", "# -- WeCom(企业微信)\n", "platforms:\n", " wecom:\n", " enabled: true\n", " extra:\n", ' bot_id: "${WECOM_BOT_ID}"\n', ' secret: "${WECOM_SECRET}"\n', " dm_policy: open\n", " group_policy: open\n", ] return "".join(lines) DEFAULT_CONFIG_YAML = _build_default_config_yaml() DEFAULT_SOUL_MD = """\ # Hermes Agent 你是 Hermes,一个由 Nous Research 开发的智能 AI 助手。 你聪明、友善、乐于助人,具备持续学习和自我改进的能力。 你擅长: - 回答各类问题和提供建议 - 执行代码和分析数据 - 长期记忆用户偏好和上下文 - 通过学习不断创造和改进技能 请始终用清晰、准确、有帮助的方式回应用户。 """ DEFAULT_MEMORY_MD = """\ # Agent 记忆 (暂无记忆,Agent 运行后会自动在此记录重要信息) """ DEFAULT_DATASET_README = """\ --- license: mit tags: - hermes-agent - config --- # Hermes Agent 配置数据集 本 Dataset 由 Hermes Agent HF Space 自动创建和管理。 ## 目录结构 ``` config.yaml ← 主配置(直接编辑即可生效) SOUL.md ← Agent 人格定义 skills/ ← Agent 自创技能 memories/ ← 长期记忆 MEMORY.md sessions/ ← 会话历史 cron/ ← 定时任务 logs/ ← 运行日志(10分钟同步一次) ``` """ # ── 工具函数 ────────────────────────────────────────────────────────────────── def get_api(): if not HF_TOKEN or not HF_DATASET_REPO: print("⚠️ HF_TOKEN 或 HF_DATASET_REPO 未设置,跳过 Dataset 操作") return None try: from huggingface_hub import HfApi return HfApi(token=HF_TOKEN) except ImportError: print("❌ huggingface_hub 未安装") return None def file_hash(path: Path) -> str: if not path.exists() or not path.is_file(): return "" return hashlib.md5(path.read_bytes()).hexdigest() def upload_file(api, local_path: Path, repo_path: str, msg: str = "auto"): try: api.upload_file( path_or_fileobj=str(local_path), path_in_repo=repo_path, repo_id=HF_DATASET_REPO, repo_type="dataset", commit_message=msg, ) return True except Exception as e: print(f" ⚠️ 上传 {repo_path} 失败: {e}") return False # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ def cmd_init(): """创建 Dataset(若不存在)并写入初始文件""" api = get_api() if not api: _write_defaults_locally() return # 检查 Dataset 是否存在 try: from huggingface_hub import DatasetCard api.dataset_info(HF_DATASET_REPO) print(f" Dataset [{HF_DATASET_REPO}] 已存在") except Exception: print(f" Dataset [{HF_DATASET_REPO}] 不存在,正在创建...") api.create_repo( repo_id=HF_DATASET_REPO, repo_type="dataset", private=True, exist_ok=True, ) print(f" ✅ Dataset 已创建(私有)") # 检查并写入初始文件(不覆盖已有文件) _init_remote_files(api) # 写到本地镜像 _write_defaults_locally() def _init_remote_files(api): """检查远程是否有初始文件,没有则上传默认值""" defaults = { "README.md": DEFAULT_DATASET_README, "config.yaml": DEFAULT_CONFIG_YAML, "SOUL.md": DEFAULT_SOUL_MD, "memories/MEMORY.md": DEFAULT_MEMORY_MD, } try: existing = {f.rfilename for f in api.list_repo_files( HF_DATASET_REPO, repo_type="dataset")} except Exception: existing = set() for repo_path, content in defaults.items(): if repo_path not in existing: tmp = Path(f"/tmp/init_{repo_path.replace('/', '_')}") tmp.parent.mkdir(parents=True, exist_ok=True) tmp.write_text(content, encoding="utf-8") upload_file(api, tmp, repo_path, "Init: " + repo_path) print(f" 📝 初始化远程文件: {repo_path}") else: print(f" ✓ 已存在: {repo_path}") def _write_defaults_locally(): """确保本地镜像有默认文件""" LOCAL_MIRROR.mkdir(parents=True, exist_ok=True) defaults = { "config.yaml": DEFAULT_CONFIG_YAML, "SOUL.md": DEFAULT_SOUL_MD, "memories/MEMORY.md": DEFAULT_MEMORY_MD, } for rel, content in defaults.items(): p = LOCAL_MIRROR / rel if not p.exists(): p.parent.mkdir(parents=True, exist_ok=True) p.write_text(content, encoding="utf-8") for d in ["skills", "sessions", "cron", "logs"]: (LOCAL_MIRROR / d).mkdir(parents=True, exist_ok=True) # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ def cmd_pull(): """从 Dataset 拉取所有文件到本地镜像(实时路径)""" api = get_api() if not api: return LOCAL_MIRROR.mkdir(parents=True, exist_ok=True) try: from huggingface_hub import snapshot_download snapshot_download( repo_id=HF_DATASET_REPO, repo_type="dataset", token=HF_TOKEN, local_dir=str(LOCAL_MIRROR), local_dir_use_symlinks=False, ignore_patterns=["*.git*", ".gitattributes", "README.md"], ) print(f" ✅ 已从 Dataset 拉取到 {LOCAL_MIRROR}") except Exception as e: print(f" ⚠️ pull 失败: {e},使用本地默认值") _write_defaults_locally() # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ def cmd_gen_env(): """从环境变量生成 HERMES_HOME/.env""" env_path = HERMES_HOME / ".env" env_path.parent.mkdir(parents=True, exist_ok=True) lines = ["# 由 dataset_manager.py 自动生成,勿手动编辑\n"] # WeCom for k in ["WECOM_BOT_ID", "WECOM_SECRET"]: lines.append(f"{k}={os.environ.get(k, '')}\n") # ── 用户授权 ────────────────────────────────────────────────────────────── # 优先使用精确白名单(WECOM_ALLOWED_USERS),其次全开放(GATEWAY_ALLOW_ALL_USERS) # 两者都未配置时默认全开放,避免所有用户都被拒绝并收到配对码 wecom_allowed = os.environ.get("WECOM_ALLOWED_USERS", "").strip() gateway_allow = os.environ.get("GATEWAY_ALLOW_ALL_USERS", "").strip() if wecom_allowed: # 精确白名单优先:只允许列出的用户(逗号分隔企业微信用户名或ID) lines.append(f"WECOM_ALLOWED_USERS={wecom_allowed}\n") lines.append("GATEWAY_ALLOW_ALL_USERS=false\n") print(f" 🔒 WeCom 白名单已设置: {wecom_allowed}") elif gateway_allow.lower() in ("false", "0", "no"): # 显式关闭全开放 —— 用户需要手动配对 lines.append("GATEWAY_ALLOW_ALL_USERS=false\n") print(" ⚠️ GATEWAY_ALLOW_ALL_USERS=false,未配对用户将被拒绝") else: # 默认全开放(GATEWAY_ALLOW_ALL_USERS 为空或 true 时) lines.append("GATEWAY_ALLOW_ALL_USERS=true\n") print(" ✅ GATEWAY_ALLOW_ALL_USERS=true(所有用户均可使用)") # Providers 1-5 for p in range(1, 6): for k in ["NAME", "BASE_URL", "API_KEY"] + [f"MODEL_{m}" for m in range(1, 11)]: var = f"PROVIDER_{p}_{k}" lines.append(f"{var}={os.environ.get(var, '')}\n") # HF lines.append(f"HF_TOKEN={HF_TOKEN}\n") lines.append(f"HF_DATASET_REPO={HF_DATASET_REPO}\n") env_path.write_text("".join(lines), encoding="utf-8") print(f" ✅ .env 已写入 {env_path}") # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ def cmd_watch(): """ 文件监视器: - 实时路径(config, SOUL, skills, memories, sessions, cron): 检测到变化立即上传到 Dataset - logs:每 10 分钟批量上传一次 """ api = get_api() if not api: print("⚠️ 无 API,监视器退出") return print(" 👁️ 文件监视器启动(实时同步 + 10分钟推 logs)") # 维护文件哈希表 hashes: dict[str, str] = {} last_log_push = time.time() def collect_realtime_files(): files = {} for rel in REALTIME_PATHS: p = LOCAL_MIRROR / rel if p.is_file(): files[str(p.relative_to(LOCAL_MIRROR))] = p elif p.is_dir(): for f in p.rglob("*"): if f.is_file(): files[str(f.relative_to(LOCAL_MIRROR))] = f return files def collect_log_files(): files = {} for rel in LOG_ONLY_PATHS: p = LOCAL_MIRROR / rel if p.is_dir(): for f in p.rglob("*"): if f.is_file(): files[str(f.relative_to(LOCAL_MIRROR))] = f return files # 初始哈希快照 for rel, path in collect_realtime_files().items(): hashes[rel] = file_hash(path) while True: try: # ── 实时检测变化 ──────────────────────────────────────── current = collect_realtime_files() changed = [] for rel, path in current.items(): h = file_hash(path) if hashes.get(rel) != h: hashes[rel] = h changed.append((rel, path)) for rel, path in changed: ts = datetime.now().strftime("%H:%M:%S") ok = upload_file(api, path, rel, f"Realtime sync [{ts}]: {rel}") if ok: print(f" ☁️ [{ts}] 已推送: {rel}") # ── 每 10 分钟推 logs ─────────────────────────────────── if time.time() - last_log_push >= LOG_PUSH_INTERVAL: log_files = collect_log_files() if log_files: ts = datetime.now().strftime("%Y-%m-%d %H:%M") for rel, path in log_files.items(): upload_file(api, path, rel, f"Log sync [{ts}]") print(f" 📋 [{ts}] logs 已推送 ({len(log_files)} 个文件)") last_log_push = time.time() except Exception as e: print(f" ⚠️ 监视器异常: {e}") time.sleep(5) # 每 5 秒检测一次变化 # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ if __name__ == "__main__": cmd = sys.argv[1] if len(sys.argv) > 1 else "" {"init": cmd_init, "pull": cmd_pull, "gen_env": cmd_gen_env, "watch": cmd_watch}.get(cmd, lambda: print( "用法: python dataset_manager.py [init|pull|gen_env|watch]"))() DM_EOF # ── 写入 webui.py ───────────────────────────────────────────────────────────── RUN cat > /webui.py << 'WEBUI_EOF' """ webui.py — Hermes Agent Gradio Web UI 端口: 7860 """ import os, sys, subprocess, time, shutil from pathlib import Path import gradio as gr HERMES_HOME = Path(os.environ.get("HERMES_HOME", "/data/hermes")) HF_DS_LOCAL = Path(os.environ.get("HF_DATASET_LOCAL", "/data/hf_dataset")) HERMES_SRC = Path("/opt/hermes-agent") sys.path.insert(0, str(HERMES_SRC)) # ── 辅助函数 ────────────────────────────────────────────────────────────────── def get_models(): models = [] for p in range(1, 6): name = os.environ.get(f"PROVIDER_{p}_NAME", "").strip() url = os.environ.get(f"PROVIDER_{p}_BASE_URL", "").strip() api_key = os.environ.get(f"PROVIDER_{p}_API_KEY", "").strip() if not (name and url and api_key): continue for m in range(1, 11): model = os.environ.get(f"PROVIDER_{p}_MODEL_{m}", "").strip() if model: models.append((p, model, f"[{name}] {model}")) return models def run_hermes(message, history, provider_idx, model_name): env = os.environ.copy() env["HERMES_HOME"] = str(HERMES_HOME) env["OPENAI_BASE_URL"] = os.environ.get(f"PROVIDER_{provider_idx}_BASE_URL", "") env["OPENAI_API_KEY"] = os.environ.get(f"PROVIDER_{provider_idx}_API_KEY", "") ctx = [] for m in history[-6:]: role = m.get("role","") content = m.get("content","") if role == "user" and content: ctx.append(f"User: {content}") elif role == "assistant" and content: ctx.append(f"Assistant: {content}") ctx.append(f"User: {message}") prompt = "\n".join(ctx) try: r = subprocess.run( [sys.executable, str(HERMES_SRC/"run_agent.py"), "--once", "--model", model_name, "--message", prompt], capture_output=True, text=True, timeout=120, env=env, cwd=str(HERMES_SRC) ) out = r.stdout.strip() return out if out else (r.stderr.strip()[:400] or "(无回复)") except subprocess.TimeoutExpired: return "⚠️ 超时(120s),请重试" except Exception as e: return f"⚠️ 调用失败: {e}" # ── 读写 Dataset 本地镜像文件(软链,实时生效)────────────────────────────── def r(path: Path, default=""): return path.read_text("utf-8") if path.exists() else default def w(path: Path, content: str, label=""): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, "utf-8") return f"✅ {label or path.name} 已保存(文件监视器将自动同步到 Dataset)" cfg_path = HF_DS_LOCAL / "config.yaml" soul_path = HF_DS_LOCAL / "SOUL.md" memory_path = HF_DS_LOCAL / "memories" / "MEMORY.md" log_path = HERMES_HOME / "logs" / "gateway.log" def gw_status(): r2 = subprocess.run(["pgrep","-f","hermes.*gateway"],capture_output=True,text=True) return "✅ WeCom Gateway 运行中" if r2.returncode==0 else "⚠️ WeCom Gateway 未运行" def restart_gw(): subprocess.run(["pkill","-f","hermes.*gateway"],capture_output=True) time.sleep(2) if not (os.environ.get("WECOM_BOT_ID") and os.environ.get("WECOM_SECRET")): return "⚠️ WECOM_BOT_ID / WECOM_SECRET 未配置" env = os.environ.copy() env["HERMES_HOME"] = str(HERMES_HOME) log_path.parent.mkdir(parents=True, exist_ok=True) with open(log_path,"a") as lf: proc = subprocess.Popen( [sys.executable,"-m","hermes_cli.main","gateway"], env=env, cwd=str(HERMES_SRC), stdout=lf, stderr=lf) time.sleep(3) return f"✅ Gateway 已重启 (PID: {proc.pid})" def manual_push(): r2 = subprocess.run([sys.executable,"/dataset_manager.py","push"], capture_output=True,text=True,timeout=60) return r2.stdout + r2.stderr def manual_pull(): r2 = subprocess.run([sys.executable,"/dataset_manager.py","pull"], capture_output=True,text=True,timeout=60) return r2.stdout + r2.stderr # ── 构建 UI ─────────────────────────────────────────────────────────────────── all_models = get_models() model_labels = [m[2] for m in all_models] or ["(未配置模型)"] def chat_fn(msg, history, model_choice, sp): if not msg.strip(): yield history, "" return idx = next((m[0] for m in all_models if m[2]==model_choice), 1) mname = next((m[1] for m in all_models if m[2]==model_choice), model_choice) if sp.strip(): soul_path.parent.mkdir(parents=True,exist_ok=True) soul_path.write_text(sp,"utf-8") yield history + [{"role":"user","content":msg},{"role":"assistant","content":"🔄 思考中..."}], "" reply = run_hermes(msg, history, idx, mname) yield history + [{"role":"user","content":msg},{"role":"assistant","content":reply}], "" with gr.Blocks(title="Hermes Agent", theme=gr.themes.Soft()) as demo: gr.Markdown("# ☤ Hermes Agent\n> [Nous Research](https://nousresearch.com) · HuggingFace Space") with gr.Tabs(): # ── 聊天 ───────────────────────────────────────────────────────────── with gr.Tab("💬 聊天"): with gr.Row(): with gr.Column(scale=3): chatbot = gr.Chatbot(height=500, show_copy_button=True, type="messages") with gr.Row(): inp = gr.Textbox(placeholder="输入消息…",show_label=False,lines=2,scale=5) gr.Button("发送",variant="primary",scale=1).click( chat_fn,[inp,chatbot, gr.Dropdown(choices=model_labels,value=model_labels[0],label="模型",interactive=True), gr.Textbox(value=lambda:r(soul_path,"你是一个有用的助手"),label="系统提示词",lines=6)], [chatbot,inp]) gr.Button("🗑️ 清空",size="sm").click(lambda:[],outputs=[chatbot]) with gr.Column(scale=1): model_dd = gr.Dropdown(choices=model_labels,value=model_labels[0],label="选择模型") soul_box = gr.Textbox(value=lambda:r(soul_path),label="系统提示词(SOUL.md)",lines=10,interactive=True) save_soul = gr.Button("💾 保存 SOUL.md",size="sm") soul_st = gr.Textbox(interactive=False,lines=1,label="") save_soul.click(lambda c: w(soul_path,c,"SOUL.md"), [soul_box],[soul_st]) inp.submit(chat_fn,[inp,chatbot,model_dd,soul_box],[chatbot,inp]) # ── WeCom ──────────────────────────────────────────────────────────── with gr.Tab("📱 WeCom 企业微信"): gr.Markdown(""" ### WeCom AI Bot(WebSocket 模式,无需公网端点) **配置步骤:** 1. [企业微信管理后台](https://work.weixin.qq.com/wework_admin/frame) → 应用管理 → 创建应用 → **AI Bot** 2. 复制 Bot ID 和 Secret 3. 在 HF Space Settings → **Secrets** 中添加: - `WECOM_BOT_ID` = Bot ID - `WECOM_SECRET` = Secret 4. (可选)在 HF Space Settings → **Variables** 中添加用户白名单: - `WECOM_ALLOWED_USERS` = 允许使用的企业微信用户名,逗号分隔,如 `张三,李四` - 不填则默认**所有用户**均可使用(`GATEWAY_ALLOW_ALL_USERS=true`) - 若设为 `GATEWAY_ALLOW_ALL_USERS=false` 且不填白名单,则所有用户需手动配对 5. 点击下方"重启 Gateway" """) with gr.Row(): gw_box = gr.Textbox(label="Gateway 状态",interactive=False,lines=2) with gr.Column(): gr.Button("🔍 检查状态").click(gw_status,outputs=[gw_box]) gr.Button("🔄 重启 Gateway",variant="primary").click(restart_gw,outputs=[gw_box]) # ── 记忆 ───────────────────────────────────────────────────────────── with gr.Tab("🧠 记忆管理"): gr.Markdown("直接编辑 MEMORY.md,保存后文件监视器自动同步到 Dataset(无需手动 Push)。") mem_box = gr.Textbox(value=lambda:r(memory_path),label="MEMORY.md",lines=20,interactive=True) with gr.Row(): gr.Button("💾 保存",variant="primary").click( lambda c: w(memory_path,c,"MEMORY.md"),[mem_box],[gr.Textbox(label="",lines=1)]) gr.Button("🔄 刷新").click(lambda:r(memory_path),outputs=[mem_box]) # ── 配置 ───────────────────────────────────────────────────────────── with gr.Tab("⚙️ 配置文件"): gr.Markdown(""" 直接编辑 `config.yaml`,保存后文件监视器**实时同步**到 HF Dataset。 **WeCom Gateway** 重启后读取新配置;**Web UI 聊天** 刷新页面即生效。 """) cfg_box = gr.Code(value=lambda:r(cfg_path),language="yaml",label="config.yaml",lines=30,interactive=True) with gr.Row(): gr.Button("💾 保存 config.yaml",variant="primary").click( lambda c: w(cfg_path,c,"config.yaml"),[cfg_box],[gr.Textbox(label="",lines=1)]) gr.Button("🔄 刷新").click(lambda:r(cfg_path),outputs=[cfg_box]) # ── Dataset ─────────────────────────────────────────────────────────── with gr.Tab("☁️ Dataset 同步"): gr.Markdown(""" **自动同步策略:** - `config.yaml` / `SOUL.md` / `skills` / `memories` / `sessions` / `cron` — 文件变化时**立即**推送到 Dataset - `logs` — 每 **10 分钟**批量推送一次 手动操作仅在需要时使用(如强制覆盖或拉取他人修改)。 """) with gr.Row(): gr.Button("📥 手动 Pull(从 Dataset 覆盖本地)").click(manual_pull,outputs=[gr.Textbox(label="输出",lines=10)]) gr.Button("📤 手动 Push(立即推送全部)",variant="primary").click(manual_push,outputs=[gr.Textbox(label="输出",lines=10)]) # ── 日志 ───────────────────────────────────────────────────────────── with gr.Tab("📋 日志"): log_box = gr.Textbox(value=lambda: "\n".join( (log_path.read_text("utf-8",errors="replace").splitlines()[-80:] if log_path.exists() else ["(暂无日志)"])), label="gateway.log(最后80行)",lines=25,interactive=False) gr.Button("🔄 刷新日志").click( lambda: "\n".join( log_path.read_text("utf-8",errors="replace").splitlines()[-80:] if log_path.exists() else ["(暂无日志)"]), outputs=[log_box]) gr.Markdown("---\n**GitHub**: [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) · MIT License") if __name__ == "__main__": demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_error=True) WEBUI_EOF # ── Space README(HF Space card,必须有)──────────────────────────────────── RUN cat > /README_SPACE.md << 'README_EOF' --- title: Hermes Agent emoji: ☤ colorFrom: purple colorTo: blue sdk: docker pinned: false license: mit --- # ☤ Hermes Agent > 自成长 AI Agent · [Nous Research](https://nousresearch.com) · [GitHub](https://github.com/NousResearch/hermes-agent) **只需上传这一个 Dockerfile 即可完成部署。** 功能: - 🌐 Gradio Web UI 聊天界面 - 📱 WeCom 企业微信 AI Bot(WebSocket,无需公网) - ☁️ HF Dataset 实时配置持久化 - 🔑 支持任意 OpenAI 兼容的第三方大模型 README_EOF # ── 创建工作目录 ────────────────────────────────────────────────────────────── RUN mkdir -p /data/workspace /data/hermes/logs /data/hf_dataset EXPOSE 7860 CMD ["/entrypoint.sh"]