首页
/ OpenClaw macOS Parallels Discord 双向投递冒烟测试实战指南

OpenClaw macOS Parallels Discord 双向投递冒烟测试实战指南

2026-09-05 15:33:38作者:伍霜盼Ellen

本文基于 OpenClaw 仓库中的 Parallels Discord Roundtrip 技能文档(.agents/skills/parallels-discord-roundtrip/SKILL.md),讲解如何在 macOS Parallels 虚拟机的全新快照上完成 OpenClaw 的安装、onboard、gateway 健康检查,并通过 Discord 完成“客端发送 → 主机可见 → 主机回帖 → 客端读回”的完整双向投递证明。读完后,你可以复制命令直接运行这套 smoke 测试,理解 harness 每个阶段的实现细节与判定逻辑,并能依据产物日志排查失败用例。

测试覆盖目标

该 skill 明确说明其适用场景:“macOS Parallels smoke 必须端到端证明 Discord 双向投递时使用”。整条 fresh/upgrade 泳道需要覆盖以下六个环节:

  • 在全新 macOS 快照上完成安装;
  • 完成 onboard 并通过 gateway 健康检查;
  • 客端(guest)执行 message send 向 Discord 发送消息;
  • 主机(host)侧能在 Discord 中看到该消息;
  • 主机主动向 Discord 发送一条新消息;
  • 客端 message read 能读到这条新消息。

也就是说,这套冒烟测试验证的不只是“bot 能发消息”,而是消息链路的双向闭环:出站消息必须真实出现在频道历史中,入站消息必须能被 guest 内的 OpenClaw 读取,两者互为证明。

前置输入

运行前需要准备四项输入(均来自 skill 文档 Inputs 一节):

输入 说明
Discord bot token 通过主机环境变量注入,例如 OPENCLAW_PARALLELS_DISCORD_TOKEN
Discord guild ID 目标服务器(guild)的雪花 ID
Discord channel ID 目标频道(channel)的雪花 ID
OPENAI_API_KEY agent 首轮对话(first-agent-turn)阶段使用的模型 API key

skill 文档给出的推荐运行方式是:先在主机上通过 SSH 从已部署的 Mac 主机把 token 提取到本机环境变量,再运行 smoke 脚本。注意仓库中并不提交任何 Discord token 或频道配置,harness 会在 guest 内部现场写入配置:

export OPENCLAW_PARALLELS_DISCORD_TOKEN="$(
  ssh peters-mac-studio-1 'jq -r ".channels.discord.token" ~/.openclaw/openclaw.json' | tr -d '\n'
)"

pnpm test:parallels:macos \
  --discord-token-env OPENCLAW_PARALLELS_DISCORD_TOKEN \
  --discord-guild-id 1456350064065904867 \
  --discord-channel-id 1456744319972282449 \
  --json

入口脚本与参数解析

pnpm test:parallels:macospackage.json 中映射到 bash scripts/e2e/parallels-macos-smoke.sh,该脚本只负责切换仓库根目录后转调 TypeScript 实现(scripts/e2e/parallels-macos-smoke.sh):

exec node --import tsx scripts/e2e/parallels/macos-smoke.ts "$@"

真正的逻辑在 scripts/e2e/parallels/macos-smoke.ts 中。其 usage() 输出了完整参数表,与 Discord 相关的三个参数为:

  • --discord-token-env <var>:主机上 Discord bot token 所在环境变量的名称;
  • --discord-guild-id <id>:smoke 双向投递使用的 guild ID;
  • --discord-channel-id <id>:smoke 双向投递使用的 channel ID;
  • --json:输出机器可读的 JSON summary。

其他常用参数包括 --vm(默认 macOS Tahoe)、--snapshot-hint(默认 macOS 26.5 latest)、--mode <fresh|upgrade|both>(默认 both)、--provider(默认 openai)、--model <provider/model>--install-url(默认指向官方安装脚本)、--host-port(默认 18425,用于把当前 main 打成的 tgz 通过临时 HTTP 服务发给 guest)。

Discord 参数之间存在强一致性校验:validateDiscord() 要求只要设置了任一 Discord 参数,--discord-token-env--discord-guild-id--discord-channel-id 三者必须齐全,且指定的环境变量必须非空,否则直接报错退出。只有当 token、guild、channel 三者同时具备时,discordEnabled() 才返回 true,各泳道中的 Discord 阶段才会执行;否则 summary 中对应字段保持 skip

fresh 泳道的阶段划分

runFreshLane() 中,每个阶段都通过 PhaseRunner 记录独立日志并施加超时。完整阶段顺序及超时(秒)如下:

阶段 超时(秒) 作用
fresh.restore-snapshot 780 恢复快照;失败会先 prlctl stop --kill 再重试一次
fresh.reset-state 180 杀掉旧 gateway 进程、卸载全局 openclaw、删除 ~/.openclaw 与 npm 缓存,确保干净状态
fresh.install-main 420 从主机 tgz 下载并通过 npm install -g 安装
fresh.verify-main-version 60 校验 --version 输出包含构建 commit
fresh.verify-bundle-permissions 180 检查安装产物不存在 world-writable 文件
fresh.install-companions 600 按 provider 安装运行时伴侣组件
fresh.onboard-ref 420 非交互式 onboard(--secret-input-mode ref--gateway-port 18789--gateway-bind loopback
fresh.gateway-start 180 sudo 传输模式下手动拉起 gateway
fresh.gateway-status 180 gateway status --deep --require-rpc 最多重试 8 次
fresh.dashboard-load 180 抓取控制 UI 页面并验证其静态资源全部可加载
fresh.first-agent-turn 2700 本地 agent 对话,期望模型精确回复 “OK”
fresh.discord-config 600 在 guest 内写入 Discord 配置并重启 gateway
fresh.discord-gateway-ready 180 channels status --probe --json 中必须出现 "discord"
fresh.discord-roundtrip 180 执行双向投递证明

upgrade 泳道结构相同:恢复快照 → 安装 latest 版本 → 通过 dev 通道或目标包升级 → onboard → gateway/dashboard/agent 校验 → 同样的三个 Discord 阶段。任一 install-main/update-dev 或泳道主状态失败时,进程退出码置为 1。

guest 内的 Discord 配置写入

fresh.discord-config / upgrade.discord-config 阶段实际执行的是 scripts/e2e/parallels/macos-discord.tsMacosDiscordSmoke.configure() 生成的一段 guest 内 shell 脚本,其写入序列为:

openclaw config set channels.discord.token <TOKEN>
openclaw config set channels.discord.enabled true
openclaw config set channels.discord.groupPolicy allowlist
openclaw config set channels.discord.guilds <单对象 JSON> --strict-json
openclaw doctor --fix --yes --non-interactive
# 用 Node 脚本把 "discord" 追加进 plugins.allow 数组
openclaw plugins enable discord
openclaw gateway restart
openclaw channels status --probe --json

其中 guilds 的 JSON 形状是把目标频道显式放行:

{
  "<guildId>": {
    "channels": {
      "<channelId>": { "enabled": true, "requireMention": false }
    }
  }
}

requireMention: false 很关键——它保证主机主动发的消息不需要 @机器人 就能被客端接收,这正是入站方向读回的前提。

skill 文档在这里给出两条重要的实操告诫,与源码实现相互印证:

  1. channels.discord.guilds 必须整体以一个 JSON 对象写入(--strict-json,不能拆成 config set channels.discord.guilds.<snowflake>... 的点路径逐级写入。原因是数值型雪花 ID 在逐段 config set 时会被当作数组下标处理,导致结构被写坏。
  2. Discord 配置阶段不要使用 prlctl enter / expect 交互终端,长命令会被行折叠或截断损坏;应改用 prlctl exec --current-user /bin/sh -lc ... 以脚本方式执行。这也解释了源码中 guest 传输的两种模式:优先 --current-user,探测失败时回退到 root sudo 并手动指定 HOME/USER 等环境(waitForCurrentUser()startManualGatewayIfNeeded())。

双向投递证明:nonce 闭环

runRoundtrip(phase) 是整个 skill 的核心,用 randomUUID() 生成一个 nonce,派生出 outboundinbound 两个标记,然后按四步推进(对应 macos-discord.ts 第 65-92 行):

第一步:客端出站。 在 guest 内执行:

openclaw message send \
  --channel discord \
  --target channel:<channelId> \
  --message parallels-macos-smoke-outbound-<outboundNonce> \
  --silent --json

输出 JSON 落盘为 <phase>.discord-send.json,并从中解析出 messageId 写入 <phase>.discord-sent-message-id。这里有个实现细节:skill 文档特别指出,guest 内的 message send/read 必须走 openclaw 包装命令,而不是 node openclaw.mjs message ...——直接调用入口文件不会以相同方式暴露 lazy message 子命令。源码中出站确实用的是 guestOpenClaw(即 openclaw),与文档告诫一致。

第二步:主机可见性校验。 harness 直接用 bot token 轮询 Discord 官方 API(Authorization: Bot <token>),在 180 秒期限内每 2 秒检查一次:先 GET /channels/<channelId>/messages/<messageId> 取消息正文,取不到再 GET .../messages?limit=20 拉取最近 20 条,只要任一处包含 outbound nonce 即视为通过;超时则抛出 “Discord host visibility timed out”。

第三步:主机回帖。 harness 以 bot 身份 POST /channels/<channelId>/messages 发送 parallels-macos-smoke-inbound-<inboundNonce>,请求体带 flags: 4096(即 SUPPRESS_EMBEDS 之外的系统标记位,用于让这条入站消息以不触发的方式送达),并把返回的 id 存为 <phase>.discord-host-message-id

第四步:客端读回。 在 guest 内轮询:

openclaw message read \
  --channel discord \
  --target channel:<channelId> \
  --limit 20 --json

只要 stdout 中出现 inbound nonce 即通过,否则 180 秒后报 “Discord guest readback timed out”。

这四步完成后,出站 nonce 出现在频道历史中、入站 nonce 出现在 message read 输出里,双向投递闭环成立。

退出时的清理与关机

harness 在 finally 块中无条件做两件事(macos-smoke.tsrun() 收尾逻辑):

  1. 清理临时消息cleanupMessages() 读取四个 <phase>.discord-*-message-id 文件,对其中记录的 guest 出站消息与主机回帖逐一 DELETE /channels/<channelId>/messages/<id>,避免污染频道。
  2. 成功即关机stopVmAfterSuccessfulSmoke() 检查 freshDiscord / upgradeDiscord 状态,只要任一为 pass,就执行 prlctl stop <vm> 关闭 guest。skill 文档进一步强调:Discord 配置好的 VM 不能留着运行——证明完成后它仍可能继续在 #maintainer 频道读写、发垃圾消息;自动化流程会自动关机,但手动做 ad-hoc Discord 检查后仍要记得 prlctl stop "macOS Tahoe"

判定标准与产物

skill 文档定义的 Pass criteria 有四项:

  • 请求的泳道(fresh 或 upgrade)整体通过;
  • summary 中该泳道报告 discord=pass
  • guest 出站 nonce 出现在频道历史中;
  • 主机入站 nonce 出现在 openclaw message read 输出中。

summary 由 writeSummary() 生成 summary.json(含 freshMain.discordupgrade.discord、快照、版本、runDir 等字段),--json 时直接输出到 stdout,否则打印人类可读文本。排查 flaky 时,优先看 run 目录中的 fresh.discord-roundtrip.logdiscord-last-readback.json 等产物;各阶段的 per-phase 日志按 skill 文档说明位于 /tmp/openclaw-parallels-smoke.*(macOS 泳道的 run 目录由 makeTempDir("openclaw-parallels-macos.") 创建,见 scripts/e2e/parallels/filesystem.ts)。

快照恢复的运维要点

skill 文档 Notes 一节还包含几条与 Parallels 快照恢复强相关的经验,其中两条有源码佐证:

  • poweroff 快照优先:快照解析器(scripts/e2e/parallels/snapshots.ts)在做模糊匹配时,会把 *-poweroff* 名称剥离后与 hint 比对,并对状态为 poweroff 的快照额外加 0.5 分。这让 harness 可以直接复用“仅磁盘的恢复快照”,而不必传递更长的 hint。恢复后若快照状态是 poweroff 或 VM 已停止,restoreSnapshot() 会自动 prlctl start 拉起 VM。
  • PET_QUESTION_SNAPSHOT_STATE_INCOMPATIBLE_CPU 处理:Windows/Linux 泳道恢复快照时若日志出现该错误,丢弃一次 suspended 状态、新建一个 *-poweroff* 替代快照再重跑即可;smoke 脚本现在会自动启动恢复后的 power-off 快照。
  • 三 OS 全量扫描的并行性:共享构建锁在并行下是安全的,但快照恢复本身是 Parallels 的瓶颈;主机已有负载时,建议对 Windows/Linux 的恢复密集型重跑串行执行。

小结

这套 Discord roundtrip smoke 的价值在于把“消息链路是否真的双向打通”从人工抽查变成了可重复、可机读判定的自动化流程:参数校验(三参数齐全且 token 非空)、guest 内一次性 JSON 写入频道配置、基于 UUID nonce 的四步闭环验证、退出时清理消息并关机,每一个环节都能在 scripts/e2e/parallels/macos-smoke.tsscripts/e2e/parallels/macos-discord.ts 中找到对应实现。对于需要在新版本、新快照或新通道上回归 Discord 投递的维护者,直接按 skill 文档的推荐运行方式执行,再对照 Pass criteria 检查 summary.json 即可。

登录后查看全文
热门项目推荐
相关项目推荐