Resources
2Install
npx skillscat add russ534/myopenclawbot/wb2api-hermes-setup Install via the SkillsCat registry.
WorkBuddy2API 接 hermes 完整运维
生产状态(2026-10-19 切换):飞牛
192.168.31.201上只有 wb-hub(8788)一个 WB 容器。原 sliverkiss-go 三件套(workbuddy2api:7863/workbuddy2api-gui:8787/workbuddy-manager:7864)已 stop+rm,hermesconfig.yaml、mdc/config.json、mp2 app_settings.dynamic_app_config全部切到 hub,hermes/modelpicker 里指向老10082反代的buddy2api自定义 plugin 已删。SKILL.md 里 7863 / 8787 / buddy2api 引用是历史 demo,看参考references/workbuddy2api-hub-deploy.md看真实配置。Hub 默认 dashboard 密码 =
admin——任何新装 hub 首次起来必须立刻改(除非 bind 到127.0.0.1且反向代理一定要 auth 套外面)。本机 (192.168.31.201:8788) 当前 panel 密码 =work1234(沿用 wb2api-sliverkiss-go 旧约定,下次部署新 hub 也用work1234保持一致)。改法 + 改完会 invalidate 所有 dashboard session 但不动 launcher_key 见references/workbuddy2api-hub-deploy.md"Hub scheduler / 自动签到" 节。容器数 = 5 cn 账号在 hub accounts/:
50b78b75(166****9445)、22211169(米粒儿)、82f85471(晴天)、cb5bd756(🌕星期日不上发条)、d64722ef(於菟)。第 5 个(手机号)漏在/vol1/1000/Docker/workbuddy2api/auths/(宿主层),不是在workbuddy2api-source/auths/(容器内置)。每次从老 wb2api 迁移账号都查这两个目录。
0. 两条轨道:sliverkiss-go (7863) vs arde hub (8788)
两种 wb2api 同时存在过,已全部切到 hub(合 3 容器 + 自动签到 + cn/intl 双域):
- hub =
ghcr.io/ardeyouxipianyi/workbuddy2api-hub(Python),8788,单容器全包(反代+看板+多账号+自动任务) - sliverkiss-go = 历史版(7863 main + 8787 gui + 7864 manager),Go 单反代。已 2026-10-19 删 3 旧 wb 容器,全面切 hub
切换决策依据:hub 同时给 cn + intl 模型、自动切号、自动签到/Cookie 续期,sliverkiss-go 只反代。唯一会留 sliverkiss-go 的场景:hub 不可用期间当 fallback。
部署细节(compose、账号迁移、realm 坑、CLI 选项)见 references/workbuddy2api-hub-deploy.md。
1. 架构拓扑(飞牛 192.168.31.201)
Client (hermes TG/微信)
↓ HTTP POST /v1/chat/completions, Bearer <api_key>
[workbuddy2api :7863] ← Go 网关核心
├─ 鉴权 (config.api_key)
├─ 选号 (pool, weighted random) → 读 /vol1/.../auths/*.json
├─ payload.go 改写: 强制 stream:true, 思维链注入, tool_choice 归一
├─ HTTP → CodeBuddy 国内 cn: (copilot.tencent.com) / 国际 global: (www.workbuddy.ai)
│ ├─ /v2/chat/completions (SSE)
│ ├─ /billing/...
│ └─ /auth/...
└─ scheduler: 签到09/21, 旅行09/21, 活跃10, 保活22, 开学12, 夜猫01
[workbuddy2api-gui :8787] ← Node 可视化面板 (read-only /v1/status)
[workbuddy-manager :7864] ← 容器管理 (挂 docker.sock)源码: /vol1/1000/Docker/workbuddy2api/workbuddy2api-source/ (Docker 大写)
挂载: ./auths:/app/auths, ./data:/app/data, ./config.json:/app/config.json:ro
2. hermes 配置(已验证生效)
/opt/data/config.yaml:
model:
provider: minimax-cn
default: minimax-m3
providers:
buddy2api:
api: http://192.168.31.201:7863/v1
api_key: <from fnos config.json api_key 字段,带日期戳会变>
api_mode: openai
model: cn:deepseek-v4.1-flash
models:
- cn:deepseek-v4.1-flash # credit=0
- cn:glm-5.3-flash # credit=0
- cn:kimi-k3-1 # credit≈0.07
- cn:minimax-m3 # credit=0
- cn:glm-5.3 # credit≈0.01
fallback_providers:
- provider: buddy2api
model: cn:deepseek-v4.1-flash
model_catalog:
excluded_providers:
- nous # 排除 catalog 自动加载
- buddy2api # ⚠️ 这只挡 catalog 静态入口,挡不住用户配的 providers.buddy2api关键陷阱:/model 面板 = catalog 缓存 + 用户配 providers.*。excluded_providers 只对 catalog 生效。
→ 面板里 "buddy2api (5)" 是用户配的 5 个模型(providers.buddy2api.models),无法被 excluded 隐藏。
→ 如果想"面板完全干净只剩 minimax + deepseek",要删 providers.buddy2api 整段(fallback 也没了)。
2b. 接入新反代 / 改 providers.*.models 之前的 standing rule 🔴
任何人(你 / me)说"配 hermes 调某个 hub / OpenAI-compatible 服务"——第一步必先 curl /v1/models 拿模型真实 id 列表,绝不可以凭 README / 记忆 / 工具截图配 models: 字段。会出 4 种典型错:
- 错加
cn:前缀——README 提过但 hub/v1/models实际不返回带前缀的 id。前缀写到 hermes models 里 + 转发给 hub 时 hubdetect_model_realm会兜底(按 token iss),请求能过;但配置漂移、模型名找不到、analytics 全乱。 - 大小写搞错——
minimax-m3vsMiniMax-M3vsMiniMax-m3在 hub/v1/models实际全大写或全小写,写错就直接 404。 - 配错模型数——README 列 5 个但 hub 实际 18 个(或反过来),用户面板 picker 只显示配置的 5 个,剩下调不到。
- providers.X.model 与 fallback_providers[X].model 不一致——fallback 失败找不到这个 model。
正确工作流(强制 5 步):
# 1. 拿到真实模型列表
ssh fnos-root "curl -sS http://192.168.31.201:8788/v1/models \
-H 'Authorization: Bearer $(jq -r .launcher_key \
/vol1/1000/Docker/workbuddy2api-hub/accounts/settings.json)' \
| jq -r '.data[].id'" > /tmp/real-models.txt
wc -l /tmp/real-models.txt # 数真实数量
# 2. 直接 model 字段 + models 列表都用这个列表原样配进 hermes
# 不要做任何"觉得应该这样但其实不一定"的 normalize配置完跑一发真 probe 确认通:见 §11 末尾的"验证"。
2c. 配 contextWindow / maxTokens / 思考档位 —— 只信网关 /v1/models 自报的元数据 🔴
hub 的 /v1/models 每个模型除了 id 还带整块能力元数据(实测 deepseek-v4.1-flash):
context_length: 1000000 max_output_tokens: 128000
context_windows: [300000, 600000, 1000000]
reasoning_efforts: ["low","high","max"] reasoning_default_effort: "high" reasoning_can_disable: true
capabilities: {vision: true, tool_calls: true, reasoning: true}任何客户端(hermes / mdc / dsh / opencode / 自写脚本)配 context、输出上限、思考档位前,先把这块抓下来,不要用 README、模型名直觉或"1M 应该够"来填数字。
KEY=$(grep -A3 '^ workbuddy-hub:' /opt/data/config.yaml | grep api_key | awk '{print $2}')
curl -s --noproxy '*' -H "Authorization: Bearer $KEY" http://192.168.31.201:8788/v1/models \
| python3 -c "import sys,json;[print(json.dumps(m,ensure_ascii=False,indent=1)) for m in json.load(sys.stdin)['data'] if m['id']=='deepseek-v4.1-flash']"档位写完必须实测每一档——reasoning_effort 传了但上游静默忽略是常规失败模式,只有响应的 usage.completion_tokens_details.reasoning_tokens 能证伪(reasoning_content 字符数只是旁证)。一次跑完三档用 scripts/probe-model-caps.py:
python3 <skill_dir>/scripts/probe-model-caps.py deepseek-v4.1-flash
# 每档输出: effort | HTTP | reasoning_chars | reasoning_tokens | 首句回答客户端配置格式不是 hermes 的:出现 api: openai-completions + compat: {thinkingFormat, supportsReasoningEffort} + models[].contextWindow/maxTokens/reasoningEfforts 这种块,是 DeepSeek Harness(dsh)的 models.yml,不是 hermes schema。对译关系:
| dsh | hermes |
|---|---|
api: openai-completions |
api_mode: openai |
baseURL |
providers.<x>.api |
models[].id |
providers.<x>.models: [ids] |
contextWindow / maxTokens |
model_overrides.<provider>.<model>.context_window(hermes 没有 maxTokens 字段,输出上限由请求带) |
reasoningEfforts |
无对应配置;hermes 走 agent.reasoning_effort 单值,档位集合由 /v1/models 的 reasoning_efforts 提供 |
baseURL: http://127.0.0.1:8788 说明客户端跑在飞牛宿主上——hermes 容器内 8788 不监听,容器里必须换 192.168.31.201:8788。看到 127.0.0.1 别直接抄进 /opt/data/config.yaml。
thinkingFormat: openai 有坑:hub 把思考内容放在 reasoning_content 里(DeepSeek 方言)。客户端声明 openai 却读不到思考过程时,改 thinkingFormat: deepseek,请求形状不用动。
3. hermes config 命令工具坑
patch/write_file工具会拦 Hermes config("use 'hermes config' instead")单值:
hermes config set providers.buddy2api.api http://...列表必须 JSON 格式:
hermes config set providers.buddy2api.models '["a","b"]'逗号分隔字符串会被当 1 个元素(这个会让人以为只配了 1 个模型)
删除:
hermes config unset providers.buddy2api.modelsfallback_providers 是 list of dicts:
hermes config set fallback_providers '[{"provider":"buddy2api","model":"cn:..."}]'
🔴hermes config set providers.X.model <name>会顺手把model.default = <name>, model.provider = X——即你的 primary 静默被改。联动规则不只model字段:实测触发联动的字段是model(设值即同步 model.default = 设的 model, model.provider = X);不触发联动的字段:api/api_key/api_mode/models/base_url这五个。正确做法:编辑providers.*字段全程用这五个之一,model 字段单独用hermes config set model '{"provider":"...","default":"..."}'显式指定,或走hermes model交互 picker 一次到位。任何时候设完providers.X.model都立刻hermes config get model确认 primary 没被改。-p <provider>是显式路由的强制写法(切到自定义 provider 调 chat 必须带)。hermes chat -m deepseek-v4.1-flash -q "ping"不带-p会卡 30+ 秒超时(hermes 不知道走哪个 provider,扫 fallback_chain 兜底)——正确写法:hermes chat --provider workbuddy-hub -m deepseek-v4.1-flash -q "ping 答 1 字 pong"。带-p后 5-15s 内出 200。判断"切了后不能聊"的根因:99% 是-p没带,不是配置错。也用-p验证新建 provider 是否真通——配置对 + key 对 + 上游活的"全链真测"必须用-p,不靠/modelpicker。
4. 探针负缓存 (重要陷阱)
hermes_cli/models.py probe_api_models 有 _probe_neg_cache 内存缓存失败结果 5min TTL。
- 触发: 任何一次
/v1/models失败(网络抖动/timeout)→ 缓存该 base_url 5min - 表现: 后续 5min 内
/model切到该 provider 都会弹 "could not reach" 警告 - 实际 7863 是通的(实测 1ms 200, 23KB),但 hermes 不重试
- 修法: 重启 hermes gateway 清内存(代价:会断当前 session);或等 5min;或
hermes model --refresh(需 TTY)
5. /model 切换模式
- session-only (默认): 只改 session 内存,
sessions表model_config字段不变,session 结束失效 - --global: 持久化到 sessions 表
model_config.gateway_runtime - 切回命令:
/model minimax-cn:MiniMax-M3 --global
6. session 配置继承坑
/opt/data/state.db sessions 表 model_config.gateway_runtime.{provider,base_url,api_mode} 决定实际请求发哪。
_reset_from字段: 新 session reset 时继承旧 session 的 gateway_runtime- 历史上 10082 端口(已死的旧反代)曾通过 _reset_from 污染 9 月的 session,又通过 reset 污染 10 月的 session
- 表现: 配置正确 (providers.buddy2api = 7863) 但 session 用 10082 → 502
- 修法: 直接 SQL 改:
UPDATE sessions SET model_config=json_set(model_config, '$.gateway_runtime', json('{"provider":"minimax-cn","base_url":"https://api.minimaxi.com/anthropic","api_mode":"anthropic_messages"}')) WHERE id='<session_id>'; - 预防: 不定期
SELECT id, model_config FROM sessions WHERE model_config LIKE '%10082%' AND archived=0清查
7. 排查 502 / 401 / 警告的标准流程
- 直接探 7863 (从 hermes 容器内):
import urllib.request, json req = urllib.request.Request("http://192.168.31.201:7863/v1/models", headers={"Authorization": "Bearer <key>"}) with urllib.request.urlopen(req, timeout=5) as r: print(r.status, len(r.read().decode())) - 看 /opt/data/sessions/request_dump_*.json(失败请求会自动 dump):
request.url: 实际发的 URL(看是不是 10082 旧死链)request.body.model: 实际 modelerror.status_code: 502/401/etc
- 看当前 session routing:
SELECT id, model, model_config FROM sessions WHERE id='<current_session_id>'; - 看 hermes auth:
hermes auth status minimax-cn→ logged in / logged outhermes auth status buddy2api→ 同
- 看容器:
ssh fnos sudo docker ps --format '{{.Names}}\t{{.Status}}' | grep workbuddy- 看 healthy / unhealthy
8. 模型白嫖验证 (2026-10-18 实测)
| 模型 | credit | 状态 |
|---|---|---|
| cn:deepseek-v4.1-flash | 0 | ✓ 稳 |
| cn:glm-5.3-flash | 0 | ✓ 稳 |
| cn:kimi-k3-1 | 0.07 | ✓ 几乎白嫖 |
| cn:minimax-m3 | 0 | ✓ 稳(同主用) |
| cn:glm-5.3 | 0.01 | ✓ 几乎白嫖 |
不能用的 (no_healthy_account): cn:kimi-k2-thinking, cn:glm-4.6, cn:glm-5.0-turbo
选模型时先实际 probe 一次再配进 hermes。
9. minimax-cn (主用) 协议细节
- base_url:
https://api.minimaxi.com/anthropic(不是 /v1) - api_mode:
anthropic_messages(不是 chat_completions) - 5 个模型: MiniMax-M3, M2.7, M2.5, M2.1, M2
- 当前主用: MiniMax-M3
10. 飞牛凭据
- SSH:
Russ534@192.168.31.201(大写 R),密码 ${FNOS_PASSWORD},Russ534 不在 docker 组 → 要 sudo - 别名
ssh fnos在 hermes 容器有效 (有 ~/.ssh/fnos_ed25519 + config) - sudo 密码 = SSH 密码
- key 在飞牛 config.json:
/vol1/1000/Docker/workbuddy2api/workbuddy2api-source/config.json的api_key字段
11. 让其他容器消费 workbuddy2api (7863) —— OpenAI 兼容配置通用模式
通用前置
ssh fnos 'curl -sS http://192.168.31.201:7863/v1/models -H "Authorization: Bearer <key>" | jq .data[].id'拿到模型 id 列表(别瞎猜 model 名,要用上游实际暴露的)- base_url 用 容器 IP(如
http://172.18.0.x:7863/v1)或 LAN IPhttp://192.168.31.201:7863/v1—— 取决于容器是否能解析 LAN IP - api_key 必填,不能省;省了服务返回 401
容器配置字段对照表(2026-10-19 实测)
| 容器 | 配置文件 / 接口 | 字段名 | 备注 |
|---|---|---|---|
| moviepilot-v2 (4.x+) | PG app_settings.dynamic_app_config jsonb |
ai_api_key / ai_base_url / ai_model_name / ai_provider='openai' |
改完不需要重启容器,下次 LLM 调用即生效。plugin.ChatGPT.openai_url 是遗留字段,改了不生效 |
mdc (mdcng/mdc) |
/config/config.json metadata 块 |
openai_base / openai_key / openai_model |
字段名不是 openai_base_url!前端 JS bundle 才认这三个名字(看 /opt/frontend/.next/ 找字段名) |
mdc 配置实例(用 workbuddy2api 7863)
import json
p = '/vol1/1000/Docker/mdc/config/config.json'
d = json.load(open(p))
m = d.setdefault('metadata', {})
m['translate_engine'] = 'OpenAI' # 必须设为 OpenAI 才会读 openai_* 字段
m['openai_base'] = 'http://192.168.31.201:7863/v1'
m['openai_key'] = '<workbuddy2api api_key>'
m['openai_model'] = 'cn:deepseek-v4.1-flash' # 从 /v1/models 拿
json.dump(d, open(p, 'w'), indent=2, ensure_ascii=False)
# 改完重启容器让 Node.js 重新读 JSON
docker restart mdcmp2 配置实例(同样思路)
UPDATE app_settings
SET value_json = value_json || jsonb_build_object(
'ai_api_key', '<key>',
'ai_base_url', 'http://192.168.31.201:7863/v1',
'ai_model_name','cn:deepseek-v4.1-flash',
'ai_provider', 'openai'
)
WHERE setting_key='dynamic_app_config';
-- 不需要 restart mp2陷阱
- mdc 配置目录是
/vol1/1000/Docker/mdc/config/config.json不是/app/...或~/.config/...——容器内/configbind mount 到宿主这个路径 - 前端 SPA 没有
/api/config之类的 REST 端点暴露字段名——必须grep -rE "openai_(base|key|model)" /opt/frontend/.next/在容器内搜 JS bundle 找真实字段名(容器内 grep 慢,限路径) - 修改完 mdc config.json 必须
docker restart mdc——Node.js 启动时一次读完整个 JSON,运行期不监听文件变化 - mp2 不需要 restart——pg 改完下次 LLM 调用即生效
- mihomo 代理拦不拦 7863?——容器用 LAN IP
192.168.31.201:7863直连 mihomo(mihomo 在 host);workbuddy2api 自己也跑在 host 上(host network 模式),不需绕代理。LAN → LAN 不走 mihomo
12. 用户偏好(操作 wb2api / mp2 / mdc 等容器时的硬规则)
从多次纠正里抽出来的硬规则,不要再犯:
- "自己检查" / "你去瞅一眼" / "你检查一下" = 当场实测回数据,不要凭记忆或推理给结论。被纠正"你又瞎猜啊"就是因为 agent 给"应该"/"肯定"答复。规则:每次涉及"X 现在能不能用"都跑真 probe,
curl /v1/.../ssh fnos 'docker ...'/cat ...,再回报数字。 - "用现有的 X" = grep across containers/envs/DBs first, then ask. 爸爸说"就用现有的 mp 的"指的就是当前正在跑的 mp2 容器里的 TG token。先查 docker exec / cat config.json / 翻 PG dynamic_app_config,别上来就问。完整 discovery ladder 见
fnnas-adminskill "Use the existing one" 段。 - 不要在确认之前反复追问"你确认一下 X 吗"——给出我看到的事实 + 我打算做的,让爸爸一条话修正,别连续问 3-4 个 yes/no。一次列完。
- "你看着来" / "你自己判断" / "干" = execute, don't enumerate. 说完就执行,不要再列 3 个选项问爸爸选。
- 同一段连续发了几条消息("A ①" / "A B" / "B ②")= 上一条方案不对,按这条执行,不要回头再确认上一条。
- Shell heredoc 中文括号炸 bash.
echo === foo(bar)===在bash <<'EOF'里报unexpected token。用 ASCII()/ 跳过括号。每次手输代码时注意。 - "你快闭嘴吧" / "别说话直接干" = 收到后不要回完整 plan,只回一句确认 + 直接开始执行(不要反问 "你想我 X 还是 Y")。例:收到"你快闭嘴吧"——回 "好" 或直接进入执行态,不要解释为什么这么做、不要列举下一步、不要请求更多细节。
- "我通过 X 修好了" = 你/外部脚本改过了,立刻 重读 state,不要凭"上一轮 config 是 Y"继续——已经 drift 了。例:用户说"通过 hermes 终端修复好了",agent 不能继续推断"上次改的是 NO_PROXY",必须 ssh 重读实际 env。重读优先于凭记忆。
- agent 不要重述自己"我要做 A B C","你拍板" 等发问式段落——已经在 §16 / §18 / §19 等多处写过,但每次踩坑。当下行动是什么 + 下一步第一个动作直接说,不问"行不行 / 怎么做 / 拍板"。
13. 操作容器配置的标准 3 步 (避免"只改了不验证")
任何"改完就走"的操作(PG 改 config / compose 改 env / config.json 改字段),按顺序:
- 改之前先记录当前值(grep / SELECT / cat 落盘到 /tmp/before.json)
- 改 + 验证 YAML / JSON 语法(
docker compose config -q/python3 -c 'json.load(open(p))') - 改完重启 / 重读 + 验证生效(
docker ps看 restart ok / 端点 curl 200 / 服务日志里看到新值被加载)
反例:只改 config.json 不 docker restart mdc → Node.js 跑的还是旧 JSON → 翻译引擎继续打 "无 key" 错。
14. API token 在 hermes stdout 跨域输出截断
飞牛上某些服务返回的敏感字符串(TG bot token、API key),在 Hermes Python 端 json.loads() 拿到的实际是完整字符串(165 字符),但 print(json.dumps(...)) 在 stdout 会被 mask 成 eyJhbG...W3gE(中间 ***),并且 len(token) 也被错报成 13(mask 后的字符数)。症状:用 Python 端拿到的 token 调 API 返回 401。
修法:把 token 写到 fnos 文件,再从 fnos 那边直接读:
# fnos 端
ssh fnos-root 'python3 -c "import json;print(json.load(open(\"/path/to/login_resp.json\"))[\"data\"][\"token\"])" > /tmp/tok.txt'
wc -c /tmp/tok.txt # ← 文件大小是真实长度(165 字节,不是 13)
# 直接 cat 文件给后续 curl 用,不再走 Hermes 输出wc -c 在 fnos 文件系统上是真实字节数,永远准确——跟 Hermes Python print 的 mask 无关。
15. "判断云盘挂载是 clouddrive2 起的 vs rclone 起的" — 用 mount 输出
ssh fnos 'mount | grep fuse'
# fusectl on /sys/fs/fuse/connections type fusectl (rw,...)
# 1000-1-7b565032:dav on /vol02/1000-1-7b565032 type fuse.rclone (...)type=fusectl是内核暴露的元数据文件系统,无关type=fuse.rclone的 mount 是 rclone 进程直接挂在宿主——跟 clouddrive2 容器无关clouddrive2容器的 fuse mount 是它自己进程内的,不通过宿主 mount 命令查
Clouddrive2 容器升级时,不影响 rclone 挂载——rclone 是宿主独立进程。升级只让 cd2 WebUI (:19798) 重启,业务挂载 /vol02/1000-1-7b565032/... 一直在。
实际:docker pull cloudnas/clouddrive2:latest && docker compose up -d → 容器名会变成 adoring_lewin(recreate 旧容器名被 docker 随机名占),需要 docker rm -f adoring_lewin 清僵尸。完整流程:
ssh fnos-root "cd /vol1/1000/Docker/clouddrive2 && docker compose pull && docker compose up -d"
sleep 5
ssh fnos-root 'docker ps -a --filter ancestor=cloudnas/clouddrive2 --format "{{.Names}} {{.Status}}"'
# 如果出现 random-named 旧容器:docker rm -f <random-name>
curl -sS -m 5 -o /dev/null -w "cd2 web=%{http_code}\n" http://127.0.0.1:19798业务挂载验证:
ssh fnos-root 'mount | grep fuse.rclone' # rclone 挂在的宿主路径,clouddrive2 升级不影响
ssh fnos-root 'docker exec moviepilot-v2 ls /CD2 | head' # mp2 读得到 115open 内容就 OK16. 区分消息来自 Telegram 还是微信 (rule)
每个 user 消息 gateway 都会带 Gateway message origin (JSON data, ...) 块(含 platform / chat_id),不要从消息内容、用户语气、机器人用户名瞎猜——同一个用户同一个人,TG 和微信都连着同一台 hermes,路由/routing/state 不一样。判定三秒完成就按脚本完,不要来回跟爸爸确认:
# 解析 origin JSON 即可
import json
origin = json.loads(origin_block[origin_block.find('{'):origin_block.rfind('}')+1])
platform = origin['platform'] # 'telegram' | 'weixin'
chat_id = origin['chat_id']反例:看到"Open"/MyGPT /MyOpenclaw 这种机器人名别立刻当微信——它也可能是 TG 的某个 bot 用户名;看到 chat_id 7271375745(TG Russ534)/ o9cq...@im.wechat(微信)才能定。
判错后果:把 TG 的故障排查跑去改微信 state.db → 修错地方,浪费时间还被纠正。
17. /model picker 里的脏 provider 清理 (3 个地方必须同时清)
/model picker 列什么由三处共同决定:
- 磁盘 catalog 缓存:
/opt/data/provider_models_cache.json(key 是 provider slug,value 是 models 列表) - 磁盘模型元数据缓存:
/opt/data/models_dev_cache.json+/opt/data/models_dev_cache.json.etag - 用户配
providers.*段:/opt/data/config.yaml里providers.X
加 excluded_providers: [X, Y] 只挡 #1(catalog 静态入口),挡不住 #3(用户配的)——skill §2 已写。但:删完 providers.X 之后 picker 里还能看到 X 一段时间,因为 #1 #2 还在缓存里没刷。要 picker 立刻干净必须三处一起清:
cp /opt/data/provider_models_cache.json /opt/data/provider_models_cache.json.bak.$(date +%Y%m%d-%H%M)
rm /opt/data/provider_models_cache.json /opt/data/models_dev_cache.json /opt/data/models_dev_cache.etag
# config.yaml 改 excluded_providers / unset providers.X
hermes config set model_catalog.excluded_providers '["nous", "buddy2api", "workbuddy-hub"]'
hermes config unset providers.workbuddy-hub # 如果想彻底干掉(fallback 也丢了)不删 .etag 文件的后果:下次 gateway 启动会带着 If-None-Match 头去拉远端 etag,远端 etag 不变就拿 304 + 老 body——清缓存等于没清。
18. hermes 外部 config drift 防护 (rule)
/opt/data/config.yaml 不只你会改——其他 session / kanban / 别的 agent / 自动同步脚本都可能在两次对话之间把它改了。绝不可凭"上一轮是 X"或"我配的是 X"做推断,每一轮独立重新 hermes config get:
hermes config get model
hermes config get providers
hermes config get fallback_providers
hermes config get model_catalog.excluded_providers断言"我配过 buddy2api"前,先 cat /opt/data/config.yaml 看一眼当前实际是不是 buddy2api。session 间被改成 workbuddy-hub(同字段不同名)+ base_url 换 8788 + api_key 换 wb-37S9... 都发生过。
配合 §2b:上游 base_url 变了,原来的 /v1/models 探针结果(model id 列表、是否带 cn: 前缀、大小写)全部作废——必须对新 base_url 重新跑 §2b 的真实 probe。反向特别坑:旧 gateway(7863 Go 版)model id 是 cn:deepseek-v4.1-flash(带前缀),新 hub(8788 Python)model id 是 deepseek-v4.1-flash(裸名小写)——同一套"看起来对"的配置在新老 gateway 间迁移一字不变就直接 404。
19. hermes auth status <custom_provider> 永远 logged out (rule, 别误诊)
hermes auth status minimax-cn 这种内置 provider 返回 logged in / logged out 是真实鉴权状态。但 hermes auth status workbuddy-hub(任何 providers.* 里自定义的)一律返回 logged out——因为 hermes 的 auth 池只对内置 provider 维护凭据,custom provider 走 providers_dict_to_custom_providers 逻辑不接入 auth 池。用 logged out 判断 custom provider "不能用"是误诊。
真实可用性判断(按这条顺序):
- 实际探一发
/v1/chat/completions(最权威)—— 配置正确 + key 对 + 上游在 = 200 hermes config get providers.X看 api / api_key / api_mode / models 字段实际有hermes config check看Config version: 50 ✓(语法合法)
不要用 auth status 单独判 custom provider 通不通——会把自己骗到以为配错了然后反复改 yaml。
20. /v1/models cache 命中背后藏的型号漂移(重要防呆)
/v1/models 探针走的代码路径是「先读 disk cache + 再去上游验证」——大部分时候用户切 /model 看到的列表都是缓存里的旧数据,不是当下上游的。上游换了、新模型上线了、模型重命名了,picker 不会立刻反映,必须 rm 缓存文件后下次 gateway tick 才会拉新(或者 hermes model --refresh 需 TTY)。
反向案例:上一轮删了 buddy2api 段 + 加了 workbuddy-hub 段,以为 /model 面板应该立刻只剩 hub 的 18 个——结果面板还显示 Buddy 2API 旧 5 个,因为 disk cache 还在。等清完缓存再测,hub 的 18 个就在了。
配合 §17 的三处一起清——catalog cache 不清,picker 永远是过时的视图。
21. JSON 序列化的 hermes 端大小写失真(rule, 接 hub 后多发)
hub(Python 实现的)/v1/models 返回 minimax-m3 / glm-5.3 / deepseek-v4.1-flash(全小写)。老 sliverkiss-go gateway(7863)/v1/models 返回 cn:deepseek-v4.1-flash(带前缀)。接入 hub 不要照搬老 gateway 的 model id 列表——必须 curl -sS /v1/models | jq -r '.data[].id' 拿当下的列表原样配。
hub 自动重定向 alias:glm-5.2 实测返回 glm-5.2-x,但请求发 glm-5.2 也 200(hub 内部 token 表别名映射)。但 glm-5.1 发过去 400 "reasoning effort value is not supported by the current model"——上游拒了。所以实测 17/18 ≠ 100% 可用,要每个真 probe 一发,配错的删掉。
22. hermes 容器被 mihomo proxy 截胡 LAN IP(502 Bad Gateway)
Hermes 容器 shell 默认 HTTP_PROXY=http://192.168.31.201:7890 + HTTPS_PROXY=...(mihomo 出公网代理)。Python urllib 看到这俩 env var 自动让所有 HTTP 流量——包括 LAN 内网 http://192.168.31.201:8788/v1/models——也走 mihomo。mihomo 看 LAN target 不认识 → 返回 502 Bad Gateway(不是 timeout、不是 DNS 错)。
症状(最难辨):
- 从容器外(fnOS host / 你 ssh 进飞牛)
curl http://192.168.31.201:8788/v1/models→ 200 OK(走 host network 绕过 mihomo) - 从 hermes 容器内
python3 urllib.urlopen(...)→ 502 Bad Gateway - hermes
/model切到workbuddy-hub报 "could not reach this custom endpoint's model listing"(不是 "auth failed"、不是 "model not found"——是 reachability) hermes_cli/models.py probe_api_models有 5min_probe_neg_cache,一次失败后续 5min 全 cached 失败
修法(按顺序):
# 1. .env 必须含 NO_PROXY(Python urllib 读这个;curl 读 no_proxy 也读)
# 写到 /opt/data/.env 里:
echo 'NO_PROXY="*"' >> /opt/data/.env
echo 'no_proxy="*"' >> /opt/data/.env
# 2. 验证(hermes 容器内):
NO_PROXY='*' no_proxy='*' python3 -c "
import urllib.request
req = urllib.request.Request('http://192.168.31.201:8788/v1/models',
headers={'Authorization':'Bearer <hub launcher_key>'})
print(urllib.request.urlopen(req, timeout=5).status)
"
# 期望 200
# 3. hermes .env 是 hermes CLI 启动时 load(`load_env()` memoized,subprocess 重新读)。
# **不需要重启 hermes gateway 进程**——下次 `hermes chat`/`hermes model` 自然 reload绝对不要 unset HTTP_PROXY——hermes 还要靠它走公网(OpenRouter / Anthropic 真实 API)。
🔴 但也不能写 NO_PROXY=*(2026-10-19 真实事故:TG 掉线 10 分钟):* 让所有出口直连,包括 api.telegram.org。容器直连 Telegram 必死(DNS 被污染成 199.96.59.61 之类 / 149.154.x 被墙),日志表现为 ConnectTimeout 反复重试 8 次后 telegram connect timed out after 180s。正确值是定点 LAN 列表:
NO_PROXY="192.168.31.201,127.0.0.1,localhost,::1"
no_proxy="192.168.31.201,127.0.0.1,localhost,::1"验证一对(改完 .env 前后各跑一遍):
python3 -c "from hermes_cli.env_loader import load_hermes_dotenv; load_hermes_dotenv()
from gateway.platforms.base import resolve_proxy_url, should_bypass_proxy
print('tg bypass', should_bypass_proxy(['api.telegram.org'])) # 必须 False
print('tg proxy', resolve_proxy_url('TELEGRAM_PROXY', target_hosts=['api.telegram.org'])) # 必须给出 mihomo URL
print('hub bypass', should_bypass_proxy(['192.168.31.201'])) # 必须 True"日志侧铁证:连接成功时会出现 [Telegram] Proxy detected; passing explicitly to HTTPXRequest: http://…:7890 —— 没有这一行 = 走直连 = 必掉。
.env 改动对已在跑的 gateway 不生效(load_env 在进程启动时读),改完必须 hermes gateway restart;重启后看 ✓ telegram connected。
诊断步骤(用于"我之前是好的现在不行了")**:
# A. 容器内 HTTP_PROXY 是否还在
env | grep -E '^HTTP_PROXY=|^HTTPS_PROXY='
# B. 同一个 URL 直接 curl vs urllib,对比:
curl -sS -m 5 -o /dev/null -w 'curl=%{http_code}\n' http://<LAN>:<port>/v1/models -H 'Authorization: Bearer <key>'
python3 -c "import urllib.request; print('urllib=', urllib.request.urlopen(urllib.request.Request('http://<LAN>:<port>/v1/models', headers={'Authorization':'Bearer <key>'}), timeout=5).status)"
# curl=200 + urllib=502 → mihomo 截胡
# curl=502 + urllib=502 → 端口根本不通,先看 hub 容器 alive
# C. NO_PROXY 临时测:
NO_PROXY='*' python3 -c "import urllib.request; print('NO_PROXY=*:', urllib.request.urlopen(urllib.request.Request('http://<LAN>:<port>/v1/models', headers={'Authorization':'Bearer <key>'}), timeout=5).status)"常见踩坑:
NO_PROXY=192.168.31.0/24不生效——Python urllibno_proxy不识别 CIDR,只认*/ 单 IP / hostname- 只设大写
NO_PROXY不设小写no_proxy——历史代码两条都读 - 修了 .env 但当前
hermes chat还报"could not reach"——_probe_neg_cache5min TTL,跑hermes model --refresh(需 TTY)强制清缓存
CLI vs gateway 的 NO_PROXY 修复(分两套)
.env 改 → 只对 hermes chat / hermes model CLI 子进程生效(CLI 启动时 load .env,subprocess 重新读)。gateway 长进程(s6 起的)是 /run/s6/container_environment/* 文件——容器启动时 bake 进去,运行时改 .env 它看不到。
关键判断:
- 用户在 WeChat/TG 跟 hermes 聊,用 gateway 进程——需要
NO_PROXY在/run/s6/container_environment/+ 容器重启 - 用户在主机跑
hermes chat -q "..."测试 / agent 自己跑 CLI 验证——.env加NO_PROXY立刻生效,不需要 gateway 重启
用户从 WeChat/TG 抱怨"对话不能聊" = gateway 路径——必须改 /run/s6/container_environment/ + docker restart hermes。用户跑 hermes chat 测试 = CLI 路径——.env 加 NO_PROXY 就行。
🔴 s6 gateway 进程的 NO_PROXY 修复(.env 改不够)
坑:上一节说"修 /opt/data/.env 就行"只对 hermes chat CLI 子进程生效。gateway 长进程是 s6-overlay 启动的,env 来源不是 .env,是 /run/s6/container_environment/* 文件——这些文件是容器启动时 bake 进去的,gateway 已经跑起来之后改 .env 它看不到。
症状:CLI Python urllib 设了 NO_PROXY=* 能通 200,但 gateway 进程对外发的 chat 请求依然 502——因为 gateway 还是 HTTP_PROXY=..., NO_PROXY=。
验证 gateway 是否也走 mihomo:
# 看 gateway 进程的实际 env(hermes 容器里)
for pid in $(pgrep -f 'hermes.*gateway'); do
grep -aE '^(HTTP_PROXY|HTTPS_PROXY|NO_PROXY|no_proxy)=' /proc/$pid/environ 2>/dev/null | tr '\0' '\n'
done
# 期望:HTTP_PROXY=...; HTTPS_PROXY=...; 没 NO_PROXY → 它走代理两种 502 表现(实测命中):
urllib.error.URLError: Connection refused—— mihomo 拒 LAN IP(最常见,§22 配 NO_PROXY=* 就解决)urllib.error.HTTPError: 502 Bad Gateway—— urllib 成功连到 mihomo 但 mihomo 自己回 502(也配 NO_PROXY=*)
修复 3 种(按优先级):
- 整容器重启(持久化推荐):改 docker-compose 加
NO_PROXY: "*"+docker compose up -d hermes,s6 重新 bake env 到/run/s6/container_environment/。代价:断当前所有 session。 - 只重启 gateway service(无容器重启):在飞牛 SSH 用 root 写
/run/s6/container_environment/NO_PROXY+s6-svc -h /run/s6/services/main-hermes。不断其它不依赖 gateway 的会话,但 session 引用 gateway 进程会被踢——用户感觉跟整容器重启一样。 - s6-rc change(hermes 用户可跑):
/command/s6-rc -h list、/command/s6-rc change main-hermes—— hermes 用户有 s6-rc 调用权,但/run/s6/container_environment/hermes 用户写不了,所以光 change 不写文件等于白跑。
完整 root 修法(在飞牛 SSH 跑,最稳):
ssh fnos-root 'echo "NO_PROXY=*" > /run/s6/container_environment/NO_PROXY
echo "no_proxy=*" > /run/s6/container_environment/no_proxy
docker restart hermes' # 整容器重启,s6 重新 bake env
sleep 5
ssh fnos-root 'docker exec hermes grep -aE "NO_PROXY" /proc/$(pgrep -f "hermes.*gateway" | head -1)/environ | tr "\0" "\n"'
# 期望看到 NO_PROXY=* / no_proxy=*完整 compose 修法(持久化,避免每次重启容器都要再设):
ssh fnos-root 'cat /vol1/1000/Docker/hermes/docker-compose.yml' # 先看现有 env
# 在 environment 段加:
# - NO_PROPY=*
# - no_proxy=*
ssh fnos-root 'cd /vol1/1000/Docker/hermes && docker compose up -d hermes'验证 gateway 修好了:hero 用 /model <provider>:<model> 切到 hub 模型,发消息能秒回;wb-hub docker logs --since 2m | grep chat: 能看到对应 chat 请求日志。
常见踩坑:
NO_PROXY=192.168.31.0/24不生效 ——Python urllibno_proxy不识别 CIDR,只认*/ 单 IP / hostname- 只设大写
NO_PROXY不设小写no_proxy——历史代码两条都读 - 修了
/run/s6/container_environment/但没重启 gateway service ——s6-init 启动时读 env 文件,运行期不监听 env 文件变化(这是 s6-overlay 设计,不要去 grep 文档争论) - 修了 .env 重启了 hermes 但 NO_PROXY 还是空 ——说明 docker-compose / docker run 命令本身没传 NO_PROXY,光改 .env 不够(
.env影响 hermes CLI,不影响 s6-supervised 长进程) - 飞牛 hermes 容器是
docker run直接启的,没有 docker-compose 文件 ——要写一个 compose 或者改宿主/etc/systemd/system/hermes.service之类。短期最快的办法:在/run/s6/container_environment/写文件 +docker restart hermes,长期再去 compose 化。
🔴 Agent 不能从 hermes 容器内核重启自己(kernel 死了 = session 死了)
踩坑(2026-10-18 实测):agent 在 hermes kernel 里跑 docker stop hermes 想重启加载新 NO_PROXY env——docker stop 杀掉了自己运行的容器,session 立刻中断,kernel 死,user 看到 Operation interrupted,agent 后续所有 execute_code / terminal 都 Session kernel died while running the cell。即使 s6 auto-restart 把 hermes 容器重新拉起来(59 秒后 Up),那个 kernel session 已经永久死掉,新的对话会起新 session。
规则:永远不要在 hermes kernel 内执行以下任一:
docker stop hermes/docker kill hermes/docker restart hermes/docker rm hermespkill -f 'hermes.*gateway'/kill -9 <gateway_pid>(gateway 是 s6-supervised 但其他 hermes 进程会被波及)- 任何会杀 PID 1 (s6-init) 的操作 ——
kill 1、reboot、shutdown
改 hermes gateway env 的正确流程(从飞牛 SSH 端走,不在 hermes kernel 里):
# Step 1: 备份当前 hermes 容器配置(防止 docker run 命令拼不回去)
ssh -tt fnos 'echo ${FNOS_PASSWORD} | sudo -S docker inspect hermes > /tmp/hermes-inspect.json'
# Step 2: 备份完整 env(含敏感 key)
ssh -tt fnos 'echo ${FNOS_PASSWORD} | sudo -S docker inspect hermes --format "{{range .Config.Env}}{{println .}}{{end}}" > /tmp/hermes-env.txt'
# Step 3: stop + rm(host 端执行,hermes kernel 不参与)
ssh -tt fnos 'echo ${FNOS_PASSWORD} | sudo -S docker stop hermes && echo ${FNOS_PASSWORD} | sudo -S docker rm hermes'
# Step 4: 重建(保留 bind/network/image,加新 env)
ssh -tt fnos "echo ${FNOS_PASSWORD} | sudo -S docker run -d --name hermes \\
--network hermes_default \\
-v /vol1/1000/Docker/hermes:/opt/data:rw \\
--restart always \\
-e HTTP_PROXY=http://192.168.31.201:7890 \\
-e HTTPS_PROXY=http://192.168.31.201:7890 \\
-e NO_PROXY='*' -e no_proxy='*' \\
-e LLM_MODEL=deepseek/deepseek-v4-flash \\
-e DEEPSEEK_API_KEY=<真 key> \\
-e TELEGRAM_BOT_TOKEN=<真 token> \\
-e GATEWAY_ALLOW_ALL_USERS=true \\
-e PYTHONUNBUFFERED=1 \\
-e PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright \\
-e HERMES_HOME=/opt/data \\
-e HERMES_RUNTIME_DIR=/opt/hermes/tools \\
-e HERMES_PYTHON=/opt/hermes/.venv/bin/python \\
-e HERMES_TUI_DIR=/opt/hermes/ui-tui \\
-e HERMES_WEB_DIST=/opt/hermes/hermes_cli/web_dist \\
-e HERMES_WRITE_SAFE_ROOT=/opt/data \\
-e HERMES_DISABLE_LAZY_INSTALLS=1 \\
-e HERMES_LAZY_INSTALL_TARGET=/opt/data/lazy-packages \\
-e PYTHONDONTWRITEBYTECODE=1 \\
-e XDG_RUNTIME_DIR=/tmp/hermes-runtime \\
-e CC=gcc -e CXX=g++ \\
nousresearch/hermes-agent:latest gateway run"
# Step 5: 验证
sleep 5
ssh -tt fnos 'echo ${FNOS_PASSWORD} | sudo -S docker ps --filter name=hermes --format "{{.Names}}\t{{.Status}}"'
ssh -tt fnos 'echo ${FNOS_PASSWORD} | sudo -S docker exec hermes pgrep -af gateway | head -3'改 env 而不是重建:如果只是想改 env,先备份 env 全量再用相同参数重建(docker run 不支持改 env),没有捷径。docker container update --env-add 不支持,只能 stop+rm+run。
核验 env 真的进了容器(不是 .env 或 set -f):
ssh -tt fnos 'echo ${FNOS_PASSWORD} | sudo -S docker exec hermes sh -c "for pid in \$(pgrep -f gateway); do grep -aE \"^(HTTP_PROXY|HTTPS_PROXY|NO_PROXY|no_proxy)=\" /proc/\$pid/environ 2>/dev/null | tr \"\\0\" \"\\n\"; done"'
# 期望: 看到 HTTP_PROXY=..., HTTPS_PROXY=..., NO_PROXY=*, no_proxy=*
# 期望中**不**看到的: 任何来自 .env 但 s6-supervised 进程没读到的变量关于 s6 service restart(不重启整个容器)的限制:理论上 s6-svc -h /run/s6/services/main-hermes 只重启 gateway service 不重启容器,理论上不杀 agent session——但 agent 跑在 main-hermes 启动的 python 里(/proc/self/exe 是 hermes 内核),service 重启 = main-hermes 全组进程重启 = agent 也被杀。实测无差异,还不如整容器重启来得稳。
为什么 .env 的 NO_PROXY 不够:s6-supervised 长进程启动时完全从 /run/s6/container_environment/* 文件读 env,不读 /opt/data/.env。.env 是 hermes config / hermes chat CLI 启动时 load_env_file() 才读,对用户从 WeChat/TG 跟 hermes 聊天的路径(gateway 长进程)完全无效。两次踩坑写下来:改 .env → 跑 hermes chat -p hub -m X -q "ping" → CLI 自己通 → 但用户说"还是聊不了" → 因为 gateway 没读 .env → 还是走 mihomo 502。只有重启容器并把 NO_PROXY 写进 /run/s6/container_environment/(或 docker run -e)才能修。
23. "mp2 智能助手能用" ≠ "hermes 调这个反代能用" (rule, 别外推)
用户说"mp2 的 LLM 助手调这个反代好好的" 不能 当 hermes 调同反代也通的证据。两个消费者走的是不同路径:
| 消费者 | 配置位置 | base_url 通常是 | 鉴权 key | 容器网络栈 |
|---|---|---|---|---|
mp2 (jxxghp/moviepilot-v2) |
PG app_settings.dynamic_app_config jsonb |
http://192.168.31.201:7863/v1(老 sliverkiss-go gateway,跟 hub 8788 是不同容器) |
Wb2Api-Russ534-20261018-... |
mp2 容器自己的网络,不走 hermes gateway |
| hermes | /opt/data/config.yaml providers.workbuddy-hub |
http://192.168.31.201:8788/v1(hub) |
wb-37S9Wcidx6Wcjo4rLNQ-iOoV2yKc0HQL |
hermes 容器,走 s6-overlay 起的 gateway 进程 + mihomo proxy |
踩坑链路(实测多次踩):
- mp2 用 7863 端口通 → 用户推断 hermes 也通 → 但 hermes 配的是 8788 → 8788 跑不了或 hermes 调它有别的问题
- mp2 容器直连 LAN 没代理 → hermes 容器走 mihomo 代理 → mihomo 拒 LAN → 502(§22 链)
- mp2 用的 key 跟 hermes 用的 key 不是同一个 → 一边通 ≠ 另一边通
- mp2 直连 7863 不走 hermes gateway ——mp2 容器不是 hermes 的下游,是独立消费者(PG
app_settings.dynamic_app_config配的ai_base_url=http://192.168.31.201:7863/v1,直接出 container)。改 sliverkiss-go → hub 时,mp2 的 PG 字段也要同步改——mp2LLM_BASE_URL=http://192.168.31.201:7863/v1不跟着切 hub,mp2 同样 502。 - mp2 直连走的是容器内直连(不绕 mihomo),所以 mp2 调 7863 OK 不等于 hermes 调 8788 也走容器直连 ——两个容器的网络栈独立,mihomo 注入也独立
反推不可用的两种(实测命中):
- 用户说"mp2 OK" → agent 推"hermes 调 hub 也 OK" → 错(不同容器不同配置不同 base_url)
- 用户说"hub 5/5 模型跑通" → agent 推"hermes 调 hub 也 OK" → 错(hermes kernel urllib 通但 s6 gateway 走 mihomo 502)
"X 容器用着好,所以新容器也会通" 的逻辑完全错——每个容器有独立 env / 网络栈 / 配置位置。
正确判断 hermes 是否真通(不靠外推):
hermes config get providers.workbuddy-hub看实际配的 api / api_key- hermes 当前 TG session routing 里
model_config.gateway_runtime.base_url是啥 request_dump看真实 gateway 实际发的 URL + 响应docker logs wb-hub --since 2m看 hub 端有没有收到那个请求 + chat 状态- 不用 "mp2 通" / "wb-hub /v1/models 通" 当 hermes 通的证据
23a. mp2 v2 Web 智能助手流式对话 endpoint (实测 2026-10-19 真通)
mp2 v2 (jxxghp/moviepilot-v2) 的"智能助手" web UI 是反代过来的,endpoint 在 /api/v1/message/agent/stream(不是 /agent/stream,前缀 /message/agent 在 /app/app/api/apiv1.py:14 include_router(agent.router, prefix="/message/agent"))。完整流程:
# 1. 登录拿 token(oauth2 form)
TOK=$(curl -sS -m 8 -X POST http://192.168.31.201:3000/api/v1/login/access-token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=${FNOS_PASSWORD}" | jq -r .access_token)
# 路径错位: /api/v1/user/access-token → 405, /api/v1/login/access-token → 200
# 2. POST /api/v1/message/agent/stream,body 字段是 text (不是 message)
curl -sS -m 120 -X POST http://192.168.31.201:3000/api/v1/message/agent/stream \
-H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
-d '{"text":"ping 答 1 字 pong","session_id":"e2e-test"}'
# 期望 SSE 响应:
# data: {"type": "start", "session_id": "web-agent:..."}
# data: {"type": "delta", "content": "pong"}
# data: {"type": "done"}endpoint 发现路径(任何 mp2 / 类似项目的 web API 都这套思路):
# 找 router 注册位置
ssh fnos-root 'docker exec moviepilot-v2 cat /app/app/api/apiv1.py | head -20'
# 找目标文件里 @router.post 端点
ssh fnos-root 'docker exec moviepilot-v2 grep -nE "@router.post.*chat|@router.post.*stream" /app/app/api/endpoints/<file>.py'
# 看 input schema
ssh fnos-root 'docker exec moviepilot-v2 grep -nE "class AgentWebChatRequest" /app/app/schemas/*.py'关键陷阱:
- 字段
text(不是message/content/prompt)—— 看 schema 文件不是瞎猜 session_id复用历史对话;测试用一次性 UUID-style ID 避免污染~/.hermes_sequence/state.db会话历史- 超时设 ≥ 60s(agent 调用 hub 模型 + 反思可能要 20-40s)
- 改了 PG
dynamic_app_config的ai_*字段确实不需要重启 mp2 容器(容器启动后 settings loaded 一次,但每个 LLMHelper 调用都从 settings.get_* 重新读)—— 但 改了 .env 的LLM_*后必须 restart(env 是 process 启动时读)
为什么必须测这个 endpoint:mp2 的 LLM_* env + PG dynamic_app_config.ai_* 经常不同步(mp2 v2 4.x 双层配置),改 PG 但 mp2 实际走 env 是常见坑。只看 .env LLM_BASE_URL 是 hub 不是 mp2 真在用——唯一权威测试是发一发 /api/v1/message/agent/stream,响应内容是不是 hub 模型答的(不是 siliconflow 旧答)+ docker logs wb-hub --since 30s | grep "POST /v1/chat" 有对应请求。
24. 验证 hermes 调某个 provider 真通,必须三处都看 (rule, 反"自测 OK"陷阱)
踩坑:agent 说"我自己 curl /v1/chat 通 200"就拍胸脯"已生效"——这条证据只覆盖第 1 个环节,hermes gateway 实际有没有走对不走,不看下面几处就是漏:
| 检查点 | 命令 | 期望 |
|---|---|---|
| 1) 上游 API 真活 | curl http://<LAN>:<port>/v1/models -H 'Authorization: Bearer <key>' |
200, N 模型 |
| 2) 容器内直连也活 | hermes kernel 内 urllib.request.urlopen(...) |
200(验证 NO_PROXY 不被截胡) |
| 3) gateway 进程 env 对 | cat /proc/$(pgrep -f 'hermes.*gateway')/environ 看 HTTP_PROXY / NO_PROXY |
NO_PROXY=* 必须有 |
| 4) gateway 配置对 | hermes config get providers.X |
api / api_key / models 字段全有 |
| 5) session routing 对 | SELECT id, model_config FROM state.db WHERE id='<session>' |
gateway_runtime.base_url 指向 X |
| 6) hub 端实际收到 | docker logs wb-hub --since 2m | grep "chat:" |
看到对应 chat 请求 200 |
只要 1+2 通就报"已生效" → 至少缺 3+5——gateway 用的 env 跟 kernel 内 Python 不同,配置 vs session 持久化又是两层。用户抱怨"切了不灵"通常栽在 3 或 5。
报告顺序:每条独立一行,先数字(pid / port / status code / 行数)后结论。不要 只给"通了"两字。
端到端 chat 真测命令(任何 provider 切换后必跑,必须带 --provider/-p,否则卡 30+ 秒超时,详见 §3 末尾):
hermes chat --provider <provider_slug> -m <model_id> -q "ping 答 1 字 pong"
# 例: hermes chat --provider workbuddy-hub -m deepseek-v4.1-flash -q "ping"25. "加 1 个新模型" ≠ "替换现有 5 个的某一个" (rule)
用户说"加 1 个新模型 / 你看着加 1 个 / 还有 1 个没加"——默认解读:在已有列表基础上追加新模型(不替换、不去重)。agent 经常错误地把 "加 1 个 cn:deepseek-v4.1-flash" 当成"换成这个",然后抱怨"已经在了"——实际原列表里那个可能是别的/旧的:
- "再加 1 个
cn:deepseek-v4.1-flash":如果列表里已有这个,用户说"加"的可能是想加deepseek-v4-pro或别的同名变体;必须问而不是猜 - 真想确认要不要换 → 直接问"列表里已有 X,替换成哪个新模型?"
- 不想问 → 试探性加一个明确不重复的候选(从
/v1/models取个用户当前 5 个列表里没有的),报给用户看
反例:本次对话用户问"再加 1 个 cn:deepseek-v4.1-flash",agent 答"已经在了不用动"——用户实际是想要更多可用模型,agent 误解为确认意图。正解:答"这个已经在列表里了,你是不是想加 cn:deepseek-v4-pro 或别的?我列 5 个候选你选"。
26. 配完配置必做的 5 步验证(防"配置看着对,实际没生效")
任何改完 config.yaml / .env / docker-compose / state.db 后的强制 checklist,用户问"你确认了没"就对照这个:
- 重新读配置(防 session 间漂移):
hermes config get <key>、cat /opt/data/config.yaml、cat /opt/data/.env——自己读而不是引用上一轮的记忆 - 重探上游:
curl http://<LAN>:<port>/v1/models -H 'Authorization: Bearer <key>'——这次的,不是上轮的 - 核对模型命名:
/v1/models的 id 列表 vsproviders.X.models列表——精确匹配(大小写、前缀、引号都看) - 端到端 chat 真过:
hermes chat --provider <provider_slug> -m <model_id> -q "<一句短提示>"— 必须显式带--provider(详见 §30)
完成 ≠ "set 命令退出 0"。完成 = 5 步全过 + 实际一次端到端 chat 通了(hero 手动发或 hermes chat --provider X -m Y -q "ping" 模拟)。
27. 容器 hostname = 当前 kernel 所在容器(hermes 自身的别名)
Hermes 的 host: hermes runtime env,hostname 命令输出 hermes——这是当前 kernel 所在容器的 hostname,不是飞牛宿主。任何"在 hermes 里 ssh 到 hermes 容器"或"docker exec hermes 在 hermes 容器内"的表述都是错的——docker exec hermes 是从别的环境(飞牛 SSH)发起的,从 hermes 容器内核视角,自身 hostname 就是 hermes。
实际含义:
- hermes kernel 内 ssh 到飞牛(
ssh fnos)→ 是从 hermes 容器往外连飞牛 - 飞牛 SSH 内
docker exec hermes <cmd>→ 是从飞牛进 hermes 容器跑命令 - hermes kernel 内
docker ps→ hermes 容器内没 docker CLI(sandbox 没装),会command not found cat /proc/1/cgroup显示0::/→ 在容器内(root cgroup namespace);在宿主会显示完整 cgroup path
别再说 "我先在 hermes 里 ssh 到飞牛,然后在飞牛上 docker exec 到 hermes 容器里跑命令"——这种绕法不存在。要么 hermes kernel 内直接执行(self ssh not needed),要么从飞牛宿主端直接 docker exec hermes。
28. hermes 实测网络 / 探针 跟"上游是否真的收到请求" 必须分别确认
跑下面这条常被当成"hermes 通" 的检查只证明第 1 个环节:
# hermes kernel 内:
import urllib.request, json
r = urllib.request.urlopen("http://192.168.31.201:8788/v1/chat/completions", ...)
print(r.status) # 200实际 hermes gateway 发请求时,可能:
- 走不同的 proxy 配置(§22 讲过)
- 用不同的 model id(用户在
/model选了别的) - session routing 不一样(state.db 里 base_url 是旧值)
完整链路确认:
- hermes kernel 内 urllib 直连 8788 → 200(只证明 8788 容器自己 + hermes kernel 网络 + 没代理拦)
- hermes
/v1/models探针 success(只证明 hermes catalog 拿到了列表) docker logs wb-hub --since 5m | grep POST /v1/chat看到 hermes 的请求 → 这是真凭实据
只做 1+2 就报"通了"→ 100% 缺第 3 步——而第 3 步恰恰是用户最关心的"切了不灵"那种隐性失败。
29. tg 微信 / v 微信 / v 微信端" 误判 5 次 = 不强提示(rule)
本对话中 agent 多次把 TG(chat_id=7271375745)的消息说成"微信"或"微信端",反复被纠正。已记在 §16 + memory,但对话中复现率高。
强提醒:每次回复前 1 秒检查 origin JSON 的 platform 字段——platform == "telegram" 就说 TG,platform == "weixin" 就说微信。不要凭记忆、不要凭上次的消息、不要凭机器人用户名。
最常踩的两个误判:
- 截图里有"Openclaw"/"MyGPT"/"Open"机器人名 → 当成微信(实是 TG 的 bot 截图——微信 i-bot 是不同 UI 元素)
- 截图里有"公众号"提示 → 当成微信(实是 TG 转发卡片)
判别关键:chat_id 的格式 + platform 字段。TG chat_id 是纯数字(7271375745),微信 chat_id 是 o9cq...@im.wechat 形式。只看 chat_id 前缀即可一秒定:数字 → TG,o9cq... → 微信。
session 层另一根线:每次回复前查 origin JSON 同时确认 chat_id 跟 system prompt 里的 Home Channels(TG=7271375745 / 微信=o9cq806xgTLbQAbl_N01pCJLw9Bg@im.wechat)对应关系——发现不一致(如 origin 说 TG 但 chat_id 是 o9cq...)立刻报错而不是凭记忆解释。
30. "改了 = 真生效" vs "改了 = 配置看起来对" (rule, 加 wb2api-hub 后多发)
部署完 OpenAI-compatible 反代 (wb2api-hub) → 改 4 个下游配置文件 (hermes / mp2 PG / mdc JSON / .env) → 报告"切换完成"是常见流。这个报告等于 0 实际保证——只代表你改了文件, 不代表下游服务真用上了新配置 / 新 key / 新 base_url。
踩坑实例 (2026-10-19 实测, 爸爸原话: "告诉我你改了个啥? mdc 容器我还没检查, hermes 这边一直连通不了我怀疑也是这个原因吧"):
- 我改了 mp2 PG
dynamic_app_config.ai_api_key+ .envLLM_API_KEY(用的 launcher_key 塞给所有下游) - 我改了 mdc
metadata.openai_key(用的同一个 launcher_key) - 我改了 hermes
providers.workbuddy-hub.api_key(用的同一个 launcher_key) - 报告"切换完成"
- 实际 mdc 容器没检查是不是还在用 wb2api 老地址
- 实际 hermes gateway 长进程 env 里
NO_PROXY没改 (配置改了, s6-supervised 进程不监听 .env) - 实际 mp2 智能助手 endpoint 没真发过 一发测试 chat, 不知道响应是不是 hub 模型答的
修法 - 改完必做 5 件真测 (按这个顺序跑, 任何一项 fail 都得继续 fix, 不能报"切换完成"):
| # | 测试 | 命令 / 步骤 | 期望 |
|---|---|---|---|
| 1 | mp2 智能助手真发 | 进 mp2 WebUI :3000 智能助手 tab, 发 "ping" 1 字 |
收到 hub 模型答的 (不是 siliconflow 旧答) + docker logs wb-hub --since 30s | grep chat 有请求 |
| 2 | mdc 翻译真发 | 触发一个 mdc 转种任务, 或手动 docker exec mdc python3 -c "..." 调翻译 |
不再 "无 key" 报错, mdc 日志里没 TranslationError |
| 3 | hermes 链路 | §24 三处 + hermes chat -p workbuddy-hub -m deepseek-v4.1-flash -q "ping" |
gateway 进程 env 有 NO_PROXY=*, chat 命令实际答 1 字, 不 timeout |
| 4 | key 类型对 | 看所有下游配置的 key 是不是 97ff4fe0... 格式 hex (dashboard key), 不是 wb-... 格式 (launcher_key) |
launcher_key 只 在 hub 运维端点 (改 settings.json / 看 /health), 不 下游 .env / config.yaml / mdc JSON |
| 5 | 完整报告 | 上面的 1-4 全过 + 列所有改了哪几处 + 给爸爸一份"切换清单" | "切完" = 5 件全过 + 报告逐条数字 |
"改了 = 生效" 是错误假设, 任何报告 "切换完成" 必须前面有真实发过请求的响应作为证据。
"改了 = 通过默认 verify" — PG 改字段会立即生效 (无 restart), config.json 改字段需要 restart (Node.js 启动时一次读完), .env 改 env var 改不影响 s6-supervised 长进程 (只影响 hermes CLI 子进程)。
31. "我以为...结果..." (rule, 加 launcher_key / dashboard key 误区后多发)
部署 hub 时我把 accounts/settings.json.launcher_key (自动生成的 wb-37S9Wcidx6Wcjo4rLNQ-iOoV2yKc0HQL) 当成 "hub 的 API key" 直接塞给所有下游——错了。launcher_key 是 hub 第一次启动自动写的 系统级 admin key, 跟 dashboard / panel 里 手动新建的 业务 API key (97ff4fe052d0fd446028e4cd4da05abfda64 这种 hex) 是两套独立系统:
- launcher_key: 全权限 (等同管理 dashboard), 改 settings.json 重启就失效所有调用方 (重启所有下游跟着死)
- dashboard API key: 业务调用方专用, dashboard 可单独 revoke 不影响其他调用方, label 区分 (eg
hermes-prod,mp2-assistant,mdc-translate)
为什么我之前会用错:
- 看到
wb-前缀 → 以为这是 hub 的"主 key", 拿来用 - 看到能调
/v1/models→ 以为有权限 - 没去 dashboard 找 "新建 API key" tab, 没意识到还有另一套系统
- README 提 "Dashboard 默认密码 admin" 但没说 launcher_key ≠ dashboard 业务 key
怎么避免 (下次加新反代 / OpenAI-compatible 中间件):
- 装完不要立刻用 launcher_key 配下游
- 先改 dashboard 默认密码
- dashboard 里逐一为每个下游新建 key + 给清晰 label
- 再用 dashboard key 配下游配置 (hermes / mp2 PG / mdc JSON / 各 .env)
- launcher_key 留在 hub 服务器本地用, 不下发任何下游
- 最后跑 §30 的 5 件真测
详细 key 类型对比 + 下游分配清单 + 抓 key CLI 见 references/workbuddy2api-hub-deploy.md "launcher_key ≠ dashboard 发的 API key" 节。
32. 国际版 (intl) 免费模型 + 用 X-Realm 强制出口(实测 2026-10-20)
hub 有两套账单:同一个 model id 在两个 realm 价格不同。/v1/models?realm=intl 里 credits: x0.00 就是免费。
实测(2026-10-20):
| model | intl | cn |
|---|---|---|
| deepseek-v4.1-flash | x0.00 免费 | x0.11 |
| hy3 | x0.00 | x0.00 |
| step-5-preview | x0.00 | x0.00 |
| glm-5.3-flash | x0.08 | x0.08 |
源码口径:wb_proxy.free_models_by_realm() 只认 credits in ("x0.00","x0","0","0.00"),按 realm 分别算;wb_accounts.is_free_cap_model(realm, model) 是限额联动范围。查当下集合:
ssh fnos-root 'docker exec -i wb-hub python3 -' <<'PY'
import sys; sys.path.insert(0,"/app")
import wb_proxy
fm = wb_proxy.free_models_by_realm()
for r in ("intl","cn"): print(r, sorted(fm.get(r) or ()))
PY首选:把该 Key 的 realm 绑成 intl(面板 → 设置 → API Key → 编辑 → 出口),服务端强制,客户端漏带头也不会掉回 cn。次选:X-Realm: intl 请求头(优先级 wb_proxy._request_realm():?realm= 显式 > key 绑定的 realm > X-Realm 头 > ?realm= query > 全局 active_realm——key 一旦绑了 realm,头就失效)。所以先看 key 有没有绑 realm(accounts/settings.json 的 api_keys[].realm 为空才轮到头生效)。
hermes 侧接法:
hermes config set providers.workbuddy-hub.extra_headers '{"X-Realm":"intl"}'验证不能只看 200,要看哪个账号接的单:
ssh fnos-root 'docker logs wb-hub --since 2m 2>&1 | grep "chat done" | tail -5'
# acct=05e0ef5f / 6554baa5 = intl 账号;d64722ef 等 = cn三个必踩的坑
extra_headers按 base_url 匹配(config_providers._entries_for_route只比 route base_url,不看 provider 名)。同一个 base_url 不能挂两个 header 不同的 provider(想"一个走 cn 一个走 intl"这样配 = 两个都被打上 intl 头)。要么单 provider 全 intl,要么用 key 绑 realm 分两把 key(同一 base_url 无需 header,就不会互相污染)。- cn 独占模型在 intl 出口下 400:
exclusive_realm()判定CN_EXCLUSIVE = {deepseek-v4-pro, glm-5.1, glm-5v-turbo, kimi-k3-1, kimi-k2.7, minimax-m3, ...}+ 前缀minimax-/deepseek-v4-pro。实测minimax-m3.1-flash、deepseek-v4-pro走 intl 直接报「只在国内版提供」。intl 链里不能放这些,哪怕/v1/models?realm=intl里列了 minimax-m3.1-flash(列表有 ≠ 不跨域)。intl 可用:deepseek-v4.1-flash / hy3 / hy4-preview / glm-5.3(-flash) / glm-5.2 / kimi-k3 / kimi-k2.6 / kimi-k2.8-preview / step-5-preview / gpt-5.x / gpt-6.x / grok-4.7 / gemini-3.5-3.8-flash。 - 改完对在跑的 session 不生效:provider/header 是 agent init 时定死的,当前会话仍走老出口,新会话才走 intl。判断时别把"当前对话还在 cn 账号"当成配置没生效——用
hermes chat --provider ... -q另起一发验。
附带:custom provider 的 fallback 被"空凭据池"挡死(2026-10-19 真实事故)
症状:429 后日志出现 Fallback skip: <provider>/<model> credential pool is exhausted (every entry in cooldown),但其实没有任何 cooldown。
根因:auth.json 的 credential_pool.<provider> 有一行 source: env:XXX_API_KEY,env 源条目不落盘 secret,每次 load 重新水合;水合走 _seed_from_env() → PROVIDER_REGISTRY.get(provider),自定义 provider 不在注册表里 → 直接 return,密钥永远补不上 → _available_entries 因 runtime_api_key 为空丢弃该条目 → has_available()=False 且 next_available_at()=None → _candidate_pool_exhausted() 判定"池耗尽"跳过整条 fallback。
修法:删掉这个不可水合的池行(密钥在 providers.X.api_key 里本就有):
hermes auth remove <provider> <id> # 会顺带清 .env 里的变量并抑制重新播种验证:_candidate_pool_exhausted(agent_stub, provider, model) 应变 False。别用 hermes auth status <custom_provider> 判断——它对自定义 provider 恒 logged out(§19)。
33. 看板设置巡检(登录 → 抓全量设置 → 按实际用量给优化)
用户问"哪些设置需要优化 / 你看看看板"时的完整流程、每个开关的判断依据、以及"面板积分列跨 realm 混算"这类读数据的坑,见 references/hub-dashboard-review.md。
三条最常被忽略的结论先记在这里:
- Key 绑出口 > 客户端带头:5 个 Key 全"跟随面板切换"时,任何客户端漏发
X-Realm就静默回到付费出口;面板上按 Key 绑定出口是服务端强制的。 - 国际版"每日活跃"不要用主力那个免费模型:活跃打卡也吃"账号 × 模型"的 24h 免费窗口,和真实流量抢同一个窗口,撞 6004 后主力直接 429;把活跃模型换成另一个免费模型(hy3 / step-5-preview)。
- Anthropic Messages 默认关:开了等于给 Claude Code/SDK 开一条丢
cache_control的路(长会话积分飙升)。确认logs.sqlite3里/v1/messages命中为 0 再关掉。
34. hub 看板运维 API + 真浏览器登录路径(2026-10-20 实测)
34.1 不用点 UI,直接打面板 API(改设置/绑出口/开限额)
# 1. 登录拿 token(面板密码在 settings.json 里是 PBKDF2 摘要,明文要问爸爸/看密码库)
TOK=$(curl -s --noproxy '*' -X POST http://192.168.31.201:8788/panel/login \
-H 'Content-Type: application/json' -d '{"password":"<panel_pwd>"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
# 2. 写设置(合并语义:只传要改的字段,不传的保持原值)
curl -s --noproxy '*' -X POST http://192.168.31.201:8788/settings/save \
-H "Authorization: Bearer $KEY" -H "X-Panel-Token: $TOK" -H 'Content-Type: application/json' -d '{...}'/settings/save 常用字段(都实测可用):
| 字段 | 形状 |
|---|---|
api_keys |
全量 list,每行 {id,name,realm,models,concurrency_limit,enabled,key:""}(key 空=保留原密钥;必须带 deleted_api_key_ids:[] 才是 upsert,否则是 replace-by-omission) |
limits |
{reserve_credits:{global:200}, daily_credit_limit:{global:300}, daily_token_limit:{...}, model_daily_token_limit:{...}, expiring_window_days:{global:7}},子作用域 intl/cn 传 null=继承全局 |
daily_chat_model |
国际版每日活跃用的模型 id |
egress_backoff_enabled |
出口级瞬时失败退避 |
enable_anthropic_messages |
面板开关 |
pricing_enabled / pricing_refresh_minutes / quota_scan_models / update_check_enabled / local_web_tools / window_probe / remaining_priority_enabled |
同名布尔/数组 |
34.2 Key 绑出口 > 客户端发 X-Realm 头
api_keys[].realm(intl/cn/空=跟随面板全局)是服务端强制的,_request_realm() 里 key 绑定的优先级高于 X-Realm 头。能绑就绑:客户端(hermes / mp2 / mdc)升级、换库、漏带头都不会偷偷回到付费出口。实测绑完 key 后,连已经在跑的 session(客户端还带着老 header)也立刻改走 intl,不用重启客户端。
注意:绑了 intl 的 key 调 cn 独占模型(deepseek-v4-pro、minimax-*、glm-5.1)会直接 400,所以按 key 实际调的模型绑:只跑 deepseek-v4.1-flash 的客户端(mp2 助手、mdc 翻译)完全可以绑 intl 白嫖。
34.3 WB_ENABLE_ANTHROPIC_MESSAGES 是 OR,不是 AND
_anthropic_messages_enabled() = 面板 enable_anthropic_messages 或 环境变量。只关面板不够,还要从 compose 删掉环境变量并 recreate 容器。验证:POST /v1/messages 应返回 400 Anthropic Messages is disabled by default。
34.4 重启容器会“变出”重复账号
accounts/ 里若残留旧格式 workbuddy-<uid>.json(5 个 3KB 小文件),启动时会被当成独立账号加载 → /health 的 accounts 从 7 变 12,等于同一账号在池里两份(重复计额度/调度)。
ssh fnos-root 'cd /vol1/1000/Docker/workbuddy2api-hub && mkdir -p legacy-accounts-archive && mv accounts/workbuddy-*.json legacy-accounts-archive/ && docker compose restart wb-hub'
curl -s --noproxy '*' http://192.168.31.201:8788/health # accounts 应回到真实数量每次 recreate/restart 后都要核对 /health 的 accounts 数。
34.5 飞牛上的真浏览器打开看板(LAN 被 mihomo 502)
fygo-browser 的 Chrome 启动带 --proxy-server=127.0.0.1:7890,打开 http://192.168.31.201:8788 会 502;而 hub 端口只绑在 LAN IP 上,127.0.0.1:8788 是 refused。但 Chrome 默认绕过 localhost——所以在 fnOS 上起一个 loopback 转发即可:
ssh fnos-root 'python3 /tmp/wbfwd.py' # 127.0.0.1:8799 -> 192.168.31.201:8788
# 然后 Chrome 开 http://127.0.0.1:8799/?pwd=<面板密码>CDP 侧(hermes 容器内):ssh -L 127.0.0.1:16025:127.0.0.1:16005 -N fnos-root 建隧道,用 websockets 连 ws://127.0.0.1:16025/devtools/page/<id>;/json/new 必须用 PUT,且 urllib 要 ProxyHandler({}) 关掉代理(否则 16025 被代理截胡超时)。
35. 用 platform_hints 让 hermes 自己识别"我在哪个端口"
加 config.yaml.platform_hints.<platform>.append 即可让 hermes 在 system prompt 追加"我在哪个端"的提示——新 session 启动时自动加载。用途:爸爸在 WeChat/TG/容器终端跟同一台 hermes 对话,agent 默认不知道自己跑在哪个端,配上 platform_hints 就知道了。
最小完整配置(用 hermes config set):
hermes config set platform_hints.telegram.append '
【当前会话平台:Telegram(TG)端】
你正在通过 Telegram 与爸爸对话。每条回复开头应隐含或显式确认这是 TG 端消息(如用 TG 友好的 markdown 格式、避免微信专有语法)。
其他端(微信/容器终端)的最近对话已通过 AGENTS.md 注入块同步给你,可参考但 TG 端回复不依赖对方。'
hermes config set platform_hints.weixin.append '
【当前会话平台:微信(WeChat)端】
你正在通过微信 iLink bot 与爸爸对话。回复请用微信友好的格式(无 <br> 等 HTML,用换行;markdown 表格会被截断,可用 bullet list 替代)。
其他端(TG/容器终端)的最近对话已通过 AGENTS.md 注入块同步给你,可参考但微信端回复不依赖对方。'
hermes config set platform_hints.cli.append '
【当前会话平台:飞牛 NAS hermes 容器终端(CLI)】
你正在 hermes 容器的命令行会话中与爸爸对话。回复可使用完整 markdown(含表格、代码块),无平台渲染限制;输出会显示在 docker exec / ssh 终端里。
其他端(TG/微信)的最近对话已通过 AGENTS.md 注入块同步给你,可参考但 CLI 端回复不依赖对方。'三个关键事实:
- platform key 是 source 名(
telegram/weixin/cli/cron/tui),不是显示名(WeChat/Telegram)——agent.platform直接拿来匹配这个 key。判断方法:grep -rn 'platform = .*lower()' /opt/hermes/agent/看_resolve_platform_hint的来源(agent.platform在 gateway 注入 session context 时设值,来源是MessageEvent.platform)。 - appended 文本在 agent init 时读,已经在跑的 session 不会重新读——改了
.append后,已经在跑的 agent 看不到,新 session 才会带上。判断"配了生效"必须看新发消息触发的新 session 的 system prompt,不是当前对话。 appendvsreplace:默认(不传replace)只 append;想完全替换默认 hint 用replace: "..."。反例:用replace替代默认 hint → 默认 hint 里通常有平台相关的 render 提示(如 TG rich messages 提示),丢了之后客户端拿到 markdown 可能炸。
配合 cross-platform sync:platform_hints 是 system prompt 内的"我在哪"标识,AGENTS.md 注入块(hermes-cross-platform-sync skill + sync-digest.py cron)是"其他端聊什么"上下文。两个机制叠加才完整:agent 既知道自己当前端(platform_hints),又知道其他端最近说了啥(AGENTS.md 注入)。
判断当前端用什么 key:跑 hermes chat --provider <any> 触发一发,agent.log 看 conversation turn: ... platform=<key>,key 就是 platform_hints 应该用的名字。反例:想给"微信平台"配 hint 但 key 写成 wechat——_resolve_platform_hint 找不到 wechat,落回默认 hint(看不出来生效了)。
验证生效:
hermes config get platform_hints # 看配置对
# 然后从对应端发一条消息触发**新 session**,agent 第一句回复里应该带"当前会话平台:X"踩坑:.append 改完当前对话还是看不到——预期行为(不是 bug),新 session 才会注入。