diff --git a/index.html b/index.html index b0bf914..a8353b9 100644 --- a/index.html +++ b/index.html @@ -222,6 +222,8 @@
+ 读到这里你可能发现一件事 —— 本文讲了很多"platform adapter", + 但始终没讲一个网页聊天界面。这不是遗漏,是 v0.8 的事实: + Hermes 上游 v0.8 已经把 Web UI 整个删掉了。 +
+| 版本 | web/ 子项目 | docker-compose.deploy.yml 有 hermes-web 服务? | Docker 镜像 |
|---|---|---|---|
| v0.7.x(2026-04-06 本地快照) | ✅ 有 | ✅ 有(ports 4410:80) | hermes-agent-hermes-agent + hermes-agent-hermes-web(两个容器) |
| v0.8.0(本文分析版本) | ❌ 整个被删 | ❌ 连文件都没了 | 只剩 hermes-agent(单容器) |
+ v0.8.0 发布日:2026-04-08(见 RELEASE_v0.8.0.md:3)。
+
+ gateway/platforms/api_server.py:1736-1754 列出 v0.8 API Server 的**全部** 17 个路由
+ (实际 grep router\.add_ 得到):
+
GET /health
+GET /v1/health
+GET /v1/models ← OpenAI 兼容 models 列表
+POST /v1/chat/completions ← OpenAI 兼容聊天(支持 stream)
+POST /v1/responses ← OpenAI Responses API 兼容
+GET /v1/responses/{id}
+DEL /v1/responses/{id}
+
+GET /api/jobs ← cron 任务管理
+POST /api/jobs
+GET /api/jobs/{id}
+PATCH /api/jobs/{id}
+DEL /api/jobs/{id}
+POST /api/jobs/{id}/pause
+POST /api/jobs/{id}/resume
+POST /api/jobs/{id}/run
+
+POST /v1/runs
+GET /v1/runs/{run_id}/events ← 结构化事件 SSE 流
+
+ 0 个 HTML 路由,0 个 static 路由,0 个 template 渲染。
+ 你在浏览器里访问根路径 / 会直接得到 404,因为 API Server 没有配 root handler。
+
+ v0.8 仓库里确实还有两个 HTML 相关目录,但都不会被 Hermes 运行时加载: +
+| 目录 | 内容 | 用途 |
|---|---|---|
landingpage/ | 665 行 HTML + 521 JS + 1178 CSS + banner/icons | Nous 官网营销页(hermes-agent.nousresearch.com)— 拉人下载用,不是聊天 UI |
website/ | Docusaurus 项目(docusaurus.config.ts + docs/) | 官方文档站(hermes-agent.nousresearch.com/docs)— 静态生成,独立部署 |
+ 也就是说:这两个目录是上游自己官网用的源码,不会被 Hermes 自己 serve,更不会跟着 docker compose 起来。 +
+ ++ v0.8 把"对话界面"完全交给三类外部入口: +
+hermes 命令 + prompt_toolkit TUI)— 本地最直接/v1/* OpenAI 兼容)— 配合任意第三方前端
+ (Open WebUI / LobeChat / 自建前端)hermes.milejoy.com(我们部署的实例)默认是看不到聊天界面的 —
+ 看到的是我们自己写的介绍页 + 隔离层 Basic Auth。如果要聊天 UI,方案是自建一层聊天前端调用 /v1/*
+ 或者挂一个 Open WebUI 容器指向 Hermes API。
++ 本文不光是源码解析,也是一次真部署的实录。目标是把 Hermes v0.8 跑到一台已经运行其他服务 + (LobeChat + PostgreSQL + RustFS 等)的公司 VPS 上,且不允许 Hermes 意外碰到邻居。 + 以下是实际方案和踩过的坑,**每一条都来自真实 debug 现场**。 +
+ +
+ 直接 docker run 的问题是 — Docker 默认 bridge 和宿主网络之间有一定可见性,
+ 错误命令可以访问到邻居容器(LobeChat 的 PostgreSQL 端口)。我们想要的是:
+
incus restore hermes-box fresh,整个环境秒回基线incus delete hermes-box --force 不留任何污染
+ Incus 选 Debian 13 作为 rootfs,security.nesting=true 允许容器内部再跑 Docker,
+ 形成LXC → Docker → Hermes runtime 三层。
+
+ 这不是抱怨清单,是给下一个想做同样事情的人节省 4 小时。 +
+ +
+ Dockerfile:14-15 的 apt 包列表:
+
build-essential nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps
+
+ 没有 git。但 npm install 阶段需要 git 拉 git 类型依赖,
+ build 在第 17 步失败:npm ERR! syscall spawn git / errno ENOENT。
+
+ 修法:sed 补一行 +
+sed -i 's/procps/procps git/' Dockerfile
+
+
+ .env 里写:
+
TELEGRAM_BOT_TOKEN= # 等你从 @BotFather 拿到后填
+
+ 结果 docker compose 把整个 # 等你从 @BotFather 拿到后填 当作 TOKEN 的值,
+ Hermes 拿去尝试连 Telegram 时报 telegram.error.InvalidToken,容器 crash loop。
+
+ 修法:所有行内注释独立成一行。env_file 解析只认行首 #。
+
+ 日志:Refusing to start: binding to 0.0.0.0 requires API_SERVER_KEY。
+ 这是 Hermes 的硬约束(gateway/platforms/api_server.py),
+ 防止无 key 的 API server 对外暴露。
+
API_SERVER_KEY=$(openssl rand -hex 32)
+
+
+ 改了 .env 之后 docker compose restart 依然用老环境变量。必须 down && up -d
+ 才会重读。这是 Docker Compose 的设计,不是 bug,但容易让人白花 10 分钟 debug。
+
+ incus list IPv4 列永远是空的,容器 eth0 只有 IPv6。
+ 理论上 incusbr0 的 dnsmasq 应该分配 IPv4,实测 DHCPDISCOVER 发出但无 OFFER 回来。
+ 可能跟宿主已有的 Docker FORWARD DROP + UFW 规则有关,iptables -I FORWARD 没修好。
+
+ 修法:跳过 DHCP,直接写静态 IP 到容器内的 /etc/systemd/network/eth0.network:
+
[Match]
+Name=eth0
+
+[Network]
+Address=10.146.223.10/24
+Gateway=10.146.223.1
+DNS=8.8.8.8
+
+
+ 在 LXC 容器里跑 docker compose build,npm 某些原生包(better-sqlite3)postinstall 报:
+
npm ERR! code EACCES
+npm ERR! syscall spawn sh
+npm ERR! path /opt/hermes/node_modules/better-sqlite3
+
+ 尝试无效的修法:security.privileged=true、security.nesting=true、
+ security.syscalls.intercept.*、raw.lxc: lxc.apparmor.profile=unconfined、
+ DOCKER_BUILDKIT=0(legacy builder 又被 Dockerfile 的 --chmod 卡住)。
+
+ 最终有效的修法:build 不在 LXC 里做。改为: +
+docker build -t hermes-agent:latest ./source(已知能成功)docker save hermes-agent:latest | gzip -1 > /tmp/hermes.tar.gz(~2.5GB)incus file push /tmp/hermes.tar.gz hermes-box/tmp/incus exec hermes-box -- docker load -i /tmp/hermes.tar.gzbuild: 改为 image: hermes-agent:latestdocker rmi hermes-agent:latest(保持宿主干净,只让 LXC 里有)+ Build 在宿主跑,运行时在 LXC,隔离边界一点没破。宿主只多了短暂的 tar 文件。 +
+ ++ 容器启动后立刻 crash,Python traceback: +
+File "/usr/lib/python3.13/asyncio/selector_events.py", line 120, in _make_self_pipe
+ self._ssock, self._csock = socket.socketpair()
+PermissionError: [Errno 13] Permission denied
+
+ 原因:Docker default seccomp 在 LXC 嵌套里拦了 socketpair()。
+
+ 修法:docker-compose.yml 给 Hermes 容器加:
+
privileged: true
+ security_opt:
+ - seccomp:unconfined
+ - apparmor:unconfined
+
+
+ v0.7 的 docker-compose.deploy.yml 引用 ./web 作为单独服务,
+ v0.8 这个目录不存在了(见第 20 章)。如果你还用老 compose 文件 build,第一步就会:
+
unable to prepare context: path "./source/web" not found
+
+ 修法:compose 文件只留 hermes-agent 服务,暴露 8642 端口即可。
+
+ 关键 debug 时刻。写完聊天 UI 部署上线后,用 curl 测 /v1/chat/completions 返回 200,
+ 但浏览器打开 UI 点发送按钮返回 HTTP 403。
+
+ 根因:gateway/platforms/api_server.py:183-201 的 CORS middleware 对带
+ Origin header 的请求默认拒绝(curl 不带 Origin 能过,浏览器必带 Origin 被拦)。
+ _origin_allowed() 在 api_server.py:393-401,检查 self._cors_origins:
+
def _origin_allowed(self, origin: str) -> bool:
+ if not origin:
+ return True # ← curl 这类非浏览器,放行
+ if not self._cors_origins:
+ return False # ← 浏览器且未配允许列表,拦
+ return "*" in self._cors_origins or origin in self._cors_origins
+
+ 修法:.env 加一行
+
API_SERVER_CORS_ORIGINS=https://hermes.milejoy.com
+
+ 多个 origin 用逗号分隔。代码读 env 在 api_server.py:322-323。改完
+ docker compose down && docker compose up -d 让 env 重读。
+
| 维度 | 有效的隔离 | 失去的保护 |
|---|---|---|
| 文件系统 | ✅ Hermes 看不到宿主和邻居容器的 /opt | 容器内 root 能 mount / chroot |
| 进程 | ✅ PID namespace 完全独立 | — |
| 网络 | ✅ 独立 bridge 10.146.223.0/24,跟 LobeChat docker0 不互通;宿主 Nginx 反代是唯一入口 | — |
| seccomp / AppArmor | — | ❌ 都 unconfined(为了让 socket.socketpair 能用) |
| kernel 漏洞 | — | ❌ 共享宿主 kernel,kernel exploit 可穿透到宿主 |
| 销毁 | ✅ incus delete --force 秒清整个容器 + docker 层 + volume | — |
+ 对单用户内部场景够用。如果你要做 Manus 那种"每个用户独立 sandbox 防恶意 prompt"的场景, + 这套方案的 kernel 共享就是痛点 — 那时应该上 Firecracker / Kata Containers / gVisor, + 不是 LXC。 +
+ +| 维度 | Manus 公共服务 | Hermes 本次部署 |
|---|---|---|
| 隔离技术 | 每 session 一个 Firecracker microVM | 共用 LXC 容器(长命) |
| 启动时间 | ~1-2s(microVM 冷启) | ~0.5s(LXC 容器本就启着) |
| kernel 隔离 | ✅(独立 kernel) | ❌(共享宿主 kernel) |
| 用户模型 | 多租户陌生人 | 单用户 / 内部团队 |
| 成本模型 | 按 session 付费 | 固定资源预留 |
| 运维复杂度 | 需要 orchestrator + session 生命周期管理 | incus 命令 + 快照 |
+ 结论:"威胁模型决定隔离技术"。不要看到 Manus 用 Firecracker 就觉得自己也得用 — + 先问自己这三个问题: +
++ 部署好之后的公司实例: +
+boss / mile)/v1/chat/completions OpenAI 兼容,Nginx 自动注入 Bearer,浏览器零密钥