dsh-web-fetch-playwright
v0.2.7
Published
Playwright/CDP web-fetch provider for DeepSeek Harness: renders pages in a real browser, denoises them (Readability + DOMPurify), and returns markdown.
Downloads
2,528
Maintainers
Readme
dsh-web-fetch-playwright
DeepSeek Harness(DSH)双端插件:为内置 web_fetch 工具提供 Playwright/CDP 后端——用真实浏览器渲染网页,经 Readability + DOMPurify + Turndown + GFM 降噪(清洗导航栏、侧边栏、页脚、贴片广告)后输出 Markdown。
特性
- 真实浏览器渲染 —— 以用户视角加载页面,SPA 客户端渲染内容也能抓到,而非只有原始 HTML。
- 降噪管线 —— Mozilla Readability 提取正文,DOMPurify 移除布局/噪音标签(导航、侧边栏、页脚、广告、表单),Turndown + GFM 插件按与内置
tool-web渲染器一致的风格转成 Markdown。内联data:图片(Docusaurus 等构建工具会把截图以 base64 内嵌进 HTML)会被替换为带大小的占位符,如,避免 base64 字符流刷屏。 - 两种后端 —— 本地启动 Playwright 浏览器,或通过 DevTools 协议(CDP)驱动一个已在运行的浏览器。
- 浏览器解析 —— 配置路径 →
$PATH上的playwrightCLI → 插件自带的playwright-core;CDP 模式完全不需要本地浏览器。 - 共享或隔离会话(CDP) —— 每次抓取严格限定为一个标签页。本地后端每次抓取启动并关闭自己的浏览器;CDP 后端对远端浏览器保持一条共享连接,每次抓取只在其里开一个标签页、用完即关。默认该标签页位于远端浏览器的真实 profile(沿用其 cookie、localStorage 与已登录会话,效果类似
playwright-cli open);取消勾选「共享浏览器上下文」则切换为每次抓取全新隔离 context。 - 热配置 —— 「设置 → 插件 → 插件配置」卡片可随时切换后端、上下文模式、降噪开关与并发数,改动对下一次抓取即时生效,无需重启。
- 预算控制 —— 单次抓取 45s 超时;并发按后端定价(
maxConcurrency,默认本地 4 个浏览器 / CDP 50 个标签页;排队的抓取等不到空位会在 20s 内尽快报错并提示重试,而不是一直挂到被工具层中止);拦截图片/字体/媒体子请求;返回体 10 万字符封顶。 - Cloudflare 挑战有界等待 —— 导航落到验证中间页("Just a moment…" 及其多语言同族,通过官方
cf-mitigated: challenge响应头 + 结构性页面标记识别)时,抓取保持同一标签页与上下文,等待浏览器自行通过验证:跟踪最后一次主 frame 响应(真实页面随后重载进来),并轮询活 DOM 以捕获 SPA 式清除。有界且可配置(challengeWaitMs,默认 15s;0恢复旧版首响应行为),附带同标签页有界重试(challengeRetries,默认 1)。预算耗尽时以独立的WEB_FETCH_CHALLENGE错误码明确失败,而不是把中间页当正文返回。全程不点击、不注入验证码答案、不伪造浏览器状态、不导出或复制 cookie。
工作原理
| 半端 | 位置 | 职责 |
| --- | --- | --- |
| 宿主(服务端) | src/ | 向 ctx.web 注册 fetch provider(id playwright);cordis.patch.yml 把 web seam 的 fetchProvider 固定为本插件,并启用 web_fetch 工具(60s 预算)。 |
| 浏览器(客户端) | src/client/ | 注册 Playwright 网页爬取 配置卡片,通过 settings 服务把改动热写入 $DSH_HOME/settings.yaml。 |
web_fetch (tool-web)
└─ ctx.web.fetchProvider = playwright
├─ local: 解析(路径 → $PATH → 内置 playwright-core)→ chromium.launch
├─ cdp: connectOverCDP(endpoint)
├─ page.goto → 等待稳定(networkidle,尽力而为)→ page.content()
├─ 降噪:jsdom → 内联 data: 图片改占位符 → Readability → DOMPurify → Turndown(GFM)
└─ Markdown(关闭降噪时返回原始 HTML)环境要求
- DSH web profile(
dsh web),Node.js ≥ 20。 - 本地后端:装有 Chromium 的 Playwright、Chromium 系浏览器可执行文件,或默认缓存里有浏览器的
playwright-core。 - CDP 后端:任意已带
--remote-debugging-port启动的浏览器(如chromium --headless --remote-debugging-port=9222)。
安装
从 npm registry 安装(预构建产物,无需构建授权):
dsh plugin --profile web add dsh-web-fetch-playwright从 GitHub 仓库安装(源码型,pnpm 会在安装时跑 prepare 构建;若 pnpm 拦截构建脚本,请在 profiles/web/pnpm-workspace.yaml 中放行该包):
dsh plugin --profile web add github:chendefine/dsh-web-fetch-playwright或通过 DSH 插件市场(设置 → DSH插件市场)一键安装——本仓库带 dsh-plugin topic,会被自动收录。
bundle 插件加入 profile 层栈后需重启 dsh web 生效;卸载用 dsh plugin --profile web remove dsh-web-fetch-playwright 后重启。
配置项
设置卡片(设置 → 插件 → 插件配置 → Playwright 网页爬取)实时编辑 web-fetch-playwright 设置段:

| 字段 | 默认 | 说明 |
| --- | --- | --- |
| backend | local | radio:本地 Playwright / 远端 CDP 地址,每个选项内嵌各自的填空。 |
| playwrightPath | 空 | 本地后端:playwright 可执行文件或 Chromium 系浏览器二进制路径;留空按 $PATH 查找,再回退到内置 playwright-core。 |
| cdpEndpoint | 127.0.0.1:9222 | 远端后端:host:port、http(s)://… 或 ws(s)://…。 |
| shareBrowserContext | true | 仅 CDP 后端。勾选(profile 模式):每次抓取是远端浏览器默认 context(真实 profile)里的一个标签页,cookie/localStorage 与之互通、已登录会话直接生效,抓取结束只关标签页;取消勾选(隔离模式):每次抓取使用全新隐身式 context,互不共享。本地后端忽略此字段。 |
| denoise | true | 是否启用降噪;关闭时返回整页渲染 HTML,交由工具层转换。 |
| maxConcurrency | (自动) | 同时渲染的页面上限(1–200)。留空按后端取默认:本地 4(每个槽位启动一个浏览器)/ CDP 50(远端浏览器已就位,每个槽位只是一个标签页)。超出的请求短暂排队;20s 内等不到空位则以 WEB_FETCH_TIMEOUT 尽快失败并提示重试或调大该值,而不是一直挂起直到工具层预算中止。 |
| challengeWaitMs | 15000 | Cloudflare 挑战的有界自然等待上限(毫秒,0–60000),在同一标签页内等待浏览器自行通过验证。0 关闭整条挑战处理链路——直接返回首次响应(0.2.5 之前的旧行为)。 |
| challengeRetries | 1 | 一个等待窗口耗尽后的同标签页重新导航次数(0–3);浏览器已拿到的通关 cookie 留在上下文里供重试使用。总耗时始终受 45s 单次抓取预算约束。 |
本地后端解析顺序:
- 配置的路径(自动判别 Playwright CLI / 浏览器二进制);
$PATH上的playwright(其包自带该安装的浏览器注册表);- 插件内置的
playwright-core——需要PLAYWRIGHT_BROWSERS_PATH或默认缓存里有浏览器,否则报错会提示playwright install chromium。
Windows 说明 ——
$PATH已按平台分隔符(;)扫描,但 npm/pnpm 全局安装暴露的playwright是.cmd/.ps1垫片,从垫片位置向上找不到包根,自动发现可能仍落在第 3 步(内置 core)。要使用指定安装的浏览器注册表,请把playwrightPath显式指向playwright包目录或浏览器二进制。
CDP 模式不需要本地浏览器:插件在生命周期内对远端浏览器保持一条共享连接(连接断开自动重连,地址改动后自动换连),每次抓取只租用远端浏览器里的一个标签页,抓取结束即关闭。因此并发数按"标签页"计,默认也更高(50)。插件卸载时断开共享连接(绝不会关闭远端浏览器本身)。
CDP 上下文模式(是否共享浏览器 profile)
「共享浏览器上下文」勾选(默认,profile 模式)时,每次抓取是远端浏览器默认 context——真实 profile——里的一个标签页:cookie 与 localStorage 双向互通,浏览器里已登录的站点会以登录态被抓取,和你手动开标签页一样。共享 context 永不关闭;资源过滤与弹窗守卫只挂在本次抓取自己的标签页上,不会干扰你人工打开的其他标签页。取消勾选(隔离模式)时,每次抓取使用全新隐身式 context——匿名读取,什么都不保留。
profile 模式风险须知 —— 它把 web_fetch 从"匿名读网页"升级为"以浏览器登录身份行动":
- 被抓取的恶意页面若诱导 agent 请求 GET 型状态变更 URL(登出、改设置、API 操作),请求会自动携带会话 cookie。
- 同一站点的并发抓取共享一个 cookie jar,一方的登出 /
Set-Cookie会影响另一方。 - 输出开始依赖浏览器历史(A/B 分桶、语言偏好);远端 profile 的站点数据只增不减,插件不做清理。
登录态持久的前提是 user-data-dir 持久化。有头(推荐,手动登录一次):
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.config/chrome-dsh-profile"无头服务器(先在有头环境预置登录态):chromium --headless=new --remote-debugging-port=9222 --user-data-dir=/data/chrome-dsh-profile。不要叠加 --incognito 或一次性 user-data-dir——都会让 profile 模式失效。设计依据与已核实的 playwright-core 源码事实见 docs/context-mode-profile.md。
Cloudflare 挑战处理(有界自然等待)
部分严格站点会在返回真实页面前先给一个 Cloudflare 验证中间页。真实浏览器通常几秒内就能自行通过验证;但只看第一次响应的抓取会把中间页当成正文返回(0.2.5 之前的旧行为;把 challengeWaitMs 设为 0 可随时复现,或在仓库检出、执行 pnpm build 后运行 node scripts/challenge-demo.mjs 看本地模拟站点的前后对比、node scripts/challenge-online.mjs <url> 对真实站点做在线对比)。
开启等待(默认)后的流程:
- 识别 —— 响应带
cf-mitigated: challenge(Cloudflare 官方文档注明所有挑战页类型都带此头),或 403/503 且server: cloudflare的 HTML 文档,或本地化的中间页本身("Just a moment…" / "请稍候…" / "Минутку…" 等 title 家族,以及结构性标记:/cdn-cgi/challenge-platform/脚本、#challenge-*元素、cf-chl-widget-框架、window._cf_chl_opt)。内容级标记只是兜底层,且仅对"挑战兼容"的响应(403/429/503 或来自 Cloudflare 边缘——server: cloudflare/cf-ray)运行——因为中间页从不会以普通 200 返回,所以正文里引用了挑战文案的普通文章绝不可能被误判。Cloudflare Bot Management 的被动 JavaScript-Detections 遥测(/cdn-cgi/challenge-platform/scripts/jsd/,会注入受保护站点的每一个正常页面——如 openrouter.ai)在前缀扫描前被显式中和,因此带真实内容的 200 正常页绝不会被误判。硬封锁页("Sorry, you have been blocked")单独分类并立即失败——等待无法解除。 - 同标签页、同上下文的有界等待 —— 每 500ms 轮询活 DOM,等浏览器跑完自己的验证;同时跟踪最后一次主 frame 导航响应,所以重载进来的真实文档的状态码和响应头才是最终上报的。SPA 式清除(无导航、纯内容替换)由同一个 DOM 探测捕获。
- 有界重试 —— 窗口耗尽后,同一标签页默认再导航一次(
challengeRetries),上下文里已有的通关 cookie 继续生效。 - 明确失败 —— 返回独立的
WEB_FETCH_CHALLENGE错误码(web seam 的code是开放字符串,允许 provider 专属码),消息中写明站点、等待预算与最后一次挑战响应的状态。
安全边界(刻意为之):不点击 Turnstile、不解验证码、不注入 token、不伪装指纹/UA、不轮换代理、不导出 cookie——隔离模式下本次抓取挣到的通关态随其 context 一起销毁;profile 模式下它留在远端浏览器自己的 profile 里,插件从不复制或清理。等待始终受 challengeWaitMs 与 45s 单次抓取预算双重约束,永不无限阻塞。
开发
pnpm install
pnpm typecheck # tsc --noEmit
pnpm test # vitest run(无浏览器时浏览器集成用例自动跳过)
pnpm build # tsc 声明 + tsdown(宿主 ESM + 客户端 module-registration bundle)仓库结构:
src/
├── index.ts # 宿主入口:注册 provider 与设置段
├── config.ts # schemastery schema、CDP 端点归一化
├── provider.ts # WebFetchProvider:导航、超时、信号量、截断
├── markdown.ts # 降噪管线(Readability + DOMPurify + Turndown/GFM)
├── playwright-resolve.ts # 本地后端发现(路径 / $PATH / 内置 core)
├── types.ts # Playwright 结构化类型(运行时模块动态发现)
└── client/ # 浏览器半端:设置卡片、表单模型、多语言
tests/ # 单元 + provider + 浏览器集成(可自跳过)开发与发布流程见 CONTRIBUTING.md,安全模型与漏洞报告见 SECURITY.md。
安全边界
与内置 HTTP provider 同立场:未实现 SSRF/私网防护——浏览器能访问的目标,本 provider 就能抓。CDP 地址由设置页配置,不做回环限制,请在可信环境暴露设置页。抓取仅在本地渲染,除目标页面自身外不会向任何地方发送数据——但 profile 模式下请求(以及恶意页面诱导 agent 触发的状态变更)会携带远端浏览器的登录会话,见上文风险须知。
许可证
MIT © 2026 chendefine
