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

dsh-plugin-tlsearch

v1.0.0

Published

Low-token web search for dsh: compact, sanitized {title, url, snippet} results with a pluggable, auto-failing-over backend chain (Tavily / Exa / SearXNG / Brave / Google CSE) and a token budget you control

Readme

dsh-plugin-tlsearch

面向 DeepSeek Harness 的低 Token 消耗联网搜索插件。

把「一次搜索」塞进模型上下文的东西压到最少:只返回清洗过的 { title, url, snippet }, 没有提供方散文、没有 Raw HTML、没有埋点参数、没有每轮重复的引用指引。 后端可插拔(Tavily / Brave / SearXNG / Google CSE / Exa),端点可自建(SearXNG、new-api 等中转网关)。 面板在宿主的 设置 →「联网搜索」,全部配置项都能在那里改,改完立即写回配置并生效。 适配 DSH 0.2.1-rc.2(dsh.client + external: ["react"])。


✨ 核心特性

  • 结构化最小输出:每条结果最多两行(N. 标题 — URL + 缩进摘要),零装饰词、零填充句。
  • 摘要硬上限:maxSnippetChars(默认 250)逐条截断并尽量落在词边界,Token 成本可预算、可封顶。
  • 结果数硬上限:maxResults(默认 5,上限 10)——原生 web_search 默认 8 条。
  • 清洗是确定性的:剥离 HTML 标签(<strong>、<p>、<style> 正文整块丢弃)、 解码 HTML 实体(&#x27; 6 个字符 → ' 1 个字符)、折叠换行/制表/NBSP/零宽字符。
  • URL 减负:剔除 utm_* / fbclid / gclid / srsltid 等纯埋点参数, 保留路径、其余查询参数与 #hash;解析失败的 URL 逐字节原样返回,绝不改写。
  • 先去重再截断:按归一化后的 URL 去重,避免同一篇文章的多个镜像先占满名额再被去重。
  • 超时 + 熔断:每次请求挂 AbortController 超时;连续失败 3 次即熔断 60 秒, 冷却期内不发网络请求直接快速失败。调用方主动中断不计入熔断。
  • 零运行时依赖:网络层直接用 Node 原生 fetch / AbortSignal,不引入 axios 等重库。
  • 错误可据以行动:缺凭据、端点错、非 JSON、限流、超时各有分类错误码与修复指引。
  • 零必填配置:Config 里没有一个必填字段——不填 key 也能装配,工具照常出现在模型可用的工具表里, 只在真正调用时返回「去哪儿补」的可读原因。
  • 面板内即改即生效:全部字段都是 volatile,Settings 页改完下一次调用就按新值跑(不重载插件); 密钥走宿主脱敏通道,只显示「已保存」不回显。

💰 为什么更省 Token(可复现的度量)

用同一批真实脏数据(8 条 Brave 风格原始结果:带 <strong> 高亮、HTML 实体、 埋点参数、均值 372 字符的长摘要)走真实管线对比:

| 指标 | 原生 web_search 风格 | tlsearch 默认 | 差异 | |---|---|---|---| | 结果条数 | 8 | 5 | −37% | | 单条摘要字符(均值) | 372(未清洗、无上限) | 248(清洗 + 250 上限) | −33% | | 单条 URL 字符(均值) | 105(含 utm_*/fbclid) | 34 | −68% | | 总字符数 | 4346 | 1608 | 2.70× | | 粗估 Token(英文 ≈ 4 字符/token) | ≈ 1087 | ≈ 402 | 2.70× |

节省来自六个确定性机制,其中贡献最大的是摘要上限、结果数上限与实体/标签清洗:

  1. 摘要按 maxSnippetChars 硬截断(原生工具没有这个旋钮);
  2. 结果数默认 5 条(原生默认 8 条);
  3. 剥离 HTML 标签并解码实体(&#x27; → ' 即 6 字符省到 1 字符);
  4. 剔除 URL 埋点参数;
  5. 不返回 age / score / engine / raw_content / 图片等元数据;
  6. 「引用来源」的整句指引放在工具描述里只付一次,而不是每次调用都重复一遍。

复现方式:packages/tlsearch 构建后,用 normalizeHits + renderOutcome 处理同一批载荷即可 得到上表数字(本插件导出这两个函数,便于自行核对与调参)。


🔌 支持的后端

| 后端 | 密钥 | 默认端点 | 免费额度 | 说明 | |---|---|---|---|---| | tavily(默认) | 必需 | https://api.tavily.com | 1000 credits/月(1 credit = 1 次搜索) | 结构化 JSON、字段干净;密钥同时走 Authorization 头与请求体,兼容自建/中转网关 | | searxng | 不需要 | 无(必须自配) | 无限(自建) | 自托管元搜索,聚合 Google/Bing/DDG;baseUrl 必填,并需在实例 settings.yml 的 search.formats 中启用 json | | google | 必需(key + cx) | https://www.googleapis.com | ⚠️ 新客户已无法开通 | Google Custom Search JSON API。自 2026 年起不再向新客户开放(官方文档:"This API is not available for new customers"),并将于 2027-01-01 停服;新建的 Programmable Search Engine 只能做站点搜索(≤50 域名),无法搜整个网络。适配器保留仅供存量老 CSE 在停服前使用;新项目请勿投入。实测不具备资格的项目会返回 403 This project does not have the access to Custom Search JSON API(本插件归类为 config 错误并如实说明) | | brave | 必需 | https://api.search.brave.com | 以官方定价页为准(条款多次调整) | 请求时关闭 text_decorations(源头不产生 <strong>)并限定 result_filter=web | | exa | 必需 | https://api.exa.ai | $10 credits/月 ≈ 1400 次 | 语义/神经检索,适合「找概念、找相似」;搜索结果默认不含正文,本插件显式请求 contents.text 取摘要(Exa 对 contents 单独计费 $1/1k 页) |

计费口径差异很大,务必分清:Tavily 与 Google 按请求计费(与 maxResults 无关, 调大条数不花额度只花 Token);Exa 的搜索与 contents 分开计费。


🔀 调度规则(四个服务分别在什么时候用)

插件维护一条有序后端链:[0] 是主后端,其后按 chain 数组顺序排列。 搜索时从链首开始依次尝试,第一个成功的就返回。

一句话规则

按链的顺序依次降级。前面的不可用(失败 / 熔断 / 配额耗尽 / 缺凭据),就落到下一个。

推荐顺序

- id: dsh-plugin-tlsearch
  config:
    provider: tavily          # 1
    apiKey: "tvly-…"
    chain:
      - provider: exa         # 2
        apiKey: "…"
      - provider: searxng     # 3
        baseUrl: "http://localhost:8080"

| 顺位 | 后端 | 什么时候真正被使用 | 额度 | 为什么排在这里 | |---|---|---|---|---| | 1 | tavily | 每次搜索的首选 | 1000 credits/月 | 按请求计费、字段干净、质量稳定,是这三家里最适合当主力的一档 | | 2 | exa | Tavily 不可用时 | ≈833/月 | 语义检索,检索方法与前一级不同 —— 前一级失效时它仍有独立价值 | | 3 | searxng | 商业额度都耗尽 / 全部失效时的永久地板 | 无限 | 唯一不会用完的,放最后保证「永远还能搜」 |

为什么把有额度的放前面、无限免费的放最后? 因为免费月度额度按月过期,不用就是浪费; 而 SearXNG 是自建的、不会枯竭。所以正确策略是「先把会过期的额度烧完,把无限的留在最后兜底」, 而不是反过来「为了省钱先用免费的」—— 后者会让你每月白扔近 2000 次商业额度,还一直用质量最不稳定的那一层。

关于 Google CSE:曾经的「3000/月、真实 Google 结果」是最优选择,但 Custom Search JSON API 已不再向新客户开放(官方文档明确写着 "This API is not available for new customers"),并将于 2027-01-01 停服;新引擎只能做站点搜索(≤50 域名), 无法搜整个网络。因此不建议把它放进调度链 —— 适配器保留只为存量老 CSE 过渡用。

四条运行期规则

  1. 失败即降级。除「调用方主动中断」外,任何失败都改试下一级 —— 包括缺凭据、配额耗尽、 正在熔断中。这几项恰恰是最需要下一级救场的场景。
  2. 每级一个独立熔断器(连续失败 3 次 → 冷却 60 秒)。共用是错的:SearXNG 挂掉会把 健康的 Tavily 一起熔断,正好摧毁分级互补的全部价值。 副产品:Google 的日额度用尽后会连续失败,从而被熔断 60 秒,于是同一批搜索会直接 跳过它 —— 不必等你自己发现「今天已经搜了 100 次」。
  3. 调用方中断不降级。模型换了目标或宿主超时,用户已经不想要这次搜索了,不该再去打下一级。
  4. 只有全部失败才报错。此时抛合并错误,把每一级的原因都摆出来 (all configured search backends failed — Google [http]: … | Tavily [timeout]: …); 只配了一级时则原样上抛,保留精确的错误码与文案。

其它细节:链上重复的 provider 只保留首次出现;fallback 是「单个备用」的简写, 等价于 chain 的第一个元素(兼容旧配置,两者同时存在时顺序为 fallback 在前、chain 在后)。

这套规则长什么样

宿主日志里有一行就是调度规则的可执行说明:

[tlsearch] 装配: 主=google@https://www.googleapis.com → 备1=tavily@https://api.tavily.com
           → 备2=exa@https://api.exa.ai → 备3=searxng@http://localhost:8080

而每一次降级都会留痕,否则你永远查不出「为什么这次结果风格变了」:

[warn] [tlsearch] Google 搜索失败(http),改试备用后端: google quota exhausted (HTTP 403) …
[info] [tlsearch] Tavily 返回 5 条结果 [via Tavily](query="…")

链上某一级配置不完整时只记 info(搜索仍能工作,只是少一层兜底); 只有全部不可用才升级为 warn,以保证那条告警的信噪比。


📊 额度自查工具 tlsearch_usage

配置里存在 Tavily 后端时,会额外注册一个无入参的 tlsearch_usage 工具, 返回一行额度信息:

Tavily "Researcher": 137/1000 credits used this cycle; 863 remaining (each search costs 1 credit).

用途:让 agent 在搜索开始出现配额/限流错误时(或准备发起大批搜索前)自己查一下余量, 据此收敛搜索频率,而不是把额度打空后才发现。

设计取舍:只在配置了 Tavily 时注册。其余后端(Brave / SearXNG / Google / Exa) 没有机器可读的额度接口,注册一个永远只会说「请去控制台看」的工具纯粹是每轮 Token 浪费。 被调用时若后端确实不支持,会如实说明而不是编一个看起来合理的数字—— 一个假的剩余额度比没有额度信息更糟。


📦 安装

本包是 bundle 形态(package.json 的 dsh.bundle.patch 指向包内自带的 cordis.patch.yml)。因此只要被列进 profile 的 dsh.profile.bundles 就会自动激活, 不需要再往 profile 的 cordis.patch.yml 写挂载条目。

方式一:一条命令安装并激活(推荐)

dsh plugin --profile web add dsh-plugin-tlsearch

dsh plugin add 会自动装依赖并把该包归并进 dsh.profile.bundles(幂等)。bundle 形态 无需额外补丁条目,这正是 dsh plugin 分类器认可的形态。dsh plugin --profile web list 可核对激活状态,dsh plugin --profile web remove dsh-plugin-tlsearch 可一键摘除。

方式二:手工安装(等价)

cd ~/.dsh/profiles/web
pnpm add dsh-plugin-tlsearch

然后把它加进 ~/.dsh/profiles/web/package.json 的 dsh.profile.bundles 数组:

"dsh": { "profile": { "bundles": ["…", "dsh-plugin-tlsearch"] } }

用本地源码调试时,依赖写成 "dsh-plugin-tlsearch": "file:D:/Code/my-dsh-plugins/packages/tlsearch", 并且同样必须出现在 dsh.profile.bundles 里才会生效 —— 只装依赖不激活是不会生效的。


⚙️ 配置

零必填:不写任何配置项,包被挂载即可调用 search。密钥按「设置页 → 环境变量 → 备用后端 → 后端链」顺序解析,全都缺失时只会在工具调用处返回一条可读的「后端不可用」, 不会影响宿主启动与加载。

配置有两条路径:宿主 GUI 的 设置 →「联网搜索」, 或在 Profile 目录下的 ~/.dsh/profiles/web/cordis.patch.yml 里按 id 覆盖 (注意是覆盖,不是再插一条 - insert: —— 挂载已由 bundle 完成,重复插入会挂载两次):

- id: dsh-plugin-tlsearch
  config:
    provider: searxng             # tavily | searxng | google | brave | exa
    baseUrl: "http://localhost:8080"   # SearXNG 必填;也可指向自建代理
    # apiKey: "…"                 # SearXNG 不需要;其余后端必填
    # cx: "…"                     # 仅 provider=google 需要(CSE 引擎 ID)
    fallback:                     # 可选:主后端失败/熔断时自动改用备用后端
      provider: tavily
      apiKey: "tvly-xxxxxxxxxxxx"
    maxResults: 5                 # 1-10,默认 5
    maxSnippetChars: 250          # 单条摘要字符上限,默认 250(控制 Token 的主旋钮)
    timeoutMs: 15000              # 单次请求超时(毫秒),默认 15000
    outputFormat: markdown        # markdown(最省)| json
    language: ""                  # 如 zh / en;留空由后端判定
    includeAnswer: false          # 是否附带后端的一句话答案(默认 false 以省 Token)

也可以完全不在这里写 apiKey,改用环境变量(见下节),配置层就不必碰密钥。

配置项

| 配置项 | 默认值 | 说明 | |---|---|---| | provider | tavily | 主后端:tavily / searxng / google / brave / exa | | apiKey | '' | 主后端密钥;留空时回退环境变量(SearXNG 可留空) | | baseUrl | 后端默认端点 | 主后端端点根地址;SearXNG 必填 | | cx | '' | Google CSE 引擎 ID;provider=google 时必填(与 apiKey 缺一不可) | | fallback | none | 单个备用后端(简写,等价于 chain 的第一个元素)。字段同 chain 的元素 | | chain[] | [] | 后备后端链:主后端失败/熔断时按数组顺序依次尝试。每项含 provider / apiKey / baseUrl / cx。重复的 provider 只保留首次出现 | | maxResults | 5 | 返回条数上限(钳制在 1-10) | | maxSnippetChars | 250 | 单条摘要字符上限(钳制在 40-2000) | | timeoutMs | 15000 | 单次请求超时(钳制在 1000-60000) | | language | '' | 检索语言(Tavily 不发送该字段) | | includeAnswer | false | 是否附带一句话答案(Tavily answer / SearXNG answers) | | outputFormat | markdown | markdown(最省 Token)或 json(结构化,便于精确解析) |

越界值一律钳制而非报错;null / 空串等「未提供」形态退回默认值而不是被压成下限。 配置不完整不会导致插件装载失败——工具照常注册,调用时返回分类错误, 便于模型把「去哪儿补配置」原样转述给你。

设置面板(改完即生效,不用重启)

在宿主的 Settings > Plugins > tlsearch(settings.section 插槽,id=tlsearch、order=31) 里可以直接编辑上表全部字段——没有只读项,也没有需要改 YAML 才能动的字段。

  • 即时生效:写入走 remote.settings.mutate(ns, [{op:'set', path, value}], revision), 宿主把变更下发到 volatile 引用(cosmokit.volatile.write 全局符号),插件每次工具调用 都重新读取当前配置(ctx.fiber.config 优先,回退到装配时收到的对象),不缓存快照。 因此改 maxResults / provider / 超时后,下一次调用立刻按新值跑,无需重载插件。
  • 密钥不回显:apiKey 与 fallback.apiKey 标了 role('secret'),宿主 describe() 会把它们 从返回值里摘掉、只给 secrets[].set 标记。面板因此显示「已保存(不回显)」,留空即不动它, 点「清空」才真正 unset;写入用路径化操作,未触碰的密钥不会跟着请求走。
  • 并发安全:写入带上一次读到的 revision;被别处改动时宿主抛 SETTINGS_CONFLICT, 面板提示「配置已被其它窗口修改」并自动重读,不会静默覆盖。
  • 后端链在面板里是一个 JSON 文本域(数组元素无法逐字段脱敏,所以整段编辑、失焦时校验)。

环境变量回退

配置留空时按顺序回退(通用变量优先于后端专用变量):

| 用途 | 变量 | |---|---| | 通用密钥 | TLSEARCH_API_KEY | | 通用端点 | TLSEARCH_BASE_URL | | 通用 Google 引擎 ID | TLSEARCH_CX | | Tavily | TAVILY_API_KEY、TAVILY_BASE_URL | | Brave | BRAVE_SEARCH_API_KEY、BRAVE_API_KEY、BRAVE_BASE_URL | | SearXNG | SEARXNG_BASE_URL | | Google | GOOGLE_CSE_API_KEY、GOOGLE_API_KEY、GOOGLE_CSE_ID、GOOGLE_CSE_CX | | Exa | EXA_API_KEY、EXA_BASE_URL |

与原生 web_search 的关系

两者可以并存(模型按需选用)。若要替代原生搜索,在 dsh-tool-web 的配置里关掉它的搜索工具:

- id: tool-web
  config:
    search: false   # 保留 web_fetch,仅关闭原生 web_search

🧾 输出格式

模型看到的只有结果本身,没有任何前言后语。

outputFormat: markdown(默认,最省)

1. DeepSeek Harness 文档 — https://example.com/docs
   紧凑的 agent 运行时,支持工具注册与会话事件流。
2. 插件开发指南 — https://example.com/plugins

摘要为空时整行省略,不会渲染出只有空白的缩进行。零命中时返回 No results for "查询串".(回显查询串且截断到 120 字符)。

outputFormat: json

[{"title":"DeepSeek Harness 文档","url":"https://example.com/docs","snippet":"紧凑的 agent 运行时…"}]

恒为 [{ title, url, snippet }] 裸数组;开启 includeAnswer 时答案单独成块 ({"answer":"…"}),结果块的形状不会漂移,解析方无需分情况处理。


🛡️ 超时、熔断与错误分类

  • 超时:每次请求挂 AbortController,timeoutMs 到点即中止; 同时向宿主声明 timeoutMs + 5s 的工具超时——先由本插件给出分类错误,宿主再兜底硬停。
  • 熔断:连续失败 3 次即熔断,冷却 60 秒。冷却期内不发网络请求, 直接返回 circuit-open 并提示「不要立即重试」。冷却期满放行一次试探, 试探失败立即重新熔断(而不是退回闭合并重新数满阈值)。
  • 不计入熔断:调用方主动中断(aborted)属于用户/宿主行为,不算后端故障。
  • 体积上限:响应超过 2,000,000 字符直接拒绝,避免异常端点拖垮内存与 JSON 解析。

| 错误码 | 含义 | 处置 | |---|---|---| | config | 配置不合法(缺 baseUrl、查询为空) | 按错误信息补齐配置 | | credential | 缺密钥或密钥被拒(HTTP 401/403) | 检查 apiKey 或环境变量 | | timeout | 后端未在 timeoutMs 内响应 | 调大超时或换后端;会计入熔断 | | aborted | 调用方主动中断 | 无需处理;不计入熔断 | | http | 其他 HTTP 错误(含 429 限流) | 看错误信息中的响应摘要 | | network | 传输层失败(DNS/连接/TLS) | 检查网络与端点可达性 | | response | 响应不是合法 JSON 或超体积 | SearXNG 需启用 json 格式 | | circuit-open | 熔断中 | 等待冷却,或换后端 |


🔧 后端配置要点

SearXNG(最常见的坑):默认只对浏览器提供 HTML,必须在其 settings.yml 中启用 JSON:

search:
  formats:
    - html
    - json

未启用时本插件会返回明确提示(而不是把 HTML 页面当成结果解析)。

自建 / 中转网关:baseUrl 支持带路径前缀,例如 https://proxy.example/tavily; Tavily 的密钥同时以请求头与请求体两种形式携带,因此 new-api 这类只读请求体的网关也能直接用。

Brave:需要有效的订阅令牌(X-Subscription-Token);本插件已在请求侧关闭文本装饰 并限定 result_filter=web,避免 <strong> 标记与 infobox/FAQ 噪声进入上下文。


🛠️ 开发

本仓库统一使用 pnpm(见根目录 AGENTS.md):

# 类型检查(服务端 tsc --noEmit + 客户端 tsc -p tsconfig.client.json,零错误)
pnpm --filter dsh-plugin-tlsearch run typecheck

# 构建产物:dist/index.js (ESM) + dist/index.cjs (CJS) + dist/index.d.ts + web/client.js(面板)
pnpm --filter dsh-plugin-tlsearch run build

# 单元测试(192 个用例,零网络:全部使用假 fetch 与假 Cordis 上下文)
pnpm --filter dsh-plugin-tlsearch run test

# 产物契约自检:volatile 标记 / 脱敏标记 / 零运行时依赖 / 客户端模块协议
pnpm --filter dsh-plugin-tlsearch run assert:build

# 面板冒烟:在最小 shell 桩里真跑一遍 web/client.js 的 apply()
pnpm --filter dsh-plugin-tlsearch run smoke:client

# 全工作区递归构建
pnpm run build

源码结构

| 文件 | 职责 | |---|---| | src/index.ts | 插件入口:Config schema(全字段 volatile)、apply 装配、实时配置读取、生命周期与可逆注销 | | src/tool.ts | 模型可见工具:定义、参数校验、熔断判定、在途请求登记、错误分类(每次调用现读配置) | | src/providers.ts | 五个后端适配器 + 配置解析 + 归一化管线(清洗/去重/截断) | | src/http.ts | 超时、体积上限、熔断状态机、错误分类 | | src/sanitize.ts | HTML/实体/空白清洗、词边界截断、URL 埋点参数剔除 | | src/format.ts | 工具返回契约收敛层 + 紧凑渲染(markdown/json) | | src/types.ts | 领域模型与 Cordis 上下文类型合并 | | src/volatile.ts | volatile 引用机制:schema 标记、校验时按路径包引用、读取时解引用(跨包靠全局 Symbol 互认) | | src/client/index.tsx | 面板入口:inject = ['slots','locale','remote']、settings.section 注册、settings.tlsearch 文案 | | src/client/store.ts | 面板数据层:remote.settings.describe/mutate、revision 冲突处理、脱敏标记、变更订阅 | | src/client/SettingsPage.tsx | 面板 UI:分组表单、密钥「已保存」状态、后端链 JSON 编辑、可用性判定 | | src/client/{model,i18n,styles,host,entry}.ts | 表单模型/默认值/钳制、zh+en 词表、样式、宿主结构类型、bundle 入口 | | scripts/build-client.mjs | esbuild 打 src/client/entry.ts → web/client.js,包成 window.__ModuleLoader__.load({id, factory}) | | scripts/{assert-build,probe-dist,smoke-client}.mjs | 产物契约自检 / 无 node_modules 沙箱里的加载探针 / 面板运行时冒烟 |

四个值得记录的宿主事实

  1. 工具返回必须是 { content: [{ type: 'text', text }] }。 宿主的工具流水线会按 output.schema 校验返回值,再以 (args, value) 双参调用 output.render,随后对结果执行 .some(...)。若 render 返回裸字符串, 宿主会抛 content.some is not a function 且工具链不可恢复。 本插件对畸形输入做兜底归一,任何情况下都返回合法非空块数组。
  2. Cordis 3.0.0 会丢弃 apply 的返回值(ensure(async () => plugin.apply(...)))。 因此真正的卸载路径绑定在 ctx 上:工具注销器由 ctx.tools.register 返回(本身即 effect), 在途请求中止与熔断复位挂在 ctx.on('dispose', …)。同时返回幂等的 dispose 函数, 对会消费返回值的宿主与单元测试同样可用。
  3. 设置表单只为 volatile 字段生成,且 volatile 不能套在数组元素上 (dsh-app-boot 的 validateVolatilePlacement:volatile fields require a fixed object path without an enclosing volatile field)。所以 chain 是整节点 volatile,元素内部不能再标; fallback 只标 4 个叶子、容器保持普通对象,面板才会渲染成 4 个字段而不是一段 JSON。 引用本身是 { get(), [Symbol.for('cosmokit.volatile.write')] } —— 符号是全局的, 因此本包自带的实现与宿主(fork 版 cosmokit)能互认,无需把 fork 作为运行时依赖。
  4. 客户端面板的注入是白名单代理:只有写进 export const inject 的服务能读, 读未声明的会直接抛(?. 挡不住)。所以 inject = ['slots','locale','remote'], 而 logger 这类可选服务一律走 ctx.get('logger')。web/client.js 必须是 window.__ModuleLoader__.load({ id: '<包名>', factory: (require) => … }) 的 IIFE, id 与包名不一致时宿主报 loaded without registering …;react 通过 dsh.client.external 声明为外部依赖。

🤝 贡献

欢迎 Issue 与 PR:https://github.com/ltl0312/my-dsh-plugins

  • 新增搜索后端:在 src/providers.ts 实现 ProviderAdapter(build + parse)并注册进 PROVIDERS;清洗、去重、截断、熔断、限流由共享管线负责,无需重复实现。
  • 提交前请确保 typecheck / build / test 三项全绿。
  • 本仓库一律使用 pnpm(package.json 的 packageManager + devEngines 与 .npmrc 的 engine-strict=true 会拒绝 npm)。

📄 License

MIT License

本项目是 DeepSeek Harness 的第三方插件,与 DeepSeek 官方无隶属关系。