fix(web-access): 增加外部浏览器确定性前置探测 (#94)

## 中文

### 背景

L3-external 过去只用 curl 判断 9222,无法区分浏览器未安装、端口未开启、错误产品或伪 CDP
服务,也可能静默回落内置浏览器。

### 变更

- web-access 升级到 3.4.0,并要求先调用 BrowserExternalProbe
- 结构化处理未安装、端口关闭、多候选、产品不匹配、无效 CDP 与宿主不可用
- 显式浏览器不可自动替换;泛指 external 在多候选且无 ready 端口时先询问
- 所有 Playwright 配方使用 probe 返回端口和 DesireCore 隔离 Profile
- 纳入并修复 #93 的浏览器收尾规则与重复编号 finding

### 验证

- validate-i18n:通过
- translate --check:通过
- validate-i18n --online:通过
- source hash:sha256:1704b973e3a90e89
- 双语结构:31 个标题、20 个代码围栏

取代 #93;关联 desirecore/desirecore#2369。

## English

### Background

L3-external previously relied on curl against port 9222, which could not
distinguish a missing browser, a closed debug port, a different product,
or a fake CDP service, and could silently fall back to the built-in
browser.

### Changes

- Bump web-access to 3.4.0 and require BrowserExternalProbe before CDP
attach
- Handle missing installation, closed port, multiple candidates, product
mismatch, invalid CDP, and unavailable host as separate states
- Never replace an explicitly requested browser; ask when a generic
external request has multiple candidates and no ready endpoint
- Use the probe result port and a DesireCore-isolated profile in all
Playwright recipes
- Incorporate #93 browser wrap-up guidance and fix its duplicate
numbering review finding

### Validation

- i18n validation passed
- translation freshness check passed
- online validation passed
- source hash: sha256:1704b973e3a90e89
- bilingual structure: 31 headings and 20 code fences

Supersedes #93; related to desirecore/desirecore#2369.

---------

Co-authored-by: xieyuanxiang <124608760@qq.com>
This commit is contained in:
2026-08-29 14:10:46 +08:00
committed by GitHub
parent 2891f9dc06
commit 2ccd176dad
3 changed files with 219 additions and 141 deletions

View File

@@ -4,7 +4,7 @@
## L0一句话摘要
联网访问工具包——搜索公开页面、Jina 优化抓取、内置受管浏览器完成登录态访问与交互,以及用户点名时接管他自己的 Chrome。
联网访问工具包——搜索公开页面、Jina 优化抓取、内置受管浏览器完成登录态访问与交互,以及用户点名时接管他自己的 Chrome/Edge/Chromium
## L1概述与使用场景
@@ -16,17 +16,18 @@ web-access 是一个**流程型技能Procedural Skill**,提供四层互
- **L2**Jina ReaderJS 渲染的重页面,默认节省 Token
- **L3**内置受管浏览器v3.0 能力面补全):到达、操作并**读取**登录态/交互站点——每个任务独立 BrowserSpace 隔离、零 Python 依赖、每次动作都有可审计回执。批量取文(`page.extract-text`)、判别式等待(`page.wait`)、代码模式(`BrowserScript`)都在本层内闭环
- **L3-external**(用户自己的 Chrome经 CDP + Python Playwright 接管):**用户点名要用他自己那个浏览器时走这条**——他的登录态、他的窗口、他能全程看着并随时接管
- **L3-external**(用户点名的 Chrome/Edge/Chromium,经 CDP + Python Playwright 接管):**仅在用户点名,或 Agent 解释原因后用户明确同意时走这条**——DesireCore 隔离外部 Profile 中由用户手工登录形成的状态、可见窗口,以及用户随时接管的能力
关于 L3-external 的一段历史v3.0 曾把它整个删掉理由是「它存在的每一条技术理由无批量取文通道、evaluate 不可用、截图必须串行 activate都已被内置浏览器覆盖」。那个技术判断没错**作为「内置浏览器不够用时的兜底」它确实不再需要**。但删除时顺带丢掉了一个完全不同的用例:用户想用**他自己那个**浏览器。这跟能力够不够无关,内置浏览器替代不了,所以 v3.2 把它作为一条**由用户意图触发**的平级选择恢复回来——注意它不再是 fallback判据见下方「两个浏览器按用户意图选」。
### v3.0:内置受管浏览器(默认隐藏,激活后才暴露)
调用 `Skill('web-access')` 加载本技能时,以下 9 个工具被注入到当前会话,让 LLM 直接驱动内置浏览器
调用 `Skill('web-access')` 加载本技能时会注入以下工具。`Browser*` 工具驱动内置浏览器;`BrowserExternalProbe` 只检查外部浏览器前置条件
| 工具 | 用途 |
|------|------|
| BrowserManage | 建/销隔离 BrowserSpace、启动会话、管理标签页 |
| BrowserExternalProbe | 只读检查 Chrome/Edge/Chromium 安装与 loopback CDP 就绪状态;绝不启动浏览器或读取 Profile |
| BrowserSnapshot | `semantic` / `text` / `accessibility` / `visual` 四种快照——读页面的主通道 |
| BrowserAct | 一次调用一个受管动作:导航、输入、取文、等待、元素操作、截图…… |
| BrowserScript | **代码模式**:一段异步 JS 连续下发浏览器命令,消除逐动作往返(信任级别等同 Bash |
@@ -37,7 +38,7 @@ web-access 是一个**流程型技能Procedural Skill**,提供四层互
> **重要**:未调用 Skill('web-access') 之前,这些工具**不会**出现在 LLM 的 tools 列表里——默认对话不消耗其 token。详见 [references/browser-tools.md](references/browser-tools.md)。
>
> **v2.1 已移除**`BrowserListTabs` / `BrowserNavigate` / `BrowserEval` / `BrowserClick` / `BrowserScreenshot` / `BrowserScroll` / `BrowserSetFiles` / `BrowserCloseTab` 及其背后的 cdp-proxy 已停用,调用会返回「该旧 BrowserXxx/cdp-proxy 入口已停用」。本要求客户端 v10.0.98+`page.extract-text` / `page.element` / `page.wait` / 内联 wait 块 / 跨源 iframe 快照 v10.0.112+`BrowserScript` 需包含 S17/S18 的更新版本。
> **v2.1 已移除**`BrowserListTabs` / `BrowserNavigate` / `BrowserEval` / `BrowserClick` / `BrowserScreenshot` / `BrowserScroll` / `BrowserSetFiles` / `BrowserCloseTab` 及其背后的 cdp-proxy 已停用,调用会返回「该旧 BrowserXxx/cdp-proxy 入口已停用」。本技能版本要求客户端 v10.0.128+`page.extract-text` / `page.element` / `page.wait` / 内联 wait 块 / 跨源 iframe 快照 v10.0.112 起可用`BrowserScript` 需包含 S17/S18 的更新版本。
### 使用场景
@@ -52,7 +53,7 @@ web-access 是一个**流程型技能Procedural Skill**,提供四层互
- **分层递进**:从轻量搜索到重度 JS 渲染到登录态访问,按需选择;用户点名时还可直接用他自己的浏览器
- **Token 优化**Jina Reader 默认减少 50-80% Token 消耗;`page.extract-text` 的 maxBytes/cursor 分页让登录态长文也可控
- **登录态复用**Host 授予 `browser.import.*` 时用 BrowserImport 把 Cookie 导入隔离 Space不必重新登录
- **默认零外部依赖**:内置浏览器不要求 Python/Playwright也不要求用户手工启动调试 ChromeL3-external 需要,且仅在用户点名时才用)
- **默认零外部依赖**:内置浏览器不要求 Python/Playwright也不要求用户手工启动调试外部 Chromium 浏览器L3-external 需要,且仅在明确用户意图后才用)
## L2详细规范
@@ -64,50 +65,42 @@ When you complete a research task, you **MUST** cite all source URLs in your res
If any fetch fails, explicitly tell the user which URL failed and which fallback you used.
## Prerequisites: Chrome CDP Setup(仅 L3-external 需要)
## Prerequisites:外部浏览器 + CDP(仅 L3-external 需要)
**只有走 L3-external(用户点名要用他自己的浏览器)时才需要这一步** 内置浏览器零前置条件。
**只有走 L3-external 时才需要。** 内置浏览器零前置条件。
### One-time setup
### 每次连接前必须先结构化探测
用户带远程调试端口启动 Chrome
用户原话映射精确请求
**macOS**:
```bash
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir="${DESIRECORE_ROOT}/chrome-profile"
```
- 「我的 Chrome」→ `BrowserExternalProbe({ requestedBrowser: 'chrome' })`
- 「我的 Edge」→ `BrowserExternalProbe({ requestedBrowser: 'edge' })`
- 只说「我的外部/系统浏览器」、没点产品名 → `BrowserExternalProbe({ requestedBrowser: 'any' })`
- 「本地浏览器」语义两可 → 先问是内置浏览器还是用户自己的外部浏览器,再探测
**Linux**:
```bash
google-chrome \
--remote-debugging-port=9222 \
--user-data-dir="${DESIRECORE_ROOT}/chrome-profile"
```
严格按结构化状态处理:
**Windows (PowerShell)**:
```powershell
& "C:\Program Files\Google\Chrome\Application\chrome.exe" `
--remote-debugging-port=9222 `
--user-data-dir="$env:USERPROFILE\.desirecore\chrome-profile"
```
| status | 必须采取的动作 |
|---|---|
| `ready` | 才能继续 Playwright `connect_over_cdp`,并如实说出检测到的外部浏览器;`any` 已有 ready 端口时,即使还安装了其他产品,也以该端口作为用户已准备的选择 |
| `browser_not_installed` | 明确说未检测到用户点名的浏览器;若有 `alternatives`,询问是否改用其中之一,**绝不自动替换** |
| `debug_port_closed` | 展示返回的 `launchCommand`,请用户启动并手工登录,然后等待并重新 probe |
| `browser_choice_required` | 没有 ready 端口且检测到多个外部浏览器;只列 id/name 并询问用户选哪个,再 probe 精确选择 |
| `browser_mismatch` | 说明端口上实际是什么、用户点名的是什么;让用户修正端口或明确同意改用实际浏览器 |
| `invalid_cdp_endpoint` | 说明端口虽有服务但不是合法 Chrome DevTools 端点;不得连接 |
| `host_unavailable` | 说明当前 Agent Service 无法探测用户桌面宿主;不得猜已安装浏览器,也不得静默改用内置浏览器 |
启动后:
1. 用户在这个 Chrome 里手工登录需要的站点
2. 这个 Chrome 窗口保持开着
3. 验证调试端点:`curl -s http://localhost:9222/json/version` 应返回 JSON
`launchCommand` 使用 DesireCore 专属隔离 Profile。启动后:
### 每次操作前先验就绪
1. 用户在该外部浏览器里手工登录所需站点。
2. 外部浏览器窗口保持打开。
3. 再调用一次 `BrowserExternalProbe`;只有 `ready` 才允许尝试 CDP attach。
```bash
curl -s http://localhost:9222/json/version | python3 -c "import sys,json; d=json.load(sys.stdin); print('CDP ready:', d.get('Browser'))"
```
不得用 `curl` 替代本探测:连接拒绝无法区分「未安装浏览器」与「已安装但没开调试」,普通 HTTP 服务也不能冒充 CDP。
失败就告诉用户:「请先启动 Chrome 并开启远程调试端口(见 web-access 技能的 Prerequisites 部分)」,
**然后等他**——不要因为内置浏览器也能做就擅自改用内置的。
必须按上表的结构化状态提示用户并等待;不要因为内置浏览器也能做就擅自改用内置的。
⚠️ 用 CDP attach 时**绝不能调 `browser.close()`**,那会关掉用户自己的 Chrome;只关你开的 page。
⚠️ 用 CDP attach 时**绝不能调 `browser.close()`**,那会关掉用户自己的外部浏览器;只关你开的 page。
完整配方见 [references/cdp-browser.md](references/cdp-browser.md)。
---
@@ -143,8 +136,8 @@ User intent
└─ "Real-time interactive task" (click, fill form, scroll, screenshot)
├─→ **用户点名「我本机的 / 我自己的 / 外部的浏览器」** → L3-external
先验 CDP 就绪(见 Prerequisites再 python3 playwright.connect_over_cdp()
没就绪就给启动命令并等,不要擅自改用内置浏览器
BrowserExternalProbe精确点名的浏览器仅 `ready` 后 connect_over_cdp()
其余状态按表处理并等,不要擅自改用内置浏览器
└─→ **其余情况(默认)**:内置受管浏览器 (BrowserManage → BrowserAct → BrowserSnapshot —
see references/browser-tools.md, no Python needed)
```
@@ -155,10 +148,10 @@ DesireCore 能驱动**两个**浏览器,它们是平级的选项:
| | L3 内置受管浏览器 | L3-external 用户自己的浏览器 |
|---|---|---|
| 是什么 | 应用内的浏览器实例(`Browser*` 工具族) | 用户机器上装的 Chrome经 CDP + Python Playwright 接管 |
| 是什么 | 应用内的浏览器实例(`Browser*` 工具族) | 用户点名的 Chrome/Edge/Chromium,经 CDP + Python Playwright 接管 |
| 登录态 | 独立隔离;需 Host 授予 `browser.import.*` 才能用 `BrowserImport` 导 Cookie | **就是用户本人的登录态**,无需导入 |
| 用户能看到吗 | Agent 开的标签页默认离屏,需展示到工作台 | **就在用户自己的窗口里**,他能全程看着、随时接管 |
| 前置条件 | 无 | 用户需先带 `--remote-debugging-port=9222` 启动 Chrome见 Prerequisites |
| 前置条件 | 无 | `BrowserExternalProbe` 必须返回 `ready`;否则严格按结构化状态处理 |
| 默认 | ✅ 是 | 用户点名时 |
> 登录态那一栏容易读成「内置浏览器复用不了用户的登录态」——不是那个意思。准确说法是
@@ -169,13 +162,13 @@ DesireCore 能驱动**两个**浏览器,它们是平级的选项:
**选层判据是用户意图,不是技术难度。** v3.0 把这一层当作「内置浏览器不够用时的兜底」删掉过,
那个技术判断本身没错取文、evaluate、截图这些内置浏览器现在都能做但它顺带删掉的是一个
**完全不同的用例**:用户想用**他自己那个**浏览器。那跟能力够不够无关——他的登录态在他自己的
Chrome 里,他想亲眼看着操作、随时接管。这个需求内置浏览器替代不了。
**完全不同的用例**:用户想用**他自己那个**浏览器。那跟能力够不够无关——他在点名的外部浏览器中
建立登录态,想亲眼看着操作、随时接管。这个需求内置浏览器替代不了。
**用户点名了就按点名的来:**
- 说「我本机的 / 我自己的 / 外部浏览器 / 我的 Chrome」→ 走 **L3-external**。先
Prerequisites 验 CDP 就绪;没就绪就告诉他启动命令并等**不要因为「内置浏览器也能做」
- 说「我本机的 / 我自己的 / 外部浏览器 / 我的 Chrome」→ 走 **L3-external**。先 probe
用户精确点名的浏览器;没就绪就按结构化状态处理并等**不要因为「内置浏览器也能做」
就擅自改用内置的**
- 说「内置浏览器」或没点名 → 走 **L3 内置浏览器**(默认,零前置条件)
- 拿不准他指哪个 → 问一句,别猜
@@ -190,7 +183,7 @@ Chrome 里,他想亲眼看着操作、随时接管。这个需求内置浏览
| L1 | Public, static | `WebFetch` | Low |
| L2 | JS-heavy, long articles, token savings | `Bash curl r.jina.ai` | **Lowest** (Markdown pre-cleaned) |
| **L3** | **登录态导航、交互与取文 (PRIMARY)** | **内置受管浏览器BrowserManage / BrowserAct / BrowserSnapshot / BrowserScript** | Medium |
| L3-external | **用户点名要用他自己的浏览器**;或需要他本人的登录态而 `BrowserImport` 不可用 | `Bash + Python Playwright connect_over_cdp`(见 references/cdp-browser.md | Medium |
| L3-external | **用户点名要用他自己的浏览器**,或 Agent 解释原因后用户明确同意改走此路径 | `BrowserExternalProbe` `Bash + Python Playwright connect_over_cdp`(见 references/cdp-browser.md | Medium |
**Default priority**: L1 for simple public pages → L2 for heavy → **L3 for login-gated含正文与站内接口取数**
@@ -264,10 +257,36 @@ See [references/jina-reader.md](references/jina-reader.md) for advanced endpoint
4. `BrowserSnapshot(semantic)` 拿可交互元素 `ref`(跨源 iframe 的元素也在同一棵树里ref 全局连续编号)
5. 交互用 `input.*`(拟真轨迹)或 `page.element`(表单批量写);等结果用 `page.wait` 或内联 wait 块
6. 取正文用 `BrowserSnapshot(text)``BrowserAct(page.extract-text)`;取接口数据用 fetch.browser 配方
7. 任务收尾 `BrowserManage(close_session)`
7. 收尾:**先确认、再决定关不关**,见下方「收尾:汇报之前与关闭之前」
多动作连续编排(导航→快照→点击→等待→取文)可用 `BrowserScript` 一段脚本完成,省去逐动作 IPC 往返。
### 收尾:汇报之前与关闭之前
**一、向用户汇报浏览器结果之前,先用一次快照确认页面仍在。**
不要拿上一次成功的导航结果直接汇报。会话可能在那之后被系统终止——最常见的是页面持续占用
过多资源触发配额保护(`BROWSER_RESOURCE_QUOTA_EXCEEDED`),重 JS 站点很容易命中。真机发生过:
Agent 导航成功后直接汇报「✅ 已打开,页面已可在内置浏览器面板中查看」,而会话早已 crashed
用户看到的是一片空白。
汇报前补一次 `BrowserSnapshot``text``visual` 均可)即可暴露这类情况:会话若已终止,
工具会返回 `BROWSER_TOOL_SESSION_TERMINATED` 并说明原因。此时**如实告诉用户会话中断了**
再决定是重试、换更轻量的页面,还是交由用户处理——绝不能把先前的成功当作现状。
**二、不要无条件 `close_session`。判据是「用户还需不需要看」。**
| 任务性质 | 收尾 |
|---|---|
| 演示 / 交互 / 用户要看结果 | **保留会话**,并告诉用户页面停在哪、可以直接接管 |
| 纯数据抓取,正文已取回 | 关闭,释放资源 |
| 用户明确说「用完关掉」 | 关闭 |
内置浏览器的画面要展示给用户,靠的是工作台把会话呈现出来;会话一关,用户就什么都看不到了。
真机发生过Agent 用两分钟做完演示随即 `close_session`,用户回头去看只剩空白面板——它做的事
没错,只是没留给用户任何查看的机会。拿不准时**保留**:留着最多占一点资源,关早了用户就得
从头再来一遍。
## L3 速查v3.0
### 读页面:按需求选通道
@@ -336,8 +355,9 @@ BrowserAct:
← 正文直接读出;太长就传上一页返回的 nextCursor 续读
7. 需要确认渲染效果 → BrowserSnapshot({ mode: 'visual' })(像素直接可看)
8. SitePatternRead({ domain: 'xiaohongshu.com' }) ← 读累积经验
9. 任务结束 → BrowserManage({ action: 'close_session', sessionId })
10. 如发现新陷阱 → SitePatternWrite({ domain, scope: 'agent', mode: 'merge', content })
9. 汇报前 → 再来一次 BrowserSnapshot 确认页面仍在(会话可能已被配额终止)
10. 任务结束 → 用户还要看就**保留会话**并告知页面位置;纯抓取才 close_session
11. 如发现新陷阱 → SitePatternWrite({ domain, scope: 'agent', mode: 'merge', content })
```
## 站点经验积累