# 插件贡献说明 Voyager 的插件系统优先支持声明式插件:用 `plugin.json` 描述插件信息和 DOM 操作,用 CSS 描述样式。插件本身不运行远程 JavaScript,而是由 Voyager 内置的插件引擎解释这些数据。 这让插件更容易审核和维护。如果你想贡献一个插件,建议先从这个方向开始。 ## 只给自己用:本地插件 只在自己浏览器里用的插件不需要走这套流程。把它做成本地插件,在 popup 里导入即可:不用提 Issue、不用提 PR,也不用拉取仓库。[编写自己的插件](./write-a-plugin.md)介绍了三种做法,包括 `create-voyager-plugin` 检查工具和给编程 Agent 用的 skill。 ## 推荐路径 1. 先确认它适合做成插件:阅读宽度、排版修复、主题微调、隐藏/标记页面元素、简单的站点适配,通常都适合声明式插件。 2. 先在 Voyager 主仓库提交 Issue,说明它解决的问题、目标网站、和现有插件相比的区别;等待维护者明确同意方案后再编码和提交 PR。 3. 使用 `plugin.json` 写插件元信息、匹配站点和贡献内容。 4. 将样式放进同目录的 `style.css`,再由 `plugin.json` 的 `contributes.styles` 引用。 5. 本地测试后提交 PR,并附上测试页面、截图或录屏。维护者会根据插件成熟度决定是否进入官方 catalog。 ## 目录结构 官方内置插件都放在 `src/features/plugins/catalog/` 下面,一个插件平台一个目录: ``` src/features/plugins/catalog/ marketplace.json 文档站插件市场读取的索引 sites//site.json 站点适配器(数据形式) sites//plugins//plugin.json 一个声明式插件 sites//plugins//style.css 它的样式 sites//plugins//README.md 它修了什么、为什么这么修 ``` 发现过程是自动的:`catalog/sites/index.ts` 用 `import.meta.glob` 找出每个 `site.json` 和每个 `plugin.json`,所以新增站点或插件就是新增文件,没有映射表需要维护。 `marketplace.json` 不是那张映射表,它只是文档站插件市场读取的索引;有测试保证它和自动发现的结果一致,所以新插件还要在里面加一条,`source` 写 catalog 相对路径,例如 `sites/deepseek/plugins/reading-width/plugin.json`。`sites/gemini/` 和 `sites/aistudio/` 下的插件是例外:它们随 Voyager 打包,不进 `marketplace.json`。 插件的 `matches` 必须落在所属站点的 `matches` 范围内;越界的插件会让构建失败。 ## 语义选择器键 `site.json` 把一组固定的语义键映射到站点自己的 CSS 选择器。这组词表定义在 `src/features/plugins/sites/semanticKeys.ts`,`site.json` 只能使用表内的键,用了表外的键会被拒绝。 - `userTurn`:用户消息容器。 - `assistantTurn`:助手消息容器。 - `thinkingBlock`:助手回答里的思考段落。 - `codeBlock`:渲染后的代码块。 - `composer`:用户输入框。 - `sidebar`:会话列表或导航栏。 - `sidePanel`:次级面板,例如 artifacts 或 canvas。 - `headerActions`:会话页右上角的操作区。 - `scrollContainer`:负责滚动会话的元素。 - `composerRoot`:输入框及其操作按钮所在的容器。 - `sendButton`:发送按钮;有的站点只在输入框有内容时才显示它。 站点只需要声明它真正能提供的键。插件用 `{ "kind": "semantic", "key": "userTurn" }` 作为 DOM 操作的 `target` 来引用语义键,而不是写死选择器,这样站点改版时只要改 `site.json` 一个文件。 `conversationIdPattern` 是 `site.json` 里的另一个字段,不是选择器:它是一个匹配 URL 路径的正则,第一个捕获组就是会话 id,例如 `^/chat/([^/?#]+)`。 ## 新增平台 新增一个聊天网站分为两个相互独立的层次: 1. **站点适配器**,即 `catalog/sites//site.json`,它告诉 Voyager 如何识别该网站,并找到用户消息、助手消息、输入框、侧边栏和主题标记。 2. **插件**,位于 `catalog/sites//plugins//` 下,用于解决该网站上某个面向用户的具体问题,例如阅读宽度或渲染修复。 `site.json` 包含 `id`(与目录名相同)、`label`、`matches`、`selectors`、`theme`、`brandColor`、`capabilities`,以及可选的 `conversationIdPattern` 和 `turnKeyAttributes`。`turnKeyAttributes` 指定在页面重新渲染用户消息时,用于保持其身份的属性。发送追踪(用于文件夹的 Activity 和时间线时间戳)只会在设置了该字段、`composer`、`userTurn` 和 `conversationIdPattern` 的站点上运行。`src/features/plugins/sites/siteAdapterData.ts` 中的 `validateSiteAdapterData` 负责校验它;`bun run catalog:build` 会在发布前校验每个站点和插件。 `site.json` 就是站点适配器本身,只是写成了数据。目前的插件平台是 ChatGPT、Claude 和 DeepSeek。Gemini 和 AI Studio 是 Voyager 的原生界面,同样有各自的 `site.json`(`catalog/sites/gemini/`、`catalog/sites/aistudio/`),但它随扩展打包,不会发布到远端目录;`sites/adapters/` 下的 `gemini.ts`、`aistudio.ts`、`claude.ts`、`chatgpt.ts`、`deepseek.ts` 现在都只是 `site.json` 的一行壳,要改就改 JSON,不要改 TypeScript。 发布的分站点目录里也带上了站点数据,所以选择器修复不发新版本也能到达用户。全新的站点仍然需要扩展本身的支持,因为它还需要随包提供的主机权限和 content script 注册。 对于 DeepSeek 这样的新平台,请让适配器工作专注于平台契约本身。将 `site.json`、必要的选择器测试,以及解释该支持所需的最少文档,放在同一个完整的变更中。站点注册表会直接从文件中识别新适配器,因此无需编辑任何注册列表。不要为每个插件都新增一个站点。 适配器通过审核后,后续插件可以作为独立的、聚焦的变更单独提交。如果多个变更彼此依赖,可以使用堆叠 PR 链:每个后续分支基于前一个分支,每个 PR 只展示自己增量的功能。整条链仍应遵循同样的范围原则:每个 PR 只对应一个明确的用户目标,并附带必要的测试和清理。 站点支持不需要模型 API 集成。不要为新平台添加静态 content script 或必需的主机权限;对于仅插件的站点,请使用现有的可选权限与动态注册流程。 可选权限与动态注册路径依赖于浏览器版本:Chrome 和 Edge 支持扩展所使用的流程;Firefox 需要 128 或更高版本才能使用可选主机权限;Safari 需要 16.4 或更高版本才能进行动态 content script 注册。在较旧的受支持浏览器版本上,弹窗可能会正确地拒绝为自定义站点启用插件,因为所需的 API 无法提供持久授权。不要通过添加宽泛的静态主机权限来绕过此限制,除非得到维护者的批准。 ## 插件粒度 插件应该以“用户要解决的问题”为边界,而不是机械地按平台拆开。 catalog 里的插件属于单个站点文件夹,它的每个 `matches` 都必须落在该文件夹 `site.json` 的范围内(否则 `bun run plugin:check` 会失败),所以同一个插件不能同时覆盖 Claude 和 ChatGPT。如果同一个功能适用于多个平台,例如“阅读宽度”“翻页体验”“代码块排版”,就在每个站点文件夹各做一个插件(例如 `sites/claude/plugins/reading-width` 和 `sites/chatgpt/plugins/reading-width`),并让名称、设置和说明保持一致。 但如果不同平台需要完全不同的设置、DOM 逻辑或用户文案,拆成多个插件会更清晰。不要为了“一个插件管所有事”把无关功能硬塞在一起;一个插件最好只解决一个明确问题。 简单判断: - 同一个用户目标、同一组设置、只是站点选择器不同:每个站点文件夹各做一个插件,名称与设置保持一致。 - 同一个主题但每个平台体验差异很大:可以拆开,但名称和说明里保持关联。 - 功能目标不同:不要合并。 ## 避免重复插件 提交前请先看插件市场和已有官方插件。如果已经有一个好用的插件,优先给它提改进 PR,而不是再做一个类似插件。 重复插件只有在有明显提升时才值得接受,例如: - 覆盖了原插件不支持的重要平台。 - 修复了原插件长期无法解决的兼容性问题。 - 有明显更好的性能、可访问性或维护性。 - 提供了不同但足够清晰的用户体验,而不只是换名字或微调样式。 这样插件市场会更干净,用户也更容易选择。 ## 最小示例 不用从空目录开始:`bun run plugin:new --site ` 会生成 `catalog/sites//plugins//`,里面有 `plugin.json`、`style.css` 和 `README.md`,并顺手往 `marketplace.json` 追加对应条目。它生成的只是起点,不代表已经通过审核;你要填的清单长这样: ```json { "$schema": "https://voyager.nagi.fun/plugin.schema.json", "id": "your-name.example-plugin", "name": "Example Plugin", "version": "1.0.0", "description": "A short description of what this plugin improves.", "author": "your-name", "category": "readability", "license": "MIT", "engine": ">=1.0.0", "tier": "declarative", "matches": ["https://claude.ai/*"], "contributes": { "styles": [{ "file": "style.css" }], "domOps": [ { "op": "addClass", "target": "body", "className": "gv-plugin-example" } ] } } ``` `$schema` 这行是可选的,只服务于编辑器:它指向清单的 JSON Schema,让编辑器边写边校验和补全。`site.json` 也有自己的一份,地址是 `https://voyager.nagi.fun/site.schema.json`。 `style.css` 可以像普通 CSS 一样写,但建议所有插件样式都挂在自己的 `gv-plugin-*` 类下面: ```css .gv-plugin-example .some-target { max-width: 880px; } ``` ## Manifest 注意事项 - `id` 使用反向域名或作者前缀,例如 `your-name.reading-width`,避免和其他插件冲突。 - `matches` 尽量收窄,只匹配插件真正需要生效的网站。 - `matches` 只有在所属站点的 `site.json` 全部覆盖时,才能列出多个主机(例如 chatgpt.com 和 chat.openai.com);要支持其他站点,请在该站点的文件夹里新增插件。 - `category` 建议使用 `render-fix`、`theme`、`layout`、`readability`、`productivity`、`integration` 或 `other`。 - `engine` 写清楚需要的插件引擎版本。官方插件可参考当前目录中的示例。 - `i18n` 推荐补齐中文、英文和其他常用语言的名称、描述、设置项文案。 ## CSS 与资源限制 声明式插件会被当作不可信输入校验,所以请保持资源自包含: - 不要使用 `@import`。 - 不要引用外链图片、外部字体或远程 CSS。 - 可以使用普通 CSS、自定义属性和 Voyager 提供的设置值替换。 - 类名请使用 `gv-plugin-` 前缀,避免污染宿主网站或 Voyager 自身样式。 如果插件需要用户设置,推荐先使用数字型设置;例如阅读宽度这类插件可以用设置值写入 CSS 变量,再由样式消费。 ## DOM 操作边界 当前声明式插件支持这些操作: - `addClass`:给目标元素添加类名。 - `setAttribute`:设置属性。 - `setStyle`:设置内联样式或 CSS 变量。值以 `!important` 结尾时会以该优先级应用;旧版本的 Voyager 会丢弃这样的值,因此如果插件依赖它,请将 `engine` 设为 `>=1.9.0`。`margin` 之类的简写属性按各自的长属性分别记录,因此关闭某个插件时,不会清除另一个插件设置的 `margin-top`。 - `hide`:隐藏目标元素。 目标可以是 CSS 选择器,也可以是上面列出的语义键,写成 `{ "kind": "semantic", "key": "userTurn" }`。语义键更稳,但需要当前站点适配器声明了这个键。 声明式操作必须是可撤销、可重复执行的。不要依赖一次性的页面状态,也不要假设页面 DOM 永远不变。 ### 原语 有些行为没法只用 CSS 和可撤销的 DOM 修改描述出来。原语是随 Voyager 一起打包的第一方代码,清单可以通过 `native` 操作按名字调用它: ```json { "engine": ">=1.3.0", "requires": { "handlers": ["formulaCopy"] }, "contributes": { "domOps": [{ "op": "native", "handler": "formulaCopy", "params": {} }] } } ``` 清单只负责挑一个原语并给它配置,不提供逻辑;`params` 会先由该原语校验,然后才会执行。 用到原语的插件要遵守两条规则: - 插件匹配的每个站点都必须声明该原语读取的语义键(例如 `mermaid` 读取 `assistantTurn` 和 `codeBlock`)。`requires.handlers` 可写可不写:`native` 操作本身就隐含了它用到的原语。 - `engine` 至少要写到首次提供该原语的引擎版本。`formulaCopy` 从引擎 1.3.0 开始提供,所以用到它的插件写 `">=1.3.0"`。写低了 `bun run catalog:build` 会失败,因为旧版 Voyager 那时会报缺少 handler,而不是提示用户升级。 原语只增不改:新参数一定是可选的,破坏性变更会换一个新名字。 有行为变化的插件还可以写一行 `changelog`,popup 会把它显示在版本号旁边。翻译放在 `i18n..changelog`,和 `name`、`description` 并列。 ## 什么时候不适合做成普通插件 如果功能必须执行 JavaScript、拦截请求、读写 Voyager 内部数据,或依赖复杂的运行时逻辑,它就不适合作为普通声明式插件提交。 这类功能请先开 Issue 说明需求。确实需要内置能力时,我们会考虑把它做成 Voyager 主仓库里的 builtin/native 插件,例如 Formula Copy。 ## PR 前检查 先跑 `bun run plugin:check <插件目录>`,并把输出贴进 PR。它会一次列出所有问题:清单和 CSS、`matches` 是否越出站点范围、原语、语义键、十种语言的翻译,以及 README;CI 也会对每个内置插件跑同一套检查。 - 插件默认关闭,用户需要自己启用。 - 已检查没有功能几乎相同的现有插件;如果有,优先改进现有插件。 - 在目标网站的浅色和深色主题都测试过。 - `matches` 没有覆盖无关网站。 - 没有远程资源引用。 - 插件目录包含 `plugin.json`、必要的 CSS 文件和简短 README。 - 官方插件的目录位于 `catalog/sites//plugins//`,`matches` 落在站点 `matches` 范围内,并且 `catalog/marketplace.json` 里有对应条目(Gemini 和 AI Studio 插件随 Voyager 一起发布,不需要此条目)。 - PR 描述里写清楚测试页面、截图或录屏,以及可能影响的页面区域。 保持简单、克制、可撤销。一个插件只解决一个明确问题,通常会更容易合并和维护。