<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pinellia Blog</title><description>Markdown everything.</description><link>https://pinellia.uk/</link><language>zh_CN</language><item><title>GNU Screen 仅保留 Ctrl+A Ctrl+D，其余按键透传</title><link>https://pinellia.uk/posts/gnu-screen-key-pass-through/</link><guid isPermaLink="true">https://pinellia.uk/posts/gnu-screen-key-pass-through/</guid><description>通过调整 Screen 命令前缀与输入序列映射，仅保留 Ctrl+A Ctrl+D 用于 detach，同时让其他按键尽量原样传递给内部程序。</description><pubDate>Wed, 16 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;GNU Screen 默认使用 &lt;code&gt;Ctrl+A&lt;/code&gt; 作为命令前缀。因此，进入 Screen 后，很多以 &lt;code&gt;Ctrl+A&lt;/code&gt; 开头的按键序列会被 Screen 截获，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ctrl+A C&lt;/code&gt;：创建窗口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+A N&lt;/code&gt;：切换到下一个窗口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+A K&lt;/code&gt;：关闭窗口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+A Ctrl+A&lt;/code&gt;：切换窗口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这会影响 Screen 内部程序对 &lt;code&gt;Ctrl+A&lt;/code&gt; 及其后续按键的使用。&lt;/p&gt;
&lt;p&gt;目标是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;仅保留 &lt;code&gt;Ctrl+A Ctrl+D&lt;/code&gt;，用于 detach 当前 Screen；&lt;/li&gt;
&lt;li&gt;其他按键尽量原样传递给 Screen 内运行的程序；&lt;/li&gt;
&lt;li&gt;不修改、不重载、不终止当前已经运行的 Screen 服务进程；&lt;/li&gt;
&lt;li&gt;如果现有 Screen 版本能实现，就不额外安装新版本。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;环境&lt;/h2&gt;
&lt;p&gt;验证时使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GNU Screen 4.09.00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该版本已经支持所需的 &lt;code&gt;escape&lt;/code&gt;、&lt;code&gt;bindkey&lt;/code&gt; 和 &lt;code&gt;stuff&lt;/code&gt;/输入映射机制，因此不需要在 &lt;code&gt;~/.local/bin&lt;/code&gt; 中安装新版 Screen，也不需要替换系统的 &lt;code&gt;/usr/bin/screen&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;核心原理&lt;/h2&gt;
&lt;h3&gt;1. 不能只解除默认按键绑定&lt;/h3&gt;
&lt;p&gt;一个容易想到的方案是解除 Screen 的默认快捷键，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bind c
bind n
bind k
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这种方式只会让 Screen 不再执行相应命令，并不保证完整的按键序列被传递给内部程序。Screen 已经截获了命令前缀 &lt;code&gt;Ctrl+A&lt;/code&gt;，所以单纯 &lt;code&gt;unbind&lt;/code&gt; 可能导致按键被吞掉。&lt;/p&gt;
&lt;h3&gt;2. 将 Screen 自身的命令前缀移走&lt;/h3&gt;
&lt;p&gt;Screen 的 &lt;code&gt;escape&lt;/code&gt; 配置格式是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;escape xy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt; 是 Screen 命令前缀；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y&lt;/code&gt; 是用于输入字面命令前缀的 meta 字符；&lt;/li&gt;
&lt;li&gt;默认值相当于 &lt;code&gt;Ctrl+A&lt;/code&gt; 和 &lt;code&gt;A&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了不让 Screen 默认截获 &lt;code&gt;Ctrl+A&lt;/code&gt;，把 Screen 的内部命令前缀设置为终端键盘通常不会发送的字节 &lt;code&gt;0xff&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;escape \377\377
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;\377&lt;/code&gt; 是八进制写法，对应十六进制 &lt;code&gt;0xff&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这样，默认的 &lt;code&gt;Ctrl+A&lt;/code&gt; 命令体系不再占用常规键盘输入。&lt;/p&gt;
&lt;h3&gt;3. 单独绑定 &lt;code&gt;Ctrl+A Ctrl+D&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;然后使用 Screen 的输入序列映射功能，单独识别两个连续字节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;\001&lt;/code&gt;：&lt;code&gt;Ctrl+A&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\004&lt;/code&gt;：&lt;code&gt;Ctrl+D&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bindkey &quot;\001\004&quot; detach
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Screen 收到该序列时执行 &lt;code&gt;detach&lt;/code&gt;；其他输入则继续发送给 Screen 内运行的程序。&lt;/p&gt;
&lt;h2&gt;最终配置&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;~/.screenrc&lt;/code&gt; 中加入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Pass keyboard input through to applications, except Ctrl-A Ctrl-D (detach).
# 0xff is used as Screen&apos;s otherwise-unused internal command/meta prefix so
# Ctrl-A is no longer captured by Screen&apos;s default command key. bindkey still
# recognizes the two-byte detach sequence; a lone Ctrl-A is forwarded after
# Screen&apos;s short sequence-disambiguation timeout.
#
# This file is read when a Screen server starts. Existing Screen processes keep
# their current bindings and are intentionally unaffected by this change.
escape \377\377
bindkey &quot;\001\004&quot; detach
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本机修改后的完整 &lt;code&gt;~/.screenrc&lt;/code&gt; 为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Advertise only the color depth that this GNU Screen build can render.
# Screen 4.09 misparses 24-bit SGR colors as bold/underline/reverse attributes,
# so do not inherit a truecolor hint from the outer terminal.
term screen-256color
unsetenv COLORTERM
setenv PI_TRUE_COLOR 0

# Pass keyboard input through to applications, except Ctrl-A Ctrl-D (detach).
# 0xff is used as Screen&apos;s otherwise-unused internal command/meta prefix so
# Ctrl-A is no longer captured by Screen&apos;s default command key. bindkey still
# recognizes the two-byte detach sequence; a lone Ctrl-A is forwarded after
# Screen&apos;s short sequence-disambiguation timeout.
#
# This file is read when a Screen server starts. Existing Screen processes keep
# their current bindings and are intentionally unaffected by this change.
escape \377\377
bindkey &quot;\001\004&quot; detach

# Keep native scrollback and enable bracketed paste while Screen is attached.
# Most modern terminals (VS Code, GNOME Terminal, Kitty, WezTerm, iTerm2, etc.)
# identify as an xterm variant and understand these sequences.
termcapinfo xterm* ti=\E[?2004h:te=\E[?2004l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中颜色和 bracketed paste 配置是本机原有设置，与本次按键修改没有直接关系。真正与本次需求相关的只有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;escape \377\377
bindkey &quot;\001\004&quot; detach
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;为什么单独按 &lt;code&gt;Ctrl+A&lt;/code&gt; 会有短暂延迟&lt;/h2&gt;
&lt;p&gt;Screen 收到 &lt;code&gt;Ctrl+A&lt;/code&gt; 后，无法立即知道用户接下来是否会按 &lt;code&gt;Ctrl+D&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;因此它必须短暂等待：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果后续是 &lt;code&gt;Ctrl+D&lt;/code&gt;，执行 detach；&lt;/li&gt;
&lt;li&gt;如果后续不是 &lt;code&gt;Ctrl+D&lt;/code&gt;，则将输入交给内部程序；&lt;/li&gt;
&lt;li&gt;如果没有后续按键，等待序列判定超时后，将单独的 &lt;code&gt;Ctrl+A&lt;/code&gt; 传入内部程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是识别多字节快捷键时不可避免的消歧过程。延迟只影响可能构成该序列开头的 &lt;code&gt;Ctrl+A&lt;/code&gt;，普通按键不受影响。&lt;/p&gt;
&lt;p&gt;配置中没有给 &lt;code&gt;bindkey&lt;/code&gt; 添加 &lt;code&gt;-t&lt;/code&gt;。这是有意为之：Screen 使用短暂的字符间超时进行序列判定，使单独的 &lt;code&gt;Ctrl+A&lt;/code&gt; 最终能够被转发。如果使用 &lt;code&gt;bindkey -t&lt;/code&gt; 禁用字符间计时，可能导致单独的 &lt;code&gt;Ctrl+A&lt;/code&gt; 一直等待后续字符。&lt;/p&gt;
&lt;h2&gt;不影响现有 Screen 会话的方法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;~/.screenrc&lt;/code&gt; 在新的 Screen 服务进程启动时读取。直接修改该文件，不会自动修改已经运行的 Screen 服务进程。&lt;/p&gt;
&lt;p&gt;因此安全操作原则是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;只修改 &lt;code&gt;~/.screenrc&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;不对现有会话执行 &lt;code&gt;screen -X source ~/.screenrc&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;不向现有会话发送其他 &lt;code&gt;screen -X&lt;/code&gt; 配置命令；&lt;/li&gt;
&lt;li&gt;不关闭或重启现有 Screen 服务；&lt;/li&gt;
&lt;li&gt;使用一个全新且唯一命名的临时会话测试配置。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;结果是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已经运行的 Screen 服务继续使用旧按键配置；&lt;/li&gt;
&lt;li&gt;修改后新启动的 Screen 服务使用新按键配置；&lt;/li&gt;
&lt;li&gt;老会话自然结束后，新配置会逐步成为默认行为。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要注意，Screen 的服务进程和窗口可能长期存在。连接到一个已经存在的老 Screen 服务时，使用的仍可能是该服务启动时加载的旧配置；只有新建的 Screen 服务进程才能自然加载新配置。&lt;/p&gt;
&lt;h2&gt;修改前备份&lt;/h2&gt;
&lt;p&gt;修改前执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp -a ~/.screenrc \
  ~/.screenrc.backup-before-pass-through-$(date +%Y%m%d-%H%M%S)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本次实际备份文件是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.screenrc.backup-before-pass-through-20260916-213152
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;验证思路&lt;/h2&gt;
&lt;p&gt;为了避免误操作正在运行的会话，验证时应新建唯一命名的临时 Screen 会话，并在内部运行一个读取原始终端字节的小程序。&lt;/p&gt;
&lt;p&gt;主要验证点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;普通字符原样传入；&lt;/li&gt;
&lt;li&gt;方向键等 ANSI 转义序列原样传入；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+C&lt;/code&gt; 等控制字符原样传入；&lt;/li&gt;
&lt;li&gt;原 Screen 快捷键，例如 &lt;code&gt;Ctrl+A C&lt;/code&gt;、&lt;code&gt;Ctrl+A N&lt;/code&gt;、&lt;code&gt;Ctrl+A K&lt;/code&gt;，作为字节原样传入；&lt;/li&gt;
&lt;li&gt;单独的 &lt;code&gt;Ctrl+A&lt;/code&gt; 在短暂等待后传入；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl+A Ctrl+D&lt;/code&gt; 不传入内部程序，而是 detach；&lt;/li&gt;
&lt;li&gt;测试结束后只清理该临时测试会话。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本次字节级测试结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pass_through_ok=true
detach_ok=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试确认：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ctrl+A C&lt;/code&gt;、&lt;code&gt;Ctrl+A N&lt;/code&gt;、&lt;code&gt;Ctrl+A K&lt;/code&gt; 均被原样传给内部程序；&lt;/li&gt;
&lt;li&gt;普通字符、&lt;code&gt;Ctrl+C&lt;/code&gt;、方向键转义序列均保持不变；&lt;/li&gt;
&lt;li&gt;单独的 &lt;code&gt;Ctrl+A&lt;/code&gt; 在短暂超时后被传入；&lt;/li&gt;
&lt;li&gt;只有 &lt;code&gt;Ctrl+A Ctrl+D&lt;/code&gt; 被 Screen 捕获并执行 detach。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;日常使用&lt;/h2&gt;
&lt;p&gt;修改之后启动一个全新的 Screen 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;screen -S example
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;退出并保留内部任务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Ctrl+A，然后按 Ctrl+D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新连接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;screen -r example
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在新会话中，Screen 原有的其他 &lt;code&gt;Ctrl+A&lt;/code&gt; 管理快捷键不再可用。这是本方案的预期行为，因为目标就是只保留 detach 功能。&lt;/p&gt;
&lt;p&gt;如果后续确实需要执行其他 Screen 管理命令，可以在 Screen 外部通过命令行针对会话操作，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;screen -ls
screen -S example -X quit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;quit&lt;/code&gt; 会结束对应 Screen 会话及其窗口，必须谨慎执行。&lt;/p&gt;
&lt;h2&gt;回滚&lt;/h2&gt;
&lt;p&gt;若要恢复修改前的配置，可以用备份覆盖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp -a \
  ~/.screenrc.backup-before-pass-through-20260916-213152 \
  ~/.screenrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样，回滚后的配置只会由以后新启动的 Screen 服务自然加载。不要为了立即应用而重载或重启仍在执行任务的现有 Screen 服务。&lt;/p&gt;
&lt;p&gt;也可以只删除以下两行，恢复 Screen 默认命令前缀行为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;escape \377\377
bindkey &quot;\001\004&quot; detach
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最终结果&lt;/h2&gt;
&lt;p&gt;这套方案不逐个解除 Screen 的默认快捷键，而是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将 Screen 自身命令前缀从 &lt;code&gt;Ctrl+A&lt;/code&gt; 移到通常不会由键盘产生的 &lt;code&gt;0xff&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;bindkey&lt;/code&gt; 仅识别 &lt;code&gt;Ctrl+A Ctrl+D&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;让其余输入尽量保持原始字节并传递给内部程序；&lt;/li&gt;
&lt;li&gt;仅通过修改 &lt;code&gt;~/.screenrc&lt;/code&gt; 影响未来的新 Screen 服务，不触碰当前正在运行的服务。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;核心配置只有两行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;escape \377\377
bindkey &quot;\001\004&quot; detach
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>使用 YubiKey 配置 OpenPGP：密钥生成、备份与日常使用</title><link>https://pinellia.uk/posts/yubikey-openpgp-guide/</link><guid isPermaLink="true">https://pinellia.uk/posts/yubikey-openpgp-guide/</guid><description>从零配置 YubiKey 或兼容 OpenPGP 智能卡，涵盖 GnuPG 安装、密钥规划、离线备份、卡槽迁移、签名、加密、Git 与 SSH，以及丢失后的恢复和吊销。</description><pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;把 OpenPGP 私钥放进 YubiKey 一类的智能卡，核心价值不是“免密码”，而是让私钥不再以可直接复制的文件形式长期留在电脑中。签名、解密和身份认证都由卡片内部完成，即使电脑遭到入侵，攻击者也很难直接导出私钥。&lt;/p&gt;
&lt;p&gt;下面从密钥结构设计开始，依次完成备份验证、迁移入卡、Git 签名、SSH 登录和灾难恢复准备。文中的身份、邮箱、指纹、文件名和公钥地址均为脱敏示例，使用时需要替换成自己的实际信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Example User &amp;lt;user@example.com&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主密钥指纹统一使用以下占位符表示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;PRIMARY_KEY_FINGERPRINT&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
OpenPGP 公钥、完整指纹和卡片序列号不是同一种东西。公钥和完整指纹可以公开；私钥、PIN、Reset Code 和私钥备份绝不能公开。本文不会记录真实 PIN、Reset Code 或设备序列号。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;最终要得到什么&lt;/h2&gt;
&lt;p&gt;OpenPGP 卡提供三个相互独立的私钥槽：&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;Signature&lt;/td&gt;
&lt;td&gt;&lt;code&gt;S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文件、邮件和 Git commit 签名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encryption&lt;/td&gt;
&lt;td&gt;&lt;code&gt;E&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;解密发给自己的数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;&lt;code&gt;A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SSH 等身份认证&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;本文复现当前已经使用的结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;主密钥      [SC]  → Signature 槽
加密子密钥   [E]  → Encryption 槽
认证子密钥   [A]  → Authentication 槽
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;S&lt;/code&gt;、&lt;code&gt;C&lt;/code&gt;、&lt;code&gt;E&lt;/code&gt;、&lt;code&gt;A&lt;/code&gt; 分别表示 Sign、Certify、Encrypt 和 Authenticate。&lt;/p&gt;
&lt;p&gt;还有一种隔离更严格的结构：让主密钥只具有 &lt;code&gt;[C]&lt;/code&gt; 能力并永久离线，另外创建 &lt;code&gt;[S]&lt;/code&gt;、&lt;code&gt;[E]&lt;/code&gt;、&lt;code&gt;[A]&lt;/code&gt; 三把子密钥放入卡片。这样日常使用的卡片无法直接签发新子密钥或修改身份，适合长期维护的重要身份。本文采用 &lt;code&gt;[SC] + [E] + [A]&lt;/code&gt;，优点是结构简单、能够复现当前配置；如果以后准备重建长期身份，可以再评估离线 &lt;code&gt;[C]&lt;/code&gt; 方案。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;code&gt;keytocard&lt;/code&gt; 会在保存后把当前钥匙环里的对应私钥材料替换为智能卡引用。卡内私钥不能再导出，所以必须在迁移之前完成完整私钥备份，并实际验证备份可以恢复。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 准备 GnuPG 与卡片&lt;/h2&gt;
&lt;h3&gt;macOS&lt;/h3&gt;
&lt;p&gt;使用 Homebrew 安装 GnuPG 和图形化 PIN 输入程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install gnupg pinentry-mac
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果终端无法弹出 PIN 输入窗口，可以在 &lt;code&gt;~/.gnupg/gpg-agent.conf&lt;/code&gt; 中加入实际路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pinentry-program /opt/homebrew/bin/pinentry-mac
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Intel Mac 的路径通常是 &lt;code&gt;/usr/local/bin/pinentry-mac&lt;/code&gt;，以 &lt;code&gt;command -v pinentry-mac&lt;/code&gt; 的输出为准。修改后重启 agent：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpgconf --kill gpg-agent
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Linux&lt;/h3&gt;
&lt;p&gt;Debian 或 Ubuntu 可以安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install gnupg scdaemon pcscd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;部分发行版还需要启动 PC/SC 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable --now pcscd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;检查 OpenPGP 应用&lt;/h3&gt;
&lt;p&gt;插入设备后执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --card-status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应能看到以下字段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Application type .: OpenPGP
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果卡槽里已经有指纹，不要继续覆盖，先确认它们是否属于仍在使用的密钥。&lt;/p&gt;
&lt;p&gt;本文使用的设备在 Reader 字段中显示为 &lt;code&gt;LibreKeys ... yubikey&lt;/code&gt;，而不是标准的 Yubico 厂商名称。因此，算法、触摸确认和 KDF 等能力一律以 &lt;code&gt;gpg --card-status&lt;/code&gt; 的实际结果及固件文档为准，不能只根据 USB 名称推断。&lt;/p&gt;
&lt;h2&gt;2. 先规划算法与有效期&lt;/h2&gt;
&lt;p&gt;当前卡片报告的属性是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Key attributes ...: rsa2048 rsa2048 rsa2048
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此本文统一使用 RSA-2048，优先保证导入成功和客户端兼容性。不要在没有确认固件支持的情况下直接修改 &lt;code&gt;key-attr&lt;/code&gt;。如果使用的是明确支持 RSA-3072、RSA-4096 或 ECC 的设备，可以在创建身份之前重新评估算法，但三把待迁移密钥必须与对应卡槽兼容。&lt;/p&gt;
&lt;p&gt;本文把有效期设为 &lt;code&gt;0&lt;/code&gt;，即 OpenPGP 元数据中的“永不过期”。这不表示算法和设备会永远安全，只表示 GnuPG 不会因为日期自动判定密钥失效。长期身份仍应定期评估算法、设备状态和备份介质。&lt;/p&gt;
&lt;h2&gt;3. 创建主密钥与子密钥&lt;/h2&gt;
&lt;p&gt;先创建具有认证和签名能力的主密钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --quick-generate-key \
  &quot;Example User &amp;lt;user@example.com&amp;gt;&quot; \
  rsa2048 cert,sign 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为本地私钥设置一条足够强、且与卡片 PIN 不同的保护密码。&lt;/p&gt;
&lt;p&gt;查看完整指纹：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --list-secret-keys \
  --keyid-format long \
  --with-subkey-fingerprint \
  &quot;user@example.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;复制 &lt;code&gt;sec&lt;/code&gt; 下一行的 40 位完整主密钥指纹，并设置变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FPR=&apos;&amp;lt;PRIMARY_KEY_FINGERPRINT&amp;gt;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不要使用只有 8 位或 16 位的短 Key ID。先验证变量确实指向预期身份：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --list-secret-keys --keyid-format long &quot;$FPR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加加密子密钥和认证子密钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --quick-add-key &quot;$FPR&quot; rsa2048 encrypt 0
gpg --quick-add-key &quot;$FPR&quot; rsa2048 auth 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次检查结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --list-secret-keys \
  --keyid-format long \
  --with-subkey-fingerprint \
  &quot;$FPR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时应类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sec   rsa2048/... [SC]
ssb   rsa2048/... [E]
ssb   rsa2048/... [A]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 在迁移前制作完整备份&lt;/h2&gt;
&lt;p&gt;把下面的目录改为加密移动硬盘或其他离线介质上的真实路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BACKUP_DIR=&quot;/Volumes/EncryptedBackup/openpgp-backup&quot;
install -d -m 700 &quot;$BACKUP_DIR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导出公钥、完整私钥、子密钥和 ownertrust：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --armor \
  --output &quot;$BACKUP_DIR/example-public.asc&quot; \
  --export &quot;$FPR&quot;

gpg --armor \
  --output &quot;$BACKUP_DIR/example-secret-full.asc&quot; \
  --export-secret-keys &quot;$FPR&quot;

gpg --armor \
  --output &quot;$BACKUP_DIR/example-secret-subkeys.asc&quot; \
  --export-secret-subkeys &quot;$FPR&quot;

gpg --export-ownertrust \
  &amp;gt; &quot;$BACKUP_DIR/ownertrust.txt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--export-ownertrust&lt;/code&gt; 应通过 Shell 重定向保存；某些 GnuPG 版本不会把这个命令的输出写入 &lt;code&gt;--output&lt;/code&gt; 指定的文件。如果信任记录直接打印在终端里，应检查 &lt;code&gt;ownertrust.txt&lt;/code&gt; 是否真的存在。&lt;/p&gt;
&lt;p&gt;GnuPG 通常会在生成主密钥时自动创建吊销证书。把它复制到备份介质：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp &quot;$HOME/.gnupg/openpgp-revocs.d/$FPR.rev&quot; \
  &quot;$BACKUP_DIR/example-revocation-certificate.rev&quot;

chmod 600 &quot;$BACKUP_DIR&quot;/*
ls -la &quot;$BACKUP_DIR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;备份目录至少应包含：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;example-public.asc
example-secret-full.asc
example-secret-subkeys.asc
example-revocation-certificate.rev
ownertrust.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;推荐保存两份加密离线备份，并放在不同地点。私钥保护密码、卡片 PIN 和 Reset Code 不要与备份介质放在一起。&lt;/p&gt;
&lt;h3&gt;实际验证备份&lt;/h3&gt;
&lt;p&gt;“文件存在”不等于“能够恢复”。创建一个隔离的临时 GnuPG 主目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VERIFY_HOME=&quot;$(mktemp -d)&quot;
chmod 700 &quot;$VERIFY_HOME&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导入完整私钥备份：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --homedir &quot;$VERIFY_HOME&quot; \
  --import &quot;$BACKUP_DIR/example-secret-full.asc&quot;

gpg --homedir &quot;$VERIFY_HOME&quot; \
  --list-secret-keys \
  --keyid-format long \
  --with-subkey-fingerprint \
  &quot;$FPR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进行一次签名测试。不要把待签名内容通过管道传给 GnuPG，否则 pinentry 可能无法从当前终端读取保护密码，并报错 &lt;code&gt;Inappropriate ioctl for device&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export GPG_TTY=&quot;$(tty)&quot;

VERIFY_FILE=&quot;$(mktemp /tmp/openpgp-backup-verification.XXXXXX)&quot;
VERIFY_SIGNATURE=&quot;$BACKUP_DIR/$(basename &quot;$VERIFY_FILE&quot;).asc&quot;
printf &apos;OpenPGP backup verification\n&apos; &amp;gt; &quot;$VERIFY_FILE&quot;

gpg --homedir &quot;$VERIFY_HOME&quot; \
  --local-user &quot;$FPR&quot; \
  --armor \
  --clearsign \
  --output &quot;$VERIFY_SIGNATURE&quot; \
  &quot;$VERIFY_FILE&quot;

gpg --homedir &quot;$VERIFY_HOME&quot; \
  --verify &quot;$VERIFY_SIGNATURE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有看到 &lt;code&gt;Good signature&lt;/code&gt;，并确认临时钥匙环中存在 &lt;code&gt;[SC]&lt;/code&gt;、&lt;code&gt;[E]&lt;/code&gt;、&lt;code&gt;[A]&lt;/code&gt; 的完整私钥后，才能继续。临时环境中 UID 显示 &lt;code&gt;[未知]&lt;/code&gt; 只是因为没有导入 ownertrust，不表示备份损坏，也不影响签名验证。&lt;/p&gt;
&lt;h2&gt;5. 修改卡片 PIN&lt;/h2&gt;
&lt;p&gt;进入卡片编辑模式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --card-edit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;依次进入管理模式和 PIN 菜单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;admin
passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通常可以看到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 - change PIN
2 - unblock PIN
3 - change Admin PIN
4 - set the Reset Code
Q - quit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至少完成以下三项：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;修改用户 PIN；&lt;/li&gt;
&lt;li&gt;修改管理员 PIN；&lt;/li&gt;
&lt;li&gt;设置独立的 Reset Code。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;标准原厂 YubiKey 的默认用户 PIN 通常为 &lt;code&gt;123456&lt;/code&gt;，管理员 PIN 通常为 &lt;code&gt;12345678&lt;/code&gt;，但兼容设备不一定相同。以厂商文档为准，不要连续猜测，因为每类 PIN 通常只有三次重试机会。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Max. PIN lengths&lt;/code&gt; 显示的是最大长度，不是最小长度。标准 OpenPGP 卡通常要求用户 PIN 至少 6 位、管理员 PIN 和 Reset Code 至少 8 位；兼容固件还可能有更严格的字符限制。为了兼容性，本文建议使用 8 位以上的用户 PIN，以及 12～16 位 ASCII 字母和数字组成的管理员 PIN、Reset Code，不要使用 4 位数字 PIN。&lt;/p&gt;
&lt;p&gt;修改 PIN 时，pinentry 会先收集当前 PIN、新 PIN 和重复的新 PIN，最后才一次性交给卡片验证。因此，看到“输入新 PIN”的界面不代表当前 PIN 已经通过验证。如果最终提示 &lt;code&gt;Bad PIN&lt;/code&gt; 或“损坏的 PIN”，先退出并运行 &lt;code&gt;gpg --card-status&lt;/code&gt;：管理员 PIN 对应 &lt;code&gt;PIN retry counter&lt;/code&gt; 的最后一位；若计数没有减少，更可能是新 PIN 太短或格式不受支持，若计数减少则说明当前 PIN 错误。&lt;/p&gt;
&lt;p&gt;三类凭据的用途不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户 PIN：执行签名、解密和认证；&lt;/li&gt;
&lt;li&gt;管理员 PIN：修改卡片设置、写入或删除密钥；&lt;/li&gt;
&lt;li&gt;Reset Code：用户 PIN 被锁定后的恢复凭据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. 把三把私钥迁移到卡片&lt;/h2&gt;
&lt;p&gt;进入密钥编辑界面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --edit-key &quot;$FPR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;list
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;主密钥迁移到 Signature 槽&lt;/h3&gt;
&lt;p&gt;刚进入编辑界面时不要选择任何子密钥，直接执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;keytocard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;选择：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是 &lt;code&gt;Signature key&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;GnuPG 会询问“真的要移动主密钥吗”，确认后可能显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Note: the local copy of the secret key will only be deleted with &quot;save&quot;.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这表示主密钥已经写入卡片，而钥匙环中的本地副本要到最后执行 &lt;code&gt;save&lt;/code&gt; 才会移除。此时先继续迁移 &lt;code&gt;[E]&lt;/code&gt; 和 &lt;code&gt;[A]&lt;/code&gt;，不要提前保存。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;[E]&lt;/code&gt; 子密钥迁移到 Encryption 槽&lt;/h3&gt;
&lt;p&gt;如果 &lt;code&gt;[E]&lt;/code&gt; 是第一把子密钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;key 1
list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认只有 &lt;code&gt;[E]&lt;/code&gt; 子密钥前面出现 &lt;code&gt;*&lt;/code&gt;，然后执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;keytocard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;选择：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;迁移完成后取消选择：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;key 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;[A]&lt;/code&gt; 子密钥迁移到 Authentication 槽&lt;/h3&gt;
&lt;p&gt;如果 &lt;code&gt;[A]&lt;/code&gt; 是第二把子密钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;key 2
list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认只有 &lt;code&gt;[A]&lt;/code&gt; 子密钥前面出现 &lt;code&gt;*&lt;/code&gt;，然后执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;keytocard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;选择：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以再输入一次 &lt;code&gt;key 2&lt;/code&gt; 取消选择，然后运行 &lt;code&gt;list&lt;/code&gt;，确认没有误选其他子密钥，最后保存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;key 2
list
save
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
&lt;code&gt;key N&lt;/code&gt; 是切换第 N 把子密钥的选择状态，不是把选择直接切换过去；再次输入同一个命令才会取消其前面的 &lt;code&gt;*&lt;/code&gt;。如果 &lt;code&gt;[E]&lt;/code&gt; 和 &lt;code&gt;[A]&lt;/code&gt; 前面同时出现 &lt;code&gt;*&lt;/code&gt;，先对不需要的那一把再执行一次 &lt;code&gt;key N&lt;/code&gt;。子密钥编号取决于实际钥匙环，以 &lt;code&gt;[E]&lt;/code&gt; 和 &lt;code&gt;[A]&lt;/code&gt; 标志为准，不要盲目照搬编号。执行 &lt;code&gt;save&lt;/code&gt; 前再次确认卡槽选择正确。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;7. 验证卡片状态&lt;/h2&gt;
&lt;p&gt;查看 OpenPGP 卡：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --card-status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置成功后应同时出现三把指纹：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Signature key ....: &amp;lt;PRIMARY_KEY_FINGERPRINT&amp;gt;
Encryption key....: &amp;lt;ENCRYPTION_SUBKEY_FINGERPRINT&amp;gt;
Authentication key: &amp;lt;AUTH_SUBKEY_FINGERPRINT&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再查看本地钥匙环：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --list-secret-keys \
  --keyid-format long \
  --with-subkey-fingerprint \
  &quot;$FPR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预期类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sec&amp;gt;  rsa2048/... [SC]
ssb&amp;gt;  rsa2048/... [E]
ssb&amp;gt;  rsa2048/... [A]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;gt;&lt;/code&gt; 表示私钥操作由智能卡完成，本地只保留卡片引用。它不表示私钥已经被公开或可以从卡中导出。&lt;/p&gt;
&lt;h2&gt;8. 分别测试三个卡槽&lt;/h2&gt;
&lt;h3&gt;签名与验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export GPG_TTY=&quot;$(tty)&quot;

SIGN_TEST=&quot;$(mktemp /tmp/openpgp-card-signing.XXXXXX)&quot;
SIGNATURE=&quot;${SIGN_TEST}.sig.asc&quot;
printf &apos;OpenPGP smartcard signing test: %s\n&apos; \
  &quot;$(date &apos;+%Y-%m-%dT%H:%M:%S%z&apos;)&quot; \
  &amp;gt; &quot;$SIGN_TEST&quot;

gpg --local-user &quot;$FPR&quot; \
  --armor \
  --detach-sign \
  --output &quot;$SIGNATURE&quot; \
  &quot;$SIGN_TEST&quot;

gpg --verify &quot;$SIGNATURE&quot; &quot;$SIGN_TEST&quot;

printf &apos;测试文件：%s\n签名文件：%s\n&apos; \
  &quot;$SIGN_TEST&quot; &quot;$SIGNATURE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预期看到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Good signature from &quot;Example User &amp;lt;user@example.com&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;加密与解密&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;printf &apos;OpenPGP smartcard encryption test\n&apos; \
  &amp;gt; /tmp/openpgp-plaintext.txt

gpg --armor \
  --recipient &quot;$FPR&quot; \
  --output /tmp/openpgp-encrypted.asc \
  --encrypt /tmp/openpgp-plaintext.txt

gpg --decrypt /tmp/openpgp-encrypted.asc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加密只需要公钥，因此拔掉卡片仍可加密；解密必须使用卡片中的 &lt;code&gt;[E]&lt;/code&gt; 私钥。可以拔卡后再尝试解密，确认操作确实依赖设备。&lt;/p&gt;
&lt;h3&gt;导出 SSH 公钥&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;gpg --export-ssh-key &quot;$FPR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应得到类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-rsa AAAA... openpgp:0x...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这只能证明认证子密钥可以转换为 SSH 公钥。完整 SSH 登录配置见后文。&lt;/p&gt;
&lt;h2&gt;9. 配置 Git commit 签名&lt;/h2&gt;
&lt;p&gt;让 Git 使用这把 OpenPGP 密钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global user.signingkey &quot;$FPR&quot;
git config --global commit.gpgsign true
git config --global tag.gpgSign true
git config --global gpg.program &quot;$(command -v gpg)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进行一次本地测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir /tmp/gpg-git-sign-test
cd /tmp/gpg-git-sign-test
git init
git commit --allow-empty -m &quot;test OpenPGP signing&quot;
git log --show-signature -1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要让 GitHub 显示 &lt;code&gt;Verified&lt;/code&gt;，还需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把 &lt;code&gt;user@example.com&lt;/code&gt; 添加到 GitHub 账号并完成验证；&lt;/li&gt;
&lt;li&gt;在 GitHub 的 SSH and GPG keys 页面添加 ASCII 公钥；&lt;/li&gt;
&lt;li&gt;commit 的作者邮箱必须属于该账号。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;导出可粘贴到 GitHub 的公钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --armor --export &quot;$FPR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10. 把认证子密钥用于 SSH&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;~/.gnupg/gpg-agent.conf&lt;/code&gt; 中加入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enable-ssh-support
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启 gpg-agent：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpgconf --kill gpg-agent
gpgconf --launch gpg-agent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当前终端指向 GnuPG 提供的 SSH agent socket：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export SSH_AUTH_SOCK=&quot;$(gpgconf --list-dirs agent-ssh-socket)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把同一行写入 &lt;code&gt;~/.zshrc&lt;/code&gt; 或 &lt;code&gt;~/.bashrc&lt;/code&gt;，然后重新打开终端。检查 agent 是否识别卡片：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-add -L
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以直接导出公钥保存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --export-ssh-key &quot;$FPR&quot; &amp;gt; ~/.ssh/openpgp-backup.pub
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把这行公钥添加到服务器的 &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; 或 GitHub 的 SSH keys 页面，再执行实际登录测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -T git@github.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拔掉卡后再次连接，应当无法使用这把私钥完成认证。&lt;/p&gt;
&lt;h2&gt;11. 发布公钥与指纹&lt;/h2&gt;
&lt;p&gt;导出公开版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --armor \
  --output example-public.asc \
  --export &quot;$FPR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以把它发布到个人网站，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://example.com/pgp.asc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个地址必须直接返回 &lt;code&gt;example-public.asc&lt;/code&gt; 的 ASCII 公钥内容。OpenPGP 卡的公钥 URL 是单个字符串字段，只支持一个 URL；如果需要多个镜像，应使用一个长期稳定的地址，再由服务器通过 HTTP 重定向到当前镜像。&lt;/p&gt;
&lt;p&gt;设置卡片上的公钥 URL：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --card-edit
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;admin
url
https://example.com/pgp.asc
quit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上传后检查服务器返回的指纹：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL https://example.com/pgp.asc |
  gpg --show-keys --fingerprint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只能公开 &lt;code&gt;example-public.asc&lt;/code&gt;。不要上传整个备份目录，也不要公开 &lt;code&gt;example-secret-full.asc&lt;/code&gt;、&lt;code&gt;example-secret-subkeys.asc&lt;/code&gt;、吊销证书或 &lt;code&gt;ownertrust.txt&lt;/code&gt;；备份签名测试文件也不是公钥。&lt;/p&gt;
&lt;p&gt;完整指纹应通过另一个独立渠道公开，供别人核对：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;PRIMARY_KEY_FINGERPRINT&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还可以把公钥上传到支持的密钥服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --keyserver hkps://keys.openpgp.org \
  --send-keys &quot;$FPR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;密钥服务器上的记录通常无法像普通文件一样彻底删除，只能通过吊销来声明失效。上传前应确认 UID 中的邮箱适合长期公开；使用 keys.openpgp.org 时，还需要按邮件完成 UID 验证，其他人才可按邮箱搜索到它。&lt;/p&gt;
&lt;h2&gt;12. 可选的卡片安全设置&lt;/h2&gt;
&lt;h3&gt;每次签名都重新验证 PIN&lt;/h3&gt;
&lt;p&gt;当前状态如果显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Signature PIN ....: 非强制
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示一次成功验证后，同一会话中的后续签名可能不再要求 PIN。重要发布密钥可以切换为强制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --card-edit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;admin
forcesig
quit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;forcesig&lt;/code&gt; 是切换命令，再执行一次会恢复原状态。频繁签 Git commit 时，非强制更方便；签软件发布包或重要文件时，强制更稳妥。&lt;/p&gt;
&lt;h3&gt;物理触摸确认 UIF&lt;/h3&gt;
&lt;p&gt;如果设备明确支持触摸确认，可以先单独测试签名槽：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --card-edit
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;admin
uif 1 on
quit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认签名能在触摸后成功，再按需启用解密和认证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uif 2 on
uif 3 on
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应关系为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UIF 1 = Sign
UIF 2 = Decrypt
UIF 3 = Auth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启用 &lt;code&gt;Sign=on&lt;/code&gt; 后，签名流程通常仍会先要求用户 PIN；PIN 窗口关闭后，GnuPG 可能只是在终端中静默等待，并不会再弹出“请按键”窗口。此时直接触摸设备即可。可以故意暂时不触摸：若签名命令持续等待，说明 UIF 正在生效；若签名立即完成，先确认 &lt;code&gt;gpg --list-secret-keys&lt;/code&gt; 中签名主密钥显示为 &lt;code&gt;sec&amp;gt;&lt;/code&gt;。如果确实是 &lt;code&gt;sec&amp;gt;&lt;/code&gt; 且无需触摸也能签名，说明兼容固件虽然报告 &lt;code&gt;Sign=on&lt;/code&gt;，但没有实际执行触摸策略。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
兼容卡未必实现了与原厂 YubiKey 相同的触摸行为。先用 &lt;code&gt;on&lt;/code&gt; 测试，不要直接设为 &lt;code&gt;permanent&lt;/code&gt;；永久模式通常无法关闭，只能重置 OpenPGP 应用并清空密钥。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;KDF&lt;/h3&gt;
&lt;p&gt;OpenPGP Card KDF 会先在客户端对 PIN 进行加盐和迭代派生，再把结果交给卡片。它可以强化 PIN 处理，但要求所有客户端和固件都正确支持。&lt;/p&gt;
&lt;p&gt;当前兼容设备的状态是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;KDF setting ......: off
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在固件来源、旧客户端兼容性和恢复流程都没有验证之前，建议保持关闭。KDF 不是越多越好的通用开关，错误启用可能导致旧设备无法验证 PIN。&lt;/p&gt;
&lt;h3&gt;卡片元数据&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;name&lt;/code&gt;、&lt;code&gt;lang&lt;/code&gt;、&lt;code&gt;login&lt;/code&gt; 和公钥 URL 都只是元数据，不决定密钥安全。可以在以下界面按需设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --card-edit
admin
name
lang
login
url
quit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有当 ASCII 公钥已经长期托管在稳定的 HTTPS 地址时，才建议填写 URL。URL 指向的只能是公钥，绝不能是私钥备份。&lt;/p&gt;
&lt;h2&gt;13. 在新电脑上恢复使用&lt;/h2&gt;
&lt;p&gt;新电脑不需要导入完整私钥，只需导入公钥并让 GnuPG 学习卡片：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --import example-public.asc
gpg --card-status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果本地没有正确建立卡片引用，可以执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg-connect-agent &quot;scd serialno&quot; &quot;learn --force&quot; /bye
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重新检查：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --list-secret-keys \
  --keyid-format long \
  --with-subkey-fingerprint \
  &quot;$FPR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要 SSH 时，再配置 &lt;code&gt;enable-ssh-support&lt;/code&gt; 和 &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt;。不要为了日常使用而把完整私钥备份导入联网电脑；那会抵消智能卡隔离私钥的主要价值。&lt;/p&gt;
&lt;h2&gt;14. 卡片丢失、损坏或 PIN 锁定&lt;/h2&gt;
&lt;h3&gt;用户 PIN 锁定&lt;/h3&gt;
&lt;p&gt;如果用户 PIN 的重试次数降到 0，可以使用事先设置的 Reset Code 或管理员 PIN 解锁。不要继续猜测，以免管理员 PIN 也被锁定。&lt;/p&gt;
&lt;h3&gt;卡片损坏但身份仍需继续使用&lt;/h3&gt;
&lt;p&gt;从经过验证的完整私钥备份恢复到隔离环境，准备一枚新卡，再将密钥写入新卡。因为原 OpenPGP 私钥并没有从旧卡中导出，而是从迁移前的备份重新获得，所以备份是整个恢复流程的唯一可靠基础。&lt;/p&gt;
&lt;h3&gt;卡片丢失或怀疑已被复制使用&lt;/h3&gt;
&lt;p&gt;如果无法确认卡片仍在自己控制下，应使用离线保存的吊销证书吊销主密钥，并尽快发布更新后的公钥状态。吊销代表整套身份失效；如果只是轮换某一把子密钥，则需要使用认证主密钥吊销旧子密钥、添加新子密钥并重新发布公钥。&lt;/p&gt;
&lt;p&gt;导入自动生成的吊销证书前，应先阅读文件头部的说明。GnuPG 生成的 &lt;code&gt;.rev&lt;/code&gt; 文件通常会在 Armor 开头故意加入一个冒号，防止误导入；正式使用时需要按文件内说明去掉该字符，再导入并发布吊销后的公钥。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
吊销通常不可逆。只有在密钥确实需要作废时才执行，不要把“测试吊销证书”放在真实钥匙环或公开密钥服务器上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;15. 日常检查清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;定期执行 &lt;code&gt;gpg --card-status&lt;/code&gt;，确认三个卡槽指纹和 PIN 重试次数正常；&lt;/li&gt;
&lt;li&gt;每次修改 UID、有效期或子密钥后，重新导出并发布公钥；&lt;/li&gt;
&lt;li&gt;至少每年检查两份离线备份是否仍可读取；&lt;/li&gt;
&lt;li&gt;保持 GnuPG、scdaemon、pinentry 和设备固件更新；&lt;/li&gt;
&lt;li&gt;不把私钥备份、保护密码、PIN 与 Reset Code 放在同一位置；&lt;/li&gt;
&lt;li&gt;保留一枚备用兼容卡，或至少记录经过验证的换卡流程；&lt;/li&gt;
&lt;li&gt;“永不过期”不等于永远安全，定期重新评估算法和设备寿命。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;验收结果&lt;/h2&gt;
&lt;p&gt;完成配置后，应得到类似以下结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;主密钥 [SC]
&amp;lt;PRIMARY_KEY_FINGERPRINT&amp;gt;

加密子密钥 [E]
&amp;lt;ENCRYPTION_SUBKEY_FINGERPRINT&amp;gt;

认证子密钥 [A]
&amp;lt;AUTH_SUBKEY_FINGERPRINT&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仅看到三把指纹还不算完成。最终验收应同时满足：离线备份已在隔离钥匙环中成功导入并签名，签名与解密均需要卡片，SSH 已完成一次真实登录，&lt;code&gt;sec&amp;gt;&lt;/code&gt; / &lt;code&gt;ssb&amp;gt;&lt;/code&gt; 显示本地只保留卡片引用，公钥和完整指纹也已发布到稳定位置。可选安全开关应在这些基础流程通过后再逐项启用。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnupg.org/documentation/manuals/gnupg/Smart-Card-Tool.html&quot;&gt;GnuPG Manual：Smart Card Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Card.html&quot;&gt;GnuPG Manual：OpenPGP Card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html&quot;&gt;GnuPG Manual：GPG Configuration Options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.yubico.com/PGP/&quot;&gt;Yubico OpenPGP 文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/zh/authentication/managing-commit-signature-verification&quot;&gt;GitHub：管理 commit 签名验证&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://keys.openpgp.org/about/usage&quot;&gt;keys.openpgp.org 使用说明&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>从 giffgaff 转入 VOXI：eSIM 激活、APN 排障与保号说明</title><link>https://pinellia.uk/posts/giffgaff-to-voxi/</link><guid isPermaLink="true">https://pinellia.uk/posts/giffgaff-to-voxi/</guid><description>整理 VOXI eSIM 开通、iPhone 无服务时的 APN 排障、giffgaff PAC 携号转入，以及套餐取消和长期保号时需要留意的风险。</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近 giffgaff 账号异常、封停的讨论变多，不少人开始把英国号码转到 VOXI。论坛里已经有开卡、eSIM 激活和携号转入的成功案例，但相关信息散落在不同帖子中，而且 APN、PAYG 和保号规则的说法并不完全一致。&lt;/p&gt;
&lt;p&gt;下面按实际操作顺序整理：先开通 VOXI eSIM，处理可能出现的无服务问题，再使用 PAC 将 giffgaff 号码转入 VOXI。文中的时效和排障参数来自社区实测，不代表运营商承诺；正式操作前仍应以 VOXI 和 giffgaff 当时显示的规则为准。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
携号转入成功后，VOXI 开卡时分配的新号码会被原 giffgaff 号码覆盖。转号期间还可能暂时收不到电话和短信，不要把银行、邮箱等关键验证码操作安排在同一天。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;开始前需要准备什么&lt;/h2&gt;
&lt;p&gt;至少确认以下条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;手机支持 eSIM，且没有运营商网络锁；&lt;/li&gt;
&lt;li&gt;有一个可以正常收信的邮箱；&lt;/li&gt;
&lt;li&gt;有 VOXI 接受的付款方式，账单资料与付款信息保持真实、匹配；&lt;/li&gt;
&lt;li&gt;仍能登录 giffgaff，或至少能在账号受限后的可操作期限内申请 PAC；&lt;/li&gt;
&lt;li&gt;VOXI eSIM 已经能够连接网络或接收短信，再开始携号转入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;教育优惠码、首月减免等活动可能随时变化，并不是携号转入的必要条件。论坛里出现的代购和优惠码属于第三方交易，本文不作推荐。&lt;/p&gt;
&lt;h2&gt;第一步：开通并写入 VOXI eSIM&lt;/h2&gt;
&lt;p&gt;在 VOXI 官网选择 eSIM 套餐，按页面要求填写资料并完成付款。社区帖子提到，订单提交后通常几分钟就能在邮箱中收到 eSIM 邮件，附件是一份带二维码的 PDF。&lt;/p&gt;
&lt;p&gt;开通过程中有几个细节容易踩坑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;尽量一次完成订单，不要在支付阶段反复返回、刷新或重复提交；&lt;/li&gt;
&lt;li&gt;不要开启浏览器的整页翻译，部分用户遇到过翻译扩展改变表单参数的问题；&lt;/li&gt;
&lt;li&gt;记住开户时填写的出生日期，社区案例中它会用于解锁 eSIM PDF；&lt;/li&gt;
&lt;li&gt;使用真实的个人、地址和付款资料，不要照搬论坛中的随机地址做法；&lt;/li&gt;
&lt;li&gt;二维码通常只能安全写入一次，确认要安装的设备后再扫描。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 iPhone 上打开“设置 → 蜂窝网络 → 添加 eSIM”，扫描 PDF 中的二维码并按提示完成安装。安装后先等待一段时间，社区案例中从写入到收到网络欢迎短信约需 30 分钟，但实际时间可能更长。&lt;/p&gt;
&lt;p&gt;如果需要确认 VOXI 分配的新号码，可以登录 VOXI 账户查看。登录过程可能向 eSIM 发送一次性验证码；有信号后也可以尝试拨打：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*#100#
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;eSIM 写入后一直无服务怎么办&lt;/h2&gt;
&lt;p&gt;先完成最基础的检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确认 VOXI 线路已经启用；&lt;/li&gt;
&lt;li&gt;开关一次飞行模式，并等待 1～2 分钟；&lt;/li&gt;
&lt;li&gt;重启手机；&lt;/li&gt;
&lt;li&gt;暂时关闭 VPN 或代理；&lt;/li&gt;
&lt;li&gt;在“网络选择”中恢复自动选择。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;仍然无服务时，可以尝试修改 APN。现有帖子给出了两套不同的成功参数，它们来自不同设备或 eSIM 配置，不能视为 VOXI 的统一官方设置。&lt;/p&gt;
&lt;h3&gt;方案 A：只修改 LTE APN&lt;/h3&gt;
&lt;p&gt;奶昔论坛的 iPhone 成功案例只修改了“LTE 设置（选填）”：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;APN: 3internet
用户名: 留空
密码: 留空
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;路径为“设置 → 蜂窝网络 → VOXI 线路 → 蜂窝数据网络”。保存后开关一次飞行模式，再观察能否完成激活。&lt;/p&gt;
&lt;h3&gt;方案 B：填写 Talkmobile APN&lt;/h3&gt;
&lt;p&gt;另一篇 NodeSeek 帖子给出的方案是将可见的 APN 项（包括 LTE 选填项）改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;APN: payg.talkmobile.uk
用户名: wap
密码: wap
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样需要在修改后开关飞行模式并等待一会儿。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
两套参数不要混填。建议先记录原值，每次只测试一套；无效就恢复原设置，再尝试下一套。APN 主要影响数据注册和联网，出现信号并不等于语音、短信或 Wi-Fi Calling 一定可用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第二步：从 giffgaff 获取 PAC&lt;/h2&gt;
&lt;p&gt;PAC（Porting Authorisation Code）是英国手机号携号转网所需的授权码。先在 giffgaff 账户中申请 PAC，再到 VOXI 提交转入。&lt;/p&gt;
&lt;p&gt;原帖特别提醒：如果 giffgaff 账号已经被封，应尽快处理，并称需要在封停后的 14 天内申请。这里的“14 天”是帖子针对受限账号给出的经验窗口，不要把它和 PAC 自身的有效期混为一谈；页面显示的截止日期才是最终依据。&lt;/p&gt;
&lt;p&gt;申请后先保存好以下信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要转入的 giffgaff 手机号；&lt;/li&gt;
&lt;li&gt;PAC；&lt;/li&gt;
&lt;li&gt;PAC 的到期时间；&lt;/li&gt;
&lt;li&gt;giffgaff 和 VOXI 两边的账户资料。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第三步：在 VOXI 提交携号转入&lt;/h2&gt;
&lt;p&gt;打开 VOXI 官方转号页面：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://voxi.co.uk/account/settings/transfer/confirmation&quot;&gt;https://voxi.co.uk/account/settings/transfer/confirmation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;该链接会先要求登录 VOXI 账户。登录后按照页面要求填写原号码、PAC 和计划转入日期，再确认提交。不要在 VOXI eSIM 还无法收短信时急着转号，因为登录和确认过程可能需要一次性验证码。&lt;/p&gt;
&lt;p&gt;转号完成后通常会收到确认短信，同时发生两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;原 giffgaff 号码转到 VOXI；&lt;/li&gt;
&lt;li&gt;VOXI 开卡时临时分配的号码被覆盖，不再继续保留。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;提交后如果暂时失去服务，先等待运营商完成切换，再重启手机或开关飞行模式。不要因为短时间无信号就重复提交 PAC。&lt;/p&gt;
&lt;h2&gt;套餐取消、PAYG 与保号&lt;/h2&gt;
&lt;p&gt;这部分是社区讨论中分歧最大的地方。&lt;/p&gt;
&lt;p&gt;有成功案例称，取消 VOXI 的月度 Plan 后会转为按量付费，并以“180 天内产生一次余额变化”作为保号依据；但也有用户从客服处得到“只有实体卡能够转 PAYG，且需要联系客服”的答复。eSIM 能否直接取消套餐并进入 PAYG，不能仅凭一个案例下结论。&lt;/p&gt;
&lt;p&gt;更稳妥的做法是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 VOXI 账户中确认取消套餐后的明确状态；&lt;/li&gt;
&lt;li&gt;如果页面没有显示 PAYG，直接联系 VOXI 客服确认 eSIM 是否支持转换；&lt;/li&gt;
&lt;li&gt;免费月或当前套餐结束后保留少量余额；&lt;/li&gt;
&lt;li&gt;不要卡在第 179 天才充值或消费，提前完成一次可计费活动；&lt;/li&gt;
&lt;li&gt;确认该活动确实改变了余额，而不只是手机显示“短信已发送”。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;论坛中有人提醒，零余额号码可能在 180 天之前就被回收。因此，“180 天余额变化”更适合作为最长检查周期，而不是可以放心拖到最后一天的保证。&lt;/p&gt;
&lt;p&gt;在中国大陆长期使用时，保号通常还依赖短信或通话能否正常走 Wi-Fi Calling。仅修改 APN 获得 4G/5G 信号，并不能证明可以发短信完成余额变动。&lt;/p&gt;
&lt;h2&gt;长期漫游风险&lt;/h2&gt;
&lt;p&gt;VOXI 本质上是面向英国用户的移动服务，社区也提到其条款不鼓励长期漫游。当前能够在境外激活、收短信或维持一段时间，不代表以后不会触发风控。&lt;/p&gt;
&lt;p&gt;如果这个号码绑定了重要账户，至少准备以下备份：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为银行、邮箱和双重验证预留第二种恢复方式；&lt;/li&gt;
&lt;li&gt;不把 VOXI 作为唯一验证码号码；&lt;/li&gt;
&lt;li&gt;定期登录账户检查余额、套餐和号码状态；&lt;/li&gt;
&lt;li&gt;保存开户资料、PAC 记录和客服沟通记录；&lt;/li&gt;
&lt;li&gt;关注 VOXI 对漫游、PAYG 和停机回收规则的调整。&lt;/li&gt;
&lt;/ul&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;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;td&gt;重复扫描 eSIM 二维码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;有信号但不能上网&lt;/td&gt;
&lt;td&gt;记录原值后逐一测试 APN&lt;/td&gt;
&lt;td&gt;混用两套 APN 参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;有数据但不能发短信&lt;/td&gt;
&lt;td&gt;Wi-Fi Calling、账户余额、线路能力&lt;/td&gt;
&lt;td&gt;认为 APN 能解决所有语音短信问题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VOXI 登录失败&lt;/td&gt;
&lt;td&gt;eSIM 是否能收一次性验证码&lt;/td&gt;
&lt;td&gt;反复提交转号申请&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;转号后 VOXI 新号消失&lt;/td&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;/td&gt;
&lt;td&gt;默认 eSIM 一定会自动转 PAYG&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nodeseek.com/post-847659-1&quot;&gt;VOXI eSIM 开通、扫码和 APN 排障讨论&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://forum.naixi.net/thread-12580-1-1.html&quot;&gt;VOXI iPhone 激活与 &lt;code&gt;3internet&lt;/code&gt; APN 实例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.nodeseek.com/post-847066-1&quot;&gt;giffgaff 成功转入 VOXI 的社区记录&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://voxi.co.uk/account/settings/transfer/confirmation&quot;&gt;VOXI 官方携号转入入口&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
本文整理于 2026 年 8 月。论坛帖子能够证明“有人按此方法成功”，不能证明运营商长期支持同样的套餐、漫游或保号策略。涉及重要号码时，以账户页面和客服书面答复为准。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>不安装 Android Studio 也能开发 Android</title><link>https://pinellia.uk/posts/android-development-without-android-studio/</link><guid isPermaLink="true">https://pinellia.uk/posts/android-development-without-android-studio/</guid><description>使用 JDK、Android SDK Command-line Tools、Gradle Wrapper 和 ADB，完成 Android 项目的构建、测试与真机安装。</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Android Studio 不是 Android 开发的必需品。真正参与构建的只有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDK；&lt;/li&gt;
&lt;li&gt;Android SDK；&lt;/li&gt;
&lt;li&gt;Gradle Wrapper；&lt;/li&gt;
&lt;li&gt;ADB。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;编辑代码可以使用 VS Code、Neovim 或其他文本编辑器。下面以 Kotlin 原生 Android 项目为例，使用项目内工具链完成测试、打包和真机安装。&lt;/p&gt;
&lt;h2&gt;准备环境&lt;/h2&gt;
&lt;p&gt;以 &lt;code&gt;compileSdk = 35&lt;/code&gt; 的项目为例，需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JDK 17；&lt;/li&gt;
&lt;li&gt;Android SDK Command-line Tools；&lt;/li&gt;
&lt;li&gt;Platform 35；&lt;/li&gt;
&lt;li&gt;Build-Tools 35.0.0；&lt;/li&gt;
&lt;li&gt;Platform-Tools。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下载 Command-line Tools 后，将其放入项目目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.android-toolchain/sdk/cmdline-tools/latest/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后安装 SDK 组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export ANDROID_SDK_ROOT=&quot;$PWD/.android-toolchain/sdk&quot;
export ANDROID_HOME=&quot;$ANDROID_SDK_ROOT&quot;
export ANDROID_USER_HOME=&quot;$PWD/.android-user-home&quot;
export GRADLE_USER_HOME=&quot;$PWD/.gradle-user-home&quot;

SDKMANAGER=&quot;$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager&quot;

yes | &quot;$SDKMANAGER&quot; --sdk_root=&quot;$ANDROID_SDK_ROOT&quot; --licenses
&quot;$SDKMANAGER&quot; --sdk_root=&quot;$ANDROID_SDK_ROOT&quot; \
  &quot;platforms;android-35&quot; \
  &quot;build-tools;35.0.0&quot; \
  &quot;platform-tools&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在项目根目录创建不提交到 Git 的 &lt;code&gt;local.properties&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sdk.dir=/absolute/path/to/project/.android-toolchain/sdk
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;构建与测试&lt;/h2&gt;
&lt;p&gt;项目应提交 Gradle Wrapper，因此无需全局安装 Gradle：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./gradlew testDebugUnitTest assembleDebug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成的 Debug APK 位于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app/build/outputs/apk/debug/app-debug.apk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只需要打包时执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./gradlew assembleDebug
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装到真机&lt;/h2&gt;
&lt;p&gt;在手机上启用“开发者选项”和“USB 调试”，连接后确认设备已授权：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.android-toolchain/sdk/platform-tools/adb devices -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;覆盖安装 APK：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.android-toolchain/sdk/platform-tools/adb install -r \
  app/build/outputs/apk/debug/app-debug.apk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到 &lt;code&gt;Success&lt;/code&gt; 即安装完成。&lt;/p&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;将 &lt;code&gt;.android-toolchain/&lt;/code&gt;、&lt;code&gt;.android-user-home/&lt;/code&gt;、&lt;code&gt;.gradle-user-home/&lt;/code&gt;、&lt;code&gt;local.properties&lt;/code&gt; 和构建目录加入 &lt;code&gt;.gitignore&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Debug APK 不需要额外签名；发布 APK 或 AAB 时，仍需使用 &lt;code&gt;keytool&lt;/code&gt; 创建 keystore 并配置 Release 签名。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完成测试、APK 构建和真机安装后，这套命令行工具链就已验证可用。Android Studio 仍可用于布局预览和性能分析，但不是构建项目的前置条件。&lt;/p&gt;
</content:encoded></item><item><title>远程服务器 Codex 使用本地 AiMaMi 中转服务指南</title><link>https://pinellia.uk/posts/codex-router-switching/</link><guid isPermaLink="true">https://pinellia.uk/posts/codex-router-switching/</guid><description>在远程服务器配置 Codex，通过 SSH 反向隧道使用本地 AiMaMi 中转服务，并在官方模型与智能路由之间安全切换。</description><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我的实际场景是：Codex 运行在远程服务器上，而 AiMaMi 中转服务运行在本地电脑。为了让服务器上的 Codex 使用本地中转，需要先通过 SSH 反向隧道连接两端，再在服务器上配置 Provider 和模型切换方式。&lt;/p&gt;
&lt;p&gt;完成后，远程服务器上的 Codex 既可以使用官方模型，也可以按需切换到本地 AiMaMi 智能路由，不必每次手工修改 &lt;code&gt;~/.codex/config.toml&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;以 AiMaMi 智能路由为例，可以选择两条路线：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 Codex Profile，在启动 CLI 时选择配置层；&lt;/li&gt;
&lt;li&gt;使用 Shell 脚本，原地启用或禁用全局配置块，并按需关闭 &lt;code&gt;app-server&lt;/code&gt; 使配置生效。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;两条路线解决的是同一个问题，但适用范围不同：Profile 更干净，适合 CLI；脚本会修改全局配置，更适合 Codex 桌面 App 或常驻 &lt;code&gt;app-server&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
不要使用 &lt;code&gt;cc-switch-cli&lt;/code&gt; 切换本文涉及的 Codex 配置。它可能改写或接管配置状态，导致不同 Codex 进程读取到不一致的配置，最终出现会话不同步。本文只使用 Codex 原生 Profile 或定向开关脚本；会话记录同步则单独使用 &lt;code&gt;codex-provider-sync&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;配置结构与切换边界&lt;/h2&gt;
&lt;p&gt;智能路由的开关配置位于 &lt;code&gt;~/.codex/config.toml&lt;/code&gt; 顶部：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# &amp;gt;&amp;gt;&amp;gt; aimami-relay codex-router top start (DO NOT EDIT MANUALLY)
# Codex 智能路由：使用 HTTP-only 自定义 provider，避开内建 openai WebSocket 路径
# 菜单模型来自 model_catalog_json；官方 / 中转分流由本地 proxy 决定
# 在 AiMaMi 里关闭智能路由开关后，本段会被自动移除
model_provider = &quot;aimai1&quot;
model_catalog_json = &quot;/sdc/home/&amp;lt;username&amp;gt;/.codex/model-catalogs/codex_router_catalog.json&quot;
# &amp;lt;&amp;lt;&amp;lt; aimami-relay codex-router top end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体的自定义 Provider 可以继续放在公共配置中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[model_providers.aimai1]
name = &quot;AiMaMi 智能路由&quot;
base_url = &quot;http://127.0.0.1:25123/codex/router/v1&quot;
wire_api = &quot;responses&quot;
requires_openai_auth = true
supports_websockets = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里要区分两类配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[model_providers.aimai1]&lt;/code&gt; 只是定义 Provider，可以长期保留；&lt;/li&gt;
&lt;li&gt;顶层的 &lt;code&gt;model_provider&lt;/code&gt; 和 &lt;code&gt;model_catalog_json&lt;/code&gt; 决定本次使用哪个 Provider 和模型目录，是实际需要切换的部分。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;code&gt;config.toml&lt;/code&gt; 是 TOML 文件，不是 Shell 脚本。&lt;code&gt;model_catalog_json = &quot;$HOME/...&quot;&lt;/code&gt; 或 &lt;code&gt;model_catalog_json = &quot;$CODEX_HOME/...&quot;&lt;/code&gt; 通常不会展开环境变量，应填写真实绝对路径。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;在本机单独运行 SSH 反向转发&lt;/h2&gt;
&lt;p&gt;在服务器上的 Codex 访问本地 AiMaMi 路由之前，需要把服务器的 &lt;code&gt;127.0.0.1:25123&lt;/code&gt; 反向转发到本机的 &lt;code&gt;127.0.0.1:25817&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;服务器 Codex
    ↓ 127.0.0.1:25123
SSH 反向隧道
    ↓
本机 AiMaMi：127.0.0.1:25817
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当前环境无法依赖 SSH Config 自动建立这条转发，因此更推荐在运行 AiMaMi 的本机单独打开一个终端，直接执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -N -R 127.0.0.1:25123:127.0.0.1:25817 服务器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数含义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-N&lt;/code&gt;：只建立隧道，不在服务器执行远程命令；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-R&lt;/code&gt;：创建反向端口转发；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;127.0.0.1:25123&lt;/code&gt;：服务器侧监听地址；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;127.0.0.1:25817&lt;/code&gt;：本机 AiMaMi 服务地址；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;服务器&lt;/code&gt;：SSH 主机名、别名或 &lt;code&gt;user@host&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个终端只负责维持隧道，不要关闭，也不需要在其中执行其他命令。终端断开或 SSH 进程退出后，反向转发会立即失效；此时服务器上的 Codex 也就无法继续访问本地 AiMaMi。需要恢复时，重新打开一个终端并再次运行该命令即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
我当前无法通过 &lt;code&gt;~/.ssh/config&lt;/code&gt; 中的 &lt;code&gt;RemoteForward&lt;/code&gt; 正常使用这条隧道，所以本文不再把 SSH Config 作为可用方案。主机地址、用户名、密钥等常规连接信息仍可由 SSH Config 提供，但反向转发本身使用命令行中的 &lt;code&gt;-R&lt;/code&gt; 显式创建。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果服务器上的 AiMaMi Provider 通过该反向隧道连接本机，对应地址应使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;base_url = &quot;http://127.0.0.1:25123/codex/router/v1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本机直接访问 AiMaMi 时则使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;base_url = &quot;http://127.0.0.1:25817/codex/router/v1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;建立隧道之后，再从下面两种 Codex 配置切换方案中选择一种。&lt;/p&gt;
&lt;h2&gt;方案 A：使用 Codex Profile&lt;/h2&gt;
&lt;h3&gt;Profile 如何工作&lt;/h3&gt;
&lt;p&gt;Profile 是 Codex 启动时加载的配置覆盖层。执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;codex --profile aimami-router
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Codex 会先读取：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.codex/config.toml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再叠加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.codex/aimami-router.config.toml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，公共配置保留在主配置中，只有存在差异的字段才需要写进 Profile。&lt;/p&gt;
&lt;p&gt;从 Codex 0.134.0 开始，每个 Profile 应使用独立文件，并通过 &lt;code&gt;--profile&lt;/code&gt; 指定，不再推荐写成 &lt;code&gt;config.toml&lt;/code&gt; 中的 &lt;code&gt;[profiles.xxx]&lt;/code&gt; 表，也不能通过顶层 &lt;code&gt;profile = &quot;xxx&quot;&lt;/code&gt; 自动选择。&lt;/p&gt;
&lt;h3&gt;调整主配置&lt;/h3&gt;
&lt;p&gt;从 &lt;code&gt;~/.codex/config.toml&lt;/code&gt; 顶部移除以下两个选择项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model_provider = &quot;aimai1&quot;
model_catalog_json = &quot;/sdc/home/&amp;lt;username&amp;gt;/.codex/model-catalogs/codex_router_catalog.json&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Provider 定义仍然保留：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[model_providers.aimai1]
name = &quot;AiMaMi 智能路由&quot;
base_url = &quot;http://127.0.0.1:25123/codex/router/v1&quot;
wire_api = &quot;responses&quot;
requires_openai_auth = true
supports_websockets = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样主配置只保存公共设置，不再强制所有 Codex 实例经过智能路由。&lt;/p&gt;
&lt;h3&gt;创建官方模型 Profile&lt;/h3&gt;
&lt;p&gt;创建 &lt;code&gt;~/.codex/official.config.toml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model_provider = &quot;openai&quot;
model = &quot;gpt-5.6-sol&quot;
model_reasoning_effort = &quot;high&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;交互式启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;codex --profile official
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;非交互任务也可以使用相同 Profile：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;codex exec --profile official &quot;检查当前代码改动&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建智能路由 Profile&lt;/h3&gt;
&lt;p&gt;创建 &lt;code&gt;~/.codex/aimami-router.config.toml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model_provider = &quot;aimai1&quot;
model_catalog_json = &quot;/sdc/home/&amp;lt;username&amp;gt;/.codex/model-catalogs/codex_router_catalog.json&quot;
model = &quot;gpt-5.6-sol&quot;
model_reasoning_effort = &quot;high&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动方式相同：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;codex --profile aimami-router
codex exec --profile aimami-router &quot;检查当前代码改动&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;添加便捷别名&lt;/h3&gt;
&lt;p&gt;可以在 &lt;code&gt;~/.bashrc&lt;/code&gt; 或 &lt;code&gt;~/.zshrc&lt;/code&gt; 中加入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alias codex-official=&apos;codex --profile official&apos;
alias codex-router=&apos;codex --profile aimami-router&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以后直接执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;codex-official
codex-router
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Profile 的优缺点&lt;/h3&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不需要反复修改主配置；&lt;/li&gt;
&lt;li&gt;不同终端可以同时运行不同 Profile；&lt;/li&gt;
&lt;li&gt;切换只影响新启动的 Codex 进程；&lt;/li&gt;
&lt;li&gt;适合 CLI 和 &lt;code&gt;codex exec&lt;/code&gt; 自动化任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Profile 是 CLI 启动参数，不等于修改桌面 App 的全局配置；&lt;/li&gt;
&lt;li&gt;已经运行的 Codex 进程不会自动切换；&lt;/li&gt;
&lt;li&gt;每次启动必须指定 &lt;code&gt;--profile&lt;/code&gt;，或者使用别名封装。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;方案 B：使用脚本切换全局配置&lt;/h2&gt;
&lt;p&gt;如果主要使用 Codex 桌面 App，或者希望所有新会话统一切换到同一条路由，可以直接修改全局 &lt;code&gt;config.toml&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里不删除配置块，而是给块内有效 TOML 行添加专用注释：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# aimami-router-disabled: model_provider = &quot;aimai1&quot;
# aimami-router-disabled: model_catalog_json = &quot;/sdc/home/&amp;lt;username&amp;gt;/.codex/model-catalogs/codex_router_catalog.json&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新开启时，脚本只移除 &lt;code&gt;# aimami-router-disabled: &lt;/code&gt; 前缀。原有说明注释、块标记和块外配置都不会被修改。&lt;/p&gt;
&lt;h3&gt;完整 Linux 脚本&lt;/h3&gt;
&lt;p&gt;将下面的内容保存为 &lt;code&gt;~/codex-switch.sh&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh

set -eu

START_MARKER=&apos;# &amp;gt;&amp;gt;&amp;gt; aimami-relay codex-router top start (DO NOT EDIT MANUALLY)&apos;
END_MARKER=&apos;# &amp;lt;&amp;lt;&amp;lt; aimami-relay codex-router top end&apos;
DISABLED_PREFIX=&apos;# aimami-router-disabled: &apos;

CONFIG_FILE=${AIMAMI_CODEX_CONFIG:-&quot;$HOME/.codex/config.toml&quot;}
ACTION=toggle
ASSUME_YES=0

usage() {
    printf &apos;%s\n&apos; \
        &quot;用法: $(basename &quot;$0&quot;) [-y] [on|off|toggle|status]&quot; \
        &apos;&apos; \
        &quot;默认配置: $CONFIG_FILE&quot; \
        &apos;也可通过 AIMAMI_CODEX_CONFIG 指定其他 config.toml。&apos; \
        &apos;-y：修改后不询问，直接关闭 Codex app-server。&apos;
}

ACTION_SET=0
for ARG in &quot;$@&quot;; do
    case &quot;$ARG&quot; in
        -y|--yes)
            ASSUME_YES=1
            ;;
        on|off|toggle|status)
            if [ &quot;$ACTION_SET&quot; -eq 1 ]; then
                printf &apos;错误: 只能指定一个操作。\n&apos; &amp;gt;&amp;amp;2
                usage &amp;gt;&amp;amp;2
                exit 2
            fi
            ACTION=$ARG
            ACTION_SET=1
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            printf &apos;错误: 未知参数: %s\n&apos; &quot;$ARG&quot; &amp;gt;&amp;amp;2
            usage &amp;gt;&amp;amp;2
            exit 2
            ;;
    esac
done

if [ ! -f &quot;$CONFIG_FILE&quot; ]; then
    printf &apos;错误: 配置文件不存在: %s\n&apos; &quot;$CONFIG_FILE&quot; &amp;gt;&amp;amp;2
    exit 1
fi

get_state() {
    awk -v start=&quot;$START_MARKER&quot; -v end=&quot;$END_MARKER&quot; -v prefix=&quot;$DISABLED_PREFIX&quot; &apos;
        $0 == start {
            starts++
            if (inside) bad = 1
            inside = 1
            next
        }
        $0 == end {
            ends++
            if (!inside) bad = 1
            inside = 0
            next
        }
        inside {
            if (index($0, prefix) == 1) disabled++
            else if ($0 !~ /^[[:space:]]*($|#)/) active++
        }
        END {
            if (inside || starts != 1 || ends != 1 || bad) {
                print &quot;错误: 路由块的起止标记缺失、重复或顺序错误&quot; &amp;gt; &quot;/dev/stderr&quot;
                exit 1
            }
            if (active &amp;amp;&amp;amp; disabled) {
                print &quot;错误: 路由块同时含有启用和禁用的配置行&quot; &amp;gt; &quot;/dev/stderr&quot;
                exit 1
            }
            if (disabled) print &quot;off&quot;
            else if (active) print &quot;on&quot;
            else print &quot;empty&quot;
        }
    &apos; &quot;$CONFIG_FILE&quot;
}

STATE=$(get_state)

if [ &quot;$ACTION&quot; = status ]; then
    printf &apos;aimami-relay codex-router: %s\n&apos; &quot;$STATE&quot;
    exit 0
fi

if [ &quot;$ACTION&quot; = toggle ]; then
    case &quot;$STATE&quot; in
        on) ACTION=off ;;
        off) ACTION=on ;;
        *)
            printf &apos;错误: 路由块中没有可切换的配置行\n&apos; &amp;gt;&amp;amp;2
            exit 1
            ;;
    esac
fi

if [ &quot;$ACTION&quot; = &quot;$STATE&quot; ]; then
    printf &apos;aimami-relay codex-router 已经是 %s，无需修改。\n&apos; &quot;$STATE&quot;
    exit 0
fi

TEMP_FILE=$(mktemp &quot;${CONFIG_FILE}.tmp.XXXXXX&quot;)
trap &apos;rm -f &quot;$TEMP_FILE&quot;&apos; EXIT HUP INT TERM

awk -v start=&quot;$START_MARKER&quot; -v end=&quot;$END_MARKER&quot; \
    -v prefix=&quot;$DISABLED_PREFIX&quot; -v action=&quot;$ACTION&quot; &apos;
    $0 == start { inside = 1; print; next }
    $0 == end { inside = 0; print; next }
    inside &amp;amp;&amp;amp; action == &quot;off&quot; &amp;amp;&amp;amp; $0 !~ /^[[:space:]]*($|#)/ {
        print prefix $0
        next
    }
    inside &amp;amp;&amp;amp; action == &quot;on&quot; &amp;amp;&amp;amp; index($0, prefix) == 1 {
        print substr($0, length(prefix) + 1)
        next
    }
    { print }
&apos; &quot;$CONFIG_FILE&quot; &amp;gt; &quot;$TEMP_FILE&quot;

if ! FILE_MODE=$(stat -c &apos;%a&apos; &quot;$CONFIG_FILE&quot; 2&amp;gt;/dev/null); then
    printf &apos;错误: 无法读取配置文件权限: %s\n&apos; &quot;$CONFIG_FILE&quot; &amp;gt;&amp;amp;2
    exit 1
fi
chmod &quot;$FILE_MODE&quot; &quot;$TEMP_FILE&quot;
mv &quot;$TEMP_FILE&quot; &quot;$CONFIG_FILE&quot;
trap - EXIT HUP INT TERM

printf &apos;aimami-relay codex-router 已切换为 %s。\n&apos; &quot;$ACTION&quot;

SHOULD_STOP=0
if [ &quot;$ASSUME_YES&quot; -eq 1 ]; then
    SHOULD_STOP=1
else
    printf &apos;是否关闭 Codex app-server 以使配置生效？[y/N] &apos;
    if ! read -r ANSWER; then
        ANSWER=
    fi
    case &quot;$ANSWER&quot; in
        y|Y|yes|YES|Yes) SHOULD_STOP=1 ;;
    esac
fi

if [ &quot;$SHOULD_STOP&quot; -eq 1 ]; then
    if pkill -u &quot;$(id -u)&quot; -TERM -f &apos;[c]odex.*app-server&apos;; then
        printf &apos;已向 Codex app-server 发送 TERM 信号。\n&apos;
    else
        printf &apos;未找到正在运行的 Codex app-server。\n&apos;
    fi
else
    printf &apos;已保留当前 Codex app-server。\n&apos;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加执行权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x ~/codex-switch.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 查看当前状态
~/codex-switch.sh status

# 开启、关闭或反转智能路由
~/codex-switch.sh on
~/codex-switch.sh off
~/codex-switch.sh toggle
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后，脚本会询问是否结束当前用户的 Codex &lt;code&gt;app-server&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;是否关闭 Codex app-server 以使配置生效？[y/N]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在自动化脚本中，可以使用 &lt;code&gt;-y&lt;/code&gt; 跳过确认：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/codex-switch.sh -y on
~/codex-switch.sh -y off
~/codex-switch.sh -y toggle
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际执行的结束命令是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pkill -u &quot;$(id -u)&quot; -TERM -f &apos;[c]odex.*app-server&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它只匹配当前用户的 Codex &lt;code&gt;app-server&lt;/code&gt;，并发送 &lt;code&gt;TERM&lt;/code&gt; 信号。当前会话可能因此中断，所以执行前应保存尚未完成的工作。&lt;/p&gt;
&lt;p&gt;如需操作其他配置文件，可以临时指定：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AIMAMI_CODEX_CONFIG=/path/to/config.toml ~/codex-switch.sh status
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;脚本的安全边界&lt;/h3&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;只处理标记块内的有效 TOML 行；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt; 不修改配置，也不结束服务；&lt;/li&gt;
&lt;li&gt;重复执行同一状态不会再次修改文件或结束服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
该版本使用 GNU &lt;code&gt;stat -c&lt;/code&gt;，面向 Linux，不兼容 macOS/BSD。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;同步会话记录&lt;/h2&gt;
&lt;p&gt;Provider 配置切换与会话记录同步是两个独立操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置切换：使用 Codex Profile 或 &lt;code&gt;codex-switch.sh&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;记录同步：使用 &lt;code&gt;codex-provider-sync&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;不要使用 &lt;code&gt;cc-switch-cli&lt;/code&gt; 代替上述任一流程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首次使用时，通过 npm 从 GitHub 全局安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -g git+https://github.com/Dailin521/codex-provider-sync.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要同步记录时执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;codex-provider sync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;推荐的完整顺序是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 &lt;code&gt;codex --profile ...&lt;/code&gt; 启动所需 Profile，或者用 &lt;code&gt;codex-switch.sh&lt;/code&gt; 切换全局配置；&lt;/li&gt;
&lt;li&gt;如果使用脚本方案，按提示关闭旧的 &lt;code&gt;app-server&lt;/code&gt;，再重新启动 Codex；&lt;/li&gt;
&lt;li&gt;执行 &lt;code&gt;codex-provider sync&lt;/code&gt; 同步会话记录。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样可以让配置切换和记录同步各自使用专门工具，避免其他切换器额外改写 Codex 状态。&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;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Codex CLI 交互使用&lt;/td&gt;
&lt;td&gt;Profile&lt;/td&gt;
&lt;td&gt;启动时选择，不修改全局状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codex exec&lt;/code&gt; 自动化任务&lt;/td&gt;
&lt;td&gt;Profile&lt;/td&gt;
&lt;td&gt;命令中可以显式声明运行环境&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;两个终端同时使用不同路由&lt;/td&gt;
&lt;td&gt;Profile&lt;/td&gt;
&lt;td&gt;每个进程拥有独立配置层&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex 桌面 App&lt;/td&gt;
&lt;td&gt;开关脚本&lt;/td&gt;
&lt;td&gt;桌面 App 主要读取全局配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;希望所有新会话统一切换&lt;/td&gt;
&lt;td&gt;开关脚本&lt;/td&gt;
&lt;td&gt;直接改变全局 &lt;code&gt;config.toml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无人值守切换并刷新服务&lt;/td&gt;
&lt;td&gt;开关脚本配合 &lt;code&gt;-y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;可修改配置并结束旧 &lt;code&gt;app-server&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;主要使用 CLI 时优先选择 Profile；主要依赖桌面 App 或常驻服务时，使用脚本更直接。&lt;/p&gt;
&lt;h2&gt;最终目录结构&lt;/h2&gt;
&lt;p&gt;采用 Profile 方案时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.codex/
├── config.toml
├── official.config.toml
├── aimami-router.config.toml
└── model-catalogs/
    └── codex_router_catalog.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;采用脚本方案时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.codex/
├── config.toml
└── model-catalogs/
    └── codex_router_catalog.json

~/codex-switch.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.chatgpt.com/docs/config-file/config-advanced#profiles&quot;&gt;Codex Advanced Configuration：Profiles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.chatgpt.com/docs/config-file/config-reference&quot;&gt;Codex Configuration Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.chatgpt.com/docs/config-file/environment-variables&quot;&gt;Codex Environment Variables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>macOS 读写 NTFS 移动硬盘：从只读 FSKit 到 macFUSE</title><link>https://pinellia.uk/posts/macos-ntfs-read-write/</link><guid isPermaLink="true">https://pinellia.uk/posts/macos-ntfs-read-write/</guid><description>在 macOS 上识别只读 NTFS 分区，并通过 macFUSE 与 NTFS-3G 将移动硬盘手动挂载为可写模式。</description><pubDate>Fri, 17 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Windows 常用的 NTFS 移动硬盘接到 macOS 后，文件通常可以读取，却不能新建、修改或删除。下面记录一次把 &lt;code&gt;WD elements&lt;/code&gt; 移动硬盘从系统默认的只读挂载，切换为 NTFS-3G 可写挂载的过程。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
这不是“开启 macOS 原生 NTFS 写入”。本文实测环境中的 &lt;code&gt;mount_ntfs&lt;/code&gt; 来自 Homebrew 安装的 &lt;code&gt;ntfs-3g-mac&lt;/code&gt;，底层依赖 macFUSE。macOS 自带的 NTFS 支持仍以只读为主。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;实测环境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;macOS 26.5.1&lt;/li&gt;
&lt;li&gt;Apple FSKit 默认只读挂载&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ntfs-3g-mac 2022.10.3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;macFUSE&lt;/li&gt;
&lt;li&gt;NTFS 分区：&lt;code&gt;/dev/disk8s1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;卷标：&lt;code&gt;WD elements&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;磁盘编号会随连接顺序变化，&lt;code&gt;/dev/disk8s1&lt;/code&gt; 只适用于这次实测。执行命令前一定要先确认自己的设备编号，不要直接照抄。&lt;/p&gt;
&lt;h2&gt;先确认 &lt;code&gt;mount_ntfs&lt;/code&gt; 来自哪里&lt;/h2&gt;
&lt;p&gt;参考文章写于较早的 macOS 版本，其测试系统为 10.13.6。较新的 macOS 不应再把 &lt;code&gt;mount_ntfs&lt;/code&gt; 理解为系统自带的可写 NTFS 驱动。&lt;/p&gt;
&lt;p&gt;先检查当前实际调用的程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;command -v mount_ntfs
readlink &quot;$(command -v mount_ntfs)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本机输出指向：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/opt/homebrew/sbin/mount_ntfs
../Cellar/ntfs-3g-mac/2022.10.3/sbin/mount_ntfs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这说明命令是 NTFS-3G 的 Homebrew 包装脚本。重新挂载后，&lt;code&gt;mount&lt;/code&gt; 显示的文件系统类型也会从 &lt;code&gt;ntfs&lt;/code&gt; 变成 &lt;code&gt;macfuse&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果提示 &lt;code&gt;command not found: mount_ntfs&lt;/code&gt;，需要先安装 macFUSE 和 NTFS-3G：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew tap gromgit/fuse
brew install --cask macfuse
brew install gromgit/fuse/ntfs-3g-mac
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装 macFUSE 后，按系统提示在“系统设置 → 隐私与安全性”中允许其扩展，并在需要时重新启动。不要为了省事直接关闭 SIP；若系统要求调整 Apple 芯片 Mac 的启动安全策略，应以 &lt;a href=&quot;https://github.com/macfuse/macfuse/wiki&quot;&gt;macFUSE 官方说明&lt;/a&gt; 为准。&lt;/p&gt;
&lt;h2&gt;第一步：识别 NTFS 分区&lt;/h2&gt;
&lt;p&gt;插入移动硬盘后，先列出外置磁盘并核对分区信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diskutil list external
diskutil info /dev/disk8s1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认设备编号无误后，查看当前挂载参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mount | grep ntfs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本次输出为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/dev/disk8s1 on /Volumes/WD elements (ntfs, local, nodev, nosuid, read-only, noowners, noatime, fskit)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中几个关键字段是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ntfs&lt;/code&gt;：分区文件系统为 NTFS；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;read-only&lt;/code&gt;：当前明确以只读方式挂载；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fskit&lt;/code&gt;：当前走的是 macOS 的 FSKit 挂载路径；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;noowners&lt;/code&gt;：不在该卷上执行常规的所有者权限检查。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二步：卸载系统的只读挂载&lt;/h2&gt;
&lt;p&gt;卸载分区不会清除数据，但执行前仍应关闭正在访问移动硬盘的 Finder 窗口、终端和应用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo umount /dev/disk8s1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以让 Disk Utility 框架处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diskutil unmount /dev/disk8s1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里只是卸载文件系统，不是把物理硬盘弹出。设备节点仍然存在，下一步可以立即重新挂载。&lt;/p&gt;
&lt;h2&gt;第三步：建立挂载目录&lt;/h2&gt;
&lt;p&gt;这里把挂载点放在桌面，名称为 &lt;code&gt;md1&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p &quot;$HOME/Desktop/md1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;挂载目录必须已经存在，而且应当是空目录。路径中如果包含空格，记得加引号。&lt;/p&gt;
&lt;h2&gt;第四步：以可写方式重新挂载&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo mount_ntfs -o rw,nobrowse /dev/disk8s1 &quot;$HOME/Desktop/md1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数含义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rw&lt;/code&gt;：请求以读写模式挂载；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nobrowse&lt;/code&gt;：不把该挂载点作为普通磁盘显示在 Finder 侧边栏；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/disk8s1&lt;/code&gt;：需要挂载的 NTFS 分区；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&quot;$HOME/Desktop/md1&quot;&lt;/code&gt;：访问该分区的新入口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;命令成功时通常没有输出，这是正常现象。因为使用了 &lt;code&gt;nobrowse&lt;/code&gt;，可以直接打开挂载目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;open &quot;$HOME/Desktop/md1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第五步：验证是否真的可写&lt;/h2&gt;
&lt;p&gt;不要只看 &lt;code&gt;mount_ntfs&lt;/code&gt; 是否报错。先检查实际挂载类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mount | grep &apos;/Desktop/md1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本机重新挂载后的结果为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/dev/disk8s1 on /Users/&amp;lt;username&amp;gt;/Desktop/md1 (macfuse, local, synchronous, noatime, nobrowse)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始状态中的 &lt;code&gt;read-only&lt;/code&gt; 已经消失，并且文件系统类型变成了 &lt;code&gt;macfuse&lt;/code&gt;。最后创建一个临时文件，验证真实写入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test_file=&quot;$HOME/Desktop/md1/.ntfs-write-test&quot;
touch &quot;$test_file&quot; &amp;amp;&amp;amp; rm &quot;$test_file&quot; &amp;amp;&amp;amp; echo &quot;NTFS 写入测试通过&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有这一步成功，才能确认当前分区确实可写。&lt;/p&gt;
&lt;h2&gt;安全卸载与恢复默认挂载&lt;/h2&gt;
&lt;p&gt;使用完毕后，不要直接拔出移动硬盘。先卸载 macFUSE 挂载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo umount &quot;$HOME/Desktop/md1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后弹出整块物理磁盘。注意这里使用 &lt;code&gt;disk8&lt;/code&gt;，而不是分区 &lt;code&gt;disk8s1&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diskutil eject /dev/disk8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果只是想退出可写模式，并恢复 macOS 默认的只读挂载，可以执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo umount &quot;$HOME/Desktop/md1&quot;
rmdir &quot;$HOME/Desktop/md1&quot;
diskutil mount /dev/disk8s1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;手动挂载不会永久修改 NTFS 分区；移动硬盘重新插入后，macOS 通常仍会按默认只读方式挂载。&lt;/p&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;Finder 中看不到移动硬盘&lt;/h3&gt;
&lt;p&gt;这是 &lt;code&gt;nobrowse&lt;/code&gt; 的预期行为，不代表挂载失败。使用下面的命令打开即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;open &quot;$HOME/Desktop/md1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望它作为普通卷显示，可以尝试去掉 &lt;code&gt;nobrowse&lt;/code&gt;，但手动挂载点的位置与 Finder 展示效果可能因系统版本而异。&lt;/p&gt;
&lt;h3&gt;提示 &lt;code&gt;Resource busy&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;通常是原来的只读卷尚未卸载，或者某个应用仍在使用它。先关闭相关 Finder 窗口和文件，再检查：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mount | grep disk8s1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认旧挂载消失后再执行 &lt;code&gt;mount_ntfs&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;提示 macFUSE 不可用&lt;/h3&gt;
&lt;p&gt;检查 macFUSE 是否已安装、系统扩展是否获准，以及安装后是否完成重启。还可以确认 NTFS-3G 是否存在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew list --versions ntfs-3g-mac
command -v mount_ntfs
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Windows 休眠或快速启动导致拒绝写入&lt;/h3&gt;
&lt;p&gt;如果 NTFS-3G 提示卷处于 hibernated、unclean 或 unsafe state，不要强制挂载。把硬盘接回 Windows，关闭“快速启动”和休眠，执行完整关机；必要时先运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chkdsk X: /f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 &lt;code&gt;X:&lt;/code&gt; 替换为移动硬盘的盘符，修复完成并正常弹出后再回到 macOS。&lt;/p&gt;
&lt;h3&gt;是否适合长期使用&lt;/h3&gt;
&lt;p&gt;NTFS-3G + macFUSE 适合临时兼容已有的 NTFS 硬盘，但性能、系统升级兼容性和异常断电后的恢复能力都需要谨慎对待。重要数据应保留备份；需要长期高频写入时，可以考虑成熟的商业 NTFS 驱动。若硬盘可以重新格式化且主要在 Windows 与 macOS 间交换文件，exFAT 往往更省心，但格式化前必须先备份全部数据。&lt;/p&gt;
&lt;h2&gt;操作结论&lt;/h2&gt;
&lt;p&gt;这次操作的状态变化可以概括为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;macOS FSKit 只读挂载
        ↓ 卸载
NTFS-3G + macFUSE 可写挂载
        ↓ 安全卸载
重新插入后恢复系统默认行为
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最重要的不是记住 &lt;code&gt;disk8s1&lt;/code&gt;，而是每次先确认设备编号；也不要因为 &lt;code&gt;mount_ntfs&lt;/code&gt; 没有输出就默认成功，始终用 &lt;code&gt;mount&lt;/code&gt; 和一次真实的临时文件写入完成验证。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/82665550&quot;&gt;怎样在苹果电脑上用移动硬盘（使用 NTFS 格式硬盘）？&lt;/a&gt;：原文测试于 macOS 10.13.6，提供了“卸载后重新挂载”的基本思路。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/macfuse/macfuse&quot;&gt;macFUSE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tuxera/ntfs-3g&quot;&gt;NTFS-3G&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/gromgit/homebrew-fuse&quot;&gt;gromgit/homebrew-fuse&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Claude Code 斜杠命令使用指南</title><link>https://pinellia.uk/posts/claude-code-slash-commands/</link><guid isPermaLink="true">https://pinellia.uk/posts/claude-code-slash-commands/</guid><description>按会话、配置、审查、调试和扩展等场景整理 Claude Code CLI 的斜杠命令及使用边界。</description><pubDate>Fri, 12 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://claude.com/claude-code&quot;&gt;Claude Code&lt;/a&gt; 是 Anthropic 的终端编程工具。多数任务可以直接用自然语言描述；以 &lt;code&gt;/&lt;/code&gt; 开头的斜杠命令更适合会话管理、配置、代码审查和项目初始化等需要明确控制的操作。&lt;/p&gt;
&lt;p&gt;下面按使用场景整理命令。命令集合会随版本变化，实际可用项以当前客户端的 &lt;code&gt;/help&lt;/code&gt; 为准。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 会话管理&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/clear&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;清空当前会话上下文&lt;/td&gt;
&lt;td&gt;开始全新对话，不影响项目文件和设置。适合切换话题或上下文过长时使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/compact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;压缩会话上下文&lt;/td&gt;
&lt;td&gt;将对话历史总结为摘要以释放 token 空间。压缩前确保重要内容已落地到文件中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/rename&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重命名当前会话标题&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/rename &quot;修复登录页面布局问题&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/resume&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;恢复之前的会话&lt;/td&gt;
&lt;td&gt;继续之前的对话&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/export&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;导出当前会话记录&lt;/td&gt;
&lt;td&gt;支持多种格式，用于存档、分享或审计&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/prompt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示当前系统提示词&lt;/td&gt;
&lt;td&gt;查看 CLAUDE.md 注入的项目上下文、记忆、环境信息等&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 配置与偏好&lt;/h2&gt;
&lt;p&gt;调整 Claude Code 的外观、行为和模型偏好。&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;&lt;code&gt;/config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;打开设置界面&lt;/td&gt;
&lt;td&gt;修改主题、模型、权限策略、hooks 等。等价于编辑 &lt;code&gt;~/.claude/settings.json&lt;/code&gt; 和 &lt;code&gt;.claude/settings.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/theme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;切换终端颜色主题&lt;/td&gt;
&lt;td&gt;暗色 / 亮色 / 系统跟随&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/statusline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;配置状态栏显示项&lt;/td&gt;
&lt;td&gt;选择显示模型名、分支、token 用量等信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;切换 AI 模型&lt;/td&gt;
&lt;td&gt;可用模型取决于订阅计划和最新模型列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/fast&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;切换 Fast 模式&lt;/td&gt;
&lt;td&gt;使用 Opus 模型并加速输出，适合需要快速响应的场景&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 项目初始化与记忆&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;code&gt;/init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;生成项目的 CLAUDE.md&lt;/td&gt;
&lt;td&gt;分析项目结构，自动生成包含技术栈、目录结构、常用命令的文档。建议每个项目根目录运行一次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;管理持久记忆&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/memory 记住：这个项目使用 Biome 格式化代码&lt;/code&gt;。记忆存储在 &lt;code&gt;.claude/projects/&amp;lt;path&amp;gt;/memory/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/recall&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;检索相关记忆&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/recall 代码格式化工具&lt;/code&gt;。Claude Code 启动时自动加载 &lt;code&gt;MEMORY.md&lt;/code&gt; 索引&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 代码审查与质量&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;code&gt;/review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;审查当前 Pull Request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/review --effort high&lt;/code&gt; 指定审查强度；覆盖正确性、安全性、性能、可维护性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/security-review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;安全漏洞专项审查&lt;/td&gt;
&lt;td&gt;覆盖 OWASP Top 10：注入、认证绕过、敏感信息泄露、不安全反序列化等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/simplify&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;审查并自动简化代码&lt;/td&gt;
&lt;td&gt;识别可简化、复用、提效的代码模式并自动修复，侧重质量而非 bug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/code-review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分级代码审查&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/code-review --effort low&lt;/code&gt;（仅高置信度）/ &lt;code&gt;max&lt;/code&gt;（全面）/ &lt;code&gt;--comment&lt;/code&gt;（发布到 PR）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 开发与调试&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;code&gt;/run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;启动并驱动项目应用&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/run &quot;测试登录功能是否正常&quot;&lt;/code&gt;。自动检测项目类型并选择运行方式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/verify&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;验证变更实际效果&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/verify &quot;检查导航栏响应式布局是否修复&quot;&lt;/code&gt;。运行应用并观察行为来确认&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 任务与后台管理&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;code&gt;/tasks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看任务列表&lt;/td&gt;
&lt;td&gt;显示每个任务的状态（pending / in_progress / completed）和依赖关系&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/bashes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列出后台 Shell 任务&lt;/td&gt;
&lt;td&gt;查看所有运行中的后台任务及其状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/terminate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;终止后台任务&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/terminate &amp;lt;task-id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 自动化与工作流&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;code&gt;/loop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按固定间隔循环执行命令&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/loop 5m /run &quot;检查 Vercel 最新部署状态&quot;&lt;/code&gt;（每 5 分钟）；默认间隔 10 分钟&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/workflows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看和管理多 Agent 编排工作流&lt;/td&gt;
&lt;td&gt;支持 &lt;code&gt;parallel()&lt;/code&gt;、&lt;code&gt;pipeline()&lt;/code&gt;、&lt;code&gt;phase()&lt;/code&gt; 等编排原语，用 JS 编写脚本&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 扩展与集成&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;code&gt;/mcp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;管理 MCP 服务器连接&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/mcp add &amp;lt;server-name&amp;gt;&lt;/code&gt; 添加外部工具/数据源（数据库、API 等）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/ide&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;管理 IDE 集成&lt;/td&gt;
&lt;td&gt;与 VS Code、JetBrains 等协作，在 IDE 中打开文件、定位到指定行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/add-dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;添加额外工作区目录&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/add-dir /path/to/another-project&lt;/code&gt;，允许操作多个目录下的文件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 技能类命令（Skills）&lt;/h2&gt;
&lt;p&gt;Skills 是扩展 Claude Code 能力边界的专业化模块：&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;&lt;code&gt;/deep-research&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;多源深度调研：自动搜索、交叉验证并生成带引用的报告&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/claude-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claude API / Anthropic SDK 参考：模型 ID、定价、参数、token 计算等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/update-config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通过 settings.json 配置 hooks、权限、环境变量等自动化行为&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/keybindings-help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自定义键盘快捷键&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/fewer-permission-prompts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分析操作历史，生成权限白名单以减少重复授权弹窗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;启动并驱动项目应用以观察变更效果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/verify&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;验证代码变更是否达到预期效果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/code-review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对当前 diff 进行分级审查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/simplify&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;审查变更代码的质量优化点并自动修复&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;审查 Pull Request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/security-review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;安全审查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;初始化项目 CLAUDE.md&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/loop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定时循环执行任务&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;Skills 列表会随版本更新持续扩展。使用 &lt;code&gt;/help&lt;/code&gt; 可查看当前可用的完整 Skills 列表。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;10. 信息与帮助&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;code&gt;/help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示帮助信息和命令概览&lt;/td&gt;
&lt;td&gt;遇到不熟悉的命令时的第一站&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/cost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看 token 用量及费用&lt;/td&gt;
&lt;td&gt;当前会话和累计明细，帮助控制 API 消费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/doctor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;系统健康检查&lt;/td&gt;
&lt;td&gt;验证安装完整性、依赖项（Node.js、pnpm 等）、配置文件有效性&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;11. 账户管理&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;code&gt;/login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;登录或切换 Anthropic 账户&lt;/td&gt;
&lt;td&gt;暂无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/logout&lt;/code&gt;&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;hr /&gt;
&lt;h2&gt;12. 实用技巧&lt;/h2&gt;
&lt;h3&gt;12.1 命令组合&lt;/h3&gt;
&lt;p&gt;多个命令可以串联达到高效 workflow：先 &lt;code&gt;/review&lt;/code&gt; 审查，再 &lt;code&gt;/simplify&lt;/code&gt; 自动修复；先 &lt;code&gt;/init&lt;/code&gt; 初始化，再 &lt;code&gt;/config&lt;/code&gt; 自定义。&lt;/p&gt;
&lt;h3&gt;12.2 善用 &lt;code&gt;/prompt&lt;/code&gt; 排查问题&lt;/h3&gt;
&lt;p&gt;当 Claude Code 的行为不符合预期时，用 &lt;code&gt;/prompt&lt;/code&gt; 查看它实际「看到」了哪些指令，包括 CLAUDE.md、memory、系统提示词等。&lt;/p&gt;
&lt;h3&gt;12.3 &lt;code&gt;/compact&lt;/code&gt; 的时机&lt;/h3&gt;
&lt;p&gt;当出现以下情况时建议执行压缩：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上下文 token 使用率超过 80%&lt;/li&gt;
&lt;li&gt;对话中已完成多个独立任务&lt;/li&gt;
&lt;li&gt;响应速度明显下降&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;12.4 项目级 vs 用户级配置&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;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;code&gt;~/.claude/settings.json&lt;/code&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&gt;.claude/settings.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;仅对当前项目生效，覆盖用户级设置&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;使用 &lt;code&gt;/config&lt;/code&gt; 编辑时注意区分两者的作用范围。&lt;/p&gt;
&lt;h3&gt;12.5 Memory 的最佳实践&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;记录项目特有的约定和非标准做法&lt;/li&gt;
&lt;li&gt;记录用户偏好（语言、格式化风格等）&lt;/li&gt;
&lt;li&gt;避免记录可从代码或 git 历史自动推导的信息&lt;/li&gt;
&lt;li&gt;定期用 &lt;code&gt;/recall&lt;/code&gt; 检查记忆的有效性&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;使用原则&lt;/h2&gt;
&lt;p&gt;能直接描述的任务就使用自然语言；需要配置固化、会话管理或多步骤编排时再使用命令。遇到命令不存在或行为与文中不同的情况，先用 &lt;code&gt;/help&lt;/code&gt; 和 &lt;code&gt;/status&lt;/code&gt; 确认当前版本与可用能力。&lt;/p&gt;
</content:encoded></item><item><title>服务器环境配置指南</title><link>https://pinellia.uk/posts/server-env-setup/</link><guid isPermaLink="true">https://pinellia.uk/posts/server-env-setup/</guid><description>在无 root 权限的 Linux 服务器上，从零搭建命令行与 Python 开发环境，并使用 rclone 将本机 SFTP 目录挂载为普通目录。</description><pubDate>Fri, 12 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这套流程用于初始化没有 root 权限的 Linux 服务器。目标是在用户目录中完成 Shell、Python、项目目录和远程文件挂载配置，不依赖系统管理员安装软件。&lt;/p&gt;
&lt;p&gt;主要内容包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shell 环境初始化与 &lt;code&gt;zsh&lt;/code&gt; 编译安装&lt;/li&gt;
&lt;li&gt;&lt;code&gt;oh-my-zsh&lt;/code&gt; 配置与 &lt;code&gt;Powerlevel10k&lt;/code&gt; 主题&lt;/li&gt;
&lt;li&gt;&lt;code&gt;micromamba&lt;/code&gt; 部署与初始化&lt;/li&gt;
&lt;li&gt;个人共享目录与项目工作区建立&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;rclone&lt;/code&gt; 将本机 SFTP 目录挂载为 FUSE 目录&lt;/li&gt;
&lt;li&gt;Python 环境创建&lt;/li&gt;
&lt;li&gt;pip 国内镜像配置&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以下步骤均无需 &lt;code&gt;sudo&lt;/code&gt; 或 root 权限，仅在用户目录下操作。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. Shell 环境升级：编译安装 zsh&lt;/h2&gt;
&lt;p&gt;大部分 Linux 服务器预装的是 &lt;code&gt;bash&lt;/code&gt;，但如果你习惯 &lt;code&gt;zsh&lt;/code&gt; 的补全和主题生态，建议自行编译一份用户态 &lt;code&gt;zsh&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;1.1 准备编译依赖&lt;/h3&gt;
&lt;p&gt;由于没有 root 权限，需要借助 &lt;code&gt;micromamba&lt;/code&gt; 创建临时环境来提供编译工具链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 下载 micromamba 二进制（详见下一节）
curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -xvj .local/micromamba
chmod +x .local/micromamba/micromamba

# 创建一个用于编译的环境
./.local/micromamba/micromamba create -n make make ncurses
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 下载并解压 zsh 源码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 下载 zsh 源码包
wget https://sourceforge.net/projects/zsh/files/zsh/5.9.1/zsh-5.9.1.tar.xz/download -O zsh-5.9.1.tar.xz
tar -xJf zsh-5.9.1.tar.xz
cd zsh-5.9.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3 配置与编译&lt;/h3&gt;
&lt;p&gt;设置编译环境变量，让编译器找到 &lt;code&gt;micromamba&lt;/code&gt; 环境中安装的 &lt;code&gt;ncurses&lt;/code&gt; 库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ENV_PATH=&quot;$HOME/.local/micromamba/envs/make&quot;

make distclean || rm -f config.cache

./configure --prefix=&quot;$HOME/.local/zsh&quot; \
    CFLAGS=&quot;-I${ENV_PATH}/include -I${ENV_PATH}/include/ncurses&quot; \
    CPPFLAGS=&quot;-I${ENV_PATH}/include -I${ENV_PATH}/include/ncurses&quot; \
    LDFLAGS=&quot;-L${ENV_PATH}/lib -Wl,-rpath,${ENV_PATH}/lib&quot; \
    --with-termlib=ncurses

make &amp;amp;&amp;amp; make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译完成后，&lt;code&gt;zsh&lt;/code&gt; 会被安装到 &lt;code&gt;$HOME/.local/zsh&lt;/code&gt; 下。&lt;/p&gt;
&lt;h3&gt;1.4 切换默认 Shell&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 确认 zsh 路径
which zsh
# 输出: ~/.local/zsh/bin/zsh

# 查看当前系统已登记的 shell
cat /etc/shells

# 切换默认登录 shell
chsh -s &quot;$HOME/.local/zsh/bin/zsh&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
如果 &lt;code&gt;chsh&lt;/code&gt; 在服务器上可用（部分共享服务器允许用户自行切换），该命令会直接生效。重新登录后即为 &lt;code&gt;zsh&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;若 &lt;code&gt;chsh&lt;/code&gt; 不可用，可在 &lt;code&gt;~/.bash_profile&lt;/code&gt; 末尾添加 &lt;code&gt;exec $HOME/.local/zsh/bin/zsh&lt;/code&gt;，每次登录时自动进入 &lt;code&gt;zsh&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1.5 安装 oh-my-zsh 与 Powerlevel10k 主题&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://ohmyz.sh&quot;&gt;oh-my-zsh&lt;/a&gt; 是 zsh 最流行的配置框架，提供丰富的主题和插件生态。&lt;a href=&quot;https://github.com/romkatv/powerlevel10k&quot;&gt;Powerlevel10k&lt;/a&gt;（p10k）是一款高性能的 oh-my-zsh 主题，提供清晰的信息展示和便捷的配置向导。&lt;/p&gt;
&lt;h4&gt;1.5.1 安装 oh-my-zsh&lt;/h4&gt;
&lt;p&gt;通过 curl 一键安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sh -c &quot;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装脚本会自动备份已有的 &lt;code&gt;~/.zshrc&lt;/code&gt; 并生成一份新的默认配置。&lt;/p&gt;
&lt;h4&gt;1.5.2 安装 Powerlevel10k 主题&lt;/h4&gt;
&lt;p&gt;将 p10k 主题克隆到 oh-my-zsh 的自定义主题目录下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone --depth=1 https://github.com/romkatv/powerlevel10k.git \
    &quot;${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.5.3 启用主题&lt;/h4&gt;
&lt;p&gt;编辑 &lt;code&gt;~/.zshrc&lt;/code&gt;，将主题设置为 &lt;code&gt;powerlevel10k/powerlevel10k&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 修改前
ZSH_THEME=&quot;robbyrussell&quot;

# 修改后
ZSH_THEME=&quot;powerlevel10k/powerlevel10k&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.5.4 运行配置向导&lt;/h4&gt;
&lt;p&gt;重新加载 &lt;code&gt;~/.zshrc&lt;/code&gt; 或新开一个终端标签页，p10k 会自动启动交互式配置向导：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置向导会依次询问：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;字体&lt;/strong&gt;：是否安装 Meslo Nerd Font（推荐安装，否则部分图标显示异常）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;样式&lt;/strong&gt;：经典、纯净、彩虹、瘦线等多种样式可选&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提示符元素&lt;/strong&gt;：选择要显示的信息（时间、电池、Python 虚拟环境、git 状态等）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;间距与换行&lt;/strong&gt;：单行或双行提示符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置完成后，p10k 会自动生成 &lt;code&gt;~/.p10k.zsh&lt;/code&gt; 文件，后续可通过 &lt;code&gt;p10k configure&lt;/code&gt; 随时重新运行向导。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
如果终端连接后图标显示为乱码或方框，说明缺少 Nerd Font。可以在本地终端（如 iTerm2、Windows Terminal）中下载安装 &lt;a href=&quot;https://github.com/romkatv/powerlevel10k/#fonts&quot;&gt;Meslo Nerd Font&lt;/a&gt;，并将终端字体设置为 &lt;code&gt;MesloLGS NF&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
p10k 配置向导提供了丰富的自定义选项，强烈建议首次使用时花几分钟过一遍向导，后续使用体验会好很多。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 安装 Micromamba&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;micromamba&lt;/code&gt; 是 &lt;code&gt;conda&lt;/code&gt; 的 C++ 重实现，静态链接、启动速度快、无环境嵌套问题，非常适合在无 root 权限的环境下管理 Python 版本和包。&lt;/p&gt;
&lt;h3&gt;2.1 下载与安装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -xvj .local/micromamba
chmod +x .local/micromamba/micromamba
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 初始化 Shell&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;./.local/micromamba/micromamba shell init -s bash
# 如果使用 zsh，替换为：
./.local/micromamba/micromamba shell init -s zsh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化完成后，重新加载 shell 配置文件或重新登录即可使用 &lt;code&gt;micromamba&lt;/code&gt; 命令。&lt;/p&gt;
&lt;h3&gt;2.3 验证安装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;micromamba env list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果正确输出了环境列表（即使为空），说明安装成功。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 建立目录结构&lt;/h2&gt;
&lt;h3&gt;3.1 服务器摸底&lt;/h3&gt;
&lt;p&gt;在新服务器上，先确认磁盘容量、配额和共享目录情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df -h                          # 查看磁盘挂载与容量
quota -v                       # 查看用户配额
groups                         # 查看所属组
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 创建个人工作目录&lt;/h3&gt;
&lt;p&gt;根据共享存储的布局，建议建立标准化的个人目录结构。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这样做的目的是将自己的文件（代码、数据、模型、产出）存放在&lt;strong&gt;干净的独立目录&lt;/strong&gt;中，与 &lt;code&gt;~/.cache&lt;/code&gt;、shell 配置文件、micromamba 环境等系统/工具生成的文件彻底分离。这样无论是备份、迁移还是清理缓存，都不会误伤自己的数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;/share/&amp;lt;group_name&amp;gt;/&amp;lt;username&amp;gt;/
├── Datasets/     # 数据集
├── Models/       # 预训练模型
├── Outputs/      # 输出与实验结果
└── Projects/     # 项目工作区
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建命令（按实际情况修改组名）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GROUP_NAME=&quot;your-group&quot;
USER_NAME=&quot;${USER:?当前 shell 未设置 USER}&quot;
SHARE_BASE=&quot;/share/${GROUP_NAME}/${USER_NAME}&quot;
mkdir -p &quot;${SHARE_BASE}&quot;/{Datasets,Models,Outputs,Projects}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 创建项目工作区&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/Projects/work1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;建议将工作区与共享目录配合使用，项目产出可以软链接或直接存放到共享目录中，便于团队协作。&lt;/p&gt;
&lt;h3&gt;3.4 使用 rclone 挂载本机 SFTP 目录&lt;/h3&gt;
&lt;p&gt;有些程序不能正确跟随符号链接，或者要求目标必须表现为普通目录。此时可以让 &lt;code&gt;rclone&lt;/code&gt; 通过本机的 SSH/SFTP 读取实际工作目录，再将它挂载到另一个位置。程序看到的是一个普通的 FUSE 目录，而不是符号链接。&lt;/p&gt;
&lt;p&gt;这样做的主要目的是把当前希望通过网页查看的内容统一挂载到一个固定目录，再由 OpenList 等外部服务挂载并提供网页浏览，而不必移动或复制原始文件。&lt;/p&gt;
&lt;p&gt;以下示例假设服务器的目录结构为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/sdc/home/&amp;lt;username&amp;gt;/&amp;lt;username&amp;gt;/
├── ai-workspace/w1/  # 实际目录
└── Pan/w1/           # FUSE 挂载点
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例中的用户名从 &lt;code&gt;$USER&lt;/code&gt; 获取，不需要在命令或文章中写入真实账号。&lt;/p&gt;
&lt;h4&gt;前置条件&lt;/h4&gt;
&lt;p&gt;此方案不需要 root 权限，但需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$HOME/bin/rclone&lt;/code&gt; 已安装且可执行；&lt;/li&gt;
&lt;li&gt;系统提供 &lt;code&gt;fusermount3&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;本机 &lt;code&gt;sshd&lt;/code&gt; 正在运行；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.ssh/id_ed25519&lt;/code&gt; 可以免密登录 &lt;code&gt;localhost&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;挂载点存在且为空。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以先验证依赖和本机 SSH：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;command -v &quot;$HOME/bin/rclone&quot;
command -v fusermount3
ssh -i &quot;$HOME/.ssh/id_ed25519&quot; localhost true
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;定义脱敏路径变量&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;USER_NAME=&quot;${USER:?当前 shell 未设置 USER}&quot;
USER_ROOT=&quot;/sdc/home/${USER_NAME}/${USER_NAME}&quot;
SOURCE_DIR=&quot;${USER_ROOT}/ai-workspace/w1&quot;
MOUNT_DIR=&quot;${USER_ROOT}/Pan/w1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先确认实际目录存在，再创建空挂载点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test -d &quot;$SOURCE_DIR&quot;
mkdir -p &quot;$MOUNT_DIR&quot;

if [ -n &quot;$(find &quot;$MOUNT_DIR&quot; -mindepth 1 -maxdepth 1 -print -quit)&quot; ]; then
  echo &quot;挂载点非空：$MOUNT_DIR&quot; &amp;gt;&amp;amp;2
  exit 1
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;执行挂载&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;rclone&lt;/code&gt; 的按需 SFTP remote 使用双斜杠表示远端绝对路径。下面先移除 &lt;code&gt;SOURCE_DIR&lt;/code&gt; 开头的 &lt;code&gt;/&lt;/code&gt;，再显式拼接 &lt;code&gt;//&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;REMOTE=&quot;:sftp,host=localhost,user=${USER_NAME},key_file=${HOME}/.ssh/id_ed25519,known_hosts_file=none://${SOURCE_DIR#/}&quot;
LOG_FILE=&quot;$(dirname &quot;$MOUNT_DIR&quot;)/.rclone-w1.log&quot;

&quot;$HOME/bin/rclone&quot; mount &quot;$REMOTE&quot; &quot;$MOUNT_DIR&quot; \
  --daemon \
  --vfs-cache-mode writes \
  --log-file &quot;$LOG_FILE&quot; \
  --log-level INFO
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;known_hosts_file=none&lt;/code&gt; 会跳过 localhost 的主机密钥校验，仅适合明确连接本机回环 SSH 的场景。如果改为连接其他主机，应配置并校验 &lt;code&gt;known_hosts&lt;/code&gt;，不要沿用这个选项。&lt;/p&gt;
&lt;p&gt;检查挂载是否成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mountpoint &quot;$MOUNT_DIR&quot;
findmnt -T &quot;$MOUNT_DIR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功后，访问 &lt;code&gt;Pan/w1&lt;/code&gt; 实际会通过本机 SFTP 读写 &lt;code&gt;ai-workspace/w1&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;卸载&lt;/h4&gt;
&lt;p&gt;正常卸载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fusermount3 -u &quot;$MOUNT_DIR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果提示设备忙，先关闭终端、编辑器、训练任务等正在访问该目录的程序，再重新卸载。必要时可以延迟卸载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fusermount3 -uz &quot;$MOUNT_DIR&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
挂载期间不要删除或移动 &lt;code&gt;w1&lt;/code&gt; 挂载点。服务器重启后 FUSE 挂载会失效，需要重新执行挂载命令；若要长期使用，可以后续再封装为登录脚本或用户级服务。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 创建 Python 环境&lt;/h2&gt;
&lt;h3&gt;4.1 基础 Python 环境&lt;/h3&gt;
&lt;p&gt;创建一个通用 Python 3.10 环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;micromamba create -n py10 python=3.10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 环境管理常用命令&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;micromamba env list                    # 列出所有环境
micromamba activate py10               # 激活环境
micromamba deactivate                  # 退出当前环境
micromamba remove -n py10 --all        # 删除环境
micromamba install -n py10 &amp;lt;package&amp;gt;   # 安装包
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 配置 pip 国内镜像&lt;/h2&gt;
&lt;p&gt;在国内网络环境下，配置 pip 镜像源可以显著提升包下载速度。&lt;/p&gt;
&lt;h3&gt;5.1 写入配置文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/.config/pip
cat &amp;gt; ~/.config/pip/pip.conf &amp;lt;&amp;lt;&apos;EOF&apos;
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
trusted-host = pypi.tuna.tsinghua.edu.cn
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2 验证配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;micromamba run -n py10 python -m pip config list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出中应包含 &lt;code&gt;index-url&lt;/code&gt; 指向清华源，即配置生效。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 启动配置文件整理&lt;/h2&gt;
&lt;p&gt;初始化过程中需要调整以下 shell 配置文件，确保每次登录环境正确：&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;&lt;code&gt;~/.bashrc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bash 交互式 shell 的配置文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.bash_profile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bash 登录时读取的配置文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.zshrc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;zsh 的配置文件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;建议将 &lt;code&gt;micromamba&lt;/code&gt; 的初始化代码、环境变量（如 &lt;code&gt;PATH&lt;/code&gt;、&lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt;）统一写入对应 shell 的配置文件中。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 配置 Git 提交模板&lt;/h2&gt;
&lt;p&gt;在服务器上进行开发时，经常需要提交代码。配置一个标准化的提交信息模板可以让每次 &lt;code&gt;git commit&lt;/code&gt; 时都有结构化的提示，避免遗漏关键信息。&lt;/p&gt;
&lt;h3&gt;7.1 创建模板文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;gt; ~/.git-commit-template.txt &amp;lt;&amp;lt; &apos;EOF&apos;

**note:**
-

---
**todo:**
- []
---
**fixed:**
- []

EOF
&lt;/code&gt;&lt;/pre&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;&lt;code&gt;**note:**&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;备注说明、背景信息、相关上下文&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;**todo:**&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;待完成的待办项，使用 &lt;code&gt;- []&lt;/code&gt; 标记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;**fixed:**&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;本次提交修复的问题或完成的事项&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;7.2 配置 Git 使用模板&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git config --global commit.template ~/.git-commit-template.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
如果希望只对特定项目生效，去掉 &lt;code&gt;--global&lt;/code&gt;，在对应仓库目录下执行 &lt;code&gt;git config commit.template ~/.git-commit-template.txt&lt;/code&gt; 即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;7.3 效果&lt;/h3&gt;
&lt;p&gt;配置完成后，每次执行 &lt;code&gt;git commit&lt;/code&gt;（不带 &lt;code&gt;-m&lt;/code&gt;）时，编辑器会自动打开并预填充模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
**note:**
-

---
**todo:**
- []
---
**fixed:**
- []

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只需在对应位置填写内容即可。使用 &lt;code&gt;-m&lt;/code&gt; 参数时模板不会生效，适用于简单的一次性提交。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;初始化结果确认&lt;/h2&gt;
&lt;p&gt;逐项执行以下检查，确认初始化结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[x] 用户态 &lt;code&gt;micromamba&lt;/code&gt; 已安装，可管理 Python 环境&lt;/li&gt;
&lt;li&gt;[x] &lt;code&gt;zsh&lt;/code&gt; 已编译安装并设为默认登录 shell&lt;/li&gt;
&lt;li&gt;[x] 个人共享目录与标准子目录结构已建立&lt;/li&gt;
&lt;li&gt;[x] 项目工作区已创建&lt;/li&gt;
&lt;li&gt;[x] Python 3.10 环境（&lt;code&gt;py10&lt;/code&gt;）已创建&lt;/li&gt;
&lt;li&gt;[x] pip 已配置清华镜像源&lt;/li&gt;
&lt;li&gt;[x] Git 提交模板已配置（note/todo/fixed 三段式）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些项目全部通过后，环境即可用于数据集管理、模型下载和实验脚本开发。后续升级仍应保持工具安装在用户目录中，避免与系统包管理器混用。&lt;/p&gt;
</content:encoded></item><item><title>Zsh 中的 Conda 自定义命令框架</title><link>https://pinellia.uk/posts/conda-json-command-framework/</link><guid isPermaLink="true">https://pinellia.uk/posts/conda-json-command-framework/</guid><description>通过 JSON 注册表 + 通用分发器 + 独立函数实现，构建可扩展、可维护的 conda/micromamba 自定义命令框架。</description><pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;长期使用 &lt;code&gt;conda&lt;/code&gt; 或 &lt;code&gt;micromamba&lt;/code&gt; 后，Shell 配置中通常会积累一些高频的自定义操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根据当前目录自动激活某个环境&lt;/li&gt;
&lt;li&gt;快速停用当前环境&lt;/li&gt;
&lt;li&gt;维护“项目目录 -&amp;gt; 环境名”的映射关系&lt;/li&gt;
&lt;li&gt;给 &lt;code&gt;conda&lt;/code&gt; 增加一些自己的子命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最直接的做法，通常是在 &lt;code&gt;~/.zshrc&lt;/code&gt; 里写一个很长的 &lt;code&gt;conda()&lt;/code&gt; 函数，用 &lt;code&gt;case&lt;/code&gt; 分发不同子命令。这个方案能用，但随着逻辑增加，会很快出现几个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.zshrc&lt;/code&gt; 越来越臃肿&lt;/li&gt;
&lt;li&gt;子命令扩展不方便&lt;/li&gt;
&lt;li&gt;help 信息难以维护&lt;/li&gt;
&lt;li&gt;实现和注册耦合太紧，不利于长期演进&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里把自定义 &lt;code&gt;conda&lt;/code&gt; 子命令拆成一个三层的小型框架：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;JSON&lt;/code&gt; 指令定义&lt;/li&gt;
&lt;li&gt;通用分发器&lt;/li&gt;
&lt;li&gt;各自独立的实现函数&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终效果是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.zshrc&lt;/code&gt; 中只保留一行 &lt;code&gt;source&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;执行 &lt;code&gt;conda&lt;/code&gt; 时，除了原有 &lt;code&gt;micromamba&lt;/code&gt; help，还能显示你自己注册的子命令和说明&lt;/li&gt;
&lt;li&gt;新增一个子命令时，只需要：
&lt;ul&gt;
&lt;li&gt;在 JSON 中注册&lt;/li&gt;
&lt;li&gt;写一个实现函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不需要再去修改一大段 &lt;code&gt;case&lt;/code&gt; 分支&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;一、适用场景&lt;/h2&gt;
&lt;p&gt;这套方案适合下面这些人：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;zsh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;底层环境管理器是 &lt;code&gt;micromamba&lt;/code&gt;，但希望继续使用 &lt;code&gt;conda&lt;/code&gt; 这个命令入口&lt;/li&gt;
&lt;li&gt;想给 &lt;code&gt;conda&lt;/code&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;code&gt;conda ac&lt;/code&gt;：按目录映射自动激活环境&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conda de&lt;/code&gt;：快速停用环境&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conda map curr&lt;/code&gt;：把当前目录和当前环境写入映射表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conda map&lt;/code&gt;：直接编辑映射配置&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、最终结构&lt;/h2&gt;
&lt;p&gt;建议把相关文件拆成下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.zshrc
~/.zsh/
  conda-wrapper.zsh
  conda-commands.json
~/.conda_map.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;各文件职责如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.zshrc&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;只负责初始化 shell，并 &lt;code&gt;source&lt;/code&gt; 你的自定义框架&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.zsh/conda-wrapper.zsh&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;框架主逻辑&lt;/li&gt;
&lt;li&gt;包含注册表加载、命令分发、help 输出、各实现函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.zsh/conda-commands.json&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;子命令注册表&lt;/li&gt;
&lt;li&gt;声明每个命令的类型、目标和说明&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.conda_map.ini&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;保存“路径 -&amp;gt; 环境”的映射关系&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三、整体设计思路&lt;/h2&gt;
&lt;p&gt;核心思想很简单：&lt;/p&gt;
&lt;h3&gt;1. &lt;code&gt;conda&lt;/code&gt; 不再直接硬编码全部逻辑&lt;/h3&gt;
&lt;p&gt;不再写这种结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda() {
  case &quot;$1&quot; in
    ac) ... ;;
    de) ... ;;
    map) ... ;;
    *) micromamba &quot;$@&quot; ;;
  esac
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而是改成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先看看 &lt;code&gt;$1&lt;/code&gt; 是否在 JSON 注册表里&lt;/li&gt;
&lt;li&gt;如果在：
&lt;ul&gt;
&lt;li&gt;按注册信息执行对应逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果不在：
&lt;ul&gt;
&lt;li&gt;回落到原始 &lt;code&gt;micromamba &quot;$@&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样你自己的子命令和 &lt;code&gt;micromamba&lt;/code&gt; 原生能力就自然共存了。&lt;/p&gt;
&lt;h3&gt;2. JSON 只管声明，不管实现&lt;/h3&gt;
&lt;p&gt;JSON 负责定义：&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;help 描述&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ac&lt;/code&gt; 对应某个 zsh 函数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;de&lt;/code&gt; 对应 &lt;code&gt;micromamba deactivate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;map&lt;/code&gt; 对应某个 zsh 函数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是“注册”和“实现”分离。&lt;/p&gt;
&lt;h3&gt;3. 实现函数彼此独立&lt;/h3&gt;
&lt;p&gt;每个子命令的业务逻辑用独立函数承载，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_conda_cmd_ac&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_conda_cmd_map&lt;/code&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;h3&gt;第一步：确保 &lt;code&gt;micromamba&lt;/code&gt; 已初始化&lt;/h3&gt;
&lt;p&gt;你的 &lt;code&gt;~/.zshrc&lt;/code&gt; 中应先有 &lt;code&gt;micromamba shell init&lt;/code&gt; 生成的初始化内容，典型形式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# &amp;gt;&amp;gt;&amp;gt; mamba initialize &amp;gt;&amp;gt;&amp;gt;
export MAMBA_EXE=&quot;$HOME/micromamba/micromamba&quot;
export MAMBA_ROOT_PREFIX=&quot;$HOME/micromamba&quot;
__mamba_setup=&quot;$(&quot;$MAMBA_EXE&quot; shell hook --shell zsh --root-prefix &quot;$MAMBA_ROOT_PREFIX&quot; 2&amp;gt; /dev/null)&quot;
if [ $? -eq 0 ]; then
    eval &quot;$__mamba_setup&quot;
else
    alias micromamba=&quot;$MAMBA_EXE&quot;
fi
unset __mamba_setup
# &amp;lt;&amp;lt;&amp;lt; mamba initialize &amp;lt;&amp;lt;&amp;lt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一段的作用是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置 &lt;code&gt;MAMBA_EXE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;设置 &lt;code&gt;MAMBA_ROOT_PREFIX&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;把 &lt;code&gt;micromamba&lt;/code&gt; 的 shell hook 注入当前 shell&lt;/li&gt;
&lt;li&gt;让 &lt;code&gt;activate/deactivate&lt;/code&gt; 等命令在 shell 环境里生效&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：这一步非常关键。因为环境激活本质上不是一个普通子进程动作，而是要修改当前 shell 的环境变量。&lt;/p&gt;
&lt;h3&gt;第二步：在 &lt;code&gt;~/.zshrc&lt;/code&gt; 里只保留简短加载语句&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;mamba initialize&lt;/code&gt; 之后添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source &quot;$HOME/.zsh/conda-wrapper.zsh&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 &lt;code&gt;~/.zshrc&lt;/code&gt; 本身就不会塞满业务逻辑。&lt;/p&gt;
&lt;p&gt;推荐结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# &amp;gt;&amp;gt;&amp;gt; mamba initialize &amp;gt;&amp;gt;&amp;gt;
# ... micromamba init generated block ...
# &amp;lt;&amp;lt;&amp;lt; mamba initialize &amp;lt;&amp;lt;&amp;lt;

source &quot;$HOME/.zsh/conda-wrapper.zsh&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第三步：创建注册表文件 &lt;code&gt;~/.zsh/conda-commands.json&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;这个文件是整个框架的命令声明中心。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;commands&quot;: {
    &quot;ac&quot;: {
      &quot;type&quot;: &quot;function&quot;,
      &quot;target&quot;: &quot;_conda_cmd_ac&quot;,
      &quot;description&quot;: &quot;根据当前目录匹配 ~/.conda_map.ini 并激活对应环境（可用于手动触发）。&quot;
    },
    &quot;de&quot;: {
      &quot;type&quot;: &quot;micromamba&quot;,
      &quot;target&quot;: &quot;deactivate&quot;,
      &quot;description&quot;: &quot;停用当前环境（等价于 micromamba deactivate）。&quot;
    },
    &quot;map&quot;: {
      &quot;type&quot;: &quot;function&quot;,
      &quot;target&quot;: &quot;_conda_cmd_map&quot;,
      &quot;description&quot;: &quot;维护路径-&amp;gt;环境映射：map curr 写入当前目录；map 打开 ~/.conda_map.ini。&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字段说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;commands&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;所有自定义子命令的集合&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;每个子命令 key，例如 &lt;code&gt;ac&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;就是用户执行的 &lt;code&gt;conda ac&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;命令类型&lt;/li&gt;
&lt;li&gt;当前支持：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;function&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;micromamba&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;target&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;目标执行体&lt;/li&gt;
&lt;li&gt;若 &lt;code&gt;type=function&lt;/code&gt;，则是 zsh 函数名&lt;/li&gt;
&lt;li&gt;若 &lt;code&gt;type=micromamba&lt;/code&gt;，则是转发给 &lt;code&gt;micromamba&lt;/code&gt; 的子命令&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;用于 help 输出的说明文字&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一步完成后，你的子命令元信息就集中管理起来了。&lt;/p&gt;
&lt;h3&gt;第四步：创建框架主文件 &lt;code&gt;~/.zsh/conda-wrapper.zsh&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;这是整个系统的核心。它一般需要包含下面几部分：&lt;/p&gt;
&lt;h4&gt;1. 注册表缓存结构&lt;/h4&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typeset -g _CONDA_CMD_REGISTRY_FILE=&quot;$HOME/.zsh/conda-commands.json&quot;
typeset -g _CONDA_CMD_REGISTRY_LOADED=0
typeset -gA _CONDA_CMD_TYPE_MAP
typeset -gA _CONDA_CMD_TARGET_MAP
typeset -gA _CONDA_CMD_DESC_MAP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保存 JSON 文件位置&lt;/li&gt;
&lt;li&gt;标记是否已经加载过注册表&lt;/li&gt;
&lt;li&gt;用关联数组缓存命令类型、目标和说明&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样 shell 会在首次使用时读取 JSON，之后直接用内存数据，不需要每次反复解析文件。&lt;/p&gt;
&lt;h4&gt;2. 统一执行 &lt;code&gt;micromamba&lt;/code&gt; 的函数&lt;/h4&gt;
&lt;p&gt;建议做一个包装器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_conda_exec_mamba() {
    if command -v micromamba &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
        micromamba &quot;$@&quot;
    elif [[ -n &quot;$MAMBA_EXE&quot; &amp;amp;&amp;amp; -x &quot;$MAMBA_EXE&quot; ]]; then
        &quot;$MAMBA_EXE&quot; &quot;$@&quot;
    else
        echo &quot;未找到 micromamba，请先完成 micromamba 初始化。&quot; &amp;gt;&amp;amp;2
        return 127
    fi
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数解决两个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;micromamba&lt;/code&gt; 在某些子 shell 中可能不在 PATH&lt;/li&gt;
&lt;li&gt;但 &lt;code&gt;MAMBA_EXE&lt;/code&gt; 可能已经可用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;统一封装后，你后续所有转发都走它，避免到处重复写兜底逻辑。&lt;/p&gt;
&lt;h4&gt;3. 注册表加载器&lt;/h4&gt;
&lt;p&gt;这是最关键的一部分。思路是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;python3&lt;/code&gt; 解析 JSON&lt;/li&gt;
&lt;li&gt;输出 tab 分隔的数据&lt;/li&gt;
&lt;li&gt;zsh 再把这些内容读进关联数组&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;典型实现思路如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_conda_load_command_registry() {
    (( _CONDA_CMD_REGISTRY_LOADED )) &amp;amp;&amp;amp; return 0
    [[ -f &quot;$_CONDA_CMD_REGISTRY_FILE&quot; ]] || return 1

    local key type target desc
    while IFS=$&apos;\t&apos; read -r key type target desc; do
        [[ -z &quot;$key&quot; || -z &quot;$type&quot; || -z &quot;$target&quot; ]] &amp;amp;&amp;amp; continue
        _CONDA_CMD_TYPE_MAP[$key]=&quot;$type&quot;
        _CONDA_CMD_TARGET_MAP[$key]=&quot;$target&quot;
        _CONDA_CMD_DESC_MAP[$key]=&quot;$desc&quot;
    done &amp;lt; &amp;lt;(
        python3 - &quot;$_CONDA_CMD_REGISTRY_FILE&quot; &amp;lt;&amp;lt;&apos;PY&apos;
import json
import sys

path = sys.argv[1]
with open(path, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
    data = json.load(f)

for key, spec in data.get(&quot;commands&quot;, {}).items():
    cmd_type = str(spec.get(&quot;type&quot;, &quot;&quot;)).strip()
    target = str(spec.get(&quot;target&quot;, &quot;&quot;)).strip()
    desc = str(spec.get(&quot;description&quot;, &quot;&quot;)).strip()
    desc = desc.replace(&quot;\t&quot;, &quot; &quot;).replace(&quot;\n&quot;, &quot; &quot;)
    if key and cmd_type and target:
        print(f&quot;{key}\t{cmd_type}\t{target}\t{desc}&quot;)
PY
    )

    _CONDA_CMD_REGISTRY_LOADED=1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有几个细节很重要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;read -r key type target desc&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;必须读 4 列&lt;/li&gt;
&lt;li&gt;如果只读 3 列，&lt;code&gt;description&lt;/code&gt; 就不会进缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;desc.replace(&quot;\t&quot;, &quot; &quot;).replace(&quot;\n&quot;, &quot; &quot;)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;防止描述里出现换行或 tab，破坏 shell 侧解析&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_CONDA_CMD_REGISTRY_LOADED&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;避免每次都反复解析 JSON，提高性能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 通用分发器&lt;/h4&gt;
&lt;p&gt;分发器根据注册表决定如何执行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_conda_dispatch() {
    local subcommand=&quot;$1&quot;
    shift

    _conda_load_command_registry || return 1

    local cmd_type=&quot;${_CONDA_CMD_TYPE_MAP[$subcommand]}&quot;
    local target=&quot;${_CONDA_CMD_TARGET_MAP[$subcommand]}&quot;
    [[ -z &quot;$cmd_type&quot; || -z &quot;$target&quot; ]] &amp;amp;&amp;amp; return 1

    case &quot;$cmd_type&quot; in
        function)
            if typeset -f &quot;$target&quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
                &quot;$target&quot; &quot;$@&quot;
            else
                echo &quot;conda 子命令 &apos;$subcommand&apos; 对应函数 &apos;$target&apos; 不存在。&quot; &amp;gt;&amp;amp;2
                return 1
            fi
            ;;
        micromamba)
            _conda_exec_mamba &quot;$target&quot; &quot;$@&quot;
            ;;
        *)
            echo &quot;conda 子命令 &apos;$subcommand&apos; 的类型 &apos;$cmd_type&apos; 不支持。&quot; &amp;gt;&amp;amp;2
            return 1
            ;;
    esac
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段逻辑的价值在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;conda&lt;/code&gt; 本身不再关心每个命令细节&lt;/li&gt;
&lt;li&gt;它只负责查注册表，然后调用正确执行方式&lt;/li&gt;
&lt;li&gt;后续如果你想支持更多类型，比如：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;alias&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;script&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shell&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;python&lt;/code&gt;
也可以继续扩展&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5. 自定义 help 输出&lt;/h4&gt;
&lt;p&gt;当用户只输入 &lt;code&gt;conda&lt;/code&gt; 时，希望除了原生 &lt;code&gt;micromamba&lt;/code&gt; help，还能看到自定义命令及说明。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_conda_print_custom_help() {
    _conda_load_command_registry || return 0

    local cmd
    echo &quot;&quot;
    echo &quot;Custom subcommands:&quot;
    for cmd in ${(k)_CONDA_CMD_TYPE_MAP}; do
        local desc=&quot;${_CONDA_CMD_DESC_MAP[$cmd]}&quot;
        if [[ -n &quot;$desc&quot; ]]; then
            echo &quot;  ${cmd} - ${desc}&quot;
        else
            echo &quot;  ${cmd}&quot;
        fi
    done
    echo &quot;&quot;
    echo &quot;Use &apos;conda &amp;lt;subcommand&amp;gt; ...&apos; to run a custom command.&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出效果类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Custom subcommands:
  de - 停用当前环境（等价于 micromamba deactivate）。
  ac - 根据当前目录匹配 ~/.conda_map.ini 并激活对应环境（可用于手动触发）。
  map - 维护路径-&amp;gt;环境映射：map curr 写入当前目录；map 打开 ~/.conda_map.ini。

Use &apos;conda &amp;lt;subcommand&amp;gt; ...&apos; to run a custom command.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样用户执行 &lt;code&gt;conda&lt;/code&gt; 时，既不会丢掉原生帮助，又能顺便看到你自己的扩展命令。&lt;/p&gt;
&lt;h4&gt;6. &lt;code&gt;conda()&lt;/code&gt; 统一入口&lt;/h4&gt;
&lt;p&gt;最后把入口收束成一个非常干净的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda() {
    if [[ $# -eq 0 ]]; then
        _conda_exec_mamba
        _conda_print_custom_help
        return 0
    fi

    local subcommand=&quot;$1&quot;
    if [[ -n &quot;$subcommand&quot; ]] &amp;amp;&amp;amp; _conda_dispatch &quot;$subcommand&quot; &quot;${@:2}&quot;; then
        return $?
    fi
    _conda_exec_mamba &quot;$@&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的行为很自然：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无参数：显示原生 help + 自定义 help&lt;/li&gt;
&lt;li&gt;注册命令：走自定义逻辑&lt;/li&gt;
&lt;li&gt;非注册命令：回落到原始 &lt;code&gt;micromamba&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是整个框架的主入口。&lt;/p&gt;
&lt;h2&gt;五、如何实现具体子命令&lt;/h2&gt;
&lt;h3&gt;1. &lt;code&gt;ac&lt;/code&gt;：根据目录映射激活环境&lt;/h3&gt;
&lt;p&gt;这个命令通常会依赖两个辅助函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_load_auto_map&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_find_env_for_dir&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后由 &lt;code&gt;_auto_activate_on_cd&lt;/code&gt; 决定是否切换环境，最后由 &lt;code&gt;_conda_cmd_ac&lt;/code&gt; 调用它。&lt;/p&gt;
&lt;p&gt;思路如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读取 &lt;code&gt;~/.conda_map.ini&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;找到与当前目录最匹配的前缀&lt;/li&gt;
&lt;li&gt;如果找到目标环境且当前环境不同，则执行 &lt;code&gt;conda activate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果没找到目标环境且当前环境不是 &lt;code&gt;base&lt;/code&gt;，则执行 &lt;code&gt;conda deactivate&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样你只需要运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda ac
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就可以手动按目录切环境。&lt;/p&gt;
&lt;h3&gt;2. &lt;code&gt;de&lt;/code&gt;：快速停用环境&lt;/h3&gt;
&lt;p&gt;这个命令最适合注册成 &lt;code&gt;micromamba&lt;/code&gt; 类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;de&quot;: {
  &quot;type&quot;: &quot;micromamba&quot;,
  &quot;target&quot;: &quot;deactivate&quot;,
  &quot;description&quot;: &quot;停用当前环境（等价于 micromamba deactivate）。&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样无需写实现函数，框架会自动转发到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;micromamba deactivate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;优点是非常轻量。&lt;/p&gt;
&lt;h3&gt;3. &lt;code&gt;map&lt;/code&gt;：维护目录和环境映射&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;map&lt;/code&gt; 适合写成函数型命令，因为它有分支逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;conda map curr&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;把当前目录和当前激活环境写入 &lt;code&gt;~/.conda_map.ini&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conda map&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;直接打开 &lt;code&gt;~/.conda_map.ini&lt;/code&gt; 编辑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这类“带业务状态、带文件读写”的命令，用函数承载更清晰。&lt;/p&gt;
&lt;h2&gt;六、目录环境映射文件怎么设计&lt;/h2&gt;
&lt;p&gt;映射文件 &lt;code&gt;~/.conda_map.ini&lt;/code&gt; 可以做成很简单的键值格式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/home/&amp;lt;username&amp;gt;/project/a = py38
/home/&amp;lt;username&amp;gt;/project/b = ml
/home/&amp;lt;username&amp;gt;/workspace/demo = testenv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解析规则可以是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一行一个映射&lt;/li&gt;
&lt;li&gt;&lt;code&gt;=&lt;/code&gt; 左边是目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;=&lt;/code&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;code&gt;/home/&amp;lt;username&amp;gt;/project/a/subdir&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;已有映射：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/home/&amp;lt;username&amp;gt;/project = base-env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/home/&amp;lt;username&amp;gt;/project/a = py38&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终应选择 &lt;code&gt;py38&lt;/code&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;code&gt;conda lsmap&lt;/code&gt;，用于打印当前所有映射。&lt;/p&gt;
&lt;h3&gt;方式一：函数型命令&lt;/h3&gt;
&lt;p&gt;先在 JSON 中注册：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;lsmap&quot;: {
  &quot;type&quot;: &quot;function&quot;,
  &quot;target&quot;: &quot;_conda_cmd_lsmap&quot;,
  &quot;description&quot;: &quot;列出当前所有目录到环境的映射。&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再在 &lt;code&gt;conda-wrapper.zsh&lt;/code&gt; 中实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_conda_cmd_lsmap() {
    local file=&quot;$HOME/.conda_map.ini&quot;
    if [[ ! -f &quot;$file&quot; ]]; then
        echo &quot;映射文件不存在：$file&quot;
        return 1
    fi
    sed &apos;/^\s*[#;]/d; /^\s*$/d&apos; &quot;$file&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重新加载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在就能使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda lsmap
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且 &lt;code&gt;conda&lt;/code&gt; help 中也会自动出现对应说明。&lt;/p&gt;
&lt;h3&gt;方式二：直接转发到 &lt;code&gt;micromamba&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;如果你只是想做某个简短别名，也可以直接注册成 &lt;code&gt;micromamba&lt;/code&gt; 类型。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;rm&quot;: {
  &quot;type&quot;: &quot;micromamba&quot;,
  &quot;target&quot;: &quot;remove&quot;,
  &quot;description&quot;: &quot;删除当前环境中的包（等价于 micromamba remove）。&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就能用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda rm numpy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会等价执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;micromamba remove numpy
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;八、为什么这种设计比在 &lt;code&gt;~/.zshrc&lt;/code&gt; 里硬写 &lt;code&gt;case&lt;/code&gt; 更好&lt;/h2&gt;
&lt;h3&gt;1. 结构更清晰&lt;/h3&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;职责很明确。&lt;/p&gt;
&lt;h3&gt;2. 扩展成本更低&lt;/h3&gt;
&lt;p&gt;新增命令只需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;改 JSON&lt;/li&gt;
&lt;li&gt;加函数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不需要反复修改分发主逻辑。&lt;/p&gt;
&lt;h3&gt;3. help 自动生成&lt;/h3&gt;
&lt;p&gt;命令和说明写在同一个 JSON 中，不容易出现“实现变了、帮助文案没改”的情况。&lt;/p&gt;
&lt;h3&gt;4. &lt;code&gt;~/.zshrc&lt;/code&gt; 更干净&lt;/h3&gt;
&lt;p&gt;把复杂逻辑移出 &lt;code&gt;~/.zshrc&lt;/code&gt; 后，shell 初始化文件更易读，也更方便迁移和备份。&lt;/p&gt;
&lt;h3&gt;5. 后续可继续演化&lt;/h3&gt;
&lt;p&gt;比如未来你可以进一步加上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hidden: true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;usage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;examples&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aliases&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;group&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;order&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这个小框架逐步做成真正可维护的 CLI 扩展层。&lt;/p&gt;
&lt;h2&gt;九、部署时的常见坑&lt;/h2&gt;
&lt;h3&gt;1. &lt;code&gt;description&lt;/code&gt; 没显示&lt;/h3&gt;
&lt;p&gt;这是最容易踩的坑之一。&lt;/p&gt;
&lt;p&gt;如果你 JSON 里有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;description&quot;: &quot;xxx&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但 help 里没显示，通常原因是 shell 读取时没有把描述字段读出来。&lt;/p&gt;
&lt;p&gt;比如如果你写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while IFS=$&apos;\t&apos; read -r key type target; do
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那就只读了 3 列，第四列 &lt;code&gt;desc&lt;/code&gt; 会丢失。&lt;/p&gt;
&lt;p&gt;正确写法必须是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while IFS=$&apos;\t&apos; read -r key type target desc; do
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且要写入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_CONDA_CMD_DESC_MAP[$key]=&quot;$desc&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. &lt;code&gt;micromamba&lt;/code&gt; 在某些 shell 中不可用&lt;/h3&gt;
&lt;p&gt;有时你手动 &lt;code&gt;source&lt;/code&gt; 某个文件时，&lt;code&gt;micromamba&lt;/code&gt; 不一定在 PATH 里。建议统一通过 &lt;code&gt;_conda_exec_mamba&lt;/code&gt; 执行，并回退到 &lt;code&gt;$MAMBA_EXE&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;否则会出现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;command not found: micromamba
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. JSON 描述里包含换行或 tab&lt;/h3&gt;
&lt;p&gt;如果你直接把 JSON 的 &lt;code&gt;description&lt;/code&gt; 原样打印为 tab 分隔数据，shell 解析时可能错位。&lt;/p&gt;
&lt;p&gt;建议在 Python 侧先做清理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;desc = desc.replace(&quot;\t&quot;, &quot; &quot;).replace(&quot;\n&quot;, &quot; &quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. &lt;code&gt;activate&lt;/code&gt; / &lt;code&gt;deactivate&lt;/code&gt; 需要在 shell 环境生效&lt;/h3&gt;
&lt;p&gt;环境切换不是普通可执行程序逻辑。如果 &lt;code&gt;micromamba shell hook&lt;/code&gt; 没有正确初始化，&lt;code&gt;activate/deactivate&lt;/code&gt; 就可能行为不符合预期。&lt;/p&gt;
&lt;p&gt;所以一定要确保 &lt;code&gt;mamba initialize&lt;/code&gt; 块有效。&lt;/p&gt;
&lt;h2&gt;十、可以继续增强的方向&lt;/h2&gt;
&lt;p&gt;这套框架已经够实用，但如果你想把它继续产品化，还可以加这些能力。&lt;/p&gt;
&lt;h3&gt;1. 命令顺序控制&lt;/h3&gt;
&lt;p&gt;现在 help 中的顺序通常取决于解析和关联数组的枚举方式，不一定稳定。你可以在 JSON 中增加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;order&quot;: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 Python 解析时按 &lt;code&gt;order&lt;/code&gt; 排序输出。&lt;/p&gt;
&lt;h3&gt;2. 支持别名&lt;/h3&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;deactivate&quot;: {
  &quot;type&quot;: &quot;micromamba&quot;,
  &quot;target&quot;: &quot;deactivate&quot;,
  &quot;description&quot;: &quot;停用当前环境。&quot;,
  &quot;aliases&quot;: [&quot;de&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样 help 中可以同时显示别名，或者允许多个入口指向同一个实现。&lt;/p&gt;
&lt;h3&gt;3. 支持示例输出&lt;/h3&gt;
&lt;p&gt;增加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;examples&quot;: [
  &quot;conda map curr&quot;,
  &quot;conda ac&quot;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 &lt;code&gt;conda help &amp;lt;subcommand&amp;gt;&lt;/code&gt; 中展示更详细帮助。&lt;/p&gt;
&lt;h3&gt;4. 支持 &lt;code&gt;conda commands&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;可以新增一个框架级命令，列出所有已注册命令及类型、说明，用于调试注册表。&lt;/p&gt;
&lt;h3&gt;5. 自动监听 &lt;code&gt;cd&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;你当前的自动激活逻辑如果只是手动调用 &lt;code&gt;conda ac&lt;/code&gt;，已经够稳。如果想进一步实现“每次 &lt;code&gt;cd&lt;/code&gt; 自动切环境”，可以挂载 &lt;code&gt;chpwd&lt;/code&gt; hook。&lt;/p&gt;
&lt;p&gt;不过这个行为更激进，建议先确认你确实需要，否则有时会让 shell 行为变得过于“主动”。&lt;/p&gt;
&lt;h2&gt;十一、一套可落地的最小工作流程&lt;/h2&gt;
&lt;p&gt;如果你现在要从零部署，推荐按下面顺序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保证 &lt;code&gt;micromamba shell init&lt;/code&gt; 已在 &lt;code&gt;~/.zshrc&lt;/code&gt; 中生效&lt;/li&gt;
&lt;li&gt;新建目录：&lt;pre&gt;&lt;code&gt;mkdir -p ~/.zsh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;创建注册表：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.zsh/conda-commands.json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;创建主逻辑文件：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.zsh/conda-wrapper.zsh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;~/.zshrc&lt;/code&gt; 中加入：&lt;pre&gt;&lt;code&gt;source &quot;$HOME/.zsh/conda-wrapper.zsh&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;重新加载：&lt;pre&gt;&lt;code&gt;source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;验证：&lt;pre&gt;&lt;code&gt;conda
conda ac
conda de
conda map
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;十二、总结&lt;/h2&gt;
&lt;p&gt;这套方案本质上是把 shell 中零散的“命令快捷方式”升级成了一个简易命令框架。&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;code&gt;~/.zshrc&lt;/code&gt; 保持干净&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过 &lt;code&gt;JSON 注册 + 通用分发 + 独立实现&lt;/code&gt; 这三层结构，你就可以把原本容易失控的 &lt;code&gt;conda()&lt;/code&gt; 包装函数，整理成一个长期可演进的小系统。&lt;/p&gt;
&lt;p&gt;如果你的 shell 中已经存在越来越多的“我自己常用的小命令”，这类思路其实不只适用于 &lt;code&gt;conda&lt;/code&gt;，同样也可以推广到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git&lt;/code&gt; 增强命令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker&lt;/code&gt; 快捷命令&lt;/li&gt;
&lt;li&gt;项目脚手架命令&lt;/li&gt;
&lt;li&gt;本地开发环境切换命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;适合迁移到这套结构的判断标准很直接：自定义命令已经需要独立说明、频繁增删，或者一个 &lt;code&gt;case&lt;/code&gt; 分支开始牵动其他逻辑。只有少量固定别名时，继续保留简单函数反而更容易维护。&lt;/p&gt;
</content:encoded></item><item><title>机器学习环境配置</title><link>https://pinellia.uk/posts/machine-learning-env/</link><guid isPermaLink="true">https://pinellia.uk/posts/machine-learning-env/</guid><description>在无 root 权限的服务器上使用 micromamba 配置隔离、轻量且可复现的机器学习环境，并接入远程 VS Code 的 Python 环境管理。</description><pubDate>Tue, 28 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么选择 micromamba&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;micromamba&lt;/code&gt; 是使用 &lt;code&gt;conda-forge&lt;/code&gt; channel 的 Conda 兼容环境管理器。相比完整 Conda，它更适合无 root 权限的服务器和需要控制安装体积的场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用静态链接的 C++ 可执行文件，安装后可以直接运行；&lt;/li&gt;
&lt;li&gt;默认使用 &lt;code&gt;conda-forge&lt;/code&gt; channel，软件包覆盖范围较广；&lt;/li&gt;
&lt;li&gt;不需要 root 权限，安装和卸载都在用户目录中完成；&lt;/li&gt;
&lt;li&gt;安装体积较小，适合服务器环境。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;micromamba&lt;/code&gt; 也可以管理 JDK、Node.js 和 CUDA 等 Conda 包。是否使用这些包，仍应根据项目需求决定。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
现在可以使用conda进行jdk,node,甚至cuda的安装管理以及隔离&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;安装 micromamba&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
下面使用官方安装脚本，不依赖 Homebrew。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;官方指导：&lt;a href=&quot;https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html&quot;&gt;link&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;mac, linux, windows git bash:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;${SHELL}&quot; &amp;lt;(curl -L micro.mamba.pm/install.sh)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自动升级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;micromamba self-update
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;在远程 VS Code 中使用 micromamba&lt;/h2&gt;
&lt;p&gt;安装 &lt;strong&gt;Python&lt;/strong&gt; 与 &lt;strong&gt;Python Environments&lt;/strong&gt; 扩展后，可以让远程 VS Code 将 &lt;code&gt;micromamba&lt;/code&gt; 作为 Conda 兼容的环境管理器使用。&lt;/p&gt;
&lt;h2&gt;远程主机设置&lt;/h2&gt;
&lt;p&gt;通过 Remote SSH 连接服务器后，打开命令面板，执行 &lt;code&gt;Preferences: Open Remote Settings (JSON)&lt;/code&gt;。对应的远程配置文件通常位于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.vscode-server/data/Machine/settings.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加入以下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;python-envs.defaultEnvManager&quot;: &quot;ms-python.python:conda&quot;,
  &quot;python.condaPath&quot;: &quot;~/micromamba/micromamba&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;python.condaPath&lt;/code&gt; 必须指向远程服务器上的 &lt;code&gt;micromamba&lt;/code&gt; 可执行文件。如果安装位置不同，可以先执行以下命令确认：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;command -v micromamba
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
示例使用 &lt;code&gt;~&lt;/code&gt; 表示当前用户的 home 目录，避免在共享配置、截图或文章中暴露真实用户名。请不要直接写入 &lt;code&gt;/home/&amp;lt;username&amp;gt;/...&lt;/code&gt; 或其他包含个人账号的绝对路径。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;项目设置&lt;/h2&gt;
&lt;p&gt;在项目根目录创建或编辑 &lt;code&gt;.vscode/settings.json&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;python-envs.pythonProjects&quot;: [
    {
      &quot;path&quot;: &quot;.&quot;,
      &quot;envManager&quot;: &quot;ms-python.python:conda&quot;,
      &quot;packageManager&quot;: &quot;ms-python.python:conda&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里将当前目录声明为 Python 项目，并指定使用 Conda 兼容的环境与包管理器；实际执行程序由远程主机设置中的 &lt;code&gt;python.condaPath&lt;/code&gt; 指向 &lt;code&gt;micromamba&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
&lt;code&gt;python.condaPath&lt;/code&gt; 属于远程主机级设置，不要提交到项目的 &lt;code&gt;.vscode/settings.json&lt;/code&gt;。这样既能避免泄露服务器目录结构，也能让不同开发者分别配置自己的安装路径。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;保存后执行 &lt;code&gt;Developer: Reload Window&lt;/code&gt;，再通过 &lt;code&gt;Python: Select Interpreter&lt;/code&gt; 或侧边栏的 Python Environments 视图选择环境。若环境未出现，可执行 &lt;code&gt;Python Environments: Refresh All Environment Managers&lt;/code&gt; 后重试。&lt;/p&gt;
&lt;h2&gt;按目录自动加载 Conda 环境&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;使用bashrc自动加载(&lt;strong&gt;仅在终端创建时自动&lt;/strong&gt;)文件夹的 conda 环境&lt;/p&gt;
&lt;p&gt;可使用man命令来加载最合适的环境(未指定则用父文件夹的)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- ~/.zshrc --&amp;gt;
# --- 自动按路径激活 micromamba 环境（从 ~/.conda_map.ini 读取） ---
_load_auto_map() {
    local file=&quot;$HOME/.conda_map.ini&quot;
    typeset -gA AUTO_ACTIVATE_MAP
    [[ -f &quot;$file&quot; ]] || return
    local line key val
    while IFS= read -r line || [[ -n &quot;$line&quot; ]]; do
        # 删除注释（# 或 ;）和首尾空白
        line=&quot;${line%%[#;]*}&quot;
        line=&quot;${line#&quot;${line%%[![:space:]]*}&quot;}&quot;
        line=&quot;${line%&quot;${line##*[![:space:]]}&quot;}&quot;
        [[ -z &quot;$line&quot; ]] &amp;amp;&amp;amp; continue
        if [[ &quot;$line&quot; == *=* ]]; then
            key=&quot;${line%%=*}&quot;
            val=&quot;${line#*=}&quot;
            key=&quot;${key#&quot;${key%%[![:space:]]*}&quot;}&quot;; key=&quot;${key%&quot;${key##*[![:space:]]}&quot;}&quot;
            val=&quot;${val#&quot;${val%%[![:space:]]*}&quot;}&quot;; val=&quot;${val%&quot;${val##*[![:space:]]}&quot;}&quot;
            AUTO_ACTIVATE_MAP[$key]=&quot;$val&quot;
        fi
    done &amp;lt; &quot;$file&quot;
}

_find_env_for_dir() {
    local pwd=&quot;$PWD&quot;
    local best_prefix=&quot;&quot;
    local best_env=&quot;&quot;
    local prefix
    for prefix in &quot;${(@k)AUTO_ACTIVATE_MAP}&quot;; do
        # 确保以目录前缀匹配（支持不带斜杠的写法）
        if [[ &quot;${pwd%/}&quot; == &quot;${prefix%/}&quot;* ]]; then
            if (( ${#prefix} &amp;gt; ${#best_prefix} )); then
                best_prefix=&quot;$prefix&quot;
                best_env=&quot;${AUTO_ACTIVATE_MAP[$prefix]}&quot;
            fi
        fi
    done
    printf &apos;%s&apos; &quot;$best_env&quot;
}

_auto_activate_on_cd() {
    _load_auto_map
    local target_env cur_env
    target_env=&quot;$(_find_env_for_dir)&quot;

    cur_env=&quot;${CONDA_DEFAULT_ENV:-}&quot;
    if [[ -z &quot;$cur_env&quot; &amp;amp;&amp;amp; -n &quot;$CONDA_PREFIX&quot; ]]; then
        cur_env=&quot;$(basename &quot;$CONDA_PREFIX&quot;)&quot;
    fi

    if [[ -n &quot;$target_env&quot; ]]; then
        if [[ &quot;$cur_env&quot; != &quot;$target_env&quot; ]]; then
            conda activate &quot;$target_env&quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || true
        fi
    else
        if [[ -n &quot;$cur_env&quot; &amp;amp;&amp;amp; &quot;$cur_env&quot; != &quot;base&quot; ]]; then
            conda deactivate &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || true
        fi
    fi
}
_auto_activate_on_cd
alias ac=_auto_activate_on_cd
# ac 来手动激活对应环境（前缀匹配激活，子文件夹都可用）
# --- 结束 自动激活 ---
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- ~/.bashrc --&amp;gt;
# --- 自动按路径激活 micromamba 环境（从 ~/.conda_map.ini 读取） ---
_load_auto_map() {
    local file=&quot;$HOME/.conda_map.ini&quot;
    # 使用全局关联数组
    declare -gA AUTO_ACTIVATE_MAP=()
    [[ -f &quot;$file&quot; ]] || return
    local line key val
    while IFS= read -r line || [[ -n &quot;$line&quot; ]]; do
        # 删除注释（# 或 ;）和首尾空白
        line=&quot;${line%%[#;]*}&quot;
        line=&quot;${line#&quot;${line%%[![:space:]]*}&quot;}&quot;
        line=&quot;${line%&quot;${line##*[![:space:]]}&quot;}&quot;
        [[ -z &quot;$line&quot; ]] &amp;amp;&amp;amp; continue
        if [[ &quot;$line&quot; =~ ^([^=]+)=(.+)$ ]]; then
            key=&quot;${BASH_REMATCH[1]}&quot;
            val=&quot;${BASH_REMATCH[2]}&quot;
            key=&quot;${key#&quot;${key%%[![:space:]]*}&quot;}&quot;; key=&quot;${key%&quot;${key##*[![:space:]]}&quot;}&quot;
            val=&quot;${val#&quot;${val%%[![:space:]]*}&quot;}&quot;; val=&quot;${val%&quot;${val##*[![:space:]]}&quot;}&quot;
            AUTO_ACTIVATE_MAP[&quot;$key&quot;]=&quot;$val&quot;
        fi
    done &amp;lt; &quot;$file&quot;
}

_find_env_for_dir() {
    local pwd=&quot;$PWD&quot;
    local best_prefix=&quot;&quot;
    local best_env=&quot;&quot;
    local prefix
    for prefix in &quot;${!AUTO_ACTIVATE_MAP[@]}&quot;; do
        # 确保以目录前缀匹配（支持不带斜杠的写法）
        if [[ &quot;${pwd%/}&quot; == &quot;${prefix%/}&quot;* ]]; then
            if (( ${#prefix} &amp;gt; ${#best_prefix} )); then
                best_prefix=&quot;$prefix&quot;
                best_env=&quot;${AUTO_ACTIVATE_MAP[$prefix]}&quot;
            fi
        fi
    done
    printf &apos;%s&apos; &quot;$best_env&quot;
}

_auto_activate_on_cd() {
    _load_auto_map
    local target_env cur_env
    target_env=&quot;$(_find_env_for_dir)&quot;

    cur_env=&quot;${CONDA_DEFAULT_ENV:-}&quot;
    if [[ -z &quot;$cur_env&quot; &amp;amp;&amp;amp; -n &quot;$CONDA_PREFIX&quot; ]]; then
        cur_env=&quot;$(basename &quot;$CONDA_PREFIX&quot;)&quot;
    fi

    if [[ -n &quot;$target_env&quot; ]]; then
        if [[ &quot;$cur_env&quot; != &quot;$target_env&quot; ]]; then
            conda activate &quot;$target_env&quot; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || true
        fi
    else
        if [[ -n &quot;$cur_env&quot; &amp;amp;&amp;amp; &quot;$cur_env&quot; != &quot;base&quot; ]]; then
            conda deactivate &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || true
        fi
    fi
}
_auto_activate_on_cd
alias ac=_auto_activate_on_cd
# ac 来手动激活对应环境（前缀匹配激活，子文件夹都可用）
# --- 结束 自动激活 ---
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;验收结果&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;无 root 权限或希望减少安装体积时，推荐使用 &lt;code&gt;micromamba&lt;/code&gt; 管理 Conda 环境。&lt;/li&gt;
&lt;li&gt;远程 VS Code 中，将 &lt;code&gt;python.condaPath&lt;/code&gt; 指向 &lt;code&gt;micromamba&lt;/code&gt;，并为项目设置 Conda 环境管理器，即可完成环境发现、选择和包管理。&lt;/li&gt;
&lt;li&gt;主机相关路径应放在远程 Machine 设置中，项目配置只保留可共享内容，避免暴露用户名和服务器目录结构。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Mihomo 设置指南</title><link>https://pinellia.uk/posts/mihomo-config/</link><guid isPermaLink="true">https://pinellia.uk/posts/mihomo-config/</guid><description>整理 Mihomo 在 Android 等场景下的配置补充、模块设置与常见注意事项。</description><pubDate>Sun, 05 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Android&lt;/h2&gt;
&lt;p&gt;将新模块推送到 Mihomo 配置目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adb push &quot;$HOME/Downloads/TT/Clash配置.yaml&quot; /sdcard/Android/Clash
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;macOS&lt;/h2&gt;
&lt;p&gt;macOS 版 Clash Verge Rev 在这台设备上运行不稳定，改为直接运行 Mihomo 内核。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;macOS sudo mv mihomo.plist /Library/LaunchDaemons
# 加载服务
sudo launchctl load /Library/LaunchDaemons/mihomo.plist
# 查看服务
sudo launchctl list | grep mihomo
# 卸载停止服务
sudo launchctl unload /Library/LaunchDaemons/mihomo.plist

# 启动服务
sudo launchctl start mihomo
# 停止服务
sudo launchctl stop mihomo


&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;macOS TUN 与 DNS 排查&lt;/h2&gt;
&lt;p&gt;记录日期：2026-08-12。&lt;/p&gt;
&lt;p&gt;macOS 使用 Mihomo TUN 时，抖音、百度等国内网站明显卡顿，Google 也无法访问。关闭 TUN 后恢复正常，设置系统 SOCKS 代理后 Google 可以访问。&lt;/p&gt;
&lt;p&gt;排查确认问题不在代理节点或规则：即使切换为全局 DIRECT，流量仍然需要经过 TUN 入站协议栈，因此 TUN 路径异常依然会影响连接。将 TUN 从 &lt;code&gt;gvisor&lt;/code&gt; 调整为 &lt;code&gt;system&lt;/code&gt;，移除无效的 &lt;code&gt;device: wlan9&lt;/code&gt;，关闭 &lt;code&gt;strict-route&lt;/code&gt; 和 IPv6，并将 MTU 调整为 1380 后，仍未解决 Google 访问问题。&lt;/p&gt;
&lt;p&gt;最终确认根因是 macOS 的局域网 DNS 没有被 Mihomo 劫持。系统 DNS 指向路由器（例如 &lt;code&gt;192.168.x.1&lt;/code&gt;），同时配置又排除了整个局域网地址段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tun:
  enable: true
  stack: system
  auto-route: true
  auto-detect-interface: false
  dns-hijack:
    - any:53
    - tcp://any:53
  strict-route: false
  mtu: 1380
  route-exclude-address:
    - 192.168.0.0/16
    - fc00::/7

interface-name: en0
ipv6: false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于路由器地址位于被排除的 &lt;code&gt;192.168.0.0/16&lt;/code&gt; 中，发往路由器的 53 端口请求不会进入 TUN，&lt;code&gt;dns-hijack&lt;/code&gt; 无法生效。路由器当时为 &lt;code&gt;www.google.com&lt;/code&gt; 返回了错误的公网地址，而 Mihomo DNS 返回的是正常的 Fake-IP &lt;code&gt;198.18.x.x&lt;/code&gt;。显式设置 SOCKS 代理时，域名直接交给 Mihomo，所以 Google 可以访问。&lt;/p&gt;
&lt;p&gt;解决方法是把 macOS Wi-Fi 的普通 DNS 改为公网 DNS，使 53 端口查询进入 TUN 后由 Mihomo 劫持：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo networksetup -setdnsservers Wi-Fi 223.5.5.5 119.29.29.29
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后可通过以下命令验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dscacheutil -q host -a name www.google.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启 Fake-IP TUN 时，正常结果应为 &lt;code&gt;198.18.x.x&lt;/code&gt;，而不是路由器返回的错误公网 IP。&lt;/p&gt;
&lt;p&gt;修改普通 DNS 不会使 Bonjour/mDNS 失效。mDNS 使用 UDP 5353 和组播地址 &lt;code&gt;224.0.0.251&lt;/code&gt;/&lt;code&gt;FF02::FB&lt;/code&gt;，与普通 DNS 的 53 端口不同；&lt;code&gt;.local&lt;/code&gt; 设备发现、AirPlay、AirDrop 和 Bonjour 打印机仍可正常工作。可能受影响的是由路由器普通 DNS 提供的 &lt;code&gt;nas&lt;/code&gt;、&lt;code&gt;router.lan&lt;/code&gt; 等内部域名，而不是 &lt;code&gt;设备名.local&lt;/code&gt;。可使用 &lt;code&gt;dns-sd -G v4v6 设备名.local&lt;/code&gt; 验证 mDNS。&lt;/p&gt;
</content:encoded></item><item><title>VS Code macOS C++ 环境配置</title><link>https://pinellia.uk/posts/vscode-mac-cpp/</link><guid isPermaLink="true">https://pinellia.uk/posts/vscode-mac-cpp/</guid><description>在 macOS 上配置 VS Code 的 C++ 编译、运行与调试环境，并整理常用插件和设置。</description><pubDate>Wed, 27 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这套配置用于在 macOS 的 VS Code 中编译、运行和调试单文件 C++ 程序。编译器使用系统 Clang，调试器使用 LLDB。&lt;/p&gt;
&lt;h2&gt;扩展&lt;/h2&gt;
&lt;p&gt;安装以下扩展：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C/C++ 语言支持；&lt;/li&gt;
&lt;li&gt;CodeLLDB；&lt;/li&gt;
&lt;li&gt;Code Runner（仅用于快速运行，不代替调试配置）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;配置文件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tasks.json&lt;/code&gt; 定义编译任务；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;launch.json&lt;/code&gt; 定义 F5 调试入口；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;settings.json&lt;/code&gt; 配置头文件关联和 Code Runner 参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- launch.json --&amp;gt;
{
    &quot;version&quot;: &quot;0.2.0&quot;,
    &quot;configurations&quot;: [
        {
            &quot;name&quot;: &quot;Debug C++&quot;,
            &quot;type&quot;: &quot;lldb&quot;,
            &quot;request&quot;: &quot;launch&quot;,
            &quot;program&quot;: &quot;${fileDirname}/bin/${fileBasenameNoExtension}&quot;,
            &quot;args&quot;: [],
            &quot;cwd&quot;: &quot;${workspaceFolder}&quot;,
            &quot;preLaunchTask&quot;: &quot;build&quot;,
            &quot;console&quot;: &quot;integratedTerminal&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- tasks.json --&amp;gt;
{
    &quot;version&quot;: &quot;2.0.0&quot;,
    &quot;tasks&quot;: [
        {
            &quot;label&quot;: &quot;build&quot;,
            &quot;type&quot;: &quot;shell&quot;,
            &quot;command&quot;: &quot;mkdir&quot;,
            &quot;args&quot;: [
                &quot;-p&quot;,
                &quot;${fileDirname}/bin&quot;,
                &quot;&amp;amp;&amp;amp;&quot;,
                &quot;g++&quot;,
                &quot;-std=c++17&quot;,
                &quot;-g&quot;,
                &quot;${file}&quot;,
                &quot;-o&quot;,
                &quot;${fileDirname}/bin/${fileBasenameNoExtension}&quot;
            ],
            &quot;group&quot;: &quot;build&quot;,
            &quot;problemMatcher&quot;: [
                &quot;$gcc&quot;
            ]
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--settings.json -&amp;gt;
{
    // C++扩展配置
    &quot;C_Cpp.default.includePath&quot;: [
        &quot;${workspaceFolder}/cpp_includes&quot;,
        &quot;${workspaceFolder}/**&quot;
    ],
    &quot;C_Cpp.default.compilerPath&quot;: &quot;/usr/bin/g++&quot;,
    &quot;C_Cpp.default.cppStandard&quot;: &quot;c++17&quot;,
    &quot;C_Cpp.default.intelliSenseMode&quot;: &quot;macos-gcc-arm64&quot;,

    // Code Runner配置 - 与C++扩展保持一致的头文件路径
    &quot;code-runner.executorMap&quot;: {
        &quot;cpp&quot;: &quot;cd$workspaceRoot &amp;amp;&amp;amp; mkdir -p bin &amp;amp;&amp;amp; g++ -std=c++17 -O2 -g -I cpp_includes $fullFileName -o ./bin/$fileNameWithoutExt &amp;amp;&amp;amp; ./bin/$fileNameWithoutExt&quot;
    },
    &quot;code-runner.runInTerminal&quot;: true,
    &quot;code-runner.saveFileBeforeRun&quot;: true,
    &quot;code-runner.clearPreviousOutput&quot;: false,
    &quot;code-runner.preserveFocus&quot;: false,
    &quot;code-runner.showExecutionMessage&quot;: true,

    // 文件关联
    &quot;files.associations&quot;: {
        &quot;*.h&quot;: &quot;cpp&quot;,
        &quot;*.hpp&quot;: &quot;cpp&quot;,
        &quot;*.cpp&quot;: &quot;cpp&quot;,
        &quot;*.cc&quot;: &quot;cpp&quot;,
        &quot;*.cxx&quot;: &quot;cpp&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>LeetCode 刷题列表</title><link>https://pinellia.uk/posts/leetcode-list/</link><guid isPermaLink="true">https://pinellia.uk/posts/leetcode-list/</guid><description>整理 LeetCode 刷题记录与题解索引，便于按题目和知识点持续复盘。</description><pubDate>Tue, 26 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这份列表按数据结构和算法主题整理 LeetCode 题目，用于记录刷题范围与后续复盘。编号沿用原始笔记顺序，不代表推荐完成顺序。&lt;/p&gt;
&lt;h2&gt;第一章：基础数据结构&lt;/h2&gt;
&lt;h3&gt;数组双指针&lt;/h3&gt;
&lt;h4&gt;⼆分搜索&lt;/h4&gt;
&lt;ol&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;阶乘函数后 K 个零&lt;/li&gt;
&lt;li&gt;爱吃⾹蕉的珂珂&lt;/li&gt;
&lt;li&gt;在 D 天内送达包裹的能⼒&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;滑动窗⼝&lt;/h4&gt;
&lt;ol&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;/ol&gt;
&lt;h4&gt;其他题⽬&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;三数之和&lt;/li&gt;
&lt;li&gt;四数之和&lt;/li&gt;
&lt;li&gt;⼩于 K 的两数之和&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;/li&gt;
&lt;li&gt;接⾬⽔&lt;/li&gt;
&lt;li&gt;优势洗牌&lt;/li&gt;
&lt;li&gt;较⼩的三数之和&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;链表双指针&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;两数相加&lt;/li&gt;
&lt;li&gt;删除链表的倒数第 N 个结点&lt;/li&gt;
&lt;li&gt;合并两个有序链表&lt;/li&gt;
&lt;li&gt;合并 K 个升序链表&lt;/li&gt;
&lt;li&gt;环形链表&lt;/li&gt;
&lt;li&gt;环形链表 II&lt;/li&gt;
&lt;li&gt;相交链表&lt;/li&gt;
&lt;li&gt;链表的中间结点&lt;/li&gt;
&lt;li&gt;K 个⼀组翻转链表&lt;/li&gt;
&lt;li&gt;删除排序链表中的重复元素&lt;/li&gt;
&lt;li&gt;反转链表 II&lt;/li&gt;
&lt;li&gt;回⽂链表&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;前缀和&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;区域和检索 - 数组不可变&lt;/li&gt;
&lt;li&gt;⼆维区域和检索 - 矩阵不可变&lt;/li&gt;
&lt;li&gt;和为 K 的⼦数组&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;差分数组&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;区间加法&lt;/li&gt;
&lt;li&gt;拼⻋&lt;/li&gt;
&lt;li&gt;航班预订统计&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;队列/栈算法&lt;/h4&gt;
&lt;ol&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;li&gt;滑动窗⼝最⼤值&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;⼆叉堆&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;合并 K 个升序链表&lt;/li&gt;
&lt;li&gt;数组中的第 K 个最⼤元素&lt;/li&gt;
&lt;li&gt;数据流的中位数&lt;/li&gt;
&lt;li&gt;数据流中的第 K ⼤元素&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;数据结构设计&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;LRU 缓存机制&lt;/li&gt;
&lt;li&gt;扁平化嵌套列表迭代器&lt;/li&gt;
&lt;li&gt;O(1) 时间插⼊、删除和获取随机元素&lt;/li&gt;
&lt;li&gt;LFU 缓存&lt;/li&gt;
&lt;li&gt;最⼤频率栈&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第二章：进阶数据结构&lt;/h2&gt;
&lt;h4&gt;⼆叉树&lt;/h4&gt;
&lt;ol&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;li&gt;从中序与后序遍历序列构造⼆叉树&lt;/li&gt;
&lt;li&gt;最⼤⼆叉树&lt;/li&gt;
&lt;li&gt;⼆叉树的层序遍历 II&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;/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;/li&gt;
&lt;li&gt;N 叉树的最⼤深度&lt;/li&gt;
&lt;li&gt;N 叉树的前序遍历&lt;/li&gt;
&lt;li&gt;N 叉树的后序遍历&lt;/li&gt;
&lt;li&gt;寻找重复的⼦树&lt;/li&gt;
&lt;li&gt;单值⼆叉树&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;⼆叉搜索树&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;不同的⼆叉搜索树 II&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;⼆叉搜索树中第 K ⼩的元素&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;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;图论算法&lt;/h3&gt;
&lt;h4&gt;图的遍历&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;所有可能的路径&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;⼆分图&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;判断⼆分图&lt;/li&gt;
&lt;li&gt;可能的⼆分法#### 环检测/拓扑排序&lt;/li&gt;
&lt;li&gt;课程表&lt;/li&gt;
&lt;li&gt;课程表 II&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;并查集算法&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;被围绕的区域&lt;/li&gt;
&lt;li&gt;等式⽅程的可满⾜性&lt;/li&gt;
&lt;li&gt;情侣牵⼿&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;最⼩⽣成树&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;以图判树&lt;/li&gt;
&lt;li&gt;最低成本联通所有城市&lt;/li&gt;
&lt;li&gt;连接所有点的最⼩费⽤&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;最短路径&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;⽹络延迟时间&lt;/li&gt;
&lt;li&gt;概率最⼤的路径&lt;/li&gt;
&lt;li&gt;最⼩体⼒消耗路径&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第三章：暴力搜索&lt;/h2&gt;
&lt;h4&gt;回溯算法&lt;/h4&gt;
&lt;ol&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;li&gt;⼦集&lt;/li&gt;
&lt;li&gt;N 皇后&lt;/li&gt;
&lt;li&gt;⼆叉树的最⼤深度&lt;/li&gt;
&lt;li&gt;⽬标和&lt;/li&gt;
&lt;li&gt;划分为 k 个相等的⼦集&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;DFS 算法&lt;/h4&gt;
&lt;ol&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;li&gt;统计⼦岛屿&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;BFS 算法&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;⼆叉树的层序遍历&lt;/li&gt;
&lt;li&gt;⼆叉树的锯⻮形层序遍历&lt;/li&gt;
&lt;li&gt;⼆叉树的层序遍历 II&lt;/li&gt;
&lt;li&gt;⼆叉树的最⼩深度&lt;/li&gt;
&lt;li&gt;打开转盘锁&lt;/li&gt;
&lt;li&gt;滑动谜题&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第四章：动态规划&lt;/h2&gt;
&lt;h4&gt;⼀维 DP&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;跳跃游戏 II&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;打家劫舍 II&lt;/li&gt;
&lt;li&gt;打家劫舍 III&lt;/li&gt;
&lt;li&gt;最⻓递增⼦序列&lt;/li&gt;
&lt;li&gt;零钱兑换&lt;/li&gt;
&lt;li&gt;俄罗斯套娃信封问题&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;⼆维 DP&lt;/h4&gt;
&lt;ol&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;买卖股票的最佳时机 II&lt;/li&gt;
&lt;li&gt;买卖股票的最佳时机 III&lt;/li&gt;
&lt;li&gt;买卖股票的最佳时机 IV&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;/li&gt;
&lt;li&gt;⾃由之路&lt;/li&gt;
&lt;li&gt;零钱兑换 II&lt;/li&gt;
&lt;li&gt;两个字符串的删除操作&lt;/li&gt;
&lt;li&gt;两个字符串的最⼩ ASCII 删除和&lt;/li&gt;
&lt;li&gt;最⻓公共⼦序列&lt;/li&gt;
&lt;li&gt;K 站中转内最便宜的航班&lt;/li&gt;
&lt;li&gt;鸡蛋掉落&lt;/li&gt;
&lt;li&gt;下降路径最⼩和&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;背包问题&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;分割等和⼦集&lt;/li&gt;
&lt;li&gt;⽬标和&lt;/li&gt;
&lt;li&gt;零钱兑换 II&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第五章：其他经典算法&lt;/h2&gt;
&lt;h4&gt;数学算法&lt;/h4&gt;
&lt;ol&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;位 1 的个数&lt;/li&gt;
&lt;li&gt;2 的幂&lt;/li&gt;
&lt;li&gt;阶乘后的零&lt;/li&gt;
&lt;li&gt;阶乘函数后 K 个零&lt;/li&gt;
&lt;li&gt;计数质数&lt;/li&gt;
&lt;li&gt;丢失的数字&lt;/li&gt;
&lt;li&gt;Nim 游戏&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;/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;/ol&gt;
&lt;h4&gt;区间问题&lt;/h4&gt;
&lt;ol&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;li&gt;单线程 CPU&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Cloudflare 加速实践</title><link>https://pinellia.uk/posts/better-cf/</link><guid isPermaLink="true">https://pinellia.uk/posts/better-cf/</guid><description>记录一次改善 Cloudflare 服务国内访问速度的实践、测试过程与未达预期的原因。</description><pubDate>Wed, 20 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这次测试以 &lt;code&gt;blog.pinellia.uk&lt;/code&gt; 为例，目标是改善 Cloudflare 服务在国内网络中的直连表现。参考方案来自 &lt;a href=&quot;https://cmliussss.com/p/BestWorkers/&quot;&gt;BestWorkers&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;测试结果&lt;/h2&gt;
&lt;h3&gt;ITDOG TCPing 正常&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmYaCGNTTrshP8FbKaCkbfCfpqWMBE3aVE3G8UtmNF2s2U&quot; alt=&quot;1755619518954&quot; /&gt;&lt;/p&gt;
&lt;p&gt;ITDOG 的 TCPing 测试全部通过，但电脑和平板直连时仍然经常无法访问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to blog.pinellia.uk
浏览器：ERR_CONNECTION_CLOSED
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;排除 DNS 解析差异&lt;/h3&gt;
&lt;p&gt;Clash 的 DNS 解析结果与 ITDOG 一致：&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;display: flex; gap: 16px;&quot;&amp;gt;
&amp;lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmfHVxTswG6Yr3B4ZJVjTMzkSzg5BszYAiLabQH6UN3iXt&quot; alt=&quot;1755619893625&quot; style=&quot;width: 49%;&quot;&amp;gt;
&amp;lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmQ1x4uCyp56m4X7q65sS3DJbACVn79DnKv87b2JpjMbpM&quot; alt=&quot;1755619405601&quot; style=&quot;width: 49%;&quot;&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;解析出的 IP 可以 Ping 通，走代理时也能正常连接，因此问题不像是单纯的 DNS 解析错误。测试期间没有找到稳定复现的具体原因；电脑和平板等待一段时间后恢复直连。&lt;/p&gt;
&lt;p&gt;这次调整没有形成可以验证的加速结论。后续排查应分别记录 DNS 结果、TLS 握手、路由和不同运营商网络下的表现，不能只根据 TCPing 全绿判断站点已经可用。&lt;/p&gt;
</content:encoded></item><item><title>夹带私货：简单文件隐写</title><link>https://pinellia.uk/posts/document-hide/</link><guid isPermaLink="true">https://pinellia.uk/posts/document-hide/</guid><description>使用 cat 拼接文件，并通过 binwalk 检测和提取隐藏内容，演示一种简单的文件隐写方法。</description><pubDate>Wed, 20 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一种利用文件格式解析差异实现的简单隐藏方式：把 ZIP 数据追加到图片末尾。图片查看器通常仍按图片格式显示文件，&lt;code&gt;binwalk&lt;/code&gt; 等工具则能识别并提取后面的压缩包。&lt;/p&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://zhuanlan.zhihu.com/p/590378572&quot;&gt;文件隐写示例&lt;/a&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
这不是加密。任何知道文件结构的人都可以扫描并提取追加内容，不适合保护敏感数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;操作过程&lt;/h2&gt;
&lt;h3&gt;隐写&lt;/h3&gt;
&lt;p&gt;先把待隐藏内容打包为 &lt;code&gt;a.zip&lt;/code&gt;，再追加到 &lt;code&gt;b.png&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat a.zip &amp;gt;&amp;gt; b.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图片外观不变，但文件大小会增加。
&amp;lt;p align=&quot;center&quot;&amp;gt;
&amp;lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmTMVbJQhP77CQm4bT5BEudQBCEbExLsDT2ybNQFHTTzGZ&quot; alt=&quot;&quot; width=&quot;40%&quot; /&amp;gt;
&amp;lt;br/&amp;gt;
&amp;lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmXrvW7ob9uXPcmQcdCFBXXX7hbnH6KeXin3kXtyXdcJLZ&quot; alt=&quot;&quot; width=&quot;40%&quot; /&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;h3&gt;拆解&lt;/h3&gt;
&lt;p&gt;这里借用 &lt;code&gt;binwalk&lt;/code&gt;命令进行拆解，也可以使用其他的，建议参考上面的知乎。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;binwalk b.png&lt;/code&gt; 查看文件结构：
&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmaJNcm8N1shbqZRBbURjCsLh5WfQ9Tc4yLbBPMxr93S5G&quot; alt=&quot;&quot; /&gt;
输出中应同时识别出图片和 ZIP 数据。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;binwalk -e b.png&lt;/code&gt; 提取文件：
&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmcY28Q3CDDAEphWpiNH7gYr85r6oXFE3TiC8gp8Vn4unp&quot; alt=&quot;&quot; /&gt;
提取目录会以 ZIP 数据的起始偏移命名，其中包含解压后的文件。
&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmasDZYXmVZMc14CeqJChtWjiMfTa1mviaSLT3c1GGFRUt&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmbqUQSSxruM84e4qZTswg9KbaD4YD5ZbLipGmGXDJHd2J&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;p&gt;建议先把待隐藏文件打包。Word 和 PDF 等格式本身可能包含多个数据结构，直接追加会增加 &lt;code&gt;binwalk&lt;/code&gt; 判断和提取的难度。
&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmddebmCSkKv33jM7X1BVrZPP3QwKyeWp3KaqJrH6r5Cv4&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果自动提取失败，可以根据起止偏移使用 &lt;code&gt;dd&lt;/code&gt; 手动截取。打包只会改善识别和组织方式，不会提供加密保护。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Python 环境配置</title><link>https://pinellia.uk/posts/conda/</link><guid isPermaLink="true">https://pinellia.uk/posts/conda/</guid><description>通过视频和简要选型说明，了解 Miniconda、conda-forge 与 mamba 的适用场景。</description><pubDate>Fri, 08 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这篇短文保留一段 Python 环境配置视频，并补充工具选择结论。需要在无 root 服务器上部署时，可改用体积更小的 &lt;code&gt;micromamba&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;介绍视频&lt;/h2&gt;
&lt;p&gt;&amp;lt;div style=&quot;position: relative; width: 60%; aspect-ratio: 16/9; border-bottom: 20px solid #ffffff00;&quot;&amp;gt;
&amp;lt;iframe src=&quot;https://player.bilibili.com/player.html?bvid=BV1Fm4ZzDEeY&quot;
style=&quot;position: absolute; top: 0; left: 0; width: 100%; height: 100%;&quot;
scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;
&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;选择建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;需要完整 Conda 体验时使用 &lt;code&gt;Miniconda&lt;/code&gt;，代价是安装和环境求解相对较重。&lt;/li&gt;
&lt;li&gt;已经使用 &lt;code&gt;conda-forge&lt;/code&gt; 时，可以用 &lt;code&gt;mamba&lt;/code&gt; 加快依赖求解。&lt;/li&gt;
&lt;li&gt;无 root 权限或希望减少安装体积时，优先考虑 &lt;code&gt;micromamba&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>PTA 天梯赛 L1 刷题总结（二）：10 分题型</title><link>https://pinellia.uk/posts/pta-l1-2/</link><guid isPermaLink="true">https://pinellia.uk/posts/pta-l1-2/</guid><description>总结 PTA 天梯赛 L1 阶段常见的 10 分题型、输入输出规律与精简代码思路。</description><pubDate>Wed, 26 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这组 10 分题主要考查输入输出、基础分支、循环和 STL 的直接使用。题目通常按样例逐组处理，不必为了“最后统一输出”而额外保存全部输入。&lt;/p&gt;
&lt;p&gt;需要反复检查的细节有三项：计数变量初始化、行尾空格和换行位置。&lt;/p&gt;
&lt;h2&gt;1. 数字与拼音映射&lt;/h2&gt;
&lt;h3&gt;L1-007 念数字&lt;/h3&gt;
&lt;p&gt;输入一个整数，逐位输出对应拼音；负数先输出 &lt;code&gt;fu&lt;/code&gt;。数字与拼音是一一映射关系，直接按字符串读取可以保留原始顺序，也能自然处理负号。&lt;/p&gt;
&lt;p&gt;下面保留三种实现用于比较。第一种按整数取模，需要逆序保存各位数字：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

void num(int s)
{
switch (s)
{
case 1:
printf(&quot;yi&quot;);break;
case 2:
printf(&quot;er&quot;);break;
case 3:
printf(&quot;san&quot;);break;
case 4:
printf(&quot;si&quot;);break;
case 5:
printf(&quot;wu&quot;);break;
case 6:
printf(&quot;liu&quot;);break;
case 7:
printf(&quot;qi&quot;);break;
case 8:
printf(&quot;ba&quot;);break;
case 9:
printf(&quot;jiu&quot;);break;
case 0:
printf(&quot;ling&quot;);break;
}
}
int main()
{
int n,i,j;
int a[100]={0};
scanf(&quot;%d&quot;,&amp;amp;n);
if(n&amp;lt;0)
  {
       printf(&quot;fu &quot;);
       n*=(-1);
  }
   i=0;
   while(n/10)
  {
       a[i++]=n%10;
       n/=10;
  }
   a[i]=n;
   for(j=i;j&amp;gt;=0;j--)
  {
       if(j==0) num(a[j]);
       else
      {
           num(a[j]);
           printf(&quot; &quot;);
      }
  }
return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二维字符数组&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;strlen(s)&lt;/code&gt; 获取字符串长度，&lt;code&gt;s[i] - &apos;0&apos;&lt;/code&gt; 将数字字符转换为对应整数。只有两种分支时，可以使用条件运算符简化标志位初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include&amp;lt;cstring&amp;gt;
#include&amp;lt;cstdio&amp;gt;
using namespace std;
int main() {
   char a[10][10] = {&quot;ling&quot;, &quot;yi&quot;, &quot;er&quot;, &quot;san&quot;, &quot;si&quot;, &quot;wu&quot;, &quot;liu&quot;, &quot;qi&quot;, &quot;ba&quot;, &quot;jiu&quot;};
   char s[1000];
   scanf(&quot;%s&quot;,s);
   int len=strlen(s);
   int flag = s[0] == &apos;-&apos; ? 1 : 0;
   if(flag == 1)
      printf(&quot;fu&quot;);
   for(int i = flag; i &amp;lt; len; i++)
  {
       if(i == 0)
      {
           printf(&quot;%s&quot;,a[s[i]-&apos;0&apos;]);
           continue;
      }

       printf(&quot; %s&quot;,a[s[i]-&apos;0&apos;]);
  }
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;string&lt;/code&gt; 数组&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;flag&lt;/code&gt; 记录起始下标。若首字符为 &lt;code&gt;&apos;-&apos;&lt;/code&gt;，先输出 &lt;code&gt;fu&lt;/code&gt;，再从下标 1 开始查表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;

using namespace std;

int main()
{
   string s[10]={&quot;ling&quot;,&quot;yi&quot;,&quot;er&quot;,&quot;san&quot;,&quot;si&quot;,&quot;wu&quot;,&quot;liu&quot;,&quot;qi&quot;,&quot;ba&quot;,&quot;jiu&quot;};
   string a;
   int h;
   int i,flag;
   flag=0;
   cin&amp;gt;&amp;gt;a;
   if(a[0]==&apos;-&apos;)
  {
       flag=1;
       cout&amp;lt;&amp;lt;&quot;fu &quot;;
  }
   for(int i=flag;a[i]!=&apos;\0&apos;;i++)
  {
        h = a[i]-&apos;0&apos;;
       if(i&amp;lt;a.length()-1)
           cout&amp;lt;&amp;lt;s[h]&amp;lt;&amp;lt;&quot; &quot;;
       else
            cout&amp;lt;&amp;lt;s[h];
  }

   return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. 排序&lt;/h2&gt;
&lt;h3&gt;L1-010 比较大小&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;std::sort(first, last)&lt;/code&gt; 默认升序排列左闭右开区间。数组有三个元素时，范围写作 &lt;code&gt;sort(a, a + 3)&lt;/code&gt;；需要其他顺序时再提供比较函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;algorithm&amp;gt;
using namespace std;

int main()
{
   int a[3];
   cin&amp;gt;&amp;gt;a[0]&amp;gt;&amp;gt;a[1]&amp;gt;&amp;gt;a[2];
   sort(a,a+3);
   for(int i=0;i&amp;lt;3;i++)
  {
       if(i&amp;lt;2)
           cout&amp;lt;&amp;lt;a[i]&amp;lt;&amp;lt;&quot;-&amp;gt;&quot;;
       else cout&amp;lt;&amp;lt;a[i];
  }
   return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 格式化输出&lt;/h2&gt;
&lt;h3&gt;L1-008 求整数段和&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;printf(&quot;%5d&quot;, value)&lt;/code&gt; 将整数按 5 个字符宽度右对齐。用计数器控制每 5 个数换行，同时避免在最后一个数后多输出空行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstdio&amp;gt;

using namespace std;

int main()
{
   int m,n,flag,sum;
   cin&amp;gt;&amp;gt;m&amp;gt;&amp;gt;n;
   sum=flag=0;
   for(int i=m;i&amp;lt;=n;i++)
  {
       printf(&quot;%5d&quot;,i);
       flag++;
       if(flag%5==0&amp;amp;&amp;amp;i!=n)
           printf(&quot;\n&quot;);
       sum+=i;
  }
   cout&amp;lt;&amp;lt;endl;
   cout&amp;lt;&amp;lt;&quot;Sum = &quot;&amp;lt;&amp;lt;sum;
   return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 分类计算&lt;/h2&gt;
&lt;h3&gt;L1-031 到底是不是太胖了&lt;/h3&gt;
&lt;p&gt;整数绝对值可使用 &lt;code&gt;abs&lt;/code&gt;，浮点数使用 &lt;code&gt;fabs&lt;/code&gt;。公共输出逻辑不要误放进某个 &lt;code&gt;else&lt;/code&gt; 分支：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;math.h&amp;gt;
using namespace std;

int main()
{
   int n,W,w,H;
   cin&amp;gt;&amp;gt;n;
   while(n--)
  {
       cin&amp;gt;&amp;gt;H&amp;gt;&amp;gt;W;
       w=(H-100)*1.8;
       if(fabs(W-w)&amp;lt;w*0.1)
           cout&amp;lt;&amp;lt;&quot;You are wan mei!&quot;&amp;lt;&amp;lt;endl;
       else if(W&amp;gt;w)
           cout&amp;lt;&amp;lt;&quot;You are tai pang le!&quot;&amp;lt;&amp;lt;endl;
       else
           cout&amp;lt;&amp;lt;&quot;You are tai shou le!&quot;&amp;lt;&amp;lt;endl;
  }
   return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 素数判断&lt;/h2&gt;
&lt;h3&gt;L1-028 判断素数&lt;/h3&gt;
&lt;p&gt;试除范围只需到 &lt;code&gt;sqrt(n)&lt;/code&gt;。计数从 2 开始，并单独处理 1 不是素数的边界：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;math.h&amp;gt;
#include &amp;lt;cstdio&amp;gt;
using namespace std;

int main()
{
   int i,j,n,m;
   cin&amp;gt;&amp;gt;n;
   for(i=0;i&amp;lt;n;i++)
  {
        int flag=0;
        cin&amp;gt;&amp;gt;m;
       for(j=2;j&amp;lt;=sqrt(m);j++)
      {
           if(m%j==0)
          {
               flag=1;
               break;
          }
      }
           if(flag==1||m==1)
               cout&amp;lt;&amp;lt;&quot;No&quot;&amp;lt;&amp;lt;endl;
           else if(flag==0)
               cout&amp;lt;&amp;lt;&quot;Yes&quot;&amp;lt;&amp;lt;endl;
  }
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 满足条件后退出&lt;/h2&gt;
&lt;h3&gt;L1-041 寻找 250&lt;/h3&gt;
&lt;p&gt;输入数量未知时持续读取；第一次遇到 250 就输出位置并用 &lt;code&gt;break&lt;/code&gt; 结束循环：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#define N 10010
using namespace std;

int main()
{
   int i;
   int m=0;
   do
  {
       cin&amp;gt;&amp;gt;i;
       if(i==250)
      {
           cout&amp;lt;&amp;lt;m+1;
           break;
      }
       m++;
  }while(1);

   return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7. 按规则判断&lt;/h2&gt;
&lt;h3&gt;L1-055 谁是赢家&lt;/h3&gt;
&lt;p&gt;分别记录观众票数和三名评委的投票，再直接翻译题目的获胜条件。重点是确认 0、1 分别代表哪位选手：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstdio&amp;gt;
using namespace std;

int main()
{
   int i,pa,pb,a,b,c,qa,qb;
   cin&amp;gt;&amp;gt;pa&amp;gt;&amp;gt;pb;
   qa=qb=0;
   for(i=0;i&amp;lt;3;i++)
  {
        cin&amp;gt;&amp;gt;a;
        a==0?qa++:qb++;
  }
   if((pa&amp;gt;pb&amp;amp;&amp;amp;qa&amp;gt;=1)||(pa&amp;lt;pb&amp;amp;&amp;amp;qa==3))
       cout&amp;lt;&amp;lt;&quot;The winner is &quot;&amp;lt;&amp;lt;&quot;a&quot;&amp;lt;&amp;lt;&quot;: &quot;&amp;lt;&amp;lt;pa&amp;lt;&amp;lt;&quot; + &quot;&amp;lt;&amp;lt;qa;
   else
       cout&amp;lt;&amp;lt;&quot;The winner is &quot;&amp;lt;&amp;lt;&quot;b&quot;&amp;lt;&amp;lt;&quot;: &quot;&amp;lt;&amp;lt;pb&amp;lt;&amp;lt;&quot; + &quot;&amp;lt;&amp;lt;qb;
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>PTA 天梯赛 L1 刷题总结（三）：15 分题型</title><link>https://pinellia.uk/posts/pta-l1-3/</link><guid isPermaLink="true">https://pinellia.uk/posts/pta-l1-3/</guid><description>按知识点整理 PTA 天梯赛 L1 阶段常见的 15 分题型与对应解题方法。</description><pubDate>Wed, 26 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这组 15 分题按核心数据结构与处理方式分为五类：哈希映射、结构体、按题意模拟、字符串和矩阵。先判断题目属于哪一类，再检查输入边界、状态记录和输出格式。&lt;/p&gt;
&lt;h2&gt;一、哈希映射问题&lt;/h2&gt;
&lt;h3&gt;L1-003 个位数统计&lt;/h3&gt;
&lt;p&gt;此题中N是一个不超过 1000 位的正整数，遇到这样的问题一般采用字符串遍历的方式解决。（超出了整型的范围）。题目要求对 N 中每一种不同的个位数字，以 D:M 的格式在一行中输出该位数字 D 及其在 N 中出现的次数 M。要求按 D 的升序输出。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;常见实现中，&lt;code&gt;int&lt;/code&gt; 是 32 位有符号整数，范围约为 $-2^{31}$ 到 $2^{31}-1$；&lt;code&gt;long long&lt;/code&gt; 通常是 64 位，范围约为 $-2^{63}$ 到 $2^{63}-1$。1000 位整数远超这两种类型的范围，应按字符串处理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;根据题目的要求，第一想到的是用哈希表来存储每位数字对应的次数，这个是一一映射的关系。每一位上就是0到9十个数字，用一个长度为10的整型数组就行。对于出现次数为0的，就不用输出了。这里加个判断就可以。&lt;/p&gt;
&lt;p&gt;对于字符串问题，首先先读入每一个字符，遇到一个字符，则对应出现次数+1。最后输出时如果映射的值不为0，则按照格式输出即可。&lt;/p&gt;
&lt;p&gt;本题有两个注意点：先用 &lt;code&gt;memset(s, 0, sizeof(s))&lt;/code&gt; 初始化计数数组，并包含 &lt;code&gt;&amp;lt;cstring&amp;gt;&lt;/code&gt;；遍历字符串时，&lt;code&gt;str[i]&lt;/code&gt; 是字符，需要用 &lt;code&gt;str[i] - &apos;0&apos;&lt;/code&gt; 转换为对应数字。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;string.h&amp;gt;

using namespace std;

int main()
{
   string str;
   int s[10];
   int i;
   cin&amp;gt;&amp;gt;str;
   memset(s,0,sizeof(s)); //关键，对数组初始化都设置为0，否则会出错
   for(i=0;i&amp;lt;str.length();i++)
  {
       s[str[i]-&apos;0&apos;]++;    //注意str[i]是一个字符，要str[i]-&apos;0&apos;来转化为int类型
  }
   for(i=0;i&amp;lt;10;i++)
  {
       if(s[i]!=0)
           cout&amp;lt;&amp;lt;i&amp;lt;&amp;lt;&quot;:&quot;&amp;lt;&amp;lt;s[i]&amp;lt;&amp;lt;endl;
  }
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;下面给出&lt;/strong&gt;第二种方法**，博主去刷力扣时的常用方法。我们可以利用map来解决这个哈希数组问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这种方法有几个学习点&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1）定义map的时候记得&lt;/strong&gt;加上头文件#include &lt;strong&gt;&lt;code&gt;&amp;lt;map&amp;gt;&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;** 如map&amp;lt;string,int&amp;gt;就是代表从string类型到int类型的映射，且&lt;strong&gt;map默认是按key值从小到大排序。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;2）访问map容器内元素方法，一个是通过访问下标，比如下方的mp[c-’0’]，还有一种就是通过迭代器（如map&amp;lt;int, int&amp;gt;::iterator it）。通过it-&amp;gt;first访问键，it-&amp;gt;second访问值。&lt;/p&gt;
&lt;p&gt;下边的写法可以这么写&lt;/p&gt;
&lt;p&gt;for(map&amp;lt;int,int&amp;gt;::iterator it = mp.begin();it!=mp.end();it++)&lt;/p&gt;
&lt;p&gt;{&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cout &amp;lt;&amp;lt; it-&amp;gt;first &amp;lt;&amp;lt; &quot;:&quot; &amp;lt;&amp;lt; it-&amp;gt;second &amp;lt;&amp;lt; endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;3）for(char c:str)这是for循环的一个简便写法，遍历字符串str的每一个字符就是c，也就是方法一中的str[i]，比方法一的遍历要方便很多对吧。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;map&amp;gt;

using namespace std;

int main()
{
   string str;
   cin&amp;gt;&amp;gt;str;
   map&amp;lt;int,int&amp;gt; mp;
   for(char c:str)
  {
       mp[c-&apos;0&apos;]++;
  }
   map&amp;lt;int, int&amp;gt;::iterator it = mp.begin();

while (it != mp.end()) {
cout &amp;lt;&amp;lt; it-&amp;gt;first &amp;lt;&amp;lt; &quot;:&quot; &amp;lt;&amp;lt; it-&amp;gt;second &amp;lt;&amp;lt; endl;
it++;
}

   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、结构体问题&lt;/h2&gt;
&lt;h3&gt;L1-005 考试座位号&lt;/h3&gt;
&lt;p&gt;针对本题，题目的要求是对应每个需要查询的试机座位号码，在一行中输出对应考生的准考证号和考试座位号码，中间用 1 个空格分隔。&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;**    考虑到每个人是有准考证号、试机座位号、考场座位号的。这时候考虑用结构体代表每个学生。&lt;strong&gt;特别是在不同数据类型的变量或数组组合在一起的时候应用结构体能够便于存储。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;比如struct studentInfo{int id; char gender; string name;}Bob,stu[10];&lt;/p&gt;
&lt;p&gt;其中studentInfo是结构体名，Bob是结构体变量，stu[10]是结构体数组（包含了10个学生的信息）。访问结构体可以通过.或者-&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;实现：&lt;/p&gt;
&lt;p&gt;准考证号是16位数字，可以考虑用string类型来存储。首先遍历给定的N个学生的信息。再依次读入试机号，遍历结构体中是否有相同的试机号，相同则输出其准考证号和考试座位号。&lt;/p&gt;
&lt;p&gt;当遍历的次数和循环的内容没有关系时，可以用while(m--)作循环&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstdio&amp;gt;
#define N 100000
using namespace std;

struct node
{
   string z;
   int s,k;
}stu[N];
int main()
{
   int i,m,n,l;
   cin&amp;gt;&amp;gt;n;
   for(i=0;i&amp;lt;n;i++)
  {
      cin&amp;gt;&amp;gt;stu[i].z&amp;gt;&amp;gt;stu[i].s&amp;gt;&amp;gt;stu[i].k;
  }
   cin&amp;gt;&amp;gt;m;
   while(m--)
  {
       cin&amp;gt;&amp;gt;l;
       for(i=0;i&amp;lt;n;i++)
      {
           if(stu[i].s==l)
               cout&amp;lt;&amp;lt;stu[i].z&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;stu[i].k&amp;lt;&amp;lt;endl;
      }
  }
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;或者我们可以优化一下，建立试机号到结构体（准考证号，座位号）的映射。下面给出&lt;/strong&gt;第二种方法**。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;map&amp;gt;
#define N 100000
using namespace std;

struct node
{
   string z;//准考证号
   int k; //考场座位号
}stu;

int main()
{
   int i,m,n,l;
   map&amp;lt;int,node&amp;gt; mp;
   cin&amp;gt;&amp;gt;n;
   int s;
   for(i=0;i&amp;lt;n;i++)
  {
      cin&amp;gt;&amp;gt;stu.z&amp;gt;&amp;gt;s&amp;gt;&amp;gt;stu.k;
      mp[s] = stu;
  }
   cin&amp;gt;&amp;gt;m;
   while(m--)
  {
       cin&amp;gt;&amp;gt;l;
       cout&amp;lt;&amp;lt;mp[l].z&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;mp[l].k&amp;lt;&amp;lt;endl;
  }
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;L1-030 一帮一&lt;/h3&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;输入已经按照名次大小排列了，那遍历的边界在哪里呢？是n/2。要不然再后面差一点的学生就排到前面去了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;因为要和异性组队，那么是不是有可能出现第一名和最后一名同学是同性，于是第一名和倒数第二名同学组队了，那第二名和最后一名是异性，他们组队。那这时候就得&lt;/strong&gt;考虑有个标志位来记录这个同学是不是组过队了。**&lt;/p&gt;
&lt;p&gt;实现：&lt;/p&gt;
&lt;p&gt;结构体我们上面已经说了，可以定义一个大小为N的结构体数组，在头部宏定义#define N 10000&lt;/p&gt;
&lt;p&gt;**在输入第i个学生的性别和姓名时，&lt;strong&gt;初始化是否组队的标记位为0。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**在输出时，按照名词顺序，用两重循环，外层循环先输出名次高的同学名字，再嵌套一个循环，从j=n-1开始，若不满足j&amp;gt;i则跳出。如果这个同学之前未匹配并且和名次高的同学性别不同，则匹配成功，输出这个同学姓名，并置标记位为1。否则break（&lt;strong&gt;只会跳出这一层循环！&lt;/strong&gt;）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#define N 10000
using namespace std;
struct node
{
   int x,f;
   string m;
}s[N];
int main()
{
   int i,j,n;
   cin&amp;gt;&amp;gt;n;
   for(i=0;i&amp;lt;n;i++)
  {
       cin&amp;gt;&amp;gt;s[i].x&amp;gt;&amp;gt;s[i].m;
       s[i].f=0;
  }
   for(i=0;i&amp;lt;n/2;i++)
  {
       for(j=n-1;j&amp;gt;i;j--)
      {
           if(s[i].x!=s[j].x&amp;amp;&amp;amp;s[j].f!=1)
          {
               cout&amp;lt;&amp;lt;s[i].m&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;s[j].m&amp;lt;&amp;lt;endl;
               s[j].f=1;
               break;
          }
      }
  }
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;三、按题意模拟&lt;/h2&gt;
&lt;h3&gt;L1-015 跟奥巴马一起画方块&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;题目要求是输入在一行中给出正方形边长N&lt;/strong&gt;，**输出由给定字符C画出的正方形，且输出的行数实际上是列数的50%（四舍五入取整）。&lt;/p&gt;
&lt;p&gt;双重循环，外层代表多少行，内层代表多少列。要求行数是列数的50%。给定的边长其实就是列数，那行数是四舍五入求整。&lt;/p&gt;
&lt;p&gt;**本题比较简单，**注意点:&lt;strong&gt;int类型除法是向下取整&lt;/strong&gt;，如果要做到四舍五入的话应写为l=(n+1)/2;&lt;/p&gt;
&lt;h3&gt;L1-016 查验身份证&lt;/h3&gt;
&lt;p&gt;定义两个数组，一个存储权重，一个存储校验码，按题意对比取模后对应校验码的值是否与身份证最后一位相同即可。为了便于核对，可用字符串类型存储身份证。&lt;/p&gt;
&lt;h3&gt;L1-017 到底有多二&lt;/h3&gt;
&lt;p&gt;输入第一行给出整数 N。“犯二的程度”是数字 2 的数量与位数之比；负数将结果乘以 1.5，偶数再乘以 2。&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;首先依题意N不超过50位，考虑用字符串类型定义N。遍历字符串记录2的个数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意位数需要排除符号位&lt;/strong&gt;，如果首位是负号，用一个flag位标记是否*1.5（增加0.5倍）那么长度为s.length(）-1（可以用l-flag表示位数）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;看末位是否能被2整除，如果是结果*2。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**注意点：**小数点后保留两位  printf(&quot;%.2f&quot;,b);。&lt;strong&gt;比值要转为double类型&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstdio&amp;gt;
using namespace std;

int main()
{
   string s;
   double b;
   int i,cnt=0;
   cin&amp;gt;&amp;gt;s;
   int flag=0;
   if(s[0]==&apos;-&apos;) flag=1;
   int l = s.length();
   for(i=flag;i&amp;lt;s.length();i++)
  {
       if(s[i]-&apos;0&apos;==2)
           cnt++;
  }
   b = (double)cnt/(l-flag)*100;
   if(flag==1)
       b*=1.5;
   if((s[l-1]-&apos;0&apos;)%2==0)
       b*=2;
   printf(&quot;%.2f&quot;,b);
   cout&amp;lt;&amp;lt;&quot;%&quot;;
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;L1-019 谁先倒&lt;/h3&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;这题关键点在于判断谁赢谁输。比如甲输的条件是什么？注意同输是进入下一轮的，不喝酒。因此if中有两个判断条件。谁输了谁喝的杯数+1。&lt;/p&gt;
&lt;p&gt;在for循环中我们每次遍历都看看是不是有人已经到达他的酒量了。有人倒下就终止（喝的杯数&amp;gt;酒量），输出输家和赢家喝的杯数，用break跳出循环。&lt;/p&gt;
&lt;p&gt;注意点：&lt;/p&gt;
&lt;p&gt;输入的时候用到getchar();  目的是清空回车符，从标准输入流读取一个字符，读回车符时退出&lt;/p&gt;
&lt;p&gt;最好定义的变量名能够很好的体现含义，特别这题一行四个变量，要避免后面写乱了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstdio&amp;gt;
using namespace std;

int main()
{
   int a,b,n,i,yf,jf,yh,jh,ma,mb;
   ma=mb=0;
   cin&amp;gt;&amp;gt;a&amp;gt;&amp;gt;b;
   getchar();  //不用也可以，目的是清空回车符，从标准输入流读取一个字符，读回车符时退出
   cin&amp;gt;&amp;gt;n;
   for(i=0;i&amp;lt;n;i++)
  {
       cin&amp;gt;&amp;gt;jh&amp;gt;&amp;gt;jf&amp;gt;&amp;gt;yh&amp;gt;&amp;gt;yf;
       if(jf==jh+yh&amp;amp;&amp;amp;yf!=jh+yh)
           ma++;
       if(yf==jh+yh&amp;amp;&amp;amp;jf!=jh+yh)
           mb++;
       if(ma&amp;gt;a||mb&amp;gt;b)
      {
         if(mb&amp;gt;b)
          {
               cout&amp;lt;&amp;lt;&quot;B&quot;&amp;lt;&amp;lt;endl;
               cout&amp;lt;&amp;lt;ma;
          }
          if(ma&amp;gt;a)
          {
              cout&amp;lt;&amp;lt;&quot;A&quot;&amp;lt;&amp;lt;endl;
              cout&amp;lt;&amp;lt;mb;
          }
          break;
      }

  }

   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;L1-062 幸运彩票&lt;/h3&gt;
&lt;p&gt;用字符串保存字符串，再依题意判断输出即可。&lt;/p&gt;
&lt;h3&gt;L1-069 胎压监测&lt;/h3&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;这题可以用数组存储各个轮胎的值。求数组中最大值可以用到max()。注意读题内容准确，遍历每个轮胎，看最大值与此轮胎气压差大于阈值或者这个轮胎气压值小于最低气压时，轮胎异常数+1，并且标记轮胎编号。对于异常轮胎个数用一个变量表示，最后根据轮胎异常数为0,1还是&amp;gt;1的情况分别输出结果。
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cmath&amp;gt;
using namespace std;

int main()
{
   int a[4];
   int yu,ya;
   scanf(&quot;%d %d %d %d&quot;,&amp;amp;a[0],&amp;amp;a[1],&amp;amp;a[2],&amp;amp;a[3]);
   scanf(&quot;%d %d&quot;,&amp;amp;ya,&amp;amp;yu);

   int maxY = 0,cnt =0,index,i;
   for(i=0;i&amp;lt;4;i++)
  {
       maxY = max(maxY,a[i]);
  }
   for(i=0;i&amp;lt;4;i++)
  {
       if(maxY-a[i]&amp;gt;yu||a[i]&amp;lt;ya)
      {
           cnt++;
           index = i;
      }
  }
   if(cnt==0) cout&amp;lt;&amp;lt;&quot;Normal&quot;;
   else if(cnt==1) printf(&quot;Warning: please check #%d!&quot;,index+1);
   else cout&amp;lt;&amp;lt;&quot;Warning: please check all the tires!&quot;;
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;L1-077 大笨钟的心情&lt;/h3&gt;
&lt;p&gt;**这道题也是不知道有多少行输入的，在遇到非法数据（不在[0,23]区间）时停止。那就把while循环()里内容改为while(cin&amp;gt;&amp;gt;n&amp;amp;&amp;amp;n&amp;gt;=0&amp;amp;&amp;amp;n&amp;lt;=23)。&lt;strong&gt;注意格式正确。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;四、字符串处理&lt;/h2&gt;
&lt;h3&gt;字符串转换为整数&lt;/h3&gt;
&lt;h3&gt;L1-025 正整数A+B&lt;/h3&gt;
&lt;p&gt;A和B都在区间[1,1000] A、B有时候可能是超出范围的数字、负数、带小数点的实数、甚至是一堆乱码。把输入中出现的第1个空格认为是A和B的分隔。&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;首先A、B不一定都是整数，可能是乱码，因此采用字符串来存储A、B。读入A之后一定要加getchar()读入缓冲区中的空格，getline(cin,str)读入整行字符串B。

对于不符合条件的数字用?代替，只要加法的任一加数为？，结果均为？
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**    我们只对符合条件的两个加数进行计算。由于A、B都是[1,1000]的数，因此我们可以把他们转化为整型再相加获得结果，这里可以&lt;strong&gt;专门写一个函数来实现字符串到整型的转换&lt;/strong&gt;。如果返回值为-1，表示这个数不符合条件，否则返回转化为整型的值。最后以规定的格式输出等式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输出结果时有四种可能：一是两个都是整数，第二、三种是A或B满足条件，另一个不符合条件，第四种是A、B都不符合条件。可用if进行判断。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现：&lt;/p&gt;
&lt;p&gt;**    那么这个函数要怎么写呢？我们将字符串和字符串长度作为参数传入（只传一个字符串也没有关系）。遍历这个字符串，如果出现这个字符&amp;lt;’0’且&amp;gt;’9’就说明这个字符串不符合条件了，这时直接返回-1，别继续遍历了。否则，&lt;strong&gt;sum=sum10+a[i]-&apos;0&apos;;&lt;/strong&gt; 想想是不是这样。最后返回这个sum，也就是转化的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;string&amp;gt;
using namespace std;

int pan(string a,int len)
{
   int i,sum=0;
   for(i=0;i&amp;lt;len;i++)
  {
       if(a[i]&amp;lt;&apos;0&apos;||a[i]&amp;gt;&apos;9&apos;)
           return -1;
       else
           sum=sum*10+a[i]-&apos;0&apos;;
  }

   return sum;
}
int main()
{
   string a,b;
   cin&amp;gt;&amp;gt;a;
   getchar();
   getline(cin,b);
   int lena = a.length();
   int lenb = b.length();
   int A = pan(a,lena);
   int B = pan(b,lenb);
   if((A&amp;gt;0&amp;amp;&amp;amp;A&amp;lt;=1000)&amp;amp;&amp;amp;(B&amp;gt;0&amp;amp;&amp;amp;B&amp;lt;=1000))
       cout&amp;lt;&amp;lt;A&amp;lt;&amp;lt;&quot; + &quot;&amp;lt;&amp;lt;B&amp;lt;&amp;lt;&quot; = &quot;&amp;lt;&amp;lt;A+B;
   if((A&amp;gt;0&amp;amp;&amp;amp;A&amp;lt;=1000)&amp;amp;&amp;amp;(B&amp;lt;1||B&amp;gt;1000))
       cout&amp;lt;&amp;lt;A&amp;lt;&amp;lt;&quot; + ? = ?&quot;;
   if(B&amp;gt;0&amp;amp;&amp;amp;B&amp;lt;=1000&amp;amp;&amp;amp;(A&amp;lt;1||A&amp;gt;1000))
       cout&amp;lt;&amp;lt;&quot;? + &quot;&amp;lt;&amp;lt;B&amp;lt;&amp;lt;&quot; = ?&quot;;
   if((A&amp;lt;1||A&amp;gt;1000)&amp;amp;&amp;amp;(B&amp;lt;1||B&amp;gt;1000))
       cout&amp;lt;&amp;lt;&quot;? + ? = ?&quot;;
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;提取整数的每一位&lt;/h3&gt;
&lt;h3&gt;L1-033 出生年&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;输入在一行中给出出生年份y和目标年份中不同数字的个数n，其中y在[1, 3000]之间。根据输入，输出x和能达到要求的年份。数字间以1个空格分隔，行首尾不得有多余空格。注意不足4位的年份要在前面补零
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;题目已经说了&lt;/strong&gt;所谓“n个数字都不相同”是指不同的数字正好是n个。**&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;考虑到set不会存放相同的元素&lt;/strong&gt;，可以把当前年份所含的数字insert()加入到集合中，如果集合中数字个数==n就break退出。返回年龄和当前年份（注意输出格式！）。起始值为出生年份，循环到能找到这个年份为止（年份一年一年往上加）。&lt;/p&gt;
&lt;p&gt;实现：&lt;/p&gt;
&lt;p&gt;**    这里就和上题不同的是，不需将年份转化为字符串，而是需要将整型的一位位取出来。怎么取呢？假设当前处理的年份是n，这里&lt;strong&gt;也不需要按顺序来存&lt;/strong&gt;，所以每次把n%10添加到set中（取出当前数的末位数字），再n/10。循环多少次呢?因为y在[1, 3000]，&lt;strong&gt;保证是个4位数&lt;/strong&gt;。设置j的起始值为4就可以了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;while(j--)&lt;/p&gt;
&lt;p&gt;{    s.insert(n%10);&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; n/=10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意这时候n的值已经发生改变，用一个变量来保存一下它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;set&amp;gt;
using namespace std;

int main()
{
   int y,n,m;
   cin&amp;gt;&amp;gt;y&amp;gt;&amp;gt;n;
   m = y;

   while(1)
  {
      int h = m;
      set&amp;lt;int&amp;gt; s;   //注意定义的位置
      int j=4;
      while(j--)
      {
          s.insert(h%10);
          h/=10;
      }
       if(s.size()==n) break;
       m++;
  }
   printf(&quot;%d %04d&quot;,m-y,m);
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;遇到终止标记时停止输入&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;还有种情况就是给一行字符串，我们使用&lt;/strong&gt;getline(cin,str)后用s.length()就知道字符串的长度**，便可以用for循环遍历整个字符串了。（如58题）&lt;/p&gt;
&lt;h3&gt;L1-035 情人节&lt;/h3&gt;
&lt;p&gt;输入按照点赞的先后顺序给出不知道多少个点赞的人名，每个人名占一行，为不超过10个英文字母的非空单词，以回车结束。一个英文句点.标志输入的结束。若存在第2个人A和第14个人B，则输出“A and B are inviting you to dinner...”；若只有A没有B，则输出“A is the only one for you...”；若连A都没有，则输出“Momo... No one is for you ...”。&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;输入时注意条件是不知道多少个点赞的人名（可用字符串表示），这时候就可以用while循环，特别是题目说了用英文句点.标志输入的结束。倘若读入的字符串是’.’，就可以跳出循环。&lt;/p&gt;
&lt;p&gt;**因为要输出第2和第14个人名，因此需要存储人名。&lt;strong&gt;可以用字符串数组，也可以单纯存放第2个和第14个人名。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这题在输出的时候考虑如题所示的三种情况。如果人数&amp;gt;=14,输出…… 如果人数&amp;gt;=2且&amp;lt;14，需要输出……如果人数&amp;lt;2,输出…… 这时候还需要一个变量来标记点赞人数（有多少次循环）&lt;/p&gt;
&lt;p&gt;**第一种方法是用字符串数组来存储人名，&lt;strong&gt;用while(1)循环&lt;/strong&gt;，直到遇到‘.’break跳出循环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#define N 10000
using namespace std;

int main()
{
   string s[N];
   int i=0;
   while(1)
  {
       cin&amp;gt;&amp;gt;s[i];
       if(s[i][0]==&apos;.&apos;)
           break;
       i++;

  }
   if(i&amp;gt;1&amp;amp;&amp;amp;i&amp;lt;=13)
           cout&amp;lt;&amp;lt;s[1]&amp;lt;&amp;lt;&quot; is the only one for you...&quot;;
   if(i&amp;lt;=1)
       cout&amp;lt;&amp;lt;&quot;Momo... No one is for you ...&quot;;
   if(i&amp;gt;13)
           cout&amp;lt;&amp;lt;s[1]&amp;lt;&amp;lt;&quot; and &quot;&amp;lt;&amp;lt;s[13]&amp;lt;&amp;lt;&quot; are inviting you to dinner...&quot;;
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第二种方法&lt;/strong&gt;是用字符串数组来存储人名，用&lt;strong&gt;while(&lt;strong&gt;cin&amp;gt;&amp;gt;s&amp;amp;&amp;amp;s!=&quot;.&quot;&lt;/strong&gt;)&lt;/strong&gt;（注意是双引号！）**来看是否进入循环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#define N 10000
using namespace std;

int main()
{
   string s,A,B;
   int i=0;
   while(cin&amp;gt;&amp;gt;s&amp;amp;&amp;amp;s!=&quot;.&quot;)
  {
       i++;
       if(i==2) A = s;
       if(i==14) B = s;

  }
   if(i&amp;gt;1&amp;amp;&amp;amp;i&amp;lt;=13)
           cout&amp;lt;&amp;lt;A&amp;lt;&amp;lt;&quot; is the only one for you...&quot;;
   else if(i&amp;lt;=1)
       cout&amp;lt;&amp;lt;&quot;Momo... No one is for you ...&quot;;
   else
        cout&amp;lt;&amp;lt;A&amp;lt;&amp;lt;&quot; and &quot;&amp;lt;&amp;lt;B&amp;lt;&amp;lt;&quot; are inviting you to dinner...&quot;;
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;L1-044 稳赢&lt;/h3&gt;
&lt;p&gt;根据对方的出招，给出对应的赢招。需要每隔K次就让一个平局。&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这题石头剪刀布有个很巧的方法，我们不要专门设一个函数，比如传入参数为剪刀，返回石头；出石头，返回布……而是可以用一个字符串数组的方法，比如string s[3]={&quot;ChuiZi&quot;,&quot;JianDao&quot;,&quot;Bu&quot;};比如锤子，我们用布来对。布就用剪刀。&lt;/strong&gt;（规律：差2 再对三取模）**s[(i+2)%3]就是本局要赢对应的划拳。&lt;/p&gt;
&lt;p&gt;因此对于输入的字符串我们可以遍历下字符串数组，看当前的字符串在字符串数组的索引i（一个个比较是否相等）。&lt;/p&gt;
&lt;p&gt;其次注意K是间隔次数，每隔K局就输出一个和输入的字符串相同的划拳。（比如k=2，前两局赢了，我第三局就要平局）这时就要用一个变量表示局数（初始化为1），如果局数%(k+1)==0就说明这句要平局。&lt;/p&gt;
&lt;p&gt;输入到End就退出循环，这个方法和上一题一样，就不再赘述。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

using namespace std;

int main()
{
   string s[3]={&quot;ChuiZi&quot;,&quot;JianDao&quot;,&quot;Bu&quot;};
   string str;
   int n,i;
   cin&amp;gt;&amp;gt;n;
   int c=1;
   while(1)
  {
       cin&amp;gt;&amp;gt;str;
       if(str==&quot;End&quot;)
           break;
       for(i=0;i&amp;lt;3;i++)
      {
           if(str==s[i])
          {
               if(c%(n+1)!=0)
                   cout&amp;lt;&amp;lt;s[(i+2)%3]&amp;lt;&amp;lt;endl;
               else cout&amp;lt;&amp;lt;s[i]&amp;lt;&amp;lt;endl;
          }
      }
        c++;
  }

   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;L1-058 6翻了&lt;/h3&gt;
&lt;p&gt;从左到右扫描输入的句子：如果句子中有超过 3 个连续的 6，则将这串连续的 6 替换成 9；但如果有超过 9 个连续的 6，则将这串连续的 6 替换成 27。其他内容不受影响，原样输出。&lt;/p&gt;
&lt;p&gt;这道题的难点就在于如何看有多少个连续的6，以及把连续的6替换为对应数字。&lt;/p&gt;
&lt;p&gt;实现：&lt;/p&gt;
&lt;p&gt;首先 用getline(cin,s)读入这行句子。&lt;/p&gt;
&lt;p&gt;考虑用string类型中的replace()函数，&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;str.replace(pos,len,str2);表示用str2替换str中从pos位置开始长度为len的子串。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

using namespace std;

int main()
{
    string s;
   int i,j,n;
   getline(cin,s);
   for(i=0;i&amp;lt;s.length();i++)
  {
       for(j=0;i+j&amp;lt;s.size()&amp;amp;&amp;amp;s[i+j]==&apos;6&apos;;j++);//记得这里有个分号，只是这样求一串6的长度
if(j&amp;gt;9) s.replace(i,j,&quot;27&quot;);//j就是一串6的长度
else if(j&amp;gt;3) s.replace(i,j,&quot;9&quot;);
  }
   cout&amp;lt;&amp;lt;s;
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;判断字符串是否包含子串&lt;/h3&gt;
&lt;h3&gt;L1-070 吃火锅&lt;/h3&gt;
&lt;p&gt;自动检查你朋友给你发来的信息里有没有 chi1 huo3 guo1。输入每行给出一句不超过 80 个字符的、以回车结尾的朋友信息。当读到某一行只有一个英文句点 . 时，输入结束，此行不算在朋友信息里。&lt;/p&gt;
&lt;p&gt;首先在一行中输出朋友信息的总条数。然后对朋友的每一行信息，检查其中是否包含 chi1 huo3 guo1，并且统计这样厉害的信息有多少条。在第二行中首先输出第一次出现 chi1 huo3 guo1 的信息是第几条（从 1 开始计数），然后输出这类信息的总条数，其间以一个空格分隔。题目保证输出的所有数字不超过 100。&lt;/p&gt;
&lt;p&gt;如果朋友从头到尾都没提 chi1 huo3 guo1 这个关键词，则在第二行输出一个表情 -_-#。&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;首先要看看朋友到底发了多少条信息。这里每行就是一条，看有多少行。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意二者区别   cin&amp;gt;&amp;gt;str：遇到空格将停止输入 getline()：获得完整一行字符串&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;怎么体现遇到chi1 huo3 guo1呢？输入的这一行字符串包含这个字符串即可。这里插入一个知识点：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在string类型中有一个find()函数，str.find(str2)，如果str包含str2，就返回在str中第一次出现str2的位置，否则返回string::npos。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意用一个变量来存储第一次出现“chi1 huo3 guo1”的行数以及出现”chi1 huo3 guo1”的条数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意是&lt;/strong&gt;提到次数为0**时而不是行数为0时输出表情！这个在定义变量的时候做好区分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;

using namespace std;
int main()
{
   string s;
   string str = &quot;chi1 huo3 guo1&quot;;
   int cnt = 0,ans = 0,first = 0,flag=0 ;
   while(getline(cin,s)&amp;amp;&amp;amp;s!=&quot;.&quot;)
  {
       cnt++;
       if(s.find(str)!=string::npos)
      {
           ans++;
           if(ans == 1)  first = cnt;
      }
  }
   cout&amp;lt;&amp;lt;cnt&amp;lt;&amp;lt;endl;
   if(ans ==0)  cout&amp;lt;&amp;lt;&quot;-_-#&quot;;
else cout&amp;lt;&amp;lt;first&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;ans;
return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;L1-078 吉老师的回归&lt;/h3&gt;
&lt;p&gt;逐行检查字符串。如果其中包含区分大小写的 &lt;code&gt;qiandao&lt;/code&gt; 或 &lt;code&gt;easy&lt;/code&gt;，就跳过这道题。&lt;/p&gt;
&lt;p&gt;输入表示本次天梯赛有 N 道题目，吉老师现在做完了 M 道。在一行中输出吉老师当前正在做的题目对应的题面（即做完了 M 道题目后，吉老师正在做哪个题）。如果吉老师已经把所有他打算做的题目做完了，输出一行 Wo AK le。&lt;/p&gt;
&lt;p&gt;思路（本题脑子清醒比较重要）：&lt;/p&gt;
&lt;p&gt;如果吉老师没把想做的题目做完，就输出吉老师正在做哪题（第m+1个不包含qiandao 或者 easy的题面）。如果吉老师都做完了，就输出指定内容。&lt;/p&gt;
&lt;p&gt;这里就需要遍历完每一行的题面，看看究竟有多少题吉老师要完成的（与M比对是否已做完）。这就需要一个变量来记录吉老师做的题数，不包含：题数+1。&lt;/p&gt;
&lt;p&gt;**当题数==M+1时，输出第M+1个不包含qiandao 或者 easy的题面。(&lt;strong&gt;注意在判断时二者是&amp;amp;&amp;amp;关系&lt;/strong&gt;)。字符串包含的问题上题已说明。&lt;/p&gt;
&lt;p&gt;什么情况下吉老师做完所有题目呢？ t&amp;lt;=m 也就是已完成的题数&amp;gt;=需要做的题数的情况&lt;/p&gt;
&lt;p&gt;**注意：**此题的输入  要么cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;m;之后跟上一个getchar()接收一下回车 再接着循环存储题面。要么就是scanf(&quot;%d %d\n&quot;,&amp;amp;n,&amp;amp;m);进行输入。否则会出错！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;string&amp;gt;

using namespace std;

int main()
{
   int n,m,t=0;
   cin&amp;gt;&amp;gt;n&amp;gt;&amp;gt;m;
   //scanf(&quot;%d %d\n&quot;,&amp;amp;n,&amp;amp;m);
   getchar();
   string s;
   for(int i=0;i&amp;lt;n;i++)
  {
       getline(cin,s);
       //if(s.find(&quot;qiandao&quot;)==string::npos&amp;amp;&amp;amp;s.find(&quot;easy&quot;)==string::npos)
       if(s.find(&quot;qiandao&quot;)==-1&amp;amp;&amp;amp;s.find(&quot;easy&quot;)==-1)    //也可以用-1表示string::npos，虽然会显示等号两边类型不匹配的warning
      {
           t++;
      }
       if(t==m+1)
      {
          cout&amp;lt;&amp;lt;s;
          break;
      }
  }
   if(t&amp;lt;=m) cout&amp;lt;&amp;lt;&quot;Wo AK le&quot;&amp;lt;&amp;lt;endl;
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;按位推导规律&lt;/h3&gt;
&lt;h3&gt;L1-050 倒数第N个字符串&lt;/h3&gt;
&lt;p&gt;给定一个完全由小写英文字母组成的字符串等差递增序列，该序列中的每个字符串的长度固定为 L，从 L 个 a 开始，以 1 为步长递增。对于任意给定的 L，本题要求你给出对应序列倒数第 N 个字符串。&lt;/p&gt;
&lt;p&gt;给定一个完全由小写英文字母组成的字符串等差递增序列，该序列中的每个字符串的长度固定为 L，从 L 个 a 开始，以 1 为步长递增。对于任意给定的 L，本题要求你给出对应序列倒数第 N 个字符串。&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;倒数第N个字符串，转化为正数第M个字符串来解决。L个字母会产生26^L个字符串，用函数表示就是pow(26,l);&lt;/p&gt;
&lt;p&gt;感觉过去像是个找规律的问题，比如说aaaa、aaab…aaaz、aaba、aabb、aabc……zzzz。就好比满26了向前进1。可以看出到第28个（第一个记为0）时，把28%26的值为当前最低位（‘a’+2），接着28/26为1，对26求余为1，也就是倒数第二位。然后不断把这个值/26再对26求模（循环l次），就可以从最后一位往前推当前位的字符，结果是aabc。最后逆序逐个输出每一位上的字符。(这题对我来说还挺难想的…）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;math.h&amp;gt;
#define N 10
using namespace std;

int main()
{
   int l,n,m,i,t;
   char r[N];
   cin&amp;gt;&amp;gt;l&amp;gt;&amp;gt;n;
   t = pow(26,l);
   m = t-n;
   for(i=l-1;i&amp;gt;=0;i--)
  {
       r[i] =&apos;a&apos;+m%26;
       m/=26;
  }
   for(i=0;i&amp;lt;l;i++)
       cout&amp;lt;&amp;lt;r[i];

   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;五、矩阵处理&lt;/h2&gt;
&lt;h3&gt;矩阵相乘&lt;/h3&gt;
&lt;h3&gt;L1-048 矩阵A乘以B&lt;/h3&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;输入先后给出两个矩阵A和B。对于每个矩阵，首先在一行中给出其行数R和列数C。只有Ca与Rb相等时，两个矩阵才能相乘。&lt;/p&gt;
&lt;p&gt;若输入的两个矩阵的规模是匹配的，则按照输入的格式输出乘积矩阵AB，否则输出Error: Ca != Rb&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可以在草稿纸上模拟一下矩阵相乘有什么特点。首先结果是Ra×Cb大小的矩阵c。比如c&lt;/strong&gt;&lt;a href=&quot;http://blog.pinellia.uk/2025/02/ptal115.html#&quot;&gt;i&lt;/a&gt;的结果为a的第i行分别与b的第j列元素相乘结果再相加。&lt;/p&gt;
&lt;p&gt;实现：&lt;/p&gt;
&lt;p&gt;首先输入两个矩阵，用二维数组a和b来存储（在输入矩阵a的行数和列数时就创建符合大小的二维数组）。存入数组元素也不难，双重循环读入每个位置元素就可以了。&lt;/p&gt;
&lt;p&gt;其次对Ca和Rb进行比较，不相等直接输出Error，就不要进行矩阵相乘了。可以用个if-else判断&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现起来是不是a&lt;/strong&gt;&lt;a href=&quot;http://blog.pinellia.uk/2025/02/ptal115.html#&quot;&gt;i&lt;/a&gt;*c&lt;a href=&quot;http://blog.pinellia.uk/2025/02/ptal115.html#&quot;&gt;k&lt;/a&gt;。这个k的范围就是a的列数，即b的行数。因此设置三层循环，最里层计算sum+=a&lt;a href=&quot;http://blog.pinellia.uk/2025/02/ptal115.html#&quot;&gt;i&lt;/a&gt;*b&lt;a href=&quot;http://blog.pinellia.uk/2025/02/ptal115.html#&quot;&gt;k&lt;/a&gt;;  第二层循环需要更新sum=0;&lt;/p&gt;
&lt;p&gt;**注意！**行首尾没有多余的空格。可用一个变量来标记此时是这一行第几个元素（若初始化为0）&lt;/p&gt;
&lt;p&gt;非每行末尾元素，我们输出结果时再加一个空格。如果当前计数==Cb-1 就输出结果并换行即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstdio&amp;gt;
#include &amp;lt;cstring&amp;gt;
using namespace std;

int main()
{
   int i,j,k,ra,ca,rb,cb,sum;
   cin&amp;gt;&amp;gt;ra&amp;gt;&amp;gt;ca;
   int a[ra][ca];
   for(i=0;i&amp;lt;ra;i++)
  {
       for(j=0;j&amp;lt;ca;j++)
           cin&amp;gt;&amp;gt;a[i][j];
  }
   cin&amp;gt;&amp;gt;rb&amp;gt;&amp;gt;cb;
   int b[rb][cb];
   for(i=0;i&amp;lt;rb;i++)
  {
       for(j=0;j&amp;lt;cb;j++)
           cin&amp;gt;&amp;gt;b[i][j];
  }
   if(ca!=rb) printf(&quot;Error: %d != %d&quot;,ca,rb);
   else
  {
       cout&amp;lt;&amp;lt;ra&amp;lt;&amp;lt;&quot; &quot;&amp;lt;&amp;lt;cb&amp;lt;&amp;lt;endl;
       for(i=0;i&amp;lt;ra;i++)
      {
           for(j=0;j&amp;lt;cb;j++)
          {
                int sum=0;
                for(k=0;k&amp;lt;rb;k++)
                   sum+=a[i][k]*b[k][j];
               if(j==cb-1) cout&amp;lt;&amp;lt;sum&amp;lt;&amp;lt;endl;
               else  cout&amp;lt;&amp;lt;sum&amp;lt;&amp;lt;&quot; &quot;;
          }
      }
  }
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;倒置矩阵&lt;/h3&gt;
&lt;h3&gt;L1-054 福到了&lt;/h3&gt;
&lt;p&gt;读入字符和网格规模，输出倒置的网格，如样例所示。但是，如果这个字正过来倒过去是一样的，就先输出bu yong dao le，然后再用输入指定的字符将其输出。&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;p&gt;**看到这题想法是应该用一个二维数组/字符串数组来存储这个网格。然后创建一个同样大小的网格把原始网格倒置存储。但是怎么判断这是一个不用倒了的网格呢？是判断每个有字符的地方都处于它对应的位置吗？**比如在原始NN网格中一个元素位置是a&lt;a href=&quot;http://blog.pinellia.uk/2025/02/ptal115.html#&quot;&gt;i&lt;/a&gt;，那么倒置之后它的位置在a&lt;a href=&quot;http://blog.pinellia.uk/2025/02/ptal115.html#&quot;&gt;n-i-1&lt;/a&gt;，**注意用给定字符替代。&lt;/p&gt;
&lt;p&gt;但是，或许我们根本不需要再额外花费空间来存储倒后的元素，可以直接输出呢？&lt;/p&gt;
&lt;p&gt;实现：&lt;/p&gt;
&lt;p&gt;**注意：**读入字符和规模N之后要getchar();（上文L1-078中已说过类似问题）再接着进行网格内容输入。可以用字符串数组来输入，一行就是getline(cin,s[i]);&lt;/p&gt;
&lt;p&gt;因为如果不用倒了，就说明这个网格是对称的，那它倒过来的时候元素对应位置一定是有字符的。一旦发现倒置后位置没有字符，就用置标志位flag为1（初始化为0），然后break跳出循环。根据flag的值判断是否需要输出“不用倒了”。&lt;/p&gt;
&lt;p&gt;在输出的时候，通过判断它（要输出的网格）倒置的位置（也就是原来的网格规模）是否有元素决定输出的地方是输出空格还是指定字符，注意到达每行尾部时换行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstdio&amp;gt;
#define N 1000
using namespace std;

int main()
{
   char c;
   int n,i,j;
   int flag=0;
   cin&amp;gt;&amp;gt;c&amp;gt;&amp;gt;n;
   getchar();
   string s[N];
   char m[n][n];
   for(i=0;i&amp;lt;n;i++)
  {
       getline(cin,s[i]);
  }
   for(i=0;i&amp;lt;n;i++)
  {
       for(j=0;j&amp;lt;n;j++)
      {
           if(s[i][j]!=s[n-i-1][n-j-1])
              {
                 flag=1;
                 break;
              }
      }
  }
   if(!flag) cout&amp;lt;&amp;lt;&quot;bu yong dao le&quot;&amp;lt;&amp;lt;endl;
   for(i=0;i&amp;lt;n;i++)
  {
       for(j=0;j&amp;lt;n;j++)
          {
               if(s[n-1-i][n-1-j]!=&apos;@&apos;)
                   cout&amp;lt;&amp;lt;&quot; &quot;;
               else cout&amp;lt;&amp;lt;c;
          }
       cout&amp;lt;&amp;lt;endl;
  }
   return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>景深、虚化与动态模糊：光圈和快门对照片的影响</title><link>https://pinellia.uk/posts/photography-aperture-shutter/</link><guid isPermaLink="true">https://pinellia.uk/posts/photography-aperture-shutter/</guid><description>结合拍摄示例理解光圈、快门、景深、背景虚化与动态模糊之间的关系。</description><pubDate>Fri, 31 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;光圈和快门都会影响曝光，但它们也分别控制画面的空间与时间表现：光圈改变景深和虚化，快门改变运动模糊。拍摄时可以从目标反推参数，想突出主体就调整光圈，想凝固或表现运动就调整快门。&lt;/p&gt;
&lt;h2&gt;光圈、景深与虚化&lt;/h2&gt;
&lt;h3&gt;景深&lt;/h3&gt;
&lt;p&gt;镜头前只有一段距离范围能够保持可接受的清晰度，这个范围就是景深。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/Qmbz1on3QEdRU6ByBL5smoMa1P6KmFB2ecUxo3Aa73mBhC&quot; alt=&quot;&quot; /&gt;
大光圈→浅景深。即清晰的区域小，背景会模糊。⬇️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmP8vV9ZE3KMahLc1vsXADTNwrk3C9jDs1pcmN3bypgEKk&quot; alt=&quot;&quot; /&gt;
小光圈→深景深。即清晰的区域大，背景也清晰。⬇️（简单理解为近视眼更喜欢眯缝着看人吧）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmP8vV9ZE3KMahLc1vsXADTNwrk3C9jDs1pcmN3bypgEKk&quot; alt=&quot;&quot; /&gt;
②虚化&lt;/p&gt;
&lt;p&gt;大光圈，背景虚化越强⬇️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmQHyUhvEvkdPtXHCzrBL3JsE62PsjbKcHAcH1mFhksqDJ&quot; alt=&quot;&quot; /&gt;
小光圈，背景虚化越弱⬇️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmUS394ov5o7NJyYqjKt1GkbK3mow6U6gEWq4SnVKqonq3&quot; alt=&quot;&quot; /&gt;
③虚化强弱的理解&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;光斑的形状来源于光圈扇叶形成的圆洞&lt;/strong&gt;
[&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmV1s9osojYYsVgdBSuk3gJF2ffMKc8LC8c19C281gDa3y&quot; alt=&quot;&quot; /&gt;
假如用卡纸剪出心形置于镜头前，光斑也成心形⬇️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmZEjzyReiSeLp1h34DvHsBffTGbS1nemLgkKMYXLWsCyS&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmbuZtrtU44MBvrjevDqX1bUH89A9u2wZZVbnYoYPjziuj&quot; alt=&quot;&quot; /&gt;
④虚化的不止背景，还可以有前景。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;前景虚化&lt;/strong&gt; ，可增添照片的梦幻感⬇️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmVs1MscQWeFa3NADBe1wu1L1D5HzGExsBZ4NjTaqkh64W&quot; alt=&quot;&quot; /&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;慢快门时，假如相机抖动，将导致光在CMOS上有运动轨迹，进而导致照片糊掉&lt;/p&gt;
&lt;p&gt;自动挡时，使用大光圈，增大ISO，闪光灯或补光灯照亮主体，使用三脚架，可以尽可能避免相机使用慢快门，进而避免拍摄时，相机抖动导致照片糊掉。&lt;/p&gt;
&lt;p&gt;③&lt;strong&gt;慢速快门&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;是一种拍摄技巧，可用于拍摄光绘，星轨，车流，火花，王家卫式电影，运动类题材。&lt;/p&gt;
&lt;p&gt;eg:&lt;/p&gt;
&lt;p&gt;车流（为防止糊片，需要三脚架固定）⬇️
&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmVs1MscQWeFa3NADBe1wu1L1D5HzGExsBZ4NjTaqkh64W&quot; alt=&quot;&quot; /&gt;
星轨⬇️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmcE4WUd6Y4r4DDHy4uMh8u2Xyj2ZsRh5Mn3W67JDuAVPF&quot; alt=&quot;&quot; /&gt;
烟花⬇️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmZjFs1aZ1x33ouiisypqfrbikGEtDyW8bLp3FmP6jAuq8&quot; alt=&quot;&quot; /&gt;
运动镜头⬇️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmRVSsDda3Mh4jTj6nJ8a4F7kT7G92Ty8UrJVaztiMgBXX&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;参数选择&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;光圈大→提高亮度、产生虚化&lt;/strong&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;p&gt;如图⬇️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmRw4jvmt7a1k7Ho5rBrRCgDDmRJbYQk6ZPxhjAr8BPSpy&quot; alt=&quot;&quot; /&gt;
答:___________________________&lt;/p&gt;
&lt;p&gt;强光下即使 ISO 已降到 100，画面仍可能过曝。如果又不希望缩小光圈或提高快门速度，可以使用固定档或可变档 ND 滤镜减少进光量。&lt;/p&gt;
&lt;p&gt;效果:使用前⬇️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmZHB13kZNuMPYuqG5UbYLsndCT7dTbMDptoEqQW3RRoEW&quot; alt=&quot;&quot; /&gt;
使用后⬇️&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ipfs.pinellia.uk/ipfs/QmUHxXGmeM42uu4js4pbiobacMcNYFVGLsTN9GnHjGfHQ3&quot; alt=&quot;&quot; /&gt;
4.人对事物理解的四个层次:（勉励自己运用）&lt;/p&gt;
&lt;p&gt;5.作业:拍摄尝试&lt;/p&gt;
</content:encoded></item></channel></rss>