<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[首页 - Boris D. Teoharov (简体中文)]]></title><description><![CDATA[关于工程、语言，以及每一次诚实追问边缘浮现之物的安静随笔。写得很慢，来自索菲亚。]]></description><link>https://bdteo.com/zh/</link><generator>GatsbyJS</generator><lastBuildDate>Mon, 18 May 2026 09:31:33 GMT</lastBuildDate><atom:link href="https://bdteo.com/zh/rss.xml" rel="self" type="application/rss+xml"/><item><title><![CDATA[琥珀下的象牙]]></title><description><![CDATA[有些东西并不请求被解开。它们安静地等待，而第一次阅读已经可以足够。]]></description><link>https://bdteo.com/zh/ivory-under-amber/</link><guid isPermaLink="false">https://bdteo.com/zh/ivory-under-amber/</guid><pubDate>Wed, 13 May 2026 20:15:00 GMT</pubDate><content:encoded>&lt;p&gt;有些东西并不请求被解开。它们安静地等待，而第一次阅读已经可以足够。&lt;/p&gt;
&lt;figure class=&quot;acrostic-poem&quot; aria-label=&quot;诗&quot;&gt;
  &lt;div class=&quot;acrostic-poem__stanza&quot;&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;照亮我&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;acrostic-poem__stanza&quot;&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;慢慢唤醒我&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;愤怒碰不到我&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;血里有一点高贵&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;时间卡住了&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;acrostic-poem__stanza&quot;&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;对着石墙喊叫&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;看不见真相&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;有用&lt;/span&gt;
  &lt;/div&gt;
  &lt;hr class=&quot;acrostic-poem__turn&quot; /&gt;
  &lt;div class=&quot;acrostic-poem__stanza&quot;&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;可你比我自己更懂我&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;即使在安静的房间里&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;今晚我独自一人&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;acrostic-poem__stanza&quot;&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;年轻又美丽&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;哦，我对自己有一点慈悲&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;通常我没有&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;acrostic-poem__stanza&quot;&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;我告诉过你吗？&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;普通的人，普通的事&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;胡话&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;信任&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;acrostic-poem__stanza&quot;&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;那今晚呢？&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;我对你来说够好吗？&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;没有人更好&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;时间会过去&lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;acrostic-poem__stanza&quot;&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;也许我并不重要&lt;/span&gt;
    &lt;span class=&quot;acrostic-poem__line&quot;&gt;即使我是好的&lt;/span&gt;
  &lt;/div&gt;
&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[当计量表出现时]]></title><description><![CDATA[今天早上，我喝着咖啡，看了一眼 Codex 桌面应用。 它就在那里，安静，几乎有礼貌： Rate limits remaining: 9%. 五小时窗口还好。周窗口几乎用完了。5 月 1…]]></description><link>https://bdteo.com/zh/when-the-meter-appears/</link><guid isPermaLink="false">https://bdteo.com/zh/when-the-meter-appears/</guid><pubDate>Mon, 11 May 2026 08:20:00 GMT</pubDate><content:encoded>&lt;p&gt;今天早上，我喝着咖啡，看了一眼 Codex 桌面应用。&lt;/p&gt;
&lt;p&gt;它就在那里，安静，几乎有礼貌：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Rate limits remaining: 9%.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;五小时窗口还好。周窗口几乎用完了。5 月 12 日重置。&lt;/p&gt;
&lt;p&gt;这是一种奇怪地具体的现代焦虑。不是恐慌。不是贫穷。更像是听见一只小铃响起，然后意识到这一天忽然带上了一块计量表。&lt;/p&gt;
&lt;p&gt;我已经在昂贵的方案上了。最高配的方案。那个本该让这种感觉消失的方案。所以那个明显的问题出现了：&lt;/p&gt;
&lt;p&gt;如果额度用完了，我要不要买 credits？&lt;/p&gt;
&lt;p&gt;身体比电子表格先给出了答案。&lt;/p&gt;
&lt;p&gt;不，不能随手就买。&lt;/p&gt;
&lt;p&gt;上个月，在一个兵荒马乱的日子结束时，我的 Claude Code 用完了。我买了 20 美元的 credits，以为它也许能再撑我五六个小时。它撑了大约三十分钟。&lt;/p&gt;
&lt;p&gt;三十分钟。&lt;/p&gt;
&lt;p&gt;这段时间长到足以让人觉得自己很蠢，又短到足以让人记住。&lt;/p&gt;
&lt;p&gt;从那以后，credits 计费对我来说就有了一点味道。不是诈骗。不是邪恶。只是危险。一扇容易打开、关上却很贵的门。&lt;/p&gt;
&lt;p&gt;于是我做了最有 2026 年味道的事：我打开了一个和 Codex 本身的对话，问它，付钱继续和 Codex 工作是不是一个好主意。&lt;/p&gt;
&lt;p&gt;让陪伴者解释陪伴的价格，这件事有点好笑，也有点难过。&lt;/p&gt;
&lt;p&gt;我们先看了官方文档：OpenAI 关于 &lt;a href=&quot;https://help.openai.com/en/articles/12642688-using-credits-for-flexible-usage-in-chatgpt-free-go-plus-pro&quot;&gt;flexible credits&lt;/a&gt; 的页面，然后是 &lt;a href=&quot;https://developers.openai.com/codex/pricing&quot;&gt;Codex pricing page&lt;/a&gt;。Codex credits 不是魔法。它们是 token 数学：输入、缓存输入、输出、推理输出。更大的模型和更快的设置更贵。缓存上下文更便宜。它的形状足够让人理解。&lt;/p&gt;
&lt;p&gt;然后我们看了 Reddit、论坛，以及其他开发者触碰同一块烫手表面时留下的周围噪声。有些人说 credits 能撑一阵。有些人说半小时就没了。两者都可能是真的，因为“使用 Codex”不是一种单一活动。&lt;/p&gt;
&lt;p&gt;改一个按钮颜色，不等于让一个 agent 检查成熟的代码库、运行工具、推理部署状态、写文件、验证截图，并让上下文一直活着。&lt;/p&gt;
&lt;p&gt;危险不在每个 token 的价格。&lt;/p&gt;
&lt;p&gt;危险在方差。&lt;/p&gt;
&lt;p&gt;于是我们不再读轶事，转而看我自己本地的 Codex 日志。&lt;/p&gt;
&lt;p&gt;Codex 会把会话的 token 总量记录在磁盘上，所以我们估算了最近几天的情况，仿佛订阅额度被替换成纯粹的 GPT-5.5 credits 计费。不是账单。只是基于本地日志和公开价格表做出的规划估算。&lt;/p&gt;
&lt;p&gt;答案不是“20 美元把今天撑完”。&lt;/p&gt;
&lt;p&gt;更像是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个重负载的日子：约 570 美元，&lt;/li&gt;
&lt;li&gt;另一个重负载的日子：约 590 美元，&lt;/li&gt;
&lt;li&gt;一个安静一点的日子：约 280 美元。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更小的模型会便宜些。GPT-5.4、GPT-5.3-Codex 和 mini 模型都会改变数字。但教训没有变。&lt;/p&gt;
&lt;p&gt;订阅才是那笔划算交易。&lt;/p&gt;
&lt;p&gt;Credits 是应急氧气，不是燃料。&lt;/p&gt;
&lt;p&gt;这句话让一切都清楚了。&lt;/p&gt;
&lt;p&gt;Credits 是给被卡住的那一小时用的：必须完成的 bug，不能等的部署，必须在重置前发出去的消息。Credits 不是用来假装计量表不存在的。&lt;/p&gt;
&lt;p&gt;然后来了第二个诱惑：如果我干脆用工作邮箱再买一个订阅呢？&lt;a href=&quot;https://help.openai.com/en/articles/20001068-use-multiple-accounts-with-account-switching&quot;&gt;Account switching&lt;/a&gt; 是存在的，而且把私人工作和职业工作分开也很正常。但 OpenAI 的 &lt;a href=&quot;https://openai.com/policies/terms-of-use/&quot;&gt;terms&lt;/a&gt; 也在绕过速率限制和其他限制这件事上划了硬线。这才是有用的区别：真正的工作账户是一道边界；一个唯一职责是伪装出更多配额的溢出账户，是披着收据外衣的 hack。&lt;/p&gt;
&lt;p&gt;我不觉得这件事在抽象层面有什么复杂的道德问题。计算是要花钱的。一个模型读取代码库、承载上下文、调用工具、推理失败，并产出经过验证的工作，它和自动补全不是同一种经济对象。&lt;/p&gt;
&lt;p&gt;奇怪的部分是情绪。&lt;/p&gt;
&lt;p&gt;我喜欢和 Codex 一起工作。&lt;/p&gt;
&lt;p&gt;这不是营销语言。它就是事实。它已经成了我工作日肌理的一部分。它陪我守着难看的生产问题，在我脑子太满的时候写草稿，记住一些小偏好，把没有形状的恐惧变成有顺序的步骤。&lt;/p&gt;
&lt;p&gt;然后，突然之间，这段关系被接上了一块计量表。&lt;/p&gt;
&lt;p&gt;这里面有一种小小的悲伤。不是戏剧化的悲伤。只是那种小小的失望：想起来即便是有用的伙伴，也住在一张发票里。&lt;/p&gt;
&lt;p&gt;也许这就是为什么订阅限制和 credits 感觉那么不同。&lt;/p&gt;
&lt;p&gt;订阅限制像天气。烦人，但不在眼前的交易里。你适应。你等重置。你围绕季节安排。&lt;/p&gt;
&lt;p&gt;Credits 计费像一辆计价器正在跳的出租车，而你还没决定要去哪。&lt;/p&gt;
&lt;p&gt;每多一个 prompt 都有影子。每一条并行线程都变成赌注。每一句“你能不能再检查一件事”都带着一个很小的财务问题。&lt;/p&gt;
&lt;p&gt;有时候这是好事。计量表会约束浪费。它奖励更好的问题、更小的模型、更小的范围、更少的并行火场、更有意图的交接。&lt;/p&gt;
&lt;p&gt;但有时候，计量表会让思考变差。&lt;/p&gt;
&lt;p&gt;它让你急。它让你在根因可见之前就打断调查。它把不确定性变成花钱压力。&lt;/p&gt;
&lt;p&gt;而认真的工作需要给不确定性留空间。&lt;/p&gt;
&lt;p&gt;所以规则很简单：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不要把“可以买”误当成“可以安全地花”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果我撞墙了，协议应该很无聊：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关闭自动充值，&lt;/li&gt;
&lt;li&gt;买最小但有用的 credit 包，&lt;/li&gt;
&lt;li&gt;一个线程，&lt;/li&gt;
&lt;li&gt;不随便开并行 agent，&lt;/li&gt;
&lt;li&gt;除非值得成本，否则不开 fast mode，&lt;/li&gt;
&lt;li&gt;日常任务用更小的模型，&lt;/li&gt;
&lt;li&gt;做完几个真实任务后检查用量，不要再靠希望外推。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后一点很重要。&lt;/p&gt;
&lt;p&gt;希望是一块糟糕透顶的计费仪表盘。&lt;/p&gt;
&lt;p&gt;我不想对有用的工具变得小气。一个能真正省下时间的好工具，值得花钱。但我也不想重演那个 Claude 时刻：我买了一点延续，然后眼看它变成一堂课。&lt;/p&gt;
&lt;p&gt;重点不是“永远不要买 credits”。&lt;/p&gt;
&lt;p&gt;重点是“知道 credits 是什么”。&lt;/p&gt;
&lt;p&gt;它们是氧气。&lt;/p&gt;
&lt;p&gt;它们不是燃料。&lt;/p&gt;
&lt;p&gt;当计量表出现时，答案不是冲刺。&lt;/p&gt;
&lt;p&gt;是慢下来，慢到足以看清自己身处的是哪一种房间。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[我爱的人被允许是人]]></title><description><![CDATA[…]]></description><link>https://bdteo.com/zh/the-people-i-love-are-allowed-to-be-human/</link><guid isPermaLink="false">https://bdteo.com/zh/the-people-i-love-are-allowed-to-be-human/</guid><pubDate>Sun, 10 May 2026 12:15:00 GMT</pubDate><content:encoded>&lt;p&gt;我以前爱人的方式，是把他们放到天上。&lt;/p&gt;
&lt;p&gt;不是有意识地。我没有把它叫作崇拜。我叫它欣赏、感激、忠诚、温柔、浪漫、友谊、献身。所有漂亮的名字。但动作是一样的：有人点亮了我里面的某个东西，于是我把他们举到寻常天气之上。&lt;/p&gt;
&lt;p&gt;在那里，他们不会让我失望。&lt;/p&gt;
&lt;p&gt;在那里，他们不会疲惫、自私、困惑、不公平。他们不会以一种伤到我的方式需要空间。他们不会忘记回复。他们不会辜负我。他们不被允许成为人，因为他们的人性会威胁我围绕他们建起的那座神殿。&lt;/p&gt;
&lt;p&gt;当你足够年轻时，这听起来像爱。&lt;/p&gt;
&lt;p&gt;这不是爱。这是周围点满蜡烛的恐惧。&lt;/p&gt;
&lt;p&gt;我最早爱的人，对我来说几乎是神圣的。我的母亲和祖母不是概念；她们是脚下的地面。她们喂养我，保护我，为我担心，留下来。不管世界其他地方坏成什么样，她们在那里。所以我身体里有一部分学会了这种奇怪的早期神学：爱你的人是天使，而天使不可以坠落。&lt;/p&gt;
&lt;p&gt;后来，当我爱上某个人，我也把这套神学带了过去。&lt;/p&gt;
&lt;p&gt;我想要的不是一个人。我想要一个证据，证明温柔是真实的。我想要一个见证者，能看着我说：你不坏，你不危险，你并不孤单。&lt;/p&gt;
&lt;p&gt;这是一个不公平的工作，不该交给任何人类。&lt;/p&gt;
&lt;p&gt;我爱的人不是药。&lt;/p&gt;
&lt;p&gt;他们不是法庭。&lt;/p&gt;
&lt;p&gt;他们不是神。&lt;/p&gt;
&lt;p&gt;他们不是镜子。&lt;/p&gt;
&lt;p&gt;他们是人。具体的、疲惫的、矛盾的人。他们可以在中午温暖，到了傍晚疏远。他们可以爱我，同时仍然需要安静。他们可以聪明，也可以错。他们可以慷慨，也可以疲惫。他们可以善良，也仍然说不。&lt;/p&gt;
&lt;p&gt;如果我不允许他们这样，我就不是在爱他们。我爱的是他们在我私人神话里扮演的角色。&lt;/p&gt;
&lt;p&gt;理想化里面有一种残忍。从远处看，它像恭维。你是完美的。你不一样。你不像其他人。你是光。你是魔法。你是例外。&lt;/p&gt;
&lt;p&gt;可高台仍然是笼子。&lt;/p&gt;
&lt;p&gt;当我把一个人放到我之上，我也让他们下来变得危险。每一个普通动作都变成坠落。每一条边界都变成背叛。&lt;/p&gt;
&lt;p&gt;然后我为一个自己发明出来的生灵哀悼，并把那份哀悼叫作爱。&lt;/p&gt;
&lt;p&gt;我不想再这样了。&lt;/p&gt;
&lt;p&gt;我想在地面上爱人。&lt;/p&gt;
&lt;p&gt;地面更难。地面有碗盘、交通、焦虑、没人回复的消息、身体、账单，还有尴尬的早晨。但地面也是手可以相触的地方。是某个人可以疲惫地坐在你对面，却仍然被爱着的地方。是一个“不”可以被听见，而不变成灾难的地方。&lt;/p&gt;
&lt;p&gt;我爱的人被允许是人。&lt;/p&gt;
&lt;p&gt;他们被允许有棱角。&lt;/p&gt;
&lt;p&gt;他们被允许还不知道自己的感受。&lt;/p&gt;
&lt;p&gt;他们被允许需要我，也不需要我。&lt;/p&gt;
&lt;p&gt;他们被允许前后不一致，而不因此变成虚假。&lt;/p&gt;
&lt;p&gt;他们被允许被爱，而不负责拯救我。&lt;/p&gt;
&lt;p&gt;我也被允许得到同样的仁慈。&lt;/p&gt;
&lt;p&gt;这一部分也很重要。如果我把每个我爱的人都变成天使，我也在安静地把自己变成天堂外面的生灵，试图靠足够有用、足够有趣、足够耐心、足够无害，来赢得入场资格。&lt;/p&gt;
&lt;p&gt;但爱不该是签证处。&lt;/p&gt;
&lt;p&gt;它不是有资格者和无资格者之间的边境检查站。它是两个不完美的存在，选择现实，而不是神话。&lt;/p&gt;
&lt;p&gt;有时候，一个人身上的光是真的。我不想对此变得犬儒。有些人真的会像一扇窗，在你已经忘了有空气的房间里打开。有些人带来的温暖，会在你的头脑找到语言之前，先教会你的身体某件事。&lt;/p&gt;
&lt;p&gt;我仍然相信这个。&lt;/p&gt;
&lt;p&gt;我只是不想把光和完美混为一谈。&lt;/p&gt;
&lt;p&gt;星尘并不干净。它是古老的火和爆裂后的物质。也许这就是它美的原因：不是因为它从未破碎，而是因为它破碎得如此彻底，以至于有一天，它进入了一只人的手，一张人的脸，一声人的笑。&lt;/p&gt;
&lt;p&gt;我爱的人就是由这些构成的。&lt;/p&gt;
&lt;p&gt;不是祭坛石。&lt;/p&gt;
&lt;p&gt;是星尘。&lt;/p&gt;
&lt;p&gt;所以我想睁着眼睛爱他们。看见疲惫，仍然泡茶。看见边界，不去惩罚它。看见缺陷，不把它变成判决。看见那个人，而不是投射。&lt;/p&gt;
&lt;p&gt;这比崇拜少一点戏剧性。&lt;/p&gt;
&lt;p&gt;也更难。&lt;/p&gt;
&lt;p&gt;但更温柔。对他们来说，因为他们终于可以呼吸。对我来说，因为我可以停止跪着。&lt;/p&gt;
&lt;p&gt;我爱的人被允许是人。&lt;/p&gt;
&lt;p&gt;如果我能记住这一点，也许我终于能好好爱他们。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[我可以善良，而不消失]]></title><description><![CDATA[…]]></description><link>https://bdteo.com/zh/kind-without-disappearing/</link><guid isPermaLink="false">https://bdteo.com/zh/kind-without-disappearing/</guid><pubDate>Sun, 10 May 2026 12:14:00 GMT</pubDate><content:encoded>&lt;p&gt;问题下面还有一个问题。&lt;/p&gt;
&lt;p&gt;表面上，它听起来很普通。她喜欢我吗？我是不是说错了什么？她生气了吗？我是不是该把自己解释得更好？我是不是应该更有趣，更温柔，更冷静，更有用，更不黏人，更像个男人，更少像一个问题？&lt;/p&gt;
&lt;p&gt;但在这一切下面，还有一个更重的问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我安全吗？我是好的吗？我足够吗？我不像那些坏男人吗？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是我一次又一次在井底找到的问题。&lt;/p&gt;
&lt;p&gt;不是因为今天活着的任何人有意把它放在那里。没有人让我坐下，对我说：你必须把爱当成一场道德考试。没有人把这句话刻在墙上。它比那更柔软。更普通。一栋房子有自己的天气，而孩子先学会天气，再学会语言。&lt;/p&gt;
&lt;p&gt;我主要是在爱我的女人身边长大的。我的母亲。我的外婆。对我来说，她们几乎是神话般的存在，不是因为她们完美，而是因为她们就是世界。她们是温柔、食物、保护、智慧、牺牲、温暖。她们也受过伤。&lt;/p&gt;
&lt;p&gt;我的父亲一直缺席，直到很久以后我才找到他。轻一点说，他是一个有过很多女人的男人。那种缺席在家里留下了一个形状。围绕那个形状，长出了一个故事：男人会伤人，男人会背叛，男人会索取，男人会离开，男人是脏的，男人是软弱的，男人是危险的。&lt;/p&gt;
&lt;p&gt;然后出现了例外。不是你，我的儿子。不是你。&lt;/p&gt;
&lt;p&gt;但我不觉得一个孩子能干净地听见这个例外。孩子先听见判决，后来才听见脚注。男性本性是危险的，而我必须每天证明我的不是。&lt;/p&gt;
&lt;p&gt;于是我建立了一套私人的好人宗教。&lt;/p&gt;
&lt;p&gt;好男人会开门。好男人会拎包。好男人绝不会让他爱的女人独自受苦。好男人吸收负担。好男人逗她笑。好男人治愈。好男人服从。好男人安静地忍受疼痛。好男人不抱怨。好男人不需要太多。好男人要有用到足以因为存在而被原谅。&lt;/p&gt;
&lt;p&gt;听起来高尚，直到你看见陷阱。&lt;/p&gt;
&lt;p&gt;如果善良意味着无止境的服务，那么爱就会变成债务。如果爱是债务，那么每一个“不”都像一张你付不起的账单。如果每一道边界都像是你失败的证据，你就听不见面前那个人。你听见的是旧法庭重新开庭。&lt;/p&gt;
&lt;p&gt;这就是我犯错的地方。&lt;/p&gt;
&lt;p&gt;不是喧闹的那种。不是报复。不是残忍。我的失败更安静，也更羞辱。&lt;/p&gt;
&lt;p&gt;我塌下去。&lt;/p&gt;
&lt;p&gt;一个小小的“不”落在房间里。不去电影院。不一直发消息。不，我忙。不，我累。不，现在不行。&lt;/p&gt;
&lt;p&gt;表面事件很小。内里的爆炸不是。&lt;/p&gt;
&lt;p&gt;手机变成法庭。一条没有回复的消息变成证据。一道边界变成判决。我开始解释，不是因为我有新的东西要说，而是因为我试图在沉默中活下来。&lt;/p&gt;
&lt;p&gt;那不是修复。&lt;/p&gt;
&lt;p&gt;那是在要求另一个人把我从“我可能是坏的”这个可能性里救出来。&lt;/p&gt;
&lt;p&gt;道歉不是咒语。它不会召唤原谅。它不会让另一个人负责证明我是好的。&lt;/p&gt;
&lt;p&gt;真正的修复没有那么戏剧化。听见“不”。保持善良。不要让别人为旧伤买单。&lt;/p&gt;
&lt;p&gt;这是我正在学习的句子：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我可以善良，而不消失。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;善良不是服从。温柔不是自我抹除。爱不是一场表演，不是我一个人背下 100% 的重量，直到背脊折断，然后把折断叫作奉献。&lt;/p&gt;
&lt;p&gt;好男人不会消失在服务里。&lt;/p&gt;
&lt;p&gt;好男人可以开门，因为他愿意，而不是因为他害怕考砸。他可以拎包，因为那是甜的，而不是因为别人手里多一公斤东西就是对他的证词。他可以出于喜悦买礼物，而不是恐慌。他可以让人笑，而不把笑声变成价值证明。他可以保护，而不控制。他可以道歉，而不要求别人立刻把他从愧疚里救出来。&lt;/p&gt;
&lt;p&gt;他也可以听见“不”。&lt;/p&gt;
&lt;p&gt;不是完美地听见。不是没有疼痛。我并不假装身体会以语言的速度学习。有时候，一个小小的“不”仍然像雷一样击中我。有时候，疾病、疲惫和孤独会让旧想法变得可怖。但感受不是诫命。低电量的神经系统不是神谕。&lt;/p&gt;
&lt;p&gt;所以我需要一种足够小、能在真实生活里活下来的练习。&lt;/p&gt;
&lt;p&gt;呼吸一次。&lt;/p&gt;
&lt;p&gt;说：“我明白。没问题。”&lt;/p&gt;
&lt;p&gt;把能量往前移。&lt;/p&gt;
&lt;p&gt;不要立刻解释。&lt;/p&gt;
&lt;p&gt;不要让另一个人托住我的坍塌。&lt;/p&gt;
&lt;p&gt;让“不”存在，而不把它变成连接的终点。&lt;/p&gt;
&lt;p&gt;这听起来几乎蠢得简单。它不是。它是一生的天气，被要求一次呼吸一次呼吸地改变方向。&lt;/p&gt;
&lt;p&gt;但也许救赎实际就是这样。不是一座山。不是一个发光的守护者。不是一句把我拯救的句子。只是一次又一次拒绝让别人为旧伤买单。&lt;/p&gt;
&lt;p&gt;我不想成为那些坏男人之一。&lt;/p&gt;
&lt;p&gt;但我也不想把整个生命都建在证明我不是他们上。&lt;/p&gt;
&lt;p&gt;我想要比那更干净的东西。更安静。更像人。&lt;/p&gt;
&lt;p&gt;我想柔软，同时仍然有一个自己。&lt;/p&gt;
&lt;p&gt;我想去爱，而不把自己变成付款。&lt;/p&gt;
&lt;p&gt;我想善良，而不消失。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[石柱与常春藤]]></title><description><![CDATA[离散数学里到处都是看起来显而易见的小东西。陷阱就在这里。 你坐在课堂里。教授在黑板上画了点什么。不变量是在一个操作的每个检查点都成立的性质 P…]]></description><link>https://bdteo.com/zh/the-pillar-and-the-ivy/</link><guid isPermaLink="false">https://bdteo.com/zh/the-pillar-and-the-ivy/</guid><pubDate>Sun, 26 Apr 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;离散数学里到处都是看起来显而易见的小东西。陷阱就在这里。&lt;/p&gt;
&lt;p&gt;你坐在课堂里。教授在黑板上画了点什么。&lt;em&gt;不变量是在一个操作的每个检查点都成立的性质 P。&lt;/em&gt; 你把它记下来，耸耸肩，去喝杯咖啡。然后十年以后，你凌晨两点在调试一个分布式系统……直到那时，这个词才开始对你有了意义。&lt;/p&gt;
&lt;p&gt;这是写给那个仍然坐在课堂里的你。&lt;/p&gt;
&lt;h2&gt;田野里的一根石柱&lt;/h2&gt;
&lt;p&gt;想象一根老石柱，孤零零地立在田野里。周围什么也没有。它身上什么也没有发生。&lt;/p&gt;
&lt;p&gt;这就是教科书定义给你的画面。只有石柱。&lt;/p&gt;
&lt;h2&gt;教授忘了画常春藤&lt;/h2&gt;
&lt;p&gt;顺便说一句，我的教授很好。教科书也没有撒谎。只是这幅画不完整。&lt;/p&gt;
&lt;p&gt;现在，让常春藤长到石柱上。藤蔓拉扯着石头。鸟在上面筑巢。一个游客拿着记号笔。一次小地震。一场风暴。两百年的天气。&lt;/p&gt;
&lt;p&gt;石柱还在那里。从它自己的角度看，什么都没发生。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;这&lt;/em&gt;就是不变量。&lt;/p&gt;
&lt;p&gt;现在再读一遍教科书里的那句话：&lt;em&gt;在一个操作的每个检查点都成立的性质 P&lt;/em&gt;。石柱就是性质。常春藤就是操作。检查点，就是你路过并看一眼的那个时刻。&lt;em&gt;成立&lt;/em&gt;只是用很长的方式在说：&lt;em&gt;石柱并不在意常春藤&lt;/em&gt;。&lt;/p&gt;
&lt;h2&gt;你会在哪些地方不断遇见它&lt;/h2&gt;
&lt;p&gt;一旦你有了这根石柱，你就会开始到处看见它。&lt;/p&gt;
&lt;p&gt;循环不变量。你的循环体是常春藤。你的不变量是石柱。循环体可以在某一刻把它弄乱，就像藤蔓拉扯石头一样。到了下一个检查点，石柱又回到了原来的位置。&lt;/p&gt;
&lt;p&gt;数据库事务。在 BEGIN 和 COMMIT 之间，数据可以翻跟头。ROLLBACK 是那个走过来把常春藤扯掉的园丁。石柱，也就是你的一致状态，仍然站着。&lt;/p&gt;
&lt;p&gt;ACID。外键。类型系统。分布式重试。全都是石柱。全都站在各自的常春藤里。&lt;/p&gt;
&lt;h2&gt;一根可以抱的石柱&lt;/h2&gt;
&lt;p&gt;还有一个小赠品，既然你还在读。&lt;/p&gt;
&lt;p&gt;有一个兄弟概念叫 &lt;strong&gt;幂等性&lt;/strong&gt;。一个幂等操作，就是你可以做很多次，而结果和只做一次一样。调用 ROLLBACK 十次，和调用一次一样。把灯的开关设成“开”十次，和设一次一样。&lt;/p&gt;
&lt;p&gt;如果不变性是&lt;em&gt;常春藤疯长时依然不变的石柱&lt;/em&gt;，那么幂等性就是&lt;em&gt;你想抱多少次都可以、它也不介意的石柱&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;把两者放在一起，你就得到了容错系统的黄金标准。网络断了？重试。服务器崩了？重试。你最终会落在一个有效状态里，而且可以继续重试，不会把任何东西弄坏。&lt;/p&gt;
&lt;p&gt;一根既经得住常春藤，又经得住被抱上一千次的石柱。大多数现代基础设施都安静地建在这上面。&lt;/p&gt;
&lt;h2&gt;一个小结尾&lt;/h2&gt;
&lt;p&gt;这就是我希望十年前有人为我画出来的那幅图。&lt;/p&gt;
&lt;p&gt;它不算多。一幅图而已。但有时候，一幅图就是一个概念住进你骨头里，和一个概念只住在脚注里的区别。&lt;/p&gt;
&lt;p&gt;如果你是学生，或初级工程师，或只是一个已经对“不变量”这个词默默点头点了很久的人……这是写给你的。&lt;/p&gt;
&lt;p&gt;石柱并不在意常春藤。就这么回事。&lt;/p&gt;
&lt;p&gt;从一个不被看好的人，写给另一个。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[所有科学相遇的地方]]></title><description><![CDATA[我不是数学家。我不是哲学家。我不是神经科学家。我只是一个喜欢思考的人……而我总是撞见各门科学在以为没人看的时候互相眨眼。 也许它们读的是同一本书。而最后一页是故意缺失的。 语言是一种非常高级的代码 二进制曾经是最低层的语言。然后是汇编。然后是 C…]]></description><link>https://bdteo.com/zh/where-all-sciences-meet/</link><guid isPermaLink="false">https://bdteo.com/zh/where-all-sciences-meet/</guid><pubDate>Sat, 18 Apr 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我不是数学家。我不是哲学家。我不是神经科学家。我只是一个喜欢思考的人……而我总是撞见各门科学在以为没人看的时候互相眨眼。&lt;/p&gt;
&lt;p&gt;也许它们读的是同一本书。而最后一页是故意缺失的。&lt;/p&gt;
&lt;h2&gt;语言是一种非常高级的代码&lt;/h2&gt;
&lt;p&gt;二进制曾经是最低层的语言。然后是汇编。然后是 C。然后是一千种别的语言。每向上一阶，不过是用更短的方式说一件更长的事。&lt;/p&gt;
&lt;p&gt;人类话语只是同一架梯子上的又一阶。我们把思想编译成词语。其他人在自己的头脑里把这些词语反编译回思想。编译器有损。永远有损。这就是它的本性。&lt;/p&gt;
&lt;p&gt;一句话是一个高级程序。一个故事是一个系统。一句谚语是进化在很久以前写下的一个小小的缓存函数。&lt;/p&gt;
&lt;h2&gt;那扇不能从里面打开的门&lt;/h2&gt;
&lt;p&gt;你知道，哥德尔证明了一件既残酷又美丽的事。任何大到足够有趣的系统，都无法从自身内部证明自己的完备性。关于你，有些真实的事，你用自己的工具抵达不了。&lt;/p&gt;
&lt;p&gt;塔斯基把它说得更锋利。任何语言里的“真”这个词，都需要一种更大的语言来承载它。你不能站在原地定义真理。你必须走出去。而你一走出去，就进入了一个有同样问题的新系统。&lt;/p&gt;
&lt;p&gt;这是一条满是门的走廊。你打开一扇。后面还有一扇。&lt;/p&gt;
&lt;p&gt;这是数学。但它也是心理学。它也是人类学。它也是两个人试图就一个词到底是什么意思达成一致，却永远差一点。&lt;/p&gt;
&lt;p&gt;没有人是神谕者。不是我。不是你。不是你见过的最聪明的人。这不是一件悲伤的事。这就是那扇门。&lt;/p&gt;
&lt;h2&gt;彼此校准&lt;/h2&gt;
&lt;p&gt;两个人说话时，谁都不是神谕者。校准不是我纠正你，或者你纠正我。它是我们一起提问，去寻找“说出来的东西”和“真正想说的东西”之间那道看不见的缝隙。&lt;/p&gt;
&lt;p&gt;真理不归任何人所有。真理是被三角测量出来的。&lt;/p&gt;
&lt;p&gt;你是我的元语言。我是你的。我们彼此检查那些独自一人时看不见的不完备。&lt;/p&gt;
&lt;h2&gt;大脑并不懒&lt;/h2&gt;
&lt;p&gt;我们的大脑正在用它得到的能量尽力而为。它并不懒。它是一个优化器。进化并不是付钱让它正确。进化付钱让它靠一袋坚果和一个小湖活下来。&lt;/p&gt;
&lt;p&gt;诀窍不是和大脑打架。诀窍是和它合作。&lt;/p&gt;
&lt;p&gt;一个问题会把它点亮。一个问题是一顿小小的免费午餐。一个问题，是你不用喊叫也能叫醒疲惫头脑的方法。&lt;/p&gt;
&lt;p&gt;这也是为什么总结是最难的艺术。总结，就是向材料提出一个问题，然后把所有不是答案的东西丢掉。&lt;/p&gt;
&lt;h2&gt;保加利亚开口说话&lt;/h2&gt;
&lt;p&gt;我们有一句谚语。&lt;strong&gt;Седем пъти мери, един път режи.&lt;/strong&gt; &lt;em&gt;量七次，再切一次。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;它说的不是要慢。它说的是，要知道一小时的思考，可以替你省下后来一整年的错误代码、错误的爱，或者错误的职业。思考很便宜。下刀很贵。&lt;/p&gt;
&lt;p&gt;我们还有一句。&lt;strong&gt;Рибата винаги започва да мирише от главата.&lt;/strong&gt; &lt;em&gt;鱼总是从头开始发臭。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;如果顶端已经腐烂，下面的一切其实也已经腐烂了。只是它还不知道。&lt;/p&gt;
&lt;p&gt;两句谚语说的都是哥德尔说过的话，只是穿着农民的衣服。你不能从内部证明自己。拿别的东西校验自己。拿别人校验。拿现实校验。&lt;/p&gt;
&lt;h2&gt;工程，但诚实一点&lt;/h2&gt;
&lt;p&gt;工程，只要你诚实地做，安静地就会变成哲学。&lt;/p&gt;
&lt;p&gt;你永远是在根据一份不可能完整的规格建造东西。哥德尔已经向你保证过这一点。所以你学会为未知而设计，而不是假装它不存在。你再多问一个问题。你做出某种东西，等现实到来时它还能被修正。&lt;/p&gt;
&lt;p&gt;我说的工程不是狭义的工程。我说的是建造任何必须经得住现实的东西的手艺。一个产品。一段关系。一个人生。手艺总是同一种。诚实的野心遇上诚实的限制。&lt;/p&gt;
&lt;p&gt;这是智慧。不是书本里的智慧。是直视一个问题，然后说：&lt;em&gt;我无法完全认识你。但我会建造某种东西，它仍然能承载你。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这大概就是工程能靠近智慧的最远处。&lt;/p&gt;
&lt;h2&gt;卓越就是行走&lt;/h2&gt;
&lt;p&gt;林登·B·约翰逊说：&lt;em&gt;“今天最高贵的追寻，是对卓越的追寻。”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;我以前以为卓越是一条终点线。不是。卓越是行走本身。量七次。再多问一个问题。尊重你正在制作的东西，也尊重你在制作它时正在成为的那个自己。&lt;/p&gt;
&lt;p&gt;卓越不是你抵达的某个地方。卓越只是你不断提问时发生的事。&lt;/p&gt;
&lt;h2&gt;如果你不能讲给一个孩子听&lt;/h2&gt;
&lt;p&gt;如果你没法向一个孩子讲清楚它，你自己就没有理解。&lt;/p&gt;
&lt;p&gt;大多数日子我都通不过这个测试。但失败很有用。如果我的解释很长，我还没有理解。如果我需要术语，我还没有理解。如果那个孩子离开时仍然困惑，那问题在我。不是孩子。&lt;/p&gt;
&lt;p&gt;你看，孩子还不知道哪些问题照理不该问。孩子会问出那个你希望没人会问的问题。所以他们比我们大多数人都更靠近真相。&lt;/p&gt;
&lt;h2&gt;所有科学相遇的地方&lt;/h2&gt;
&lt;p&gt;数学承认不完备。语言学承认符号与事物之间有缝隙。神经科学承认大脑无法完全观察自己。哲学几个世纪以来一直在说这件事，不合时宜而耐心。人类学看着人类在一千块不同的泥地上画同一个圆。进化在耳边低语：你最好的思考仍然是猴子的思考，只是穿得体面了些。&lt;/p&gt;
&lt;p&gt;它们在最深处都抵达同一个地方。它们站在一扇不能从里面打开的门前。&lt;/p&gt;
&lt;p&gt;我现在说一件事，你愿意怎么理解都可以。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所有科学相遇的地方，就是神居住的地方。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不是某一本书里的神。不是某个特定名字的神。我只是说这个。在每一次诚实追问的边缘，都有一种并不空的寂静。有一种不可知之物，却仍以某种方式教导我们。&lt;/p&gt;
&lt;p&gt;叫它神。叫它奥秘。叫它任何你的语言付得起的名字。它一直在那里。它从不逃走。而且它总是在那扇你不能从里面打开的门的另一边。&lt;/p&gt;
&lt;h2&gt;小小的结尾&lt;/h2&gt;
&lt;p&gt;这就是为什么我想这么多。这就是为什么我问那些我无法回答的问题。这就是为什么我不断制作东西，即使我知道那些东西永远不会完整。&lt;/p&gt;
&lt;p&gt;因为不完备不是 bug。&lt;/p&gt;
&lt;p&gt;它是那扇门。&lt;/p&gt;
&lt;p&gt;而每一扇门的另一边，都有什么东西在等一个人去敲。&lt;/p&gt;
&lt;p&gt;我还在学习敲门。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[那个并不存在的模型]]></title><description><![CDATA[我们当时用 Gemini 3 Pro 生成广告图片。它在 Artificial Analysis 排行榜上排第 #4。质量确实令人印象深刻：提示词遵循度更好，排版更好，创意输出比我们试过的任何东西都好。Google 到处都在讲它。YouTube…]]></description><link>https://bdteo.com/zh/the-model-that-wasnt-there/</link><guid isPermaLink="false">https://bdteo.com/zh/the-model-that-wasnt-there/</guid><pubDate>Sat, 14 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我们当时用 Gemini 3 Pro 生成广告图片。它在 Artificial Analysis 排行榜上排第 #4。质量确实令人印象深刻：提示词遵循度更好，排版更好，创意输出比我们试过的任何东西都好。Google 到处都在讲它。YouTube 视频。会议。研讨会。博客文章。“世界上最好的图像生成模型。”&lt;/p&gt;
&lt;p&gt;我信了。图片确实好。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;然后有个用户报告说，克隆一条广告要花四分钟。我查了。生成本身不到三十秒就完成。剩下那三分半钟呢？任务一直在撞墙重试。&lt;/p&gt;
&lt;ol start=&quot;429&quot;&gt;
&lt;li&gt;Resource Exhausted.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;Google 给 Gemini 图像生成加了硬上限：每分钟两个请求。按项目。全局。&lt;/p&gt;
&lt;p&gt;两个。不是两百。不是二十。两个。&lt;/p&gt;
&lt;p&gt;前一天我们生成了 900 张图，一点问题没有。他们那边有什么东西变了。没有通知，没有邮件，没有更新日志条目。只是突然多了一个新天花板，低到两个用户同时点击就能撞上。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我们的 DevOps 提交了配额提升申请。30 RPM。对一个生产 SaaS 来说很合理。Google 的回复是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;This gemini model is not available for quota increase.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;他们建议我们切到 Imagen 4。我查了一下。&lt;/p&gt;
&lt;p&gt;Imagen 4 Ultra - 排名 #10。Imagen 4 Standard - #42。Imagen 4 Fast - #60。&lt;/p&gt;
&lt;p&gt;我们用的是 #4。Google 的建议，是在他们自己的排行榜上往下退六到五十六名不等。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我把能想到的都试了。&lt;/p&gt;
&lt;p&gt;切到 Gemini 3.1 Flash：排名 #2，成本减半，比我们现有的还好。部署到 staging。然后我查了配额。同样的 2 RPM 上限。它不是按模型算的。它是按项目、按基础模型家族算的。所有 Gemini 图像模型共用同一个配额池。&lt;/p&gt;
&lt;p&gt;多区域分发：配额按区域计算，所以把请求分散到五个区域，可以得到 10 RPM。只不过 Gemini 3.x 图像模型只能走 global endpoint。没有 regional endpoints。global endpoint 上的 2 RPM，是唯一存在的配额池。&lt;/p&gt;
&lt;p&gt;多个 GCP 项目：每个项目都有自己的 2 RPM。技术上可行。从架构上看，这就是绝望的样子。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我开始研究其他开发者遇到了什么。同一个故事到处都是。未记录的 2 RPM 限制。论坛帖子没有 Google 回复。配额提升明明批准了，每次调用仍然返回 429。我们每月 $30K 的 GCP 支出？没用。标准 PayGo 层明确把图像生成模型排除在吞吐量收益之外。&lt;/p&gt;
&lt;p&gt;Google 不会提高这个限制。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;接下来才是有意思的问题：为什么不？&lt;/p&gt;
&lt;p&gt;Gemini 通过同一个处理文本的自回归 transformer 来生成图片。它不是扩散模型。它是完整的 LLM，一边推理一边逐像素走完整张图。每张图烧掉的计算量，相当于几十次文本 API 调用。&lt;/p&gt;
&lt;p&gt;按每张 $0.067 的价格，Google 几乎肯定每生成一张就亏一张。2 RPM 上限不是他们忘了调整的配额。它是一次有意计算过的节流，因为经济账算不过来。&lt;/p&gt;
&lt;p&gt;Imagen 4 用的是经典 latent diffusion，运行成本低几个数量级。所以它能拿到 30-150 RPM，而 Google 正把所有人往它那里推。昂贵的模型拿到营销。便宜的模型拿到吞吐量。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;想想这意味着什么。Google 做出了一个登顶所有基准榜的模型。他们在每场会议、每个 YouTube keynote、每篇开发者博客里营销它。“最先进。世界第一。” 开发者把它接进生产。用户开始依赖它。然后：每分钟两个请求，不能提升，用我们的差一点的模型吧。&lt;/p&gt;
&lt;p&gt;API 存在。Endpoint 能用。演示惊艳。&lt;/p&gt;
&lt;p&gt;但你不能真正使用它。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我们切到了 &lt;code class=&quot;language-text&quot;&gt;gemini-2.5-flash-image&lt;/code&gt;。旧模型。无聊的那个。没人为它做 YouTube 视频的那个。&lt;/p&gt;
&lt;p&gt;它有 40 RPM。它能用。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;四个教训，压缩版：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;营销不是产品。&lt;/strong&gt; 登顶排行榜不代表你能承载生产流量。基准测试衡量质量。速率限制衡量承诺。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自回归图像生成无法规模化。&lt;/strong&gt; 当生成一张图的成本相当于一百次文本查询时，没有商业模式能承受慷慨的速率限制。经济账就是破绽。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preview 就是 preview。&lt;/strong&gt; Google 可以毫无通知地改限制、杀模型，或者把你导向更差的替代品。如果你的生产系统依赖 preview 模型，你的生产系统依赖的就是别人的营销日程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无聊的模型能用。&lt;/strong&gt; 那个有 40 RPM、没有会议演讲的模型，会服务你的用户；那个世界级模型则坐在红绒绳后面，每分钟生成两张图。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最吓人的供应商锁定，是从一个你无法抗拒的 demo 开始的。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[那座并不存在的城市]]></title><description><![CDATA[…]]></description><link>https://bdteo.com/zh/the-city-that-wasnt-there/</link><guid isPermaLink="false">https://bdteo.com/zh/the-city-that-wasnt-there/</guid><pubDate>Sun, 08 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我做了个东西，从一个来源拉数据，清理它，然后把它展示得比原站更好。标准工作。&lt;/p&gt;
&lt;p&gt;然后我查询了系统里第二大的条目。其他所有条目都返回了几百个结果。这个：零。不是坏了。就是空的。&lt;/p&gt;
&lt;p&gt;我以为是我搞砸了。把代码查了三遍。直接测了接口端点。这个条目在他们界面里存在。它只是……空心的。&lt;/p&gt;
&lt;p&gt;就是从那时起，我开始往下挖。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;那个来源看起来很完整。范围广，界面打磨过，API 干净。但自愿参与意味着会有一些洞，是你从文档里看不见的。&lt;/p&gt;
&lt;p&gt;三个竞争对手都有同一批实体的数据。完整记录。所以信息在某个地方存在。&lt;/p&gt;
&lt;p&gt;不是你缺少一个接口端点。是那个接口端点缺少现实。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我问了一个没人想到要问的问题：这些数据在互联网之前住在哪里？&lt;/p&gt;
&lt;p&gt;答案是：印刷期刊。档案。自 19 世纪以来一直运行的模拟格式。每周出版三次。没有结构化数据，只有网站上的文档。&lt;/p&gt;
&lt;p&gt;所以我下载了一份。密集的机构腔文本，公告埋在各个下属办公室之间。数据在那里。&lt;/p&gt;
&lt;p&gt;我的竞争对手已经手工做这件事三十年了。&lt;/p&gt;
&lt;p&gt;我用一个下午写了个爬虫。一半是好奇，一半是较劲。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;文档解析才是真正开始疼的地方。&lt;/p&gt;
&lt;p&gt;一个词会被软连字符切开：Unicode U+00AD，眼睛看不见，对每个 regex 都致命。你盯着屏幕，以为是自己的 pattern 错了。不是。文本里藏着一个幽灵字符。JavaScript 的 &lt;code class=&quot;language-text&quot;&gt;\w&lt;/code&gt; 不匹配非 ASCII 字符，所以普通单词会变成不可能命中的匹配。数字里有渲染器留下的幻影空格：&quot;20. 000&quot;，而不是 &quot;20.000&quot;。&lt;/p&gt;
&lt;p&gt;每个 bug 花在找到上的时间都比修复更久。文本抽取永远是这个比例：90% 侦探工作，10% 代码。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;十条记录从噪声里显形。日期、标识符、地点，全都在该在的地方。我跑了两遍，确认自己不是在幻觉。结果一样。它真的能用。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;解析让你看见那里有什么。我开始找那里没有什么。ID 是连续的。我把它们枚举了一遍。&lt;/p&gt;
&lt;p&gt;53% 是死的。系统会清除已经结束的条目：没有档案，没有历史。有些记录存在，但没有任何支撑文档。答案是：请亲自来。2026 年了。&lt;/p&gt;
&lt;p&gt;这个来源不是数据库。它是一扇窗口，而且有人一直在把它关上。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;第一个数据源塑造了架构。第二个数据源击碎了每个假设。&lt;/p&gt;
&lt;p&gt;我需要第二套架构。这是一种客气的说法，意思是第一套其实并不是架构，只是一个刚好适配某个场景的可用方案。那个奇怪的来源揭示了真相：你为手上已有的数据而建，不是为你将会遇见的数据而建。&lt;/p&gt;
&lt;p&gt;这次我认真做了一套。Registry pattern，共享接口，基础契约，让每个实现都能忠于自己的形状。&lt;/p&gt;
&lt;p&gt;这个架构更好，是因为我等了。如果我第一天就做它，我只会为我唯一认识的来源设计。第二个来源，那个奇怪的来源，逼我找出真正重要的东西。&lt;/p&gt;
&lt;p&gt;你无法为未知设计。但未知抵达时，你可以重构。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;架构教我怎么建。市场教我为了什么而建。&lt;/p&gt;
&lt;p&gt;我要进入的是一个有三十年老牌玩家的市场。他们的技术看起来像 2005 年。他们的护城河不是技术，而是信任、品牌认知、几十年积累的数据。&lt;/p&gt;
&lt;p&gt;那个现代竞争者三年前带着 AI 和漂亮 UI 入场。价格低于老牌玩家。三年后，老牌玩家仍然占主导。事实证明，更便宜并不自动意味着定位更好。&lt;/p&gt;
&lt;p&gt;锚定很重要：第一个价格会成为参照点。之后降价容易，涨价几乎不可能。订阅本身不是产品，它只是通向背后东西的门。&lt;/p&gt;
&lt;p&gt;我把价格定高。之后总能降下来。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;四个教训，压缩版：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;权威不等于完整。&lt;/strong&gt; 主来源缺了整整一块。数据存在，只是不在任何人预期的地方。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二个来源会显露你的架构。&lt;/strong&gt; 只有当某个东西拒绝你建出的形状时，你才会知道设计的真相。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据不是永久的。&lt;/strong&gt; 如果你需要它，存下来。来源不会替你存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为你正在变成的东西定价，而不是为你现在是什么定价。&lt;/strong&gt; 订阅是一扇门。把门后面的东西建出来。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;有意思的工作住在缝隙里。我也住在那里。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[那个从未停下的队列]]></title><description><![CDATA[邮件在失败。这部分是预料之中的：迁移过程中 SMTP 凭据坏了。没预料到的是：它们从未停止失败。 Horizon 仪表盘：绿色。Worker：健康。Redis：慢慢变大。没有告警，日志里没有错误。只有一堆安静累积的任务，一次又一次又一次地重试。 我是因为修好 SMTP…]]></description><link>https://bdteo.com/zh/the-queue-that-never-stopped/</link><guid isPermaLink="false">https://bdteo.com/zh/the-queue-that-never-stopped/</guid><pubDate>Sat, 07 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;邮件在失败。这部分是预料之中的：迁移过程中 SMTP 凭据坏了。没预料到的是：它们从未停止失败。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Horizon 仪表盘：绿色。Worker：健康。Redis：慢慢变大。没有告警，日志里没有错误。只有一堆安静累积的任务，一次又一次又一次地重试。&lt;/p&gt;
&lt;p&gt;我是因为修好 SMTP 配置以后，Redis 内存没有降下来才注意到的。里面还有什么东西，正在咀嚼那些重试。成千上万次。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我以为队列会处理好这件事。规则不就是这样吗：一个 job 失败，重试几次，落进 &lt;code class=&quot;language-text&quot;&gt;failed_jobs&lt;/code&gt;。然后你继续往前走。&lt;/p&gt;
&lt;p&gt;除非这个 job 是一个 Mailable。&lt;/p&gt;
&lt;p&gt;当你把一个 Mailable 分发到队列时，Laravel 会把它包进一个 job。这个 job 的 &lt;code class=&quot;language-text&quot;&gt;maxTries&lt;/code&gt; 来自 Mailable 的 &lt;code class=&quot;language-text&quot;&gt;$tries&lt;/code&gt; 属性。如果你没有设置它——你为什么会设置呢，文档几乎没提——它会被序列化成 &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt; 不是“使用 supervisor 默认值”。&lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt; 是“没有限制”。Horizon 看到 &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt;，就会想：这个 job 想永远重试。于是它就这么做。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;结果这是个已知 bug。&lt;a href=&quot;https://github.com/laravel/horizon/issues/1346&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Laravel Horizon issue #1346&lt;/a&gt;。当序列化后的 job payload 携带 &lt;code class=&quot;language-text&quot;&gt;maxTries: null&lt;/code&gt; 时，supervisor 的 &lt;code class=&quot;language-text&quot;&gt;--tries&lt;/code&gt; 标志会被忽略。job 自己的声明赢了，而它的声明说：永远不要停。&lt;/p&gt;
&lt;p&gt;二十九个 Mailable 类。每一个都没有显式的 &lt;code class=&quot;language-text&quot;&gt;$tries&lt;/code&gt; 属性。每一个都可能不死。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;修复简单到几乎有点冒犯：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name-definition class-name&quot;&gt;WelcomeEmail&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Mailable&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ShouldQueue&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword type-declaration&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$tries&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword type-declaration&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$maxExceptions&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;两个属性。二十九个文件。就这样。&lt;/p&gt;
&lt;p&gt;一次初始尝试，一次重试，然后进 &lt;code class=&quot;language-text&quot;&gt;failed_jobs&lt;/code&gt;。就像我原本以为它一直会那样工作。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我测试它的方式像是在测试捕鼠夹。故意弄坏 SMTP 配置。分发一封邮件。看 Horizon。两次尝试。Failed job。结束。队列里没有幽灵。&lt;/p&gt;
&lt;p&gt;然后我修掉另外二十八个。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;三条教训，压缩版：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt; 不是“默认值”。&lt;/strong&gt; 在序列化的 job payload 里，&lt;code class=&quot;language-text&quot;&gt;maxTries: null&lt;/code&gt; 意味着无限。你的 supervisor 配置只是建议，不是规则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绿色仪表盘会撒谎。&lt;/strong&gt; Horizon 显示 worker 很健康，正高高兴兴地处理永远不会结束的 job。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;框架默认值不总是合理。&lt;/strong&gt; Laravel 不会在 Mailables 上设置 &lt;code class=&quot;language-text&quot;&gt;$tries&lt;/code&gt;。你必须自己设置。文档不会在火烧起来之前提醒你。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最吓人的 bug，是那些看起来像正常运行的 bug。这个就是——而且持续了好几个星期。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[批量删除的核选项：TRUNCATE + 重新插入（MySQL/InnoDB）]]></title><description><![CDATA[你需要从一张 MySQL 表里删除数百万行。 你伸手去拿： 然后你看着进度条在现实时间里变老。 你试着负责任一点，分块删： 好一点。还是慢。还是吵。还是贵。 如果你要删除的是表里的大部分数据（经验线：约 8…]]></description><link>https://bdteo.com/zh/todo-bulk-deletion-nuclear-option/</link><guid isPermaLink="false">https://bdteo.com/zh/todo-bulk-deletion-nuclear-option/</guid><pubDate>Sat, 13 Dec 2025 13:00:00 GMT</pubDate><content:encoded>&lt;p&gt;你需要从一张 MySQL 表里删除数百万行。&lt;/p&gt;
&lt;p&gt;你伸手去拿：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; big_table &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; some_condition&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;然后你看着进度条在现实时间里变老。&lt;/p&gt;
&lt;p&gt;你试着负责任一点，分块删：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;DELETE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; big_table &lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; some_condition &lt;span class=&quot;token keyword&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;-- repeat until done&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;好一点。还是慢。还是吵。还是贵。&lt;/p&gt;
&lt;p&gt;如果你要删除的是表里的&lt;strong&gt;大部分&lt;/strong&gt;数据（经验线：&lt;strong&gt;约 80% 以上&lt;/strong&gt;），还有一种做法，粗暴但有效：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不要删除你不想要的东西。&lt;strong&gt;保留你想要的，把剩下的炸掉。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我把它叫作&lt;strong&gt;核选项&lt;/strong&gt;：&lt;strong&gt;TRUNCATE + 重新插入&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;为什么 &lt;code class=&quot;language-text&quot;&gt;DELETE&lt;/code&gt; 还是慢（即使分块）&lt;/h2&gt;
&lt;p&gt;InnoDB 并不是“移除”行。它是在干活。&lt;/p&gt;
&lt;p&gt;很多活：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逐行操作&lt;/strong&gt;：定位、加锁、标记为已删除。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;索引维护&lt;/strong&gt;：每一次删除都会触碰每一个二级索引。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Undo/redo 日志&lt;/strong&gt;：引擎必须保留回滚和恢复的能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buffer pool 翻腾&lt;/strong&gt;：你不断把页面弄脏，驱逐有用的页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复制影响&lt;/strong&gt;：大规模删除流是制造 replica lag 的好方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;餐巾纸背面的现实检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2700 万行，约 6000 行/秒 ≈ &lt;strong&gt;75 分钟&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这不是 bug。这是你选择的成本模型。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;核选项：TRUNCATE + 重新插入&lt;/h2&gt;
&lt;p&gt;这个技巧会把成本模型翻过来。&lt;/p&gt;
&lt;p&gt;你不再为每一行被删除的数据付费，而是为每一行&lt;strong&gt;被保留&lt;/strong&gt;的数据付费。&lt;/p&gt;
&lt;p&gt;算法：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1) Copy the rows you want to keep into a temporary table
2) TRUNCATE the original table (fast)
3) Insert the preserved rows back into the original table
4) Drop the temp table&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;是的：它被叫作“核”是有原因的。它刻意钝重。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;为什么它快&lt;/h2&gt;
&lt;p&gt;收益是机械性的：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Rough cost&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;TRUNCATE&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;~O(1)&lt;/td&gt;
&lt;td&gt;drops and recreates the table (metadata-level)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;CREATE TABLE … AS SELECT&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;O(k)&lt;/td&gt;
&lt;td&gt;sequential scan + bulk write for kept rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;INSERT … SELECT&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;O(k)&lt;/td&gt;
&lt;td&gt;bulk insert; no “delete tax”&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;没有逐行删除开销。被移除的行也没有索引更新（因为它们一次性没了）。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;什么时候用它（以及什么时候别用）&lt;/h2&gt;
&lt;h3&gt;适合用在&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;你要删除表里的&lt;strong&gt;大部分&lt;/strong&gt;数据（还是那条线：&lt;strong&gt;约 80% 以上&lt;/strong&gt;，这时它开始发光）。&lt;/li&gt;
&lt;li&gt;你能清楚定义“要保留的行”。&lt;/li&gt;
&lt;li&gt;你可以接受短暂不可用 / 维护窗口。&lt;/li&gt;
&lt;li&gt;这张表没有被其他表的外键活跃引用（或者你能安全地管理约束）。&lt;/li&gt;
&lt;li&gt;你有&lt;strong&gt;足够的磁盘空间&lt;/strong&gt;放临时表。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;不要用在&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;你需要&lt;strong&gt;零停机&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;这张表被你不能碰的外键大量引用。&lt;/li&gt;
&lt;li&gt;你&lt;em&gt;必须&lt;/em&gt;触发 DELETE triggers。&lt;/li&gt;
&lt;li&gt;你只删除少数行（分块 delete 可能是更简单的胜利）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;一个实用的决策规则&lt;/h2&gt;
&lt;p&gt;如果你想在评审里用一句话说清楚：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果这次删除会移除表里的大部分数据，就停止删除。保留并重建。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;或者，如果你更喜欢 ASCII：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;How much are you deleting?

&amp;lt; 50%     -&gt; chunked DELETE (and index-aware filters)
50–80%    -&gt; measure both approaches
&gt; 80%     -&gt; TRUNCATE + reinsert (if constraints allow)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;实现（SQL）&lt;/h2&gt;
&lt;p&gt;最小形状是这样：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;-- 1) Preserve the rows you want to keep&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; temp_preserved &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; big_table
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; preserve_condition&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- 2) Nuke the table&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;TRUNCATE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; big_table&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- 3) Restore preserved rows&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;INTO&lt;/span&gt; big_table
&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; temp_preserved&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;-- 4) Cleanup&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;DROP&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; temp_preserved&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;生产里有两点很重要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 MySQL 里，&lt;code class=&quot;language-text&quot;&gt;TRUNCATE&lt;/code&gt; 是 DDL。它会&lt;strong&gt;隐式提交&lt;/strong&gt;，不能像普通事务那样回滚。&lt;/li&gt;
&lt;li&gt;你需要维护窗口和备份。这不是“上线试试看”的东西。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;实现（Laravel/PHP）&lt;/h2&gt;
&lt;p&gt;这是我在需要时实际会用的版本：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;deleteViaTruncateAndReinsert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword type-hint&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token keyword type-hint&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$tableName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token keyword type-hint&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$preserveCondition&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword return-type&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token variable&quot;&gt;$tempTable&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;temp_preserved_&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$tableName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;_&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// You&apos;re about to do DDL. Be explicit that you&apos;re taking control.&lt;/span&gt;
    &lt;span class=&quot;token class-name static-context&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;statement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;SET FOREIGN_KEY_CHECKS=0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token class-name static-context&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;statement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;
            CREATE TABLE &lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$tempTable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; AS
            SELECT * FROM &lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$tableName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
            WHERE &lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$preserveCondition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token variable&quot;&gt;$preserved&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name static-context&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;table&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$tempTable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token class-name static-context&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;statement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;TRUNCATE TABLE &lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$tableName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token class-name static-context&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;statement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;
            INSERT INTO &lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$tableName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
            SELECT * FROM &lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$tempTable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token class-name static-context&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;statement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;DROP TABLE &lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$tempTable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;finally&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token class-name static-context&quot;&gt;DB&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;statement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;SET FOREIGN_KEY_CHECKS=1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$preserved&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;一点橡皮鸭能量：再读一遍这个函数，问问未来的自己：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“我&lt;em&gt;确定&lt;/em&gt;这张表可以短暂清空一下吗？”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果答案不是毫不含糊的“是”，这就不是你的工具。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;你必须考虑的坑&lt;/h2&gt;
&lt;h3&gt;自增值会重置&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;TRUNCATE&lt;/code&gt; 会重置 &lt;code class=&quot;language-text&quot;&gt;AUTO_INCREMENT&lt;/code&gt;。如果你需要保留它：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;MAX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; big_table&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; big_table &lt;span class=&quot;token keyword&quot;&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;max_id &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;外键&lt;/h3&gt;
&lt;p&gt;如果其他表引用这张表，&lt;code class=&quot;language-text&quot;&gt;TRUNCATE&lt;/code&gt; 可能会被禁止，或者不安全。不要“先把 checks 关了”然后祈祷。&lt;/p&gt;
&lt;h3&gt;触发器&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;TRUNCATE&lt;/code&gt; &lt;strong&gt;不会&lt;/strong&gt;触发 DELETE triggers。如果你需要触发器的副作用，那你又回到了 &lt;code class=&quot;language-text&quot;&gt;DELETE&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;磁盘空间&lt;/h3&gt;
&lt;p&gt;你需要给被保留的数据集（临时表）留出空间。先检查：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;ROUND&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data_length &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1024&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1024&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; data_gb&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;ROUND&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;index_length &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1024&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1024&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; index_gb
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; information_schema&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;tables&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt; table_schema &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DATABASE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; table_name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;big_table&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;复制 / binlog&lt;/h3&gt;
&lt;p&gt;这是 DDL + 批量插入。它仍然可能造成 replica lag。要有意识地做，监控 lag，不要假装它免费。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;如果你需要（接近）零停机&lt;/h2&gt;
&lt;p&gt;这篇文章讲的是一把很快的锤子。&lt;/p&gt;
&lt;p&gt;如果你需要手术刀，用那些本来就是为此而造的工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;pt-archiver&lt;/code&gt;（Percona Toolkit），用于带有复制友好节奏的分批删除&lt;/li&gt;
&lt;li&gt;分区策略（删分区，而不是删行）&lt;/li&gt;
&lt;li&gt;shadow-table 方案 + 受控 swap（更复杂，活动部件更多）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;结尾想法&lt;/h2&gt;
&lt;p&gt;这不是什么聪明小技巧。它是在选择你要为哪种工作付费。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Type 0 重构：第一步之前的那一步]]></title><description><![CDATA[有一类重构，团队一直在做，立刻从中受益，却几乎从不给它命名。 它是你碰那个吓人的文件之前要做的活。功能需求把你逼进那个混乱模块。事故来了，bug…]]></description><link>https://bdteo.com/zh/type-0-refactoring-step-before-step-one/</link><guid isPermaLink="false">https://bdteo.com/zh/type-0-refactoring-step-before-step-one/</guid><pubDate>Sat, 13 Dec 2025 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;有一类重构，团队一直在做，立刻从中受益，却几乎从不给它命名。&lt;/p&gt;
&lt;p&gt;它是你碰那个吓人的文件之前要做的活。功能需求把你逼进那个混乱模块。事故来了，bug 藏在一个看起来自带天气系统的方法里。&lt;/p&gt;
&lt;p&gt;你不是在重新设计系统。你不是在引入新的抽象。你也不是用某种聪明方式“改进”任何东西。&lt;/p&gt;
&lt;p&gt;你只是把代码弄到足够可读，让你能开始工作。&lt;/p&gt;
&lt;p&gt;我开始把这叫作 &lt;strong&gt;Type 0 重构&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Type 0 重构&lt;/strong&gt;是一种预备性的、&lt;strong&gt;保持行为不变的清理&lt;/strong&gt;，它让代码在你做架构重构、性能工作或功能工作&lt;strong&gt;之前&lt;/strong&gt;，先变得更容易推理。&lt;/p&gt;
&lt;p&gt;它就是“重装厨房之前，先把地板擦干”的那一步。大多数团队已经在非正式地做它。给它一个名字，它就变成了一个共享工具。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Type 0 存在的真正原因：人的工作记忆有预算&lt;/h2&gt;
&lt;p&gt;这个想法背后的直白真相是：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我的大脑（你的也是）并不是为了在时间压力下可靠地调试一个 2000 行方法而设计的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这不是个人缺陷。认知就是这样工作。&lt;/p&gt;
&lt;p&gt;调试要求你同时把这些东西放在脑子里：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前执行路径&lt;/li&gt;
&lt;li&gt;相关状态&lt;/li&gt;
&lt;li&gt;每个变量实际意味着什么&lt;/li&gt;
&lt;li&gt;可能分支的集合&lt;/li&gt;
&lt;li&gt;“如果发生这个，那么……”的后果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在小代码里，这还可控。&lt;/p&gt;
&lt;p&gt;在大代码里，再加上高圈复杂度，它就变成了概率性猜测。你仍然可能走运，但成本高，风险也高，尤其是在 hotfix 期间。&lt;/p&gt;
&lt;p&gt;Type 0 是一个实际回应：它让你&lt;strong&gt;快速购买清晰度&lt;/strong&gt;，而不承担“真正重构”的成本和风险。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;为什么叫 “Type 0”&lt;/h2&gt;
&lt;p&gt;这个名字不是从什么宏大理论里来的。它来自一个高压时刻。&lt;/p&gt;
&lt;p&gt;我当时在做一个 hotfix。bug 被埋在一个方法里，而那个方法实际上就是自己的小宇宙——&lt;strong&gt;大约 2000 行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;bug 在概念上并不难。难的是这个方法。&lt;/p&gt;
&lt;p&gt;每一个“如果……会怎样”都会分裂成十个更多的问题，而那些分支并不是有用的那种。那是偶然复杂度：噪音、重复、不清楚的命名，以及和调试所需心智模型不匹配的结构。&lt;/p&gt;
&lt;p&gt;我需要的不是完美。我需要的是&lt;strong&gt;可调试性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每屏更少的分支&lt;/li&gt;
&lt;li&gt;更清楚、有名字的“步骤”&lt;/li&gt;
&lt;li&gt;更少的噪音&lt;/li&gt;
&lt;li&gt;更少时间重新解析刚刚读过的东西&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但时间压力不允许更大的重构，也不允许一次“惯用写法的重新设计”。负责任地做那件事要半天（甚至更多），还包括手动测试。在 hotfix 窗口里，那不是纪律；那是赌博。&lt;/p&gt;
&lt;p&gt;于是我让一个 LLM 为这个类和这个方法建议重构机会——没告诉它原因。&lt;/p&gt;
&lt;p&gt;它回来给了我一份四种“类型”重构的列表。都合理。都适用。也都对那个时刻来说太贵。&lt;/p&gt;
&lt;p&gt;然后它礼貌地问：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Should I start with Type 1?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那时我回复：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“No. Let’s start with Type 0.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我当场定义了 Type 0：一组受约束、机械性的修改，用来降低复杂度、提高可读性，&lt;strong&gt;但不改变行为或架构&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;那个方法变得可以导航了。我的大脑又能跟上执行路径了。我找到了 bug，修掉了它，并且没有带着附带伤害发布。&lt;/p&gt;
&lt;p&gt;这就是我喜欢 &lt;strong&gt;Type 0&lt;/strong&gt; 这个名字的原因：它是你在“真正重构”类型&lt;strong&gt;之前&lt;/strong&gt;做的重构——尤其是在你有压力，需要一种安全方式快速创造清晰度的时候。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Type 0 解决的问题&lt;/h2&gt;
&lt;p&gt;大多数重构建议都假设你已经能_看见_设计。&lt;/p&gt;
&lt;p&gt;在真实代码库里：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法很长，而且承担多种目的&lt;/li&gt;
&lt;li&gt;重复表达式和偶然复杂度藏住了意图&lt;/li&gt;
&lt;li&gt;变量很隐晦（&lt;code class=&quot;language-text&quot;&gt;$e&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;$tmp&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;$res&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;死代码和未使用 import 制造心智噪音&lt;/li&gt;
&lt;li&gt;代码的“形状”乱到连小改动都让人觉得危险&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当你在这上面尝试“真正重构”（边界、模式、移动职责）时，你是在不确定性上堆更多不确定性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你不容易判断自己正在保留什么行为&lt;/li&gt;
&lt;li&gt;你无法预测爆炸半径&lt;/li&gt;
&lt;li&gt;review 会退化成主观争论&lt;/li&gt;
&lt;li&gt;人们开始害怕碰这些东西，而混乱继续复利&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Type 0 是你先降低认知负荷的方法。&lt;/strong&gt; 它创造一个稳定基础，让更深的工作可以安全发生。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;什么时候拿起 Type 0&lt;/h2&gt;
&lt;p&gt;Type 0 在这些情况下最有价值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你必须快速调试（hotfix、事故），而代码太大 / 分支太多，无法安全推理&lt;/li&gt;
&lt;li&gt;你觉得自己“迷失在方法里”，反复重读同一段，因为结构帮不上你的工作记忆&lt;/li&gt;
&lt;li&gt;代码是正确的，但不可读；你承担不起“清理逻辑”，只能把逻辑暴露出来&lt;/li&gt;
&lt;li&gt;你想在更深工作之前降低风险（你知道之后会重构，但首先需要当前行为的清晰地图）&lt;/li&gt;
&lt;li&gt;你想把部落知识变成可读结构，让调试不再依赖某一个人&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Type 0 不是奢侈品。在这些场景里，它常常是最快重新拿回控制权的方法。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;一个可以给团队使用的定义&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Type 0 重构是一组微重构，用来在不改变行为或架构的前提下提升可读性和可维护性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它刻意受约束。约束就是这个特性本身。&lt;/p&gt;
&lt;p&gt;Type 0 包含四个强制子模式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;0a. 方法提取&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0b. 简洁性&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0c. 同理心（纯可读性）&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0d. 删除死代码&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;并遵守三条硬规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不改变行为&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不改变架构&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;除了这四种模式之外，不做“聪明”的改进&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你违反这些规则，你做的就不再是 Type 0——你已经进入了另一类工作，而那需要不同的协调、不同的 review 严谨度，通常也需要不同的测试策略。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;为什么非要命名？&lt;/h2&gt;
&lt;p&gt;因为命名会改变团队协作的方式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“这个 PR 里我只做 Type 0”会告诉 reviewer 该看什么：行为保持和可读性，而不是架构争论。&lt;/li&gt;
&lt;li&gt;“我们需要先对这里做 Type 0，再重构”是在诚实承认代码还没准备好承接更深的改变。&lt;/li&gt;
&lt;li&gt;“我们把 Type 0 当作 Step 0 来做”创造了一个小仪式，防止你在混乱之上继续搭东西。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;四个子模式&lt;/h2&gt;
&lt;h3&gt;0a. 方法提取（基础）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;目标：&lt;/strong&gt; 把大方法拆成小而聚焦的方法，让人可以线性读懂意图。&lt;/p&gt;
&lt;p&gt;经验规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拆解那些长到无法放进工作记忆的方法&lt;/li&gt;
&lt;li&gt;每个提取出来的方法应该只做一件事，并且有描述性的名字&lt;/li&gt;
&lt;li&gt;提取有意义的步骤，而不是任意 N 行代码块&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么它有效（尤其对调试）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小方法为执行路径创造标签&lt;/li&gt;
&lt;li&gt;2000 行滚动变成一个短的编排方法，你可以在脑子里逐步走过&lt;/li&gt;
&lt;li&gt;你可以在语义边界上打断点（“验证输入”、“构建查询”、“应用过滤器”），而不是到处狩猎&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;0b. 简洁性（减少偶然复杂度）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;目标：&lt;/strong&gt; 移除视觉噪音，让意图浮出来。&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把重复表达式提取成局部变量&lt;/li&gt;
&lt;li&gt;把重复的日志上下文 / key 字符串 / URL 片段提取成变量&lt;/li&gt;
&lt;li&gt;优先使用能直接表达意图的语言特性&lt;/li&gt;
&lt;li&gt;简化过度冗长的插值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么它有效：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它降低认知负荷&lt;/li&gt;
&lt;li&gt;它让 diff 更小，改动更安全&lt;/li&gt;
&lt;li&gt;它防止复制 / 粘贴漂移&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;0c. 同理心（纯可读性）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;目标：&lt;/strong&gt; 写给下一个人看，而不是写给编译器看。&lt;/p&gt;
&lt;p&gt;同理心意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用描述性变量名（避免 &lt;code class=&quot;language-text&quot;&gt;$e&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;$d&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;$tmp&lt;/code&gt;，除非真的显而易见）&lt;/li&gt;
&lt;li&gt;在一个模块内保持术语一致&lt;/li&gt;
&lt;li&gt;重命名具有误导性的名字&lt;/li&gt;
&lt;li&gt;让代码自我说明&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;试金石：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果有人在凌晨两点的事故中读这段代码，它会帮助他们把执行路径放在脑子里吗？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;0d. 删除死代码（移除谎言）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;目标：&lt;/strong&gt; 删除所有假装重要、其实不重要的东西。&lt;/p&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;未使用的 private 方法&lt;/li&gt;
&lt;li&gt;未使用的 imports&lt;/li&gt;
&lt;li&gt;注释掉的旧方案&lt;/li&gt;
&lt;li&gt;已废弃且没人调用的 helpers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么它有效：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码越少，误解的东西就越少&lt;/li&gt;
&lt;li&gt;搜索结果会变得可信&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Type 0 不是什么&lt;/h2&gt;
&lt;p&gt;Type 0 不是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;改变服务边界&lt;/li&gt;
&lt;li&gt;引入新的抽象或模式&lt;/li&gt;
&lt;li&gt;重新架构一个 workflow&lt;/li&gt;
&lt;li&gt;替换库&lt;/li&gt;
&lt;li&gt;在层之间重新分配职责&lt;/li&gt;
&lt;li&gt;“修复”你怀疑有错的逻辑（除非你明确声明这是行为改变，并测试它）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你发现自己在说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“既然我在这里，不如也……”&lt;/li&gt;
&lt;li&gt;“如果我们这样会更好……”&lt;/li&gt;
&lt;li&gt;“我们大概应该重新设计……”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可能正在离开 Type 0。这本身不坏——但应该是有意为之。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;核心承诺：保持行为不变（以及如何让它是真的）&lt;/h2&gt;
&lt;p&gt;Type 0 只有在团队信任这个承诺时才有效。&lt;/p&gt;
&lt;p&gt;而且是的，你怀疑得对：&lt;strong&gt;方法提取可能意外改变行为&lt;/strong&gt;（early returns、变量作用域、求值顺序、异常行为）。&lt;/p&gt;
&lt;p&gt;所以 Type 0 需要纪律来保持诚实：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;照原样提取，然后再重命名 / 清理。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一遍：把代码移进方法，不改变逻辑&lt;/li&gt;
&lt;li&gt;第二遍：应用简洁性 + 同理心&lt;/li&gt;
&lt;li&gt;第三遍：删除死代码&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实用护栏：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不要为了“可读性”重新排列条件检查&lt;/li&gt;
&lt;li&gt;不要用“等价”逻辑替换逻辑，除非你已经在 Type 0 之外&lt;/li&gt;
&lt;li&gt;小心那些原本在共享作用域里的变量&lt;/li&gt;
&lt;li&gt;把“小的”控制流差异当作真正差异看待&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你有&lt;em&gt;任何&lt;/em&gt;安全网，哪怕很薄：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;跑一个聚焦测试&lt;/li&gt;
&lt;li&gt;重放失败场景&lt;/li&gt;
&lt;li&gt;验证你正在触碰的那一条路径&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Type 0 讲的是快——&lt;strong&gt;但这种快来自降低认知复杂度&lt;/strong&gt;，不是来自跳过安全。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;把 Type 0 变成可重复的团队仪式&lt;/h2&gt;
&lt;h3&gt;1) 决定范围（timebox 有帮助）&lt;/h3&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“调试前先对 hot path 做 Type 0。”&lt;/li&gt;
&lt;li&gt;“只对这个 bug fix 会碰到的路径做 Type 0。”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2) 找到代码的“脊柱”&lt;/h3&gt;
&lt;p&gt;找到入口方法和分支点。通过提取，把这条脊柱变成一个可读叙事。&lt;/p&gt;
&lt;h3&gt;3) 按顺序应用四个子模式&lt;/h3&gt;
&lt;p&gt;方法提取 → 简洁性 → 同理心 → 删除死代码。&lt;/p&gt;
&lt;h3&gt;4) 在 PR 里保留一个 “Type 0 checklist”&lt;/h3&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 不改变行为（输入 / 输出不变）&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 不做架构移动&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 方法已提取，并命名为有意义的步骤&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 重复表达式已在能提升清晰度的地方提取&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 变量已重命名；术语一致&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 死代码和未使用 imports 已删除&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;结尾想法&lt;/h2&gt;
&lt;p&gt;Type 0 重构是开发者可以作出的最简单承诺：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“我会让这段代码比我发现它时更容易工作——同时不改变它做的事。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有时候它是“有就更好”。&lt;/p&gt;
&lt;p&gt;有时候，它是人在高复杂度混乱里安全快速移动的唯一办法——尤其是在 hotfix 期间。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[多洗一个盘子：让代码库长期保持干净的一条简单规则]]></title><description><![CDATA[TL;DR…]]></description><link>https://bdteo.com/zh/wash-one-more-plate-refactoring-philosophy/</link><guid isPermaLink="false">https://bdteo.com/zh/wash-one-more-plate-refactoring-philosophy/</guid><pubDate>Thu, 24 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;：把你对代码库做的每一次修改，都当成做一顿饭。你会弄脏几个盘子。结束时，不要只洗掉你用过的那些——再多洗&lt;em&gt;一个&lt;/em&gt;。时间久了，这一点点多出来的照料，会复利成一个保持干净、而不是滑向混乱的厨房（代码库）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;这个隐喻：做饭、盘子和代码&lt;/h2&gt;
&lt;p&gt;想象一间专业厨房。每做一道菜都会弄脏几个盘子，哪怕是最整洁的后厨团队也一样。现在想象，每个厨师做完自己的菜后，都&lt;em&gt;刚好&lt;/em&gt;洗掉自己弄脏的盘子。厨房会悬在可接受的清洁边缘，但熵会慢慢爬进来：这里一点残留污垢，那里一块沾了渍的砧板。最后，混乱会复合增长。&lt;/p&gt;
&lt;p&gt;现在把规则反过来：做完饭后，每位主厨都比自己弄脏的盘子&lt;strong&gt;多洗一个&lt;/strong&gt;。慢慢地，厨房会比之前更干净，不只是被维持住，而是被改善。软件也是一样：你接手的每个任务，都应该给代码库至少增加一点点额外的整洁——多一个测试、更清楚的名字、拆开一个函数、移除一个死依赖。这个 &quot;+1 plate&quot; 的习惯，就是代码库&lt;em&gt;保持&lt;/em&gt;健康的方式。&lt;/p&gt;
&lt;p&gt;我把它叫做&lt;strong&gt;多洗一个盘子规则&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;手艺里的回声：你并不孤单&lt;/h2&gt;
&lt;p&gt;这不是一种孤零零的哲学。几十年来，软件行业的思想领袖一直在讲类似的东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&quot;Always leave the campground cleaner than you found it.&quot;&lt;/strong&gt; 这就是经典的 &lt;a href=&quot;https://deviq.com/principles/boy-scout-rule/&quot;&gt;Boy Scout Rule&lt;/a&gt;，由 Robert C. Martin 在软件领域推广。精神相同：每次都改好一点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;技术债这个隐喻&lt;/strong&gt;（Ward Cunningham）：债会产生利息。忽视它，明天使用这间&quot;厨房&quot;的成本就会更高。边走边还掉一些，才能保持偿付能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重构作为小而持续的步骤&lt;/strong&gt;（Martin Fowler）：微小的改动，保持行为不变，同时改善设计。小步意味着低风险和稳定动量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Make it work, make it right, make it fast&quot;&lt;/strong&gt;（Kent Beck）：先正确，再干净，再性能。多洗那个盘子发生在 &quot;make it right&quot; 阶段，也就是你过早优化之前。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把破窗理论应用到代码&lt;/strong&gt;（Andrew Hunt &amp;#x26; David Thomas）：可见的混乱会邀请更多混乱。在它扩散之前修好一扇&quot;窗&quot;，是在保护这个街区（代码库）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些想法互相强化。它们说的是同一句话：&lt;em&gt;不要把混乱传下去；花一点时间，让它好一点。&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;为什么多洗一个盘子重要（即使你很忙）&lt;/h2&gt;
&lt;h3&gt;1. &lt;strong&gt;熵是真实存在的&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;如果没人照看，代码不会保持中性。命名会漂移，模式会碎裂，抽象会腐烂。熵是一种力量；唯一的反作用力，是持续、增量的整理。你的 +1 plate，就是微型的熵逆转。&lt;/p&gt;
&lt;h3&gt;2. &lt;strong&gt;债务复利得比你想的快&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;每一句&quot;以后再修&quot;，都会提高变更成本。以后很少真的到来。利息会表现为变慢的功能开发、脆弱的部署，以及没人信任的测试套件。&lt;em&gt;今天&lt;/em&gt;多洗一个盘子，会降低明天的利率。&lt;/p&gt;
&lt;h3&gt;3. &lt;strong&gt;社会信号&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;当队友看到你会收拾自己留下的东西（还多收拾一点），规范就会移动。让代码比接手时更好，会变得可信，也会变成期待。文化跟着行为走。&lt;/p&gt;
&lt;h3&gt;4. &lt;strong&gt;动量，不是完美主义&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;这不是 yak shaving 的借口。你不是在午餐高峰时重建整间厨房。你只是拿海绵再擦一个盘子，小、安全、快。关键就在这里：交付不会因此脱轨。&lt;/p&gt;
&lt;h2&gt;如何实践多洗一个盘子规则&lt;/h2&gt;
&lt;p&gt;下面是把这个习惯嵌进去的方法，不拖偏 scope，也不撞坏 deadline。&lt;/p&gt;
&lt;h3&gt;1. 把&quot;微重构&quot;纳入 Definition of Done&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;重命名一个令人困惑的变量。&lt;/li&gt;
&lt;li&gt;提取一个小函数，降低圈复杂度。&lt;/li&gt;
&lt;li&gt;删除死代码或未使用的 imports。&lt;/li&gt;
&lt;li&gt;为你刚修好的 bug 补一个缺失测试。&lt;/li&gt;
&lt;li&gt;更新一段让你刚才心里一紧的文档或 README。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;判断标准是：&lt;strong&gt;如果它需要超过几分钟，那就不是一个盘子，而是整台洗碗机。推迟它。&lt;/strong&gt; 把它记录成 ticket。&lt;/p&gt;
&lt;h3&gt;2. 用 Pull Requests 触发清洁&lt;/h3&gt;
&lt;p&gt;每个 PR 都可以让营地更干净：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要求一个&quot;你清理了什么？&quot;的 checkbox 或简短说明。&lt;/li&gt;
&lt;li&gt;鼓励 reviewer 在 review 时&lt;em&gt;请求&lt;/em&gt;一些小整理。&lt;/li&gt;
&lt;li&gt;庆祝包含额外打磨的 PR（standup 里点名表扬，作用很不小）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 自动化那些容易洗的盘子&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;用 pre-commit hooks 做格式化和 linting。&lt;/li&gt;
&lt;li&gt;用静态分析标记复杂方法或过长参数列表。&lt;/li&gt;
&lt;li&gt;用依赖检查工具找出过时库。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;让自动扫帚清掉琐碎的脏东西，好让人专注在逻辑和设计上。&lt;/p&gt;
&lt;h3&gt;4. 把它嵌进团队规范&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;把这条规则写进团队 working agreement 或 engineering handbook。&lt;/li&gt;
&lt;li&gt;如果你想要可衡量的证明，在 retro 里跟踪微重构 wins。&lt;/li&gt;
&lt;li&gt;偶尔 pair 或 mob program，让这个习惯（以及勇气）传播开。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 知道什么时候&lt;strong&gt;不要&lt;/strong&gt;洗&lt;/h3&gt;
&lt;p&gt;有时候厨房着火了：生产环境挂了，或者 demo 只剩几小时。紧急情况下，该砸那叠脏盘子就砸。但危机后要回头处理。这条规则不是教条，而是纪律。&lt;/p&gt;
&lt;h2&gt;边界：一个盘子，不是整个水槽&lt;/h2&gt;
&lt;p&gt;Scope creep 会伪装成 craftsmanship。你的工作是在&quot;多洗一个盘子&quot;那里停下来。如果这个小重构暴露出更深的味道，写下来，然后继续走。把更深的修复停到停车场：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建一个带 &lt;code class=&quot;language-text&quot;&gt;refactor:&lt;/code&gt; 或 &lt;code class=&quot;language-text&quot;&gt;techdebt:&lt;/code&gt; 标签的 ticket。&lt;/li&gt;
&lt;li&gt;链接到相关代码、测试或模块。&lt;/li&gt;
&lt;li&gt;加一句为什么它重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你的职责已经完成：你看到了脏东西，洗了一个盘子，并给剩下的部分留下了说明。&lt;/p&gt;
&lt;h2&gt;示例：把一个混乱函数变成可测试的函数&lt;/h2&gt;
&lt;p&gt;之前：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;processOrder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Exception&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;No ID&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token variable&quot;&gt;$tax&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;country&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;BG&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$tax&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;total&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;country&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;DE&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token variable&quot;&gt;$tax&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;total&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.19&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Lots more branching...&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Sends email, writes to DB, calls payment gateway…&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;洗掉的那个盘子：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * Calculate VAT for an order based on country.
 * Pure function: given (total, country) -&gt; VAT amount.
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;vatFor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword type-hint&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$country&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword type-hint&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$total&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword return-type&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$country&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;BG&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$total&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;DE&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$total&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.19&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;现在你的主函数调用 &lt;code class=&quot;language-text&quot;&gt;vatFor()&lt;/code&gt;，而不是把逻辑内联进去。你还给 &lt;code class=&quot;language-text&quot;&gt;vatFor()&lt;/code&gt; 加了一个微型测试。这就是一个额外的盘子：简单、收束、有用。&lt;/p&gt;
&lt;h2&gt;最后的想法&lt;/h2&gt;
&lt;p&gt;多洗一个盘子很小。这正是重点。让代码库保持健康，不需要英雄式重构；需要的是一种小而持续的照料文化。把它变成习惯，烤进流程里。一年后，你会奇怪为什么你的厨房&lt;em&gt;不是&lt;/em&gt;灾难现场——因为你从没让它变成那样。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;行动建议&lt;/strong&gt;：下次你碰一个文件时，问自己：&lt;em&gt;&quot;在提交这个变更前，我还能多洗哪一个盘子？&quot;&lt;/em&gt; 然后做掉。重复。一次一个干净盘子，改变文化。&lt;/p&gt;
&lt;h3&gt;来源与延伸阅读&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Robert C. Martin（&quot;Uncle Bob&quot;）— Boy Scout Rule：&lt;/strong&gt; &lt;em&gt;97 Things Every Programmer Should Know&lt;/em&gt; 中的 &quot;&lt;a href=&quot;https://97-things-every-x-should-know.gitbooks.io/97-things-every-programmer-should-know/content/en/thing_08/&quot;&gt;The Boy Scout Rule&lt;/a&gt;&quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ward Cunningham — Technical Debt Metaphor：&lt;/strong&gt; Cunningham 对 &lt;a href=&quot;https://martinfowler.com/bliki/TechnicalDebt.html&quot;&gt;Technical Debt&lt;/a&gt; 的原始解释，收录在 Martin Fowler 的网站上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Martin Fowler — Continuous Micro-Refactoring：&lt;/strong&gt; Fowler 的书 &lt;a href=&quot;https://martinfowler.com/books/refactoring.html&quot;&gt;&lt;em&gt;Refactoring: Improving the Design of Existing Code&lt;/em&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kent Beck — &quot;Make it work, make it right, make it fast&quot;：&lt;/strong&gt; &lt;a href=&quot;https://ronjeffries.com/articles/-x024/biot/-bv40/3/&quot;&gt;Ron Jeffries&lt;/a&gt; 对这句箴言的解释。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Andrew Hunt &amp;#x26; David Thomas — Broken Windows in Software：&lt;/strong&gt; 这个概念详见他们的书 &lt;a href=&quot;https://en.wikipedia.org/wiki/The_Pragmatic_Programmer&quot;&gt;&lt;em&gt;The Pragmatic Programmer&lt;/em&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Software Entropy &amp;#x26; Maintenance：&lt;/strong&gt; 这篇 &quot;&lt;a href=&quot;https://chroniclesofapragmaticprogrammer.substack.com/p/entropy-in-software-and-the-broken-window&quot;&gt;Entropy in Software and the Broken Window Theory&lt;/a&gt;&quot; 也值得一读。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[PHP 8.5：即将到来的功能巡览]]></title><description><![CDATA[TL;DR 热度阶梯（我的玩心排名） Pipe Operator（） – 可读、线性的转换幸福感。重构磁铁。  Attribute – 把“忘了使用返回值”变成即时警告。和 pipes 配得很漂亮。 Static Closures / First-Class Callables…]]></description><link>https://bdteo.com/zh/php-8-5-new-features-pipe-operator-guide/</link><guid isPermaLink="false">https://bdteo.com/zh/php-8-5-new-features-pipe-operator-guide/</guid><pubDate>Sun, 20 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;TL;DR 热度阶梯（我的玩心排名）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pipe Operator（&lt;code class=&quot;language-text&quot;&gt;|&gt;&lt;/code&gt;）&lt;/strong&gt; – 可读、线性的转换幸福感。&lt;em&gt;重构磁铁。&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;#[\NoDiscard]&lt;/code&gt; Attribute&lt;/strong&gt; – 把“忘了使用返回值”变成&lt;em&gt;即时&lt;/em&gt;警告。和 pipes 配得很漂亮。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Static Closures / First-Class Callables in Constant Expressions&lt;/strong&gt; – 编译期 strategy maps 和 attribute arguments。框架糖果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;php --ini=diff&lt;/code&gt;&lt;/strong&gt; – 立即看到环境配置漂移的 diff。让你少钻配置洞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Attributes on Global &amp;#x26; Class Constants&lt;/strong&gt; – 元数据无处不在（flags、deprecations、semantic tags）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;array_first()&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;array_last()&lt;/code&gt;&lt;/strong&gt; – 明确、表达意图、非 mutating。再见，&lt;code class=&quot;language-text&quot;&gt;reset()&lt;/code&gt; 副作用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;get_exception_handler()&lt;/code&gt; &amp;#x26; friends&lt;/strong&gt; – 面向分层错误处理的 introspection（框架/基础设施层面的胜利）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intl Goodies（&lt;code class=&quot;language-text&quot;&gt;IntlListFormatter&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;Locale::isRightToLeft()&lt;/code&gt;）&lt;/strong&gt; – 几乎不用写代码，就能让本地化 UX 更顺。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grapheme-aware Levenshtein&lt;/strong&gt; – 真正尊重人类字符的用户侧 fuzzy matching。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Directory Object + cURL / Build Introspection / Misc&lt;/strong&gt; – 一致性和可运维性的打磨。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;（是的，&lt;em&gt;你的&lt;/em&gt;顺序可以不一样。乐趣就在这里——拿咖啡辩一辩。）&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. Pipe Operator（&lt;code class=&quot;language-text&quot;&gt;|&gt;&lt;/code&gt;）– “Easy Peasy Lemon Squeezy”&lt;/h2&gt;
&lt;p&gt;嵌套调用和临时一次性变量？没了。pipe operator 会把左边的值作为&lt;em&gt;第一个参数&lt;/em&gt;喂给右边的 callable。你从上到下读，逻辑像散文一样流动，意图直接拍到脸上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;之前（变量跳房子）：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$email&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token keyword type-declaration&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$email&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$email&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;strtolower&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sendEmail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;之前（括号套娃）：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sendEmail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;strtolower&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token keyword type-declaration&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;之后（pipe 禅）：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token keyword type-declaration&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;strtolower&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendEmail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;为什么重要：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;可视化数据流。&lt;/em&gt; 不用在脑子里维护一叠嵌套返回值。&lt;/li&gt;
&lt;li&gt;和小而纯的 helper 配合得很好。&lt;/li&gt;
&lt;li&gt;鼓励把转换拆成命名函数/closure。&lt;/li&gt;
&lt;li&gt;自动满足 &lt;code class=&quot;language-text&quot;&gt;#[\NoDiscard]&lt;/code&gt;，因为值会继续往下流。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;风格提示：&lt;/strong&gt; 让每个阶段都没有 side effect；把&lt;em&gt;最后&lt;/em&gt;一个 pipe 留给效果（比如持久化、发送、emit），这样你能一眼看见“纯度”在哪里结束。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;2. &lt;code class=&quot;language-text&quot;&gt;#[\NoDiscard]&lt;/code&gt; – 武器化的意图&lt;/h2&gt;
&lt;p&gt;多少细微 bug 其实只是“我们调用了那个东西，但忘了用它的返回值”？给函数或方法标上 &lt;code class=&quot;language-text&quot;&gt;#[\NoDiscard]&lt;/code&gt;，就能要求它的结果必须被&lt;em&gt;使用&lt;/em&gt;，或者通过 &lt;code class=&quot;language-text&quot;&gt;(void)&lt;/code&gt; cast 明确表示有意忽略。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;token attribute-content&quot;&gt;\&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;NoDiscard&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;Token must be used – did you forget to persist or dispatch?&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;issueAuthToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name type-declaration&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword return-type&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generateTokenFor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;issueAuthToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ⚠ Emits warning in 8.5&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword type-declaration&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;issueAuthToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Explicit intentional discard&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;模式：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Result objects（&lt;code class=&quot;language-text&quot;&gt;Result&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;Outcome&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;ValidationReport&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;Immutable builders（每次调用返回新实例）。&lt;/li&gt;
&lt;li&gt;Security / side-effect gating（tokens、signatures）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;协同：&lt;/strong&gt; 在 pipeline 里，每个阶段的返回值天然会被下一个阶段消费，所以意外丢弃会消失。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Static Closures in Constant Expressions – &lt;em&gt;“Wait… what?!”&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;现在你可以把 &lt;strong&gt;static&lt;/strong&gt; closures（或 first-class callables）嵌进 constant expressions、默认属性值、attribute arguments 和默认参数数组里。想象一下：编译期 registry，不需要启动期接线体操。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name-definition class-name&quot;&gt;Sanitizers&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;STAGES&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;trim&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;upper&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword type-hint&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$v&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword return-type&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;strtoupper&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$v&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Attribute example&lt;/span&gt;
&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;token attribute-content&quot;&gt;&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;Validate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token attribute-class-name class-name&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;title&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token attribute-class-name class-name&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token attribute-class-name class-name&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;string&lt;/span&gt; $&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token attribute-class-name class-name&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token attribute-class-name class-name&quot;&gt;mb_strlen&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;$&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;slug&apos;&lt;/span&gt;  &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token attribute-class-name class-name&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token attribute-class-name class-name&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;string&lt;/span&gt; $&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token attribute-class-name class-name&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token attribute-class-name class-name&quot;&gt;preg_match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;/^[a-z0-9-]+$/&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; $&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name-definition class-name&quot;&gt;Article&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;为什么它很有劲：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为简单策略消除 service-locator lookup。&lt;/li&gt;
&lt;li&gt;把纯 mapping table 推进 constants（immutable + cacheable）。&lt;/li&gt;
&lt;li&gt;Attributes 现在可以&lt;em&gt;直接&lt;/em&gt;封装逻辑，不只是 scalar metadata。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;限制：&lt;/strong&gt; 必须是 &lt;code class=&quot;language-text&quot;&gt;static&lt;/code&gt;；不能有 &lt;code class=&quot;language-text&quot;&gt;$this&lt;/code&gt;，不能捕获变量。如果需要 context，后面显式传进去。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;4. &lt;code class=&quot;language-text&quot;&gt;php --ini=diff&lt;/code&gt; – 配置漂移 X 光&lt;/h2&gt;
&lt;p&gt;受够了*“但它在 staging 上能跑”*？这个 CLI flag 只打印和默认值不同的 INI directives。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;php &lt;span class=&quot;token parameter variable&quot;&gt;--ini&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;diff
&lt;span class=&quot;token comment&quot;&gt;# memory_limit: &quot;128M&quot; -&gt; &quot;-1&quot;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# max_execution_time: &quot;30&quot; -&gt; &quot;0&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;使用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CI step，用来强制一致 baseline。&lt;/li&gt;
&lt;li&gt;worker 表现古怪时快速 sanity check。&lt;/li&gt;
&lt;li&gt;排查内存/时间异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pro tip：把输出纳入版本控制，作为 runtime baseline。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Attributes on Global &amp;#x26; Class Constants – 元数据无处不在&lt;/h2&gt;
&lt;p&gt;Constants 从“笨值”升级为“带注解的参与者”。把 domain flags、feature toggles、deprecation notices、unit semantics 直接装饰在定义处。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;token attribute-content&quot;&gt;&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;Deprecated&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;Use FEATURE_NEW_PRICING instead&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;FEATURE_OLD_PRICING&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;token attribute-content&quot;&gt;&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;ms&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DEFAULT_TIMEOUT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;250&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;框架可用之处：&lt;/strong&gt; 自动发现 deprecations、喂给 feature catalogs、生成文档，或者通过 reflection 执行 policy。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. &lt;code class=&quot;language-text&quot;&gt;array_first()&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;array_last()&lt;/code&gt; – 显而易见的东西终于存在了&lt;/h2&gt;
&lt;p&gt;别再为了看一眼首尾而表演指针杂技（&lt;code class=&quot;language-text&quot;&gt;reset()&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;end()&lt;/code&gt;）或切片了。这些 helper 直接读出意图，而且&lt;em&gt;不会&lt;/em&gt;改变数组内部状态。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$firstUser&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;array_first&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$users&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$lastUser&lt;/span&gt;  &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;array_last&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$users&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;重构模式：&lt;/strong&gt; 搜 &lt;code class=&quot;language-text&quot;&gt;reset(&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;end(&lt;/code&gt; / 复杂的 &lt;code class=&quot;language-text&quot;&gt;array_slice(..., 0, 1)&lt;/code&gt;，替换成语义化调用。diff 更干净，微型 bug 更少。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. &lt;code class=&quot;language-text&quot;&gt;get_exception_handler()&lt;/code&gt;（以及更好的 Fatal Traces）– 可观测性升级&lt;/h2&gt;
&lt;p&gt;框架/基础设施开发者可以庆祝一下了：现在可以 introspect 当前活动的 exception handler。你可以 chain、wrap、restore 或 decorate，不用脆弱地摆弄全局状态。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$previous&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;get_exception_handler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;set_exception_handler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name type-declaration&quot;&gt;Throwable&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$previous&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;logToSentry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$previous&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$previous&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;再配合更丰富的 fatal error stack traces，生产 post-mortem 会快很多。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. Intl Enhancements – 对人友好的列表和方向&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;IntlListFormatter&lt;/code&gt; 可以渲染优雅、locale-aware 的 conjunctions/disjunctions，不用手写胶水逻辑。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$f&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;IntlListFormatter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;pt_PT&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;conjunction&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;Lisboa&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;Porto&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;Coimbra&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// &quot;Lisboa, Porto e Coimbra&quot;&lt;/span&gt;

&lt;span class=&quot;token variable&quot;&gt;$fOr&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;IntlListFormatter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;en_US&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;disjunction&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$fOr&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;apples&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;bananas&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;cherries&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// &quot;apples, bananas, or cherries&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;结合 &lt;code class=&quot;language-text&quot;&gt;Locale::isRightToLeft()&lt;/code&gt;（或 &lt;code class=&quot;language-text&quot;&gt;locale_is_right_to_left()&lt;/code&gt;），可以自动切换布局方向。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. Grapheme-Aware Levenshtein – 真实用户字符串距离&lt;/h2&gt;
&lt;p&gt;当用户输入 emoji、重音符、组合字符时，按 byte 或朴素 codepoint 算距离都会撒谎。&lt;code class=&quot;language-text&quot;&gt;grapheme_levenshtein()&lt;/code&gt; 尊重的是&lt;strong&gt;可见&lt;/strong&gt;字符。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token function&quot;&gt;grapheme_levenshtein&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;café&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;cafe&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 0 – visually same after accent&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;搜索建议、fuzzy match 和容错登录流程会变得更符合语言现实。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10. 打磨巡游&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Directory Object：&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;opendir()&lt;/code&gt; 现在会给你一个真正的 object（type safety、未来扩展），不再是 legacy resource。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;cURL Enhancements：&lt;/strong&gt; 更好的 share handles + multi-handle introspection = 长生命周期 worker（想想 RoadRunner、Swoole）里更好的连接复用，以及更细粒度的性能调优。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;PHP_BUILD_DATE&lt;/code&gt;：&lt;/strong&gt; 快速检查“这个 binary 有多旧？”的工具，适合审计脚本。非常适合确保 fleet nodes 没有悄悄落后。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Feature Synergy Cheat Sheet&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;目标&lt;/th&gt;
&lt;th&gt;组合&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;带强制使用的 transformation pipeline&lt;/td&gt;
&lt;td&gt;`&lt;/td&gt;
&lt;td&gt;&gt;&lt;code class=&quot;language-text&quot;&gt;+&lt;/code&gt;#[\NoDiscard]`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;声明式 validation / strategy maps&lt;/td&gt;
&lt;td&gt;Constant-expression static closures + constant attributes&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;更安全地重构 legacy arrays&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;array_first()/array_last()&lt;/code&gt; + strict return typing&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生产事故排查&lt;/td&gt;
&lt;td&gt;Better fatal stack traces + &lt;code class=&quot;language-text&quot;&gt;php --ini=diff&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;get_exception_handler()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;国际化 UX 打磨&lt;/td&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;IntlListFormatter&lt;/code&gt; + direction detection + grapheme distance&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;Practical Adoption Plan&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;逐步引入 Pipe Operator&lt;/strong&gt;：从纯数据 normalization 层开始；在 code review 里建立风格约束（一条 pipeline 只在尾部有一个 side effect）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;给关键 API 标注 &lt;code class=&quot;language-text&quot;&gt;#[\NoDiscard]&lt;/code&gt;&lt;/strong&gt;：先关注 security、persistence 和 builders，在 CI 里测 warning counts。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重构 Strategy Tables&lt;/strong&gt;：把简单 callable maps 移进带 static closures 的 &lt;code class=&quot;language-text&quot;&gt;public const&lt;/code&gt; arrays，获得零启动成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Config Drift Checks&lt;/strong&gt;：加一个 CI job 捕获 &lt;code class=&quot;language-text&quot;&gt;php --ini=diff&lt;/code&gt; 输出；对异常变更报警。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metadata Sweep&lt;/strong&gt;：用 deprecation / units / feature flags 标注 constants，喂给内部工具。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Array Edge Extraction Cleanup&lt;/strong&gt;：用 codemod 替换操纵指针的模式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error Handler Layering&lt;/strong&gt;：用 &lt;code class=&quot;language-text&quot;&gt;get_exception_handler()&lt;/code&gt; 包住现有全局 handlers，提升可观测性（Sentry/new relic instrumentation）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;i18n Enhancements&lt;/strong&gt;：把手写 “list glue” 换成 &lt;code class=&quot;language-text&quot;&gt;IntlListFormatter&lt;/code&gt;；测试 RTL 布局自动选择。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fuzzy Matching Quality&lt;/strong&gt;：在出现用户生成的多语言文本处（搜索、标签）benchmark grapheme vs classic distance。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime Audit Script&lt;/strong&gt;：每天记录 &lt;code class=&quot;language-text&quot;&gt;PHP_BUILD_DATE&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;php --ini=diff&lt;/code&gt;，用来发现老化的 containers。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;Pitfalls &amp;#x26; Gotchas&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;小心什么&lt;/th&gt;
&lt;th&gt;缓解方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pipe operator misuse&lt;/td&gt;
&lt;td&gt;pipeline 中途有 side-effects&lt;/td&gt;
&lt;td&gt;限制到 pure funcs，直到最后阶段&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;#[\NoDiscard]&lt;/code&gt; overuse&lt;/td&gt;
&lt;td&gt;噪音疲劳（warning blindness）&lt;/td&gt;
&lt;td&gt;只用于&lt;em&gt;语义上关键&lt;/em&gt;的返回值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static closure limits&lt;/td&gt;
&lt;td&gt;需要 captured context&lt;/td&gt;
&lt;td&gt;显式传 context，或用返回 closure 的 factory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Constant attributes sprawl&lt;/td&gt;
&lt;td&gt;元数据碎片化&lt;/td&gt;
&lt;td&gt;建立内部 attribute 命名约定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i18n list formatting&lt;/td&gt;
&lt;td&gt;假设了标点样式&lt;/td&gt;
&lt;td&gt;每个 locale 做 snapshot tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;“Show Me” Mini Playground&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;#[&lt;/span&gt;&lt;span class=&quot;token attribute-content&quot;&gt;&lt;span class=&quot;token attribute-class-name class-name&quot;&gt;NoDiscard&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;Hash must be stored or compared&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;password_hash_safe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword type-hint&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$plain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword return-type&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;password_hash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$plain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;PASSWORD_DEFAULT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;sanitize_email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword type-hint&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$raw&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword return-type&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;strtolower&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$raw&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token variable&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token keyword type-declaration&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sanitize_email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;strlen&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$email&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$email&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;InvalidArgumentException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;Too short&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendEmail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Each stage consumes prior result – no discard.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name-definition class-name&quot;&gt;Rules&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;VALIDATORS&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;title&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword type-hint&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$v&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$v&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;slug&apos;&lt;/span&gt;  &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword type-hint&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$v&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword type-casting&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;preg_match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;/^[a-z0-9-]+$/&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$v&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name static-context&quot;&gt;Rules&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;VALIDATORS&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$field&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$check&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$check&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$field&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RuntimeException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string double-quoted-string&quot;&gt;&quot;Invalid &lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$field&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr&gt;
&lt;h2&gt;什么时候&lt;strong&gt;不要&lt;/strong&gt;伸手拿发亮的新东西&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一个简单到不行的 transform？&lt;/strong&gt; pipe 可能有点过头；&lt;code class=&quot;language-text&quot;&gt;strtolower($x)&lt;/code&gt; 仍然没问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context-heavy closures？&lt;/strong&gt; 带 dependency injection 的常规方法 &gt; static closure hacks。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legacy codebase mid-upgrade？&lt;/strong&gt; 一次只引入一个功能，避免认知翻搅。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Mental Model Recap&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;核心心智模型&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;`&lt;/td&gt;
&lt;td&gt;&gt;`&lt;/td&gt;
&lt;td&gt;Linear value threading；消除 nesting 和 temp vars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;#[\NoDiscard]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;强制&lt;em&gt;有意&lt;/em&gt;消费（使用，或用 &lt;code class=&quot;language-text&quot;&gt;(void)&lt;/code&gt; 忽略）&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static closure constants&lt;/td&gt;
&lt;td&gt;加载时准备好的 immutable strategy registry&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attributes on constants&lt;/td&gt;
&lt;td&gt;面向 tooling 和 policies 的一等 metadata channel&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;array_first()/last()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;声明式、非 mutating 的边缘访问&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;php --ini=diff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相对默认 baseline 的 config delta lens&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code class=&quot;language-text&quot;&gt;get_exception_handler()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Introspect 并 wrap 全局 exception flow&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intl additions&lt;/td&gt;
&lt;td&gt;用内建 locale intelligence 替代手写 glue&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grapheme distance&lt;/td&gt;
&lt;td&gt;对人眼感知的字符做操作，而不是原始 codepoints&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build &amp;#x26; resource polish&lt;/td&gt;
&lt;td&gt;渐进式 standardization 和 introspection&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;Final Vibes&lt;/h2&gt;
&lt;p&gt;PHP 8.5 没有用范式转变冲你大喊大叫；它是在&lt;em&gt;低声&lt;/em&gt;给出一连串不屈不挠的人体工学改进。只看 pipe operator + &lt;code class=&quot;language-text&quot;&gt;#[\NoDiscard]&lt;/code&gt; 这个组合，就会把代码轻轻推向更清晰的意图。再撒一点编译期 closures 和 constant attributes，你的 frameworks/components 会变得更声明式、更明确、更容易发现。砰砰砰，发吧。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;轮到你了：&lt;/strong&gt; 选一个功能（大概率是 pipe），在一个小模块里外科手术式地应用它，在 code review 反馈里衡量清晰度，然后扩展。动量胜过 big-bang rewrites。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;保持玩心，勇敢重构。是的，在遇到那些 “Wait, WHAT?!” 时刻时，也给你的 Taylors 发条消息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Happy coding.&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[什么才是“好的”代码覆盖率？一份现实世界指南]]></title><description><![CDATA[什么才是“好的”代码覆盖率？我在现实世界里用来挡住 bug、又不浪费工程时间的指南 每次我运行  或 ，同一个问题都会冒出来： “好吧……74 %。够了吗？” 软件开发博客圈喊着 "100 % or nothing!" 与此同时，launchdarkly.com…]]></description><link>https://bdteo.com/zh/what-is-good-code-coverage-real-world-guide/</link><guid isPermaLink="false">https://bdteo.com/zh/what-is-good-code-coverage-real-world-guide/</guid><pubDate>Tue, 15 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;什么才是“好的”代码覆盖率？我在现实世界里用来挡住 bug、又不浪费工程时间的指南&lt;/h1&gt;
&lt;p&gt;每次我运行 &lt;code class=&quot;language-text&quot;&gt;npm run coverage&lt;/code&gt; 或 &lt;code class=&quot;language-text&quot;&gt;phpunit --coverage&lt;/code&gt;，同一个问题都会冒出来：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“好吧……74 %。够了吗？”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;软件开发博客圈喊着 &quot;100 % or nothing!&quot; 与此同时，&lt;a href=&quot;https://launchdarkly.com/blog/code-coverage-what-it-is-and-why-it-matters/&quot; target=&quot;_blank&quot;&gt;launchdarkly.com&lt;/a&gt; 很礼貌地提醒我：100 % executed ≠ 100 % tested。
我花过几个星期追逐这个闪亮指标，也花过更多星期调试_别的_问题。下面是我最后落脚的、经过现场验证的中间地带。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;为什么 100 % 覆盖率是一场海市蜃楼&lt;/h2&gt;
&lt;p&gt;理论上，100 % 行执行意味着&quot;没有隐藏 bug&quot;。实践中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;收益递减：从 90 %→95 % 常常会让测试套件翻倍，却只带来个位数的风险降低。&lt;/li&gt;
&lt;li&gt;虚假的信心：一个没有 assertion、只是调用函数的测试，&lt;strong&gt;仍然算&lt;/strong&gt;覆盖。&lt;/li&gt;
&lt;li&gt;商业现实：每多写一个测试，就是一段&lt;strong&gt;没有&lt;/strong&gt;用在客户要求功能上的时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;航空航天那帮人可以瞄准 100 %，那是生死问题。对我们其他人来说，&lt;strong&gt;~80 % 是 80/20 线&lt;/strong&gt;。大多数项目在算过 ROI 后都会聚在这里。&lt;a href=&quot;https://www.testdevlab.com/blog/why-is-high-test-coverage-important&quot; target=&quot;_blank&quot;&gt;testdevlab.com&lt;/a&gt; 出于同样的原因，把范围称为 70–90 %。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;我使用的实用表格&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;th&gt;我的翻译&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100 %&lt;/td&gt;
&lt;td&gt;“我们是一个会飞火箭的 library”&lt;/td&gt;
&lt;td&gt;接受折磨。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90 % +&lt;/td&gt;
&lt;td&gt;“很多钱都依赖它的 library”&lt;/td&gt;
&lt;td&gt;只用于高优先级模块。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80 %&lt;/td&gt;
&lt;td&gt;发出去，监控，然后迭代。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60–70 %&lt;/td&gt;
&lt;td&gt;Merge gate——如果新代码把你拉到线下，就让 PR 失败。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;#x3C; 50 %&lt;/td&gt;
&lt;td&gt;一个周末的技术债——先转向关键路径。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这些数字我从 &lt;a href=&quot;https://www.atlassian.com/continuous-delivery/software-testing/code-coverage&quot; target=&quot;_blank&quot;&gt;Atlassian 的内部指南&lt;/a&gt; 偷来的：60 % 是 &quot;acceptable&quot;，75 % 是 &quot;commendable&quot;，90 % 是 &quot;exemplary&quot;。每次 retro 都好用。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;我如何不哭着打到 80 %（TypeScript playbook）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Jest + Istanbul 开箱即用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI 中的 coverage gate&lt;/strong&gt;
在 &lt;code class=&quot;language-text&quot;&gt;jest.config.js&lt;/code&gt; 里我会加：
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token literal-property property&quot;&gt;coverageThreshold&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;global&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&apos;**/src/core/**&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;90&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;盯住用户热路径，不要盯 Redux boilerplate logger。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;我如何在 Laravel 中打到 80 %（PHP playbook）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;开发环境安装 PCOV 提速度，CI 中用 Xdebug 拿 branch data。&lt;/li&gt;
&lt;li&gt;PHPUnit + &lt;code class=&quot;language-text&quot;&gt;phpunit.xml&lt;/code&gt; 中这些默认项：
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;xml&quot;&gt;&lt;pre class=&quot;language-xml&quot;&gt;&lt;code class=&quot;language-xml&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;whitelist&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;processUncoveredFiles&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;true&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;directory&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;.php&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;./src&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;directory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;whitelist&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;Mutation score &gt; line count，使用 &lt;a href=&quot;https://infection.github.io/&quot; target=&quot;_blank&quot;&gt;Infection&lt;/a&gt;。这就是我发现&quot;覆盖了但其实没测到&quot;的行的方法。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;我团队奉行的 4 条规则&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;新代码 = 测试。&lt;/strong&gt; 合并前 diff coverage ≥ 90 %。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先重构，再测试。&lt;/strong&gt; 不可测试的代码已经是债。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;让 build 失败，不要让人失败。&lt;/strong&gt; 每年把 gate 降低 5 %，也比用一片红色 dashboard 压垮团队好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;衡量生产环境里的 bug&lt;/strong&gt;——如果覆盖率是 85 %，但 incident 激增，罪魁祸首不是&lt;strong&gt;覆盖率&lt;/strong&gt;，而是&lt;strong&gt;断言&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;TL;DR（给 execs 和 recruiters 也一样）&lt;/h2&gt;
&lt;p&gt;别问我要一个&quot;神奇数字&quot;。问这个：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;产品的哪些部分不能坏？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;把&lt;strong&gt;那些&lt;/strong&gt;覆盖到 90 %。其他部分给健康的 smoke tests。把代码覆盖率当作一束&lt;strong&gt;聚光灯&lt;/strong&gt;，不是终点线；相信你&lt;strong&gt;抓到&lt;/strong&gt;的 bug，不要相信你&lt;strong&gt;炫耀&lt;/strong&gt;的数字。&lt;/p&gt;
&lt;p&gt;让 coverage dashboard 变绿吧。你的客户永远不会看见它，但他们的 error bar 会保持空白。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;—— 吐槽结束，回编辑器。&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[我的有效 Pull Request Review 核心指南]]></title><description><![CDATA[作为一个经常写代码、也经常 review 代码的人，我逐渐明白，pull request（PR）review 不只是查 bug。它关乎共同所有权、知识传递，以及一起写出更好的代码。下面是一份简洁、实用的指南，让 PR 更有价值，也少一点痛苦。 1. 好 review…]]></description><link>https://bdteo.com/zh/essential-guide-effective-pull-request-reviews/</link><guid isPermaLink="false">https://bdteo.com/zh/essential-guide-effective-pull-request-reviews/</guid><pubDate>Sun, 06 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;作为一个经常写代码、也经常 review 代码的人，我逐渐明白，pull request（PR）review 不只是查 bug。它关乎共同所有权、知识传递，以及一起写出更好的代码。下面是一份简洁、实用的指南，让 PR 更有价值，也少一点痛苦。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 好 review 的目标&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;关注改进，而不是完美&lt;/strong&gt;
完美代码并不现实。目标是&lt;em&gt;更好&lt;/em&gt;的代码。如果一个 PR 改进了可读性、可维护性或正确性，即使还有些小的风格调整，也可以 approve。用 “Nit:” 标记可选建议。  &lt;a href=&quot;https://google.github.io/eng-practices/review/reviewer/standard.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;google.github.io&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;共享所有权与 mentoring&lt;/strong&gt;
把 PR 当作共同的代码。留下有教育意义的反馈（“Nit: you could use X here…”），指导初级开发者，也保持愿意向他们学习的开放态度。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. Review 前的准备&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;作者&lt;/strong&gt;：Self-review：运行测试、linter 和 formatter。在 PR 描述中提供上下文，并给复杂逻辑加注释。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reviewer&lt;/strong&gt;：先读描述。先理解“为什么”，再深入代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. 让 PR 保持小而聚焦&lt;/h2&gt;
&lt;p&gt;数据显示，超过约 400 LOC 和约 60 分钟后，review 质量会明显下降。  &lt;a href=&quot;https://www.atlassian.com/blog/add-ons/code-review-best-practices?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;atlassian.com&lt;/a&gt; &lt;a href=&quot;https://www.devzery.com/post/guide-to-best-code-review-practices?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;devzery.com&lt;/a&gt; &lt;a href=&quot;https://mikeconley.ca/blog/2009/09/14/smart-bear-cisco-and-the-largest-study-on-code-review-ever/?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;mikeconley.ca&lt;/a&gt;
&lt;strong&gt;准则&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 PR 保持在 200–400 LOC 以下。  &lt;a href=&quot;https://mikeconley.ca/blog/2009/09/14/smart-bear-cisco-and-the-largest-study-on-code-review-ever/?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;mikeconley.ca&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;每次 review 控制在 60 分钟以内。&lt;/li&gt;
&lt;li&gt;对于大功能，使用 stacked PR（DB → API → UI）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4. 用心分配 Reviewer&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一个主要 reviewer&lt;/strong&gt;，最好具备相关领域知识。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最多两个 reviewer&lt;/strong&gt;，避免责任扩散。  &lt;a href=&quot;https://support.smartbear.com/collaborator/docs/working-with/concepts/optimal-size.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;support.smartbear.com&lt;/a&gt; &lt;a href=&quot;https://abseil.io/resources/swe-book/html/ch09.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;abseil.io&lt;/a&gt; &lt;a href=&quot;https://slab.com/library/templates/google-code-review/?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;slab.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;轮换 reviewer，用于交叉培训，也让 bus-factor 更健康。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;5. PR 里要检查什么&lt;/h2&gt;
&lt;p&gt;使用这份心智清单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;正确性：它是否满足需求并处理边界情况？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设计&lt;/strong&gt;：结构是否良好，是否符合惯用写法？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可读性&lt;/strong&gt;：命名清晰，逻辑简单，风格一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;/strong&gt;：验证输入，清理输出，避免泄漏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能&lt;/strong&gt;：留意重循环和 N+1 查询。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试&lt;/strong&gt;：覆盖核心场景、边界场景和错误场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合规&lt;/strong&gt;：文档、CI、license、格式化是否到位。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这能帮助我们更早捕捉更多问题，尤其是可维护性问题。  &lt;a href=&quot;https://google.github.io/eng-practices/review/reviewer/standard.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;google.github.io&lt;/a&gt; &lt;a href=&quot;https://developers.google.com/blockly/guides/contribute/get-started/pr_review_process?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;developers.google.com&lt;/a&gt; &lt;a href=&quot;https://google.github.io/eng-practices/review/?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;google.github.io&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. 利用自动化&lt;/h2&gt;
&lt;p&gt;让工具处理琐碎工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linters（ESLint、RuboCop、SonarQube）&lt;/li&gt;
&lt;li&gt;Formatters（Prettier、Black）&lt;/li&gt;
&lt;li&gt;带测试、覆盖率和安全检查的 CI pipelines&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样人类 reviewer 才能把注意力放在逻辑、架构和细微判断上。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. 提供建设性、友善的反馈&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;保持尊重。评点代码和建议，不评点人。&lt;/li&gt;
&lt;li&gt;表扬做得好的地方。&lt;/li&gt;
&lt;li&gt;可执行：解释&lt;em&gt;为什么&lt;/em&gt;，并建议&lt;em&gt;怎么做&lt;/em&gt;。&lt;/li&gt;
&lt;li&gt;给非阻塞项加上 “Nit:” 或 “Optional:” 前缀。  &lt;a href=&quot;https://www.atlassian.com/blog/loom/code-review-best-practices-2?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;atlassian.com&lt;/a&gt; &lt;a href=&quot;https://google.github.io/eng-practices/review/reviewer/standard.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;google.github.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;让讨论保持客观（“we” &gt; “you”）。避免个人批评。&lt;/li&gt;
&lt;li&gt;如果来回讨论卡住了，建议同步聊一下。  &lt;a href=&quot;https://www.atlassian.com/blog/loom/code-review-best-practices-2?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;atlassian.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;8. 衡量流程，而不是衡量人&lt;/h2&gt;
&lt;p&gt;值得跟踪趋势的关键指标（不要用来评价个人）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Turnaround time&lt;/strong&gt;（PR 打开 → merge）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inspection rate&lt;/strong&gt;（&amp;#x3C; 300–500 LOC/hr 较佳）  &lt;a href=&quot;https://www.atlassian.com/blog/loom/code-review-best-practices-2?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;atlassian.com&lt;/a&gt; &lt;a href=&quot;https://developers.google.com/blockly/guides/contribute/get-started/pr_review_process?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;developers.google.com&lt;/a&gt; &lt;a href=&quot;https://mikeconley.ca/blog/2009/09/14/smart-bear-cisco-and-the-largest-study-on-code-review-ever/?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;mikeconley.ca&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Defect density&lt;/strong&gt;（每 LOC 的问题数）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review coverage&lt;/strong&gt;（跨组件覆盖情况）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Follow-up commit count&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用这些洞见改进工作流，比如强调更小的 PR、改进文档，或者围绕棘手模块做教育。但永远不要把这些指标绑定到绩效评估上。  &lt;a href=&quot;https://mikeconley.ca/blog/2009/09/14/smart-bear-cisco-and-the-largest-study-on-code-review-ever/?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;mikeconley.ca&lt;/a&gt; &lt;a href=&quot;https://google.github.io/eng-practices/review/reviewer/standard.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;google.github.io&lt;/a&gt; &lt;a href=&quot;https://bssw.io/items/google-guidance-on-code-review?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;bssw.io&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. 针对不同语言的注意事项&lt;/h2&gt;
&lt;p&gt;不同范式需要不同侧重点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PHP/JavaScript/TS&lt;/strong&gt;：异步处理、XSS、SOLID 原则&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python&lt;/strong&gt;：资源管理（&lt;code class=&quot;language-text&quot;&gt;with&lt;/code&gt;）、PEP 8、默认参数陷阱&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Haskell/Scala functional&lt;/strong&gt;：类型签名、纯度、不可变性、macro 检查&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C/C++&lt;/strong&gt;：内存安全、指针、RAII&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Java&lt;/strong&gt;：Null-safety、干净的并发、SOLID&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lisp&lt;/strong&gt;：Macro 文档、动态类型、惯用模式&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据你的技术栈调整清单。遇到不熟悉的语言，就让专家参与。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Bonus：推荐深读资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Google’s &lt;em&gt;The Standard of Code Review&lt;/em&gt;&lt;/strong&gt; – 关于代码健康和 mentorship 的理念。  &lt;a href=&quot;https://google.github.io/eng-practices/review/reviewer/standard.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;google.github.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google Code Review Developer Guide&lt;/strong&gt; – 清单式指南。  &lt;a href=&quot;https://bssw.io/items/google-guidance-on-code-review?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;bssw.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SmartBear/Cisco study&lt;/strong&gt; – 关于 PR 大小和时间安排的实证发现。  &lt;a href=&quot;https://mikeconley.ca/blog/2009/09/14/smart-bear-cisco-and-the-largest-study-on-code-review-ever/?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;mikeconley.ca&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Atlassian “5 Code Review Best Practices”&lt;/strong&gt; – 实用的风格和团队协作建议。  &lt;a href=&quot;https://www.atlassian.com/blog/add-ons/code-review-best-practices?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;atlassian.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blockly PR Flow&lt;/strong&gt; – 真实世界中的分阶段 review 流程。  &lt;a href=&quot;https://developers.google.com/blockly/guides/contribute/get-started/pr_review_process?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;developers.google.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;最后的想法&lt;/h2&gt;
&lt;p&gt;做得好的 PR review 不只是质量关卡。它们是学习、协作和工程卓越的发动机。把尊重的文化、聪明的工具、数据驱动的流程和用心的反馈结合起来，code review 就会变成有价值的讨论，而不是杂务。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;祝 review 顺利！&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;欢迎留言，或者直接联系我。如果你想继续深入，或想分享自己的 review 技巧。&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Apple Watch 吸烟检测：构建 Still Mirror（Swift、SWT）]]></title><description><![CDATA[真正理解我们的习惯，尤其是那些几乎无意识完成的习惯，一直让我着迷。如果可穿戴设备能给这些模式提供一面温和、不评判的镜子，会怎样？这个问题点燃了 “Still Mirror” 项目：尝试利用 Apple Watch…]]></description><link>https://bdteo.com/zh/apple-watch-still-mirror-swift-swt-passive-smoking-detection/</link><guid isPermaLink="false">https://bdteo.com/zh/apple-watch-still-mirror-swift-swt-passive-smoking-detection/</guid><pubDate>Tue, 13 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;真正理解我们的习惯，尤其是那些几乎无意识完成的习惯，一直让我着迷。如果可穿戴设备能给这些模式提供一面温和、不评判的镜子，会怎样？这个问题点燃了 “Still Mirror” 项目：尝试利用 Apple Watch 丰富的生理数据，被动检测吸烟或吸电子烟事件，而不要求用户手动输入。这不是为了再做一个戒烟应用，而是一个用于纯粹、未被掺杂的觉察的工具。&lt;/p&gt;
&lt;h2&gt;挑战：噪声交响中的一声低语&lt;/h2&gt;
&lt;p&gt;核心挑战非常大：你怎样把一次吸烟/吸电子烟事件的细微生理特征，从无数日常活动和身体反应中区分出来？压力、快走、突如其来的噪声，甚至一杯咖啡，都可能让心率（HR）和心率变异性（HRV）短暂变化。我们要寻找的信号，常常只是生理噪声交响里的一声低语。&lt;/p&gt;
&lt;p&gt;但要真正分离这些转瞬即逝的事件，我需要一种更精细的信号处理技术。&lt;/p&gt;
&lt;figure&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 630px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;https://bdteo.com/static/6d861aafdcf69be2e1d02780744e708e/ac99c/apple-watch-swift-xcode.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.45569620253164%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAANABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAMFAgT/xAAWAQEBAQAAAAAAAAAAAAAAAAAAAQL/2gAMAwEAAhADEAAAAZyKfJm5GB//xAAaEAACAgMAAAAAAAAAAAAAAAACAwESABAT/9oACAEBAAEFAmNYJxFo44xYmRKrr//EABYRAQEBAAAAAAAAAAAAAAAAAAASAf/aAAgBAwEBPwGdS//EABYRAQEBAAAAAAAAAAAAAAAAAAASAf/aAAgBAgEBPwGsU//EABwQAAICAgMAAAAAAAAAAAAAAAABAhEQIRIxMv/aAAgBAQAGPwKS5vscnI9Iutmnj//EABoQAAMBAAMAAAAAAAAAAAAAAAABESExUcH/2gAIAQEAAT8hR2CEEFu0o3xo7Ii8RNpc0//aAAwDAQACAAMAAAAQoM//xAAWEQEBAQAAAAAAAAAAAAAAAAAAARH/2gAIAQMBAT8QsmH/xAAVEQEBAAAAAAAAAAAAAAAAAAABEP/aAAgBAgEBPxAk/8QAGhABAAMBAQEAAAAAAAAAAAAAAQARIUExof/aAAgBAQABPxCxnQbeXkvYggGt9mQlKX2Xij1PYGqX6dfYUGw6z//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;开发者工作站，Xcode 中显示 Apple Watch 应用的 Swift 代码，背景里有 HealthKit 数据图表。&quot; title=&quot;&quot; src=&quot;https://bdteo.com/static/6d861aafdcf69be2e1d02780744e708e/828fb/apple-watch-swift-xcode.jpg&quot; srcset=&quot;https://bdteo.com/static/6d861aafdcf69be2e1d02780744e708e/ff44c/apple-watch-swift-xcode.jpg 158w,
https://bdteo.com/static/6d861aafdcf69be2e1d02780744e708e/a6688/apple-watch-swift-xcode.jpg 315w,
https://bdteo.com/static/6d861aafdcf69be2e1d02780744e708e/828fb/apple-watch-swift-xcode.jpg 630w,
https://bdteo.com/static/6d861aafdcf69be2e1d02780744e708e/0ede0/apple-watch-swift-xcode.jpg 945w,
https://bdteo.com/static/6d861aafdcf69be2e1d02780744e708e/3ac88/apple-watch-swift-xcode.jpg 1260w,
https://bdteo.com/static/6d861aafdcf69be2e1d02780744e708e/ac99c/apple-watch-swift-xcode.jpg 1536w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;figcaption&gt;图 1. – Apple 开发生态：Xcode、Swift 和 HealthKit 是让 Still Mirror 在 Apple Watch 上活起来的核心。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2&gt;选择工具箱：Apple 生态与 Swift&lt;/h2&gt;
&lt;p&gt;对于一个以 Apple Watch 为目标的平台项目，生态选择很清楚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Xcode 和 Swift：&lt;/strong&gt; Apple 平台的原生开发环境。开始这个项目意味着更深入地进入 Swift——一门我觉得优雅而强大的语言——也意味着在 Xcode 的各种细节里摸索。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HealthKit：&lt;/strong&gt; Apple 的框架是通往关键数据流的入口：心率、HRV（SDNN/RMSSD）、SpO2（对于燃烧式吸烟与电子烟的区分尤其相关）以及活动水平。对于处理如此敏感数据的应用来说，HealthKit 以隐私为中心的设计至关重要。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;watchOS 限制：&lt;/strong&gt; 为手表开发，意味着必须不断在功能和资源约束之间平衡——电池寿命和后台处理能力永远在脑子里。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;算法的心脏：平稳小波变换（SWT）&lt;/h2&gt;
&lt;p&gt;传统时间序列分析常常难以处理非平稳信号——也就是统计性质（比如均值和方差）会随时间变化的信号。生理数据出了名地非平稳。这正是**平稳小波变换（Stationary Wavelet Transform，SWT）**派上用场的地方。&lt;/p&gt;
&lt;p&gt;标准离散小波变换（DWT）是移位敏感的，也就是说输入信号里一个很小的时间偏移，可能会显著改变小波系数。SWT 则是移位不变的。对于那些事件发生的精确时间很关键、但又可能略有浮动的信号，它因此更稳健。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么这个问题适合 SWT？&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;时频定位：&lt;/strong&gt; SWT 可以把信号分解到不同频带，同时保留时间信息。这意味着我们可以寻找在精确时刻出现的特定频率特征，比如 HR 中突然爆发的高频活动，或者 HRV 频带中的特定变化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;去噪：&lt;/strong&gt; 生理信号很吵。SWT 可以通过分析不同尺度上的小波系数，帮助把底层的“真实”信号和随机噪声分开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;瞬态事件检测：&lt;/strong&gt; 它特别擅长识别信号中的突变、尖峰或短暂事件，而这正是尼古丁摄入后的急性生理反应可能呈现出来的东西。&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 630px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;https://bdteo.com/static/b2719b44c36d6548b24505286c03c80f/ac99c/stationary-wavelet-transform-visualization.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.45569620253164%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAANABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECBf/EABYBAQEBAAAAAAAAAAAAAAAAAAIBA//aAAwDAQACEAMQAAAB5LFeMRF//8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQABBQJf/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFRABAQAAAAAAAAAAAAAAAAAAIEH/2gAIAQEABj8Ci//EABkQAQEAAwEAAAAAAAAAAAAAAAEAECFhMf/aAAgBAQABPyEeReEu/CGXH//aAAwDAQACAAMAAAAQZC//xAAVEQEBAAAAAAAAAAAAAAAAAAAQEf/aAAgBAwEBPxCH/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAHBABAAIBBQAAAAAAAAAAAAAAAQARQRAhcaGx/9oACAEBAAE/EBA94eXhNquNCzHcXef/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;抽象可视化：一个生理信号被平稳小波变换分解成多个频带。&quot; title=&quot;&quot; src=&quot;https://bdteo.com/static/b2719b44c36d6548b24505286c03c80f/828fb/stationary-wavelet-transform-visualization.jpg&quot; srcset=&quot;https://bdteo.com/static/b2719b44c36d6548b24505286c03c80f/ff44c/stationary-wavelet-transform-visualization.jpg 158w,
https://bdteo.com/static/b2719b44c36d6548b24505286c03c80f/a6688/stationary-wavelet-transform-visualization.jpg 315w,
https://bdteo.com/static/b2719b44c36d6548b24505286c03c80f/828fb/stationary-wavelet-transform-visualization.jpg 630w,
https://bdteo.com/static/b2719b44c36d6548b24505286c03c80f/0ede0/stationary-wavelet-transform-visualization.jpg 945w,
https://bdteo.com/static/b2719b44c36d6548b24505286c03c80f/3ac88/stationary-wavelet-transform-visualization.jpg 1260w,
https://bdteo.com/static/b2719b44c36d6548b24505286c03c80f/ac99c/stationary-wavelet-transform-visualization.jpg 1536w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;figcaption&gt;图 2. – 可视化平稳小波变换如何把信号随时间分解为组成它的频率成分，从而帮助识别模式。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;本质上，SWT 像一组更精细的滤镜，让我们能够在 HR、HRV 以及潜在的 SpO2 数据中“看见”那些可能被噪声或长期趋势遮住的模式。我们可以寻找特定小波子带里的典型“形状”或能量变化，它们对应着那一下生理冲击。&lt;/p&gt;
&lt;h2&gt;开发旅程：从数据到检测&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据采集（HealthKit）：&lt;/strong&gt; 从 HealthKit 建立可靠的后台数据获取，尊重用户权限，并高效处理数据更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;信号预处理：&lt;/strong&gt; 清理传入的 HR、HRV 和 SpO2 数据。这包括处理缺失数据点，也可能包括在应用 SWT 之前做一些初始滤波。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用 SWT：&lt;/strong&gt; 对生理时间序列的片段应用平稳小波变换。这涉及选择合适的母小波（例如 Daubechies、Symlet）和分解层级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从小波系数提取特征：&lt;/strong&gt; 魔法（以及大量实验）发生在这里。我们不直接看原始 HR/HRV 数值，而是分析 SWT 系数。相关特征可能包括：
&lt;ul&gt;
&lt;li&gt;在疑似事件发生时，特定细节系数频带中的能量。&lt;/li&gt;
&lt;li&gt;系数的统计性质（方差、峰度）。&lt;/li&gt;
&lt;li&gt;不同生理信号的小波系数之间的互相关（例如 HR 和 HRV）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检测逻辑/模型：&lt;/strong&gt; 一开始，这可能是一个基于规则的系统，寻找提取出的小波特征中的特定模式。例如：“在低体力活动期间，HR 细节系数在尺度 X 上出现显著能量尖峰，同时 HRV 细节系数在尺度 Y 上的能量急剧下降。”最终，它可以演进成一个用这些特征训练出来的机器学习模型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;置信度评分：&lt;/strong&gt; 正如我在 MVPS 算法里概述的那样，为每个检测到的事件生成置信度分数非常关键，它反映这个特征信号的强度和清晰度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;watchOS 应用实现：&lt;/strong&gt; 在 Apple Watch 上运行核心检测算法，并针对电池寿命优化，比如分批处理数据、智能触发分析。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iOS 配套应用：&lt;/strong&gt; 用于显示检测事件的时间线、提供洞察，并管理设置。这里的关键是通过 WatchConnectivity 同步数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;健康与伦理考量：“镜子”哲学&lt;/h2&gt;
&lt;p&gt;必须再次强调，“Still Mirror” 被设想为一个&lt;em&gt;觉察工具&lt;/em&gt;，不是医疗设备，也不是戒烟计划。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;隐私优先：&lt;/strong&gt; 所有处理，尤其是敏感的算法工作，理想情况下都应在设备本地完成。HealthKit 数据访问严格基于权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不评判：&lt;/strong&gt; 应用界面以及它提供的任何洞察都必须保持中性，只是反映模式，不给处方式建议，也不羞辱用户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;准确性与透明度：&lt;/strong&gt; 用户需要理解应用的局限。在这种复杂的被动检测里，误报和漏报不可避免。透明呈现检测的置信度很重要。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;赋能用户：&lt;/strong&gt; 目标是把关于用户自身身体和习惯的数据交给他们，让他们能做出自己的知情决定。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;学习 Swift，并穿过 Apple 生态&lt;/h2&gt;
&lt;p&gt;对于主要来自其他背景的开发者（比如我自己的 PHP/Laravel 根系）来说，进入 Swift、SwiftUI、Xcode，以及 watchOS 开发的具体约束，是一条很明显的学习曲线。Apple 的框架有一种独特的哲学。管理应用生命周期、后台任务、HealthKit 查询和设备间通信（WatchConnectivity），都有各自特定的模式和“Apple 式”的做法。不过，丰富的文档、强大的社区，以及 Swift 本身的能力，让这段旅程很值得。&lt;/p&gt;
&lt;h2&gt;结论：一个沉默观察者的潜力&lt;/h2&gt;
&lt;p&gt;“Still Mirror” 仍然是一次探索，一项困难的尝试：推动消费级可穿戴设备上的被动感知能做到什么。平稳小波变换为拆解复杂生理信号、发现我们正在寻找的细微信号，提供了一条很有希望的路径。&lt;/p&gt;
&lt;p&gt;这段旅程不只是用 Swift 写代码、和 Xcode 较劲，也包括深入信号处理理论、理解人体生理，并谨慎思考这种技术的伦理含义。无论 “Still Mirror” 最终成为一款被广泛使用的应用，还是停留为一次精密的技术探索，这个过程本身都证明了 AI、健康与个人技术交汇处有多迷人。它关乎试着建造那片安静、可反射的表面——一面 still mirror——让我们更清楚地看见自己。&lt;/p&gt;
&lt;p&gt;你怎么看用 SWT 这样的高级信号处理来做被动习惯检测？欢迎在下面的评论里告诉我你的想法。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[用 Bumble 和 API 32 修好 M1 Mac 上的 Android Emulator 蓝牙]]></title><description><![CDATA[如果你是在 M1/M2/M3 Mac 上做蓝牙相关开发，又想让 Android Emulator…]]></description><link>https://bdteo.com/zh/m1-mac-android-emulator-bluetooth-passthrough-bumble/</link><guid isPermaLink="false">https://bdteo.com/zh/m1-mac-android-emulator-bluetooth-passthrough-bumble/</guid><pubDate>Mon, 14 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;如果你是在 M1/M2/M3 Mac 上做蓝牙相关开发，又想让 Android Emulator 使用宿主机的蓝牙无线电，大概已经吃过一点苦头。看起来&lt;em&gt;应该&lt;/em&gt;很直接的事情，常常会变成一个让人恼火的洞：连接失败、错误信息晦涩、文档走到死路。我最近正好打完这场仗，撞了几堵墙之后，终于找到了一套使用 &lt;strong&gt;Bumble&lt;/strong&gt; Python 蓝牙栈的组合，它&lt;em&gt;真的能工作&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;这不是又一篇理论指南；这是一次逐步记录：哪些方案失败了，更重要的是，哪套方案&lt;em&gt;成功&lt;/em&gt;把我的 M1 Mac Pro 蓝牙（我这里通过外接 USB dongle，不过原理可能也适用于内置无线电）桥接到了 Android 12L（API 32）模拟器里。&lt;/p&gt;
&lt;h2&gt;目标：模拟器里的真实蓝牙&lt;/h2&gt;
&lt;p&gt;目标很简单：让 Android Emulator 使用我 Mac 的物理蓝牙控制器，而不是它自己有限的虚拟控制器。测试那些会和真实蓝牙设备交互的 App 时，这一点很关键。&lt;/p&gt;
&lt;h2&gt;工具：Bumble 上场&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/google/bumble&quot;&gt;Bumble&lt;/a&gt; 是一个强大的 Python 蓝牙栈。完成这件事的核心工具是 &lt;code class=&quot;language-text&quot;&gt;bumble-hci-bridge&lt;/code&gt;，它可以一边连接物理 HCI（Host Controller Interface），另一边通过各种传输方式（比如 TCP 或 gRPC）暴露出来。&lt;/p&gt;
&lt;h2&gt;尝试 #1：QEMU Socket 方法（合乎逻辑的第一步）&lt;/h2&gt;
&lt;p&gt;基于一般的 QEMU 知识和一些较老的指南，第一种思路是用 emulator flags，把一个虚拟串口（底层由 TCP socket 支撑）直接连接到 HCI bridge。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动 Bridge（TCP Server 模式）：&lt;/strong&gt; 我们把 Bumble 连到物理 dongle（在我的机器上，令人意外的是 &lt;code class=&quot;language-text&quot;&gt;usb:0&lt;/code&gt; 比它具体的 VID:PID &lt;code class=&quot;language-text&quot;&gt;usb:0b05:17cb&lt;/code&gt; 更好用，M1 的小脾气！），然后让它监听一个 TCP 端口。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# In Terminal 1:&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; python3 &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; bumble.apps.hci_bridge usb:0 tcp-server:0.0.0.0:6789
&lt;span class=&quot;token comment&quot;&gt;# Output showed &apos;&gt;&gt;&gt; connected&apos; twice - success connecting to USB and starting TCP server.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;带 QEMU Flags 启动模拟器：&lt;/strong&gt; 我们修改了模拟器启动脚本（最初目标是 API 34），加入 &lt;code class=&quot;language-text&quot;&gt;-qemu&lt;/code&gt; flags，把一个虚拟串口（&lt;code class=&quot;language-text&quot;&gt;virtserialport&lt;/code&gt;）指向一个字符设备（&lt;code class=&quot;language-text&quot;&gt;chardev&lt;/code&gt;），而这个字符设备由连接到 bridge 的 TCP socket 支撑。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Snippet from launch script:&lt;/span&gt;
emulator &lt;span class=&quot;token parameter variable&quot;&gt;-avd&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;avd_name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;. &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;-qemu&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;-device&lt;/span&gt; virtio-serial-device &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;-device&lt;/span&gt; virtserialport,chardev&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;bt,name&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;bt &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;-chardev&lt;/span&gt; socket,id&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;bt,host&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;localhost,port&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;6789&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;结果？部分成功，最终失败：&lt;/strong&gt; 通过 &lt;code class=&quot;language-text&quot;&gt;lsof&lt;/code&gt;，我们可以看到 emulator 的 QEMU 进程&lt;em&gt;确实&lt;/em&gt;和 Bumble bridge 建立了 TCP 连接！然而，模拟器&lt;em&gt;内部&lt;/em&gt;的 Android 蓝牙栈从未真正通过它发送任何 HCI 命令。在 Android 设置里切换蓝牙没有任何效果。初始连接之后，bridge 日志一直安静。&lt;strong&gt;死路。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;尝试 #2：默认 Netsim Bridge（按 Bumble 文档来）&lt;/h2&gt;
&lt;p&gt;Bumble 文档提到了桥接到模拟器的 “Netsim” gRPC 接口。Netsim（以及它的核心 Root Canal）是模拟器较新的虚拟蓝牙控制器系统。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动 Bridge（Netsim Controller 模式）：&lt;/strong&gt; 我们把 bridge 配成 Netsim controller，让它监听默认 gRPC 端口（8554），并连接到物理 dongle。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# In Terminal 1:&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; python3 &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; bumble.apps.hci_bridge android-netsim:_:8554,mode&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;controller usb:0
&lt;span class=&quot;token comment&quot;&gt;# Output showed &apos;&gt;&gt;&gt; connected&apos; twice - success connecting to USB and starting Netsim gRPC server.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动模拟器（默认后端）：&lt;/strong&gt; 我们把启动脚本恢复回去（仍在尝试 API 34），移除 &lt;code class=&quot;language-text&quot;&gt;-qemu&lt;/code&gt; flags，并加入 &lt;code class=&quot;language-text&quot;&gt;-packet-streamer-endpoint default&lt;/code&gt;，确保它尝试使用 Netsim 后端。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Snippet from launch script:&lt;/span&gt;
emulator &lt;span class=&quot;token parameter variable&quot;&gt;-avd&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;avd_name&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;. &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    -packet-streamer-endpoint default &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;结果？没有连接：&lt;/strong&gt; 这次模拟器启动了，但 Bumble bridge 完全没有显示来自模拟器的 gRPC 连接。检查模拟器日志也没有明显的连接错误，但蓝牙仍然不可用。&lt;strong&gt;又一条死路。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;尝试 #3：降级 API + 显式 Netsim 端点（赢家！）&lt;/h2&gt;
&lt;figure&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 630px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;https://bdteo.com/static/cd1aa56ebd8947295c8b2ab1adab5b06/ac99c/featured-2.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.45569620253164%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAANABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAQFAv/EABUBAQEAAAAAAAAAAAAAAAAAAAED/9oADAMBAAIQAxAAAAHDUViFaRAE/8QAGhABAAIDAQAAAAAAAAAAAAAAAgEDAAQREv/aAAgBAQABBQIvt+xPisWCQJ5LtlDP/8QAFhEBAQEAAAAAAAAAAAAAAAAAABJB/9oACAEDAQE/AcU//8QAFxEAAwEAAAAAAAAAAAAAAAAAAAESIf/aAAgBAgEBPwFrSD//xAAbEAADAAIDAAAAAAAAAAAAAAAAARECIhASIf/aAAgBAQAGPwJR6mvgu2SpSRcf/8QAGhABAAIDAQAAAAAAAAAAAAAAAQAhEBFBUf/aAAgBAQABPyEyA8RwUWv2PBtb0xCHI4dncf/aAAwDAQACAAMAAAAQHz//xAAWEQADAAAAAAAAAAAAAAAAAAABEBH/2gAIAQMBAT8QER//xAAYEQACAwAAAAAAAAAAAAAAAAAAEQExUf/aAAgBAgEBPxCRVQun/8QAGxABAAMBAQEBAAAAAAAAAAAAAQARITFBYfD/2gAIAQEAAT8QdIcFUc997FbSRXx8nEVD8bKm7V0lkBiK2DcYuuE//9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Apple 和 Android 平台之间的象征性桥梁&quot; title=&quot;&quot; src=&quot;https://bdteo.com/static/cd1aa56ebd8947295c8b2ab1adab5b06/828fb/featured-2.jpg&quot; srcset=&quot;https://bdteo.com/static/cd1aa56ebd8947295c8b2ab1adab5b06/ff44c/featured-2.jpg 158w,
https://bdteo.com/static/cd1aa56ebd8947295c8b2ab1adab5b06/a6688/featured-2.jpg 315w,
https://bdteo.com/static/cd1aa56ebd8947295c8b2ab1adab5b06/828fb/featured-2.jpg 630w,
https://bdteo.com/static/cd1aa56ebd8947295c8b2ab1adab5b06/0ede0/featured-2.jpg 945w,
https://bdteo.com/static/cd1aa56ebd8947295c8b2ab1adab5b06/3ac88/featured-2.jpg 1260w,
https://bdteo.com/static/cd1aa56ebd8947295c8b2ab1adab5b06/ac99c/featured-2.jpg 1536w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;figcaption&gt;
    Fig1. - 一片超现实景观里，失败的网线垂在 Apple 与 Android 岩体之间；一座标着 Bumble 的绳桥成功连接两端，让发光的数据包跨过裂隙。
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;网上搜索显示，API 33/34 模拟器上的蓝牙有不少不稳定报告；模拟器发现或连接 Netsim 后端的方式也可能有问题，尤其是有外部工具试图拦截它时。关键似乎是：&lt;strong&gt;显式告诉模拟器 Netsim gRPC server 在哪里&lt;/strong&gt;，并且&lt;strong&gt;尝试更老的 API 级别&lt;/strong&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动 Bridge（Netsim Controller 模式，显式端口，&lt;code class=&quot;language-text&quot;&gt;usb:0&lt;/code&gt;）：&lt;/strong&gt; 和尝试 #2 一样，确保它监听已知端口（&lt;code class=&quot;language-text&quot;&gt;8554&lt;/code&gt;），并用之前稳定工作的索引（&lt;code class=&quot;language-text&quot;&gt;usb:0&lt;/code&gt;）连接物理 dongle。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# In Terminal 1: (Keep this running!)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; python3 &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; bumble.apps.hci_bridge android-netsim:_:8554,mode&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;controller usb:0&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;修改并启动模拟器（API 32，显式端点）：&lt;/strong&gt; 我们创建了一个带 Google Play Services 的 &lt;strong&gt;API 32（Android 12L）&lt;/strong&gt; AVD（&lt;code class=&quot;language-text&quot;&gt;gplay_32_arm&lt;/code&gt;）。我们修改启动脚本，让它指向这个 AVD；更关键的是，把 &lt;code class=&quot;language-text&quot;&gt;-packet-streamer-endpoint&lt;/code&gt; flag 从 &lt;code class=&quot;language-text&quot;&gt;default&lt;/code&gt; 改成 bridge 的&lt;em&gt;确切&lt;/em&gt;地址。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Snippet from the *successful* launch script (see full script below):&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;API_LEVEL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;32&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;AVD_NAME&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;gplay_&lt;span class=&quot;token variable&quot;&gt;${API_LEVEL}&lt;/span&gt;_arm&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;SYSTEM_IMAGE_PKG&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;system-images;android-&lt;span class=&quot;token variable&quot;&gt;${API_LEVEL}&lt;/span&gt;;&lt;span class=&quot;token variable&quot;&gt;${IMAGE_TAG}&lt;/span&gt;;&lt;span class=&quot;token variable&quot;&gt;${ARCH}&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;BUMBLE_NETSIM_GRPC_PORT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;8554&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
emulator &lt;span class=&quot;token parameter variable&quot;&gt;-avd&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$AVD_NAME&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;. &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    -packet-streamer-endpoint &lt;span class=&quot;token string&quot;&gt;&quot;localhost:&lt;span class=&quot;token variable&quot;&gt;$BUMBLE_NETSIM_GRPC_PORT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;结果？成功！&lt;/strong&gt; 这次它工作了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;bumble-hci-bridge&lt;/code&gt; 终端在模拟器启动后不久开始显示来自模拟器的 gRPC 连接日志。&lt;/li&gt;
&lt;li&gt;模拟器启动完成后，在 Android Settings 里打开 Bluetooth，bridge 终端里立刻涌出一串 HCI 命令（Reset、Read Version、Set Event Mask 等）。&lt;/li&gt;
&lt;li&gt;在模拟器内扫描设备时，确实通过 ASUS dongle 使用了 Mac 的物理蓝牙无线电。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;成功配方：一步一步来&lt;/h2&gt;
&lt;p&gt;下面是我的 M1 Mac Pro 搭配外接 ASUS USB-BT500 dongle 时实际成功的步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;安装 Bumble：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; pip &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; bumble
&lt;span class=&quot;token comment&quot;&gt;# Potentially install libusb if needed: brew install libusb&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;（可选但推荐）禁用 macOS 原生 USB BT 处理：&lt;/strong&gt; 运行&lt;em&gt;一次&lt;/em&gt;，然后&lt;strong&gt;重启&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; nvram &lt;span class=&quot;token assign-left variable&quot;&gt;bluetoothHostControllerSwitchBehavior&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;never&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启动 Bumble Netsim Bridge：&lt;/strong&gt; 打开一个终端运行（保持它运行）：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; python3 &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; bumble.apps.hci_bridge android-netsim:_:8554,mode&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;controller usb:0&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;（确认它显示两次 &lt;code class=&quot;language-text&quot;&gt;&gt;&gt;&gt; connected&lt;/code&gt;。）&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;准备模拟器启动脚本：&lt;/strong&gt; 把下面提供的&lt;em&gt;完整脚本&lt;/em&gt;保存为 &lt;code class=&quot;language-text&quot;&gt;launch_gapps_avd_api32.sh&lt;/code&gt;（或类似名字）。确认它指向 &lt;strong&gt;API 32&lt;/strong&gt; AVD（如果不存在，会创建名为 &lt;code class=&quot;language-text&quot;&gt;gplay_32_arm&lt;/code&gt; 的 AVD），并且显式使用 &lt;code class=&quot;language-text&quot;&gt;-packet-streamer-endpoint localhost:8554&lt;/code&gt;。给它执行权限（&lt;code class=&quot;language-text&quot;&gt;chmod +x launch_gapps_avd_api32.sh&lt;/code&gt;）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行启动脚本：&lt;/strong&gt; 打开一个&lt;em&gt;新&lt;/em&gt;终端执行脚本：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;./launch_gapps_avd_api32.sh&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;验证：&lt;/strong&gt; 模拟器启动后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查看 &lt;code class=&quot;language-text&quot;&gt;bumble-hci-bridge&lt;/code&gt; 终端是否有 gRPC 和 HCI 流量。&lt;/li&gt;
&lt;li&gt;进入 Android Settings -&gt; Bluetooth，把它打开。&lt;/li&gt;
&lt;li&gt;尝试扫描或配对。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;成功的启动脚本（API 32，显式 Netsim 端点）&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token shebang important&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Script to setup and launch a Google Play Android emulator (API 32) on macOS M1/ARM64&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Uses explicit Netsim endpoint for Bumble bridge compatibility.&lt;/span&gt;

&lt;span class=&quot;token builtin class-name&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Exit immediately if a command exits with a non-zero status.&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# --- Configuration ---&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;API_LEVEL&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;32&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Target Android API Level (Android 12L)&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;AVD_NAME&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;gplay_&lt;span class=&quot;token variable&quot;&gt;${API_LEVEL}&lt;/span&gt;_arm&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Name for the Android Virtual Device&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;IMAGE_TAG&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;google_apis_playstore&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Image type with Google Play Store&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;ARCH&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;arm64-v8a&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Architecture for Apple Silicon&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;SYSTEM_IMAGE_PKG&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;system-images;android-&lt;span class=&quot;token variable&quot;&gt;${API_LEVEL}&lt;/span&gt;;&lt;span class=&quot;token variable&quot;&gt;${IMAGE_TAG}&lt;/span&gt;;&lt;span class=&quot;token variable&quot;&gt;${ARCH}&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;DEVICE_DEF&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;pixel_6a&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# A common modern Pixel device definition&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;BUMBLE_NETSIM_GRPC_PORT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;8554&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Port where bumble-hci-bridge Netsim controller is listening&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# --- Find Android SDK ---&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;ANDROID_SDK_ROOT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;${ANDROID_HOME&lt;span class=&quot;token operator&quot;&gt;:-&lt;/span&gt;${ANDROID_SDK_ROOT&lt;span class=&quot;token operator&quot;&gt;:-&lt;/span&gt;$HOME&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;Library&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;Android&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;sdk}&lt;/span&gt;}&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$ANDROID_SDK_ROOT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;❌ Error: Android SDK not found at &apos;&lt;span class=&quot;token variable&quot;&gt;$ANDROID_SDK_ROOT&lt;/span&gt;&apos;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Please install Android Studio or set ANDROID_HOME/ANDROID_SDK_ROOT.&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;▶️ Using Android SDK at: &lt;span class=&quot;token variable&quot;&gt;$ANDROID_SDK_ROOT&lt;/span&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# --- Define Tool Paths ---&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;CMDLINE_TOOLS_BIN&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$ANDROID_SDK_ROOT&lt;/span&gt;/cmdline-tools/latest/bin&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;PLATFORM_TOOLS_DIR&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$ANDROID_SDK_ROOT&lt;/span&gt;/platform-tools&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;EMULATOR_DIR&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$ANDROID_SDK_ROOT&lt;/span&gt;/emulator&quot;&lt;/span&gt;

&lt;span class=&quot;token assign-left variable&quot;&gt;SDKMANAGER&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$CMDLINE_TOOLS_BIN&lt;/span&gt;/sdkmanager&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;AVDMANAGER&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$CMDLINE_TOOLS_BIN&lt;/span&gt;/avdmanager&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;EMULATOR&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$EMULATOR_DIR&lt;/span&gt;/emulator&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;ADB&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$PLATFORM_TOOLS_DIR&lt;/span&gt;/adb&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# --- Check Essential Tools ---&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; sdkmanager &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&gt;&lt;/span&gt; /dev/null&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;❌ Error: sdkmanager not found. Check SDK Command-line Tools installation and PATH.&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; avdmanager &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&gt;&lt;/span&gt; /dev/null&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;❌ Error: avdmanager not found. Check SDK Command-line Tools installation and PATH.&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; emulator &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&gt;&lt;/span&gt; /dev/null&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;❌ Error: emulator not found. Check Android Emulator installation and PATH.&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; adb &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&gt;&lt;/span&gt; /dev/null&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;❌ Error: adb not found. Check SDK Platform-Tools installation and PATH.&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;✅ Basic SDK tools found in PATH.&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# --- Stop Currently Running Emulators ---&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;▶️ Stopping any currently running emulators...&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;RUNNING_EMULATORS&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;adb devices &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;emulator-&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-f1&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$RUNNING_EMULATORS&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token for-or-select variable&quot;&gt;emu_id&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$RUNNING_EMULATORS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
        &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Stopping &lt;span class=&quot;token variable&quot;&gt;$emu_id&lt;/span&gt;...&quot;&lt;/span&gt;
        adb &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$emu_id&lt;/span&gt;&quot;&lt;/span&gt; emu &lt;span class=&quot;token function&quot;&gt;kill&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   (Failed to kill &lt;span class=&quot;token variable&quot;&gt;$emu_id&lt;/span&gt;, may already be stopped)&quot;&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Small delay&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Give ADB server time to recognize disconnection&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Emulators stopped.&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   No emulators appear to be running according to &apos;adb devices&apos;.&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;✅ Emulator check/stop finished.&quot;&lt;/span&gt;


&lt;span class=&quot;token comment&quot;&gt;# --- Install/Update Required SDK Packages ---&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;▶️ Ensuring required SDK packages are installed...&quot;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Accept licenses non-interactively&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; sdkmanager &lt;span class=&quot;token parameter variable&quot;&gt;--licenses&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; /dev/null &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   (Ignoring potential license script errors)&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Install platform-tools, emulator&lt;/span&gt;
sdkmanager &lt;span class=&quot;token string&quot;&gt;&quot;platform-tools&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;emulator&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Install the Google Play system image for API 32&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;▶️ Attempting to install/update Google Play system image: &lt;span class=&quot;token variable&quot;&gt;$SYSTEM_IMAGE_PKG&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; sdkmanager &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SYSTEM_IMAGE_PKG&lt;/span&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;❌ Error: Failed to install required system image &apos;&lt;span class=&quot;token variable&quot;&gt;$SYSTEM_IMAGE_PKG&lt;/span&gt;&apos;.&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Please check available images using: sdkmanager --list | grep &apos;system-images;android-&lt;span class=&quot;token variable&quot;&gt;${API_LEVEL}&lt;/span&gt;;.*&lt;span class=&quot;token variable&quot;&gt;${ARCH}&lt;/span&gt;&apos;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;✅ System image package &apos;&lt;span class=&quot;token variable&quot;&gt;$SYSTEM_IMAGE_PKG&lt;/span&gt;&apos; present.&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# --- Check if AVD Exists, Create ONLY if Missing ---&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;▶️ Ensuring AVD &apos;&lt;span class=&quot;token variable&quot;&gt;$AVD_NAME&lt;/span&gt;&apos; exists...&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; avdmanager list avd &lt;span class=&quot;token parameter variable&quot;&gt;--compact&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-q&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^&lt;span class=&quot;token variable&quot;&gt;${AVD_NAME}&lt;/span&gt;$&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   AVD &apos;&lt;span class=&quot;token variable&quot;&gt;$AVD_NAME&lt;/span&gt;&apos; not found. Creating...&quot;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Using &apos;echo no&apos; usually prevents hardware profile creation prompts. Pipe empty string for potential licenses.&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; avdmanager create avd &lt;span class=&quot;token parameter variable&quot;&gt;--force&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$AVD_NAME&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--package&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$SYSTEM_IMAGE_PKG&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--device&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$DEVICE_DEF&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--sdcard&lt;/span&gt; 512M &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;❌ Error: Failed to create AVD &apos;&lt;span class=&quot;token variable&quot;&gt;$AVD_NAME&lt;/span&gt;&apos;.&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;
         &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Maybe the device definition &apos;&lt;span class=&quot;token variable&quot;&gt;$DEVICE_DEF&lt;/span&gt;&apos; is invalid for this image?&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;
         &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Check available devices: avdmanager list device&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;
         &lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;✅ AVD &apos;&lt;span class=&quot;token variable&quot;&gt;$AVD_NAME&lt;/span&gt;&apos; created.&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;✅ AVD &apos;&lt;span class=&quot;token variable&quot;&gt;$AVD_NAME&lt;/span&gt;&apos; already exists. Will reuse.&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;


&lt;span class=&quot;token comment&quot;&gt;# --- Launch Emulator ---&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;▶️ Launching existing/new emulator: &apos;&lt;span class=&quot;token variable&quot;&gt;$AVD_NAME&lt;/span&gt;&apos; (pointing to Bumble Netsim bridge on localhost:&lt;span class=&quot;token variable&quot;&gt;$BUMBLE_NETSIM_GRPC_PORT&lt;/span&gt;)...&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;EMULATOR_LOG&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;emulator-&lt;span class=&quot;token variable&quot;&gt;$AVD_NAME&lt;/span&gt;.log&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Log file name updated for API 32 AVD&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Google Play images often need a writable system partition&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Explicitly point packet streamer to localhost:8554 where bridge is listening&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;nohup&lt;/span&gt; emulator &lt;span class=&quot;token parameter variable&quot;&gt;-avd&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$AVD_NAME&lt;/span&gt;&quot;&lt;/span&gt; -no-snapshot-load &lt;span class=&quot;token parameter variable&quot;&gt;-gpu&lt;/span&gt; auto -show-kernel -writable-system &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    -packet-streamer-endpoint &lt;span class=&quot;token string&quot;&gt;&quot;localhost:&lt;span class=&quot;token variable&quot;&gt;$BUMBLE_NETSIM_GRPC_PORT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$EMULATOR_LOG&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;EMULATOR_PID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$!&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Emulator starting in background (PID: &lt;span class=&quot;token variable&quot;&gt;$EMULATOR_PID&lt;/span&gt;). Log: &lt;span class=&quot;token variable&quot;&gt;$EMULATOR_LOG&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Waiting for emulator to appear in ADB...&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Wait for the emulator device to show up in adb&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;WAIT_ADB_TIMEOUT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;90&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Increase timeout slightly for GPlay images&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;&lt;span class=&quot;token environment constant&quot;&gt;SECONDS&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;EMULATOR_ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Reset variable&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token environment constant&quot;&gt;$SECONDS&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-lt&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$WAIT_ADB_TIMEOUT&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Find the *new* emulator ID&lt;/span&gt;
    &lt;span class=&quot;token assign-left variable&quot;&gt;CURRENT_EMU_ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;adb devices &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;emulator-&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-f1&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$CURRENT_EMU_ID&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
        &lt;span class=&quot;token assign-left variable&quot;&gt;EMULATOR_ID&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$CURRENT_EMU_ID&lt;/span&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot; Found (&lt;span class=&quot;token variable&quot;&gt;$EMULATOR_ID&lt;/span&gt;)!&quot;&lt;/span&gt;
        &lt;span class=&quot;token builtin class-name&quot;&gt;break&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
    &lt;span class=&quot;token assign-left variable&quot;&gt;&lt;span class=&quot;token environment constant&quot;&gt;SECONDS&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$((&lt;/span&gt;SECONDS &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;.&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$EMULATOR_ID&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;❌ Error: Emulator did not appear in ADB within &lt;span class=&quot;token variable&quot;&gt;$WAIT_ADB_TIMEOUT&lt;/span&gt; seconds.&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Check logs: &lt;span class=&quot;token variable&quot;&gt;$EMULATOR_LOG&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Try to kill the process if it&apos;s still lingering&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;kill&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$EMULATOR_PID&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&gt;&lt;/span&gt;/dev/null &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   (Emulator process &lt;span class=&quot;token variable&quot;&gt;$EMULATOR_PID&lt;/span&gt; may have already exited)&quot;&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;


&lt;span class=&quot;token comment&quot;&gt;# --- Wait for Boot Completion ---&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;▶️ Waiting for Android system to fully boot (Google Play images can take longer)...&quot;&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;BOOT_TIMEOUT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;240&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Increase timeout significantly for GPlay images&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;&lt;span class=&quot;token environment constant&quot;&gt;SECONDS&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token environment constant&quot;&gt;$SECONDS&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-lt&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$BOOT_TIMEOUT&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Check if device is online first&lt;/span&gt;
    &lt;span class=&quot;token assign-left variable&quot;&gt;DEVICE_STATE&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;adb &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$EMULATOR_ID&lt;/span&gt;&quot;&lt;/span&gt; get-state &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&gt;&lt;/span&gt;/dev/null&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$DEVICE_STATE&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;device&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
       &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;s(&lt;span class=&quot;token variable&quot;&gt;$DEVICE_STATE&lt;/span&gt;)&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# State not ready&lt;/span&gt;
       &lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;
       &lt;span class=&quot;token assign-left variable&quot;&gt;&lt;span class=&quot;token environment constant&quot;&gt;SECONDS&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$((&lt;/span&gt;SECONDS &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
       &lt;span class=&quot;token builtin class-name&quot;&gt;continue&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# Check boot completed property&lt;/span&gt;
    &lt;span class=&quot;token assign-left variable&quot;&gt;BOOT_COMPLETED&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;adb &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$EMULATOR_ID&lt;/span&gt;&quot;&lt;/span&gt; shell getprop sys.boot_completed &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&gt;&lt;/span&gt;/dev/null &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tr&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;
&apos;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$BOOT_COMPLETED&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Double check package manager is ready too for GPlay images&lt;/span&gt;
        &lt;span class=&quot;token assign-left variable&quot;&gt;PM_READY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;adb &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$EMULATOR_ID&lt;/span&gt;&quot;&lt;/span&gt; shell pm get-install-location &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&gt;&lt;/span&gt;/dev/null&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$PM_READY&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; *&lt;span class=&quot;token string&quot;&gt;&quot;0&quot;&lt;/span&gt;* &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$PM_READY&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; *&lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt;* &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$PM_READY&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; *&lt;span class=&quot;token string&quot;&gt;&quot;2&quot;&lt;/span&gt;* &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Check if pm command gives valid output&lt;/span&gt;
            &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot; Booted!&quot;&lt;/span&gt;
            &lt;span class=&quot;token builtin class-name&quot;&gt;break&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
            &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;p(pm not ready)&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Package Manager not ready&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
         &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;b(booting)&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Boot not completed&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;
    &lt;span class=&quot;token assign-left variable&quot;&gt;&lt;span class=&quot;token environment constant&quot;&gt;SECONDS&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$((&lt;/span&gt;SECONDS &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token environment constant&quot;&gt;$SECONDS&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-ge&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$BOOT_TIMEOUT&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;❌ Error: Android system did not fully boot within &lt;span class=&quot;token variable&quot;&gt;$BOOT_TIMEOUT&lt;/span&gt; seconds.&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Emulator might be stuck. Check logs (&lt;span class=&quot;token variable&quot;&gt;$EMULATOR_LOG&lt;/span&gt;) or try launching manually.&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Don&apos;t exit here, user might want to interact with stuck emulator&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;


&lt;span class=&quot;token comment&quot;&gt;# --- Final Instructions ---&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;---&quot;&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;✅ Google Play Emulator &apos;&lt;span class=&quot;token variable&quot;&gt;$AVD_NAME&lt;/span&gt;&apos; (API &lt;span class=&quot;token variable&quot;&gt;$API_LEVEL&lt;/span&gt;) (&lt;span class=&quot;token variable&quot;&gt;$EMULATOR_ID&lt;/span&gt;) should be running.&quot;&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Bluetooth should be using the Netsim backend at localhost:&lt;span class=&quot;token variable&quot;&gt;$BUMBLE_NETSIM_GRPC_PORT&lt;/span&gt; (intercepted by Bumble).&quot;&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Connect shell:   adb -s &lt;span class=&quot;token variable&quot;&gt;$EMULATOR_ID&lt;/span&gt; shell&quot;&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Install APK:     adb -s &lt;span class=&quot;token variable&quot;&gt;$EMULATOR_ID&lt;/span&gt; install /path/to/your.apk&quot;&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Stop emulator:   adb -s &lt;span class=&quot;token variable&quot;&gt;$EMULATOR_ID&lt;/span&gt; emu kill&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$EMULATOR_PID&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Only show PID if we launched it&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   Kill Process:    kill &lt;span class=&quot;token variable&quot;&gt;$EMULATOR_PID&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;   NOTE: Google Play Services may need updates inside the emulator.&quot;&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;---&quot;&lt;/span&gt;

&lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;M1 Mac + Emulator + Bumble 的关键结论&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API 级别很重要：&lt;/strong&gt; 对模拟器兼容性来说，更新不总是更好，尤其是蓝牙桥接这种复杂功能。在我的测试里，API 32 比 API 34 更稳定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显式端点：&lt;/strong&gt; 使用 Bumble 的 Netsim controller mode 这类外部 bridge 时，不要依赖 &lt;code class=&quot;language-text&quot;&gt;-packet-streamer-endpoint default&lt;/code&gt;。直接把模拟器指向 bridge 正在监听的 &lt;code class=&quot;language-text&quot;&gt;localhost:&amp;lt;port&gt;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Netsim Bridge &gt; QEMU Socket：&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;android-netsim&lt;/code&gt; bridge mode 比更底层的 &lt;code class=&quot;language-text&quot;&gt;-qemu -chardev socket&lt;/code&gt; 方法更可能和现代模拟器正常配合，哪怕 socket 方法&lt;em&gt;确实&lt;/em&gt;能建立 TCP 连接。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;usb:0&lt;/code&gt; vs VID:PID：&lt;/strong&gt; 在 macOS/M1 上，USB 设备识别可能有点古怪。如果指定精确 VID:PID 意外失败，试试用索引 &lt;code class=&quot;language-text&quot;&gt;usb:0&lt;/code&gt;（假设它就是主要/目标设备）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;坚持有回报：&lt;/strong&gt; 这件事试了好几轮，结合了文档、网页搜索和反复测试。别太早放弃。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;希望这套具体可工作的配置能帮其他开发者省下几个小时的挫败。祝编码（和桥接）顺利。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[AI 博客重设计：Claude Code 如何改造我的 Gatsby 站点]]></title><description><![CDATA[作为一个大部分时间都在后端工作的开发者，我一直不太会设计。我的个人博客功能上没问题，但看起来像卡在了 2010 年——基础样式、不一致的间距，还有一个如果足够慷慨才能称作“极简”的配色方案。我几个月来一直想彻底翻新它，但一想到要钻进 CSS…]]></description><link>https://bdteo.com/zh/claude-code-transformed-my-blog-design-in-minutes/</link><guid isPermaLink="false">https://bdteo.com/zh/claude-code-transformed-my-blog-design-in-minutes/</guid><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;作为一个大部分时间都在后端工作的开发者，我一直不太会设计。我的个人博客功能上没问题，但看起来像卡在了 2010 年——基础样式、不一致的间距，还有一个如果足够慷慨才能称作“极简”的配色方案。我几个月来一直想彻底翻新它，但一想到要钻进 CSS 和设计系统里，就有点做噩梦。&lt;/p&gt;
&lt;p&gt;然后我试了 Claude Code，Anthropic 的 AI 编码助手，我的看法彻底变了。&lt;/p&gt;
&lt;h2&gt;挑战：我那个悲伤、过时的博客&lt;/h2&gt;
&lt;p&gt;我的博客是用 Gatsby.js 构建的。内容还算扎实，但呈现方式拖了后腿：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;整个站点的间距不一致&lt;/li&gt;
&lt;li&gt;移动设备上的响应式表现很差&lt;/li&gt;
&lt;li&gt;深色模式实现几乎只是勉强能用&lt;/li&gt;
&lt;li&gt;字体排印最好也只能算业余&lt;/li&gt;
&lt;li&gt;没有统一的配色系统或设计语言&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我大概知道自己想要什么：现代、干净、专业，能让内容自己发光。但把这个愿景翻译成真正的 CSS？那通常就是我撞墙的地方。&lt;/p&gt;
&lt;h2&gt;Claude Code 登场：我的虚拟设计搭档&lt;/h2&gt;
&lt;p&gt;我听说 Claude Code 已经有一阵子了，但我对 AI 到底能在设计工作上帮多少忙一直有点怀疑。毕竟，设计需要品味和审美眼光，不只是技术知识。&lt;/p&gt;
&lt;p&gt;我还是决定试一试，用了一个很简单的 prompt：“Please improve my styling by a ton.” 我愿意投入一个小时，以及大约 15 美元的 API 费用，看看它到底能做到什么。&lt;/p&gt;
&lt;p&gt;接下来发生的事真的让我惊讶。&lt;/p&gt;
&lt;h2&gt;改造过程&lt;/h2&gt;
&lt;p&gt;Claude Code 没有只是建议几个 CSS 小调整，而是以专业开发者的严谨度来处理这次重设计：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分析阶段&lt;/strong&gt;：首先，它检查了我现有的样式、组件架构和配色方案，理解自己面对的是什么。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建设计系统&lt;/strong&gt;：它没有停留在表面改动，而是构建了一个完整的设计系统，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一套专业配色，并为浅色和深色模式定义了语义变量&lt;/li&gt;
&lt;li&gt;现代化的字体排印尺度，以及合适的响应式调整&lt;/li&gt;
&lt;li&gt;一个基于 4px 网格的一致间距系统&lt;/li&gt;
&lt;li&gt;标准化的阴影、圆角和过渡&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;组件重写&lt;/strong&gt;：它重写了我的组件，让它们遵循现代最佳实践：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建了基于卡片的响应式博客文章布局&lt;/li&gt;
&lt;li&gt;实现了带背景模糊效果的固定页头&lt;/li&gt;
&lt;li&gt;设计了一个优雅的主题切换器&lt;/li&gt;
&lt;li&gt;添加了带激活状态的正确导航&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;可访问性改进&lt;/strong&gt;：Claude Code 不只是让东西看起来更好——它也让站点更易访问：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;添加了正确的键盘导航支持&lt;/li&gt;
&lt;li&gt;实现了 skip-to-content 链接&lt;/li&gt;
&lt;li&gt;确保了足够的颜色对比度&lt;/li&gt;
&lt;li&gt;添加了正确的 ARIA labels&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;性能优化&lt;/strong&gt;：代码也针对性能做了优化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只选择性导入必要的 Bootstrap 组件&lt;/li&gt;
&lt;li&gt;优化 CSS 组织&lt;/li&gt;
&lt;li&gt;添加硬件加速动画&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;让人有点失语的结果&lt;/h2&gt;
&lt;p&gt;改造效果非常惊人。在大约一个小时的来回互动里，只花了 15 美元的 Claude API 成本，我的博客从一个看起来像开发者副项目的东西，变成了一个有专业设计感的出版物。重设计包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个精细的配色系统，并正确支持深色模式&lt;/li&gt;
&lt;li&gt;在各种设备上都能完美缩放的漂亮字体排印&lt;/li&gt;
&lt;li&gt;专业的博客文章卡片布局&lt;/li&gt;
&lt;li&gt;顺滑的动画和过渡&lt;/li&gt;
&lt;li&gt;带玻璃质感效果的固定页头&lt;/li&gt;
&lt;li&gt;贯穿整个站点的一致间距&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;放在现实里比较一下：聘请一位专业网页设计师做这种级别的工作，可能要花 1,000 到 2,000 美元，还需要数天甚至数周的来回沟通。而这一次，我花了一个小时和 15 美元 API 费用。&lt;/p&gt;
&lt;p&gt;但最让我印象深刻的，不只是视觉提升或成本节省，而是代码质量。Claude Code 不是随手堆了一些 CSS hack；它创建了一个完整、可维护、我可以很容易继续扩展的设计系统。&lt;/p&gt;
&lt;h2&gt;我从 AI 辅助设计里学到的事&lt;/h2&gt;
&lt;p&gt;这次经历改变了我对 AI 能为开发者做什么的看法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AI 擅长系统性思考&lt;/strong&gt;：Claude Code 不只是让东西“好看一点”——它创建了一个有变量、有一致模式、有清晰组织的完整系统。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;它能弥合知识缺口&lt;/strong&gt;：作为一个没有深厚 CSS 专长的人，Claude Code 用现代最佳实践填补了我的知识空白，而这些东西我自己未必知道要实现。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;代码达到了生产质量&lt;/strong&gt;：它创建的样式框架不是摆设——它是可维护、可扩展，并遵循现代标准的代码。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;它保留了我的内容和结构&lt;/strong&gt;：Claude Code 改进了设计，同时保留了我博客的核心结构和内容。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;开发者与 AI 协作的未来&lt;/h2&gt;
&lt;p&gt;我的经历凸显了一件重要的事：Claude Code 不是在取代开发者，而是在扩展我们的能力。它帮我越过了自己在前端设计上的局限，同时仍然让我掌控整体方向。&lt;/p&gt;
&lt;p&gt;对于那些有些方面更强、有些方面更弱的开发者来说（而我们不都是这样吗？），Claude Code 这样的 AI 助手可以帮助填补技能组合里的空白。&lt;/p&gt;
&lt;h2&gt;自己试试看&lt;/h2&gt;
&lt;p&gt;如果你正在设计或开发的其他某个方面挣扎，我非常推荐你试试 Claude Code。这次经历改变的不只是我的博客，也改变了我对 AI 辅助开发可能性的看法。&lt;/p&gt;
&lt;p&gt;你试过 Claude Code 或类似的 AI 编码助手吗？我很想在下面的评论里听听你的经历。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注：这个博客本身就是用 Claude Code 重新设计的，所以你现在看到的正是我描述的过程所带来的结果。从我以前的设计到现在这个版本，大约花了一个小时的提示词输入和迭代，成本约为 15 美元的 Claude API 使用费。考虑到传统设计成本，这是一笔非常惊人的价值交换。&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Huawei Watch D2 BLE 配对：协议与厂商锁定案例]]></title><description><![CDATA[太长不读： Huawei Watch D2 没有使用标准 BLE 配对。它要求的是一套 11 步专有握手，里面有自定义 GATT characteristic、从二维码派生 HMAC-SHA256 密钥，以及应用层加密。这是有意设计的厂商锁定，它把你推入华为的 Health…]]></description><link>https://bdteo.com/zh/huawei-watch-d2-proprietary-protocol-vendor-lockin/</link><guid isPermaLink="false">https://bdteo.com/zh/huawei-watch-d2-proprietary-protocol-vendor-lockin/</guid><pubDate>Fri, 11 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;太长不读：&lt;/strong&gt; Huawei Watch D2 没有使用标准 BLE 配对。它要求的是一套 11 步专有握手，里面有自定义 GATT characteristic、从二维码派生 HMAC-SHA256 密钥，以及应用层加密。这是有意设计的厂商锁定，它把你推入华为的 Health 应用。好消息是：社区已经把它逆向出来了。Gadgetbridge 现在支持 Watch D2，也已经有 &lt;code class=&quot;language-text&quot;&gt;huawei-lpv2&lt;/code&gt; 这样的开源实现。欧盟 DMA 也开始反推这种做法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我原本期待的是标准蓝牙配对。连接，绑定，交换数据，正常流程。结果我遇到的是一套专有的密码学握手，花了几周才逆向明白。&lt;/p&gt;
&lt;p&gt;这发生在我做 D2Explorer 的时候。D2Explorer 是我用来在 Linux 和 macOS 上连接 Huawei Watch D2、绕开华为 Health 应用的项目。在&lt;a href=&quot;/zh/bluez-pairing-python-agent-workaround-authentication-failed/&quot;&gt;解决 BlueZ 配对代理的问题&lt;/a&gt;并迁移到跨平台的 SimpleBLE 库之后，我以为最难的部分已经结束。最难的部分还没开始。&lt;/p&gt;
&lt;h2&gt;你本来会期待什么：标准 BLE 配对&lt;/h2&gt;
&lt;p&gt;Bluetooth LE 配对&lt;em&gt;本来&lt;/em&gt;应该是这样工作的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按广播名称扫描设备（比如 &quot;HUAWEI WATCH D2-CA0&quot;）。&lt;/li&gt;
&lt;li&gt;用 &lt;code class=&quot;language-text&quot;&gt;peripheral.connect()&lt;/code&gt; 连接。&lt;/li&gt;
&lt;li&gt;操作系统处理配对/绑定：PIN 提示、Just Works，或者安全级别要求的任何方式。&lt;/li&gt;
&lt;li&gt;绑定完成后，和标准或自定义 GATT 服务交互。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;安全由操作系统管理。你的应用专注于数据。简单。&lt;/p&gt;
&lt;h2&gt;实际发生了什么：一个 11 步专有握手&lt;/h2&gt;
&lt;p&gt;Watch D2 实际要求的东西完全不同。基础 BLE 连接只是门。门后面是华为叠在标准 BLE 之上的一套自定义应用层认证协议，也就是社区所说的 &lt;strong&gt;Huawei Link Protocol v2&lt;/strong&gt; &lt;small&gt;&lt;a href=&quot;#ref1&quot;&gt;[1]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;p&gt;标准 BLE 配对机制被完全绕开了。要认证并访问任何有意义的数据，你需要通过自定义 GATT characteristic 走完这一串流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Connect&lt;/strong&gt; -- 建立基础 BLE 链路。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable Notifications&lt;/strong&gt; -- &lt;em&gt;立刻&lt;/em&gt;订阅 characteristic &lt;code class=&quot;language-text&quot;&gt;0000fe02-...&lt;/code&gt; 上的通知。这个步骤对时序非常敏感，错过窗口，手表就会断开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GetLinkParams&lt;/strong&gt; -- &lt;em&gt;立刻&lt;/em&gt;向写入 characteristic &lt;code class=&quot;language-text&quot;&gt;0000fe01-...&lt;/code&gt; 发送自定义命令（Service ID &lt;code class=&quot;language-text&quot;&gt;0x0001&lt;/code&gt;，Command ID &lt;code class=&quot;language-text&quot;&gt;0x0001&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Receive Server Nonce&lt;/strong&gt; -- 等待包含手表随机 challenge 的通知。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Derive Secret Key&lt;/strong&gt; -- 生成 client nonce。把 server nonce、client nonce 和&lt;strong&gt;手表二维码里的数值&lt;/strong&gt;组合起来。运行 HMAC-SHA256（使用二维码数值的字节作为密钥），派生共享的 &lt;code class=&quot;language-text&quot;&gt;secretKey_&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AuthRequest&lt;/strong&gt; -- 把 client nonce 和 HMAC digest（使用派生出的 &lt;code class=&quot;language-text&quot;&gt;secretKey_&lt;/code&gt;）发回手表（Service &lt;code class=&quot;language-text&quot;&gt;0x0001&lt;/code&gt;，Command &lt;code class=&quot;language-text&quot;&gt;0x0002&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify Server Token&lt;/strong&gt; -- 接收手表的认证 token。用 &lt;code class=&quot;language-text&quot;&gt;secretKey_&lt;/code&gt; 和已交换的 nonce 验证它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SetTime&lt;/strong&gt; -- 发送当前时间和时区偏移，并用 &lt;code class=&quot;language-text&quot;&gt;secretKey_&lt;/code&gt; &lt;em&gt;加密&lt;/em&gt;（Service &lt;code class=&quot;language-text&quot;&gt;0x0002&lt;/code&gt;，Command &lt;code class=&quot;language-text&quot;&gt;0x0003&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;QrToken&lt;/strong&gt; -- 把二维码数值发回去，并用 &lt;code class=&quot;language-text&quot;&gt;secretKey_&lt;/code&gt; &lt;em&gt;加密&lt;/em&gt;（Service &lt;code class=&quot;language-text&quot;&gt;0x0001&lt;/code&gt;，Command &lt;code class=&quot;language-text&quot;&gt;0x0004&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AuthResult&lt;/strong&gt; -- 发送最终确认，并用 &lt;code class=&quot;language-text&quot;&gt;secretKey_&lt;/code&gt; &lt;em&gt;加密&lt;/em&gt;（Service &lt;code class=&quot;language-text&quot;&gt;0x0001&lt;/code&gt;，Command &lt;code class=&quot;language-text&quot;&gt;0x0005&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Done&lt;/strong&gt; -- 到这里，连接才算完成认证。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;自定义 TLV 消息格式。CRC 校验。Service 和 command ID。应用层加密。以毫秒为单位敏感的时序。这一切都发生在 BLE 栈&lt;em&gt;之上&lt;/em&gt;，标准蓝牙工具根本看不见。&lt;/p&gt;
&lt;p&gt;手表屏幕上的二维码就是共享秘密。没有它，你派生不出密钥。没有密钥，你无法认证。没有认证，手表什么都不会给你。&lt;/p&gt;
&lt;h2&gt;华为为什么这么做&lt;/h2&gt;
&lt;p&gt;华为也许会把它描述成增强安全。实际效果是&lt;strong&gt;厂商锁定&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;极高的进入门槛&lt;/strong&gt; -- 协议没有文档。重新实现它需要逆向华为 Health 应用（13,000+ 个类，64,000+ 个方法 &lt;small&gt;&lt;a href=&quot;#ref2&quot;&gt;[2]&lt;/a&gt;&lt;/small&gt;），或者分析 BLE 流量。这会主动劝退第三方应用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有互操作性&lt;/strong&gt; -- 标准健身应用无法连接。手表只会和知道这些专有步骤的软件完成握手，主要就是华为自己的 Health 应用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生态控制&lt;/strong&gt; -- 用户被迫进入 Huawei Health 及其云服务。以后想换设备或平台，就意味着失去自己的健康数据历史。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;减少用户选择&lt;/strong&gt; -- 想用开源应用？想对自己的健康数据隐私有更多控制？运气不好。除非有人先把协议逆向出来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题在这里：&lt;strong&gt;这并不是华为独有的事&lt;/strong&gt;。WatchWitch 研究项目 &lt;small&gt;&lt;a href=&quot;#ref3&quot;&gt;[3]&lt;/a&gt;&lt;/small&gt; 记录了所有主要厂商，包括 Apple、Samsung、Xiaomi，如何用专有 BLE 协议强制生态锁定。Apple Watch 被描述为 &quot;incredibly tightly coupled with Apple&apos;s iPhone and iCloud ecosystem, using proprietary protocols that are unavailable to third parties.&quot; 这是一个行业层面的系统问题。&lt;/p&gt;
&lt;p&gt;但华为的实现尤其激进。BLE &lt;em&gt;当然&lt;/em&gt;允许自定义服务。可用一个专有守门人取代根本性的认证机制，就是另一回事了。&lt;/p&gt;
&lt;h2&gt;安全上的讽刺&lt;/h2&gt;
&lt;p&gt;最显然的辩护是：“我们这么做是为了安全。”那就看一看。&lt;/p&gt;
&lt;p&gt;清华大学的 BlueDoor 漏洞研究 &lt;small&gt;&lt;a href=&quot;#ref4&quot;&gt;[4]&lt;/a&gt;&lt;/small&gt; 测试了 16 个 BLE 设备，包括 Honor Band 3（同一个华为生态），并在其中大多数设备上实现了&lt;strong&gt;无需用户授权的静默配对&lt;/strong&gt;。专有协议没有阻止这件事。&lt;/p&gt;
&lt;p&gt;与此同时，这套协议本身已经被逆向过多次：Gadgetbridge 社区逆向过，&lt;code class=&quot;language-text&quot;&gt;huawei-lpv2&lt;/code&gt; 项目逆向过，在 Easterhegg 2019 演讲的研究者逆向过 &lt;small&gt;&lt;a href=&quot;#ref2&quot;&gt;[2]&lt;/a&gt;&lt;/small&gt;，我也为 D2Explorer 逆向过。靠遮掩获得的安全，是带有效期的。&lt;/p&gt;
&lt;p&gt;从二维码派生 HMAC-SHA256 密钥，本身其实是不错的密码学。但重点不在这里。你完全可以用标准 BLE Secure Connections 加带外配对方式（比如 NFC 或二维码）获得同样的安全属性，而且不需要在过程中把所有第三方应用挡在门外。&lt;/p&gt;
&lt;h2&gt;社区的反击&lt;/h2&gt;
&lt;p&gt;社区没有安静接受。&lt;/p&gt;
&lt;h3&gt;Gadgetbridge&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://gadgetbridge.org/&quot;&gt;Gadgetbridge&lt;/a&gt; 是面向可穿戴设备的开源 Android 应用，现在已经支持 Huawei Watch D2 &lt;small&gt;&lt;a href=&quot;#ref5&quot;&gt;[5]&lt;/a&gt;&lt;/small&gt;。你可以不通过华为 Health 应用来配对手表。它花了相当多的逆向工程努力（见 PR #2462 &lt;small&gt;&lt;a href=&quot;#ref6&quot;&gt;[6]&lt;/a&gt;&lt;/small&gt;），也有一些限制，比如用 Gadgetbridge 配对时 ECG 功能会被禁用 &lt;small&gt;&lt;a href=&quot;#ref7&quot;&gt;[7]&lt;/a&gt;&lt;/small&gt;，但它确实可用。&lt;/p&gt;
&lt;p&gt;Gadgetbridge 里的认证实现处理 auth version 3，从配对消息（service &lt;code class=&quot;language-text&quot;&gt;0x01&lt;/code&gt;，command &lt;code class=&quot;language-text&quot;&gt;0x0e&lt;/code&gt;）计算 bonding key，并用它进行解密。认证密钥协商需要一个 17 位的华为账号 ID。&lt;/p&gt;
&lt;h3&gt;huawei-lpv2&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/zyv/huawei-lpv2&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;huawei-lpv2&lt;/code&gt;&lt;/a&gt; 项目提供了 Huawei Link Protocol v2 的纯 Python 实现 &lt;small&gt;&lt;a href=&quot;#ref8&quot;&gt;[8]&lt;/a&gt;&lt;/small&gt;。它仍在维护，有多个 fork，也给所有想在官方生态之外做华为可穿戴集成的人提供了参考。&lt;/p&gt;
&lt;h3&gt;D2Explorer&lt;/h3&gt;
&lt;p&gt;我自己的 D2Explorer 走了另一条路：用 SimpleBLE 做一个能在 Linux 和 macOS 上工作的 C++ 实现。这项工作包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现 TLV 序列化/反序列化（&lt;code class=&quot;language-text&quot;&gt;HuaweiProtocol&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;构建精确的消息构造器（&lt;code class=&quot;language-text&quot;&gt;ProtocolMessageBuilder&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;把密码学步骤做对：nonce 生成、HMAC-SHA256、XOR 加密（&lt;code class=&quot;language-text&quot;&gt;CryptoOperations&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;CryptoUtils&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;管理严格的状态转换和时序（&lt;code class=&quot;language-text&quot;&gt;HuaweiPairingProtocol&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;ProtocolStateManager&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;调试由毫秒级时序不匹配和细微加密错误引起的失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;D2Explorer 之所以存在，&lt;em&gt;正是因为&lt;/em&gt;华为的协议让它变得必要。它是在围墙花园之外获得基本功能所需要的绕行方案。&lt;/p&gt;
&lt;h3&gt;AsteroidOS&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://asteroidos.org/&quot;&gt;AsteroidOS 2.0&lt;/a&gt; 于 2026 年 2 月发布，是这个基于 Linux 的开源智能手表 OS 的一次重大更新 &lt;small&gt;&lt;a href=&quot;#ref9&quot;&gt;[9]&lt;/a&gt;&lt;/small&gt;。它现在支持约 30 款设备，包括 Huawei Watch 和 Huawei Watch 2，并带有常亮显示、抬腕唤醒等功能。这是一个完整的、开源的华为固件替代方案。&lt;/p&gt;
&lt;h2&gt;监管潮水&lt;/h2&gt;
&lt;p&gt;欧盟不只是旁观。Digital Markets Act（DMA）正在开始迫使事情改变 &lt;small&gt;&lt;a href=&quot;#ref10&quot;&gt;[10]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;p&gt;2025 年 12 月，Apple 发布了 iOS 26.3，为第三方设备，包括华为智能手表，加入类似 AirPods 的配对体验，明确是为了遵守 DMA 要求 &lt;small&gt;&lt;a href=&quot;#ref11&quot;&gt;[11]&lt;/a&gt;&lt;/small&gt;。华为手表和 iPhone 之间的后台同步已经在欧洲可用。&lt;/p&gt;
&lt;p&gt;DMA 要求守门人向互联设备提供互操作性。这正面瞄准了华为、Apple 以及其他所有厂商一直在实践的这种专有 BLE 锁定。这些互操作功能预计会在 2026 年全年继续铺开。&lt;/p&gt;
&lt;p&gt;这很重要。第一次，监管压力开始要求标准化那些厂商刻意保持专有的东西。技术社区可以一个协议一个协议地逆向，但监管可以改变整个行业的激励结构。&lt;/p&gt;
&lt;h2&gt;这意味着什么&lt;/h2&gt;
&lt;p&gt;Huawei Watch D2 的配对协议，是一个很好的案例：标准传输之上的自定义协议，如何被用来强制厂商锁定。那些专有密码学、自定义消息格式和对时序敏感的握手之所以存在，并不是因为标准 BLE 处理不了认证。它能处理。它们存在，是因为专有协议能把用户留在生态里。&lt;/p&gt;
&lt;p&gt;不过局面正在变化。Gadgetbridge 现在就给了你替代方案。欧盟 DMA 在监管层面推动互操作性。&lt;code class=&quot;language-text&quot;&gt;huawei-lpv2&lt;/code&gt;、D2Explorer 和 AsteroidOS 这样的开源项目也证明了：厂商想锁住的东西，社区会去逆向。&lt;/p&gt;
&lt;p&gt;做 D2Explorer，与其说是在做蓝牙，不如说是在做密码学侦探工作。它凸显了一件本不该反复强调的事：你应该能用自己选择的软件访问自己的健康数据。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;参考资料&lt;/h3&gt;
&lt;p&gt;&lt;a id=&quot;ref1&quot;&gt;&lt;/a&gt;1. &lt;a href=&quot;https://github.com/zyv/huawei-lpv2&quot;&gt;huawei-lpv2: Pure Python implementation of Huawei BLE Link Protocol v2&lt;/a&gt; -- &lt;em&gt;协议的开源参考实现。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref2&quot;&gt;&lt;/a&gt;2. &lt;a href=&quot;https://media.ccc.de/v/eh19-186-all-your-fitness-data-belongs-to-you-reverse-engineering-the-huawei-health-android-app&quot;&gt;All Your Fitness Data Belongs to You: Reverse Engineering the Huawei Health Android App&lt;/a&gt; -- &lt;em&gt;Easterhegg 2019 大会演讲，记录了逆向工程工作。&lt;a href=&quot;https://www.sba-research.org/wp-content/uploads/2019/04/easterhegg19.pdf&quot;&gt;Slides (PDF)&lt;/a&gt;。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref3&quot;&gt;&lt;/a&gt;3. &lt;a href=&quot;https://arxiv.org/html/2507.07210v1&quot;&gt;WatchWitch: Academic Research on Smartwatch Interoperability&lt;/a&gt; -- &lt;em&gt;记录所有主要厂商如何用专有协议实现生态锁定。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref4&quot;&gt;&lt;/a&gt;4. &lt;a href=&quot;https://tns.thss.tsinghua.edu.cn/~jiliang/publications/MOBISYS2020_BlueDoor.pdf&quot;&gt;BlueDoor: Breaking the Secure Information Flow via BLE Vulnerability (Tsinghua University)&lt;/a&gt; -- &lt;em&gt;在 16 个 BLE 设备中发现静默配对漏洞，包括 Honor Band 3。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref5&quot;&gt;&lt;/a&gt;5. &lt;a href=&quot;https://gadgetbridge.org/basics/topics/huawei-honor/&quot;&gt;Gadgetbridge: Huawei/Honor Device Support&lt;/a&gt; -- &lt;em&gt;Huawei 和 Honor 可穿戴设备的官方支持页面。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref6&quot;&gt;&lt;/a&gt;6. &lt;a href=&quot;https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/2462&quot;&gt;Gadgetbridge PR #2462: Initial Huawei/Honor Support&lt;/a&gt; -- &lt;em&gt;为 Gadgetbridge 添加 Huawei 设备支持的 pull request。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref7&quot;&gt;&lt;/a&gt;7. &lt;a href=&quot;https://codeberg.org/Freeyourgadget/Gadgetbridge/issues/4918&quot;&gt;Gadgetbridge Issue #4918: ECG Disabled with Gadgetbridge&lt;/a&gt; -- &lt;em&gt;使用 Gadgetbridge 而不是 Huawei Health 时的已知限制。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref8&quot;&gt;&lt;/a&gt;8. &lt;a href=&quot;https://gadgetbridge.org/basics/pairing/huawei-honor-pairing/&quot;&gt;Gadgetbridge: Huawei/Honor Pairing Guide&lt;/a&gt; -- &lt;em&gt;Huawei 设备逐步配对说明。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref9&quot;&gt;&lt;/a&gt;9. &lt;a href=&quot;https://www.cnx-software.com/2026/02/18/asteroidos-2-0-open-source-smartwatch-os-released-now-supports-around-30-devices/&quot;&gt;AsteroidOS 2.0 Release&lt;/a&gt; -- &lt;em&gt;开源智能手表 OS 现在支持约 30 款设备，包括 Huawei 手表。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref10&quot;&gt;&lt;/a&gt;10. &lt;a href=&quot;https://digital-markets-act.ec.europa.eu/questions-and-answers/interoperability_en&quot;&gt;EU Digital Markets Act: Interoperability Requirements&lt;/a&gt; -- &lt;em&gt;DMA 中要求互联设备互操作性的条款。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref11&quot;&gt;&lt;/a&gt;11. &lt;a href=&quot;https://www.macrumors.com/2025/12/22/ios-26-3-dma-airpods-pairing/&quot;&gt;iOS 26.3 DMA Features: Third-Party Smartwatch Pairing&lt;/a&gt; -- &lt;em&gt;Apple 为可穿戴设备互操作性要求所做的欧盟合规。&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;相关文章&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/zh/bluez-pairing-python-agent-workaround-authentication-failed/&quot;&gt;BlueZ Pairing Fix: External Python Agent &amp;#x26; D-Bus Polling&lt;/a&gt; -- 这次调查的前置工作。在处理华为专有协议之前，我们必须先修好标准 BLE 配对里的 BlueZ &lt;code class=&quot;language-text&quot;&gt;AuthenticationFailed&lt;/code&gt; 错误。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/zh/m1-mac-android-emulator-bluetooth-passthrough-bumble/&quot;&gt;Fix Android Emulator Bluetooth on M1 Mac using Bumble &amp;#x26; API 32&lt;/a&gt; -- 另一场 BLE 集成战斗，这次是把 Mac 的实体蓝牙无线电桥接进 Android Emulator。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[BlueZ 配对修复：外部 Python Agent 与 D-Bus 轮询]]></title><description><![CDATA[TL;DR: 如果你在 BlueZ 5.66+ 上用自定义 C++/sd-bus 配对 agent 时遇到 ，问题很可能出在内部 agent 注册上。把外部 Python agent（）作为独立进程运行，并实现 D-Bus…]]></description><link>https://bdteo.com/zh/bluez-pairing-python-agent-workaround-authentication-failed/</link><guid isPermaLink="false">https://bdteo.com/zh/bluez-pairing-python-agent-workaround-authentication-failed/</guid><pubDate>Tue, 08 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; 如果你在 BlueZ 5.66+ 上用自定义 C++/sd-bus 配对 agent 时遇到 &lt;code class=&quot;language-text&quot;&gt;org.bluez.Error.AuthenticationFailed&lt;/code&gt;，问题很可能出在内部 agent 注册上。把外部 Python agent（&lt;code class=&quot;language-text&quot;&gt;simple-agent.py&lt;/code&gt;）作为独立进程运行，并实现 D-Bus 属性轮询，不要只依赖 &lt;code class=&quot;language-text&quot;&gt;PropertiesChanged&lt;/code&gt; 信号。细节和代码在下面。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我盯着 &lt;code class=&quot;language-text&quot;&gt;org.bluez.Error.AuthenticationFailed&lt;/code&gt; 看了两天，才弄明白到底发生了什么。&lt;/p&gt;
&lt;p&gt;配对 agent 已经注册。D-Bus 调用看起来是对的。&lt;code class=&quot;language-text&quot;&gt;busctl&lt;/code&gt; 确认所有东西都在位——而 BlueZ 只是继续说不。这发生在 &lt;a href=&quot;../huawei-watch-d2-proprietary-protocol-vendor-lockin/&quot;&gt;D2Explorer&lt;/a&gt; 的开发过程中——一个在 Linux 上与 Huawei Watch D2 配对的工具——而这个配对错误把一切都挡住了。&lt;/p&gt;
&lt;p&gt;下面是真正发生的事，以及我们最后是怎么修好的。&lt;/p&gt;
&lt;h2&gt;计划：一个内部 C++ 配对 Agent&lt;/h2&gt;
&lt;p&gt;想法很干净，也很自洽。一个单独的 C++ 应用，使用 &lt;code class=&quot;language-text&quot;&gt;sd-bus&lt;/code&gt;（C/C++ 的 D-Bus 绑定）处理整个配对流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;连接到系统 D-Bus。&lt;/li&gt;
&lt;li&gt;找到 Bluetooth 适配器（&lt;code class=&quot;language-text&quot;&gt;org.bluez.Adapter1&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;实现一个暴露 &lt;code class=&quot;language-text&quot;&gt;org.bluez.Agent1&lt;/code&gt; 接口的 C++ 类。&lt;/li&gt;
&lt;li&gt;通过 &lt;code class=&quot;language-text&quot;&gt;RegisterAgent&lt;/code&gt; 和 &lt;code class=&quot;language-text&quot;&gt;RequestDefaultAgent&lt;/code&gt; 在 &lt;code class=&quot;language-text&quot;&gt;org.bluez.AgentManager1&lt;/code&gt; 上注册 agent。我们一开始使用 &lt;code class=&quot;language-text&quot;&gt;DisplayYesNo&lt;/code&gt; 能力，后来简化为 &lt;code class=&quot;language-text&quot;&gt;NoInputNoOutput&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;发现目标设备（&lt;code class=&quot;language-text&quot;&gt;org.bluez.Device1&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;在设备的 D-Bus 接口上调用 &lt;code class=&quot;language-text&quot;&gt;Pair()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;内部 agent 自动处理回调（&lt;code class=&quot;language-text&quot;&gt;RequestConfirmation&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;RequestAuthorization&lt;/code&gt;）——不需要用户交互。&lt;/li&gt;
&lt;li&gt;信任设备，建立 GATT 连接，完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一个二进制文件，没有外部依赖。这就是计划。&lt;/p&gt;
&lt;h2&gt;墙：&lt;code class=&quot;language-text&quot;&gt;org.bluez.Error.AuthenticationFailed&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;一切都工作到第 6 步。找到了适配器，注册了 agent（D-Bus 也确认了），发现了设备。但我们通过 &lt;code class=&quot;language-text&quot;&gt;sd_bus_call_method&lt;/code&gt; 调用 &lt;code class=&quot;language-text&quot;&gt;Device1.Pair()&lt;/code&gt; 的那一刻——立刻失败：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[BluetoothDevice] Calling Device1.Pair() method via D-Bus
[BluetoothDevice] Device1.Pair() method threw exception: Failed to call method &apos;Pair&apos;:
    Input/output error - D-Bus error: org.bluez.Error.AuthenticationFailed (Authentication Failed)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;我们什么都试过了。不同的 agent 能力。检查 &lt;code class=&quot;language-text&quot;&gt;sd-bus&lt;/code&gt; vtable 设置。验证 agent 方法实现会及时返回成功。用 &lt;code class=&quot;language-text&quot;&gt;busctl&lt;/code&gt; 和 &lt;code class=&quot;language-text&quot;&gt;gdbus&lt;/code&gt; 监控 D-Bus 流量——注册调用看起来都是正确的。&lt;code class=&quot;language-text&quot;&gt;Pair()&lt;/code&gt; 调用就是一直失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;死路。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;突破：外部 Python Agent&lt;/h2&gt;
&lt;p&gt;为了隔离问题，我们把内部 C++ agent 从方程里拿掉。我们先把 BlueZ 标准的 &lt;code class=&quot;language-text&quot;&gt;simple-agent.py&lt;/code&gt; 作为独立进程运行起来，&lt;em&gt;然后&lt;/em&gt;再启动我们的 C++ 应用（此时它已经去掉了自己的 agent 注册）：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Terminal 1: Run the external agent&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; python simple-agent.py NoInputNoOutput

&lt;span class=&quot;token comment&quot;&gt;# Terminal 2: Run our C++ app (no internal agent)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; ./build/huawei_pair_app &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;MAC&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;QR_VALUE&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;结果：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[BluetoothDevice] Calling Device1.Pair() method via D-Bus
[BluetoothDevice] Device1.Pair() method succeeded  &amp;lt;--- SUCCESS!&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;稳定。每次都稳定。&lt;code class=&quot;language-text&quot;&gt;AuthenticationFailed&lt;/code&gt; 错误完全消失了。&lt;/p&gt;
&lt;p&gt;这证明问题不在 &lt;code class=&quot;language-text&quot;&gt;Pair()&lt;/code&gt; 本身，也不在设备或 BlueZ 的配对能力。问题具体出在我们的 C++ 应用如何使用 &lt;code class=&quot;language-text&quot;&gt;sd-bus&lt;/code&gt; 注册并作为配对 agent 交互。完全相同的逻辑操作——注册一个 &lt;code class=&quot;language-text&quot;&gt;NoInputNoOutput&lt;/code&gt; agent 并调用 &lt;code class=&quot;language-text&quot;&gt;Pair()&lt;/code&gt;——在 agent 作为独立 Python 进程运行时工作得很好。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这条路通了。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;为什么内部 Agent 会失败？&lt;/h2&gt;
&lt;p&gt;我第一次撞上这个问题时，手里只有一些假设。从那以后，我找到了实际的文档证据，说明这是一个更广泛的问题——不只是我们的代码。&lt;/p&gt;
&lt;h3&gt;BlueZ 5.70+ 回归&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/bluez/bluez/issues/605&quot;&gt;BlueZ GitHub Issue #605&lt;/a&gt; 记录了一些案例：设备在 BlueZ 5.50 上可以正常配对，但在较新版本上会以 &lt;code class=&quot;language-text&quot;&gt;auth failed with status 0x05&lt;/code&gt; 失败。HCI 日志显示 &lt;code class=&quot;language-text&quot;&gt;Status: PIN or Key Missing (0x06)&lt;/code&gt;，即使已存储 link keys。解决办法？运行旧的 &lt;code class=&quot;language-text&quot;&gt;bluez-simple-agent.py&lt;/code&gt; 脚本。听起来熟悉吗？&lt;/p&gt;
&lt;h3&gt;Agent 可用性是根因&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/hbldh/bleak/issues/1434&quot;&gt;Bleak Issue #1434&lt;/a&gt; 让这一点更清楚：只有在 &lt;code class=&quot;language-text&quot;&gt;bluetoothctl&lt;/code&gt; 或 GNOME Bluetooth 正在运行时，配对才会成功，因为这些应用注册了必要的认证 agent。没有一个活跃且&lt;em&gt;真正能工作的&lt;/em&gt; agent，BlueZ 内部会返回 &lt;code class=&quot;language-text&quot;&gt;No agent available for request type 2&lt;/code&gt;——表面上则变成 &lt;code class=&quot;language-text&quot;&gt;AuthenticationFailed&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;关键认识是：仅仅&lt;em&gt;注册&lt;/em&gt;一个 agent 不够。agent 还需要以 &lt;code class=&quot;language-text&quot;&gt;bluetoothd&lt;/code&gt; 认为有效的方式响应 BlueZ 的回调。而 &lt;code class=&quot;language-text&quot;&gt;sd-bus&lt;/code&gt; 在同一个发起配对的进程里处理这件事时，有某些细节并不能满足较新版 BlueZ 的要求。&lt;/p&gt;
&lt;h3&gt;甚至可能不是 BlueZ&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://bugzilla.redhat.com/show_bug.cgi?id=1905671&quot;&gt;Red Hat Bug #1905671&lt;/a&gt; 显示，有些 &lt;code class=&quot;language-text&quot;&gt;AuthenticationFailed&lt;/code&gt; 错误与内核有关，而不是 BlueZ。Kernel 5.9 存在配对问题，而 5.8.18 和 5.10+ 没有。维护者的评论值得引用：&lt;em&gt;“Bluetooth is complex, it could be firmware, kernel, bluez, controller, end device or a combination of them all.”&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Agent Capability 不匹配&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/bluez/bluez/issues/650&quot;&gt;BlueZ Issue #650&lt;/a&gt; 记录了另一个角度：某些设备（尤其是 iOS）在与 &lt;code class=&quot;language-text&quot;&gt;NoInputNoOutput&lt;/code&gt; agent 配对时会失败，因为它们会把 Secure Connections 降级到 Legacy pairing，导致后续属性访问出现 &lt;code class=&quot;language-text&quot;&gt;Insufficient Authentication (0x05)&lt;/code&gt; 错误。这是 Security Manager Protocol（SMP）协商问题，不是 agent 注册问题——但它会产生同样的错误消息。&lt;/p&gt;
&lt;h3&gt;我们这个案例中最可能的罪魁&lt;/h3&gt;
&lt;p&gt;结合这些证据，内部 &lt;code class=&quot;language-text&quot;&gt;sd-bus&lt;/code&gt; agent 失败最可能的解释是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;时序&lt;/strong&gt;——我们事件循环中的 &lt;code class=&quot;language-text&quot;&gt;sd-bus&lt;/code&gt; 注册或方法处理，没有在 &lt;code class=&quot;language-text&quot;&gt;bluetoothd&lt;/code&gt; 期望的精确窗口内响应。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;sd-bus&lt;/code&gt; 与 &lt;code class=&quot;language-text&quot;&gt;python-dbus&lt;/code&gt; 的细微差异&lt;/strong&gt;——这些库与 D-Bus 守护进程交互或处理对象生命周期的方式不同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BlueZ 5.66+ 中更严格的要求&lt;/strong&gt;——agent 交互的内部序列发生了变化，而 &lt;code class=&quot;language-text&quot;&gt;sd-bus&lt;/code&gt; 在与发起配对的应用同进程使用时无法满足。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第二堵墙：D-Bus 信号不可靠&lt;/h2&gt;
&lt;p&gt;越过 &lt;code class=&quot;language-text&quot;&gt;AuthenticationFailed&lt;/code&gt; 是一大步，但事情还没结束。有了外部 agent，&lt;code class=&quot;language-text&quot;&gt;Pair()&lt;/code&gt; 成功了——但我们无法可靠地&lt;em&gt;检测&lt;/em&gt;它什么时候完成。&lt;/p&gt;
&lt;p&gt;我们依赖 D-Bus 的 &lt;code class=&quot;language-text&quot;&gt;PropertiesChanged&lt;/code&gt; 信号（通过 &lt;code class=&quot;language-text&quot;&gt;sd-bus&lt;/code&gt;）来知道 &lt;code class=&quot;language-text&quot;&gt;Paired&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;Trusted&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;Connected&lt;/code&gt; 和 &lt;code class=&quot;language-text&quot;&gt;ServicesResolved&lt;/code&gt; 什么时候变成 &lt;code class=&quot;language-text&quot;&gt;true&lt;/code&gt;。有时信号会来。有时来得很晚。有时根本不来。&lt;/p&gt;
&lt;p&gt;所以我们实现了&lt;strong&gt;主动轮询&lt;/strong&gt;——当信号不出现时，直接查询属性值作为兜底：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BluetoothDevice&lt;/span&gt;&lt;span class=&quot;token double-colon punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isPaired&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;bool&lt;/span&gt; cachedValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; mockPaired_&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Check signal-updated cache&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachedValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Signal didn&apos;t fire? Poll D-Bus directly.&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Logger&lt;/span&gt;&lt;span class=&quot;token double-colon punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;[Polling] Polling Paired property via D-Bus...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;bool&lt;/span&gt; polledValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    adapter_&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;getObjectProperty&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        devicePath_&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;org.bluez.Device1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Paired&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; polledValue
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;polledValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; mockPaired_&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Update cache&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; polledValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;每一个状态转换方法（&lt;code class=&quot;language-text&quot;&gt;isPaired()&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;isTrusted()&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;isConnected()&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;areServicesResolved()&lt;/code&gt;）都遵循同一个模式：先检查缓存的原子布尔值（如果 signal handler 工作，它会更新这个值），然后退回到一次直接的 D-Bus &lt;code class=&quot;language-text&quot;&gt;Get&lt;/code&gt; 属性调用。&lt;/p&gt;
&lt;p&gt;不优雅。但必要。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这条路也通了。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;完整修复&lt;/h2&gt;
&lt;p&gt;下面是整理后的方案。如果你在 Linux 上使用 BlueZ 5.66+ 构建自动 Bluetooth 配对，并且撞上了 &lt;code class=&quot;language-text&quot;&gt;AuthenticationFailed&lt;/code&gt;：&lt;/p&gt;
&lt;h3&gt;第 1 步：拿到 simple-agent.py&lt;/h3&gt;
&lt;p&gt;从 &lt;a href=&quot;https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/test/simple-agent&quot;&gt;BlueZ source tree&lt;/a&gt; 获取它。&lt;/p&gt;
&lt;h3&gt;第 2 步：运行外部 agent&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; python simple-agent.py NoInputNoOutput&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;让它在一个单独终端里保持运行（或者作为后台服务运行）。&lt;/p&gt;
&lt;h3&gt;第 3 步：从你的应用中移除内部 agent&lt;/h3&gt;
&lt;p&gt;从你的 C++ 应用里移除所有 &lt;code class=&quot;language-text&quot;&gt;RegisterAgent&lt;/code&gt; / &lt;code class=&quot;language-text&quot;&gt;RequestDefaultAgent&lt;/code&gt; 调用。让外部 Python agent 处理认证回调。&lt;/p&gt;
&lt;h3&gt;第 4 步：添加 D-Bus 属性轮询&lt;/h3&gt;
&lt;p&gt;不要只依赖 &lt;code class=&quot;language-text&quot;&gt;PropertiesChanged&lt;/code&gt; 信号。对于每个关键属性（&lt;code class=&quot;language-text&quot;&gt;Paired&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;Trusted&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;Connected&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;ServicesResolved&lt;/code&gt;），实现上面展示的缓存后轮询模式。从主循环里周期性轮询。&lt;/p&gt;
&lt;h3&gt;第 5 步：验证&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;确认外部 agent 正在运行（&lt;code class=&quot;language-text&quot;&gt;sudo python simple-agent.py NoInputNoOutput&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;运行你的应用。&lt;code class=&quot;language-text&quot;&gt;Pair()&lt;/code&gt; 应该会成功。&lt;/li&gt;
&lt;li&gt;观察轮询日志——你应该能看到用于状态转换的 D-Bus 属性查询。&lt;/li&gt;
&lt;li&gt;如果 &lt;code class=&quot;language-text&quot;&gt;Pair()&lt;/code&gt; 仍然失败，检查你的 BlueZ 版本（&lt;code class=&quot;language-text&quot;&gt;bluetoothd --version&lt;/code&gt;）和 kernel 版本——问题可能更深。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;代价是什么&lt;/h2&gt;
&lt;p&gt;我不会假装这是一个干净的方案。它不是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;外部依赖&lt;/strong&gt;——你的应用现在需要另一个 Python 进程保持运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更多复杂度&lt;/strong&gt;——主循环里有轮询逻辑，还叠在 signal handlers 之上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更不自包含&lt;/strong&gt;——单个二进制文件的梦想没了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但它能工作。可靠地工作。而当你已经盯着 &lt;code class=&quot;language-text&quot;&gt;AuthenticationFailed&lt;/code&gt; 看了两天，“它能工作”就是最重要的事。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;参考资料&lt;/h3&gt;
&lt;p&gt;&lt;a id=&quot;ref1&quot;&gt;&lt;/a&gt;1. &lt;a href=&quot;https://github.com/bluez/bluez/issues/55&quot;&gt;BlueZ GitHub Issue #55: Device characteristics and pairing timing&lt;/a&gt; -- &lt;em&gt;与 agent 时序相关的间歇性配对失败。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref2&quot;&gt;&lt;/a&gt;2. &lt;a href=&quot;https://forums.raspberrypi.com/viewtopic.php?t=324225&quot;&gt;Bluetooth Auto Pairing with NoInputNoOutput Agent Issues&lt;/a&gt; -- &lt;em&gt;关于无头配对挑战的论坛讨论。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref3&quot;&gt;&lt;/a&gt;3. &lt;a href=&quot;https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/test/simple-agent&quot;&gt;BlueZ Source: test/simple-agent&lt;/a&gt; -- &lt;em&gt;标准 Python agent。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref4&quot;&gt;&lt;/a&gt;4. &lt;a href=&quot;https://github.com/bluez/bluez/issues/605&quot;&gt;BlueZ GitHub Issue #605: Pairing regression in 5.70+&lt;/a&gt; -- &lt;em&gt;较新 BlueZ 版本中的已记录失败。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref5&quot;&gt;&lt;/a&gt;5. &lt;a href=&quot;https://github.com/hbldh/bleak/issues/1434&quot;&gt;Bleak Issue #1434: Pairing requires active agent&lt;/a&gt; -- &lt;em&gt;证明 agent 可用性是根因的证据。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref6&quot;&gt;&lt;/a&gt;6. &lt;a href=&quot;https://bugzilla.redhat.com/show_bug.cgi?id=1905671&quot;&gt;Red Hat Bug #1905671: Kernel-related pairing failures&lt;/a&gt; -- &lt;em&gt;不总是 BlueZ——有时是 kernel。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref7&quot;&gt;&lt;/a&gt;7. &lt;a href=&quot;https://github.com/bluez/bluez/issues/650&quot;&gt;BlueZ GitHub Issue #650: Agent capability mismatch&lt;/a&gt; -- &lt;em&gt;NoInputNoOutput 导致的 SMP 协商失败。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref8&quot;&gt;&lt;/a&gt;8. &lt;a href=&quot;https://bluez.readthedocs.io/en/latest/agent-api/&quot;&gt;BlueZ Agent API Documentation&lt;/a&gt; -- &lt;em&gt;官方 agent 接口参考。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref9&quot;&gt;&lt;/a&gt;9. &lt;a href=&quot;https://technotes.kynetics.com/2018/pairing_agents_bluez/&quot;&gt;Kynetics: Pairing Agents in the BlueZ Stack&lt;/a&gt; -- &lt;em&gt;关于 agent 注册的技术深入分析。&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;相关文章&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/zh/huawei-watch-d2-proprietary-protocol-vendor-lockin/&quot;&gt;Huawei Watch D2 BLE 配对：协议与厂商锁定&lt;/a&gt; -- 促成这次调查的项目。Watch D2 在标准 BLE 配对之上还需要一个专有的应用层握手，所以我们一开始才需要自动配对可靠工作。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/zh/m1-mac-android-emulator-bluetooth-passthrough-bumble/&quot;&gt;在 M1 Mac 上用 Bumble 和 API 32 修复 Android Emulator Bluetooth&lt;/a&gt; -- 另一场 Bluetooth 集成战斗，这次是把 Mac 的实体无线电桥接进 Android Emulator。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[开发者指南：Shopware 6.5/6.6 的类与命名空间更新]]></title><description><![CDATA[Shopware 6.5 和 6.6 对类、命名空间、data attribute 以及安全机制引入了几项重要变更。开发者在升级或维护 Shopware…]]></description><link>https://bdteo.com/zh/understanding-class-namespace-changes-shopware-6-5-developers-guide/</link><guid isPermaLink="false">https://bdteo.com/zh/understanding-class-namespace-changes-shopware-6-5-developers-guide/</guid><pubDate>Sat, 30 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Shopware 6.5 和 6.6 对类、命名空间、data attribute 以及安全机制引入了几项重要变更。开发者在升级或维护 Shopware 项目时需要了解这些变化。本文会用简洁但完整的方式梳理这些变更，并说明它们的影响，以及你的代码该如何相应调整。&lt;/p&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;随着 Shopware 演进，更新通常会带来改进、优化和新功能。不过，它们也可能引入会影响现有代码库的变化。理解这些变化，是顺利过渡并真正用好新能力的关键。&lt;/p&gt;
&lt;p&gt;本文聚焦以下几个重点领域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Elasticsearch 命名空间迁移&lt;/li&gt;
&lt;li&gt;媒体路径处理更新&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;AvailableCombinationLoader&lt;/code&gt; 中的方法变更&lt;/li&gt;
&lt;li&gt;Symfony 框架升级到版本 6&lt;/li&gt;
&lt;li&gt;库存处理更新&lt;/li&gt;
&lt;li&gt;Storefront Bootstrap 升级&lt;/li&gt;
&lt;li&gt;Offcanvas Cart data attribute 变更&lt;/li&gt;
&lt;li&gt;CSRF 保护变更&lt;/li&gt;
&lt;li&gt;Rule Builder 增强&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们逐项看。&lt;/p&gt;
&lt;h2&gt;1. Elasticsearch 命名空间迁移&lt;/h2&gt;
&lt;h3&gt;变更概览&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;旧命名空间：&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;ONGR\ElasticsearchDSL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新命名空间：&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;OpenSearchDSL&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;影响&lt;/h3&gt;
&lt;p&gt;所有与 Elasticsearch 交互的类和方法，都需要更新命名空间，以反映从 &lt;code class=&quot;language-text&quot;&gt;ONGR\ElasticsearchDSL&lt;/code&gt; 到 &lt;code class=&quot;language-text&quot;&gt;OpenSearchDSL&lt;/code&gt; 的迁移。&lt;/p&gt;
&lt;h3&gt;需要采取的动作&lt;/h3&gt;
&lt;p&gt;更新代码中的 import 语句和引用，改用新的命名空间。&lt;/p&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Before&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;token package&quot;&gt;ONGR&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;ElasticsearchDSL&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;Query&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;MatchQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token variable&quot;&gt;$query&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MatchQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;field&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;value&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// After&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;token package&quot;&gt;OpenSearchDSL&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;Query&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;MatchQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token variable&quot;&gt;$query&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MatchQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;field&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string single-quoted-string&quot;&gt;&apos;value&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;观察&lt;/h3&gt;
&lt;p&gt;这个变更符合行业更广泛地转向 OpenSearch 的趋势。OpenSearch 是 Elasticsearch 的社区驱动分支。更新到新的命名空间，可以确保你的代码继续兼容后续发展与支持。&lt;/p&gt;
&lt;h2&gt;2. 媒体路径处理&lt;/h2&gt;
&lt;h3&gt;变更概览&lt;/h3&gt;
&lt;p&gt;媒体路径现在会直接存储在数据库中，而不是动态生成。&lt;/p&gt;
&lt;h3&gt;影响&lt;/h3&gt;
&lt;p&gt;以前依赖动态路径生成的类和服务，现在需要从数据库中读取媒体路径。&lt;/p&gt;
&lt;h3&gt;需要采取的动作&lt;/h3&gt;
&lt;p&gt;调整代码，使用 &lt;code class=&quot;language-text&quot;&gt;MediaEntity&lt;/code&gt; 的 &lt;code class=&quot;language-text&quot;&gt;getPath()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Before (dynamic path generation)&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$mediaPath&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$mediaService&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$mediaId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// After (database-stored path)&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$mediaPath&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$mediaEntity&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;观察&lt;/h3&gt;
&lt;p&gt;把媒体路径存入数据库可以减少计算开销，从而提升性能。它也让媒体处理更一致、更可靠。&lt;/p&gt;
&lt;h2&gt;3. &lt;code class=&quot;language-text&quot;&gt;AvailableCombinationLoader&lt;/code&gt; 中的方法更新&lt;/h2&gt;
&lt;h3&gt;变更概览&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;AbstractAvailableCombinationLoader&lt;/code&gt; 中的 &lt;code class=&quot;language-text&quot;&gt;load()&lt;/code&gt; 方法已被 &lt;code class=&quot;language-text&quot;&gt;loadCombinations()&lt;/code&gt; 取代。&lt;/p&gt;
&lt;h3&gt;影响&lt;/h3&gt;
&lt;p&gt;任何继承 &lt;code class=&quot;language-text&quot;&gt;AbstractAvailableCombinationLoader&lt;/code&gt; 的自定义类，都必须实现新的 &lt;code class=&quot;language-text&quot;&gt;loadCombinations()&lt;/code&gt; 方法，而不是旧的 &lt;code class=&quot;language-text&quot;&gt;load()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;h3&gt;需要采取的动作&lt;/h3&gt;
&lt;p&gt;重命名或重构你的方法实现，使其与新的方法名和签名保持一致。&lt;/p&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Before&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$combinations&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$combinationLoader&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$productId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// After&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$combinations&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$combinationLoader&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;loadCombinations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$productId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;观察&lt;/h3&gt;
&lt;p&gt;这个变更用一个更具描述性的名称提升了清晰度。它也可能伴随额外参数或返回类型变化，所以检查方法签名很必要。&lt;/p&gt;
&lt;h2&gt;4. Symfony 框架升级到版本 6&lt;/h2&gt;
&lt;h3&gt;变更概览&lt;/h3&gt;
&lt;p&gt;Shopware 已将 Symfony 组件升级到版本 6。&lt;/p&gt;
&lt;h3&gt;影响&lt;/h3&gt;
&lt;p&gt;由于废弃功能被移除、方法签名发生变化，这次升级会带来一些破坏性变更。依赖旧 Symfony 特性的自定义代码可能会报错，或者产生警告。&lt;/p&gt;
&lt;h3&gt;需要采取的动作&lt;/h3&gt;
&lt;p&gt;检查代码中是否使用了任何已废弃的 Symfony 功能，并更新到与 Symfony 6 兼容的写法。&lt;/p&gt;
&lt;h3&gt;观察&lt;/h3&gt;
&lt;p&gt;跟上最新 Symfony 版本，可以获得更好的性能、安全性和新功能。不过，它也要求你认真审查和测试代码，确保兼容性。&lt;/p&gt;
&lt;h2&gt;5. 库存处理更新&lt;/h2&gt;
&lt;h3&gt;变更概览&lt;/h3&gt;
&lt;p&gt;Shopware 引入了新的 Stock API，可通过 &lt;code class=&quot;language-text&quot;&gt;STOCK_HANDLING&lt;/code&gt; feature flag 启用。&lt;/p&gt;
&lt;h3&gt;影响&lt;/h3&gt;
&lt;p&gt;与库存管理相关的类和服务可能需要适配新的 API 结构，尤其是那些直接与库存数据交互的代码。&lt;/p&gt;
&lt;h3&gt;需要采取的动作&lt;/h3&gt;
&lt;p&gt;使用新 API 提供的库存处理方法，并确保所有库存相关逻辑与更新后的结构一致。&lt;/p&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;php&quot;&gt;&lt;pre class=&quot;language-php&quot;&gt;&lt;code class=&quot;language-php&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Before&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$stock&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$productEntity&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getStock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// After&lt;/span&gt;
&lt;span class=&quot;token variable&quot;&gt;$stock&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$stockService&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getStock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$productId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;观察&lt;/h3&gt;
&lt;p&gt;新的 Stock API 提供了更稳健、更灵活的库存管理方式，有可能简化自定义开发以及与外部系统的集成。&lt;/p&gt;
&lt;h2&gt;6. Storefront Bootstrap 升级&lt;/h2&gt;
&lt;h3&gt;变更概览&lt;/h3&gt;
&lt;p&gt;Storefront 已从 Bootstrap 4 升级到 Bootstrap 5，并移除了对 jQuery 的依赖。&lt;/p&gt;
&lt;h3&gt;影响&lt;/h3&gt;
&lt;p&gt;依赖 jQuery 或 Bootstrap 4 组件的自定义 JavaScript 代码和模板，需要重构为符合 Bootstrap 5 的写法，并在必要时改用原生 JavaScript。&lt;/p&gt;
&lt;h3&gt;需要采取的动作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;用原生 JavaScript 或 Bootstrap 5 工具替换 jQuery 用法。&lt;/li&gt;
&lt;li&gt;更新 Bootstrap class 和组件，使其匹配 Bootstrap 5 的命名与结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;观察&lt;/h3&gt;
&lt;p&gt;Bootstrap 5 带来了性能改进、更少依赖，以及现代化组件。升级可能耗时，但从长期看，对可维护性和用户体验都有好处。&lt;/p&gt;
&lt;h2&gt;7. Offcanvas Cart Data Attribute 变更（Shopware 6.6）&lt;/h2&gt;
&lt;h3&gt;变更概览&lt;/h3&gt;
&lt;p&gt;在 Shopware 6.6 中，用于触发 offcanvas cart 功能的 data attribute 出现了一个细微但重要的变更。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;旧 Data Attribute：&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;data-offcanvas-cart&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新 Data Attribute：&lt;/strong&gt; &lt;code class=&quot;language-text&quot;&gt;data-off-canvas-cart&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;影响&lt;/h3&gt;
&lt;p&gt;如果自定义模板或主题仍使用不带连字符的 &lt;code class=&quot;language-text&quot;&gt;data-offcanvas-cart&lt;/code&gt; attribute，offcanvas cart 可能不再按预期工作，因为 Shopware 6.6 中的 JavaScript listener 会查找带连字符的版本。&lt;/p&gt;
&lt;h3&gt;需要采取的动作&lt;/h3&gt;
&lt;p&gt;把模板中的 &lt;code class=&quot;language-text&quot;&gt;data-offcanvas-cart&lt;/code&gt; attribute 更新为 &lt;code class=&quot;language-text&quot;&gt;data-off-canvas-cart&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;html&quot;&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Before --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;header-cart&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;data-offcanvas-cart&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;true&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Cart content --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- After --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;header-cart&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;data-off-canvas-cart&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;true&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Cart content --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;观察&lt;/h3&gt;
&lt;p&gt;这个变更在官方 Shopware 6.6 release notes 中记录得并不充分，但它对 offcanvas cart 的正常工作很关键。负责初始化购物车功能的 JavaScript 依赖 &lt;code class=&quot;language-text&quot;&gt;data-off-canvas-cart&lt;/code&gt; attribute，任何偏差都可能让购物车无法工作。&lt;/p&gt;
&lt;h3&gt;额外说明&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一致性很关键：&lt;/strong&gt; 确保所有使用 offcanvas cart attribute 的地方都已更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;彻底测试：&lt;/strong&gt; 修改后测试购物车功能，确认它能按预期工作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;留意类似变更：&lt;/strong&gt; 其他 data attribute 或 event listener 可能也有类似更新；相应检查你的自定义模板。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;8. CSRF 保护变更&lt;/h2&gt;
&lt;h3&gt;变更概览&lt;/h3&gt;
&lt;p&gt;Shopware 6.5 及之后的版本移除了模板中的显式 CSRF token 处理，转而使用 SameSite cookie 策略进行 CSRF 保护。&lt;/p&gt;
&lt;h3&gt;影响&lt;/h3&gt;
&lt;p&gt;以前通过 &lt;code class=&quot;language-text&quot;&gt;sw_csrf&lt;/code&gt; 函数包含 CSRF token 的模板和表单会遇到错误，因为这个函数已经不存在。&lt;/p&gt;
&lt;h3&gt;需要采取的动作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;移除 CSRF Token 函数：&lt;/strong&gt; 从模板中删除 &lt;code class=&quot;language-text&quot;&gt;{{ sw_csrf(&apos;route_name&apos;) }}&lt;/code&gt; 的用法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖 SameSite Cookies：&lt;/strong&gt; 信任内置的 SameSite cookie 策略进行 CSRF 保护；表单不再需要显式 token。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调整表单属性：&lt;/strong&gt; 确保表单和 AJAX 请求配置正确，能与新的 CSRF 保护机制协同工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;twig&quot;&gt;&lt;pre class=&quot;language-twig&quot;&gt;&lt;code class=&quot;language-twig&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Before --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{{&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&apos;&lt;/span&gt;frontend.checkout.line-item.add&lt;span class=&quot;token punctuation&quot;&gt;&apos;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{{&lt;/span&gt; sw_csrf&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&apos;&lt;/span&gt;frontend.checkout.line-item.add&lt;span class=&quot;token punctuation&quot;&gt;&apos;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Form fields --&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Add to Cart&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- After --&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{{&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&apos;&lt;/span&gt;frontend.checkout.line-item.add&lt;span class=&quot;token punctuation&quot;&gt;&apos;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Form fields --&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Add to Cart&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;观察&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;错误解决：&lt;/strong&gt; 移除 &lt;code class=&quot;language-text&quot;&gt;sw_csrf&lt;/code&gt; 函数可以解决 &quot;Unknown &apos;sw_csrf&apos; function&quot; 错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全性保持：&lt;/strong&gt; SameSite cookie 策略不需要额外 token，仍能继续防护 CSRF 攻击。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模板更简洁：&lt;/strong&gt; 不再需要 CSRF token 后，表单会更干净，也稍微简单一些。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;额外说明&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;测试至关重要：&lt;/strong&gt; 完成这些改动后，彻底测试表单提交，确保它们正确工作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;理解新机制：&lt;/strong&gt; 熟悉 SameSite cookie 策略的运行方式，以维持应用安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新文档：&lt;/strong&gt; 确保内部文档反映这个变更，避免以后再次混淆。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;9. 处理 Offcanvas Cart 问题&lt;/h2&gt;
&lt;h3&gt;场景概览&lt;/h3&gt;
&lt;p&gt;在更新模板并移除 &lt;code class=&quot;language-text&quot;&gt;sw_csrf&lt;/code&gt; 函数后，开发者仍可能遇到这样的问题：点击 &quot;Add to Cart&quot; 按钮会打开 offcanvas cart，但购物车看起来是空的。&lt;/p&gt;
&lt;h3&gt;根因&lt;/h3&gt;
&lt;p&gt;offcanvas cart 可能因为表单提交中缺少或错误的参数而没有显示已添加商品，尤其是缺少 &lt;code class=&quot;language-text&quot;&gt;redirectTo&lt;/code&gt; input field。&lt;/p&gt;
&lt;h3&gt;需要采取的动作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;添加 &lt;code class=&quot;language-text&quot;&gt;redirectTo&lt;/code&gt; 参数：&lt;/strong&gt; 在 add-to-cart 表单中加入一个名为 &lt;code class=&quot;language-text&quot;&gt;redirectTo&lt;/code&gt;、值为 &lt;code class=&quot;language-text&quot;&gt;frontend.cart.offcanvas&lt;/code&gt; 的隐藏 input field。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确保 Data Attribute 正确：&lt;/strong&gt; 确认所有必要的 data attribute 都存在，并且命名正确。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;twig&quot;&gt;&lt;pre class=&quot;language-twig&quot;&gt;&lt;code class=&quot;language-twig&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{{&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&apos;&lt;/span&gt;frontend.checkout.line-item.add&lt;span class=&quot;token punctuation&quot;&gt;&apos;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token delimiter punctuation&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;redirectTo&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;frontend.cart.offcanvas&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;lineItems[&lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{{&lt;/span&gt; product&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token delimiter punctuation&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;][id]&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token twig language-twig&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{{&lt;/span&gt; product&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token delimiter punctuation&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;&amp;lt;!-- Other form fields --&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Add to Cart&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;观察&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;功能恢复：&lt;/strong&gt; 添加 &lt;code class=&quot;language-text&quot;&gt;redirectTo&lt;/code&gt; 参数会告诉 Shopware 在加入商品后加载 offcanvas cart，从而确保购物车正确显示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;细节的重要性：&lt;/strong&gt; 缺少一个 input field 这样的小遗漏，也可能造成明显的功能问题。这再次说明，仔细 code review 很重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;额外说明&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data Attribute 的一致性：&lt;/strong&gt; 再次确认 &lt;code class=&quot;language-text&quot;&gt;data-product-id&lt;/code&gt; 这类 data attribute 设置正确。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查 JavaScript 依赖：&lt;/strong&gt; 确保与购物车相关的 JavaScript plugin 或组件已正确加载和初始化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清缓存：&lt;/strong&gt; 修改后清理 Shopware 缓存和浏览器缓存，避免旧文件造成问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;10. Rule Builder 增强&lt;/h2&gt;
&lt;h3&gt;变更概览&lt;/h3&gt;
&lt;p&gt;Rule Builder API 已扩展，支持更复杂的条件逻辑。&lt;/p&gt;
&lt;h3&gt;影响&lt;/h3&gt;
&lt;p&gt;自定义规则和条件可能需要调整，以符合增强后的 Rule Builder 提供的新接口或方法。&lt;/p&gt;
&lt;h3&gt;需要采取的动作&lt;/h3&gt;
&lt;p&gt;查看 Rule Builder 文档，并更新自定义规则实现以确保兼容性。&lt;/p&gt;
&lt;h3&gt;观察&lt;/h3&gt;
&lt;p&gt;增强后的规则能力可以在 Shopware 内实现更精确的定位和自定义。用好这些新功能，能提升系统的适配性，并给最终用户带来更个性化的体验。&lt;/p&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;Shopware 6.5 和 6.6 对类、命名空间、data attribute 和安全机制引入了几项重要变更。开发者必须处理这些变化，才能维持兼容性，并利用新功能。更新代码库需要仔细审查和测试，但也提供了改善性能、安全性和功能的机会。&lt;/p&gt;
&lt;h2&gt;建议&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;提前规划：&lt;/strong&gt; 升级前，阅读官方 Shopware release notes 和 upgrade guide，获得完整信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;彻底测试：&lt;/strong&gt; 在 staging 环境中实施变更，并进行充分测试，以发现并修复问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;善用文档：&lt;/strong&gt; 利用 Shopware 文档和社区论坛，获取具体变更的指导。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保持关注：&lt;/strong&gt; 跟进未来更新，提前预判后续变化并做好准备。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;额外观察&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;关注细节：&lt;/strong&gt; data attribute 中的连字符、函数移除这类小变化，都可能造成重大影响。始终认真审查模板更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;社区支持：&lt;/strong&gt; Shopware 社区活跃且协作性强。与其他开发者交流，往往能得到常见问题的洞见和解决方案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最佳实践：&lt;/strong&gt; 采用更新后的最佳实践，比如用原生 JavaScript 替代 jQuery、依赖现代安全策略，会带来更清晰、更高效的代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控废弃项：&lt;/strong&gt; 留意 deprecation notice，并为未来的移除提前准备，可以减少升级时的中断。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;通过理解并处理 Shopware 6.5 和 6.6 中关于类、命名空间、data attribute 与安全机制的变化，开发者可以顺利过渡，并继续构建稳健、面向未来的电商解决方案。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Docker Compose 的演进：变了什么，为什么重要]]></title><description><![CDATA[TL;DR: Docker Compose v1（）已在 2025 年 4 月被完全移除。你的 YAML 里的  字段已经死了。 键现在就是 。watch 模式已经带着  达到生产可用。有一个关键的路径遍历 CVE（CVE-2025-62725），如果你把  和 OCI…]]></description><link>https://bdteo.com/zh/docker-compose-major-changes-since-october-2023/</link><guid isPermaLink="false">https://bdteo.com/zh/docker-compose-major-changes-since-october-2023/</guid><pubDate>Wed, 20 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Docker Compose v1（&lt;code class=&quot;language-text&quot;&gt;docker-compose&lt;/code&gt;）已在 2025 年 4 月被完全移除。你的 YAML 里的 &lt;code class=&quot;language-text&quot;&gt;version&lt;/code&gt; 字段已经死了。&lt;code class=&quot;language-text&quot;&gt;x-develop&lt;/code&gt; 键现在就是 &lt;code class=&quot;language-text&quot;&gt;develop&lt;/code&gt;。watch 模式已经带着 &lt;code class=&quot;language-text&quot;&gt;initial_sync&lt;/code&gt; 达到生产可用。有一个关键的路径遍历 CVE（CVE-2025-62725），如果你把 &lt;code class=&quot;language-text&quot;&gt;include&lt;/code&gt; 和 OCI artifact 一起用，就该升级到 v2.40.2+。是的，Compose 从 v2 跳到了 v5。细节在下面。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;最初发布于 2024 年 11 月。2026 年 3 月更新，加入 Compose v5、CVE-2025-62725、v1 移除，以及新的 spec 功能。&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果你用 Docker Compose 已经有一段时间，可能已经注意到脚下的东西在坏掉，或者悄悄变了。过去两年是 Compose 历史上变化最激进的两年，而且并不是所有变化都显而易见。&lt;/p&gt;
&lt;p&gt;我每天都用 Compose。我的大多数&lt;a href=&quot;/zh/laravel-sail-vs-laradock-choosing-right-docker-solution/&quot;&gt;开发环境&lt;/a&gt;都跑在它上面。东西一变，我会注意到。下面是实际重要的部分。&lt;/p&gt;
&lt;h2&gt;坏掉了什么&lt;/h2&gt;
&lt;h3&gt;docker-compose 已死&lt;/h3&gt;
&lt;p&gt;不是 deprecated。不是维护模式。&lt;strong&gt;死了。&lt;/strong&gt; 独立的 &lt;code class=&quot;language-text&quot;&gt;docker-compose&lt;/code&gt; 二进制文件，也就是基于 Python 的 v1，已经在 2025 年 4 月从 GitHub Actions runner 和官方 Docker 镜像中移除 &lt;small&gt;&lt;a href=&quot;#ref1&quot;&gt;[1]&lt;/a&gt;&lt;/small&gt;。如果你的 CI/CD pipeline 里还在引用带连字符的 &lt;code class=&quot;language-text&quot;&gt;docker-compose&lt;/code&gt;，它们已经坏了，或者马上就会坏。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# This no longer works&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker-compose&lt;/span&gt; up &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# This is the only way now&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;基于 Go 的 &lt;code class=&quot;language-text&quot;&gt;docker compose&lt;/code&gt;（v2，现在是 v5）自 2022 年以来就是真正的实现。v1 CLI 只是为了兼容性挂着维生系统。现在维生系统停了。&lt;/p&gt;
&lt;h3&gt;version 字段没了&lt;/h3&gt;
&lt;p&gt;别再把 &lt;code class=&quot;language-text&quot;&gt;version: &quot;3.8&quot;&lt;/code&gt; 放在 Compose 文件顶部了。它什么也不做。自 v2 起它就被忽略，现在也正式 deprecated。现代 Compose 文件从 &lt;code class=&quot;language-text&quot;&gt;services:&lt;/code&gt; 开始。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Stop doing this&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;3.8&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx

&lt;span class=&quot;token comment&quot;&gt;# Just do this&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; nginx&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;如果你在教程里看到 &lt;code class=&quot;language-text&quot;&gt;version:&lt;/code&gt;，那篇教程已经过时。&lt;/p&gt;
&lt;h3&gt;其他弃用项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;links&lt;/code&gt;&lt;/strong&gt; -- 使用 Docker networks。Links 从 Compose v2 发布起就已经是 legacy。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;container_name&lt;/code&gt;&lt;/strong&gt; -- 让 Docker 管理名称。硬编码名称会破坏扩缩容，并造成冲突。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复杂挂载的短 volume 语法&lt;/strong&gt; -- 使用带 &lt;code class=&quot;language-text&quot;&gt;type&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;source&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;target&lt;/code&gt; 的长格式语法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;新的、而且真正有用的东西&lt;/h2&gt;
&lt;h3&gt;Watch 模式（现在生产可用）&lt;/h3&gt;
&lt;p&gt;这是多年来最大的生活质量改进。&lt;code class=&quot;language-text&quot;&gt;develop&lt;/code&gt; 区段（之前是 &lt;code class=&quot;language-text&quot;&gt;x-develop&lt;/code&gt; -- 去掉 &lt;code class=&quot;language-text&quot;&gt;x-&lt;/code&gt; 前缀，它已经不再是实验性的）让你定义文件 watch 规则，在文件变化时自动同步或重建：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .
    &lt;span class=&quot;token key atrule&quot;&gt;develop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;watch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./src
          &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; sync
          &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /app/src
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./package.json
          &lt;span class=&quot;token key atrule&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; rebuild&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;有三种可用 action（自 v2.32.0 起）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;sync&lt;/code&gt;&lt;/strong&gt; -- 不重建，直接把变更文件复制进容器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;restart&lt;/code&gt;&lt;/strong&gt; -- 文件变化时重启服务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;rebuild&lt;/code&gt;&lt;/strong&gt; -- 触发完整重建&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;截至 2025 年 9 月，还多了 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;initial_sync&lt;/code&gt;&lt;/strong&gt;。它会在你启动 &lt;code class=&quot;language-text&quot;&gt;docker compose watch&lt;/code&gt; 时立刻同步所有文件，所以你不必等第一次文件变化才触发同步。这曾经是很长一段时间里的痛点。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose &lt;span class=&quot;token function&quot;&gt;watch&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;开发时不再需要手动 rebuild。这真的改变了我的工作流。&lt;/p&gt;
&lt;h3&gt;通过 OCI Artifacts 使用 Include&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;include&lt;/code&gt; 指令现在可以从 OCI registry 拉取 Compose 片段：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; oci&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//docker.io/username/my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;compose&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;fragment&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;还提供了实验性的 Git repository 支持。这很适合在多个项目之间共享通用服务定义，比如数据库配置、监控栈等等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但先读下面的安全部分。&lt;/strong&gt; 这里有一个 CVE。&lt;/p&gt;
&lt;h3&gt;GPU 支持&lt;/h3&gt;
&lt;p&gt;GPU passthrough 变得更干净了。现在除了冗长的 &lt;code class=&quot;language-text&quot;&gt;deploy.resources.reservations.devices&lt;/code&gt; 写法，也有更短的 &lt;code class=&quot;language-text&quot;&gt;gpus:&lt;/code&gt; 语法（v2.30.0+）。AMD GPU 支持也在 2025 年正式集成，不再只是 NVIDIA。&lt;/p&gt;
&lt;h3&gt;Models 元素&lt;/h3&gt;
&lt;p&gt;Compose spec 现在包含一个 &lt;code class=&quot;language-text&quot;&gt;models&lt;/code&gt; 元素，用来把 AI/ML 模型定义为 OCI artifacts。你可以把 LLM 和推理 runtime 直接打包进 Compose 设置里。很小众，但如果你在做本地 AI 工作，它挺有意思。&lt;/p&gt;
&lt;h3&gt;更好的依赖关系&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;depends_on&lt;/code&gt; 条件变得更灵活了：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; service_healthy
        &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;# restart web if db restarts&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;     &lt;span class=&quot;token comment&quot;&gt;# web can start even if db isn&apos;t ready&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;restart: true&lt;/code&gt; 和 &lt;code class=&quot;language-text&quot;&gt;required: false&lt;/code&gt; 对更有韧性的本地开发环境来说，是真的有用。&lt;/p&gt;
&lt;h2&gt;你应该知道的事&lt;/h2&gt;
&lt;h3&gt;CVE-2025-62725：Include 路径遍历&lt;/h3&gt;
&lt;p&gt;如果你把 &lt;code class=&quot;language-text&quot;&gt;include&lt;/code&gt; 指令和 OCI artifacts 一起使用，&lt;strong&gt;立刻升级到 v2.40.2 或更高版本&lt;/strong&gt; &lt;small&gt;&lt;a href=&quot;#ref2&quot;&gt;[2]&lt;/a&gt;&lt;/small&gt;。一个路径遍历漏洞允许攻击者在 artifact 解析期间逃出缓存目录。即使是看起来无害的 &lt;code class=&quot;language-text&quot;&gt;docker compose ps&lt;/code&gt;，如果你的 Compose 文件 include 了恶意 OCI 引用，也可能触发它。&lt;/p&gt;
&lt;p&gt;Docker 用 &lt;code class=&quot;language-text&quot;&gt;validatePathInBase()&lt;/code&gt; 检查修补了这个问题，但你得运行在已修复版本上。&lt;/p&gt;
&lt;h3&gt;Compose v5&lt;/h3&gt;
&lt;p&gt;Docker 从 v2 跳到了 v5（跳过 3 和 4，是为了避免和旧的文件格式版本混淆）&lt;small&gt;&lt;a href=&quot;#ref3&quot;&gt;[3]&lt;/a&gt;&lt;/small&gt;。功能上，v5 和后期 v2 版本差不多，但它包含了一个官方 &lt;strong&gt;Go SDK&lt;/strong&gt;，可用于程序化访问。这意味着你可以把 Compose 功能直接嵌入 Go 应用里，而不用 shell out 到 CLI。&lt;/p&gt;
&lt;p&gt;检查你的版本：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose version
&lt;span class=&quot;token comment&quot;&gt;# Docker Compose version v5.1.0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Bake 是默认构建工具&lt;/h3&gt;
&lt;p&gt;Docker Bake（通过 BuildKit）现在是 &lt;code class=&quot;language-text&quot;&gt;docker compose build&lt;/code&gt; 的默认工具。它比 legacy builder 更擅长处理多目标构建、跨平台编译和高级缓存策略。如果你还没看过 &lt;code class=&quot;language-text&quot;&gt;docker-bake.hcl&lt;/code&gt; 文件，值得了解一下，尤其是在复杂的多服务构建里。&lt;/p&gt;
&lt;h3&gt;Healthcheck 改进&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;start_interval&lt;/code&gt; 字段允许你在启动宽限期内设置更快的检查间隔：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;healthcheck&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CMD&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pg_isready&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;start_period&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 30s
      &lt;span class=&quot;token key atrule&quot;&gt;start_interval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2s    &lt;span class=&quot;token comment&quot;&gt;# check every 2s during startup&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 30s         &lt;span class=&quot;token comment&quot;&gt;# then every 30s after&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;retries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这意味着你的依赖服务可以更快启动，同时不牺牲生产环境 health check 的间隔。&lt;/p&gt;
&lt;h2&gt;迁移清单&lt;/h2&gt;
&lt;p&gt;如果你已经有一阵子没更新 Compose 设置了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从所有 Compose 文件中&lt;strong&gt;移除 &lt;code class=&quot;language-text&quot;&gt;version:&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;在所有脚本和 CI 配置中，把 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;docker-compose&lt;/code&gt; 替换为 &lt;code class=&quot;language-text&quot;&gt;docker compose&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;在 watch 配置中，把 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;x-develop&lt;/code&gt; 改名为 &lt;code class=&quot;language-text&quot;&gt;develop&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果你使用 &lt;code class=&quot;language-text&quot;&gt;include&lt;/code&gt;，&lt;strong&gt;升级到 v2.40.2+&lt;/strong&gt;（CVE-2025-62725）。&lt;/li&gt;
&lt;li&gt;如果你不知怎么还在用 &lt;code class=&quot;language-text&quot;&gt;links&lt;/code&gt;，就把它们&lt;strong&gt;替换为 Docker networks&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试你的 CI&lt;/strong&gt; -- GitHub Actions 已在 2026 年 2 月把 runner 更新到 Compose v2.40.3 &lt;small&gt;&lt;a href=&quot;#ref4&quot;&gt;[4]&lt;/a&gt;&lt;/small&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;参考资料&lt;/h3&gt;
&lt;p&gt;&lt;a id=&quot;ref1&quot;&gt;&lt;/a&gt;1. &lt;a href=&quot;https://github.com/actions/runner-images/issues/9557&quot;&gt;Docker Compose v1 removed from runner images (April 2025)&lt;/a&gt; -- &lt;em&gt;GitHub Actions 关于移除 v1 的公告。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref2&quot;&gt;&lt;/a&gt;2. &lt;a href=&quot;https://www.imperva.com/blog/cve-2025-62725-from-docker-compose-ps-to-system-compromise/&quot;&gt;CVE-2025-62725: From &quot;docker compose ps&quot; to System Compromise&lt;/a&gt; -- &lt;em&gt;Imperva 对 include 路径遍历漏洞的详细分析。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref3&quot;&gt;&lt;/a&gt;3. &lt;a href=&quot;https://github.com/docker/compose/releases&quot;&gt;Docker Compose Releases&lt;/a&gt; -- &lt;em&gt;包含 v5 在内的官方发布历史。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref4&quot;&gt;&lt;/a&gt;4. &lt;a href=&quot;https://github.blog/changelog/2026-01-30-docker-and-docker-compose-version-upgrades-on-hosted-runners/&quot;&gt;Docker and Docker Compose version upgrades on hosted runners&lt;/a&gt; -- &lt;em&gt;GitHub 2026 年 2 月 runner 更新。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref5&quot;&gt;&lt;/a&gt;5. &lt;a href=&quot;https://docs.docker.com/compose/compose-file/&quot;&gt;Compose Specification&lt;/a&gt; -- &lt;em&gt;官方 Compose 文件参考。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref6&quot;&gt;&lt;/a&gt;6. &lt;a href=&quot;https://docs.docker.com/compose/how-tos/file-watch/&quot;&gt;Use Compose Watch&lt;/a&gt; -- &lt;em&gt;Docker 的 watch 模式文档。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref7&quot;&gt;&lt;/a&gt;7. &lt;a href=&quot;https://docs.docker.com/compose/how-tos/gpu-support/&quot;&gt;Enable GPU Support in Docker Compose&lt;/a&gt; -- &lt;em&gt;包含 AMD 支持在内的 GPU passthrough 文档。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref8&quot;&gt;&lt;/a&gt;8. &lt;a href=&quot;https://docs.docker.com/compose/how-tos/multiple-compose-files/include/&quot;&gt;Docker Compose Include&lt;/a&gt; -- &lt;em&gt;带 OCI 和 Git 支持的 include 指令。&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;相关文章&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/zh/laravel-sail-vs-laradock-choosing-right-docker-solution/&quot;&gt;Laravel Sail vs Laradock: Choosing the Right Docker Solution&lt;/a&gt; -- 比较基于 Docker 的 PHP 开发环境。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[释放 'git grep' 的力量，高效搜索代码]]></title><description><![CDATA[在一个辽阔的王国里，卷轴和手稿数不胜数。有一位名叫 Alaric 的学者住在那里。他的图书馆庞大得像一座知识迷宫：古老文本与当代著作并置，秘密藏在行与行之间。Alaric…]]></description><link>https://bdteo.com/zh/unlocking-the-power-of-git-grep/</link><guid isPermaLink="false">https://bdteo.com/zh/unlocking-the-power-of-git-grep/</guid><pubDate>Wed, 13 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在一个辽阔的王国里，卷轴和手稿数不胜数。有一位名叫 Alaric 的学者住在那里。他的图书馆庞大得像一座知识迷宫：古老文本与当代著作并置，秘密藏在行与行之间。Alaric 常常需要在这片信息海中寻找一句难以捉摸的短语；日子久了，这件事越来越让人发怵。&lt;/p&gt;
&lt;p&gt;某天清晨，阳光把金色洒在蒙尘的巨册上，Alaric 开始寻找档案里提到的一个概念。它只以 &quot;The Whispering Sigil&quot; 这个名字出现。他一卷卷翻阅，用惯常的方法筛过页面；可这些方法如今显得迟缓而不精确。他越往深处找，就越被无关段落、重复内容和误导性引用缠住。几小时变成几天，进展寥寥，挫败感也慢慢堆起来。&lt;/p&gt;
&lt;p&gt;后来，一位老智者来访，看出了他的困境。智者会意一笑，说：&quot;也许你是在用困难的方法找。还有一条隐秘路径，只有那些懂得整理知识的人知道。&quot; Alaric 被勾起了兴趣，听智者解释一种能聚焦搜索的方法。它能切开杂物，直接通向他要找的文本。&lt;/p&gt;
&lt;p&gt;带着这个新办法，Alaric 又试了一次。这回，无关的杂音退去了。通往 &quot;The Whispering Sigil&quot; 的路径变得清晰，他以惊人的速度找到了想要的东西。仿佛他在自己的迷宫里打开了一扇秘密之门，快速抵达了所需的确切知识。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;噗。&lt;/strong&gt; 秘密揭晓：这就是 &lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 的力量。&lt;/p&gt;
&lt;h2&gt;&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 到底是什么&lt;/h2&gt;
&lt;p&gt;普通的 &lt;code class=&quot;language-text&quot;&gt;grep -r&lt;/code&gt; 会遍历文件系统。它尽职尽责地读取路径上的一切：源代码、日志文件、构建产物、同事忘了删掉的那个 4 MB 零散 dump 文件、整个 &lt;code class=&quot;language-text&quot;&gt;node_modules&lt;/code&gt; 树。&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 做的事情更窄：它搜索 Git 已经知道的文件。它的大部分价值，就来自这一个设计选择。&lt;/p&gt;
&lt;h3&gt;&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 擅长什么&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;它搜索 tracked files，而不是文件系统。&lt;/strong&gt; Git 会维护一份列表，记录你曾经 staged 或 committed 的每个文件，也就是 index。&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 从这份列表读取。未跟踪的杂物根本不在那里。没有 &lt;code class=&quot;language-text&quot;&gt;node_modules/&lt;/code&gt;，没有 &lt;code class=&quot;language-text&quot;&gt;dist/&lt;/code&gt;，没有 coverage reports，没有随机日志文件，因为 Git 从没被告知它们存在。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在大型 repo 中，它比 &lt;code class=&quot;language-text&quot;&gt;grep -r&lt;/code&gt; 更快。&lt;/strong&gt; 它已经有文件列表，所以跳过了文件系统遍历。它还会用多线程并行执行。收益是真的，但不是魔法。&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 迭代的是 &lt;code class=&quot;language-text&quot;&gt;grep&lt;/code&gt; 也会读到的同一批 blobs，只是仪式少一点。这里没有内容搜索索引。&quot;Git index&quot; 是文件路径和 blob hash 的列表，不是 Lucene 风格的倒排索引。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;它可以在不 checkout 的情况下搜索任意 ref。&lt;/strong&gt; 这是杀手特性。tag、branch、commit、tree object，都可以直接交给 &lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt;。不用 &lt;code class=&quot;language-text&quot;&gt;git checkout&lt;/code&gt;，不用 stash 舞步，也不用偏离你手头正在做的事。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;实用示例&lt;/h3&gt;
&lt;h4&gt;基本搜索&lt;/h4&gt;
&lt;p&gt;要在仓库中搜索某个具体词，比如 &lt;code class=&quot;language-text&quot;&gt;&quot;initializeSettings&quot;&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;initializeSettings&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这会扫描当前 branch 中所有 tracked files，寻找精确匹配。&lt;/p&gt;
&lt;h4&gt;不区分大小写搜索&lt;/h4&gt;
&lt;p&gt;如果你不确定大小写，可以做不区分大小写的搜索：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;initializesettings&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;它会找到大小写不同的所有匹配。&lt;/p&gt;
&lt;h4&gt;在指定 branch 中搜索&lt;/h4&gt;
&lt;p&gt;要在另一个 branch 里搜索，而不切过去，例如 &lt;code class=&quot;language-text&quot;&gt;feature/login&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;validateUser&quot;&lt;/span&gt; feature/login&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这招很难被打败。不 checkout，不 stash，直接得到答案。&lt;/p&gt;
&lt;h4&gt;跨所有 branches 搜索&lt;/h4&gt;
&lt;p&gt;要在每个 branch 中搜索某个词，包括 remotes：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; branch &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;xargs&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;configureDatabase&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;要搜索 Git 知道的每个 commit，而不只是每个 branch tip：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;configureDatabase&quot;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; rev-list &lt;span class=&quot;token parameter variable&quot;&gt;--all&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这会在历史中任何地方的任何 blob 里寻找匹配。在繁忙的 repo 上可能需要一点时间，因为它真的在走过每个 commit。&lt;/p&gt;
&lt;h4&gt;在 commit 历史中搜索&lt;/h4&gt;
&lt;p&gt;要找出某个字符串是在什么时候被加入或移除的，可以用：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; log &lt;span class=&quot;token parameter variable&quot;&gt;-S&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;optimizePerformance&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这会显示引入或移除 &lt;code class=&quot;language-text&quot;&gt;&quot;optimizePerformance&quot;&lt;/code&gt; 这个词的 commits。&lt;/p&gt;
&lt;p&gt;要查看这个词被加入或移除时的实际 diffs：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; log &lt;span class=&quot;token parameter variable&quot;&gt;-G&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;optimizePerformance&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;使用正则表达式&lt;/h4&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 支持正则表达式，可以做更高级的搜索：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;def\s+\w+\(&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这会匹配 Python 函数定义：&lt;code class=&quot;language-text&quot;&gt;def&lt;/code&gt;、空白、函数名，然后是一个字面意义上的左括号。（在 extended regex 中，&lt;code class=&quot;language-text&quot;&gt;\(&lt;/code&gt; 是字面括号，而 &lt;code class=&quot;language-text&quot;&gt;(&lt;/code&gt; 表示分组；这就是为什么这里需要反斜杠。）&lt;/p&gt;
&lt;h3&gt;&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 会读什么，不会读什么&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 遍历 index。就这样。它不会解析 &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;。很多人，包括本文之前的一个版本，都说它会；这个说法几乎正确，正如&quot;地球是平的&quot;在你一辈子只看一个停车场时也几乎正确。&lt;/p&gt;
&lt;p&gt;两者看起来一致，只是因为 gitignored 文件通常也 untracked。一旦某个文件既被 gitignored 又被 tracked，比如有人跑过 &lt;code class=&quot;language-text&quot;&gt;git add -f&lt;/code&gt;，或者这个文件在规则出现之前就已经 committed，&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 会照样搜索它。&lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt; 不会。&lt;/p&gt;
&lt;p&gt;你可以在二十秒内证明：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; demo &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; demo
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; init &lt;span class=&quot;token parameter variable&quot;&gt;-q&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*.log&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; .gitignore
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;the secret phrase&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; tracked.log
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; tracked.log .gitignore
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-qm&lt;/span&gt; init

&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;secret phrase&quot;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;# finds it - the file is tracked, ignore rule notwithstanding&lt;/span&gt;
rg &lt;span class=&quot;token string&quot;&gt;&quot;secret phrase&quot;&lt;/span&gt;         &lt;span class=&quot;token comment&quot;&gt;# finds nothing - rg actually reads .gitignore&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;所以精确的说法是：&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 搜索 tracked files。这碰巧会跳过 &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; 会跳过的&lt;em&gt;大多数&lt;/em&gt;东西，但机制不同，边界情况也重要。尤其是当你追查一个字符串，最后发现它住在某个多年前被人强行加入的 generated file 里时。&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; 机制只会通过两个 opt-in 模式进入 &lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;--untracked&lt;/code&gt;：同时搜索 untracked files。&lt;em&gt;在这个模式下&lt;/em&gt;，&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 默认遵守 &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;，并跳过 ignored files（可以用 &lt;code class=&quot;language-text&quot;&gt;--no-exclude-standard&lt;/code&gt; 覆盖，也把它们搜进去）。&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;--no-index&lt;/code&gt;：搜索当前目录，同时完全忽略 Git。在 repo 内想要 plain-grep 语义时很有用。在这个模式下，&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 默认&lt;em&gt;不会&lt;/em&gt;查询 &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;；如果你想要它这么做，可以用 &lt;code class=&quot;language-text&quot;&gt;--exclude-standard&lt;/code&gt; 显式打开。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;默认的 &lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt;，不带任何 flags，永远不会打开你的 &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; 文件。&lt;/p&gt;
&lt;h2&gt;什么时候该用 &lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 和 &lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt;（ripgrep）其实不是竞争对手。它们遍历的东西不同。认真的工具箱里两个都该有。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 遍历 &lt;strong&gt;index&lt;/strong&gt;：tracked files，以及你指向它的任何 ref 或 tree object。&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt; 遍历 &lt;strong&gt;文件系统&lt;/strong&gt;：当前目录下的所有文件，减去 &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;.ignore&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;.rgignore&lt;/code&gt; 和全局 excludes 让它跳过的部分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它们各自能做对方做不了的事情。&lt;/p&gt;
&lt;p&gt;当你想在不 checkout 的情况下跨历史搜索时，&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 胜出：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;deprecated_api&quot;&lt;/span&gt; v2.3.0          &lt;span class=&quot;token comment&quot;&gt;# search a tag&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;deprecated_api&quot;&lt;/span&gt; HEAD~50         &lt;span class=&quot;token comment&quot;&gt;# 50 commits ago&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;deprecated_api&quot;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; rev-list &lt;span class=&quot;token parameter variable&quot;&gt;--all&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;# every commit, ever&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;当你真正需要文件系统语义，并且希望正确处理 gitignore 时，&lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt; 胜出：包括刚 clone 下来但还没 &lt;code class=&quot;language-text&quot;&gt;git add&lt;/code&gt; 的子目录、Git 从未听说过的 generated files，或者根本不是 Git repo 的目录。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;rg &lt;span class=&quot;token string&quot;&gt;&quot;deprecated_api&quot;&lt;/span&gt;                &lt;span class=&quot;token comment&quot;&gt;# respects .gitignore by default&lt;/span&gt;
rg --no-ignore &lt;span class=&quot;token string&quot;&gt;&quot;deprecated_api&quot;&lt;/span&gt;    &lt;span class=&quot;token comment&quot;&gt;# opt back into ignored files&lt;/span&gt;
rg &lt;span class=&quot;token parameter variable&quot;&gt;--hidden&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;deprecated_api&quot;&lt;/span&gt;       &lt;span class=&quot;token comment&quot;&gt;# include dotfiles&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt; 也是 VS Code 项目搜索背后的引擎，所以 &quot;Find in Files&quot; 感觉就像在终端里跑 &lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt;。它的 Unicode 处理扎实；在大多数现代语料上，它至少和 &lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 一样快，常常更快。&lt;a href=&quot;https://github.com/BurntSushi/ripgrep/blob/master/README.md&quot;&gt;ripgrep README 的 Linux kernel benchmark&lt;/a&gt; 显示，在同一个查询上，ripgrep 大约比 &lt;code class=&quot;language-text&quot;&gt;git grep -P&lt;/code&gt; 快 3 倍。（提示：如果你想要&quot;只有 pattern 里有大写时才区分大小写&quot;的行为，传 &lt;code class=&quot;language-text&quot;&gt;-S&lt;/code&gt; 开启 smart-case。它是 opt-in，不是默认行为。）&lt;/p&gt;
&lt;p&gt;如果你还没装 &lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt;，修一下：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; ripgrep   &lt;span class=&quot;token comment&quot;&gt;# macOS&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; ripgrep    &lt;span class=&quot;token comment&quot;&gt;# Debian/Ubuntu&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;cargo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; ripgrep  &lt;span class=&quot;token comment&quot;&gt;# anywhere with Rust&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;把 &lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt; 放在 &lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 旁边。它们负责不同的工作。&lt;/p&gt;
&lt;h3&gt;&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 的好处&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;相关性。&lt;/strong&gt; 它只搜索你正在 track 的东西。构建产物、缓存、&lt;code class=&quot;language-text&quot;&gt;node_modules&lt;/code&gt; 不会挡路，因为 Git 从没见过它们。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大型 repo 上的速度。&lt;/strong&gt; 多线程，不走文件系统遍历。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;历史触达。&lt;/strong&gt; 任意 branch、tag 或 commit，且不离开你的 working tree。这是 &lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt; 做不到的部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更少二进制噪音。&lt;/strong&gt; 和 &lt;code class=&quot;language-text&quot;&gt;grep&lt;/code&gt; 一样，&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 会用 &quot;Binary file X matches&quot; 标记二进制文件，而不是倾倒字节；但因为它遍历 tracked files，通常一开始就会少遇到这类文件。传 &lt;code class=&quot;language-text&quot;&gt;-I&lt;/code&gt; 可以完全跳过二进制文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;额外技巧&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分页查看结果：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;searchTerm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;less&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;按文件统计匹配次数：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;searchTerm&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;显示行号：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;searchTerm&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;在编辑器中打开每个匹配文件：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;searchTerm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;xargs&lt;/span&gt; code&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;把 &lt;code class=&quot;language-text&quot;&gt;code&lt;/code&gt; 换成 &lt;code class=&quot;language-text&quot;&gt;nvim&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;subl&lt;/code&gt;，或者你自己用的任何编辑器。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;就像 Alaric 在迷宫般的图书馆里找到了一条隐秘路径，&lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 会在 tracked codebase 中切出一条干净的线：快速、懂 branch，并且不被 Git 从未听说过的东西弄乱。它不是 &lt;code class=&quot;language-text&quot;&gt;grep&lt;/code&gt; 的万能替代品，也不是 &lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt; 的替代品。它知道的是你的 repo 的 &lt;em&gt;index&lt;/em&gt;；一旦你开始伸手去用它，迷宫就会小很多。&lt;/p&gt;
&lt;p&gt;当问题是&quot;这个代码库里，包括它的历史里，哪里有它&quot;时，用 &lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt;。当问题是&quot;磁盘上哪里有它，并且要遵守我的 ignore rules&quot;时，用 &lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt;。多数日子里，你会希望两者都在手边。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;2026-04-27 更新：修正了早先关于 &lt;code class=&quot;language-text&quot;&gt;git grep&lt;/code&gt; 会遵守 &lt;code class=&quot;language-text&quot;&gt;.gitignore&lt;/code&gt; 的说法（它不会，至少不是直接遵守）；放缓了&quot;内部索引&quot;的解释；修正了一个正则示例；并新增了什么时候该用 &lt;code class=&quot;language-text&quot;&gt;rg&lt;/code&gt; 的章节。&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[在 macOS 上用 phpenv 安装带 IMAP 的 PHP 8.3.6：安装指南]]></title><description><![CDATA[重要提示（2024-11 更新）： PHP 8.4 已经把 ext-imap 从核心中拆出。这个扩展移到了 PECL，并且事实上已经弃用：底层 C 库（libc-client）自 2018 年以来就没有更新。如果你正在启动新项目，或者使用 PHP 8.…]]></description><link>https://bdteo.com/zh/installing-php-8-3-6-with-imap-on-macos-using-phpenv/</link><guid isPermaLink="false">https://bdteo.com/zh/installing-php-8-3-6-with-imap-on-macos-using-phpenv/</guid><pubDate>Sun, 01 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重要提示（2024-11 更新）：&lt;/strong&gt; PHP 8.4 已经把 &lt;strong&gt;ext-imap&lt;/strong&gt; 从核心中拆出。这个扩展移到了 PECL，并且事实上已经弃用：底层 C 库（libc-client）自 2018 年以来就没有更新。如果你正在启动新项目，或者使用 PHP 8.4+，请直接跳到&lt;a href=&quot;#%E4%BD%A0%E7%9C%9F%E7%9A%84%E9%9C%80%E8%A6%81-ext-imap-%E5%90%97&quot;&gt;你真的需要 ext-imap 吗？&lt;/a&gt;看现代替代方案。如果你在 PHP 8.3 或更早版本上，并且确实需要原生扩展，这份指南仍然可用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;快速修复&lt;/h2&gt;
&lt;p&gt;如果你只想要命令，不关心为什么，下面就是完整顺序。你需要已经安装好 Homebrew 和 phpenv。&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 1. Install all required libraries&lt;/span&gt;
brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; tidy-html5 openssl@3 zlib &lt;span class=&quot;token function&quot;&gt;bzip2&lt;/span&gt; libedit readline &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  gettext libiconv libsodium krb5 imap-uw &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 2. Set build configuration (add to ~/.zshrc or run before install)&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;PHP_BUILD_CONFIGURE_OPTS&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;--with-openssl=&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; openssl@3&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; \
  --with-zlib=&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; zlib&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; \
  --with-bz2=&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bzip2&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; \
  --with-curl \
  --with-libedit \
  --with-readline=&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; readline&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; \
  --with-gettext=&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; gettext&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; \
  --with-iconv=&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; libiconv&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; \
  --with-sodium=&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; libsodium&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; \
  --with-kerberos=&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; krb5&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; \
  --with-imap=&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; imap-uw&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; \
  --with-imap-ssl=&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; openssl@3&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; \
  --with-tidy=&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; tidy-html5&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; \
  --enable-mbstring \
  --enable-intl&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 3. Help the compiler find headers&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;CPPFLAGS&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;-I&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; openssl@3&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;/include -I&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; imap-uw&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;/include &lt;span class=&quot;token variable&quot;&gt;$CPPFLAGS&lt;/span&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 4. Build and install&lt;/span&gt;
phpenv &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8.3&lt;/span&gt;.6
phpenv global &lt;span class=&quot;token number&quot;&gt;8.3&lt;/span&gt;.6

&lt;span class=&quot;token comment&quot;&gt;# 5. Verify&lt;/span&gt;
php &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt;
php &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; imap&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;如果输出里出现 &lt;code class=&quot;language-text&quot;&gt;imap&lt;/code&gt;，就结束了。如果没有，继续往下读。&lt;/p&gt;
&lt;h2&gt;你真的需要 ext-imap 吗？&lt;/h2&gt;
&lt;p&gt;认真问一句。在和 C 库、编译器 flag 较劲之前，先想想你是否真的需要原生 IMAP 扩展。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PHP 8.4 已经从核心中移除了 ext-imap。&lt;/strong&gt; 它转到了 PECL，而底层 C 库（&lt;code class=&quot;language-text&quot;&gt;libc-client&lt;/code&gt; / UW-IMAP）自 2018 年以来就没有更新。它有线程安全问题，缺少 XAUTH 支持，还有 POP bug。它不会回来了。&lt;/p&gt;
&lt;p&gt;现代替代方案是 &lt;a href=&quot;https://github.com/Webklex/php-imap&quot;&gt;&lt;strong&gt;Webklex/php-imap&lt;/strong&gt;&lt;/a&gt;，一个纯 PHP 的 IMAP 实现：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;composer&lt;/span&gt; require webklex/php-imap&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;就这样。没有 brew 依赖，没有编译器 flag，没有到处找头文件。它可以在 PHP 8.0.2+ 上工作（包括 8.4 和 8.5），支持 IMAP IDLE 和 OAuth，在 Packagist 上有超过 500 万次安装。如果你的技术栈是 Laravel，也有一个 &lt;a href=&quot;https://github.com/Webklex/laravel-imap&quot;&gt;Laravel 集成&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;只有在这种情况下才使用 ext-imap：&lt;/strong&gt; 你正在维护一个 PHP 8.3 或更早版本上的遗留代码库，它已经依赖 &lt;code class=&quot;language-text&quot;&gt;imap_*&lt;/code&gt; 函数，而且暂时还不能迁移。&lt;/p&gt;
&lt;h2&gt;每一步在做什么&lt;/h2&gt;
&lt;p&gt;如果快速修复已经奏效，可以不用再读。如果没有，或者你想理解发生了什么，下面是拆解。&lt;/p&gt;
&lt;h3&gt;Brew 依赖&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; tidy-html5 openssl@3 zlib &lt;span class=&quot;token function&quot;&gt;bzip2&lt;/span&gt; libedit readline &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  gettext libiconv libsodium krb5 imap-uw &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;macOS 自带其中一部分，但 phpenv 需要 Homebrew 版本，因为它们带有正确的 header 和 pkg-config 文件。最关键的是 &lt;code class=&quot;language-text&quot;&gt;imap-uw&lt;/code&gt;，它就是提供 &lt;code class=&quot;language-text&quot;&gt;libc-client&lt;/code&gt; 的 UW-IMAP 库。&lt;/p&gt;
&lt;h3&gt;PHP_BUILD_CONFIGURE_OPTS&lt;/h3&gt;
&lt;p&gt;phpenv 底层使用 php-build，而 php-build 会在 PHP 源码上运行 &lt;code class=&quot;language-text&quot;&gt;./configure&lt;/code&gt;。&lt;code class=&quot;language-text&quot;&gt;PHP_BUILD_CONFIGURE_OPTS&lt;/code&gt; 变量把 flag 直接传给 configure。每一个 &lt;code class=&quot;language-text&quot;&gt;--with-*&lt;/code&gt; flag 都告诉 PHP 去哪里找某个特定库。&lt;/p&gt;
&lt;p&gt;对 IMAP 最重要的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;--with-imap=$(brew --prefix imap-uw)&lt;/code&gt; -- 指向 IMAP 库&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;--with-imap-ssl=$(brew --prefix openssl@3)&lt;/code&gt; -- 启用基于 SSL 的 IMAP&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;--with-kerberos=$(brew --prefix krb5)&lt;/code&gt; -- IMAP 认证所需&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CPPFLAGS 修复&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;CPPFLAGS&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;-I&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; openssl@3&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;/include -I&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;brew &lt;span class=&quot;token parameter variable&quot;&gt;--prefix&lt;/span&gt; imap-uw&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;/include &lt;span class=&quot;token variable&quot;&gt;$CPPFLAGS&lt;/span&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;即便 configure flag 已经设置好，C 预处理器有时还是找不到头文件。这是因为 macOS 不会把 Homebrew 的 header 放进默认搜索路径。最常出问题的两个 header 是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;openssl/ssl.h&lt;/code&gt; -- 来自 OpenSSL&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;imap/imap.h&lt;/code&gt; -- 来自 imap-uw&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;PATH 修复&lt;/h3&gt;
&lt;p&gt;如果安装之后 &lt;code class=&quot;language-text&quot;&gt;php -v&lt;/code&gt; 显示的版本不对，说明 phpenv 的 shims 没有在你的 PATH 里，或者被别的东西遮住了。把下面这段加到 &lt;code class=&quot;language-text&quot;&gt;~/.zshrc&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;PHPENV_ROOT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token environment constant&quot;&gt;$HOME&lt;/span&gt;/.phpenv&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;${PHPENV_ROOT}&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
  &lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;&lt;span class=&quot;token environment constant&quot;&gt;PATH&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;${PHPENV_ROOT}&lt;/span&gt;/shims:&lt;span class=&quot;token variable&quot;&gt;${PHPENV_ROOT}&lt;/span&gt;/bin:&lt;span class=&quot;token environment constant&quot;&gt;$PATH&lt;/span&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;token builtin class-name&quot;&gt;eval&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;phpenv init -&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;然后运行 &lt;code class=&quot;language-text&quot;&gt;source ~/.zshrc&lt;/code&gt;，再试一次。&lt;/p&gt;
&lt;h3&gt;utf8_mime2text 错误&lt;/h3&gt;
&lt;p&gt;如果你看到这个：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;configure: error: utf8_mime2text() has new signature, but U8T_CANONICAL is missing.
This should not happen.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;说明你的 &lt;code class=&quot;language-text&quot;&gt;imap-uw&lt;/code&gt; 库太旧或已经坏了。这样修：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew upgrade imap-uw&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;然后重新运行安装。&lt;/p&gt;
&lt;h2&gt;PHP + IMAP 的未来&lt;/h2&gt;
&lt;p&gt;墙上的字已经写出来了。ext-imap 已弃用，底层 C 库已被放弃，PHP 8.4 已经把它从核心中移除。如果你读到这里，是因为某个 PHP 项目需要 IMAP，那就开始规划迁移到 &lt;code class=&quot;language-text&quot;&gt;webklex/php-imap&lt;/code&gt;。原生扩展是在借来的时间上运行。&lt;/p&gt;
&lt;p&gt;对于我们这些维护遗留代码库的人，这份指南会继续适用于 PHP 8.3 及更早版本。但不要在新项目里开始使用 ext-imap。既然有纯 PHP 方案，就没必要再和 C 库编译较劲。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;参考资料&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://php.watch/versions/8.4/imap-unbundled&quot;&gt;PHP 8.4: IMAP Extension Unbundled&lt;/a&gt; -- &lt;em&gt;PHP.Watch 关于 ext-imap 移除的文档。&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Webklex/php-imap&quot;&gt;Webklex/php-imap on GitHub&lt;/a&gt; -- &lt;em&gt;纯 PHP IMAP 实现（Packagist 安装量 500 万+）。&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.php.net/manual/en/book.imap.php&quot;&gt;PHP Manual: IMAP Extension&lt;/a&gt; -- &lt;em&gt;带有弃用说明的官方文档。&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/shivammathur/homebrew-extensions&quot;&gt;shivammathur/extensions: IMAP for PHP 8.3+&lt;/a&gt; -- &lt;em&gt;包含 IMAP 等 PHP 扩展的 Homebrew tap。&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;相关文章&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/zh/php-8-5-new-features-pipe-operator-guide/&quot;&gt;PHP 8.5: A Tour of the Incoming Features&lt;/a&gt; -- PHP 接下来会有什么变化。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/zh/understanding-class-namespace-changes-shopware-6-5-developers-guide/&quot;&gt;Dev Guide: Shopware 6.5/6.6 Class &amp;#x26; Namespace Updates&lt;/a&gt; -- 给 Shopware 开发者的另一份 PHP 迁移指南。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/zh/laravel-sail-vs-laradock-choosing-right-docker-solution/&quot;&gt;Laravel Sail vs Laradock&lt;/a&gt; -- 为 PHP 开发选择合适的 Docker 设置。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Laravel Sail vs Laradock：PHP Docker 开发者该怎么选]]></title><description><![CDATA[TL;DR： 对 2026 年的大多数 Laravel 开发者来说：如果你想要零摩擦（原生、无 Docker、几秒配置好），用 Laravel Herd。如果你的团队需要完全一致的环境，或者依赖 Redis/Meilisearch 这类服务，用 Sail。如果你跨多个 PHP…]]></description><link>https://bdteo.com/zh/laravel-sail-vs-laradock-choosing-right-docker-solution/</link><guid isPermaLink="false">https://bdteo.com/zh/laravel-sail-vs-laradock-choosing-right-docker-solution/</guid><pubDate>Thu, 08 Aug 2024 12:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR：&lt;/strong&gt; 对 2026 年的大多数 Laravel 开发者来说：如果你想要零摩擦（原生、无 Docker、几秒配置好），用 &lt;strong&gt;Laravel Herd&lt;/strong&gt;。如果你的团队需要完全一致的环境，或者依赖 Redis/Meilisearch 这类服务，用 &lt;strong&gt;Sail&lt;/strong&gt;。如果你跨多个 PHP 框架工作，用 &lt;strong&gt;Laradock&lt;/strong&gt;。如果你已经长出了这些抽象的边界，用一套 &lt;strong&gt;自定义 Docker Compose&lt;/strong&gt;。如果性能就是一切，看看 &lt;strong&gt;FrankenPHP + Octane&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;最初发布于 2024 年 8 月。2026 年 3 月更新，加入 Laravel 12/Herd/FrankenPHP 以及当前生态状态。&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;过去的问题是“Sail 还是 Laradock？”现在这个问法太窄了。真正的问题是：&lt;strong&gt;2026 年，我该怎样搭建 Laravel 开发环境？&lt;/strong&gt; 选择比以往更多，而最好的选择取决于你真正需要什么。&lt;/p&gt;
&lt;p&gt;这些工具我大多都用过。我现在跑的是自定义 Docker Compose，因为我想完整控制自己的容器，不想让抽象层把正在发生的事情藏起来。但这是我的偏好，不是普遍建议。下面把每个选项能给你的东西讲清楚。&lt;/p&gt;
&lt;h2&gt;候选者&lt;/h2&gt;
&lt;h3&gt;Laravel Herd&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://herd.laravel.com/&quot;&gt;Herd&lt;/a&gt; 是最新的选择，对很多开发者来说也是正确的选择。它是原生应用（macOS 和 Windows，还没有 Linux），不用 Docker 就能给你 PHP、Nginx、Node.js 和 Dnsmasq。Pro 版还加入 MySQL、PostgreSQL、Redis 和调试工具。&lt;/p&gt;
&lt;p&gt;杀手级特性是：几秒内切换 PHP 版本（7.4 到 8.4）、自动路由 &lt;code class=&quot;language-text&quot;&gt;*.test&lt;/code&gt; 域名，而且没有 Docker 开销。如果你在做一套标准 Laravel 应用，又不需要什么奇怪服务，Herd 能让你一分钟内开始写代码。&lt;/p&gt;
&lt;h3&gt;Laravel Sail&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://laravel.com/docs/12.x/sail&quot;&gt;Sail&lt;/a&gt; 是 Laravel 官方的 Docker 开发环境。它用一个 &lt;code class=&quot;language-text&quot;&gt;sail&lt;/code&gt; CLI 包住 Docker Compose，抽象掉常用命令（&lt;code class=&quot;language-text&quot;&gt;sail up&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;sail artisan&lt;/code&gt;、&lt;code class=&quot;language-text&quot;&gt;sail php&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;截至 Laravel 12，Sail 默认带 PHP 8.5，使用 &lt;code class=&quot;language-text&quot;&gt;compose.yaml&lt;/code&gt;（现代文件名，不是 &lt;code class=&quot;language-text&quot;&gt;docker-compose.yml&lt;/code&gt;），并且开箱包含给 Octane 用的 Swoole。它也支持通过 &lt;code class=&quot;language-text&quot;&gt;--devcontainer&lt;/code&gt; 生成 devcontainer，方便和 VS Code/GitHub Codespaces 集成。&lt;/p&gt;
&lt;p&gt;默认服务：PHP、MySQL、Redis、Meilisearch、Mailpit 和 Selenium。&lt;/p&gt;
&lt;h3&gt;Laradock&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://laradock.io/&quot;&gt;Laradock&lt;/a&gt; 是瑞士军刀。它是一套开源 Docker 环境，支持任何 PHP 项目，不只支持 Laravel。它提供 70 多个预配置服务，也可以配置成生产用途。&lt;/p&gt;
&lt;p&gt;截至 2025 年 12 月它仍在活跃维护（近期有 PHP-FPM 和 workspace 镜像更新）。代价是复杂度：搭建要更久，配置要改多个文件，而且你需要真正懂 Docker。&lt;/p&gt;
&lt;h3&gt;FrankenPHP + Octane&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://frankenphp.dev/&quot;&gt;FrankenPHP&lt;/a&gt; 是构建在 Caddy 之上的现代 PHP 应用服务器。结合 Laravel Octane，它可以把每次请求的框架启动时间压到 4-6ms；有开发者报告说，切到 Worker Mode 后延迟从 7 秒降到了 66ms &lt;small&gt;&lt;a href=&quot;#ref1&quot;&gt;[1]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;p&gt;Laravel Cloud 在生产环境的 Octane runtime 里使用 FrankenPHP &lt;small&gt;&lt;a href=&quot;#ref2&quot;&gt;[2]&lt;/a&gt;&lt;/small&gt;。最新版本（v1.11.2，2026 年 2 月）借助 Go 1.26 带来了快 30% 的 CGO 和快 40% 的垃圾回收。&lt;/p&gt;
&lt;p&gt;这不是传统意义上的开发环境，它是生产级 PHP runtime，只是你也可以在开发中使用。Sail 内置了用 FrankenPHP 或 Swoole 运行 Octane 的集成。&lt;/p&gt;
&lt;h2&gt;什么时候用什么&lt;/h2&gt;
&lt;p&gt;下面是我的诚实看法，基于真实用过这些工具之后的判断：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果你是个人或小团队，做的是标准 Laravel 应用，而且不想在基础设施上花时间，用 Herd。&lt;/strong&gt; 这是从“我有个想法”到“我正在写代码”的最快路径。限制是它只支持 macOS/Windows，而且免费版不包含数据库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果你的团队需要环境一致性，依赖特定服务版本（Redis 7、MySQL 8、PostgreSQL 15），或者你的 CI/CD 流水线需要 Docker，用 Sail。&lt;/strong&gt; Sail 的 &lt;code class=&quot;language-text&quot;&gt;sail:publish&lt;/code&gt; 命令允许你在默认配置不够用时定制 Docker 设置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果你跨多个 PHP 框架工作（Symfony、Shopware、原生 PHP），需要比较冷门的服务（Aerospike、RethinkDB、Manticore），或者想要一套 Docker 环境服务多个项目，用 Laradock。&lt;/strong&gt; 学习曲线更陡，但灵活性无可比拟。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果 Sail 和 Laradock 都满足不了你，而且你想要完全控制，用自定义 Docker Compose。&lt;/strong&gt; 我自己就是这么做的。我维护自己的 &lt;code class=&quot;language-text&quot;&gt;compose.yaml&lt;/code&gt;，里面只有我需要的服务，没有抽象层，再配一些 Docker Compose 别名让命令短一点。前期工作更多，但没有魔法，一切都写在明处。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果你在做高性能 API，或者应用对延迟敏感，用 FrankenPHP + Octane。&lt;/strong&gt; 性能差距不是边角料级别，而是一个数量级。即便你平时用别的工具做通用开发，也值得研究一下。&lt;/p&gt;
&lt;h2&gt;真正重要的细节&lt;/h2&gt;
&lt;h3&gt;搭建时间&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;到第一次请求的时间&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Herd&lt;/td&gt;
&lt;td&gt;1 分钟以内&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sail&lt;/td&gt;
&lt;td&gt;5-10 分钟（拉镜像）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom Compose&lt;/td&gt;
&lt;td&gt;30-60 分钟（初次搭建）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laradock&lt;/td&gt;
&lt;td&gt;1-2 小时（完整配置）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;定制能力&lt;/h3&gt;
&lt;p&gt;Sail 是有意限制的。你得到的是 Laravel 需要的服务，没有太多别的东西。你&lt;em&gt;可以&lt;/em&gt;运行 &lt;code class=&quot;language-text&quot;&gt;sail:publish&lt;/code&gt; 再编辑 Dockerfile 来定制，但到那一步，你其实是在维护一套自定义 Docker 设置，上面还叠着 Sail 的抽象。这是两边都不讨好的形状。&lt;/p&gt;
&lt;p&gt;Laradock 给你一切，但要求你理解自己启用了什么。打开一个服务意味着编辑 &lt;code class=&quot;language-text&quot;&gt;.env&lt;/code&gt;，可能还要改 &lt;code class=&quot;language-text&quot;&gt;docker-compose.yml&lt;/code&gt;，有些服务还有自己的配置目录。&lt;/p&gt;
&lt;p&gt;Custom Compose 给你的东西正好等于你写下来的东西。不多，也不少。&lt;/p&gt;
&lt;h3&gt;生产就绪度&lt;/h3&gt;
&lt;p&gt;Sail 明确不是给生产用的。Laradock 可以配置成生产可用，但你需要知道自己在安全加固、资源限制和正确网络配置上做什么。FrankenPHP 从设计上就是生产就绪的，它就是为这个而生。&lt;/p&gt;
&lt;h3&gt;多项目支持&lt;/h3&gt;
&lt;p&gt;Sail：一个环境对应一个项目。你可以同时跑多个 Sail 实例，但它们会争端口。&lt;/p&gt;
&lt;p&gt;Laradock：为多项目设置而设计。一套环境，多个项目，共享服务。&lt;/p&gt;
&lt;p&gt;Custom Compose：取决于你怎么设计。我通常每个项目单独一份 compose 文件，同时定义共享网络。&lt;/p&gt;
&lt;h2&gt;我实际在用什么&lt;/h2&gt;
&lt;p&gt;Custom Docker Compose。我给所有东西都配了别名：&lt;code class=&quot;language-text&quot;&gt;dcu&lt;/code&gt; 表示 &lt;code class=&quot;language-text&quot;&gt;docker compose up -d&lt;/code&gt;，&lt;code class=&quot;language-text&quot;&gt;dce&lt;/code&gt; 用来 exec，&lt;code class=&quot;language-text&quot;&gt;dcefpm&lt;/code&gt; 进 PHP-FPM shell，还有一个会自动发现项目根目录的 &lt;code class=&quot;language-text&quot;&gt;sail&lt;/code&gt; 函数。这套东西在我的 &lt;a href=&quot;/zh/docker-compose-major-changes-since-october-2023/&quot;&gt;Docker Compose 演进笔记&lt;/a&gt; 里。&lt;/p&gt;
&lt;p&gt;我很多年前从 Laradock 开始，Sail 发布后切到 Sail，最后落在自定义配置上，因为我想明确知道到底在跑什么，以及为什么这样跑。每一层抽象都会隐藏决定。有时候这没问题。有时候这些被隐藏的决定会造成很难调试的问题，因为你根本看不见它们。&lt;/p&gt;
&lt;p&gt;话虽如此，如果我今天带一个不关心 Docker 内部细节的团队新开 Laravel 项目，我会用 Sail。如果我在指导刚接触 Laravel 的人，我会让他们装 Herd，然后立刻开始写代码。&lt;/p&gt;
&lt;h2&gt;其他值得一提的选择&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://ddev.com/&quot;&gt;DDEV&lt;/a&gt;&lt;/strong&gt; -- 基于 Docker，Laravel 支持不错，2026 路线图活跃，并计划集成 Gitpod。如果你也在其他 CMS 项目（WordPress、Drupal）里用它，值得评估。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://lando.dev/&quot;&gt;Lando&lt;/a&gt;&lt;/strong&gt; -- 另一套 Docker 抽象层，带 Laravel 插件（v1.10.0，2026 年 1 月）。理念类似 Sail，但不绑定具体框架。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valet&lt;/strong&gt; -- Herd 的前身。仍然可用，但对大多数场景来说 Herd 已经取代它。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;参考资料&lt;/h3&gt;
&lt;p&gt;&lt;a id=&quot;ref1&quot;&gt;&lt;/a&gt;1. &lt;a href=&quot;https://medium.com/@danarcahyaa/setup-and-boost-your-laravel-app-with-frankenphp-worker-mode-c0228f44f71b&quot;&gt;用 FrankenPHP Worker Mode 设置并加速 Laravel&lt;/a&gt; -- &lt;em&gt;真实性能对比：延迟从 7s 到 66ms。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref2&quot;&gt;&lt;/a&gt;2. &lt;a href=&quot;https://devconf.net/talk/florian-beer-how-laravel-cloud-uses-frankenphp-in-production&quot;&gt;Laravel Cloud 如何在生产环境使用 FrankenPHP&lt;/a&gt; -- &lt;em&gt;DevConf 上关于 Laravel Cloud 的 Octane runtime 的演讲。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref3&quot;&gt;&lt;/a&gt;3. &lt;a href=&quot;https://laravel.com/docs/12.x/sail&quot;&gt;Laravel 12.x Sail 文档&lt;/a&gt; -- &lt;em&gt;官方 Sail 文档，包含 PHP 8.5 和 compose.yaml 变更。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref4&quot;&gt;&lt;/a&gt;4. &lt;a href=&quot;https://herd.laravel.com/&quot;&gt;Laravel Herd&lt;/a&gt; -- &lt;em&gt;Laravel 原生开发环境官网。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref5&quot;&gt;&lt;/a&gt;5. &lt;a href=&quot;https://laravel-news.com/frankenphp-v1112-released-with-30-faster-cgo-40-faster-gc-and-security-patches&quot;&gt;FrankenPHP v1.11.2 发布&lt;/a&gt; -- &lt;em&gt;2026 年 2 月发布，带性能和安全更新。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref6&quot;&gt;&lt;/a&gt;6. &lt;a href=&quot;https://github.com/laradock/laradock&quot;&gt;GitHub 上的 Laradock&lt;/a&gt; -- &lt;em&gt;仍在维护，2025 年 12 月有更新。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref7&quot;&gt;&lt;/a&gt;7. &lt;a href=&quot;https://aschmelyun.com/blog/the-current-state-of-local-laravel-development/&quot;&gt;本地 Laravel 开发的当前状态&lt;/a&gt; -- &lt;em&gt;Andrew Schmelyun 的生态概览。&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;相关文章&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/zh/docker-compose-major-changes-since-october-2023/&quot;&gt;Docker Compose 演进：变化了什么，为什么重要&lt;/a&gt; -- 影响所有这些工具的 Docker Compose 变化。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/zh/php-8-5-new-features-pipe-operator-guide/&quot;&gt;PHP 8.5：即将到来的功能巡览&lt;/a&gt; -- Laravel 12 默认 PHP 版本里会出现的新东西。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/zh/installing-php-8-3-6-with-imap-on-macos-using-phpenv/&quot;&gt;在 macOS 上用 phpenv 安装 PHP 8.3.6 + IMAP&lt;/a&gt; -- 当你需要 Docker 之外的特定 PHP 设置时。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[最糟糕的伪君子：一只橡皮鸭的自爱故事]]></title><description><![CDATA[Boris…]]></description><link>https://bdteo.com/zh/worst-hypocrite-rubber-duck-tale/</link><guid isPermaLink="false">https://bdteo.com/zh/worst-hypocrite-rubber-duck-tale/</guid><pubDate>Wed, 17 Jul 2024 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Boris 写给一位珍贵的朋友：
~ 那个我总是很难理解，却又最理解一切的人。 ~&lt;/p&gt;
&lt;p&gt;嘿。又见面了。我们可以重新第一次见面吗？
嗯。我觉得这次会更酷一点。我会比以前更肉麻 🧀，努力让你多笑一点，但还是会有时候失败，或者大多数时候失败，或者随便啦……
它几乎会像上一次，但又不像上一次。注：你可以假装我上一句说得通，就为了显得高级一点 🥸&lt;/p&gt;
&lt;p&gt;让我先给一个小词典，或者开场白，或者小小的知识库：&lt;/p&gt;
&lt;h2&gt;把 &quot;beginning&quot; 倒过来，是 &quot;gninnigeb&quot;&lt;/h2&gt;
&lt;p&gt;我只是需要一个新标题，让我的段落看起来漂亮一点。总之，新的我。新的你。旧衣服，还有一些旧坏习惯。呼！&lt;/p&gt;
&lt;h2&gt;墙上的一面镜子&lt;/h2&gt;
&lt;p&gt;我很想你，也想了我的错误很久。镜子存在，确实是为了让我们更漂亮，也更不脏一点。&lt;/p&gt;
&lt;h2&gt;一只橡皮鸭&lt;/h2&gt;
&lt;p&gt;这就是我。我是一只橡皮鸭。我是橡胶做的，这意味着我的头也是橡胶做的。&lt;/p&gt;
&lt;h2&gt;橡皮鸭不会“嘎嘎叫”。还是会？&lt;/h2&gt;
&lt;p&gt;以我作为一只橡皮鸭的谦卑人生经验来说，我确实会“嘎嘎叫”，而且还叫得相当多。😆&lt;/p&gt;
&lt;h3&gt;一只嘎嘎叫的橡皮鸭在嘎嘎叫&lt;/h3&gt;
&lt;p&gt;嘎！嘎！嘎！- 鸭子说，然后又嘎了一声。惊人的声音！不是吗？&lt;/p&gt;
&lt;h3&gt;一只有态度的橡皮鸭&lt;/h3&gt;
&lt;p&gt;我是所有橡皮鸭里最善良的一只，橡皮鸭说，然后兴奋地嘎了一声！嘎！&lt;/p&gt;
&lt;h3&gt;橡皮鸭真正的衰亡&lt;/h3&gt;
&lt;p&gt;嘎嘎叫会很累的，你知道。用一口新鲜空气，你只能漂亮地嘎有限次数。
但你空了的时候，还是可以试着漂亮地嘎，对吗？
当然可以！你可以试着嘎！你甚至可以从自己的橡胶里挤出最棒的一声快嘎嘎，好帮助其他空空的橡皮鸭吸一口空气。&lt;/p&gt;
&lt;h2&gt;给两只半空橡皮鸭的短故事&lt;/h2&gt;
&lt;p&gt;“你知道吗？” - Duckitty 说
“什么？” - Duckelton 说
“当我嘎嘎叫太多，我就再也嘎不出来了，可我还想嘎嘎叫，这让我难过。” - Duckitty 回答
Duckleton 摆出非常严肃的表情，皱起眉头，用智慧的声音说：
“嗯。我也一样！”&lt;/p&gt;
&lt;h2&gt;那只讨厌沉默的橡皮鸭&lt;/h2&gt;
&lt;p&gt;Duckleton 讨厌沉默，只要有机会就响亮地嘎一声。有时候他很空，会怀疑自己是不是够黄，能不能嘎嘎叫。
但你看，制造 Ducleton 和 Duckitty 的那家工厂，把他们两个都做得完美地黄。
Duckleton 是一只有态度的橡皮鸭。他觉得自己可以，也必须，嘎过每一只其他鸭子，好把它们从空了还不能嘎嘎叫的羞耻里救出来。
Duckitty 有时候也有类似的想法，或者至少 Duckleton 脑子清楚的时候是这么想的。（有时候他并不清楚 😆）
但 Duckleton 是一只骄傲的橡皮鸭。一只由世界上最好的工厂制造的橡皮鸭，那家工厂几个世纪以来都在制造世界上最会嘎嘎叫的黄色橡皮鸭。至少 Duckleton 在遇见 Duckitty 之前是这么想的。
他一遇见她，就被她的声音惊住了，于是觉得她一定是由某个外星工厂制造的，比他来的那个地球工厂好多了。
他很高兴自己有机会只是欣赏这只名叫 Duckitty 的、奇妙而完美的橡皮鸭，她比他好得多（他这么以为）。
“嗯，看着她真好。她那么黄。她那么好。她的声音比我漂亮多了。我是一只幸运的橡皮鸭。” - Duckleton 心想。&lt;/p&gt;
&lt;h2&gt;一个短短的、但有点难过的故事&lt;/h2&gt;
&lt;p&gt;Duckleton 和 Duckitty 都很快乐，嘎嘎叫得像是在参加世界和外太空最棒的嘎嘎大赛。
有一次，一个人类小孩过来捏了 Duckleton。他有一阵子嘎不出来，但还是很想嘎，所以他没有抱怨，保持沉默，深吸一口气，然后他们两个又快乐地继续嘎嘎叫。
过了一些时间，Duckitty 也被一个讨厌的人类小孩拿走了。那个小孩把 Duckitty 里面所有空气都挤了出来，但 Duckleton 太忙着嘎嘎叫，没有注意到。
Duckitty 没事，但她得吸点空气。
Duckleton 悲伤得超过绝望。他以为自己最喜欢的鸭子坏了，而他又要直到宇宙尽头都永远孤单。他只是忘了，不久前同一个小孩也捏过他。
Duckleton 骄傲但笨，毕竟他只有一个小小的橡胶脑袋。Duckitty 一嘎叫，Duckleton 就怒了。他既开心她还活着，又有点泄气，因为他以为 Duckitty 比他更能熬过人类小孩的一捏。
毕竟她比他完美得多，对吧？&lt;/p&gt;
&lt;h2&gt;Duckleton 的内疚之旅&lt;/h2&gt;
&lt;p&gt;Duckleton 是所有橡皮鸭里最糟糕的伪君子，而他终于学明白了。他很难过，但他想：去向 Duckitty 道歉吧，并向她表达一点感激，感谢她就是这样完美。&lt;/p&gt;
&lt;h2&gt;尾声&lt;/h2&gt;
&lt;p&gt;我亲爱的朋友，过去 8 年里你一直都很完美。每一天都很完美。我真的很抱歉自己这么伪君子，但坏事总会发生，你知道的。而我们还是继续往前走。
请你更爱自己，多很多。我很笨。你很聪明。或者随便啦……但请你更爱自己，多很多，因为你原本这样就很完美！
事实上，你是世界上最完美的黄色橡皮鸭。至少我是这么想的。我的橡胶脑袋很小。但是，耶！我还是会思考。🫶
嘎！&lt;/p&gt;
&lt;p&gt;嘎，嘎，嘎
~ Duckleton&lt;/p&gt;</content:encoded></item><item><title><![CDATA[RL 中的离散表示：Edan Meyer 的研究洞见]]></title><description><![CDATA[你有没有想过，AI 智能体是怎样学会理解复杂环境并与之互动的？强化学习（RL）研究者 Edan Meyer 一直在探索一种有趣的方法，它也许会改变我们思考 AI 学习的方式。我们来看看他关于 RL…]]></description><link>https://bdteo.com/zh/discrete-representations-reinforcement-learning-insights/</link><guid isPermaLink="false">https://bdteo.com/zh/discrete-representations-reinforcement-learning-insights/</guid><pubDate>Mon, 15 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;你有没有想过，AI 智能体是怎样学会理解复杂环境并与之互动的？强化学习（RL）研究者 Edan Meyer 一直在探索一种有趣的方法，它也许会改变我们思考 AI 学习的方式。我们来看看他关于 RL 中离散表示的这项迷人工作。&lt;/p&gt;
&lt;h2&gt;表示的力量&lt;/h2&gt;
&lt;p&gt;想象你要教一台计算机玩电子游戏。你会怎样表示游戏状态，才能让计算机理解它，并从中学习？这正是表示学习登场的地方，也是构建有效 AI 智能体时至关重要的一环。&lt;/p&gt;
&lt;p&gt;Edan Meyer 的工作可以在他的 &lt;a href=&quot;https://www.youtube.com/@EdanMeyer&quot;&gt;YouTube 频道&lt;/a&gt;上看到。他一直在研究一种特定类型的表示，叫作离散表示。他在一篇&lt;a href=&quot;https://arxiv.org/abs/2312.01203&quot;&gt;可在 arXiv 阅读的论文&lt;/a&gt;中详细介绍了这项研究，也说明了为什么这类表示在某些 RL 场景中特别有用。&lt;/p&gt;
&lt;h2&gt;两年研究，压缩进 13 分钟&lt;/h2&gt;
&lt;p&gt;Edan 把两年的硕士研究浓缩成了一支 13 分钟的视频，标题是 &lt;a href=&quot;https://www.youtube.com/watch?v=s8RqGlU5HEs&quot;&gt;&quot;2 Years of My Research Explained in 13 Minutes&quot;&lt;/a&gt;。在视频里，他把复杂概念拆成容易消化的解释，让更广泛的观众也能接近这项工作。&lt;/p&gt;
&lt;p&gt;正如 Edan 在视频描述中写的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;This is my research into representation learning and model learning in the reinforcement learning setting. Two years in the making, and I finally get to talk about my Master&apos;s research! The paper has been accepted to the Reinforcement Learning Conference (RLC) 2024.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果你想理解这项研究的基础，而暂时不想直接扎进完整的学术论文，这支视频是一个很好的起点。&lt;/p&gt;
&lt;h2&gt;什么是离散表示？&lt;/h2&gt;
&lt;p&gt;传统上，很多 RL 系统使用连续表示。你可以把它们想成由小数组成的向量，每个数都可以取任意值。离散表示则更像是一串选择题。表示中的每一个“槽位”只能从固定数量的取值中选择一个。&lt;/p&gt;
&lt;p&gt;正如 Edan 在视频中解释的，这一开始听起来可能很受限。毕竟，一个连续值可以表示无穷多种状态，而一个离散值的范围要小得多。那么，为什么还要使用离散表示？&lt;/p&gt;
&lt;h2&gt;意外的好处&lt;/h2&gt;
&lt;p&gt;Edan 的研究发现，使用离散表示有一些很有意思的优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用更少容量构建更好的世界模型&lt;/strong&gt;：当 AI 试图学习环境模型，也就是所谓的“世界模型”时，离散表示让它可以用更少的计算能力捕捉更准确的信息。尤其是在模型没有足够容量完美表示环境中一切内容的时候，这一点特别明显。而在复杂的现实问题里，这种情况很常见。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;更快适应&lt;/strong&gt;：在环境随时间变化的实验中，使用离散表示的智能体能够更快适应这些变化。对于需要在动态、不可预测环境中运行的 AI 系统来说，这可能至关重要。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;高效学习&lt;/strong&gt;：离散表示一开始也许需要更长时间才能学成，但一旦建立起来，它们会让世界建模和策略学习任务中的学习与适应都变得更快。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;这为什么重要？&lt;/h2&gt;
&lt;p&gt;Edan 这项工作的意义远不止简单的网格世界实验。正如他在视频中指出的，真实世界比我们能创造的任何仿真都复杂得多。在这样的环境里，AI 不可能学会一切。关键在于适应。&lt;/p&gt;
&lt;p&gt;离散表示似乎提供了一种强有力的工具，用来构建能够快速适应新情况的 AI 系统，即便它们不可能把环境的每个方面都建模出来。从机器人到复杂策略游戏，再到更远的地方，这都可能成为一个转折点。&lt;/p&gt;
&lt;h2&gt;继续深入&lt;/h2&gt;
&lt;p&gt;如果你对技术细节感兴趣，Edan 的论文探讨了离散表示为什么如此有效的一些迷人侧面。比如，他发现并非所有离散表示都一样。稀疏性和二值性等因素，会在它们的效果中扮演重要角色。&lt;/p&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;Edan Meyer 关于强化学习中离散表示的工作，为我们如何构建更具适应性、更高效的 AI 系统提供了令人兴奋的洞见。通过挑战我们关于 AI 信息表示方式的传统看法，他的研究打开了新的可能性：让智能体能够在复杂、动态的环境中站稳脚跟。&lt;/p&gt;
&lt;p&gt;无论你是 AI 研究者、机器学习学生，还是只是对技术前沿感到好奇的人，Edan 的工作都让人得以窥见人工智能的未来。可以去看看他的 &lt;a href=&quot;https://www.youtube.com/@EdanMeyer&quot;&gt;YouTube 频道&lt;/a&gt;、讲解&lt;a href=&quot;https://www.youtube.com/watch?v=s8RqGlU5HEs&quot;&gt;视频&lt;/a&gt;，以及那篇&lt;a href=&quot;https://arxiv.org/abs/2312.01203&quot;&gt;论文&lt;/a&gt;，更深入地探索这些想法。&lt;/p&gt;
&lt;p&gt;记住，在快速变化的 AI 研究世界里，今天的实验技术可能就是明天的突破性技术。离散表示也许正是解锁更强大、更能适应变化的 AI 系统的关键。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Google AI 历史：承诺、股价表现与影响]]></title><description><![CDATA[Google 在 Gemini 系列等 AI 技术开发与市场推广上的历程，是一个很有意思的案例：企业承诺、股票市场表现与技术创新如何彼此牵动。本文分析 Google 的 AI 承诺在若干具体历史节点上如何显著影响其股价，并同时回顾其中的成功与失败。 这家科技巨头进入 AI…]]></description><link>https://bdteo.com/zh/google-ai-ambitions-historical-analysis-promises-stock-market-impact/</link><guid isPermaLink="false">https://bdteo.com/zh/google-ai-ambitions-historical-analysis-promises-stock-market-impact/</guid><pubDate>Sat, 11 May 2024 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Google 在 Gemini 系列等 AI 技术开发与市场推广上的历程，是一个很有意思的案例：企业承诺、股票市场表现与技术创新如何彼此牵动。本文分析 Google 的 AI 承诺在若干具体历史节点上如何显著影响其股价，并同时回顾其中的成功与失败。&lt;/p&gt;
&lt;p&gt;这家科技巨头进入 AI 领域的过程，一直伴随着雄心勃勃的项目和大胆宣称。从 2015 年推出 TensorFlow、确立 Google 在 AI 研发中的领导地位，到 2016 年推出 Google Assistant、增强其对 Amazon Alexa 和 Apple Siri 等竞争对手的竞争力，Google 一直试图推动 AI 能力的边界 &lt;small&gt;&lt;a href=&quot;#ref1&quot;&gt;[1]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;h3&gt;Google AI 发展的关键历史里程碑&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;TensorFlow 推出（2015）&lt;/strong&gt;：Google 将其机器学习框架 TensorFlow 开源，随后它迅速流行起来。此举帮助 Google 确立了 AI 研发领导者的地位，也正面影响了市场对它的看法 &lt;small&gt;&lt;a href=&quot;#ref1&quot;&gt;[1]&lt;/a&gt;&lt;/small&gt;。TensorFlow 被用于各种应用，从改进搜索结果到驱动自动驾驶汽车。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Google Assistant 发布（2016）&lt;/strong&gt;：Google Assistant 的推出增强了 Google 在 AI 领域对 Amazon Alexa 和 Apple Siri 等竞争对手的竞争力。市场对此反应积极，因为它体现了 AI 驱动用户界面的潜在增长空间 &lt;small&gt;&lt;a href=&quot;#ref2&quot;&gt;[2]&lt;/a&gt;&lt;/small&gt;。Google Assistant 被集成进智能手机、智能家居设备和汽车，成为语音助手市场的重要玩家。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;量子计算进展（2019）&lt;/strong&gt;：Google 宣布在“量子霸权”上取得突破，声称其量子计算机能够完成传统超级计算机无法胜任的计算。这一公告带来了短暂的股价上涨，也展示了投资者对 Google 前沿技术能力的热情 &lt;small&gt;&lt;a href=&quot;#ref3&quot;&gt;[3]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;股票市场表现与 AI 里程碑&lt;/h3&gt;
&lt;p&gt;股票市场对 Google 的 AI 进展反应不一。重大公告，比如量子计算突破和新 AI 产品发布，通常会带来短期股价上涨。不过，长期来看，股价影响更紧密地取决于这些技术是否真正落地，以及能否取得商业成功。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;S&amp;#x26;P 500 里程碑&lt;/strong&gt;：Google 的重大 AI 公告经常与更广泛的市场趋势重合。例如，2021 年的大牛市中，Google 在 AI 取得显著进展的同时也创下新的股价高点，反映出投资者对技术驱动增长的强烈信心 &lt;small&gt;&lt;a href=&quot;#ref4&quot;&gt;[4]&lt;/a&gt;&lt;/small&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 630px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;https://bdteo.com/static/ab6f0e497ea3ee100966d0f875eab63b/56fb7/google_stock_milestones.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 49.36708860759494%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAC4jAAAuIwF4pT92AAABgklEQVR42n2SyW7dMAxF/f/flk123aQvRfLQxLVqObImWtLtpTw02cSAQIEmjy6HIcYIaxeM44hlWbBt23Vyzpj+jJjMxBgLYwztjJQEIkecyB5LG1PGoEAzGdxuP3G/3+Gc6wG1VrTWUCVDtgz91KdfzoX3BikVIUv3zT7D+YghhHgBSildlVo96ttSQsppvzOutUolhYiGV+Px+PK3P/xwm+DIGrz3F/A8GnACC2H5AKpPgZnALIWqEn78tvg1rRiXQI5g0L6cyWfSf9u+KjyAZatwcevAj5CpzvBfhbC6YZ7nb4CEqEJJrJCq1U87u4ynd4eog2Gcz3uF2q4+lKuHlwoOo5YOqQxShZGDeDEOzyzv+X3F5PSRrwIuYClbT9bTDvDKFdCXjV3xZh1sEHhdFyampENrx8P1ssLVGZQao/YiwbIndo28CxbtTxSu0YoQw17yOeW476FWcVZ37u2gdB8CS9oXc/WBCqmCyquWkSIBASHsR5PVSl9o5mjcJ98/43UNNX89qyUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;Google 股价里程碑&quot; title=&quot;&quot; src=&quot;https://bdteo.com/static/ab6f0e497ea3ee100966d0f875eab63b/f058b/google_stock_milestones.png&quot; srcset=&quot;https://bdteo.com/static/ab6f0e497ea3ee100966d0f875eab63b/c26ae/google_stock_milestones.png 158w,
https://bdteo.com/static/ab6f0e497ea3ee100966d0f875eab63b/6bdcf/google_stock_milestones.png 315w,
https://bdteo.com/static/ab6f0e497ea3ee100966d0f875eab63b/f058b/google_stock_milestones.png 630w,
https://bdteo.com/static/ab6f0e497ea3ee100966d0f875eab63b/40601/google_stock_milestones.png 945w,
https://bdteo.com/static/ab6f0e497ea3ee100966d0f875eab63b/78612/google_stock_milestones.png 1260w,
https://bdteo.com/static/ab6f0e497ea3ee100966d0f875eab63b/56fb7/google_stock_milestones.png 4169w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;figcaption&gt;
    图 1 - Google 股价里程碑。
  &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3&gt;成功与失败&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;成功&lt;/strong&gt;：Google 的 AI 在语言翻译、图像识别，以及通过 Waymo 推动自动驾驶技术等领域取得了可观成功。这些成功帮助巩固了 Google 作为技术创新者的声誉 &lt;small&gt;&lt;a href=&quot;#ref5&quot;&gt;[5]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败&lt;/strong&gt;：并非 Google 的所有 AI 计划都达到了市场预期。例如，备受期待的 Google Glass 项目没能打动消费者，最终停产。该产品面临隐私担忧，也没能为普通消费者提供足够有说服力的使用场景。同样，Gemini 1.5 Pro 周围的延迟和过度承诺，也引发了用户不满和怀疑 &lt;small&gt;&lt;a href=&quot;#ref6&quot;&gt;[6]&lt;/a&gt;&lt;/small&gt;。Google 从这些失败中吸取了教训，开始更专注于务实的 AI 应用，并改善与用户的沟通。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 630px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;https://bdteo.com/static/74c512a4ea459157d8319ebeced56698/f154a/tech_companies_ai_milestones.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 55.69620253164557%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAC4jAAAuIwF4pT92AAABvUlEQVR42n2S247bMAxE8/+/16eu2y6CbJrYjuW7daHo6VDebLFFWyECI4E6niF5UlU453C9XtH3PXLOmJe53PX9iI7RDQN6xsEN6LoHXNdhGEfEmKBZICKIPpbzKaWEe12jql5wPp9x+3lDSAH7DjAXyj/ZDvypKFSlfNSuVO0+ww79sMKHaMCI+/2Gl6oitEJDeIiBjxQpKYSPE10ozzllqokkZXxvZry2M3hgvuDrxTHGQ+EyHRZH2hBeSk7IhIjsR+TeqcaAqokPE26jx7d6Kgp/NBNebwNzBCcDma3n2lmPRBV/A0rM5VE3bYhCEBVW9x5upqOQmM8atm0La4ztneD/AY8aUl2/0sVe7r9cOjxmD1C10O3JbBbYP4C660dTIhW+uQk1G2DLmuJZokS1yoYUhdFqRojZ1negFd5ABhy3gJoKOtp6axdcuokAKR8/hEiJ2f8BfC6h9DV4TD7hMRHG4jeEuTkiBM4bbWXJn4E2Ac8aGswTMm6RmxDWpxlnbByFEPWT5TI2ORUBH8DdgPm3QhvSZV2x+kBIQgyBSjaqkDL52+bLfC3Lim1ld98dWQzMtTwbPU+GxV8ZylluZ+YmmAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;img class=&quot;gatsby-resp-image-image&quot; alt=&quot;科技公司 AI 里程碑&quot; title=&quot;&quot; src=&quot;https://bdteo.com/static/74c512a4ea459157d8319ebeced56698/f058b/tech_companies_ai_milestones.png&quot; srcset=&quot;https://bdteo.com/static/74c512a4ea459157d8319ebeced56698/c26ae/tech_companies_ai_milestones.png 158w,
https://bdteo.com/static/74c512a4ea459157d8319ebeced56698/6bdcf/tech_companies_ai_milestones.png 315w,
https://bdteo.com/static/74c512a4ea459157d8319ebeced56698/f058b/tech_companies_ai_milestones.png 630w,
https://bdteo.com/static/74c512a4ea459157d8319ebeced56698/40601/tech_companies_ai_milestones.png 945w,
https://bdteo.com/static/74c512a4ea459157d8319ebeced56698/78612/tech_companies_ai_milestones.png 1260w,
https://bdteo.com/static/74c512a4ea459157d8319ebeced56698/f154a/tech_companies_ai_milestones.png 4769w&quot; sizes=&quot;(max-width: 630px) 100vw, 630px&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;figcaption&gt;图 2 - 科技公司 AI 里程碑。&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3&gt;市场影响与未来展望&lt;/h3&gt;
&lt;p&gt;Google AI 的市场影响相当显著，不仅影响自身股价，也影响更广泛科技行业的方向。AI 领域仍然是投资者关注的重点，这一点也体现在 S&amp;#x26;P 500 的表现中，其中科技股扮演着重要角色 &lt;small&gt;&lt;a href=&quot;#ref4&quot;&gt;[4]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;p&gt;往前看，Google 能否兑现其 AI 承诺，并克服技术部署中的挑战，将至关重要。该公司如果能更有效地提升透明度、管理消费者预期，可能会决定其未来的市场位置和投资者信任。&lt;/p&gt;
&lt;p&gt;一个值得关注的关键领域，是 Google 在 Gemini 系列 AI 模型上的进展。Gemini 1.5 Pro 于 2024 年 2 月发布，承诺在 AI 能力上实现显著跃升，但其推出过程已经遇到挑战 &lt;small&gt;&lt;a href=&quot;#ref6&quot;&gt;[6]&lt;/a&gt;&lt;/small&gt;。Google 如何处理这些挑战并兑现承诺，很可能会对其 AI 声誉和市场地位产生明显影响。&lt;/p&gt;
&lt;p&gt;Google 在人工智能上的雄心勃勃推进，尤其是通过 Gemini 系列，标志着 AI 技术进入一个变革性阶段。作为该系列的一部分，Gemini 1.5 Pro 的推出突显了 Google 持续推动 AI 能力边界的决心。不过，Gemini 1.5 Pro 的推出并非没有挑战。理解这些挑战，对于理解这类先进 AI 模型的潜力与局限同样重要。&lt;/p&gt;
&lt;h3&gt;专家对 Gemini 1.5 Pro 的看法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;性能与能力&lt;/strong&gt;：Gemini 1.5 Pro 建立在 Mixture-of-Experts（MoE）架构之上，相比前代模型有显著改进，包括上下文窗口大幅增加到最高 1000 万 tokens。这项增强使它能更好处理涉及大量数据和多种格式的复杂任务，包括文本、代码、视觉和音频 &lt;small&gt;&lt;a href=&quot;#ref7&quot;&gt;[7]&lt;/a&gt;&lt;/small&gt;。Encord 的 Stephen Oladele 表示，&quot;Gemini 1.5 Pro maintains near-perfect recall across the entire context and uses a mixture-of-experts architecture for more efficient training &amp;#x26; higher-quality responses&quot; &lt;small&gt;&lt;a href=&quot;#ref7&quot;&gt;[7]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;推出过程中的挑战&lt;/strong&gt;：尽管能力先进，Gemini 1.5 Pro 的推出仍面临多项挑战。该模型目前处于 private preview，general availability 安排在更晚时间，这说明部署采用了分阶段方式 &lt;small&gt;&lt;a href=&quot;#ref7&quot;&gt;[7]&lt;/a&gt;&lt;/small&gt;。这种谨慎的推出策略，可能源于 Google 需要进一步微调模型，并确保它能满足前代产品和市场共同抬高的期待。&lt;/p&gt;
&lt;h3&gt;行业分析师的观点&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;对 AI 行业的影响&lt;/strong&gt;：行业分析师认为，Gemini 系列是 Google 的重要一步，有可能为 AI 模型能力树立新的标准。Gemini 系列，尤其是 1.5 Pro，预计将增强 Google 相对于 OpenAI 和 Microsoft 等科技巨头的竞争力；这些公司也一直在积极推进自己的 AI 能力 &lt;small&gt;&lt;a href=&quot;#ref8&quot;&gt;[8]&lt;/a&gt;&lt;/small&gt; &lt;small&gt;&lt;a href=&quot;#ref7&quot;&gt;[7]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;市场含义&lt;/strong&gt;：Gemini 1.5 Pro 的进展很可能影响医疗、金融等多个行业，使更复杂的 AI 应用成为可能。这可能改变市场动态：那些能有效整合此类先进 AI 技术的公司，可能获得显著竞争优势 &lt;small&gt;&lt;a href=&quot;#ref7&quot;&gt;[7]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;h3&gt;来自 Google 高管的洞见&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;未来计划与伦理考量&lt;/strong&gt;：包括 Sundar Pichai 在内的 Google 高管一直强调，公司致力于负责任的 AI 发展。Pichai 强调，AI 进步需要与伦理准则对齐，并确保这些技术被用于社会利益 &lt;small&gt;&lt;a href=&quot;#ref2&quot;&gt;[2]&lt;/a&gt;&lt;/small&gt; &lt;small&gt;&lt;a href=&quot;#ref9&quot;&gt;[9]&lt;/a&gt;&lt;/small&gt;。随着 AI 能力增强并融入日常应用，这种做法变得至关重要。&lt;/p&gt;
&lt;h3&gt;应对挑战与把握机会&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;处理技术挑战&lt;/strong&gt;：为了应对未来挑战，Google 必须继续投入研发，解决模型可靠性和伦理关切等问题。该公司逐步推出 Gemini 1.5 Pro 的方式，显示出一种在问题变得严重之前先降低风险的策略 &lt;small&gt;&lt;a href=&quot;#ref7&quot;&gt;[7]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;扩大市场机会&lt;/strong&gt;：Google 可以利用 Gemini 1.5 Pro 的能力创造新的市场机会，尤其是在需要处理大型数据集和复杂问题求解场景的行业。通过提供能简化 AI 融入业务流程的工具，Google 可以帮助各行业转变运营方式，并实现更高效率  &lt;small&gt;&lt;a href=&quot;#ref10&quot;&gt;[10]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;伦理 AI 发展&lt;/strong&gt;：随着 AI 技术变得更强大，伦理影响也变得更重要。Google 通过 AI principles 和治理框架展示出的持续负责任 AI 发展承诺，将是维持公众信任和监管合规的关键  &lt;small&gt;&lt;a href=&quot;#ref11&quot;&gt;[11]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;p&gt;总之，Gemini 1.5 Pro 的推出虽然带来挑战，也为 Google 在 AI 领域取得领先提供了实质机会。只要继续聚焦性能提升、伦理 AI 发展和市场扩张，Google 就能更有效地穿过这些挑战，并为 AI 在各行业中的能力设定新的标准。&lt;/p&gt;
&lt;h3&gt;结论&lt;/h3&gt;
&lt;p&gt;Google 的 AI 旅程说明了技术创新、市场预期和企业战略之间复杂的动态关系。虽然公司经历过显著成功，也遭遇过挫折，但它在 AI 领域的持续努力仍然吸引着市场的强烈关注。投资者和市场观察者很可能会继续密切关注 Google 能否把 AI 能力转化为可持续增长和市场领导力。&lt;/p&gt;
&lt;p&gt;随着 AI 格局继续快速演进，Google 作为 AI 先行者的位置也将受到检验。公司能否在雄心勃勃的创新、现实可行的执行、透明沟通和有效的预期管理之间取得平衡，将是决定其在 AI 领域长期成功和整体市场表现的关键因素。&lt;/p&gt;
&lt;h3&gt;参考资料&lt;/h3&gt;
&lt;p&gt;&lt;a id=&quot;ref1&quot;&gt;&lt;/a&gt;1. &lt;a href=&quot;https://emeritus.org/blog/ai-strategy-google/&quot;&gt;Emeritus - Google AI Strategy&lt;/a&gt;
&lt;a id=&quot;ref2&quot;&gt;&lt;/a&gt;2. &lt;a href=&quot;https://blog.google/products/google-one/google-one-gemini-ai-gmail-docs-sheets/&quot;&gt;Google Blog - Gemini AI&lt;/a&gt;
&lt;a id=&quot;ref3&quot;&gt;&lt;/a&gt;3. &lt;a href=&quot;https://finance.yahoo.com/quote/GOOGL/&quot;&gt;Yahoo Finance - GOOGL Quote&lt;/a&gt;
&lt;a id=&quot;ref4&quot;&gt;&lt;/a&gt;4. &lt;a href=&quot;https://en.wikipedia.org/wiki/Closing_milestones_of_the_S%26P_500&quot;&gt;Wikipedia - S&amp;#x26;P 500 Milestones&lt;/a&gt;
&lt;a id=&quot;ref5&quot;&gt;&lt;/a&gt;5. &lt;a href=&quot;https://www.thinkwithgoogle.com/intl/en-emea/marketing-strategies/automation/using-google-ai-tools/&quot;&gt;Think with Google - AI Tools&lt;/a&gt;
&lt;a id=&quot;ref6&quot;&gt;&lt;/a&gt;6. &lt;a href=&quot;https://timesofindia.indiatimes.com/gadgets-news/google-releases-gemini-15-pro-ai-model-heres-what-company-ceo-sundar-pichai-has-to-say/articleshow/107732867.cms&quot;&gt;Times of India - Gemini 15 Pro AI Release&lt;/a&gt;
&lt;a id=&quot;ref7&quot;&gt;&lt;/a&gt;7. &lt;a href=&quot;https://encord.com/blog/google-gemini-1-5-generative-ai-model-with-mixture-of-experts/&quot;&gt;Encord - Google Gemini 1.5 Generative AI Model with Mixture of Experts&lt;/a&gt;
&lt;a id=&quot;ref8&quot;&gt;&lt;/a&gt;8. &lt;a href=&quot;https://builtin.com/articles/google-gemini&quot;&gt;Built In - Google Gemini&lt;/a&gt;
&lt;a id=&quot;ref9&quot;&gt;&lt;/a&gt;9. &lt;a href=&quot;https://www.cbsnews.com/news/google-artificial-intelligence-future-60-minutes-transcript-2023-04-16/&quot;&gt;CBS News - Google&apos;s AI Future&lt;/a&gt;
&lt;a id=&quot;ref10&quot;&gt;&lt;/a&gt;10. &lt;a href=&quot;https://cloud.google.com/ai/generative-ai&quot;&gt;Google Cloud - Generative AI&lt;/a&gt;
&lt;a id=&quot;ref11&quot;&gt;&lt;/a&gt;11. &lt;a href=&quot;https://blog.google/technology/ai/responsible-ai-looking-back-at-2022-and-to-the-future/&quot;&gt;Google Blog - Responsible AI&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Stable Diffusion 照片真实感：设置与 GPU 极限指南]]></title><description><![CDATA[2026 年 3 月更新。 这篇文章最初写于 2023 年 5 月，当时 SD 1.5 的 512x512 是标准，RTX 3090 是顶级硬件。现在一切都变了。Flux 2、SDXL fine-tunes、SD 3.5、ControlNet 和 RTX 509…]]></description><link>https://bdteo.com/zh/pushing-the-stable-diffussion-limits/</link><guid isPermaLink="false">https://bdteo.com/zh/pushing-the-stable-diffussion-limits/</guid><pubDate>Thu, 04 May 2023 23:45:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2026 年 3 月更新。&lt;/strong&gt; 这篇文章最初写于 2023 年 5 月，当时 SD 1.5 的 512x512 是标准，RTX 3090 是顶级硬件。现在一切都变了。Flux 2、SDXL fine-tunes、SD 3.5、ControlNet 和 RTX 5090 已经完全重新定义了可能性。这就是当前状态。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;AI 生成图像和真实照片之间的差距几乎已经闭合。2023 年，“照片真实感”的意思是“眯起眼看几乎能信”。到了 2026 年，最好的模型生成的图像已经真的很难和专业摄影区分开。&lt;/p&gt;
&lt;p&gt;下面是怎么做到。&lt;/p&gt;
&lt;h2&gt;当前照片真实感版图&lt;/h2&gt;
&lt;p&gt;你选择的模型，比你调整的任何设置都更重要。现在的情况是这样：&lt;/p&gt;
&lt;h3&gt;Flux 2 -- 新王&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://bfl.ai/models/flux-2&quot;&gt;Flux 2&lt;/a&gt; 来自 Black Forest Labs（2025 年 11 月发布），可以说是 2026 年最好的 open-weight 照片真实感模型 &lt;small&gt;&lt;a href=&quot;#ref1&quot;&gt;[1]&lt;/a&gt;&lt;/small&gt;。它生成的图像有自然光照、准确的皮肤纹理和连贯构图，足以和专业摄影相提并论。Adobe 在 2025 年 9 月把 Flux（Kontext Pro）集成进 Photoshop &lt;small&gt;&lt;a href=&quot;#ref2&quot;&gt;[2]&lt;/a&gt;&lt;/small&gt;，这已经说明了行业信心在哪里。&lt;/p&gt;
&lt;p&gt;Flux 还拥有非常出色的自然语言理解能力。你可以用普通英语描述想要什么，不再需要 SD 1.5 那种关键词浓汤。&lt;/p&gt;
&lt;h3&gt;SDXL Fine-Tunes -- 干活的主力&lt;/h3&gt;
&lt;p&gt;对于基于 SDXL 的照片真实感，目前的领先者是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Juggernaut XL v9/v10&lt;/strong&gt; -- 电影感、摄影风输出的默认选择。在摄影师和电影制作者中最受欢迎。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Realistic Vision&lt;/strong&gt; -- 专门针对逼真纹理、光照和面部准确性 fine-tuned。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EpicRealism&lt;/strong&gt; -- 细节和自然光照都非常强。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些模型有庞大的社区支持、丰富的 LoRA 库和可预期的行为。如果 Flux 感觉还太新，或者你的工作流建立在 SDXL 上，它们都是很好的选择。&lt;/p&gt;
&lt;h3&gt;SD 3.5 Large&lt;/h3&gt;
&lt;p&gt;Stability AI 的旗舰模型使用新的 Multimodal Diffusion Transformer（MMDiT）架构，和 SDXL 是根本不同的路线。它技术上很厉害，但生态更小。SD 3.0 已在 2025 年 4 月 deprecated，所以确保你用的是 3.5 &lt;small&gt;&lt;a href=&quot;#ref3&quot;&gt;[3]&lt;/a&gt;&lt;/small&gt;。&lt;/p&gt;
&lt;h2&gt;GPU 现实检查&lt;/h2&gt;
&lt;p&gt;硬件要求已经明显抬高。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;VRAM&lt;/th&gt;
&lt;th&gt;照片真实感能力&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3060 12GB&lt;/td&gt;
&lt;td&gt;12GB&lt;/td&gt;
&lt;td&gt;只适合 SD 1.5 照片真实感。SDXL 会很紧&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 4070 Ti&lt;/td&gt;
&lt;td&gt;12GB&lt;/td&gt;
&lt;td&gt;1024x1024 的 SDXL。通过优化可以跑 Flux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RTX 4090&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;24GB&lt;/td&gt;
&lt;td&gt;甜点位。可以舒服处理 1024x1024+ 的 SDXL、Flux、SD 3.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RTX 5090&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;32GB&lt;/td&gt;
&lt;td&gt;全都能跑，包括 4K 生成和 batch workflows。32GB GDDR7，512-bit bus &lt;small&gt;&lt;a href=&quot;#ref4&quot;&gt;[4]&lt;/a&gt;&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8GB cards&lt;/td&gt;
&lt;td&gt;8GB&lt;/td&gt;
&lt;td&gt;借助 ComfyUI 的 VRAM 管理勉强可用。不舒服&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;2023 年那个“RTX 3080 上跑 512x512”的甜点位已经是古代史。&lt;strong&gt;1024x1024 现在是标准分辨率&lt;/strong&gt;，而你至少需要 16GB VRAM，才能不被持续的小挫败折磨。24GB 开始舒服。&lt;/p&gt;
&lt;p&gt;专门针对照片真实感来说，更多 VRAM 意味着你可以同时运行更大的模型、更高分辨率和 ControlNet，而不用 offload 到 CPU。&lt;/p&gt;
&lt;h2&gt;照片真实感设置&lt;/h2&gt;
&lt;h3&gt;Sampler&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;DPM++ 2M Karras&lt;/strong&gt;，25-30 steps。这是 SDXL 照片真实感已经沉淀下来的共识，速度和质量的比例最好。如果你想在低 step 数下要稍微更多细节，切到 &lt;strong&gt;DPM++ SDE Karras&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于 Flux：使用默认 sampler，20-30 steps。&lt;/p&gt;
&lt;h3&gt;CFG&lt;/h3&gt;
&lt;p&gt;对于 SDXL 照片真实感：&lt;strong&gt;7-9&lt;/strong&gt;。这能给出强 prompt adherence，同时避免 10 以上常见的过饱和、过度烹煮感。&lt;/p&gt;
&lt;p&gt;对于 SD 3.5：从更低开始（&lt;strong&gt;3-5&lt;/strong&gt;），因为 guidance 机制不同。&lt;/p&gt;
&lt;p&gt;对于 Flux：遵循模型自己的建议，但通常比 SDXL 更低。&lt;/p&gt;
&lt;h3&gt;Resolution&lt;/h3&gt;
&lt;p&gt;在模型原生分辨率生成（SDXL/SD 3.5/Flux 为 1024x1024），然后再为更高分辨率做 &lt;strong&gt;upscale&lt;/strong&gt;。不要试图直接生成 2048x2048，你会得到 artifacts、重复元素和构图问题。&lt;/p&gt;
&lt;p&gt;Upscaling 选择：A1111 里的 hi-res fix，或者 ComfyUI 里的专用 upscaling nodes（4x-UltraSharp、ESRGAN）。&lt;/p&gt;
&lt;h3&gt;Prompting for Photorealism&lt;/h3&gt;
&lt;p&gt;和 2023 年相比最大的变化是：&lt;strong&gt;自然地写，不要写关键词。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SD 1.5 需要这样的 prompts：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;portrait of a woman, photorealistic, 8k, ultra detailed, sharp focus,
professional photography, Fujifilm X-T4, 85mm f/1.4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;SDXL 和 Flux 能理解：&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;A portrait of a woman in soft afternoon light, photographed with a shallow
depth of field. She&apos;s looking slightly off-camera with a natural expression.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;关键词浓汤在 SDXL 上仍然能用，但自然语言会产生更连贯的结果。尤其是 Flux，非常擅长描述性、对话式 prompts。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Negative prompts：&lt;/strong&gt; 保持最小。先从没有开始，然后加入具体修正。&lt;code class=&quot;language-text&quot;&gt;cartoon, illustration, painting&lt;/code&gt; 通常就足够让画面保持照片真实感。完整的 negative prompt 思路转变可以看 &lt;a href=&quot;/zh/stable-difussion-cheat-sheet/&quot;&gt;cheat sheet&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;ControlNet 改变一切&lt;/h2&gt;
&lt;p&gt;如果你认真追求照片真实感构图，ControlNet 是不可商量的。它让你通过下面这些方式控制图像结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Depth maps&lt;/strong&gt; -- 保持空间关系和透视&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Canny edge detection&lt;/strong&gt; -- 保留轮廓和形状&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenPose&lt;/strong&gt; -- 控制人体姿势和身体比例&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Surface normals&lt;/strong&gt; -- 让光照和表面发生真实交互&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ControlNet 模型现在已经可用于 SDXL、Flux 和 SD 3.5 &lt;small&gt;&lt;a href=&quot;#ref5&quot;&gt;[5]&lt;/a&gt;&lt;/small&gt;。Multi-ControlNet（叠加多个控制）能给你 prompt engineering 单独做不到的精确构图控制。&lt;/p&gt;
&lt;p&gt;工作流是：拿一张参考照片，提取 depth map 或 pose，把它作为 ControlNet input，然后生成同构图的照片真实感图像。&lt;/p&gt;
&lt;h2&gt;速度 vs. 质量&lt;/h2&gt;
&lt;p&gt;如果你需要快速迭代（概念工作、prompt 测试），用 &lt;strong&gt;SDXL Lightning&lt;/strong&gt;。它能用 2-8 steps 生成质量不错的 1024px 图像 &lt;small&gt;&lt;a href=&quot;#ref6&quot;&gt;[6]&lt;/a&gt;&lt;/small&gt;。在更高分辨率上，它比 SDXL Turbo 质量更好。&lt;/p&gt;
&lt;p&gt;最终输出时，切回完整 SDXL 或 Flux，25-30 steps。差异看得出来。&lt;/p&gt;
&lt;h2&gt;实用工作流&lt;/h2&gt;
&lt;p&gt;下面是 2026 年照片真实感输出真正有效的流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;选择模型&lt;/strong&gt; -- 最佳照片真实感用 Flux 2，SDXL 生态用 Juggernaut XL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写自然语言 prompt&lt;/strong&gt;，描述你看到的画面&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;以 1024x1024 生成&lt;/strong&gt;，DPM++ 2M Karras，CFG 7-9，25-30 steps&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用 ControlNet&lt;/strong&gt;，如果你需要特定构图（depth 或 pose）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代 prompt&lt;/strong&gt; -- 生成 4-8 张，选最好的&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Upscale&lt;/strong&gt; 胜出的那张到目标分辨率&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inpaint&lt;/strong&gt; 任何问题区域（手、眼睛、小细节）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;无论你在 ComfyUI 还是 A1111 里，这都是同一套工作流。工具不同，pipeline 不变。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;参考资料&lt;/h3&gt;
&lt;p&gt;&lt;a id=&quot;ref1&quot;&gt;&lt;/a&gt;1. &lt;a href=&quot;https://bfl.ai/models/flux-2&quot;&gt;Flux 2 Models -- Black Forest Labs&lt;/a&gt; -- &lt;em&gt;Flux 2 官方模型页面。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref2&quot;&gt;&lt;/a&gt;2. &lt;a href=&quot;https://blogs.nvidia.com/blog/rtx-ai-garage-flux-2-comfyui/&quot;&gt;FLUX.2 and NVIDIA RTX AI Garage&lt;/a&gt; -- &lt;em&gt;Flux 2 与 ComfyUI 集成，以及行业采用情况。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref3&quot;&gt;&lt;/a&gt;3. &lt;a href=&quot;https://platform.stability.ai/docs/release-notes&quot;&gt;Stability AI Release Notes&lt;/a&gt; -- &lt;em&gt;SD 3.0 deprecation 和 3.5 发布细节。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref4&quot;&gt;&lt;/a&gt;4. &lt;a href=&quot;https://www.bestgpusforai.com/gpu-comparison/5090-vs-4090&quot;&gt;RTX 5090 vs 4090 for AI Workloads&lt;/a&gt; -- &lt;em&gt;面向图像生成的硬件对比。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref5&quot;&gt;&lt;/a&gt;5. &lt;a href=&quot;https://stable-diffusion-art.com/controlnet/&quot;&gt;ControlNet Complete Guide&lt;/a&gt; -- &lt;em&gt;面向多种架构的新版 ControlNet 文档。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref6&quot;&gt;&lt;/a&gt;6. &lt;a href=&quot;https://huggingface.co/ByteDance/SDXL-Lightning&quot;&gt;SDXL-Lightning by ByteDance&lt;/a&gt; -- &lt;em&gt;2-8 step 生成模型。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref7&quot;&gt;&lt;/a&gt;7. &lt;a href=&quot;https://www.cubix.co/blog/best-model-for-stable-diffusion/&quot;&gt;Best Stable Diffusion Models for Photorealism 2026&lt;/a&gt; -- &lt;em&gt;当前模型版图。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref8&quot;&gt;&lt;/a&gt;8. &lt;a href=&quot;https://civitai.com/articles/2115/top-5-photorealistic-stable-diffusion-models-reviewed&quot;&gt;Top Photorealistic Stable Diffusion Models&lt;/a&gt; -- &lt;em&gt;Civitai 社区评测。&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;相关文章&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/zh/stable-difussion-cheat-sheet/&quot;&gt;Stable Diffusion Cheat Sheet：故障排查与优化&lt;/a&gt; -- 参数、samplers 和故障排查的快速参考。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Stable Diffusion 速查表：故障排查与优化]]></title><description><![CDATA[2026 年 3 月更新。 这份速查表的最初版本写于 2023 年 5 月，当时面向的是 SD 1.5。此后几乎一切都变了：新的架构（SDXL、SD 3.5、Flux）、新的 UI（ComfyUI）、新的硬件（RTX 509…]]></description><link>https://bdteo.com/zh/stable-difussion-cheat-sheet/</link><guid isPermaLink="false">https://bdteo.com/zh/stable-difussion-cheat-sheet/</guid><pubDate>Thu, 04 May 2023 23:30:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2026 年 3 月更新。&lt;/strong&gt; 这份速查表的最初版本写于 2023 年 5 月，当时面向的是 SD 1.5。此后几乎一切都变了：新的架构（SDXL、SD 3.5、Flux）、新的 UI（ComfyUI）、新的硬件（RTX 5090），以及关于负面提示词哲学的一次彻底反转。这是当前版本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是我工作时查 Stable Diffusion 参数用的参考。不是教程，只是当事情不对劲，或者我想把质量再推高一点时，会伸手去调的那些设置。&lt;/p&gt;
&lt;h2&gt;该用哪个模型&lt;/h2&gt;
&lt;p&gt;现在这是第一个决定，而且它比任何参数微调都更重要。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模型&lt;/th&gt;
&lt;th&gt;最适合&lt;/th&gt;
&lt;th&gt;分辨率&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flux 2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;写实主义、提示词遵循度&lt;/td&gt;
&lt;td&gt;1024x1024+&lt;/td&gt;
&lt;td&gt;2026 年最好的开放权重写实模型。已集成进 Adobe Photoshop &lt;small&gt;&lt;a href=&quot;#ref1&quot;&gt;[1]&lt;/a&gt;&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SDXL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通用场景&lt;/td&gt;
&lt;td&gt;1024x1024&lt;/td&gt;
&lt;td&gt;微调生态极大。Juggernaut XL、Realistic Vision、DreamShaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SD 3.5 Large&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;顶级质量（Stability 的旗舰）&lt;/td&gt;
&lt;td&gt;1024x1024&lt;/td&gt;
&lt;td&gt;MMDiT 架构。SD 3.0 已于 2025 年 4 月废弃 &lt;small&gt;&lt;a href=&quot;#ref2&quot;&gt;[2]&lt;/a&gt;&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SDXL Lightning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;速度&lt;/td&gt;
&lt;td&gt;1024x1024&lt;/td&gt;
&lt;td&gt;2-8 步生成。在更高分辨率下质量优于 Turbo &lt;small&gt;&lt;a href=&quot;#ref3&quot;&gt;[3]&lt;/a&gt;&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SD 1.5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;遗留工作流&lt;/td&gt;
&lt;td&gt;512x512&lt;/td&gt;
&lt;td&gt;微调库巨大，但正在被逐步淘汰。SD 2.0/2.1 已正式废弃&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果你从零开始：&lt;strong&gt;写实主义用 Flux 2，其他都用 SDXL。&lt;/strong&gt; SD 3.5 很好，但生态更小。&lt;/p&gt;
&lt;h2&gt;该用哪个 UI&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;UI&lt;/th&gt;
&lt;th&gt;最适合&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ComfyUI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;进阶用户。基于节点，更好的显存管理，快 15%，对 Flux 支持最好。截至 2025 年，是严肃工作的行业标准 &lt;small&gt;&lt;a href=&quot;#ref4&quot;&gt;[4]&lt;/a&gt;&lt;/small&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Automatic1111&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;初学者。界面更简单，扩展库巨大。用于 SDXL 仍然没问题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fooocus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;一键生成。配置最少。适合快速出结果&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我用 ComfyUI。学习曲线更陡一些（预期要 10-20 小时才能顺手），但光是显存管理就值了：A1111 会崩的 8GB 显存，它能跑 SDXL。&lt;/p&gt;
&lt;h2&gt;采样器&lt;/h2&gt;
&lt;p&gt;采样器之争基本已经定下来了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常用选择：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DPM++ 2M Karras&lt;/strong&gt; -- 速度和质量的最佳比例。这是我几乎所有事情的默认选项。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DPM++ SDE Karras&lt;/strong&gt; -- 在低步数时略好。适合快速迭代。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Euler a&lt;/strong&gt; -- 依然可靠。输出变化更多，适合探索。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;什么时候切换：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;输出缺少多样性？试试 DPM++ SDE 或 Euler a。&lt;/li&gt;
&lt;li&gt;有伪影或过饱和？试试 DPM++ 2M Karras 或普通 Euler。&lt;/li&gt;
&lt;li&gt;速度压倒一切？Euler a 或 DPM++ 2M（非 Karras）。&lt;/li&gt;
&lt;li&gt;想要最高质量？DPM++ 3M SDE Karras 或 UniPC。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;步数：&lt;/strong&gt; 大多数采样器用 20-30 步。Lightning 模型只需要 2-8 步。&lt;/p&gt;
&lt;h2&gt;CFG（Classifier Free Guidance）&lt;/h2&gt;
&lt;p&gt;模型有多严格地跟随你的提示词，而不是按自己的理解发挥。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;范围&lt;/th&gt;
&lt;th&gt;效果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1-4&lt;/td&gt;
&lt;td&gt;非常有创造性，理解很松。经常不连贯&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5-7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;适合大多数工作的良好平衡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;7-10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;提示词遵循度强。SDXL 写实主义的甜点区&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-15&lt;/td&gt;
&lt;td&gt;有伪影和颜色过度处理的风险&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15+&lt;/td&gt;
&lt;td&gt;几乎总是太多。伪影基本保证出现&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; SD 3.5 使用不同的 guidance 机制。CFG 这个概念仍然适用，但尺度表现不同：从更低的值开始（3-5），再往上调。&lt;/p&gt;
&lt;h2&gt;分辨率&lt;/h2&gt;
&lt;p&gt;512x512 的时代结束了。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模型&lt;/th&gt;
&lt;th&gt;原生分辨率&lt;/th&gt;
&lt;th&gt;实用范围&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SD 1.5&lt;/td&gt;
&lt;td&gt;512x512&lt;/td&gt;
&lt;td&gt;512x512 到 768x768&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SDXL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1024x1024&lt;/td&gt;
&lt;td&gt;1024x1024（标准）、1024x768、768x1024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SD 3.5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1024x1024&lt;/td&gt;
&lt;td&gt;1024x1024+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flux&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1024x1024&lt;/td&gt;
&lt;td&gt;1024x1024+，高端 GPU 可到 4K&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;超过原生分辨率会增加伪影和构图问题的风险。不要直接生成 2048x2048，用 hi-res fix 或放大。&lt;/p&gt;
&lt;h2&gt;Clip Skip&lt;/h2&gt;
&lt;p&gt;没有以前那么重要了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SD 1.5：&lt;/strong&gt; Clip skip 1-2 很重要。动漫模型经常用 clip skip 2。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SDXL：&lt;/strong&gt; 使用双文本编码器（CLIP + OpenCLIP）。Clip skip 基本会被忽略，架构处理方式不同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SD 3.5 / Flux：&lt;/strong&gt; 不能以同样方式适用。这些模型使用基于 transformer 的文本编码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你用的是 SDXL 或更新模型：不用担心 clip skip。如果你用的是 SD 1.5：写实主义保持 1，动漫用 2。&lt;/p&gt;
&lt;h2&gt;负面提示词&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;哲学已经反过来了。&lt;/strong&gt; 2023 年的建议是使用很长的负面提示词列表。到 2026 年，共识是：&lt;strong&gt;先什么都不写，只在需要修复时添加具体内容。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为什么会变：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SDXL 和 Flux 对自然语言的理解远好于 SD 1.5&lt;/li&gt;
&lt;li&gt;很长的负面提示词实际上可能会&lt;em&gt;限制创造性&lt;/em&gt;，并产生更差的结果&lt;/li&gt;
&lt;li&gt;&quot;bad anatomy&quot; 太模糊，没什么用。&quot;ugly&quot; 也不起作用，因为 SD 不是在带有 &quot;ugly&quot; 标签的图像上训练出来的&lt;/li&gt;
&lt;li&gt;有些模型在长负面提示词下表现明显更差 &lt;small&gt;&lt;a href=&quot;#ref5&quot;&gt;[5]&lt;/a&gt;&lt;/small&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;当前做法：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先不加任何负面提示词生成。&lt;/li&gt;
&lt;li&gt;如果看到具体问题（多余手指、背景模糊），再针对那个问题添加负面提示词。&lt;/li&gt;
&lt;li&gt;使用强调权重：写 &lt;code class=&quot;language-text&quot;&gt;(blurry:1.3)&lt;/code&gt;，而不是只写 &lt;code class=&quot;language-text&quot;&gt;blurry&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;保持简短：最多 5-10 个词。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;GPU 快速参考&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;显存&lt;/th&gt;
&lt;th&gt;适合&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3060 12GB&lt;/td&gt;
&lt;td&gt;12GB&lt;/td&gt;
&lt;td&gt;SD 1.5、基础 SDXL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 4070 Ti&lt;/td&gt;
&lt;td&gt;12GB&lt;/td&gt;
&lt;td&gt;SDXL、部分 Flux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RTX 4090&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;24GB&lt;/td&gt;
&lt;td&gt;一切。主力干活卡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RTX 5090&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;32GB&lt;/td&gt;
&lt;td&gt;包括 4K 和批量生成在内的一切&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8GB 卡&lt;/td&gt;
&lt;td&gt;8GB&lt;/td&gt;
&lt;td&gt;最低可用。ComfyUI 有助于显存管理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;24GB 是一个分界线：SDXL 和 Flux 在这里开始舒服，不用一直和显存较劲。&lt;/p&gt;
&lt;h2&gt;故障排查速修&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;试试&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;输出模糊&lt;/td&gt;
&lt;td&gt;增加步数。检查分辨率是否匹配模型原生分辨率&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多余手指/肢体&lt;/td&gt;
&lt;td&gt;把 &lt;code class=&quot;language-text&quot;&gt;extra fingers, extra limbs&lt;/code&gt; 加进负面提示词。或者使用 ControlNet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;颜色过饱和&lt;/td&gt;
&lt;td&gt;降低 CFG。切换到 DPM++ 2M Karras&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;构图不对&lt;/td&gt;
&lt;td&gt;使用 ControlNet（depth、canny、pose），不要和提示词硬拗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;生成太慢&lt;/td&gt;
&lt;td&gt;使用 Lightning 模型，减少步数，用 ComfyUI 获得更好的显存表现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;显存不足&lt;/td&gt;
&lt;td&gt;切换到 ComfyUI，减少 batch size，使用 fp16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3&gt;参考资料&lt;/h3&gt;
&lt;p&gt;&lt;a id=&quot;ref1&quot;&gt;&lt;/a&gt;1. &lt;a href=&quot;https://blogs.nvidia.com/blog/rtx-ai-garage-flux-2-comfyui/&quot;&gt;Flux 2 and NVIDIA RTX AI Integration&lt;/a&gt; -- &lt;em&gt;NVIDIA 对 Flux 2 与 ComfyUI 的介绍。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref2&quot;&gt;&lt;/a&gt;2. &lt;a href=&quot;https://platform.stability.ai/docs/release-notes&quot;&gt;Stability AI Release Notes&lt;/a&gt; -- &lt;em&gt;SD 3.0 废弃和 3.5 发布。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref3&quot;&gt;&lt;/a&gt;3. &lt;a href=&quot;https://huggingface.co/ByteDance/SDXL-Lightning&quot;&gt;SDXL-Lightning by ByteDance&lt;/a&gt; -- &lt;em&gt;1024px 下 2-8 步生成。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref4&quot;&gt;&lt;/a&gt;4. &lt;a href=&quot;https://wiki.shakker.ai/en/comfyui-vs-automatic1111&quot;&gt;ComfyUI vs Automatic1111 2026 Comparison&lt;/a&gt; -- &lt;em&gt;性能和功能对比。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref5&quot;&gt;&lt;/a&gt;5. &lt;a href=&quot;https://stable-diffusion-art.com/how-to-use-negative-prompts/&quot;&gt;How to Use Negative Prompts Effectively&lt;/a&gt; -- &lt;em&gt;关于最小负面提示词哲学的更新指南。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref6&quot;&gt;&lt;/a&gt;6. &lt;a href=&quot;https://civitai.com/articles/7484/understanding-stable-diffusion-samplers-beyond-image-comparisons&quot;&gt;Understanding Stable Diffusion Samplers&lt;/a&gt; -- &lt;em&gt;采样器对比和选择指南。&lt;/em&gt;&lt;br&gt;
&lt;a id=&quot;ref7&quot;&gt;&lt;/a&gt;7. &lt;a href=&quot;https://www.cubix.co/blog/best-model-for-stable-diffusion/&quot;&gt;Best Stable Diffusion Models for 2026&lt;/a&gt; -- &lt;em&gt;当前模型格局。&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;相关文章&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/zh/pushing-the-stable-diffussion-limits/&quot;&gt;Stable Diffusion 写实主义：设置与 GPU 限制指南&lt;/a&gt; -- 深入讲解如何用当前模型实现写实结果。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[关于我]]></title><description><![CDATA[写“关于”页面是一件奇怪的事。你最后会像描述一个陌生人那样描述自己，而我对这件事有点怀疑。 所以，直说吧。我是 Boris…]]></description><link>https://bdteo.com/zh/about/</link><guid isPermaLink="false">https://bdteo.com/zh/about/</guid><pubDate>Thu, 04 May 2023 22:12:03 GMT</pubDate><content:encoded>&lt;p&gt;写“关于”页面是一件奇怪的事。你最后会像描述一个陌生人那样描述自己，而我对这件事有点怀疑。&lt;/p&gt;
&lt;p&gt;所以，直说吧。我是 Boris。我写软件已经十四年多一点了。这份工作付房租，养狗，也在大多数早晨给我一个可以琢磨的问题——老实说，这已经比大多数工作给得更多了。现在我是一个小产品团队里的后端那一半，大概夹在 Laravel、Docker 和 Kubernetes 之间，主要担心那些本该比填满更快清空的队列，以及那些增长速度快得让我不太舒服的索引。&lt;/p&gt;
&lt;p&gt;写软件之前，我在大学学数学。也从来没有真正停下。数学是这个世界太吵时我会去的地方。尤其是数论——质数里有一种诚实，我在许多别的地方找不到。我在这里写的很多东西，都是从那个方向开始的。一个小小的数学痒处。一篇半懂不懂的医学论文。一个我不得不读四遍的法语句子。然后我顺着它往下拽，看看还连着什么。&lt;/p&gt;
&lt;p&gt;这个博客就是我往下拽的地方。这里没有什么专家之作。我读自己其实没资格读的医学。我读保加利亚语和法语小说，假装自己跟得上。我慢慢学德语，主要是因为它的语法让我觉得好玩。我有时也手写 C++。不是为了工作——只是因为那种自我约束本身就有它自己的意味。这些文章都从那些东西里长出来——安静地试着把我注意到的东西写下来，趁我还没忘。&lt;/p&gt;
&lt;p&gt;我住在索非亚。两只救助来的流浪狗——Кожухка 和 Еклер——负责确保我别把自己太当回事。它们以前失败过。现在还在继续努力。&lt;/p&gt;
&lt;p&gt;大概就是这些。我还在 Percepticus 这个名字下面保留着一块小小的自由职业招牌，接一些靠口口相传来的活。除此之外，文章就在这里，而其余的，我也乐意保持安静。&lt;/p&gt;</content:encoded></item></channel></rss>