搜索框集成

这一页讲插件与主搜索框的三项交互能力,都在 apiLevel 7 引入:

能力 权限 做什么
onMainPush feature:mainpush 往搜索结果里推送条目
setFeature / getFeatures / removeFeature feature:dynamic 运行期增删自己的搜索入口
onPluginReady 不需要 宿主告诉插件「代码已就绪」

动态增删搜索入口的完整说明见动态指令,本页重点讲另外两项。

往搜索框推送结果(onMainPush)

用户在搜索框打字时,宿主会同步调用插件注册的回调,插件返回一组结果项,这些结果会混进搜索结果列表;用户选中某一条后,宿主再回调插件告知选了哪条。

api.onMainPush(
  callback: (query: string) => PluginMainPushItem[],
  onSelect?: (id: string) => void
): () => void
const remove = api.onMainPush(
  (query) => {
    // 同步返回数组;返回空数组表示这一轮没有结果
    return recentFiles
      .filter((file) => file.name.toLowerCase().includes(query.toLowerCase()))
      .slice(0, 5)
      .map((file) => ({
        id: file.path,              // 稳定标识,选中后原样回传
        title: file.name,
        description: file.path,
        icon: '📄'                  // 可省:http(s) 地址、data URL,或单个字符
      }))
  },
  (id) => {
    // 用户选了 id 这一条
    void openFile(id)
  }
)

结果项结构

type PluginMainPushItem = {
  id: string           // 必填,稳定标识
  title: string        // 必填,主标题
  description?: string // 副标题 / 说明
  icon?: string        // http(s) 地址、data URL,或单个字符
}

id 或 title 为空的结果项会被丢弃——没有 id 就没法回传选中,没有标题用户在列表里看不到它。

必须声明的两件事

1. 权限:manifest.permissions 里要有 feature:mainpush。

2. 功能上的开关:任意一条功能声明 mainPush: true,宿主才会调用这个插件的推送回调。

{
  "permissions": ["feature:mainpush"],
  "features": [
    {
      "name": "最近文件",
      "description": "在搜索框里直接找最近打开的文件",
      "code": "recent",
      "mainPush": true,
      "cmds": ["recent"]
    }
  ]
}

为什么要有这个开关:推送回调是同步的、在每次按键时都会被调用。让所有插件都参与会拖慢搜索框,所以要求插件显式声明「我愿意参与」。不声明就是不参与。

回调要快、要稳

这是输入热路径——用户每敲一个字符都会调一次。

  • 必须同步返回数组。返回 Promise 会被当作「这一轮没有结果」(宿主不等异步结果,否则打字时列表会闪)。
  • 不要在里面做网络请求、读磁盘或大计算。需要异步数据就提前缓存好,回调里只做内存里的过滤。
  • 抛异常不会影响别的插件(宿主隔离了),但你自己那批结果会全部丢掉。
  • 单次最多返回 8 条,多的会被截断。

结果什么时候会被问

宿主只在启动器首页、且输入非空时询问插件。空输入不问——否则启动器一打开就会收到一堆「热门推荐」,把最近使用挤掉。

声明了 mainPush 的插件会被宿主预挂载:启动器可见时,宿主把插件代码跑起来(不触发 onPluginEnter、界面也不显示),这样打字时回调才在。离开启动器就整批卸掉。同时最多预挂载 3 个插件,按插件列表顺序取前 3 个。

这意味着插件被预挂载时不要做重活——它可能只是为了搜索被挂起来,用户并没有要用它。把初始化推迟到真正的 onPluginEnter 之后。

插件就绪回调(onPluginReady)

插件代码加载完成时触发,用来做一次性初始化。

api.onPluginReady(callback: () => void): () => void
api.onPluginReady(() => {
  // 这里做「插件起来了」才能做的事:建立索引、预热缓存、注册搜索推送
  void warmUpIndex()
})

要点:

  • 注册时若已经就绪,回调会立刻被调用一次。所以不必担心「注册晚了错过事件」——插件的 setup() 与宿主的就绪信号谁先谁后都不影响。
  • 它不需要任何权限:只是宿主通知插件「你已就绪」,不读也不改用户数据。
  • 与 onPluginEnter 的分工:onPluginReady 是「代码跑起来了」(预挂载时也会触发),onPluginEnter 是「用户进入了这个插件」。做与用户操作无关的初始化用前者,读 enterAction 处理本次进入用后者。
  • 订阅返回取消函数,组件卸载时应当调用。

老客户端上的行为

这三项能力都是 apiLevel 7 引入的。老客户端上:

  • onMainPush / onPluginReady 不存在 → 注册方法返回一个空的取消函数,插件照常调用与清理都不会炸;
  • setFeature 等返回 false / 空数组。
function supports(api, name) {
  try {
    return typeof api.hasCapability === 'function' && api.hasCapability(name) === true
  } catch {
    return false
  }
}

if (supports(api, 'feature:mainpush')) {
  // 新路径:注册推送
} else {
  // 降级:退回只靠静态 cmds 声明
}

也可以直接在 manifest 里声明 requires / optional,让宿主替你判定——能用降级解决的写 optional,详见版本与兼容性。