# 事故复盘（postmortem） 0003：Web agent（智能体）验收了替代服务器，而非其当前 GUI

[English](0003-web-agent-gui-feedback-loop.md) | 中文

状态：已解决

## 摘要

Web agent 修改了 GUI 源码，却不知道当前会话对应哪个 URL、由哪个进程承载。它把验收交还给用户，随后在 `window.__DSH_BOOT__` 缺失导致白屏的情况下，仍把裸 Vite 返回的 HTTP 200 当作成功；最后，原页面其实已经加载了重建产物，它却去验收另一个端口上的替代 `dsh web` 服务器。修复让当前 URL 和运行模式对模型可见且可由 shell 查询，在独立 Vite 开始监听前拒绝启动，并依据外部状态验收生产模式刷新与开发模式 HMR（热模块替换）。

## 概述

该会话运行在端口 3081 的 DeepSeek Harness Web GUI 中，而用户选择的 Workspace 是空的 `test/` 目录。模型请求既未指明该 GUI，也未提供它的源码检出目录、URL、进程或更新模式。仓库在 `apps/web` 中提供了 Vite 开发脚本，完整的浏览器组合则由 `dsh web` 提供。

由此产生的各个动作单看都合理，却没有指向同一个验收目标。源码修改、成功构建、HTTP 200、注入的启动 manifest（元数据清单）和用户原本打开的页面，被当成了可以相互替代的事实。

证据源是 `session-3eb796c2-5159-4686-affe-df8719f6f987` 的持久化事件日志，其头部记录的 cwd 为 `/Users/tn.shen/Documents/deepseek-harness-gui-master/test`。初始请求头位于序列 6；面向用户的交接、裸 Vite 启动、替代宿主启动、启动 manifest 探测，以及首次探测 3081 进程，分别位于序列 30939、31865、34309、34441 和 34681。下方时间线以这些事件为依据，而不是根据后续报告反推意图。

## 影响

用户不得不连续指出三个错误：agent 把验收交还给用户；建议预览的页面一片空白；报告成功的 URL 并不是用户正在使用的页面。一个不受管理的替代服务器还持续运行到下一个轮次，直到用户提出质疑。

本次调查没有重启或修改只读的 3081 和 3082 试验服务。

## 时间线

- 在第 2 个轮次中，agent 修改主题后，在序列 30939 的消息中让用户运行 `pnpm run demo:tui` 或打开一个未明确指定的 Web 应用。它没有对组装后的 Web 应用执行任何验收。
- 在第 3 个轮次中，agent 读取 `apps/web/package.json`，在序列 31865 于端口 5173 上启动裸 Vite，观察到 HTTP 200 后便宣布成功。浏览器却抛出 `client-modules: window.__DSH_BOOT__ is missing or not an object`，并显示白屏。
- 在第 4 个轮次中，agent 找到了完整的 `dsh web` 启动路径，重新构建 shell，在序列 34309 于端口 3334 上启动一个不受管理的进程，并且只在序列 34441 检查了这个替代服务是否返回 200 和启动 manifest。它从未探测端口 3081。
- 在第 5 个轮次中，用户在序列 34556 报告 3081 已经显示新主题。直到序列 34681，agent 才检查既有进程并移除冗余服务器。

## 根因

Web 组合没有向模型提供当前 GUI、规范 URL 或运行模式的身份信息。会话 cwd 正确标识了用户选择的 Workspace，但模型把这个项目目录当成了应用目录。系统也没有持久记录将 GUI 源码检出目录、构建产物、服务进程、目标 origin 和浏览器验收关联起来。

裸 Vite 返回 HTTP 200，使错误的启动路径看似合理。`window.__DSH_BOOT__` 只由完整宿主注入，因此传输层就绪不代表应用已就绪。首个回归测试以另一种方式重复了同样的错误：超时机制终止 Vite 后，非零退出断言仍会通过。真实复现暴露了这一误报。

agent 还通过 shell `&` 绕过了后台进程语义，因此任务身份、完成通知、结果收集和清理机制均未生效。验证端口 3334 只能证明第二个服务可以工作。

## 已添加的防护措施

- Web 启动器在记录到日志的 `app:web-surface` 提示词区段和受管的 `$DSH_WEB_URL` 环境变量中发布规范环回 URL。
- 生产指南要求重新构建产物，并在刷新后验证既有 URL。开发指南说明 HMR 接收端始终开启；同一源码检出目录中的 `pnpm run dev:web` 会重新构建客户端插件 bundle，实现免刷新的重载，而 Web shell 和普通包的改动仍然需要刷新页面。
- `apps/web` 的独立 Vite 服务模式会在配置阶段拒绝启动。其子进程测试验证进程自然退出，并插桩 `Server.listen()`，确保短暂绑定端口也不会漏检。
- 分层的真实路径测试覆盖 CLI（命令行界面）请求、精确的生产／开发模式提示词、shell 运行时事实、同端口静态产物替换、源码 watcher 重建、宿主 stat 轮询，以及页面 identity 不变的浏览器 HMR。
- PR（Pull Request）证据保留了原始 3081 会话的截图，以及真实模型驱动的 GUI 修改前后对比；验收以外部浏览器、HTTP、进程和会话日志的观测结果为准。

## 教训

- agent 必须先知道隐藏的运行时前置条件，才能指导用户；启动模式属于应用上下文，不应依赖团队口口相传。
- HTTP 就绪、构建成功和启动 manifest 是不同的事实。验收必须明确指定确切的 origin，并从外部观察所请求的改动是否在该 origin 生效。
- 替代服务无法证明既有页面已经改变。确实收到启动长时间运行进程的请求时，应使用受管任务生命周期。
- 回归测试必须能够针对所报告的机制失败。进程超时不等同于快速失败，进程退出后端口可用也不能证明该端口从未被绑定。
