# UI 检测与 CDP(cdp.ts / detector-ui.ts)
本文件解析 L3 UI 检测线:为什么需要真实 DOM、无依赖 CDP 客户端、文本判据、探测时序与浏览器发现。
相关文档:[架构总览](architecture.zh.md) · [守卫生命周期](guard-lifecycle.zh.md)
---
## 1. 为什么是"读真实 DOM"
DSH Web 存在"宿主活、UI 红屏"的失败形态:进程在、端口通、`curl` 拿得到 HTML,但浏览器渲染出
`Failed to load plugins`。原因:服务端 HTML 里 `
` 是空的,由 React 运行时填充;
而红屏的结构类名是 CSS-Module hash(`_boot_
`),跨构建不稳定。
**结论**:唯一可靠且非侵入的探测 = 用 headless 浏览器打开页面、读取 `document.body.innerText`。
---
## 2. 无依赖 CDP 客户端(src/cdp.ts)
零 Playwright/Puppeteer,运行时只依赖 `ws`:
```
launchSession({ debugPort })
├─ findBrowser():按候选清单找 Chrome/Chromium/Edge(Windows ProgramFiles/LOCALAPPDATA
│ + POSIX /usr/bin + macOS /Applications),每个候选 existsSync 验证
├─ spawn 专用 headless Chrome:
│ --headless=new --remote-debugging-port= --remote-allow-origins=*
│ --user-data-dir=<临时目录> --no-first-run --disable-gpu about:blank
├─ 轮询 GET /json 直到拿到 page target 的 webSocketDebuggerUrl(上限 15s)
└─ 返回 CdpSession(evaluate/close)
```
- `evaluate(expr)`:`Runtime.evaluate` + `returnByValue`;异常/错误返回 `null`(防御式)。
- `close()`:`Browser.close` → 关 WS → kill 浏览器进程 → 清理临时 user-data 目录(调用方必须 close)。
- **调试端口冲突**:随机端口范围 9000-9899;冲突时 `detectUi` 换端口重试(最多 3 次),
显式指定端口则不重试。
---
## 3. DOM 探测与文本判据(src/detector-ui.ts)
### 3.1 探测脚本(每次 evaluate 注入)
```js
const bodyText = document.body ? document.body.innerText : '';
hasComposer = root.querySelector('textarea') !== null; // 健康 UI 的标志(composer 业务容器)
isBootPage = bodyText.includes('HARNESS') && !hasComposer; // 启动页(wordmark,无 composer)
return { bodyText: bodyText.slice(0, 1200), hasComposer, isBootPage };
```
### 3.2 分类优先级(`classifyDom`)
```
1. bodyText 含 "Failed to load plugins"(FAILED_MARKER) → kind='failed'(红屏,优先生效)
2. hasComposer → kind='ok'(健康)
3. isBootPage → kind='loading'(仍在启动)
4. 其它(资源未加载完) → kind='loading'
```
### 3.3 失败详情提取
- `extractFailureDetail`:正则抓取 `web boot:...` 行(如 `web boot: 1 entry did not activate dsh-x: pending (waiting for service: y)`)。
- `parseFailedEntries`:解析缺失的插件/服务名(Format A 单行内联 + Format B 逐行 sweeper 报告,去重)。
---
## 4. 探测时序(pollUi / detectUi)
```
detectUi(url, timeoutMs, port=0)
├─ 选定调试端口(默认随机,冲突自动换端口重试 ≤3 次)
├─ launchSession → pollUi
│ pollUi: evaluate("window.location.href = ") → 导航到目标页
│ do { probeOnce(); 若 failed/ok 立即返回; sleep(500) }
│ while (耗时 < timeoutMs)
│ → 超时返回最后一次判定(或 error)
└─ finally close session
```
- **至少探测一次**(do-while):`--confirm-ms 0` 等 0ms 场景也要有一次真实 DOM 读取,否则被误判 error。
- `probeOnce` 复用同一浏览器会话(不重复起 Chrome),超时/异常统一走 session.close 清理。
---
## 5. 判据的维护要点
| 点 | 说明 |
|----|------|
| `FAILED_MARKER` | 红屏固定文本,跨构建稳定(DSH AppRoot.tsx 钉死)。若 DSH 改文案,这里是唯一需要同步的位置 |
| `