<?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>Sun, 14 Jun 2026 12:28:15 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>Apple App Attest简介</title>
		<link>https://luy.li/2026/06/14/attest/</link>
					<comments>https://luy.li/2026/06/14/attest/#respond</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>0</slash:comments>
		
		
			</item>
		<item>
		<title>whois 不让用了？聊聊它的接班人 RDAP</title>
		<link>https://luy.li/2026/06/11/rdap/</link>
					<comments>https://luy.li/2026/06/11/rdap/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Thu, 11 Jun 2026 13:45:05 +0000</pubDate>
				<category><![CDATA[备忘]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2503</guid>

					<description><![CDATA[<p>起因是最近我的域名要过期了，在操作续费(顺便还换了个注册商)的过程中，习惯性地敲了个 whois： [cray [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/06/11/rdap/">whois 不让用了？聊聊它的接班人 RDAP</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>起因是最近我的域名要过期了，在操作续费(顺便还换了个注册商)的过程中，习惯性地敲了个 whois：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">$ whois luy.li
Requests of this client are not permitted. Please use https://www.nic.ch/whois/ for queries.</pre><p></p>
<p>被拒了。换 whois 服务器、加参数，都是同一句话：请去网页上查。哈哈，用了快二十年的命令，说不让用就不让用了。</p>
<p>查了一下才知道，.li 域名的注册局是瑞士的 <a href="https://www.switch.ch/">SWITCH</a>（和 .ch 同一家），他们已经把传统的 43 端口 whois 服务关掉了，只留了个带验证码的<a href="https://www.nic.li/whois/">网页查询入口</a>（甚至whois返回的网址都是错的&#8230;）。原因也不难猜：一是 GDPR 之后，欧洲的注册局对注册人信息的批量获取管得很严，而 whois 这个协议天生没有任何访问控制，谁都能无限爬；二是网页入口可以加验证码和限流，挡掉数据挖掘的脚本。</p>
<p>那命令行党就没活路了吗？还真有——RDAP。</p>
<h2>RDAP 是什么</h2>
<p>一句话概括：RDAP 就是基于 HTTPS + JSON 的 whois。</p>
<p>whois 这个协议是 1982 年的产物，比 DNS 还老。它的问题攒了几十年：输出格式没有任何标准，每家注册局返回的文本长得都不一样，想程序化解析就得给每家写一套正则；协议里压根没有字符编码的概念，中文注册人姓名怎么显示全看运气；更要命的是没有认证和权限控制，这也是 GDPR 之后各家注册局纷纷关门的直接原因。</p>
<p>IETF 在 2015 年发布了 RDAP（Registration Data Access Protocol）来接班，核心就是 <a href="https://datatracker.ietf.org/doc/html/rfc7480">RFC 7480</a> 那一组标准。ICANN 从 2019 年起强制要求所有 gTLD 注册局和注册商部署。所以 .com / .org / .net 这些域名，现在都有标准的 RDAP 接口可以查。</p>
<h2>怎么用</h2>
<p>不需要装任何东西，一个 curl 就够了：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">curl -s &quot;https://rdap.nic.ch/domain/luy.li&quot; | python3 -m json.tool</pre><p></p>
<p>返回的是规规矩矩的 JSON。我自己这个域名查出来大概长这样（节选）：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">{
    &quot;objectClassName&quot;: &quot;domain&quot;,
    &quot;ldhName&quot;: &quot;luy.li&quot;,
    &quot;status&quot;: [&quot;active&quot;],
    &quot;entities&quot;: [
        { &quot;roles&quot;: [&quot;registrar&quot;], ... &quot;Dynadot Inc.&quot; ... }
    ],
    &quot;events&quot;: [
        { &quot;eventAction&quot;: &quot;registration&quot;, &quot;eventDate&quot;: &quot;2006-10-01&quot; }
    ],
    &quot;nameservers&quot;: [
        { &quot;ldhName&quot;: &quot;byron.ns.cloudflare.com&quot; },
        { &quot;ldhName&quot;: &quot;itzel.ns.cloudflare.com&quot; }
    ]
}</pre><p></p>
<p>注册商、注册日期、NS、状态，一目了然。所有时间都是 ISO 8601 格式，状态码用的是标准的 EPP 状态（active、client transfer prohibited 这种），不再是各家自己发明的描述。对写脚本的人来说，这比解析 whois 文本舒服太多了。</p>
<p>顺便发现一个副作用：查一个没注册的域名，RDAP 直接返回 HTTP 404。所以拿状态码就能批量探测域名是否被注册，连响应体都不用解析。</p>
<h2>不用记每家的地址：Bootstrap 机制</h2>
<p>用 whois 有个老问题：你得先知道该问哪台服务器。查 .com 要问 Verisign，查 .org 要问 PIR，记不住。</p>
<p>RDAP 把这事标准化了。IANA 维护着一份 <a href="https://data.iana.org/rdap/dns.json">dns.json</a>，里面列着每个顶级域对应的 RDAP 服务地址：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">[[&quot;com&quot;, &quot;net&quot;], [&quot;https://rdap.verisign.com/com/v1/&quot;]],
[[&quot;org&quot;],        [&quot;https://rdap.publicinterestregistry.org/rdap/&quot;]],
[[&quot;ch&quot;, &quot;li&quot;],   [&quot;https://rdap.nic.ch/&quot;]],
...</pre><p></p>
<p>客户端缓存这份文件，就能自动路由到正确的服务器。嫌麻烦的话，直接用 <a href="https://rdap.org/">rdap.org</a> 这个公共服务，它帮你做转发：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">curl -s &quot;https://rdap.org/domain/example.com&quot; | python3 -m json.tool</pre><p></p>
<p>查任何域名都是这一个入口，再也不用记谁家域名归谁管了。</p>
<h2>那些 xn-- 开头的乱码是什么</h2>
<p>翻 dns.json 的时候会看到一堆奇怪的顶级域，比如 xn--kpry57d、xn--fiqs8s。这不是乱码，是国际化域名（IDN）的 <a href="https://zh.wikipedia.org/wiki/%E5%9B%BD%E9%99%85%E5%8C%96%E5%9F%9F%E5%90%8D%E7%BC%96%E7%A0%81" rel="noopener" target="_blank">Punycode</a> 编码。</p>
<p>DNS 天生只认 ASCII，但域名总不能只让用拉丁字母。于是有了 Punycode：把 Unicode 字符串编码成纯 ASCII，再加个 xn-- 前缀。解码出来其实都是各国文字：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">xn--fiqs8s   &rarr;  .中国
xn--j6w193g  &rarr;  .香港
xn--kpry57d  &rarr;  .台灣
xn--q9jyb4c  &rarr;  .みんな（日文&quot;大家&quot;）
xn--80adxhks &rarr;  .москва（莫斯科）</pre><p></p>
<p>想自己玩的话，Python 一行就能互转：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">$ python3 -c &quot;print('fiqs8s'.encode().decode('punycode'))&quot;
中国
$ python3 -c &quot;print('中国'.encode('punycode').decode())&quot;
fiqs8s</pre><p></p>
<p>RDAP 对这个的支持也很到位：响应里同时给 ldhName（ASCII 形式）和 unicodeName（Unicode 形式）两个字段，客户端想显示哪个自己挑。而 whois 协议连编码都没定义，IDN 的处理完全看各家心情。</p>
<h2>隐私这块儿</h2>
<p>GDPR 干掉了 whois 里的注册人信息，RDAP 则把&#8221;隐藏&#8221;这件事做成了标准。响应里有个 redacted 数组，明确列出哪些字段被隐藏了、用的什么方式（整个移除、替换成占位值、还是置空），机器可读。需要完整数据的执法机构或商标方，可以走认证通道申请。这套分级访问的设计，whois 是完全做不到的——它只能简单粗暴地把字段换成 REDACTED FOR PRIVACY，别的什么都表达不了。</p>
<h2>踩到的坑</h2>
<p>也不是处处顺利。RDAP 标准归标准，各家实现的完整度差很多：</p>
<p>1. ccTLD 不强制部署。gTLD 是 ICANN 管的，必须上 RDAP；但国家域名各自为政，有的部署了，有的没有，有的部署了但缺斤短两。</p>
<p>2. 比如 .li / .ch 的 RDAP 就不返回到期时间。events 里只有 registration，没有 expiration，想知道域名什么时候过期，还是得去注册商后台看。而 .com / .org 的 RDAP 是给全的。</p>
<p>3. 注册局和注册商是两级数据。注册局（Registry）的 RDAP 只有基本信息，更详细的注册人信息要顺着响应里 rel 为 &#8220;related&#8221; 的链接去注册商（Registrar）的 RDAP 再查一次。</p>
<p>4. 部分url有反爬机制，直接用curl会失败，如果失败可以试试正常用浏览器打开。</p>
<h2>最后</h2>
<p>RDAP 其实没什么革命性的东西，本质上就是把四十多年前的纯文本协议用 HTTPS + JSON 重写了一遍。但恰恰是这种&#8221;无聊的现代化&#8221;，把格式混乱、没法国际化、没有隐私控制这几个老大难问题全解决了。另外它不只能查域名，IP 地址和 AS 号也是同一套协议（curl rdap.org/ip/8.8.8.8 试试），五大 RIR 都已经支持了。</p>
<p>以后再想 whois 什么东西，可以先试试 curl rdap.org。毕竟传统 whois 关一家少一家，而 RDAP 才刚刚开始。</p>
<p>就此，完毕。</p><p>The post <a href="https://luy.li/2026/06/11/rdap/">whois 不让用了？聊聊它的接班人 RDAP</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/06/11/rdap/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>滕王阁序</title>
		<link>https://luy.li/2026/06/06/twgx/</link>
					<comments>https://luy.li/2026/06/06/twgx/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sat, 06 Jun 2026 07:14:27 +0000</pubDate>
				<category><![CDATA[流水帐]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2499</guid>

					<description><![CDATA[<p>和之前的awk手册一样，又是一个Claude design的作品。 这次做了《滕王阁序》的逐字解析，还挺有意思 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/06/06/twgx/">滕王阁序</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>和之前的<a href="https://luy.li/2026/06/03/awk_handbook-2/" title="awk中文手册 再版">awk手册</a>一样，又是一个Claude design的作品。</p>
<p>这次做了《滕王阁序》的逐字解析，还挺有意思的，丢个<a href="https://luy.li/data/guwen/TWGX/">链接</a>。</p>
<p>如果有错误之处，可以去<a href="https://github.com/bones7456/guwen">这里</a>提issue，也不排除以后做其他经典古文的解析。</p><p>The post <a href="https://luy.li/2026/06/06/twgx/">滕王阁序</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/06/06/twgx/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>从接手到日用：我把 Notchy 改成了什么样</title>
		<link>https://luy.li/2026/06/04/notchy_now/</link>
					<comments>https://luy.li/2026/06/04/notchy_now/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Thu, 04 Jun 2026 04:45:43 +0000</pubDate>
				<category><![CDATA[精华]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2486</guid>

					<description><![CDATA[<p>还记得上次那篇吗？当时我接手 Notchy 的时候，基本就是原作者 Adam Lyttle 的初始版本——点子 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/06/04/notchy_now/">从接手到日用：我把 Notchy 改成了什么样</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>还记得<a href="/2026/05/15/notchy/">上次那篇</a>吗？当时我接手 Notchy 的时候，基本就是原作者 Adam Lyttle 的初始版本——点子非常好，但功能比较基础，bug 也不少。我本来只是想&#8221;修修 bug，打个包&#8221;就完事了。</p>
<p>结果一改就停不下来了。</p>
<p>55 个 commit、4600 多行 Swift 之后（当然大部分都是 vibing 的），Notchy 已经从一个&#8221;能用&#8221;的 demo 变成了我日常干活的主力终端。是的，之前我还是混着状态，现在 iTerm2 已经从 Dock 上消失了。</p>
<p>这篇就来聊聊，到底改了些啥，才让我有底气做出这个切换。<br />
<video src="/data/notchy.mp4" autoplay loop muted playsinline width="600"></video></p>
<h2>Terminal UX：从&#8221;能打字&#8221;到&#8221;能干活&#8221;</h2>
<p>原版的终端体验非常朴素——打开一个黑框，能输入命令，仅此而已。要把它当日用终端，差的东西太多了。</p>
<p><strong>动画和视觉</strong>：面板从菜单栏后面滑出来（slide-down），背景是 <code>NSVisualEffectView</code> 的毛玻璃效果。看起来比较像一个系统原生组件，而不是一个第三方窗口硬贴在那里。</p>
<p><strong>快捷键</strong>：这是最影响手感的部分。</p>
<ul>
<li>全局热键 <code>Ctrl+&#96;</code> 呼出/收起面板，任何应用中随时可用</li>
<li><code>Cmd+1..9</code> 切 tab，<code>Cmd+W</code> 关 tab，<code>Ctrl+Tab</code> 和 <code>Ctrl+Shift+Tab</code> 循环切换</li>
<li><code>Cmd++</code> / <code>Cmd+-</code> 缩放字体（全局生效，持久化），<code>Cmd+0</code> 重置</li>
<li><code>Shift+Enter</code> 发送换行而不是提交（通过 kitty CSI u 协议实现），这对 Claude Code 的多行输入至关重要</li>
<li><code>Cmd+Backspace</code> 清行（发 Ctrl-U）</li>
<li>Copy-on-selection，选中即复制，iTerm2 用户的肌肉记忆</li>
</ul>
<p><strong>滚动</strong>：这块踩了不少坑。原版在 TUI 应用（比如 Claude Code 自己的界面）里滚动完全不工作。修了 alternate screen buffer 的滚轮转发，修了自动跟随输出的逻辑（在底部时跟随新输出，在回看历史时保持位置不动），还修了退出 vim/less 之后视口跳到顶部的 bug——这个 bug 的原因是 alt buffer 的 <code>yDisp</code> 始终是 0，退出时被误判为&#8221;用户在回看滚动历史&#8221;。Scrollback buffer 大小也做成了可配置的（默认 1000 行，最大 50000）。</p>
<p><strong>字体</strong>：支持 Nerd Font，Powerline 图标正常显示。</p>
<h2>从 Claude 专属到多 Agent 支持</h2>
<p>原版 Notchy 是纯粹为 Claude Code 设计的——检测到 <code>CLAUDE.md</code> 就自动启动 <code>claude</code>，写死的，没有别的选项。</p>
<p>但现实是，越来越多人在用不同的 AI coding agent。OpenAI 的 Codex 出来之后，我公司也给我们同时配备了Claude 和 Codex，我会在不同项目中用不同的agent，Notchy应该能做到自动判断：</p>
<ul>
<li>项目里有 <code>CLAUDE.md</code> → 启动 <code>claude</code></li>
<li>项目里有 <code>AGENTS.md</code> → 启动 <code>codex</code></li>
<li>两个都有 → 看 Settings 里的 Preferred Agent 设置来决定</li>
<li>两个都没有 → 不启动，给你一个普通 shell</li>
</ul>
<p>终端状态检测也做了相应适配。原版只认 Claude 的输出模式（大写的 <code>Esc to interrupt</code>、<code>Esc to cancel</code> 等），Codex 的输出格式不一样——小写的 <code>esc to cancel</code>、<code>you approved … to run …</code>、<code>Conversation interrupted</code>。现在都能正确识别，notch 上的状态指示对两个 agent 都能工作。</p>
<p>这个改动的价值在于：Notchy 不再是一个&#8221;Claude Code 的前端&#8221;，而是一个通用的 AI coding agent 终端。以后再出新的 agent，加个 case 就行。</p>
<h2>Tab 管理：三种 Tab，各司其职</h2>
<p>原版只有 Xcode 自动检测的 tab。我加了一套完整的 tab 类型系统：</p>
<ul>
<li><strong>Xcode tab</strong>（青色边框）：自动创建，跟 Xcode 项目生命周期绑定</li>
<li><strong>Pinned tab</strong>（橙色边框）：手动固定的 tab，跨重启持久化。固定时会通过 <code>proc_pidinfo</code> 快照当前 shell 的 CWD，重启后自动 <code>cd</code> 回去并重新检测 AI agent，适用于非 Xcode 的项目。</li>
<li><strong>Normal tab</strong>（无边框）：<code>+</code> 按钮创建的临时 tab，关掉 app 就没了</li>
</ul>
<p>另外加了 <strong>Shadow Tab</strong>——右键一个 Xcode 或 Pinned tab，选 Shadow Tab，会在旁边开一个 plain shell，<code>cd</code> 到同一个目录但不启动 Claude/Codex。跑 <code>git status</code>、<code>npm run build</code> 这种临时命令特别方便，不用打断正在工作的 agent。名字后面会加个 <code>$</code> 后缀以示区分。</p>
<p>关 Pinned 和 Xcode tab 之前会弹确认框，防止手滑。这些 tab 带着恢复状态，误关了成本很高。</p>
<h2>IME 输入法支持</h2>
<p>SwiftTerm 的 <code>NSTextInputClient</code> 实现有问题，输入法的 marked text（预编辑文本）直接被吞掉了。打拼音的时候只能看到候选窗，看不到自己输入了什么。</p>
<p>第一版我做了一个 HUD 风格的浮动面板，显示在光标上方。后来改成了 inline 渲染，和 macOS Terminal.app 的行为一致——用终端前景色画文字，背景色填充遮住底下的块状光标。视觉上自然多了。</p>
<p>这个功能对中文用户来说是刚需。</p>
<h2>自动更新 (Sparkle)</h2>
<p>手动下载更新太烦了，用户也不会主动去看 GitHub Releases。所以集成了 <a href="https://sparkle-project.org">Sparkle</a>——macOS 上事实标准的自动更新框架。</p>
<p>这块的详细过程我单独写了一篇：<a href="/2026/05/23/sparkle/">给 macOS App 加自动更新：Sparkle 入门</a>。大家可以参考这里。</p>
<h2>CI/CD 发布流水线</h2>
<p>推一个 <code>v*</code> tag 到 GitHub，Actions 自动搞定剩下的事：</p>
<ol>
<li><code>xcodebuild archive</code> 构建并用 Developer ID Application 签名</li>
<li><code>notarytool</code> 提交公证（Apple 审查恶意代码）</li>
<li>打包成 DMG 和 ZIP</li>
<li>用 EdDSA 私钥签名 ZIP，生成 <code>appcast.xml</code></li>
<li>把所有产物挂到 GitHub Release 上</li>
</ol>
<p>如意要长期维护这个应用，这些都是必不可少的基础设施了。</p>
<h2>其他细节</h2>
<ul>
<li><strong>外接显示器支持</strong>：接了外接显示器（比如 Studio Display）的时候，鼠标悬停在外屏顶部中央（摄像头区域）也能唤出面板，和 MacBook notch 的交互保持一致</li>
<li><strong>通话静音</strong>：检测到麦克风在使用（Zoom、FaceTime 等），自动把 Notchy 的提示音静音，不会在开会的时候突然&#8221;叮&#8221;一声</li>
<li><strong>Checkpoint 增强</strong>：加了一个 popover 列出所有 checkpoint，可以浏览、恢复、删除任意一个，不再只能操作最近的那个</li>
<li><strong>Settings 窗口</strong>：从一个简单的菜单 toggle 变成了完整的 Settings 窗口（<code>Cmd+,</code>），分 General / Integrations / About 三个 tab</li>
<li><strong>Notch 动画优化</strong>：改成更平滑的 ease-in-out 曲线，修了 notch 和屏幕顶部之间的缝隙，修了 hover → click 模式切换时 notch 缩小的问题</li>
<li><strong>面板大小持久化</strong>：拖动调整大小后会记住，下次打开恢复。调整时右上角还会显示尺寸指示</li>
</ul>
<h2>为什么能替代 iTerm2</h2>
<p>这个问题的答案很简单：我日常用终端 90% 的场景是跑 AI coding agent。</p>
<p>在这个场景下，Notchy 比 iTerm2 好用。<code>Ctrl+&#96;</code> 一按就出来，不用切窗口；Xcode 项目自动检测，不用手动 <code>cd</code>；agent 自动启动，不用手动输命令；状态一目了然，notch 上的小药丸告诉你 agent 是在干活还是在等你。</p>
<p>剩下 10% 的临时命令？Shadow Tab 搞定。</p>
<p>当然，如果你的主要场景是 SSH 管理十几台服务器、或者需要 tmux 分屏，iTerm2 仍然是更好的选择。但如果你和我一样，日常就是在本地项目里跑 Claude Code 或 Codex——试试 Notchy 吧。</p>
<p>GitHub: <a href="https://github.com/bones7456/notchy">bones7456/notchy</a>，非常欢迎提issue、MR等。。。</p>
<p>安装方式：去 <a href="https://github.com/bones7456/notchy/releases/latest">Releases</a> 下载 DMG 或 zip，拖进 /Applications 就行。因为签名、公证过，所以不会弹 Gatekeeper 警告。</p>
<p>全文完。</p><p>The post <a href="https://luy.li/2026/06/04/notchy_now/">从接手到日用：我把 Notchy 改成了什么样</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/06/04/notchy_now/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>awk中文手册 再版</title>
		<link>https://luy.li/2026/06/03/awk_handbook-2/</link>
					<comments>https://luy.li/2026/06/03/awk_handbook-2/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Wed, 03 Jun 2026 00:51:20 +0000</pubDate>
				<category><![CDATA[流水帐]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2483</guid>

					<description><![CDATA[<p>将近20年前，我自己在学习awk的时候，整理过一个awk中文手册（整理、输出真的是学习的最佳方式之一），当时内 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/06/03/awk_handbook-2/">awk中文手册 再版</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>将近20年前，我自己在学习awk的时候，整理过一个<a href="https://luy.li/2007/10/17/awk_handbook/" title="awk手册 简体中文版">awk中文手册</a>（整理、输出真的是学习的最佳方式之一），当时内容都校对过，但格式真的挺乱的。<br />
而刚刚的5月底，我发现我的Claude design额度要用不完了，于是干脆给这个手册来个再版。样式现代化了不少！<br />
<a href="https://luy.li/data/awk.html" rel="noopener" target="_blank">链接</a>不变，之前的<a href="https://luy.li/data/awk_old.html" rel="noopener" target="_blank">老版本</a>在这。</p><p>The post <a href="https://luy.li/2026/06/03/awk_handbook-2/">awk中文手册 再版</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/06/03/awk_handbook-2/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
