I am LAZY bones?
AN ancient AND boring SITE

2026年 07月 的归档

代码库放 iCloud 文件夹会怎样?

之前我有个习惯,会把代码(整个repo)放到 iCloud 管理的 ~/Documents 下。一直觉得挺方便的,因为即使没有 commit+push,我到另一台电脑上也能接着干活,登了同一个iCloud账号的目录会自动同步。

但最近遇到一个特别邪门的问题。一个用 uv 管理的 Python 项目,editable 方式安装,前一个小时还好好的,突然就:

邪门在哪呢?同一个 venv、同一个解释器,pytest 跑起来一千六百多个用例全绿。测试说包在,入口脚本说包没了。去 site-packages 里看,editable 安装落下的 .pth 文件好好躺在那,内容就是一行指向源码目录的路径,权限 644,路径也真实存在。文件在、内容对、权限对,但它就是不生效。

排查:谁把我的 .pth 吞了

先补一句背景:editable 安装的机制,是往 site-packages 里放一个 .pth 文件,Python 启动时 site 模块会读它,把里面的路径追加进 sys.path。这个文件失效,包就从解释器眼里消失。

做了三个对照实验,结果非常有意思:

1. 自己写一个探针 .pth(内容随便指个存在的目录)放进同一个目录——生效;
2. 把出问题的 .pth 原样 cp 成另一个文件名——生效;
3. 出问题的那个文件本身——死活不生效。

同目录、同内容、同权限,副本能用原件不能用,那唯一的区别只剩下文件的元数据了。ls 加个 -O 把 BSD 文件标志打出来:

flags 一栏赫然写着 hidden——macOS 的 UF_HIDDEN 文件标志。cp 不会复制 BSD flags,所以副本是干净的,这就解释了实验 2。而 CPython 从 3.11 起,site.py 处理 .pth 时加了一条安全加固(防止恶意软件用隐藏 .pth 做隐蔽注入):

带隐藏标志的 .pth,静默跳过,一个字的错都不报。两个各自都算合理的行为叠在一起,效果就是:文件在,import 没了。

那谁给文件打的 hidden?chflags nohidden 清掉,几秒钟之后再看,又变回 hidden 了——有个进程在实时跟我对抗。答案到这基本明了:这个仓库在 ~/Documents 底下,而这台 Mac 开着 iCloud 的“桌面与文稿”同步。把仓库挪出 Documents、重建 venv,flags 干干净净,问题再没复发。

iCloud Drive 到底在做什么

开了”桌面与文稿”同步之后,~/Documents 就不再是普通目录,而是由 fileproviderd 守护进程托管的同步空间。它主要干三类事:

1. 监听并上传每一次文件变更——它假设文件是”偶尔被人编辑的文档”;
2. 冲突消解——当它认为同一个文件出现两个竞争版本,不丢弃任何一边,而是生成”xxx 2″”xxx 3″这样的冲突副本;
3. 元数据管理——给托管文件打 xattr 和文件标志(上面那个 hidden 就是它干的),开了”优化 Mac 存储”还会把冷文件驱逐成无数据的占位符,读取时才按需下载。

对文档来说这些设计都挺好。但代码库不是文档。

代码库为什么全中

第一,写入模式冲突。开发工具链的写入是高频、批量、依赖原子性的:包管理器一次重写上万个小文件,Python 用”临时文件 + rename”做原子写,git 靠锁文件和 rename 更新引用。同步进程和这些写入异步竞争,竞争输了就落冲突副本。我这次在 site-packages 里就看到了 “_editable_impl_mycli 2.pth”、”3.pth”、”4.pth” 一窝副本,主 .pth 的内容更是被拼接成了四份路径首尾相连的乱码:

第二,元数据篡改。就是前面 UF_HIDDEN 那一段,而且它是主动维护的,你清掉它还会打回来。

第三,驱逐。.git/objects 里的 packfile、venv 里的二进制,都可能被”优化存储”驱逐成占位符——在线时表现为构建随机卡顿,离线时表现为仓库”损坏”。

第四,git 自己的风险。.git 里的 index、refs 一旦出现冲突副本,仓库状态可能真损坏。git 本身就是分布式同步工具,外面再套一层文件级同步,等于双重同步,语义必然打架。

这次踩到的坑

1. chflags nohidden 修不好——几秒内被 fileproviderd 顶回来,别在这条路上浪费时间;
2. 症状自相矛盾极具误导性:pytest 全绿(它靠 rootdir 机制自己把源码目录塞进了路径),入口脚本却挂了,两个工具对”包是否存在”给出相反答案,直觉上会先怀疑一万个别的东西,最后才怀疑文件系统在说谎;
3. 损坏是异步、随机发生的:同一个 venv 一小时内坏了两次,中间检查全都正常——因为坏不坏取决于同步进程什么时候追上来;
4. 诊断口诀:文件明明在但 import 不到,先 ls -lO 看 flags 列。

结论

iCloud(以及 Dropbox、OneDrive 这些文件级同步盘,机制细节不同但三板斧一样)适合放终态文档:文稿、图片、表格。任何带衍生状态的目录——代码库、venv、node_modules、.git、构建缓存——都不该放进去。代码的跨机同步交给 git,这本来就是它的职责,而且语义正确。仓库放个非托管路径(比如 ~/dev)就好;实在有目录必须留在 iCloud 里又想排除,macOS 没有官方排除项,只有给目录名加 .nosync 后缀这个 hack。

一个按文档假设设计的同步器,遇上一堆违反它全部假设的文件,双方都没有 bug,组合在一起就是灾难。就此,完毕。

每日一辨 DailyDiff

最近上架了一个新 App:DailyDiff(每日一辨)。做它的初衷很简单:一是我自己想把英语再往上提一提,二是儿子也在学英语,我想给他(也给自己)找一个每天花几分钟、细水长流的练法。

市面上背单词的软件很多,但它们解决的基本都是”认识”:看见 soldier 知道是战士,看见 warrior 也知道是战士。可这两个词的区别是什么、什么场合该用哪个——这种”掌握”层面的功夫,几乎没有软件管。而”认识一个单词”和”掌握一个单词”之间恰恰隔着一条鸿沟,它决定了你的英语是只能读,还是真的能用。所以我做了一个专门练这个的 App。

每天一道题,它长什么样

玩法抄了 Wordle 的作业:每天全球所有人拿到同一道题——一对容易混淆的英语近义词,各配一张黑白线稿插图。你用英语写出这两个词在含义、语气、用法上的区别,一两句话就行,然后 AI 给你批改。

批改是这个 App 的核心。不是给个分就完事,而是从五个维度(准确性、覆盖度、语言、清晰度、洞察)各打一个 0–100 的分,每个维度都附一句针对你这次答案的点评——指出你答到位的地方、漏掉的要点,像一对一外教改作业。五个分数汇总成总分和等级,从 D 到 SSS。SSS 很难拿:不光要总分 95 以上,还要求最低的那个维度也不低于 90,纯靠某一项拉平均分是蒙不到的。

批改完揭晓标准答案,对照着学。之后就是熟悉的套路:每日连击打卡、日历一格格点亮,还能导出一张成绩卡分享——卡上只有词对、插图、等级和雷达图,刻意不含答案,朋友看到照样能去玩。

题库目前 200 道,四百张插图全是 AI 生成的黑白线稿,风格统一得像一个插画师画的。

技术上几个有意思的决定

作为技术博客,还是得聊聊后端。整个服务端就是一个 Cloudflare Worker,配 D1(题库和用户数据)和 R2(插图),没有一台服务器要运维,账单基本可以忽略——独立开发选这套栈真的省心。

我之前的服务,都是部署在自己的home lab上的,这也是一次全套采用cloudflare的技术方案,发觉这个赛博菩萨果然名不虚传,免费额度够用不说,wrangler的开发体验还非常棒!

几个细节自认为做得还算讲究:

1. 评分标准(rubric)永远不下发到客户端。客户端只上传题目 ID 和你写的答案,评分要点由服务端按 ID 查表后喂给模型。否则抓个包就能看到得分点,这游戏就没法玩了。

2. 总分和等级不信任模型自己报的数。LLM 的算术是出了名的不可靠,让它算五个维度的加权和,隔三差五给你算错一个。所以模型只负责逐维度打分和写点评,加总、定级在代码里重算。

3. 匿名用户不用注册就能玩,每天免费批改一次。防滥用靠的不是强制登录,而是 Apple 的 App Attest——让苹果证明请求来自真机上的正版 App,机器人和脚本过不来。这套东西的原理和服务端验证的坑,我之前单独写过一篇《Apple App Attest 简介》,感兴趣可以看看。

4. 批改额度是”先预扣、失败退还”。最早的实现是先只读检查额度、批改成功了再扣,看起来很稳妥——但批改一次要跑上一两分钟,这个窗口里并发发请求,每个请求检查时都显示”还有额度”,就都放行了,白烧 token。改成预扣制之后,靠 D1 单写者的串行化保证并发下最多放行额度上限那么多个请求,批改失败再把额度退回去。

免费与收费

说说钱的事,明码标价:每天的题目永远免费,匿名一天能批改 1 次,登录后 2 次。Pro 订阅(月付或年付)解锁 200 道历史题库、研读模式(先看标准答案再作答)和每天 15 次批改。订阅收入拿去付 AI 推理的账单——每一次批改都是真金白银的 API 调用,所以免费额度给得抠门,请理解哈哈。

来玩

App Store 搜「DailyDiff」或「每日一辨」,或者直接点这个链接。中英文界面都有,今天的题不用注册就能做。

如果你试了之后觉得 AI 批改哪次明显不靠谱,或者有任何建议,欢迎邮件 support@dailydiff.vip 或者直接在下面留言——独立开发,每一条反馈我都会看。

顺便说一句,今天的题你打算拿几分?我至今没拿到过自己 App 里的 SSS。

就此,完毕。

fastlane——App Store Connect CLI(非官方)

我的打鼾监测 App NightSnore 支持 7 种语言(简中、英、日、韩、德、法、阿拉伯语)。这带来一个每次发版都要经历的痛苦环节:在 App Store Connect 后台,把 What’s New(新功能介绍)逐个语言粘贴进去——切语言、粘贴、保存,再切下一个,七遍。要是 Promotional Text(推广文本)也更新了,那就是十四遍。发布过APP的朋友,肯定对此就深有体会了。

这次发 2.3.0 的时候我终于忍不住了:这玩意儿就没有 CLI 能自动化吗?

还真有,而且就是 fastlane。有意思的是,fastlane 我其实早就装了——之前一直拿它给 App Store 截图加设备边框(frameit),我一直以为它就是个截图美化工具,哈哈。这次才发现,截图加框只是它十八般武艺里最不起眼的一样。

fastlane 到底是什么

fastlane 的定位是”把 iOS/Android 发布流程的每个环节都变成可脚本化的命令”。它其实是一整套工具的集合,每个工具管一段:

1. deliver:上传元数据(What’s New、描述、关键词、截图)到 App Store Connect,本文主角。
2. snapshot:跑 UI 测试自动截图,能覆盖每种语言 × 每种设备尺寸。
3. frameit:给截图加设备边框,我之前唯一用过的那个。
4. gym / pilot:打包上传、TestFlight 分发和测试员管理。
5. match / cert / sigh:证书和描述文件的团队共享管理。
6. precheck:上传前扫描文案里的审核高危词。

单人开发、Xcode 自动签名的话,match 这类团队工具基本用不上;但 deliver 对多语言 App 来说是刚需级的效率工具。

deliver:把 ASC 表单变成本地文件

deliver 的思路很直接:ASC 后台的每个表单字段,对应本地一个文本文件,目录按语言组织:

一个很贴心的设计是:目录里有什么文件,它就只上传什么。我只放了 release_notes.txt 和 promotional_text.txt,那么描述、关键词、截图这些都不会被碰。配置文件 Deliverfile 里再把二进制和截图明确跳过:

搭一条 What’s New 流水线

我的 App Store 文案一直维护在仓库的 AppStore/*.md 里(七个语言各一个文件),每次发版往里追加一段”## What’s New (X.Y.Z)”。这个 Markdown 就是唯一数据源,所以流水线只需要一个提取脚本:从 md 里抠出指定版本的段落,写到 deliver 要的 metadata 目录去。再包一个 fastlane lane 串起来:

提取脚本里顺手做了两层校验:七个语言缺任何一个对应版本的段落就直接报错(强制多语言同步,防漏),超过 ASC 的字符上限(What’s New 4000 字符、Promotional Text 170 字符)也直接拦下。以后发版就是一条命令:

从跑命令到 ASC 七个语言全部填好,9 秒。之前手动粘贴至少十分钟,还得祈祷别粘串了语言。

API Key,和一个专门坑你的格式问题

deliver 走的是 App Store Connect API,需要一个 API 密钥:ASC 后台”用户和访问 → 集成 → App Store Connect API”里创建一个团队密钥,角色选 App Manager,会得到一个 .p8 私钥文件(只能下载一次)加 Key ID 和 Issuer ID。

然后我就结结实实踩了个坑。deliver 支持用一个 JSON 文件传密钥,我很自然地写成了指向 .p8 文件路径的形式,结果:

查了才知道:fastlane 的 Fastfile 里有个 app_store_connect_api_key 这个 action,它支持 key_filepath 参数指向 .p8 文件;但 deliver 的 api_key_path 参数指向的 JSON 文件,只认内联的 key 字段——你得把 .p8 的 PEM 内容整个塞进 JSON 字符串里(换行转成 \n)。同一个工具链里两种密钥写法长得几乎一样但互不兼容,这不纯纯挖坑嘛。正确格式长这样:

这个文件含私钥,务必进 .gitignore,待遇跟你的其他 secrets 一样。

踩坑与注意事项

1. deliver 只能写”可编辑状态”的版本(准备提交、被拒等),已在审核中或已上架的版本改不了。所以要在上传 build 之后、点提交审核之前跑它;版本还没建也没关系,deliver 会自动创建。

2. locale 代码不都带地区后缀:日语是 ja 不是 ja-JP,韩语是 ko,但英语是 en-US、德语是 de-DE。写映射表的时候留意。

3. metadata 目录里有什么就传什么,这既是特性也是风险:目录里残留一个过期的 description.txt,就会把线上描述覆盖掉。我的做法是 metadata 目录整个 gitignore,每次由脚本从 md 重新生成,保证它永远是纯派生产物。

用 skill 操作,vibe coding 更顺畅

还有一层我觉得比工具本身更有意思:这整条流水线,从功能开发、写七语言文案,到搭 fastlane、踩坑、修好,都是在 Claude Code 里完成的。搭好之后我顺手让它把整个发版流程沉淀成一个项目 skill——仓库里的一个 .claude/skills/release/SKILL.md 文件,把九个步骤写成清单:bump 版本号 → 编译验证 → 七语言 What’s New / Promotional Text → commit → xcodebuild 归档上传 → 打 tag → fastlane 同步 ASC 表单,连”deliver 的 JSON 只认内联 key”这种坑位说明都写在里面。

下次发版,我只需要说一句 /release 2.4.0,AI 就照着清单把整条链路跑完,唯一剩下的手动操作是去 ASC 点提交审核。skill 跟着仓库走,clone 下来就有,等于把发版的”部落知识”固化成了可执行的文档。对 vibe coding 来说这很关键:写代码交给 AI 大家都会了,但发布环节往往还是人肉在各个后台之间点来点去——把这段也纳入对话式工作流,从开发到上架才算真正闭环。

下一个目标

尝到甜头之后我看了一圈 fastlane 工具箱,对我这种多语言独立开发场景,下一个最值得上的是 snapshot:七个语言的商店截图现在还是手动截的,每次界面大改就是一下午。snapshot 跑 UI 测试自动出全语言截图,再接上我已经会用的 frameit 加框、deliver 上传,理论上截图这条线也能变成一条命令。挖个坑,做完再写。

发布流程自动化这件事,本质上是把”每次发版都要凭记忆和手感重复一遍的操作”变成”写一次、以后白嫖”的脚本。对独立开发者来说,省的那十几分钟是小事,真正值钱的是不再需要担心”这次是不是又漏了哪个语言”——机器不会漏。

就此,完毕。