DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-local-browser-web

x

dsh-local-browser-web

Manifest valid
UI (client)

dsh-local-browser-web

一个 DeepSeek Harness(DSH)插件:用本机浏览器(CDP,可选 Playwright)为 ctx.web 能力接缝提供 搜索web_search 与 抓取web_fetch provider,并补齐内置远程 API 之外的可扩展引擎注册表、四阶段清洗管线、质量过滤层与图形化设置页。

  • 搜索 provider id:local-browser-search
  • 抓取 provider id:local-browser-fetch
  • profile 条目 id(也是设置命名空间):local-browser-web
  • 包名:dsh-local-browser-web(插件的浏览器半侧挂在根行上,见下文「挂载」)

前置依赖:本机 Chrome

本插件不自带浏览器,也不会替你下载 Chrome。 browser 类引擎(百度、Google、百度桌面端)与 cdp 抓取后端都需要一台监听了调试端口的 Chrome。没有它时:搜索会回退到 HTTP 引擎(Bing / DuckDuckGo / 360 仍可用),抓取会直接报 BROWSER_UNAVAILABLE。

推荐做法:设置 → 本地浏览器 Web → 抓取 → 「检测并启动」。它会先探测 cdpEndpoint;未连接时自动找到本机 Chrome,用插件自有的数据目录启动,并等到端口真正响应后才给出结果——不需要打开终端。

等价的命令行(macOS):

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  --remote-debugging-port=9222 \
  --user-data-dir="$HOME/.dsh/local-browser-web/chrome-profile"
  • Chrome 136+ 必须给独立数据目录:在默认数据目录上加 --remote-debugging-port 会被静默忽略,端口根本不开。这也是「检测并启动」不复用你日常 profile 的原因。
  • Windows:chrome.exe --remote-debugging-port=9222 --user-data-dir=%LOCALAPPDATA%\dsh\local-browser-web\chrome-profile
  • Linux:google-chrome --remote-debugging-port=9222 --user-data-dir=~/.dsh/local-browser-web/chrome-profile
  • 这个实例可以和日常 Chrome 同时运行;登录态只存在该目录里。在里面登录一次 Google / 百度,能显著降低 browser 类引擎遇到验证码的概率。
  • 想让它随终端自动后台启动,可以把上面的命令写进 ~/.zshrc(或用 LaunchAgent)。端口已经监听时,插件只会报告「已连接」,不会重复启动。
  • 自动检测只覆盖常见安装位置。Chrome 装在别处(或用的是 Chromium)时,在设置页填「Chrome 可执行文件」即可。

目录


能力总览

层做什么
搜索 Providerctx.web.registerSearchProvider 注册 local-browser-search。执行链:引擎注册表 → bail/parallel 调度 → 可注册解析策略 → 质量排序。
引擎注册表Bing、DuckDuckGo、百度、百度桌面端、Google、360 都只是默认条目的数据,不是代码路径。同一张表里可增删启停、调顺序、改 endpoint、换解析策略。
解析策略注册表内置 html-bing / html-ddg / html-baidu / html-google / html-so360 / json-generic;第三方可通过公开 service 注册更多,零改本包源码。
抓取 Providerctx.web.registerFetchProvider 注册 local-browser-fetch。执行链:四阶段清洗管线。
抓取后端注册表cdp(默认,连本机 Chrome)/ playwright(可选)/ http(纯传输);可注册新后端。
质量过滤来源分层(政府/学术/官方项目/媒体/参考/社区/聚合站)+ 一手性 + 时效 + 伪装检测 + 薄内容 / SEO 垃圾 / 低质 AIGC / 域名信誉,逐条可开关、阈值可调、总有总开关。
来源标注与冲突并排把来源性质、一手/二手、发布日期、与其他来源的数值分歧写进结果 summary,模型直接可见。
评分器(可选)规则排序之后、交给模型之前的可插拔一层:内置 rerank-http(cross-encoder 重排,60/40 与质量分混合);evidence 类型专门留给本地 7B 做结构化证据抽取。默认关闭,失败自动退回原排序,且永不丢结果。
设置页挂在 settings.section 槽位的独立导航页,覆盖全部可调项;保存经 form.mutate 写入 profile 条目 config 并由 volatile HMR 热更新。
Agent 工具local_search_test:真实测试每个引擎并报告失败阶段;也可只读最近的操作诊断。

内置引擎

新装默认带六条引擎条目,全部只是 engines 表里的数据(界面里可增删启停):

id显示名kind端点解析策略默认说明
bingBinghttp-htmlhttps://www.bing.com/searchhtml-bing启用服务端渲染,无需浏览器。
duckduckgoDuckDuckGohttp-htmlhttps://html.duckduckgo.com/html/?q={query}html-ddg启用走纯 HTML 端点:主站是 React 应用,代理下脚本包加载不全时整页渲染成空白(实测 body 文本长度为 0),而 html/ 端点把同样十条结果服务端渲染好,不需要浏览器。
baidu百度搜索browserhttps://m.baidu.com/s?word={query}html-baidu启用百度对纯 HTTP 请求直接返回「百度安全验证」,所以走浏览器;默认用移动端 SERP,因为桌面端 /s 对陌⽣网络与新建 profile 仍会拦截。
baidu-pc百度搜索(桌面端)browserhttps://www.baidu.com/s?wd={query}&ie=utf-8html-baidu停用规范桌面 SERP;当浏览器 profile 已被百度信任(有 cookie / 可信网络)时,在设置页把它打开即可。
googleGoogle 搜索browserhttps://www.google.com/search?q={query}&num=10html-google启用需要能直连 Google 的网络,或在设置页「代理」里配一个能出网的代理(见下文);结果链接是加密的 /goto?url= 跳转链,由引擎的 unwrapRedirect 换回真实地址。
so360360 搜索http-htmlhttps://www.so.com/s?q={query}html-so360启用服务端渲染的完整 SERP,走最便宜的 HTTP 路径,不依赖浏览器/CDP。

三个新解析策略各自的要点:

  • html-baidu — 同时认桌面端(#content_left 里的 .result / .c-container,真实地址在 mu 属性)与移动端(div.c-result,真实地址在 data-log JSON 的 mu / logCommonInfo.exposureInfo.url)两种 DOM;命中「百度安全验证 / wappass」时抛 WEB_FETCH_CHALLENGE 而不是返回 0 条,bail 调度据此继续回退。
  • html-google — 只取 #rso 下的 div.MjjYud(避免 div.g 嵌套导致同一条结果被读两次),标题锚点取 h3 所在链接,/url?q= 包装会被解包,Google 自家页面(cache / preferences 等)与 [data-text-ad] 广告块被丢弃。
  • html-so360 — ul.result > li.res-list,真实地址优先 data-mdurl;丢弃 ec_ad 推广块,以及指回 360 自身 SERP 的「大家都在搜」建议链接。

SERP 正文上限与文档上限是分开的:搜索引擎页面会在结果列表之前塞进几百 KB 的内联 CSS 与启动 JSON(360 的首条结果在 330 KB 左右),所以搜索路径把正文上限抬到 SEARCH_BODY_CHARS = 2 000 000,不受 maxBodyChars(默认 100 000,用于抓取文档)截断——否则页面明明有结果也会被判成 0 条。

就绪条件:解析策略自己声明「页面什么时候算渲染好」

loadEventFired 并不可信。Google 的首个文档是个空壳(body 文本只有 17 个字符),结果列表在 2.5 秒后才由脚本注入 #rso;代理线路慢一点,事件与列表之间能差好几秒。谁在事件触发时立刻读 DOM,谁就拿到 0 条。

所以「等什么」由解析策略声明,而不是写进某个执行分支:

export const htmlGoogleParser: ResultParser = (input) => { /* … */ }
htmlGoogleParser.waitFor = ['#rso']        // 任一选择器命中即开工
  • 渲染后端(CDP / Playwright)在 loadEventFired 之后等这些选择器,最多 8 秒,超时也照读(慢页面不该被丢掉);
  • loadEventFired 自身也有 8 秒软上限:代理下某个第三方资源挂住会让 readyState 永远是 loading,等满整个 60 秒预算等于白扔一个已经能读的页面;
  • browser 类引擎若声明了就绪条件却解析出 0 条,会自动重渲染一次(脚本包掉链子、注入抢跑都靠这次兜回来);没有声明条件的引擎不重试,空就是空。

跳转解包:/goto?url= 这类跳转链

Google 不再在 DOM 里给出目标地址,#rso 里的链接全是 /goto?url=<加密 token>(旧式的 /url?q= 已经见不到),token 只有 Google 服务端能解。唯一的出路是问一次跳转器再读它 302 的 Location。

这同样是引擎条目上的一个数据字段,靠它声明自己的跳转链长什么样:

- id: google
  unwrapRedirect: '^https?://[^/]*\.google\.[a-z.]+/(?:goto|url)\?'

命中的结果会各发一次 HEAD(不下载目标页),并发 6 条,失败就保留原跳转链——可点的跳转链仍然能用,比丢掉这条结果划算。实测一条 /goto?url= 只要一次 302 就能拿到 Location: https://github.com/deepseek-ai/deepseek-harness,10 条结果总共多花约 1 秒。


出网代理

插件有两条独立的出网通道,代理必须能同时覆盖它们才有意义 —— 需要代理才能访问 Google 的人,同样需要它给 http-html 引擎和浏览器渲染一起用:

通道覆盖对象实现
HTTPhttp-html / api 类引擎(Bing、360 …)与 http 抓取后端直接与代理对话:明文目标走 absolute-form 请求,TLS 目标走 CONNECT 隧道,socks5:// 走 SOCKS5 握手
浏览器browser 类引擎(百度、Google …)与 CDP / Playwright 抓取把 proxyServer / proxyBypassList 交给浏览器上下文(Target.createBrowserContext)

设置页「代理」页签对应的配置:

字段默认含义
proxy.enabledfalse总开关。关闭时两条通道都直连。
proxy.url空代理地址。接受 http://127.0.0.1:7890、https://…、socks5://127.0.0.1:7891,也接受省略协议的 127.0.0.1:7890(按 HTTP 处理,这是代理客户端最常见的展示形式);HTTP 代理支持 http://user:pass@host:port。
proxy.bypasslocalhost,127.0.0.1,::1直连名单,逗号/空格分隔。example.com 覆盖其子域,* 表示全部直连。CDP 端点就在本机,所以回环默认在名单里。国内引擎请写进这里(见下)。
proxy.httptrueHTTP 通道是否走代理。
proxy.browsertrue浏览器渲染是否走代理。

要点:

  • 改完即生效,下一次搜索/抓取就读到新值;不需要重载插件(只有 Provider 切换需要重载)。
  • 保存即校验:地址无法解析、协议不支持(ftp:// 等)、端口非法都会让 Host 拒绝保存。
  • 直连名单按目标判定:命中的目标连代理都不连(proxyFetch 在发请求前先查名单),而不是「连了代理再说」。
  • 重定向仍走同一出口:httpGet 的重定向循环把每个 hop 都交给同一个 send,所以代理下的 3xx 不会掉回直连。实测 360 的验证页跳转(qcaptcha.so.com)在代理日志里也是经代理取回的。

国内站点写进直连名单(实测关键)

代理多半是为了出海(Google / DuckDuckGo),但同一个代理会把国内引擎也带出境,而百度、360 对境外 IP 的处置是直接回人机验证页(HTTP 200 + wappass / qcaptcha),看起来就是「引擎坏了」。实测对照:

配置bingbaidubaidu-pcso360
全部走代理请求失败人机验证人机验证时通时拦
国内域名直连10 条 / 0.5s11 条10 条6 条

所以建议这样填:

proxy:
  enabled: true
  url: 192.168.3.74:7897
  bypass: localhost,127.0.0.1,::1,baidu.com,so.com,bing.com,qq.com,zhihu.com,csdn.net,163.com

直连名单还会改变浏览器的上下文策略:命中名单的目标不建代理上下文,因此仍使用 shared profile(保留登录态与 cookie)。否则一份"代理开着但目标直连"的配置会白白丢掉登录态。

浏览器侧的两个前提

  1. 代理按上下文生效,因此开了浏览器代理就一定会新建上下文,即使是 shared 模式 —— 那个上下文里没有你默认 profile 的登录态。需要「代理 + 登录态」时,二者目前无法同时满足(Chrome 没有浏览器级、可运行时修改的代理开关)。
  2. 浏览器侧不使用地址里的用户名密码:Target.createBrowserContext 的 proxyServer 只接受 scheme://host:port,带 userinfo 会被忽略而不是认证。需要认证的代理请用 HTTP 通道,或在本机起一个免认证的中转。

自测代理是否真的生效

仓库外带一个最小转发代理(只做 absolute-form 与 CONNECT,逐条打印),用来「从代理那一侧」确认流量确实改道:

node E:/dsh-plugins/local-forward-proxy.mjs 7899     # 或任意端口
DSH_PROXY_URL=http://127.0.0.1:7899 node E:/dsh-plugins/verify-engines.mjs "deepseek"

代理日志里应出现 CONNECT www.bing.com:443、CONNECT m.baidu.com:443(浏览器渲染)、以及明文目标的 REQ GET http://… —— 三条分别对应 HTTP 通道、浏览器通道与重定向 hop。


安装

研发期提示:本仓库在开发期间不会把插件安装进正在运行的 DSH。~/.dsh/profiles/web 及 cordis.patch.yml 保持干净;只有在研发完成并得到许可后,才执行下面的安装步骤。

1. 准备依赖链接

插件通过 symlink 从 DSH 检出与已安装 profile 借用 @deepseek-ai/*、react、jsdom、turndown 与构建工具(不复制、不写入 DSH):

node scripts/setup-links.mjs

2. 构建

node node_modules/typescript/bin/tsc -p tsconfig.build.json   # 宿主半侧 → lib/
node node_modules/tsdown/dist/run.mjs                         # 浏览器半侧 → lib/client.js

(等价于 pnpm build;需要 Node 22,见「开发:构建与测试」。)

3. 让 DSH 能解析该包

DSH 的 loader 以 profile 目录为解析根(ctx.baseUrl),因此把本插件挂进 profile 的 node_modules 即可:

ln -sfn /Users/wangshuai/traeWorkSpace/dsh_plugin \
  ~/.dsh/profiles/<profile>/node_modules/dsh-local-browser-web

为什么不用 dsh plugin add:官方入口 dsh plugin --profile <name> <pnpm-args> 会把参数转发给 profile 目录里的 pnpm。如果该 profile 的 node_modules 是由不同大版本的 pnpm 安装的(本机 profile 来自 pnpm v10 store,而当前 pnpm 想要 v11 store),它会以 ERR_PNPM_UNEXPECTED_STORE 直接失败。不要为装插件而重装 profile——那会动到正在运行的 DSH;上面的 symlink 与 pnpm 的 link: 结果等价。

注意:日后在该 profile 里执行任何 pnpm add/remove 会把这个「多余」的 symlink 清掉,重跑上面的 ln 即可。

4. 挂载(用一次性 profile 试跑,勿先写现网)

dsh web --profile local-browser-web-dev --patch <本目录>/cordis.patch.yml

cordis.patch.yml 做两件事:insert 本插件行,并把 web 行固定到本地 provider(见下一节「手动编辑」)。

5. 启动本机 Chrome 的调试端口

优先用界面:设置 → 本地浏览器 Web → 抓取 → 「检测并启动」。手动启动(macOS):

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  --remote-debugging-port=9222 \
  --user-data-dir="$HOME/.dsh/local-browser-web/chrome-profile"

Chrome 136+ 必须带独立 --user-data-dir,否则端口不会开——原因与各平台命令见文首「前置依赖:本机 Chrome」。

Windows 安装补充

scripts/setup-links.mjs 里的检出 / profile 路径是作者 macOS 上的绝对路径。Windows 上按下面三处等价替换即可(本机实际安装即按此完成):

  1. 依赖链接:把 REPO 指向本机 DSH 检出、PROFILE 指向 ~/.dsh/profiles,并用 junction(无需管理员权限)代替 symlink —— Node 的模块解析会穿透 junction,等价于 pnpm 的 link:。脚本见 E:\dsh-plugins\setup-links.win.mjs。
  2. 构建:与 macOS 相同,tsc -p tsconfig.build.json 出宿主半侧,tsdown(经 node node_modules/tsdown/dist/run.mjs)出浏览器半侧。
  3. 挂进 profile:把插件目录 junction 到 <DSH_HOME>\profiles\<profile>\node_modules\dsh-local-browser-web,再把 cordis.patch.yml 的两段(insert 与 id: web)追加到该 profile 的补丁层。

调试端口在 Windows 上同样要独立 --user-data-dir(Chrome 136 起默认数据目录不再接受 --remote-debugging-port),登录态只存在那个目录里:

chrome.exe --remote-debugging-port=9222 --user-data-dir=E:\dsh-plugins\.chrome-cdp

GUI 入口与界面操作

设置页挂在官方 settings.section 槽位,因此它在 DSH 设置里是独立导航页,而不是「设置 → 插件」分区里的一个标签页:

设置(侧栏齿轮 / 设置快捷键)
└── 左侧导航:本地浏览器 Web        ← 本插件设置页
    ├── 搜索   (搜索设置 + 引擎注册表编辑器 + 测试方式说明)
    ├── 抓取   (Chrome 连接卡片 + 抓取设置)
    ├── 代理   (代理开关与直连名单)
    ├── 清洗   (四阶段开关)
    ├── 质量过滤(规则开关 / 阈值 / 域名列表)
    └── 通用   (日志 + 配置导出/导入)

页面内所有控件都是暂存式:改动先进入草稿,点「保存」才作为一次原子写提交。保存成功后草稿清空并从 Host 回读。

例外是抓取页顶部的 Chrome 连接卡片:它不是配置项,点「检测」「检测并启动」会立即发一次请求并刷新状态,不需要保存。

想做的事操作位置
调调度模式 / 并发上限 / 超时搜索 → 搜索设置
检测 / 启动监听 9222 的 Chrome抓取 → Chrome 调试端口 → 「检测」/「检测并启动」
增删改引擎、调顺序、启停搜索 → 搜索引擎注册表
为引擎录入 API key搜索引擎注册表 → 该引擎行 → 「引擎凭据」
测试某个引擎搜索引擎注册表 → 该引擎行 → 「测试连接」(见下)
选抓取后端 / CDP 端点 / 无头 / profile抓取 → 抓取设置
逐项开关四阶段清洗清洗
调质量阈值 / 改域名黑白名单 / 一键旁路质量过滤
日志级别 / 导出导入配置通用

Chrome 连接卡片

抓取页顶部会显示当前 CDP 端点的实时状态,并提供两个按钮:

按钮行为
检测只请求 GET <cdpEndpoint>/json/version,不启动任何进程。已连接时显示浏览器版本;未连接时显示失败原因。
检测并启动先检测;未连接则按「Chrome 可执行文件」→ 自动检测顺序找到 Chrome,用「Chrome 数据目录」(留空则为插件自有目录)以 --remote-debugging-port 启动,并轮询直到端口响应或超出「启动等待预算」。

失败时会直接给出可操作的信息:找不到 Chrome 时列出已经尝试过的所有路径;启动了但端口没响应时提示「同一数据目录可能已有 Chrome 在运行,请先完全退出它」。

为什么面板能启动 Chrome、却不能真实测试引擎:DSH 的 ctx.remote 端点需要 typert 代码生成管线,独立仓库的插件无法接入;但 Host 可以按官方方式在 webServer 上注册具名路由。Chrome 控制就走这条路由(POST /local-browser-web/browser,先经 connection 服务的 Host/Origin 与登录令牌校验),而引擎测试涉及结果解析,仍然交给 agent 工具,见下。

引擎「测试连接」怎么用

设置面板运行在浏览器里。除上面那条 Chrome 控制路由外,它没有通用的 Host 调用通道:DSH 的 ctx.remote 端点需要 typert 代码生成管线,独立仓库的插件无法接入。因此引擎「测试连接」不做真实网络请求,而是给出可直接在会话里运行的 agent 工具调用并支持一键复制:

local_search_test engine="bing"

它会返回可达性、耗时、结果条数、首条标题,失败时给出失败阶段(dns / http / parse / credential)。不带 engine 参数则批量测试所有已启用引擎;recent: true 只返回最近诊断,不发请求。


在界面中添加自定义引擎

「搜索 → 搜索引擎注册表 → 添加自定义引擎」展开内联表单:

字段说明
引擎 id唯一标识;与默认条目同 id 会覆盖它。
显示名称界面显示名。
执行方式(kind)http-html:轻量 HTTP 取 HTML(不执行 JS);browser:浏览器渲染后取 DOM;api:带凭据请求 JSON。
搜索端点完整 URL。browser/api 可用 {query}(另支持 {region} / {language})占位;不带占位时按 extraParams.queryParam(默认 q)附加查询词。
结果解析策略内置:html-bing、html-ddg、html-baidu、html-google、html-so360、json-generic。第三方注册的策略会出现在同一处。
需要凭据 / 凭据引用名打开后填引用名(如 MY_SEARCH_KEY)——配置里只存引用名,不存明文。
引擎凭据(配置名对应行)粘贴 key 值,保存时经凭据域写入(ctx.remote.credentials.set),值不进入配置文档、不进表单响应、不进日志。
顺序新建引擎默认排到末尾;创建后用行内的「上移 / 下移」调整,保存会重写 priority。

json-generic 的字段映射通过引擎的 extraParams 暴露:itemsPath(结果数组路径,默认 results)、urlPath/titlePath/snippetPath/publishedAtPath(默认 url/title/snippet/publishedAt)。区域/语言/安全搜索按引擎自动映射到真实参数名(Bing:cc/setlang/adlt;DuckDuckGo:kl/kp;其他:region/language/safeSearch)。

调整顺序、启停、调度、清洗与阈值

  • 顺序 / 启停:引擎行内「上移 / 下移」与「启用」开关,保存后立即参与调度。
  • 调度模式:搜索 → bail(按优先级顺序回退,首个有结果者胜出)/ parallel(并发查询前 N 个引擎,按 rank 轮转合并并按 URL 去重)。
  • 清洗开关:清洗 → 四阶段逐项开关。
  • 质量阈值 / 域名名单 / 总开关:质量过滤。
  • 一键旁路质量过滤:质量过滤 → 「质量过滤总开关」关掉,或清洗 → 「阶段四:质量过滤」关掉。

设置字段含义与默认值

搜索

字段默认含义
searchProviderlocal-browser-search注册本插件的本地搜索;选 deepseek-official 则不注册,回退到内置远程搜索(需重载插件生效,见「回退」)。
dispatchModebailbail / parallel。
parallelMaxEngines3仅 parallel 生效,1–8,且不得超过已启用引擎数。
searchTimeoutMs20000单引擎单次搜索预算,1000–120000 ms。
enginesBing、DuckDuckGo、百度、百度桌面端(默认停用)、Google、360引擎注册表内容。
searchScorer空结果评分器 id(见「结果重排与本地模型接口」)。空 = 只用质量规则;rerank-http = 内置 HTTP 重排器;也可填第三方 registerScorer 注册的 id。
rerankEndpointhttp://127.0.0.1:8080/rerank内置重排器的地址,searchScorer=rerank-http 时必须为合法 http(s) URL。
rerankModelBAAI/bge-reranker-v2-m3重排服务认识的模型 id。
rerankTimeoutMs4000单次重排预算,500–30000 ms;超时按失败降级(原排序不变)。

抓取

字段默认含义
fetchProviderlocal-browser-fetch注册本插件的本地抓取;选 http 则不注册,回退到内置纯传输抓取。
fetchBackendcdpcdp(默认,连本机 Chrome)/ playwright(可选本地浏览器)/ http(纯传输,不执行 JS)。
cdpEndpointhttp://127.0.0.1:9222CDP 端点;fetchBackend=cdp 时必须为合法 URL。
browserPath空仅 Playwright 后端使用;留空由 Playwright 自行解析。
headlesstrue仅 Playwright 后端;CDP 后端沿用你启动 Chrome 的方式。
profileModesharedshared 复用同一浏览器 profile(带登录态)/ isolated 每次新建隔离上下文。
fetchTimeoutMs45000单次抓取(含渲染)预算。
chromePath空「检测并启动」使用的 Chrome 可执行文件;留空自动检测常见位置。必须是绝对路径,否则保存被拒。
chromeProfileDir空「检测并启动」使用的数据目录;留空为插件自有目录($DSH_HOME/local-browser-web/chrome-profile)。必须是绝对路径。
launchTimeoutMs15000启动后等待调试端口响应的预算,3000–60000 ms。
maxConcurrency2并发抓取上限,1–8。
maxResponseBytes5000000响应体字节上限;超过直接报 CONTENT_TOO_LARGE,不做部分读取。
maxBodyChars100000解码后字符上限;超出截断并标记 truncated。
userAgentdeepseek-harness-local-browser/0.1.0HTTP 请求与 http-html 引擎使用的 UA(产品标识,非浏览器伪装)。

代理

字段默认含义
proxy.enabledfalse代理总开关。
proxy.url空代理地址;支持 http / https / socks5,可省略协议,HTTP 代理支持 user:pass@。
proxy.bypasslocalhost,127.0.0.1,::1直连名单;后缀匹配子域,* 全部直连。
proxy.httptrueHTTP 通道(http-html / api 引擎与 http 抓取后端)走代理。
proxy.browsertrue浏览器渲染(CDP / Playwright)走代理;开启后每次渲染使用独立上下文。

清洗管线(四阶段)

字段默认含义
clean.transport.enabledtrue阶段一开关。URL 协议校验与 SSRF 守卫始终生效;此开关控制字节/字符/重定向上限与人机验证识别。
clean.render.enabledtrue阶段二:浏览器渲染。关闭后改用纯传输读取(fetchBackend 被强制为 http)。
clean.extract.enabledtrue阶段二:Readability 式 DOM 打分提取正文。
clean.extract.fallbacktrue阶段二:打分不足时启用 Trafilatura 式段落启发式。
clean.markdown.enabledtrue阶段三:turndown 转 Markdown,规则与内置 dsh-tool-web 对齐(GFM 表格 / 删除线 / 剔除隐藏元素)。关闭则把 HTML 交给 dsh-tool-web 自行转换。
clean.quality.enabledtrue阶段四:质量过滤。

与内置 provider 的清洗对照

同一条 URL 分别走「内置 web-fetch-http + dsh-tool-web」与「本插件四阶段管线」,逐项对照:

能力内置 web-fetch-http + dsh-tool-web本插件
协议 / 凭据 / URL 长度校验有有
内网与云元数据地址不拦(实测 http://127.0.0.1:<port>/ 能取回内容)拦(IPv4/IPv6 私网段 + localhost / 元数据主机名)
重定向只跟同源,跨源报 WEB_REDIRECT_BLOCKED(http→https 也算跨源)跟最多 5 跳,每跳重新过守卫
字符集按 Content-Type 的 charset 解码同上,外加 <meta charset> / http-equiv 回退(GBK、GB2312 老站)
渲染 JavaScript无CDP / Playwright
正文提取无(整页转 Markdown)DOM 打分 + 段落启发式回退
HTML→Markdownturndown + GFM(表格、删除线)、剔除隐藏元素已对齐到同一行为
病态嵌套防护512 层上限,超限输出省略标记512 层上限,超限退化为纯文本(不再把 markup 交给转换器)
文档上限100 k 字符,先截断再做转换200 万字符供提取,maxBodyChars 只截断清洗后的输出
质量评估无五族规则打分(只排序与记录,不静默丢弃)

能取代的部分:抓取「把一篇文章变成干净 Markdown」这件事,本插件是内置 provider 的超集——守卫更严、多一次渲染、多一次正文提取,且 Markdown 输出已与 dsh-tool-web 对齐(同一条 HTML 现在产出同样的表格行与同样的隐藏元素剔除,见 tests/clean-parity.test.ts)。

不能取代的部分:

  • 无浏览器环境:内置 provider 只依赖 undici,本插件默认要一个可达的 CDP/Playwright。需要纯 HTTP 时把 clean.render 关掉即可退回 http 后端。
  • 内网与本地服务:本插件拒绝私网地址(这是刻意的 SSRF 守卫),所以抓不了 127.0.0.1 上的 dev server、内网 wiki。内置 provider 反而能(但它没有这层防护,不该靠它当护栏)。
  • 内存占用:提取需要完整 DOM,单页最多 200 万字符,比内置 provider 的 100 k 高;这是「提取准确率换内存」的取舍,不是免费收益。
  • 判断力:质量过滤判定的是「来源可不可信」——发布者层级、是否一手、是否过时、是否有伪装的域名、是否与其他来源矛盾。它不做事实核查:判断一句话真假需要外部证据与推理,那是模型的工作,规则层给不出。它也不替代检索排序本身。

一次实测(compare-clean.mjs / compare-offline.mjs,同一份 HTML 喂给两侧,网络因素已排除):

页面内置 provider本插件
维基百科条目(562 KB)25 971 字符、14 处站点导航词、29 行重复、3 行表格69 282 字符、1 处、0 行重复、39 行表格
nodejs.org/about(49 KB)8 981 字符、8 处导航词、27 行重复5 072 字符、0 处、15 行重复(内容重合 90%)
维基「编辑器比较」(704 KB,整页是表格)26 229 字符、24 行表格(受 100 k 截断)100 655 字符、120 行表格、0 行重复
5 000 层嵌套炸弹0 ms,输出省略标记修复前 8.2 s 后 RangeError;修复后 1 ms,退化为纯文本

对照前提:内置 provider 的 100 k 字符上限作用在 HTML 上,大页面会先被切掉,所以最后两行的差距里有一部分是「截断位置」而不是「清洗算法」。要等价比,看前两行——那里两侧拿到的 HTML 是完整的。

质量过滤

字段默认含义
quality.enabledtrue质量过滤总开关(调试可一键旁路)。
quality.thinContent.enabled / minCharstrue / 200正文字符数低于阈值命中 thin-content。
quality.seoSpam.enabled / maxKeywordDensity / maxDuplicateRatiotrue / 0.12 / 0.5最高频词占比超限命中 seo-keyword-stuffing;重复段落占比超限命中 seo-duplicate-boilerplate。
quality.lowQualityAigc.enabled / maxAigcMarkerRatiotrue / 0.05含套话标记的句子占比超限命中 low-quality-aigc。
quality.domainReputation.enabled / blocklist / allowlisttrue / 空 / 空每行一个域名;黑名单命中降权 domain-blocklisted,白名单命中豁免 domain-allowlisted。
quality.snippetMinChars40搜索摘要的字符下限;摘要天然比正文短,套用 200 会把每条结果都判成薄内容。
quality.authority.enabled / minScoretrue / 30权威度 = 来源分层基础分 + 一手性 ±6 + 时效 ±8;低于下限命中 low-authority。不再统计文内英文关键词。
quality.freshness.enabled / staleDaystrue / 365≤30 天 +8(fresh),超期 −8(stale);抓取的文章缺日期 −4(undated),摘要缺日期不扣。
quality.conflict.enabledtrue同题不同源给出不同版本号/日期/比例/金额时并排标注。
quality.annotate.enabledtrue把 〔项目官方·一手·2026-03-01·与 x.com 分歧:A vs B〕 写进 summary。

通用

字段默认含义
logLevelinfosilent/error/warn/info/debug;每次搜索/抓取都会产出结构化日志行。
structuredLogfalse开启后每行输出一条 JSON 记录。

结果重排与本地模型接口

质量层只按规则排序(来源分层、一手性、时效、伪装、冲突),它不知道「这段摘要到底答不答这个问句」。补这一层需要一个小模型,插件把它做成可插拔评分器(SearchScorer),默认全部关闭。

检索 → 规则层(排序 + 来源标注,全部结果保留)
     → [可选] reranker 重排(cross-encoder,0.6B 量级,60/40 与质量分混合)
     → [可选] evidence 评分器(7B 只抽主张/数值,不改顺序、不判真假)
     → 云端模型裁决

三条硬约束(实现与测试都钉住了):

  • 只排序不丢弃:评分器改的是顺序,结果条数永远不变;top_n 截断时缺失项一律排在最后而不是被删。
  • 失败即降级:未注册 → SCORER_UNAVAILABLE;异常/超时/响应不可解析 → SCORER_FAILED;两者都保留原排序并进诊断环(local_search_test recent:true)。
  • 可整体旁路:searchScorer 留空就等于没有这一层,行为与之前完全一致。

内置重排器:rerank-http

用 cross-encoder 而不是生成式模型:一次前向打分,比让 7B 生成判断便宜一到两个数量级,且分数可直接比较。服务端兼容两种主流响应协议:

服务响应形状
Text Embeddings Inference(POST /rerank,bge / mxbai)[{ "index": 0, "score": 0.93 }, …]
Cohere / Jina / Infinity{ "results": [{ "index": 0, "relevance_score": 0.93 }, …] }

请求体固定为:

{ "model": "BAAI/bge-reranker-v2-m3", "query": "原查询", "documents": ["标题\n摘要", "…"], "top_n": 10 }

起一个本地服务的例子(CPU 也能跑,模型约 0.6B):

docker run --rm -p 8080:80 -v "$PWD/models:/data" \
  ghcr.io/huggingface/text-embeddings-inference:cpu-latest \
  --model-id BAAI/bge-reranker-v2-m3

然后把设置页的 searchScorer 填 rerank-http、rerankEndpoint 指向 http://127.0.0.1:8080/rerank 即可(volatile,保存即生效,不必重启)。

混合方式:blendScores() 把「规则质量分」与「重排分」各自做 min-max 归一化后按 0.6 × 相关性 + 0.4 × 质量 相加,稳定排序(同分保持原相对次序)。权重给相关性更高是刻意的——质量分已经写进了来源标注,重排补的是「和这个问句有多相关」。只给权重而不直接覆盖,是为了不让一个只见过标题和摘要的 cross-encoder 推翻「这是政府/一手来源」这类结构性证据。

7B 证据抽取接口(预留,不内置模型)

不要让本地 7B 判真伪:它没有额外检索能力,只能复述摘要,判决质量和「让云端模型自己看」没有区别,却多了一次本地推理。它真正有价值的地方是结构化抽取——把散在多个来源里的主张、版本号、日期、金额抽成可比对的条目,交给云端模型裁决。

接口已经预留好了形状:kind: 'evidence' 的评分器,其 scores 被忽略(不改顺序),evidence 进诊断记录。注册一个即可:

ctx.effect(() => ctx.localBrowserWeb.registerScorer({
  id: 'evidence-7b',
  label: 'Local 7B evidence extractor',
  kind: 'evidence',
  async score({ query, candidates }, signal) {
    const response = await fetch('http://127.0.0.1:8081/extract', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ query, sources: candidates }),
      signal,
    })
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
    const body = await response.json() as { items?: ScoreEvidence[] }
    return { scores: candidates.map(() => 0), evidence: body.items ?? [] }
  },
}), 'acme: evidence-7b')

服务端要返回的形状(claim 必须有,其余可省):

{ "items": [{ "claim": "最新版本", "value": "2.4.0", "sourceUrl": "https://example.com/releases" }] }

产出落在两条出口:结构化日志行的 evidence=<条数>,以及 local_search_test recent:true 返回记录里的 evidence 数组。它不进 seam 的结果对象——WebSearchSource 是 dsh-web 持有的封闭形状(同 D7)。要把它带给模型,最省事的方式是在你自己的调用侧读诊断环,或让评分器自己把结论写进 snippet/另走一条工具。


手动编辑 cordis.patch.yml

界面保存与手动编辑通往同一处。 界面上的每一次保存,都是通过 DSH 的配置表单服务 ctx.configForms.get('local-browser-web') → form.mutate(ops, revision) 提交,最终落到当前 profile 的 cordis.patch.yml 中本插件条目的 config,再由 volatile HMR 热更新到插件实例(无需重启)。因此:

  • 手改 cordis.patch.yml 依旧有效,但不再是首选路径;
  • 两者混用时以配置文件为最终文档,界面只写它声明的 volatile 字段;
  • 清单与界面一致:所有可调项都在 Config schema 里标了 .volatile(),非 volatile 字段不可能从任何表单编辑(DSH 的硬性规则)。

cordis.patch.yml 示例:

- insert:
    - id: local-browser-web
      name: 'dsh-local-browser-web'
      config:
        searchProvider: local-browser-search
        fetchProvider: local-browser-fetch
        dispatchMode: bail
        fetchBackend: cdp
        cdpEndpoint: 'http://127.0.0.1:9222'

- id: web            # 把 ctx.web 接缝固定到本地 provider
  config:
    searchProvider: local-browser-search
    fetchProvider: local-browser-fetch

为什么 web 行必须显式固定:ctx.web 每个能力只选一个 provider,选择项在 web 行的 config 上。本地 provider 一旦注册,与其并存的 deepseek-official/http 就会让接缝抛 WEB_PROVIDER_AMBIGUOUS,除非显式指定。插件启动时会把需要补的那几行原样打进日志,照抄即可。


回退到 deepseek-official

DSH 的 web 行 schema 没有 .volatile() 字段,因此 SettingsForms 会拒绝任何表单对其写入(volatileForm() 返回 undefined → "has no volatile fields")。没有任何设置面板能改它。 本插件的做法是把「Provider 选择」实现成真实的注册开关:

  1. 通用界面路径:把 搜索 →「默认搜索 Provider」选成 deepseek-official、抓取 →「默认抓取 Provider」选成 http,保存后重载插件(/reload 或重启 DSH)。插件随之不再注册本地 provider,接缝回退到内置 provider。
  2. 同时把固定的那一行移除或改回:若 cordis.patch.yml 里仍把 web.searchProvider 固定为 local-browser-search,而本地 provider 已不注册,接缝会抛 WEB_PROVIDER_CONFIGURED_MISSING。因此请把该行一起改回内置值,或整行删掉(删掉后:仅注册内置 provider → 自动选择)。

这也是唯一一处「界面无法独立完成」的动作:web 行不在表单可编辑范围内,必须动一次 profile 补丁。插件在启动日志里会明确写出当前该保留/调整哪些行。


本地浏览器依赖与 CDP 连接

  • 默认不需要安装任何浏览器:fetchBackend: cdp 连接你已经在跑的 Chrome。
  • 启动 Chrome 带调试端口:--remote-debugging-port=9222,端点默认 http://127.0.0.1:9222。插件用 /json/version 探活,随后建页、导航、读渲染后的 DOM。
  • profileMode 语义:shared 直接用你那个 Chrome(含登录态);isolated 通过 Target.createBrowserContext 为每次请求开隔离上下文并在结束时销毁。
  • Playwright 是可选依赖:插件不声明它。要用 fetchBackend: playwright 时自行 npm i playwright;未安装时该后端 available() 为 false,调用会以 BROWSER_UNAVAILABLE 明确报错,不影响其它后端。
  • CDP 传输用的是 Node 22 内置的全局 WebSocket,所以插件没有 ws 依赖。

第三方扩展 API

插件注册了 cordis service ctx.localBrowserWeb:

// 新增解析策略(例如 SearXNG JSON / Brave / Serper / 内网搜索)
ctx.effect(() => ctx.localBrowserWeb.registerResultParser('json-searxng', parser), 'acme-search: parser')

// 运行时新增引擎(不入用户配置,随本插件 fiber 一起卸载)
ctx.effect(() => ctx.localBrowserWeb.registerEngine({ ...spec, id: 'searxng-custom' }), 'acme-search: engine')

// 新增抓取后端
ctx.effect(() => ctx.localBrowserWeb.registerFetchBackend('my-renderer', backend), 'acme-search: backend')

// 新增结果评分器(重排或证据抽取,见「结果重排与本地模型接口」)
ctx.effect(() => ctx.localBrowserWeb.registerScorer(scorer), 'acme-search: scorer')

只读面:engines() / resultParsers() / fetchBackends() / scorers() / recentDiagnostics(limit?, onlyFailures?)。

RunnerContext.runners 也允许按 kind 替换执行器(自带传输、录制回放的部署可用)。


错误码

与 ctx.web 共用一套码空间(消费方必须容忍 provider 私有码):

码何时抛出
BROWSER_UNAVAILABLECDP 端点不可达 / 未安装 Playwright
WEB_FETCH_TIMEOUT渲染或请求超时
WEB_FETCH_CHALLENGE人机验证/拦截页;或访问被 SSRF 守卫拒绝的内网/元数据地址
EXTRACTION_FAILED渲染成功但提不出正文
CONTENT_TOO_LARGE响应体超过字节上限
UNSUPPORTED_CONTENT_TYPE非 html/text 的内容类型;或非 http(s) 协议
ENGINE_CONFIG_INVALID自定义引擎元数据不合法(缺字段、endpoint 不可解析、解析策略未注册、id 重复)
ENGINE_TEST_FAILED测试连接失败(含 dns/http/parse/credential 阶段归类)
NO_ENABLED_ENGINE所有引擎被禁用,无可执行引擎
SCORER_UNAVAILABLEsearchScorer 指向的 id 没有注册(结果照常返回,只记诊断)
SCORER_FAILED评分器抛错/超时/响应不可解析(结果照常返回,原排序不变)
FORM_REVISION_CONFLICT面板保存命中版本冲突(内部为 Host 的 SETTINGS_CONFLICT,面板以界面语义码提示并保留草稿)

开发:构建与测试

node scripts/setup-links.mjs                                   # 建立依赖 symlink
node node_modules/typescript/bin/tsc -p tsconfig.json          # 类型检查(宿主 + 客户端)
node node_modules/typescript/bin/tsc -p tsconfig.build.json    # 宿主构建 → lib/
node node_modules/tsdown/dist/run.mjs                          # 客户端 bundle → lib/client.js
node --experimental-transform-types --test tests/*.test.ts     # 单测

需要 Node 22(nvm use 22):仓库要求 ^22.19 || >=24;测试用到构造器参数属性,因此要 --experimental-transform-types(仅剥类型的 --experimental-strip-types 不支持)。

测试覆盖(258 例,全绿):schema/面板字段双射(schema 的 volatile 叶子 == 面板字段路径 + {engines},见 D8)、config 校验分支、引擎注册表增删改排序、三个内置解析器(含 URL 归一化、DDG 跳转解包、Google /goto? 放行、百度推广位过滤)、解析策略就绪条件声明、browser runner 的重试次数(恰好一次)、bail/parallel 调度与去重、质量规则(来源分层梯度、一手/二手、时效、伪装、摘要与正文双阈值)与排序稳定性、跨源冲突检测的「该报」与「不该报」两侧、来源标注的可见契约(含长摘要不挤掉标注)、传输层守卫(SSRF / 协议 / 内容类型 / 人机验证)、正文提取与 Markdown、与内置渲染路径的清洗一致性(字符集解码、GFM 表格 / 删除线 / 隐藏元素、嵌套深度防护)、四阶段管线各开关与失败码(含「上限作用在清洗后的输出」与「提取拒绝 markup 后继续走文本」)、评分器接缝(注册表语义、混排不丢源、长度不匹配/NaN 回退、TEI 与 Cohere 两种响应协议、top_n 截断排最后、provider 侧重排与 SCORER_FAILED/SCORER_UNAVAILABLE 降级、evidence 不改顺序且进诊断环),以及客户端 bundle 冒烟测试(加载 lib/client.js,校验 Lazy-CJS factory 契约、settings.section 注册、注入面字段表覆盖)。


与提示词的功能点变更记录

实现期间按实际 DSH API 做了以下调整(功能点变更,均记录于此):

#提示词表述实际实现原因
D1跨字段校验用 .check()validateConfig()(apply() 时执行)+ 面板侧即时反馈本仓库 Schemastery 构建没有 .check();DSH 官方插件同样用 apply() 内显式断言。
D2面板写 ctx.settings客户端用 ctx.configForms.get(ns) / whileServed;Host 侧才叫 ctx.settings提示词此处把两侧服务名混用了,按真实 API 对齐。
D3错误码 FORM_REVISION_CONFLICT内部 Host 抛 SETTINGS_CONFLICT,面板映射为界面语义码 FORM_REVISION_CONFLICT实际冲突码是 SETTINGS_CONFLICT;保留提示词要求的界面码并做映射。
D4界面切换搜索/抓取 Provider,configForms.get('web')改为注册开关(选中本地值才注册本地 provider)+ 启动日志给出必需的 web 行补丁web 行 schema 非 volatile,SettingsForms 会拒绝一切表单写入(volatileForm() 为 undefined);该行只能通过 profile 补丁修改。
D5抓取/清洗阶段逐项关闭四阶段均可关;但协议校验与 SSRF 守卫始终生效,clean.transport 只关字节/字符/重定向上限与人机验证识别把「禁止探测内网」做成可关的调试开关等于开漏洞;这条按安全优先偏离提示词。
D6同时向 plugins.detail.section 贡献配置摘要未实现(只挂 settings.section)该槽位需额外依赖 dsh-client-ui-plugin-manager/client 且贡献形态受限;settings.section 已提供独立导航入口,故不做次要重复面。
D7结果携带 quality_score / quality_flags 元数据不塞进 seam 的结果对象;改为结构化日志行 + 诊断环(localBrowserWeb.recentDiagnostics() / local_search_test recent:true),结果只做排序不静默丢弃WebSearchSource 与 WebFetchResult 是 dsh-web 持有的封闭形状,provider 无法附加字段;dsh-tool-web 的投影也只复制固定字段。
D8(提示词要求"所有界面可编辑字段用 .volatile()")一次真实装载暴露出漏洞:嵌套的 clean.* / quality.* 叶子漏标 .volatile(),导致运行期 section 里是普通布尔值,插件以 config.clean.transport.enabled.get is not a function 激活失败。已补齐这些叶子并新增 tests/schema.test.ts面板的字段表在浏览器半侧、schema 在宿主半侧,两侧无法互相引用,缺一个机器可校验的不变式。新测试断言二者构成双射:schema 的 volatile 叶子 == 面板字段路径 + {engines}。
D9(未提及)安装走 symlink,而非 dsh plugin add官方入口会转发 pnpm,本机 profile 与当前 pnpm 的 store 大版本不一致(ERR_PNPM_UNEXPECTED_STORE);重装 profile 会危及运行中的 DSH。
D11(追加需求)可配置代理服务入口新增 proxy 配置组(enabled / url / bypass / http / browser)与设置页「代理」页签;HTTP 通道在 src/proxy.ts 里自行实现 absolute-form、CONNECT 与 SOCKS5(Node 的 fetch 不认 HTTP_PROXY,且 undici 的 ProxyAgent 无法从 Node 内部导入,所以不引依赖自己实现);浏览器通道用 Target.createBrowserContext 的 proxyServer / proxyBypassList关键约束:两条通道必须分别可控(只有 HTTP 代理不够,需要代理的引擎往往要渲染);直连名单必须在发请求前判定;浏览器代理按上下文生效 → 必然放弃默认 profile 登录态,这条要写进文档而不是藏起来。
D10(追加需求)新增百度 / Google / 360 搜索三家作为引擎条目 + 解析策略加入,而不是新增代码路径:html-baidu / html-google / html-so360 进解析器注册表,引擎只是 DEFAULT_ENGINES 的条目;另加 SEARCH_BODY_CHARS 与「SERP 命中人机验证即抛 WEB_FETCH_CHALLENGE」两条修正计划里「引擎是数据不是代码路径」这条约束必须继续成立;同时实测暴露两个必须修的缺陷:360 的结果在 330 KB 之后(被 maxBodyChars 截断成 0 条),百度桌面端把验证页当 200 返回(会被误报成「0 条结果」)。
D12(装好代理后首次实跑,Google 10 条结果解析为 0)查出四个独立缺陷,全部按「数据/策略声明」修,不新增执行分支:① UA 默认值原本是插件标识 deepseek-harness-local-browser/0.1.0,浏览器渲染也用它覆盖 → 引擎判定为爬虫,Google 回「需启用 JavaScript」降级页(87 KB,#rso 不存在)。改为:浏览器通道不再覆盖 UA(用本机 Chrome 真身),HTTP 通道留空时回落到真实桌面 Chrome UA。② 就绪条件:loadEventFired 会早于结果注入触发(实测空壳 body 仅 17 字符,#rso 在 2.5s 后才出现),给 ResultParser 加 waitFor 声明,渲染后端据此等待(8s 上限,超时照读);loadEventFired 自身也加 8s 软上限(代理下挂住的第三方资源会让它永不到来)。③ 跳转解包:Google 的 href 全变成 /goto?url=<加密 token>,真实地址不在 DOM 里;给 EngineSpec 加 unwrapRedirect 正则,命中的结果各发一次 HEAD 读 302 的 Location(并发 4、单条 6s、失败重试一次)。④ DuckDuckGo 主站在代理下渲染成空白(body 文本长度 0),改走 html.duckduckgo.com/html/ 服务端渲染端点,kind 降为 http-html这四条都属于「引擎是数据、策略自带知识」的边界内:等待什么由解析策略声明,哪种链接需要解包由引擎条目声明。顺带修掉两个实测缺陷:proxy.bypass 命中时仍会建代理上下文(白白丢掉登录态,百度因此一直人机验证)、百度移动端把 recommend_list / ubs 推广位当结果收进来。
D13(要求「检查清洗功能能否取代官方」)逐项对照内置 web-fetch-http + dsh-tool-web 后补掉四处真实差距:① Markdown 规则与官方对齐(GFM 表格、删除线,剔除 display:none / [hidden] / aria-hidden)——原先整张表格被拆成一行一个单元格;② 字符集改为按 Content-Type 的 charset 解码并回退 <meta charset>——原先硬编码 UTF-8,GBK 页面整页替换字符;③ DOM 解析加 512 层嵌套上限,超限退化为纯文本——原先 5 000 层嵌套会阻塞事件循环 8.2 秒后 RangeError;④ 提取预算抬到 200 万字符、maxBodyChars 改为截断清洗后的输出——原先 562 KB 的维基页在 100 k 处被切,容器丢失后只抽出 1 905 字符(完整 HTML 下是 29 922)结论不是读代码得出的:把同一份 HTML 同时喂给真实的内置 provider/tool 渲染与插件管线(compare-clean.mjs、compare-offline.mjs),四条差距都是量出来的。新版新增 tests/clean-parity.test.ts 钉住前三条,管线测试钉住第四条。
D15(追加需求)Chrome 依赖要在 README 讲清,并提供「一键检测 + 启动」按钮,不让用户敲命令新增 src/browser/launcher.ts(探测 /json/version、按平台定位可执行文件、独立数据目录、spawn 后轮询到端口响应)与 src/server/browser-control.ts(POST /local-browser-web/browser,actions:status / launch);面板「抓取」页加连接卡片(状态 pill + 检测 / 检测并启动 + 失败路径列表);配置新增 chromePath / chromeProfileDir / launchTimeoutMs;README 文首加「前置依赖:本机 Chrome」面板无法用 ctx.remote(typert 需构建期装配,第三方插件接不进去),但 Host 可以按官方方式在 webServer 上注册具名路由(与 open-in-app 同一范式),并复用 connection.requestRejection 的 Host/Origin + 登录令牌校验。启动只允许「固定参数 + 插件自有数据目录」,不接受用户输入拼进命令行;Chrome 136+ 的默认目录限制是必须固定 --user-data-dir 的直接原因。
D14(要求「质量层能否甄别搜索结果的权威性与真伪」)实测证伪旧的 authority 规则后整体重做:① 权威度改为来源分层(政府 95 / 学术 88 / 项目官方 80 / 媒体 72 / 参考 66 / 社区 45 / 聚合站 25)+ 一手性 ±6,删除英文词表计数;② 接入 publishedAt 时效(原先该字段完全没参与打分);③ 新增伪装检测(品牌被他人注册域借用 −40、明文 HTTP −5、短链 −3);④ 搜索摘要改用独立的 snippetMinChars(原先把正文 200 字下限套在 50–120 字的摘要上,10/10 恒中 thin-content);⑤ 新增跨源冲突检测与 summary 来源标注,把「来源性质 + 一手性 + 日期 + 与谁分歧」直接交给模型旧规则三条硬伤都是量出来的:10 条真实结果只产出 2 种判定(9 条并列);candidate 因含 date 被判有权威、source source source updated date reference citation 拿满分;中文权威正文(世卫报告句)被判 low-authority,因为 8 个提示词全是英文。修复后同一份 SERP 按真实发布者分成 3 层且语义正确,中文官方页拿 95 分。新增 tests/source-tiers.test.ts、tests/conflict.test.ts 钉住分层与「不误报冲突」。
D16(追加需求)先接入 reranker,并为本地 7B 预留接口新增可插拔评分器接缝 src/search/scorers.ts(SearchScorer:kind: 'rerank' | 'evidence',ScorerRegistry,blendScores() 60/40 混合)与内置 src/search/rerank.ts(rerank-http,兼容 TEI 数组与 Cohere/Jina results 两种协议);provider 在质量排序后调用 applyScorer,配置新增 searchScorer / rerankEndpoint / rerankModel / rerankTimeoutMs(全 volatile),面板「搜索」页可填,公开 API 增 registerScorer() / scorers();kind: 'evidence' 的产出(ScoreEvidence[])不改顺序,落到诊断记录与日志行结论是权衡后的取舍:cross-encoder(0.6B)一次前向就给出可比分数,比让 7B 生成判决便宜一到两个数量级,因此先接 reranker;7B 只保留「结构化证据抽取」的位置,因为本地模型没有额外检索能力,让它判真假等于让云端模型自己看两遍。三条硬约束写成代码不变式:不丢弃、失败降级、可旁路。新增 tests/scorers.test.ts、tests/rerank.test.ts。

已知限制

  • L1 面板不能直接测试引擎:DSH 的 ctx.remote 端点要 typert 代码生成,独立仓库插件接不进去。因此「测试连接」给的是可复制的 local_search_test 调用(见上文)。
  • L2 Provider 切换需重载插件:searchProvider / fetchProvider 是注册期开关,volatile 热更新只改值不会重注册 provider。
  • L3 web 行不由任何表单承载:见「回退到 deepseek-official」。
  • L4 只有 settings.section 一个界面入口:未向 plugins.detail.section 贡献摘要(D6)。
  • L5 渲染后端的字节上限:maxResponseBytes 对 HTTP 后端是「超限即失败」,对 CDP/Playwright 是「截断」,因为渲染结果已经是一整份 DOM 字符串。
  • L6 质量判定的文本来源:搜索结果按 snippet 判定(seam 的 source 只带标题/摘要/链接),抓取结果按提取后的正文判定。
  • L7 browser 类引擎依赖 CDP:baidu / baidu-pc / google / duckduckgo 都要渲染,必须有一个可达的 cdpEndpoint(默认 http://127.0.0.1:9222)。Chrome 136 起,--remote-debugging-port 对默认数据目录不再生效,所以要用独立 --user-data-dir;那个 profile 的登录态与 cookie 会随实例保留。
  • L8 百度桌面端的人机验证:baidu-pc 默认停用。百度会对陌生网络与新建浏览器 profile 返回 wappass 图形验证(HTTP 200),这类请求一律报 WEB_FETCH_CHALLENGE;在该 Chrome 里过一次验证或换成可信网络后再启用即可。baidu(移动端)不受此影响。
  • L9 Google 需要可达网络:google 条目已内置,但网络不可直连 Google 时(即使配了代理,代理自己也出不去)会以 net::ERR_CONNECTION_TIMED_OUT / ERR_EMPTY_RESPONSE 失败。
  • L10 浏览器代理与登录态基本不可兼得:Chrome 没有可运行时修改的浏览器级代理,代理只能按上下文给,所以真正需要走代理的渲染一定是新上下文(shared 也一样),默认 profile 的 cookie 不参与。例外是直连名单:目标命中 proxy.bypass 时根本不需要代理上下文,于是仍用 shared profile(保留登录态)——所以把常用站点写进名单,既省代理也保住登录态。
  • L11 浏览器侧不接受代理凭据:proxyServer 只吃 scheme://host:port,http://user:pass@host:port 里的用户名密码在浏览器通道被忽略(HTTP 通道则正常使用)。需要认证代理时,要么只用 proxy.http,要么在本机起一个免认证中转。
  • L12 国内引擎的风控按频率触发:百度(wappass)、360(qcaptcha)在短时间内被同一出口连打十几次后会开始回验证页,HTTP 200,本插件报 WEB_FETCH_CHALLENGE。这是出口信誉问题,不是解析问题:真实使用一次搜索只碰 1–2 个引擎,不会触发;连续压测(或把国内站点放进代理)才会。bail 调度下换一个引擎即可继续。
  • L13 代理质量决定 Google 可用性:google 需要代理真正到得了 Google,且线路要能连续完成一次页面渲染(主文档 + 脚本包 + #rso 注入)。实测同一个代理对 www.google.com 的 8 次探测里有 2 次失败、延迟在 0.8–5.4 秒之间抖动,对应表现就是 ERR_CONNECTION_CLOSED 或偶尔 0 条。插件已做「空结果重试一次」,但线路本身不可用时无法补偿。
  • L14 Google 结果仍需一跳解包:/goto?url= 是加密 token,只能问一次跳转器。跳转链解析失败时该条结果会保留跳转链(点开仍能到达目标),不会丢结果;成功率取决于代理对 www.google.com 的稳定性。
  • L15 提取只做到「够用」:正文打分按文字量与链接密度,没有 Readability 的完整排版启发式。以表格或列表为主体的页面(维基「比较」类条目、目录页)会把参考列表、脚注一并带上,输出比人工阅读所需更长;这类页面适合关掉 clean.extract 直接用整页 HTML。
  • L16 字符集回退有边界:<meta charset> 只读响应体前 4 KB,且仅在 Content-Type 未声明 charset 时生效;真正要按声明解码的页面(GBK 政府/院校老站)不受影响。
  • L17 分层只看域名,不看页面归属:github.com/xxx/awesome-list 属于第三方整理,但 github.com 整域按「项目官方」计入一手。同理 *.gov.cn 上转载的第三方内容也按官方计。判断「这条 repo 是不是项目官方」需要 owner 级规则,当前不做。
  • L18 冲突检测只比标题里的数值:同一话题的两个来源若把版本号/日期/比例写在摘要而非标题里,检测不到(宁可不报也不误报)。跨语言、跨单位(1.5 万 vs 15000)也不做归一。
  • L19 「检测并启动」启动的是 Host 所在机器上的 Chrome:DSH 跑在远端(SSH / 容器 / 无图形界面的服务器)时,按钮只会在那台机器上找 Chrome;远端没有浏览器时仍然报 CHROME_NOT_FOUND,此时应在本地起 CDP 并用隧道暴露端口,再把 cdpEndpoint 指过去。
  • L20 自动检测只覆盖 Google Chrome 的常规安装位置:Chromium、Edge(msedge.exe 也能开 CDP,但需手动填路径)、自定义目录都检测不到,请填 chromePath。启动参数是固定的四项(端口 / 数据目录 / --no-first-run / --no-default-browser-check),不提供自定义 argv。
  • L21 重排只看得见标题与摘要:rerank-http 的输入是 标题\n摘要,不是正文。摘要由搜索页给出(通常 50–120 字),所以「正文里其实答了这个问题但摘要没写」的结果会被压后。要让重排看正文,得先 fetch 再自建一条评分链路,插件不做。
  • L22 重排与质量分的混合是固定权重:0.6 × 相关性 + 0.4 × 质量,且两者各自做 min-max 归一化——同一批结果内的相对排序才是它的语义,跨查询的绝对值不可比。权重不开放配置(避免多一个调错的旋钮)。
  • L23 evidence 评分器的产出不进结果对象:WebSearchSource 是封闭形状(D7),所以证据只落在诊断环与日志里。要把它送到模型面前,需在调用侧读 recentDiagnostics(),或由评分器自行改写 snippet。
  • L24 7B 抽取的接口是预留而非内置:仓库里没有任何本地模型代码,也不托管权重;kind: 'evidence' 只是一个形状约定 + 降级保证。要接真模型需另写插件调用 registerScorer()。

Comments

Loading…