☰
Flashlight Evernote 搜索插件实战:用 AppleScript 在 Spotlight 中检索与打开笔记
2026/10/15 12:07:29 网站建设 项目流程
  • 桌面应用
  • 插件系统

【免费下载链接】Flashlight

The missing Spotlight plugin system

项目地址:https://gitcode.com/gh_mirrors/fl/Flashlight
点击查看免费下载

导读

本文以 Flashlight 插件体系中的 evernote-search.bundle 为研究对象,完整讲解这款"Evernote Search"插件的设计思路、AppleScript 集成方式与源码实现细节。读完本文,你将掌握:Flashlight 插件如何通过results()/run()双入口与 Spotlight 结果面板交互、如何用osascript调用 Evernote 脚本桥执行"搜索笔记—收集元数据—打开笔记窗口"的完整链路,以及如何在搜索结果 HTML 中借助flashlight.bash()桥接能力执行系统命令。仓库源码与配置文件给出的路径,均可直接在当前仓库中验证。

一、插件概览:在 Spotlight 中搜索 Evernote

Evernote Search 插件(displayName: "Evernote Search")的目标是:当用户在 Spotlight 输入框输入诸如 "search evernote for workout routines"、"evernote funny documents" 这样的自然语言时,Flashlight 能够调用 Evernote 的搜索能力,把匹配笔记的标题与所属笔记本直接渲染进 Spotlight 的结果列表,点击条目即可在 Evernote 中打开对应笔记窗口。

插件元数据位于 info.json:

{ "categories" : ["Utilities"], "displayName" : "Evernote Search", "name" : "evernote-search", "description" : "Search your Evernotes", "examples" : [ "search evernote for workout routines", "evernote funny documents", "notes for cooking recipes" ], "creator_name": "Richard van der Dys", "creator_url": "http://richardvanderdys.com" }

关键字段说明:

  • name:插件目录内的唯一标识(即evernote-search.bundle的名称部分),也是插件在目录索引中注册的 key;
  • displayName:插件目录/设置界面展示的用户友好名称;
  • categories:归类到Utilities(工具类),与同仓库下如 copycolor.bundle、timestamp.bundle 等插件的分类方式一致;
  • examples:供用户参考的示例查询,这些示例还与examples.txt相互配合;
  • creator_name/creator_url:作者署名信息。

目录 PluginDirectories/1/evernote-search.bundle 还包含以下文件:

  • plugin.py:插件主逻辑,见下文源码剖析;
  • examples.txt:解析引擎用到的查询模板与可替换参数定义;
  • info.json:插件元数据;
  • 1.version:插件版本哈希(内容为 SHA-256 摘要);
  • Icon.png/Screenshot.png:插件图标与效果截图。

版本文件的由来

1.version中存储的是一串 64 位十六进制哈希。仓库的 compute_version.py 揭示了它的生成规则:脚本会对.bundle目录内全部文件(忽略隐藏文件、*.version与*.pyc)递归计算 SHA-256 哈希;每次更新插件后重新哈希,若摘要变化则删除旧N.version并生成N+1.version。因此 evernote-search 的1.version实际就是其插件内容快照的校验和,目录服务据此判断插件是否有新版本可下发。

二、查询模板与参数槽:examples.txt 的语法

examples.txt 内容如下:

search evernote for ~query(Workout Routine) evernote ~query(Funny documents) notes ~query(Cooking Recipes)

每行是一条"示例查询模板",其中~query(...)是参数槽:括号内为示例值,而~query则是解析时抽取的命名参数。当用户输入evernote funny documents时,Flashlight 的 Parsnip 解析引擎会把funny documents匹配进~query槽,随后以fields['~query']的形式传入插件。

对比同仓库模板插件 SearchTemplate.bundle:

show a notification saying ~message(hello) toast ~message(hello world) notify ~message(good morning!)

可见~参数名(示例值)是 Flashlight 插件的通用占位符语法——示例值同时承担"自动补全提示"与"离线学习语料"双重职责。

三、插件主逻辑源码剖析

插件核心实现在 plugin.py,由四个函数构成。下面逐段拆解。

3.1 入口函数 results():产出搜索结果

def results(fields, original_query): query = fields['~query'] return { "title": "Searching your notes for {0}".format(query), "run_args": [], "html": createHTML(query) }

这是 Flashlight 插件协议规定的核心入口。其运行机制可对照 invoke_plugin.py:加载器把args(由解析树转换出的嵌套字典)、query原查询传入plugin.results(...),若results函数声明了第三个参数,还会把ParseTree对象一并注入;返回值以 JSON 序列化输出到 stdout,由 FlashlightKit 的查询引擎解析后渲染为 Spotlight 结果(见 FlashlightQueryEngine.m)。

返回值字典的三个键含义:

  • title:结果行显示的主标题,这里动态拼接出 "Searching your notes for {查询词}";
  • run_args:回车执行时透传给run()的参数列表,此处为空;
  • html:自定义结果视图的 HTML 内容,由createHTML(query)生成——这是该插件将笔记列表渲染进 Spotlight 的关键。

3.2 run():执行动作

def run(): print ''

Flashlight 的插件协议要求提供run()以响应用户对结果项的回车操作。此插件的主结果本身没有关联动作(run_args为空,run()仅打印空行占位),真正的"打开笔记"动作发生在结果 HTML 内部——见 3.5 小节对 JavaScript 桥接的说明。

3.3 asrun():AppleScript 执行器

def asrun(ascript): osa = subprocess.Popen(['osascript', '-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) return osa.communicate(ascript)[0]

asrun()通过subprocess.Popen启动系统命令osascript -,把 AppleScript 源码从标准输入喂给脚本解释器,并从 stdout 取回脚本输出。这是本插件与 macOS 系统能力交互的底层通道——osascript是 macOS 自带脚本工具,能对支持 Apple Events 的应用程序(Evernote 即属于此类)执行脚本桥调用。

3.4 getEvernoteSearchResults():向 Evernote 发起搜索

def getEvernoteSearchResults(query): script = ''' tell application "Evernote" set find_results to find notes "{0}" set output_list to {} repeat with find_result in find_results set o_title to get title of find_result set o_id to local id of find_result set o_notebook to name of notebook of find_result set end of output_list to {title:o_title, local id:o_id, notebook:o_notebook} end repeat get output_list end tell '''.replace('{0}', query) return asrun(script)

该函数动态构造一段 AppleScript 并交给asrun()执行,流程为:

  1. find notes "{query}":利用 Evernote 的 AppleScript 字典执行全文搜索;
  2. 遍历find_results,逐条提取笔记的三个关键属性:
    • title:笔记标题;
    • local id:笔记在本地数据库中的唯一标识;
    • name of notebook:所属笔记本名称;
  3. 组装为 AppleScript record 列表({title:..., local id:..., notebook:...})并输出。

注意两点工程细节:一是查询词直接以字符串插值方式嵌入脚本(replace('{0}', query)),因此含引号或特殊字符的查询可能导致脚本语法问题,这是早期 Flashlight 插件常见的简朴处理方式;二是输出格式为 AppleScript record 的文本表示(形如{title:foo, local id:123, notebook:bar}),正好与 3.5 小节的解析逻辑相呼应。

3.5 createHTML():渲染结果面板与打开笔记

createHTML(query)负责把 Evernote 搜索结果转成可点击的结果页。代码分三段:

(1)样式与容器:定义浅灰背景(#eaedef)、无下划线链接、悬停描边等内联 CSS,并嵌入一个 16×16 的 base64 PNG 图标作为笔记条目的前置标记。

(2)解析 AppleScript 输出:

for line in re.split(',\stitle:', results[6:]): m = re.match('(.*),\sid:(.*), notebook:(.*)', line)

results[6:]跳过 AppleScript record 文本的头部冗余(record 列表前缀{等),再按, title:切分得到逐条记录;正则(.*),\sid:(.*), notebook:(.*)依次捕获标题、本地 id、笔记本名,并用cgi.escape()做 HTML 转义后填入链接。

(3)生成可点击条目与桥接脚本:

<a onclick="openNote('{1}', '{2}', '{0}');"> <header>{0}</header> <i></i> <small>{2}</small> </a>

点击任一条目触发openNote(id, notebook, title),该 JavaScript 函数会拼出一条新的 AppleScript 命令:

function openNote(id, notebook, title) { var script = 'osascript'; script += line('tell application "Evernote"'); script += line('open note window with note id "' + id + '" of notebook "' + notebook + '"'); script += line('activate'); script += line('end tell'); flashlight.bash(script); }

这里调用了flashlight.bash()——Flashlight 提供给结果 HTML 的 JavaScript 桥接 API。其 Objective-C 实现位于 FlashlightWebScriptObject.m:通过NSTask以/bin/bash -l -c执行传入命令并返回输出,同时该对象还暴露了invoke()(触发打开目标结果)等能力(见 FlashlightWebScriptObject.m)。由此,用户点击笔记标题即执行osascript打开对应笔记窗口并激活 Evernote——"搜索→展示→打开"闭环在 Spotlight 面板内一步完成。

四、运行链路:从输入到打开的完整调用链

综合上述源码,一次完整的搜索经历如下阶段:

  1. 解析:用户输入匹配 examples.txt 模板,~query槽被填充为实际查询词;
  2. 加载:FlashlightKit 通过 invoke_plugin.py 在插件目录下导入plugin模块,以args、query(以及可选的ParseTree)调用results();
  3. 取数:results()调createHTML()→getEvernoteSearchResults()→asrun()→osascript -,Evernote 返回笔记记录文本;
  4. 渲染:HTML 被包装进结果项,FlashlightQueryEngine 解析 JSON 后把结果注入 Spotlight 列表(详见 FlashlightQueryEngine.m 中对动态插件的执行与结果合并逻辑);
  5. 打开:用户点击条目,HTML 内嵌 JavaScript 经flashlight.bash()执行osascript,在 Evernote 中打开对应笔记窗口。

该链路同时展示了 Flashlight 插件体系的两条并行能力路径:其一是results()/run()返回 JSON 的结构化结果协议(普通插件通用);其二是结果 HTML 内嵌 JavaScript +flashlight.bash()的系统桥接(本插件"就地打开笔记"所依赖)。后者在仓库其他插件中也有体现,例如 say.bundle 与 EasySIMBL 模板插件 SearchTemplate.bundle。

五、使用与注意事项

前提条件:

  • 需要 macOS 系统(osascript、Evernote 桌面客户端均依赖 macOS);
  • Evernote 桌面版需已安装并登录,且允许 Apple Events(系统设置中的自动化权限需授权 Flashlight 控制 Evernote);
  • 需要先安装并运行 Flashlight 本体(通过 SIMBL 机制注入 Spotlight,见 FlashlightApp 目录下的工程结构),并把插件放入插件目录PluginDirectories/1后启用。

使用方式:在 Spotlight 输入示例查询,如evernote funny documents或search evernote for workout routines,结果面板会实时列出匹配笔记;点击条目即可跳转打开。

局限与改进方向(从源码可推断):

  • 查询词以纯字符串拼接嵌入 AppleScript,未做引号转义,包含特殊字符的查询可能失败;
  • 依赖 AppleScript record 的文本输出格式做正则解析,对 Evernote 脚本桥输出格式变动较敏感;
  • 每条结果都是一个可直接点击的 HTML 链接,但插件本身未提供"回车直达第一个结果"之类的默认动作(run()为空实现),主要交互完全靠点击完成。

六、关联阅读

  • 插件协议与加载器:invoke_plugin.py、run_plugin.py
  • 查询引擎对插件结果的处理:FlashlightQueryEngine.m
  • HTML 结果与系统命令桥接:FlashlightWebScriptObject.m
  • 插件模板与示例语法:SearchTemplate.bundle/examples.txt、SearchTemplate.bundle/plugin.py
  • 插件目录与版本机制:PluginDirectories/1、compute_version.py

通过 evernote-search 这一实例,可以清晰看到 Flashlight 插件如何把"自然语言解析 + 系统脚本桥 + Web 结果视图"三者组合起来,在 Spotlight 中落地一个完整的第三方搜索体验。

  • 桌面应用
  • 插件系统

【免费下载链接】Flashlight

The missing Spotlight plugin system

项目地址:https://gitcode.com/gh_mirrors/fl/Flashlight
点击查看免费下载
上一篇:MindSpeed-LLM 长序列微调实战:上下文并行(Context Parallel)微调参数详解与源码解析
下一篇:RGF 图标类型枚举 EmIcon 详解:Big / Small / Small2 与 Win32 窗口图标消息的映射

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询