Voyager 仓库贡献避坑指南:从 PR 机制到插件系统的全链路实战手册
【免费下载链接】voyagerEnhancement suite for Gemini, AI Studio, Claude & ChatGPT — plus a prompt manager for any websites, DeepSeek Harness included. / 面向 Gemini、AI Studio、Claude 与 ChatGPT 的增强套件;其中的提示词管理器可用于任意网站,如 DeepSeek Harness。项目地址: https://gitcode.com/gh_mirrors/ge/voyager
本指南以仓库维护者沉淀的《Repository-Specific Traps》repo-traps.md 为主体,逐条还原 Voyager(面向 Gemini、AI Studio、Claude 与 ChatGPT 的浏览器增强套件)在代码贡献、PR 提交、插件开发、文档维护过程中真实踩过的坑,并结合src/下可验证的源码实现给出对应的正确做法。读完你将对本仓库的 squash-merge 协作模型、@codex review自动评审流程、"新站点必须走插件系统"的架构红线、主题解析优先级、文档多语言镜像的维护边界有完整认知,能显著减少一轮又一轮的 review 返工。
这份清单不是空泛的社区行为准则,而是每条条目都对应至少一次真实 review 教训(来源 PR 编号在括号中标注),且"CI 一项都抓不到它们"——所以它被 voyager-contribute 技能强制列为 preflight 阶段的必读材料(第 5 步:Read repo-traps.md)。
一、Git 与 PR 机制:squash-merge 的连锁反应
Voyager 仓库采用squash-merge(压缩合并),这一决策直接决定了本仓库所有 Git 操作的正确姿势,也是新手最容易翻车的环节。
1.1 合并后本地分支"看起来没合"是正常现象
PR 合并后,你的本地分支会显示为 unmerged 或 conflicted——这是 squash-merge 的固有特征,不是操作失误。正确的处理方式是:
- 删除旧分支;
- 从最新的
main重新拉取新分支; - 绝不从旧分支重新开 PR。文档记载了真实案例:PR #867 就是因为从旧分支重复开 PR,被作为 self-duplicate 关闭。
1.2 PR 标题就是 squash commit 的提交头
由于 squash 合并会把整个 PR 压缩成一个 commit,PR 标题会成为该 commit 的 header。因此标题必须符合 Conventional Commits 格式:
type(scope): imperative summary例如fix(sendBehavior): respect gvCtrlEnterSend during IME composition。文档明确禁止用分支名或Fixes #N作为标题(#859、#865、#867 均为反面教材)。这与 SKILL.md 第 3 阶段"Create a Conventional Commit with a lowercase scope and a header no longer than 100 characters"的规范互相印证,且提交时可附Fixes #<issue>/Closes #<issue>关联 Issue。
1.3 开 PR 前必须检查 diff 范围
文档给出的硬性命令是:
git diff origin/main --stat(origin/main指跟踪基础仓库main的远端;也可用其他跟踪该分支的远端名。)目的是排查"无关 churn"是否混入:例如 #865 曾意外地删除了sponsors.svg(仓库根目录的赞助商图片),这类资产、lockfile 的意外变更在 squash 合并后会永久进入历史。注意本指令不允许使用rm/mv等破坏性命令,实际排查以只读 diff 为主。
1.4@codex review是唯一触发自动评审的方式
- 裸写
@codex不触发任何评审,必须写@codex review; - 每个新的 head 触发一次,rebase/force-push 后必须重新触发;
- 有评审正在运行时不要重复触发(#854);
- 人类评审者在 Codex 覆盖完当前 head SHA 之后才会做最终确认——即 Codex 评审是人工评审的前置关卡。
这解释了为什么 SKILL.md 要求 push 后用gh核验 PR author、base branch、commit 集、changed files 与 CI 状态,确保 head 与评审快照一致。
二、复用既有实现:reviewer 视"重复造轮子"为缺陷
本仓库最大的隐性规则是:动手写并行逻辑前,先 grep 是否已有同平台的原语(primitive)。reviewer 会把"对既有行为的临时重写"直接判为缺陷。以下是文档点名的五个关键先例及其源码位置。
2.1 发送/键盘处理:复用sendBehavior
任何涉及"Enter 发送 / 换行"的新逻辑,都应复用 sendBehavior 模块,而不是另写一套英文-only 的选择器或无条件的 Enter 处理器(#854)。
为什么不能重写?看实现就明白它处理了多少平台细节:
- 本地化标签:
isSendActionButton已覆盖发送/送信/enviar 等多语言按钮标签,且兼容仅图标的按钮(SEND_BUTTON_SELECTORS+isVisibleButton判定offsetParent !== null); - 设置项:行为受
gvCtrlEnterSend(Gemini Ctrl+Enter 发送)、gvAIStudioEnterSend(AI Studio Enter 发送)、gvSafariEnterFix(Safari 双击 Enter 修复)三个存储开关控制,见 common.ts 中的StorageKeys; - 平台优先级:同时开启时 Ctrl+Enter 发送优先(Enter → 换行),且监听器仅在至少一个模式启用时才激活(
shouldBeActive()→activateListeners()/deactivateListeners()),全部关闭时零 DOM 监听开销; - 健壮性细节:
handleKeyDown中event.isComposing直接 return(规避 IME 组合输入误触发,对应 Issue 260);查找发送按钮采用closest()限定容器再querySelector的受限搜索,并跳过附件上传中的progress/role="progressbar"状态; - 多行插入:对 Gemini 的 Quill 编辑器(
ql-editor)用document.execCommand('insertParagraph')优先、insertHTML('<br><br>')其次、模拟 Shift+Enter 兜底的三级策略。
配套测试位于 sendBehavior.test.ts,新增发送类逻辑时应扩充它而不是新建平行实现。
2.2 文本插入:chatInput的多行感知插入
向输入框插入文本时必须复用 chatInput。裸用createTextNode会把多行 prompt 体压扁成单行——因为 Gemini 的 Quill 编辑器会静默丢弃通过insertText传入的\n。
正确的多行插入实现(insertMultilineViaExecCommand)先把文本按\n分段,段间用insertParagraph插入换行:
const segments = text.split('\n'); for (let i = 0; i < segments.length; i++) { if (i > 0) { let paragraphInserted = false; try { paragraphInserted = document.execCommand('insertParagraph', false); } catch { /* 忽略 */ } if (!paragraphInserted) return false; // …写入当前段 } }(完整实现见 chatInput/index.ts。)它同时维护selection光标位置并派发input事件保证数据同步。
2.3 导出 CSS:扩展共享样式构建器
为导出功能添加样式时,要扩展共享样式构建器buildKatexExportStyles(位于 katexExportStyles.ts),而不是把 CSS 复制粘贴进第二个导出服务(#847)。该工具函数目前被 PDFPrintService.ts、DeepResearchPDFPrintService.ts、ImageExportService.ts 三个导出服务共同引用——这正是"共享一份、多处生效"的范例:任何主题修复只需改一处。
2.4 导出 DOM 遍历:DOMContentExtractor的单一共享遍历
DOMContentExtractor(DOMContentExtractor.ts,约 1500 行)负责从 Gemini DOM 提取富文本内容。Gemini 会把section/div/response-element等层级任意嵌套,任何第二条独立的遍历代码路径都会漏掉某层结构(#847 为此花了 4+ 轮评审)。
一个非常典型的实现细节是queryOutsideThoughts:当用户展开 Gemini 的 "thinking" 面板时,DOM 顺序里会在真正的回复之前出现第二个message-content元素,普通的querySelector会先匹配到思考面板导致导出抓错内容。该函数遍历所有候选后排除model-thoughts, .thoughts-container, .thoughts-content内的元素(见 DOMContentExtractor.ts)。所以导出相关改动必须在这一个文件中扩展,并同步 DOMContentExtractor.test.ts。
2.5 弹层/全局监听/浮层:复制gv-pm-*集成模式
凡是 body 级 popover、全局事件监听、遮罩层,都要照抄已有的gv-pm-*集成,即 prompt-manager 系列组件沉淀的完整模式:close-outside(点击外部关闭)处理器、teardown(卸载清理)、主题覆盖。例如 coachmark/index.ts 就明确注释"Visual style mirrors the existinggv-pm-*body-appended popover(anchored…)";platformTheme 则通过注入--gv-pm-brand变量 +gv-platform-themedbody 类完成平台品牌色覆盖。
三、新站点功能必须走插件系统
这是本仓库最核心的架构红线:为 ChatGPT、Claude、DeepSeek 等新站点添加支持,一律通过插件系统,禁止硬编码进扩展主体(#865)。
3.1 两类插件的分界线
文档给出了清晰的两分法:
| 插件类型 | 适用场景 | 存放位置 | 加载方式 |
|---|---|---|---|
| 声明式插件(CSS + JSON) | 纯样式/声明式增强 | src/features/plugins/catalog/(经BundledCatalogPluginSource打包进扩展) | 随 catalog 快照加载 |
| 原生函数插件(真正需要 JS) | 必须运行代码的功能 | src/features/plugins/builtin/ | 默认禁用 + 可选 host 权限 + 动态 content-script 注册 +start/stop生命周期 |
源码证据:createDefaultPluginSources()按 tier 顺序组装三层源——BuiltinPluginSource(一方原生插件)→BundledCatalogPluginSource(声明式快照)→HostCatalogSource(每 host 的远程目录,只读后台刷新缓存),见 defaultSources.ts。合并规则(计划 D6/D20)保证 builtin id 永不被远程目录覆盖,远程版本不兼容引擎时回退快照并标记blockedUpdate。
目录实况:catalog 下有chatgpt/、claude/、deepseek/三个站点的声明式插件(如sites/deepseek/plugins/wrap-code/),index.test.ts 会校验"每个插件 id 等于其目录名、CSS 文件真实非空、每个matches模式都落在站点匹配范围内、marketplace.json与发现结果保持同步";builtin 下则有formula-copy、inputVim、claudeUsage、claudeTimeline、chatgptExport、chatgptTemporaryHandoff等原生函数插件,builtin/index.ts 的注释明确:"Like every plugin, builtin plugins shipDISABLED by default— the user turns them on in the popup"。
3.2 manifest 权限红线(hard stop)
严禁在manifest*.json(对应 manifest.json、manifest.dev.json)中新增静态content_scripts或必需的host_permissions。原生函数插件需要通过插件自身的 manifest 声明可选host 权限 +动态content-script 注册。未经 Issue 批准就提升 manifest 权限是 hard stop(#865)——这与 CI 无关,是评审硬门槛。
3.3 生命周期完整性与注册表只增不减
- 任何由存储开关控制的功能都需要完整生命周期:页面加载时能正确
start,运行时切换开关能正确destroy。"隐藏 UI 不等于禁用功能"(#854)。这与 sendBehavior 中activateListeners/deactivateListeners/cleanup的对称设计一脉相承:注册的 listener 通过cleanupFns数组统一摘除,MutationObserver同步 disconnect。 - 平台注册表是增量式的:绝不为了给自己的平台腾位置而删除或收窄其他平台的 adapter 或测试(#865)。例如站点适配器 gemini.ts、aistudio.ts 各自声明独立的主题选择器,只能增加不能改动别人的。
四、文档与多语言镜像:20+ 个文档表面的维护边界
文档相关的陷阱最容易让人措手不及,因为"文档"在本仓库不是一个文件,而是20+ 个表面:
- 主 README.md;
- 九个
.github/README_*.md语言镜像(README_ZH.md、README_AR/ES/FR/JA/KO/PT/RU/ZH_TW); - 十个
docs/<locale>/文档树(如 docs/zh、docs/en 等,每个含 40+ 篇 guide); - docs/public/llms.txt 与 docs/public/llms-full.txt(面向 LLM 的检索入口)。
关键的认知盲区:CI 里那个绿灯的i18n检查只比对src/locales/*/messages.json的 key 一致性(见 ci.yml 中i18njob 调用的node scripts/check-locale-keys.mjs),它完全覆盖不到上述 20+ 个文档表面(#874、#853)。也就是说,改了文档后 CI 可能全绿,但某语言镜像已经过期——这部分只能靠人工纪律。
另一条硬规则:永远不要删除已发布的文档路由——仓库没有重定向层。退役页面必须在原 URL 上替换为本地化说明(notice),而不是删文件(#874)。
五、主题与数据正确性
5.1 主题解析顺序:三档优先级 + 冲突标记测试
主题解析必须遵循固定顺序(#859):
- 优先:
.theme-host.light-theme/.theme-host.dark-theme(Gemini 专属的 theme-host 元素); - 其次:泛化的
body/html/data-theme标记; - 兜底:
prefers-color-scheme媒体查询。
源码印证:Gemini 适配器声明hostSelector: '.theme-host'、lightSelector: '.theme-host.light-theme'、darkSelector: '.theme-host.dark-theme'(gemini.ts);AI Studio 则用body.light-theme/body.dark-theme(aistudio.ts);通用解析逻辑与单测见 contentStyleTheme.test.ts。
测试要求:写主题相关测试时必须用冲突标记(同时存在.theme-host.dark-theme和body.dark-theme但取其一),而不是单个信号——因为单个信号无法证明优先级实现正确(#859)。
主题一致性:导出产物要整体一致地应用主题——在强制白色文档里出现暗色图表不算"支持暗色模式"(#847)。
5.2 对话 ID 的命名空间与账户作用域
Gemini 的对话 ID 是带命名空间的(gemini:conv:<id>)。任何共享代码如果直接处理裸 ID,会悄悄孤立掉已存在的星标/书签数据(#865)。同时,构造任何路由时都必须保留/u/<index>/...的账户作用域。
5.3 提示词/文件夹数据的合并入口
Prompt 与文件夹数据存在多个合并入口:src/utils/merge.ts(含 merge.test.ts 配套测试)以及页面级的 Drive 合并。规则是:
- 数据形状(data-shape)变更必须路由到同一个共享合并 helper;
- 合并冲突时绝不丢弃或重命名已有用户记录(#854)。
这样保证星标、文件夹、历史记录在不同合并路径下结果一致。
5.4 回归笔记:Trap / Rule / Guard 三字段
涉及非平凡功能或修复时,需遵循 .github/docs/REGRESSION_NOTES.md 记录 Trap/Rule/Guard 条目(对应子目录 .github/docs/regressions/ 下的主题文件)。该格式由 validate-regression-notes.mjs 脚本强制校验:
- 每个主题文件必须恰好一个顶层标题(
#); - 每条 entry 必须有且仅有一个
- **Trap:**、- **Rule:**、- **Guard:**字段,且内容非空; - Guard 中引用的测试路径必须是仓库相对路径(禁止裸文件名),且引用的
.github|Voyager|docs|public|scripts|src路径必须真实存在(见validateGuardPaths)。
文档同时强调:只有当引入点影响解释时才添加 commit 细节;引用的任何 PR/commit 必须是真实的,不能用占位符(#859)。
六、范围纪律:评审比对的是 Issue 范围
最后一条常被忽视却决定成败的纪律:reviewer 拿你的行为与 Issue 确认过的范围做 diff,并双向拦截偏差——超范围做多了、或范围内的没做完,都会被拦(#854)。
- 当某个边界情况诱惑你改变产品约束时(例如强制名称唯一性),先在 Issue 里提问,不要自作主张;
- 在 PR 描述里明确列出 non-goals(非目标);
- 提交策略上的分界:一行可验证的修复可以直接提 PR(#876);新功能需要维护者在 Issue 或当前任务指令中显式批准方案——"被分配 Issue(assignment)或 /claim"只代表选中了负责人,不等于批准(#865)。这与 SKILL.md preflight 第 2 步"新功能需要维护者对方案的显式批准"完全一致。
结语:把这份清单变成你的提交前检查表
归纳起来,Voyager 的贡献者文化可以用四句话概括:
- 先 grep 再写码——
sendBehavior、chatInput、buildKatexExportStyles、DOMContentExtractor、gv-pm-*是五个必须复用的"前辈"; - 新站点走插件系统——catalog 放声明式 CSS+JSON,builtin 放原生函数插件,manifest 权限不升级;
- 文档是 20+ 个表面——i18n 绿灯不代表文档同步,已发布路由不删除;
- 范围即契约——一行修复直通 PR,功能先获批准,non-goals 写进 PR 描述。
每条陷阱都来自真实 PR 的学费(#854、#859、#865、#867、#874、#876),而 CI 一道都抓不到它们。把它当作写代码之前、而非 review 之后的读物,你的 Voyager 首次贡献将大幅减少往返轮次。
【免费下载链接】voyagerEnhancement suite for Gemini, AI Studio, Claude & ChatGPT — plus a prompt manager for any websites, DeepSeek Harness included. / 面向 Gemini、AI Studio、Claude 与 ChatGPT 的增强套件;其中的提示词管理器可用于任意网站,如 DeepSeek Harness。项目地址: https://gitcode.com/gh_mirrors/ge/voyager
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考