WRITING
三个开源贡献复盘:Raycast · WeChatTweak · Cardinal

三个开源贡献复盘:Raycast · WeChatTweak · Cardinal
2026 年上半年,我给三个性质完全不同的上游项目各提了一次 PR:
- Raycast DocKit —— 一个上架 Raycast Store 的文档格式化扩展(TypeScript + Python 后端)
- WeChatTweak 微信 4.x 防撤回 —— 一次 macOS 二进制字节补丁(Swift + ARM64 逆向)
- Cardinal 持久化搜索历史 —— 给一个 1610⭐ 的 Rust+Tauri 文件搜索 app 加功能(React/TS)
三件事横跨「扩展生态 / 系统逆向 / 桌面应用」三个圈子,语言、方法、难点都不一样。这篇不是炫耀清单,是复盘:每个的必要性(为什么非做不可)、方法(具体怎么做)、以及合并的门槛到底卡在哪。
总览对照
| Raycast DocKit | WeChatTweak 4.x 防撤回 | Cardinal 搜索历史 | |
|---|---|---|---|
| 上游 | raycast/extensions | sunnyyoung/WeChatTweak | cardisoft/cardinal(1610⭐) |
| PR | #26665 · ✅ 已合并 | #1039 · 🟡 open | #222 · 🟡 open · CI 全绿 |
| 语言/栈 | TypeScript + Python CLI | Swift + ARM64 汇编逆向 | React + TypeScript |
| 改动量 | +3499/-0(11 文件,多为 lock) | +114/-10(5 文件) | +517/-3(22 文件) |
| 必要性来源 | 自己的院报告 docx 排版重活 | 自己想在微信 4.x 上防撤回 | 自己常一次查多个关键词 |
| 难点 | PR 流程规范 + 熬过 stale-bot | 无符号 dylib 里精确定位补丁点 | 定位为「补充」而非重复、干净分支 |
| 产物 | 上架 Store | fork + 可复现字节补丁 | 自编译版已装机 + PR |
一句话概括三种「合并的门槛」:**Raycast 卡在流程规范,WeChatTweak 卡在逆向严谨度,Cardinal 卡在上游礼仪。**代码本身反而都不是最难的部分。
一、Raycast DocKit:代码没问题,两次却死在「没写好 PR」
PR #26665 · 已合并 2026-04-30 · 上架 Store
必要性
日常写院里的报告,Word/PPT/Excel 里全是重复的中文排版体力活:中英标点混用要统一、引号要配对、单位符号要规范、PPT 字体和表格样式要统一、表格要在 xlsx/csv/txt 之间转。这些我早就写了个 Python CLI(python3 -m dockit)在跑,但每次都要开终端敲命令。DocKit 就是把这个 CLI 包成三条无界面 Raycast 命令,对着 Finder 选中的文件一个快捷键就跑完:
- Format Word —— 引号配对、英文标点转中文、单位符号标准化
- Convert Spreadsheet —— xlsx ↔ csv ↔ txt(下拉选目标格式)
- Standardize PowerPoint —— 统一字体、修文本格式、设表格样式
选文件 → 触发命令 → HUD 弹结果 → 输出写回同目录。
方法与真正的复盘点
技术上不复杂:TS 写三个 no-view 命令,run-dockit.ts 调本地 Python 后端。真正值得记的是这已经是我第三次给 raycast/extensions 提扩展了——前两次全部 stalled 关闭。

教训很直接:前两次(ts2、quick-open-app)代码未必有问题,死因是 PR 用了裸模板——描述空着、没录 demo、没回评审、然后被 stale-bot 熬到自动关闭。DocKit 反过来做全套:填满 PR body(命令表 + How it works + 安装步骤 + 在线 demo + 明确的 screencast 描述),过 ray lint/tsc/CI,而且主动跟进——逐条回评审意见,还在快被 stale-bot 关掉前顶评论续命,直到 maintainer approve。
Raycast 的合并,PR 卫生 + 熬过 stale-bot 的耐心,和代码本身一样重要。这是三个贡献里最「非技术」的一课。
二、WeChatTweak 微信 4.x 防撤回:无符号 dylib 里翻一条跳转指令
PR #1039 · open · fork zengtianli/WeChatTweak
必要性
上游 WeChatTweak 只覆盖到微信 3.8.x——撤回逻辑在主程序 Contents/MacOS/WeChat 里。微信 4.x 把这块逻辑搬进了 Contents/Resources/wechat.dylib(strip 过的 C++ 内核)。结果就是:工具还按写死的老路径 patch 主程序,在 4.x 上打不到任何撤回代码,每个 4.x 构建号都报 unsupported。想在 4.x 上防撤回,就得自己进 dylib 找补丁点。
方法:翻 4 个字节,且翻之前先验字节
核心只有一句话——把 parseRevokeXML 入口的一条条件跳转 cbz w0, SKIP 改成无条件跳转 b SKIP。撤回指令照收照解析,但删消息的代码永远走不到:

严谨度体现在几个地方,这是逆向补丁工具和「随便改改」的分水岭:
- 原地等长替换:
cbz和b都是 4 字节定长、目标偏移相同,只翻E00F0034→7F000014,不改二进制布局。 - 地址靠几何特征定位,不靠符号(符号被 strip 了):用
parseRevokeXML的入口stp序言 +entry+0x270的cbz w0+entry+0xA04的str x0三点组合,在整个 arm64 切片里唯一命中,再反汇编逐字节核对。 - 写入前先验原始字节(
expected字段):只有目标处确实是E00F0034才写;打错微信版本报expectedMismatch拒写,不会盲写把微信弄坏。 - 重签名顺序:先单独 ad-hoc 签被改的 dylib,再
--deep签整个 App,避免运行到改动代码页时Code Signature Invalid。 - 验证只能靠实机:找人真发一条消息再撤回,实测拦住才算数——符号剥离,静态无法确认。
后续还实现了「留提示」变体(
--variant keeptip):不拦解析,而是把newmsgid清零(str x0→str xzr)让删除落空、提示照插。中间还修正过一次方向错误——早先以为要去 NOP 下游的删除调用(在虚派发接收侧、静态难定位而搁置),后来发现根本不用找删除调用,源头清零即可。把走过的弯路也记下来,这是复盘的价值。
附带产出:帮社区把「三堵墙」讲清楚
issue #1038 里一堆「版本和我不一样」的用户卡住,但大家其实卡在三个不同的问题上,却都被同一句 Unsupported version 搅混了。我把它画成一条链:

| 墙1 · 版本墙 | 墙2 · 路径墙 | 墙3 · 写入墙 | |
|---|---|---|---|
| 报什么 | Unsupported version | Unsupported version(同报错、不同根因) | You don't have permission to save "wechat.dylib" |
| 根因 | 构建号地址全变、config 里没有 | patch 默认读远程 master,看不见你本地加的版本 | macOS 14+ 的 App Management/TCC 拦改签名 app,sudo 无效(TCC 认发起进程不认 root) |
| 解了吗 | ✅ locate_revoke.py 自动定位 | ✅ 改默认本地优先读 config | ⚠️ 根因已定位,解法=给终端授「App 管理」权限,非代码问题 |
墙1、墙2 是同一句报错的两个不同原因,最容易搅混;墙3 是另一层(写入权限),跟版本、config 路径都无关。把它们拆开画在一条链上,比逐条回复「你 addr 抄错了吧」有用得多——给开源项目最有价值的贡献,有时不是代码,是把混乱的问题诊断清楚。
三、Cardinal 搜索历史:怎么让一个改动「配得上」被合并
PR #222 · open · CI 全绿 · MERGEABLE
必要性
Cardinal 是个「Everything 式」的 macOS 文件名极速搜索 app(Rust+Tauri,自建索引,本机实测索引了 643 万文件)。我常常一次要查好几个东西,但它的搜索历史只存在内存里——关掉 app 就没了,而且只能靠 ↑/↓ 键翻,没有可点的界面。我想要的是持久化 + 一个能点的下拉面板。
方法:功能本身 + 让它「可被合并」
功能技术含量不高:一个 useRecentSearches hook 把查询写进 localStorage(置顶去重、上限 50),一个时钟按钮下拉面板点了就重跑。有意思的设计是记录时机——不只在按 Enter 时记,打开某个结果时也记(一次导致「打开文件」的搜索,才是值得留的搜索):

但给一个 1610⭐ 的仓库提 PR,真正的功夫在让这个改动配得上被合并:
- 先查重、再定位为「补充」:上游 PR #31(已合并)加过内存态 ↑/↓ 翻历史。我这个是持久化 + 可点面板——是补充不是替换,PR 文案专门讲清这层关系并致谢 #31(用的是独立 hook 文件,完全不动 #31 的代码)。
- 发 PR 前先自我审查修 nit:一轮对抗式 code review 挑出下拉误用了
role="listbox"/"option"的 ARIA 反模式——在一个对可访问性很讲究的仓库里,维护者大概率会要求改。所以发之前就改成纯 button 语义 +aria-haspopup,不留把柄。 - PR 分支做干净:只含功能本身的 22 个文件,剔除我自己 fork 维护用的 CLAUDE.md/.claude——上游不该看到这些。
- 本地过全部 CI 门再推:
format:check/typecheck/vitest(278 全过)/build全绿才 push。结果上游 CI 10 项一次全绿。
上游合并与否,看的是「你有没有替维护者把该想的都想到」:查重、致谢前人、修掉可预见的评审意见、分支干净、CI 绿。礼仪就是把评审的成本降到最低。
跨项目复盘:三种「上游合并」的门槛
三个贡献踩下来,最大的收获不是某段代码,是看清了不同生态里「被合并」到底卡在哪:
| 生态 | 真正的门槛 | 我的应对 |
|---|---|---|
| Raycast(扩展 Store) | 流程规范 + 耐心 —— 代码可能没问题,但空 PR body + 不跟进 = 被 stale-bot 熬死 | 填满 PR、录 demo、逐条回评审、顶掉 stale-bot |
| WeChatTweak(系统逆向) | 严谨度 —— 无符号二进制里改错一个字节就把 app 弄坏 | 几何特征唯一定位 + 写前字节校验 + 实机验证 + 诚实标注未解部分 |
| Cardinal(成熟 app) | 上游礼仪 —— 维护者凭什么花时间审你 | 查重定位为补充、致谢前人、发前修 nit、干净分支、CI 全绿 |
几条通用的:
- 每个「必要性」都来自我自己的真实工作流——院报告排版、想防撤回、常查多词。不是为了刷贡献而找项目,是自己先有痛点、顺手做成了公共产出。这是能持续做下去的根本。
- 诚实标注「没解决的部分」比假装全能更值钱——WeChatTweak 里我明写了群聊无提示、写入墙是权限问题不是代码问题;Cardinal 里明说 app 二进制
strings抓不到前端、a11y 验证靠的是「源码→build→CI」而非 grep。复盘要经得起自己回看。 - 「让别人愿意合并」是一门独立于写代码的手艺——它跨三个生态都成立,而且是前两次 Raycast PR 失败教我的。
对职业前途的意义
这三个都是公开可验证、领域+代码交叉的产出(正好卡在我「水利领域 + 代码 + AI 应用」的核心定位上),比简历上一句「熟悉 XX」硬得多——PR 链接、Store 上架、合并记录都能直接甩出来。逆向那一个尤其能说明「能啃硬骨头」。后续价值锚点:Cardinal #222 一旦被上游合并,就是一个 1610⭐ MIT 项目的 merged contribution,值得盯着跟进到合并。
源材料:三个 PR 本体 + ~/Apps/WeChatTweak/docs/{anti-revoke-patch,user-blockers}.md(逆向笔记)+ Cardinal 自编译 fork(~/Dev/cardinal)。