npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@arcaneorion/dsh-tavily-web

v0.2.2

Published

DSH Tavily search and page fetch: a model-visible tavily_search tool backed by a rotating multi-key pool, plus web_fetch through the shell seam.

Readme

@arcaneorion/dsh-tavily-web

DSH 的 Tavily 检索 + 网页抓取能力层(host 半,profile bundle)。注册两个模型可见工具,并把自身作为 唯一的 fetch provider 挂进宿主 web 注册表。

  • tavily_search —— Tavily 检索,返回 { sources: [{url, title, snippet, publishedAt}], truncated, content? }。 多 key 轮询池:单账号额度耗尽不再等于工具失效。
  • web_fetch —— 经 shell seam 的 curl 抓单页,返回 HTTP 状态 + 去标签正文。不依赖 Tavily,无 key 需求。

检索不注册为 web.search() 的 provider:旁边已有出厂的 DeepSeek 检索 provider,再挂一个可用 provider 会让 web.search() 的选择变歧义。检索只以模型可见工具的形式存在。

安装

dsh plugin --profile web add @arcaneorion/dsh-tavily-web
# 然后重启 dsh --profile web —— host 插件不走热重载

profile 级 bundle:装一次,该 profile 下所有会话都拿到 tavily_search / web_fetch。

发布状态:已发布到 npm,上面的命令可直接用。最新版本号永远现查、别信文档里写死的号: npm view @arcaneorion/dsh-tavily-web version(404 即尚未发布)。 想在本地改源码即时生效,则走 link: 挂载:~/.dsh/profiles/<profile>/package.json 写 "@arcaneorion/dsh-tavily-web": "link:/home/arcaneorion/AI/AI-DSH/plugin/tavily-web-plugin", 并在 dsh.profile.bundles 追加包名。

另外 npm tarball 只含 src/、cordis.patch.yml、README.md(files 白名单),tests/ 不随包发布—— 要跑下面的离线用例请用仓库副本。

兼容性(DSH 版本)

当前工作树已适配 DSH 0.2.0-rc.1(peer 按 0.2.0-rc.1 声明;版本 0.2.0)。下列 0.1.1-rc.2 记录仅作历史基线。

| 宿主包 | 声明 | 用途 | |---|---|---| | @deepseek-ai/dsh-tools | >=0.2.0-rc.1 | defineTool 注册 tavily_search / web_fetch | | @deepseek-ai/dsh-web | 0.2.0-rc.1 | 把自身挂成 web 注册表的 fetch provider | | @deepseek-ai/dsh-shell | 0.2.0-rc.1 | 走 shell seam 调 curl 抓页 | | @deepseek-ai/cordis | ^4.0.4 | 插件生命周期 | | @deepseek-ai/schemastery | ^3.18.4 | 配置 schema(keyRefs / apiKeys / cooldownSeconds) |

0.1.1-rc.2 → 0.2.0-rc.1 的唯一变更:shell seam 由 run() 改为 execute().result()

0.2 的 ctx.shell 只有一个执行入口 execute(spec),返回进程句柄;前台结果要再 await handle.result()。 本插件只用了前台执行,因此改动集中在 runCurl 一处:结果对象字段(exitCode / stdout.text / stderr.text / truncated / timedOut / aborted)与 0.1 一致,其余逻辑未动。

无 client 半,故不声明 react。换 DSH 版本必须先重新验证再放宽 peer:web 注册表与 shell seam 的契约跨版本会变,精确钉住的 peer 会在安装时报冲突,好过装上去静默失效。

文件

| 文件 | 说明 | |---|---| | src/tavily-web.ts | 源码:配置 schema、key 池、检索、抓取、工具注册(带完整注释) | | src/tavily-web.js | 发布入口(package.json 的 main):由 .ts 转译而来,随包发布 | | cordis.patch.yml | bundle 声明(行 id tavily-web),已挂载进 web profile | | tests/pool.test.cjs | 池行为离线用例(脚本化 shell:桩件实现 0.2 的 execute() → result(),不联网、不消耗额度) | | tests/live-pool-check.cjs | 真实密钥 + 真实网络的端到端探针 | | LICENSE | MIT(package.json 同名字段;npm 打包时自动附带,无需写进 files) |

为什么有两个同名文件(改代码前先读这段)

main 必须是普通 JS,入口写成 .ts 会让「线上版本在任何机器上都加载不了」:

  • 本地 link: 挂载时能用——pnpm 建的是 symlink,Node ESM 默认解析 realpath,文件真实路径在 plugin/ 下而不在 node_modules 里,所以 Node 的原生类型擦除放行;
  • 从 npm 正常安装后,文件真实路径落进 node_modules,Node 直接拒绝: Stripping types is currently unsupported for files under node_modules。

失败形态是静默的:loader 连 fiber 都建不起来(fiberPhase: null),apply() 从不执行, tavily_search 永远不出现,且没有显式报错。所以:

npm run build        # 改完 src/tavily-web.ts 后重新生成 src/tavily-web.js

提交时两个文件一起提交(.ts 保留完整注释作源码,.js 是发布产物)。

发布前必须用安装形态验证,不能用 link: 形态验证——那正是这个坑躲过检查的原因:

mkdir -p /tmp/probe/node_modules/@arcaneorion
cp -r . /tmp/probe/node_modules/@arcaneorion/dsh-tavily-web
cd /tmp/probe && node -e "import('@arcaneorion/dsh-tavily-web').then(m=>console.log(Object.keys(m)))"
# 打印 [ 'Config', 'apply', 'inject', 'name' ] 才算通过

key 池

Tavily 的额度是按 key 计的,一个账号用尽不该把整个工具带走。池按引用名轮询,并按 key 的健康状况退避:

| 情况 | 处理 | |---|---| | HTTP 200 | 该 key 解除退避;游标前移,下次调用从下一把开始(轮询铺开) | | 401 | 判为常驻失效(密钥本身被拒),本轮及后续跳过 | | 403 / 429 / 432 | 退避 cooldownSeconds,到期自动回池 | | 400 / 5xx / 传输失败 | 立即抛出,不烧池——这类失败换 key 也救不回来,试下去只会掩盖真问题 |

三个设计要点:

  1. 按引用名寻址,缓存 key 值。 credentials 服务要求"每次操作重新 resolve"(改过的凭据必须在下一次调用生效, 无需重启),所以池只保存引用名;每次调用现取现用。
  2. 指纹而非密钥。 池为每个条目存一个 FNV-1a 指纹,用来识别同一个引用名下换了一把新密钥——指纹变了就立刻 解除退避。因此"把 TAVILY_API_KEY 的值改成一个新账号"会即时生效,而不会被旧的退避状态挡住。
  3. 最后一轮兜底重试。 所有健康 key 都失败后,被退避的 key 会再试一次。配额重置后无需重启即可自动恢复, 且失败时报的是 API 原话,而不是含糊的"所有 key 都在退避中"。

全池失败时错误里逐把列出原因,便于直接定位:

tavily search: all 8 key(s) in the pool failed
  - TAVILY_API_KEY: HTTP 432 — This request exceeds your plan's set usage limit. (retry after 2026-09-14T04:59:52Z)
  - TAVILY_API_KEY_2: not configured
  ...

为什么默认池是 8 个名字

credentials 服务故意不提供引用枚举("the reference half, which has no enumeration")——配置面是从 schema 得知存在哪些引用的,而不是从服务。所以池成员必须预先声明。于是默认值直接写成 TAVILY_API_KEY、TAVILY_API_KEY_2 … TAVILY_API_KEY_8 整个家族:未配置的名字 resolve 回来是 undefined, 被池直接跳过、零成本、零报错。

因此加第二把 key 不需要改任何 composition,只要:

# ~/.dsh/.credentials.yaml
refs:
  TAVILY_API_KEY: tvly-dev-...
  TAVILY_API_KEY_2: tvly-dev-...

配置

全部可选,默认值即上文的家族。要覆盖时改用户层 ~/.dsh/profiles/<profile>/cordis.patch.yml(在所有 bundle 层之后应用),而不是改本包自带的 patch:

- id: tavily-web
  config:
    keyRefs: [TAVILY_API_KEY, TAVILY_WORK_KEY]   # 整体替换默认家族
    apiKeys: []                                  # 字面量 key,排在 keyRefs 之后;密钥更该放 seam
    cooldownSeconds: 900                         # 403/429/432 后的退避秒数

keyRefs 与代码共用一个 DEFAULT_KEY_REFS 常量,避免"schema 声明的默认"与"运行时实际生效的默认"漂移。

加载/验证

node tests/pool.test.cjs        # 离线用例;VERIFY_TAVILY_KEY=<key> 时额外跑一项真实网络用例
node tests/live-pool-check.cjs  # 真实凭据 + 真实网络,打印每次调用实际用了哪把 key

# 改了包源后:host 插件不走热重载,必须重启 dsh --profile web
# 启动日志出现 (key pool: N ref(s), cooldown 900s) 即表示新代码已加载

踩坑:curl 的错误体是合法 JSON

Tavily 的报错体(如 432 的 {"detail":{"error":"..."}})是合法 JSON,而 curl 遇到 HTTP 错误 退出码仍是 0。于是"不检查状态码"的写法会一路顺利走完:退出码 0 → JSON.parse 成功 → data.results 为 undefined → 返回 {sources: [], truncated: false}。

任何 API 错误(401/429/432)都长得像"搜到 0 条结果",无法自证。因此检索的 curl 必须带 -w '%{http_code}' 取回状态码并在解析前判定;这也是本插件唯一处对 curl 的硬性要求。