<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>I am LAZY bones?</title>
	<atom:link href="https://luy.li/feed/" rel="self" type="application/rss+xml" />
	<link>https://luy.li</link>
	<description>AN ancient AND boring SITE</description>
	<lastBuildDate>Fri, 17 Jul 2026 15:15:31 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>代码库放 iCloud 文件夹会怎样？</title>
		<link>https://luy.li/2026/07/17/code_shoud_not_in_icloud/</link>
					<comments>https://luy.li/2026/07/17/code_shoud_not_in_icloud/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Fri, 17 Jul 2026 15:15:31 +0000</pubDate>
				<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2522</guid>

					<description><![CDATA[<p>之前我有个习惯，会把代码（整个repo）放到 iCloud 管理的 ~/Documents 下。一直觉得挺方便 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/07/17/code_shoud_not_in_icloud/">代码库放 iCloud 文件夹会怎样？</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>之前我有个习惯，会把代码（整个repo）放到 iCloud 管理的 <code>~/Documents</code> 下。一直觉得挺方便的，因为即使没有 commit+push，我到另一台电脑上也能接着干活，登了同一个iCloud账号的目录会自动同步。</p>
<p>但最近遇到一个特别邪门的问题。一个用 uv 管理的 Python 项目，editable 方式安装，前一个小时还好好的，突然就：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">$ .venv/bin/mycli --help
Traceback (most recent call last):
  File &quot;.venv/bin/mycli&quot;, line 4, in &lt;module&gt;
    from mycli.cli import main
ModuleNotFoundError: No module named 'mycli.cli'</pre><p></p>
<p>邪门在哪呢？同一个 venv、同一个解释器，pytest 跑起来一千六百多个用例全绿。测试说包在，入口脚本说包没了。去 site-packages 里看，editable 安装落下的 .pth 文件好好躺在那，内容就是一行指向源码目录的路径，权限 644，路径也真实存在。文件在、内容对、权限对，但它就是不生效。<br />
<img fetchpriority="high" decoding="async" src="https://luy.li/wp-content/uploads/2026/07/icloud_venv_blog_cover.png" alt="" width="1200" height="630" class="alignnone size-full wp-image-2524" /></p>
<h2>排查：谁把我的 .pth 吞了</h2>
<p>先补一句背景：editable 安装的机制，是往 site-packages 里放一个 .pth 文件，Python 启动时 <a href="https://docs.python.org/zh-cn/3/library/site.html">site 模块</a>会读它，把里面的路径追加进 sys.path。这个文件失效，包就从解释器眼里消失。</p>
<p>做了三个对照实验，结果非常有意思：</p>
<p>1. 自己写一个探针 .pth（内容随便指个存在的目录）放进同一个目录——生效；<br />
2. 把出问题的 .pth 原样 cp 成另一个文件名——生效；<br />
3. 出问题的那个文件本身——死活不生效。</p>
<p>同目录、同内容、同权限，副本能用原件不能用，那唯一的区别只剩下文件的元数据了。ls 加个 -O 把 BSD 文件标志打出来：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">$ ls -lO .venv/lib/python3.12/site-packages/*.pth
-rw-r--r--@ 1 me  staff  hidden  56 Jul 16 17:42 _editable_impl_mycli.pth
-rw-r--r--@ 1 me  staff  hidden  18 Jul 16 17:45 _virtualenv.pth</pre><p></p>
<p>flags 一栏赫然写着 hidden——macOS 的 UF_HIDDEN 文件标志。cp 不会复制 BSD flags，所以副本是干净的，这就解释了实验 2。而 CPython 从 3.11 起，<a href="https://github.com/python/cpython/blob/main/Lib/site.py">site.py</a> 处理 .pth 时加了一条安全加固（防止恶意软件用隐藏 .pth 做隐蔽注入）：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">if ((getattr(st, 'st_flags', 0) &amp; stat.UF_HIDDEN) or
    (getattr(st, 'st_file_attributes', 0) &amp; stat.FILE_ATTRIBUTE_HIDDEN)):
    _trace(f&quot;Skipping hidden .pth file: {fullname!r}&quot;)
    return</pre><p></p>
<p>带隐藏标志的 .pth，静默跳过，一个字的错都不报。两个各自都算合理的行为叠在一起，效果就是：文件在，import 没了。</p>
<p>那谁给文件打的 hidden？chflags nohidden 清掉，几秒钟之后再看，又变回 hidden 了——有个进程在实时跟我对抗。答案到这基本明了：这个仓库在 ~/Documents 底下，而这台 Mac 开着 iCloud 的<a href="https://support.apple.com/zh-cn/HT206985">&#8220;桌面与文稿&#8221;同步</a>。把仓库挪出 Documents、重建 venv，flags 干干净净，问题再没复发。</p>
<h2>iCloud Drive 到底在做什么</h2>
<p>开了&#8221;桌面与文稿&#8221;同步之后，~/Documents 就不再是普通目录，而是由 fileproviderd 守护进程托管的同步空间。它主要干三类事：</p>
<p>1. 监听并上传每一次文件变更——它假设文件是&#8221;偶尔被人编辑的文档&#8221;；<br />
2. 冲突消解——当它认为同一个文件出现两个竞争版本，不丢弃任何一边，而是生成&#8221;xxx 2&#8243;&#8221;xxx 3&#8243;这样的冲突副本；<br />
3. 元数据管理——给托管文件打 xattr 和文件标志（上面那个 hidden 就是它干的），开了&#8221;优化 Mac 存储&#8221;还会把冷文件驱逐成无数据的占位符，读取时才按需下载。</p>
<p>对文档来说这些设计都挺好。但代码库不是文档。</p>
<h2>代码库为什么全中</h2>
<p>第一，写入模式冲突。开发工具链的写入是高频、批量、依赖原子性的：包管理器一次重写上万个小文件，Python 用&#8221;临时文件 + rename&#8221;做原子写，git 靠锁文件和 rename 更新引用。同步进程和这些写入异步竞争，竞争输了就落冲突副本。我这次在 site-packages 里就看到了 &#8220;_editable_impl_mycli 2.pth&#8221;、&#8221;3.pth&#8221;、&#8221;4.pth&#8221; 一窝副本，主 .pth 的内容更是被拼接成了四份路径首尾相连的乱码：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">/Users/me/Documents/dev/mycli/src/Users/me/Documents/dev/mycli/src/Users/me/...</pre><p></p>
<p>第二，元数据篡改。就是前面 UF_HIDDEN 那一段，而且它是主动维护的，你清掉它还会打回来。</p>
<p>第三，驱逐。.git/objects 里的 packfile、venv 里的二进制，都可能被&#8221;优化存储&#8221;驱逐成占位符——在线时表现为构建随机卡顿，离线时表现为仓库&#8221;损坏&#8221;。</p>
<p>第四，git 自己的风险。.git 里的 index、refs 一旦出现冲突副本，仓库状态可能真损坏。git 本身就是分布式同步工具，外面再套一层文件级同步，等于双重同步，语义必然打架。</p>
<h2>这次踩到的坑</h2>
<p>1. chflags nohidden 修不好——几秒内被 fileproviderd 顶回来，别在这条路上浪费时间；<br />
2. 症状自相矛盾极具误导性：pytest 全绿（它靠 rootdir 机制自己把源码目录塞进了路径），入口脚本却挂了，两个工具对&#8221;包是否存在&#8221;给出相反答案，直觉上会先怀疑一万个别的东西，最后才怀疑文件系统在说谎；<br />
3. 损坏是异步、随机发生的：同一个 venv 一小时内坏了两次，中间检查全都正常——因为坏不坏取决于同步进程什么时候追上来；<br />
4. 诊断口诀：文件明明在但 import 不到，先 ls -lO 看 flags 列。</p>
<h2>结论</h2>
<p>iCloud（以及 Dropbox、OneDrive 这些文件级同步盘，机制细节不同但三板斧一样）适合放终态文档：文稿、图片、表格。任何带衍生状态的目录——代码库、venv、node_modules、.git、构建缓存——都不该放进去。代码的跨机同步交给 git，这本来就是它的职责，而且语义正确。仓库放个非托管路径（比如 ~/dev）就好；实在有目录必须留在 iCloud 里又想排除，macOS 没有官方排除项，只有给目录名加 .nosync 后缀这个 hack。</p>
<p>一个按文档假设设计的同步器，遇上一堆违反它全部假设的文件，双方都没有 bug，组合在一起就是灾难。就此，完毕。</p><p>The post <a href="https://luy.li/2026/07/17/code_shoud_not_in_icloud/">代码库放 iCloud 文件夹会怎样？</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/07/17/code_shoud_not_in_icloud/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>每日一辨 DailyDiff</title>
		<link>https://luy.li/2026/07/11/dailydiff/</link>
					<comments>https://luy.li/2026/07/11/dailydiff/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sat, 11 Jul 2026 02:52:24 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[精华]]></category>
		<category><![CDATA[学习打卡]]></category>
		<category><![CDATA[独立开发]]></category>
		<category><![CDATA[英语app推荐]]></category>
		<category><![CDATA[英语单词]]></category>
		<category><![CDATA[英语学习]]></category>
		<category><![CDATA[近义词辨析]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2519</guid>

					<description><![CDATA[<p>最近上架了一个新 App：DailyDiff（每日一辨）。做它的初衷很简单：一是我自己想把英语再往上提一提，二 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/07/11/dailydiff/">每日一辨 DailyDiff</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>最近上架了一个新 App：<a href="https://dailydiff.vip/?src=luyli">DailyDiff（每日一辨）</a>。做它的初衷很简单：一是我自己想把英语再往上提一提，二是儿子也在学英语，我想给他（也给自己）找一个每天花几分钟、细水长流的练法。</p>
<p>市面上背单词的软件很多，但它们解决的基本都是&#8221;认识&#8221;：看见 soldier 知道是战士，看见 warrior 也知道是战士。可这两个词的区别是什么、什么场合该用哪个——这种&#8221;掌握&#8221;层面的功夫，几乎没有软件管。而&#8221;认识一个单词&#8221;和&#8221;掌握一个单词&#8221;之间恰恰隔着一条鸿沟，它决定了你的英语是只能读，还是真的能用。所以我做了一个专门练这个的 App。</p>
<h2>每天一道题，它长什么样</h2>
<p>玩法抄了 Wordle 的作业：每天全球所有人拿到同一道题——一对容易混淆的英语近义词，各配一张黑白线稿插图。你用英语写出这两个词在含义、语气、用法上的区别，一两句话就行，然后 AI 给你批改。</p>
<p>批改是这个 App 的核心。不是给个分就完事，而是从五个维度（准确性、覆盖度、语言、清晰度、洞察）各打一个 0–100 的分，每个维度都附一句针对你这次答案的点评——指出你答到位的地方、漏掉的要点，像一对一外教改作业。五个分数汇总成总分和等级，从 D 到 SSS。SSS 很难拿：不光要总分 95 以上，还要求最低的那个维度也不低于 90，纯靠某一项拉平均分是蒙不到的。</p>
<p>批改完揭晓标准答案，对照着学。之后就是熟悉的套路：每日连击打卡、日历一格格点亮，还能导出一张成绩卡分享——卡上只有词对、插图、等级和雷达图，刻意不含答案，朋友看到照样能去玩。</p>
<p>题库目前 200 道，四百张插图全是 AI 生成的黑白线稿，风格统一得像一个插画师画的。</p>
<h2>技术上几个有意思的决定</h2>
<p>作为技术博客，还是得聊聊后端。整个服务端就是一个 <a href="https://workers.cloudflare.com/">Cloudflare Worker</a>，配 D1（题库和用户数据）和 R2（插图），没有一台服务器要运维，账单基本可以忽略——独立开发选这套栈真的省心。</p>
<p>我之前的服务，都是部署在自己的home lab上的，这也是一次全套采用cloudflare的技术方案，发觉这个赛博菩萨果然名不虚传，免费额度够用不说，wrangler的开发体验还非常棒！</p>
<p>几个细节自认为做得还算讲究：</p>
<p>1. 评分标准（rubric）永远不下发到客户端。客户端只上传题目 ID 和你写的答案，评分要点由服务端按 ID 查表后喂给模型。否则抓个包就能看到得分点，这游戏就没法玩了。</p>
<p>2. 总分和等级不信任模型自己报的数。LLM 的算术是出了名的不可靠，让它算五个维度的加权和，隔三差五给你算错一个。所以模型只负责逐维度打分和写点评，加总、定级在代码里重算。</p>
<p>3. 匿名用户不用注册就能玩，每天免费批改一次。防滥用靠的不是强制登录，而是 Apple 的 App Attest——让苹果证明请求来自真机上的正版 App，机器人和脚本过不来。这套东西的原理和服务端验证的坑，我之前单独写过一篇<a href="https://luy.li/2026/06/14/attest/">《Apple App Attest 简介》</a>，感兴趣可以看看。</p>
<p>4. 批改额度是&#8221;先预扣、失败退还&#8221;。最早的实现是先只读检查额度、批改成功了再扣，看起来很稳妥——但批改一次要跑上一两分钟，这个窗口里并发发请求，每个请求检查时都显示&#8221;还有额度&#8221;，就都放行了，白烧 token。改成预扣制之后，靠 D1 单写者的串行化保证并发下最多放行额度上限那么多个请求，批改失败再把额度退回去。</p>
<h2>免费与收费</h2>
<p>说说钱的事，明码标价：每天的题目永远免费，匿名一天能批改 1 次，登录后 2 次。Pro 订阅（月付或年付）解锁 200 道历史题库、研读模式（先看标准答案再作答）和每天 15 次批改。订阅收入拿去付 AI 推理的账单——每一次批改都是真金白银的 API 调用，所以免费额度给得抠门，请理解哈哈。</p>
<h2>来玩</h2>
<p>App Store 搜「DailyDiff」或「每日一辨」，或者直接点<a href="https://apps.apple.com/app/apple-store/id6780440998?pt=126682695&#038;ct=luyli&#038;mt=8">这个链接</a>。中英文界面都有，今天的题不用注册就能做。</p>
<p>如果你试了之后觉得 AI 批改哪次明显不靠谱，或者有任何建议，欢迎邮件 support@dailydiff.vip 或者直接在下面留言——独立开发，每一条反馈我都会看。</p>
<p>顺便说一句，今天的题你打算拿几分？我至今没拿到过自己 App 里的 SSS。</p>
<p>就此，完毕。</p><p>The post <a href="https://luy.li/2026/07/11/dailydiff/">每日一辨 DailyDiff</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/07/11/dailydiff/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>fastlane——App Store Connect CLI（非官方）</title>
		<link>https://luy.li/2026/07/04/fastlane/</link>
					<comments>https://luy.li/2026/07/04/fastlane/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sat, 04 Jul 2026 01:34:44 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[CLI软件]]></category>
		<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2516</guid>

					<description><![CDATA[<p>我的打鼾监测 App NightSnore 支持 7 种语言（简中、英、日、韩、德、法、阿拉伯语）。这带来一个 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/07/04/fastlane/">fastlane——App Store Connect CLI（非官方）</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>我的打鼾监测 App <a href="https://nightsnore.com">NightSnore</a> 支持 7 种语言（简中、英、日、韩、德、法、阿拉伯语）。这带来一个每次发版都要经历的痛苦环节：在 App Store Connect 后台，把 What&#8217;s New（新功能介绍）逐个语言粘贴进去——切语言、粘贴、保存，再切下一个，七遍。要是 Promotional Text（推广文本）也更新了，那就是十四遍。发布过APP的朋友，肯定对此就深有体会了。</p>
<p>这次发 2.3.0 的时候我终于忍不住了：这玩意儿就没有 CLI 能自动化吗？</p>
<p>还真有，而且就是 fastlane。有意思的是，fastlane 我其实早就装了——之前一直拿它给 App Store 截图加设备边框（frameit），我一直以为它就是个截图美化工具，哈哈。这次才发现，截图加框只是它十八般武艺里最不起眼的一样。</p>
<h2>fastlane 到底是什么</h2>
<p><a href="https://fastlane.tools">fastlane</a> 的定位是&#8221;把 iOS/Android 发布流程的每个环节都变成可脚本化的命令&#8221;。它其实是一整套工具的集合，每个工具管一段：</p>
<p>1. deliver：上传元数据（What&#8217;s New、描述、关键词、截图）到 App Store Connect，本文主角。<br />
2. snapshot：跑 UI 测试自动截图，能覆盖每种语言 × 每种设备尺寸。<br />
3. frameit：给截图加设备边框，我之前唯一用过的那个。<br />
4. gym / pilot：打包上传、TestFlight 分发和测试员管理。<br />
5. match / cert / sigh：证书和描述文件的团队共享管理。<br />
6. precheck：上传前扫描文案里的审核高危词。</p>
<p>单人开发、Xcode 自动签名的话，match 这类团队工具基本用不上；但 deliver 对多语言 App 来说是刚需级的效率工具。</p>
<h2>deliver：把 ASC 表单变成本地文件</h2>
<p><a href="https://docs.fastlane.tools/actions/deliver/">deliver</a> 的思路很直接：ASC 后台的每个表单字段，对应本地一个文本文件，目录按语言组织：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">fastlane/metadata/
├── zh-Hans/
│   ├── release_notes.txt      &larr; What's New
│   └── promotional_text.txt   &larr; 推广文本
├── en-US/
├── ja/
├── ko/
├── de-DE/
├── fr-FR/
└── ar-SA/</pre><p></p>
<p>一个很贴心的设计是：目录里有什么文件，它就只上传什么。我只放了 release_notes.txt 和 promotional_text.txt，那么描述、关键词、截图这些都不会被碰。配置文件 Deliverfile 里再把二进制和截图明确跳过：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">app_identifier &quot;Senob.NightSnore&quot;

skip_binary_upload true
skip_screenshots true
force true                        # 跳过上传前的 HTML 预览确认
run_precheck_before_submit false
submit_for_review false           # 只填表单，提交审核仍手动</pre><p></p>
<h2>搭一条 What&#8217;s New 流水线</h2>
<p>我的 App Store 文案一直维护在仓库的 AppStore/*.md 里（七个语言各一个文件），每次发版往里追加一段&#8221;## What&#8217;s New (X.Y.Z)&#8221;。这个 Markdown 就是唯一数据源，所以流水线只需要一个提取脚本：从 md 里抠出指定版本的段落，写到 deliver 要的 metadata 目录去。再包一个 fastlane lane 串起来：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">lane :whatsnew do |options|
  version = options[:version] || get_version_number(
    xcodeproj: &quot;NightSnore.xcodeproj&quot;,
    target: &quot;NightSnore&quot;
  )

  # 从 AppStore/*.md 生成 fastlane/metadata/&lt;locale&gt;/release_notes.txt
  sh(&quot;python3&quot;, &quot;../utils/whatsnew_sync.py&quot;, version)

  deliver(
    api_key_path: &quot;fastlane/api_key.json&quot;,
    app_version: version
  )
end</pre><p></p>
<p>提取脚本里顺手做了两层校验：七个语言缺任何一个对应版本的段落就直接报错（强制多语言同步，防漏），超过 ASC 的字符上限（What&#8217;s New 4000 字符、Promotional Text 170 字符）也直接拦下。以后发版就是一条命令：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">$ fastlane whatsnew version:2.3.0
[18:02:20]: ▸ 同步版本 2.3.0 的 What's New + Promotional Text &rarr; fastlane/metadata/
[18:02:20]: ▸ zh-Hans  notes  240 字符 / promo  57 字符
[18:02:20]: ▸ en-US    notes  659 字符 / promo 158 字符
...
[18:02:29]: Uploading metadata to App Store Connect for localized version 'ja'
[18:02:29]: Uploading metadata to App Store Connect for localized version 'ar-SA'
[18:02:31]: <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 2.3.0 七语言 What's New 已同步到 ASC
[18:02:31]: fastlane.tools finished successfully <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f389.png" alt="🎉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></pre><p></p>
<p>从跑命令到 ASC 七个语言全部填好，9 秒。之前手动粘贴至少十分钟，还得祈祷别粘串了语言。</p>
<h2>API Key，和一个专门坑你的格式问题</h2>
<p>deliver 走的是 App Store Connect API，需要一个 API 密钥：ASC 后台&#8221;用户和访问 → 集成 → App Store Connect API&#8221;里创建一个团队密钥，角色选 App Manager，会得到一个 .p8 私钥文件（只能下载一次）加 Key ID 和 Issuer ID。</p>
<p>然后我就结结实实踩了个坑。deliver 支持用一个 JSON 文件传密钥，我很自然地写成了指向 .p8 文件路径的形式，结果：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">Spaceship::ConnectAPI::Token.from_json_file':
[!] App Store Connect API key JSON is missing field(s): key (RuntimeError)</pre><p></p>
<p>查了才知道：fastlane 的 Fastfile 里有个 app_store_connect_api_key 这个 action，它支持 key_filepath 参数指向 .p8 文件；但 deliver 的 api_key_path 参数指向的 JSON 文件，只认内联的 key 字段——你得把 .p8 的 PEM 内容整个塞进 JSON 字符串里（换行转成 \n）。同一个工具链里两种密钥写法长得几乎一样但互不兼容，这不纯纯挖坑嘛。正确格式长这样：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">{
  &quot;key_id&quot;: &quot;ABCD123456&quot;,
  &quot;issuer_id&quot;: &quot;12345678-abcd-....&quot;,
  &quot;key&quot;: &quot;-----BEGIN PRIVATE KEY-----\nMIGT...\n-----END PRIVATE KEY-----&quot;,
  &quot;in_house&quot;: false
}</pre><p></p>
<p>这个文件含私钥，务必进 .gitignore，待遇跟你的其他 secrets 一样。</p>
<h2>踩坑与注意事项</h2>
<p>1. deliver 只能写&#8221;可编辑状态&#8221;的版本（准备提交、被拒等），已在审核中或已上架的版本改不了。所以要在上传 build 之后、点提交审核之前跑它；版本还没建也没关系，deliver 会自动创建。</p>
<p>2. locale 代码不都带地区后缀：日语是 ja 不是 ja-JP，韩语是 ko，但英语是 en-US、德语是 de-DE。写映射表的时候留意。</p>
<p>3. metadata 目录里有什么就传什么，这既是特性也是风险：目录里残留一个过期的 description.txt，就会把线上描述覆盖掉。我的做法是 metadata 目录整个 gitignore，每次由脚本从 md 重新生成，保证它永远是纯派生产物。</p>
<h2>用 skill 操作，vibe coding 更顺畅</h2>
<p>还有一层我觉得比工具本身更有意思：这整条流水线，从功能开发、写七语言文案，到搭 fastlane、踩坑、修好，都是在 Claude Code 里完成的。搭好之后我顺手让它把整个发版流程沉淀成一个项目 skill——仓库里的一个 .claude/skills/release/SKILL.md 文件，把九个步骤写成清单：bump 版本号 → 编译验证 → 七语言 What&#8217;s New / Promotional Text → commit → xcodebuild 归档上传 → 打 tag → fastlane 同步 ASC 表单，连&#8221;deliver 的 JSON 只认内联 key&#8221;这种坑位说明都写在里面。</p>
<p>下次发版，我只需要说一句 /release 2.4.0，AI 就照着清单把整条链路跑完，唯一剩下的手动操作是去 ASC 点提交审核。skill 跟着仓库走，clone 下来就有，等于把发版的&#8221;部落知识&#8221;固化成了可执行的文档。对 vibe coding 来说这很关键：写代码交给 AI 大家都会了，但发布环节往往还是人肉在各个后台之间点来点去——把这段也纳入对话式工作流，从开发到上架才算真正闭环。</p>
<h2>下一个目标</h2>
<p>尝到甜头之后我看了一圈 fastlane 工具箱，对我这种多语言独立开发场景，下一个最值得上的是 <a href="https://docs.fastlane.tools/actions/snapshot/">snapshot</a>：七个语言的商店截图现在还是手动截的，每次界面大改就是一下午。snapshot 跑 UI 测试自动出全语言截图，再接上我已经会用的 frameit 加框、deliver 上传，理论上截图这条线也能变成一条命令。挖个坑，做完再写。</p>
<p>发布流程自动化这件事，本质上是把&#8221;每次发版都要凭记忆和手感重复一遍的操作&#8221;变成&#8221;写一次、以后白嫖&#8221;的脚本。对独立开发者来说，省的那十几分钟是小事，真正值钱的是不再需要担心&#8221;这次是不是又漏了哪个语言&#8221;——机器不会漏。</p>
<p>就此，完毕。</p><p>The post <a href="https://luy.li/2026/07/04/fastlane/">fastlane——App Store Connect CLI（非官方）</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/07/04/fastlane/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>聊聊 Google 的 OKF</title>
		<link>https://luy.li/2026/06/28/okf/</link>
					<comments>https://luy.li/2026/06/28/okf/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sun, 28 Jun 2026 02:55:39 +0000</pubDate>
				<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2513</guid>

					<description><![CDATA[<p>但凡自己动手给 LLM 接过点内部数据的人，大概都被同一件事恶心过：上下文到处都是，就是没法用。表结构在元数据 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/06/28/okf/">聊聊 Google 的 OKF</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>但凡自己动手给 LLM 接过点内部数据的人，大概都被同一件事恶心过：上下文到处都是，就是没法用。表结构在元数据目录里，藏在某个有专有 API 的系统后面；指标的口径写在某个 Wiki 页面，链接还时不时失效；运维手册在网盘，注释在代码里，剩下一半干脆只在某个老同事脑子里。你想做个能回答&#8221;上周活跃用户怎么算的&#8221;的 agent，光是把这些上下文拼起来就得脱层皮，而且换个数据源、换个团队，这套拼装逻辑还得从头再来一遍。</p>
<p>Google 最近发了个东西想治这个病，叫 <a href="https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing" rel="noopener" target="_blank">OKF</a>（Open Knowledge Format，开放知识格式），目前是 <a href="https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf">v0.1</a>。我看完第一反应是：这不就是给&#8221;内部知识&#8221;定了个通用文件格式嘛——类比一下，有点像知识界的 Markdown，或者说，想成为 Git 之于代码那样的存在。</p>
<h2>OKF 到底是个啥</h2>
<p>说穿了特别朴素：一堆带 YAML frontmatter 的 Markdown 文件，按目录组织起来，就这么点东西。没有复杂的压缩、没有运行时、不强制你装任何 SDK。一个 OKF 的 bundle（知识包）就是一个目录，每个概念对应一个 .md 文件：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">sales/
├── datasets/
│   └── orders_db.md
├── tables/
│   ├── orders.md
│   └── customers.md
└── metrics/
    └── weekly_active_users.md</pre><p></p>
<p>每个文件头上用 YAML 写结构化字段，正文用 Markdown 写说明、schema 之类。比如一张订单表，用Google自己的例子，大概长这样：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">---
type: BigQuery Table
title: Orders
description: One row per completed customer order.
resource: https://console.cloud.google.com/bigquery?p=acme&amp;d=sales&amp;t=orders
tags: [sales, revenue]
timestamp: 2026-05-28T14:30:00Z
---

# Schema
| Column | Type | Description |
|--------|------|-------------|
| `order_id` | STRING | Globally unique order identifier. |
| `customer_id` | STRING | FK to [customers](/tables/customers.md). |
......</pre><p></p>
<p>注意最后那行——<code>customer_id</code> 直接用 Markdown 链接指向了 customers 表。文档之间这么互相一引用，整个知识包其实就织成了一张图。人能读，agent 也能顺着链接爬。</p>
<h2>很克制</h2>
<p>我个人比较欣赏它的一点是&#8221;少管闲事&#8221;。整个规范里强制要求的字段只有一个：<code>type</code>。其余全交给生产者自己定。这意味着它不会因为你的场景特殊就用不了。</p>
<p>围绕这点，它立了三条原则，我觉得是整个设计的魂：</p>
<p>1. 极简约束——只认 <code>type</code>，绝不替你过度规定。<br />
2. 生产者和消费者解耦——格式本身就是契约。谁来生成这些文件、谁来消费它们，两边可以各自独立替换，互不绑死。<br />
3. 是格式，不是平台——没有厂商锁定，不依赖某朵云，不要专有 SDK。这条是它跟那些&#8221;元数据目录产品&#8221;最本质的区别。</p>
<p>这三条凑一块，好处就很实在了：人可读（任意编辑器能打开，GitHub 能直接渲染），可移植（打成 tarball、塞进 git、挂任意文件系统都行），可互操作（A 团队产的知识包，B 团队的 agent 不用翻译直接就能吃）。而且因为就是一堆文本文件，它可以躺在版本控制里，跟代码放一起，agent 能读也能改，团队像 review 代码一样 review 知识。</p>
<h2>Google 顺手给了几个轮子</h2>
<p>光发个规范是空的，所以这次还附带了配套，降低上手门槛：一个参考用的&#8221;富化 agent&#8221;，能从 BigQuery 数据集自动生成 OKF 文档；一个纯静态的 HTML 可视化器，不需要任何后端，打开就能看那张知识图谱长啥样；外加三个示例 bundle——GA4、Stack Overflow、Bitcoin 数据集，拿来照着学格式正好。</p>
<p>这里头我觉得最值得单拎出来说的是那个富化 agent，因为它远不只是&#8221;导出元数据&#8221;那么简单。</p>
<h3>富化 agent：让 LLM 去读文档</h3>
<p>名字里&#8221;富化&#8221;（enrichment）这俩字是关键。光靠 BigQuery 的元数据，你能拿到的只是表名、列名、类型这些骨架；可 <code>customer_id</code> 到底什么含义、<code>orders</code> 和 <code>customers</code> 怎么关联、某个枚举值代表哪种业务状态——这些&#8221;肉&#8221;通常只长在人写的文档里。富化 agent 干的就是把骨架和肉缝起来。它的做法是两段式（two-pass）：</p>
<p>第一段，BigQuery Pass。扫源数据集，光靠 BigQuery 自身元数据，给每个发现的概念（表、字段、数据集）生成一份初始 OKF 文档。这步是机械的、确定的，产出骨架。</p>
<p>第二段，Web Pass，这段才是精华——它把一个 LLM（Gemini，走 AI Studio 或 Vertex AI）当成一个会自主决策的爬虫：你丢给它几个种子 URL（<code>--web-seed</code>，一般是官方文档地址），agent 用一个 <code>fetch_url</code> 工具去抓页面，然后 LLM 自己判断每页&#8221;看起来像不像权威文档&#8221;，再决定怎么处理——是拿来丰富某个已有的概念文档，还是单独建一份参考文档，还是这页没用直接跳过。它不是傻爬，是带着&#8221;我手里有哪些概念、这页能补哪个&#8221;的目标在选择性吸收。</p>
<p>既然是放 LLM 出去爬网，缰绳得备好：<code>--web-max-pages</code> 限制最多抓多少页，防止跑飞；<code>--web-allowed-host</code> 上域名白名单，只许它在你信任的站点里转悠。</p>
<p>最后吐出来的，就是一组分层组织好、带 frontmatter、能直接进版本控制的自包含 bundle。说白了，这个 agent 是 Google 给的一份&#8221;如何把现有系统改造成 OKF 生产者&#8221;的样板：结构化元数据打底，再让 LLM 读人类文档来富化——你完全可以照着这套路，给自家的数据系统写个类似的生产者。</p>
<h3>可视化器：一个 HTML 文件就是查看器</h3>
<p>如果说富化 agent 是&#8221;生产端&#8221;的样板，那这个可视化器就是&#8221;消费端&#8221;的样板，而且它把 OKF &#8220;是格式不是平台&#8221;那套哲学贯彻得相当彻底——连查看器都拒绝引入后端，就一个静态 HTML 文件。生成命令是富化 agent 的一个子命令：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">.venv/bin/python -m reference_agent visualize --bundle ./bundles/stackoverflow
# 写出 bundles/stackoverflow/viz.html</pre><p></p>
<p>它在生成时把整个 bundle 当成一段 JSON 直接嵌进 HTML 里，所以产物是个自给自足的单文件，可以分享、可以丢任意静态服务器、也可以跟 bundle 一起 commit。Google 就是这么干的——仓库里直接躺着三个生成好的 viz.html（GA4 50KB、Stack Overflow 122KB、Bitcoin 28KB），双击就能在浏览器里打开，看的人那边什么都不用装。</p>
<p>打开后是个左图右文的单页：左边一张力导向图，每个概念是个节点，颜色按 <code>type</code> 区分（table / dataset / reference…），节点之间的有向边就是文档正文里那些 Markdown 互链——前面说的那张&#8221;知识图谱&#8221;，在这儿被画了出来；点某个节点，右边详情面板就用 marked 把那份 .md 的 frontmatter 和正文实时渲染出来，正文里的内部链接还被改写成&#8221;在查看器里跳转&#8221;，点了不会把你甩走，而是切到对应节点。顶上一排是搜索框（匹配标题、ID、tags）、type 过滤、还有好几种图布局可切（cose、concentric、breadth-first、circle…）。图用 <a href="https://js.cytoscape.org/">Cytoscape.js</a> 渲染，全程在浏览器里跑，数据不出本地。</p>
<p>光说没用，我把 Stack Overflow 那个样例的 viz.html 直接搬到了自己博客下，你点开就能玩：<a href="/data/stackoverflow_viz.html">stackoverflow_viz.html</a>。其实我这个动作本身就是 OKF &#8220;好传播&#8221;的一个现成注脚——一个别人生成的知识查看器，我啥都不改、原样拷过来挂上就能用，这要是搁个需要起后端、连数据库的&#8221;平台&#8221;，可没这么轻巧。</p>
<h2>几个我会留意的点</h2>
<p>新东西总归要泼点冷水，几个我看完留了心眼的地方：</p>
<p>1. 目前才 v0.1。规范号摆在这，意思是接口和约定都还可能动，真要拿去做生产，得做好跟着升级的准备。<br />
2. &#8220;只强制 type&#8221;是把双刃剑。约束少确实灵活，但也意味着不同团队产出的字段可能五花八门，跨组织互操作时，光有格式统一、字段语义不统一，照样得对齐。这部分活儿规范帮不了你。<br />
3. 它解决的是&#8221;知识怎么存、怎么传&#8221;，不解决&#8221;知识怎么保证是对的、是新的&#8221;。frontmatter 里那个 <code>timestamp</code> 写得再漂亮，背后没人维护，照样会过期。说到底它是个格式，不是个能自动保鲜的系统。</p>
<h2>说点本质的</h2>
<p>抛开这些细节，我觉得 OKF 真正想干的事很清楚：在这个人人都在拿内部数据喂 agent 的当口，给&#8221;知识&#8221;这件东西定一个最朴素、最不挑食的通用载体。用最普通的文件约定，换跨工具、跨组织、跨时间的可移植性——这套思路，过去在代码上是 Git 干成的，在文档上是 Markdown 干成的。OKF 想在&#8221;知识&#8221;这一层复刻同样的故事。</p>
<p>能不能成，要看有多少人愿意给自己的系统写&#8221;生产者&#8221;、给自己的 agent 写&#8221;消费者&#8221;，把生态滚起来。但方向我是认的：与其每家都把知识锁在自己那套不兼容的专有系统里，不如先在格式上达成一致。毕竟，朴素的东西才传得开。</p>
<p>就此，完毕。</p><p>The post <a href="https://luy.li/2026/06/28/okf/">聊聊 Google 的 OKF</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/06/28/okf/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Apple App Attest简介</title>
		<link>https://luy.li/2026/06/14/attest/</link>
					<comments>https://luy.li/2026/06/14/attest/#comments</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sun, 14 Jun 2026 12:28:15 +0000</pubDate>
				<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2509</guid>

					<description><![CDATA[<p>在这个AI时代，越来越多的应用（APP）是和AI相关的，其中有不少，对用户的请求需要调用LLM来处理，也就是要 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/06/14/attest/">Apple App Attest简介</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>在这个AI时代，越来越多的应用（APP）是和AI相关的，其中有不少，对用户的请求需要调用LLM来处理，也就是要消耗token。如果这个应用，是对免费用户甚至未注册用户有一定的体验使用量的话，就要考虑怎么防止token被刷爆的问题了。恰巧我就在做一个这样的应用。</p>
<p>此时，一个自然而然的问题是：要怎么证明&#8221;这是正版 App 发来的&#8221;？</p>
<p>Apple App Attest 就是来解决这个问题的。（PS：大家如果有安卓的、PWA的解决方案，可以留言）</p>
<p>我想实现的是&#8221;匿名用户每天送一次 AI 评分&#8221;，不登录就能用，体验好、转化高。可这接口裸奔在公网上，谁拿 curl 写个循环都能把额度刷爆。加验证码太伤体验，强制登录又把&#8221;尝鲜&#8221;这个卖点废了。</p>
<p>我想要的其实是一句话：能不能让服务器确信&#8221;这条请求确实是从我那个正版 App、在一台真机上发出来的&#8221;？Apple 的 <a href="https://developer.apple.com/documentation/devicecheck">App Attest</a> 就是干这个的。这篇把它的原理、整体流程，以及服务端到底该怎么验，讲清楚；我自己趟过的几个坑放在最后当佐料。</p>
<h2>App Attest 解决的是什么问题</h2>
<p>传统的&#8221;防刷&#8221;思路是给请求带个密钥或 token，可只要密钥在客户端，逆向、抓包、改包就能仿造，挡不住有心人。App Attest 换了个思路：它借助 Secure Enclave（设备上独立的安全芯片），由<strong>苹果</strong>来给你的 App 背书，证明两件事——这是从 App Store 渠道的正版 App 发出的，且跑在一台真实的苹果设备上。</p>
<p>这个背书是密码学保证的：签名私钥生成在 Secure Enclave 里、永远导不出来，连越狱也偷不走。所以它特别适合&#8221;匿名但要防滥用&#8221;的场景：免费额度、防注册机刷号、防接口被脚本薅。它不是用户身份认证（那是 Sign in with Apple 的活），它认的是&#8221;设备 + App&#8221;这个组合可信。</p>
<p>代价是它只在真机、正版渠道下成立——模拟器用不了，这点后面会再提。</p>
<h2>两段式：attestation 和 assertion</h2>
<p>理解 App Attest，关键是分清它的两段，这俩验证逻辑完全不同。</p>
<p>第一段 attestation，一次性的。App 首次要证明自己时，在 Secure Enclave 里生成一对密钥，请苹果给这把公钥签发一张证书；证书连同一坨 authenticator data 打包成 attestation 对象，发给你的服务器。服务器验完，把这把公钥存进库，跟这台设备绑定。这一步只做一次。</p>
<p>第二段 assertion，每次请求都做。App 用第一段那把私钥，对&#8221;本次请求的内容&#8221;签个名，随请求发出。服务器用之前存下的公钥验签——对得上，就说明这条请求确实来自那台被证明过的设备，且内容没被篡改。</p>
<p>客户端的代码很薄，DCAppAttestService 几个方法调一调就行。真正有讲究的是服务端这两段验证，下面分开说。</p>
<h2>服务端怎么验 attestation</h2>
<p>attestation 对象 CBOR 解开后，核心是一条证书链 x5c 和一段 authData。要验的东西不少，挑要点说：</p>
<p>证书链。x5c 里只有两张证书：给设备公钥签的叶子证书，和一张中间证书。你要做的是把它验到苹果的 App Attest 根证书。这里有个反直觉的点——<strong>根证书不在链里</strong>。别去比对&#8221;链里最后一张是不是根&#8221;，那是中间证书。正确做法是把苹果根证书内嵌进代码，用它的公钥验中间证书，再用中间证书验叶子。根证书是信任锚，得自己持有，不能从对方给的链里取。苹果根证书在 <a href="https://www.apple.com/certificateauthority/">certificate authority 页面</a>下载，建议顺手核对哈希：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">curl -fsS https://www.apple.com/certificateauthority/Apple_App_Attestation_Root_CA.pem \
  | openssl x509 -outform DER | shasum -a 256
1cb9823ba28ba6ad2d33a006941de2ae4f513ef1d4e831b9f7e0fa7b6242c932  -</pre><p></p>
<p>nonce。光证明&#8221;证书合法&#8221;挡不住重放——截一个合法 attestation 反复发也行。苹果的办法是：服务器先发一个随机 challenge，苹果会在叶子证书的扩展里（OID 1.2.840.113635.100.8.2）塞进 nonce = SHA256(authData ‖ SHA256(challenge))。服务器照样算一遍比对，对上才说明这份证明是冲着你这次的 challenge 来的，authData 也没被动过。</p>
<p>剩下几项是常规校验：rpIdHash 要等于 SHA256(appId)（appId = Team ID 加 Bundle ID）；新鲜的 attestation 计数器必须是 0；aaguid 标明这是 App Attest（正式环境是 appattest，Xcode 调试走的开发环境是 appattestdevelop，两者都要放行）；最后把 authData 里的公钥 SHA256 一下，应当等于凭证 ID。全过了，把这把公钥存库。</p>
<p>证书链验签自己撸 ASN.1 容易出隐蔽 bug，我直接用了 <a href="https://github.com/PeculiarVentures/x509">@peculiar/x509</a>，它在 Cloudflare Worker 的 WebCrypto 环境里能跑。</p>
<h2>服务端怎么验 assertion</h2>
<p>每次请求的验证简单些：CBOR 解开 assertion 拿到签名和 authenticatorData，先验 rpIdHash、再查计数器是否比库里存的大（防重放，每签一次苹果会自增），最后用存下的公钥验签。</p>
<p>验签的消息构造是这段里唯一的&#8221;暗礁&#8221;。苹果文档说签名覆盖的是 authenticatorData ‖ clientDataHash，但你要是把这俩直接拼起来交给 ECDSA-SHA256 去验，会失败。真相是：苹果用 ES256 签的是 nonce = SHA256(authData ‖ clientDataHash)，而 ES256 自己还会再 hash 一层，所以最终参与 ECDSA 的摘要是 SHA256(nonce)。WebCrypto 的 ECDSA 必定做一次 hash、跳不过，因此正确写法是先把 nonce 算出来，再把 <strong>nonce 当消息</strong>传进去，让它在上面再 hash 一次：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">const base  = concat(authData, clientDataHash);
const nonce = new Uint8Array(await crypto.subtle.digest(&quot;SHA-256&quot;, base));
const ok = await crypto.subtle.verify(
  { name: &quot;ECDSA&quot;, hash: &quot;SHA-256&quot; }, key, sigP1363, nonce
);</pre><p></p>
<p>这层&#8221;看不见的 hash&#8221;我是把原始字节 dump 出来在本地穷举才定位到的——推理走不动时，让事实说话往往更快。</p>
<h2>实现时几个容易绊倒的点</h2>
<p>主线讲完了，把我真机联调时踩到的坑列一下，纯属佐料，但能省你几个小时：</p>
<p>1. Team ID 不一定是你以为的那个。报 &#8220;RP ID hash mismatch&#8221; 时我很懵，appId 明明拼对了。后来去构建产物里一看，签名证书的 team 和描述文件的 team 是两个，App Attest 取的是 application-identifier 里那个：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">codesign -dvv MyApp.app
Authority=Apple Development: 我的名字 (TEAMBBBBBB)
TeamIdentifier=TEAMAAAAAA          # appId 用的是这个</pre><p></p>
<p>2. COSE 公钥是整数键。authData 里那把公钥是 COSE 格式，x、y 的键是 -2、-3 这种整数。<a href="https://github.com/rvagg/cborg">cborg</a> 默认解对象会直接抛 &#8220;non-string keys not supported&#8221;，得开 useMaps 解成 Map 再 get：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">const coseKey = cborDecode(authData.slice(coseOffset), { useMaps: true });
const x = coseKey.get(-2);
const y = coseKey.get(-3);</pre><p></p>
<p>3. 模拟器测不了。App Attest 在模拟器上直接不支持，匿名链路只能上真机。开发期可以在服务端留个开关跳过验证方便联调，但上线前务必删掉。</p>
<h2>值不值得用</h2>
<p>如果你有&#8221;匿名 / 低门槛、但又怕被脚本滥用&#8221;的接口，App Attest 是目前苹果生态里最硬的一道闸：信任根在苹果、私钥锁在 Secure Enclave，比任何塞在客户端的密钥都难仿造。代价是只覆盖真机正版、客户端服务端都得改、还得忍受一段真机调试的来回。</p>
<p>它的坑也基本都不在文档主线上，而在那些&#8221;想当然&#8221;的接缝处——根证书的位置、Team ID 的来源、COSE 的键类型、ES256 那层默认的 hash。单看每个都不难，叠在一起就够耗你一天。提前知道它们长什么样，就能少趟很多。</p>
<p>全文完。</p><p>The post <a href="https://luy.li/2026/06/14/attest/">Apple App Attest简介</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/06/14/attest/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
	</channel>
</rss>
