Skip to content

Handoff — Hermes WeChat 绑定 + LLM 直连 (pick up here)

2026-05-18 22:30 GMT+8. /clear 之后从这里继续。

当前生产状态(hanliang730 实例)

Containerhermes-cmp6vr0zq000bp601beipc3nr (running, 1 CPU / 1g RAM, dashboard 127.0.0.1:32803)
Profile dir (host)/home/twilight/hermes-profiles/cmp6vr0zq000bp601beipc3nr/
Gateway staterunning
WeChat 绑定connected, account 68f8f99a630a@im.bot
Home channelo9cq8060luFbQ74LnqJZOuUmwUKs@im.wechat
LLM 路由OpenRouter direct (key sk-or-v1-a8dc...3d81), bypasses NewAPI + CF
Last verified04:25:29 收到"hello now" → 9.6s 后回复"你好!有什么我可以帮你的吗?"
DB instance rowstatus=READY,weixinChannel=NULL(没回写,因为 backend code 还不知道 bridge 路径)

关键改动 / 当前手工 hack 列表(生产 ECS 上)

位置改动是否回写到 repo
hermes-cmp... 容器--cpus 1.0 --memory 1g(不是 spawn-profile 默认 0.25/256m)❌ 临时
backend containerapk add openssl python3 手动跑过❌ 临时
Hermes imagedocker tag nousresearch/hermes-agent:latest :0.13.0❌ 临时
profile .envOPENAI_API_KEY/OPENAI_BASE_URL 指 OpenRouter❌ 临时
profile config.yamlmodel.base_urlhttps://openrouter.ai/api/v1❌ 临时
profile dirweixin-qr-bridge.py 拷到 /home/twilight/hermes-profiles/<id>/weixin-qr-bridge.py✅ source 在 scripts/admin/weixin-qr-bridge.py
profile weixin/accounts/68f8f99a630a@im.bot.jsonqr_login confirmed 自动写的✅ 持久化
HermesInstance DB rowstatus=PENDING_BIND(早晨手工 hot-fix)→ 后续 spawn 后变 PROVISIONING / READY❌ DB 状态
HermesInstance.llmApiKey写了 NewAPI key (sk-J6It...IhMM),但当前 OpenRouter 直连不读这个字段

已落档文档

文档位置内容
全流程架构docs/architecture/payment-flow.md目标 flow + 状态机 + Gap A/B/C + Phase 1-4 进度(Phase 1+2 ✅,Phase 3-4 待做)
故障 root causedocs/err/2026-05-18-hermes-wechat-binding-and-llm-routing.md11 个坑详解 + cross-cutting themes + 全局 fix list
联调参数审计(早期)docs/superpowers/plans/2026-05-17-integration-params-audit.mdNewAPI / 3-tier plan / 模型选择决策 — 部分作废(NewAPI 路径暂停)
Hermes activation 原 plandocs/superpowers/plans/2026-05-17-hermes-activation-complete.md早期 plan,部分作废(iLink-first 设计被推翻)

进行中 / 打开的 PR

  • PR #97 feat/v0.4.3-hermes-activation — Phase B/C/E(NewAPI resilience + 3-tier plan + status machine)— 跟新流程冲突,多半放弃 status-machine 改动,保留 schema additions
  • PR #98 feat/local-dev-stack — 本地 compose + CI smoke + dry-run-migration — backend-smoke CI 已通过,等 merge

/clear 之后第一件事

读这两份文档(按顺序):

  1. docs/architecture/payment-flow.md — 整体上下文 + 目标 flow + 进度
  2. docs/err/2026-05-18-hermes-wechat-binding-and-llm-routing.md — 今天踩的坑 + 全局 fix list

然后按下面优先级走 Phase 3。

Phase 3 待做(按依赖顺序)

A. 必须做(codify 当前手工 hack)

A1-A8 全部列在 err doc 的「Global fix list / A. 必须做」。优先级最高 是:

  • [ ] A1 spawn-profile.sh path 修复(host vs container path)
  • [ ] A3 backend Dockerfile 加 openssl python3
  • [ ] A4 profile .env template 加 OPENAI_API_KEY / OPENAI_BASE_URL
  • [ ] A5 provision-worker TIER_PROFILES 默认提到 1cpu/1g
  • [ ] A7 backend service-runtime 接 bridge(替换现有 iLink 直连)

B. backend Code 改动概要

ts
// backend/src/modules/service-runtime/service-runtime.service.ts
async getWeixinQr(subscriptionId: string) {
  const instance = await this.getInstanceBySubId(subscriptionId);
  const { stdout } = await execFileAsync('docker', [
    'exec', `hermes-${instance.id}`,
    '/opt/hermes/.venv/bin/python3',
    '/opt/data/weixin-qr-bridge.py', 'start',
  ]);
  return JSON.parse(stdout);  // { status: 'wait', qrcode, scan_url }
}

async pollWeixinStatus(subscriptionId: string) {
  const instance = await this.getInstanceBySubId(subscriptionId);
  const { stdout } = await execFileAsync('docker', [
    'exec', `hermes-${instance.id}`,
    '/opt/hermes/.venv/bin/python3',
    '/opt/data/weixin-qr-bridge.py', 'poll',
  ]);
  const state = JSON.parse(stdout);
  if (state.status === 'confirmed') {
    // bridge 已经写 WEIXIN_TOKEN/ACCOUNT_ID 到 .env
    // backend 这里需要 recreate container 让新 env 生效
    await this.recreateContainer(instance.id);
    await this.prisma.hermesInstance.update({
      where: { id: instance.id },
      data: { weixinChannel: state.account_id, status: 'READY' },
    });
  }
  return state;
}

C. spawn-profile.sh 改动概要

bash
# 添加 bridge 拷贝
cp "$REPO_ROOT/scripts/admin/weixin-qr-bridge.py" "$PROFILE_DIR/weixin-qr-bridge.py"
chmod +x "$PROFILE_DIR/weixin-qr-bridge.py"
chown 10000:10000 "$PROFILE_DIR/weixin-qr-bridge.py"

# 删除 WEIXIN_HOME_CHANNEL / WEIXIN_TOKEN 入参(bridge 自己写)

# .env 模板加(短期 OpenRouter direct):
# OPENAI_API_KEY=...   # 从 provisioning 注入
# OPENAI_BASE_URL=https://openrouter.ai/api/v1

# docker run volume:传 HOST path,不是 backend-container path
# 当前 backend 容器内 PROFILE_BASE=/twilight/hermes-profiles
# 需要解析为 host /home/twilight/hermes-profiles
# 简单做法:spawn-profile.sh 接受 HOST_PROFILE_BASE 入参,docker -v 用它

别忘了

  • 当 NewAPI 路径恢复时(关 CF Bot Fight Mode 或 NewAPI 加 UA 白名单),回退 config.yaml + .envbase_url / OPENAI_BASE_URL 回 llm.fsagent.cc/v1,恢复 per-user quota 跟踪。
  • err doc Cross-cutting Theme #2:把 spawn-profile.sh 当 backend image 的一部分对待,依赖在 Dockerfile 里装齐。
  • PR #97 reconcile 之前,不要 push 任何 schema migration 上 ECS(migration 0002+0003 还没 apply)。

验证命令(拿来就用)

bash
# SSH ECS
ssh -o ProxyCommand="cloudflared access ssh --hostname ssh-ecs.fsagent.cc" root@ssh-ecs.fsagent.cc

# 看 hermes 容器状态
docker ps --filter name=hermes-cmp6 --format "{{.Names}}\t{{.Status}}"

# 看 gateway 实时状态
cat /home/twilight/hermes-profiles/cmp6vr0zq000bp601beipc3nr/gateway_state.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['gateway_state'], d['platforms']['weixin']['state'])"

# 看 agent activity
tail -20 /home/twilight/hermes-profiles/cmp6vr0zq000bp601beipc3nr/logs/agent.log

# 跑一次 bridge start(生成新 QR,仅当 force re-bind)
docker exec hermes-cmp6vr0zq000bp601beipc3nr /opt/hermes/.venv/bin/python3 /opt/data/weixin-qr-bridge.py start

# direct iLink getupdates(看 pending msg)
docker exec hermes-cmp6vr0zq000bp601beipc3nr /opt/hermes/.venv/bin/python3 -c "
import json, asyncio, sys
sys.path.insert(0, '/opt/hermes')
from gateway.platforms.weixin import _get_updates, _make_ssl_connector, _load_sync_buf
import aiohttp
async def main():
    creds = json.load(open('/opt/data/weixin/accounts/68f8f99a630a@im.bot.json'))
    sync_buf = _load_sync_buf('/opt/data', '68f8f99a630a@im.bot')
    async with aiohttp.ClientSession(trust_env=True, connector=_make_ssl_connector()) as s:
        r = await _get_updates(s, base_url=creds['base_url'], token=creds['token'], sync_buf=sync_buf, timeout_ms=5000)
        print('msgs:', len(r.get('msgs',[])))
asyncio.run(main())
"

团队内部文档