DeepSeek Harness 插件

Geighlord007/qp-exa-dynamic

Star 数 ★ 0 下载量(近 30 天) 891 分类 浏览器与网页 收录于 2026-09-18 npm qp-exa-dynamic

基于 Exa 的网页搜索提供方:默认开启 Exa Dynamic Highlights(并带上该设置必需的 Exa-Beta 请求头),开放全部 8 种 Exa 检索类型,提供 /exa 命令在运行时改高亮、检索类型与返回条数,并注册一个可自行指定条数的 exa_search 工具。

安装

# npm 包(预构建)

dsh plugin --profile web add qp-exa-dynamic

# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)

dsh plugin --profile web add github:Geighlord007/qp-exa-dynamic

装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。

README

English | 简体中文

Exa 支撑的 WebSearchProvider,接入 DeepSeek Harness 的 ctx.web seam。 默认开启 Exa Dynamic Highlights,提供 /exa 命令在运行时改高亮、检索类型和返回条数,以及一个 exa_search 工具让你按需走 Exa——而不改变 profile 里其它搜索用哪个提供方。

dsh plugin --profile web add qp-exa-dynamic

为什么需要它

官方的 @deepseek-ai/dsh-web-search-exa 够不到 Dynamic Highlights,是两处互相独立的硬伤:

  1. 请求体写死。 它发的是 contents.highlights.highlightsPerUrl,配置里没有任何字段能影响到 dynamic。

  2. Dynamic Highlights 是 beta 能力,需要请求头。 每个设了 dynamic: true 的请求都必须同时带上 Exa-Beta: dynamic-highlights-2026-08-28。那个提供方只发 authorization、content-type、 accept、user-agent——没有 Exa-Beta。缺了它 Exa 直接回 HTTP 400(实测):

    {"error":"'highlights.dynamic' is in beta. Send the 'Exa-Beta: dynamic-highlights-2026-08-28' request header to use it.","tag":"INVALID_REQUEST"}
    

本插件两样都发,并且彻底去掉了 highlightsPerUrl——对着真实 API 实测,Exa 已经忽略这个参数, 传 1 和传 5 返回逐字节相同的结果。改用 maxCharacters,那才是关掉动态高亮时真正生效的旋钮。

实测数据

真实 /search 调用,同一个查询,8 条结果:

配置 高亮字符数
官方提供方的默认值(无可用旋钮) 51,152
本插件 dynamicHighlights: false + highlightsMaxCharacters: 1500 10,973
本插件 dynamicHighlights: true(默认) 12,716

Dynamic Highlights 不是一刀切截断:它把所有召回文档拼成一条输入、只做一次前向,在全局范围分配 共享预算——好内容多给,冗余的不给。

安装

dsh plugin --profile web add qp-exa-dynamic

包自带 dsh.bundle 清单,安装后 bundle patch 会自动插入 provider 行,不需要手写。

这一步是可选的。 exa_search 工具按需走 Exa,完全不需要它;只有想让 web_search 本身也走 Exa 时才需要。在 $DSH_HOME/profiles/web/cordis.patch.yml 里覆盖 web 那一行:

- id: web
  name: '@deepseek-ai/dsh-web'
  config:
    searchProvider: exa
    fetchProvider: http

patch 是整块替换目标行的 config、不做合并,所以 fetchProvider: http 必须一起重写, 否则抓取提供方会丢。

然后给密钥,两种方式。写进插件配置:

- id: qp-exa-dynamic
  name: qp-exa-dynamic
  config:
    apiKey: '你的 Exa 密钥'

或者走环境变量。apiKey 标了 role('secret'),不会出现在任何 describe() 响应里——但明文配置文件 终究是明文配置文件,能用环境变量就尽量用。

密钥按这个顺序读取: 配置里的字面 apiKey → harness 凭据库里的 apiKeyEnv(把 EXA_API_KEY 加进去,用 GUI 的凭据卡片或 $DSH_HOME/.credentials.yaml)→ 启动环境快照(继承环境、 调用目录的 .env、Harness 主目录的 .env)。

凭据库排在快照前面,是因为快照在某些 Windows 安装上按文档就会漏掉变量——本插件正是跑在这类部署上。 如果全局选中了 exa 却报 registered but unavailable,先查凭据库,再考虑退回明文配置里的字面 apiKey。

改环境变量后要重启 dsh web。cordis.patch.yml 本身是热加载的,所以配置改动不用重启。

配置字段

全部有安全默认值,通常只需要提供密钥。

字段 默认值 含义
providerId exa 注册 id。只在需要与另一个 Exa 提供方共存时改。
apiKey 未设置 字面密钥;不设则回退到 apiKeyEnv。
apiKeyEnv EXA_API_KEY apiKey 未设时读取的环境变量名。
baseURL https://api.exa.ai Exa 端点;会追加 /search。
searchType auto 检索类型,见下。可用 /exa type 运行时改。
numResults 8 来源上限。可用 /exa results 运行时改。
dynamicHighlights true 默认开;开启时自动带上必需的 Exa-Beta 头。可用 /exa 运行时改。
highlightsMaxCharacters 未设置 每页高亮上限,仅在 dynamicHighlights 为 false 时生效。

dynamicHighlights 不会和 highlightsMaxCharacters 同时发——动态开启时共享预算由 Exa 自行分配, 官方文档也明确警告两者不要并用。

/exa 命令

敲在输入框里。它直接对界面执行,不产生模型消息。

命令 作用
/exa 切换 Dynamic Highlights
/exa on / /exa off 明确设置
/exa type 列出可用的检索类型
/exa type deep 设置检索类型
/exa results 报告来源上限
/exa results 3 设置来源上限
/exa status 一次报全:当前状态、可用的 type 列表、真实天花板

命令声明了 input.hint(on|off|status|type deep|results 3)——这是 Web 输入框把参数交给处理函数的 前提,否则整行会被当成普通聊天消息发给模型。方括号和引号都会被当作透明字符,所以那个提示模板依然可 以就地编辑。/exa status 顺带把检索类型列出来,想不起来有哪些 type 时不用再敲第二条;参数打错时会回 一行可以直接照抄的示例。

写入走 settings 服务,命名空间是 Loader 的配置项 id qp-exa-dynamic,因此落在 profile patch 的 用户层,跨重启保留。清掉那一节即回到插件配置的默认值。命令会写的每个字段都在 schema 里标了 .volatile() —— 写入路径和自动生成的配置卡片都要求这一点。

实测同一个 provider 实例、同一个查询:关掉动态高亮让同一次搜索从 12,716 字符变成 57,958 字符—— 差 4.6 倍,下一次搜索即刻生效。

exa_search 工具

插件还在 web_search 旁边注册了第二个面向模型的工具:

exa_search(query: string, maxResults?: integer)   // maxResults 1-50

它存在的目的是让 Exa 按需可达,而不改变 profile 里其它搜索用谁。seam 对每个 profile 只选一个 提供方——不是每次调用选,而且 searchProvider 在服务构造时就固化了——所以一个钉死官方搜索的 profile,只能靠一个绕过该选择的工具才够得着 Exa。这个工具就是直接调插件自己的 provider,不走 ctx.web.search。

路由就这么简单:

工具 路径 走谁
web_search ctx.web profile 钉死的那个(不受影响)
exa_search 直连 Exa

用哪个工具,就是选哪个提供方。 不改任何全局设置、不用重启,Exa 就在一次调用之外,而 web_search 照旧用默认提供方。

直连还顺带让这个工具拥有自己的 request.maxResults。dsh-tool-web 每次走 seam 都传自己的 searchMaxResults,所以任何经 seam 的提供方都不可能超过那个上限,要抬它就得 fork agent preset。 这个工具拥有自己的上限,于是条数从部署级天花板变成了每次调用的模型参数:

想要 怎么做 需要 fork preset 吗
某次要 20 条 exa_search(query, maxResults: 20)——直接用话问就行 不需要
让 /exa results 20 生效 /exa 命令 需要

因为不再经过 seam,截断这份契约改由工具自己履行(capToolResult):Exa 如果对有界请求超量返回, sources 会被裁掉并置 truncated。它 50 的上限是自己的——动态高亮实测每条约 1.6k 字符,50 条已经是 约 2 万 token 的上下文。

没有密钥时会明确报错(no Exa API key: …),而不是悄悄回落到部署的默认提供方替你查。

web_search 旁边会多一段提示告诉模型什么时候该用它(日常检索仍走 web_search)。如果组合里没有 tools 注册表,provider 照常挂载,只是没有这个工具。

检索类型

Exa 的 type 就是延迟/质量的旋钮。8 种全部对着真实 API 验证过;同一查询、8 条结果的实测耗时:

类型 实测 用途
keyword 464 ms 纯关键词,最快
neural 737 ms 语义检索
fast 798 ms 快,质量损失极小
instant 856 ms 实时场景(对话、语音)
auto 1,914 ms 默认
deep-lite 3,116 ms 轻量综合输出
deep 5,282 ms 多步推理
deep-reasoning 18,278 ms 最难的研究任务

官方提供方的 schema 只列了 auto、keyword、neural 三种——那套已经过时。本插件全部开放。

deep* 在这个 seam 下有折扣

用本插件自己的类实测,同一查询,动态高亮开启:

类型 耗时 返回来源数
fast 718 ms 8
auto 215 ms 8
deep 6,891 ms 3
deep-reasoning 14,776 ms 4

原始 API 对 deep 是返回 8 条的;其余几条没有非空白高亮,被整个丢弃了——seam 没有别的字段能当 snippet,编造一个会让 seam 说谎。而 deep* 真正值钱的是跨来源综合出的 output,WebSearchSource 里没有字段承载它。所以在这个 seam 下,实用区间是 keyword、neural、fast、instant、auto。

返回条数归 dsh-tool-web 管

这一条容易误解,说清楚:

  • 模型侧 web_search 工具的参数只有 queries——模型无法要求条数。
  • 天花板归 dsh-tool-web:searchMaxResults,默认 8。它自己的注释: "The consumer owns the returned-context limit; providers and models do not."
  • 工具每次调用都会传 maxResults,seam 再按它截断结果——所以任何提供方都不可能超过天花板。

于是本插件的 numResults 是单向的:能拉低,拉不高。

你设的 工具天花板 实际发给 Exa
3 8 3
12 8 8(夹取)
20 8 8(夹取)

/exa status 和 /exa results 会报出 provider 观测到的真实天花板,所以被夹取时是明说而不是 静默生效。

在 Web 端,抬天花板不是改 profile patch 能解决的。 dsh-web-app 把宿主那行 tool-web 设为 disabled: true——因为宿主侧只有 web 服务和它的搜索提供方,面向模型的工具是每会话一份的, 真正生效的那份来自 agent preset。随附的 standard preset 里那行只有 fetch 和 searchTimeoutMs,没有 searchMaxResults,于是取 schema 默认值 8。往 profile patch 里写 tool-web 只会落在被禁用的宿主行上,什么都不做。

所以要改就得 fork preset:把 standard 复制到 $DSH_HOME/.agent-presets/,在它的 tool-web 行加 searchMaxResults,再选为默认。这是笔真实的账——副本不会跟随随附 preset 的上游更新,而默认值是在 创建会话时读取的,运行中的会话仍停在它们当初组装的 preset 上。多数情况下留在 8 更划算:/exa results 仍然能把条数往下调,而那个方向才是省 token 的。

已知限制

  • Exa 的 beta 接口可能变。 dynamic-highlights-2026-08-28 是 research preview,Exa 改版后需要 更新 DYNAMIC_BETA_VALUE。
  • 没有高亮的结果会被整条丢弃,这是 seam 的规则。动态高亮下实测 8/8 条都带高亮,所以很少触发。
  • 没有 category、域名/日期过滤,也没有全文。 这些 Exa 能力本插件尚未暴露。
  • 本插件与官方 Exa 提供方每个 profile 只能选一个。 两者默认都注册 provider id exa;要共存需给 其中一个设不同的 providerId。
  • exa_search 需要 Exa 密钥,web_search 不需要。 这个工具直连 Exa,没密钥时会明确报错而不是 回落——这正是"选择是真的"的代价。web_search 两边都不受影响。
  • 仅测过 dsh 0.2.0-rc.2,peer 范围也锁在这条线上(>=0.2.0-rc.2 <0.3.0-0)。
  • 没有 settings 服务时 /exa 只改内存。 provider 本身照常工作;没有命令注册表时就没有 /exa。 settings 服务拒绝写入时(没有这个 entry id、字段不是 volatile、profile 只读)会如实回 could not save: …,不会静默吞掉。

开发

node test/index.test.js    # 44 个单元测试,不需要密钥
EXA_API_KEY=... node test/live.mjs   # 打真 API,会消耗额度

请直接运行测试文件,不要用 node --test:后者的 runner 会为每个文件 spawn 子进程,在受限沙箱下 会以 spawn EPERM 失败。

从本地检出安装

dsh plugin --profile web add <路径> 记下的是一个 link: 依赖,profile 按包名链接它。而 Node 解析 被链接模块自身的 import 时,用的是它的真实路径、不是 profile 里那个链接——所以放在 $DSH_HOME/profiles/ 之外的检出没有能到达 harness 各包的 node_modules 祖先目录,每一个 @deepseek-ai/dsh-* 导入都会以 ERR_MODULE_NOT_FOUND 失败:插件挂载上了,然后在第一个 import 上炸。

给它配一份指向运行时同一批实例的 node_modules:

$plugin = "<这个检出的路径>"
$hoist  = "$env:USERPROFILE\.dsh\profiles\node_modules\@deepseek-ai"
New-Item -ItemType Directory -Force "$plugin\node_modules\@deepseek-ai" | Out-Null
foreach ($pkg in @("dsh-web", "dsh-tools", "dsh-launch-environment", "schemastery")) {
  cmd /c mklink /J "$plugin\node_modules\@deepseek-ai\$pkg" "$hoist\$pkg"
}

用 junction 而不是再跑一次 pnpm install,是刻意的:@deepseek-ai/dsh-tools 是运行时单例, 嵌套一份副本会让 agent loop 在 provider 被调用之前就崩。全部指向 hoist 目录,才能保证每个包只有 一个物理实例。

卸载

dsh plugin --profile web remove qp-exa-dynamic

把 cordis.patch.yml 里的 web 覆盖删掉即回到内置的 DeepSeek 搜索。那个改动是热加载的,立刻生效。

许可

MIT

内容来自项目 README(GitHub)↗

链接

同类插件

查看整个分类 →

社区评论

评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。