- 人工智能
- AI Agent
- AI 应用
- 大模型
- 交互助手
- 工具调用
- Agent 编排
- 桌面应用
【免费下载链接】OpenMausBot
Open-source Grok Bot alternative with a virtual machine that bots can use
本文聚焦 OpenMausBot 中 Local VM(本地虚拟机)的“停止—恢复”生命周期,说明如何用仓库自带的两条命令完整验证:idle 自动停止不会删除容器、停止原因能跨服务重启保留、UI 上的 Start 只发出一次请求并在 Cua 预热期间保持忙碌状态、就绪桌面替换空态而无需 remove/recreate。读完后你将掌握这套验证的分层结构(前端面板、服务端路由、镜像启动脚本、真实容器验收)以及每个断言背后的源码依据,可以独立复现或扩展其中的任一环节。
一、核心结论先行:停止必须可恢复,而不是重造
OpenMausBot 的 Local VM 是运行在宿主机容器运行时(Docker / Podman / Apple container)里的 Cua 桌面。资源节约策略会让闲置的桌面停掉,但**“停止”语义必须是可恢复的**,否则用户的文件、浏览器登录态与已安装应用都会随一次节能动作消失。
文档 docs/verification/local-vm-resume.md 定义了三层可验证承诺:
- 服务端:idle 停止只下发
stop,绝不触发remove;停止原因写入磁盘,服务重启后仍能读到。 - 前端:面向已存在但已停止的 VM,界面给出“已停止”与原因解释,Start 只发一次请求;请求返回后容器已 running 但 Cua 尚未就绪时,按钮保持 disabled +
aria-busy,就绪后直接用预览替换空态。 - 镜像:容器复用 home 目录、甚至被
docker kill --signal KILL暴力终止后,陈旧的 X / 浏览器锁不会导致恢复被拒。
对应的源码事实是:localVmResumable()只有在运行时可用、守护进程在线、镜像存在、容器为stopped、网络为 loopback、安全加固通过、workspace 持久化安全时,才允许走start分支(server/container-computer.ts)。任何一项“未知”都会 fail closed,而不是乐观放行。
二、两条命令即可跑完整个生命周期检查
文档给出的入口非常克制,一共两行:
pnpm exec vitest run server/container-computer.test.ts server/local-vm-browser-profiles.test.ts server/local-vm-stop-reason.test.ts server/computer-idle.test.ts server/group-local-vm.e2e.test.ts server/routes/desktop-viewer.test.ts src/components/LocalComputerSection.test.ts src/components/ComputerPanel.test.ts src/components/ComputerPanel.i18n.test.ts node --experimental-strip-types scripts/verify-local-vm-resume.ts第一行是九个文件的单元 / 集成 / e2e 组合,覆盖容器动作、镜像启动脚本、停止原因、idle 计时器、真实 HTTP 路由与 UI 组件;第二行直接以 Node 的 type stripping 运行 UI 证据脚本(scripts/verify-local-vm-resume.ts),不需要额外编译步骤。
运行前提:仓库根目录、已安装依赖、有可用的 Chromium/Chrome(脚本会自行解析或下载固定版本),且不需要宿主机 Docker 守护进程——浏览器配方全程走合成传输。真实 Docker 只出现在文档记录的那次手工验收中。
两条命令的分工可以这样理解:vitest负责“逻辑与契约”,verify-local-vm-resume.ts负责“真实组件 + 真实浏览器 + 可归档证据”。
三、测试矩阵:每个文件守住一条承诺
| 测试文件 | 守护的核心行为 |
|---|---|
| server/container-computer.test.ts | 容器生命周期动作;resumes a compatible stopped desktop without removing it断言只调用docker start,且调用序列中不含rm/run |
| server/local-vm-browser-profiles.test.ts | 运行镜像真实的启动脚本与真实的 profile 抢救逻辑,验证登录态跨镜像替换存活、锁清理时机正确 |
| server/local-vm-stop-reason.test.ts | 停止原因按 target key 隔离、只在stopped_at精确匹配时返回idle |
| server/computer-idle.test.ts | idle 计时器只在完整静默窗口后挂起;忙碌时不挂起;异常按退避重试 |
| server/group-local-vm.e2e.test.ts | 真实 HTTP 路由 + 真实 idle 计时器 + 测试专用 loader 缩短窗口 |
| server/routes/desktop-viewer.test.ts | 桌面查看器/状态路由对非就绪、非受管、网络不安全等状态的拒绝路径 |
| src/components/LocalComputerSection.test.ts | 设置页的 Local VM 生命周期请求构造与破坏性操作确认 |
| src/components/ComputerPanel.test.ts | 控制权租约切换顺序(先闸门后服务端、先释放后清理) |
| src/components/ComputerPanel.i18n.test.ts | 面板文案在语言切换后语义稳定 |
这张表值得单独强调:恢复正确性不是单点函数,而是一条贯穿镜像、服务器、路由、UI 的链路。少任何一层,“停止后能否恢复”就退化成人工经验。
四、镜像准备脚本:为什么“复用 home 目录”曾是不可恢复的根因
镜像构建阶段会生成/usr/local/bin/prepare-openmausbot-workspace.sh,其脚本正文由 server/container-workspace.ts 中的workspacePreparationDockerfile()以printf形式写入 Dockerfile。理解这个脚本,就理解了文档里“browser locks are cleared once per container start”的含义。
4.1 三类 profile 与一类应用目录的迁移
脚本先建立持久化骨架:
workspace=/home/cua/workspace profiles="$workspace/.browser-profiles" apps="$workspace/.desktop-apps" mkdir -p "$profiles/google-chrome" "$profiles/chromium" "$profiles/mozilla" "$apps/local" "$HOME/.config" "$HOME/.cache"随后migrate_profile做“只在目标为空时拷入,然后把源目录换成符号链接”:
migrate_profile() { source="$1" target="$profiles/$2" if [ -d "$source" ] && [ ! -L "$source" ] && [ -z "$(find "$target" -mindepth 1 -print -quit)" ]; then cp -a "$source"/. "$target"/ fi rm -rf "$source" ln -s "$target" "$source" } migrate_profile "$HOME/.config/google-chrome" google-chrome migrate_profile "$HOME/.config/chromium" chromium migrate_profile "$HOME/.mozilla" mozilla~/.local走同样思路但方向相反:先把旧内容搬进$apps/local,再把~/.local变成指向它的链接。效果是home 目录可以被随意丢弃,真正的用户数据永远落在挂载的 workspace 上。
server/local-vm-browser-profiles.test.ts 用一个临时的 fake home/workspace 验证了这一点:跑完后~/.mozilla是指向workspace/.browser-profiles/mozilla的符号链接,profiles.ini内容仍在,Chrome/Chromium 的旧行为不受影响。
4.2 容器身份:boot ID + PID 1 启动时间
锁清理的判据是“这是不是一个新容器”:
container_start="$(cat /proc/sys/kernel/random/boot_id):$(sed "s/.*) //" /proc/1/stat | cut -d " " -f20)" prepared="$HOME/.cache/openmausbot-browser-locks" if [ "$(cat "$prepared" 2>/dev/null || true)" != "$container_start" ]; then find "$profiles" \( -name SingletonLock -o -name SingletonSocket -o -name SingletonCookie -o -name .parentlock -o -name lock -type l \) -delete printf "%s\n" "$container_start" > "$prepared" fi注意判据是boot ID 与 PID 1 启动时间的组合,而不是容器名或 home 路径。这正好解释了文档中的两条关键描述:
- “Browser locks are cleared once per container start.”——同一容器内多次执行准备脚本,
$prepared内容不变,什么都不删。 - “A changed PID 1 start time or boot ID clears old locks again, including when the container reuses its home directory.”——容器重建但复用 home 时,
/proc/1/stat第 20 字段或 boot ID 必然变化,陈旧锁被清掉。
find的目标列表也很有讲究:Chromium 系的SingletonLock/SingletonSocket/SingletonCookie,Firefox 系的.parentlock与符号链接型lock。用-type l限定lock是为了只删符号链接形态(Firefox 用它指向持锁主机),不误删普通文件。
4.3 “Cua driver 重启不清理活锁”的测试如何证明
server/local-vm-browser-profiles.test.ts 的用例名直接表达契约:keeps live browser locks across a Cua driver restart until the %s changes,并通过it.each(["PID 1 start time", "boot ID"])参数化两条路径。
测试流程是:
- 先跑一次准备脚本写入身份文件;
- 手工造出三份 Chromium 锁与一份 Firefox
lock符号链接 +.parentlock; - 再跑一次准备脚本——身份未变,逐个
readlinkSync断言目标原封不动; - 改写
boot-id或init-stat(第 20 字段从123改成456); - 第三次运行脚本——所有锁被删除。
这里有个实现细节非常值得借鉴:测试并不是直接执行 Dockerfile,而是从managedImageDockerfile()里截取真实的RUN printf片段,把/home/cua/workspace、/proc/sys/kernel/random/boot_id、/proc/1/stat三个路径替换为临时文件后再执行(server/local-vm-browser-profiles.test.ts)。这样“测的脚本”和“镜像里的脚本”是同一份文本,而不是复制品。
五、镜像替换前的抢救:rescueFirefoxProfile与rescueInstalledApps
恢复能力还有第二条支线:当旧桌面所在镜像已经不被当前版本接受时,系统不是硬重启,而是先把登录态与应用拷进 workspace 再删除容器。实现在 server/local-vm-profiles.ts。
rescueFirefoxProfile的返回值是三态契约:
"rescued":从containerName:/home/cua/.mozilla拷到随机 staging 目录,再 rename 到workspace/.browser-profiles/mozilla;"kept":workspace 里已有非空 profile,绝不覆盖;"none":容器里根本没有~/.mozilla(从未用过 Firefox),或~/.mozilla已是链接(新桌面的正常形态)。
错误处理只放行一类“预期失败”:容器内文件不存在(正则匹配 Docker/Podman 的 "Could not find the file");其余错误一律抛出,避免把权限问题伪装成“没有东西可抢救”。测试里的 fake runtime 是一个只实现cp的 shell 脚本,正如docker cp对已停止容器的行为(server/local-vm-browser-profiles.test.ts)。
rescueInstalledApps额外加了一道安全闸:如果 workspace 里已有的.desktop-apps/local与容器内的内容不一致,直接抛could not preserve installed desktop apps because the durable app area differs from the old desktop,而不是合并或覆盖。这条对应的测试断言持久区内容仍是"durable desktop"。
六、浏览器端 recipe:真实面板 + 合成桌面传输
scripts/verify-local-vm-resume.ts 是整个文档里信息密度最高的部分。它做的不是“截图工具”,而是一个受控的真 UI 试验台。
6.1 启动与隔离
const fixture = await launchVerificationServer(); const home = mkdtempSync(join(tmpdir(), "omb-vmr-")); // Keep macOS browser sockets below their path limit.launchVerificationServer()来自 scripts/control-omb.ts,会拉起一个隔离数据目录的服务器并返回{ url, pid, dataDir, logPath };runControlOmb(["new-bot", ...])建一个种子 bot。mountPreview()再以 Vite 起一个只服务指定入口的预览站点,并把/api反代到 fixture(scripts/testing/preview-fixture.ts)。
页面的入口是/scripts/testing/cloud-preview.tsx,其中真正挂载的是生产组件<ComputerPanel />与<LocalComputerSection />,只是window.fetch被替换为合成传输(scripts/testing/cloud-preview.tsx)。合成传输里的vmResumeStatus()精确模拟了恢复场景需要的字段:
container: vmRunning ? "running" : "stopped", resumable: !vmRunning, desktopReady: vmDesktopReady, ready: vmDesktopReady, stop_reason: vmRunning ? null : "idle", problem: vmDesktopReady ? null : vmRunning ? "Desktop is starting" : "The Local VM is stopped",这几行是整段 UI 验证的地基:容器已经 running、但ready=false的状态只能伪造出来,真实 Docker 里很难稳定复现在这个瞬间做断言。
6.2 复用本机已安装的浏览器
文档明确要求:Set OMB_AGENT_BROWSER_PATH and AGENT_BROWSER_EXECUTABLE_PATH to reuse installed tools.
优先级在 scripts/testing/control-omb-ui.ts 中实现:先看OMB_AGENT_BROWSER_PATH(agent-browser 可执行文件),再看工具目录,最后看 PATH;否则按固定版本下载并用大小与 SHA-256 校验。Chrome 遵循同一规则,环境变量名是AGENT_BROWSER_EXECUTABLE_PATH,且文件不存在时会直接报错提示unset it to use the Chrome agent-browser installs。两者都只会落到UI_TOOLS_DIR下一份。
6.3 证据目录、日志与录屏
const evidenceDir = process.env.OMB_UI_EVIDENCE_DIR ? resolve(process.env.OMB_UI_EVIDENCE_DIR) : dirname(fixture.info.logPath); const record = process.env.OMB_UI_RECORD === "1";- 默认证据(三张截图)与持久日志同目录,即
logPath所在目录; OMB_UI_EVIDENCE_DIR可重定向到任意目录,脚本会mkdirSync(..., { recursive: true });OMB_UI_RECORD=1且 PATH 上有ffmpeg时,额外产出resume.mp4。
脚本启动时第一行就console.log(JSON.stringify(fixture.info)),把隔离服务器 URL 与日志路径打印出来——这就是文档里说的 receipt。失败时它会在catch中打印一份 agent-browser snapshot 再抛出,便于定位是选择器错了还是状态没到。
6.4 等待条件:没有固定延迟
文档特别强调 “The readiness assertion waits for two actual status reads while the desktop is held unready; it uses no fixed delay.” 实现是:
await wait(`window.cloudPreviewFixture.vmStarts === 1`); await wait(`Array.from(document.querySelectorAll('button')).some(b => b.textContent.includes('Starting Local VM') && b.disabled && b.getAttribute('aria-busy') === 'true')`); await wait(`window.cloudPreviewFixture.vmReadinessReads >= 2`); assert.equal(await evaluate(`Array.from(document.querySelectorAll('button')).some(b => b.textContent.includes('Starting Local VM') && b.disabled)`), true);vmReadinessReads只在vmRunning && !vmDesktopReady时自增(scripts/testing/cloud-preview.tsx),所以“≥2”等价于“前端至少又真实轮询了两次状态,而桌面仍未就绪”。这是对生产轮询逻辑waitForLocalVmReady的直接观测,而不是睡眠若干秒碰运气。
生产轮询本身的参数也值得记住:默认 2000 ms 间隔、60 s 超时,循环条件是!ready && container === "running",并且每次都用AbortSignal检查取消。
七、UI 断言清单:文档那四句话的逐条落地
文档描述浏览器检查证明了四件事。对照脚本,可以逐条映射:
| 文档描述 | 脚本中的断言 |
|---|---|
| existing shared VM shows “stopped” and an idle explanation | 切到vm-stopped场景后wait('The Local VM is stopped'),并断言含Stopped after inactivity、不含isn't available for this bot |
| Start issues exactly one request | 点击Start Local VM后wait('vmStarts === 1'),结尾再assert.equal(vmStarts, 1) |
| button stays disabled and busy after that request returns while Cua is still warming up | 断言存在文案含Starting Local VM、disabled、aria-busy="true"的按钮 |
| ready desktop replaces the empty state without a remove or recreate request | 断言存在img.src === vmScreenshot && naturalWidth > 1,且paths中没有以/local-computer/remove或/local-computer/run结尾的请求 |
| Settings offers Start for a stopped VM and waits for readiness | 重置后切到 Settings 面板,点 Start,wait('Waiting for the desktop'),点Finish VM startup,wait到某个[aria-live]文本恰为Ready |
两个文案来源可以追到源码:vm.stopped.idle与vm.stopped.detail定义在 src/locales/en.json(另有 ja/it/es/tr/pl 等语言版本),面板在 src/components/ComputerPanel.tsx 与 src/components/LocalComputerSection.tsx 用status.stop_reason === "idle"二选一。
paths这个数组记录所有含/computer或/local-computer的请求路径,结尾用some(p => p.endsWith('/local-computer/remove') || p.endsWith('/local-computer/run'))反向断言。换句话说,“恢复”路径上任何一次“重建”都会让测试失败,这是本文最值得抄走的一条断言写法。
八、服务端 fixture:真实路由 + 真实计时器,只换容器边界
group-local-vm.e2e.test.ts不是普通 mock 测试。文档描述是:“runs the actual HTTP routes and idle timer, replacing only the container boundary and shortening the idle window through a test-only loader.”
具体机制在 server/testing/group-local-vm-hooks.mjs:通过module.registerHooks拦截container-computer.ts与local-vm-profiles.ts的解析,换成 fixture 版本;fixture 的containerComputerStatus()从状态文件读containers/stopped/stoppedAt/outdated/failed等字段,containerComputerAction()把run/start/stop/remove逐个记入actions。真实的服务启动、路由、租约、idle timer、MCP 配置全部保留。
“缩短 idle window”是通过改写源码文本实现的:把checkedIdleMs(idleMs)替换成读状态文件里idleMs的表达式(server/testing/group-local-vm-hooks.mjs)。所以用例里可以写vmState({ containers: ["shared"], idleMs: 300 }),让 300 ms 就触发一次真实空闲回收。
8.1 三个断言串成一条因果链
server/group-local-vm.e2e.test.ts 的核心用例:
vmState({ containers: ["shared"], idleMs: 300 }); await api("PATCH", "/api/config", { localVm: { mode: "shared", idleTimeoutMinutes: 5 } }); const { bot } = await api("POST", "/api/bots", { name: "Resume fixture", computer: "vm" }); await until(() => api("GET", "/api/local-computer"), s => s.container === "stopped"); expect((await api("GET", "/api/local-computer")).stop_reason).toBe("idle"); const stopped = JSON.parse(readFileSync(stateFile, "utf8")); expect(stopped.containers).toEqual(["shared"]); // 容器还在 expect(stopped.actions).toEqual([{ action: "stop", target: "shared" }]); // 只有 stop然后它把idleMs调大、SIGTERM结束并重启服务,再断言stop_reason仍是idle;随后POST /api/local-computer/start,得到ready === true,且stop_reason变回null,动作序列变成stop → start。没有 remove,也没有 run。
8.2 停止原因为什么能跨重启存活
server/local-vm-stop-reason.ts 的实现只有二十多行,但设计很克制:
function recordPath(key: string, dataDir: string): string { return join(dataDir, "local-vm-stops", `${createHash("sha256").update(key).digest("hex")}.json`); }- 文件名是 target key 的 SHA-256,避免把
bot:<digest>之类的键直接写进路径; - 目录
0700、文件0600,写入走writeFileAtomic; - 读取时要求
status.container === "stopped"且stopped_at存在,然后精确比较stoppedAt === status.stopped_at。
最后这条比较是关键设计:stopped_at来自容器的State.FinishedAt(server/container-computer.ts)。如果之后有人手动停了一次、或者时间戳无从确定,匹配失败,localVmStopReason返回null,UI 就回落到中性的vm.stopped.detail,而不会误报“因闲置而停止”。server/local-vm-stop-reason.test.ts 用四行断言把这个矩阵钉死:不同 target key 为null、时间戳变化为null、容器 running 为null、缺stopped_at为null。
写入时机在 idle 回调里(server/index.ts):
const status = await containerComputerStatus(undefined, undefined, target); if (status.container === "running" && status.managed) { const stopped = await containerComputerAction("stop", undefined, undefined, target); recordLocalVmIdleStop(target.key, stopped.stopped_at); }注意它先确认running && managed,再 stop,并把 stop 返回的stopped_at落盘——记录的时间戳与运行时FinishedAt天然一致,这正是文档里“The reason record matches the runtime's finish timestamp”的来源。
对外暴露则通过localVmPayload()(server/index.ts)把stop_reason: localVmStopReason(target.key, status)与idle_timeout_ms、mode、max_instances一起返回。
8.3 idle 计时器本身的行为边界
server/computer-idle.test.ts 覆盖的六条行为,构成恢复链路的前置条件:
- 只在完整静默窗口后才挂起(999 ms 不触发、1000 ms 触发);
touch()续期;- 忙碌时绝不挂起,且“第一次发现空闲”要重新等满一个窗口;
- 忙碌期间每 1/10 窗口复查一次(
BUSY_CHECKS_PER_WINDOW),让晚结束的工作仍获得完整静默窗口; - 手动 stop 后可
cancel(),不再触发; - 窗口时长变更时,从最后一次活动时间重算剩余时间。
失败路径也有明确策略:suspend()抛错时按idleMs * 2^(failures-1)退避,上限至少一个窗口、最多一小时(server/computer-idle.ts)。这解释了为什么一次设备忙碌或 Docker 抖动不会让 VM 永久失去回收能力。
九、容量上限不会阻塞“恢复已有桌面”
文档提到 per-bot 与 Auto 场景:shared and per-bot starts work, and an Auto turn resumes an existing desktop even when the per-bot capacity limit is reached.
源码里的分叉点很清晰(server/index.ts):
const action = localVmWakeAction(status); if (status.ready || !action || !status.runtime) return status; if (action === "run" && !pooled && !ownsProvision) return status; if (action === "run" && target.key.startsWith("bot:")) { const count = await existingPerBotLocalVmCount(status.runtime); if (!isCurrent() || count >= localVmMaxInstances(cfg)) return status; } ... status = await containerComputerAction(action, undefined, undefined, target);容量检查只挂在action === "run"(新建)分支上;action === "start"(恢复)不经过任何maxInstances判断。语义上这是显然的:已经在磁盘上存在的桌面不占用新的配额。
对应 e2e 用maxInstances: 1把场景推到极限(server/group-local-vm.e2e.test.ts):run → stop → start → stop,然后清掉 bot 的computer/browser配置,发一条普通消息,期望 Auto 又触发一次start。最终动作序列断言为["run", "stop", "start", "stop", "start"]。这条序列同时证明了两件事:恢复走start而非run,且 Auto turn 在配额已满时仍能接上既有桌面。
配额本身的范围定义在 server/local-vm-settings.ts:默认maxInstances = 2(1–8),默认idleTimeoutMinutes = 480(5–1440),默认模式shared,另有per-bot与pool。localVmPreservesInstalledApps默认关闭,是显式 opt-in——这与 rescue 逻辑“只在需要时抢救”的保守取向一致。
十、生产就绪探针:三道门槛决定desktopReady
文档提到真实验收“passed the production readiness probe (driver version, health report, and complete screenshot)”。这三项在 server/container-computer.ts 里是严格的前置组合,且只有在container === "running"、镜像匹配、受管、loopback、安全加固、持久化安全全部满足时才会执行:
- 版本:
cua-driver --version必须精确等于cua-driver ${CUA_DRIVER_VERSION}(当前为0.37.0),随后status --socket /run/user/1000/openmausbot-cua.sock必须成功; - 健康报告:
call health_report的 JSON 必须schema_version === "1"、checks是数组、overall为ok或degraded; - 完整截图:
get_desktop_state ... --screenshot-out-file /tmp/openmausbot-readiness.png,再用base64 -w0取回并交给wholeScreenshot()校验,不完整即判失败。
失败时它会tail -n 4 /var/log/supervisor/cua-driver.error.log并把 320 字符内的错误写进desktop_error,最终status.ready = status.problem === null。这解释了 UI 为什么能在“容器已起、桌面未就绪”区间稳定显示忙碌态:ready是一个由真实探针得出的布尔值,而不是“容器 running”的同义词。
十一、真实 Docker 验收记录:2026-10-01 的两次停止/两次强杀
文档记录了一次手工验收,这是唯一触及真实宿主 Docker 的环节,条件与结论都需要准确复述:
- 环境:Linux 上的 Docker;使用单独创建的、一次性的容器;镜像为当时固定的
driver-0.20.0-v5。 - 常规轮次:两轮 stop/start 均通过生产就绪探针(driver 版本、health report、完整截图),且
/home/cua与/opt下的标记文件都保留。 - 强杀轮次:两轮
docker kill --signal KILL之后再恢复;先用docker cp确认/tmp/.X1-lock仍留在已停止的容器里,再调用生产 Start。两轮同样通过就绪与标记文件检查。 - 根因与修复:固定镜像的
/usr/local/bin/start-vnc.sh会在启动 VNC 前删除/tmp/.X1-lock与/tmp/.X11-unix/X1,这正面解决了此前“一律拒绝恢复”背后的陈旧锁顾虑。 - 清洁度:fixture 使用空的临时 workspace,事后全部删除;不需要重建镜像。
- 不覆盖范围:Podman、Apple container、原生 iOS,以及镜像替换后的持久化。
关于版本号需要一句提醒:文档记录的是当时的driver-0.20.0-v5;当前源码中CUA_DRIVER_VERSION = "0.37.0"、IMAGE_LAYER_VERSION = "7",镜像引用形如localhost/openmausbot/cua-local-vm:driver-0.37.0-v7(server/container-computer.ts)。也就是说,那次验收证明的是机制,而不是某个永久有效的标签;重新验收时应以当前IMAGE常量与com.openmausbot.*标签为准,因为“镜像与容器标签、而非可变 tag 决定兼容性”。
十二、Review evidence:证据是怎么被约束的
文档的 Review evidence 一节给了四类产物,全部位于docs/verification/evidence/local-vm-resume/下:
| 证据 | 说明 |
|---|---|
| before.png | 在未改动生产代码的上游提交9f0c33d00fc490491bf794704fd192046d2f31eb上截取,使用同一套一次性 fixture 与被检查过的 stopped-VM 响应;其状态文案反映基线服务器拒绝恢复 |
| stopped.png | 改动后的面板:已停止 + idle 解释 + Start |
| starting.png | 请求已返回、Cua 预热中,按钮禁用且带 spinner |
| ready.png | 就绪桌面替换空态 |
| resume.mp4 | 记录 Start、两次禁用态的轮询、以及就绪预览 |
三条约束值得学习:
- before 图必须来自未修改的生产代码,只把 fixture 传输层拷进那个 checkout——否则“修复前”就没有说服力;
- 截图捕获的是真实 panel 元素(
screenshot aside),不是拼图;视频只隐藏 fixture 控件(.fixed.left-2.top-2),产品 UI 原样保留; - 桌面预览明确标注为合成,不宣称真实远程会话或原生移动端验收。
十三、复现清单与常见坑
按顺序执行,可以让这套验证在你的机器上稳定跑通:
pnpm install,确认 Node 版本支持--experimental-strip-types;- 先跑九个 vitest 文件,定位是逻辑层还是 UI 层的问题;
- 需要离线/复用时,导出
OMB_AGENT_BROWSER_PATH与AGENT_BROWSER_EXECUTABLE_PATH指向本机已安装的 agent-browser 与 Chrome; - 需要归档证据时,导出
OMB_UI_EVIDENCE_DIR;录屏再叠加OMB_UI_RECORD=1并确保ffmpeg在 PATH 上; - 运行
node --experimental-strip-types scripts/verify-local-vm-resume.ts,从首行打印的 JSON 取隔离 URL 与日志路径; - 想验证真实容器时,不要在正在使用的数据目录上试验:按文档做法新建一次性容器与空临时 workspace,验收后清理。
常见坑与对应源码位置:
- 以为
pnpm dev的实例会被脚本改动:不会,脚本自带隔离数据目录与随机端口,mountPreview只绑定127.0.0.1。 - 在 macOS 上路径过长导致浏览器 socket 失败:脚本用
mkdtempSync(join(tmpdir(), "omb-vmr-"))并在注释里写明这一动机,不要在深层目录里改TMPDIR。 - 误判“停止原因丢了”:
localVmStopReason要求stopped_at与记录精确一致,容器被外部再停一次就会回落为中性文案,这是设计而非缺陷。 - 以为 idle 到点必然停:
ComputerIdleTimer在isBusy()为真时只复查不挂起;localVmIdleFor的忙碌判定包含镜像忙碌、生命周期忙碌与活跃线程(server/index.ts)。 - 在旧镜像上期待恢复成功:
localVmResumable会因imageMatches、安全、持久化等任一不满足而拒绝,此时应走 rescue + recreate,而不是手工docker start。
结语
这套验证的价值不在于九条命令本身,而在于它把“停止后可恢复”拆成了四个可独立证伪的层:镜像启动脚本的锁语义、服务端的 stop reason 与 idle 计时器、UI 的一次启动与无固定延迟就绪等待、以及一次带明确边界的真实 Docker 验收。任何一层回退,都会在对应文件里立刻显形——而 docs/verification/local-vm-resume.md 正是这条链路的索引地图。
- 人工智能
- AI Agent
- AI 应用
- 大模型
- 交互助手
- 工具调用
- Agent 编排
- 桌面应用
【免费下载链接】OpenMausBot
Open-source Grok Bot alternative with a virtual machine that bots can use
相关推荐
PRAXIST 直接 CLI 运维指南:从校验、启动到观察、停止与恢复的完整生命周期
PRAXIST 直接 CLI 运维指南:从校验、启动到观察、停止与恢复的完整生命周期 本文是面向运维人员(operator)的 PRAXIST 直接 CLI 操
人工智能AI Agent多智能体Agent 工作流深度研究AI 评测OpenSandbox CLI 沙箱生命周期管理实战:从配置解析到创建、验证、续期、暂停/恢复与清理
OpenSandbox CLI 沙箱生命周期管理实战:从配置解析到创建、验证、续期、暂停/恢复与清理 本文围绕 OpenSandbox CLI( osb )内置
人工智能AI AgentAgent 沙箱云原生容器编排虚拟化后端前端网络安全密钥管理CLI微服务Protoactor-go生命周期管理终极指南:从启动到停止的完整流程
Protoactor go生命周期管理终极指南:从启动到停止的完整流程 Protoactor go是Go语言中一个高性能的分布式Actor框架,其生命周期管理机
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考