在 2026 年,OpenClaw 网关上线最常被卡住的错误之一仍是 Error: listen EADDRINUSE: address already in use。团队往往误以为是上游模型 API 故障,反复轮换密钥,却忽略昨天冒烟测试遗留的 node 进程仍占着 TCP 8787。macOS 上 launchd 可能在旧套接字尚未完全释放前就拉起新作业;多人共用一台租用的 Mac mini 时,配置文件与手工终端会话叠加;合成健康检查常常只访问 127.0.0.1,而网关实际绑在内网地址。本文给出可复用的排查顺序——先用 lsof,再核对 plist,再对齐探针——并与 doctor 网关诊断、首次运行与 LaunchAgent 冒烟、网关健康与可用性监控 串联,减少拍脑袋式排障。
把端口冲突当作容量类事故来记录:写明监听接口、占用 PID、LaunchAgent 标签,事后复盘才能一页纸结束。
看起来像上游故障的症状
客户端可能看到连接被拒绝、curl 空响应,或面板显示“网关离线”,而 CPU 几乎空闲。在指责模型厂商之前,先确认 HTTP 服务是否进入 LISTEN。若进程在启动阶段崩溃,日志可能在打印横幅前就截断,只剩 errno。请收集 StandardErrorPath 的 stderr,并把时间戳与 launchctl print 的状态迁移对齐。
多人共享租用的 Mac mini 时,遗留的 npm run dev 会话最常见;它们往往未接入生产 Grafana,因为从未注册指标端点。
一分钟内给出答案的 lsof 用法
lsof -nP -iTCP:8787 -sTCP:LISTEN
-n 跳过 DNS 解析以免拖慢扫描;-P 显示数字端口。若输出里出现不认识的 node PID,在杀进程前执行 ps -p PID -o args= 留存命令行——运维最讨厌“无名进程”。若 lsof 无监听而客户端仍失败,检查是否客户端走 HTTPS 而网关该端口只提供明文 HTTP。
对仅 IPv6 监听,可再跑 lsof -nP -i6TCP:8787;部分 Node 栈会创建双栈套接字,列表里可能出现两次。
LaunchAgent ProgramArguments 与端口参数
plist 里常见同一 --port 被传两次:一次来自包装脚本,一次来自复制到各环境的模板,后者静默覆盖前者,导致 diff 极难读。保持单一事实来源——要么用 EnvironmentVariables,要么只用显式参数,不要混用。编辑后执行 launchctl bootout gui/$UID/label 再 launchctl bootstrap gui/$UID path,确保旧套接字在启新作业前关闭。
核对 WorkingDirectory 是否指向真正含有你以为在运行的那份 package.json 的目录;cwd 错配再加 npx 时,很容易在默认端口上再起第二个监听。
127.0.0.1、0.0.0.0 与局域网 IP
绑定 127.0.0.1 能避免 casual 内网扫描,但会破坏从另一台主机发起的健康检查——甚至同一宿主机上另一颗虚拟机的探针。绑定 0.0.0.0 接受所有接口,必须配合防火墙策略。绑定固定办公室 IP 会在 DHCP 续租后悄悄失效。把约定写进 runbook,并在 openclaw doctor 的预期里同步同一套接口语义。
会“撒谎”的健康探针
若合成监控只 curl http://127.0.0.1:8787/readyz,而远程用户访问 10.0.40.12:8787,在路由表或分流 VPN 不包含该子网时,探针会一直绿。让探针源 IP 与用户真实路径一致,或经同一堡垒机隧道。网关在启动时导出 gateway_bind_interface 指标,便于 Grafana 发现各环境漂移。
决策表
| 场景 | 首选绑定 | 注意点 |
|---|---|---|
| 单人笔记本实验 | 127.0.0.1 | 远程演示前记得切换 |
| 企业防火墙后的共享 Mac mini | 内网 IP + 白名单 | 与 DHCP 预留或静态租约协同 |
| 公网边缘经反向代理 | 回环 + 前置 nginx | 尽量不让网关进程直接暴露在公网接口 |
TIME_WAIT 与高频重载
调试期每 30 秒 自动重启的脚本会让旧连接处于 TIME_WAIT 长达约 60 秒,或耗尽临时端口。在 bootout 与 bootstrap 之间插入约 5 秒 等待,或在验证热修分支时把管理端口临时 +1。
应用防火墙的坑
macOS 可能为每个新 Node 二进制路径弹出“是否允许传入连接”。若点了拒绝,本机绑定成功而远程 SYN 超时,表象像端口冲突。统一二进制路径或签名流程,避免升级时对话框轰炸值班同事。
同一台 Mac 上多网关
蓝绿演练需要不同端口(如 8787 与 8788)以及互不重复的 LaunchAgent 标签。用表格约定端口段:例如 8700–8799 预留给 OpenClaw,8800–8899 给模拟上游。没有文档时,周末外包往往会随便挑“看起来空闲”的端口。
在同一物理机上并行跑 staging 与 production——租用 Mac mini 时很常见——至少分用户或分日志目录,否则 lsof 输出难以解读。
launchctl kickstart 与残留监听
释放端口后优先使用 launchctl kickstart -k gui/$UID/com.example.openclaw,让 launchd 对顽固子进程发送 SIGKILL,而不是礼貌等待。没有 -k 时,挂起的中间件线程可能仍持有 FD,尽管父进程已打印“shutdown complete”。在前后各截取一页 launchctl print gui/$UID/com.example.openclaw,证明状态从 running 变为 not running。
临时客户端端口与出站风暴
工具调用扇出会打开大量出站连接;macOS 可能耗尽临时端口区间,而入站 LISTEN 仍显示正常。若值班把客户端侧的 EADDRINUSE 误判为网关监听冲突,会白查一轮。调优与网关同机的 CI runner 时,关注 sysctl net.inet.ip.portrange.hifirst 等参数。
绑定失败的结构化日志
输出 JSON 日志字段 event="bind_failed"、errno、尝试绑定的 host、port、以及 argv 哈希。事后回放日志时不应再靠 SSH 猜基础事实。errno 48(EADDRINUSE)应与同一条日志里的 lsof 快照命令并列,方便新人直接复制粘贴。
为何 Linux CI 抓不到 macOS 绑定竞态
容器重启快、网络命名空间与 GUI 会话下的 macOS 行为不同。把 Linux CI 当作编译期检查;涉及 plist 的改动仍应在 Apple 硬件上做冒烟绑定。按天租用 Mac mini 的成本大约相当于工程师一小时的时薪,却能闭环验证。
常见问题
macOS 会立刻复用同一端口吗?
高频重启后可能短暂处于 TIME_WAIT,需要等待或换临时端口。
为什么健康检查绿而用户连不上?
探针与真实用户的网络接口或路径不一致。
0.0.0.0 比回环更安全吗?
暴露面更大,不是更安全;需配防火墙或隧道。
何时租用 Mac mini?
当你必须在笔记本之外复现与生产一致的 macOS 套接字与 launchd 生命周期时。
端口争抢无聊却昂贵:每一次误报都会消耗跨团队信任。在 MacHTML 租用一台 Apple Silicon Mac mini,每天约 16.9 美元,即可获得与生产网关相同的 launchd 生命周期、默认套接字行为与防火墙弹窗,而无需给每位外包寄硬件。发布周拉起实例收集 lsof 证据,队列清空后再关机即可。
低发热噪声也有利于你长时间 SSH 反复验证绑定而不打扰邻座。
在真实 macOS 上复现 OpenClaw 绑定问题
租用云端 Mac mini,验证端口、LaunchAgent plist 与健康探针,获得与 macOS 一致的套接字行为。