Skip to content

fix(desktop): 处理已存在其它 Hermes Agent 时桌面端无法启动飞书网关 (#168) - #26

Merged
Eynzof merged 2 commits into
mainfrom
fix/desktop-feishu-gateway-wsl-conflict-issue168
Jun 7, 2026
Merged

Eynzof merged 2 commits into
mainfrom
fix/desktop-feishu-gateway-wsl-conflict-issue168

Conversation

@Eynzof

@Eynzof Eynzof commented Jun 7, 2026

Copy link
Copy Markdown
Owner

背景

Ref: Eynzof/Hermes-CN-Desktop#168

较多 Windows 用户反馈:以前在 Windows 上装过 Hermes Agent 的情况下,用中文社区桌面版完成飞书扫码、保存参数后点击「保存并启动飞书消息服务」,网关拒绝启动,提示类似“当前 dashboard 不是桌面端托管进程”。用户停留在“参数已存但飞书消息服务起不来”的状态。

根因(已在代码中核实,两种触发源、本质相同)

桌面端(Electron)自己拉起并托管一个 dashboard,点击保存后调用 POST /api/gateway/restart → spawn hermes gateway restart。失败的本质都是「真正(没)跑起来的网关不是桌面端托管的那个」:

  • T1 · Windows 已装网关服务:hermes gateway restart 在 Windows 会优先 gateway_windows.restart()(hermes_cli/gateway.py),去重启那个外部服务网关——它跑在自己的 HERMES_HOME 下,永远看不到桌面端刚保存的飞书参数。桌面端从不安装 Windows 网关服务,所以“已安装服务”对桌面端即外部。
  • T2 · WSL / 第二个安装(不同 HERMES_HOME):重复实例守卫按 HERMES_HOME 隔离,跨 Windows↔WSL 看不到对方;冲突通过共享 localhost 暴露(WSL2 转发 127.0.0.1),飞书 webhook 端口 8765 被占 → 被吞成模糊的 "Feishu startup failed: …"。

方案:检测 + 清晰提示 + 一键接管(混合)

  • 桌面端 spawn dashboard 时注入 HERMES_DESKTOP_MANAGED=1,让 Python 侧能判定“应由桌面端托管网关”。
  • 不再静默重启外部 Windows 服务:desktop-managed 且发现外部服务时,未带 force → 记录结构化冲突 gateway_conflict_service 并拒绝(exit 2);带 force(用户点「强制接管」)→ 停掉外部服务 / 其它本机网关并以 --replace 跑桌面端托管网关。desktop-managed 的手动重启统一 --replace。
  • 飞书 connect 对 EADDRINUSE(含 Windows WSAEADDRINUSE)分类为 gateway_conflict_port,用 psutil 解析占用者是否为本机可接管进程(WSL/跨 VM → 不可接管)。
  • 经 gateway_state.json → /api/messaging/platforms 透出结构化 error_detail;桌面消息页渲染冲突面板:「强制由桌面端接管并重启」(同机可点)/ 「查看如何停止其他实例」(WSL 等不可接管时给指引)。
  • POST /api/gateway/restart 新增 force 参数,透传一次性 HERMES_GATEWAY_FORCE_TAKEOVER。

改动文件

Python

  • gateway/status.py — write_runtime_status 增加 error_detail;新增 set_gateway_conflict、classify_port_conflict。
  • gateway/platforms/base.py — _set_fatal_error 支持 error_detail。
  • gateway/platforms/feishu.py — _classify_address_in_use,端口冲突归类。
  • hermes_cli/gateway.py — restart 的 desktop-managed 分支 + 强制接管 + 冲突标记/清除。
  • hermes_cli/web_server.py — /api/gateway/restart?force=1;payload 透出 error_detail 与网关级冲突。

Desktop

  • apps/desktop/electron/main.cjs — 两处 spawn 注入 HERMES_DESKTOP_MANAGED=1。
  • apps/desktop/src/hermes.ts — restartGateway(force)。
  • apps/desktop/src/types/hermes.ts — MessagingConflictDetail 类型 + error_detail。
  • apps/desktop/src/app/messaging/index.tsx — 冲突面板与一键接管。
  • apps/desktop/src/i18n/{types,en,zh}.ts — 新增文案。

测试 / 文档

  • tests/gateway/test_gateway_conflict.py — 状态往返、端口冲突分类、EADDRINUSE 识别、重启分支三路径。
  • docs/issue-168-gateway-conflict-plan.md — 根因与设计说明。

验证

  • 5 个改动 Python 文件 + 测试均 py_compile 通过。
  • 用 stdlib unittest.mock 在本地 runtime 验证了重启分支 4 个场景:外部服务拒绝(exit 2)+记录冲突 / 强制接管(停服务+run_gateway(replace=True)) / 非 desktop-managed 维持原行为 / desktop-managed 无服务正常 --replace 启动并清除陈旧冲突。
  • 状态与飞书分类函数单测全部通过(psutil 缺失时安全降级为不可接管)。
  • ⚠️ 桌面端 TS 因环境未安装依赖(无 node_modules)未跑 tsc/构建;已做人工类型核对(类型/导入/Button 变体/i18n key 齐全)。建议 CI 跑一遍 type-check 与单测。

范围与边界

  • 非 desktop-managed 行为完全不变(零回归)。
  • 强制接管仅经用户显式点击触发,且只停同机服务/进程;跨 VM(WSL)不做破坏性操作,仅给停止指引。
  • 本次按 issue 聚焦 Windows;Linux 上“预装 systemd 网关服务”的等价情形仍走原 systemd 路径(端口冲突检测跨平台生效)。

🤖 Generated with Claude Code

当 Windows 上已存在另一个 Hermes Agent(已安装的网关服务,或运行在
WSL/第二个安装中、通过共享 localhost 可达的实例)时,桌面端完成飞书扫码、
保存参数后点击「保存并启动飞书消息服务」会失败:真正(没)跑起来的网关并不
是桌面端托管的那个,用户停留在“参数已存但服务起不来”的状态。

根因(两种触发源,本质相同):
- Windows 已装网关服务:`hermes gateway restart` 会优先 `gateway_windows.restart()`
  重启那个外部服务网关,它跑在自己的 HERMES_HOME 下,永远看不到桌面端刚存的参数。
- WSL/第二安装:重复实例守卫按 HERMES_HOME 隔离看不到对方,冲突通过共享 localhost
  暴露(飞书 webhook 8765 端口 EADDRINUSE 被吞成模糊错误)。

方案:检测 + 清晰提示 + 一键接管
- 桌面端 spawn dashboard 时注入 HERMES_DESKTOP_MANAGED=1,让 Python 侧能判定
  “应由桌面端托管网关”。
- 桌面端发起的网关重启不再静默重启外部 Windows 服务:未带 force 时记录结构化冲突
  (gateway_conflict_service)并拒绝;带 force(用户点击「强制接管」)时停掉外部服务/
  其它本机网关并以 --replace 跑桌面端托管网关。desktop-managed 的手动重启统一用 --replace。
- 飞书 connect 对 EADDRINUSE 分类为 gateway_conflict_port,并用 psutil 解析占用者
  是否为本机可接管进程(WSL/跨 VM 不可接管)。
- 经 gateway_state.json -> /api/messaging/platforms 透出结构化 error_detail;
  桌面消息页渲染冲突面板:「强制由桌面端接管并重启」(同机可点)/「查看如何停止其他实例」。
- POST /api/gateway/restart 新增 force 参数透传一次性 HERMES_GATEWAY_FORCE_TAKEOVER。

非 desktop-managed 行为完全不变(零回归);跨 VM(WSL)不做破坏性操作,仅给指引。
新增 tests/gateway/test_gateway_conflict.py 覆盖状态往返、端口冲突分类、EADDRINUSE
识别与重启分支三种路径。

Ref: Eynzof/Hermes-CN-Desktop#168

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 7, 2026 •

Copy link
Copy Markdown

🔎 Lint report: fix/desktop-feishu-gateway-wsl-conflict-issue168 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9897 on HEAD, 9889 on base (🆕 +8)

🆕 New issues (3):

Rule Count
unsupported-operator 1
unresolved-import 1
not-subscriptable 1
First entries
tests/gateway/test_gateway_conflict.py:63: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["gateway_conflict"]` and `dict[str, Any] | None`
tests/gateway/test_gateway_conflict.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/test_gateway_conflict.py:184: [not-subscriptable] not-subscriptable: Cannot subscript object of type `None` with no `__getitem__` method

✅ Fixed issues: none

Unchanged: 5135 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@Eynzof
Eynzof merged commit fc1ab0e into main Jun 7, 2026
24 checks passed
@Eynzof
Eynzof deleted the fix/desktop-feishu-gateway-wsl-conflict-issue168 branch June 7, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant