在 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 能避免隨意的內網掃描,但會破壞從另一台主機發起的健康檢查——甚至同一台宿主機上另一顆虛擬機的探針。綁定 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 一致的通訊端行為。