1. macOS 上 OpenClaw Gateway 为什么需要 launchd 守护
OpenClaw 在 macOS 上的运行方式和很多人想的不太一样。它不是一个把 Node 运行时和 Gateway 全部打包进 App 的「一体化」程序,而是把 Gateway 当作一个独立的用户级后台服务来管理。也就是说,OpenClaw.app 本身不负责把 Gateway 作为子进程拉起来,它期望你通过外部的openclawCLI 安装好运行时,然后由 launchd 来常驻托管 Gateway。这个设计的好处是:你关掉 App 窗口,Gateway 依然在跑;你重启登录 Mac,Gateway 也能自动恢复。
如果你只是临时在终端里敲一句openclaw gateway,那关掉终端窗口进程就没了,App 会连不上,或者每次都要手动重启,非常折腾。真正稳定的做法,是写一个 LaunchAgent plist,让 launchd 帮你守护这个 Node 服务。这篇就围绕 macOS 环境下 OpenClaw Gateway 的常驻运行问题,给出可复制的 launchd plist 骨架、Node 启动参数配置,以及用launchctl加载、看日志、验证 Gateway 存活的完整动作。
适合谁看:在 Mac 上跑 OpenClaw、希望 Gateway 开机自启且崩溃能自动拉起的开发者;用 launchd 托管 Node 服务、想找一份能直接改的 plist 模板的人;以及遇到 Gateway 时有时无、App 提示连不上、想搞清楚日志在哪的人。核心检索词就三个:OpenClaw、macOS、launchd,下面全部围绕它们展开。
先说清楚一个前提:OpenClaw.app 不再捆绑 Node/Bun 或 Gateway 运行时。你需要先在 Mac 上装好 Node 22+,再全局安装openclawCLI。macOS 应用里的 Install CLI 按钮,本质上也是通过 npm/pnpm 执行同样的安装流程,官方不推荐用 bun 作为 Gateway 运行时。所以第一步不是写 plist,而是把 CLI 装对。
# 确认 Node 版本,必须是 22 及以上 node -v # 全局安装 openclaw CLI,版本号替换成与你 App 匹配的 npm install -g openclaw@<版本> # 确认 CLI 可用 openclaw --version版本这块要特别注意:macOS 应用会检查 gateway 版本与自身版本的兼容性。如果 CLI 版本和应用版本不匹配,App 可能拒绝连接或提示不兼容。遇到这种情况,直接更新全局 CLI 到与应用一致的版本即可。我一般会先openclaw --version记下当前版本,再去核对 App 的版本号。
2. TaoToken 前置:把模型调用凭证准备好
Gateway 跑起来之后,真正干活的是背后的模型调用。OpenClaw 的 Gateway 需要能访问到模型服务,这里我用 TaoToken 来做统一接入。它的官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。对本地 Gateway 来说,你需要的是一把可用的 API Key,以及一个稳定的接入地址。
操作路径很直接:登录后进入控制台,在 API Keys 页面创建一把新 Key。创建时建议按用途命名,比如openclaw-local-gateway,方便以后区分。Key 只在创建时完整显示一次,复制后先存到安全的地方,别直接写进会提交到 Git 的配置文件里。
拿到 Key 之后,本地 Gateway 的模型请求就可以指向 TaoToken 的 API 地址。这里有个习惯值得养成:把 Key 放进环境变量或独立的.env文件,而不是硬编码在 plist 里。plist 是明文 XML,放在~/Library/LaunchAgents/下虽然只有本机可读,但一旦你截图分享或备份到云端,Key 就泄露了。更稳妥的做法是让 plist 引用一个包装脚本,脚本里再读取环境变量。
如果你还没创建 Key,可以直接走这个入口:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有各语言 SDK 的调用示例,配置 Gateway 时对照着填 base_url 和 api_key 就行。
需要提醒的是,Gateway 本身不替代编辑器,它只是把模型能力以本地服务的形式暴露出来。你的代码编辑、文件操作还是在原来的工具里完成,Gateway 负责的是请求转发和会话管理。理解这一点,后面排查问题时思路会清晰很多。
3. 可复制的 launchd plist 骨架与 Node 启动参数
现在进入正题。LaunchAgent 的 plist 放在用户级目录~/Library/LaunchAgents/下,文件名建议用ai.openclaw.gateway.plist。Label 用ai.openclaw.gateway,旧版本可能还残留com.openclaw.*的 Label,如果你机器上有旧的,先卸载再装新的,避免两个服务抢同一个端口。
下面是一份可以直接改的 plist 骨架。关键点我都在注释里标了,注意 plist 不支持真正的注释语法,这里的<!-- -->是给你看的,实际使用时删掉或保留都不影响解析(XML 注释是合法的)。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <!-- 服务唯一标识,建议与文件名一致 --> <key>Label</key> <string>ai.openclaw.gateway</string> <!-- 启动命令:用绝对路径,launchd 不读你的 shell PATH --> <key>ProgramArguments</key> <array> <string>/usr/local/bin/node</string> <string>/usr/local/lib/node_modules/openclaw/bin/openclaw.js</string> <string>gateway</string> <string>--port</string> <string>18999</string> <string>--bind</string> <string>loopback</string> </array> <!-- 环境变量:把模型接入信息放这里 --> <key>EnvironmentVariables</key> <dict> <key>OPENCLAW_SKIP_CHANNELS</key> <string>1</string> <key>OPENCLAW_SKIP_CANVAS_HOST</key> <string>1</string> <key>OPENCLAW_API_BASE</key> <string>https://taotoken.net/api</string> <key>OPENCLAW_API_KEY</key> <string>你的_API_Key</string> <key>PATH</key> <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string> </dict> <!-- 崩溃或退出后自动重启 --> <key>KeepAlive</key> <true/> <!-- 登录时自动加载 --> <key>RunAtLoad</key> <true/> <!-- 日志输出,目录要先手动创建 --> <key>StandardOutPath</key> <string>/tmp/openclaw/openclaw-gateway.log</string> <key>StandardErrorPath</key> <string>/tmp/openclaw/openclaw-gateway.log</string> <!-- 工作目录 --> <key>WorkingDirectory</key> <string>/Users/你的用户名</string> </dict> </plist>几个容易踩的坑,我逐个说。第一,ProgramArguments里的 node 路径必须是绝对路径。launchd 不加载你的 shell 配置,所以which node出来的路径要写死。如果你用 nvm 管理 Node,路径通常在~/.nvm/versions/node/vXX/bin/node,这个路径会随版本变化,升级 Node 后 plist 要同步改。第二,openclaw.js的入口路径取决于你的全局安装位置,用npm root -g可以查到全局模块目录,再拼上openclaw/bin/openclaw.js。第三,日志目录/tmp/openclaw/不会自动创建,先手动mkdir -p /tmp/openclaw,否则 launchd 写日志会失败,服务可能起不来。
关于 Node 启动参数,--port 18999和--bind loopback是冒烟测试里用的组合,正式跑也可以沿用。--bind loopback表示只监听本地回环地址,外部网络访问不到,安全性更好。如果你需要局域网内其他设备访问,再考虑改成对应网卡地址,但那样要额外做访问控制。
环境变量里OPENCLAW_SKIP_CHANNELS=1和OPENCLAW_SKIP_CANVAS_HOST=1是跳过一些非必要子系统的开关,本地跑 Gateway 时能减少资源占用和启动报错。OPENCLAW_API_BASE指向 TaoToken 的 API 地址,OPENCLAW_API_KEY填你创建的 Key。再次强调,如果这份 plist 会被分享,把 Key 换成从外部文件读取的方式。
4. 用 launchctl 加载、查看日志、验证 Gateway 存活
plist 写好后,先做语法检查,再加载。macOS 现在推荐用launchctl bootstrap和launchctl bootout,老的load/unload也能用,但新系统上行为略有差异。
# 1. 检查 plist 语法是否合法,输出 OK 说明没问题 plutil -lint ~/Library/LaunchAgents/ai.openclaw.gateway.plist # 2. 创建日志目录 mkdir -p /tmp/openclaw # 3. 加载服务(新写法,gui/$(id -u) 表示当前用户会话) launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.openclaw.gateway.plist # 如果之前加载过,先卸载再加载 launchctl bootout gui/$(id -u)/ai.openclaw.gateway launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.openclaw.gateway.plist加载后确认服务状态:
# 查看服务是否在运行,能看到 PID 和退出码 launchctl print gui/$(id -u)/ai.openclaw.gateway # 只看关键行 launchctl list | grep openclawlaunchctl list输出里,第一列是 PID,第二列是上次退出码,第三列是 Label。如果 PID 有数字、退出码是 0,说明服务正常在跑。如果 PID 是-、退出码非 0,说明启动失败,这时候去看日志。
日志在/tmp/openclaw/openclaw-gateway.log,stdout 和 stderr 都写到这里。实时跟踪:
tail -f /tmp/openclaw/openclaw-gateway.log常见的启动失败日志包括:找不到 node、找不到 openclaw.js、端口被占用、API Key 无效。对着日志改 plist,改完bootout再bootstrap重新加载。
服务起来后,做一次冒烟测试验证 Gateway 存活。先确认 CLI 版本,再直接调 health 接口:
# 确认 CLI 版本 openclaw --version # 手动前台跑一次,确认参数没问题(可选,用于对比) OPENCLAW_SKIP_CHANNELS=1 \ OPENCLAW_SKIP_CANVAS_HOST=1 \ openclaw gateway --port 18999 --bind loopback # 另开一个终端,调用 health 检查 openclaw gateway call health --url ws://127.0.0.1:18999 --timeout 3000如果 health 返回正常状态,说明 Gateway 在ws://127.0.0.1:18999上活着。这时候再回到 OpenClaw.app,它应该能连上这个本地 Gateway。注意 App 的行为:如果配置端口上已经有 Gateway 在跑,App 会直接连接它,而不是再启动一个新实例。所以你先用 launchd 把 Gateway 拉起来,App 打开后就是连现成的,不会重复启动。
「OpenClaw Active」这个开关控制的是 LaunchAgent 的启用和禁用。你退出 App 不会停止 Gateway,因为 launchd 在托管它。想彻底停掉,用launchctl bootout,或者把 plist 从 LaunchAgents 目录移走再卸载。
5. 本篇常见错误排查
错误一:launchctl bootstrap报 Input/output error 或 service already loaded。说明这个 Label 已经加载过了。先launchctl bootout gui/$(id -u)/ai.openclaw.gateway卸载,再重新 bootstrap。如果 bootout 也报错,用launchctl list | grep openclaw找到确切 Label,注意旧版可能是com.openclaw.*,Label 对不上就卸载不掉。
错误二:服务加载了但 PID 一直是-,日志为空。多半是ProgramArguments里的路径不对,launchd 根本没执行到写日志那一步。把 node 和 openclaw.js 的绝对路径复制出来,在终端里手动执行一遍,能跑通再写回 plist。nvm 用户尤其注意,plist 里的 node 路径不会跟着 nvm 切换版本走。
错误三:日志里报EADDRINUSE,端口被占用。说明 18999 已经被别的进程占了,可能是你之前手动跑的 Gateway 没退干净,或者另一个 LaunchAgent 也在用这个端口。用lsof -i :18999查占用进程,杀掉或换端口。换端口后记得同步改 App 里的连接配置。
错误四:health 调用超时,但服务在跑。检查--bind参数和 URL 是否一致。如果 Gateway 绑的是 loopback,URL 就必须是ws://127.0.0.1:18999,不能用局域网 IP。另外--timeout 3000是 3 秒,机器负载高时可能不够,适当调大再试。
错误五:App 提示 gateway 版本不兼容。这是 CLI 版本和 App 版本不匹配。openclaw --version看当前版本,更新全局 CLI 到与应用一致的版本,然后重启 LaunchAgent。更新 CLI 后,如果安装路径变了,plist 里的openclaw.js路径也要跟着改。
错误六:改了 plist 但行为没变化。launchd 会缓存已加载的服务定义,改完 plist 必须bootout再bootstrap,光改文件不重新加载是不生效的。改完用plutil -lint再检查一遍语法,避免 XML 写错导致加载失败。
6. 把 Gateway 稳定跑在本地之后
到这里,OpenClaw Gateway 在 macOS 上就算稳定托管起来了。回顾一下关键动作:装对 Node 22+ 和匹配版本的 openclaw CLI,写好 LaunchAgent plist,用launchctl bootstrap加载,通过/tmp/openclaw/openclaw-gateway.log看日志,用openclaw gateway call health验证存活。这一套跑通后,App 退出、终端关闭、甚至重启登录,Gateway 都能自己回来。
如果你后面要长期跑编码类任务或 Agent 工作流,可以考虑用 Coding Plan 来管理调用配额和模型切换,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。日常想快速验证某个模型是否可用,直接用模型对话页面测一下最省事:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。需要管理多把 Key 或查看用量,控制台在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
最后留一个实用习惯:把 plist 里的 API Key 抽到一个只有本机可读的配置文件里,plist 只引用路径。这样你分享配置模板、备份 dotfiles 时都不用担心泄露。Gateway 这种常驻服务,配置一次能管很久,前期把路径、日志、版本这三样对齐,后面基本不用再动它。