OpenClaw macOS Parallels Discord 双向投递冒烟测试实战指南
本文基于 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:macos 在 package.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.ts 中 MacosDiscordSmoke.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 文档在这里给出两条重要的实操告诫,与源码实现相互印证:
channels.discord.guilds必须整体以一个 JSON 对象写入(--strict-json),不能拆成config set channels.discord.guilds.<snowflake>...的点路径逐级写入。原因是数值型雪花 ID 在逐段config set时会被当作数组下标处理,导致结构被写坏。- 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,派生出 outbound 与 inbound 两个标记,然后按四步推进(对应 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.ts 的 run() 收尾逻辑):
- 清理临时消息:
cleanupMessages()读取四个<phase>.discord-*-message-id文件,对其中记录的 guest 出站消息与主机回帖逐一DELETE /channels/<channelId>/messages/<id>,避免污染频道。 - 成功即关机:
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.discord、upgrade.discord、快照、版本、runDir 等字段),--json 时直接输出到 stdout,否则打印人类可读文本。排查 flaky 时,优先看 run 目录中的 fresh.discord-roundtrip.log 与 discord-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.ts 与 scripts/e2e/parallels/macos-discord.ts 中找到对应实现。对于需要在新版本、新快照或新通道上回归 Discord 投递的维护者,直接按 skill 文档的推荐运行方式执行,再对照 Pass criteria 检查 summary.json 即可。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00