简介:scite-zotero-plugin 是一款面向科研工作者的 Zotero 扩展插件,用于在文献管理器中直接查看每篇论文基于 Smart Citation 数据的分类统计(Supporting、Mentioning、Contrasting),并快速跳转到 scite 的站点报告页面,解决科研人员快速评估文献引用质量与倾向性的问题。压缩包共 31 个文件,以 TypeScript 源码、JSON 配置、XUL 界面定义与 PNG 图标为核心,整体大小约 1.24MB;其中 TS 文件实现插件逻辑,JSON 负责元数据与依赖管理,XUL 描述 Firefox/Zotero 界面,PNG 提供示例截图和按钮图标。已有 1161 人学习下载。通过该资源可获得一套完整的 Zotero 插件工程,包括 Webpack 构建配置、TSLint 与 CircleCI 集成、语言包及皮肤目录,能够帮助有一定 TypeScript 基础的开发者理解插件打包、界面挂载和 scite API 数据展示的完整流程,也可作为二次开发或学术文献工具定制的参考起点。
1. scite 插件到底给 Zotero 补了什么:不是显示引用数,是显示引用语境
用过 Zotero 的人都知道,它帮你把文献存得整整齐齐,但文献之间的引用关系它基本不管。scite 这个工具的定位恰好补上这块:它不只是告诉你“这篇论文被引了多少次”,而是把每一条引用语境拆出来,标明这条引用是支持、反对还是仅提及。scite-zotero-plugin 就是把 scite 的引用语境数据塞进 Zotero 条目里的桥。装上之后,你在 Zotero 里选中一篇文献,就能直接看到 scite 的报告、引用分类统计,不用再单独开网页去 scite.ai 查。适合每天跟文献打交道、又不想在 Zotero 和 scite 之间来回切的研究生、科研工作者。这篇文章我会从插件工程结构讲到本地构建,再讲几个我实际踩过的坑,最后给一个自定义右键菜单的进阶改法。
2. 插件工作原理与工程骨架:Zotero 插件为什么用 TypeScript 写
2.1 scite 的核心能力:Citation Statement 与 Classification
scite 的底层数据模型和普通引文索引最大的不同在于它把每条引文拆成了“Citation Statement”——也就是引用这句话出现的上下文片段,同时给这条引用打一个分类标签。分类通常是三类:supporting(支持)、contrasting(对比/质疑)、mentioning(仅提及,不带立场)。这个分类是算法加人工审核混合产出的,所以它比单纯数引用次数更能反映一篇文献在学术对话里的真实生态位。
scite-zotero-plugin 做的事情,本质上就是把 Zotero 条目映射到 scite 的 DOI 或 arXiv ID,然后拉取这个映射对应的 Citation Statement 和分类统计。注意它和 Zotero 自带那种“抓取 PDF 元数据”的插件不同,它不解析你本地 PDF 内容,而是依赖 scite 的数据库。这意味着你的条目必须带 DOI 或者 arXiv 编号,否则插件找不到对应记录。
2.2 插件在 Zotero 里的加载模型:bootstrap.js 与 manifest.json
Zotero 的插件机制和 Firefox 老式扩展类似,但又有自己的一套。一个插件本质是一个 .xpi 文件,里面至少要有一个 manifest.json,声明插件 ID、版本、权限和入口脚本。入口脚本通常叫 bootstrap.js,但它不是你自己手写的——在 scite-zotero-plugin 这类基于 zotero-plugin-template 的工程里,bootstrap.js 是构建流程自动生成的加载器,它负责在 Zotero 启动时把你的主逻辑注入进去。
这里有个关键认知:Zotero 插件的启动方式是“bootstrap 式”,也就是插件安装后立刻生效,不需要重启 Zotero。它通过生命周期钩子来管理:install、startup、shutdown、uninstall。你在源码里写的 hooks.ts 或 lifecycle.ts,会被构建工具编译并打包进 bootstrap.js,Zotero 在相应时机调用这些钩子。
2.3 工程目录与构建链路:从 TS 源码到 xpi
scite-zotero-plugin 用的是 TypeScript,这不是为了花哨,而是因为 Zotero 插件要操作的对象模型(Services、ZoteroPane、Zotero.Item)结构复杂,类型标注能少翻很多文档。工程里典型的目录划分是:
- src/ 放 TypeScript 源码,比如 hooks.ts、addon.ts、prefs.ts
- addon/ 放静态资源和 manifest.json 的模板
- build/ 或 dist/ 是构建输出目录,最终生成 .xpi
- package.json 统一管理依赖和构建脚本
构建链路一般是这样的:TypeScript 源码经 esbuild 打包成几个 chunk,再和 addon/ 下的静态文件一起打进一个 .xpi。esbuild 选得比较多,因为 Zotero 插件的总代码量不大,esbuild 的冷启动速度和增量编译在这个场景里优势明显。
# 安装依赖(项目用 pnpm 的话) pnpm install # 开发模式:监听文件变化并持续构建 pnpm run build --watch # 一次性构建出 xpi 包 pnpm run build逻辑说明:--watch模式会在你改代码时自动重新构建,生成新的 xpi,适合配合 Zotero 的“重新加载插件”功能做调试循环。一次性构建则适合最终打包分发。构建产物路径一般在 build/release/ 下,文件名带有版本号。
参数说明:如果你的网络环境拉不到依赖,可以切到 npm 镜像源,但需要确认 package.json 里锁定的 pnpm 版本和 Node 版本。zotero-plugin-template 对 Node 版本有下限要求,太老的 Node 跑不了 esbuild。
3. 本地构建与安装调试:把插件跑进你的 Zotero
3.1 环境准备与依赖安装
先把工程克隆下来。这里我假定你已经装了 Node.js 和 pnpm。Node 版本建议 18 以上,esbuild 对 Node 版本比较敏感,太老会直接报错。安装依赖时注意,zotero-plugin-toolkit 这个包的主体逻辑依赖 Zotero 的全局对象,本地安装时它只做类型检查和工具函数打包,真正跑起来是在 Zotero 里,所以安装过程不会真的去下载 Zotero 本体。
git clone https://github.com/scite/scite-zotero-plugin.git cd scite-zotero-plugin pnpm install逻辑说明:git clone 拿到源码后,pnpm install 会安装 package.json 里声明的依赖,包括 zotero-plugin-toolkit、esbuild、typescript 等。这一步如果失败,九成是网络问题,按 pnpm 的报错去换镜像即可。
参数说明:安装完成后,可以看一眼 node_modules/.bin/ 下有没有 esbuild 和 tsconfig 的软链,有就说明核心工具链没问题。
3.2 构建插件并安装 xpi
构建命令会根据 ZOTERO_PLUGIN_ID、ZOTERO_PLUGIN_VERSION 这些环境变量来生成 manifest.json。如果你直接跑pnpm run build,它会读取 .env 或默认配置。构建完会生成一个 .xpi,在 Zotero 里通过“工具 → 插件 → 齿轮图标 → Install Plugin From File”装上。
pnpm run build ls build/release/*.xpi逻辑说明:这步把 TypeScript 源码编译成实际能在 Zotero 里执行的 JavaScript,并连同 manifest.json、prefs.js 等打包成 xpi。拿到 xpi 的路径后,在 Zotero 的插件管理界面选择那个文件即可。
参数说明:如果你改了版本号,一定要同步改 package.json 和 addon/manifest.json 模板里的 version 字段,否则 Zotero 会因为版本号不合规拒绝安装。
3.3 调试与日志排查
Zotero 插件调试有几个入口。最笨但有效的办法是看 Zotero 的调试输出窗口:在 Zotero 里按 Shift+F2,或者菜单栏“帮助 → 调试输出日志”。插件里用 console.log 打印的内容都会到这里。
// src/hooks.ts 里加一段临时日志 console.log("[scite-zotero-plugin] startup called, version:", addon.data.version);逻辑说明:Zotero 的调试输出窗口会显示 console.log 和 console.error。插件启动时如果这段日志没打出来,说明 bootstrap.js 根本没被执行,问题多半出在 manifest.json 的入口配置或者插件 ID 冲突。
参数说明:Zotero 7 之后调试输出窗口的位置和样式有变化,但不影响内容。另外,Zotero 的 profile 目录下也有 logs/ 目录,里面会有更底层的错误记录,适合排查插件崩溃这类问题。MAC 和 Windows 路径不同,以 Zotero 的帮助菜单里显示的 profile 路径为准。
4. 核心代码路径与参数:读透 scite 插件的关键文件
4.1 manifest.json 的配置项
manifest.json 是 Zotero 插件的门面。它决定了插件在插件列表里显示什么名字、能操作哪些 Zotero 对象、入口脚本是哪个。scite-zotero-plugin 里的 manifest.json 有几个关键字段值得注意。
{ "manifest_version": 2, "name": "scite-zotero-plugin", "version": "1.0.0", "applications": { "zotero": { "id": "scite-plugin@scite.ai", "update_url": "https://scite.ai/static/zotero/update.json", "strict_min_version": "6.0", "strict_max_version": "7.0.*" } }, "bootstrap": true, "scripts": ["bootstrap.js"] }逻辑说明:applications.zotero.id是插件的唯一标识,Zotero 用它来区分插件,改名字可以但尽量别改这个 ID,否则 Zotero 会认为是两个插件。update_url指向一个 JSON 文件,用来支持自动更新,如果你是在本地调试,这个 URL 可以是任意合法地址甚至不写。bootstrap: true表示这个插件走的是 bootstrap 加载模型。
参数说明:strict_min_version和strict_max_version很关键。scite 插件为什么经常出现“装了没反应”?多半是版本约束太严,比如 max 写的是 7.0.*,而你的 Zotero 已经升到 7.1,插件就被静默禁用了。自己改的时候,要么放宽范围,要么升插件版本。
4.2 hooks 与生命周期
bootstrap.js 被加载后,Zotero 会调用它导出的生命周期函数。zotero-plugin-template 的设计里,hooks 文件通常是 src/hooks.ts,里面通过 toolkit 的 ZoteroPlugin 类来注册生命周期。
// src/hooks.ts import { ZoteroPlugin } from "zotero-plugin-toolkit"; const plugin = new ZoteroPlugin({ id: "scite-plugin@scite.ai", name: "scite-zotero-plugin", version: "1.0.0", }); plugin.hooks.onStartup = () => { console.log("[scite-zotero-plugin] onStartup fired"); // 在这里注册菜单、快捷键、偏好面板等 }; plugin.hooks.onShutdown = () => { console.log("[scite-zotero-plugin] onShutdown fired"); }; export default plugin;逻辑说明:onStartup是插件每次随 Zotero 启动时执行的入口。所有需要常驻的功能——比如给 ZoteroPane 的条目右键菜单加一项——都要在这里注册。onShutdown则是清理现场的地方,比如移除菜单项、释放监听器。
参数说明:如果你改动了 hooks 里的代码,需要重新构建并重装插件。Zotero 的“重新加载插件”功能在调试时会用到,但要注意重新加载不等于重新安装,生命周期函数会重新走一遍。
4.3 从选中条目到 scite 数据:关键函数与参数
这个插件最有价值的逻辑,是把 Zotero 条目变成 scite 能识别的 DOI,再请求 scite API。核心流程分三段:拿选中条目 → 提取 DOI → 请求 scite 接口。
// 示例:从 Zotero 选中条目中提取 DOI function getSelectedItemDOI(): string | null { const items = ZoteroPane.getSelectedItems(); if (items.length === 0) return null; const item = items[0]; const doi = item.getField("DOI") as string; return doi ? doi.trim() : null; }逻辑说明:ZoteroPane.getSelectedItems()返回当前选中条目的数组,通常只取第一个。item.getField("DOI")是 Zotero 的数据层接口,能拿到条目注册表里的 DOI 字段。注意有些条目没有 DOI,而是 arXiv ID,这时需要额外判断item.libraryID或 extra 字段里是否有 arXiv 编号。
参数说明:Zotero 的字段名是固定的,“DOI” 就是 DOI 字段的接口名。如果你要改成支持 arXiv,需要自己解析 item 的 extra 字段,那种情况要处理正则匹配。
拿到 DOI 后,插件会请求 scite 的 API。scite 官方提供了一个面向公众的 API 端点,返回 JSON 格式的引用数据。
// 示例:请求 scite API 获取引用统计 async function fetchSciteData(doi: string): Promise<any> { const url = `https://api.scite.ai/reports/${doi}`; const resp = await fetch(url, { headers: { "Content-Type": "application/json" }, }); if (!resp.ok) throw new Error(`scite API error: ${resp.status}`); return resp.json(); }逻辑说明:这个接口返回的 JSON 里通常包含total_citations、supporting_citations、contrasting_citations、mentioning_citations这几个字段。插件拿到后直接显示条数,或者更进一步,把支持/反对的引用列表展示出来。注意 fetch 在 Zotero 环境里是全局可用的,不需要额外引入。
参数说明:这个 API 端点是 scite 公共接口,一般不需要 key。但如果你用到了需要鉴权的接口(比如批量搜索),就要在请求头加Authorization字段,格式是Bearer <your-token>。token 不要在源码里硬编码,放到 Zotero 的 prefs 里更合适。
5. 避坑笔记:scite 插件最常见的五个坑
5.1 装了插件没反应
现象:在 Zotero 插件管理界面装好 scite 插件,重启后选中文献,右键没有任何新菜单,工具栏也没出现新图标。
原因:最常见的是 Zotero 版本不匹配。scite 插件 manifest.json 里strict_max_version如果写的是 7.0.*,而你的 Zotero 是 7.1,插件会被自动禁用,但插件列表里不会立刻标红,只是功能全部失效。还有一种是插件 ID 冲突,比如你同时装了之前手动打包的另一个版本,Zotero 认为重复。
解决:先在“工具 → 插件”里点开 scite 插件,看右侧状态是启用还是禁用。禁用的话,要么换旧版 Zotero,要么自己改源码里的 manifest.json 放宽版本号上限,然后重新构建。重复安装的,全部卸载后只装一个。
5.2 构建出来没有 bootstrap.js
现象:pnpm run build执行成功,但打开生成的 xpi,里面只有 manifest.json 和一堆静态资源,没有 bootstrap.js,装到 Zotero 里提示缺少入口脚本。
原因:esbuild 的入口配置指向了不存在的文件,或者 hooks.ts 里没有默认导出 ZoteroPlugin 实例。后者是新手常踩的坑:bootstrap.js 生成时依赖源码导出的 plugin 对象,你没导出它就生成不出来。
解决:检查 package.json 里 build 脚本的 esbuild 入口参数,确认指向 src/hooks.ts。再检查 hooks.ts 里有没有export default plugin。改成正确导出一行即可。
5.3 Zotero 7 升级后 API 不兼容
现象:插件在 Zotero 6 里一切正常,升级到 Zotero 7 后,右键菜单没了,控制台报错说ZoteroPane未定义或getSelectedItems不存在。
原因:Zotero 7 对内部对象做了一次大清理,很多老式 API 被移到了 Services 命名空间下,或者改了调用方式。ZoteroPane 的获取方式从全局变量改成了Zotero.getMainWindow().ZoteroPane这种形式。
解决:把ZoteroPane.getSelectedItems()改成
const zp = Zotero.getMainWindow().ZoteroPane; const items = zp.getSelectedItems();逻辑说明:在 Zotero 7 里,ZoteroPane不再是全局变量,而是挂在主窗口对象下的属性。Zotero.getMainWindow()能拿到当前的主窗口实例。这样改完之后,兼容 Zotero 6 和 7 的写法是在代码里做一层判断,判断ZoteroPane是否已是全局。
参数说明:getSelectedItems在 Zotero 7 里还有个可选的参数控制是否包含子条目,默认是 false,一般不用动。
5.4 scite API 请求失败或超时
现象:插件能弹窗,但里面的引用数据一直是加载中,打开调试窗口看到 fetch 请求报 CORS 或超时。
原因:scite API 的 CORS 策略可能阻止了 Zotero 里的网页请求。Zotero 插件跑在特权环境里,但 fetch 请求如果目标是跨域接口,仍受同源策略限制。另外,scite 的 API 有时会因为参数格式不对返回 403。
解决:在 manifest.json 的权限声明里加上
"permissions": ["https://api.scite.ai/*"]逻辑说明:这个声明相当于告诉 Zotero 插件运行时,允许向 scite 接口发起跨域请求。加了之后,CORS 报错一般就消失了。如果还超时,多半是网络问题或者 API 端点在当前地区不稳定,这种就属于外部依赖故障,插件本身没得修,只能等网络恢复。
参数说明:permissions数组里可以写多个域名,但不要写"<all_urls>"这种通配,Zotero 对宽泛权限审核比较严,通配容易导致安装警告。
5.5 插件版本号格式不对装不上
现象:构建出的 xpi 拖进 Zotero 安装时,提示“插件版本格式错误”或“invalid version”。
原因:Zotero 要求 version 字段遵循严格的数字点分格式,比如1.0.0。如果你 package.json 里写的是1.0.0-beta.1这种语义化版本号,Zotero 7 里会直接拒绝安装。esbuild 构建时不校验这个,但 Zotero 装的时候会校验。
解决:把版本号改成1.0.0或1.0.1。如果你一定要用预发布标记,可以写成1.0.0.1这种四段数字,或者1.0.0b1,Zotero 都能认。
6. 进阶:给 scite 插件加右键菜单与自定义偏好
6.1 在 Item 菜单加一个“查看 scite 引用语境”入口
到这个阶段,你已经有了一份能编译、能安装、能拉数据的 scite 插件。接下来可以按自己的使用习惯改。我见过多数人会想加一个右键菜单入口,因为 zotero-plugin-template 默认可能没有绑定到条目右键菜单。加法的核心是调用Zotero.IntegratedContextMenu或老式的ZoteroPane.ctxMenuPopulated事件。推荐后者,Zotero 7 还在用。
ZoteroPane.ctxMenuPopulated = function (menu, item) { menu.append( new Zotero.MenuSeparator(), new Zotero.MenuItem({ label: "View scite Citations", command: "scite-view-citation", }) ); };逻辑说明:这个钩子会在条目右键菜单弹出时被调用,menu是待填充的菜单对象,item是当前选中的条目。这里每弹一次菜单就插入一个分隔符和一项“View scite Citations”,点击后通过 command 触发的回调里再去处理 scite 请求。
6.2 把偏好存进 Zotero 的 prefs
scite API 如果要用 token,建议存到 prefs 而不是硬编码。Zotero 的 prefs 系统用起来很简单。
function getSciteApiKey(): string { return Zotero.Prefs.get("scite.apiKey", true) as string; } function setSciteApiKey(key: string): void { Zotero.Prefs.set("scite.apiKey", key, true); }逻辑说明:Zotero.Prefs.get第三个参数传 true 表示这是一个全局偏好,不区分 profile。API key 这么存,至少不散落在源码里。
参数说明:偏好字段名可以自定义,但建议在 manifest.json 里用prefs段预声明,比如
"prefs": { "scite.apiKey": { "type": "string", "value": "" } }预声明的好处是 Zotero 会帮你建好默认值,就算用户没碰过设置项,这个 key 也始终存在。
6.3 验证插件更新链路是否健康
改完这些,不要急着就完事。我习惯在每次改动后过一遍这事:插件更新时,Zotero 会访问update_url指向的 JSON,如果这个 JSON 里的addons数组格式不正确,Zotero 会反复弹更新失败的提示。scite 官方如果更新了update_url指向的版本号,而本地源码没跟着改,就会出现“本地版本和在线版本不一致”的死循环。
从那以后,我每次构建 scite 插件,都先把update_url暂时清掉,或者指向本地的update.json,确认安装、卸载、升级链路没问题,再把线上地址补回去。这个习惯帮我省了不少排查时间。希望这篇拆解能让你少走几步弯路,顺利跑起自己的 scite 插件。
本文还有配套的精品资源,点击获取