wigolo 插件开发实战:为 AI Agent 接入自定义搜索引擎与内容提取器
【免费下载链接】wigoloThe go-to web for your AI coding agent — local-first search, fetch, crawl & research over MCP. No API keys, no cloud, $0/query. Public beta.项目地址: https://gitcode.com/GitHub_Trending/wi/wigolo
wigolo 是一套面向 AI 编码 Agent 的本地优先检索方案,通过 MCP 提供搜索、抓取、爬取与研究能力。本文聚焦其插件体系:如何用纯 Node 模块为 wigolo 接入自定义搜索引擎(内部 Wiki、私有索引、垂直领域引擎)和站点级内容提取器(修复提取质量差的站点),并深入解析插件加载、校验、注册的完整链路与失败隔离机制。读完本文,你将掌握从wigolo plugin add到编写完整插件、再到用plugin validate排查问题的全套实战能力。
插件体系一览:两类插件、一个目录
wigolo 从~/.wigolo/plugins目录加载两类插件,目录位置可用环境变量WIGOLO_PLUGINS_DIR覆盖:
- 搜索搜索引擎(search engine):加入多引擎搜索调度池,与内置引擎并列参与融合、去重与端上重排;
- 内容提取器(content extractor):在通用提取流水线之前获得优先机会,把页面转成 Markdown 结构化结果。
两类插件都是普通 Node 模块——不需要构建步骤,不依赖任何框架,一个目录加一个入口文件即可。
从配置实现看,插件目录的解析位于 src/config.ts:优先读取WIGOLO_PLUGINS_DIR(若以~开头会展开为主目录),否则回退到dataDir/plugins(即~/.wigolo/plugins)。因此安装插件前确认WIGOLO_PLUGINS_DIR是否被设置,就能准确判断实际生效的插件目录。
插件管理命令:add / list / validate / remove
插件管理的 CLI 入口在 src/cli/plugin.ts,完整用法如下:
wigolo plugin add https://github.com/you/your-plugin # clone;安装前会弹出信任确认 wigolo plugin list [--json] # 列出已安装插件 wigolo plugin validate [--json] # 校验插件能否正确加载与导出 wigolo plugin remove <name> # 卸载插件plugin add:先确认信任,再执行 clone
plugin add会把 git 仓库克隆进插件目录。因为插件是会在 wigolo 进程内运行、且拥有你的凭据与网络访问权限的代码,所以命令在 clone 之前必须经过用户确认——只安装你信任或阅读过源码的插件。
从 src/cli/plugin.ts 的实现看,信任边界被刻意做得非常显眼:
- 未传
--yes时,会先在 stderr 打印安装横幅,展示url、解析出的repo名和目标目录target,并明确警告"克隆的仓库会在每次 wigolo 服务启动时以 Node 代码运行"; - 随后从 TTY 读取一行确认(
Install this plugin? [y/N]),只有输入y/yes才继续; - 非交互式(CI、管道)环境下默认拒绝安装,必须显式传
--yes才会放行;WIGOLO_PLUGIN_AUTO_YES=1环境变量是脚本化安装的等价开关; - clone 使用
git clone --depth 1(浅克隆),超时 60 秒。
安装完成后,命令会检查目标目录的package.json是否存在、是否有main字段,缺任一都会给出 WARNING,提示插件可能无法正常加载——这是 add 阶段的第一道体检。
plugin list:结构化盘点
list扫描插件目录,读取每个子目录的package.json,输出插件名与版本(缺失时回退为目录名与unknown)。加--json时向 stdout 输出单条 JSON 文档({"plugins": [...]}),便于脚本消费。
plugin validate:加载与导出契约校验
validate会对每个已安装插件做静态校验:package.json是否存在、main字段是否存在且指向的文件真实存在于磁盘。值得注意的是,注释明确说明这是"lint 而非 load"——它刻意不 import 插件(import 会执行插件代码),因此可以在不运行任意代码的前提下给出快速体检。命令以退出码表达结果:全部通过返回 0,任一失败返回 1;--json模式输出{"status": "ok"|"error", "plugins": [...]}。
plugin remove:安全卸载
remove会先做名称合法性校验(拒绝路径分隔符、..、绝对路径),再删除对应目录;插件不存在时报错退出。
插件包结构:package.json + 入口模块
一个插件就是一个目录,其package.json的main指向入口模块:
{ "name": "my-wigolo-plugin", "version": "1.0.0", "main": "index.mjs" }入口模块导出searchEngine、extractor,或两者同时导出。导出会在加载时被校验,无效的插件会被报告并跳过,绝不会拖垮服务器。
加载器 src/plugins/loader.ts 的具体流程是:读取并解析package.json(缺失或 JSON 解析失败 → 记错误并跳过);要求存在main字段;用pathToFileURL把入口路径转成file://URL 后await import()——这意味着插件可以用 ESM,也可以从 CommonJS 模块导出。加载成功的模块进入validatePluginExports校验。
搜索引擎插件:把私有索引送进多引擎调度池
搜索引擎插件遵循的契约定义在 src/types.ts:
interface SearchEngine { name: string; search(query: string, options?: SearchEngineOptions): Promise<RawSearchResult[]>; }其中SearchEngineOptions(src/types.ts)携带调度器传入的丰富上下文:maxResults(结果数上限)、timeRange、language、timeoutMs、includeDomains/excludeDomains、fromDate/toDate、category(general/news/code/docs/papers/images)、country(ISO 3166-1 alpha-2 国家码)等。一个尊重这些选项的插件,才能与内置引擎在同等条件下公平竞争。
返回值RawSearchResult(src/types.ts)的核心字段为title、url、snippet、relevance_score、engine;可选的published_date(ISO 日期串)会在解析成功时驱动freshness_signal;image_url/image_alt会在调用方开启include_images时汇入图片聚合。evidence_score、_score_breakdown等字段则由核心编排器在排序后统一填充,插件无需关心。
一个完整可用的搜索引擎插件
仓库自带的 examples/plugin-search-engine/index.mjs 就是整个插件的全部内容,可直接复制作为起点:
export const searchEngine = { name: 'example-search-engine', async search(query) { return [ { title: `Example result for ${query}`, url: 'https://example.com/search-engine-example', snippet: 'Minimal search engine plugin example.', relevance_score: 1, engine: 'example-search-engine', }, ]; }, };就是这样——一个内部 Wiki、一个私有索引、一个垂直领域引擎,用不到 100 行就能进入调度池。插件返回的结果会与内置引擎一起走完全相同的**融合(fusion)、去重(dedup)、端上重排(on-device reranking)**流程,并像其他引擎一样出现在engines_used/engine_telemetry字段中。
搜索引擎插件的运行时接入
在服务器启动阶段(src/server.ts),wigolo 先构造内置引擎(BingEngine、DuckDuckGoEngine),随后调用loadPlugins();每个通过校验的插件搜索引擎会被推入searchEngines数组,参与后续的统一调度。这也解释了为什么插件引擎的返回结果能天然进入engines_used(贡献了至少 1 条去重后结果的引擎)与engine_telemetry(每个被尝试引擎的原始延迟、结果数、结果、dedup_kept)——因为它在调度器眼中与内置引擎没有任何区别。
内容提取器插件:站点级修复的优先通道
提取器插件的契约同样定义在 src/types.ts:
interface Extractor { name: string; canHandle(url: string, html?: string): boolean; extract(html: string, url: string): ExtractionResult | null; }canHandle(url, html?)决定你的提取器认领哪些 URL——典型场景是"公司文档平台的特殊 DOM 结构";extract(html, url)返回结构化结果;返回null则把页面交还给内置提取器组合(defuddle → readability → turndown 的兜底链)。
插件提取器在通用流水线之前被咨询,所以站点专属提取器正是修复"某个站点提取质量差"的官方手段。
ExtractionResult(src/types.ts)要求返回title、markdown、metadata(可选description/author/date/language/og_image/canonical_url/keywords等)、links、images、extractor类型。额外的site_data字段(Reddit、YouTube、Amazon 等站点提取器会填充的每站点结构化 JSON)可被后续调用方直接消费,而无需再从 Markdown 里反向抓取。
提取器插件的调用链
从源码看,插件提取器与内置站点提取器共享同一注册表:服务器启动时(src/server.ts)对每个插件提取器调用registerExtractor(ext),而 src/extraction/pipeline.ts 的registerExtractor是兼容别名,真正写入 src/extraction/v1/site-extractors.ts 的共享列表——因此 v1 路由同样能看到插件注册的提取器。
在 v1 提取路由中(src/extraction/v1/routed.ts),trySiteExtractors用extractors.find((e) => e.canHandle(url, originalHtml))找到第一个认领该 URL 的提取器并调用其extract;没有匹配则落入 defuddle/readability/turndown 的兜底链。这意味着:插件提取器拥有最高优先权,canHandle返回true即接管该 URL 的提取任务。
加载与校验机制源码解析
插件加载的核心实现在 src/plugins/loader.ts,整体结构清晰:
- 读取配置,得到插件目录;目录不存在或读取失败时静默返回空结果(服务器正常启动);
- 遍历目录下每个子目录(
statSync会跟随符号链接,因此符号链接的插件目录同样被支持); - 读取
package.json——缺失或解析失败记入错误列表;无main字段报"no main field";main指向的文件不存在报"entry point not found"; - 通过
import()加载入口模块,抛错则记入错误并continue; - 用
validatePluginExports(src/plugins/validate.ts)校验导出,收集所有不满足契约的原因。
校验规则本身很直接:extractor导出必须是对象,且name为非空字符串、canHandle与extract均为函数;searchEngine导出同理要求name非空、search为函数;两者都不合法时给出"neither a valid extractor nor a valid searchEngine"的提示。校验通过后进入 src/plugins/registry.ts 的PluginRegistry登记。
值得注意的细节是名称去重:loader 与 registry 两层都会用Set拦截同名 extractor / search engine,重复名称的注册会被警告并跳过,避免调度时发生歧义。加载完成后,服务器日志会汇总"加载了 X 个提取器、Y 个搜索引擎、Z 个错误",同时plugin validate会以退出码 1 反映失败状态,便于 CI 集成。
失败行为:端到端防御性设计
插件加载是全程防御性的:package.json缺失、main错误、import 抛异常、导出无效——每一种失败都会在plugin validate(以及服务器日志)中产生针对该插件的独立错误信息,而其他所有插件与服务器本体继续正常工作。
这一设计的证据贯穿全链路:
- 加载器对单个插件任何一步失败都
continue到下一个,绝不中断整体循环(src/plugins/loader.ts); - 服务器启动对
loadPlugins()的整体调用也包在 try/catch 里,即便最坏情况出现,也只是记录错误而不会终止启动(src/server.ts); plugin validate的静态校验甚至刻意不 import 插件,把"检查插件"与"执行插件"彻底分离,让体检过程本身零风险。
实战路线:从复制示例到生产插件
- 复制起点:拷贝 examples/plugin-search-engine/(含
index.mjs与配套package.json),按需改名; - 本地调试:把插件目录放入
~/.wigolo/plugins(或WIGOLO_PLUGINS_DIR指向的目录),重启 wigolo,观察日志中"loaded plugin search engine"; - 静态体检:
wigolo plugin validate确认契约完整;非交互环境直接wigolo plugin add <git-url> --yes走脚本化安装; - 发布与安装:推送到 git 仓库后,用
wigolo plugin add <git-url>在目标机器安装(会再次经过信任确认); - 回归检查:仓库的插件测试(见 tests/integration/plugins/ 与 tests/unit/plugins/)覆盖了加载、校验、注册的典型路径,可作为你编写插件时验证行为预期的重要参考。
插件目录相关的问题排查,可结合 docs/troubleshooting.md;插件体系与搜索、提取功能的整体关系,见 docs/README.md。
【免费下载链接】wigoloThe go-to web for your AI coding agent — local-first search, fetch, crawl & research over MCP. No API keys, no cloud, $0/query. Public beta.项目地址: https://gitcode.com/GitHub_Trending/wi/wigolo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考