<?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>Sat, 23 May 2026 10:59:32 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>给 macOS App 加自动更新：Sparkle 入门</title>
		<link>https://luy.li/2026/05/23/sparkle/</link>
					<comments>https://luy.li/2026/05/23/sparkle/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sat, 23 May 2026 10:59:32 +0000</pubDate>
				<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2474</guid>

					<description><![CDATA[<p>最近给 Notchy 加了自动更新。原先的&#8221;检查更新&#8221;功能很糙：调一下 GitHub  [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/05/23/sparkle/">给 macOS App 加自动更新：Sparkle 入门</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>最近给 <a href="https://github.com/bones7456/notchy">Notchy</a> 加了自动更新。原先的&#8221;检查更新&#8221;功能很糙：调一下 GitHub Releases API 看有没有新版本，发现有就开浏览器跳到 release 页面，让用户自己下 dmg、自己拖进 Applications。</p>
<p>这显然算不上&#8221;自动&#8221;。理想体验是：发现新版本 → 自动下载 → 校验签名 → 替换 App → 重启。这一套自己写也可以，但 macOS 生态二十年前就有现成的轮子 —— <strong><a href="https://sparkle-project.org" rel="noopener" target="_blank">Sparkle</a></strong>。</p>
<p>接进去之后回头看，整个事情比想象中简单，但中间踩了几个不算直觉的坑，整理一下。</p>
<p><img fetchpriority="high" decoding="async" src="https://luy.li/wp-content/uploads/2026/05/notchy-sparkle.png" alt="" width="1072" height="804" class="alignnone size-full wp-image-2476" /></p>
<h2>Sparkle 是什么</h2>
<p><strong>Sparkle 是 macOS 上事实标准的开源自动更新框架</strong>，从 2006 年用到现在。Transmission、VLC、Handbrake、Sequel Pro、Tower、Hammerspoon、iStat Menus …… 基本上你在 App Store 之外用过的、有自动更新的 Mac App，十有八九背后都是它。</p>
<p>工作流大致这样：</p>
<ol>
<li>App 启动或定时，Sparkle 去服务器拉一份 <code>appcast.xml</code></li>
<li>xml 里列着最新版本号、下载地址、文件大小、加密签名</li>
<li>客户端对比版本，发现有更新就弹框问用户</li>
<li>用户点&#8221;安装&#8221;，Sparkle 后台下载 zip、校验签名是不是你这个开发者签的、没问题就解压、替换 <code>.app</code>、关掉旧进程、启动新的</li>
</ol>
<p>对用户来说就是一个对话框加一次重启。对开发者来说就是发版时多产出一份 <code>appcast.xml</code>。</p>
<h2>谁适合用</h2>
<p>两种情况不太适合：<strong>上架 Mac App Store 的 App</strong>（商店本身有更新机制，且 App Store 也不允许 App 自己拉远端代码下来执行），以及<strong>强沙盒 App</strong>（Sparkle 2 技术上支持沙盒，但配置麻烦得多，要走 XPC service 隔离）。</p>
<p>比较适合的：<strong>自己 Developer ID 签名 + 公证 + 通过官网或 GitHub Releases 分发</strong>的独立 Mac App。这也是 Notchy 这类小工具最常见的发布方式。</p>
<h2>什么是 EdDSA</h2>
<p>Sparkle 在客户端校验更新包真伪用的是 <strong>EdDSA</strong> 签名。这一步对安全至关重要 —— 没有它，任何能劫持你 appcast URL 的人都能给你的用户推一个伪造的更新包。</p>
<p>EdDSA（Edwards-curve Digital Signature Algorithm）是基于椭圆曲线的数字签名算法。相比经典的 RSA，密钥短得多（公私钥各 32 字节，base64 后大约一行），签名快，对边信道攻击有自带防御。</p>
<p>具体在 Sparkle 里：</p>
<ol>
<li>你本机生成一对密钥（私钥 + 公钥）</li>
<li><strong>公钥</strong>写进 App 的 <code>Info.plist</code>（<code>SUPublicEDKey</code> 字段，会被烧进每一份发出去的 App）</li>
<li><strong>私钥</strong>只留在你的本机 Keychain 和 CI 的 secret 里</li>
<li>每次发版，用私钥给 zip 算一个签名，写进 <code>appcast.xml</code></li>
<li>客户端下载 zip 后，用嵌在自己 binary 里的公钥校验签名</li>
</ol>
<p>关键是：<strong>就算坏人完全控制了 appcast 服务器，他也伪造不出合法签名</strong>—— 除非他拿到了你的私钥。Apple 的公证和 Developer ID 不是 Sparkle 安全的最后一道防线，Sparkle 自带的 EdDSA 签名才是。</p>
<p>也因此：<strong>私钥一旦丢了，就再也没法给老用户推新版了</strong>—— 公钥已经烧进每一份分发出去的 App 里，没有匹配的私钥就永远签不出合法的更新包。生成后立刻备份到 1Password 或加密 U 盘里，是必做的事。</p>
<h2>怎么接</h2>
<p>整个接入分四部分：客户端、Info.plist、密钥、CI 流水线。一个个来。</p>
<h3>1. SPM 引依赖</h3>
<p>Xcode 里 File → Add Package Dependencies，地址：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">https://github.com/sparkle-project/Sparkle</pre><p></p>
<p>版本选 up to next major，最低 2.x。Sparkle 2 是现代版本，支持沙盒、XPC 隔离、阶段化推送等。</p>
<p>引入之后写一个最简的 controller：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">import Sparkle

@MainActor
final class UpdaterController {
    static let shared = UpdaterController()
    let controller: SPUStandardUpdaterController

    private init() {
        controller = SPUStandardUpdaterController(
            startingUpdater: true,
            updaterDelegate: nil,
            userDriverDelegate: nil
        )
    }

    func checkForUpdates() {
        controller.checkForUpdates(nil)
    }
}</pre><p></p>
<p>然后在 <code>AppDelegate.applicationDidFinishLaunching</code> 里引用一下让它实例化：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">_ = UpdaterController.shared</pre><p></p>
<p>定时检查、自动下载、弹框 UI、安装重启 —— Sparkle 全包了。&#8221;Check for Updates…&#8221; 按钮只需要接到 <code>controller.checkForUpdates(nil)</code>。</p>
<h3>2. 配 Info.plist</h3>
<p>Sparkle 至少需要这三个 key：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">&lt;key&gt;SUFeedURL&lt;/key&gt;
&lt;string&gt;https://你的域名/appcast.xml&lt;/string&gt;
&lt;key&gt;SUPublicEDKey&lt;/key&gt;
&lt;string&gt;你生成的 EdDSA 公钥（base64）&lt;/string&gt;
&lt;key&gt;SUEnableAutomaticChecks&lt;/key&gt;
&lt;true/&gt;</pre><p></p>
<p><code>SUFeedURL</code> 一般有两种托管方式：</p>
<ul>
<li>单独搞个 GitHub Pages / 自家 CDN，URL 固定</li>
<li><strong>直接用 GitHub Release 的稳定链接</strong>：<code>https://github.com/&lt;owner&gt;/&lt;repo&gt;/releases/latest/download/appcast.xml</code></li>
</ul>
<p>第二种最省事 —— GitHub 这个 URL 永远 redirect 到最新 release 里叫 <code>appcast.xml</code> 的那个 asset，发版时把 xml 当资产上传一份就行，零额外服务。</p>
<h3>3. 生成 EdDSA 密钥</h3>
<p>Sparkle 自带 <code>generate_keys</code> 工具。SPM 解出来后能在这里找到：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">~/Library/Developer/Xcode/DerivedData/&lt;你的项目&gt;-*/SourcePackages/artifacts/sparkle/Sparkle/bin/generate_keys</pre><p></p>
<p>直接跑（不带参数）会在 macOS Keychain 里生成一对 ed25519 密钥，并打印公钥。把这个公钥贴进 <code>SUPublicEDKey</code>。</p>
<p>要拿私钥（CI 要用）：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">generate_keys -x ~/Desktop/private.key</pre><p></p>
<p>文件里就是私钥的 base64 字符串。<strong>把这个字符串塞进 CI 的 secret 里</strong>（GitHub Actions 里我用的名字是 <code>SPARKLE_PRIVATE_KEY</code>）。本地的私钥文件备份后立刻删掉，因为它是明文。</p>
<h3>4. 发版流水线</h3>
<p>每次发版多做两件事：</p>
<ol>
<li><strong>用私钥给 zip 签名</strong>：Sparkle 自带的 <code>sign_update</code> 工具，输入 zip + 私钥文件，输出 <code>sparkle:edSignature="..." length="..."</code> 这一行</li>
<li><strong>生成 appcast.xml</strong>：把版本号、下载 URL、上一步的签名、文件大小、release notes 套进 RSS 模板</li>
</ol>
<p>最终 <code>appcast.xml</code> 长这样：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;rss version=&quot;2.0&quot; xmlns:sparkle=&quot;http://www.andymatuschak.org/xml-namespaces/sparkle&quot;&gt;
    &lt;channel&gt;
        &lt;title&gt;YourApp&lt;/title&gt;
        &lt;item&gt;
            &lt;title&gt;Version 1.2.3&lt;/title&gt;
            &lt;sparkle:version&gt;1.2.3&lt;/sparkle:version&gt;
            &lt;sparkle:shortVersionString&gt;1.2.3&lt;/sparkle:shortVersionString&gt;
            &lt;description&gt;&lt;![CDATA[
                &lt;h2&gt;Changes&lt;/h2&gt;
                &lt;ul&gt;&lt;li&gt;release notes 的 HTML&lt;/li&gt;&lt;/ul&gt;
            ]]&gt;&lt;/description&gt;
            &lt;pubDate&gt;Sat, 23 May 2026 10:00:00 +0000&lt;/pubDate&gt;
            &lt;enclosure
                url=&quot;https://.../YourApp-1.2.3.zip&quot;
                sparkle:edSignature=&quot;...&quot;
                length=&quot;2607122&quot;
                type=&quot;application/octet-stream&quot; /&gt;
        &lt;/item&gt;
    &lt;/channel&gt;
&lt;/rss&gt;</pre><p></p>
<p>注意 <code>&lt;description&gt;</code> 里直接内嵌 release notes 的 HTML —— Sparkle 弹框会原地渲染。如果用 <code>&lt;sparkle:releaseNotesLink&gt;</code> 指向 GitHub release 详情页，Sparkle 会用 WebView 加载那个 URL，<strong>GitHub 整站的导航条、登录按钮、左侧栏全会被渲染进对话框里，体验极差</strong>。这是亲自踩过的坑。</p>
<h2>几个不那么直觉的细节</h2>
<h3>Sparkle helper 的重签问题</h3>
<p>这是当时折腾最久的一个。接入做完、编译一切正常、archive 顺利出来、<code>codesign --verify --deep --strict</code> 全绿、所有嵌套件都 <code>--validated</code>。一切看着都对，提交给 notarytool 跑公证 —— 几十秒后回来一个 <code>status: Invalid</code>。</p>
<p>紧接着脚本继续往下跑 staple，挂在了一个看似毫不相关的错误上：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">CloudKit query for YourApp.app (...) failed due to &quot;Record not found&quot;.
The staple and validate action failed! Error 65.</pre><p></p>
<p>第一反应是网络抖动或 Apple 服务器问题，重跑一次 —— 还是一样。其实 CloudKit 这个错是 staple 在找还没存在的公证票据，根因是上一步 notarytool 返回了 Invalid。</p>
<p>跑一下 <code>xcrun notarytool log &lt;submission-id&gt;</code>，真相终于浮出水面：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">&quot;path&quot;: &quot;.../Sparkle.framework/Versions/B/Updater.app/Contents/MacOS/Updater&quot;,
&quot;message&quot;: &quot;The binary is not signed with a valid Developer ID certificate.&quot;
&quot;path&quot;: &quot;.../Sparkle.framework/Versions/B/Updater.app/Contents/MacOS/Updater&quot;,
&quot;message&quot;: &quot;The signature does not include a secure timestamp.&quot;
&hellip; （Autoupdate、Downloader.xpc、Installer.xpc 同样两条 &times; 2）</pre><p></p>
<p>原因清楚了：Sparkle 通过 SPM 编译出来时，里面这四个 helper（<code>Autoupdate</code>、<code>Updater.app</code>、<code>Downloader.xpc</code>、<code>Installer.xpc</code>）自带的是 ad-hoc 签名，不是你的 Developer ID 签的，也没有安全时间戳。<code>codesign --verify</code> 只检查签名链是否完整、本机能否校验通过，不检查&#8221;谁签的&#8221;，所以看上去全绿 —— 但 Apple 公证要求每个嵌套可执行文件都用你的 Developer ID 加 secure timestamp 签。</p>
<p>解决办法：archive 完成之后，按由内向外的顺序手动重签 —— 四个 helper、framework 本身、外层 <code>.app</code>，每一步都带 <code>--options runtime --timestamp</code>，最外层那一次还要带回原本的 entitlements 文件。每改动一层嵌套件，外面的封印就破了，必须重新盖一次。</p>
<h3>首次升级有断层</h3>
<p>接入 Sparkle 的那一版才开始有 Sparkle，再老的版本没有。所以&#8221;老版本 → 你接入 Sparkle 的这一版&#8221;这次升级用户还得手动下，没办法。从下个版本起才是真的自动。</p>
<h3>版本号字段都要写</h3>
<p>Sparkle 客户端比较新旧版本时看的是 <code>CFBundleShortVersionString</code> 和 <code>CFBundleVersion</code>。两个字段都不能漏，且服务端 appcast 里 <code>&lt;sparkle:version&gt;</code> 和 <code>&lt;sparkle:shortVersionString&gt;</code> 要和 App 里的对得上，否则版本判断会出意外。</p>
<h2>小结</h2>
<p>接入 Sparkle 的核心动作就那几下：</p>
<ul>
<li>SPM 加依赖、写个最小的 <code>SPUStandardUpdaterController</code></li>
<li>Info.plist 写好 feed URL 加公钥</li>
<li>生成 EdDSA 密钥对，公钥写 plist、私钥进 Keychain 和 CI secret</li>
<li>发版时多产出 appcast.xml，签好上传</li>
</ul>
<p>接完之后，发版就是推个 tag，CI 自动签、公证、生成 appcast、上传。用户那边就是某天打开 App，弹框告诉他有新版本，点一下、重启，完事。</p>
<p>如果你也在写独立的 macOS 小工具，自动更新这一步真的值得做 —— 它把&#8221;我有时间发版&#8221;和&#8221;老用户能用上新功能&#8221;这两件事彻底脱钩。</p><p>The post <a href="https://luy.li/2026/05/23/sparkle/">给 macOS App 加自动更新：Sparkle 入门</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/05/23/sparkle/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Notchy</title>
		<link>https://luy.li/2026/05/15/notchy/</link>
					<comments>https://luy.li/2026/05/15/notchy/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Fri, 15 May 2026 15:01:21 +0000</pubDate>
				<category><![CDATA[GUI软件]]></category>
		<category><![CDATA[精华]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2465</guid>

					<description><![CDATA[<p>还记得3月底的时候，在X上火过一段时间的那个Notchy吗？ 当时我就下载着试用过，发现点子非常不错，也很实用 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/05/15/notchy/">Notchy</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>还记得3月底的时候，在X上火过一段时间的那个Notchy吗？</p>
<p>当时我就下载着试用过，发现点子非常不错，也很实用，但当时还有一些小bug，于是搁置了。</p>
<p>今天突然又想到了，想着过了这么久，应该进化了不少吧？</p>
<p>于是有去GitHub看了一眼，发现原作者似乎没有在维护了。。。</p>
<p>想着这么好的一个项目，就这么荒废着实有点可惜，我就给接手维护<a href="https://github.com/bones7456/notchy">一个版本</a>吧。正好昨天研究CCAS的打包，也就顺手给打了<a href="https://github.com/bones7456/notchy/releases">一个包</a>。供大家更方便地下载使用。。。</p>
<p>虽然我也修了一些，也合并了一些原repo里的PR，但肯定还有不少问题，如果大家有遇到，欢迎提issue，更欢迎提PR。<br />
<img decoding="async" src="https://luy.li/wp-content/uploads/2026/05/icon_256x256.png" alt="" width="256" height="256" class="alignnone size-full wp-image-2468" /></p>
<p>附上原作者的推文，以示感谢。<br />
<span id="more-2465"></span></p>
<blockquote class="twitter-tweet">
<p lang="en" dir="ltr">Screw it, I made it open source..</p>
<p>This is Notchy -_-</p>
<p>He stops you getting distracted when using Claude code by replacing your Macbooks notch with a terminal</p>
<p>He lets you know when claude needs your attention<br />And plays a sound when tasks are complete</p>
<p>Best of all: he stops your… <a href="https://t.co/tSUCkSe8Rj">https://t.co/tSUCkSe8Rj</a> <a href="https://t.co/fhNxbSb8ET">pic.twitter.com/fhNxbSb8ET</a></p>
<p>&mdash; Adam Lyttle (@adamlyttleapps) <a href="https://twitter.com/adamlyttleapps/status/2037731313562923121?ref_src=twsrc%5Etfw">March 28, 2026</a></p></blockquote>
<p> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p><p>The post <a href="https://luy.li/2026/05/15/notchy/">Notchy</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/05/15/notchy/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Keychain和security简介</title>
		<link>https://luy.li/2026/05/14/keychain_and_security/</link>
					<comments>https://luy.li/2026/05/14/keychain_and_security/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Thu, 14 May 2026 14:55:51 +0000</pubDate>
				<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2460</guid>

					<description><![CDATA[<p>写 CCAS 的时候要做的事其实很本质：在多个 Claude 账号之间切换。第一个要弄清楚的是 —— Clau [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/05/14/keychain_and_security/">Keychain和security简介</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/05/02/ccas/" title="Claude Code Account Switcher">CCAS</a> 的时候要做的事其实很本质：在多个 Claude 账号之间切换。第一个要弄清楚的是 —— Claude Code 把自己的 OAuth credentials 存在哪？</p>
<p>翻一下之前泄露的代码很容易就知道了：在 macOS Keychain 里，service 名字是 <code>Claude Code-credentials</code>。你现在就可以在终端里看一眼：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">security find-generic-password -s &quot;Claude Code-credentials&quot; -w</pre><p></p>
<p>会打印出一坨 JSON，里面就是 access token、refresh token 这些。</p>
<p>这不是 Claude Code 的&#8221;小众&#8221;选择 —— 各种 IDE、CLI、Apple 自家的 Safari Mail，所有在 macOS 上要存&#8221;密码、token、密钥&#8221;的程序，默认就该放进 Keychain。它是这台机器上事实标准的敏感数据存储。</p>
<p>那它到底是什么？</p>
<p><img decoding="async" src="https://luy.li/wp-content/uploads/2026/05/Keychain-Access.app_.png" alt="" width="400" height="400" class="alignnone size-full wp-image-2463" /></p>
<h2>Keychain 是什么</h2>
<p>最直白的描述：Keychain 是 macOS 自带的<strong>加密键值数据库</strong>，专门为存敏感数据设计。每条记录由系统加密，挂在登录用户的身份下，由 <code>securityd</code> 这个常驻守护进程管理。</p>
<p>它的物理形态是磁盘上的几个文件，常见的有：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">~/Library/Keychains/login.keychain-db        # 用户登录 keychain
/Library/Keychains/System.keychain           # 系统级（如 WiFi 密码）</pre><p></p>
<p>你不会直接读这些文件 —— 它们是加密的 SQLite，主密钥跟你的登录密码绑定。读写都要走系统 API，由 <code>securityd</code> 解锁后再返回明文。</p>
<h2>它解决了什么</h2>
<p>如果不用 Keychain，自己拿个 JSON 文件存 token 行不行？技术上可以，但你要面对几个问题：</p>
<h3>1. 磁盘加密</h3>
<p>文件丢在 <code>~/.config/</code> 下是明文。任何能读你 home 目录的进程都能拿到（包括别的 app、误装的恶意脚本）。即便你给文件 chmod 600，磁盘镜像被拷走照样能看。</p>
<p>Keychain 用 AES 加密，主密钥来自你的登录密码 —— 机器没登录、密码不对，磁盘镜像里的内容就解不出来。</p>
<h3>2. 进程隔离</h3>
<p>文件系统只有&#8221;用户级&#8221;权限。但同一用户跑的所有 app 是平等的：Claude Code 写下的 token，理论上你装的任何一个三方 app 都能读。</p>
<p>Keychain 的每条记录带 ACL（access control list），记录&#8221;谁创建的、谁可以读&#8221;。当一个 app 试图读另一个 app 创建的条目时，系统会弹窗让用户确认。这是文件系统本身做不到的。</p>
<h3>3. 用户可见、可管理</h3>
<p>打开 <em>Keychain Access.app</em>，你能看到所有条目、谁创建的、什么时候改的，可以手动删。要 export 还得再输一次登录密码。</p>
<h3>4. 跨设备同步</h3>
<p>勾上 &#8220;iCloud Keychain&#8221; 之后，相应条目会端到端加密同步到你登录同一 Apple ID 的其他 Mac、iPhone、iPad。Safari 的密码、WiFi 密码、Apple Pay 卡号走的都是这个。</p>
<h2>数据模型</h2>
<p>Keychain 里有几类 item，最常用的是 <strong>generic password</strong>（任意 key-value 的密钥/token，多数 app 自定义存储用这个）和 <strong>internet password</strong>（带 protocol/host/port/path 字段，Safari 存网站密码用）。此外还有 certificate、key（公私钥对）、identity（cert + key 对）。</p>
<p>generic password 的核心字段就两个：</p>
<ul>
<li><strong>service</strong>：通常用 reverse-DNS 风格的命名空间，例如 <code>com.apple.account</code> 或 <code>Claude Code-credentials</code>。</li>
<li><strong>account</strong>：同一 service 下区分多条记录的标识，通常是用户名或邮箱。</li>
</ul>
<p>service + account 唯一定位一条记录。Password 字段本身可以是任意字节（你完全可以塞一整段 JSON 进去，Claude Code 就是这么干的）。</p>
<h2>怎么读写：<code>security</code> 命令</h2>
<p>Apple 给开发者两套接口：</p>
<ul>
<li><strong>C API</strong>：<code>SecItem*</code> 这一族（旧的 <code>SecKeychain*</code> 已经不推荐）。Swift / Objective-C app 一般用这个。</li>
<li><strong>命令行 <code>/usr/bin/security</code></strong>：脚本和调试用。</li>
</ul>
<p>CCAS 走的是后者，省得对接 C API；好处是用户自己在终端就能验证一切。最常用的三条：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag"># 写入
security add-generic-password \
    -s &quot;com.example.myapp&quot; \
    -a &quot;alice@example.com&quot; \
    -w &quot;the-secret&quot;

# 读出
security find-generic-password \
    -s &quot;com.example.myapp&quot; \
    -a &quot;alice@example.com&quot; \
    -w

# 删除
security delete-generic-password \
    -s &quot;com.example.myapp&quot; \
    -a &quot;alice@example.com&quot;</pre><p></p>
<p><code>-s</code> 是 service，<code>-a</code> 是 account，<code>-w</code> 表示&#8221;只把 password 内容打到 stdout&#8221;。不加 <code>-w</code> 会打印所有属性元数据，但隐去 password。</p>
<p>更多有用的子命令：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">security list-keychains -d user        # 列出当前 keychain search list
security unlock-keychain               # 手动解锁
security dump-keychain                 # 列出当前 keychain 中所有 item 的元数据
security add-internet-password ...     # 写网站密码
security find-certificate ...          # 找证书</pre><p></p>
<h2>几个不那么直觉的细节</h2>
<p><strong>search list</strong>：系统其实管理着一个 keychain &#8220;搜索列表&#8221;，<code>security find-*</code> 默认遍历整个列表。常见情况下只有 <code>login.keychain-db</code>，但用户/MDM 可能加挂别的。如果某条记录意外落在搜索列表外的 keychain 里，会出现&#8221;dump 看得到、find 找不到&#8221;的现象，这时候要在命令末尾显式带上 keychain 路径。</p>
<p><strong>密码存二进制</strong>：用 <code>-w "value"</code> 写入时整个参数被当作 UTF-8 字符串。如果 value 里有换行、引号、控制字符，shell 转义会很烦人。更可靠的方式是 <code>-X hex</code>，把内容先转 hex 再交给 <code>security</code>，由它自己 decode 后存原始字节。</p>
<p><strong><code>-U</code> 不一定是真正的 upsert</strong>：<code>add-generic-password -U</code> 文档说&#8221;如果存在则更新&#8221;，但匹配条件偏严，遇到某些属性差异会判定为&#8221;新条目&#8221;再插一条。同 service+account 的重复记录会越积越多。要 idempotent 的话，先 delete 再 add 更稳。</p>
<p><strong>account 不是严格过滤</strong>：<code>find-generic-password -s SVC -a ACCT</code> 如果 account 没精确匹配，可能 fallback 命中同 service 下别的 account —— 不报错，直接返回别人的内容。读完后再校验返回值是个好习惯。</p>
<h2>小结</h2>
<p>Keychain 是 macOS 上存敏感数据的标准答案。它做的事不复杂 —— 加密、隔离、用户可控、可同步 —— 但是把这四件事做齐了，且对开发者基本零成本。</p>
<p>下次写 Mac app 要存 token、API key 或者密码，不用犹豫，直接 <code>SecItem</code> / <code>security</code> 走起，比自己掂量&#8221;放哪、加什么密、谁能读&#8221;省太多事。</p><p>The post <a href="https://luy.li/2026/05/14/keychain_and_security/">Keychain和security简介</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/05/14/keychain_and_security/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Claude Code Account Switcher</title>
		<link>https://luy.li/2026/05/02/ccas/</link>
					<comments>https://luy.li/2026/05/02/ccas/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sat, 02 May 2026 14:31:09 +0000</pubDate>
				<category><![CDATA[GUI软件]]></category>
		<category><![CDATA[精华]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2453</guid>

					<description><![CDATA[<p>如今这AI时代，如果你也写写代码，我相信你肯定在用一些AI工具了吧？ 如果你恰好用的也是Claude code [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/05/02/ccas/">Claude Code Account Switcher</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>如今这AI时代，如果你也写写代码，我相信你肯定在用一些AI工具了吧？</p>
<p>如果你恰好用的也是Claude code，那大概率也会因为每月20$的pro套餐用量不够而烦恼吧？这时，如果你不差钱，可能就直接订阅200$的max套餐了，但如果你也觉得200刀有点下不去手，那可能再买一个20刀，就是更加可以接受的方案了。</p>
<p>此时，你就会遇到两个Claude账号频繁切换的问题了。。。那你可能就需要这个小工具了。</p>
<p>功能挺简单的，也无需我过多介绍了，直接看图就能明白了。这是一个macOS下的菜单栏小工具。可以同时登录多个Claude账号，能查看每个账号的余量，能帮你快速切换账号。<br />
<figure id="attachment_2455" aria-describedby="caption-attachment-2455" style="width: 790px" class="wp-caption alignnone"><img loading="lazy" decoding="async" src="https://luy.li/wp-content/uploads/2026/05/ScreenShot.png" alt="CCAS" width="790" height="888" class="size-full wp-image-2455" /><figcaption id="caption-attachment-2455" class="wp-caption-text">CCAS</figcaption></figure></p>
<p>截图里，我的账号1是公司给开的enterprise账号，一个月200$的token（也是不经用的）；账号2是我自己的pro账号。两种账号都是支持的。</p>
<p>工具以MIT开源。代码在<a href="https://github.com/bones7456/ccas" rel="noopener" target="_blank">GitHub</a>。等我再整理一下，放个编译好的二进制吧。</p><p>The post <a href="https://luy.li/2026/05/02/ccas/">Claude Code Account Switcher</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/05/02/ccas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>uv 的 inline script metadata 太香了</title>
		<link>https://luy.li/2026/04/18/uv_inline_script_metadata/</link>
					<comments>https://luy.li/2026/04/18/uv_inline_script_metadata/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sat, 18 Apr 2026 09:19:52 +0000</pubDate>
				<category><![CDATA[CLI软件]]></category>
		<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2445</guid>

					<description><![CDATA[<p>最近又玩了下 uv，感觉它在“小工具开发”这个场景里特别顺手。 uv 本质上是一个把 Python 依赖管理、 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/04/18/uv_inline_script_metadata/">uv 的 inline script metadata 太香了</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>最近又玩了下 <code>uv</code>，感觉它在“小工具开发”这个场景里特别顺手。</p>
<p><code>uv</code> 本质上是一个把 Python 依赖管理、虚拟环境、脚本运行这些事情揉在一起的工具。它最大的优点当然是快，但更让我喜欢的是：<strong>很多原本零碎又烦的动作，被压缩成了几个很好记的命令。</strong></p>
<p>比如平时写个小脚本，常常只是想：</p>
<ul>
<li>调一下接口</li>
<li>批量处理一点文件</li>
<li>验证一个第三方库</li>
<li>给自己做个顺手的小命令</li>
</ul>
<p>这类东西通常不值得认真建个项目，但又往往会依赖 <code>requests</code>、<code>httpx</code>、<code>rich</code> 之类的包。以前一般要先建 <code>venv</code>，再装依赖，可能还要改个 <code>.gitignore</code>，多少有点麻烦。</p>
<p>这时候 <code>uv</code> 很方便的一个点，就是可以直接用 <code>--with</code>：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">uv run --with requests demo.py</pre><p></p>
<p>或者：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">uv run --with httpx --with requests foo.py</pre><p></p>
<p>这种方式特别适合刚开始写小工具的时候。先跑起来，先验证思路，不用一上来就准备完整项目。</p>
<p>不过，<code>--with</code> 也有个问题：<strong>依赖信息是在命令里，不在脚本里。</strong></p>
<p>今天你记得怎么跑，过几天自己可能都忘了。发给别人时，也得额外告诉对方要带哪些依赖。</p>
<p>这时候真正的大招就来了：<strong>inline script metadata</strong>。</p>
<p>你可以直接把依赖用这样的格式，写到脚本头部：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag"># /// script
# requires-python = &quot;&gt;=3.11&quot;
# dependencies = [
#   &quot;httpx&quot;,
#   &quot;rich&quot;,
# ]
# ///

import httpx
from rich import print

resp = httpx.get(&quot;https://luy.li&quot;)
print(resp.status_code)</pre><p></p>
<p>可以看到，去掉<code># /// script</code>这些包裹层之后，里面的内容，其实就是<a href="https://toml.io/" rel="noopener" target="_blank">TOML</a>的语法。</p>
<p>这样之后就不用再写长命令了，直接：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">uv run foo.py</pre><p></p>
<p>就行。这个体验非常好。因为脚本自己就说明白了：</p>
<ul>
<li>需要什么 Python 版本</li>
<li>依赖哪些包</li>
<li>应该怎么运行</li>
</ul>
<p>更进一步，你甚至可以再加个 <a href="https://zh.wikipedia.org/wiki/Shebang" rel="noopener" target="_blank">shebang</a>，把它写成一个可以直接执行的脚本：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">#!/usr/bin/env -S uv run --script
# /// script
# requires-python = &quot;&gt;=3.11&quot;
# dependencies = [
#   &quot;httpx&quot;,
#   &quot;rich&quot;,
# ]
# ///

import httpx
from rich import print

print(httpx.get(&quot;https://luy.li&quot;).status_code)</pre><p></p>
<p>这样给脚本加上执行权限(<code>chmod +x</code>)之后，连 <code>uv run foo.py</code> 都可以省掉，直接 <code>./foo.py</code> 就可以跑起来了。</p>
<p>最后总结一下：</p>
<ul>
<li>刚开始随手写个小工具，用 <code>uv run --with ...</code></li>
<li>觉得这个脚本值得留下来，就上 <code>inline script metadata</code></li>
<li>再彻底一点，可以用<code>#!/usr/bin/env -S uv run --script</code>这个shebang </li>
</ul><p>The post <a href="https://luy.li/2026/04/18/uv_inline_script_metadata/">uv 的 inline script metadata 太香了</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/04/18/uv_inline_script_metadata/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
