- 人工智能
- AI Agent
- MCP 服务
- 工具调用
- GUI 自动化
- 语音
- 数字人
- 交互助手
【免费下载链接】airi
💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。
本篇技术指南围绕 pnpm 的补丁(patch)机制展开,系统讲解如何用pnpm patch/pnpm patch-commit直接修改第三方包源码、如何在pnpm-workspace.yaml中声明与管理工作区级补丁,并结合开源仓库 AIRI 中真实存在的 6 个补丁(Responses 流式 SDK、Minecraft 寻路库、Live2D 渲染库、赞助图生成、标签页选举、全局快捷键原生模块)逐一拆解补丁动机、diff 结构与配套测试。读完本文,你将掌握补丁的创建、格式、配置、更新与故障排查全流程,并理解大仓(monorepo)中"先打补丁、后追上游"的工程化套路。
一、为什么需要 pnpm 的补丁机制
pnpm 的补丁特性允许你直接修改安装到node_modules中的第三方包源码。它的典型适用场景有两个:
- 上游尚未发布修复:依赖包存在 bug 或行为缺陷,但 upstream 的 PR 还在 review 中,等不及发版;
- 按项目定制行为:依赖本身没问题,但你需要针对自己的业务场景调整它的运行逻辑(例如让 AI 代理的寻路行为更符合游戏场景)。
与"fork 一份再发布私有包"相比,补丁机制的成本低得多:它只保存一份 diff,不维护整份源码副本,升级依赖时也能通过重建补丁快速跟上。AIRI 仓库在根目录patches/下集中存放了 6 个补丁文件,并在 pnpm-workspace.yaml 的patchedDependencies字段中统一声明,是研究补丁管理最佳实践的现成案例。
二、创建补丁:三步流程
第 1 步:初始化补丁
pnpm patch <pkg>@<version> # 示例 pnpm patch express@4.18.2该命令会把指定版本的包源码解包到一个临时目录,并在终端输出该目录的路径:
You can now edit the following folder: /tmp/abc123...注意这里的包名与版本必须与patchedDependencies中将要声明的键精确一致,版本号决定补丁的绑定粒度(见下文"无版本键"一节)。
第 2 步:编辑文件
进入临时目录,直接修改源码:
cd /tmp/abc123... # Edit files as neededAIRI 的实际操作方式与此一致,见 patches/README.md 中对@xsai-ext/responses补丁的更新说明:
pnpm patch @xsai-ext/responses@0.5.1 # Edit the directory printed by pnpm. pnpm patch-commit <directory>第 3 步:提交补丁
pnpm patch-commit <path-from-step-1> # 示例 pnpm patch-commit /tmp/abc123...提交后 pnpm 会做两件事:
- 在
patches/目录生成.patch文件; - 在
pnpm-workspace.yaml的patchedDependencies中登记这条映射。
patches/ └── express@4.18.2.patchpatchedDependencies: express@4.18.2: patches/express@4.18.2.patch⚠️ 重要:
patchedDependencies(以及所有 pnpm 设置)现在位于pnpm-workspace.yaml,而不是package.json#pnpm字段。这一点在 AIRI 仓库中体现得十分彻底:6 条补丁全部登记在根目录 pnpm-workspace.yaml 中。
三、补丁文件格式:unified diff 解析
补丁采用标准的unified diff 格式,pnpm 在安装时据此把改动重新应用到目标包上。一个典型的文件修改 hunk 长这样:
diff --git a/lib/router/index.js b/lib/router/index.js index abc123..def456 100644 --- a/lib/router/index.js +++ b/lib/router/index.js @@ -100,6 +100,7 @@ function createRouter() { // Original code - const timeout = 30000; + const timeout = 60000; // Extended timeout return router; }各部分含义:
diff --git a/... b/...:diff 的源路径与目标路径;index abc123..def456 100644:文件 blob 哈希与权限位,pnpm 应用补丁时用于校验文件内容是否匹配;---/+++:旧文件 / 新文件标记;@@ -100,6 +100,7 @@:hunk 头,表示旧文件第 100 行起 6 行、新文件第 100 行起 7 行,并附带函数上下文;- 以
-开头的行为删除行,以+开头的行为新增行,空格开头的是上下文行。
除了修改已有文件,补丁还可以删除整个文件。AIRI 中 patches/pixi-live2d-display.patch 的开头就是删除文件的形式:
diff --git a/core/README.md b/core/README.md deleted file mode 100644而 patches/uiohook-napi@1.5.5.patch 仅有一行binding.gyp的删除声明——一个极其精简的补丁(详见下文第六节)。
四、管理补丁的生命周期
列出已补丁的包
pnpm list --depth=0 # Shows (patched) marker for patched packagespnpm list会为被补丁的依赖标注(patched)标记,方便你快速核对哪些包携带了本地改动。
更新一个补丁
# 重新打开临时目录 pnpm patch express@4.18.2 # 编辑完成后重新提交 pnpm patch-commit <path>再次执行pnpm patch会基于已补丁的版本重新展开源码,因此更新补丁是增量式的:旧的改动仍保留在展开目录中,你只需叠加新改动后重新patch-commit即可。
移除一个补丁
pnpm patch-remove <pkg>@<version> # 示例 pnpm patch-remove express@4.18.2也可以手动移除,三步完成:
- 删除
patches/中的补丁文件; - 从
pnpm-workspace.yaml的patchedDependencies中删掉对应条目; - 重新执行
pnpm install。
五、补丁配置详解
多包与工作区共享
补丁在整个 workspace 内从根目录的pnpm-workspace.yaml全局共享,所有使用匹配版本的 workspace 包都会自动应用:
patchedDependencies: express@4.18.2: patches/express@4.18.2.patch lodash@4.17.21: patches/lodash@4.17.21.patch '@types/node@20.10.0': patches/@types__node@20.10.0.patch注意 scoped 包(如@types/node)在生成补丁文件名时,@与/会被替换成__。AIRI 的 @xsai-ext__responses@0.5.1.patch 正是这种命名(@xsai-ext/responses@0.5.1→@xsai-ext__responses@0.5.1.patch)。
无版本键:patch 所有版本
如果一个键不带版本号,它会作用于该包被安装的每一个版本:
patchedDependencies: express: patches/express.patch # 所有 express 版本都应用AIRI 仓库中 mineflayer-pathfinder.patch 与 pixi-live2d-display.patch 都采用了无版本键(见 pnpm-workspace.yaml)。这意味着依赖升级时补丁会自动应用到新版本上——优点是省去每次升级重建补丁的麻烦,缺点是新版本源码变动可能导致补丁失效,因此无版本键通常要求配合足够的回归测试。
从 config dependency 中提供补丁
补丁文件也可以放在一个共享的配置依赖(config dependency)包中,通过路径引用:
configDependencies: my-patches: '1.0.0' patchedDependencies: react: node_modules/.pnpm-config/my-patches/react.patch这样多个项目可以复用同一份补丁集,由配置依赖的版本号统一管理补丁的发布与升级。
allowUnusedPatches
allowUnusedPatches: true # 列出的补丁未被应用时不报错默认情况下,如果patchedDependencies里列出了某个补丁,但安装时并没有真正命中它(例如该依赖没有被任何包使用),pnpm 会报错。显式开启allowUnusedPatches可以放宽这一限制,适合补丁条目较多的场景。
⚠️ 注意:
ignorePatchFailures已在 pnpm v11 中移除。补丁应用失败现在总是抛错;当多个补丁被分组时,所有错误会在最后一起报告。
六、AIRI 仓库的 6 个实战补丁
AIRI 的patchedDependencies声明了 6 条补丁,覆盖"修复上游行为""定制运行逻辑""适配构建环境"三类用途,是理解补丁实战价值的绝佳样本。
1.@xsai-ext/responses@0.5.1:让 Responses 流终态严格化
这是 AIRI 中最"重型"的一个补丁,动机记录在 patches/README.md 中:
- 拒绝
response.incomplete与流式 error 事件,避免 AIRI 把失败回合持久化为成功; - 为原生
web_search工具及其输出 Item 补齐类型,使其可在多步(step)间回放; - 在 transcript 提交前
awaitonNativeEvent,让消费方能保留 sources 与搜索活动。
从 补丁本体 可以看到具体改动:dist/index.d.ts中新增WebSearchTool、WebSearchCall类型并扩展ItemParam/ItemField/tools,新增onNativeEvent回调;dist/index.js中把response.incomplete分支由return改为throw new Error(...),error 事件同样直接抛出,并在事件入队前await options.onNativeEvent?.(event)。
该补丁的行为被两层测试锁定:
- 浏览器侧回归测试 packages/provider-inference/src/responses.browser.test.ts:验证"SSE 流在终态事件前结束会被 reject"、"HTTP 成功响应内携带 provider failure 会被 reject"、"异步事件消费者在 finalize 前被 await"以及 OpenAI
response.reasoning_text.delta事件名映射; - 核心集成测试 packages/core-agent/src/runtime/responses.test.ts:验证 hosted search 与本地工具同轮执行、
web_search_callItem 在下一轮请求中被完整回放、citations 事件被正确派发。
因为 npm 发布的是编译后的产物,补丁直接改的是dist;若要向 upstream 贡献,需要把这些改动移植回源码仓库再跑同样的回归。
2.mineflayer-pathfinder:Minecraft 寻路行为定制
mineflayer-pathfinder.patch(无版本键)对index.js与lib/movements.js做了多处寻路增强,主要改动包括:
- 熔岩规避动态化:新增
updateLavaAvoidance(),机器人身处熔岩时允许寻路穿过熔岩逃生,否则仍规避熔岩; - 卡死保护:引入
stuckCount与lastStuckPos,连续 3 次重置路径却无进展时直接放弃,防止无限重置循环; - 门与活板门支持:
canOpenDoors置为true,开放活板门视为可通行、关闭的活板门视为实体,并新增getMoveClimbUpThroughTrapdoor/getMoveClimbTop两个爬梯/爬藤动作; - 到达判定收紧:节点到达阈值从
0.35收紧到0.175,减少提前到达导致的路径抖动; - 攀爬扩展:把藤蔓(vine)及多种垂藤类方块加入 climbables。
在 AIRI 中,这个补丁是 Minecraft 集成(integrations/minecraft)能力的基础:integrations/minecraft/src/cognitive/reflex/runtime.ts直接使用pathfinderModel.Movements与GoalFollow实现自动跟随玩家(runtime.ts);而 integrations/minecraft/src/skills/patched-goto.ts 则是在补丁后的 pathfinder 之上封装的健壮寻路包装器,提供 ETA 动态超时(15s~5min 钳制)、每 5s 一次的停滞检测(STAGNATION_THRESHOLD = 1.5格、连续 3 次判定停滞则取消)以及结构化返回结果,供collect-block.ts、world-interactions.ts等技能模块调用。
3.pixi-live2d-display:Live2D 模型加载适配
pixi-live2d-display.patch(无版本键)同时修改了cubism2、cubism4与index的 ES/UMD 两个产物,核心改动两处:
- 查找模型 settings 文件时排除
items_pinned_to_model.json:原先createSettings只按.endsWith("model.json") || .endsWith("model3.json")匹配,而 Live2D 模型目录里常附带items_pinned_to_model.json,会被误判为 settings 文件导致加载失败; - 去掉
encodeURI包装:validateFiles直接用file.webkitRelativePath,避免包含空格或中文字符的模型文件路径被编码后无法解析。
此外补丁还删除了包内附带的core/README.md、core/live2d.d.ts与cubism/.vscode/等多余文件,减小安装体积。
在仓库中,该库服务于 Live2D 角色模型渲染:apps/stage-tamagotchi/electron.vite.config.ts的optimizeDeps.exclude中排除了整套 Live2D Cubism SDK 模块(electron.vite.config.ts),packages/stage-ui/src/components/scenarios/dialogs/model-selector/model-selector.vue通过handleAddLive2DModel让用户从本地文件加载模型,补丁确保了含非 ASCII 路径与附带 JSON 的模型包都能正确解析。
4.sponsorkit@17.1.0:Open Collective API 请求头适配
sponsorkit@17.1.0.patch 非常小:把fetchOpenCollectiveSponsors中 GraphQL 请求的请求头从"Api-Key"改为"Personal-Token"(两处),以适配 Open Collective 当前的 API 鉴权方式。
该补丁支撑着根目录 sponsorkit.config.js 的赞助图生成:配置声明了patreon、opencollective与自定义kofi三个 provider,并以tiers渲染器输出svg/json格式的赞助者榜单。
5.tab-election@4.6.2:多标签页 leader 选举竞态修复
tab-election@4.6.2.patch 同时修改dist/tab.js与src/tab.ts,修复 leader 选举期间的调用派发竞态:
Deferred新增awaitingLeader标志:调用在等待hasLeader()结果期间标记为"等待 leader",leader 选举广播与重发逻辑都会跳过这些尚未确定派发路径的调用,避免重复执行;_onReturn中找不到 deferred 时静默返回(原实现console.error):因为消息投递是 at-least-once 语义,重复或迟到的 return 可能出现在调用已 settle 之后,此时不应再报错。
在 AIRI 中,tab-election服务于多窗口场景下的"单 leader"约束:apps/stage-tamagotchi/src/renderer/App.vue通过syncedPinia.onLeadershipChange监听 leader 变化,只有主 Stage 渲染器成为 leader 时才刷新内置工具、MCP 工具与 Home Assistant 工具 store(App.vue),配套的app.browser.test.ts也验证了 leader 选举的收敛行为。
6.uiohook-napi@1.5.5:跳过源码编译
uiohook-napi@1.5.5.patch 全文件只有一个动作:删除binding.gyp。其效果是阻止 pnpm 在安装时尝试从 C 源码编译该原生模块,强制使用预编译二进制。
这与 AIRI 桌面端的构建配置环环相扣:
- electron.vite.config.ts 将
uiohook-napi加入externalizeDeps.include,避免把含__dirname用法的原生模块打进 ESM bundle; - electron-builder.config.ts 在打包时排除
node_modules/uiohook-napi/libuiohook{,/**},不携带 C 源码; - 运行时由 apps/stage-tamagotchi/src/main/services/electron/global-shortcut-uiohook.ts 使用
uIOhook/UiohookKey实现跨平台全局快捷键监听,并把 W3C 键名(KeyA、Digit0、F1…F24)映射到 uiohook 键码。
七、最佳实践
结合原文档要点与 AIRI 的工程实践,维护补丁时应遵循以下原则:
- 版本特异性:补丁与精确版本绑定(无版本键除外)。升级依赖时同步重建补丁,避免 diff 漂移导致应用失败。
- 文档化补丁:在
patches/README.md中记录每个补丁存在的原因、解决的问题与上游跟踪信息。AIRI 的 patches/README.md 就是范例——它不仅写清了三个修复动机,还明确指出了"何时可以移除补丁"(上游发版并通过相同回归测试时)。 - 最小化补丁:保持补丁小而聚焦。像
uiohook-napi那样一行删文件的补丁,远比几百行的大改好维护。 - 追踪上游:记录对应的 upstream issue/PR,上游修复发版后及时评估移除。AIRI 对
@xsai-ext/responses明确写着"移除前需重查 incomplete 行为——AIRI 将 incomplete 回合视为失败"。 - 测试补丁:补丁必须配套回归测试。AIRI 的做法是把测试落在仓库内(如 responses.browser.test.ts),当补丁被移除时这些测试会成为上游发版是否合格的验收标准。
八、故障排查
补丁应用失败
ERR_PNPM_PATCH_FAILED Cannot apply patch最常见的根因是依赖版本变了,diff 上下文已无法匹配。处理方式是重建补丁:
pnpm patch-remove express@4.18.2 pnpm patch express@4.18.2 # Reapply changes pnpm patch-commit <path>如果失败信息与 hunk 行号相关,也可能是上游在该版本内改动过同一段代码——此时需要手动对比展开目录与补丁内容,调整 diff 或重新制作。
补丁未生效
按以下清单逐一核对:
patchedDependencies中的版本是否与锁定的安装版本精确一致(无版本键除外);- 确认补丁配置写入了
pnpm-workspace.yaml(而不是package.json#pnpm); - 修改配置后必须重新执行
pnpm install,补丁在安装阶段应用; - 用
pnpm list --depth=0检查目标包是否带(patched)标记; - 若使用了无版本键,确认升级后的新版本源码结构与补丁兼容。
补丁机制是 pnpm monorepo 工程中"以最小成本驾驭第三方依赖"的关键手段:它把"等上游修复"的被动等待,转化为"本地先行修复 + 测试锁定 + 上游发版后移除"的主动工程循环。AIRI 仓库的 6 个补丁完整展示了从流式 SDK 行为修正、游戏寻路定制到原生模块构建适配的各类场景,可作为自托管项目补丁管理的直接参考模板。
- 人工智能
- AI Agent
- MCP 服务
- 工具调用
- GUI 自动化
- 语音
- 数字人
- 交互助手
【免费下载链接】airi
💖🧸 自托管、归你拥有的 Grok 风格 AI 伴侣与 waifu / 赛博生命灵魂容器,目标是接近 Neuro-sama 的高度;支持实时语音聊天、Minecraft 和 Factorio 游玩,支持 Web / macOS / Windows。
相关推荐
pnpm Patches 实战指南:以 AIRI 仓库为例,看懂如何为第三方包打补丁
pnpm Patches 实战指南:以 AIRI 仓库为例,看懂如何为第三方包打补丁 本文以 AIRI 这个 pnpm 多包 monorepo 为实例,完整讲解
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染pnpm 补丁机制完全指南:用 patchedDependencies 直接修改第三方依赖
pnpm 补丁机制完全指南:用 patchedDependencies 直接修改第三方依赖 本篇技术指南基于本仓库 pnpm 技能包 https://link.
AI 技能人工智能airi 仓库实战:pnpm Overrides 强制依赖版本与传递依赖治理完整指南
airi 仓库实战:pnpm Overrides 强制依赖版本与传递依赖治理完整指南 本篇技术指南以 pnpm 的 overrides 机制为核心,结合 moe
人工智能AI AgentMCP 服务工具调用GUI 自动化语音数字人交互助手后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考