dsh-plugin
DayuanJiang/next-ai-draw-iodsh 的 draw.io 绘图插件:通过 next-ai-drawio MCP 服务器作图,点回复里的链接在右侧栏打开实时预览,可以手动修改图,AI 的每次修改都是可以恢复的版本。需 Node.js 18+,首次使用由 npx 下载 MCP 服务器。
低 Token 联网搜索:只把结果裁成 {title, url, snippet} 三个字段返回,厂商附带的 answer / score / raw_content 等一律不进上下文。后端可在 Tavily、Brave、SearXNG、Google CSE、Exa 五种之间配置,主后端失败时按 fallback / chain 顺序自动切换到下一个可用后端(同一后端去重)。返回的 URL 与摘要经清洗,除去跟踪参数。
面向 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 条。<strong>、<p>、<style> 正文整块丢弃)、
解码 HTML 实体(' 6 个字符 → ' 1 个字符)、折叠换行/制表/NBSP/零宽字符。utm_* / fbclid / gclid / srsltid 等纯埋点参数,
保留路径、其余查询参数与 #hash;解析失败的 URL 逐字节原样返回,绝不改写。AbortController 超时;连续失败 3 次即熔断 60 秒,
冷却期内不发网络请求直接快速失败。调用方主动中断不计入熔断。fetch / AbortSignal,不引入 axios 等重库。Config 里没有一个必填字段——不填 key 也能装配,工具照常出现在模型可用的工具表里,
只在真正调用时返回「去哪儿补」的可读原因。用同一批真实脏数据(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× |
节省来自六个确定性机制,其中贡献最大的是摘要上限、结果数上限与实体/标签清洗:
maxSnippetChars 硬截断(原生工具没有这个旋钮);' → ' 即 6 字符省到 1 字符);age / score / engine / raw_content / 图片等元数据;复现方式:
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 过渡用。
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,
面板提示「配置已被其它窗口修改」并自动重读,不会静默覆盖。配置留空时按顺序回退(通用变量优先于后端专用变量):
| 用途 | 变量 |
|---|---|
| 通用密钥 | 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_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 的工具超时——先由本插件给出分类错误,宿主再兜底硬停。circuit-open 并提示「不要立即重试」。冷却期满放行一次试探,
试探失败立即重新熔断(而不是退回闭合并重新数满阈值)。aborted)属于用户/宿主行为,不算后端故障。| 错误码 | 含义 | 处置 |
|---|---|---|
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 沙箱里的加载探针 / 面板运行时冒烟 |
{ content: [{ type: 'text', text }] }。
宿主的工具流水线会按 output.schema 校验返回值,再以 (args, value) 双参调用
output.render,随后对结果执行 .some(...)。若 render 返回裸字符串,
宿主会抛 content.some is not a function 且工具链不可恢复。
本插件对畸形输入做兜底归一,任何情况下都返回合法非空块数组。apply 的返回值(ensure(async () => plugin.apply(...)))。
因此真正的卸载路径绑定在 ctx 上:工具注销器由 ctx.tools.register 返回(本身即 effect),
在途请求中止与熔断复位挂在 ctx.on('dispose', …)。同时返回幂等的 dispose 函数,
对会消费返回值的宿主与单元测试同样可用。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 作为运行时依赖。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 三项全绿。package.json 的 packageManager + devEngines 与
.npmrc 的 engine-strict=true 会拒绝 npm)。MIT License
本项目是 DeepSeek Harness 的第三方插件,与 DeepSeek 官方无隶属关系。