<?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>Mon, 05 Oct 2026 10:38:21 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>老V100跑图片embedding的那些事</title>
		<link>https://luy.li/2026/10/05/v100_embedding/</link>
					<comments>https://luy.li/2026/10/05/v100_embedding/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Mon, 05 Oct 2026 10:37:59 +0000</pubDate>
				<category><![CDATA[备忘]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2615</guid>

					<description><![CDATA[<p>去年我写过一篇用ktransformers跑671b的DeepSeek R1，里面那台&#8221;4块V10 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/10/05/v100_embedding/">老V100跑图片embedding的那些事</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/2025/02/27/ktransformers_deepseek/">用ktransformers跑671b的DeepSeek R1</a>，里面那台&#8221;4块V100的GPU卡&#8221;的机器，最后只跑出了 1 token/s 左右，基本只能拿来研究研究。没想到一年多过去，它又被我翻出来干活了：这次是给一大批图片做 embedding，用来做以图搜图。用的模型是 <a href="https://huggingface.co/Qwen/Qwen3-VL-Embedding-8B">Qwen3-VL-Embedding-8B</a>，全量跑一遍要十几天。</p>
<p>活儿本身倒是跑起来了，但过程中我被问（也自己问自己）了好几个问题：V100 为啥用不了 BF16？FA2 到底是个啥？每张图 256 个 visual tokens 到底算多还是算少，会不会影响搜索效果？换个 token 数速度又差多少？查资料加实测折腾了一圈，算是把这几个概念理顺了，整理成一篇学习笔记，分享给有需要的朋友。<br />
<span id="more-2615"></span></p>
<h2>先说结论：V100 能用的只有 FP16</h2>
<p>这台机器的配置跟那篇里一样：4 块 V100，每块 16G 显存，计算能力（compute capability）是 7.0，也就是 Volta 架构。这个 7.0 是关键，后面一堆&#8221;用不了&#8221;都是因为它。</p>
<p>8B 的模型，FP16 权重差不多就把一张 16G 的卡塞满了（实测只剩一百来 MB 余量），所以最后的部署方式是每张卡跑一个副本、batch 1，四张卡加起来大概 30 张图/秒。至于为什么不把一个模型切到多张卡上、为什么不开更大的 batch，那又是另一个故事了。。。</p>
<h2>FP16、BF16、FP8 有什么区别？</h2>
<p>浮点数由三部分组成：符号位、指数位、尾数位。<b>指数位决定能表示的范围，尾数位决定精度</b>。理解了这一句，这几种格式的区别就很好懂了：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">格式        位宽   指数/尾数   最大值          大约有效位数   最早支持的硬件
FP32        32     8 / 23     ~3.4e38         ~7 位          所有 GPU
FP16        16     5 / 10     65504           ~3 位          Volta（V100）起有 Tensor Core
BF16        16     8 / 7      ~3.4e38         ~2 位          Ampere（A100）起
FP8 E4M3    8      4 / 3      448             ~1 位          Hopper（H100）/ Ada 起
FP8 E5M2    8      5 / 2      57344           更低           同上</pre><p></p>
<h3>FP16</h3>
<p>FP16 的优点是：在 16 位格式里精度最高，而且 V100 就已经有 Tensor Core 支持，算力是 FP32 的好几倍，显存和带宽占用也减半。</p>
<p>缺点是<b>范围太小</b>，超过 65504 就溢出成 inf，然后就是一路 NaN。大模型的激活值里经常有少量特别大的离群值，尤其是那些用 BF16 训练出来的模型，训练的时候根本不用操心溢出，拿到 FP16 下推理就可能在某一层炸掉。Qwen3-VL 官方就是按 BF16 发布的，在 V100 上只能用 FP16 硬跑（好在我们这边输出一直是正常的）。</p>
<h3>BF16</h3>
<p>BF16 的思路很直接：<b>拿精度换范围</b>。它的指数位和 FP32 一样是 8 位，所以范围也和 FP32 一样大，基本不会溢出，训练时也不需要 loss scaling，从 FP32 转过来几乎没坑。代价是尾数只剩 7 位，精度比 FP16 差。</p>
<p>对深度学习来说，范围比精度重要得多，所以 BF16 成了现在大模型训练的事实标准。可惜要 A100（计算能力 8.0）及以上才支持，V100 就只能看着了。。。</p>
<h3>FP8</h3>
<p>FP8 是再砍一半：Tensor Core 算力大约是 BF16 的 2 倍，权重显存也减半，对推理吞吐很有吸引力。但它的精度和范围都非常有限，<b>必须配合缩放因子</b>（per-tensor、per-channel 或 per-block 的 scale）才能用，还要校准，可能带来能感知到的精度损失。</p>
<p>它有两个变体：E4M3 精度稍高，一般用于前向的权重和激活；E5M2 范围更大，一般用于反向的梯度。需要 H100、L40 这一代的卡。PS：即使是 E4M3，能表示的最小正数，也只是 0.00195 而已，是个什么精度，可以以此为参考。</p>
<p>对 embedding 这种要比较向量相似度的任务，我会比较谨慎：真要上 FP8，得先拿一批固定的图，对比 FP8 和 BF16 算出来的向量 cosine 相似度，再看 top-k 召回的重合率，不能光看速度。</p>
<h2>FA2 又是什么？</h2>
<p><b>FA2（FlashAttention-2）不是数值格式</b>，而是 attention 的一种计算实现（kernel）。它之所以总跟 BF16、FP8 被放在一起说，是因为它们都卡硬件，而 V100 正好都用不上（哈哈，难兄难弟）。</p>
<p>标准的 attention 要先算出一个 N×N 的分数矩阵（N 是序列长度），写回显存，再读回来做 softmax，再乘 V。这个过程的瓶颈其实不在计算，而在<b>显存读写</b>，而且显存占用是 O(N²)，序列一长就爆。</p>
<p><a href="https://github.com/Dao-AILab/flash-attention">FlashAttention</a> 的做法是把 Q、K、V 切成小块，放进 GPU 片上的 SRAM 里，把&#8221;matmul → softmax → matmul&#8221;融合进一个 kernel 里算完。softmax 用&#8221;online softmax&#8221;的技巧逐块更新，<b>完全不需要生成完整的 N×N 矩阵</b>。所以：</p>
<p>1. 结果是精确的，不是近似 attention。<br />
2. 显存从 O(N²) 降到 O(N)。<br />
3. 一般比标准实现快 2～4 倍，序列越长越明显。</p>
<p>但它要求 Ampere 及以上（计算能力 8.0+），只支持 FP16/BF16 输入。去年那篇里我还专门折腾过 flash-attn 的安装，现在回头看，V100 本来就不是它的目标硬件。。。在 V100 上能用的替代是 PyTorch 的 SDPA，它有一个 memory-efficient 后端，能省一些显存，但速度比不上 FA2。</p>
<p>另外要说一句：我们每张图只有 256 个 visual tokens，序列很短，attention 本来就不是主要开销。所以就算换了新卡，<b>提升主要来自更强的 Tensor Core 和更大的显存</b>（终于能开真正的 batch 了），FA2 只是锦上添花。</p>
<h2>256 个 visual tokens 是什么水平？</h2>
<p>先搞清楚一个 token 对应多少像素。Qwen3-VL 的视觉编码器把图切成 16×16 的 patch，再把 2×2 个 patch 合并成 1 个 token，所以 <b>1 个 token = 32×32 像素</b>。注意这跟 Qwen2-VL 的 28×28 不一样，网上很多资料还是按老的算法讲的。</p>
<p>这样算下来，256 tokens 大约是 26 万像素，正方形图就相当于 <b>512×512</b>。非正方形的图会保持长宽比，总像素不变。跟常见的检索模型比一下：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">方案                          等效输入分辨率
CLIP ViT-L/14 @224            224&times;224（256 个 patch）
SigLIP @384                   384&times;384（729 个 patch）
Qwen3-VL @256 tokens          约 512&times;512
Qwen3-VL 默认上限 1280 tokens  约 1145&times;1145</pre><p></p>
<p>所以 256 在 Qwen3-VL 自己的可选范围里算偏低档，差不多只有默认上限的 1/5；但跟业界常用的检索模型比，分辨率一点也不低，像素量差不多是 CLIP 224 的 5 倍。</p>
<h2>它会影响以图搜图吗？</h2>
<p>先理解一点：不管输入多少 token，最后都会被压成<b>一个 4096 维的向量</b>。这个向量主要编码的是图片的整体语义：是什么东西、什么风格、什么颜色、什么构图。token 数决定的是模型&#8221;看得多清楚&#8221;，而一个向量本身能装下的细节也是有上限的。</p>
<p><b>基本不受影响的场景：</b></p>
<p>1. 同一张图，或者压缩、缩放、轻微裁剪、加水印后的近似重复图。实测拿库里已有的图去搜，自己匹配自己的得分是 1.0001，非常稳。<br />
2. 同款商品换了角度、背景或模特，找同类、找风格相似的图。这些靠的是整体外观，512 像素已经足够看清。</p>
<p><b>可能明显受影响的场景：</b></p>
<p>1. 图里的文字：品牌名、型号、价格标签、包装上的小字。512 像素下小字大概率已经糊了，Qwen3-VL 本来不错的 OCR 能力在这里就浪费了。<br />
2. 细粒度区分：同一系列的不同型号、纹理图案上的细微差别、很小的 logo。<br />
3. 小目标：要找的东西只占原图很小一块。<br />
4. 极端长宽比：长截图、长图、横幅。总像素是固定的，长边一拉长，短边就被压得很小。</p>
<p>要说明的是，上面这些是根据原理做的判断，不同 token 数下的召回率我还没有实测过（GPU 正忙着跑全量，抽不出来<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f602.png" alt="😂" class="wp-smiley" style="height: 1em; max-height: 1em;" />）。打算等全量跑完，抽一部分图片分别用 256、512、1280 建几个小索引，按场景分组比一下 recall@10，用数据说话。</p>
<h2>换个 token 数，速度差多少？</h2>
<p>这个倒是实测过的。同一批图，8B 模型，batch 8，计时前已经做完 resize，只算 GPU 部分：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">visual tokens   等效分辨率    每张耗时     相对 256
64              ~256&sup2;        48.7 ms      0.40&times;
128             ~362&sup2;        67.2 ms      0.55&times;
256             ~512&sup2;        121.3 ms     1&times;
512             ~724&sup2;        223.3 ms     1.84&times;
1024            ~1024&sup2;       538.9 ms     4.44&times;</pre><p></p>
<p>可以看到，<b>token 数每翻一倍，耗时大约变成原来的 2.0～2.6 倍，比线性增长还快</b>。原因也好理解：MLP 部分的开销跟 token 数成正比，attention 部分则是 O(n²)。</p>
<p>换算到全量任务上：现在 256 tokens 要跑十几天，换成 512 就得一个多月，换成 1024 得两个多月，而且 1024 在单张 16G 的 V100 上大概率直接 OOM（权重已经快把显存占满了，激活值一大就放不下）。这也是当初选 256 的原因。</p>
<p>另外有两个一开始让我有点意外的结论：</p>
<p>1. <b>原图分辨率几乎不影响 GPU 速度。</b> 256 tokens 预算下，原图是 512²、1024²、2048²、4096² 时，GPU 耗时分别是 102.3、102.9、102.6、102.8 ms，几乎是一条直线，因为图片会先被缩到 token 预算以内。原图越大，只会多花一点 CPU 解码 JPEG 的时间（大约每百万像素 8.4 ms），远远赶不上 GPU 的速度，不会成为瓶颈。所以<b>速度只看 token 预算</b>。<br />
2. <b>长宽比也几乎不影响速度。</b> 图片是等比缩放的，不加 padding 也不裁剪，耗时只跟最终的 token 数有关，3:1 和 1:3 只差 3%。</p>
<h2>几个坑</h2>
<p>1. <b>查询和建库必须用同一个 token 数。</b> 这是最隐蔽的一个坑：库是用 256 建的，而服务端默认是 512。我拿库里已有的一张图去搜，用 512 算出来的向量跟库里自己的相似度只有 0.938，换成 256 才是 1.0001。它不报任何错，只是悄悄返回更差的结果。这也说明 token 数一改，向量就变了，以后想升级只能全量重算，新旧向量绝对不能混用。<br />
2. <b>测速时别把 resize 算进去。</b> 直接计时 <code>encode()</code> 的话，里面包含了 processor 的 resize，而 resize 的耗时随原图像素增长，会让原本不变的 GPU 耗时看起来从 512² 到 4096² 翻了三倍。我差点就得出&#8221;原图越大越慢&#8221;的错误结论。<br />
3. <b>直接调 <code>processor.image_processor(images=...)</code> 会绕过 token 预算。</b> 预算是作为 <code>min_pixels</code>/<code>max_pixels</code> 在 embedder 的处理流程里生效的，单独调 image_processor 得到的 token 数跟实际流水线对不上，拿来做估算是没有意义的。<br />
4. <b>长宽比超过 200 会直接报 <code>ValueError</code>。</b> 不会优雅降级。如果图片来源不可控（比如用户上传），得在上游先校验一下。</p>
<h2>最后</h2>
<p>折腾了一圈，结论其实挺朴素的：V100 只能用 FP16，没有 BF16、FA2、FP8；256 tokens 做以图搜图基本够用，但小字和细节是短板；想要更高的 token 数，代价几乎是翻倍往上涨的。</p>
<p>这台老伙计去年跑 DeepSeek 只有 1 token/s，今年总算干上了一份正经工作，接下来还要连轴转二十来天。vibe了一阵之后，利用率已经压榨地比较狠了，如图：<br />
<img fetchpriority="high" decoding="async" src="https://luy.li/wp-content/uploads/2026/10/V100x4.webp" alt="" width="1500" height="364" class="alignnone size-full wp-image-2617" /></p>
<p>看看能不能顺利跑完吧～</p><p>The post <a href="https://luy.li/2026/10/05/v100_embedding/">老V100跑图片embedding的那些事</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/10/05/v100_embedding/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>博客安全头踩坑记</title>
		<link>https://luy.li/2026/09/26/https_headers/</link>
					<comments>https://luy.li/2026/09/26/https_headers/#comments</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sat, 26 Sep 2026 00:18:14 +0000</pubDate>
				<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2606</guid>

					<description><![CDATA[<p>我这个博客一直是自部署的，很久之前就装了 Really Simple Security（那时候还叫Really [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/09/26/https_headers/">博客安全头踩坑记</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>我这个博客一直是自部署的，很久之前就装了 <a href="https://wordpress.org/plugins/really-simple-ssl/">Really Simple Security</a>（那时候还叫Really Simple SSL） 这个插件，最近它一直提示我的博客不安全，就按它提供的链接<a href="https://scan.really-simple-ssl.com/?domain=https://luy.li">扫了一下</a>，结果页面上一排 &#8220;Not Found&#8221;，HSTS、CSP、Referrer-Policy……六个安全头一个都没有。我心想，不对啊，好歹也是跑了这么多年的站，怎么会一个都没有？（后来看扫描页才发现，帮你加这些头是它 Pro 版的功能，免费版只负责提醒你。。。）</p>
<p><span id="more-2606"></span><br />
<img decoding="async" src="https://luy.li/wp-content/uploads/2026/09/blog_headers.webp" alt="" width="1500" height="711" class="alignnone size-full wp-image-2609" /></p>
<p>curl 一下看看：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">$ curl -sI https://luy.li/
HTTP/2 200
server: nginx/1.24.0 (Ubuntu)
date: Fri, 25 Sep 2026 06:54:12 GMT
content-type: text/html; charset=UTF-8
link: &lt;https://luy.li/wp-json/&gt;; rel=&quot;https://api.w.org/&quot;</pre><p></p>
<p>还真是干干净净，除了 nginx 很热情地把自己的版本号和系统都报出来了，啥安全相关的头都没有。。。行吧，补。结果补着补着，把同域名下的另一个服务给补挂了，哈哈。整个过程学到不少东西，写下来，分享给有需要的朋友。</p>
<h2>这六个头都是干嘛的？</h2>
<p>先说结论：这几个都不是&#8221;漏洞&#8221;，而是&#8221;浏览器端的防护没开&#8221;。WordPress 默认不加，nginx 默认也不加，所以只能自己加。</p>
<p>1. Strict-Transport-Security（HSTS）：告诉浏览器&#8221;以后只许用 HTTPS 访问我&#8221;。我的 80 端口虽然早就 301 到 https 了，但用户每次敲 luy.li 的时候，都会先发一次明文请求再被跳走，公共 Wi-Fi 下就有被劫持降级的可能。有了 HSTS，浏览器会直接在本地改成 https，明文请求压根不发出去。不过它保护不了第一次访问（得先收到过这个头才行），想连第一次也保护，就得靠后面会说到的 preload。<br />
2. X-Content-Type-Options: nosniff：不让浏览器瞎猜文件类型，防止有人传个伪装成图片的 HTML 被当脚本执行。零副作用，无脑加。<br />
3. Referrer-Policy：跳去外站的时候少带点信息。推荐 <code>strict-origin-when-cross-origin</code>，跨站只带域名，不带完整路径和参数。<br />
4. Permissions-Policy：把摄像头、麦克风、定位这些用不上的浏览器能力直接关掉。<br />
5. X-XSS-Protection：这个比较有意思，推荐值是 <code>0</code>，也就是<b>关掉</b>。老浏览器内置的 XSS 过滤器本身就有漏洞，已经被废弃了，明确关掉反而更安全。<br />
6. Content-Security-Policy（CSP）：白名单，限制页面能从哪加载脚本、样式、能不能被别人 iframe 嵌套。防 XSS 最有效的一层，但也是最容易把站搞坏的一个。</p>
<h2>nginx 里怎么加？</h2>
<p>在 443 的那个 <code>server {}</code> 里加上：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag"># 隐藏 nginx 版本号
    server_tokens off;

    # 安全响应头（注意：任何 location 里只要自己写了 add_header，就不会再继承这里的所有 add_header）
    add_header Strict-Transport-Security &quot;includeSubDomains; preload; max-age=63072000&quot; always;
    add_header X-Content-Type-Options &quot;nosniff&quot; always;
    add_header Referrer-Policy &quot;strict-origin-when-cross-origin&quot; always;
    add_header Permissions-Policy &quot;camera=(), microphone=(), geolocation=(), payment=()&quot; always;
    add_header X-XSS-Protection &quot;0&quot; always;
    add_header Content-Security-Policy &quot;upgrade-insecure-requests; frame-ancestors 'self'; object-src 'none'; base-uri 'self'&quot; always;</pre><p></p>
<p>几个要注意的地方：</p>
<p>1. <b>add_header 的继承是&#8221;全有或全无&#8221;</b>。某个 location 里只要自己写了一条 add_header（比如给静态文件加 Cache-Control），server 级的所有 add_header 在这个 location 里就全部失效了。这个坑非常隐蔽，加完一定要挨个 location curl 一下。<br />
2. 带上 <code>always</code>，不然 404、500 这些错误页是不带这些头的。<br />
3. CSP 我只给了个宽松版。看了下首页，脚本倒是都来自本站，但 WordPress 和插件会往页面里塞好几段内联 script，要是写 <code>script-src 'self'</code>，页面立马就坏了。所以我只做了不会弄坏页面的几件事：升级混合内容、禁止被别的站 iframe 嵌套（顺便防了点击劫持）、禁 object、锁 base。想收紧的话，先用 <code>Content-Security-Policy-Report-Only</code> 跑几天，看控制台报了啥再说。</p>
<p>改完 <code>nginx -t &amp;&amp; systemctl reload nginx</code>，再 curl：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">$ curl -sI https://luy.li/
HTTP/2 200
server: nginx
...
strict-transport-security: includeSubDomains; preload; max-age=63072000
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(), payment=()
x-xss-protection: 0
content-security-policy: upgrade-insecure-requests; frame-ancestors 'self'; object-src 'none'; base-uri 'self'</pre><p></p>
<p>server 那行也不再报版本号了，舒服～</p>
<h2>顺手堵上的几个口子</h2>
<p>扫描器只看响应头，但既然都动配置了，WordPress 常见的几个口子也顺便看了看。</p>
<p><b>xmlrpc.php</b>。这个默认是开着的，而且支持 <code>system.multicall</code>，意思是一个请求里可以塞上百组用户名密码去试，爆破效率极高。我又不用手机 App 发文章，直接关：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">location = /xmlrpc.php {
        deny all;
    }</pre><p></p>
<p><b>readme.html 和 license.txt</b>。这俩文件能直接访问，里面写着 WordPress 版本号，等于告诉别人该拿哪个版本的漏洞来试。同样 deny 掉（删文件也行，但每次升级 WordPress 会重新生成，不如 nginx 里一劳永逸）。</p>
<h2>HSTS 是不认端口的！</h2>
<p>到此，我以为都结束了，但看进度条就知道（喂，你是blog哎，哪有进度条？）还没完。配置上线之后，我检查了一下在这个域名下还跑着另一个服务，之前是 <code>http://luy.li:端口号</code> 这种明文HTTP地址访问的，发现已经挂掉了。</p>
<p>HSTS 是按<b>主机名</b>生效的，跟端口没有关系（<a href="https://datatracker.ietf.org/doc/html/rfc6797">RFC 6797</a> 里写得明明白白）。浏览器只要从 <code>https://luy.li</code> 收到过一次 HSTS 头，之后访问 <code>http://luy.li:任意端口</code> 都会被自动改写成 <code>https://luy.li:同一个端口</code>。而那个端口上跑的是 HTTP，并且我不想改成 HTTPS。</p>
<p>也就是说，部署完之后，所有看过我博客的浏览器，再打开那个服务都会连不上，而且这个状态会被浏览器记两年。好在非浏览器的客户端一般不遵守 HSTS，只有浏览器会中招，不幸中的万幸 <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f602.png" alt="😂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>咱们换个思路：HSTS 只管下发它的那个主机名，如果我不加 <code>includeSubDomains</code>，它就管不到子域名。刚好我的 DNS 配了泛解析，随便起个子域名就能解析到同一台机器上，换成 <code>http://xxx.luy.li:端口号</code> 访问，一行配置都不用改。可问题是，扫描器推荐的值里偏偏就带着 <code>includeSubDomains</code>。。。那能不能加上它，再给那个子域名开个白名单呢？</p>
<h2>includeSubDomains 能加白名单吗？</h2>
<p>不能。HSTS 标准里压根没有排除机制：</p>
<p>1. 端口：HSTS 完全不看端口，没法单独放过某个端口。<br />
2. 子域名：父域名一旦带了 <code>includeSubDomains</code>，所有子域名、不管几层都被覆盖。子域名自己返回 <code>max-age=0</code> 也没用，它只能清掉自己的那条策略，管不了父域名的。<br />
3. preload：header 里写上 <code>preload</code> 只是个表态，要去 <a href="https://hstspreload.org/">hstspreload.org</a> 提交、审核通过，才会真正被写进浏览器的内置列表。它的硬性要求就是必须带 <code>includeSubDomains</code>，所有子域名都得能正常走 HTTPS（我的证书一开始就没包含 www，差点栽在这）。而且一旦进了列表，想撤出来得等浏览器发新版，几个月起步。</p>
<p>所以，如果某个服务就是要跑明文 HTTP，又想上 <code>includeSubDomains</code>，只剩两条路：给它另起一个独立的域名，或者直接用 IP 访问（HSTS 对 IP 不生效）。我似乎有洁癖，所以选择了换个域名跑，扫描就全绿了。</p>
<h2>浏览器把 HSTS 存哪了？</h2>
<p>踩完这个坑，自然会想：已经中招的浏览器，怎么把 luy.li 的 HSTS 记录删掉？</p>
<p>Chrome 最方便。打开 <code>chrome://net-internals/#hsts</code>（Edge 是 <code>edge://net-internals/#hsts</code>），先在 &#8220;Query HSTS/PKP domain&#8221; 里查一下 luy.li，能看到记录就说明中招了；再在 &#8220;Delete domain security policies&#8221; 里输入 luy.li，点 Delete 就行。底层文件在 Profile 目录下的 <code>TransportSecurity</code>，是个 JSON，但里面的主机名是哈希过的，直接看是看不出来的。注意这里只能删浏览器动态记下的，preload 内置列表里的删不掉。</p>
<p>Firefox 在历史记录里找到这个站，右键&#8221;忘记此网站&#8221;，不过会把 Cookie 什么的一起清掉。底层文件是 Profile 目录下的 <code>SiteSecurityServiceState.bin</code>。</p>
<p>Safari 的话，我在 <code>~/Library</code> 下搜了一圈，现在的 macOS 是各个 App 自己存一份的，比如 <code>Caches/com.apple.helpd/HSTS.plist</code>、<code>Caches/us.zoom.xos/WebKit/HSTS</code>。Safari 是沙盒应用，它那份应该在 <code>~/Library/Containers/com.apple.Safari/</code> 下面，终端去访问直接报 <code>Operation not permitted</code>，除非给终端开&#8221;完全磁盘访问权限&#8221;。所以老老实实走界面吧：Safari 设置 → 隐私 → 管理网站数据，搜 luy.li，移除，然后彻底退出 Safari 再开。</p>
<p>不过要清楚一点：删了也白删。只要这个浏览器再打开一次 <code>https://luy.li</code>，HSTS 头又会被写回来。删除只适合临时救急，想长期共存，还是得像上面那样把服务挪到别的域名上。</p>
<h2>几个值得记住的坑</h2>
<p>1. nginx 的 add_header：location 里写了任何一条，就不再继承 server 级的全部 add_header。<br />
2. HSTS 按主机名生效，不认端口。同一个域名下只要有一个端口跑明文 HTTP，就不能在这个域名上开 HSTS。<br />
3. includeSubDomains 没有白名单，preload 基本是单行道，加之前想清楚。<br />
4. 撤销 HSTS 不能只删掉这个头，要返回 <code>max-age=0</code>，已经缓存了策略的浏览器才会清掉。已经进了 preload 列表的，这招也不管用。</p>
<p>好了，我去把各个设备上的书签都换成其他域名了。本来只是想让扫描器变绿，结果差点把自己的另一个服务搞瘫。。。<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p><p>The post <a href="https://luy.li/2026/09/26/https_headers/">博客安全头踩坑记</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/09/26/https_headers/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>rtorrent 挪文件后续种那些事</title>
		<link>https://luy.li/2026/09/25/rtorrent-after-mv/</link>
					<comments>https://luy.li/2026/09/25/rtorrent-after-mv/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Fri, 25 Sep 2026 05:02:57 +0000</pubDate>
				<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2601</guid>

					<description><![CDATA[<p>前两天整理 Jellyfin 的媒体库，把电影和剧集分到了不同目录（因为剧集在电影库里会展开显示，就很乱），一 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/09/25/rtorrent-after-mv/">rtorrent 挪文件后续种那些事</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/02/20/jellyfin/">Jellyfin</a> 的媒体库，把电影和剧集分到了不同目录（因为剧集在电影库里会展开显示，就很乱），一通 mv 下来，Jellyfin 那边是清爽了。然后我突然想起来：这些片子都是古老但依旧好用的 rtorrent 下的，而 rtorrent 一直开着在做种呢！我意识到，不妙，来活儿了～</p>
<p>好在折腾一圈之后，文件不用挪回去，种子也没重新下，校验 100% 继续做种。中间踩了几个挺反直觉的坑，网上中文资料也不多，值得写一篇，分享给有需要的朋友。<br />
<span id="more-2601"></span></p>
<h2>mv 之后，rtorrent 会怎样？</h2>
<p>先说结论：<b>不会马上出问题，但迟早会出问题。</b></p>
<p>同一个文件系统内的 mv 其实只是改了个名字，文件本身没动。rtorrent 手里如果还攥着这个文件的句柄，就还能照常读，所以刚 mv 完的时候，上传可能看起来一切正常。</p>
<p>但 rtorrent 会定期关掉空闲的文件，下次再按原路径去打开，就找不到了。更要命的是重启 rtorrent 之后，它会重新校验，原路径下啥都没有，种子直接被判成 0%。这时候要是一启动，它就会勤勤恳恳地在原目录把几十个 G 重新下一遍。。。</p>
<p>所以正确的做法是：告诉 rtorrent 文件搬到哪儿了。</p>
<h2>正确的改目录姿势</h2>
<p>rtorrent 里改种子目录有两个命令，区别挺关键：</p>
<p>1. <code>d.directory_base.set</code>：填的是种子的<b>最终路径</b>。多文件种子就是那个种子文件夹本身。<br />
2. <code>d.directory.set</code>：填的是<b>父目录</b>。对多文件种子，rtorrent 会自动在后面拼上种子自己的名字。</p>
<p>我最后是用第二个搞定的（原因见下一节）。完整流程如下，在 rtorrent 界面里选中这个种子：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">Ctrl-D                                   # 停止种子（只按一次！连按两次就会从列表里删除掉）
Ctrl-X  d.close=                         # 关闭种子，状态从 [OPEN] 变成 [CLOSED]
Ctrl-X  d.directory.set=/DOWNLOAD/PATH   # 只填父目录
Ctrl-X  print=$d.base_path=              # 看一眼当前路径对不对
Ctrl-R                                   # 重新校验
Ctrl-S                                   # 启动，继续做种</pre><p></p>
<p>校验跑完显示已下载和总大小一致，就说明路径改对了。最后顺手存一下会话，免得 rtorrent 还没来得及自动保存就被重启，新路径又丢了：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">Ctrl-X  session.save=</pre><p></p>
<h2>踩到的坑</h2>
<h3>1. 种子没关，目录改不了</h3>
<p>我一开始 Ctrl-D 停了种子就去改目录，结果底部蹦出来一行：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">Input failed: Cannot change the directory of an open download atter the files have been moved.</pre><p></p>
<p>（对，atter，rtorrent 源码里就是这么拼的，哈哈，方便大家原样搜索）</p>
<p>意思是：种子还处于 open 状态，而且已经在原目录创建过文件，这时不让改目录。<b>Ctrl-D 只是停止，并不是关闭</b>，得再用 <code>d.close=</code> 把它关掉才行。</p>
<p>还有一个细节：<b>Ctrl-R 校验会把种子重新打开</b>。所以顺序一定是先关、再改、最后校验。我中间手快按了一次 Ctrl-R，列表里又变回了 <code>[OPEN]</code>，还显示 0%，当时着实吓了一跳：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">* [OPEN]   0.0 / 28395.6 MB Rate: 0.0 / 0.0 KB Uploaded: 200046.0 MB
* Inactive: Cannot change the directory of an open download atter the files have been moved.</pre><p></p>
<p>这时千万别 Ctrl-S，一启动就开始重新下载了。</p>
<h3>2. Ctrl-X 输入框吃掉了中文</h3>
<p>这个坑最隐蔽。我的种子目录叫 <code>[太平年].Swords.into.Plowshares.S01.2026.1080p.IQ.WEB-DL.H264.AAC-CMCTV</code>，用 <code>d.directory_base.set</code> 把完整路径粘进去之后，没报错，看着挺顺利。结果 print 一下：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">/home/shared/TV/[].Swords.into.Plowshares.S01.2026.1080p.IQ.WEB-DL.H264.AAC-CMCTV</pre><p></p>
<p>「太平年」三个字凭空消失了！<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f602.png" alt="😂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 列表里明明能正常显示中文，但 Ctrl-X 的命令输入框很可能只收 ASCII，粘贴进去的中文被悄悄丢掉了，目录就被改成了一个根本不存在的路径。</p>
<p>解决办法就是前面说的 <code>d.directory.set</code>：只填纯英文的父目录 <code>/home/shared/TV</code>，中文部分让 rtorrent 从种子文件里自己取，一个汉字都不用输入。单文件种子也一样，填文件所在的目录就行，文件名也是从种子里取的。</p>
<p>前提是：<b>挪过去的文件夹名和文件名，必须和种子里的一模一样。</b>为了让 Jellyfin 识别得好而顺手改名的，就续不了种了。</p>
<h2>顺手整理：rtorrent 常用按键和命令</h2>
<p>rtorrent 是纯键盘操作，没有菜单可点，平时用熟的就那几个键，一到这种时候就得现翻文档。。。干脆把常用的整理一下，下次直接来这儿抄。</p>
<h3>主界面按键</h3>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">&uarr; / &darr;          在种子列表里上下选择
&rarr;              进入选中种子的详情页（Peer、文件列表、Tracker 等）
&larr;              返回上一级
1 ~ 9          切换视图，1 是 main，其余是 started/stopped/complete/seeding 这类筛选视图

Ctrl-S         启动种子
Ctrl-D         停止种子；对已停止的种子再按一次 = 从列表移除（不删文件）
Ctrl-K         停止并关闭种子（相当于 d.stop= 加 d.close=）
Ctrl-R         重新校验
Ctrl-O         修改种子的保存目录（需要种子处于关闭状态）
+ / -          调整种子优先级

Backspace      输入种子文件路径或 URL，添加并立即开始
Enter          输入种子文件路径或 URL，添加但不开始

a / s / d      全局上传限速 +1 / +5 / +50 KB
z / x / c      全局上传限速 -1 / -5 / -50 KB
A S D / Z X C  同上，改的是下载限速

Ctrl-X         输入命令（下面那一堆都靠它）
Ctrl-Q         退出，再按一次强制退出</pre><p></p>
<p>对，<b>Ctrl-K 其实一步就能完成「停止 + 关闭」</b>，比我前面先 Ctrl-D 再 <code>d.close=</code> 省事。不过拆开写更能看清楚「停止」和「关闭」是两回事，就先这样吧，哈哈。</p>
<p>Ctrl-O 也能改目录，但它同样是在 rtorrent 的输入框里敲路径，碰到中文目录名应该也逃不过被吞的命运，所以我还是更推荐 <code>d.directory.set</code> 只填父目录的办法。</p>
<p>在详情页里选中文件列表（File list）时，按空格可以切换单个文件的优先级，比如 off 掉不想下的文件，只下合集里的某几集。</p>
<h3>Ctrl-X 常用命令</h3>
<p>以 <code>d.</code> 开头的命令作用于当前选中的种子，其他的是全局命令：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">d.start=                           启动
d.stop=                            停止
d.close=                           关闭
d.erase=                           从列表移除（不删文件）
d.check_hash=                      重新校验，同 Ctrl-R
d.tracker_announce=                立即向 Tracker 汇报一次

d.directory.set=/path              改目录，填父目录，种子名自动拼接
d.directory_base.set=/path         改目录，填最终路径
print=$d.base_path=                打印当前种子的实际路径
print=$d.name=                     打印种子名

throttle.global_up.max_rate.set_kb=1000     全局上传限速（KB/s，0 表示不限）
throttle.global_down.max_rate.set_kb=5000   全局下载限速
session.save=                      立即保存会话</pre><p></p>
<p><code>print=</code> 的输出会显示在界面最底部那一行，想确认某个值的时候特别好用。</p>
<h3>~/.rtorrent.rc 里常用的几行</h3>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">directory.default.set = /data/torrents     # 默认下载目录
session.path.set = ~/.session              # 会话目录，存种子状态，重启不丢
schedule2 = watch_directory,5,5,load.start=/data/watch/*.torrent
                                           # 监视目录：往里丢 .torrent 就自动开始下载</pre><p></p>
<p>session 目录一定要配，否则 rtorrent 一重启，所有种子都没了。watch 目录也很方便，配合 Samba 或者 Syncthing，在别的电脑上把种子文件往里一扔就行。</p><p>The post <a href="https://luy.li/2026/09/25/rtorrent-after-mv/">rtorrent 挪文件后续种那些事</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/09/25/rtorrent-after-mv/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>《一句顶一万句》读书笔记</title>
		<link>https://luy.li/2026/09/13/978-7-5354-3976-5/</link>
					<comments>https://luy.li/2026/09/13/978-7-5354-3976-5/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sun, 13 Sep 2026 03:55:47 +0000</pubDate>
				<category><![CDATA[读书笔记]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2597</guid>

					<description><![CDATA[<p>最近，我读的书是《一句顶一万句》。读这本书，不是因为朋友给我介绍说这本书好看，也不是因为这书得了很多奖非常畅销 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/09/13/978-7-5354-3976-5/">《一句顶一万句》读书笔记</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>最近，我读的书是《一句顶一万句》。读这本书，不是因为朋友给我介绍说这本书好看，也不是因为这书得了很多奖非常畅销，而是因为刘震云老师的江湖地位近几年特别高，很难让人不注意到他。所以我就找了一本刘老师的书来读。</p>
<p>这个开头，是不是还挺符合这本书的写作风格的？哈哈。<br />
<span id="more-2597"></span></p>
<p>在这个注意力高度分散的年代，各种短视频平台上，常常会有刘震云这种文人参加综艺的切片。起的标题往往是：刘震云怒怼XXX，点进去就会发现，文化人怼人确实厉害，不动声色地怼得对方哑口无言。而他说话的那种不慌不忙地、慢吞吞地感觉，就和读他的书的感觉是一致的。</p>
<p>这本书里，也大多都是短句，用词也并不深奥偏僻，甚至有点朴实。对话描写经常是说一句话，“又说”。。。“又说”。。。每句也都是短句。</p>
<p>但他就是用这种风格，写出了<strong>两个时代的同一种落寞</strong>。上部我认为大概是发生在民国时期后期或者刚建国时期的故事，下部却大概是世纪交接的年代。虽然时代不同，但主人公确是有一丝丝联系的，而且人物的关系网也都是最底层的劳动人民。可以看出，作者确实也是底层出身，观察非常细致，积累也非常深厚。</p>
<p>总之，在我看来这书挺艺术的。但一句顶一万句，那句话到底是什么话，直到最后也都没有说，突然就结束了，就有点难受。。。不过我也理解这是一种写作手法，哈哈！</p>
<p>剧情就不展开说了，不然算剧透，也不太好。听说有部同名电影，是刘震云担任编剧，还是他女儿执导的，我这就去找来看看。</p><p>The post <a href="https://luy.li/2026/09/13/978-7-5354-3976-5/">《一句顶一万句》读书笔记</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/09/13/978-7-5354-3976-5/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>给 Claude 的 5 小时窗口掐个点</title>
		<link>https://luy.li/2026/08/30/claude-5h/</link>
					<comments>https://luy.li/2026/08/30/claude-5h/#respond</comments>
		
		<dc:creator><![CDATA[bones7456]]></dc:creator>
		<pubDate>Sun, 30 Aug 2026 08:16:33 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[经验技巧]]></category>
		<guid isPermaLink="false">https://luy.li/?p=2592</guid>

					<description><![CDATA[<p>用 Claude Code 的都知道，额度是按 5 小时一个窗口算的，而这个窗口的起点，是你某个周期内发出的第 [&#8230;]</p>
<p>The post <a href="https://luy.li/2026/08/30/claude-5h/">给 Claude 的 5 小时窗口掐个点</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>用 <a href="https://claude.com/claude-code">Claude Code</a> 的都知道，额度是按 5 小时一个窗口算的，而这个窗口的起点，是你某个周期内发出的第一条消息。</p>
<p>为了充分利用这些窗口，我就想让这几个窗口卡在我自己定的点上——5:58、10:59、16:00、21:01，一天四个，首尾正好接得上。</p>
<p>办法也很土：到点了自动发一句 hello 过去，窗口就开了。但我平时是在MacBook和Mac mini上跑claude，&#8221;到点自动跑&#8221;这件事，在 macOS 上还真有个坑，值得写一篇，分享给有需要的朋友。<br />
<span id="more-2592"></span></p>
<h2>别用 cron，用 launchd</h2>
<p>第一反应肯定是 crontab，写四行就完事。但 Mac 是会睡觉的。cron 在机器睡眠期间错过的任务，醒来之后不会补——那一条 hello 就这么静悄悄地没了，窗口也就没开成。</p>
<p>launchd 不一样。<code>StartCalendarInterval</code> 错过的时间点，会在唤醒后立刻补执行一次。对&#8221;激活窗口&#8221;这个目的来说，这个行为正合适。</p>
<p>而且四个时间点不用建四个 plist，<code>StartCalendarInterval</code> 直接收一个数组：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">&lt;key&gt;StartCalendarInterval&lt;/key&gt;
    &lt;array&gt;
        &lt;dict&gt;
            &lt;key&gt;Hour&lt;/key&gt;&lt;integer&gt;5&lt;/integer&gt;
            &lt;key&gt;Minute&lt;/key&gt;&lt;integer&gt;58&lt;/integer&gt;
        &lt;/dict&gt;
        &lt;dict&gt;
            &lt;key&gt;Hour&lt;/key&gt;&lt;integer&gt;10&lt;/integer&gt;
            &lt;key&gt;Minute&lt;/key&gt;&lt;integer&gt;59&lt;/integer&gt;
        &lt;/dict&gt;
        &lt;dict&gt;
            &lt;key&gt;Hour&lt;/key&gt;&lt;integer&gt;16&lt;/integer&gt;
            &lt;key&gt;Minute&lt;/key&gt;&lt;integer&gt;0&lt;/integer&gt;
        &lt;/dict&gt;
        &lt;dict&gt;
            &lt;key&gt;Hour&lt;/key&gt;&lt;integer&gt;21&lt;/integer&gt;
            &lt;key&gt;Minute&lt;/key&gt;&lt;integer&gt;1&lt;/integer&gt;
        &lt;/dict&gt;
    &lt;/array&gt;</pre><p></p>
<p>plist 放 <code>~/Library/LaunchAgents/</code>，然后 load 一下就生效：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">plutil -lint ~/Library/LaunchAgents/com.lly.claude-keepalive.plist
launchctl load ~/Library/LaunchAgents/com.lly.claude-keepalive.plist
launchctl list | grep claude-keepalive</pre><p></p>
<p>最后那行输出里，中间那个数字是上次的退出码，是 0 就说明跑通了。plist 的其它字段没什么特别的，<a href="https://www.launchd.info/">launchd.info</a> 上都有，照抄即可。</p>
<h2>脚本本身：记得用 haiku</h2>
<p>被调起来的脚本没什么内容，核心就一行：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">&quot;$CLAUDE&quot; -p &quot;hello&quot; --model haiku &gt;&gt; &quot;$LOG&quot; 2&gt;&amp;1</pre><p></p>
<p>这里的 <code>--model haiku</code> 是关键。5 小时窗口是账号级的，任何模型的第一条消息都会把它打开，那当然挑最便宜的来开——总不能为了开个窗口，先请 Opus 出来打个招呼吧，哈哈。</p>
<h2>几个注意点</h2>
<p>1. <b>路径必须写全。</b> launchd 拉起来的进程不走你的 shell 配置，<code>~/.local/bin</code> 根本不在默认 PATH 里，脚本里直接写 <code>claude</code> 是找不到的。要么在 plist 的 <code>EnvironmentVariables</code> 里把路径补上，要么脚本里写死绝对路径。我两个都做了，保险。</p>
<p>2. <b>补跑是把双刃剑。</b> 前面夸 launchd 会补跑，但反过来说：如果 5:58 的时候机器是睡着的，八点你才掀开盖子，那次补跑就发生在八点，窗口也就从八点开始算——精心排的那套首尾相接的节奏，当天就错位了。这个绕不过去，属于&#8221;别漏掉&#8221;和&#8221;要准时&#8221;之间必须二选一。</p>
<p>3. <b>日志记得轮转。</b> 一天四次，每次都往同一个文件里追加 Claude 的回复，攒几个月也挺可观。脚本开头加一句超过 1MB 就 mv 成 .1，两行的事儿。</p>
<h2>两天后才发现：其实一直在假装成功</h2>
<p>上线两天后回头翻日志，好家伙——除了第一次，后面十次全认证失败，报错是 <code>OAuth session expired and could not be refreshed</code>。但脚本每次都记着&#8221;退出码=0&#8243;，日志表面干干净净，窗口压根没开成也看不出来。</p>
<p>根子是 launchd 起的进程摸不到交互终端那份浏览器登录状态，用 <code>launchctl start</code> 手动触发一次就能稳定复现。解法是换成不依赖这份易失效登录的长期令牌：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">claude setup-token   # 生成一个 sk-ant-oat01-... 开头的长期令牌，存进 .zshrc ，据说这个令牌能管1年。</pre><p></p>
<p>因为我把令牌存在了 <code>.zshrc</code> 里，但 launchd 不会加载它，脚本里得自己挖出来：</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">eval &quot;$(grep '^export CLAUDE_CODE_OAUTH_TOKEN=' ~/.zshrc)&quot;</pre><p></p>
<p>一个教训：<b>退出码不能全信</b>，认证失败时它照样是 0。真要验证，就拿 <code>launchctl start</code> 手动戳一次，肉眼看日志内容。</p>
<h2>顺带</h2>
<p>这套东西是我让 Claude Code 自己写的——描述完需求，脚本、plist、load 命令一条龙全办了，中间它还顺手看了眼我已有的那个备份任务的 plist，把格式风格对齐了过来（<code>EnvironmentVariables</code> 那段的写法就是照抄我自己之前写的）。让 AI 给自己写定时唤醒脚本，这个循环还挺有意思的。</p><p>The post <a href="https://luy.li/2026/08/30/claude-5h/">给 Claude 的 5 小时窗口掐个点</a> first appeared on <a href="https://luy.li">I am LAZY bones?</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://luy.li/2026/08/30/claude-5h/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
