From d25bb3efd5f0369267b69a3a90779ff46b6709d5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 13 Apr 2026 14:56:51 +0800 Subject: [PATCH] first commit --- .gitignore | 3 + docker-compose.cn.yml | 37 ++++++ hermes-agent | 1 + hermes-docker.env.example | 54 +++++++++ start-docker-cn.sh | 239 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 334 insertions(+) create mode 100644 .gitignore create mode 100644 docker-compose.cn.yml create mode 160000 hermes-agent create mode 100644 hermes-docker.env.example create mode 100755 start-docker-cn.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f2672a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# 本地密钥与数据(配置模板请用 hermes-docker.env.example) +hermes-docker.env +open-webui-data/ diff --git a/docker-compose.cn.yml b/docker-compose.cn.yml new file mode 100644 index 0000000..e8f07fe --- /dev/null +++ b/docker-compose.cn.yml @@ -0,0 +1,37 @@ +services: + hermes: + image: hermes-agent:cn-local + container_name: hermes + restart: unless-stopped + command: gateway run + ports: + - "8642:8642" + volumes: + - /Users/dannier/.hermes:/opt/data + shm_size: "1gb" + env_file: + - /Users/dannier/Desktop/living/hh/hermes-docker.env + environment: + - TZ=Asia/Shanghai + - API_SERVER_ENABLED=true + - API_SERVER_HOST=0.0.0.0 + - "API_SERVER_KEY=change-me-local-dev" + - "API_SERVER_CORS_ORIGINS=http://127.0.0.1:8642,http://localhost:8642,http://localhost:3000,http://127.0.0.1:3000" + + open-webui: + image: ghcr.io/open-webui/open-webui:main + container_name: hermes-open-webui + restart: unless-stopped + depends_on: + - hermes + ports: + - "3000:8080" + environment: + - "OPENAI_API_BASE_URL=http://hermes:8642/v1" + - "OPENAI_API_KEY=change-me-local-dev" + - "WEBUI_SECRET_KEY=dev-change-me-in-production" + - ENABLE_SIGNUP=true + - ENABLE_OLLAMA_API=false + volumes: + - /Users/dannier/Desktop/living/hh/open-webui-data:/app/backend/data + diff --git a/hermes-agent b/hermes-agent new file mode 160000 index 0000000..fff237e --- /dev/null +++ b/hermes-agent @@ -0,0 +1 @@ +Subproject commit fff237e11198a8918086bc4a2f53300a0a48dfcf diff --git a/hermes-docker.env.example b/hermes-docker.env.example new file mode 100644 index 0000000..25ee8ad --- /dev/null +++ b/hermes-docker.env.example @@ -0,0 +1,54 @@ +# ============================================================================= +# Hermes 本地 Docker 配置(可见、可改) +# 用法:复制为 hermes-docker.env 后编辑;start-docker-cn.sh 会自动读取 +# cp hermes-docker.env.example hermes-docker.env +# ============================================================================= + +# --- Hermes HTTP API(给 Open WebUI / curl 使用)--- +API_PORT=8642 +HERMES_API_KEY=change-me-local-dev + +# --- LLM 推理(必填其一,否则对话报错:No inference provider configured)--- +# start-docker-cn.sh 会把下面非空变量同步到 ~/.hermes/.env(必须:Hermes 启动时用该文件 +# override Docker 注入的环境变量,只写在 hermes-docker.env 而不同步时会出现 401) +# OpenAI: https://platform.openai.com/api-keys +# OPENAI_API_KEY=sk-proj-... +# OpenRouter: https://openrouter.ai/keys +# OPENROUTER_API_KEY=sk-or-v1-... +# 其他见 hermes-agent/.env.example + +# --- Open WebUI(浏览器聊天界面)--- +WEBUI_PORT=3000 +ENABLE_WEBUI=true +# macOS 启动脚本完成后是否自动打开浏览器(Linux 忽略) +OPEN_BROWSER=true +# 会话加密用,本地可保留;若暴露到公网请改成随机长字符串 +WEBUI_SECRET_KEY=dev-change-me-in-production + +# Open WebUI 镜像(国内拉不动时可换镜像代理前缀,见脚本注释) +OPEN_WEBUI_IMAGE=ghcr.io/open-webui/open-webui:main + +# --- 数据目录(宿主机)--- +# 留空则使用 ~/.hermes +HERMES_DATA_DIR= + +# Open WebUI 持久化数据目录(留空则使用项目下 open-webui-data/) +OPEN_WEBUI_DATA_DIR= + +# --- 构建镜像用(一般不用改)--- +# BASE_IMAGE=docker.m.daocloud.io/library/debian:13.4 +# APT_MIRROR_CANDIDATES=mirrors.aliyun.com,mirrors.ustc.edu.cn,mirrors.huaweicloud.com,mirrors.tuna.tsinghua.edu.cn + +# ============================================================================= +# 看不到 Web 界面时排查: +# 1) 地址必须是 http://127.0.0.1:与 WEBUI_PORT 一致(不要用 https://) +# 2) 首次启动等 1~3 分钟再刷新;日志: docker logs -f hermes-open-webui +# 3) 自检: curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3000/ +# 返回 200 说明服务已好,多半是浏览器缓存或输错端口 +# 4) 端口被占用可改 WEBUI_PORT,再执行 ./start-docker-cn.sh +# +# 浏览器控制台若出现: +# - Manifest / tiptap 警告:多为 Open WebUI 前端已知提示,可忽略 +# - /ollama/api/version 500:已用 ENABLE_OLLAMA_API=false;若仍有,可清浏览器缓存或删 open-webui-data 后重建容器 +# - Hermes 日志 401 Missing Authentication:在 ~/.hermes/.env 或本文件填写 OPENROUTER_API_KEY(勿留空行) +# ============================================================================= diff --git a/start-docker-cn.sh b/start-docker-cn.sh new file mode 100755 index 0000000..1b3ef06 --- /dev/null +++ b/start-docker-cn.sh @@ -0,0 +1,239 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# One-click local Docker deployment using China-friendly mirrors. +# Run from workspace root: /Users/dannier/Desktop/living/hh +# +# 配置:编辑同目录下的 hermes-docker.env(可由 hermes-docker.env.example 复制) +# Usage: +# bash start-docker-cn.sh + +WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="${WORKSPACE_DIR}/hermes-docker.env" +ENV_EXAMPLE="${WORKSPACE_DIR}/hermes-docker.env.example" + +if [ ! -f "${ENV_FILE}" ] && [ -f "${ENV_EXAMPLE}" ]; then + cp "${ENV_EXAMPLE}" "${ENV_FILE}" + echo "==> Created ${ENV_FILE} from example — edit API keys if needed" +fi + +if [ -f "${ENV_FILE}" ]; then + set -a + # shellcheck disable=SC1090 + source "${ENV_FILE}" + set +a +fi + +HERMES_PROJECT_DIR="${HERMES_PROJECT_DIR:-${WORKSPACE_DIR}/hermes-agent}" +HERMES_DATA_DIR="${HERMES_DATA_DIR:-$HOME/.hermes}" +IMAGE_NAME="${IMAGE_NAME:-hermes-agent:cn-local}" +CONTAINER_NAME="${CONTAINER_NAME:-hermes}" +COMPOSE_FILE="${WORKSPACE_DIR}/docker-compose.cn.yml" +API_PORT="${API_PORT:-8642}" +HERMES_API_KEY="${HERMES_API_KEY:-change-me-local-dev}" +WEBUI_PORT="${WEBUI_PORT:-3000}" +ENABLE_WEBUI="${ENABLE_WEBUI:-true}" +WEBUI_SECRET_KEY="${WEBUI_SECRET_KEY:-dev-change-me-in-production}" +OPEN_WEBUI_IMAGE="${OPEN_WEBUI_IMAGE:-ghcr.io/open-webui/open-webui:main}" +OPEN_WEBUI_DATA_DIR="${OPEN_WEBUI_DATA_DIR:-${WORKSPACE_DIR}/open-webui-data}" +# macOS 下启动完成后是否自动打开浏览器(OPEN_BROWSER=false 可关闭) +OPEN_BROWSER="${OPEN_BROWSER:-true}" + +# China mirrors (can be overridden by env vars or hermes-docker.env) +BASE_IMAGE="${BASE_IMAGE:-}" +BASE_IMAGE_CANDIDATES="${BASE_IMAGE_CANDIDATES:-docker.m.daocloud.io/library/debian:13.4}" +APT_MIRROR_CANDIDATES="${APT_MIRROR_CANDIDATES:-mirrors.aliyun.com,mirrors.ustc.edu.cn,mirrors.huaweicloud.com,mirrors.tuna.tsinghua.edu.cn}" +PIP_INDEX_URL="${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}" +NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmmirror.com}" +PLAYWRIGHT_DOWNLOAD_HOSTS="${PLAYWRIGHT_DOWNLOAD_HOSTS:-https://npmmirror.com/mirrors/playwright,https://registry.npmmirror.com/-/binary/playwright}" +INSTALL_PLAYWRIGHT_BROWSER="${INSTALL_PLAYWRIGHT_BROWSER:-0}" +PLAYWRIGHT_ONLY_SHELL="${PLAYWRIGHT_ONLY_SHELL:-0}" + +# Hermes 在容器内会 load ~/.hermes/.env 且 override=True,会覆盖 Docker env_file 注入的密钥。 +# 将 hermes-docker.env 里已解析的 LLM 变量写入 ~/.hermes/.env,避免「Docker 有 key、运行时仍 401」。 +sync_llm_keys_to_hermes_dotenv() { + local h="${HERMES_DATA_DIR}/.env" + local keys=( + OPENAI_API_KEY OPENROUTER_API_KEY GOOGLE_API_KEY GEMINI_API_KEY + GLM_API_KEY KIMI_API_KEY ANTHROPIC_API_KEY MINIMAX_API_KEY HF_TOKEN + DASHSCOPE_API_KEY DASHSCOPE_BASE_URL + ) + local key val any=0 + touch "$h" + for key in "${keys[@]}"; do + val="${!key:-}" + [ -z "$val" ] && continue + any=1 + if [ -s "$h" ]; then + grep -v "^${key}=" "$h" > "${h}.tmp.$$" && mv "${h}.tmp.$$" "$h" + fi + printf '%s=%s\n' "$key" "$val" >> "$h" + done + if [ "$any" -eq 1 ]; then + echo "==> Synced LLM keys from ${ENV_FILE} -> ${h} (required: Hermes overrides Docker env with this file)" + fi +} + +echo "==> Checking project path: ${HERMES_PROJECT_DIR}" +if [ ! -f "${HERMES_PROJECT_DIR}/Dockerfile" ]; then + echo "ERROR: Dockerfile not found under ${HERMES_PROJECT_DIR}" + exit 1 +fi + +echo "==> Checking Docker availability" +if ! command -v docker >/dev/null 2>&1; then + echo "ERROR: docker not found. Please install Docker first." + exit 1 +fi + +if docker compose version >/dev/null 2>&1; then + COMPOSE_CMD=(docker compose) +elif command -v docker-compose >/dev/null 2>&1; then + COMPOSE_CMD=(docker-compose) +else + echo "ERROR: docker compose not found (docker compose or docker-compose)." + exit 1 +fi + +echo "==> Preparing data directories" +mkdir -p "${HERMES_DATA_DIR}" +mkdir -p "${OPEN_WEBUI_DATA_DIR}" +sync_llm_keys_to_hermes_dotenv + +echo "==> Building image with China mirrors: ${IMAGE_NAME}" +if [ -n "${BASE_IMAGE}" ]; then + BUILD_BASE_IMAGES=("${BASE_IMAGE}") +else + IFS=',' read -r -a BUILD_BASE_IMAGES <<< "${BASE_IMAGE_CANDIDATES}" +fi + +BUILD_OK=0 +for base in "${BUILD_BASE_IMAGES[@]}"; do + echo "==> Trying BASE_IMAGE: ${base}" + if docker build \ + --build-arg BASE_IMAGE="${base}" \ + --build-arg APT_MIRROR_CANDIDATES="${APT_MIRROR_CANDIDATES}" \ + --build-arg PIP_INDEX_URL="${PIP_INDEX_URL}" \ + --build-arg NPM_REGISTRY="${NPM_REGISTRY}" \ + --build-arg PLAYWRIGHT_DOWNLOAD_HOSTS="${PLAYWRIGHT_DOWNLOAD_HOSTS}" \ + --build-arg INSTALL_PLAYWRIGHT_BROWSER="${INSTALL_PLAYWRIGHT_BROWSER}" \ + --build-arg PLAYWRIGHT_ONLY_SHELL="${PLAYWRIGHT_ONLY_SHELL}" \ + -t "${IMAGE_NAME}" \ + "${HERMES_PROJECT_DIR}"; then + BUILD_OK=1 + break + fi + echo "WARN: Build failed with BASE_IMAGE=${base}, trying next candidate..." +done + +if [ "${BUILD_OK}" -ne 1 ]; then + echo "ERROR: image build failed for all BASE_IMAGE candidates." + echo "You can manually specify one, e.g.:" + echo " BASE_IMAGE=docker.m.daocloud.io/library/debian:13.4 bash start-docker-cn.sh" + exit 1 +fi + +echo "==> Writing compose file: ${COMPOSE_FILE}" +CORS_ORIGINS="http://127.0.0.1:${API_PORT},http://localhost:${API_PORT},http://localhost:${WEBUI_PORT},http://127.0.0.1:${WEBUI_PORT}" + +{ + cat < "${COMPOSE_FILE}" + +echo "==> Starting containers" +if [ "${ENABLE_WEBUI}" = "true" ]; then + echo "==> Pulling Open WebUI image if needed: ${OPEN_WEBUI_IMAGE}" + docker pull "${OPEN_WEBUI_IMAGE}" || true +fi +"${COMPOSE_CMD[@]}" -f "${COMPOSE_FILE}" up -d --remove-orphans + +if [ "${ENABLE_WEBUI}" = "true" ]; then + echo "==> Waiting for Web UI to listen on port ${WEBUI_PORT}..." + for _i in $(seq 1 90); do + if curl -sf "http://127.0.0.1:${WEBUI_PORT}/" >/dev/null 2>&1; then + echo "==> Web UI is responding." + break + fi + sleep 1 + done + if [ "${OPEN_BROWSER}" = "true" ] && [ "$(uname -s)" = "Darwin" ]; then + open "http://127.0.0.1:${WEBUI_PORT}" 2>/dev/null || true + fi +fi + +echo "==> Done" +echo "Config file : ${ENV_FILE}" +echo "Hermes data : ${HERMES_DATA_DIR}" +echo "Container : ${CONTAINER_NAME}" +echo +echo "Hermes API (OpenAI-compatible):" +echo " http://127.0.0.1:${API_PORT}/v1" +echo " API key: ${HERMES_API_KEY}" +echo +if [ "${ENABLE_WEBUI}" = "true" ]; then + echo "Web UI (Open WebUI) — 在浏览器地址栏手动打开(须 http,不要用 https):" + echo " http://127.0.0.1:${WEBUI_PORT}" + echo " 首次启动可能要 1~3 分钟(下载嵌入模型等),白页请多等一会或看日志。" + echo " 自检: curl -sS -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:${WEBUI_PORT}/" + echo " 首次进入: 注册本地账号(数据在 ${OPEN_WEBUI_DATA_DIR})" + echo +fi +echo "Quick test:" +echo " curl -sS http://127.0.0.1:${API_PORT}/v1/models -H \"Authorization: Bearer ${HERMES_API_KEY}\"" +echo +echo "Useful commands:" +echo " ${COMPOSE_CMD[*]} -f ${COMPOSE_FILE} logs -f hermes" +echo " ${COMPOSE_CMD[*]} -f ${COMPOSE_FILE} logs -f open-webui" +echo " ${COMPOSE_CMD[*]} -f ${COMPOSE_FILE} down" +echo +echo "LLM 密钥(必填,否则 Open WebUI 对话会报 No inference provider):" +echo " 编辑 ${ENV_FILE} 取消注释并填写 OPENROUTER_API_KEY=(或见同文件说明)" +echo " 然后: ${COMPOSE_CMD[*]} -f ${COMPOSE_FILE} up -d" +echo " 亦可写入 ${HERMES_DATA_DIR}/.env 后: ${COMPOSE_CMD[*]} -f ${COMPOSE_FILE} restart hermes" +echo "Optional: docker exec -it ${CONTAINER_NAME} hermes model" +echo " docker exec -it ${CONTAINER_NAME} hermes setup"