mateclaw/docs/superpowers/specs/2026-07-03-plugin-search-provider-design.md
倪程伟 0a58b3fb35
feat(plugin): 插件化搜索 Provider — PluginType.SEARCH + PluginSearchProvider SPI (#477) (#479)
* docs: add plugin search provider design spec and plan (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* feat(plugin-api): add SEARCH plugin type and PluginSearchProvider SPI (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* feat(search): make SearchProviderRegistry accept runtime plugin providers (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* test(search): cover blank plugin provider id rejection (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* feat(plugin): bridge PluginSearchProvider to the core SearchProvider chain (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* feat(plugin): registerSearchProvider lifecycle — register, disable, rollback (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* fix(plugin): preserve cause when wrapping registry conflict as PluginException (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* feat(plugin): add search provider sample plugin module (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* docs(plugin): note unused query params and narrow parse exception in search sample (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* docs(architecture): document the standalone-jar plugin system and SEARCH type (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK
2026-07-03 16:57:27 +08:00

11 KiB
Raw Blame History

插件化搜索 Provider + 搜索设置页重构 设计文档

日期:2026-07-03 状态:待评审 相关:vip.mate.tool.search(现有搜索 provider 链)、mateclaw-plugin-api(插件 SDK)、/settings/system 搜索设置区块

1. 背景与问题

1.1 自定义搜索 provider 没有插件化路径

当前 SearchProviderRegistry 通过 Spring 构造器注入 List<SearchProvider> 收集 provider,只认同一 ApplicationContext 里的 bean。要新增一个搜索源,唯一办法是在 vip.mate.tool.search 源码树里加 @Component 类并重新编译部署整个 server。

而项目已有一套真正的运行时插件系统(mateclaw-plugin-api + PluginManager):独立 jar 丢进 ~/.mateclaw/plugins/ 或工作区 plugins/,URLClassLoader 隔离加载,支持运行时 enable/disable,配置走 manifest 声明的 schema(mateclaw-plugin.json 的 config 字段)+ plugin 表 config_json 持久化 + PUT /api/v1/plugins/{name}/config 接口。但 PluginType 只有 TOOL / PROVIDER(LLM) / CHANNEL / MEMORY 四类,没有 SEARCH,PluginContext 也没有对应注册方法。

LLM provider 已有"内置 @Component 链 + 插件注册表"双轨并存的先例(ModelProviderService.pluginChatModels),搜索 provider 缺的就是同构的第二轨。

1.2 搜索设置 UI 平铺、下拉菜单硬编码

/settings/system 的搜索区块把 4 个 provider 的开关/key/url 共 9 个配置项拍平在一个列表里;主 provider 下拉菜单是写死的两个 <option>(serper/tavily),searxng/duckduckgo 无法显式选中,只能靠后端自动探测兜底;管理员也无法看到"当前实际生效的是哪个 provider"。

1.3 插件配置表单缺失(前端)

后端 PluginInfo 已返回 configSchema(来自 manifest)和脱敏后的 currentConfig,updateConfig() 已有 schema 白名单 + required 校验,前端 pluginApi.updateConfig 客户端也已存在——但 Plugins.vue 没有任何配置编辑 UI,这条链路在前端是死代码。所有类型的插件目前都无法在界面上配置。

2. 目标 / 非目标

目标

  1. 第三方以独立 jar 形式提供搜索 provider:实现 SDK 接口 + manifest 声明,丢进 plugins 目录即用,mateclaw-server 源码零改动。
  2. 搜索设置页:主 provider 选择动态化(含插件 provider 与"自动选择")、按 provider 分组折叠、显示当前实际生效的 provider。
  3. 补上 schema 驱动的插件配置表单(服务所有插件类型,不只 search)。

非目标

  • 不改内置 4 个 provider 的配置存储方式(继续走 SystemSettingsDTO / mate_system_setting)。
  • 不删除、不重命名 GET/PUT /api/v1/settings 现有字段(无破坏性改动)。
  • 不做搜索结果聚合/多 provider 并发查询。

3. 设计

3.1 SDK 侧(mateclaw-plugin-api)

新增 vip.mate.plugin.api.search 包,接口不依赖任何 server 类(jar 隔离加载下的硬约束;对比核心 SearchProvider 依赖 SystemSettingsDTO,SDK 版必须自包含):

public interface PluginSearchProvider {
    String id();                                      // 全局唯一,如 "my-search"
    String label();                                   // 显示名
    default boolean requiresCredential() { return true; }
    default int autoDetectOrder() { return 500; }     // 默认排在内置 provider(50~400)之后
    boolean isAvailable();                            // 插件自查:如 context.getConfig 拿 key 判空
    List<PluginSearchResult> search(PluginSearchQuery query);
}

public record PluginSearchQuery(String query, String freshness, String language, Integer count) {}
public record PluginSearchResult(String title, String url, String snippet, String source, String date) {}
  • PluginType 增加 SEARCH。
  • PluginContext 增加 void registerSearchProvider(PluginSearchProvider provider);。 (接口新增方法对已编译的存量插件无影响——它们不调用即可。)
  • 插件的配置(API key 等)不进搜索设置页,走插件系统自己的机制:manifest config 声明 schema,运行时 context.getConfig(key, type) 读取。职责天然分离:搜索设置页只管"选谁",插件页管"配它"。

3.2 Server 桥接侧

bridge/PluginSearchBridge.java(模式照抄 PluginChannelBridge):把 PluginSearchProvider 适配成核心 SearchProvider:

  • search(SearchQuery, SystemSettingsDTO) → 转调插件 search(PluginSearchQuery),忽略 DTO;
  • 结果转核心 SearchResult,providerId 填插件 provider id;
  • isAvailable(SystemSettingsDTO) → 委托插件无参 isAvailable();
  • 插件抛出的异常原样上抛(WebSearchService.tryProvider() 已有 catch-and-fallback 语义)。

SearchProviderRegistry 可变化:从"构造时定死的 immutable list"改为两层合并视图:

  • 基底:Spring 注入的内置 provider(不变);
  • 插件区:ConcurrentHashMap<String, SearchProvider>,新增 registerPluginProvider(SearchProvider) / unregisterPluginProvider(String id);
  • allSorted() / getById() / resolve() 全部查合并视图,排序仍按 autoDetectOrder;
  • id 冲突拒绝注册(插件 id 与内置或已注册插件 id 重复时抛 PluginException,不允许顶掉 serper 等内置项)。

生命周期(与现有四类完全对称):

  • PluginContextImpl.registerSearchProvider() → 包 bridge 后调 registry 注册,记录到 LoadedPlugin;
  • disablePlugin() 与加载失败 rollback 路径各加一个 searchProviderRegistry.unregisterPluginProvider(...)(best-effort,同现有风格);
  • 插件被 disable 后,若它正是 searchProvider 显式指定项,resolve() 因 getById() 查不到而自动落入 auto-detect 分支——行为安全,无需额外处理。

3.3 动态 provider catalog 接口

GET /api/v1/settings/search-providers(SystemSettingController,@RequireWorkspaceRole("admin")),只读:

{
  "providers": [
    { "id": "serper",   "label": "Serper (Google)", "builtin": true,  "requiresCredential": true,  "available": false },
    { "id": "my-search","label": "My Search",       "builtin": false, "requiresCredential": true,  "available": true,
      "pluginName": "my-search-plugin" }
  ],
  "resolved": { "id": "my-search", "source": "configured" }
}
  • 数据源:SearchProviderRegistry.allSorted()(合并视图,插件 provider 自动出现)+ resolve(config)(暴露"当前实际生效"与原因:configured / auto-detect / keyless-fallback)。
  • pluginName 供前端渲染"去插件页配置"跳转。
  • 不含任何敏感值。

3.4 搜索设置页重构(views/Settings/System/index.vue)

  • 主 provider 选择:选项从 catalog 接口动态渲染,新增首项"自动选择(推荐)"——对应 searchProvider=""(后端 resolve() 对空值本就走 auto-detect,无需引入 "auto" 特殊值)。下方常驻一行状态提示:✓ 当前实际生效: Xxx(原因)。
  • 分组折叠卡片:每个 provider 一张可折叠卡片,标题行 = 名称 + 徽标(已配置/未配置/生效中),默认只展开"当前生效"的那张。
    • 内置 provider:卡片内是现有的 key/url 输入框(字段与保存逻辑不变,仍走 PUT /api/v1/settings);
    • 插件 provider:卡片内不放表单,显示"该 Provider 由插件 {pluginName} 提供,请在插件页配置" + 跳转链接。
  • 现有保存语义不变(API key 仅在用户输入新值时提交)。

3.5 插件配置表单(views/Plugins.vue,纯前端)

插件卡片增加"配置"入口(有 configSchema 时显示),弹出 schema 驱动的通用表单:

  • 按 configSchema 渲染字段:secret=true → password 输入框(placeholder 显示脱敏值,留空表示不修改);其余按 type 渲染 text/number/boolean;required 标星并做前端必填校验(后端已有兜底校验);description 作为字段提示。
  • 提交走已存在的 pluginApi.updateConfig;保存后刷新列表。
  • 该表单对所有 PluginType 通用,非 search 专属。
  • 注意:manifest ConfigField.type 是自由字符串,前端对未知 type 一律降级为 text 输入。

3.6 参考实现(mateclaw-plugin-sample)

sample 模块增加一个最小 PluginSearchProvider 实现(如包装一个可配 baseUrl+apiKey 的通用 HTTP 搜索 API),manifest 声明 type: "search" + config schema——同时充当文档示例与集成测试素材。

4. 交付拆分(遵循上游单一关注点规范)

  • 上游 issue 先行:动手前在 mateaix/mateclaw 提 issue 说明设计(本文档摘要),获认可后实施。
  • PR-1(后端 + SDK):PluginType.SEARCH + SDK 接口/record + PluginSearchBridge + registry 可变化 + PluginContextImpl/PluginManager 生命周期 + sample 参考实现 + 单测。
  • PR-2(接口 + 前端):catalog 接口 + 搜索设置页分组折叠重构 + Plugins.vue schema 配置表单。PR-2 不依赖 PR-1 合并(catalog 对纯内置 provider 同样成立),但先后合并时插件 provider 自动出现在下拉中。

5. 测试

PR-1

  • registry:注册/反注册/合并排序/resolve() 三分支含插件项/id 冲突拒绝。
  • bridge:SearchQuery↔PluginSearchQuery、SearchResult 转换、异常透传。
  • 生命周期:disable 后 registry 查不到该 id;显式指定的插件 provider 被 disable 后 resolve 落回 auto-detect。
  • sample 插件 jar 端到端:打包 → 放插件目录 → 启动加载 → getAllToolCallbacks 路径外单独验证 web_search 走插件 provider。

PR-2

  • catalog 接口:内置/插件混合列表、resolved 三种 source、无敏感值泄露。
  • 前端:下拉动态渲染、"自动选择"存空串、折叠展开状态、secret 字段留空不覆盖。

6. 兼容性与风险

  • 存量插件:PluginType 加枚举值 + PluginContext 加方法,均为增量,不影响已编译插件。
  • GET/PUT /api/v1/settings 字段不动,旧前端/脚本不受影响。
  • SearchProviderRegistry 由不可变转可变:并发读多写少,ConcurrentHashMap + 每次读时合并排序(provider 总数 <10,无性能顾虑)。
  • 插件 provider 质量不可控:WebSearchService 现有 15s 超时属于各 provider 自身实现,插件侧超时由插件自负;catch-and-fallback 链保证坏插件不拖垮搜索功能(最多浪费一次尝试)。
  • 安全:插件 jar 本身即任意代码执行(现有插件系统的既定信任模型,本设计不扩大攻击面);catalog 接口仅 admin 可见。